/* Academy — the app's THIN accent sheet over suite-kit.
   The kit owns the topbar, nav, account menu, footer, dark mode, drawer, boot splash, buttons, cards,
   chips, tables, empty states and toast. This file owns THREE things: the accent seam, the study
   surface (which the kit has no equivalent of), and the course-page layout.

   ⚠ Everything here that duplicates a kit primitive is drift. Two audits have now found invented
   colour tokens that the kit does not define — first --ink-muted, --ink-faint, --ok and --sunken
   (body copy measured 3.0:1 in dark mode), then --fail, --card and --rule-strong. Every one took its
   CSS fallback silently: --fail a hard-coded light-mode red that measured 4.21:1 on dark where the
   kit's --red gives 6.38:1, --card `transparent` so the error panel, the ops rows, the exercise
   inputs and every interest field had no fill at all, --rule-strong the faintest hairline available.
   A token that does not exist does not fail loudly; it just quietly looks slightly wrong.

   Use the kit's names: --muted, --faint, --green, --red, --amber, --panel, --surface, --bg, --rule.
   If a token is not declared in suite-kit.css or at the top of this file, it does not exist. */

:root {
  /* ── THE BRAND ── Red Door Analytics' own palette, as the website uses it (read from its stylesheet, 11 Oct 2026).
     THREE REDS, each for a measured job, and the rule for choosing is the website's:
       #FA0036  --rda-red       the brand red: fills, rules, glyphs, large headings. 4.10:1 on white, so NEVER small text.
       #C4002A  --rda-red-dark  normal-size text on a light ground (links, eyebrows, the wrong-value mark): 6.21 white · 5.84 --bg · 5.44 --accent-soft.
       #E3002F  --rda-red-btn   a red FILL under white text, the primary button: 4.88:1 (the brand red under white is 4.10).
     On NAVY the website uses WHITE for small text and links (red there is 2.25-3.40:1): red appears on a dark ground only as a rule, a dot,
     a large word or a button fill. Navy #152D49 is the ink and the chrome. */
  --rda-red: #FA0036;
  --rda-red-dark: #C4002A;
  --rda-red-btn: #E3002F;
  --rda-navy: #152D49;
  /* A LARGE word set in red (the hero's last words, the marking band's "why"). On a light ground that is the text red: the brand red is 4.10:1 there and
     tests/test_brand_tokens.py refuses it as a text colour in any size. On the dark ground it is the brand red itself, 4.18:1 on --bg, over the 3:1 a large
     word needs (small text is white there). Never for anything under 24px. */
  --red-display: var(--rda-red-dark);

  /* ── THE ACCENT ── --accent is INK for light grounds: links, eyebrows, the 2px rule, the practical edge, the progress fill, the focus ring.
     It is the website's small-text red. (Until 11 Oct 2026 it was a cyan; the ramp below keeps its rung structure.)
     --accent-dark 8.69 white · 7.61 --accent-soft; --accent 6.21 white · 5.84 --bg · 5.44 --accent-soft. */
  --accent-dark: #9B0021;   /* the pressed state */
  --accent:      #C4002A;
  /* The chrome rung: ink that is legal on the NAVY topbar, the code surface and the exhibit header. White, as on the website. */
  --accent-mid:  #FFFFFF;
  --accent-soft: #ECF0F7;   /* A GROUND, never ink: the brand's blue-soft. --accent on it 5.44 · --ink 12.20 */

  /* ⚠ THE SEAM. --brand is what the KIT fills with: .seg.on, #toast, .fchip.on, .av, .nd-ic: navy, the brand's chrome.
     The primary button is the exception and is red (.btn-primary below). */
  --brand: var(--rda-navy);

  /* The topbar and the footer: the brand navy, in both themes. */
  --topbar: #152D49;
  --nav-ink: var(--topbar);
  /* What differs between the themes in the brand layout (step 6): the website's header ground, the lockup's colour, the strength of the tilted squares. */
  --site-bar: #fff;
  --lockup-filter: none;
  --tilt-opacity: .38;
  /* The hero is flat navy: the brand has no gradients. */
  --hero-2: var(--hero-1);

  /* ── TYPE ──────────────────────────────────────────────────────────────────────────────────
     Nine steps, ratios 1.143 → 1.267: tight at the UI end, widening into display. Replaces 27
     distinct sizes, EIGHTEEN of which sat between 8.5px and 18px at half-pixel steps — a 0.5px
     step at 12px is 4%, and two sizes need 12–15% before they read as different levels rather
     than as a rendering inconsistency. The exercise pane alone carried five of them.

     It lives HERE and not in the kit. Parking a nine-step app scale in shared :root on a "changes
     nothing until consumed" argument is speculative generality inside code two other apps load,
     and it would hand the next maintainer a scale that reads as the kit's. */
  --fs-1: 10.5px; --fs-2: 12px;  --fs-3: 13.5px; --fs-4: 15px; --fs-5: 17px;
  --fs-6: 20px;   --fs-7: 24px;  --fs-8: 30px;   --fs-9: 38px;
  --fs-splash: 46px;   /* the boot splash's word, which is a brand moment and not a step of the scale */
  --lh-1: 1.40; --lh-2: 1.45; --lh-3: 1.55; --lh-4: 1.55; --lh-5: 1.62;
  --lh-6: 1.35; --lh-7: 1.25; --lh-8: 1.16; --lh-9: 1.08;
  --ls-3: -.004em; --ls-4: -.006em; --ls-5: -.008em; --ls-6: -.014em;
  --ls-7: -.018em; --ls-8: -.022em; --ls-9: -.026em;
  --ls-caps-xs: .09em; --ls-caps-sm: .075em; --ls-caps-md: .055em;

  /* (The next two notes were written for Geist. The face has been DM Sans since 11 Oct 2026: its weight axis is continuous too, so these
     steps are kept, and a measure tuned to Geist's advance should be re-measured, not trusted.) */
  /* Weights. Geist's axis is continuous 100–900 and 700 does not appear in this app at all — yet
     every heading was rendering at UA bold 700, the heaviest weight in the product applied to its
     least considered element. 620 reads as set; 700 reads as unset.
     No font-optical-sizing: Geist ships no opsz axis. */
  --fw-body: 400; --fw-label: 550; --fw-strong: 560;
  --fw-ui: 600;   --fw-head: 620; --fw-display: 640;

  --fs-code: 13.5px; --lh-code: 1.62;

  /* Measure in em, NEVER ch. Geist's "0" advance is 0.671em against a 0.470em average prose
     advance, so the 62ch this file used to set was 88 characters — measure was 20–45% too wide
     on every reading surface. 33em / 0.470 = 70 characters, and it scales with font-size. */
  --measure: 33em;
  --measure-note: 30em;
  /* (There was a --measure-lesson here, 42em, from 15 Oct 2026: the lesson's text now spans its column, so it has no measure. See ".lesson-prose" below.) */
  --lesson-col: 64rem;                 /* the lesson's column, so listings and figures do not run to 1,900 px on a wide screen */

  /* ── SPACE ── 4px lattice, no half-steps; the same scale as the RDA brand's tokens/spacing.css,
     so the app and the marketing site agree. */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px; --sp-5: 20px;
  --sp-6: 24px; --sp-8: 32px; --sp-10: 40px; --sp-12: 48px; --sp-16: 64px;
  /* Three macro roles. The READING surface is generous and the WORKING surface is dense, which
     inverts how this file had it. */
  --s-rule: 24px;    --s-sec: 40px;    --s-major: 64px;
  --s-rule-op: 16px; --s-sec-op: 32px; --s-major-op: 40px;

  /* ── RADII ── the intersection of suite-kit's set and the RDA brand's, so the seam between kit
     chrome and app content cannot show. Verified present in the kit: 8, 14, 16, 999. */
  --r-ctl: 8px; --r-well: 14px; --r-sheet: 16px; --r-pill: 999px;

  /* ── CODE LISTINGS ── the lines an exhibit reserves: the taller of its two languages, set on each listing by exhibitLines() in app.js */
  --xh-lines: 0;

  /* ── ELEVATION ── ⚠ --panel and --surface INVERT rank between themes (light: surface 1.000 >
     panel 0.938; dark: panel 0.0177 > surface 0.0131), so anything built on --panel flips its
     depth when the theme does. These two are explicit and monotonic in both directions. */
  --well: #E9EDF4;

  /* Shadows. The kit ships only --shadow-card, so these are declared app-side rather than
     invented kit-side; values and the navy hue are the brand's tokens/effects.css. One light
     source — the app previously had ZERO shadows in 458 lines while the kit drew its cards with
     one, so the two were drawing containers in different languages on the same screen.

     ⚠ The comment here used to claim "one light source, four depths" and the file shipped ONE:
     --shadow-1 and --shadow-3 were declared and referenced nowhere, and every elevated surface in
     the app used --shadow-2. A ramp that is not spent is not a ramp, which is why 42 of 44 padded
     containers separated themselves with a 1px rule instead — a border is what you reach for when
     there is no depth available.

     Two layers per rung, not one. The tight layer is contact — it says the object is resting on
     the page; the wide layer is the cast. A single diffuse blur reads as a glow at low alpha,
     which is what the previous single-layer values did at 12-28px. */
  --shadow-1: 0 1px 2px rgb(21 45 73 / .05), 0 2px 6px rgb(21 45 73 / .04);
  --shadow-2: 0 2px 4px rgb(21 45 73 / .05), 0 8px 24px rgb(21 45 73 / .07);
  --shadow-3: 0 6px 14px rgb(21 45 73 / .07), 0 22px 50px rgb(21 45 73 / .11);
  /* The lit top edge. In LIGHT it is deliberately nothing: a shadow already separates a pale
     surface from a pale ground, and an inset highlight on white is invisible work. It exists so
     every elevated rule can name it unconditionally and let the DARK block switch it on. */
  --edge: 0 0 0 0 transparent;

  /* ── THE CODE SURFACE ── one value in BOTH themes, and decoupled from --nav-ink so retuning the
     topbar no longer retunes the thing being taught. It was the same navy as the topbar and the
     footer, so in any screenshot the most important object on the page wore the furniture's
     finish. All four inks measured on #152D49. */
  --code-bg:  #152D49;
  --code-ink: #DCE6F5;   /* 11.09:1 */
  --code-com: #9DB3D6;   /*  6.55:1 — on a teaching surface the comment IS the teacher talking */
  --code-str: #96D9BC;   /*  8.61:1 */
  --code-fn:  #A9C9FF;   /*  8.30:1 — the command word: stset, merlin */
  --code-lip: rgba(255, 255, 255, .06);


  /* ── THE FIGURE PALETTE ─────────────────────────────────────────────────────────────────
     INSIDE A .figure ONLY. Never in the interface; no status colour ever inside a figure.
     Enforced by tests/test_css_tokens.py, because a rule nobody can break is the only kind that
     survives — the suite has already shipped the bug where a green bar meant "at target" in one
     row and a category twenty pixels below.

     THESE ARE NOT FOUR HUES. Four categorical hues do not survive dichromacy. Two poles on the
     BLUE-YELLOW axis — the one axis red-green deficiency leaves intact — crossed with a monotone
     four-rung L* ladder, so every pair separates by hue, or by lightness, or by both, and no pair
     depends on a channel a colour-deficient reader lacks.

     Minimum pairwise ΔE2000 across all six pairs and six vision conditions: 15.8 (dark, deuter
     0.6) against the suite's bar of 8 for adjacent CVD pairs. Greyscale is a real ladder:
     #393939 / #525252 / #6D6D6D / #898989, minimum ΔL* 10.9.

     The kit's shipped --ch-1/--ch-2 could not be taken unchanged: light --ch-1 #5B87CE was
     byte-identical to Academy's old --focus, dark --ch-1 sits ΔE 2.8 from Academy's dark --accent,
     and the pair separates by only 1.16:1 in dark — it vanishes in monochrome. Re-stepped here. */
  --ch-1: #152D49;   /* navy · L*19 · on --bg 13.14 · --surface 13.97 (was petrol #004047, 11 Oct 2026) */
  --ch-2: #565500;   /* olive  · L*35 ·  7.15 ·  7.79 ·  7.33 */
  --ch-3: #2E66B0;   /* blue   · L*43 ·  5.42 ·  5.76 (was teal #007985) */
  --ch-4: #908C36;   /* gold   · L*57 ·  3.22 ·  3.51 ·  3.30 */

  /* Gutters. One left edge: these are the only two indents in the article, and the numerals in
     the contents list, the curriculum and the outcomes all hang in the same one. */
  --gutter-num: 40px;      /* ordinals */
  --gutter-label: 88px;    /* mono caps labels hanging outside the measure */

  /* The practical row's ground. A LITERAL, deliberately not a mix of --accent: an alias ramp
     whose step numbers imply lightness percentages nothing guarantees is a token that lies about
     its own value, which is no better than one that falls back.
     Measured: --accent on it 5.43 · --ink 11.89 · --muted 4.68 */


  /* "Needs captions", "no video" — a caution, not a failure. Declared per theme rather than derived,
     because both attempts to derive it were wrong in a way only measurement showed: the kit's
     --amber-ink measures 4.33:1 on --bg here, under the floor for .75rem text, and mixing it toward
     --ink to deepen it produced 1.15:1 in DARK mode — where --ink is near-white, so the mix walked
     the colour straight into the background. Two explicit values, each measured on its own ground. */
  --warn-ink: #5E3E07;

  /* Semantic inks for text sitting ON a tint of their own hue — a pill, a badge, a chip.
     The kit's --green and --amber are FILL tones: correct behind white, far too light as small
     uppercase text on a 16% wash of themselves. --green measured 2.14:1 that way. These are the
     same semantics, dark enough to read at 11px with letter-spacing. */
  --ok-ink: #08472F;
  /* Completes the trio. --red is a FILL tone too: as the ✗ beside a wrong exercise answer, and as
     the rule on an error panel, it measured 3.10-4.08:1 in light. This is the ink. */
  --fail-ink: #A12A2A;               /* 6.2:1 on --bg and on --panel */
}


/* ---------------------------------------------------------------------------
   BORDER-BOX, EVERYWHERE. There was no universal reset anywhere in this app or in the kit — the kit
   sets it on `body`, `main` and its own `.in` class, and nothing else — so any element given both a
   width and padding overflowed its parent by the padding.
   It was not hypothetical. The interest form's inputs are `width: 100%` with 12px of horizontal
   padding and a 1px border, which made them 373px inside a 347px column: 13px of horizontal scroll
   on a 375px phone, on the home page, the catalogue and every course page — the whole funnel. The
   selects beside them looked fine only because the UA stylesheet already makes a select border-box,
   which is exactly the kind of asymmetry that keeps a bug like this invisible.
   Scoped to this stylesheet rather than added to suite-kit: the kit is mounted by three apps and
   changing the box model under Lemma and Theorem is not this session's to do.
   --------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

:root[data-theme="dark"] {
  /* --accent is INK (links, marks, the outcome arrows). --brand is a FILL that carries white text.
     They must not be the same value in dark mode: white on #36B4DB measures 2.4:1, which made the
     SELECTED language — the defining control of this product — the least legible label in its own
     switch. --brand stays a mid royal that white sits on; --accent lightens for text. */
  /* PROVISIONAL, until the navy dark theme is designed (step 6 of the redesign): on a dark ground small text and links are WHITE, as on
     the website, and red is only a rule or a fill. --accent is the ink, so it is near-white here; --brand is the navy chrome fill. */
  --accent-dark: #FFFFFF;
  --accent:      #E8EEF8;
  --red-display: var(--rda-red);   /* a large word in red on the navy: 4.18:1 (see the light block) */
  --brand:       #2A4A73;   /* a fill that carries white text */
  --accent-soft: #14304F;
  --ch-1: #6F97D8;   /* L*62 · on the dark --bg 5.79 (was #008F9D) */
  --ch-2: #A6A142;   /* L*65 · 6.77 · 6.17 · 5.76 */
  --ch-3: #9DBBEA;   /* L*75 · on the dark --bg 8.76 (was #06CEE2) */
  --ch-4: #E6DD7B;   /* L*87 · 13.02 · 11.87 · 11.07 */
  --warn-ink: var(--amber-ink);      /* #E2B564 — the kit's dark-ground amber, 9.6:1 on --bg */
  /* ⚠ NOT var(--green). This aliased straight back to the fill tone and reintroduced, in dark mode,
     the exact bug the token was invented three commits earlier to fix: --green on a 16% wash of
     --green measures 3.68:1. The comment it replaces asserted the opposite without measuring, which
     is how it survived review. Measured on the COMPOSITED ground (#20383B), not on the page — that
     distinction is the whole error. */
  --ok-ink: #5ED4A9;                 /* 6.78:1 on the green-16% wash it ACTUALLY sits on */
  --fail-ink: #FF9AA2;               /* 6.80:1 on the red-16% wash · 9.03:1 on --bg */
  --well: #0D0F14;
  /* ⚠ ELEVATION IN DARK. This block previously redeclared no shadow at all, so every elevated
     surface carried rgb(21 45 73 / .06) — a NAVY shadow at 6% — over #13151C. That is a delta of
     about one level: invisible. Dark mode separated its containers with a 5.5%-white hairline and
     nothing else, which is why it read as a wireframe rather than as a set of surfaces.

     Depth in dark is not a darker shadow. It is a lighter ground plus a LIT TOP EDGE, because the
     eye reads elevation in a dark room from the highlight where the light catches the near edge,
     not from a shadow it cannot see. The shadows below are black rather than navy for the same
     reason the code surface is one value in both themes: a hue at this alpha over a near-black
     ground is a colour cast, not a shadow. */
  --shadow-1: 0 1px 2px rgb(0 0 0 / .40);
  --shadow-2: 0 2px 6px rgb(0 0 0 / .45), 0 10px 30px rgb(0 0 0 / .38);
  --shadow-3: 0 6px 14px rgb(0 0 0 / .50), 0 24px 60px rgb(0 0 0 / .45);
  --edge: inset 0 1px 0 rgb(255 255 255 / .07);
  /* --code-* are deliberately NOT redeclared. One code surface in both themes: an instrument's
     screen looks the same regardless of the room, and it removes the bug class where a colour is
     chosen for light and merely lightened for dark. */
  --topbar: #0A1626;       /* the chrome a step darker than the ground, --bg #0E1C2E */
  --nav-ink: var(--topbar);
  --site-bar: var(--topbar);
  --lockup-filter: brightness(0) invert(1);   /* the lockup in white on the dark chrome */
  --tilt-opacity: .55;
  --blue-panel: #16365A;   /* the navy-blue band and tilted squares, where the kit's dark has a slate */
}

/* ── MOTION DOCTRINE ────────────────────────────────────────────────────────────────────────
   THE ARTICLE DOES NOT MOVE; THE APPARATUS DOES.

   The kit ships a good system — one curve (--ease) and four durations — and this app spent exactly
   two of them, so everything it owns was instant. That is not restraint, it is absence, and it was
   doing damage in three specific places (see the loading veil, the language swap and the completion
   button). The scale is the kit's; do not fork it. What each rung MEANS is decided here:

     --t1  .12s  a state changes on the thing under the cursor — hover, focus, press
     --t2  .17s  something is SUBSTITUTED in place — the code swapping language, a chevron, Copied
     --t3  .24s  something ARRIVES — a marked result, a progress rule advancing, a stripe turning
     --t4  .42s  boot, and nothing else

   Anything that MOVES rather than fades goes inside @media (prefers-reduced-motion: no-preference).
   The constraint is unconditional; an exception written into shared code as a comment is the
   artefact a future change cites to justify a worse one. */

/* ── ONE MOTION LANGUAGE ────────────────────────────────────────────────────────────────────
   The doctrine above assigns a meaning to each rung and the app then spent seven transitions in
   1822 lines: 17 of 20 hover rules changed colour instantly. An instant state change is the most
   reliable "this is an old page" tell there is, and it is not a deliberate absence here — the
   rules were written one at a time and each one forgot.

   :where() so this carries ZERO specificity. Every component rule below still wins on the
   properties it sets; this only fills in the ones nobody thought about. Properties are named
   rather than `all`, because `all` animates layout and turns a reflow into a smear. */
:where(a, button, summary, .card, .stat, .lesson-row, .rail-row, .contents a,
       .btn, .pill, .seg, input, select, textarea) {
  transition: background-color var(--t1, .12s) var(--ease, ease),
              color           var(--t1, .12s) var(--ease, ease),
              border-color    var(--t1, .12s) var(--ease, ease),
              box-shadow      var(--t2, .17s) var(--ease, ease);
}
@media (prefers-reduced-motion: reduce) {
  :where(a, button, summary, .card, .stat, .lesson-row, .rail-row, .contents a,
         .btn, .pill, .seg, input, select, textarea) { transition-duration: .01ms; }
}

/* ⚠ Looks like a duplicate of the kit's default ring. It is not, and deleting it silently removes
   the focus indicator from every listing a crawler or a no-JS reader sees.

   The two renderers disagree about this element. app.js:981 builds it as
   el("pre", {class:"code", tabindex:"0", role:"region"}), which the kit's
   :where(a,button,[tabindex="0"],select,checkbox):focus-visible does cover. render.py:779 emits a
   bare <pre class="code"> — no tabindex, no role — which the kit's selector does NOT match, even
   though Chrome still makes it focusable because it scrolls (WCAG 2.1.1 wants that reachable).
   So on the server-rendered page this rule is the only ring there is.

   ⚠ The real fix is upstream: render.py should emit the same tabindex and role as app.js, and then
   this rule becomes genuinely redundant. Until it does, do not fold this into the base. */
pre.code:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The kit already rings the right set at the right offset with --focus-ring, which resolves to
   this app's --accent. What it lacks is the halo: a 2px outline alone reads as a rectangle drawn
   ON the control, where a ring plus a soft field reads as the control being held. Same selector
   set as the kit's, same zero specificity, so the eight per-site rules still win where they are
   deliberate. */
:where(a, button, [tabindex="0"], select, input, textarea):focus-visible {
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--focus-ring) 20%, transparent);
}

/* ------------------------------------------------------------------ the range control
   ⚠ NO CONSUMER YET, and that is deliberate rather than an oversight. Academy contains zero
   input[type=range] anywhere; the interactive components the tools work is heading towards all need
   one, and the alternative to declaring it once is five of them written inline and disagreeing.
   The builder's `df` and `nodes` were left as number inputs on purpose: they are read by the SYNCED
   script (app/builder.py explains the split), the site's own build shows them as numbers, and a
   slider with no readout is worse for a value you have to be able to state.

   A native range is the only form control whose parts cannot be addressed portably: WebKit wants
   ::-webkit-slider-runnable-track plus ::-webkit-slider-thumb, Firefox wants ::-moz-range-track
   plus ::-moz-range-thumb, and NEITHER browser parses a rule whose selector list contains the
   other's pseudo-element. They must therefore be written as separate rules, never comma-joined —
   one shared list silently styles nothing, in both browsers. */
.range {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  height: 24px;                 /* the touch target; the track inside it is 4px */
  margin: 0;
  background: transparent;      /* Firefox paints the element ground otherwise */
  cursor: pointer;
}
.range::-webkit-slider-runnable-track {
  height: 4px; border-radius: 2px; background: var(--field-line);
}
.range::-moz-range-track {
  height: 4px; border-radius: 2px; background: var(--field-line);
}
.range::-moz-range-progress {
  height: 4px; border-radius: 2px; background: var(--brand);
}
.range::-webkit-slider-thumb {
  appearance: none; -webkit-appearance: none;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--brand); border: 2px solid var(--surface);
  box-shadow: 0 0 0 1px var(--field-line);
  margin-top: -6px;             /* centres a 16px thumb on a 4px track */
}
.range::-moz-range-thumb {
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--brand); border: 2px solid var(--surface);
  box-shadow: 0 0 0 1px var(--field-line);
}
/* The ring belongs on the element; the thumb pseudo-elements cannot carry an outline in WebKit. */
.range:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-radius: 2px; }
.range:disabled { cursor: not-allowed; opacity: .55; }

/* The kit's .loadveil is position:absolute;inset:0 and needs a positioned ancestor. */
main { position: relative; }

/* ------------------------------------------------------------------ page rhythm
   ⚠ THE HEADINGS WERE THE BROWSER'S. There was no heading rule in this file or in suite-kit, so
   all 37 heading emissions across app.js, admin.js and render.py computed to UA defaults: h1 = 2em
   = 30px at weight 700, letter-spacing 0, and line-height `normal` — which for Geist resolves from
   its own hhea metrics to 1.24, giving the 30px course title and the 17.55px module heading
   IDENTICAL leading. That is exactly backwards, and it was the only unstyled text in a sheet whose
   comments measure greys to two decimal places.

   DESCENDANT, not child. The old `>` combinator missed the course title and the lesson title —
   both nested — which is to say it missed the two headings that matter most. */
.a-page { padding: var(--s-rule) 0 var(--s-major); position: relative; }
.a-page :where(h1, h2, h3) { text-wrap: balance; font-weight: var(--fw-head); }
.a-page h1 { font-size: var(--fs-8); line-height: var(--lh-8); letter-spacing: var(--ls-8);
  margin: 0 0 var(--sp-2); }
.a-page h2 { font-size: var(--fs-7); line-height: var(--lh-7); letter-spacing: var(--ls-7);
  margin: var(--s-sec) 0 var(--sp-3); }
.a-page h3 { font-size: var(--fs-6); line-height: var(--lh-6); letter-spacing: var(--ls-6);
  margin: var(--s-rule) 0 var(--sp-2); }

.a-lede { font-size: var(--fs-5); line-height: var(--lh-5); letter-spacing: var(--ls-5);
  color: var(--soft); max-width: var(--measure); text-wrap: pretty; margin: 0 0 var(--sp-6); }

/* The 2px accent rule under a page head is the most recognisable device in the journal
   vocabulary, and it costs one border. It and the numeral gutters carry the accent; beyond that
   it appears only on links, the practical rule, the progress fill and the focus ring. */
.masthead-rule { height: 2px; background: var(--accent); border: 0;
  margin: var(--sp-4) 0 var(--s-sec); }

/* A paper does not shout mid-sentence. */
strong { font-weight: var(--fw-strong); }

/* Digits that line up, everywhere they are compared. Prose is exempt: tabular figures in a
   sentence look like a table that lost its rules. */
.a-page, .opstable, .adm-course-meta, .exhibit { font-variant-numeric: tabular-nums; }
.a-lede, .course-prose p { font-variant-numeric: normal; }

/* ------------------------------------------------------------------ course page */
/* Named areas so the ASIDE can sit beside both the head and the body while source order stays
   head → aside → body, which is the correct reading order. The old rule reordered the whole right
   column with `order:-1` below 900px, hoisting the enrolment card and every run card above the h1 —
   so a phone opened the page on "Free / Start the course" and only then said what the course is. */
/* 1fr, and the CAP MOVED OFF THE CONTAINER.
   This track was fit-content(--measure) to fix a real bug: with 1fr the track took every pixel the
   container had while .course-prose capped ITSELF at the measure, leaving a canyon of dead track
   between the prose and the aside. fit-content closed the canyon by shrinking the track — and put
   the slack at the right margin instead, 341px of it at 1440, which was defensible when the note
   above was written because every other page did the same. Home and the catalogue now fill their
   width, so the course page became the one page with a hole in it.

   The mistake was capping the CONTAINER. A course page is not all prose: the curriculum is thirteen
   rows of title, badge and duration, and those want the width — at 495px the row contents were
   crowded against each other. So the track fills, and the measure applies to the things that are
   actually read as prose (below), which is what --measure was always for. */
.course-grid { display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(17rem, 21rem);
  grid-template-areas: "head aside" "body aside";
  column-gap: var(--sp-12); row-gap: var(--s-major); align-items: start; }
.course-head { grid-area: head; }
.course-body { grid-area: body; }
.course-grid h2, .course-grid h3 { text-wrap: balance; }
/* The buy/enrol card must not sit below thirteen lessons on a phone — it is the one thing the page
   is asking for. Source order puts content first for a screen reader; the grid reorders visually. */
@media (max-width: 1040px) {
  .course-grid { grid-template-columns: 1fr; grid-template-areas: "head" "aside" "body";
    row-gap: var(--s-sec); }
  .course-aside { max-width: 26rem; }
}
/* The measure, on the things that are READ rather than scanned. ul.lessons is deliberately absent:
   it is a table of rows, and it takes the track.

   ⚠ --measure is in EM, so it resolves against each element's own font size: 33em is 561px on the
   17px lede and 792px on a 24px h2. That is the right behaviour for a measure — a line of 24px type
   needs more pixels to hold the same number of characters — and the wrong behaviour for anything
   that wants a fixed edge, which is why the section rule below is set separately. */
.course-prose > p, .course-prose > .a-lede, .course-prose > .outcomes, .course-prose > .prereq,
.course-prose > .byline, .course-prose > .course-note { max-width: var(--measure); }
/* The title caps in rem, not em: it is one line of display type, not a paragraph, so what matters
   is where it breaks on the page rather than how many characters it holds. */
.course-prose > h1 { max-width: 34rem; }

.outcomes { list-style: none; margin: 0 0 4px; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.outcomes li { position: relative; padding-left: 20px; line-height: 1.5; }
.outcomes li::before { content: "→"; position: absolute; left: 0; color: var(--accent); font-weight: 600; }

/* SECTION SEPARATION on the course page.
   "What you will be able to do", "Curriculum" and the enquiry pitch were three sections divided by
   nothing but whitespace, and whitespace alone stops reading as division once the page is long
   enough to scroll — every gap looks like the same gap. A hairline above each h2 says where one
   thing ends, at the cost of one border and no vertical space at all.

   Not on the first: a rule above the first heading on a page draws a line under the title block,
   which is a different statement from separating two sections. */
/* The rule spans the TRACK, not the measure. A divider that stops short of the content it divides
   reads as an underline on the heading; one that crosses the column reads as the end of a section,
   which is what it is. The heading text still balances inside it. */
.course-prose > h2 { padding-top: var(--s-sec); border-top: 1px solid var(--rule);
  max-width: none; }
.course-prose > h2:first-of-type { padding-top: 0; border-top: 0; }

/* A module is a BLOCK: its heading and its lessons belong together, and consecutive modules need
   more air between them than a heading needs from its own list. The old rhythm gave both 24px, so
   four modules read as one undifferentiated run of thirteen rows. */
.course-body h3 { margin-top: var(--s-sec); }
.course-body h3:first-child { margin-top: var(--s-rule); }

.lessons { list-style: none; margin: 0; padding: 0; }
/* A lesson NAVIGATES, so it is an anchor, not a div with a click handler. That is what makes it
   reachable by Tab, openable with Enter, and ⌘-clickable into a new tab — and it is why the kit's
   zero-specificity focus ring applies to it for free. */
a.lesson-row { display: flex; align-items: center; gap: 12px; padding: 12px 8px 12px 2px;
  border-bottom: 1px solid var(--rule); text-decoration: none; color: inherit;
  border-radius: 6px; transition: background var(--t2, .12s) var(--ease, ease); }
.lessons li:last-child a.lesson-row { border-bottom: 0; }
a.lesson-row:hover { background: var(--accent-soft); }
a.lesson-row .ord { font-family: var(--mono); font-size: var(--fs-2); color: var(--muted); width: 24px;
  flex: none; text-align: center; }
a.lesson-row .nm { flex: 1; min-width: 0; }
a.lesson-row .dur { font-family: var(--mono); font-size: var(--fs-2); color: var(--muted);
  font-variant-numeric: tabular-nums; flex: none; }
/* --ok-ink, not --green: the tick is INK on the page ground, and --green is a fill tone —
   3.10:1 here. Same mistake as the badges, in the one mark that says a lesson is finished. */
a.lesson-row .tick { color: var(--ok-ink); font-weight: var(--fw-ui); flex: none; }
@media (prefers-reduced-motion: reduce) { a.lesson-row { transition: none; } }

/* ------------------------------------------------------- the study surface
   Video and code SIDE BY SIDE — the code pane is the thing being taught. */
.study { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: 24px;
  align-items: start; }
@media (max-width: 1000px) { .study { grid-template-columns: minmax(0, 1fr); } }

/* An honest placeholder. It previously rendered a play button with no handler, an aria-label
   promising "Play lesson", and a "CC · transcript" badge — three claims about a video that does not
   exist yet. Silently doing nothing was the worst of the options. */
.player-placeholder { background: var(--nav-ink); border-radius: 10px; aspect-ratio: 16/9;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
  padding: 20px; text-align: center; }
.player-placeholder .pp-t { color: #fff; font-size: var(--fs-4); font-weight: 500; }
.player-placeholder .pp-s { color: rgba(255,255,255,.62); font-size: var(--fs-3); max-width: 23em; }

pre.code { margin: 0; background: var(--nav-ink); color: #DCE6F5; border-radius: 10px;
  padding: 16px 16px; font-family: var(--mono); font-size: var(--fs-3); line-height: 1.65;
  overflow-x: auto; }
/* A scrollable region must be focusable, or a keyboard user cannot scroll it. */

.langbar { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
.langbar .hint { font-size: var(--fs-3); color: var(--muted); }

.lesson-nav { display: flex; gap: var(--sp-3); flex-wrap: wrap; margin-top: var(--sp-6);
  align-items: center; }
/* Completed is a FACT, not an action, so it stops wearing a button. See the note in app.js: while
   it was a disabled primary, the loudest control on a finished lesson was the one you could not
   press. */
.lesson-done { display: inline-flex; align-items: center; gap: 8px; padding: 0 var(--sp-2);
  font-size: var(--fs-3); font-weight: var(--fw-ui); color: var(--ok-ink); white-space: nowrap; }
/* The same place in the row, for a lesson that finishes by being DONE rather than by a button:
   a sentence, not a control, because there is nothing here to press. */
.lesson-todo { font-size: var(--fs-3); color: var(--muted); max-width: 22em; }
.lesson-nav .spacer { flex: 1; }

/* ------------------------------------------------------------------ progress
   The kit has no progress bar (.steptrack is a step tracker, a different thing), so this is
   legitimately the app's — but it is built from kit tokens, not invented ones. */
.progress-track { height: 6px; border-radius: 99px; background: var(--panel); overflow: hidden;
  border: 1px solid var(--rule); }
.progress-track > i { display: block; height: 100%; background: var(--accent); border-radius: 99px; }
.progress-pct { font-variant-numeric: tabular-nums; }

.a-meta { font-size: var(--fs-3); color: var(--muted); font-variant-numeric: tabular-nums; }
.a-note { font-size: var(--fs-3); color: var(--muted); }

/* An error that a user can act on, rather than a raw server string. */
.a-error { border: 1px solid var(--rule); border-left: 3px solid var(--red);
  border-radius: 0 8px 8px 0; padding: 12px 16px; background: var(--panel); }
.a-error h3 { margin: 0 0 4px; font-size: var(--fs-4); }
.a-error p { margin: 0 0 8px; color: var(--muted); font-size: var(--fs-3); }

/* Screen-reader-only, for the route-change live region. */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* ------------------------------------------------------------------ exercises
   Every learner's dataset is different, so the seed is SHOWN rather than hidden: it is what makes
   the run reproducible, and reproducibility is the thing the exercise is teaching. */
.ex-head { margin-bottom: 16px; }
.ex-head h2 { margin: 0 0 4px; }
.ex-seed { font-family: var(--mono); background: var(--accent-soft); border: 1px solid var(--rule);
  border-radius: 4px; padding: 1px 8px; color: var(--accent); font-variant-numeric: tabular-nums; }

.ex-answers { display: flex; flex-direction: column; gap: 12px; margin: 12px 0 16px; }
.ex-field { display: flex; flex-direction: column; gap: 4px; }
.ex-lab { font-size: var(--fs-4); font-weight: 500; color: var(--ink); }
.ex-hint { font-size: var(--fs-2); color: var(--muted); }

/* WHERE A PRACTICAL'S PARTS ARE (app.js practicalJump), under its title. Link-like buttons, not
   links — see the comment there. The answers are the destination that matters on the way back from
   R or Stata, so that one carries the accent and the arrow. */
.ex-jump { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-1) var(--sp-4);
  margin: var(--sp-3) 0 0; }
.ex-jump-k { font-family: var(--mono); font-size: var(--fs-1); letter-spacing: var(--ls-caps-xs);
  text-transform: uppercase; color: var(--muted); }
.ex-jump button { display: inline-flex; align-items: center; min-height: var(--sp-6); background: none; border: 0; padding: 0; font: inherit; font-size: var(--fs-3);
  color: var(--soft); cursor: pointer; text-decoration: underline; text-underline-offset: 3px;
  text-decoration-color: var(--rule); }
.ex-jump button:hover { color: var(--ink); text-decoration-color: currentColor; }
.ex-jump button.ex-jump-go { color: var(--accent); font-weight: var(--fw-ui);
  text-decoration-color: currentColor; }
/* The sticky topbar is 54px; a jump that lands the heading under it has landed nowhere. */
[id^="ex-data-"], [id^="ex-code-"], [id^="ex-answers-"] { scroll-margin-top: 72px; }

/* EVERY EARLIER ATTEMPT (app.js attemptHistory): closed, one line, until it is wanted. */
.ex-history { margin-top: var(--sp-4); border-top: 1px solid var(--rule); padding-top: var(--sp-3); }
.ex-history summary { cursor: pointer; font-size: var(--fs-3); color: var(--soft); }
.ex-history ol { margin: var(--sp-3) 0 0; padding: 0; list-style: none; display: grid;
  gap: var(--sp-3); }
.exh-head { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-3); align-items: baseline;
  margin: 0; font-size: var(--fs-3); }
.exh-when { font-family: var(--mono); font-size: var(--fs-2); color: var(--muted); }
.exh-score { color: var(--soft); }
.exh-score.is-ok { color: var(--ok-ink); font-weight: var(--fw-ui); }
.exh-wrong { margin: var(--sp-1) 0 0; padding-left: var(--sp-5); display: grid; gap: var(--sp-1);
  font-size: var(--fs-3); color: var(--soft); }
.exh-lab { color: var(--ink); margin-right: var(--sp-2); }
.ex-field input { font-family: var(--mono); font-size: var(--fs-4); padding: 8px 12px;
  border: 1px solid var(--rule); border-radius: 7px;
  background: var(--panel); color: var(--ink); font-variant-numeric: tabular-nums; }
.ex-field input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px;
  border-color: var(--accent); }

/* THE MARKER'S ANSWER, A LEDGER (redesign step 4): a card, a heading in the mono caps, then one table. Rules and mono numerals, the verdict in
   words, a named mistake under its row on the brand's red rule, the count under a double rule. */
.ex-result { margin-top: 24px; border: 1px solid var(--rule); border-radius: 14px; background: var(--surface); overflow: hidden; }
.ex-result h3 { margin: 0; padding: 16px 20px; border-bottom: 1px solid var(--rule); font-family: var(--mono); font-size: var(--fs-2);
  font-weight: 600; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
.ex-ledger-wrap { padding: 0 20px 8px; overflow-x: auto; }
.ex-ledger { width: 100%; border-collapse: collapse; font-size: var(--fs-4); }
.ex-ledger th, .ex-ledger td { padding: 12px 0; text-align: left; font-weight: 400; vertical-align: baseline; color: var(--ink); }
.ex-ledger tbody th, .ex-ledger tfoot th { text-transform: none; letter-spacing: 0; font-family: var(--font); font-size: var(--fs-4); }
.ex-ledger thead th { padding: 16px 0 8px; border-bottom: 2px solid var(--ink); font-family: var(--mono); font-size: var(--fs-1); font-weight: 500;
  letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
.ex-ledger .num, .ex-ledger .verdict { text-align: right; white-space: nowrap; padding-left: 20px; }
.ex-ledger .num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.ex-ledger .verdict { font-weight: 700; }
.ex-ledger tbody tr:not(.named) + tr:not(.named) > *, .ex-ledger tbody tr.named + tr > * { border-top: 1px solid var(--rule); }
.ex-ledger tbody th small { display: block; margin-top: 2px; font-size: var(--fs-2); color: var(--muted); }
.ex-ledger tr.no .verdict { color: var(--fail-ink); }
.ex-ledger tr.named td { padding: 0 0 12px; }
.ex-ledger tr.named div { border-left: 3px solid var(--rda-red); padding: 2px 0 2px 16px; line-height: 1.5; color: var(--ink); max-width: var(--measure); font-size: var(--fs-3); }
.ex-ledger tr.named b { font-weight: 700; }
.ex-ledger tfoot th, .ex-ledger tfoot td { padding: 16px 0 12px; font-weight: 700; border-top: 1px solid var(--ink); border-bottom: 3px double var(--ink); }
.ex-result.is-pass tfoot .verdict { color: var(--ok-ink); }
.ex-ledger-note { margin: 0; padding: 12px 20px 16px; }
@media (max-width: 560px) {
  .ex-result h3, .ex-ledger-note { padding-left: 16px; padding-right: 16px; }
  .ex-ledger-wrap { padding: 0 16px 8px; }
  .ex-ledger .num, .ex-ledger .verdict { padding-left: 12px; }
}
/* Paste your output. A monospace well the height of a short table; what it read is said beneath
   it in the same note voice the rest of the form uses. */
.ex-reader { display: grid; gap: var(--sp-2); margin-bottom: var(--sp-5); }
.ex-reader .ex-paste { font-family: var(--mono); font-size: var(--fs-2); line-height: 1.45;
  width: 100%; min-height: 7.5em; resize: vertical; white-space: pre; overflow: auto; }
.ex-reader .ex-read:empty { display: none; }
.ex-answers input[data-read="1"] { background: var(--accent-soft); }

/* Run it here: R in the learner's own browser (the runtime lives in a sandboxed frame; see app.js, "the browser R runtime").
   An editor and a console on the same navy ground as a listing; the frame itself is not shown. Tokens only. */
.rt-wrap[hidden] { display: none; }
.rt-pane { display: grid; gap: var(--sp-3); margin: var(--sp-6) 0; }
.rt-pane h2, .rt-pane > p { margin: 0; }
/* THE CODE BOXES GROW WITH WHAT IS IN THEM (growToFit in app.js sets the height from the content; the owner, 16 Oct 2026). The caps below are most of a window: past them a box scrolls
   inside itself rather than making the page longer than the work it holds. The output boxes (.rt-console, .rt-diff, .ex-hint-code) were capped at 28, 36 and 32em, so a
   summary of twenty lines showed fifteen. */
.ex-paste { max-height: 60vh; }
.rt-editor { max-height: 75vh; }
.rt-editor { width: 100%; min-height: 12em; resize: vertical; box-sizing: border-box; padding: var(--sp-4);
  background: var(--nav-ink); color: var(--code-ink); border: 1px solid transparent; border-radius: 10px;
  font-family: var(--mono); font-size: var(--fs-3); line-height: 1.6; white-space: pre; overflow: auto; tab-size: 2; }
.rt-editor:focus-visible, .rt-console:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.rt-actions { display: flex; gap: var(--sp-3); flex-wrap: wrap; }
.rt-state:empty { display: none; }
.rt-console { margin: 0; padding: var(--sp-4); background: var(--nav-ink); color: var(--code-ink); border-radius: 10px;
  font-family: var(--mono); font-size: var(--fs-3); line-height: 1.55; max-height: 70vh; overflow: auto; white-space: pre; }
.rt-console:empty { display: none; }
/* The previous run's output while the next run is in flight: where it was, dimmed, instead of gone (see livePane's onState). */
.rt-console.is-stale, .rt-plots.is-stale { opacity: .5; transition: opacity var(--t2, .17s) var(--ease, ease); }
/* the plots a run drew: each on its own white sheet, as paper, in both themes (the picture is a PNG the runtime drew with a white ground and the brand's navy ink, so it is
   never a dark image on a dark page); the sheet keeps the image's own proportions at any width and the figure is not wider than the pane */
.rt-compare[hidden], .rt-compare-btn[hidden] { display: none; }
.rt-compare { display: grid; gap: var(--sp-3); }
.rt-cols { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-4); }
@media (max-width: 760px) { .rt-cols { grid-template-columns: minmax(0, 1fr); } }
.rt-col h3 { margin: 0 0 var(--sp-2); font-size: var(--fs-3); }
/* a line of code is ONE line: it scrolls sideways inside its column and is never wrapped (a wrapped line reads as two, and a narrated worked listing has long comment lines) */
.rt-diff { margin: 0; padding: var(--sp-3); background: var(--nav-ink); color: var(--code-ink); border-radius: 10px; font-family: var(--mono); font-size: var(--fs-3);
  line-height: 1.55; max-height: 70vh; overflow: auto; white-space: pre; overflow-wrap: normal; }
.rt-diff:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.rt-line { display: block; }
.rt-line.is-diff { background: rgba(255, 255, 255, .09); }
.rt-gut { display: inline-block; width: 1.4em; opacity: .75; }
/* a named mistake, under the box it is about: the marker's own sentence, before Check is pressed */
.ex-named { margin: var(--sp-1) 0 var(--sp-3); padding: var(--sp-2) var(--sp-3); border-left: 3px solid var(--accent); }
.rt-plots { display: grid; gap: var(--sp-3); }
.rt-plots:empty { display: none; }
.rt-plot { margin: 0; background: #fff; border: 1px solid var(--rule); border-radius: 10px; padding: var(--sp-3); max-width: 46rem; }
.rt-plot img { display: block; width: 100%; height: auto; aspect-ratio: 640 / 480; }
.rt-host { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.rt-frame { width: 1px; height: 1px; border: 0; }

/* ------------------------------------------------------------------ operator screen
   The queue is what needs a PERSON. Automatic work is listed separately and quietly, because a list
   that mixes the two trains people to skim it — and then the one item that mattered is skimmed too. */
.opsqueue { display: flex; flex-direction: column; gap: 8px; margin: 8px 0 20px; }
.opsrow { display: flex; align-items: center; gap: 12px; padding: 12px 16px;
  background: var(--panel); border: 1px solid var(--rule);
  border-left: 3px solid var(--accent); border-radius: 0 8px 8px 0; }
.opsrow.is-look { border-left-color: var(--amber); }
.opstext { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.opswhat { font-size: var(--fs-4); color: var(--ink); }
.opswhy { font-size: var(--fs-3); color: var(--muted); }

.opstable { width: 100%; border-collapse: collapse; margin: 8px 0 20px; }
/* --muted, not --faint: at this size, uppercase and letterspaced, --faint measures 3.21:1 — the same
   failure already fixed on the stat-band label, and here it is on the words that say what each
   COLUMN of an operator's table means. --muted measures 5.04:1. */
.opstable th { text-align: left; font-size: var(--fs-2); letter-spacing: .07em; text-transform: uppercase;
  color: var(--muted); font-weight: 700; padding: 0 12px 8px 0; border-bottom: 1px solid var(--rule); }
.opstable td { padding: 8px 12px 8px 0; border-bottom: 1px solid var(--rule); font-size: var(--fs-4); }
.opstable .adm-link { display: inline-flex; align-items: center; min-height: var(--sp-6); }   /* a link that stands alone in a cell is a target of its own: 24px, not 23 */
.opstable td.num { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
/* The header of a numeric column aligns WITH its numbers. Left-aligned headers over right-aligned
   cells put each value under the next column's label — the table read one column out. */
.opstable th.num { text-align: right; }
/* a date, a state pill, a run and an access end do not break across lines: the learners table wrapped "2026-/10-15", "Self-/paced" and "IN / PROGRESS" (C6) */
.opstable td.nw, .opstable td.nw .pill { white-space: nowrap; }
/* the credential column carries a sentence (where each enrolment stands): at 110px it was a six-line cell and made every row of the roster tall */
.opstable td.credcol { min-width: 14em; }
.opstable td.who { min-width: 10em; }
/* an address that is one unbreakable word was the widest thing in the table and forced it past its window */
.opstable .adm-mail { overflow-wrap: anywhere; }
/* ON A PHONE an operator table is a list of cards, each cell labelled with its column's header (labelTables): it was 616 to 954px wide in a 362px
   window, scrolling with no cue and cut mid-word at the edge (C8). */
@media (max-width: 640px) {
  .opstable, .opstable tbody, .opstable tr, .opstable td { display: block; }
  .opstable thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  .opstable tr { padding: var(--sp-3) 0; border-top: 1px solid var(--rule); }
  .opstable td { display: flex; gap: var(--sp-3); padding: var(--sp-1) 0; border: 0; text-align: left; }
  .opstable td::before { content: attr(data-label); flex: 0 0 9em; color: var(--muted); font-size: var(--fs-2); letter-spacing: .06em; text-transform: uppercase; }
  .opstable td:empty { display: none; }
  .opstable td.nw { white-space: normal; }
}

.autolist { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.autolist li { font-size: var(--fs-3); color: var(--muted); padding-left: 16px; position: relative; }
.autolist li::before { content: "✓"; position: absolute; left: 0; color: var(--ok-ink); }

/* ------------------------------------------------------------------ interest form
   The form that replaces a Microsoft Form. Wide enough to be taken seriously, narrow enough to read. */
.interest { margin-top: 48px; padding-top: 32px; border-top: 1px solid var(--rule); max-width: 42rem; }
.if-form { display: flex; flex-direction: column; gap: 16px; margin-top: 16px; }
.if-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 600px) { .if-row { grid-template-columns: 1fr; } }

.if-field { display: flex; flex-direction: column; gap: 4px; }
.if-field label { font-size: var(--fs-4); font-weight: 500; color: var(--ink); }
.if-hint { font-size: var(--fs-2); color: var(--muted); margin-top: -3px; }
.if-field input, .if-field select, .if-field textarea {
  font: inherit; font-size: var(--fs-4); padding: 8px 12px; border-radius: 8px;
  border: 1px solid var(--rule); background: var(--panel);
  color: var(--ink); width: 100%; }
.if-field textarea { resize: vertical; min-height: 76px; }
.if-field input:focus-visible, .if-field select:focus-visible, .if-field textarea:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }

/* The honeypot. Removed from the layout AND from the accessibility tree, so no person — sighted,
   screen-reader or keyboard — can reach it, while a scraper reading the DOM fills it in. Not
   display:none, which some bots detect and skip. */
.if-trap { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.if-consent { display: flex; gap: 8px; align-items: flex-start; font-size: var(--fs-3);
  color: var(--soft); line-height: 1.5; }
.if-consent input { margin-top: 2px; flex: none; }

.if-done { border: 1px solid var(--rule); border-left: 3px solid var(--green);
  border-radius: 0 8px 8px 0; padding: 16px 20px; }
.if-done h2 { margin: 0 0 8px; font-size: var(--fs-5); }

/* The outcome of a form POST, for a page with no JavaScript to raise a toast with. Same shape as
   .if-done because it is the same news arriving by a different road. Red here is the FAIL role, not
   an accent — the only two states this element has are "saved" and "not saved". */
.if-outcome { margin: 12px 0 0; padding: 12px 16px; font-size: var(--fs-3); line-height: 1.5;
  border: 1px solid var(--rule); border-radius: 0 8px 8px 0; }
.if-outcome--ok { border-left: 3px solid var(--green); color: var(--ink); }
.if-outcome--bad { border-left: 3px solid var(--red); color: var(--ink); }

/* A mistake, said beside the field it is about (the 10 Oct sign-up review, U6). The text is --fail-ink,
   the red that is legible AS TEXT in both themes; --red is the fill/border tone. Shown by removing
   `hidden`, so an empty slot takes no room. The field itself gets a red border through aria-invalid,
   the same attribute a screen reader is told about. */
.if-error { margin: 0; font-size: var(--fs-3); line-height: 1.4; color: var(--fail-ink); }
.if-error[hidden] { display: none; }
.if-field input[aria-invalid="true"], .if-field select[aria-invalid="true"],
.if-field textarea[aria-invalid="true"] { border-color: var(--red); }
.if-consent input[aria-invalid="true"] { outline: 2px solid var(--red); outline-offset: 1px; }

/* The course-specific notify page's short form: "Tell us more" folds the fields the general form asks
   up front, so the page asks for an address and a box and nothing else unless the reader opens it. */
.if-more { border: 1px solid var(--rule); border-radius: 8px; padding: 0 12px; }
.if-more > summary { cursor: pointer; padding: 12px 0; font-size: var(--fs-4); font-weight: 500; color: var(--ink); }
.if-more[open] > summary { border-bottom: 1px solid var(--rule); margin-bottom: 12px; }
.if-more-body { display: flex; flex-direction: column; gap: 16px; padding-bottom: 16px; }

/* What the notify page says once the details are sent: where to go next. Links in the accent ink, like
   every other link in the app: the browser's default blue read as unfinished on these two blocks. */
.if-next { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.if-next li::before { content: "\2192\00a0"; color: var(--muted); }
.if-next a, .contactside .a-note a { color: var(--accent); text-underline-offset: 2px; }

/* The "Tell me when it opens" button under the title, for a phone, where the run card that carries the
   same button comes after the whole curriculum (U4). Wherever the card sits beside the page this would
   be a second button next to the first, so it only exists once the grid has collapsed to one column
   (the same 1040px as .course-grid above). */
.notify-top { display: none; margin: 16px 0 4px; }
@media (max-width: 1040px) {
  .notify-top { display: block; }
  .notify-top .btn { display: block; text-align: center; }
}
.steps-note { margin-top: -4px; }

/* ------------------------------------------------------------------ holding page
   The parked public surface. One page, no navigation, no script — see app/static/holding.html.
   Almost nothing here is new, and that is the design. The hero is the KIT's .home-hero — the same
   block Academy's signed-in home page mounts through SuiteKit.home(), rendered server-side because
   this page runs no script — so the mark, the watermark, the gradient, the brass edition chip that
   carries the date and the stat band all come from suite-kit.css and none of them is styled here.
   The grid is .cardgrid and the cards are .coursecard, the same ones the open catalogue draws, so
   the preview looks like the product because it IS the product's card. Only the "running now" band
   and the sign-in footnote belong to this page alone. */

/* ── the holding masthead ────────────────────────────────────────────────────────────────────
   Hand-written because this page runs no script and the kit builds its topbar in JavaScript, so
   there is no shell to inherit chrome from. Deliberately quiet: it sits above a hero that is doing
   the talking, and a heavy bar competing with it would be the second-loudest thing on a page whose
   whole job is the first. Sticky, because the page is long enough that "tell us what you need"
   should stay reachable without scrolling back. */
/* ---- the holding page's interest form, as a panel -------------------------------------------
   The default .interest runs down the left margin under a rule, which is right at the foot of a
   page that is already a column of prose. On the holding page there is nothing below it, so the
   form is the page's last impression and a left-aligned column of inputs in a 1280px frame reads
   as an afterthought. Centred, in a surface card, it reads as the thing you were meant to do. */
.interest--panel {
  /* ⚠ THE SAME BAND AS .holdnow ABOVE IT, not a card floating in the middle of the page. The first
     version was 40rem wide and centred, with centred type, in a column where every other block —
     hero, card grid, .holdnow, the note, the footer — is 1220px wide and left-aligned. Measured, it
     was the only element on the page starting anywhere but x=30, and it read as a modal somebody
     had dropped on top. Border, radius, padding and fill are .holdnow's, and so is the shape: the
     pitch on the left, the thing to do on the right.

     ⚠ .interest's own max-width is 42rem, for /contact's narrow column. Without releasing it the
     band starts in the right place and still stops at 672px — the same defect wearing the page's
     border. */
  display: flex; flex-wrap: wrap; align-items: flex-start;
  /* ⚠ --sp-7 DOES NOT EXIST. The scale is 1-6, 8, 10, 12, 16, and an undeclared custom
     property does not error — it falls back to `normal`, so the column gap I thought I had
     set was never applied and the band only looked right because the two flex bases
     happened to separate. tests/test_css_tokens.py caught it; the screenshot did not. */
  gap: var(--sp-5) var(--sp-8);
  max-width: none; padding: var(--sp-6); border-top: 0;
  border: 1px solid var(--rule); border-radius: 16px; background: var(--panel);
}
.interest--panel .if-pitch { flex: 1 1 17rem; max-width: 22rem; }
.interest--panel .if-pitch h2 { margin: 0 0 8px; font-size: var(--fs-6);
  line-height: var(--lh-6); letter-spacing: var(--ls-6); }
.interest--panel .if-pitch .a-lede { margin: 0; font-size: var(--fs-3); color: var(--soft);
  text-wrap: pretty; }
.interest--panel .if-form { flex: 3 1 26rem; margin-top: 0; }
.interest--panel .btn { width: 100%; }

@media (max-width: 900px) {
  /* One column, which is what .holdnow's flex-wrap does at the same kind of width. */
  .interest--panel .if-pitch { max-width: none; }
}
@media (max-width: 640px) {
  .interest--panel { padding: var(--sp-5); border-radius: 14px; }
  /* the two-up rows stack rather than squeezing two selects into a phone's width */
  .interest--panel .if-row { grid-template-columns: 1fr; }
}

/* ---- the holding page's footer ---------------------------------------------------------------
   Written here for the same reason .holdnav is: this document loads no application script and the
   kit builds its footer in JavaScript, so without this the only page a stranger sees ends on a
   form and then stops. Width and gutter are copied from the kit's `main` (1280 / 30) so the footer
   lines up with everything above it. */
/* A touch tighter than --s-major, because the footer now closes the document and the page no
   longer has to end on air. ⚠ NOT because of the gap a tall screenshot shows above the
   footer — that is the kit's `main{flex:1 0 auto}` doing the sticky-footer job it is there
   for, stretching main to the viewport on a short page. Measured in a 2200px-tall window it
   looks like 220px of dead space; at any real viewport height it is zero. */
.a-page.holding { padding-bottom: var(--s-sec); }
.holdfoot { border-top: 1px solid var(--rule); background: var(--bg); }
.holdfoot-in { max-width: var(--page-max); margin: 0 auto; padding: 28px 30px;
  display: flex; align-items: center; justify-content: space-between; gap: 24px; flex-wrap: wrap; }
.hf-brand { display: flex; align-items: center; gap: 8px; }
.hf-mark { display: inline-flex; width: 22px; height: 22px; }
.hf-mark svg { width: 100%; height: 100%; }
.hf-name { display: block; font-weight: 600; color: var(--ink); line-height: 1.2; }
.hf-by { display: block; font-size: var(--fs-2); color: var(--muted); }
.hf-links { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
.hf-link { font-size: var(--fs-3); color: var(--muted); text-decoration: none; }
.hf-link:hover { color: var(--ink); text-decoration: underline; }
.holdfoot-legal { padding-top: 0; padding-bottom: 24px; border-top: 0;
  font-size: var(--fs-2); color: var(--muted); }
@media (max-width: 640px) {
  .holdfoot-in { flex-direction: column; align-items: flex-start; gap: 16px; }
  .hf-links { gap: 12px 16px; }
}

.holdnav { position: sticky; top: 0; z-index: 20; background: var(--bg);
           border-bottom: 1px solid var(--rule); }
/* ⚠ --page-max (1280px up to a 1440px window, wider after: "THE PAGE FOLLOWS THE SCREEN" below) AND 30px MATCH the `main` rule, and the 14px below matches suite-kit.css's
   620px breakpoint. The masthead sits OUTSIDE <main> — it has to, or "skip to content" lands on the
   navigation — so it inherits none of that and would otherwise run edge to edge while every line
   beneath it stopped short. There is no --s-gut token to read: the kit writes that
   number literally, so the 30px is a copy and is marked as such. */
.holdnav-in { max-width: var(--page-max); margin: 0 auto; padding: 0 30px;
              min-height: 58px; display: flex; align-items: center; justify-content: space-between;
              flex-wrap: wrap; gap: 8px 20px; }
.hn-brand { display: inline-flex; align-items: center; gap: 8px; text-decoration: none;
            color: var(--ink); font-weight: 600; letter-spacing: -0.01em; }
.hn-mark { display: inline-flex; width: 20px; height: 22px; color: var(--accent); }
.hn-mark svg { width: 100%; height: 100%; }
.hn-links { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 16px; }
.hn-link { color: var(--soft); text-decoration: none; font-size: var(--fs-2);
           padding: 8px 0; white-space: nowrap; }
.hn-link:hover { color: var(--ink); }
.hn-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 3px; }
/* The one destination that changes who you are rather than what you are reading. */
.hn-in { color: var(--ink); font-weight: 600; }
@media (max-width: 620px) {
  .holdnav-in { padding: 8px 12px; }
  .hn-links { gap: 4px 12px; }
  .hn-link { font-size: var(--fs-1); }
}

/* No top rule and no gap of its own: the hero above it is already a hard edge. */
.holdgrid { margin-top: 0; }
/* A preview is a <div>, not an <a>, so it already misses the lift: the hover rule is written
   `a.card:hover`. Nothing to override — this only says that the absence is intended, because a card
   that rises under the cursor and then does nothing when clicked is the most irritating thing a
   coming-soon page can do, and the next person to add `.card:hover` should know it. */
.coursecard.is-preview { cursor: default; }

/* The band that points at the courses running THIS year — the only thing on the page that can be
   booked today. Same shape as .cpitch on the open home page, deliberately. */
.holdnow { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--sp-5); margin-top: var(--s-sec); padding: var(--sp-6);
  border: 1px solid var(--rule); border-radius: 16px; background: var(--panel); }
.holdnow > div { max-width: 46rem; }
.holdnow h2 { margin: 4px 0 8px; font-size: var(--fs-6); line-height: var(--lh-6);
  letter-spacing: var(--ls-6); }
.holdnow p { margin: 0; font-size: var(--fs-3); color: var(--soft); text-wrap: pretty; }
.holdnow .btn { flex: none; }

/* .interest draws a 42rem top rule, which is right inside /contact's narrow column and reads as a
   stray fragment across this page's full width. The band above it is already a hard edge.
   ⚠ :not(.interest--panel), AND THAT EXCLUSION IS THE WHOLE POINT. This is two classes, so it beat
   the panel's own `padding: var(--sp-6)` — which is a single class — and forced padding-top to 0 on
   a variant that is a BORDERED CARD rather than a rule-topped column. The result was content flush
   against the top border while .holdnow, the band directly above it, carried 24px: measured 0
   against 25. The rule below removes a top RULE; the panel has none to remove, and it is the only
   .interest on this page that does not want its padding taken away. */
.holding .interest:not(.interest--panel) {
  margin-top: var(--s-sec); padding-top: 0; border-top: 0;
}
.holding .interest--panel { margin-top: var(--s-sec); }

.holdnote { margin-top: 40px; padding-top: 16px; border-top: 1px solid var(--rule);
  font-size: var(--fs-3); color: var(--muted); }
.holdnote a { color: var(--accent); text-decoration: none; }
.holdnote a:hover { text-decoration: underline; }

/* Skip link — visually hidden until focused, then unmistakable. The first thing in the tab order.
   FIXED, not absolute: an absolutely-positioned skip link scrolls with the document, so on any page
   the user has scrolled it appears above the viewport and stays invisible even while focused. That
   is the failure this had, and it looks identical to the CSS not applying at all. */
/* --brand, NOT --accent. --accent is an INK: in dark mode it lightens to #36B4DB, and white on that
   measures 2.40:1 — so the one control a keyboard user meets before anything else was the least
   legible thing in the app. --brand is the FILL that carries white text; see the seam note above.
   Unchanged in light, where the two are equal by definition. */
.skip-link { position: fixed; left: 8px; top: -60px; z-index: 100; padding: 8px 16px;
  background: var(--brand); color: #fff; border-radius: 0 0 8px 8px; font-weight: 500;
  text-decoration: none; transition: top .12s ease; }
.skip-link:focus { top: 0; }
@media (prefers-reduced-motion: reduce) { .skip-link { transition: none; } }

/* ═══════════════════════════════════════════════════════════════════════════
   THE OPERATOR'S SCREENS  (admin.js)
   Staff-only, so this never loads for a learner in any meaningful sense — but it is the same
   stylesheet, and the rule at the top of this file still holds: every colour below is a KIT token.
   The audit that found four invented tokens falling through to light hexes in dark mode was written
   about the learner views; there is no reason this half would have been luckier.

   Semantic colour is separate from the accent: --green for ready, --amber for a warning, --red for
   destructive. --accent stays the app's identity and is never used to mean "good".

   ⚠ --soft IS AN INK, NOT A SURFACE. It resolves to --ink-soft (#2C4360), the darkest muted text
   tone. Six rules here were written with `background: var(--soft)` on the assumption that a token
   called "soft" was a light fill; in light mode that put #152D49 text on a #2C4360 background — the
   release-type buttons measured about 1.1:1 and were, in the screenshot, three dark rectangles.
   The fills are --surface (raised, on top of the page), --bg (recessed, inside a panel) and
   --panel (the page's own card tone). This is the same class of mistake as the four invented tokens
   noted at the top of this file, and it is why the rule there says to check suite-kit.css rather
   than infer from the name.
   ═══════════════════════════════════════════════════════════════════════════ */

.adm-head { display: flex; align-items: flex-start; justify-content: space-between;
  gap: 20px; flex-wrap: wrap; margin-bottom: 24px; }
.adm-head h1 { margin-bottom: 4px; }
.adm-back { display: inline-block; margin-bottom: 8px; font-size: var(--fs-3);
  color: var(--muted); text-decoration: none; }
.adm-back:hover { color: var(--accent); }

/* --- the course list ---------------------------------------------------- */
.adm-courses { display: flex; flex-direction: column; gap: 8px; }
/* A GRID, NOT A WRAPPING ROW (review of 15 Oct 2026, C5). The row wrapped when its line of facts was long, so of sixteen courses eight had their
   buttons on the right and eight beneath, and row heights ran from 87 to 144px. The actions are the right-hand column of every row; the facts wrap
   inside the left. */
.adm-course { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; column-gap: 20px;
  padding: 16px 16px; background: var(--panel);
  border: 1px solid var(--rule); border-radius: 12px; }
.adm-course-title { display: flex; align-items: center; gap: 8px;
  font-size: var(--fs-5); font-weight: 600; margin-bottom: 8px; }
.adm-course-meta { display: flex; flex-wrap: wrap; gap: 8px 12px;
  font-size: var(--fs-3); color: var(--muted); }
.adm-course-meta .adm-draftflag { color: var(--amber-ink, var(--amber)); font-weight: 500; }
.adm-course-actions { display: flex; flex-wrap: wrap; gap: 8px; flex-shrink: 0; }   /* wraps: four actions are wider than a phone, and the last one ran 9px past the card */
/* The row's own next step is outlined in the accent, not another red fill: sixteen identical red buttons on one page, and seventeen on the overview, hid
   the two rows that did need a person. The page's one filled red is its one action (New course). */
.adm-open { border-color: var(--accent); color: var(--accent); }
.adm-open:hover { background: var(--accent-soft); }
/* a course's name or an address in a table: ink, with the accent on hover; a link with no class was the browser's blue (A3) */
.adm-link { color: inherit; text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--rule-blue); }
.adm-link:hover { color: var(--accent); text-decoration-color: var(--accent); }

/* 11px uppercase with letter-spacing is small text by every measure that matters, so each variant
   below is measured at 4.5:1 against its own tinted ground — not against the page. A tint of a hue
   darkens the ground, which is why the semantic INKS are much deeper than the kit's fill tones of
   the same name; --green as text on a wash of --green measured 2.14:1. */
.adm-badge { font-size: var(--fs-1); font-weight: 600; text-transform: uppercase;
  letter-spacing: .06em; padding: 3px 8px; border-radius: 999px;
  background: var(--bg); color: var(--muted); }
.adm-badge.is-published { background: color-mix(in srgb, var(--green) 16%, transparent);
  color: var(--ok-ink); }
.adm-badge.is-retired { background: var(--bg); color: var(--muted); }

/* --- publish panel ------------------------------------------------------ */
.adm-publish { padding: 20px; margin-bottom: 32px; border-radius: 14px;
  background: var(--panel); border: 1px solid var(--rule);
  /* The status stripe carries the state as FORM, not only as a word — it is readable at a glance and
     it survives being printed or seen by someone who does not distinguish the hues. */
  border-left: 4px solid var(--amber); }
.adm-publish.is-ready { border-left-color: var(--green); }
.adm-publish-head { display: flex; align-items: center; justify-content: space-between;
  gap: 20px; flex-wrap: wrap; }
.adm-publish-head h2 { margin: 0 0 2px; }

/* The publish status, once the panel has scrolled out of the window (publishBar in admin.js). The wrapper is ZERO height and sticky, the bar
   inside it absolute: showing it takes no room, so nothing under it moves. It stands below the header at every width (until 16 Oct 2026 the
   navy rail was a column from 900px and it stood at the top of the page; the header is a bar at every width now). */
.adm-pubstick { position: sticky; top: calc(var(--bar-h) + var(--sp-2)); z-index: 30; height: 0; }
.adm-pubbar { position: absolute; right: 0; top: 0; width: max-content; max-width: 100%; display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3) var(--sp-2) var(--sp-4); background: var(--surface); border: 1px solid var(--rule); border-left: 4px solid var(--amber);
  border-radius: var(--r-well, 9px); box-shadow: 0 6px 18px rgba(10, 12, 24, .12);
  opacity: 0; visibility: hidden; transform: translateY(calc(-1 * var(--sp-2)));
  transition: opacity var(--t2, .17s) var(--ease, ease), transform var(--t2, .17s) var(--ease, ease), visibility 0s linear var(--t2, .17s); }
.adm-pubstick.is-ready .adm-pubbar { border-left-color: var(--green); }
.adm-pubstick.is-away .adm-pubbar { opacity: 1; visibility: visible; transform: none; transition-delay: 0s; }
.adm-pubbar-text { min-width: 0; font-size: var(--fs-3); font-weight: var(--fw-ui, 600); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.adm-publish { scroll-margin-top: calc(var(--bar-h) + var(--sp-4)); }
.adm-publish-sub { font-size: var(--fs-3); color: var(--muted); margin: 0; }

.adm-blockers { list-style: none; margin: 16px 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 8px; }
.adm-blockers li { position: relative; padding-left: 24px; font-size: var(--fs-4); }
.adm-blockers li::before { content: "×"; position: absolute; left: 0; top: -1px;
  width: 18px; height: 18px; line-height: 18px; text-align: center; border-radius: 50%;
  background: color-mix(in srgb, var(--red) 18%, transparent); color: var(--fail-ink);
  font-weight: 700; font-size: var(--fs-3); }
.adm-warnings { margin-top: 16px; font-size: var(--fs-3); }
.adm-warnings summary { cursor: pointer; color: var(--muted); }
.adm-warnings ul { margin: 8px 0 0 20px; color: var(--muted); }
.adm-warnings li { margin-bottom: 8px; }

.adm-release { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--rule); }
.adm-segmented { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
.adm-seg { padding: 8px 12px; font: inherit; font-size: var(--fs-3); cursor: pointer;
  background: var(--surface); color: var(--ink); border: 1px solid var(--rule);
  border-radius: 8px; }
.adm-seg:hover { border-color: var(--accent); color: var(--accent); }

/* --- sections and fields ------------------------------------------------ */
.adm-section { margin-bottom: 36px; }
.adm-section-head { display: flex; align-items: center; justify-content: space-between;
  gap: 16px; margin-bottom: 12px; }
.adm-section-head h2 { margin: 0; }

/* Inputs align to the BOTTOM of their row. Labels are one line or two depending on how long the
   field name is — "Price (minor units, 0 = free)" wraps where "Currency" does not — and with
   top-aligned items that pushed one input ~20px below its neighbours, so a row of controls did not
   read as a row. */
.adm-grid { align-items: end;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 12px 16px; margin: 12px 0; }
.adm-label { display: flex; align-items: baseline; gap: var(--sp-2);
  font-size: var(--fs-3); font-weight: var(--fw-label); color: var(--muted); }
textarea
/* Autosave feedback. It has to be quiet enough to ignore and specific enough to trust — a field that
   silently discards what you typed is worse than one with a Save button. */
/* --muted, not --faint: at .75rem this says "Saving…" and then "Saved", and --faint measured 3.21:1
   in light mode. The one piece of text that tells you whether your work was kept has to be readable. */
.adm-fieldnote { font-size: var(--fs-2); font-weight: 400; color: var(--muted); }
.adm-fieldnote.is-ok { color: var(--ok-ink); }   /* ink, not the fill tone */
.adm-fieldnote.is-bad { color: var(--fail-ink); }
/* What a number means, under its box, in a sentence. `.ff > .adm-fieldhint` and not `.adm-fieldhint`: the kit's `.ff>span` is an 11px uppercase micro-label and outranks a bare class. Empty = nothing. */
.ff > .adm-fieldhint { display: block; margin-top: 4px; font-size: var(--fs-2); font-weight: 400; color: var(--muted); text-transform: none; letter-spacing: normal; }
.ff > .adm-fieldhint:empty { display: none; }

.adm-inline { display: inline-flex; align-items: center; gap: 8px; font-size: var(--fs-4);
  cursor: pointer; }
.adm-inline-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 12px 0; }
.adm-check { width: 16px; height: 16px; accent-color: var(--brand); }

/* The learner count is the way into the roster, so it must look like a way in — but an unstyled <a>
   takes the kit's accent and becomes the loudest thing in a --muted run of facts, for a number that
   is usually zero. Inherit the colour; carry the affordance on the underline. */
.adm-course-meta a { color: inherit; text-decoration: underline; text-underline-offset: 3px;
  text-decoration-color: var(--rule); }
.adm-course-meta a:hover { color: var(--accent); text-decoration-color: var(--accent); }

/* Bar AND number in one cell: the 6px track alone is unreadable in a table row, and the number alone
   hides the shape of the cohort, which is what the operator is scanning for. */
.roster-progress { display: flex; align-items: center; gap: 8px; }
.roster-progress .progress-track { width: 84px; flex: none; }
/* A field-level note, under the input it explains. Distinct from .a-note, which is a paragraph in
   the page's own voice; this is the caption on one control. */
.adm-hint { display: block; margin-top: 4px; font-size: var(--fs-2); color: var(--muted); }

/* --- who teaches it ----------------------------------------------------- */
/* One credited person: name, role, remove. align-items:flex-end so the name sits on the same
   baseline as the select's box rather than its label. */
.adm-team-row { display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap;
  padding: 8px 12px; margin-bottom: 8px; background: var(--panel);
  border: 1px solid var(--rule); border-radius: var(--r-well); }
.adm-team-row .adm-name { flex: 1; min-width: 140px; font-size: var(--fs-4); padding-bottom: 8px; }
.adm-team-pick { max-width: 22rem; margin-top: 12px; }

/* --- curriculum --------------------------------------------------------- */
.adm-module { margin-bottom: 16px; padding: 16px; border-radius: 12px;
  background: var(--panel); border: 1px solid var(--rule); }
.adm-module-head { display: flex; align-items: flex-end; justify-content: space-between;
  gap: 16px; margin-bottom: 12px; }
.adm-module-head .adm-module-title { font-size: var(--fs-5); font-weight: 600; }

/* --- the timetable editor ------------------------------------------------
   One row per session, laid out as a grid so the start times form a column the operator can scan.
   Below 900px it stacks: a five-column row of date inputs on a phone is unusable, and the operator
   screens are the ones most likely to be opened on one, standing in a room. */
.adm-timetable { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--rule); }
.adm-session { display: grid; gap: 8px; align-items: start; padding: 8px 0;
  grid-template-columns: minmax(220px, 1.2fr) minmax(140px, 1fr) minmax(160px, 1fr) auto auto; }
.adm-session + .adm-session { border-top: 1px solid var(--rule); }
.adm-session-when { display: flex; flex-direction: column; gap: 4px; }
.adm-session-meta { display: flex; flex-direction: column; gap: 4px; }
.adm-session-acts { display: flex; gap: 8px; align-items: start; }
/* A REAL CONTROL HEIGHT. These were 20px tall (18px for the selects) — the browser default at a
   12px font — which fails WCAG 2.2 SC 2.5.8's 24px minimum outright and is not tappable on a phone
   at all. The timetable is the one operator screen most likely to be opened standing in a room. */
.adm-session input, .adm-session select { width: 100%; font-size: var(--fs-2);
  min-height: 34px; padding: 8px 8px; }
@media (max-width: 900px) {
  .adm-session input, .adm-session select { min-height: 44px; font-size: var(--fs-3); }
}
@media (max-width: 900px) {
  .adm-session { grid-template-columns: 1fr; }
  .adm-session-acts { justify-content: flex-end; }
}

/* The same timetable with no inputs in it, for a teacher: the run is the owner's, the register is
   theirs. A row rather than the grid above, because with the fields gone there are two things left
   on the line — when it is, and the way in. */
.adm-runrow { display: flex; align-items: center; justify-content: space-between;
  gap: 16px; padding: 8px 0; }
.adm-runrow + .adm-runrow { border-top: 1px solid var(--rule); }

/* A value the operator must SEE and must not type — a box that accepts a value the next save
   overwrites is worse than no box. Reads as a field, does not invite a cursor. */
.adm-readonly { padding: 8px 8px; border-radius: 8px; background: transparent;
  border: 1px dashed var(--rule); color: var(--muted); font-size: var(--fs-2);
  font-variant-numeric: tabular-nums; }

/* --- the register -------------------------------------------------------- */
.adm-register { display: flex; flex-direction: column; gap: 2px; min-width: min(420px, 80vw); }
.adm-register-row { display: flex; align-items: center; justify-content: space-between;
  gap: 16px; padding: 8px 0; }
.adm-register-row + .adm-register-row { border-top: 1px solid var(--rule); }
.adm-register-marks { display: flex; gap: 8px; flex-shrink: 0; }

.adm-rowtools { display: flex; gap: 8px; align-items: center; flex-shrink: 0; }
/* ONE SIZE AND ONE LOOK FOR A TABLE ROW'S TOOLS (the owner, 16 Oct 2026, on Who has asked: "buttons seem unbalanced and different to each other"). Measured at 1440: "Mark answered"
   109x48 (it wrapped to two lines), "Close" 58x33 and the delete mark 29x30, with radii of 8, 8 and 7px and the delete mark white where the other two were the soft grey. They are
   32px high, nowrap, 8px radius and the same soft fill; the delete mark keeps its red edge for hover and focus. */
.opstable .adm-rowtools .btn { white-space: nowrap; }
.opstable .adm-rowtools .btn.btn-sm, .opstable .adm-rowtools .adm-icon { box-sizing: border-box; height: var(--sp-8); border-radius: 8px; }
.opstable .adm-rowtools .adm-icon { width: var(--sp-8); background: color-mix(in srgb, var(--ink) 5%, transparent); border-color: transparent; color: var(--soft); }
.opstable .adm-rowtools .adm-icon:hover { background: color-mix(in srgb, var(--ink) 9%, transparent); }
.opstable .adm-rowtools .adm-icon.is-danger:hover { border-color: var(--red); }
.adm-lessontools { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--rule);
  flex-wrap: wrap; }
.adm-icon { width: 30px; height: 30px; padding: 0; font: inherit; line-height: 1; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface); color: var(--muted);
  border: 1px solid var(--rule); border-radius: 7px; }
.adm-icon:hover { color: var(--accent); border-color: var(--accent); }
.adm-icon.is-danger:hover, .btn.is-danger:hover { color: var(--fail-ink); border-color: var(--red); }

.adm-lessons { display: flex; flex-direction: column; gap: 8px; }
.adm-lesson { background: var(--bg); border: 1px solid transparent; border-radius: 9px; }
.adm-lesson > summary { padding: 8px 12px; cursor: pointer; list-style: none; }
.adm-lesson > summary::-webkit-details-marker { display: none; }
.adm-lesson[open] { border-color: var(--rule); background: var(--panel); }
.adm-lesson > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px;
  border-radius: 9px; }
.adm-lesson-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.adm-lesson-n { width: 22px; flex-shrink: 0; font-family: var(--mono); font-size: var(--fs-2);
  color: var(--muted); font-variant-numeric: tabular-nums; }
.adm-lesson-title { flex: 1; min-width: 140px; font-size: var(--fs-4); }
.adm-kind { font-size: var(--fs-2); color: var(--muted); }
.adm-free { font-size: var(--fs-1); font-weight: 600; text-transform: uppercase;
  letter-spacing: .05em; padding: 2px 8px; border-radius: 999px;
  background: var(--accent-soft); color: var(--accent); }
.adm-warn { font-size: var(--fs-2); font-weight: 500; color: var(--warn-ink); }

/* A run sitting on a version that is not the published head. WARNING, not error: nothing is broken
   at rest — the run is usually closed, which is why it was left behind — but reopening it would
   serve superseded material, because a learner resolves content through the run's version and not
   through the course head. Built from --warn-ink, which is defined in both themes, so the ground
   follows it rather than being a light-mode constant. */
.adm-stranded {
  margin: var(--sp-3) 0;
  padding: var(--sp-3) var(--sp-4);
  border-radius: 8px;
  border-left: 3px solid var(--warn-ink);
  background: color-mix(in srgb, var(--warn-ink) 8%, transparent);
}
.adm-stranded > p { margin: 0 0 var(--sp-2); }
.adm-stranded > p:first-child { font-weight: var(--fw-head); color: var(--warn-ink); }
.adm-lesson-body { padding: 4px 12px 12px; }
/* max-width: a module title of sixty characters made the select 429px wide, and a page of lessons scrolled sideways at 390 (484px) */
.adm-moveto { width: auto; max-width: 100%; min-width: 0; font-size: var(--fs-3); padding: 8px 8px; }

.adm-addlesson { width: 100%; margin-top: 8px; padding: 8px; font: inherit; font-size: var(--fs-3);
  cursor: pointer; color: var(--muted); background: transparent;
  border: 1px dashed var(--rule); border-radius: 9px; }
.adm-addlesson:hover { color: var(--accent); border-color: var(--accent);
  background: var(--accent-soft); }

.adm-media { margin: 16px 0 4px; padding: 12px; border-radius: 10px;
  background: var(--bg); border: 1px solid var(--rule); }

/* THE QUESTION EDITOR (a quiz lesson). One card per question: a header row (the kind, then the
   move and remove tools), the stem, the options with the right one marked, the explanation. */
.adm-quizbox { margin: 16px 0 8px; }
.adm-quiz { display: grid; gap: 12px; margin-top: 8px; }
.adm-quiz-q { display: grid; gap: 8px; padding: 12px; border-radius: 10px;
  background: var(--bg); border: 1px solid var(--rule); }
.adm-quiz-head { display: flex; align-items: flex-end; gap: 8px; flex-wrap: wrap; }
.adm-quiz-head > strong { flex: 1 1 100%; font-size: var(--fs-4); }
.adm-quiz-head .ff { flex: 0 1 240px; }
.adm-quiz-option { display: grid; grid-template-columns: auto 1fr auto; align-items: end; gap: 8px; }
.adm-quiz-option .adm-check { margin-bottom: 12px; }
.adm-quiz-option > .adm-icon { margin-bottom: 4px; }
.adm-quiz-q .adm-rowtools { flex-wrap: wrap; }

/* --- published banner --------------------------------------------------- */
.adm-banner { display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 16px 16px; margin-bottom: 28px; border-radius: 12px; font-size: var(--fs-4);
  background: var(--accent-soft); border: 1px solid var(--rule); }
.adm-banner .btn { margin-left: auto; }

/* --- dialogs ------------------------------------------------------------ */
.adm-modal { max-width: 480px; width: calc(100% - 32px); padding: 0; color: var(--ink);
  background: var(--panel); border: 1px solid var(--rule); border-radius: 14px; }
.adm-modal::backdrop { background: rgba(8, 14, 24, .5); }
.adm-dialog { padding: 20px 20px 8px; }
.adm-dialog h2 { margin: 0 0 8px; }
.adm-dialog-actions { display: flex; justify-content: flex-end; gap: 8px;
  padding: 16px 20px 20px; }
/* --- the operator's view of what learners said ---------------------------- */
.fb-summary { display: flex; gap: var(--sp-5); align-items: center; margin-bottom: var(--sp-4); }
.fb-figure { text-align: center; flex: 0 0 auto; }
.fb-mean { font-size: var(--fs-9); font-weight: var(--fw-head); line-height: 1;
  color: var(--accent); font-variant-numeric: tabular-nums; }
.fb-summary p { margin: 0 0 var(--sp-1); }
.fb-list { display: flex; flex-direction: column; gap: var(--sp-3); }
.fb-row p { margin: var(--sp-2) 0 0; font-size: var(--fs-3); }
.fb-rowhead { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-3);
  font-size: var(--fs-3); }
.fb-score { font-weight: var(--fw-head); color: var(--accent); font-variant-numeric: tabular-nums; }
/* Shown only when permission was given — a "no" badge on most rows would read as a rejection
   rather than as the absence of a request. */
.fb-versions { display: flex; flex-wrap: wrap; gap: var(--sp-3); }

/* --- the second permission, on the learner's card ------------------------- */
/* Hidden until the quote itself is offered. `[hidden]` is scoped rather than declared globally:
   a blanket !important rule would reach every component in the app to serve one block here. */
.fb-extra { margin-top: var(--sp-4); padding-top: var(--sp-4);
  border-top: 1px solid var(--rule); display: grid; gap: var(--sp-2); }
.fb-extra[hidden] { display: none; }
.fb-rolelabel { font-size: var(--fs-2); color: var(--muted); }
.fb-role { width: 100%; padding: var(--sp-2) var(--sp-3); border: 1px solid var(--rule);
  border-radius: 6px; background: var(--surface); color: var(--ink); font: inherit; }

/* --- the owner's publish controls ---------------------------------------- */
.fb-acts { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  margin-top: var(--sp-3); }
.fb-live { font-family: var(--mono); font-size: var(--fs-2); letter-spacing: .04em;
  color: var(--surface); background: var(--ok-ink); border-radius: 4px;
  padding: 1px var(--sp-2); }
.fb-stale { font-size: var(--fs-2); color: var(--fail-ink); margin-top: var(--sp-2); }   /* small text: the ink, never the fill red */

/* --- quotes on a public course page --------------------------------------- */
/* ⚠ render.py's _testimonials draws the same markup. Both renderers make this page and the
   server's copy is the only one a crawler sees, so a rule that only suited one of them would be a
   visible flash either side of hydration. */
.quotes { margin-top: var(--sp-10); }
.quote-list { display: grid; gap: var(--sp-5); margin-top: var(--sp-5); }
.quote { margin: 0; padding: var(--sp-5); border-left: 3px solid var(--accent);
  background: var(--accent-soft); border-radius: 0 8px 8px 0; }
.quote blockquote { margin: 0; font-size: var(--fs-4); line-height: 1.55; color: var(--ink); }
.quote figcaption { margin-top: var(--sp-3); display: flex; flex-wrap: wrap; gap: var(--sp-2);
  align-items: baseline; font-size: var(--fs-2); }
.quote-name { font-weight: 600; color: var(--ink); }
.quote-role { color: var(--muted); }

.fb-quotable { font-family: var(--mono); font-size: var(--fs-2); letter-spacing: .04em;
  color: var(--ok-ink); border: 1px solid var(--ok-ink); border-radius: 4px;
  padding: 1px var(--sp-2); }

/* --- "How was it?", on a course you finished ------------------------------ */
/* Lives in the aside, so it inherits that column's one top spacing. The stars are real buttons in
   a radiogroup rather than a styled widget: this is the only required answer on the card and it has
   to be reachable and announced without a mouse. */
.feedback-card { display: flex; flex-direction: column; gap: var(--sp-3); }
.feedback-card h3 { margin: 0; }
.fb-rating { display: flex; gap: var(--sp-1); }
.fb-star {
  background: none; border: 0; padding: 2px 4px; cursor: pointer; line-height: 1;
  font-size: var(--fs-7); color: var(--rule);
}
.fb-star.on { color: var(--accent); }
.fb-star:hover { color: var(--accent); }
.fb-star:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.fb-text {
  width: 100%; font: inherit; font-size: var(--fs-3); color: var(--ink);
  padding: var(--sp-2) var(--sp-3); border: 1px solid var(--rule); border-radius: 8px;
  background: var(--surface); resize: vertical;
}
.fb-text:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* --- the start-a-course dialog ------------------------------------------- */
/* Wider than a confirm box because it is not one: it introduces the course, then asks one optional
   question. The mark is here because this is the moment somebody joins RDA's Academy, and an
   unbranded grey box asking for an email address is what a phishing page looks like. */
.consent-modal { max-width: 540px; }
.consent-modal .adm-dialog { padding: var(--sp-6) var(--sp-6) var(--sp-3); }
.consent-head { display: flex; gap: var(--sp-4); align-items: flex-start; }
.consent-head h2 { margin: 0 0 2px; }
.consent-mark { flex: 0 0 auto; width: 34px; color: var(--accent); }
.consent-mark svg { width: 100%; height: auto; display: block; }
.consent-sub { margin: 0; font-size: var(--fs-3); color: var(--muted); }

.consent-what { margin: var(--sp-5) 0 0; padding-left: 1.1rem; display: flex;
  flex-direction: column; gap: var(--sp-2); font-size: var(--fs-3); color: var(--ink); }

/* The newsletter is a SEPARATE block with its own heading, not a checkbox tacked to the bottom of
   the course copy — it is a different question, and it has to read as one that can be declined. */
.consent-block { margin-top: var(--sp-5); padding-top: var(--sp-4); border-top: 1px solid var(--rule); }
.consent-title { margin: 0 0 var(--sp-3); font-size: var(--fs-3); font-weight: var(--fw-head); }
.consent-optin { display: flex; gap: var(--sp-3); align-items: flex-start;
  padding: var(--sp-3); border: 1px solid var(--rule); border-radius: 8px;
  background: var(--surface); cursor: pointer; font-size: var(--fs-3); line-height: 1.5; }
.consent-optin input { margin: .2em 0 0; flex: 0 0 auto; width: 1rem; height: 1rem; cursor: pointer; }
.consent-optin:hover { border-color: var(--accent); }
.consent-optin:focus-within { outline: 2px solid var(--accent); outline-offset: 1px; }
.consent-where { margin: var(--sp-2) 0 0; font-size: var(--fs-2); color: var(--muted); }
.consent-where strong { color: var(--ink); font-weight: var(--fw-ui); }
.consent-foot { margin: var(--sp-2) 0 0; font-size: var(--fs-2); }
.consent-already { color: var(--muted); }
.consent-already .consent-title { color: var(--ink); }

@media (max-width: 520px) {
  .consent-modal .adm-dialog { padding: var(--sp-5) var(--sp-4) var(--sp-3); }
  .consent-head { gap: var(--sp-3); }
  .consent-mark { width: 26px; }
}

.adm-error { min-height: 1.2em; margin: 8px 0 0; font-size: var(--fs-3); color: var(--fail-ink); }

@media (max-width: 640px) {
  .adm-head { flex-direction: column; align-items: stretch; }
  .adm-course { grid-template-columns: minmax(0, 1fr); row-gap: var(--sp-3); }
  .adm-course-actions .btn { flex: 1; }
  .adm-publish-head { flex-direction: column; align-items: stretch; }
}

/* "My courses" — resume first, the course page second. Wraps rather than shrinking, so neither
   button ends up too narrow to read on a phone. */
.mine-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }

/* ══ DATA AND PRIVACY ═══════════════════════════════════════════════════════
   The page that says what is held and removes it. Deliberately NOT tinted red: red is FAIL and
   primary-action only, and washing this panel in it would read as an error on a page that is not
   one — the person is exercising a right, not recovering from a fault. The weight sits on the
   confirm button inside the dialog, which is the one place a destructive red belongs. */
.privacy-page { max-width: none; }   /* was one 33em column; the grid below sets the measure */
.privacy-facts { display: grid; grid-template-columns: max-content 1fr;
  gap: var(--sp-2) var(--sp-6); margin: 0 0 var(--sp-5); }
.privacy-facts dt { font-size: var(--fs-3); color: var(--muted); }
.privacy-facts dd { margin: 0; font-size: var(--fs-4); }
/* The address can be long and has no spaces to break at; without this it pushes the grid wider than
   the page on a phone. */
.privacy-facts dd { overflow-wrap: anywhere; }
.privacy-erase { margin-top: var(--s-sec); padding: var(--sp-6);
  background: var(--panel); border: 1px solid var(--rule); border-radius: var(--r-well); }
.privacy-erase h2 { margin-top: 0; }
.privacy-erase code { font-family: var(--mono); font-size: var(--fs-3);
  background: var(--well); padding: 2px 8px; border-radius: 5px; overflow-wrap: anywhere; }
/* Sentence case, deliberately. The kit's .ff label is an uppercase micro-label, which is right for
   "EMAIL" and wrong for an address the person must copy character for character — it showed them a
   string that was not the one they have. */
.privacy-confirm-label { display: block; font-size: var(--fs-3); color: var(--muted);
  margin: var(--sp-5) 0 var(--sp-2); }
.privacy-confirm-label strong { color: var(--ink); font-weight: var(--fw-ui);
  font-family: var(--mono); font-size: var(--fs-2); overflow-wrap: anywhere; }
.privacy-confirm-actions { display: flex; gap: 8px; justify-content: flex-end;
  margin-top: var(--sp-5); }

/* ══ UPCOMING COURSES ═══════════════════════════════════════════════════════
   A course that is announced but not yet teachable. The visual job is to be unmistakably NOT
   available without looking like an error — this is good news, not a failure — so it takes --amber
   throughout and never --red. */

.pill-upcoming { background: color-mix(in srgb, var(--amber) 18%, transparent);
  color: var(--warn-ink); font-weight: 600; }

/* One block per run. The dashed left edge is the same signal as the pill: provisional. */
/* ⚠ LAYOUT, NOT MARGINS. This was display:block with every child bringing its own margin, so the
   card's 20px padding and the first and last child's 13.5px margins doubled up: 33.5px above the
   first line and 33.5px below the last, against a 13.5px rhythm everywhere between. Adding one
   sentence under the button in 0.13.1 made the bottom of it obvious. A flex column with one gap
   spaces siblings once, and zeroing the children's own margins is what stops the two systems
   fighting — the same rule the lesson page follows a few hundred lines down. */
.run-card { margin-bottom: 12px; display: flex; flex-direction: column; gap: var(--sp-3); }
.run-card + .run-card { margin-top: 0; }
.run-when { margin: 2px 0 8px; font-size: var(--fs-5); font-weight: 600;
  font-variant-numeric: tabular-nums; }

/* A listed-but-not-linked lesson. Same row as .lesson-row so the outline reads identically, minus
   every affordance that would promise a page — no pointer, no hover, no focus ring, and it is a
   <span>, so Tab does not stop on it. */
.lesson-row.is-pending { display: flex; align-items: center; gap: 12px;
  padding: 12px 8px 12px 2px; border-bottom: 1px solid var(--rule); color: var(--muted); }
.lessons li:last-child .lesson-row.is-pending { border-bottom: 0; }

.adm-badge.is-announced { background: color-mix(in srgb, var(--amber) 16%, transparent);
  color: var(--warn-ink); }
.adm-badge.is-open { background: color-mix(in srgb, var(--green) 16%, transparent);
  color: var(--ok-ink); }
.adm-badge.is-closed { background: var(--bg); color: var(--muted); }

.adm-mail { display: block; font-size: var(--fs-3); color: var(--soft); text-decoration: underline; text-decoration-color: var(--rule-blue); text-underline-offset: 3px; }
.adm-mail:hover { color: var(--accent); text-decoration-color: var(--accent); }
.opsrow.is-respond { border-left-color: var(--accent); }

/* ══ THE EXHIBIT ════════════════════════════════════════════════════════════
   A named, copyable script — not a <pre>. A statistician's mental model of a code block is a
   SCRIPT: it has a name, and you take it away with you. Before this it had no header, no name, no
   language label inside the frame and no copy affordance, and from a screenshot you could not tell
   whether it was Stata or R. */

.study-head { margin-bottom: var(--sp-5); }
.study-head .eyebrow { font-family: var(--mono); font-size: var(--fs-1);
  letter-spacing: var(--ls-caps-xs); text-transform: uppercase; color: var(--accent);
  margin-bottom: var(--sp-2); }
.study-head h1 { margin-bottom: var(--sp-2); }
/* The page-head rule's 40px bottom margin, plus .lesson-main's 40px flex gap, put 80px of nothing
   between every lesson's title and its first sentence — the 0.25.0 review's fifth finding. Inside a
   lesson the rule takes no margin, and the head pulls the gap after it in to 24px; the gap between
   every other block of the lesson is untouched. */
.study-head .masthead-rule { margin-bottom: 0; }
.lesson-main > .study-head { margin-bottom: calc(var(--sp-6) - var(--s-sec)); }
.study-progress { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-3);
  max-width: 30em; }
.study-progress .progress-track { flex: 1; }

.exhibit { margin: 0; border-radius: var(--r-well); overflow: hidden;
  background: var(--code-bg);
  /* --code-lip is its own lit edge and is deliberately NOT --edge: the code surface is one
     value in both themes, so its highlight cannot follow the theme either. */
  box-shadow: var(--shadow-2), inset 0 1px 0 var(--code-lip); }
/* nowrap. With flex-wrap:wrap the header broke whenever the title was long enough to fill the row,
   and the thing that fell to the second line was Copy — landing left-aligned under the eyebrow,
   detached from the file it copies. .xh-t below already carries min-width:0 + ellipsis, which is
   the pressure valve this row was designed around: the title truncates and the controls hold their
   places. Below --bp-narrow the filename is dropped instead, since Copy is the control that matters
   and the name is repeated in the caption underneath. */
.xh { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: nowrap;
  padding: var(--sp-2) var(--sp-3) var(--sp-2) var(--sp-4);
  border-bottom: 1px solid rgba(255, 255, 255, .10); }
@media (max-width: 34rem) { .xh-file { display: none; } }
.xh-n { font-family: var(--mono); font-size: var(--fs-1); font-weight: var(--fw-label);
  letter-spacing: var(--ls-caps-xs); text-transform: uppercase; color: var(--code-com);
  flex: none; }
.xh-t { font-size: var(--fs-3); color: var(--code-ink); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.xh-file { margin-left: auto; font-family: var(--mono); font-size: var(--fs-2);
  color: var(--code-com); flex: none; }
/* An unwritten language, said plainly, instead of an empty black rectangle that reads as a rendering
   fault. --code-com and not --fail-ink: an unwritten listing is unfinished work, not a failure, and
   red is FAIL and primary-action only. */
.exhibit .xh-missing { color: var(--code-com); font-style: italic; }
.xh .seg { flex: none; }
/* THE LISTING WHILE THE EDITOR IS SHOWN (review of 15 Oct 2026, B5): its header stays (language, Download, Copy), its body and caption give way to the editor
   below, so choosing R swaps the code in place and does not stack a second copy of it under the first. */
.exhibit.is-run-here > pre.code, .exhibit.is-run-here > .xh-cap { display: none; }
.exhibit.is-run-here > .xh { border-bottom: 0; }
/* THE HEADER ON A PHONE (C8): title, language and two buttons in 334px left the title 5px ("LISTING S"). It wraps, and the title keeps a share of the row
   that the controls cannot take; on a tablet the title may take two lines before it is cut. */
@media (max-width: 34rem) {
  .xh { flex-wrap: wrap; row-gap: var(--sp-2); }
  .xh-t { flex: 1 1 40%; }
}
@media (max-width: 900px) { .xh-t { white-space: normal; } }
.xh .btn { flex: none; }

.exhibit pre.code { margin: 0; border-radius: 0; background: none;
  padding: var(--sp-4) var(--sp-5); color: var(--code-ink);
  /* the taller of the two listings, in lines (exhibitLines in app.js): switching language does not move the page */
  min-height: calc(var(--xh-lines, 0) * 1lh + 2 * var(--sp-4));
  font-family: var(--mono); font-size: var(--fs-code); line-height: var(--lh-code);
  overflow-x: auto; tab-size: 4; }
/* Three classes only. A fourth hue on a two-hue brand reads as an editor's default theme. */
.exhibit .com { color: var(--code-com); }
.exhibit .str { color: var(--code-str); }
.exhibit .fn  { color: var(--code-fn); }
/* Full width, deliberately. max-width here shrank the BOX rather than the text, so the caption
   rendered as a narrow white panel floating inside the dark code area — which reads as a rendering
   fault, not as a caption. Captions are one or two sentences; the padding carries the measure. */
.xh-cap { padding: var(--sp-3) var(--sp-5); background: var(--surface);
  border-top: 1px solid var(--rule); font-size: var(--fs-3); color: var(--soft);
  text-wrap: pretty; }

/* A single-line band, not a 16:9 well, when there is no video to play. */
.study-notice { display: flex; gap: var(--sp-3); align-items: center;
  padding: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-4);
  border: 1px solid var(--rule); border-left: 3px solid var(--amber);
  border-radius: 0 var(--r-ctl) var(--r-ctl) 0;
  background: var(--panel); font-size: var(--fs-3); color: var(--muted);
  max-width: var(--measure-note); }
.study-scope { margin-top: var(--sp-3); }

/* A LESSON THE READER CANNOT OPEN (app.js lockedPanel / render.py _withheld): why, what it is, and
   the way in, where the lesson would be. The accent edge is the one the lesson callouts use —
   information, not a warning; nothing has gone wrong. */
.lesson-locked { max-width: var(--measure); display: grid; gap: var(--sp-2);
  padding: var(--sp-5) var(--sp-6); background: var(--surface);
  border: 1px solid var(--rule); border-left: 3px solid var(--accent);
  border-radius: 0 var(--r-well) var(--r-well) 0; }
.ll-why { margin: 0; font-size: var(--fs-5); font-weight: var(--fw-ui); color: var(--ink);
  text-wrap: pretty; }
.ll-what { margin: 0; font-family: var(--mono); font-size: var(--fs-2); color: var(--muted); }
.ll-sum { margin: 0; color: var(--soft); text-wrap: pretty; }
.ll-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3) var(--sp-5);
  margin-top: var(--sp-3); }

/* The code column is the equal partner now, and the sole column when there is no video. It was the
   NARROWER of the two on the surface whose entire thesis is that the code IS the lesson — 507px,
   about 67 characters, which a merlin model specification does not fit into. */
.study { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.study.no-video { grid-template-columns: minmax(0, 1fr); }
/* The mirror of .no-video. With the code column gone, a two-column grid leaves the player in half
   the page and a void beside it — the same dead-column defect fixed on the course page. Capped,
   because a 16:9 well at full page width is a billboard rather than a lecture. */
.study.no-code { grid-template-columns: minmax(0, 1fr); max-width: 46rem; }
.study-media { position: sticky; top: 66px; }   /* the topbar is 54px and sticky */
@media (max-width: 1180px) { .study { grid-template-columns: minmax(0, 1fr); }
  .study-media { position: static; } }

/* Controls inside the exhibit header sit on --code-bg, which is dark in BOTH themes — so they
   cannot use the kit's ghost/segmented styling, which assumes the page ground and takes --soft.
   The Copy button measured 1.39:1 there in light mode. These are explicit, and identical in both
   themes for the same reason the code surface is. */
.xh .btn { background: rgba(255, 255, 255, .07); color: var(--code-ink);
  border-color: transparent; padding: 4px 8px; font-size: var(--fs-2); }
.xh .btn:hover { background: rgba(255, 255, 255, .14); color: #fff; }
.xh .seg { background: rgba(255, 255, 255, .07); border-color: rgba(255, 255, 255, .12); }
/* The kit fills its unselected segment with --surface (white in light). On a dark header that
   left --code-com text on white — worse than the ghost button this block was written to fix. */
.xh .seg button { color: var(--code-com); background: transparent; }
.xh .seg button.on { background: var(--brand); color: #fff; }
.xh .seg button:not(.on):hover { color: var(--code-ink); background: transparent; }
/* The kit clips its own focus ring with .seg{overflow:hidden} — on the product's defining control. */
.xh .seg button:focus-visible { outline: 2px solid var(--accent-mid); outline-offset: -2px; }

/* ══ THE MASTHEAD, THE CONTENTS AND THE FRONT MATTER ════════════════════════ */

.masthead { margin-bottom: var(--s-sec); }
.masthead .eyebrow, .course-prose .eyebrow, .consult .eyebrow, .marked .eyebrow {
  display: block; font-family: var(--mono); font-size: var(--fs-1);
  letter-spacing: var(--ls-caps-xs); text-transform: uppercase; color: var(--accent);
  margin-bottom: var(--sp-3); }
.masthead h1 { font-size: var(--fs-9); line-height: var(--lh-9); letter-spacing: var(--ls-9);
  max-width: 20em; }

/* ══ THE HOME PAGE, UNDER THE BAND ══════════════════════════════════════════
   The hero is the kit's .home-hero — the landing band every app in the suite opens on, and the
   reason Lemma, Theorem and this look like one product. Everything here sits UNDER it, and its job
   is to not fight it: white cards on the tinted canvas, the accent used once per element, and
   section heads that say what a group IS rather than only naming it. */

/* The band already ends with 26px of its own margin. .a-page's top padding stacked on top of that,
   and the first h2's --s-sec top margin on top of THAT — ~100px of nothing between the band and the
   first card, which read as a page that had failed to load rather than as breathing room. */
.home-body { padding-top: 0; }
.home-body > .sec-head:first-child h2 { margin-top: var(--s-rule); }

/* WHAT MARKING LOOKS LIKE (app.js markedExample / render.py _marked_example): a real attempt, marked
   by the real marker, drawn the way a learner's result is — value by value, and the marker's own
   sentence under a wrong one. It replaced three cards that only described the practicals (the 0.25.0
   review's third finding). ✓ and ✗ carry the verdict as well as the colour. */
.marked { margin-top: var(--s-major); }
.marked .sec-head h2 { margin-top: 0; }   /* the eyebrow above it is the section's opening */
.marked-card { padding: var(--sp-5) var(--sp-6); max-width: 46rem; }
.marked-score { margin: 0 0 var(--sp-3); font-family: var(--mono); font-size: var(--fs-2);
  letter-spacing: var(--ls-caps-xs); text-transform: uppercase; color: var(--muted); }
.marked-items { list-style: none; margin: 0; padding: 0; }
.marked-items li { display: grid; grid-template-columns: 1.4em minmax(0, 1fr) auto;
  gap: var(--sp-1) var(--sp-3); align-items: baseline; padding: var(--sp-3) 0;
  border-top: 1px solid var(--rule); }
.marked-items li:first-child { border-top: 0; padding-top: 0; }
.mi-mark { font-weight: var(--fw-strong); }
.marked-items .is-ok .mi-mark { color: var(--ok-ink); }
.marked-items .is-wrong .mi-mark { color: var(--warn-ink); }
.mi-lab { color: var(--ink); }
.mi-val { font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--ink); }
.mi-why { grid-column: 2 / -1; margin: 0; color: var(--soft); font-size: var(--fs-3);
  line-height: 1.5; max-width: var(--measure); }
.mi-named { display: inline-block; margin-right: var(--sp-2); padding: 0 var(--sp-2);
  border-radius: 999px; background: var(--panel); border: 1px solid var(--rule);
  font-family: var(--mono); font-size: var(--fs-1); letter-spacing: var(--ls-caps-xs);
  text-transform: uppercase; color: var(--warn-ink); vertical-align: 1px; }
.marked-cap { margin-top: var(--sp-3); max-width: var(--measure); }


.sec-head { margin-bottom: var(--sp-5); }
.sec-head h2 { margin-bottom: var(--sp-2); }
.sec-head .a-note { margin: 0; max-width: var(--measure); }

/* 340px, not the kit's 280px. These cards carry a by-line and a fact line now, and at 280px the
   by-line wrapped to three lines on every card — the sentence that IS the proposition, rendered as
   the most broken-up text on the page. */
/* ⚠ min(), NOT a bare 340px floor. A minmax() floor is a hard minimum: below a container of
   340px the track stays 340 and the grid overflows its own page — at a 320px viewport the
   card measured 340 wide and the document scrolled sideways by 34px. min(340px, 100%) keeps
   the floor everywhere it fits and collapses to the container where it does not, so nothing
   changes at any width that already worked. The kit's own .cardgrid has the same shape with
   a 280px floor and the same fix. */
.cardgrid { grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr));
  gap: var(--sp-5); }
/* ⚠ NO CARD ALONE ON ITS ROW. Four courses three across left the fourth by itself (the 0.25.0
   review), and ten — the catalogue's size in code — does the same. When the count leaves one over
   on a three-across row (4, 7, 10, 13…), the grid goes two across: 2+2, or five rows of two. Only
   from 900px up: below that auto-fill gives two across or one anyway, and a forced two would crush
   a phone. Seven still leaves one — no split into twos or threes avoids it. */
@media (min-width: 900px) {
  .cardgrid:has(> :last-child:nth-child(3n+1):nth-child(n+4)) {
    grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* The course card. The same four facts in the same order on every card, so two side by side are
   comparable line for line instead of each being its own small layout. */
.coursecard { display: flex; flex-direction: column; gap: var(--sp-3); padding: 0; }
/* The card image is FLUSH: it is the top of the card, so the card's padding moves onto the text
   below it rather than framing the picture in a border of ground colour. */
.coursecard > :not(.cc-fig) { margin-left: var(--sp-5); margin-right: var(--sp-5); }
.coursecard > .cc-top { margin-top: var(--sp-5); }
/* Under the picture the first thing (the tag row, or the title when a course has no tags) sits one padding down,
   level with the card's side padding: the card's own gap already puts --sp-3 there. */
.coursecard > .cc-fig + :not(.cc-top) { margin-top: calc(var(--sp-5) - var(--sp-3)); }
.coursecard > :last-child { margin-bottom: var(--sp-5); }
.cc-fig { margin: 0; display: block; border-bottom: 1px solid var(--rule); background: var(--panel); }
/* .cc-fig picture as well as img: the holding page wraps the figure in a <picture> to pick a dark
   source, and a <picture> is inline by default — which leaves a text-baseline gap under the image
   and a hairline of card ground above the rule the figure is supposed to sit flush against. */
.cc-fig picture { display: block; }
.cc-fig img { display: block; width: 100%; height: auto; }
.coursecard h3 { font-size: var(--fs-6); line-height: var(--lh-6); letter-spacing: var(--ls-6);
  color: var(--ink); margin: 0; }
.cc-top { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
.cc-top .cc-langs { margin-left: auto; }   /* right-aligned with or without a chip beside it */
.cc-langs { display: flex; gap: var(--sp-1); }
/* Which language a course is taught in is unguessable now that it is per-course, and it is the
   first thing that disqualifies a course for a reader. Quiet, but present on every card. */
.cc-lang { font-family: var(--mono); font-size: var(--fs-1); letter-spacing: .04em;
  color: var(--muted); border: 1px solid var(--rule); border-radius: 999px; padding: 1px 8px; }
.cc-strap { color: var(--soft); margin: 0; text-wrap: pretty; }
/* What the masterplan says about a course, as chips: its level, who it is for, whether it is free. Quiet, and
   on its own row under the status, so the card still reads title-first. */
.cc-tags { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
/* ONE HEIGHT FOR EVERY TAG on a card (the status pill and the chips), so the row reads as one line and not as
   items of slightly different sizes: 22px, the same side padding, centred text. */
:is(.coursecard, .pw-step) .cc-tags > * { box-sizing: border-box; display: inline-flex; align-items: center; height: 22px;
  margin: 0; padding: 0 var(--sp-2); line-height: 1; white-space: nowrap;
  /* ONE TEXT STYLE for every tag, the status pill included: the pill kept the kit's mono capitals at 9.5px beside
     chips in the body face at 10.5px, so the row read as two typefaces on two baselines. A tag's colour says what
     it is (amber: where it stands; green: free; ink: who it is for); its typography does not change. */
  font-family: inherit; font-size: var(--fs-1); font-weight: var(--fw-ui); letter-spacing: .02em; text-transform: none;
  border: 1px solid transparent; border-radius: 999px; }
:is(.coursecard, .pw-step) .cc-chip { border-color: var(--rule); color: var(--soft); }
:is(.coursecard, .pw-step) .cc-aud { background: var(--well); border-color: transparent; color: var(--ink); }
/* The call to action on a course that is not live: a button, not a caption. The whole card is the link, so this is
   drawn as one (outlined in the accent; filled when the card is hovered or focused) and not nested as another. */
.coursecard .cc-go.cc-register { display: inline-block; align-self: flex-start; padding: 8px 12px;
  border: 1px solid var(--accent); border-radius: 8px; }
.coursecard:hover .cc-go.cc-register, .coursecard:focus-visible .cc-go.cc-register {
  background: var(--accent); color: var(--panel); }
:is(.coursecard, .pw-step) .cc-free { color: var(--ok-ink); border-color: color-mix(in srgb, var(--ok-ink) 35%, transparent); }
/* The catalogue's filters: rows of links, a label and a chip per value. The chosen value is filled. */
/* THE ROUTES PAGE (render.pathways_html, server-drawn): five routes as numbered steps, then the four ways. */
.pw-link { margin: var(--sp-3) 0 0; font-size: var(--fs-3); }
.pw-link a { color: var(--accent); font-weight: var(--fw-ui); }
/* the buyer's checkout (B2a): a link inside a sentence (the terms in the consent line, My courses on the thanks page) reads like the other links, not the browser's blue */
.a-textlink { color: var(--accent); text-underline-offset: 2px; }
.pw-routes { display: grid; gap: var(--sp-6); margin: var(--sp-6) 0; }
.pw-route h2, .pw-ways h2 { margin: 0 0 var(--sp-2); }
.pw-for { margin: 0 0 var(--sp-4); color: var(--muted); font-size: var(--fs-3); }
.pw-steps { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-3); }
.pw-step { display: grid; grid-template-columns: 34px 1fr; gap: var(--sp-3); align-items: start;
  border: 1px solid var(--rule); border-radius: var(--r-well); padding: var(--sp-3) var(--sp-4);
  background: var(--panel); }
.pw-n { display: inline-grid; place-items: center; width: 28px; height: 28px; border-radius: 50%;
  background: var(--ink); color: var(--panel); font-weight: var(--fw-ui); font-size: var(--fs-2); }
.pw-title { font-weight: var(--fw-ui); font-size: var(--fs-4); }
.pw-title a { color: inherit; }
/* The step's tags are the card's tags (class cc-tags on the row): the same one height, gap and text style, so a
   route reads in the typeface of the catalogue it links to. */
.pw-meta { margin: var(--sp-2) 0; }
.pw-line { margin: 0; color: var(--muted); font-size: var(--fs-3); }
.pw-ways { margin: var(--s-sec) 0 var(--sp-6); }
.pw-waygrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--sp-4); }
.pw-way { border: 1px solid var(--rule); border-radius: var(--r-well); padding: var(--sp-4); }
.pw-way h3 { margin: 0 0 var(--sp-1); }
.pw-way ul { margin: 0; padding-left: 1.1em; font-size: var(--fs-3); }
.pw-way li + li { margin-top: var(--sp-1); }
@media (max-width: 640px) {
  .pw-step { grid-template-columns: 28px 1fr; padding: var(--sp-3); }
}

/* The editor's masterplan tags: each set is a fieldset of checkboxes, two or three across, wrapping. */
.adm-plantags { margin: var(--sp-5) 0; display: grid; gap: var(--sp-3); }
.adm-plantags h3 { margin: 0; }
.adm-plantags > .a-note, .adm-tagset > .a-note { margin: 0; }
.adm-tagset { border: 0; padding: 0; margin: var(--sp-2) 0 0; min-width: 0; }
/* the kit's micro-label, which a <legend> does not get from the label rule */
.adm-tagset > legend { padding: 0; margin-bottom: var(--sp-2); text-transform: uppercase;
  letter-spacing: .08em; font-size: var(--fs-1); font-weight: var(--fw-ui); color: var(--muted); }
.adm-tagboxes { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: var(--sp-2) var(--sp-4); }
/* The course's teaching languages: two boxes on ONE line, so the cell is as tall as the number fields beside it. */
.adm-langboxes { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-4); min-height: 33px; }

/* The editor's method types: one disclosure per family, a row per method with its depth beside it. */
.adm-methods .adm-family { border-top: 1px solid var(--rule); padding: var(--sp-2) 0; }
.adm-methods summary { cursor: pointer; font-weight: var(--fw-ui); padding: var(--sp-1) 0; }
.adm-methodrows { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(360px, 100%), 1fr));
  gap: var(--sp-2) var(--sp-4); padding: var(--sp-2) 0 var(--sp-3); }
.adm-method { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }
.adm-method .adm-depth { width: auto; min-width: 0; font-size: var(--fs-2); }

/* THE WAYS IN: the card on a course page (one row per way, its fee at the right) and the pages /ways and /teams. */
.ways-list { list-style: none; margin: var(--sp-3) 0 var(--sp-2); padding: 0; display: grid; }
.ways-list li { display: flex; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-2) 0; border-top: 1px solid var(--rule); }
.ways-list li:first-child { border-top: 0; }
.way-name { font-weight: var(--fw-ui); }
.way-fee { color: var(--soft); text-align: right; }
.ways-grid { margin-top: var(--sp-6); }
.ways-grid .pw-way { display: flex; flex-direction: column; padding: var(--sp-5) var(--sp-4); }
.ways-grid .pw-way h2 { font-size: var(--fs-6); line-height: var(--lh-6); letter-spacing: var(--ls-6);
  margin: 0 0 var(--sp-1); }
.ways-grid .way-feeline { margin-top: auto; padding-top: var(--sp-4); }
.way-how { margin: var(--sp-3) 0 0; color: var(--soft); }
.way-more { margin: 0; }
.way-feeline { margin: var(--sp-3) 0 0; font-weight: var(--fw-ui); }
.way-feeline span { margin-right: var(--sp-2); font-size: var(--fs-2); letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
.ways-note { margin: var(--sp-6) 0; max-width: var(--measure); }
.ways-team { margin: var(--sp-6) 0; max-width: var(--measure); }
.ways-team-list li { display: list-item; list-style: disc inside; border-top: 0; }

/* The staff list (the owner's screen): one row per address, the address first, what it is beside it. */
.adm-stafflist { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }
.adm-stafflist li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3);
  border: 1px solid var(--rule); border-radius: var(--r-ctl); padding: var(--sp-2) var(--sp-3); }
.adm-staffadd { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--sp-3); }
.adm-staffadd .ff { margin: 0; flex: 0 1 28rem; min-width: min(220px, 100%); }
.adm-staffwho { font-weight: var(--fw-ui); flex: 1 1 14em; min-width: 0; overflow-wrap: anywhere; }

/* A practical's wording in the editor: one group per answer box, the spec's own words as the placeholders. */
/* Files a learner downloads (the editor's lesson and course file panels): a list, then a row to add one. */
.adm-files { margin-top: var(--sp-4); display: grid; gap: var(--sp-3); }
.adm-filelist { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }
.adm-filelist li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3);
  border: 1px solid var(--rule); border-radius: var(--r-ctl); padding: var(--sp-2) var(--sp-3); }
.adm-filelist li > :nth-child(2) { flex: 1 1 14ch; min-width: 0; overflow-wrap: anywhere; }
.adm-filerole { font-size: var(--fs-1); font-weight: var(--fw-ui); color: var(--soft); min-width: 6em; }
.adm-fileadd { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--sp-3); }
.adm-fileadd .ff { margin: 0; min-width: min(220px, 100%); }
.adm-wording { margin-top: var(--sp-4); display: grid; gap: var(--sp-3); }
.adm-wording-group { display: grid; gap: var(--sp-2); border-left: 3px solid var(--rule); padding-left: var(--sp-3); }
.adm-wording-key { font-weight: var(--fw-ui); }
/* THE BRIEF AND HINTS PANEL beside it (the staff editor, Part 2): the language toggle, the code's own words read-only in the dark code box, and four boxes (the brief and the three hints) in the code font. */
.adm-brief { margin-top: var(--sp-4); display: grid; gap: var(--sp-3); }
.adm-brief .seg { justify-self: start; }
.adm-brief > .adm-fieldnote { font-size: var(--fs-3); }
.adm-brief > .adm-fieldnote:empty { display: none; }
.adm-brief-body { display: grid; gap: var(--sp-3); }
.adm-brief-body .btn { justify-self: start; }
.adm-brief-body > p { margin: 0; }
.adm-brief-default { max-height: 60vh; overflow: auto; }
.adm-brief-box { width: 100%; box-sizing: border-box; font-family: var(--mono); font-size: var(--fs-3); line-height: 1.5; white-space: pre; overflow: auto; tab-size: 2; resize: vertical; }

/* THE ASSESSED ATTEMPT: under "Your data", what an assessed attempt is and costs, with the button to its own dataset (or back to practice); the credential's state on the course page. */
.ex-assess { margin: var(--sp-4) 0 var(--sp-3); display: grid; gap: var(--sp-2); justify-items: start; padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--rule); border-radius: var(--r-ctl); background: var(--panel); }
.ex-assess .a-note { margin: 0; }
.ex-assess.is-assessed { border-left: 3px solid var(--accent); }      /* the assessed screen reads as a different mode at a glance */
/* ⚠ `hidden` on a button is overridden by .btn's own display, so a passed, spent or closed practical still drew "Start an assessed attempt" (the same trap the tick-box fell into, found by LOOKING at the reveal's screenshot, 16 Oct 2026). */
.ex-assess .btn[hidden] { display: none; }
.exh-mode { margin-left: var(--sp-2); padding: 1px 8px; border-radius: 999px; background: var(--blue-panel); font-size: var(--fs-2);
  font-weight: var(--fw-ui); }
.cred-card p { margin: var(--sp-2) 0 0; }
.adm-cred { display: grid; gap: var(--sp-1); justify-items: start; }
.adm-cred .btn { margin-top: var(--sp-1); }

/* Before you start (a course page's Needs and Helps): a quiet list, the course names as the accent's links. */
.prereq { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }
.prereq a { color: var(--accent); font-weight: var(--fw-ui); }

.cat-filters { display: grid; gap: var(--sp-3); margin: var(--sp-5) 0 var(--sp-6); }
.cf-group { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.cf-label { min-width: 74px; font-size: var(--fs-1); font-weight: var(--fw-ui); letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted); }
.cf-chip { font-size: var(--fs-3); font-weight: var(--fw-ui); padding: 4px 12px; border-radius: 999px;
  border: 1px solid var(--rule); color: var(--soft); text-decoration: none; }
.cf-chip:hover { border-color: var(--ink); color: var(--ink); }
.cf-chip.is-on { background: var(--ink); border-color: var(--ink); color: var(--panel); }
.cf-count { margin: calc(-1 * var(--sp-3)) 0 var(--sp-5); }
/* On a phone five wrapped rows would push every card below the fold, so each group is ONE line that scrolls
   sideways, and the bar stays five lines tall however many values a group has. */
@media (max-width: 640px) {
  .cat-filters { gap: var(--sp-2); margin: var(--sp-4) 0 var(--sp-5); }
  .cf-group { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
  .cf-group::-webkit-scrollbar { display: none; }
  .cf-label { flex: 0 0 auto; min-width: 0; }
  .cf-chip { flex: 0 0 auto; white-space: nowrap; }
}
/* The by-line carries the whole proposition, so it is the one line on the card wearing the accent. */
.cc-by { margin: 0; font-size: var(--fs-3); font-weight: var(--fw-ui); color: var(--accent); }
.cc-facts { margin: auto 0 0; font-size: var(--fs-3); color: var(--muted);
  font-variant-numeric: tabular-nums; }
/* The two facts a reader actually decides on, on their own line under a rule: what it costs, and
   when it next runs. The price leads because it is the one that disqualifies. */
.cc-offer { display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: var(--sp-1) var(--sp-3);
  margin: 0; padding-top: var(--sp-3); border-top: 1px solid var(--rule);
  font-size: var(--fs-3); font-variant-numeric: tabular-nums; }
.cc-offer .cc-price { font-weight: var(--fw-ui); color: var(--ink); }
.cc-offer .cc-when { color: var(--muted); }
/* .cc-facts keeps the auto top margin that pushes the foot down; when a card has no facts line the
   offer row inherits the job, or an announced card's date rides up under its strapline. */
.coursecard > .cc-offer:not(.cc-facts + .cc-offer) { margin-top: auto; }

/* A page head: the title, and one line saying what the page holds. The catalogue had a bare h1 and
   a lede floating on the canvas with nothing grouping them. */
.pagehead { margin-bottom: var(--s-sec); }
.pagehead h1 { margin-bottom: var(--sp-3); }
.pagehead .a-lede { margin-bottom: 0; }

/* ── CARDS ARE PLACED, NOT DRAWN ────────────────────────────────────────────────────────────
   A card carried a border AND a shadow, and one course card drew three separate lines: its own
   edge, the figure's bottom rule and the offer row's top rule. A bordered box is the pre-flat-
   design idiom; a surface that sits above the page is separated by light, and the border is kept
   for edges you can actually interact with.

   The kit's --shadow-card is the same two-layer shape as --shadow-2 but in a violet-black
   (23,20,40) where the rest of this app's depth is brand navy (21,45,73). Two light sources on one
   screen is a thing you feel before you can name it, so the app's rung is used here instead. */
.card { border: 0; box-shadow: var(--shadow-2), var(--edge); }
/* An announced course is not yet buildable and has always said so with a dashed edge. That reads
   as "outlined, provisional" against the solid cards — the one place a border still carries
   meaning — so it keeps one, and forgoes the elevation the real cards get. */
.card.is-announced { border: 1px dashed var(--rule); box-shadow: none; }
/* The figure already sits inside the card; a second line under it is the same separation drawn
   twice. */
.cc-fig { border-bottom: 0; }

a.card { transition: box-shadow var(--t2, .17s) var(--ease, ease),
                     transform var(--t1, .12s) var(--ease, ease); }
/* Was border-colour. Changing an edge tint on hover is the idiom from before transforms were
   cheap; a card that lifts says the same thing in the language the rest of the OS uses. */
a.card:hover { box-shadow: var(--shadow-3), var(--edge); }
a.card:hover h3 { color: var(--accent); }
@media (prefers-reduced-motion: no-preference) {
  a.card:hover { transform: translateY(-2px); }
}
/* ⚠ Depth and the focus ring are BOTH box-shadow, so one rule replaces the other rather than
   adding to it — whichever lands later simply wins, and nothing warns you. Source order currently
   hands it to .card, which leaves the ring silently inert on the most prominent link in the app.
   Composed explicitly instead: ring first, so it reads outside the cast rather than under it. */
a.card:focus-visible {
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--focus-ring) 20%, transparent),
              var(--shadow-2), var(--edge);
}

/* The consultancy hand-off is the second half of the mandate and it was a bare paragraph after a
   rule. On a page of cards that reads as leftover text, so it becomes a panel — the one tinted
   block below the band, which is also what makes it the last thing the eye lands on. */
/* ── the enquiry pitch, and the page it points at ──────────────────────────
   --accent-soft, not --panel. --panel sits a couple of L* off --bg, which is right for a well
   inside a white card and invisible as a block ON the page ground: the pitch rendered as text
   floating in a rectangle nobody could see. This is the one tinted block below the band, which is
   also what makes it the last thing the eye lands on.

   The pitch replaced an 825px form that rendered at the foot of home, the catalogue and every
   course page — three copies of one thing, each doubling the length of the page it sat on, and none
   of them linkable. */
.cpitch { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--s-sec);
  align-items: center; margin-top: var(--s-major);
  background: var(--accent-soft); border-radius: var(--r-well); padding: var(--sp-8); }
.cpitch h2 { margin-top: 0; font-size: var(--fs-6); line-height: var(--lh-6);
  letter-spacing: var(--ls-6); }
.cpitch p { max-width: var(--measure); color: var(--soft); margin-bottom: 0; }
.cpitch-cta { white-space: nowrap; text-decoration: none; }
@media (max-width: 760px) {
  /* Stacked, the button goes full width — a nowrap pill floating right of nothing reads as
     debris. */
  .cpitch { grid-template-columns: minmax(0, 1fr); gap: var(--sp-5); padding: var(--sp-6); }
  .cpitch-cta { justify-content: center; }
}

/* /contact — the form, and the three things a reader wants to know before filling one in. */
.contactgrid { display: grid; grid-template-columns: minmax(0, 1fr) 260px; gap: var(--s-sec);
  align-items: start; }
.contactgrid .interest { margin-top: 0; padding-top: 0; border-top: 0; max-width: none; }
.contactside { display: flex; flex-direction: column; gap: var(--s-rule); }
.cs-block { padding: var(--sp-4) var(--sp-5); background: var(--panel);
  border-radius: var(--r-well); }
.cs-block .a-note { margin: var(--sp-2) 0 0; }
@media (max-width: 900px) {
  .contactgrid { grid-template-columns: minmax(0, 1fr); }
  .contactgrid .interest { max-width: 42rem; }
}

/* .consult is ONLY the enquiry at the end of a finished course, on its course page. It was also drawn under every passed
   practical until 15 Oct 2026 (the owner: no consultancy asks in the middle of courses), and before that it was the
   home page's pitch, which moved to .cpitch and left these rules saying `background: none; padding: 0`.

   A panel, so that it reads as an aside and not as more course. */
.consult { background: var(--panel); border-radius: var(--r-well); padding: var(--sp-6);
  margin-top: var(--s-sec); }
.consult h2 { margin-top: 0; font-size: var(--fs-6); line-height: var(--lh-6);
  letter-spacing: var(--ls-6); }

/* The by-line. Authorship is the entire differentiator and it rendered as a 12.5px muted footnote,
   smaller and greyer than the duration on each curriculum row. instructor.bio_md and .orcid exist
   in the schema and no renderer touched either. */
.byline { margin-top: var(--sp-5); padding: var(--sp-4) var(--sp-5);
  border: 1px solid var(--rule); border-left: 3px solid var(--accent);
  border-radius: 0 var(--r-well) var(--r-well) 0; background: var(--panel);
  max-width: var(--measure); }
/* A by-line carrying ONLY a name — no bio, no ORCID, no link — was a full-height well holding one
   line, which reads as something that failed to load rather than as a credit. The padding follows
   the content. ⚠ This is a LAYOUT fix and not a content one: the reason a name stands alone is that
   the instructor has no bio_md, and the answer to that is to write one. */
.byline:not(:has(> * + *)) { padding: var(--sp-3) var(--sp-5); }
/* THE LEARNER'S HOME (redesign step 4). "My courses" opens on a greeting, and each course under it is a home of its own: three figures, what is next
   beside the live school the learner holds a place on, the practicals as a row of tiles and, under them, what has been shown. The card chrome comes
   off .mine-card: each panel inside is the card. (The old grid of 560px cards, and the guard that kept one of them from spanning the band, went
   with it: a course home takes the width of the page by design.) */
.mine-homes { display: grid; gap: var(--s-sec); }
.mine-card { display: grid; gap: 20px; padding: 0; border: 0; border-radius: 0; background: none; box-shadow: none; }
.mine-card > h3:first-child { margin: 0; font-size: var(--fs-7); font-weight: 600; letter-spacing: -.015em; }
.mh-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr)); gap: 20px; }
.mh-stat { display: flex; flex-direction: column; gap: 8px; padding: 20px 24px; background: var(--surface); border: 1px solid var(--rule); border-radius: 14px; }
.wb-k, .mine-k, .mine-shown h4 { font-family: var(--mono); font-size: var(--fs-1); font-weight: var(--fw-label);
  letter-spacing: var(--ls-caps-xs); text-transform: uppercase; color: var(--muted); flex: none; }
.mh-v { font-size: var(--fs-9); font-weight: 600; line-height: 1.05; letter-spacing: -.02em; color: var(--ink); font-variant-numeric: tabular-nums; }
.mh-u { margin-left: 4px; font-size: var(--fs-5); font-weight: 500; letter-spacing: 0; color: var(--muted); }
.mh-s { font-size: var(--fs-3); color: var(--muted); }
.mh-stat.access-ended .mh-v { color: var(--fail-ink); }
.mh-cols { display: grid; grid-template-columns: minmax(0, 1.9fr) minmax(0, 1fr); gap: 20px; align-items: start; }
.mh-cols:not(:has(.mh-side)) { grid-template-columns: minmax(0, 1fr); }
.mh-side { display: grid; gap: 20px; }
.wb-panel { background: var(--surface); border: 1px solid var(--rule); border-radius: 14px; padding-bottom: 24px; }
.wb-panel > :not(.wb-ph) { margin-left: 24px; margin-right: 24px; }
.wb-ph { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 16px 24px; margin-bottom: 20px; border-bottom: 1px solid var(--rule); }
.wb-ph .wb-k { color: var(--soft); font-weight: 600; }
.wb-note { font-size: var(--fs-3); color: var(--muted); }
.mh-link { display: inline-flex; align-items: center; min-height: var(--sp-6); font-size: var(--fs-3); font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--accent); text-decoration: none; }
.mh-link:hover { text-decoration: underline; text-underline-offset: 3px; }
.mh-done { margin: var(--sp-5) 0 0; }
.mh-up { list-style: none; margin-top: 16px; margin-bottom: 0; padding: 0; border-top: 1px solid var(--rule); }
.mh-up li + li { border-top: 1px solid var(--rule); }
.mh-up-row { display: grid; grid-template-columns: 2.2rem 5.8rem minmax(0, 1fr) auto; align-items: baseline; gap: 12px; padding: 12px 0; color: var(--ink); text-decoration: none; }
.mh-up-row:hover .mh-t { color: var(--accent); }
.mh-n, .mh-min { font-family: var(--mono); font-size: var(--fs-2); color: var(--muted); white-space: nowrap; }
.mh-kind { font-family: var(--mono); font-size: var(--fs-1); letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
.mh-kind.is-practical { color: var(--accent); font-weight: 700; }
.mh-t { font-size: var(--fs-4); }
.mh-t .mine-k { margin-right: 8px; }
.mh-continue .progress-track { margin-bottom: 0; }
.mh-continue .mine-actions { margin-top: 20px; }
.mine-run p { margin: 0 0 var(--sp-1); }
.mh-side .mine-run { margin-bottom: 0; }
.mine-run .run-when { margin: 0 0 var(--sp-2); font-size: var(--fs-6); font-weight: 600; letter-spacing: -.01em; color: var(--ink); }
.mine-run .booked { color: var(--ok-ink); font-weight: var(--fw-ui); }
.mh-tiles { list-style: none; margin-top: 0; margin-bottom: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(min(210px, 100%), 1fr)); gap: 16px; }
.mh-tile { display: flex; flex-direction: column; gap: 8px; padding: 16px 20px; border: 1px solid var(--rule); border-radius: 12px; background: var(--surface); }
.mh-tk { font-family: var(--mono); font-size: var(--fs-1); letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
.mh-tt { font-size: var(--fs-5); font-weight: 600; line-height: 1.25; color: var(--ink); }
.mh-chip { align-self: flex-start; margin-top: 4px; padding: 3px 12px; border: 1px solid var(--rule-blue); border-radius: 99px;
  font-family: var(--mono); font-size: var(--fs-1); letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.mh-tile.is-next { border-color: var(--rule-blue); }
.mh-tile.is-next .mh-chip { border-color: var(--accent); color: var(--accent); font-weight: 700; }
.mh-tile.is-passed { background: var(--topbar); border-color: var(--topbar); }
.mh-tile.is-passed .mh-tk { color: rgba(255, 255, 255, .72); }
.mh-tile.is-passed .mh-tt { color: #fff; }
.mh-tile.is-passed .mh-chip { background: #fff; border-color: #fff; color: var(--rda-navy); font-weight: 700; }
.mh-practicals .mine-shown { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--rule); }
.mine-shown h4 { margin: 0 0 var(--sp-2); }
.mine-shown ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }
.mine-shown li { display: flex; gap: var(--sp-2); align-items: baseline; font-size: var(--fs-3); }
/* flex:none as in .lesson-row .st — something upstream gives a bare .st flex:1, and here it took the
   row and pushed the title to the far side of the card. */
.mine-shown .st { flex: none; width: 1em; text-align: center; line-height: 1; }
.mine-shown .mine-how { margin-left: auto; color: var(--muted); font-size: var(--fs-2);
  white-space: nowrap; }
/* THE STAFF ORDER SCREEN (redesign step 5). What was sold is a LEDGER: the items, the VAT on each, and what is owed under a double rule, in the
   server's own figures (the screen does no arithmetic of its own). The rest are panels; the buyer and the invoice stand beside them. */
.ord-grid { display: grid; grid-template-columns: minmax(0, 1.9fr) minmax(0, 1fr); gap: 20px; align-items: start; margin-top: 24px; }
.ord-main, .ord-side { display: grid; gap: 20px; min-width: 0; }
.wb-scroll, .ord-stmt-wrap { overflow-x: auto; }
.wb-panel.is-flush { padding-bottom: 0; overflow: hidden; }
.wb-panel.is-flush .wb-ph { margin-bottom: 0; }
.wb-panel.is-flush > .wb-scroll { margin-left: 0; margin-right: 0; }
.wb-panel.is-flush .opstable { margin: 0; }
.wb-panel.is-flush .bk-table th:first-child, .wb-panel.is-flush .bk-table td:first-child { padding-left: 24px; }
.wb-panel.is-flush .bk-table th:last-child, .wb-panel.is-flush .bk-table td:last-child { padding-right: 24px; }
.ord-stmt { width: 100%; border-collapse: collapse; font-size: var(--fs-4); }
.ord-stmt th, .ord-stmt td { padding: 12px 0; vertical-align: baseline; font-weight: 400; color: var(--ink); text-transform: none; letter-spacing: 0; }
.ord-stmt thead th { padding: 0 0 8px; border-bottom: 2px solid var(--ink); font-family: var(--mono); font-size: var(--fs-1); font-weight: 500;
  letter-spacing: .12em; text-transform: uppercase; color: var(--muted); text-align: left; }
.ord-stmt thead th.num { text-align: right; }
.ord-stmt .num { text-align: right; white-space: nowrap; font-family: var(--mono); font-variant-numeric: tabular-nums; padding-left: 20px; }
.ord-stmt tbody th { text-align: left; font-family: var(--font); font-size: var(--fs-4); }
.ord-stmt tbody th small { display: block; margin-top: 2px; font-size: var(--fs-2); color: var(--muted); }
.ord-stmt tbody tr + tr > * { border-top: 1px solid var(--rule); }
.ord-stmt tr.is-total > * { padding: 16px 0 12px; font-size: var(--fs-6); font-weight: 700; border-top: 1px solid var(--ink); border-bottom: 3px double var(--ink); }
.ord-stmt tr.is-total small { font-size: var(--fs-3); font-weight: 400; }
.ord-when { margin: 12px 0 0; font-size: var(--fs-3); color: var(--muted); }
@media (max-width: 900px) {
  .mh-cols, .ord-grid { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 560px) {
  .wb-panel > :not(.wb-ph) { margin-left: 16px; margin-right: 16px; }
  .wb-ph { padding: 16px; }
  .mh-up-row { grid-template-columns: 1.8rem minmax(0, 1fr); row-gap: 2px; }
  .mh-kind { grid-column: 2; }
  .mh-t { grid-column: 2; }
  .mh-min { display: none; }
  .mh-v { font-size: var(--fs-8); }
  .mh-stat { padding: 16px 16px; }
  .mh-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
  .mh-stat:nth-child(odd):last-child { grid-column: 1 / -1; }
}

.byline-name { font-size: var(--fs-4); font-weight: var(--fw-ui); }
.byline-role { font-size: var(--fs-3); color: var(--muted); margin-top: 2px; }
/* "TAUGHT BY": every instructor a course names, each a portrait beside the name, the job title and what the company's website prints
   about them (the owner, 15 Oct 2026). The portrait is the website's own, cropped to a disc from the top of the frame, where the face is. */
.byline-label { margin-bottom: var(--sp-3); font-size: var(--fs-1); font-weight: var(--fw-ui); letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted); }
.byline-person { display: flex; gap: var(--sp-4); align-items: flex-start; }
.byline-person + .byline-person { margin-top: var(--sp-4); padding-top: var(--sp-4); border-top: 1px solid var(--rule); }
.byline-photo { flex: none; width: 72px; height: 72px; border-radius: 50%; object-fit: cover; background: var(--well); }   /* the portraits are squares, cropped alike (see :root .ph-photo) */
.byline-who { min-width: 0; }
.byline-about { margin: var(--sp-2) 0 0; font-size: var(--fs-3); line-height: var(--lh-4); }
.byline-focus, .byline-edu { margin: var(--sp-2) 0 0; font-size: var(--fs-2); line-height: 1.5; color: var(--muted); }
.byline-focus span { margin-right: var(--sp-1); font-weight: var(--fw-ui); color: var(--ink); }
/* THE FREE WAY'S WAY TO THE CERTIFICATE (the owner, 15 Oct 2026): a card in the course aside, and one quiet line under a practical's
   answers. The card is a panel like its neighbours, with the one red action of the card; the line is a note and a link. */
/* WHAT A COURSE CONTAINS (the owner, 15 Oct 2026): a plain list on the course page, one muted line on a card. */
.contents-card h3 { margin: 0 0 var(--sp-3); }
.contents-list { margin: 0; padding: 0; list-style: none; display: grid; gap: var(--sp-2); font-size: var(--fs-3); line-height: var(--lh-4); }
.contents-list li { padding-left: var(--sp-4); position: relative; }
.contents-list li::before { content: ""; position: absolute; left: 0; top: .6em; width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent); }
.cc-format { margin: var(--sp-1) 0 0; font-size: var(--fs-2); line-height: 1.5; color: var(--muted); }
.certified-card { display: grid; gap: var(--sp-3); }
.certified-card h3, .certified-card p { margin: 0; }
.certified-cta { justify-self: start; }
.ex-practice-note { margin: var(--sp-3) 0 0; }
/* THE BRIEF'S HINTS (app/briefs.py): under the listing, a note, one button, and the hints already asked for, each a card with a title and its code on the same navy ground as a
   listing. An assessed attempt has the note and no button. Tokens only. */
.ex-hints { display: grid; gap: var(--sp-3); margin: var(--sp-4) 0; }
.ex-hints > p { margin: 0; }
.ex-hint-row { display: flex; gap: var(--sp-3); align-items: center; flex-wrap: wrap; }
.ex-hint-row .ex-hints-done { margin: 0; }
.ex-hint-list { display: grid; gap: var(--sp-3); }
.ex-hint-list:empty { display: none; }
.ex-hint-card { display: grid; gap: var(--sp-2); }
.ex-hint-card h3 { margin: 0; font-size: var(--fs-4); }
.ex-hint-code { margin: 0; max-height: 70vh; overflow: auto; }
/* THE HINTS ARE READING; THE RUN BOX IS THE INSTRUMENT. The owner, 18 Oct 2026: "Can we differentiate between code hint chunks and the section where you can actually run code? Different colour backgrounds?"; of three
   mock-ups he chose A. A hint is help to read, so it sits on a pale panel with a rule down its side and the page's own ink; the task listing and the run box (.rt-editor) keep the dark code surface, so in both themes
   dark means "where you work" and pale means "help". `pre.code` sets its own background and colour with the weight of an element and a class, so this is two classes and an element. --accent-soft is a GROUND by its own
   comment and --ink is the page's own text (navy on the light ground, near-white on the dark one: the rule is as legible as the text); tests/test_hint_panel.py holds the contrast and that the run box stays dark. */
pre.code.ex-hint-code { background: var(--accent-soft); color: var(--ink); border-left: 4px solid var(--ink); border-radius: 6px; }
.byline-orcid { display: inline-block; margin-top: var(--sp-2);
  font-family: var(--mono); font-size: var(--fs-2); color: var(--accent); }
.free-cta, .consult-cta { display: inline-block; margin-top: var(--sp-3);
  font-size: var(--fs-3); font-weight: var(--fw-ui); color: var(--accent); text-decoration: none; }
.free-cta:hover, .consult-cta:hover { text-decoration: underline; }

/* A contents list, not a card grid — see contentsList() for why. */
.contents a { display: grid; grid-template-columns: var(--gutter-num) minmax(0, 1fr) auto;
  gap: var(--sp-4); align-items: baseline; padding: var(--sp-4) 0;
  border-bottom: 1px solid var(--rule); text-decoration: none; color: inherit;
  position: relative; }
.contents a:last-child { border-bottom: 0; }
.contents .n { font-family: var(--mono); font-size: var(--fs-1); color: var(--muted); }
.contents h3 { margin: 0; font-size: var(--fs-7); }
.contents .a-note { margin: var(--sp-1) 0 0; max-width: var(--measure-note); }
.contents .meta { font-family: var(--mono); font-size: var(--fs-3); color: var(--muted);
  text-align: right; white-space: nowrap; }
.contents a::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
  background: var(--accent); transform: scaleX(0); transform-origin: left; }
.contents a:hover::before, .contents a:focus-visible::before { transform: scaleX(1); }
@media (prefers-reduced-motion: no-preference) {
  .contents a::before { transition: transform var(--t2, .17s) var(--ease, ease); }
}
@media (max-width: 640px) {
  .contents a { grid-template-columns: var(--gutter-num) 1fr; }
  .contents .meta { grid-column: 2; text-align: left; }
}

/* No left rule. It was a 1px hairline on a column that is ~400px tall beside ~1800px of curriculum,
   so it stopped a quarter of the way down the page and ended in mid-air — a divider that divides
   part of something reads as a rendering fault, not as structure. The column gap already separates
   the two, and each card carries its own surface. */
.course-aside { display: flex; flex-direction: column; gap: var(--sp-4); }
/* ⚠ ONE TOP SPACING FOR BOTH CARDS IN THIS COLUMN, and it had drifted into two. The kit pads a
   .card 20px, and a card whose first child is an <h3> then adds the heading's own 24px margin on
   top — so "Software" started 44px down while the run card, whose first child is a small note
   (and whose margins were zeroed when its spacing moved to a flex gap), started at 20px. Side by
   side in the same column, one looked squished against the other, which is exactly what it was.
   The padding does the spacing here and the first child's margin is removed, so both start in the
   same place regardless of whether that child is a heading or a line of text. */
.course-aside .card { padding-top: var(--sp-6); }
.course-aside .card > :first-child { margin-top: 0; }
.run-price, .run-when { margin: 2px 0 var(--sp-3); font-size: var(--fs-5);
  font-weight: var(--fw-ui); }
/* ⚠ LAST, AND THAT IS THE WHOLE POINT. .run-card spaces its children with one flex gap, so their
   own margins have to go — but .run-when is declared TWICE, at two points 350 lines apart, and both
   selectors are a single class. A reset placed after the first one loses to the second in the
   cascade and the date keeps a margin nobody can see the source of. Anything that sets a margin on
   a run-card child must go ABOVE this line. */
.run-card > * { margin-top: 0; margin-bottom: 0; }

/* THE TIMETABLE. Collapsed by default: a five-day school is twenty-odd rows, and a run card that
   is mostly timetable buries the two things a reader came for — when it starts, and how to book.
   The summary carries the count, so the shape of the commitment is legible without opening it. */
.timetable { margin: 0 0 var(--sp-3); }
/* ⚠ THE SCALE RUNS UPWARDS: --fs-2 is 12px and --fs-7 is 24px. This block was first written with
   --fs-6 for a row and --fs-7 for a note, on the assumption that a bigger number meant smaller
   text, and the result was a six-session timetable in 20px type that wrapped every row onto three
   lines and swallowed the run card whole. Only visible by looking at it. */
.timetable > summary { cursor: pointer; font-size: var(--fs-3); color: var(--muted);
  padding: 4px 0; list-style-position: outside; }
.timetable > summary:hover { color: var(--ink); }
.timetable > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px;
  border-radius: 4px; }
.timetable-list { list-style: none; margin: var(--sp-2) 0 0; padding: 0;
  border-left: 2px solid var(--rule); }
.timetable-list li { padding: var(--sp-2) 0 var(--sp-2) var(--sp-3); font-size: var(--fs-3); line-height: 1.45;
  /* Times line up in a column, which is the entire reason to render a timetable as a list. */
  font-variant-numeric: tabular-nums; }
.timetable-list li + li { border-top: 1px solid var(--rule); }
/* A cancelled session stays legible rather than dimmed to nothing: "we cancelled Thursday" is
   information, and a reader who already saw the timetable needs to find it still there. */
.timetable-list li.s-off { color: var(--muted); }
.timetable-list li.s-off s { text-decoration-thickness: 1px; }
/* The note sits UNDER the time rather than beside it. Inline, a cancellation reason turned a
   two-line row into a five-line one and the strikethrough ran through the middle of the reason. */
.timetable-list .s-note { display: block; color: var(--muted); font-size: var(--fs-2);
  text-decoration: none; }

/* The structured-abstract idiom: the label hangs in the margin where there is room for it. */
.course-note { position: relative; margin: var(--s-sec) 0; }
.course-note > h2 { margin: 0 0 var(--sp-3); }
.course-note p { max-width: var(--measure); }
@media (min-width: 1120px) {
  .course-note { padding-left: var(--gutter-label); }
  .course-note > h2 { position: absolute; left: 0; top: .35em;
    width: calc(var(--gutter-label) - var(--sp-4));
    font-family: var(--mono); font-size: var(--fs-1); line-height: var(--lh-1);
    font-weight: var(--fw-label); letter-spacing: var(--ls-caps-xs);
    text-transform: uppercase; color: var(--muted); margin: 0; }
}

/* The four practicals are the one thing no competitor has. They were two characters in the
   faintest ink in the system, in a column of dots. */
a.lesson-row.is-practical, .lesson-row.is-practical {
  border-left: 3px solid var(--accent); padding-left: var(--sp-3);
  background: var(--accent-soft); border-radius: 0 var(--r-ctl) var(--r-ctl) 0; }
/* The kind word: a fixed column between the number and the title, so the eye can run down it.
   Quiet for the ordinary kinds; the accent is kept for what is marked or operated. */
.lesson-row .kind { flex: none; width: 7em; font-size: var(--fs-2); font-weight: var(--fw-ui);
  letter-spacing: var(--ls-caps-xs); text-transform: uppercase; color: var(--muted); }
.lesson-row.is-practical .kind { color: var(--accent); }
/* The map's marks and facts. The mark is a glyph so its SHAPE carries the state without colour;
   the colour agrees with it. The fact after the title is in the same small voice as the duration. */
.lesson-row .st { flex: none; width: 1em; text-align: center; font-size: var(--fs-3); line-height: 1; }
.st.st-demonstrated { color: var(--ok-ink); }
.st.st-opened { color: var(--accent); }
.st.st-ahead { color: var(--faint); }
.lesson-row .ev { font-size: var(--fs-2); color: var(--muted); white-space: nowrap; }
.lesson-row .ev.is-next { color: var(--accent); font-weight: var(--fw-ui); }
a.lesson-row.is-next .nm { font-weight: var(--fw-ui); color: var(--ink); }
a.lesson-row.is-demonstrated .nm { color: var(--soft); }
.rail-row .rtick.st { font-size: var(--fs-2); }
@media (max-width: 600px) {
  /* A phone has no room for a fifth column: the word moves under the title. */
  a.lesson-row, .lesson-row.is-pending { flex-wrap: wrap; }
  .lesson-row .kind { width: auto; order: 5; flex-basis: 100%; padding-left: 36px; margin-top: -6px; }
}
.lesson-row .free { font-size: var(--fs-3); font-weight: var(--fw-ui); color: var(--accent);
  white-space: nowrap; }

/* Numbered in the same gutter as the curriculum, so the promise and the detail read as one design.
   → was the bullet here; the brand reserves it for terminating a link, and it pointed at four
   static sentences while the two things that actually navigate had no arrow at all. */
.outcomes { counter-reset: outc; }
.outcomes li { counter-increment: outc; padding-left: var(--gutter-num); font-size: var(--fs-4); }
.outcomes li::before { content: counter(outc, decimal-leading-zero);
  font-family: var(--mono); font-size: var(--fs-1); color: var(--muted); font-weight: 400; }

.consult p { max-width: var(--measure); color: var(--soft); margin-bottom: 0; }

/* ══ MOTION ═════════════════════════════════════════════════════════════════
   See the doctrine at the top of this file. Every rule below names its rung. */

/* --t3 · ARRIVAL. The progress rule advances rather than jumping, which is the only feedback a
   learner gets that finishing a lesson moved anything. */
.progress-track > i { transition: width var(--t3, .24s) var(--ease, ease); }

/* --t2 · SUBSTITUTION. The code is replaced, not moved: opacity only, no transform, so the
   reduced-motion catch-all zeroing the duration still leaves a correct instant swap. */
.exhibit pre.code { transition: opacity var(--t2, .17s) var(--ease, ease); }
.exhibit pre.code.swapping { opacity: .35; }

/* --t1 · STATE. A press needs to feel like a press. The transform lives inside no-preference
   because it MOVES; the colour changes above do not and need no guard. */
@media (prefers-reduced-motion: no-preference) {
  a.lesson-row:active, a.card:active, .contents a:active { transform: translateY(.5px) scale(.995); }
  a.lesson-row, a.card, .contents a { transition: transform var(--t1, .12s) var(--ease, ease),
    background var(--t1, .12s) var(--ease, ease); }
}

/* --t3 · ARRIVAL. The marked result is the most useful thing on the page and it appeared with no
   acknowledgement at all. `fade-rise` is the kit's emptyIn shape, declared here because the app
   owns this element. */
@media (prefers-reduced-motion: no-preference) {
  .ex-result { animation: arrive var(--t3, .24s) var(--ease, ease); }
  @keyframes arrive { from { opacity: 0; transform: translateY(5px); } }
}

/* --t3 · ARRIVAL. The publish panel's stripe turns amber→green in place when a draft becomes
   ready, instead of the whole editor repainting under the operator. */
.adm-publish { transition: border-left-color var(--t3, .24s) var(--ease, ease); }

/* ══ THE OPERATOR'S SCREENS — density and the kit's primitives ══════════════
   The reading surface is generous; the WORKING surface is dense. This file had it the other way
   round. The operator spacing triple is --s-*-op. */

.adm-section { margin-bottom: var(--s-sec-op); }
.adm-head { margin-bottom: var(--s-sec-op); }

/* A hover state is a PROMISE. The kit lights a whole table row on hover, and the attribution table
   and the registrations table have no interactive content in the row at all — so the promise was
   unbacked, which is the cheapest kind of tell. */
.opstable tr:hover td { background: transparent; }
.opstable th, .opstable td { padding-left: 0; }
.opstable th:last-child, .opstable td:last-child { padding-right: 0; }

/* Nested radii tighten INWARD. They were 12 / 9 / 10 — an inner radius wider than its container,
   which is the detail that reads as "assembled" rather than "designed". */
.adm-module { border-radius: var(--r-sheet); }
.adm-lesson { border-radius: var(--r-well); }
.adm-media  { border-radius: var(--r-ctl); }

/* The label above a kit field. .ff supplies the layout; this is the one delta Academy needs,
   because a sentence-case label is right for a public form where the kit's uppercase micro-label
   is not. */
.ff > .adm-label { display: flex; align-items: baseline; gap: var(--sp-2);
  font-size: var(--fs-3); font-weight: var(--fw-label); color: var(--muted); }
.ff.is-wide { grid-column: 1 / -1; }

/* The kit's empty-state secondary line uses --faint, which measures 3.21:1 at this size. It is the
   sentence that tells someone what to do next on a screen with nothing on it — the one place the
   copy matters most. Overridden app-side rather than in the kit, whose other consumers may sit it
   on a different ground. */
.empty .es { color: var(--muted); }

/* ══ THE FIGURE ═════════════════════════════════════════════════════════════
   The ONLY place the --ch-* palette is permitted, asserted in both directions by
   tests/test_css_tokens.py. No status colour appears here; no series colour appears anywhere else.

   Every arm carries three channels that owe nothing to colour — a dash pattern, a direct label and
   a risk-table row — because four categorical hues do not survive dichromacy. Delete the colour and
   the plot must still read. */
.figure { margin: var(--s-sec) 0; background: var(--surface); border: 0;
  border-radius: var(--r-well); padding: var(--sp-5); box-shadow: var(--shadow-2), var(--edge); }
.figure svg { display: block; width: 100%; height: auto; overflow: visible; }
.figure figcaption { margin-top: var(--sp-3); font-size: var(--fs-3); color: var(--soft);
  max-width: var(--measure-note); text-wrap: pretty; }

.figure .grid { stroke: var(--rule); stroke-width: 1; }
.figure .axis { stroke: var(--muted); stroke-width: 1; }
.figure .tick, .figure .risk-n { font-family: var(--mono); font-size: var(--fs-1); fill: var(--muted);
  font-variant-numeric: tabular-nums; }
.figure .axlab { font-family: var(--font); font-size: var(--fs-2); fill: var(--muted); }
.figure .risk-h { font-family: var(--mono); font-size: var(--fs-1); fill: var(--faint);
  letter-spacing: var(--ls-caps-xs); text-transform: uppercase; }
.figure .arm { fill: none; stroke-width: 2.1; stroke-linejoin: round; stroke-linecap: butt; }
.figure .arm-label { font-family: var(--mono); font-size: var(--fs-1); font-weight: var(--fw-ui); }

/* The follow-up strip above a curve, drawn by app/figures.py: one line per patient. The OUTCOME is
   carried by shape — a filled dot for a death, an open bar for a censoring — and not by colour, so
   the strip passes the same test as everything else in here: delete the colour and it still reads.
   .cens is the same bar again, ON the curve, where it says "this is data and not a model". */
.figure .pt { stroke: var(--faint); stroke-width: 1.6; }
.figure .pt-dead { fill: var(--muted); }
.figure .pt-cens { stroke: var(--muted); stroke-width: 1.6; fill: none; }
.figure .cens { stroke-width: 1.6; fill: none; }

/* ── A FIGURE YOU OPERATE ────────────────────────────────────────────────────────────────────────
   app/figures.py draws EVERY frame as a group inside the one SVG; static/explore.js shows one at a
   time. With no script the frames stay hidden and .fr-rest — the finished curve — is the figure,
   which is why the hiding is done HERE and the server marks nothing as showing. */
/* ⚠ FOUR-PIXEL LABELS. Text inside an SVG scales with the viewBox, so this 760-unit plot fitted to
   a 375px phone set every tick and label at 4px. A plot that is wider than the screen and scrolls
   is readable; one that fits and cannot be read is not. 640 keeps the ticks near 9px. */
.figure .fig-scroll { overflow-x: auto; overscroll-behavior-x: contain; }
.figure .fig-scroll > svg { min-width: 640px; }

.figure .fr, .figure .rv { display: none; }
.figure .fr.on, .figure .rv.on { display: inline; }
.figure.live .fr-rest { display: none; }
/* Who is in the denominator at this moment. WEIGHT separates them from the people who have already
   left, so it still reads with the colour deleted; the slot only says which curve they belong to. */
.figure .pt-risk { stroke-width: 2.6; stroke-linecap: butt; }
.figure .pt-now { fill: var(--ch-1); stroke: var(--surface); stroke-width: 1.5; }
.figure .cursor { stroke: var(--muted); stroke-width: 1; }
.figure .ask { font-family: var(--mono); font-size: var(--fs-5); font-weight: var(--fw-ui);
  fill: var(--ink); }

/* The running product. Numbers a reader checks by hand, so: mono, tabular, right-aligned, and the
   row being talked about is the only one with weight. */
.figure .fig-table { width: 100%; max-width: var(--measure-note); margin-top: var(--sp-4);
  border-collapse: collapse; font-family: var(--mono); font-size: var(--fs-2);
  font-variant-numeric: tabular-nums; color: var(--soft); }
.figure .fig-table th, .figure .fig-table td { padding: var(--sp-1) var(--sp-2); text-align: right;
  border-bottom: 1px solid var(--rule); white-space: nowrap; }
.figure .fig-table th { font-family: var(--font); font-weight: var(--fw-ui); color: var(--muted);
  font-size: var(--fs-1); letter-spacing: var(--ls-caps-xs); text-transform: uppercase; }
.figure .fig-table th:first-child, .figure .fig-table td:first-child { text-align: left; }
.figure .fig-table tr.now td { color: var(--ink); font-weight: var(--fw-ui);
  background: var(--accent-soft); }
/* Five columns of figures do not fit a phone at the desktop size: the S(t) column — the answer —
   was the one clipped off the edge. Smaller and tighter first; the scroller is the safety net. */
@media (max-width: 600px) {
  .figure .fig-table { font-size: var(--fs-1); }
  .figure .fig-table th, .figure .fig-table td { padding: 4px; }
}

/* What explore.js adds. The question borrows the quiz's own vocabulary (.quiz-stem, .quiz-opt,
   .quiz-right, .quiz-wrong, .quiz-why) because it IS the same thing — an answer you choose and are
   then told about — and a second look for it would be a second thing to learn. */
.xp-kicker { margin: 0 0 var(--sp-2); font-size: var(--fs-1); font-weight: var(--fw-ui);
  letter-spacing: var(--ls-caps-xs); text-transform: uppercase; color: var(--muted); }
.xp-gate { margin-top: var(--sp-4); padding: var(--sp-4); border: 1px solid var(--rule);
  border-radius: 10px; background: var(--bg); max-width: var(--measure); }
/* ⚠ (0,2,1), to outrank `.lesson-prose p` (0,1,1). The figure sits INSIDE the lesson's prose, so
   every <p> explore.js adds was set in the reading face at 17px/1.8 with a 20px foot — a verdict
   the size of a heading. This is furniture, not prose. */
.figure .xp-gate p, .figure p.xp-kicker, .figure p.xp-saved { font-size: var(--fs-3);
  line-height: var(--lh-4); margin: 0; }
.figure .xp-gate p.quiz-stem { font-size: var(--fs-4); margin: 0 0 var(--sp-3); }
.figure .xp-gate p.xp-kicker, .figure .xp-break p.xp-kicker { font-size: var(--fs-1);
  margin: 0 0 var(--sp-2); }
.figure p.xp-saved { margin: var(--sp-3) 0 0; }
/* Committed, so it cannot be changed — but it must still be READ, and the default disabled fade
   makes the answer you gave the hardest thing in the box to see. */
.xp-gate .quiz-opt.quiz-opt:disabled { opacity: 1; cursor: default; color: var(--soft); }
.xp-gate .quiz-opt.quiz-opt[aria-pressed="true"] { border-color: var(--accent);
  background: var(--accent-soft); color: var(--ink); font-weight: var(--fw-ui); }
.xp-gate .quiz-why { max-width: var(--measure); }

.xp-ctl { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-3);
  margin-top: var(--sp-4); }
.xp-pos { font-family: var(--mono); font-size: var(--fs-2); color: var(--muted); }

.xp-break { margin-top: var(--sp-4); }
.xp-chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.xp-chip { display: inline-flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--rule); border-radius: 10px; background: var(--surface);
  font-size: var(--fs-3); color: var(--ink); cursor: pointer; }
.xp-chip:hover { border-color: var(--accent); }
.xp-chip:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.xp-chip:has(input:focus-visible) { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.xp-chip input { accent-color: var(--accent); margin: 0; }
.figure .xp-swatch { width: 30px; height: 8px; flex: none; overflow: visible; }

/* WHAT YOU HAVE DONE, not what you have been given. A ring fills when the thing happened; the
   words do not change, so the list reads the same before and after and only the state moves. */
.xp-evidence { list-style: none; display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4);
  margin: var(--sp-5) 0 0; padding: var(--sp-3) 0 0; border-top: 1px solid var(--rule);
  font-size: var(--fs-2); color: var(--muted); }
.xp-evidence li { display: inline-flex; align-items: center; gap: 8px; margin: 0; }
.xp-evidence li::before { content: ""; width: 9px; height: 9px; border-radius: 50%;
  border: 1.5px solid var(--faint); flex: none; }
.xp-evidence li.done { color: var(--ok-ink); font-weight: var(--fw-ui); }
.xp-evidence li.done::before { background: var(--ok-ink); border-color: var(--ok-ink); }
.xp-saved { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-3);
  margin: var(--sp-3) 0 0; font-size: var(--fs-3); color: var(--warn-ink); }
/* ⚠ Scoped, for the reason .fb-extra[hidden] and .mb-row[hidden] give: explore.js hides this line by
   the `hidden` attribute, and the `display: flex` above beats the browser's own [hidden] rule. */
.xp-saved[hidden] { display: none; }

/* The four slots. This is the whole of the palette's contact with the DOM: the SVG names a slot,
   the stylesheet names a colour, and neither knows the other's business. */
/* ⚠ A SLOT STROKES A LINE AND FILLS TEXT — NEVER BOTH ON ONE ELEMENT. These four used to set `fill`
   as well as `stroke`, and they are declared AFTER `.figure .arm { fill: none }` at the same
   specificity, so they won: every survival curve in the product was painted as a SOLID WEDGE under
   its own step line, including the reward figure shown when a practical is passed. Nothing failed —
   a filled path is a valid path — and it was found by looking at the first figure ever put inside
   a lesson. Fill now belongs to text alone, which is the only thing that ever needed it. */
.figure .s1 { stroke: var(--ch-1); }
.figure .s2 { stroke: var(--ch-2); }
.figure .s3 { stroke: var(--ch-3); }
.figure .s4 { stroke: var(--ch-4); }
.figure text.s1 { fill: var(--ch-1); stroke: none; }
.figure text.s2 { fill: var(--ch-2); stroke: none; }
.figure text.s3 { fill: var(--ch-3); stroke: none; }
.figure text.s4 { fill: var(--ch-4); stroke: none; }

/* The dash is set as an SVG ATTRIBUTE by figure.js, deliberately not as a CSS custom property: it
   is a required accessibility channel, not a style, and it must not be something a later stylesheet
   can switch off without noticing what it took away. */

/* The lesson's own prose. Set from body_html, which the SERVER rendered against an explicit tag
   allowlist (app/markup.py) — the measure and rhythm here are the whole reason a lesson reads as
   teaching rather than as a form. */
/* THE LESSON PROSE FILLS ITS COLUMN, deliberately, and matches the listing beside it exactly.
   Owner's decision, 2026-08-26, after seeing all three widths rendered.

   The history is worth keeping because this rule has moved four times and twice on numbers nobody
   checked. Measured in Chrome at 1440 — rendered line box divided by the average glyph advance of
   the paragraph's own text, not the 0.5em rule of thumb that produced the bad estimates:

       --measure (33em)    561px     72 characters
       40em                680px     87 characters
       48rem               768px     98 characters   <- an older rule; its comment claimed 88
       no cap              920px    117 characters   <- here; that comment claimed 105

   So both estimates in the previous note were low, and the column is wider than any of them
   supposed. 117 is past what typography usually recommends for continuous prose; it was chosen for
   alignment, which is a real consideration on a page where every lesson pairs prose with a listing
   and a ragged right edge between them reads as two unrelated blocks.

   ⚠ IF YOU CHANGE IT AGAIN, MEASURE FIRST and put the number here. Three of the four rows above
   were guesses when they were written, and each guess was wrong in the direction that flattered the
   change being made. --measure (33em) is still the right cap for a note or an aside; it is a
   different kind of reading and it should not follow this rule. */
.lesson-prose { max-width: none; margin: 0 0 var(--s-sec); font-size: var(--fs-5);
  padding-left: var(--sp-5); }   /* the gutter the section ticks live in — see THE SPINE */
/* ⚠ REVERSED 2026-09-23, on the owner's instruction to act on the 0.25.0 design review, whose fifth
   finding was this rule: lessons read at 110+ characters a line and a long read wants 65-75. The
   CONTAINER stays uncapped, so a figure keeps the full column; the TEXT blocks — the only ones the
   markup can make (markup.py ALLOWED_TAGS) — take --measure. Counted in Chrome at 1440, characters
   on each rendered full line of three SURV lessons, median:

       before (no cap)     900px    112-114 characters
       after (--measure)   561px     69-70 characters

   What this gives up is the shared right edge with the listing beneath, which was the whole reason
   for the 2026-08-26 decision above; the listing is code, read line by line, and keeps its width.

   ⚠ REVERSED AGAIN 2026-10-16, on the owner's word: "let's have the text span the width". The lesson's text blocks now run the whole width of the lesson's column,
   which is the width of the page less the course menu and follows the screen (.lesson-shell --k), so the text, the callouts and the listings share one right
   edge again. The cost is the one named above and it is his to weigh. Counted in Chrome, the median of every full rendered line on six SURV readings: 109 characters
   at a 1440px window (column 892px), 113 at 1920px (1006px) and 118 at 2560px (1162px). --measure (33em) is still the cap for a note or an aside. */
.lesson-prose p { margin: 0 0 var(--sp-5); line-height: 1.8; }
/* THE SPINE. A lesson is 70 headings across the course and the only thing separating a section from
   the one above it was whitespace — which stopped being enough when the column went to 117
   characters. Each heading now carries a short accent tick in a gutter, and the body of the section
   sits in from it, so a section reads as one object rather than as a run of paragraphs under a
   title.

   ONE DEVICE PER BOUNDARY. This replaced a hairline rule above the heading; both were rendered and
   compared, and the rule earned nothing once the tick was there. The generous space above the
   heading is doing the other half of the work and stays.

   ⚠ THE GUTTER IS ON THE CONTAINER, NOT ON THE HEADING'S SIBLINGS. An `h3 ~ p` selector leaves the
   paragraphs BEFORE the first heading flush left, which puts a step in the left edge of every
   lesson that opens on prose — which is all of them. */
.lesson-prose h3 {
  margin: var(--s-major) 0 var(--sp-4);
  position: relative;
}
.lesson-prose h3:first-child { margin-top: 0; }
.lesson-prose h3::before {
  content: "";
  position: absolute;
  left: calc(var(--sp-5) * -1);
  top: .18em; bottom: .18em;
  width: 3px;
  border-radius: 2px;
  background: var(--accent);
}
.lesson-prose ul, .lesson-prose ol { margin: 0 0 var(--sp-4); padding-left: var(--sp-6); }
.lesson-prose li { margin-bottom: var(--sp-2); line-height: var(--lh-4); }
/* .92em, deliberately relative and deliberately not a scale step: a code span must track the size
   of the sentence it sits in, and Geist Mono runs optically larger than Geist at the same size. The
   guard below is scoped to absolute units for this reason. */
.lesson-prose code { font-family: var(--mono); font-size: .92em;
  background: var(--well); padding: 1px 4px; border-radius: 4px; }
/* TWO CALLOUTS, and the renderer tells them apart by the first character of the text — see the
   note in markup.py. A quote block was doing two jobs at once: "here is a pattern worth keeping"
   and "this will cost you an afternoon", both in one treatment, so a warning read as a sentence.
   ⚠ GROUNDS, NOT TINTS. --accent-soft is the declared replacement for the --accent-tint and
   --accent-wash this file no longer has; --accent on it measures 5.55 and --ink 12.20. The warning
   pairing is --warn-ink on --panel, 9.11 light and 8.14 dark. Nothing here invents a colour. */
.lesson-prose blockquote {
  margin: var(--s-rule) 0 var(--s-rule) calc(var(--sp-5) * -1);
  padding: var(--sp-4) var(--sp-5);
  border-left: 3px solid var(--accent);
  border-radius: 0 var(--r-well) var(--r-well) 0;
  background: var(--accent-soft);
  color: var(--ink);
}
/* ON A PHONE the 20px gutter is 5.5% of a 362px line and every paragraph paid it (C7). The section tick takes its own indent on the heading instead,
   and a callout starts at the column's edge. The desktop spine is untouched. */
@media (max-width: 640px) {
  .lesson-prose { padding-left: 0; }
  .lesson-prose h3 { padding-left: var(--sp-3); }
  .lesson-prose h3::before { left: 0; }
  .lesson-prose blockquote { margin-left: 0; }
}
.lesson-prose blockquote.warn {
  border-left-color: var(--amber);
  background: var(--panel);
  color: var(--warn-ink);
}
.lesson-prose blockquote p { margin: 0; }
/* A mathematical variable is italic by ISO 80000-2. Setting h(t) in mono would say "this is code"
   about something that is not code, and this audience notices immediately. */
.lesson-prose .mv { font-style: italic; }
/* THREE BLOCKS THE LESSON RENDERER GAINED (markup.py, 15 Oct 2026): a fenced code block, a pipe table and a displayed
   equation. They carry their own class names (lcode, ltable, lmath), so nothing in the prose rules above reaches them and
   they reach nothing there. A code block is drawn as the exhibit's listing is: the same ink, mono and line height. */
pre.lcode { margin: 0 0 var(--sp-5); padding: var(--sp-4) var(--sp-5); overflow-x: auto; tab-size: 4;
  background: var(--nav-ink); color: var(--code-ink); border-radius: var(--r-well);
  font-family: var(--mono); font-size: var(--fs-code); line-height: var(--lh-code); }
pre.lcode > code { background: none; padding: 0; border-radius: 0; font-size: inherit; color: inherit; }
pre.lcode:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ltable { border-collapse: collapse; margin: 0 0 var(--sp-5); max-width: 100%; font-size: var(--fs-4); line-height: var(--lh-4); }
.ltable th, .ltable td { text-align: left; vertical-align: top; padding: var(--sp-2) var(--sp-4) var(--sp-2) 0;
  border-bottom: 1px solid var(--rule); }
.ltable th { font-weight: var(--fw-ui); color: var(--muted); }
.ltable .al-r { text-align: right; padding-right: 0; padding-left: var(--sp-4); }
.ltable .al-c { text-align: center; }
.ltable tbody tr:last-child td { border-bottom: 0; }
.ltable code { font-family: var(--mono); font-size: .92em; }
/* A displayed equation sits in the column's own rhythm and scrolls sideways rather than breaking a long one. Math is set in
   the browser's math face, a little larger than the sentence, because a formula at body size reads small. */
.lmath { margin: 0 0 var(--sp-5); padding: var(--sp-2) 0; overflow-x: auto; }
math { font-size: 1.12em; }
.lmath math { font-size: 1.25em; }
/* The risk row's own label, in the left margin where it cannot collide with a count. It carries the
   arm colour AND the arm name — the name is what makes the table readable without the hue. */
.figure .risk-lab { font-family: var(--mono); font-size: var(--fs-1); font-weight: var(--fw-ui); }


/* --- the pressed state, at last -----------------------------------------
   --accent-dark has carried the comment "the pressed state" since it was declared and no rule has
   ever used it, so nothing in this app changed colour under the finger — the kit's .btn:active is a
   0.5px nudge and a 1.5% scale, which is motion, not feedback, and it is switched off entirely
   under prefers-reduced-motion. A reader who has asked for no motion had NO pressed state at all.

   --brand is --accent here (see the seam note at the top), so the token the kit fills the button
   with and the token named for its pressed end are the same ramp. It flips in dark with everything
   else. */
.btn-primary:active { background: var(--accent-dark); }
.btn-ghost:active { background: color-mix(in srgb, var(--accent) 12%, transparent); }

/* --- and the disabled state, which had none either -----------------------
   `grep -c disabled suite-kit.css` is zero, so a disabled button was pixel-identical to a live one:
   full brand fill, pointer cursor, nothing to see. The two places it matters most are the Publish
   button, which is disabled exactly when the operator wants to know why it will not work, and the
   editor's entry point on a course a teacher may not touch while it is announced — both carry a
   title attribute explaining themselves, and neither looked as though it had anything to explain.
   Also the `once()` running state: every button in this app dims while its action is in flight. */
.btn:disabled, .btn[disabled] { opacity: .5; cursor: not-allowed; }

/* --- the practical's loading skeleton -------------------------------------
   The shape of what is coming — a dataset line, a form, a listing — rather than one grey sentence.
   Not decoration: without it the pane is a single line and then jumps to full height, which reads
   as a fault even when nothing is wrong. */
.ex-skeleton { display: flex; flex-direction: column; gap: 8px; }
.sk { border-radius: 8px; background: var(--rule); position: relative; overflow: hidden; }
.sk-line { height: 13px; }
.sk-line.is-short { width: 62%; }
.sk-block { height: 132px; }
.sk::after { content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--surface) 70%, transparent), transparent);
  transform: translateX(-100%); animation: skshimmer 1.5s var(--ease) infinite; }
@media (prefers-reduced-motion: reduce) { .sk::after { animation: none; } }
@keyframes skshimmer { to { transform: translateX(100%); } }

/* The skeleton for the routes with no server render (see app.js SKELETONS). Card-shaped, because
   every one of those pages is a list of cards — a skeleton whose shape does not match what arrives
   is a second layout shift rather than a cure for the first. */
.pageskel { display: flex; flex-direction: column; gap: var(--sp-4); }
.pageskel h1 { margin-bottom: 0; }
.sk-card { height: 92px; border-radius: var(--r-sheet); }

/* The catalogue card's meta column, stacked. It used to be a single joined string; it now answers
   the two questions a reader is actually deciding on — what it costs, when it next runs — above the
   shape. Right-aligned so the prices form a column, and tabular so they line up digit for digit. */
.contents .meta { display: flex; flex-direction: column; align-items: flex-end; gap: 2px;
  text-align: right; font-variant-numeric: tabular-nums; }
.contents .meta .m-0 { color: var(--ink); font-weight: var(--fw-ui); }
.contents .meta .m-1, .contents .meta .m-2 { color: var(--muted); font-size: var(--fs-2); }
/* ⚠ THIS BLOCK MUST FOLLOW THE ONE ABOVE, and that is the whole reason it is here rather than up
   with the other narrow-screen rules at 640px.
   Those rules already said "on mobile the meta moves under the title and aligns left". The
   flex-column rule above was added later in the file at the same specificity, so it silently won and
   put the column back on the right — and `white-space: nowrap`, inherited from the base rule, then
   stopped the long line wrapping. The result was "next run 21 August 2026 – 4 September 2026"
   running 11px PAST the page edge on a 375px screen. Same specificity, later wins; the fix is
   order, not !important. */
@media (max-width: 640px) {
  .contents .meta { align-items: flex-start; text-align: left; white-space: normal;
    /* A date range in full month names does not fit a phone at the base size. */
    font-size: var(--fs-2); }
  .contents .meta .m-0 { font-size: var(--fs-3); }
}

/* "Your place is booked." — the line that distinguishes a run card a learner HAS from one they
   could buy. Same card, different claim, so it needs to read differently at a glance. */
.run-card .booked { color: var(--ok-ink); font-weight: var(--fw-ui); }
.timetable-list .s-note.is-ok { color: var(--ok-ink); }

/* The lesson page spaces its own blocks. LAYOUT, not margins: the study block and the prose swap
   order depending on whether the lesson has a video (the video is the lesson; a reading's listing
   is support), and a margin-bottom on whichever happened to come first collapses to nothing the
   moment they trade places — which is exactly what happened: a 0px gap between the player and the
   first line of prose. */




/* WCAG 2.2 SC 2.5.8 asks for 24x24 CSS px. The consent checkbox was 16x16 — the browser default —
   on a control a person has to hit before they can submit anything at all. */
.if-consent input[type="checkbox"], .if-field input[type="checkbox"] {
  width: 24px; height: 24px; min-width: 24px; flex-shrink: 0; accent-color: var(--accent); }

/* ⚠ minmax(0, 1fr), NEVER a bare 1fr, on any grid that can contain a code block.
   `1fr` is `minmax(auto, 1fr)`, and a grid item's automatic minimum size is its MIN-CONTENT width —
   which for a <pre> of Stata is the longest line, whether or not the pre has overflow-x. So the
   track grew past the viewport and took the page with it: 161px of horizontal scroll on a video
   lesson and 84px on a practical, at 375px, with `overflow-x: auto` already correctly set on the
   pre and doing nothing. The min-width:0 below is the same statement made on the items, so a grid
   added later without minmax cannot reintroduce it. */
.study > *, .lesson-main > *, .course-grid > * { min-width: 0; }

/* WCAG 2.2 SC 2.5.8 — 24x24 CSS px. Both of these are standalone navigational links rather than
   links inside a sentence, so the inline exemption does not cover them. */
.free-cta, .adm-back, .consult-cta, .adm-mail {
  display: inline-flex; align-items: center; min-height: 24px; }
/* The dismiss control was 22px wide — two short of the minimum, which is the kind of miss that only
   a measurement finds. */
.adm-icon { min-width: 24px; min-height: 24px; }

/* The section head and its hint share a line on a wide screen and stack on a narrow one. Fixed at
   space-between they were jammed edge to edge on a phone, with "Times are local to
   Europe/Stockholm" running to the last pixel of the page margin. */
@media (max-width: 640px) {
  .adm-module-head { flex-wrap: wrap; gap: 4px; align-items: baseline; }
  .adm-timetable .adm-module-head .adm-hint { flex-basis: 100%; }
}

/* The stat band, two-up on a phone. suite-kit sizes it minmax(190px, 1fr), and 2x190 does not fit a
   347px content width — so five numbers became five full rows and pushed "Needs a person", which is
   the operator's actual working list, about five screens down. Overridden HERE rather than in the
   kit: three apps mount that stylesheet and their bands are not all five items wide. */
@media (max-width: 640px) {
  .statband { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 16px; }
  .statband .val { font-size: var(--fs-7); }   /* the scale, not a literal — see test_css_tokens */
}

/* The queue row stacks on a phone. Side by side, the pill, a three-line wrapping title and the
   button fought over 347px — the sentence that says what needs doing became a narrow column beside
   a button, which is the wrong way round for a list whose whole job is to be read and acted on. */
@media (max-width: 640px) {
  .opsrow { flex-wrap: wrap; align-items: flex-start; }
  .opstext { flex-basis: 100%; }
  .opsrow > .btn { margin-left: auto; }
}

/* ── the study surface, under the rail ────────────────────────────────────────────────────────
   Video and code were SIDE BY SIDE at 1fr/1fr, and on a 1440px screen that made the lecture
   597x336 — smaller than a video embed on a blog — while the code came out at 68 characters. The
   rule above says 507px, "about 67 characters", was too narrow for a merlin specification and was
   widened to fix exactly that. It bought one character.

   They are siblings in the main column now, each at its full width, and the MAIN COLUMN sizes them:
   ~980px gives a 550px lecture and ~110 characters of code. No cap, because a cap is a second
   opinion about a width the layout already has one about — and the 62rem I first reached for was
   just the old side-by-side argument with a bigger number.

   Nothing needed them simultaneous: you watch the lecture, then copy the code into your own editor,
   which is what the Copy button is for. */
.study-media { position: static; width: 100%; }

/* The stacked study grid. The width decision is in "the study surface, under the rail" above —
   this is only the single column and the gap between the two panels. */
.study { grid-template-columns: minmax(0, 1fr); gap: var(--s-sec); }

/* ── the lesson shell: the course menu beside the text, and the text that follows the screen ──────────────────────────────────────────────────────────────────
   The menu is a column of the lesson page (renderStudy in app.js; the rules are in "the course menu" below the outline's rows), sticky and scrolling inside itself, so a
   13-lesson course stays reachable from anywhere in a long lesson without dragging the page back to the top. THE TEXT FOLLOWS THE SCREEN (the owner, 16 Oct 2026:
   "text width should adjust dependent on screen size"): --k is 1 up to 1699px, 1.09 from 1700px and 1.2 from 2200px, and the lesson's type, the menu and the
   column scale by it, so a large screen is not a small page in a big window, and the text spans the column (see ".lesson-prose" above; his word, 16 Oct 2026). Only the lesson
   is scaled: the bar and the other pages keep their size. */
.lesson-shell { --k: 1; --side-w: calc(18rem * var(--k)); --side-gap: calc(var(--sp-10) * var(--k)); --lesson-col: calc(64rem * var(--k));
  --fs-1: calc(10.5px * var(--k)); --fs-2: calc(12px * var(--k)); --fs-3: calc(13.5px * var(--k)); --fs-4: calc(15px * var(--k)); --fs-5: calc(17px * var(--k));
  --fs-6: calc(20px * var(--k)); --fs-7: calc(24px * var(--k)); --fs-8: calc(30px * var(--k)); --fs-9: calc(38px * var(--k)); --fs-code: calc(13.5px * var(--k)); }
@media (min-width: 1700px) { .lesson-shell { --k: 1.09; } }
@media (min-width: 2200px) { .lesson-shell { --k: 1.2; } }
.lesson-main { display: flex; flex-direction: column; gap: var(--s-sec); min-width: 0; max-width: var(--lesson-col); }
.lesson-main > * { margin-top: 0; margin-bottom: 0; }

.course-rail { min-width: 0; }
.rail-head { padding-bottom: var(--sp-3); border-bottom: 1px solid var(--rule);
  margin-bottom: var(--sp-4); }
.rail-course { display: block; font-weight: var(--fw-head); color: var(--ink);
  text-decoration: none; line-height: 1.3; }
.rail-course:hover { color: var(--accent); }
.rail-progress { display: flex; align-items: center; gap: var(--sp-2); margin-top: var(--sp-3); }
.rail-progress .progress-track { flex: 1; }
.rail-progress .a-meta { font-variant-numeric: tabular-nums; }

/* The gap between two modules has to beat the gap between two lessons, or the grouping the
   uppercase headers assert is not the grouping the spacing shows. */
.rail-mod + .rail-mod { margin-top: var(--sp-6); }
.rail-modt { font-size: var(--fs-2); font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--muted); margin: 0 0 var(--sp-3) var(--sp-3); }
.rail-lock { font-size: var(--fs-2); margin: -2px 0 var(--sp-2); }
.rail-list { list-style: none; margin: 0; padding: 0; }

/* The row is the target: full width, so the whole line is clickable rather than the title alone. */
.rail-row { display: grid; grid-template-columns: 1.6em 14px minmax(0, 1fr) auto; gap: var(--sp-2);
  /* ⚠ start, NOT baseline. On a baseline grid a two-line title pushes its own row taller from the
     top while its neighbours sit on the first line, so the gaps between rows came out uneven down
     the rail — the thing that reads as "not quite finished" before you can say why. */
  align-items: start; padding: var(--sp-2) var(--sp-3); border-radius: 8px; min-height: 30px;
  text-decoration: none; color: var(--muted); font-size: var(--fs-3); line-height: 1.4; }
.rail-row + .rail-row { margin-top: 1px; }
a.rail-row:hover { background: var(--panel); color: var(--ink); }
/* --muted, NOT --faint. This file already carries that note against the opstable header — "at this
   size, uppercase and letterspaced, --faint measures 3.21:1" — and I reached for --faint here
   anyway; it measured 3.05 and 3.21 in light. An ordinal a reader uses to find their place is text,
   not decoration. */
.rail-row .rn { font-family: var(--mono); font-size: var(--fs-1); color: var(--muted); }
.rail-row .rt { min-width: 0; }
.rail-row .rtick { color: var(--ok-ink); }
/* ◐ opened is the accent and ● done is the green, as on the course page: one vocabulary. (Both were the green, so the two marks differed by shape alone.) */
.rail-row .rtick.st-opened { color: var(--accent); }
/* ⚠ ONE ALIGNMENT, NOT THREE. With the row on `align-items: start` the ordinal and the tick sit on
   the first line while this was still centring on the whole row, so a two-line title left the
   number at the top, the kind mark floating in the middle and the tick back at the top. Everything
   now hangs off the first line; the 2px nudge is optical centring of a 14px square against the cap
   height beside it, not a guess. */
.rail-row .rkind { align-self: start; margin-top: 2px; opacity: .75; flex-shrink: 0; }
.rail-row .rn, .rail-row .rtick { align-self: start; }
.rail-row.is-now .rkind { opacity: 1; color: var(--accent); }
.rail-row .rfree { font-size: var(--fs-1); font-weight: var(--fw-ui); color: var(--accent);
  letter-spacing: .04em; text-transform: uppercase; }
/* WHERE YOU ARE, said once. The accent bar is the same device the catalogue row uses on hover, so
   "current" and "hoverable" read as one family rather than two inventions. */
.rail-row.is-now { position: relative; background: var(--accent-soft); color: var(--ink); font-weight: var(--fw-head); }
/* the mark is a straight rule inside the row's left edge, not an inset shadow: on a row with rounded corners a shadow curves round them and the mark reads as a bracket */
.rail-row.is-now::before { content: ""; position: absolute; left: 0; top: var(--sp-2); bottom: var(--sp-2); width: 3px; border-radius: 3px; background: var(--accent); }
.rail-row.is-now .rn { color: var(--accent); }
/* ⚠ THE MODULE HOLDING YOU WAS NOT MARKED AT ALL. Four identical uppercase headers, and the only
   thing saying where you were sat inside one of them — so at a glance the rail told you the lesson
   and not the section. :has() rather than a class from the renderer: the state already exists in
   the DOM and adding a second source for it is a second thing to keep in step. */
.rail-mod:has(.is-now) .rail-modt { color: var(--ink); }
.rail-row.is-done .rt { color: var(--muted); }
.rail-row.is-pending { color: var(--faint); cursor: default; }

/* THE COURSE MENU, beside the lesson from 1000px; under that, the strip says where you are and its position opens a copy of the menu under it.
   Each is drawn where the other is not, so there is never a second table of contents on screen. */
.lesson-where, .lesson-strip, .strip-panel { display: none; }
.lesson-shell > .course-rail { display: none; }
@media (min-width: 1000px) {
  .lesson-shell { display: grid; grid-template-columns: var(--side-w) minmax(0, 1fr); column-gap: var(--side-gap); align-items: start; }
  /* Sticky under the bar and as tall as the window leaves, scrolling inside itself: a long course stays reachable from anywhere in a long lesson. */
  /* margin-top 4px: the card's top edge stands level with the first line's capitals (the eyebrow's glyphs start 4px below its line box), not above them */
  .lesson-shell > .course-rail { display: block; position: sticky; top: calc(var(--bar-h) + var(--sp-6)); margin-top: var(--sp-1); max-height: calc(100dvh - var(--bar-h) - var(--sp-12) - var(--sp-1));
    overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; padding: var(--sp-5) var(--sp-3) var(--sp-4);
    background: var(--surface); border: 1px solid var(--rule); border-radius: var(--r-well); }
}
.course-rail .rail-head { padding-inline: var(--sp-3); }
.course-rail .rail-progress { flex-direction: column; align-items: stretch; gap: var(--sp-2); }
.course-rail .rail-progress .progress-track { flex: none; }     /* it was `flex: 1`, which in a column is a basis of 0: a track with no height */
@media (max-width: 999px) {
  /* The strip and its opened menu are ONE block (.lesson-where), so the column's 40px between its children is not drawn between them, and the lesson sits 24px under the block. */
  .lesson-where { display: block; margin-bottom: calc((var(--s-sec) - var(--sp-6)) * -1); }
  .lesson-strip { display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center; gap: var(--sp-3); padding-bottom: var(--sp-4); border-bottom: 1px solid var(--rule); }
  .strip-where { display: grid; gap: var(--sp-1); min-width: 0; }
  .strip-course { font-size: var(--fs-4); font-weight: var(--fw-ui); color: var(--ink);
    text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* the position IS the button that opens the menu: a button, so it is reached by Tab and says whether it is open */
  .strip-pos { display: flex; align-items: center; gap: var(--sp-2); min-height: var(--sp-6); padding: 0; background: none; border: 0; cursor: pointer; text-align: left;
    font-family: var(--mono); font-size: var(--fs-2); color: var(--muted); }
  .strip-pos-t { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .strip-chev { flex: none; transition: transform var(--t2, .17s) var(--ease, ease); }
  .strip-pos[aria-expanded="true"] .strip-chev { transform: rotate(180deg); }
  .strip-where .progress-track { height: 4px; }
  /* the arrows are 44px targets with an edge of their own, so they read as buttons and not as stray punctuation either side of the title */
  .strip-arrow { display: inline-flex; align-items: center; justify-content: center;
    width: 44px; height: 44px; border: 1px solid var(--rule); border-radius: 50%; background: var(--surface); color: var(--ink); text-decoration: none;
    font-size: var(--fs-8); line-height: 1; padding-bottom: var(--sp-1); }
  a.strip-arrow:hover { background: var(--panel); }
  span.strip-arrow { visibility: hidden; }
  .strip-panel:not([hidden]) { display: block; margin-top: var(--sp-4); }
  .strip-panel .course-rail { max-height: 60dvh; overflow-y: auto; overscroll-behavior: contain; padding: var(--sp-4) var(--sp-3);
    background: var(--surface); border: 1px solid var(--rule); border-radius: var(--r-well); }
  .strip-panel .rail-head { display: none; }
}

/* Slides and supplements, under the lecture. Deliberately quiet: a handout is a thing you take
   away after watching, not a competing call to action above the fold. */
.lesson-extras { padding: var(--sp-4) 0; border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule); }
.lesson-extras .lx-t { font-size: var(--fs-2); font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--muted); margin-bottom: var(--sp-2); }
.lesson-extras ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-5); }
.lesson-extras .lx-a { display: inline-flex; align-items: center; min-height: 24px;
  font-weight: var(--fw-ui); color: var(--accent); text-decoration: none; }
.lesson-extras .lx-a:hover { text-decoration: underline; }

/* THE BLUE EDGE ON EVERY PAGE LOAD.
 *
 * app.js moves focus to <main> after each route change so a screen reader reads the new heading and
 * a keyboard user tabs forward into the content rather than restarting at the brand link. That is
 * worth keeping. What is not worth keeping is the browser's focus ring drawn around the whole
 * content area as a result — it appears on load and on every refresh, and it reads as a rendering
 * fault rather than as focus.
 *
 * Safe to remove precisely because tabindex="-1" means the element is NOT keyboard-reachable: no
 * user can ever tab to it, so the ring communicates nothing to anybody. Scoped to the attribute
 * rather than to `main` so it cannot quietly suppress a ring on something focusable later, and the
 * :focus-visible rules elsewhere in this file are untouched. */
[tabindex="-1"]:focus { outline: none; }


/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   THE BRAND CHROME (redesign step 3, 11 Oct 2026): the website's header and hero, over the structure the SPA and the server already draw.
   The mock-ups (ops/planning/academy-redesign-2026-10-11, option A) are the reference. Both themes since step 6 (12 Oct 2026): the rules take their ground, ink and rules from the tokens, and the few things that differ (the
   header's ground, the lockup's colour, the tilted squares' strength) are tokens too. Everything here is CSS over the existing markup, so the SPA and the server-rendered pages change together.
   ═══════════════════════════════════════════════════════════════════════════════════════════════ */

/* ── the header: white, the RDA lockup, the product's name beside it, uppercase nav with a red underline on the current page ── */
/* THE PAGE FOLLOWS THE SCREEN (the owner, 16 Oct 2026, on a large screen signed in). --wrap is the width of the content: 1220px (the kit's 1280px less its two 30px gutters) up
   to a 1440px window, then 0.3px more for every pixel of window, to 1560px at 2560px. The bar, the page and the public bars take it from here, so every page has the same
   left edge and a large screen is used rather than centred in; below 1440px nothing differs from before. */
:root { --wrap: clamp(1220px, 1220px + (100vw - 1440px) * .3, 1560px); --page-max: calc(var(--wrap) + 60px); }
:root main { max-width: var(--page-max); }
:root .topbar {
  background: var(--site-bar);
  height: 88px;
  padding: 0 max(30px, calc((100% - var(--wrap)) / 2));
  border-bottom: 1px solid var(--rule);
  box-shadow: none;
  --focus-ring: var(--accent);
}
:root .brand { gap: 0; }
:root .brand-logo { width: 150px; height: 46px; background: url("/static/brand/logo-web.svg") no-repeat left center / contain; filter: var(--lockup-filter); }
:root .brand-logo svg { display: none; }
:root .brand-text {
  color: var(--ink); font-size: var(--fs-6); font-weight: 500; letter-spacing: -.01em; line-height: 30px;
  margin-left: 16px; padding-left: 16px; border-left: 1px solid var(--rule-blue);
}
:root .navbtn {
  color: var(--ink); font-size: var(--fs-4); font-weight: 500; letter-spacing: .02em; text-transform: uppercase;
  border-radius: 0; background: none; padding: 8px 2px; margin: 0 0 0 28px;
}
:root .navbtn.navhome { margin-left: 16px; padding: 8px 2px; }
:root .navbtn:hover { color: var(--accent); background: none; }
:root .navbtn.on { color: var(--accent); background: none; box-shadow: inset 0 -2px 0 var(--rda-red); }
/* On the dark ground the link ink and the hover ink are both near-white, so a hover says so with a rule under the word. */
:root[data-theme="dark"] .navbtn:hover:not(.on), :root[data-theme="dark"] .hn-link:hover { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 8px; }
:root .acctwrap::before { background: var(--rule); margin: 0 12px 0 12px; height: 30px; }
:root .acctchev { color: var(--muted); }
:root .acctbtn .av { box-shadow: none; }
:root .acctbtn:hover { background: none; }
:root .topbar .acctbtn, :root .topbar .acctgear { color: var(--ink); opacity: 1; }
:root .topbar .acctbtn:hover .acctgear { color: var(--accent); }
:root .helpbtn.on, :root .helpbtn:hover { background: var(--panel); color: var(--ink); }
@media (max-width: 820px) {
  :root .topbar { height: 64px; padding: 0 16px; }
  :root .brand-logo { width: 104px; height: 34px; }
  :root .brand-text { font-size: var(--fs-5); line-height: 22px; margin-left: 12px; padding-left: 12px; }
}

/* ── the hero: the website's masthead, not a navy band. A soft-blue tilted square behind a large headline; the statistics are white cards ── */
:root .home-hero {
  background: transparent; color: var(--ink); border-radius: 0; box-shadow: none;
  margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw); padding: 72px calc(50vw - 50%) 36px; overflow: hidden; isolation: isolate;
}
:root .home-hero::after {
  content: ""; position: absolute; z-index: -1; top: -120px; right: -80px; left: auto; width: 360px; height: 360px; bottom: auto;
  background: var(--blue-panel); opacity: var(--tilt-opacity); transform: rotate(-12deg); border-radius: 18px;
}
:root .home-hero-wm, :root .home-hero-mark { display: none; }
:root .home-hero-inner { display: block; }
:root .home-hero-name {
  color: var(--ink); font-size: clamp(36px, 5.2vw, 64px); line-height: 1.05; font-weight: 600; letter-spacing: -.025em;
  max-width: 14em; margin: 0 0 24px;
}
:root .home-hero-desc { color: var(--soft); font-size: var(--fs-6); line-height: 1.5; max-width: 34em; margin: 0; }
:root .home-hero .statband { margin-top: 44px; gap: 16px; }
:root .home-hero .statband .stat {
  position: relative; overflow: hidden; background: var(--surface); border: 1px solid var(--rule); border-radius: 14px;
  box-shadow: none; padding: 20px 20px 16px;
}
:root .home-hero .statband .stat::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--rda-red);
  transform: scaleX(0); transform-origin: left; transition: transform .25s var(--ease);
}
:root .home-hero .statband button.stat:hover { background: var(--surface); border-color: var(--rule-blue); box-shadow: var(--shadow-2); transform: translateY(-2px); }
:root .home-hero .statband button.stat:hover::before { transform: scaleX(1); }
:root .home-hero .statband .val { color: var(--ink); }
:root .home-hero .statband .val.warn { color: var(--fail-ink); }
:root .home-hero .statband .label { color: var(--muted); }
:root .home-hero .statband .meta { color: var(--muted); }
@media (max-width: 820px) {
  :root .home-hero { padding-top: 36px; padding-bottom: 20px; }
  :root .home-hero::after { width: 220px; height: 220px; top: -90px; right: -70px; }
  :root .home-hero-name { font-size: var(--fs-9); }
  :root .home-hero-desc { font-size: var(--fs-5); }
}

/* ── the page may not scroll sideways: the hero's tilted square and the full-bleed sections reach past the container ── */
:root { overflow-x: clip; }

/* ── section heads in the brand's scale: an eyebrow in DM Sans caps, a 44px headline, a 19px deck ── */
:root .sec-head h2 { font-size: clamp(28px, 3.2vw, 44px); line-height: 1.1; font-weight: 600; letter-spacing: -.015em; }
:root .sec-head .a-note { font-size: var(--fs-6); line-height: 1.5; color: var(--muted); max-width: 34em; }
:root .masthead .eyebrow, :root .course-prose .eyebrow,
:root .consult .eyebrow, :root .marked .eyebrow {
  font-family: var(--font); font-size: var(--fs-3); font-weight: 700; letter-spacing: .12em; }

/* ── the course cards: white, a hairline, the brand's red bar wiping in from the left on hover ── */
:root .coursecard { position: relative; overflow: hidden; border: 1px solid var(--rule); border-radius: 14px; box-shadow: none; }
:root .coursecard::before {
  content: ""; position: absolute; z-index: 2; top: 0; left: 0; right: 0; height: 3px; background: var(--rda-red);
  transform: scaleX(0); transform-origin: left; transition: transform .25s var(--ease); }
:root .coursecard:hover { border-color: var(--rule-blue); box-shadow: var(--shadow-2); transform: translateY(-2px); }
:root .coursecard:hover::before { transform: scaleX(1); }

/* ── WHAT MARKING LOOKS LIKE: a blue section, the explanation on the left, the marker's answer on a navy panel on the right ── */
:root section.marked {
  position: relative; margin-top: 80px; padding: 84px 0;
  background: var(--blue-panel); box-shadow: 0 0 0 100vmax var(--blue-panel); clip-path: inset(0 -100vmax);
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.08fr); grid-template-areas: "head card" "cap card";
  column-gap: 72px; align-content: center; align-items: center;
}
:root section.marked > .sec-head { grid-area: head; align-self: end; margin-bottom: 0; }
:root section.marked > .marked-card { grid-area: card; max-width: none; }
:root section.marked > .marked-cap { grid-area: cap; align-self: start; color: var(--soft); }
:root .marked .sec-head .a-note { color: var(--soft); }
:root .marked-card { background: var(--topbar); color: #fff; border: 0; border-radius: 14px; box-shadow: var(--shadow-3); padding: 0; overflow: hidden; }
:root .marked-score { margin: 0; padding: 16px 24px; border-bottom: 1px solid rgba(255, 255, 255, .1); color: rgba(255, 255, 255, .72); }
:root .marked-items li { padding: 16px 24px; border-top: 0; border-bottom: 1px solid rgba(255, 255, 255, .08); }
:root .marked-items li:last-child { border-bottom: 0; }
:root .marked-items li:first-child { padding-top: 16px; }
:root .mi-lab, :root .mi-val { color: #fff; }
:root .mi-mark {
  display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 50%; font-size: var(--fs-2); line-height: 1; }
:root .marked-items .is-ok .mi-mark { background: #fff; color: var(--rda-navy); }
:root .marked-items .is-wrong .mi-mark { background: var(--rda-red-btn); color: #fff; }
:root .mi-why { color: rgba(255, 255, 255, .78); }
:root .mi-named { background: var(--rda-red-btn); border-color: transparent; color: #fff; }
@media (max-width: 820px) {
  :root section.marked { grid-template-columns: 1fr; grid-template-areas: "head" "card" "cap"; row-gap: 28px; padding: 52px 0; margin-top: 52px; }
}

/* ── the figure palette (charts and course cover art), on the brand: navy and blue against the olive and gold, the blue-yellow axis the
      palette was built on (the poles are unchanged; only the blue pole moved from petrol and teal to navy and blue) ── */

/* ── the course page: a large title and deck, the by-line as a quiet credit with a red rule, outcomes ruled and numbered as on the website,
      and the enrol and run cards carrying the brand's red top rule (the others are plain panels) ── */
:root .course-prose > h1 { font-size: clamp(36px, 4.4vw, 56px); line-height: 1; font-weight: 600; letter-spacing: -.025em; margin: 12px 0 20px; }
:root .course-prose > .a-lede { font-size: var(--fs-6); line-height: 1.5; color: var(--soft); }
:root .byline { border: 0; border-left: 3px solid var(--rda-red); border-radius: 0; background: none; padding: 4px 0 4px 20px; }
:root .course-prose > h2 { font-size: clamp(26px, 2.6vw, 32px); font-weight: 600; letter-spacing: -.015em; }
:root .outcomes { gap: 0; }
:root .outcomes li { padding: var(--sp-4) 0 var(--sp-4) var(--gutter-num); border-top: 1px solid var(--rule); font-size: var(--fs-5); }
:root .outcomes li:last-child { border-bottom: 1px solid var(--rule); }
:root .outcomes li::before { top: 22px; font-size: var(--fs-2); font-weight: 700; color: var(--accent); }
:root .course-aside > .card:nth-child(-n+2) {
  border: 1px solid var(--rule-blue); border-top: 5px solid var(--rda-red); border-radius: 14px; box-shadow: var(--shadow-2); }
:root .course-aside > .card:nth-child(n+3) { background: var(--panel); border: 1px solid var(--rule); border-radius: 12px; box-shadow: none; }

/* ── a page head (the catalogue, contact, privacy, credential): the title large, the line under it in the soft ink, and the hero's tilted blue square behind ── */
:root .pagehead {
  position: relative; isolation: isolate; overflow: hidden;
  margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw); padding: 24px calc(50vw - 50%) 36px;
}
:root .pagehead::after {
  content: ""; position: absolute; z-index: -1; top: -130px; right: -110px; width: 440px; height: 440px;
  background: var(--blue-panel); opacity: var(--tilt-opacity); transform: rotate(-12deg); border-radius: 16px;
}
:root .pagehead h1 { font-size: clamp(36px, 4.4vw, 56px); line-height: 1; font-weight: 600; letter-spacing: -.025em; margin-bottom: 20px; }
:root .pagehead .a-lede { font-size: var(--fs-6); line-height: 1.5; color: var(--soft); max-width: 36em; }
@media (max-width: 820px) {
  :root .pagehead { padding-top: 8px; padding-bottom: 20px; }
  :root .pagehead::after { width: 260px; height: 260px; top: -90px; right: -100px; }
  :root .pagehead .a-lede { font-size: var(--fs-5); }
}

/* ── /contact: the form is a card under the brand's red rule; the three notes beside it are ruled, not boxed ── */
:root .contactgrid .interest {
  background: var(--surface); border: 1px solid var(--rule-blue); border-top: 5px solid var(--rda-red); border-radius: 14px; box-shadow: var(--shadow-2);
  padding: 32px 36px 36px;
}
:root .contactside { gap: 0; }
:root .contactside .cs-block { background: none; border-radius: 0; border-top: 1px solid var(--rule); padding: 20px 0 24px; }
:root .contactside .cs-block:first-child { border-top: 3px solid var(--rda-red); }
:root .contactside .eyebrow { color: var(--accent); }
@media (max-width: 600px) {
  :root .contactgrid .interest { padding: 20px 16px 24px; }
}

/* ── the privacy notice (/privacy), one section of prose the server draws, and the credential page: neither had a rule for its links, so they were the browser's blue ── */
.prose { max-width: 56rem; }
.prose a, .cred a:not(.btn) { color: var(--accent); text-underline-offset: 2px; }
.prose a:hover, .cred a:not(.btn):hover { color: var(--accent-dark); }
:root .prose h1 { font-size: clamp(36px, 4.4vw, 56px); line-height: 1; font-weight: 600; letter-spacing: -.025em; margin-bottom: 20px; }
:root .prose .lede { font-size: var(--fs-6); line-height: 1.5; color: var(--soft); margin-bottom: 36px; }
:root .prose h2 { margin-top: 44px; }

/* ── the sign-in door (the kit's gate): the lockup where the door mark was, the title in the display weight ── */
:root .sk-gate-brand { margin-bottom: 24px; }
:root .sk-gate-brand svg { display: none; }
:root .sk-gate-brand::before { content: ""; width: 130px; height: 40px; background: url("/static/brand/logo-web.svg") no-repeat left center / contain; filter: var(--lockup-filter); }
:root .sk-gate-brand span { font-size: var(--fs-6); font-weight: 500; margin-left: 8px; padding-left: 16px; border-left: 1px solid var(--rule-blue); line-height: 28px; }
:root .sk-gate-card h2 { font-size: var(--fs-8); font-weight: 600; letter-spacing: -.02em; line-height: 1.15; margin-bottom: 8px; }

/* ── what a stranger sees: the same header and a navy footer. The server draws .holdnav and .holdfoot (a crawler is always anonymous) and the SPA
   rebuilds them with identical markup, so this is CSS over that markup; the kit's shell (everyone signed in) is styled above. The lockup is the
   same picture as the kit header's; the footer has the lockup in white. Small text on the navy is white, red is a fill or a rule only. ── */
:root .holdnav { background: var(--site-bar); border-bottom: 1px solid var(--rule); }
:root .holdnav-in { min-height: 88px; }
:root .hn-brand { gap: 0; }
:root .hn-brand::before { content: ""; width: 150px; height: 46px; background: url("/static/brand/logo-web.svg") no-repeat left center / contain; filter: var(--lockup-filter); }
:root .hn-mark { display: none; }
:root .hn-name {
  font-size: var(--fs-6); font-weight: 500; letter-spacing: -.01em; line-height: 30px;
  margin-left: 16px; padding-left: 16px; border-left: 1px solid var(--rule-blue);
}
:root .hn-links { gap: 4px 28px; }
:root .hn-link { color: var(--ink); font-size: var(--fs-3); font-weight: 500; letter-spacing: .02em; text-transform: uppercase; }
:root .hn-link:hover { color: var(--accent); }
/* The one destination that changes who you are: the brand's red button, as the website's contact call. */
:root .hn-link.hn-in { background: var(--rda-red-btn); color: #fff; font-weight: 600; padding: 8px 20px; border-radius: 8px; margin-left: 4px; }
:root .hn-link.hn-in:hover { background: var(--rda-red-dark); color: #fff; }
@media (max-width: 820px) {
  :root .holdnav-in { min-height: 64px; padding-top: 8px; padding-bottom: 8px; }
  :root .hn-brand::before { width: 104px; height: 34px; }
  :root .hn-name { font-size: var(--fs-5); line-height: 22px; margin-left: 12px; padding-left: 12px; }
  :root .hn-links { gap: 2px 16px; }
  :root .hn-link { font-size: var(--fs-2); }
  :root .hn-link.hn-in { padding: 8px 12px; }
}

:root .holdfoot { background: var(--topbar); color: #fff; border-top: 0; }
:root .holdfoot-in { padding-top: 44px; padding-bottom: 32px; align-items: flex-start; }
:root .hf-brand { gap: 0; align-items: center; }
:root .hf-brand::before {
  content: ""; width: 150px; height: 46px; background: url("/static/brand/logo-web.svg") no-repeat left center / contain; filter: brightness(0) invert(1);
}
:root .hf-mark { display: none; }
:root .hf-brand > div { margin-left: 16px; padding-left: 16px; border-left: 1px solid rgba(255, 255, 255, .28); }
:root .hf-name { color: #fff; font-size: var(--fs-5); font-weight: 500; }
/* "from Red Door Analytics" would sit beside the lockup that says it: kept for a screen reader, which cannot read the drawn lockup. */
:root .hf-by { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
:root .hf-links { gap: 8px 28px; padding-top: 8px; }
:root .hf-link { color: rgba(255, 255, 255, .88); font-size: var(--fs-4); }
:root .hf-link:hover { color: #fff; text-decoration: underline; }
:root .hf-link:focus-visible { outline: 2px solid #fff; outline-offset: 3px; border-radius: 3px; }
:root .holdfoot-legal { position: relative; padding-top: 20px; padding-bottom: 28px; align-items: center; color: rgba(255, 255, 255, .72); }
:root .holdfoot-legal::before { content: ""; position: absolute; top: 0; left: 30px; right: 30px; height: 1px; background: rgba(255, 255, 255, .18); }
@media (max-width: 640px) {
  :root .holdfoot-in { padding-top: 32px; padding-bottom: 24px; }
  :root .holdfoot-legal { flex-direction: column; align-items: flex-start; gap: 8px; padding-top: 16px; padding-bottom: 20px; }
}

/* ═══ THE WORKING SCREENS (data-shell="work", set by app.js shellFor) ═══════════════════════════════════════════════════════════════════════════════════════════
   Signed in, the screens a person works in (My courses, a lesson, their own data, the operator's screens) and the website's own pages have ONE frame: the white
   top bar above and, on a lesson, the course menu beside the text (the owner's word, 16 Oct 2026). Until then the working screens stood beside a navy rail
   (12 Oct 2026, redesign step 4) and the rest under the top bar, so moving from Home to My courses changed the whole frame, and on a large screen the rail
   sat at the window's edge with half a window of nothing between it and the text. What still differs is the PAGE HEAD: a working one, not a masthead. */
/* The working page head: the label in the mono caps, the title at the heading size, no masthead square behind it. */
:root[data-shell="work"] .pagehead { overflow: visible; margin: 0 0 var(--sp-6); padding: 0; }
:root[data-shell="work"] .pagehead::after { display: none; }
:root[data-shell="work"] .pagehead h1 { font-size: var(--fs-8); line-height: var(--lh-8); letter-spacing: var(--ls-8); margin: var(--sp-1) 0 0; }
:root[data-shell="work"] .pagehead > h1:first-child { margin-top: 0; }
:root[data-shell="work"] .pagehead .eyebrow { display: block; font-family: var(--mono); font-size: var(--fs-2); font-weight: 600; letter-spacing: .14em; text-transform: uppercase; color: var(--accent); }
/* ONE HEAD FOR EVERY WORKING SCREEN (review of 15 Oct 2026, chunk 2, C3). Measured at 1440: the top of the first text was 21px on Privacy and the staff
   overview and list, 26 on a lesson, 27 on a staff detail page and 46 on My courses (an eyebrow and a 38px title, the only screen on `.pagehead`), and an empty
   My courses was a centred 30px title over a centred message. The page container's first line now sits at one height, the title is the heading size
   (--fs-8) everywhere, and an empty state keeps the page's left edge. */
:root[data-shell="work"] .a-page { padding-top: var(--sp-8); }
/* …except a lesson with no menu beside it: the strip is its first line, and under 1000px 32px of nothing between the bar and the strip is a gap the strip's own padding then doubles. */
@media (max-width: 999px) { :root[data-shell="work"] .a-page.lesson-shell { padding-top: var(--sp-4); } }
/* …and the course page of a course the person is on, which is a working screen too, takes the same title as every other screen there (it was the website's 56px). */
:root[data-shell="work"] .course-prose > h1 { font-size: var(--fs-8); line-height: var(--lh-8); letter-spacing: var(--ls-8); margin: var(--sp-1) 0 var(--sp-2); }
:root[data-shell="work"] .a-page > .empty { align-items: flex-start; text-align: left; padding: var(--sp-6) 0 0; }
:root[data-shell="work"] .empty-cta { justify-content: flex-start; }
:root[data-shell="work"] .a-page:has(> h1 + .empty) > h1 { text-align: left; }

/* ═══ THE DRAWER, where the bar has no room for its nav (review of 15 Oct 2026, chunk 1: A1, A2, C9) ═══════════════════════════════════════════════════════════
   Between 701 and 820px (and wherever the bar measures too tight) the nav is a drawer under the header, and below 700px "More" opens it above the tab
   bar. When the header went white (the brand step, 11-12 Oct) the kit's menu button kept its white ink and the drawer kept its navy ground with the
   header's navy ink on it, so on a tablet the button was white on white (1.00:1) and Home and Courses were navy on navy (1.00:1; the current item
   2.25:1). The drawer is a list on the navy ground: the same items, the staff section in it, one class (`sk-drawer`, set by suite-kit-navfit.js)
   for "the nav is a drawer" instead of a width and a measurement. It stands wherever that class is set, at 820px and under and wherever the bar
   measures too tight; until 16 Oct 2026 it stood under 900px only, because from 900px a navy rail took the nav. */
:root { --bar-h: 88px; --sheet-bar-h: 62px; }       /* the header, and the phone's tab bar the sheet stands on */
@media (max-width: 820px) { :root { --bar-h: 64px; } }
:root .topbar .navtoggle { color: var(--ink); font-size: var(--fs-7); min-width: 44px; min-height: 44px; padding: 0; border-radius: var(--r-ctl); }
:root .topbar .navtoggle:hover { background: var(--panel); }
:root.sk-drawer .topbar .topnav {
  top: 100%; max-height: calc(100dvh - var(--bar-h)); padding: var(--sp-2) 0 var(--sp-3);
  background: var(--topbar); border-top: 1px solid rgba(255, 255, 255, .14); box-shadow: var(--shadow-3); --focus-ring: #fff;
}
:root.sk-drawer .topbar .topnav .navbtn {
  display: flex; align-items: center; justify-content: flex-start; gap: var(--sp-3); width: 100%; min-height: 48px; margin: 0; padding: var(--sp-3) var(--sp-6);
  border-radius: 0; background: none; box-shadow: none; text-align: left; text-transform: none; letter-spacing: 0;
  font-size: var(--fs-5); font-weight: 500; color: rgba(255, 255, 255, .84);
}
:root.sk-drawer .topbar .topnav .navbtn:hover { color: #fff; background: rgba(255, 255, 255, .07); }
:root.sk-drawer .topbar .topnav .navbtn.on { color: #fff; font-weight: 600; background: rgba(255, 255, 255, .1); box-shadow: inset 3px 0 0 var(--rda-red); }
:root.sk-drawer .topbar .topnav .navhome-lbl { display: inline; }
/* the staff section, in the drawer and not only in a dropdown: "railOnly" meant "not a tab", and a tablet and a phone had no other way to it */
:root.sk-drawer .topbar .topnav .navbtn.navrail, :root.sk-drawer .topbar .topnav .navbtn.navinmenu { display: flex; }
:root.sk-drawer .topbar .topnav .navgroup {
  display: block; padding: var(--sp-6) var(--sp-6) var(--sp-2); font-family: var(--mono); font-size: var(--fs-1); font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase; color: rgba(255, 255, 255, .64);
}
/* THE STAFF DROPDOWN (the kit's `menu`; buildNavMenu in suite-kit-shell.js). In the bar the staff screens are ONE button with a panel; the flat items are for the
   drawer, which has no room to hang a panel from, so each is shown where the other is not. The panel is the account menu's own: the same surface, edge and rows. */
:root .topbar .topnav .navbtn.navinmenu { display: none; }
.navmenu { position: relative; display: flex; align-items: center; height: 100%; }
.navmenubtn { display: inline-flex; align-items: center; gap: var(--sp-1); }
.navmenubtn .navchev { margin-left: 0; color: var(--muted); }
.navmenubtn[aria-expanded="true"] .navchev { transform: rotate(180deg); }
.navmenu-panel { position: absolute; top: calc(100% + var(--sp-1)); right: calc(var(--sp-3) * -1); min-width: 14rem; padding: var(--sp-2); z-index: 50;
  background: var(--surface); border: 1px solid var(--card-line); border-radius: var(--r-well); box-shadow: var(--shadow-3); }
.navmenu-panel[hidden] { display: none; }
.navmenu-item { display: block; width: 100%; text-align: left; background: none; border: 0; border-radius: var(--r-ctl); cursor: pointer;
  font-family: var(--font); font-size: var(--fs-4); font-weight: 500; color: var(--ink); padding: var(--sp-3) var(--sp-4); }
.navmenu-item:hover { background: var(--panel); }
.navmenu-item.on { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.navmenu-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
:root.sk-drawer .navmenu { display: none; }
@media (max-width: 700px) {
  :root.sk-drawer .topbar .topnav {
    position: fixed; top: auto; bottom: calc(var(--sheet-bar-h) + env(safe-area-inset-bottom, 0px));
    max-height: calc(100dvh - var(--bar-h) - var(--sheet-bar-h) - env(safe-area-inset-bottom, 0px));
    border-top: 0; border-radius: var(--r-sheet) var(--r-sheet) 0 0;
  }
}
/* The header's own small controls were under the 24px minimum (WCAG 2.5.8): the home icon 21 wide and the phone's account button 22. */
/* The icon stays where it was: the box grows by 11px a side, the margins take the 11 back. */
:root .navbtn.navhome { min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; padding: 0; margin-left: 8px; margin-right: -11px; }
@media (max-width: 820px) { :root .topbar .acctbtn { min-width: 44px; min-height: 44px; } }
/* …and the operator's: a checkbox's target is the label round it (a click on the words ticks it), the bare radios in the quiz editor are the
   target themselves, and the links in a course's line of facts were 21px high. */
.adm-inline { min-height: var(--sp-6); }
.adm-quiz-option > .adm-check { width: var(--sp-6); height: var(--sp-6); }
.adm-course-meta a { display: inline-flex; align-items: center; min-height: var(--sp-6); }
.adm-extend > summary { min-height: var(--sp-6); }
/* The form under Extend / Withdraw / Reinstate in a roster's Access cell. It is the LAST column, and its controls ran in one row, so the confirm button of Extend sat past the edge of the table's
   scroll box (measured at 1440px: the button at x 1398 in a box that ends at 1330). They wrap now, and the sentence under Withdraw and Reinstate takes a line of its own. */
.adm-extend-form { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
.adm-place-note { flex-basis: 100%; white-space: normal; max-width: 20em; margin: var(--sp-1) 0 0; }
.rail-course, .strip-course { padding-block: var(--sp-1); }

/* ═══ SPACING AND ALIGNMENT POLISH (13 Oct 2026) ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════
   Found by looking at every page, light, dark and at a phone width, against main 7ddc9dde, and measured before it was changed (the numbers are in the pull
   request). Each rule below names the defect it ends. Tokens only (test_css_tokens): no literal colour, no literal font size. */

/* 1. THE MASTHEAD'S TILTED SQUARE. The page container's top padding left a 22 to 24px strip of page ground between the header and the masthead, and the
      square was clipped flat at the strip and again at the masthead's bottom, so it read as a floating slab with a hard top and a hard bottom on the courses
      page. The masthead now starts flush under the header (the padding it lost is its own), the square bleeds up under the header, and on a page head it is
      sized to end inside the masthead with its own corner instead of being cut off. */
:root:not([data-shell="work"]) .a-page > .pagehead:first-child { margin-top: calc(var(--sp-6) * -1); padding-top: calc(var(--sp-6) + var(--sp-6)); }
:root:not([data-shell="work"]) .a-page > .pagehead:first-child::after { top: calc(var(--sp-12) * -2); width: 270px; height: 270px; right: -40px; }
:root:not([data-shell="work"]) .home > .home-hero:first-child { margin-top: -22px; padding-top: calc(72px + 22px); }
/* A page that is only a title over an empty state (the not-found page) starts 48px under the bar, as the catalogue and the contact page do, not 24: the title sat tight under the bar's rule. */
:root:not([data-shell="work"]) .a-page:has(> h1 + .empty) { padding-top: var(--sp-12); }
@media (max-width: 820px) {
  :root:not([data-shell="work"]) .a-page > .pagehead:first-child { padding-top: calc(var(--sp-2) + var(--sp-6)); }
  :root:not([data-shell="work"]) .a-page > .pagehead:first-child::after { top: calc(var(--sp-12) * -2); width: 200px; height: 200px; right: -70px; }
  :root:not([data-shell="work"]) .home > .home-hero:first-child { padding-top: calc(36px + 22px); }
}

/* 2. COURSE CARDS. The rule above a card's price and date sat at a different height in two cards side by side, because a card with one line of offer is
      shorter than one with two and the footer is bottom-aligned. The footer keeps two lines' height, so the rules line up and the text still ends on the
      same baseline. */
.cc-offer { min-height: 3.7rem; }   /* two lines of price and date (measured 58.8px) with the divider's padding */

/* 3. THE CLOSING BAND'S LABEL. Every other label of this kind is the small red uppercase eyebrow; this one was plain body text. */
:root .cpitch .eyebrow { display: block; font-family: var(--font); font-size: var(--fs-3); font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  color: var(--accent); margin-bottom: var(--sp-3); }

/* 4. THE COURSE PAGE. (a) The outcomes' rules stopped at the reading measure while the curriculum's ran the whole column: one column, one right edge.
      (b) The aside's cards were 16, 42 and 29px apart (margins on some and not others): one gap. (c) The timetable's disclosure marker hung outside the
      text column, left of every other line in the card: it is now a chevron after the words. (d) A practical's row was indented by its red edge and its own
      padding, so its dot, number, label and title sat 13px right of every other row's: the edge now hangs in the margin and the columns line up. */
:root .course-prose > .outcomes { max-width: none; }
:root .course-aside { display: flex; flex-direction: column; gap: var(--sp-4); }
:root .course-aside > .card { margin: 0; }
details.timetable > summary { list-style: none; cursor: pointer; }
details.timetable > summary::-webkit-details-marker { display: none; }
details.timetable > summary::after { content: "\25B8"; margin-left: var(--sp-2); color: var(--muted); }
details.timetable[open] > summary::after { content: "\25BE"; }
:root .lessons { margin-left: calc(2px - 3px - var(--sp-2)); }
:root a.lesson-row { border-left: 3px solid transparent; padding-left: var(--sp-2); }
:root a.lesson-row.is-practical, :root .lesson-row.is-practical { border-left-color: var(--accent); padding-left: var(--sp-2); }

/* 5. THE PRACTICAL'S ANSWER BOXES. The four number fields were drawn in the page's panel tone with a hairline, so on the page ground they were barely
      there, while the paste box above them (the kit's field) had a firm border. One field style: the kit's. The paste box's placeholder is allowed to wrap, so
      on a phone it is not cut off at "the whole log". */
.ex-field input { background: var(--field, var(--panel)); border-color: var(--field-line, var(--rule)); }
.ex-reader .ex-paste::placeholder { white-space: pre-wrap; }

/* 6. MY COURSES. (a) The access date wrapped to two lines at the headline size and made its card's number a line taller than its neighbours: it is set
      one size down so it holds one line. (b) The Continue and Live school panels ended at different heights and their header rules sat 8px apart: they
      stretch to one height and the headers share one. (c) The state chip under a practical's title floated at a different height in each tile (a three-line
      title pushed it down): the chips are pinned to the tile's foot. */
:root .mh-stat.access-active .mh-v { font-size: var(--fs-7); white-space: nowrap; line-height: 40px; }
:root .mh-cols { align-items: stretch; }
:root .mh-side { display: flex; flex-direction: column; }
:root .mh-side > .wb-panel { flex: 1 1 auto; }
:root .mh-cols .wb-ph { min-height: 58px; box-sizing: border-box; align-items: center; }
:root .mh-tile { display: flex; flex-direction: column; }
:root .mh-tile .mh-chip { margin-top: auto; align-self: flex-start; }
:root .mh-tile .mh-tt { margin-bottom: var(--sp-3); }

/* 7. DATA AND PRIVACY. (a) The erase card sat 1px under the newsletter card: a later `.privacy-erase { margin-top: 0 }` beat the column's own gap. (b) The
      course links in the record were the browser's default blue. (c) A fact's label and its value were top-aligned, three pixels off one baseline. */
:root .privacy-side > .privacy-erase { margin-top: var(--sp-6); }
:root .privacy-list a { display: inline-flex; align-items: center; min-height: var(--sp-6); color: var(--accent); text-underline-offset: .18em; }
:root .privacy-facts { align-items: baseline; }

/* 8. THE NOT-FOUND PAGE. The two buttons touched (a 1px seam); the title sat at the left over a centred message. */
:root .empty-cta { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--sp-3); }
:root .a-page:has(> h1 + .empty) > h1 { text-align: center; }

/* ═══ THE PUBLIC CHROME AND THE HOME PAGE, TOWARDS THE MOCK-UP (13 Oct 2026) ═══════════════════════════════════════════════════════════════════════════════════
   The owner's word: the live homepage moves closer to the mock-up (ops/planning/academy-redesign-2026-10-11, a-home). Written against the markup render.py and app.js both
   draw (class for class: the server draws first and the script replaces it). Tokens only (test_css_tokens). It sits BEFORE the model builder's section for the reason
   the polish block above does. */

/* ── the header: three links, the external ones ending in the arrow, and Sign in the quiet outlined button of the mock-up (it was a red fill) ── */
:root .hn-link[rel~="noopener"]::after { content: " \2197"; font-size: .85em; }
:root .hn-link.hn-in { background: transparent; color: var(--ink); border: 1px solid var(--rule-blue); padding: 8px 16px; margin-left: var(--sp-3); font-weight: 600; }
:root .hn-link.hn-in:hover { background: transparent; color: var(--accent); border-color: var(--accent); }
:root[data-theme="dark"] .hn-link.hn-in:hover { color: var(--ink); border-color: var(--ink); }

/* ── the footer: who Academy is, then two short columns, on the navy ── */
:root .pubfoot-in { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr) minmax(0, 1fr); gap: var(--sp-8); align-items: start; }
:root .pf-about .hf-brand { margin-bottom: var(--sp-4); }
:root .pf-blurb { margin: 0; max-width: 26em; font-size: var(--fs-3); line-height: 1.55; color: rgba(255, 255, 255, .72); }
:root .pf-col { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-3); padding-top: 8px; }
:root .pf-h { margin-bottom: var(--sp-1); font-family: var(--font); font-size: var(--fs-2); font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  color: rgba(255, 255, 255, .78); }
@media (max-width: 820px) {
  :root .pubfoot-in { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--sp-6); }
  :root .pf-about { grid-column: 1 / -1; }
}
@media (max-width: 520px) {
  :root .pubfoot-in { grid-template-columns: minmax(0, 1fr); }
}


/* ── the home page: .a-page.pubhome holds the hero, the figures, the courses, the proof, who teaches and the closing band. The hero and the strip bleed to the
      viewport's edges the way the kit's .home-hero does (the margin and padding pair), so the tilted square and the rules reach the page's edge ── */
:root .pubhome { padding-bottom: 0; }
:root .pubhome > .ph-hero {
  position: relative; isolation: isolate; overflow: hidden;
  display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr); gap: var(--sp-10) var(--sp-12); align-items: center;
  margin: calc(var(--sp-6) * -1) calc(50% - 50vw) 0; padding: calc(var(--sp-6) + 64px) calc(50vw - 50%) 72px;
}
:root .pubhome > .ph-hero::after {
  content: ""; position: absolute; z-index: -1; top: -120px; right: -80px; width: 360px; height: 360px;
  background: var(--blue-panel); opacity: var(--tilt-opacity); transform: rotate(-12deg); border-radius: 18px;
}
:root .ph-eyebrow { display: block; font-family: var(--font); font-size: var(--fs-3); font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: var(--accent); }
:root .ph-hero h1 { margin: var(--sp-5) 0 var(--sp-6); max-width: 13em; font-size: clamp(36px, 5vw, 64px); line-height: 1.05; font-weight: 600; letter-spacing: -.025em; color: var(--ink); }
:root .ph-hero h1 em { font-style: normal; font-weight: 700; color: var(--red-display); }
:root .ph-lede { margin: 0 0 var(--sp-8); max-width: 34em; font-size: var(--fs-6); line-height: 1.5; color: var(--soft); text-wrap: pretty; }
:root .ph-acts { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
:root .ph-acts .btn { padding: 16px 24px; }
/* the links down the page (16 Oct 2026, "a link row"): quiet text under the buttons, naming the sections that follow; the landing sections clear the sticky bar */
:root .ph-jump { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2) var(--sp-5); margin-top: var(--sp-6); }
:root .ph-jump span { font-family: var(--mono); font-size: var(--fs-2); font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
:root .ph-jump a { font-size: var(--fs-3); font-weight: 600; color: var(--accent); text-decoration: none; }
:root .ph-jump a:hover { text-decoration: underline; text-underline-offset: 3px; }
:root .pubhome #courses, :root .pubhome #marking, :root .pubhome #teachers, :root .pubhome #interest { scroll-margin-top: 104px; }
:root .pubhome [tabindex="-1"]:focus { outline: none; }   /* a section a link has just taken the reader to holds focus without a ring round the whole band */
:root .btn-secondary { background: transparent; color: var(--ink); border: 1px solid var(--rule-blue); }
:root .btn-secondary:hover { background: transparent; color: var(--accent); border-color: var(--accent); }
:root[data-theme="dark"] .btn-secondary:hover { color: var(--ink); border-color: var(--ink); }

/* the four ways: white cards, the brand's red bar across the top of each that is open now and wiping in on the others */
:root .ph-ways { display: flex; flex-direction: column; gap: 12px; }
:root .ph-way {
  position: relative; overflow: hidden; display: block; padding: 16px 24px 20px; text-decoration: none; color: inherit;
  background: var(--surface); border: 1px solid var(--rule); border-radius: 14px; transition: border-color .2s var(--ease), box-shadow .2s var(--ease), transform .2s var(--ease);
}
:root .ph-way::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--rda-red);
  transform: scaleX(0); transform-origin: left; transition: transform .25s var(--ease);
}
:root .ph-way.is-open::before, :root .ph-way:hover::before { transform: scaleX(1); }
:root .ph-way:hover { border-color: var(--rule-blue); box-shadow: var(--shadow-2); transform: translateY(-2px); }
:root .ph-wn { display: block; font-family: var(--mono); font-size: var(--fs-2); font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--accent); }
:root .ph-wl { display: block; margin-top: var(--sp-2); font-size: var(--fs-5); font-weight: 700; letter-spacing: -.01em; line-height: 1.3; color: var(--ink); }
:root .ph-wm { display: block; margin-top: var(--sp-1); font-size: var(--fs-3); color: var(--muted); }
:root .ph-wa { display: block; margin-top: var(--sp-4); font-size: var(--fs-2); font-weight: 700; letter-spacing: .02em; text-transform: uppercase; color: var(--accent); }

/* the example's figure leads the marking band: the example dataset's Kaplan-Meier figure with its risk table, one picture drawn by the server (it moves once, on first view,
   inside the picture, and not under reduced motion). It was the hero's foot, half off screen on a laptop; now ONE blue band in two sections, this one (the data, on a white
   panel under a heading) and .marked (what the marker said about one attempt at it), which carries on from it without a gap. */
:root section.ph-example {
  position: relative; margin-top: 80px; padding: 84px 0 0;
  background: var(--blue-panel); box-shadow: 0 0 0 100vmax var(--blue-panel); clip-path: inset(0 -100vmax);
}
:root section.ph-example > .sec-head { margin-bottom: var(--sp-6); }
:root section.ph-example .sec-head h2 { margin-top: 0; }
:root section.ph-example .sec-head .a-note { color: var(--soft); }
:root section.ph-example .eyebrow {
  display: block; margin-bottom: var(--sp-3); font-family: var(--font); font-size: var(--fs-3); font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: var(--accent); }
/* The panel spans the page's own column, as the courses, the marked example and the teachers below it do (the owner, 15 Oct 2026: "needs to span the width of the page, to
   align with the other sections"): it was capped at 960px and sat 260px short of the right edge at every width from 1280 up. The picture scales with it, as one image. */
:root .ph-example > .ph-fig { margin: 0; padding: var(--sp-6) var(--sp-8) var(--sp-4); background: var(--surface); border: 1px solid var(--rule); border-radius: 14px; box-shadow: var(--shadow-2); }
:root .pubhome > .ph-example + section.marked { margin-top: 0; padding-top: 56px; }   /* (more specific than .pubhome > section.marked, which comes later) */
:root .ph-fig picture, :root .ph-fig img { display: block; width: 100%; height: auto; }
:root .ph-fig figcaption { margin-top: var(--sp-3); font-family: var(--mono); font-size: var(--fs-2); letter-spacing: .04em; color: var(--muted); }
/* the picture's reserved box agrees with the file each width is given (render._example_figure): no shift when the figure arrives */
:root .ph-fig img { aspect-ratio: 880 / 300; }
/* the held-back picture (app.js enhanceFigure sets visibility inline until the band is seen) is always there on paper */
@media print { :root .ph-fig picture { visibility: visible !important; } }
@media (max-width: 640px) {
  :root .ph-fig img { aspect-ratio: 480 / 316; } :root .ph-fig figcaption { font-size: var(--fs-3); letter-spacing: .02em; }
  :root section.ph-example { margin-top: 52px; padding-top: 52px; }
  :root .ph-example > .ph-fig { padding: var(--sp-4) var(--sp-3) var(--sp-3); }
}

/* the figures: a quiet strip between two rules, a numeral and what it counts */
:root .pubhome > .ph-strip {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--sp-8);
  margin: 0 calc(50% - 50vw); padding: 48px calc(50vw - 50%); border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
}
:root .ph-n { font-size: clamp(30px, 3.4vw, 44px); font-weight: 700; letter-spacing: -.02em; line-height: 1; color: var(--ink); }
:root .ph-l { margin-top: var(--sp-3); max-width: 18em; font-size: var(--fs-3); line-height: 1.45; color: var(--muted); }
:root .ph-l b { display: block; font-weight: 600; color: var(--ink); }

/* the sections: an eyebrow, a headline in the brand's scale, a deck */
:root .ph-courses, :root .ph-teachers { margin-top: 88px; }
:root .ph-courses .eyebrow, :root .ph-teachers .eyebrow {
  display: block; margin-bottom: var(--sp-3); font-family: var(--font); font-size: var(--fs-3); font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: var(--accent); }
:root .ph-courses .sec-head h2, :root .ph-teachers h2 { margin-top: 0; }
/* the teachers' heading is not inside a .sec-head, so it fell to .a-page h2 (24px) beside two section titles at up to 44px: the same scale; the decks and bios
   do not leave a single word on a line of their own; the proof's title keeps a heading's distance from its lede */
:root .ph-teachers h2 { font-size: clamp(28px, 3.2vw, 44px); line-height: 1.1; font-weight: 600; letter-spacing: -.015em; margin-bottom: var(--sp-3); }
:root .ph-courses .sec-head .a-note, :root .ph-person span { text-wrap: pretty; }
/* THE HOME'S COURSES: the open ones as a row, the planned ones in ONE sideways scroller (his answer, 15 Oct 2026). It snaps card by
   card, the next card peeks (a card is 340px in a 1,220px column: three and a bit), and it is a focusable region so the arrow keys
   scroll it. There is no arrow button and no Pause button, no scrollbar and a faded edge on each side (his words, 16 Oct 2026: it should
   look like the RDA site's "Interactive tools" strip): the script moves the row and the pointer, focus or a finger holds it. */
:root .ph-soon { margin-top: var(--sp-8); }
:root .ph-soon-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-bottom: var(--sp-4); }
:root .ph-soon-head h3 { margin: 0; font-size: var(--fs-6); line-height: var(--lh-6); letter-spacing: var(--ls-6); }
:root .ph-scroller { display: flex; gap: var(--sp-5); overflow-x: auto; scroll-snap-type: x mandatory; overscroll-behavior-x: contain;
  padding: var(--sp-2) 0 var(--sp-5); scrollbar-width: none; -webkit-overflow-scrolling: touch;
  -webkit-mask-image: linear-gradient(90deg, transparent, var(--ink) 3%, var(--ink) 97%, transparent);
  mask-image: linear-gradient(90deg, transparent, var(--ink) 3%, var(--ink) 97%, transparent); }
:root .ph-scroller::-webkit-scrollbar { display: none; }
:root .ph-scroller > * { flex: 0 0 min(340px, 84%); scroll-snap-align: start; }
/* The edge fade is a mask on the row, and a mask clips an outline, so while the row itself has KEYBOARD focus the fade comes off and the ring is whole. */
:root .ph-scroller:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: 6px; -webkit-mask-image: none; mask-image: none; }
@media (prefers-reduced-motion: no-preference) { :root .ph-scroller { scroll-behavior: smooth; } }
/* THE ROW MOVES ON ITS OWN (the owner, 15 Oct 2026: "scrolling along smoothly on a loop"). While the script is in charge of it,
   snapping and smooth-scrolling are off: both would turn each frame's tiny step into a jump or an animation of its own, and snapping
   would pull the row to the nearest card edge the moment the pointer rests on it (the pointer is the only way to stop it, his word
   of 16 Oct 2026, so it must stop exactly where it is). A reader's own scroll or key press still works (the script carries on from
   where they put it), and none of this is drawn for a reader who has asked for less motion (the script adds no duplicate set and
   never starts, so the row snaps card by card as before), nor without script. */
:root .ph-scroller.is-looping { scroll-snap-type: none; scroll-behavior: auto; }
:root .ph-all { margin: var(--sp-5) 0 0; }
:root .ph-all a { color: var(--accent); font-weight: var(--fw-ui); }
:root .marked .sec-head h2 { margin-bottom: var(--sp-4); }
:root .pubhome > section.marked { margin-top: 88px; }
:root .marked .sec-head h2 em { font-style: normal; color: var(--red-display); }
:root .marked-more { margin: var(--sp-5) 0 0; }
/* the two movements of the marked example (render._marked_example; app.js enhanceMarked): rows read in order once, and the same attempt re-marked with the mistake undone.
   Opacity only, so nothing moves; under reduced motion the script plays neither and the drawn state stands. */
:root .marked-fix { margin: var(--sp-5) 0 0; }
:root .marked-items li { transition: opacity .25s var(--ease); }
:root .marked.is-armed .marked-items li.is-unread { opacity: .2; }
:root .marked-items li.is-fixed .mi-why { display: none; }
:root .marked-card { display: flex; flex-direction: column; }
:root .marked-fixnote { margin: auto 0 0; padding: 12px 24px; border-top: 1px solid rgba(255, 255, 255, .1); color: rgba(255, 255, 255, .72);
  font-family: var(--mono); font-size: var(--fs-3); }
@media (prefers-reduced-motion: reduce) { :root .marked-items li { transition: none; } }
:root .marked-more a { font-size: var(--fs-2); font-weight: 700; letter-spacing: .02em; text-transform: uppercase; color: var(--accent); text-decoration: none; }
:root .marked-more a:hover { text-decoration: underline; }
:root .marked-score { display: flex; justify-content: space-between; align-items: center; gap: var(--sp-4); }
:root .marked-score b { font-weight: 700; color: #fff; }

/* THE TEACHERS, PUT FORWARD (the owner, 16 Oct 2026: "our experience is how we stand apart"; his pick, "Band + faces"). Three places, one idea: a band under the hero with a portrait,
   a name and a line each, their faces on every course card, and their own section further down, a card for each person with what the company's website prints about them.
   (Not their faces on the hero's last line as well: the owner, the same day, "having the images twice closely doesn't make sense".) */
/* on a card the faces are their own row above the words, so the by-line begins at the same place on every card whether one person teaches it or two */
:root .cc-faces { display: flex; margin-bottom: var(--sp-2); }
:root .cc-faces img { display: block; width: 32px; height: 32px; border-radius: 50%; object-fit: cover; background: var(--well); border: 2px solid var(--surface); }
:root .cc-faces img + img { margin-left: calc(var(--sp-2) * -1); }

/* the band under the hero: the words and a link down on the left, the people on the right, each a portrait, a name, the job title and two of the research tags */
:root .pubhome > .ph-meet {
  display: grid; grid-template-columns: minmax(0, 14rem) minmax(0, 1fr); gap: var(--sp-8); align-items: center;
  margin: 0 0 var(--sp-12); padding: var(--sp-8) var(--sp-6); background: var(--surface); border: 1px solid var(--rule); border-radius: 14px;
}
:root .ph-meet .eyebrow {
  display: block; margin-bottom: var(--sp-3); font-family: var(--font); font-size: var(--fs-3); font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: var(--accent); }
:root .ph-meet h2 { margin: 0; font-size: var(--fs-7); line-height: var(--lh-7); letter-spacing: var(--ls-7); font-weight: 600; text-wrap: balance; }
:root .ph-meet-more { display: inline-block; margin-top: var(--sp-4); font-size: var(--fs-2); font-weight: 700; letter-spacing: .02em; text-transform: uppercase; color: var(--accent); text-decoration: none; }
:root .ph-meet-more:hover { text-decoration: underline; text-underline-offset: 3px; }
:root .ph-meet-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); gap: var(--sp-6); margin: 0; padding: 0; list-style: none; }
:root .ph-meet-p { display: flex; align-items: flex-start; gap: var(--sp-4); }
:root .ph-meet-p img { flex: none; display: block; width: 72px; height: 72px; border-radius: 14px; object-fit: cover; background: var(--well); }
:root .ph-meet-p > div { min-width: 0; }
:root .ph-meet-p b { display: block; font-size: var(--fs-4); font-weight: 600; line-height: 1.3; color: var(--ink); }
:root .ph-meet-p span { display: block; margin-top: var(--sp-1); font-size: var(--fs-3); line-height: 1.45; color: var(--muted); text-wrap: pretty; }

/* who teaches, the longer version: the words across the top, then a card for each person: the portrait, the name with the PhD, the job title, the education line, one sentence
   and the research tags (a guest, whom the website does not carry, has the portrait or nothing, the name and the catalogue's own line) */
:root .ph-teachers { display: block; }
:root .ph-t-copy { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 26rem); gap: 0 var(--sp-12); align-items: end; margin-bottom: var(--sp-8); }
:root .ph-t-copy .eyebrow, :root .ph-t-copy h2 { grid-column: 1; }
:root .ph-teachers .a-note { grid-column: 2; grid-row: 1 / span 2; margin: 0 0 var(--sp-2); color: var(--soft); }
/* at most three to a row (one, two or three people each take a third; four to six stand 3 and 3, never 4 and an orphan), and one to a row on a phone */
:root .ph-people { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, max(16rem, calc((100% - var(--sp-12)) / 3 - 1px))), 1fr)); gap: var(--sp-6); }
:root .ph-person { display: flex; flex-direction: column; overflow: hidden; background: var(--surface); border: 1px solid var(--rule); border-radius: 14px; }
/* THE PORTRAITS are one square each, cropped alike (the .jpg files in static/team): the face the same size and centred, the top of the head the same distance from the top edge. A square box
   shows the whole file, so no face is cut however wide or narrow the box is drawn, and none needs a crop offset of its own. */
:root .ph-photo { aspect-ratio: 1 / 1; overflow: hidden; background: var(--blue-panel); }
:root .ph-photo img { display: block; width: 100%; height: 100%; object-fit: cover; }
:root .ph-p-body { display: grid; align-content: start; gap: var(--sp-2); padding: var(--sp-5) var(--sp-6) var(--sp-6); }
:root .ph-person b { display: block; font-size: var(--fs-6); font-weight: 600; line-height: var(--lh-6); color: var(--ink); }
:root .ph-person span { display: block; font-size: var(--fs-3); line-height: 1.5; color: var(--muted); }
:root .ph-person .ph-p-edu { font-family: var(--mono); font-size: var(--fs-2); letter-spacing: .02em; }
:root .ph-p-about { margin: 0; font-size: var(--fs-4); line-height: var(--lh-4); color: var(--ink); text-wrap: pretty; }
:root .ph-p-chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: var(--sp-1) 0 0; padding: 0; list-style: none; }
:root .ph-p-chips li { padding: var(--sp-1) var(--sp-3); border: 1px solid var(--rule-blue); border-radius: 99px; font-size: var(--fs-2); line-height: 1.4; color: var(--soft); }

/* the closing band: navy, edge to edge, the form on its card with the brand's red rule across it */
:root .ph-close {
  /* the bottom margin takes back the 70px the kit's `main` pads its foot with, so the band ends on the footer (a copy of suite-kit.css's number, as .holdnav's gutter is) */
  margin: 96px 0 -70px; padding: 72px 0; background: var(--topbar);
  box-shadow: 0 0 0 100vmax var(--topbar); clip-path: inset(0 -100vmax);
}
:root .ph-close .interest--panel { margin: 0; background: var(--surface); border: 1px solid var(--rule); border-top: 4px solid var(--rda-red); border-radius: 14px; }

/* the course card's last line, in the mock-up's red capitals; and the consent line's link in the accent (it was the browser's blue and underline on the server-drawn page) */
:root .cc-go { display: block; margin-top: var(--sp-3); font-size: var(--fs-2); font-weight: 700; letter-spacing: .02em; text-transform: uppercase; color: var(--accent); }
/* ONE FOOT FOR EVERY CARD IN A ROW. A live course ends in a link (18.6px high) and a course that is not live in a button (36.6px), so the rule above the price and date
   stood 18px higher on the card with the button, and the row of cards had two baselines. The link has the button's padding and an invisible edge: the same height. */
:root .coursecard .cc-go:not(.cc-register) { padding: var(--sp-2) 0; border: solid transparent; border-width: 1px 0; }   /* top and bottom only: a side edge would push the words 1px off the card's text */
:root .if-consent a { color: var(--accent); }
:root[data-theme="dark"] .if-consent a { color: var(--ink); text-decoration: underline; }
@media (max-width: 620px) {
  :root .hn-link.hn-in { margin-left: 0; padding: 8px 12px; }
}

@media (max-width: 900px) {
  :root .pubhome > .ph-hero { grid-template-columns: minmax(0, 1fr); padding-top: calc(var(--sp-6) + 36px); padding-bottom: 44px; }
  :root .pubhome > .ph-hero::after { width: 220px; height: 220px; top: -90px; right: -70px; }
  :root .pubhome > .ph-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-6); padding-top: 36px; padding-bottom: 36px; }
  :root .pubhome > .ph-meet { grid-template-columns: minmax(0, 1fr); gap: var(--sp-5); padding: var(--sp-6) var(--sp-5); margin-bottom: var(--sp-8); }
  :root .ph-t-copy { grid-template-columns: minmax(0, 1fr); }
  :root .ph-teachers .a-note { grid-column: 1; grid-row: auto; margin-bottom: 0; }
  :root .ph-courses, :root .ph-teachers, :root .pubhome > section.marked { margin-top: 56px; }
  :root .ph-close { margin-top: 56px; padding: 44px 0; }
  /* the four ways stand two by two once the hero is one column, and one by one on a phone, where the card is the link and its last line goes: four of them stacked with it
     added 180px before the first course (measured 16 Oct 2026) */
  :root .ph-ways { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 620px) {
  /* one to a row on a phone: the portrait small beside the name and the job title, the rest of what their page prints across the card under them */
  :root .ph-person { display: grid; grid-template-columns: 72px minmax(0, 1fr); align-items: center; gap: var(--sp-2) var(--sp-4); padding: var(--sp-5); }
  :root .ph-person:not(:has(.ph-photo)) { grid-template-columns: minmax(0, 1fr); }
  :root .ph-photo { grid-row: 1 / span 2; width: 72px; border-radius: 14px; }
  :root .ph-p-body { display: contents; }
  :root .ph-person b { grid-column: 2; align-self: end; font-size: var(--fs-5); }
  :root .ph-person b + span { grid-column: 2; align-self: start; }
  :root .ph-person:not(:has(.ph-photo)) b, :root .ph-person:not(:has(.ph-photo)) b + span { grid-column: 1; }
  :root .ph-person .ph-p-edu, :root .ph-p-about, :root .ph-p-chips { grid-column: 1 / -1; }
  :root .ph-ways { grid-template-columns: minmax(0, 1fr); }
  :root .ph-ways .ph-way { padding: 12px 20px 16px; }
  :root .ph-ways .ph-wa { display: none; }
}

/* ============================================================================
   THE MODEL BUILDER  ·  lessons of kind "builder"
   ----------------------------------------------------------------------------
   The markup is app/builder.py's SHELL and the behaviour is static/model-builder.js, which is
   synced verbatim from the site (tools/sync_builder.py) and is NOT ours to edit. What is ours is
   this: the site's own stylesheet is not available here and would not belong if it were, so the
   tool is dressed in Academy's tokens instead.

   ⚠ CLASS NAMES ARE PART OF THE CONTRACT, not decoration. The synced script writes markup carrying
   .mb-opt, .mb-warn and .is-done, so renaming any mb-* selector below silently unstyles a control
   rather than failing. The ids are checked against the script by tests/test_builder_lesson.py; the
   classes are not checkable that way, which is why they are called out here.
   ========================================================================== */
.study-builder { padding: var(--sp-5); display: grid; gap: var(--sp-5); }

/* ⚠ min-width: 0 everywhere a grid or flex child holds wide content. A grid item defaults to
   min-width: auto, and `fieldset` additionally carries an intrinsic min-width that ignores its
   container entirely — together they pushed the builder 75px past a 390px viewport. */
.mb { display: grid; gap: var(--sp-5); min-width: 0; }

.mb-form { display: grid; gap: var(--sp-4); margin: 0; min-width: 0; }
.mb-form fieldset {
  border: 1px solid var(--rule);
  border-radius: var(--r-well);
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
  margin: 0;
  min-width: 0;
  background: var(--surface);
}
.mb-form legend {
  font-size: var(--fs-1);
  font-weight: var(--fw-label);
  letter-spacing: var(--ls-caps-sm);
  text-transform: uppercase;
  color: var(--muted);
  padding: 0 var(--sp-2);
}
.mb-opts { display: flex; flex-wrap: wrap; gap: var(--sp-2); }

/* CHIPS, not a questionnaire. The synced script emits
   `<label class="mb-opt"><input type="radio|checkbox"><span>…</span></label>`, so the state lives
   on a native input inside the label and the label is what can be drawn. Before this the rule
   styled the wrapper and left the native control visible, and the product's ONE interactive lesson
   read as a form where the same tool on the website reads as a tool.
   Shaped on the kit's .fchip (pill, --field ground, --brand when on) rather than invented — but
   written against Academy's type scale, because the kit's 12px literal is exactly what
   tests/test_css_tokens.py::test_no_literal_font_size_outside_the_scale refuses here.
   ⚠ THE INPUT IS HIDDEN, NOT REMOVED. `appearance:none` plus zero size keeps it in the tab order,
   keeps the label's click target working, and keeps the accessibility tree intact — a chip built
   from `display:none` is a control a keyboard cannot reach and a screen reader cannot name. */
.mb-opt {
  display: inline-flex;
  align-items: center;
  gap: 0;
  font-size: var(--fs-2);
  font-weight: var(--fw-label);
  line-height: 1.2;
  color: var(--muted);
  background: var(--field);
  border: 1px solid var(--field-line);
  border-radius: 999px;
  padding: var(--sp-2) var(--sp-3);
  cursor: pointer;
  user-select: none;
  transition: background-color var(--t1, .12s) var(--ease, ease),
              color            var(--t1, .12s) var(--ease, ease),
              border-color     var(--t1, .12s) var(--ease, ease);
}
.mb-opt > input {
  appearance: none;
  -webkit-appearance: none;
  width: 0;
  height: 0;
  margin: 0;
  border: 0;
  outline: none;
}
.mb-opt:has(input:checked) {
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
}
.mb-opt:not(:has(input:checked)):hover { background: var(--panel); color: var(--soft); }
/* The ring goes on the LABEL because the input it belongs to has no box to draw one on. */
.mb-opt:has(input:focus-visible) { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .mb-opt { transition-duration: .01ms; }
}
.mb-row { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-3); }
/* ⚠ WITHOUT THIS THE TWO CONDITIONAL ROWS NEVER HIDE. The synced script controls them by the
   `hidden` attribute — `$("#mb-dfrow").hidden = !f.spline` and `$("#mb-noderow").hidden =
   state.re === "none"` (model-builder.js:132-133) — and an AUTHOR `display` rule beats the UA
   stylesheet's `[hidden]{display:none}` whatever their specificity. So `.mb-row{display:flex}`
   silently defeated it: "Spline df" sat under Weibull, which has no spline, and "Nodes" under a
   model with no random effects. The script did its job and the stylesheet undid it, with nothing
   raised anywhere — the failure mode app/builder.py's docstring warns about, arriving through the
   CSS rather than through a renamed id.
   Scoped, not global, for the reason .fb-extra[hidden] gives above: a blanket !important rule would
   reach every component in the app to serve two rows here. .mb-warn needs no such rule — it sets no
   display of its own, so the attribute already works there. */
.mb-row[hidden] { display: none; }
.mb-row label { font-size: var(--fs-3); color: var(--muted); }
.mb-row input {
  font: inherit;
  font-size: var(--fs-3);
  color: var(--ink);
  background: var(--field);
  border: 1px solid var(--field-line);
  border-radius: var(--r-ctl);
  padding: var(--sp-2) var(--sp-3);
}
.mb-row input[type="text"] { flex: 1 1 12em; }
.mb-row input[type="number"] { width: 5em; }
.mb-row input:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 1px; }

/* The three output panels. Two of syntax and one of prose, so the syntax pair sits side by side
   where there is room and the explanation runs full width under them. */
.mb-out { display: grid; gap: var(--sp-4); grid-template-columns: repeat(2, minmax(0, 1fr)); min-width: 0; }
.mb-out > .mb-panel:last-child { grid-column: 1 / -1; }
@media (max-width: 46rem) {
  .mb-out { grid-template-columns: minmax(0, 1fr); }
  .mb-out > .mb-panel:last-child { grid-column: auto; }
}

.mb-panel {
  border: 1px solid var(--rule);
  border-radius: var(--r-well);
  background: var(--surface);
  overflow: hidden;
  min-width: 0;
}
.mb-panel-h {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-4);
  border-bottom: 1px solid var(--rule);
  background: var(--panel);
}
.mb-panel-h h3 {
  margin: 0;
  font-size: var(--fs-1);
  font-weight: var(--fw-label);
  letter-spacing: var(--ls-caps-sm);
  text-transform: uppercase;
  color: var(--muted);
}
.mb-panel pre {
  margin: 0;
  padding: var(--sp-4);
  overflow-x: auto;
  font-family: var(--mono);
  font-size: var(--fs-code);
  line-height: var(--lh-code);
  color: var(--code-ink);
  background: var(--code-bg);
}

.mb-copy {
  font: inherit;
  font-size: var(--fs-1);
  font-weight: var(--fw-ui);
  color: var(--muted);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: var(--r-pill);
  padding: 2px var(--sp-3);
  cursor: pointer;
}
.mb-copy:hover { color: var(--ink); border-color: var(--card-line); }
.mb-copy.is-done { color: var(--ok-ink); border-color: var(--ok-ink); }

.mb-says { padding: var(--sp-4); }
.mb-says h3 {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-1);
  font-weight: var(--fw-label);
  letter-spacing: var(--ls-caps-sm);
  text-transform: uppercase;
  color: var(--muted);
}
.mb-says p { margin: 0; font-size: var(--fs-3); color: var(--ink); max-width: var(--measure-note); }
.mb-checks {
  margin: var(--sp-3) 0 0;
  padding-left: var(--sp-5);
  display: grid;
  gap: var(--sp-2);
  font-size: var(--fs-3);
  color: var(--muted);
  max-width: var(--measure-note);
}

/* The refusal. This is the tool's most valuable output — it fires when a combination is not one
   merlin supports — so it reads as a statement rather than as an error the reader caused. */
.mb-warn {
  margin: 0 var(--sp-4) var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border-left: 3px solid var(--amber);
  border-radius: 0 var(--r-ctl) var(--r-ctl) 0;
  background: var(--panel);
  font-size: var(--fs-3);
  color: var(--amber-ink);
}

.mb-noscript {
  margin: 0;
  padding: var(--sp-4);
  border: 1px solid var(--rule);
  border-radius: var(--r-well);
  background: var(--panel);
  font-size: var(--fs-3);
  color: var(--muted);
  max-width: var(--measure-note);
}

/* The course's own datasets, in the aside beside the software card. A size beside each one because
   that is what decides whether a reader downloads now or later. */
.a-note.ds { display: flex; align-items: baseline; gap: var(--sp-3); flex-wrap: wrap; }
.ds-size {
  font-family: var(--mono);
  font-size: var(--fs-1);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}


/* ── KIT CHROME, RETUNED APP-SIDE ───────────────────────────────────────────────────────────
   Everything below overrides suite-kit.css. It is written HERE and not there on purpose: the kit
   is shared with Lemma and Theorem, and the same edit in that file would silently retheme two
   products nobody is looking at in this session. styles.css loads after the kit, so equal
   specificity wins on source order and none of this needs !important.

   If any of it proves right for the suite, it should be promoted into the kit deliberately, with
   all three apps in front of you. ⚠ Do not assume it is safe there because it is safe here. */

/* The one place in the app where content passes BEHIND chrome, which is the only condition under
   which translucency is information rather than decoration. The kit's bar is fully opaque and
   carries a 6px 18px cast in a third shadow hue (10,12,24). */
.topbar {
  /* The brand's navy, opaque (11 Oct 2026): the frosted-glass bar over a page it was blurring read as a slate grey, not the brand's colour,
     and the brand has no glass. */
  background: var(--nav-ink);
  box-shadow: inset 0 -1px 0 rgb(255 255 255 / .08);
}

/* The kit calls these frosted-glass tiles and ships an 11% white wash with a 1px outline, which is
   the 2015 drawing of the idea. There IS a gradient behind them, so here the blur has something to
   work on. */
.home-hero .statband .stat {
  /* flat, as the brand draws a panel on navy: a hairline and a faint wash, no glass */
  background: rgb(255 255 255 / .07);
  border: 1px solid rgb(255 255 255 / .18);
}

/* ⚠ A 300x300 mark at right:-60px;top:-80px inside overflow:hidden does not read as a watermark —
   it renders as a hard-edged pale slab in the top-right corner, in both themes, and on a 390px
   viewport it overflows by 46px. Blurring it past its own clip edge is what makes it behave like
   the ink wash it was drawn to be. */
.home-hero-wm { filter: blur(8px); }

/* 700 is the heaviest weight in the product and it sits on the most-seen heading in it. This app
   set --fw-head: 620 precisely because 700 reads as unset; the kit's hero never got the memo. */
.home-hero-name { font-weight: var(--fw-head); }

/* ── OPTICAL HAIRLINES ──────────────────────────────────────────────────────────────────────
   1px is TWO device pixels at 2x. Every separator macOS and iOS draw around this app is one
   DEVICE pixel, so the app's rules ship at literally twice the weight of the OS around them —
   which reads as coarse before you can say why. Scoped to the separators that survived the move
   to elevation; a 0.5px line on a control border would disappear rather than lighten. */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
  .masthead-rule, .cc-offer, .card-h, .lesson-row, .rail-row { border-width: .5px; }
}

/* ── A CREDENTIAL, VERIFIED ─────────────────────────────────────────────────────────────────
   The one page in this product opened by somebody who has never heard of us: an employer with a
   link off a CV. It has to read as a record rather than as marketing, so it is a definition list
   on the page ground with no card around it — the facts ARE the page. */
.cred-facts {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--sp-2) var(--sp-6);
  margin: var(--s-rule) 0 var(--s-sec);
  max-width: var(--measure);
}
.cred-facts dt { font-size: var(--fs-3); color: var(--muted); }
.cred-facts dd { margin: 0; font-size: var(--fs-4); color: var(--ink); }
/* The certificate link, one line under the facts it is a copy of (the facts' own bottom margin is pulled back to a gap). */
.cred-pdf { margin: calc(var(--sp-3) - var(--s-sec)) 0 var(--s-sec); }
.cred-pdf .free-cta { margin-top: 0; }
.cred-card .cred-pdf-link { display: block; margin-top: var(--sp-2); }
/* A withdrawn credential still renders — see store.credential_by_public_id. It must be impossible
   to miss and impossible to mistake for a warning about something else, so it takes the failure
   ink rather than the amber that means "not yet". */
.cred-revoked {
  margin: var(--s-rule) 0;
  padding: var(--sp-4) var(--sp-5);
  border-radius: var(--r-well);
  background: color-mix(in srgb, var(--red) 12%, transparent);
  color: var(--fail-ink);
  max-width: var(--measure);
}
.cred-revoked strong { color: var(--fail-ink); }
.cred .cs-block ul { margin: var(--sp-2) 0 0; padding-left: var(--sp-5); }
/* What a credential rests on: one row per practical. The table scrolls inside its own box on a phone
   rather than pushing the page sideways; the seed is tabular so a verifier can read it digit by digit. */
.cred-table { overflow-x: auto; margin-top: var(--sp-3); }
.cred-evidence { width: 100%; border-collapse: collapse; font-size: var(--fs-3); }
.cred-evidence th { text-align: left; font-family: var(--mono); font-size: var(--fs-1);
  font-weight: var(--fw-label); letter-spacing: var(--ls-caps-xs); text-transform: uppercase;
  color: var(--muted); padding: 0 var(--sp-4) var(--sp-2) 0; border-bottom: 1px solid var(--rule);
  white-space: nowrap; }
.cred-evidence td { padding: var(--sp-2) var(--sp-4) var(--sp-2) 0; border-bottom: 1px solid var(--rule);
  vertical-align: top; color: var(--ink); }
.cred-evidence td.num { font-family: var(--mono); font-variant-numeric: tabular-nums; text-align: left; }
/* On a phone the four columns will not fit, and a table that scrolls sideways hides the date. Each
   practical becomes a short block, and every cell but the title says what it is (data-label, set by
   both renderers). */
@media (max-width: 34rem) {
  .cred-evidence thead { display: none; }
  .cred-evidence, .cred-evidence tbody, .cred-evidence tr, .cred-evidence td { display: block; }
  .cred-evidence tr { padding: var(--sp-3) 0; border-bottom: 1px solid var(--rule); }
  .cred-evidence td { border: 0; padding: 0; }
  .cred-evidence td:first-child { font-weight: var(--fw-ui); margin-bottom: var(--sp-1); }
  .cred-evidence td[data-label]::before { content: attr(data-label); margin-right: var(--sp-2);
    font-family: var(--mono); font-size: var(--fs-1); letter-spacing: var(--ls-caps-xs);
    text-transform: uppercase; color: var(--muted); }
}
.cred .cs-block li { font-size: var(--fs-3); color: var(--soft); margin-bottom: var(--sp-1); }

/* --- Data and privacy: the record on the left, the two things you can do on the right ------- */
.privacy-grid { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
  gap: var(--sp-6); align-items: start; margin-top: var(--sp-6); }
@media (max-width: 1000px) { .privacy-grid { grid-template-columns: minmax(0, 1fr); } }
.privacy-main > * + *, .privacy-side > * + * { margin-top: var(--sp-6); }
.privacy-card { padding: var(--sp-6); }
.privacy-card > h2, .privacy-erase > h2 { margin-top: 0; }
/* ONE CARD for the four things on the page (review of 15 Oct 2026, C11): two were cards, one was bare prose and one a flat panel with a border. */
.privacy-prose { max-width: none; }
:root .privacy-erase.card { background: var(--surface); border: 0; border-radius: var(--r-sheet); }
.privacy-block { margin-top: var(--sp-5); padding-top: var(--sp-4); border-top: 1px solid var(--rule); }
.privacy-block h3 { margin: 0 0 var(--sp-2); font-size: var(--fs-4); }
.privacy-list { list-style: none; margin: var(--sp-2) 0 0; padding: 0; }
.privacy-list li { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-3); align-items: baseline;
  padding: var(--sp-2) 0; border-top: 1px solid var(--rule); }
.privacy-list li:first-child { border-top: 0; padding-top: 0; }
.privacy-meta { font-size: var(--fs-2); color: var(--muted); }
.privacy-empty { margin: 0; }
/* the same chip as the operator's "on the course page" — it means the same thing */
.privacy-badge { font-family: var(--mono); font-size: var(--fs-2); letter-spacing: .04em;
  color: var(--surface); background: var(--ok-ink); border-radius: 4px; padding: 1px var(--sp-2); }
.privacy-erase { margin-top: 0; }
/* ⚠ THE FACTS LIST WRAPPED THE EMAIL ONE CHARACTER PER LINE ON A PHONE. `max-content 1fr` gives
   the label column whatever the longest label wants — "Address confirmed by your sign-in provider"
   — which at 375px is nearly everything, and `overflow-wrap: anywhere` then dutifully broke the
   address into a vertical string of letters. Stack the pairs on a narrow screen instead. */
@media (max-width: 600px) {
  .privacy-facts { grid-template-columns: 1fr; row-gap: 0; }
  .privacy-facts dt { margin-top: var(--sp-3); }
  .privacy-facts dt:first-child { margin-top: 0; }
  .privacy-facts dd { margin-bottom: var(--sp-1); }
}

/* an operator list being refilled in place: the old rows stay as the placeholder, dimmed */
.adm-list.is-refilling { opacity: .55; transition: opacity .12s ease; pointer-events: none; }

/* --- self-check quiz, on the study page ------------------------------------------------------
   The multiple-choice options were bare `.btn`s in a display:block box, so four answer sentences
   packed two-to-a-row with ragged wrapping and no separation between one question and the next.
   They are answers to read and choose between, not calls to action — so each is a full-width,
   left-aligned, selectable row, and the questions are ruled apart. */
.study-quiz { padding: var(--sp-6); }
.study-quiz > h3 { margin-top: 0; }

.quiz-q { margin-top: var(--sp-6); padding-top: var(--sp-5); border-top: 1px solid var(--rule); }
/* One question at a time (app.js quizPane): where you are above it, the way on below it. Only one
   .quiz-q is ever visible, so the rule that separated neighbours has nothing to separate. */
.quiz-step { margin: var(--sp-4) 0 0; font-family: var(--mono); font-size: var(--fs-1);
  letter-spacing: var(--ls-caps-xs); text-transform: uppercase; color: var(--muted); }
.quiz-step ~ .quiz-q { margin-top: var(--sp-3); padding-top: 0; border-top: 0; }
.quiz-nav { display: flex; justify-content: space-between; gap: var(--sp-3); margin-top: var(--sp-5);
  padding-top: var(--sp-4); border-top: 1px solid var(--rule); }
.quiz-q:first-of-type { margin-top: var(--sp-4); padding-top: 0; border-top: 0; }
.quiz-stem { margin: 0 0 var(--sp-4); font-weight: var(--fw-ui); line-height: 1.5; }

.quiz-controls { display: flex; flex-direction: column; gap: var(--sp-2); }
/* a numeric question is one field beside a Check button, not a stack of options */
.quiz-controls:has(.quiz-num) { flex-direction: row; flex-wrap: wrap; align-items: center;
  gap: var(--sp-3); }
.quiz-num { flex: 0 1 14rem; }

/* ⚠ DOUBLED CLASS to outrank .btn regardless of sheet order: an option is not a pill CTA. */
.quiz-opt.quiz-opt { display: block; width: 100%; text-align: left; white-space: normal;
  font-weight: 400; line-height: 1.45; padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--rule); border-radius: 10px; background: var(--surface); color: var(--ink); }
.quiz-opt.quiz-opt:hover { transform: none; border-color: var(--accent); background: var(--accent-soft); }
.quiz-opt.quiz-opt:active { transform: none; }

.quiz-feedback { display: grid; gap: 2px; }
.quiz-feedback:not(:empty) { margin-top: var(--sp-4); }
.quiz-right, .quiz-wrong { margin: 0; font-weight: var(--fw-ui); }
.quiz-right { color: var(--ok-ink); }
/* ⚠ AMBER, NOT RED. Red is FAIL/destructive only in this system; a wrong answer in a self-check
   that says "not marked, not recorded" is not a failure. */
.quiz-wrong { color: var(--warn-ink); }
.quiz-why { margin: 0; color: var(--muted); line-height: 1.5; }
/* A code span in quiz text or a practical's answer hint, set exactly as a lesson sets one
   (.lesson-prose code says why .92em is relative and off the scale). app.js codeSpans() makes
   these: neither passes through markup.py, so this is the one piece of markup they get. ONE rule
   for both, so the two plain-text surfaces cannot drift apart. */
.quiz-q code, .ex-hint code { font-family: var(--mono); font-size: .92em;
  background: var(--well); padding: 1px 4px; border-radius: 4px; }

/* THE ACCESS PERIOD. An on-demand enrolment ends on a date, and a learner is told as it nears and
   after it has passed. Amber, not red, for the same reason as .quiz-wrong: this is a state to act on,
   not a failure. Weight, not a box: it sits among the card's other small notes. */
.a-note.access-ending, .a-note.access-ended { color: var(--warn-ink); font-weight: var(--fw-ui); }
.access-ended { max-width: 46rem; }
.access-ended-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 16px; }

/* BOOKINGS (the staff queue for orders and seats). Tokens only, like the rest of the operator screens:
   a definition list for the facts of an order, a row of actions that may carry a field, a plain verdict
   box for the sentence that explains the VAT, and a card for each run in the new-booking form. */
/* The two long dialogs (the selling company and a new booking) scroll INSIDE the card so the Save button
   stays in view; the kit's veil would otherwise scroll the whole card and leave Save a screen away.
   Only these two use the wide modal in Academy. */
.modal.modal-wide { display: flex; flex-direction: column; max-height: calc(100vh - 12vh); }
.modal.modal-wide .modal-b { overflow-y: auto; }
.bk-table { min-width: 42rem; }
.bk-add { display: flex; margin: 4px 0 12px; }
.bk-needs { color: var(--warn-ink); font-weight: var(--fw-ui); }
.bk-order { color: var(--accent); white-space: nowrap; font-weight: var(--fw-ui); text-decoration: none; }
.bk-order:hover { text-decoration: underline; }
.bk-kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 8px 20px;
  margin: 8px 0 24px; font-size: var(--fs-4); }
.bk-kv dt { color: var(--muted); }
.bk-kv dd { margin: 0; overflow-wrap: anywhere; }
.bk-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 8px 0 32px; }
.bk-actions .ff { margin: 0; min-width: 240px; }
.bk-verdict { max-width: 44rem; margin: 8px 0 12px; padding: 12px 16px; border-radius: 10px;
  background: var(--panel); border: 1px solid var(--rule); font-size: var(--fs-4); line-height: 1.5; }
.bk-line { margin: 8px 0; padding: 12px; border-radius: 12px; background: var(--bg);
  border: 1px solid var(--rule); }
.bk-two { display: grid; grid-template-columns: 1fr 1fr; gap: 0 12px; }
.bk-checks { display: grid; gap: 8px; margin: 8px 0 12px; }
@media (max-width: 640px) {
  .bk-two { grid-template-columns: 1fr; }
  .bk-kv { grid-template-columns: 1fr; gap: 0; }
  .bk-kv dt { margin-top: 8px; }
  .bk-actions .ff { min-width: 0; width: 100%; }
}

/* ══ THE COURSE PAGE, 15 Oct 2026 (the owner: "the individual course pages need an overhaul. check alignment, spacing. balance.") ═════════════════════════════════
   Measured on a preview first (the server's page for a stranger, 1440 and 390 wide; a published course and a coming one), then one rule at a time. What the
   measurement found, and what each rule below answers:
   1. THE CURRICULUM'S ROWS WERE TWO LAYOUTS. A lesson that is a link had its numbers 6.8px right of the heading above it (the practical's red edge was hung in the margin
      by moving the whole list 9px left, and the number column was centred), and an announced course's outline, whose rows are not links, had every column style missing
      (`a.lesson-row .nm` and `.dur` named the anchor only): its minutes sat against the title and its titles started at a different x on every row, 7px LEFT of the
      heading. One geometry now for both, with the number's left edge ON the heading's edge, the highlight and the practical's red edge hanging in the margin, and the
      title column starting where the outcomes' text does (--gutter-num).
   2. A DOUBLED RULE. The last outcome's bottom rule and the next heading's top rule were 41px apart with nothing between them.
   3. THE ENROL CARD ON A PHONE AND A TABLET sat below the whole curriculum (18 lessons, 2,100px down at 390): the grid's named areas still spoke of a head and a body that
      the markup stopped having when it became one .course-prose. The prose's children now take part in the grid below 1040px, and the aside is placed after the head
      (title, deck, by-line) and before the body, which is what the old comment above .course-grid promises.
   4. THE STATUS PILL AND THE EYEBROW touched (0px between them). */
:root .lessons { margin-left: 0; }
:root .lesson-row { display: flex; align-items: center; gap: 12px; margin-left: calc(-1 * (3px + var(--sp-2)));
  padding: var(--sp-3) var(--sp-2) var(--sp-3) var(--sp-2); border-left: 3px solid transparent; border-bottom: 1px solid var(--rule); }
:root .lesson-row.is-practical { border-left-color: var(--accent); }
:root .lessons li:last-child .lesson-row { border-bottom: 0; }
:root .lesson-row .ord { flex: none; width: calc(var(--gutter-num) - 12px); text-align: left; font-family: var(--mono); font-size: var(--fs-2); color: var(--muted); }
:root .lesson-row .nm { flex: 1 1 0%; min-width: 0; }
:root .lesson-row .dur { flex: none; margin-left: auto; font-family: var(--mono); font-size: var(--fs-2); color: var(--muted); font-variant-numeric: tabular-nums; }
:root .lesson-row.is-pending { color: var(--muted); }
:root .lesson-row.is-pending .nm { color: var(--soft); }

:root .course-prose > .outcomes + h2 { border-top: 0; }

:root .course-prose > .pill + .eyebrow { margin-top: var(--sp-2); }
:root .course-prose > .pill { display: inline-block; margin-bottom: 0; }

@media (max-width: 1040px) {
  :root .course-grid { grid-template-columns: minmax(0, 1fr); grid-template-areas: none; row-gap: 0; }
  :root .course-prose { display: contents; }
  :root .course-prose > * { order: 2; grid-column: 1; }
  :root .course-prose > :is(.pill, .eyebrow, h1, .a-lede, .byline, .notify-top) { order: 0; }
  :root .course-prose > .pill { justify-self: start; }   /* a grid item stretches; the pill is a pill, not a bar */
  /* "Tell me when it opens" under the title existed because the run card came after the whole curriculum (U4). The card follows the head now, 40px below, so the
     same button twice in one screen is noise: the card's is the one. */
  :root .notify-top { display: none; }
  /* THE ASIDE'S CARDS TAKE PART TOO: the run and enrolment cards (what the page asks of the reader) follow the head; what is secondary (the language, the data) follows
     the whole outline. A signed-in learner's aside is four cards, a stranger's one to three; four cards ahead of "What you will be able to do" was a screen of chrome. */
  :root .course-aside { display: contents; }
  :root .course-aside > * { order: 3; grid-column: 1; margin: var(--sp-6) 0 0; max-width: 26rem; }
  :root .course-aside > :is(.run-card, .is-enrol) { order: 1; margin: 0 0 var(--sp-4); }
  :root .course-aside > :is(.run-card, .is-enrol):first-child { margin-top: var(--s-sec); }
  :root .course-prose > h2:first-of-type { margin-top: var(--s-sec); }
}
