/* ══════════════════════════════════════════════════════════════════════════
   NURSEAL — Study Room (CP3B / S3)

   The token block below is lifted VERBATIM from app/mockups/study-room.html,
   which is the locked look/feel comp (plan section 10, D14). It is promoted out
   of that file's <style> so the lobby, the test surface, and the review screen
   cannot drift from each other or from the comp.

   Zero raw hex outside :root — ENFORCED by `scripts/a0-token-gate.mjs` Gate 4,
   not asserted here. This line claimed it as a bare fact from S3 until
   2026-08-14, during which the count below it went from 0 to 23 and the comment
   stayed exactly as reassuring as the day it was true. An unenforced claim in a
   header is worse than no claim: it answers the question a reader would
   otherwise go and measure.

   WHAT THE COMP DID AND DID NOT SETTLE — worth stating, because it changes how
   this file should be read and edited.

   It settled the LANGUAGE: two type voices (a warm serif for display, a clean
   humanist sans for UI), lavender-tinted neutrals instead of dead grays, warmth
   from colour and space rather than a bubbly font, and one earned motion moment
   (the EKG trace) rather than motion everywhere.

   It did NOT contain a question card, a hint ladder, a too-hard control, or a
   review screen — the comp is the LOBBY. Those three screens are composed here
   for the first time, inside the comp's vocabulary. That is a build, not a
   redesign, but an editor who assumes every rule below was pre-approved by the
   comp will be wrong about which parts are precedent and which are new.

   TWO THINGS FROM THE COMP ARE DELIBERATELY ABSENT, and re-adding either would
   be a regression, not a restoration:
     * the personal cards (daily verse, to-do, workout week) — moved to
       ../genaya-hub/ at D17; the comp's own header says so.
     * the "your Gemini connection lives here" settings button — superseded by
       the managed-access model (platform P5/P6). There is no BYO key on her
       path any more.
   ══════════════════════════════════════════════════════════════════════════ */

:root {
  /* brand palette (plan section 10, locked) */
  --lavender:#b7aed8; --lavender-2:#cdc4ea; --plum:#7a67a4; --plum-ink:#5f5296;
  --blush:#f2a8c1;    --cream:#f8f1e7;      --mint:#a6d8bf;   --mint-ink:#2f7d59;
  --at-risk:#d98324;  /* seal / day — the SAFETY axis, never the network axis (A11/F4) */
  /* ⚠ --cream is a SURFACE and moves with the mode; the mark's belly is the
     LITERAL and does not. They were one token until A5b. */

  /* surfaces & ink — lavender-tinted neutral ramp */
  --bg:#f7f3fd; --bg-2:#efe8fa; --card:#fffdfe; --card-warm:#fbf6ef;
  --ink:#302a41; --ink-2:#514a63; --muted:#6f6785; --line:#eadff7; --line-2:#e0d3f2;

  /* semantic */
  --primary:var(--plum); --primary-ink:var(--plum-ink);
  --ok:var(--mint-ink); --ok-soft:#eaf7f0;   /* was #e7f6ee — 4.48:1 under the 4.5 floor */

  /* Miss/flag colours. Deliberately NOT red: the learning-scientist rule is
     that a bad run never reads as failure, and a wall of red on a review screen
     is the single loudest way to say "you failed" without writing the words.
     A missed question gets a warm, serious plum-brown — legible as "look here"
     rather than as an alarm. Contrast is checked against --card, not --bg. */
  --miss:#8a5a6d; --miss-soft:#fbeef2; --miss-line:#f0d8e0;

  /* Promoted here by appearance/A0 (2026-08-14). Every value below was already
     in this file as a raw literal — 23 of them across 19 rules — so NONE of it
     is a new colour and none of it changes a pixel; `scripts/a0-token-gate.mjs`
     is what proves that rather than this sentence. They are named here because
     a theme is meant to be one token block and no code, and a literal sitting
     in a rule is a colour no theme can reach. */

  /* White. The step-33 comment below used to say "there is no white token to
     point at yet" and name the omission on purpose; this is that token. */
  --white:#fff;

  /* The two radial washes on <body>, over --bg. */
  --wash-1:#efe6fb; --wash-2:#fbeef3;

  /* The panel gradient. .hero and .score carried it as three literals EACH,
     which is precisely how two surfaces meant to match drift apart — so the
     stops are named and the gradient is composed once. */
  --panel-1:#ffffff; --panel-2:#f6efff; --panel-3:#f3e7fb;
  --grad-panel:linear-gradient(160deg, var(--panel-1) 0%, var(--panel-2) 62%, var(--panel-3) 100%);

  /* Warm card edge — the border that pairs with --card-warm (.sample, .tip). */
  --line-warm:#efe2d0;

  /* Answer-option states. */
  --opt-hover:#fdfbff; --opt-on:#f8f4ff;

  /* ── A4 (2026-08-14): the two contrast defects the A1 census found ────────
     Both were in the SHIPPED day theme and predate this lane; both are
     repaired here with MEASURED ratios rather than adjusted by eye.

     `.sample .flag` is the lobby's "Start here" eyebrow — 12px/700 uppercase,
     so the floor is 4.5, not 3. It rendered `--blush` on `--card-warm`:
     1.75:1 in seal/day and 3.16:1 in sapphire/day. ⚠ It is NOT fixed by
     nudging `--blush`, which also paints the mark's cheeks, the footer dot and
     every focus ring — a shared token is the wrong lever for one consumer's
     surface. Its own token, dark in day and the plain blush at night (where
     light-on-dark already measures 9.09 and 7.71).

     `--muted-sunk` is the same shape one layer quieter. `.bd-ask.is-shut` —
     the locked Study Buddy door — puts `--muted` on `--bg-2`, a SUNK panel
     rather than a card, and that surface costs it just enough: 4.45:1, five
     hundredths short. 🔑 A colour is vetted per SURFACE, not per token: the
     same `--muted` on `--card` is comfortably clear, so the repair belongs to
     the one consumer sitting on the tinted panel, not to every quiet label in
     the product. */
  /* ── A5 (2026-08-14): found by a COLD REVIEW, and by nothing we own ───────
     ⛔ `--white` was doing two different jobs under one name, and only one of
     them is genuinely invariant. As a literal white it is correct everywhere
     (the mark's eye glint, his cap). As a SURFACE — the lift under a hovered
     control, the fill of a text field — it is a day value that a night mode
     must move, and it was left out of the varying set on the reasoning that
     "white is white in every theme". True of the colour, false of the role.

     What that shipped, until this fix: eight rules. Five `:hover` states put
     pale night ink (`--primary-ink` #cdbdf5, `--ink-2` #d6cfe8) on literal
     white at the exact moment a pointer is on the control. Three FORM FIELDS
     — the generate page's textarea, its select and its page-range number
     input — put `--ink` (#f4f1fb at night) on literal white, i.e. **white text
     in a white box**, in the default state of the product's primary action.

     🔑 NEITHER CLASS WAS REACHABLE BY THE CONTRAST CENSUS, FOR TWO DIFFERENT
     REASONS, AND BOTH ARE LENS-SHAPED HOLES RATHER THAN OVERSIGHTS. A census
     of a default render cannot enter `:hover`. And it walks elements that own
     TEXT NODES — an empty `<input>` has none, so every form field in the
     product was invisible to it. Gate 8 in a0-token-gate.mjs is the answer to
     both: it resolves foreground and background per cell STATICALLY, so it
     covers states no render reaches and elements that hold no text.

     Both replacements are aliases to tokens that already vary, so the day
     render is byte-identical and no new colour enters the product. */
  /* ── A5b: three more surface-vs-literal collisions, same family ──────────
     Gate 8 (static, per-cell) found these; no render could. Each is one token
     doing a LITERAL job and a SURFACE job under one name, which is invisible
     until a mode moves the surface and leaves the literal behind:

       --cream       the seal's belly (literal) AND .chip-stuck's fill AND the
                     legal/account hero's gradient end (surfaces). At night the
                     sign-in hero faded from dark to CREAM and swallowed its own
                     white heading — a defect visible in a screenshot that had
                     already been rendered, and that nobody had opened.
       --lavender-2  a left border, a text colour, AND the buddy badge's fill.
                     Night lifted --plum-ink to #cdbdf5 and left the badge light:
                     1.04:1, i.e. the "?" was gone.
       --line-2      a border AND the shut badge's fill; 3.73:1 in day.

     The split is always the same shape: the LITERAL keeps the old name and the
     SURFACE gets its own mode-varying token. --mark-belly now carries the
     cream literal directly, so the mark is untouched in every cell. */
  --badge-soft:var(--wash-1);  /* .bd-ico — 5.55:1 with --plum-ink */
  --field:var(--white);      /* the fill of a text field, select, number input */
  --raised:var(--white);     /* the lift under a hovered control */

  --flag-ink:#a84463;      /* 5.32:1 on --card-warm */
  --muted-sunk:#665e7c;    /* 5.09:1 on --bg-2 */

  /* The theme picker's two swatches (A3-T). Each is the OTHER profile's accent
     AS RENDERED IN THE CURRENT MODE, so the chip stays legible on the button's
     own ground — a swatch is a measurement of a theme, not a decoration, and
     one that shows a day accent on a night button is neither. */
  --swatch-seal:#5f5296; --swatch-sapphire:#2b5390;

  /* ── Promoted by appearance/A1 (2026-08-14). ──────────────────────────────
     A0 closed with "zero raw hex outside :root" and that claim was narrower
     than it read: it counted `#rrggbb` and never counted `rgba()`. Seven
     rgba() literals sat in rules in this file, and one of them —
     `.btn-ghost { background: rgba(255,255,255,.7) }` — is a near-white pill
     that is illegible the moment the ground goes dark. A census keyed to one
     colour SYNTAX is not a colour census. Gate 4 now sweeps rgb/rgba/hsl/hwb/
     lab/lch/oklab/oklch/color() as well, and names the syntaxes it covers in
     its own output. */

  /* The translucent pill four controls share (.btn-ghost, .seg, .file-btn,
     .fl-btn). One name, because four copies of one wash is how a mode gets
     three of them right. */
  --glass:rgba(255,255,255,.7);
  /* The drop-target scrim (.drop-wrap.is-over::after) — near-opaque, so it
     needs its own value rather than a heavier --glass. */
  --glass-strong:rgba(247,243,253,.92);
  /* The MODAL backdrop (.sp::backdrop). ⚠ Deliberately NOT --glass-strong,
     whose name says strong and whose job is a near-opaque drop target: this one
     darkens the page it covers rather than washing over it, and the two would
     drift apart the first time either is tuned.
     ⚠ It is defined in the two SCHEME blocks and not in the two theme blocks,
     which is a design call rather than an omission: a scrim's job is to
     suppress the page, not to express the theme, so sapphire inherits it. The
     scheme block is MORE specific than bare :root, so night wins in sapphire
     too — the specificity trap A1 documented, working in our favour here.
     ⚠ The day value matches nurseal-feedback.js's shipped ::backdrop exactly,
     because both modals open on this same page and two scrims one click apart
     must not be two different greys. (That module hardcodes it and does NOT
     vary by scheme — noted, not fixed here; it is a different file's batch.) */
  --scrim:rgba(48,42,65,.42);
  --scroll-shade:rgba(48,42,65,.20);
  --shadow-btn:0 6px 18px rgba(122,103,164,.34);
  --shadow-btn-hover:0 8px 22px rgba(95,82,150,.42);

  /* Ink that sits ON --primary (filled buttons, the on-segment, the NEW pill).
     It was `--white` in every consumer, which is correct in day and wrong at
     night: night lifts --primary to a light lavender, and white-on-light is
     ~2.2:1. A semantic name is what lets one block fix all three. Day value is
     an alias, so nothing moves. */
  --on-primary:var(--white);

  /* Native form controls, scrollbars and the UA canvas. A token because the
     answer differs per mode and `color-scheme` is not inheritable from a
     token-less rule. */
  --color-scheme:light;

  /* ── The brand mark ───────────────────────────────────────────────────────
     The largest unreachable colour surface in this product was never in a
     stylesheet: the seal is INLINE SVG in the markup, 18 colours repeated on
     six pages with no build step, so no token, theme or mode could reach one
     of them and the mark rendered as a bright near-white plate on every night
     board. These are those colours, and `scripts/a1-sync.mjs` is what keeps
     the markup pointing at them on all twelve pages.

     Two are ALIASES, not copies: the seal's body already IS --lavender and its
     belly already IS --cream, to the digit. Aliasing means a theme re-hues the
     animal for free; pasting the resolved hex would have forked it on the next
     palette tune. */
  --mark-ring-1:#b89ce0; --mark-ring-2:#7b6ab8;   /* badge ring gradient */
  --mark-plate:#fbf9ff;                            /* the disc behind him */
  --mark-flipper:#b0a7d6;
  --mark-body:var(--lavender);
  --mark-belly:#f8f1e7;
  --mark-cord:#4a3663;                             /* stethoscope tubing */
  --mark-chrome:#d8d8e4; --mark-chrome-line:#7d7d92; --mark-chrome-lit:#ececf3;
  --mark-eye:#40395e; --mark-glint:var(--white);
  --mark-cheek:#f2bcd3;                            /* near --blush, NOT equal — so its own token */
  --mark-muzzle:#7a5570; --mark-smile:#5f4a63;
  --mark-cap:var(--white); --mark-cap-line:#e7ddf3; --mark-cross:#ef9dbe;
  /* The greyed-out variant in the empty-state (.seal-sm). ⚠ It needs its OWN
     belly, added 2026-08-14 after looking at the night render: the shared
     --mark-belly is a bright cream that sits correctly on the day variant's
     light body and, once night darkened the body underneath it, became a
     white blob on a dark disc — the seal stopped reading as a seal. A quiet
     variant has to stay quiet in BOTH directions, and "quiet" on a dark
     ground is a different value, not the same one. Day is an alias, so
     nothing moves. */
  --mark-mute-plate:#f3edfb; --mark-mute-line:#e4d8f2; --mark-mute-body:#c8bfe6;
  --mark-mute-belly:var(--mark-belly);

  /* scale (4/8pt) */
  --s1:6px; --s2:10px; --s3:16px; --s4:24px; --s5:36px; --s6:56px;
  --r-card:20px; --r-ctl:13px; --r-pill:999px;
  --shadow-1:0 2px 8px rgba(95,82,150,.06);
  --shadow-2:0 10px 30px rgba(95,82,150,.12);
  --shadow-lift:0 18px 44px rgba(95,82,150,.18);

  --serif:ui-serif, Georgia, Cambria, "Times New Roman", serif;
  --sans:-apple-system, "Segoe UI", system-ui, "Helvetica Neue", Arial, sans-serif;
  --mono:ui-monospace, "SFMono-Regular", "Cascadia Code", Consolas, monospace;
}

/* ══════════════════════════════════════════════════════════════════════════
   THE TWO AXES — appearance A1 (mode) and A2 (theme), 2026-08-14.

   `data-scheme` = day | night   ·   `data-theme` = seal | sapphire
   ⚠ The mode axis is NOT called data-mode: that attribute is already taken
   three times over in this app, including by a study-mode radio at
   study/history.html:166.

   THE SPECIFICITY ARITHMETIC IS THE WHOLE DESIGN, so it is written down rather
   than trusted:

     :root                                                (0,1,0)  seal · day
     :root[data-scheme="night"]                           (0,2,0)  seal · night
     :root[data-theme="sapphire"][data-scheme="day"]      (0,3,0)  sapphire · day
     :root[data-theme="sapphire"][data-scheme="night"]    (0,3,0)  sapphire · night

   Every non-default block names BOTH axes, or is the mode block that only has
   one axis to name. Nothing ties, so NOTHING DEPENDS ON SOURCE ORDER. The
   version that does tie is the natural one to write — `:root[data-theme=x]`
   alone is (0,2,0) and ties with the night block, so a sapphire user at night
   silently gets sapphire's DAY tokens for exactly the tokens sapphire happens
   to define. `scripts/a1-token-gate.mjs` asserts the arithmetic above.

   ⛔ AND THE DEFAULT DELIBERATELY OWNS NO BLOCK — it IS `:root`. An unknown,
   unwritable or absent stored value therefore lands on a complete day palette
   by construction rather than by a fallback someone has to maintain. With
   JavaScript off, neither attribute exists and the product is exactly what it
   was before this lane opened.

   ⛔ `prefers-color-scheme` IS RESOLVED TO AN ATTRIBUTE IN THE PRE-PAINT SEED,
   NOT LEFT AS A MEDIA QUERY. A bare `@media (prefers-color-scheme: dark) {
   :root { … } }` is (0,1,0) — it loses to every theme block above, so a
   dark-OS user with no stored preference would get the theme's DAY tokens on
   a dark expectation. A media query cannot name the other axis, because there
   is no attribute for it to name; the seed is what gives it one.

   ⛔ ONLY CUSTOM PROPERTIES BELOW THIS LINE. A non-custom declaration in a
   theme block would change what the day render resolves to and would be
   invisible to the A0 equivalence proof. Gate 6 asserts it.

   A token that does NOT vary by mode or theme has ONE name and appears only in
   `:root` above — the mark's cheeks, muzzle, cap and chrome all sit on the
   seal's own body, which is itself an alias of `--lavender`, so a theme
   re-hues the animal and leaves his face alone. Gate 5 asserts the inverse:
   any token answered by ONE non-default block must be answered by ALL of
   them, because a partial answer is a day value on a night ground.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── seal · night — "Ink", picked by Sami 2026-08-14 from the A1 board ─────
   The deepest of the three arms: a near-black room for a 2am library, with
   the cards lifted well clear of it. A mode changes ground and elevation and
   NEVER re-hues the brand — that line is what let the A2 theme board vary hue
   against a settled mode instead of confounding the two. */
:root[data-scheme="night"] {
  --lavender:#b7aed8; --lavender-2:#cdc4ea; --plum:#b3a0e2; --plum-ink:#cdbdf5;
  --blush:#f2a8c1; --cream:#241e2a; --mint:#a6d8bf; --mint-ink:#86dab0;
  --at-risk:#f0a742;  /* seal / night */
  --mark-belly:#f8f1e7;
  --bg:#0d0b13; --bg-2:#12101a; --card:#1a1724; --card-warm:#261b26;
  /* ⛔ `--card-warm` WAS #1d1a25 UNTIL 2026-08-25 AND IT WAS NOT A BAND.
     A delta of (3,3,1) from --card — ΔE76 2.13, under the ~2.3 JND — so
     the chart's client header, its tab strip and the 21 other surfaces
     this token grounds all collapsed into one dark plane at night, with
     only the 1px --line rules surviving. Day ships 4.84 and reads fine;
     the night value is now 5.02, i.e. the same band, not a louder one.
     ⚠ IT IS LIGHTER *AND* WARMER THAN --card, and both halves are load-
     bearing: on a dark surface a hue-only shift at this L* is nearly
     invisible, and a lift-only one makes a token called --card-warm not
     warm. L*+2.87, warmth (a*+b*) +5.81.
     ⚖ THE DIRECTION IS THE ONE THAT WAS ALREADY SHIPPING — the old value
     was also lighter than --card, just barely. Deepening the existing
     relationship rather than inverting it keeps the band clear of --bg
     (ΔE76 10.09) instead of pushing it toward the page ground.
     Graded by n10-chart-browser.mjs §B, in all four cells. */
  --ink:#f4f1fb; --ink-2:#d6cfe8; --muted:#aba2c4; --line:#272235; --line-2:#322c42;
  --ok-soft:#16291f; --miss:#ecaabf; --miss-soft:#2a1a22; --miss-line:#402833;
  --wash-1:#171229; --wash-2:#221421;
  --panel-1:#1d1a29; --panel-2:#191624; --panel-3:#15121f;
  --line-warm:#322b3a; --opt-hover:#201c2c; --opt-on:#282237;
  --shadow-1:0 2px 8px rgba(0,0,0,.5);
  --shadow-2:0 10px 30px rgba(0,0,0,.6);
  --shadow-lift:0 18px 44px rgba(0,0,0,.7);

  /* Elevation on dark is a LIGHTER surface, not a bigger shadow — so the
     shared pill lifts off the ground instead of laying a white film over it.
     rgba(255,255,255,.7) here would be the near-white button the night board
     photographed and this rung exists to kill. */
  --glass:rgba(255,255,255,.06);
  --glass-strong:rgba(19,16,27,.94);
  /* Darker AND heavier than day, because a 42% grey wash over an already-dark
     page barely separates the dialog from what is behind it — the same reason
     --glass had to stop being rgba(255,255,255,.7) at night. */
  --scrim:rgba(4,3,8,.66);
  --scroll-shade:rgba(0,0,0,.72);
  --shadow-btn:0 6px 18px rgba(0,0,0,.55);
  --shadow-btn-hover:0 8px 22px rgba(0,0,0,.65);
  /* Night lifts --primary to a light lavender, so the ink on top must go dark.
     White here measures ~2.2:1 — the defect --on-primary exists to make
     fixable in one line instead of three. */
  --on-primary:#1a1724;
  --color-scheme:dark;

  --badge-soft:var(--opt-on);  /* 8.87:1 with --plum-ink */
  --field:var(--panel-3); --raised:var(--opt-on);
  --flag-ink:var(--blush);   /* 9.09:1 on --card-warm — light on dark already clears it */
  --muted-sunk:var(--muted); /* 7.80:1 on --bg-2 */
  --swatch-seal:#cdbdf5; --swatch-sapphire:#9dbdea;

  --mark-ring-1:#b3a0e2; --mark-ring-2:#6f5fa6;
  --mark-plate:#1e1a2b; --mark-flipper:#6f6396;
  --mark-mute-plate:#221d33; --mark-mute-line:#322c42;
  --mark-mute-body:#6f6890; --mark-mute-belly:#b8b1c9;
}

/* ── sapphire · day — profile two, ruled by Sami 2026-08-14 ────────────────
   Deep cobalt on slate-blue neutrals. Blue is the only one of the three
   candidate hues with no other job in this product: emerald landed on
   --mint-ink (how a question says "you got this right", ΔE 1.66 at night) and
   ruby landed on --miss (the deliberate not-red). Neither collision is
   visible on the lobby, which is why the board rendered the review screen.
   ⚠ --mint-ink and --miss keep their HUE in every theme on purpose: they
   carry meaning, and re-hueing them to match a skin is how the meaning goes. */
:root[data-theme="sapphire"][data-scheme="day"] {
  --lavender:#a7c0e2; --lavender-2:#c6d8f0; --plum:#3d6bae; --plum-ink:#2b5390;
  --blush:#c9743f; --cream:#f4efe6; --mint:#93cfb4; --mint-ink:#237a57;
  --at-risk:#b8471f;  /* sapphire / day — blush is ORANGE in this theme, so at-risk goes RED-ward to stay separable */
  --mark-belly:#f4efe6;
  --bg:#f3f6fd; --bg-2:#e8eefa; --card:#fdfeff; --card-warm:#f7f4ee;
  --ink:#26303f; --ink-2:#465264; --muted:#5c6879; --line:#dde7f5; --line-2:#cfdcef;
  --ok-soft:#e4f4ec; --miss:#8a5f6a; --miss-soft:#f8eef0; --miss-line:#eed7dc;
  --wash-1:#e6eefb; --wash-2:#f7eee8;
  --panel-1:#ffffff; --panel-2:#eff4fd; --panel-3:#e7effa;
  --line-warm:#e8dcc9; --opt-hover:#fbfdff; --opt-on:#f2f7ff;
  --shadow-1:0 2px 8px rgba(43,83,144,.07);
  --shadow-2:0 10px 30px rgba(43,83,144,.13);
  --shadow-lift:0 18px 44px rgba(43,83,144,.19);

  --glass:rgba(255,255,255,.7);
  --glass-strong:rgba(243,246,253,.94);
  /* ⚠ IDENTICAL to seal/day ON PURPOSE, and stated rather than inherited.
     My first pass defined --scrim only in the two SCHEME blocks and let
     sapphire fall through; Gate 5 refused it, and Gate 5 is right. Relying on
     the cascade means the four cells are only knowable by reasoning about
     specificity — which is the exact trap A1 documented when a bare
     prefers-color-scheme block lost to every [data-theme] rule. Every cell
     answers every token, even when the answer is the same one. */
  --scrim:rgba(48,42,65,.42);
  --scroll-shade:rgba(43,83,144,.20);
  --shadow-btn:0 6px 18px rgba(43,83,144,.30);
  --shadow-btn-hover:0 8px 22px rgba(43,83,144,.38);
  --on-primary:#ffffff;
  --color-scheme:light;

  --badge-soft:var(--wash-1);  /* 6.56:1 with --plum-ink */
  --field:var(--white); --raised:var(--white);
  --flag-ink:#985018;        /* 5.47:1 on --card-warm */
  --muted-sunk:#566270;      /* 5.34:1 on --bg-2 */
  --swatch-seal:#5f5296; --swatch-sapphire:#2b5390;

  --mark-ring-1:#7ba4dd; --mark-ring-2:#2b5390;
  --mark-plate:#f7fafe; --mark-flipper:#93a9c9;
  --mark-mute-plate:#eef3fb; --mark-mute-line:#d8e4f3;
  --mark-mute-body:#bfcfe6; --mark-mute-belly:var(--mark-belly);
}

/* ── sapphire · night ─────────────────────────────────────────────────────── */
:root[data-theme="sapphire"][data-scheme="night"] {
  --lavender:#a7c0e2; --lavender-2:#c6d8f0; --plum:#7ba4dd; --plum-ink:#9dbdea;
  --blush:#e59f78; --cream:#1e1a16; --mint:#93cfb4; --mint-ink:#79cfa9;
  --at-risk:#f2653f;  /* sapphire / night — same reason */
  --mark-belly:#f4efe6;
  --bg:#080b12; --bg-2:#0d1219; --card:#151b25; --card-warm:#221f28;
  /* ⛔ SAME DEFECT, SAME DAY, AND THE FILING RUNG HAD ONLY MEASURED THE
     SEAL CELL. #181d24 was ΔE76 2.48 from --card — the note that opened
     this quoted seal's (3,3,1) and said 'dark mode', which reads as one
     cell and was true of two. Sweeping all four is what found it, and it
     is theme-warden's standing rule arriving on its own charter: a
     visual property verified in the configuration the harness happened
     to be pointed at gets generalised silently.
     Now ΔE76 4.89, L*+2.82, warmth +5.54, ΔE76 10.02 clear of --bg. */
  --ink:#eef3fa; --ink-2:#c9d4e2; --muted:#98a6b8; --line:#222b38; --line-2:#2c3745;
  --ok-soft:#12291f; --miss:#e0a3b0; --miss-soft:#26191d; --miss-line:#3b272d;
  --wash-1:#101a2b; --wash-2:#1d1712;
  --panel-1:#18202b; --panel-2:#141a24; --panel-3:#10151d;
  --line-warm:#2c2a26; --opt-hover:#1a212c; --opt-on:#202938;
  --shadow-1:0 2px 8px rgba(0,0,0,.5);
  --shadow-2:0 10px 30px rgba(0,0,0,.6);
  --shadow-lift:0 18px 44px rgba(0,0,0,.7);

  --glass:rgba(255,255,255,.06);
  --glass-strong:rgba(13,18,25,.94);
  /* Identical to seal/night, explicitly — see the seal/day note. */
  --scrim:rgba(4,3,8,.66);
  --scroll-shade:rgba(0,0,0,.72);
  --shadow-btn:0 6px 18px rgba(0,0,0,.55);
  --shadow-btn-hover:0 8px 22px rgba(0,0,0,.65);
  --on-primary:#0d1219;
  --color-scheme:dark;

  --badge-soft:var(--opt-on);  /* 7.59:1 with --plum-ink */
  --field:var(--panel-3); --raised:var(--opt-on);
  --flag-ink:var(--blush);   /* 7.71:1 on --card-warm */
  --muted-sunk:var(--muted); /* 7.59:1 on --bg-2 */
  --swatch-seal:#cdbdf5; --swatch-sapphire:#9dbdea;

  --mark-ring-1:#7ba4dd; --mark-ring-2:#4a6f9e;
  --mark-plate:#18202b; --mark-flipper:#5b7192;
  --mark-mute-plate:#1a2230; --mark-mute-line:#263040;
  --mark-mute-body:#5b7192; --mark-mute-belly:#aebccd;
}

* { box-sizing:border-box; }

/* The `hidden` ATTRIBUTE is only the UA stylesheet's display:none, so ANY
   class that sets a display silently outranks it. Found the way it should be —
   the first real look at generate.html showed the sign-in gate, the form AND
   the two-door card stacked on top of each other, because .gen-form and .doors
   are both display:grid. Nothing shipped before CP3D was affected (.set-note
   and .confirm-limit set no display), so this is a fix, not a regression —
   but it is written once here rather than as a display:none per component, so
   the next thing that sets a display and toggles `hidden` inherits the answer
   instead of rediscovering the bug. */
[hidden] { display:none !important; }

/* `color-scheme` is what makes the native half of the page follow the mode —
   scrollbars, focus rings the UA draws, date/file pickers, the canvas behind a
   short page. This file never declared it at all, which meant that on a
   dark-OS machine the UA already rendered those light-page controls dark. */
html { color-scheme:var(--color-scheme); -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility; }
body {
  margin:0; font-family:var(--sans); color:var(--ink); line-height:1.55;
  background:
    radial-gradient(900px 420px at 15% -8%, var(--wash-1) 0%, transparent 60%),
    radial-gradient(760px 380px at 100% 0%, var(--wash-2) 0%, transparent 55%),
    var(--bg);
  min-height:100vh;
}
.page { max-width:860px; margin:0 auto; padding:22px 20px 72px; }

/* ── focus: one ring, everywhere ───────────────────────────────────────────
   outline rather than box-shadow, because box-shadow disappears under
   forced-colors/High-Contrast and takes the only focus indicator with it.
   The two-layer idiom keeps mouse clicks from painting a ring while never
   removing it from the keyboard. */
:focus-visible {
  outline:3px solid var(--plum-ink);
  outline-offset:2px;
  border-radius:4px;
}
:focus:not(:focus-visible) { outline:none; }

.visually-hidden {
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
}

/* Skip link — the first Tab stop on a page she uses for hours. */
.skip {
  position:absolute; left:-9999px; top:var(--s2);
  background:var(--card); color:var(--plum-ink); font-weight:600;
  padding:10px 16px; border-radius:var(--r-ctl); border:1px solid var(--line-2);
  z-index:20;
}
.skip:focus { left:var(--s3); }

/* ── top bar ───────────────────────────────────────────────────────────── */
/* ⚠ `flex-wrap` is NOT tidiness — it is a fix, and the way it was found is the
   point. The topbar carried two nav links on every page for months and fitted a
   390px screen exactly. Adding a third ("Your tests", 2026-08-11) pushed
   level.html 23px past the viewport, and NOTHING on that page changed: the
   defect was a shared component that had no answer for a third child, sitting
   one link below its limit the whole time. Caught by cp3c-t4-browser's overflow
   check on a page this batch never edited — which is the argument for running
   every suite rather than the ones that look related. */
.topbar {
  display:flex; align-items:center; justify-content:space-between;
  flex-wrap:wrap; gap:var(--s3); margin-bottom:var(--s5);
}
.mark { display:flex; align-items:center; gap:11px; text-decoration:none; }
.mark svg { width:46px; height:46px; display:block; }
.mark .wm { font-family:var(--sans); font-weight:700; letter-spacing:3px; font-size:16px; color:var(--plum-ink); }
.topnav { display:flex; flex-wrap:wrap; gap:var(--s2); align-items:center; }
.topnav a {
  min-height:44px; display:inline-flex; align-items:center; padding:0 16px;
  border:1px solid var(--line-2); background:var(--card); border-radius:var(--r-ctl);
  color:var(--ink-2); font-size:14px; font-weight:600; text-decoration:none;
  box-shadow:var(--shadow-1); transition:border-color .15s;
}
.topnav a:hover { border-color:var(--lavender); }
/* The right-hand group. It exists so the day/night control can sit beside the
   nav without becoming part of it: `.topnav` is a <nav> labelled "Leave the
   test" on take.html, and a display setting announced inside that landmark is
   simply in the wrong room. .topbar is space-between, so a third direct child
   would spread the three apart instead of pinning two to the right. */
.topbar-end { display:flex; align-items:center; flex-wrap:wrap; gap:var(--s2); }

/* ── the day/night control (A3-C: the topbar of all twelve pages) ───────────
   ⛔ HIDDEN UNTIL THE SEED HAS RUN. `data-scheme` is written by the pre-paint
   script, so its PRESENCE is the proof that JavaScript is available to answer
   a click. Rendering the button unconditionally would ship a control that does
   nothing on a page with scripting off — worse than no control at all.
   The accessible name is a real (visually hidden) string rather than an
   aria-label so voice control can match what it says, and it stays "Night
   mode" in both states: `aria-pressed` carries on/off. A name that changes on
   activation is the classic icon-toggle trap — the user hears the label move
   under them and cannot tell what they just did. */
.scheme-toggle { display:none; }
:root[data-scheme] .scheme-toggle {
  display:inline-flex; align-items:center; justify-content:center;
  -webkit-appearance:none; appearance:none;
  width:44px; height:44px; min-height:44px; padding:0;
  border:1px solid var(--line-2); border-radius:var(--r-ctl);
  background:var(--card); color:var(--ink-2);
  box-shadow:var(--shadow-1); cursor:pointer;
  transition:border-color .15s ease, color .15s ease;
}
.scheme-toggle:hover { border-color:var(--lavender); color:var(--plum-ink); }
.scheme-toggle:focus-visible { outline:3px solid var(--plum-ink); outline-offset:2px; }
.scheme-toggle svg {
  width:20px; height:20px; display:block;
  fill:none; stroke:currentColor; stroke-width:1.8;
  stroke-linecap:round; stroke-linejoin:round;
}
.scheme-toggle .ic-sun { display:none; }
:root[data-scheme="night"] .scheme-toggle .ic-sun { display:block; }
:root[data-scheme="night"] .scheme-toggle .ic-moon { display:none; }

/* ── the theme picker (A3-T) ────────────────────────────────────────────────
   Two profiles, so the picker is a TOGGLE with the same contract as its
   neighbour rather than a menu: stable accessible name ("Sapphire theme"),
   `aria-pressed` carrying which profile is live, and the swatch showing the
   profile you would switch TO — exactly the sun/moon logic, so the two
   controls cannot teach a user two different rules. ⚠ When a third profile
   arrives this becomes a real picker and A3 owns that; a two-item menu would
   be more machinery than the choice contains. */
.theme-toggle { display:none; }
:root[data-scheme] .theme-toggle {
  display:inline-flex; align-items:center; justify-content:center;
  -webkit-appearance:none; appearance:none;
  width:44px; height:44px; min-height:44px; padding:0;
  border:1px solid var(--line-2); border-radius:var(--r-ctl);
  background:var(--card); box-shadow:var(--shadow-1); cursor:pointer;
  transition:border-color .15s ease;
}
.theme-toggle:hover { border-color:var(--lavender); }
.theme-toggle:focus-visible { outline:3px solid var(--plum-ink); outline-offset:2px; }
.theme-toggle .sw {
  width:18px; height:18px; border-radius:var(--r-pill); display:block;
  border:1.5px solid var(--line-2);
}
.theme-toggle .sw-sapphire { background:var(--swatch-sapphire); }
.theme-toggle .sw-seal { background:var(--swatch-seal); display:none; }
:root[data-theme="sapphire"] .theme-toggle .sw-seal { display:block; }
:root[data-theme="sapphire"] .theme-toggle .sw-sapphire { display:none; }

/* The appearance pair reserves both 44px slots at rest. One shared hover
   ancestor covers the controls and their gap, so the swatch remains reachable
   while the pointer travels from the day/night button. */
.appearance { position:relative; display:inline-flex; align-items:center; gap:var(--s2); }
.appearance .theme-toggle {
  opacity:0; pointer-events:none;
  transform:translateX(-4px) scale(.94);
  transition:opacity .16s ease, transform .16s ease;
}
.appearance:hover .theme-toggle,
.appearance[data-reveal] .theme-toggle,
.appearance .theme-toggle:focus-visible {
  opacity:1; pointer-events:auto; transform:none;
}
@media (prefers-reduced-motion: reduce) {
  .appearance .theme-toggle { transition:none; transform:none; }
}

/* ── the reserved swatch slot, unreserved on a touch viewport ────────────────
 * ⛔ THE SLOT COSTS A NAV LINE WHERE IT BUYS NOTHING (rung 102). `.appearance
 * .theme-toggle` is laid out at `opacity:0`, so it holds its 41px whether or
 * not it is revealed. MEASURED on this tree, not inferred: the topbar is 76px
 * at 320px AND at 360px against 54px at 390px — 22px, one extra line of nav —
 * and the 320px horizontal overflow goes with it (scrollWidth 338 -> 320),
 * which the rung had recorded as pre-existing and untouched by this.
 * ⚖ THE SLOT IS NOT WRONG, IT IS WRONG *HERE*. It exists because a width or
 * display change moves the day/night button out from under the cursor
 * mid-reveal, dropping the hover and starting a flicker loop. That failure
 * needs a POINTER, so the query asks for the capability the argument rests on
 * — no hover, a coarse pointer — and NOT for a width that merely correlates
 * with it. A narrow DESKTOP window is 320px with a mouse and keeps the slot.
 * ⛔ AND IT TAKES THE TOGGLE OUT OF FLOW RATHER THAN SETTING `display:none`,
 * WHICH THE FIRST VERSION DID. `display` is the property the reveal rules
 * themselves switch (`.theme-toggle` is `display:none` until
 * `:root[data-scheme]` turns it on), so overriding it here put this block in
 * the middle of the reveal mechanism and made "is the control still reachable
 * on a phone?" a question that had to be answered by probe. It could not be:
 * the probe never actually fired the page's own handler — `data-scheme` never
 * flipped — so it was reporting on hover, not on reveal, in BOTH directions.
 * 🔑 Out-of-flow changes only FLOW PARTICIPATION. `.appearance` is already
 * `position:relative`, so the toggle keeps its `display`, its `opacity`, its
 * `pointer-events` and every reveal selector untouched, and stops reserving
 * width. The reachability question does not need a probe because the mechanism
 * is not in the change.
 * ⛔ DO NOT "fix" the nav wrap by shortening the label instead: the label is
 * shared by 14 pages and the wrap is the symptom, not the cause. */
@media (max-width: 360px) and (hover: none) and (pointer: coarse) {
  /* ⚠ COLLAPSED IN PLACE, NOT MOVED OUT OF THE BOX — and both alternatives
   * were measured before this one was kept. `position:absolute; left:100%`
   * pins it past the container and REINTRODUCES horizontal overflow
   * (scrollWidth 320 -> 359 at 320px), which is the very thing this block also
   * fixes; `right:100%` parks it on the far side, so revealing it makes the
   * swatch jump the width of the bar under her thumb. Collapsing to zero width
   * keeps it in flow, reserves nothing, overflows nothing, and grows back
   * exactly where it already sat.
   * ⚠ The row's `gap` has to go with it — a zero-width child still gets its
   * gap, which is most of what the slot was costing. */
  .appearance { gap: 0; }
  :root[data-scheme] .appearance .theme-toggle {
    width: 0; min-width: 0; padding: 0; border-width: 0; overflow: hidden;
  }
  :root[data-scheme] .appearance:hover .theme-toggle,
  :root[data-scheme] .appearance[data-reveal] .theme-toggle,
  :root[data-scheme] .appearance .theme-toggle:focus-visible {
    width: 44px; min-width: 44px; border-width: 1px; margin-left: var(--s2); overflow: visible;
  }
}


/* ── greeting ──────────────────────────────────────────────────────────── */
.greeting { margin:0 0 var(--s4); }
.greeting h1 {
  font-family:var(--serif); font-weight:600; font-size:32px; line-height:1.12; margin:0 0 4px;
  color:var(--ink); letter-spacing:.2px;
}
.greeting p { margin:0; color:var(--muted); font-size:15px; }

/* ── hero ──────────────────────────────────────────────────────────────── */
.hero {
  position:relative; overflow:hidden; border-radius:var(--r-card);
  padding:var(--s5) var(--s5) var(--s4);
  background:var(--grad-panel);
  border:1px solid var(--line-2); box-shadow:var(--shadow-2); margin-bottom:var(--s3);
}
.hero .eyebrow { font-size:12px; font-weight:600; letter-spacing:2px; text-transform:uppercase; color:var(--plum-ink); margin:0 0 var(--s2); }
.hero h2 { font-family:var(--serif); font-weight:600; font-size:27px; line-height:1.15; margin:0 0 var(--s1); color:var(--ink); max-width:22ch; }
.hero p.sub { margin:0 0 var(--s4); color:var(--ink-2); font-size:15px; max-width:46ch; }
.cta-row { display:flex; flex-wrap:wrap; gap:var(--s2); align-items:center; }
.hero-seal { position:absolute; right:22px; top:20px; width:74px; height:74px; opacity:.96; }

/* EKG signature line — the ONE earned motion moment, kept from the comp. */
.ekg { position:absolute; right:-6px; bottom:14px; width:230px; height:66px; opacity:.55; pointer-events:none; }
.ekg path {
  stroke:var(--lavender); stroke-width:2.4; fill:none; stroke-linecap:round; stroke-linejoin:round;
  stroke-dasharray:520; stroke-dashoffset:520; animation:trace 3.4s ease-in-out .3s forwards;
}
.ekg .beat { stroke:var(--blush); }
@keyframes trace { to { stroke-dashoffset:0; } }

/* ── buttons ───────────────────────────────────────────────────────────── */
.btn {
  font-family:var(--sans); font-size:15px; font-weight:600; cursor:pointer;
  border:1.5px solid transparent; border-radius:var(--r-ctl);
  padding:13px 22px; min-height:48px;
  transition:transform .06s, box-shadow .16s, background .16s;
  display:inline-flex; align-items:center; justify-content:center; gap:9px;
  /* Several .btn are anchors (navigation that looks like a button), and an
     anchor keeps its underline unless told otherwise — which put a stray
     underline through every primary button on the lobby and the review screen
     until a screenshot showed it. */
  text-decoration:none;
}
.btn:active { transform:translateY(1px); }
.btn[disabled] { opacity:.5; cursor:default; transform:none; }
.btn-primary { background:var(--primary); color:var(--on-primary); box-shadow:var(--shadow-btn); }
.btn-primary:hover:not([disabled]) { background:var(--primary-ink); box-shadow:var(--shadow-btn-hover); }
.btn-ghost { background:var(--glass); color:var(--primary-ink); border-color:var(--line-2); }
.btn-ghost:hover:not([disabled]) { border-color:var(--lavender); background:var(--raised); }
/* Deliberately demoted — "too hard" must never compete with the hint for
   attention. But demoted is not the same as unrecognisable: with no border, no
   background and no underline it read as body text rather than a control,
   especially at 375px where it sits alone under the hint button. The underline
   is the smallest thing that says "this is pressable" without promoting it. */
.btn-quiet {
  background:transparent; color:var(--muted); border-color:transparent;
  font-weight:600; font-size:14px; padding:10px 14px; min-height:44px;
  text-decoration:underline; text-underline-offset:3px; text-decoration-thickness:1px;
}
.btn-quiet:hover:not([disabled]) { color:var(--ink-2); background:var(--bg-2); }

/* ── section heading ───────────────────────────────────────────────────── */
.sec-h { display:flex; align-items:baseline; justify-content:space-between; gap:var(--s3); margin:var(--s5) 2px var(--s3); }
.sec-h h3 { font-family:var(--serif); font-weight:600; font-size:19px; margin:0; color:var(--ink); }

/* ── cards / empty ─────────────────────────────────────────────────────── */
.card {
  background:var(--card); border:1px solid var(--line); border-radius:var(--r-card);
  padding:var(--s4); box-shadow:var(--shadow-1);
}
.empty {
  text-align:center; padding:var(--s5) var(--s4); background:var(--card);
  border:1px dashed var(--line-2); border-radius:var(--r-card);
}
.empty .seal-sm { width:64px; height:64px; opacity:.9; margin-bottom:var(--s2); }
.empty p { margin:0 auto; max-width:44ch; color:var(--ink-2); font-size:14.5px; }
.empty p b { color:var(--ink); }

/* ── the sample strip (comp's warm first-run) ──────────────────────────── */
.sample {
  display:flex; align-items:center; gap:var(--s3); padding:var(--s3) var(--s4);
  background:var(--card-warm); border:1px solid var(--line-warm); border-radius:var(--r-card);
  box-shadow:var(--shadow-1); margin-bottom:var(--s5); flex-wrap:wrap;
}
.sample .flag { font-size:12px; font-weight:700; letter-spacing:1.4px; color:var(--flag-ink); text-transform:uppercase; }
.sample .body { flex:1; min-width:220px; }
.sample .ttl { font-family:var(--serif); font-size:18px; color:var(--ink); margin:2px 0 3px; }
.sample .meta { font-size:13px; color:var(--muted); }
.sample .meta b { color:var(--ink-2); font-weight:600; }

/* ── the top-level group-study door (T2c, direction A) ──────────────────────
 * A card on the Study Room home carrying both halves of the same idea: open a
 * room, or walk into one. Built to `app/mockups/group-study-t2c.html` §2 A.
 * ⚠ It reuses .btn's 48px floor and gives the join field the same 48px, so the
 *   two controls on the join row are the same height and both clear 44px. */
.gs-door {
  background:var(--card); border:1px solid var(--line); border-radius:var(--r-card);
  padding:var(--s4) var(--s4) var(--s4); box-shadow:var(--shadow-1); margin-bottom:var(--s5);
}
.gs-door h3 { font-family:var(--serif); font-weight:600; font-size:19px; margin:0 0 4px; color:var(--ink); }
.gs-door-sub { margin:0 0 var(--s3); font-size:13.5px; line-height:1.55; color:var(--muted); max-width:54ch; }
/* ⛔ CAPPED TO THE JOIN ROW'S WIDTH, AND IT IS NOT A STYLE PREFERENCE. The comp
      drew this divider full-bleed inside a ~500px card; on the real home the
      card is the page's full column, so at 1280px it ran ~775px while the
      controls it divides stop at 420 — a rule dividing nothing along most of
      its length. Found by opening the render, not by a gate. */
.gs-or {
  display:flex; align-items:center; gap:12px; margin:var(--s3) 0 var(--s2);
  max-width:420px;
  color:var(--muted); font-size:12px; letter-spacing:.04em; text-transform:uppercase;
}
/* ⛔ A BORDER, NOT A 1px BACKGROUND BLOCK, AND THE GATE IS WHY. a0-token-gate
      pairs any rule's `background` with the `color` it inherits — so a hairline
      painted as a background gets graded as text on --line and read 4.15:1
      against --muted, a contrast failure on an element that renders no text at
      all. The border form declares no background, so there is no pair to grade
      and the hairline is identical on screen. */
.gs-or::before, .gs-or::after { content:""; flex:1; height:0; border-top:1px solid var(--line); }
.gs-joinlabel { display:block; font-size:12.5px; font-weight:600; color:var(--ink-2); margin:0 0 7px; }
.gs-joinrow { display:flex; gap:var(--s2); align-items:stretch; max-width:420px; }
.gs-joinrow .btn { min-height:48px; }
/* ⛔ 16px, and it is not a type choice: iOS Safari zooms the page when a
      control under 16px takes focus, and this is a field a phone user taps. */
.gs-joinrow input {
  flex:1 1 auto; min-width:0; box-sizing:border-box; min-height:48px;
  border:1px solid var(--line-2); border-radius:var(--r-ctl);
  background:var(--paper, var(--card)); padding:12px 14px;
  font:inherit; font-size:16px; letter-spacing:.16em; text-transform:uppercase; color:var(--ink);
}
.gs-joinrow input::placeholder { letter-spacing:.16em; color:var(--muted); opacity:.7; }
.gs-joinrow input:focus-visible { outline:3px solid var(--plum-ink); outline-offset:2px; }
.gs-joinrow input.is-bad { border-color:var(--miss-line); background:var(--miss-soft); }
.gs-joinerr { display:block; font-size:12.5px; line-height:1.45; color:var(--flag-ink); margin:7px 0 0; }

/* the picker — the one screen direction A costs, rendered in place */
.gs-picklist { list-style:none; margin:0 0 var(--s3); padding:0; display:grid; gap:var(--s2); }
.gs-pick {
  display:flex; align-items:baseline; gap:var(--s3); flex-wrap:wrap;
  padding:14px var(--s3); min-height:48px; box-sizing:border-box;
  border:1px solid var(--line); border-radius:var(--r-ctl); background:var(--bg-2);
  text-decoration:none; color:var(--ink);
}
.gs-pick:hover { border-color:var(--lavender); background:var(--raised); }
.gs-pick:focus-visible { outline:3px solid var(--plum-ink); outline-offset:2px; }
.gs-pick-t { font-size:15px; font-weight:600; }
.gs-pick-m { font-size:12.5px; color:var(--muted); margin-left:auto; }

@media (max-width: 560px) {
  .gs-joinrow { max-width:none; }
  .gs-pick-m { margin-left:0; flex-basis:100%; }
}

/* ── "come back to" list ───────────────────────────────────────────────── */
.misses { list-style:none; margin:0; padding:0; display:grid; gap:var(--s2); }
.misses li {
  display:flex; align-items:center; gap:var(--s3); padding:var(--s3) var(--s4);
  background:var(--card); border:1px solid var(--line); border-radius:var(--r-ctl);
}
.misses .topic { flex:1; min-width:0; font-size:14.5px; color:var(--ink-2); }
.misses .topic b { color:var(--ink); display:block; font-weight:600; }
.misses .count {
  font-size:12.5px; font-weight:700; color:var(--miss);
  background:var(--miss-soft); border:1px solid var(--miss-line);
  padding:4px 12px; border-radius:var(--r-pill); white-space:nowrap;
}

/* ── status line (sync / offline) ──────────────────────────────────────── */
.status-line {
  display:flex; align-items:center; gap:9px; margin-top:var(--s4);
  font-size:13px; color:var(--muted); min-height:22px;
}
.status-line .dot { width:8px; height:8px; border-radius:50%; background:var(--lavender); flex:none; }
.status-line[data-state="offline"] .dot { background:var(--blush); }
.status-line[data-state="synced"] .dot { background:var(--mint); }
/* ⛔ A11/F4 — THE THIRD STATE, AND THE AXIS IT ADDS IS *SAFETY*, NOT NETWORK.
 * The cold review's image arm said the three save-screens were indistinguishable
 * and the measurement agreed: data-state was 1/3 distinct and the dot colour
 * 1/3 distinct across "everything is safe", "an answer exists nowhere" and the
 * mixed case, with only the 13px sentence carrying the difference. `offline`
 * and `synced` are both facts about the NETWORK, so there was no value meaning
 * "your work is at risk" — A11/F3 removed the false green without adding a true
 * alarm, and the safe state was left wearing a warning dot it had not earned.
 *
 * 🔑 COLOUR IS NOT THE ONLY CHANNEL, AND ON AN 8px DOT IT IS THE WEAKEST ONE.
 * Hue alone is what a tired student misses at 1am and what a red-green
 * colour-blind student cannot use at all — and in the sapphire theme --blush is
 * itself an orange, so a colour-only fix would have been near-invisible in
 * exactly one of the four palettes. This state is therefore bigger and RINGED:
 * a filled core inside a halo reads as different in FORM before hue is
 * resolved, and survives greyscale. */
.status-line[data-state="at-risk"] .dot {
  width:10px; height:10px;
  background:var(--at-risk);
  box-shadow:0 0 0 3px color-mix(in srgb, var(--at-risk) 28%, transparent);
}
@media (prefers-reduced-motion:no-preference) {
  /* One slow pulse on arrival, never a loop: it has to catch the eye once
     without becoming ambient noise she learns to ignore. */
  .status-line[data-state="at-risk"] .dot { animation:atRiskIn .9s ease-out 1; }
  @keyframes atRiskIn {
    0%   { box-shadow:0 0 0 0 color-mix(in srgb, var(--at-risk) 55%, transparent); }
    100% { box-shadow:0 0 0 3px color-mix(in srgb, var(--at-risk) 28%, transparent); }
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   TEST SURFACE — composed here for the first time; no comp precedent.
   ══════════════════════════════════════════════════════════════════════════ */

.test-head { display:flex; align-items:baseline; justify-content:space-between; gap:var(--s3); margin-bottom:var(--s2); flex-wrap:wrap; }
.test-head .progress { font-size:13px; font-weight:700; letter-spacing:1.6px; text-transform:uppercase; color:var(--plum-ink); }
.test-head .topic { font-size:13px; color:var(--muted); }

/* The progress rail. No timer anywhere on this screen — the test is untimed by
   her explicit preference (plan section 14), and a visible clock would
   reintroduce exactly the pressure that preference exists to remove. The
   answered_ms field is recorded silently and never shown mid-test. */
.rail { height:6px; border-radius:var(--r-pill); background:var(--bg-2); overflow:hidden; margin-bottom:var(--s4); }
.rail span { display:block; height:100%; background:linear-gradient(90deg, var(--lavender) 0%, var(--plum) 100%); transition:width .3s ease; }

/* ── N10 · the NGN chart, DIRECTION A ──────────────────────────────────────
   The exam's own layout: chart left, question right, each scrolling on its
   own, so reading the chart is not a decision she has to make — which is the
   habit the real exam rewards. Below 900px it becomes a stack, chart above
   question, and that is the SAME markup: one source order, one build, a
   breakpoint between the two rather than a second mobile design.

   ⛔ THE CLASS NAMES WERE CHECKED AGAINST THIS FILE BEFORE THEY WERE WRITTEN,
      and that is not ceremony — `.today` and `.chips` were both already taken
      when the comp reached for them, and the second one cost two wrong fixes
      because the chip's OWN computed height, font-size and line-height all
      read correct while an inherited margin moved its caption. `.ch`, `.ch-*`,
      `.q-split` and `.q-pane` were each zero-hit greps here first. */
.q-split { min-width:0; }
@media (min-width:900px) {
  /* The one thing direction A costs: the test column outgrows .page's 860px.
     `.is-wide` is set once per SITTING by take.html, never per question. */
  .page.is-wide { max-width:1180px; }
  /* ⛔ AND THE CHARTLESS QUESTION IN A MIXED SET HAS TO BE PUT BACK, which the
     first build did not do and its own gate BLESSED. `.is-wide` is a property
     of the sitting, so a question with no chart renders inside a 1180px page —
     and with no `.q-split` to divide it, the card simply filled the width: a
     four-option question at ~1140px, well past any readable measure, on a
     screen that had been 820px before N10 existed. §W.1 asserted `isWide ===
     true` on exactly that question and passed, because it graded the FLAG I
     had decided to set rather than the WIDTH a student would read. 🔑 Verify
     the on-screen result, never re-check your own formula.
     820px is not a new number — it is `.page`'s 860 minus its own 20px
     padding, i.e. the exact content width this screen had before this batch. */
  .page.is-wide #screen:not(:has(.q-split)) > .test-head,
  .page.is-wide #screen:not(:has(.q-split)) > .rail,
  .page.is-wide #screen:not(:has(.q-split)) > .q-card {
    max-width:820px; margin-inline:auto;
  }
  .q-split { display:flex; gap:var(--s4); align-items:stretch; }
  /* 52/48 — the chart is the wider half because a lab table has three columns
     and the question has one. `minmax(0,…)`'s flexbox twin: `min-width:0` on
     both, without which a wide table sets a min-content floor and shoves the
     question pane off its share. */
  .q-split > .ch { flex:1 1 52%; min-width:0; max-height:calc(100vh - 190px); }
  .q-split > .q-pane { flex:1 1 48%; min-width:0; }
  .q-split > .ch .ch-body { flex:1; }
}

.ch {
  display:flex; flex-direction:column; min-width:0; overflow:hidden;
  background:var(--card); border:1px solid var(--line); border-radius:var(--r-card);
  box-shadow:var(--shadow-2); margin-bottom:var(--s4);
}
@media (min-width:900px) { .ch { margin-bottom:0; } }

.ch-top { display:flex; align-items:center; gap:var(--s3); flex-wrap:wrap; padding:10px var(--s4); background:var(--card-warm); border-bottom:1px solid var(--line); }
.ch-top .who { font-size:13.5px; font-weight:600; color:var(--ink); }
.ch-top .meta { font-size:12.5px; color:var(--muted); }


/* ── N10-a · THE SCROLL CUE ────────────────────────────────────────────────
   ⛔ NOT POLISH. Mobile overlay scrollbars are hidden at rest, so the only
   signal that either surface held more was a CLIPPED WORD — which reads as
   accidental clipping, not as "there is more". Measured at 320px: 510px of
   tabs in a 290px strip, and 210px of labs content in a 190px panel. If she
   cannot tell there are five tabs, half the record is invisible — and being
   able to go and check things is the entire argument for a chart over a
   picture. Widened on 2026-08-25 from an opt-in risk to a DEFAULT-PATH one:
   the checkbox selects the prompt, not the renderer, so ordinary generations
   carry charts (N10-e).

   ⚖ A STATIC FADE WAS REJECTED BECAUSE IT LIES — it paints an edge cue on a
   strip with nothing beyond it. Here the two COVER layers are pinned to the
   CONTENT (`local`) and the two SHADE layers to the BOX (`scroll`): at rest
   the covers sit exactly over the shades and the cue is INVISIBLE, and it
   appears only as the content slides out from under them. The cue is
   therefore never present when it is not true, and that is a property of the
   MECHANISM rather than of a class some JS remembers to toggle. Nothing in
   take.html changed for this.

   ⛔ THE COVER IS OPAQUE ACROSS THE WHOLE SHADE, not merely wider than it —
   hence the 45% / 50% colour stops, which is the one number here that looks
   arbitrary. Measured: a cover fading from 100% at 0px to 0% at 26px is only
   ~81% opaque where an 11px shade is still ~55% strong, so the "hidden" edge
   leaked 1.5 luminance and the self-hide control read 1.8 on a panel with
   NOTHING to scroll to. Small, and exactly the lie this idiom was chosen to
   avoid. The solid region is the ground's own colour, so it costs nothing.

   ⛔ AND THE SHADES ARE LINEAR, NOT THE IDIOM'S USUAL `radial-gradient(
   farthest-side at …)` — a measurement, not a preference. A radial shade on
   a 320px-wide panel peaks at the CENTRE of the bottom edge and fades to
   nothing 160px away at each end: photographed at 5x it was essentially
   invisible while still measuring −16.4 luminance over a 5px band. 🔑 The
   number and the picture disagreed and the picture was right — a mean over a
   band cannot tell an even shadow from a bright edge with a dark middle.
   Linear falloff doubled every reading (14.3 → 27.8 day, 6.8 → 11.5 night).

   ⛔ THE SHADE IS A TOKEN, NOT AN rgba(), AND GATE 4 IS RIGHT TO REFUSE ONE:
   a shade that reads on #fbf6ef vanishes on #1a1724. `--scroll-shade`
   answers in all four cells, which Gate 5 also requires.

   ⚠ THE COVER MUST BE THE REAL GROUND, and the two rules cover with DIFFERENT
   tokens on purpose: `.ch-tabs` covers with --card-warm (its own), `.ch-body`
   with --card (its PARENT .ch's — it declares no ground of its own, so that
   the panel and the card stay one surface). If either ground moves, the cover
   moves with it or a pale band appears at the edge at rest. */

/* The strip SCROLLS rather than wraps or squashes. Five tabs at their real
   words do not fit 320px — the widest is "History & Physical" — and a wrapped
   strip changes the panel's top edge as tabs move between rows. */
.ch-tabs {
  display:flex; gap:2px; padding:0 6px; overflow-x:auto; scrollbar-width:thin;
  background:var(--card-warm); border-bottom:1px solid var(--line);
  background-image:
    linear-gradient(to right, var(--card-warm) 45%, transparent),
    linear-gradient(to left, var(--card-warm) 45%, transparent),
    linear-gradient(to right, var(--scroll-shade), transparent),
    linear-gradient(to left, var(--scroll-shade), transparent);
  background-position:left center, right center, left center, right center;
  background-size:26px 100%, 26px 100%, 11px 100%, 11px 100%;
  background-repeat:no-repeat;
  background-attachment:local, local, scroll, scroll;
}
.ch-tab {
  flex:none; font-family:inherit; font-size:12.5px; font-weight:600;
  padding:0 12px; min-height:44px; white-space:nowrap; cursor:pointer;
  border:0; border-bottom:2px solid transparent; background:transparent; color:var(--muted);
}
.ch-tab:hover { color:var(--ink-2); }
.ch-tab[aria-selected="true"] { color:var(--plum-ink); border-bottom-color:var(--plum); background:var(--card); }
/* Inset, because the strip clips: a 2px-offset ring on the first or last tab
   is half-eaten by `overflow-x:auto` exactly when a keyboard user needs it. */
.ch-tab:focus-visible { outline:3px solid var(--plum-ink); outline-offset:-3px; }

/* Focusable (take.html sets tabindex="0") so a keyboard user can scroll a long
   table; without it the panel is reachable but not scrollable by key. */
.ch-body {
  padding:var(--s3) var(--s4); overflow-y:auto;
  background-image:
    linear-gradient(to bottom, var(--card) 50%, transparent),
    linear-gradient(to top, var(--card) 50%, transparent),
    linear-gradient(to bottom, var(--scroll-shade), transparent),
    linear-gradient(to top, var(--scroll-shade), transparent);
  background-position:center top, center bottom, center top, center bottom;
  background-size:100% 20px, 100% 20px, 100% 9px, 100% 9px;
  background-repeat:no-repeat;
  background-attachment:local, local, scroll, scroll;
}
.ch-body[hidden] { display:none; }
.ch-body:focus-visible { outline:3px solid var(--plum-ink); outline-offset:-3px; }

/* ⛔ NO FLAG COLUMN AND NO RED ROW, EVER. Recognising the abnormal value is
   the step being tested — a coloured row answers the question before she
   reads it. There is no rule here that could paint one, and no field in the
   schema that could ask for one. */
.ch-tbl { width:100%; border-collapse:collapse; font-size:13.5px; }
.ch-tbl th { text-align:left; font-size:10.5px; letter-spacing:.05em; text-transform:uppercase; color:var(--muted); font-weight:700; padding:0 12px 6px 0; border-bottom:1px solid var(--line-2); }
.ch-tbl td { padding:7px 12px 7px 0; border-bottom:1px solid var(--line); color:var(--ink-2); vertical-align:baseline; line-height:1.4; }
.ch-tbl tr:last-child td { border-bottom:0; }
.ch-tbl td.v { font-variant-numeric:tabular-nums; color:var(--ink); font-weight:600; white-space:nowrap; }
.ch-tbl td.ref { font-variant-numeric:tabular-nums; color:var(--muted); white-space:nowrap; font-size:12.5px; }

.ch-note { border-left:2px solid var(--line-2); padding-left:12px; margin-bottom:15px; }
.ch-note:last-child { margin-bottom:0; }
.ch-note .when { display:block; font-size:10.5px; font-weight:700; letter-spacing:.05em; text-transform:uppercase; color:var(--muted); margin-bottom:4px; }
.ch-note p { margin:0; font-size:13.5px; line-height:1.6; color:var(--ink-2); }

.ch-def { margin:0; font-size:13.5px; line-height:1.6; }
.ch-def dt { font-size:10.5px; font-weight:700; letter-spacing:.05em; text-transform:uppercase; color:var(--muted); margin-bottom:3px; }
.ch-def dd { margin:0 0 12px; color:var(--ink-2); }
.ch-def dd:last-child { margin-bottom:0; }

/* Stacked. The panel is CAPPED and scrolls inside itself, because an uncapped
   chart above the question pushes the first option below the fold and the
   screen then reads as a chart with a question hidden under it. */
@media (max-width:899px) { .ch-body { max-height:240px; } }
@media (max-width:600px) {
  .ch-body { max-height:190px; padding:12px 13px; }
  .ch-tbl { font-size:12.5px; }
  .ch-tbl td.ref { font-size:11.5px; }
}

.q-card { background:var(--card); border:1px solid var(--line); border-radius:var(--r-card); padding:var(--s5) var(--s4) var(--s4); box-shadow:var(--shadow-2); }
.q-card .stem {
  font-family:var(--serif); font-size:21px; line-height:1.42; color:var(--ink);
  margin:0 0 var(--s4); font-weight:600;
}
.q-card .stem:focus { outline:none; }

/* Options are native radios in a fieldset. A div+role would have to hand-roll
   arrow keys, Home/End, and the roving tabindex that a radio group gets free —
   and would get one of them wrong. */
.options { border:0; margin:0; padding:0; }
.options legend { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); }
.opt {
  display:flex; align-items:flex-start; gap:var(--s3);
  padding:var(--s3) var(--s4); margin-bottom:var(--s2);
  border:1.5px solid var(--line-2); border-radius:var(--r-ctl);
  background:var(--card); cursor:pointer; min-height:56px;
  transition:border-color .14s, background .14s;
}
.opt:hover { border-color:var(--lavender); background:var(--opt-hover); }
.opt input { margin:3px 0 0; width:20px; height:20px; accent-color:var(--plum); flex:none; cursor:pointer; }
.opt .txt { font-size:15.5px; color:var(--ink-2); }
.opt:has(input:checked) { border-color:var(--plum); background:var(--opt-on); }
.opt:has(input:focus-visible) { outline:3px solid var(--plum-ink); outline-offset:2px; }
.opt.is-key { border-color:var(--mint); background:var(--ok-soft); }
.opt.is-key .txt { color:var(--mint-ink); font-weight:600; }
.opt input:disabled { cursor:default; }
.opt:has(input:disabled) { cursor:default; }

/* ── hint ladder ───────────────────────────────────────────────────────── */
.helpers { display:flex; flex-wrap:wrap; gap:var(--s2); align-items:center; margin-top:var(--s4); }
.hints { margin:var(--s3) 0 0; padding:0; list-style:none; display:grid; gap:var(--s2); }
.hints li {
  display:flex; gap:var(--s3); padding:var(--s3) var(--s4);
  background:var(--bg-2); border-left:3px solid var(--lavender); border-radius:0 var(--r-ctl) var(--r-ctl) 0;
  font-size:14.5px; color:var(--ink-2);
}
.hints li .rung { font-size:12px; font-weight:700; letter-spacing:1px; text-transform:uppercase; color:var(--plum-ink); flex:none; padding-top:2px; }
.hint-spent { font-size:13px; color:var(--muted); margin:var(--s3) 0 0; }

/* ── too-hard ──────────────────────────────────────────────────────────── */
.too-hard-panel {
  margin-top:var(--s4); padding:var(--s4);
  background:var(--miss-soft); border:1px solid var(--miss-line); border-radius:var(--r-ctl);
}
.too-hard-panel p { margin:0 0 var(--s3); font-size:14.5px; color:var(--ink-2); }

/* ── A10: a write that did not land ────────────────────────────────────────
   Deliberately the SAME tokens as .too-hard-panel above, and deliberately NOT
   the same class. Same tokens because this is the page's existing "something
   needs your attention" vocabulary and inventing a second one would fork the
   palette; a separate class because reusing `.too-hard-panel` would put a
   selector that names one thing in charge of another's job — the defect A6/D4
   was, running the other way. It also introduces no new foreground/background
   pair, so Gate 8 already covers it.
   No :focus-visible rule: the global one at the top of this file already
   answers it, and a duplicate would be one more declaration to explain. */
.save-fail {
  margin-top:var(--s4); padding:var(--s4);
  background:var(--miss-soft); border:1px solid var(--miss-line); border-radius:var(--r-ctl);
}
.save-fail p { margin:0 0 var(--s3); font-size:14.5px; color:var(--ink-2); }
.save-fail-act { display:flex; flex-wrap:wrap; gap:var(--s2); align-items:center; }
/* The fine print under the exit -- it appears only with the control it
   qualifies. Smaller but the SAME ink on the same ground as the panel's body:
   a quieter colour here would be a new foreground/background pair, which is
   exactly what .save-fail's own note says it avoided introducing, and the
   contrast census would have to grade it in all four cells to buy nothing. */
.save-fail-fine { margin:var(--s3) 0 0; font-size:13.5px; color:var(--ink-2); }
.unsaved-note {
  margin:var(--s3) auto 0; max-width:52ch; padding:var(--s3) var(--s4); text-align:left;
  background:var(--miss-soft); border:1px solid var(--miss-line); border-radius:var(--r-ctl);
  font-size:14px; color:var(--ink-2);
}
/* A10-F2. Same block, NEUTRAL palette -- deliberately not the miss colours.
   A row saved to her account is not a loss, and dressing it in the vocabulary
   this page uses for "you got this wrong" would make a safe answer read as a
   problem. It still gets a box, because it is still something she has to know.
   --ink-2 on --card is a pair this page already ships (.reasons button), so
   this introduces no new one for the contrast census to grade. */
.remote-note {
  margin:var(--s3) auto 0; max-width:52ch; padding:var(--s3) var(--s4); text-align:left;
  background:var(--card); border:1px solid var(--line); border-radius:var(--r-ctl);
  font-size:14px; color:var(--ink-2);
}
.reasons { display:grid; gap:var(--s2); grid-template-columns:1fr 1fr; }
.reasons button {
  min-height:48px; text-align:left; padding:12px 16px; font-size:14.5px; font-weight:600;
  background:var(--card); color:var(--ink-2); border:1.5px solid var(--miss-line);
  border-radius:var(--r-ctl); cursor:pointer; transition:border-color .14s, background .14s;
}
.reasons button:hover { border-color:var(--miss); background:var(--raised); }

/* ── revealed rationale ────────────────────────────────────────────────── */
.reveal { margin-top:var(--s4); padding-top:var(--s4); border-top:1px solid var(--line); }
.reveal h3 { font-family:var(--serif); font-size:17px; margin:0 0 var(--s3); color:var(--ink); font-weight:600; }
.rationale { list-style:none; margin:0 0 var(--s3); padding:0; display:grid; gap:var(--s2); }
.rationale li { font-size:14.5px; color:var(--ink-2); padding-left:var(--s4); position:relative; }
.rationale li .lbl { font-weight:700; color:var(--muted); position:absolute; left:0; }
.rationale li.is-key .lbl { color:var(--mint-ink); }
.tip {
  margin:var(--s3) 0 0; padding:var(--s3) var(--s4);
  background:var(--card-warm); border:1px solid var(--line-warm); border-radius:var(--r-ctl);
  font-size:14.5px; color:var(--ink-2);
}
.tip b { color:var(--plum-ink); }

.q-foot { display:flex; justify-content:flex-end; gap:var(--s2); margin-top:var(--s4); flex-wrap:wrap; }

/* ══════════════════════════════════════════════════════════════════════════
   REVIEW SCREEN
   ══════════════════════════════════════════════════════════════════════════ */

.score {
  text-align:center; padding:var(--s5) var(--s4);
  background:var(--grad-panel);
  border:1px solid var(--line-2); border-radius:var(--r-card); box-shadow:var(--shadow-2);
}
.score .num { font-family:var(--serif); font-size:52px; line-height:1; color:var(--plum-ink); font-weight:600; }
.score .of { font-size:15px; color:var(--muted); margin-top:var(--s2); }
.score .band { margin:var(--s4) auto 0; max-width:46ch; font-size:15.5px; color:var(--ink-2); }
.score .band b { color:var(--ink); }

.buddy-greeting {
  margin:var(--s4) 0 0; padding:var(--s3) var(--s4);
  background:var(--bg-2); border:1px solid var(--line); border-radius:var(--r-ctl);
  font-size:14.5px; line-height:1.55; color:var(--ink-2);
}
.buddy-greeting strong { color:var(--ink); }
/* ⛔ `--muted-sunk`, NOT `--muted`, AND THE DIFFERENCE IS ONE CELL. At 12.5px on
 * `.buddy-greeting`'s `--bg-2` ground, `--muted` measures 4.45:1 in seal-day —
 * under the 4.5 floor, and the only cell that fails (seal-night 7.80, sapphire
 * day 4.86, sapphire-night 7.59). `--muted-sunk` is the token A4 created for
 * exactly this pairing and carries the ratio against this ground in every
 * cell's comment: 5.09 / 7.80 / 5.34 / 7.59.
 * ⛔ DO NOT "FIX" THIS BY NUDGING `--muted` ITSELF — that is the change A4
 * explicitly refused, because `--muted` paints every quiet label in the
 * product and moving it re-grades all of them to repair one span. A colour is
 * vetted per SURFACE, not per token. */
.buddy-greeting .buddy-evidence {
  display:block; margin-top:5px; font-size:12.5px; color:var(--muted-sunk);
}

.review { display:grid; gap:var(--s3); margin-top:var(--s4); }
.review-item { background:var(--card); border:1px solid var(--line); border-radius:var(--r-card); padding:var(--s4); }
.review-item .verdict { font-size:12px; font-weight:700; letter-spacing:1.4px; text-transform:uppercase; margin-bottom:var(--s2); }
.review-item.correct .verdict { color:var(--mint-ink); }
.review-item.missed  .verdict { color:var(--miss); }

/* The misses carry a left accent and the right answers do not, and that
   asymmetry is the point. Reviewing a set is a scanning task with one question
   — "which ones did I get wrong?" — and the first build answered it only with a
   small coloured word at the top of otherwise identical cards, so finding them
   meant reading every card. The accent is on the miss because that is what she
   is looking for; giving the correct ones a matching green stripe would restore
   the uniformity this exists to break, and turn the page into a scorecard. */
.review-item.missed { border-left:4px solid var(--miss); }
.review-item .stem { font-family:var(--serif); font-size:17px; line-height:1.4; color:var(--ink); margin:0 0 var(--s3); font-weight:600; }
.review-item .yours { font-size:14px; color:var(--muted); margin:0 0 var(--s3); }
.review-item .yours b { color:var(--ink-2); font-weight:600; }

/* ── OPTION C — visuals over her own study data ─────────────────────────────
   Two panels between the score and the rationales: where she stands on each
   topic across every sitting, and her score each time she took this test.
   The maths is app/js/nurseal-visuals.js; nothing here computes anything.

   ⛔ ONE MEANING PER CHANNEL (Sami, 2026-08-21: "bar length means one thing
   everywhere"). Length is solidity. Colour is a BAND of that same number —
   `bandOf()` takes solidity and nothing else, so the two channels cannot
   disagree and there is no second meaning to learn. The right-hand column is
   the receipt, in counts, and is text rather than a third visual encoding.

   ⚠ THE MIDDLE BAND IS `--plum` AND THE COMP'S `--at-risk` COULD NOT SHIP.
   That token's own declaration reads "the SAFETY axis, never the network axis
   (A11/F4)" — and the safety axis is LIVE on this very screen, where
   `.status-line[data-state="at-risk"]` means "your answers are saved nowhere".
   Two meanings in one orange, one scroll apart, is exactly the defect the rule
   above exists to prevent. `--plum` is the brand's own neutral: already on this
   screen, an alarm in no theme, and separable from --miss and --mint in all
   four theme x mode cells. */
.vz { display:grid; gap:var(--s3); margin-top:var(--s4); }
.vz-panel {
  background:var(--card); border:1px solid var(--line);
  border-radius:var(--r-card); padding:var(--s4);
}
.vz-head { margin-bottom:var(--s4); }
.vz-head h2 {
  font-family:var(--serif); font-weight:600; font-size:17px; margin:0; color:var(--ink);
}
.vz-head p { margin:3px 0 0; font-size:13px; color:var(--muted); }

.vz-rows { display:grid; gap:var(--s3); }
.vz-row { display:grid; grid-template-columns:1fr 86px; gap:var(--s3); align-items:center; }
/* ⚠ THE TOPIC OUTRANKS THE NUMBER, and it did not until a cold visual pass
   said the eye lands on the right-hand percentages first. It was right and the
   hierarchy was inverted: the panel's job is "what do I practise next", whose
   answer is a topic; the percent is the receipt for the bar. */
.vz-label { font-size:15px; font-weight:600; color:var(--ink); }
/* The track is the SCALE, and it stays visible at every value — a 0% row draws
   an empty track rather than nothing, so "she has none of this yet" reads as a
   measurement instead of as a missing bar. No minimum fill width: padding a
   zero out to a visible stub is a silent lie about the one value that matters
   most on a weakest-first list. */
/* ⚠ --line-2 RATHER THAN --line, AND THE NIGHT RENDER IS WHY. On a 0% row the
   track IS the whole drawing — an empty instrument still has to read as an
   instrument — and at night the lighter border sat close enough to the card
   that the weakest topic, which is the FIRST row on the page, read as "nothing
   rendered" rather than as "none of this yet". */
.vz-track {
  height:9px; margin-top:5px; border-radius:var(--r-pill);
  background:var(--bg-2); border:1px solid var(--line-2); overflow:hidden;
}
.vz-fill { height:100%; border-radius:var(--r-pill); }
/* ONE place maps a band to a colour: the bar and its legend swatch share
   these three rules, so a legend cannot drift from the thing it explains. */
.vz-fill[data-band="lo"],  .vz-key i[data-band="lo"]  { background:var(--miss); }
.vz-fill[data-band="mid"], .vz-key i[data-band="mid"] { background:var(--plum); }
.vz-fill[data-band="hi"],  .vz-key i[data-band="hi"]  { background:var(--mint-ink); }
.vz-num { text-align:right; }
.vz-num b {
  display:block; font-size:15px; font-weight:600; color:var(--ink-2);
  font-variant-numeric:tabular-nums;
}
/* nowrap: the receipt is one line by design, and a wrapped one leaves the
   column ragged at phone widths — measured at 375px on "1 of 3 with a hint",
   which is why that branch is now one word like the other three. */
.vz-num span { display:block; font-size:11.5px; color:var(--muted); margin-top:1px;
               white-space:nowrap; }

.vz-key {
  display:flex; flex-wrap:wrap; gap:var(--s2) var(--s4);
  margin-top:var(--s4); padding-top:var(--s3); border-top:1px solid var(--line);
  font-size:12px; color:var(--muted);
}
/* Its own row, so the sentence cannot wrap one orphaned word down beside the
   swatches — measured at 375px, where "own." sat alone on a line and the
   explanation read as fragmented. */
.vz-key-lead { flex-basis:100%; }
.vz-key span { display:inline-flex; align-items:center; gap:6px; }
.vz-key i { width:9px; height:9px; border-radius:50%; display:inline-block; }
.vz-note { margin:var(--s3) 0 0; font-size:12.5px; color:var(--muted); }

/* The score line. The <svg> owns the plot; the point labels are HTML positioned
   from the SAME x the path was drawn from, so they carry real font sizes rather
   than scaling with the viewBox — SVG <text> under a stretched viewBox shears,
   and under `meet` it shrinks below legibility on a phone. */
.vz-chart { position:relative; }
.vz-svg { display:block; width:100%; height:auto; }  /* the viewBox owns the aspect,
   so the drawing scales uniformly and a dot stays a circle at every width. */
.vz-marks { position:relative; height:34px; margin-top:2px; }
.vz-mark {
  position:absolute; transform:translateX(-50%); text-align:center; white-space:nowrap;
}
.vz-mark b { display:block; font-size:13px; font-weight:650; color:var(--ink-2); font-variant-numeric:tabular-nums; }
.vz-mark span { display:block; font-size:12px; color:var(--muted); }
.vz-mark.is-now b { color:var(--plum-ink); }

/* "Not enough history yet" is a product state with its own unlock condition,
   never an error and never an absence — a single dot pretending to be a line
   would be the softened guess this panel exists to refuse. */
.vz-empty {
  margin:0; font-size:14px; color:var(--ink-2);
  background:var(--bg-2); border:1px solid var(--line);
  border-radius:var(--r-ctl); padding:var(--s3);
}

@media (max-width:520px) {
  .vz-row { grid-template-columns:1fr 74px; }
  .vz-marks[data-dense="true"] .vz-mark span { display:none; }
}

/* ── the topic picker (history.html, direction A, 2026-08-21) ──────────────
   Sami picked direction A off app/mockups/history-stand-picker.html: the topic
   filter IS the picker, so this button replaces a <select> and has to be
   indistinguishable from its two neighbours in the same row. Every declaration
   below that also appears in `.field select` is deliberately identical — a
   control that means the same thing must look the same, and the two sit side
   by side where any drift is immediately visible. */
.field .pickbtn {
  font:inherit; font-size:15px; color:var(--ink-2); width:100%;
  min-height:44px; padding:0 var(--s3);
  border:1px solid var(--line-2); border-radius:var(--r-ctl); background:var(--field);
  cursor:pointer; text-align:left;
  /* room for the chevron, which a <select> gets from the platform */
  padding-right:calc(var(--s3) + 14px);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  position:relative;
}
.field .pickbtn:focus-visible { outline:3px solid var(--plum-ink); outline-offset:2px; }
.field .pickbtn:hover { border-color:var(--lavender); }
/* ⚠ The chevron is a border trick rather than an inline SVG because this
   control is written in the page's markup, not by a1-sync.mjs — a hand-copied
   SVG here would be a second copy of the mark problem A1 spent a batch fixing.
   currentColor, so it follows the label into every theme and mode. */
.field .pickbtn::after {
  content:""; position:absolute; right:var(--s3); top:50%;
  width:6px; height:6px; margin-top:-4px;
  border-right:1.5px solid currentColor; border-bottom:1.5px solid currentColor;
  transform:rotate(45deg); opacity:.55;
}
/* A set filter is legible at a glance, the way a non-default <select> is. */
.field .pickbtn.is-set { border-color:var(--lavender); color:var(--ink); font-weight:600; }

/* The dialog. A NATIVE <dialog>, matching nurseal-feedback.js's shipped
   conventions on this same page rather than inventing a second modal look. */
.sp {
  width:min(32rem, calc(100vw - 2rem)); padding:0;
  border:1px solid var(--line); border-radius:16px;
  background:var(--card); color:var(--ink);
  font-family:var(--sans);
  box-shadow:var(--shadow-2);
}
.sp::backdrop { background:var(--scrim); }
.sp-body { padding:20px 20px 16px; }
.sp-head { display:flex; align-items:flex-start; justify-content:space-between; gap:var(--s3); }
.sp-head h2 { font-family:var(--serif); font-weight:600; font-size:19px; margin:0; color:var(--ink); }
.sp-scope { margin:3px 0 0; font-size:12.5px; color:var(--muted); }
.sp-x {
  border:0; background:none; color:var(--muted); font:inherit; font-size:22px; line-height:1;
  cursor:pointer; padding:4px 8px; border-radius:8px; margin:-4px -8px 0 0; min-height:44px;
}
.sp-x:hover { background:var(--bg-2); color:var(--ink-2); }
.sp-x:focus-visible { outline:3px solid var(--plum-ink); outline-offset:2px; }

.sp-all {
  display:flex; align-items:center; justify-content:space-between; gap:var(--s2);
  margin:var(--s3) 0 var(--s2); padding-bottom:var(--s2);
  border-bottom:1px solid var(--line); font-size:12.5px; color:var(--muted);
}
.sp-linky {
  border:0; background:none; font:inherit; font-size:12.5px; color:var(--plum-ink);
  text-decoration:underline; text-underline-offset:2px; cursor:pointer;
  padding:6px 4px; min-height:44px;
}
.sp-linky:focus-visible { outline:3px solid var(--plum-ink); outline-offset:2px; }

/* ⛔ THE CHECKBOX IS DELIBERATELY NOT INSIDE A `.field`. account.css's A6/D4
   rule widened `.field input` to a 52px bordered slab so the CP7 code box would
   stop falling through every rule keyed to input[type="email"], and its own
   note predicts this consequence: "a checkbox dropped in later inherits a 52px
   bordered slab, which is loud and immediately visible". Named before it was
   rendered rather than discovered by rendering it. */
/* ⛔ EACH TOPIC IS ITS OWN CARD, INSET INSIDE THE DIALOG'S PADDING — rewritten
   2026-08-21 after Sami read the 375px render: "the 2 that are selected look
   like they dont fit in that small box". Two things caused that and neither
   was spacing. The rows carried `margin:0 -8px`, so a selected row's tint BLED
   past the dialog's content edge and read as a band bursting out of the box;
   and the row was a `1fr 80px` grid, so the bar stopped short of the number and
   each row read as two ragged halves. Now: no bleed, a real border the selected
   state COLOURS rather than a full-width wash, and a stacked cell so the bar
   spans the width it is measuring. */
.sp-rows { display:grid; gap:var(--s2); max-height:44vh; overflow-y:auto; padding:2px; }
.sp-pick {
  display:grid; grid-template-columns:20px 1fr; gap:0 12px; align-items:start;
  padding:12px 14px; cursor:pointer;
  background:var(--card); border:1px solid var(--line); border-radius:var(--r-ctl);
  transition:border-color .12s ease, background .12s ease;
}
/* ⛔ HOVER AND SELECTED MUST NOT SHARE A CHANNEL — and the first version of
   this gave both `--bg-2`, so a row the pointer merely passed over was
   pixel-identical to a row she had actually ticked. Found by measuring the
   render, not by reading: an unticked row reported the selected background and
   `isOn: false` in the same probe.
   🔑 This is nurseal-visuals.js's own headline rule — ONE MEANING PER CHANNEL —
   broken one file over by the code that reuses its bars. Selection is a state
   she is deciding; hover is where her mouse happens to be, and it is the more
   transient of the two, so it gets the weaker signal.
   ⚠ The checkbox is still the primary indicator. This is the row-level echo of
   it, which is what makes a four-row list scannable at a glance. */
/* Hover moves the BORDER only; selection moves the border AND the ground. Both
   used `--bg-2` at first, which made a row the pointer merely passed over
   pixel-identical to a ticked one — this product's own one-meaning-per-channel
   rule, broken by the code reusing its bars. The stronger signal belongs to the
   decision, not to where the cursor happens to be. */
.sp-pick:hover { border-color:var(--lavender); }
.sp-pick.is-on { border-color:var(--primary); background:var(--bg-2); }
/* Outside the row, not inset: an inset ring on a bordered card reads as a
   SECOND border, and was part of what looked crowded.
   ⛔ `:has(input:focus-visible)`, NEVER `:focus-within` — the cold pass of
   2026-08-21 ranked this first in all three screens it looked at. `:focus-within`
   fires on a MOUSE click too, so the last row she clicked kept a 2px ring and
   read as errored, or as somehow special, beside an identically-ticked row one
   line up. Measured on that checkbox: `:focus` true, `:focus-visible` FALSE —
   the browser had already decided the ring was not wanted and the selector
   overrode it. Keyboard focus is unchanged, which is the whole point.
   ⚠ THIS EXACT PATTERN ALREADY SHIPS TWICE IN THIS FILE — `.opt:has(input:
   focus-visible)` for the answer options and `.lvl-opt:has(input:focus-visible)`
   for the level picker. The picker was the one checkbox row written a third
   way; matching the two that were already right is the fix, not a new idea. */
.sp-pick:has(input:focus-visible) { outline:2px solid var(--plum-ink); outline-offset:2px; }
.sp-pick input { width:18px; height:18px; margin:3px 0 0; accent-color:var(--primary); cursor:pointer; }
.sp-cell { min-width:0; }
/* Baseline-aligned so the topic and its number share one line however long the
   topic is; min-width:0 lets a long topic shrink rather than shove the percent
   out of the card. */
.sp-line { display:flex; align-items:baseline; justify-content:space-between; gap:var(--s3); }
.sp .vz-label { font-size:14.5px; min-width:0; overflow-wrap:anywhere; }
.sp-pct { font-size:14px; font-weight:600; color:var(--ink-2);
          font-variant-numeric:tabular-nums; flex:none; }
.sp .vz-track { margin-top:8px; }
.sp-receipt { margin-top:6px; font-size:11.5px; color:var(--muted); }

.sp-gate {
  margin:var(--s3) 0 0; padding:var(--s2) var(--s3); font-size:12.5px; color:var(--ink-2);
  background:var(--bg-2); border:1px solid var(--line); border-radius:var(--r-ctl);
}
/* ⛔ COLLAPSED, NOT `display:none` — see the #sp-gate markup note. This is a
   live region, and `display:none` (like the `hidden` attribute it replaced)
   takes it out of the accessibility tree, which is exactly what stopped the
   sentence being announced. Zeroing the box keeps it rendered at no height, so
   the region survives and nothing shows until it has something to say. */
.sp-gate:empty { margin:0; padding:0; border:0; }
/* Present and empty from first paint, then only ever text-swapped — same rule
   as .result-line and take.html's save line. */
.sp-status { margin:0; min-height:1px; font-size:12.5px; color:var(--muted); }
.sp-foot {
  margin-top:var(--s4); padding-top:var(--s3); border-top:1px solid var(--line);
  display:grid; gap:var(--s2);
}
.sp-note { margin:0; font-size:11.5px; color:var(--muted); text-align:center; }

@media (max-width:520px) {
  /* ⚠ THE OLD TWO-COLUMN OVERRIDE IS GONE, NOT RETUNED. It narrowed and then
     re-widened the number column to stop the receipt being CLIPPED at 375px
     ("3 of 3 missed" rendered "3 of 3 missec"). The stacked layout above
     removes the constraint rather than balancing it: the receipt has the full
     card width and cannot collide with the percent at any viewport. `sp-fits`
     in history-picker-browser.mjs still asserts nothing overflows, so a future
     tune is measured rather than eyeballed.
     🔑 Kept as a comment because the deleted rule was the SECOND fix for one
     symptom, and both were fixes to the wrong layer. */
  .sp { width:min(32rem, calc(100vw - 1.5rem)); }
  .sp-body { padding:18px 16px 14px; }
}

/* ── footer / legal (verbatim structure from the comp) ─────────────────── */
footer { max-width:700px; margin:var(--s6) auto 0; text-align:center; color:var(--muted); font-size:11.5px; line-height:1.65; }
footer .rule { width:40px; height:2px; background:var(--line-2); margin:0 auto var(--s3); border-radius:2px; }
footer .legal-brand { color:var(--plum-ink); font-weight:750; letter-spacing:.08em; }
footer .legal-owner { color:var(--ink-2); font-weight:600; }
footer .legal-copy { margin:7px auto 0; }
footer .legal-links { display:flex; justify-content:center; flex-wrap:wrap; gap:8px 12px; margin-top:9px; }
/* 32px targets, not the 19px these were. Text-sized links in an 11.5px footer
   land under the 32px house floor AND under WCAG 2.5.8's 24px AA floor — found
   in CP3D's access pass, and true on every Study Room page, which is why the
   fix is here once rather than per page.

   32 and not 44 deliberately: the charter reserves 44px for primary, frequent
   and touch-first controls, and a legal link at the bottom of the page is none
   of those. Height comes from PADDING rather than from font-size, so the
   footer's visual weight is unchanged and only the hit area grows. */
footer .legal-links a {
  color:var(--plum-ink); font-weight:650; text-underline-offset:3px;
  display:inline-flex; align-items:center; min-height:32px;
  padding:0 var(--s2); border-radius:var(--r-ctl);
}
footer .legal-links a:hover { background:var(--bg-2); }

/* ══ S4 — structured recall (history.html) ═════════════════════════════════
   Composed in the vocabulary the S3 screens established, not a new look: the
   same card radius, the same lavender rules, the same serif for anything that
   is a question. Two deliberate constraints inherited from above —

   NO ALARM COLOUR ANYWHERE. The palette note at the top of this file rules
   that a missed question never reads as failure. A page whose entire subject
   is her weak spots is the most tempting place to break that rule and the
   worst place to do it, so the "needs a different angle" chip is warm cream,
   not amber, and nothing here is red.

   EVERY CONTROL IS ≥44px TALL. These are the page's primary controls and get
   the touch size, not the 32px floor. ────────────────────────────────────── */

.filters {
  background:var(--card); border:1px solid var(--line); border-radius:var(--r-card);
  padding:var(--s4); box-shadow:var(--shadow-1); margin-bottom:var(--s4);
  display:grid; gap:var(--s3);
}
.filters fieldset { border:0; margin:0; padding:0; }
.filters legend, .field label {
  display:block; font-size:12px; font-weight:700; letter-spacing:1.4px;
  text-transform:uppercase; color:var(--plum-ink); margin-bottom:var(--s2); padding:0;
}
.segmented { display:flex; flex-wrap:wrap; gap:var(--s2); }
.seg {
  font:inherit; font-size:14.5px; cursor:pointer;
  min-height:44px; padding:0 var(--s3);
  border:1px solid var(--line-2); border-radius:var(--r-pill);
  background:var(--glass); color:var(--ink-2);
  transition:background .15s ease, border-color .15s ease, color .15s ease;
}
.seg:hover { border-color:var(--lavender); background:var(--raised); }
.seg.is-on { background:var(--primary); border-color:var(--primary); color:var(--on-primary); font-weight:600; }
.seg:focus-visible { outline:3px solid var(--plum-ink); outline-offset:2px; }

.filter-row { display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:var(--s3); }
.field select {
  font:inherit; font-size:15px; color:var(--ink-2); width:100%;
  min-height:44px; padding:0 var(--s3);
  border:1px solid var(--line-2); border-radius:var(--r-ctl); background:var(--field);
  cursor:pointer;
}
.field select:focus-visible { outline:3px solid var(--plum-ink); outline-offset:2px; }
.filter-note { margin:0; font-size:13px; color:var(--muted); }

.result-line {
  margin:0 2px var(--s1); font-family:var(--serif); font-size:19px;
  line-height:1.35; color:var(--ink); min-height:1px;
}
.receipt { margin:0 2px var(--s3); font-size:12.5px; color:var(--muted); }

.hist { list-style:none; margin:0; padding:0; display:grid; gap:var(--s3); }
.hist-item {
  background:var(--card); border:1px solid var(--line); border-left:4px solid var(--miss);
  border-radius:var(--r-card); padding:var(--s4); box-shadow:var(--shadow-1);
}
/* An item she has since fixed must not keep wearing the "look here" edge —
   that is D1's whole point rendered as one border colour. */
.hist-item.is-settled { border-left-color:var(--mint); }
.hist-item.is-stuck { border-left-color:var(--lavender); background:var(--card-warm); }

.hist-head { display:flex; align-items:baseline; justify-content:space-between; gap:var(--s3); flex-wrap:wrap; }
.hist-title { flex:1; min-width:0; }
.hist-title b { display:block; font-size:13px; color:var(--plum-ink); font-weight:700; letter-spacing:.3px; }
.stem-trim { font-family:var(--serif); font-size:16.5px; color:var(--ink); line-height:1.4; }
.hist-count {
  flex:none; font-size:12px; font-weight:700; letter-spacing:1.2px; text-transform:uppercase;
  color:var(--miss); background:var(--miss-soft); border:1px solid var(--miss-line);
  border-radius:var(--r-pill); padding:4px 11px;
}

/* :empty is load-bearing, not tidiness. Most items have no chips at all, and a
   flex row with a margin and no children still reserves its margin — which
   rendered as a dead band between the stem and the disclosure on every
   unchipped card. Found by looking at a screenshot; invisible in the markup. */
.chips { display:flex; flex-wrap:wrap; gap:var(--s1); margin-top:var(--s2); }
.chips:empty { display:none; }
.chip {
  font-size:12.5px; color:var(--ink-2); background:var(--bg-2);
  border-radius:var(--r-pill); padding:3px 11px;
}
.chip-settled { background:var(--ok-soft); color:var(--mint-ink); font-weight:600; }
.chip-gave { background:var(--miss-soft); color:var(--miss); }
.chip-stuck { background:var(--cream); color:var(--plum-ink); font-weight:600; }

/* <details> rather than a hand-rolled disclosure: the keyboard contract, the
   ARIA state and the SR announcement all come free and correct. */
.hist-detail { margin-top:var(--s3); }
.hist-detail summary {
  cursor:pointer; font-size:13.5px; color:var(--plum-ink); font-weight:600;
  padding:var(--s1) 0; min-height:32px; display:flex; align-items:center; gap:var(--s2);
}
/* display:flex removes the native disclosure triangle in Chromium, which left
   the control with no open/closed affordance at all — it read as a plain link.
   Own marker, rotated by state, hidden from the a11y tree because <details>
   already announces expanded/collapsed and a second announcement is noise. */
.hist-detail summary::-webkit-details-marker { display:none; }
.hist-detail summary::marker { content:""; }
.hist-detail summary::before {
  content:""; flex:none; width:0; height:0;
  border-left:5px solid currentColor;
  border-top:4px solid transparent; border-bottom:4px solid transparent;
  transition:transform .15s ease; transform-origin:25% 50%;
}
.hist-detail[open] summary::before { transform:rotate(90deg); }
.hist-detail summary:focus-visible { outline:3px solid var(--plum-ink); outline-offset:3px; border-radius:var(--r-ctl); }
.detail-body { padding-top:var(--s3); border-top:1px solid var(--line); margin-top:var(--s1); }
.why-line { margin:0 0 var(--s3); font-size:14px; color:var(--miss); }
.why-line b { color:var(--ink-2); }
.stem-full { font-family:var(--serif); font-size:16.5px; line-height:1.5; color:var(--ink); margin:0 0 var(--s3); }
.opts-review { list-style:none; margin:0 0 var(--s3); padding:0; display:grid; gap:var(--s2); counter-reset:none; }
.opts-review li { font-size:14.5px; color:var(--ink-2); padding-left:var(--s4); position:relative; }
.opts-review li .lbl { font-weight:700; color:var(--muted); position:absolute; left:0; }
.opts-review li.is-key { color:var(--mint-ink); font-weight:600; }
.opts-review li.is-key .lbl { color:var(--mint-ink); }
.opts-review li .why { display:block; font-weight:400; color:var(--muted); font-size:13.5px; margin-top:2px; }
.hint-block { margin:0 0 var(--s3); }
.hint-block h4 { font-family:var(--serif); font-size:15px; margin:0 0 var(--s2); color:var(--ink); font-weight:600; }
.receipt-line { margin:var(--s3) 0 0; font-size:12.5px; color:var(--muted); }
.item-disclaimer { margin:var(--s2) 0 0; font-size:11.5px; color:var(--muted); font-style:italic; }
.cap-note { margin:var(--s3) 2px 0; font-size:13px; color:var(--muted); }

/* take.html's truncation notice — lives outside #screen so it survives every
   question re-render. */
.set-note {
  margin:0 0 var(--s3); padding:var(--s2) var(--s3);
  background:var(--card-warm); border:1px solid var(--line);
  border-radius:var(--r-ctl); font-size:13px; color:var(--ink-2);
}

/* ── A6/D2 — the "this page never booted" notice ───────────────────────────
   Hidden at rest; revealed by take.html's <noscript> block or by the classic
   watchdog's `data-noboot`.
   ⚖ DELIBERATELY NOT RED, and that is the one design call in it. Universe B's
   copy of this notice borrows .status .bad's warning trio, because on the
   account pages red means "that did not work". Here red means "you answered
   wrongly" — --miss paints exactly that — so dressing a structural failure in
   it would tell a student she got something wrong on a page that never asked
   her anything. Full --ink on --card-warm instead: quiet vocabulary, loud
   weight. */
.noboot-note {
  display:none;
  margin:0 0 var(--s4); padding:var(--s3) var(--s4);
  background:var(--card-warm); border:1px solid var(--line-2);
  border-radius:var(--r-card); font-size:15px; color:var(--ink);
}

.noboot-note strong { display:block; margin-bottom:var(--s1); }

[data-noboot] .noboot-note { display:block; }

/* ⚠ FOUND BY LOOKING AT THE RENDER, WITH EVERY GATE GREEN — the same way A1's
   white-blob seal was found, and the reason a board exists at all. take.html's
   save-status line is a bullet plus a text span, and the module is what fills
   the span; with the module dead it painted a single disconnected dot floating
   under the notice. Harmless, and it reads as a rendering fault, which is
   precisely what this state must not do: the whole point of the notice is that
   the page looks deliberate rather than broken. Machinery that will never run
   should not leave a fragment on screen. */
[data-noboot] .status-line { display:none; }

/* ── responsive ────────────────────────────────────────────────────────── */
@media (max-width:600px) {
  .filter-row { grid-template-columns:1fr; }
  .filters { padding:var(--s3); }
  .hist-item { padding:var(--s3); }
  .hist-count { align-self:flex-start; }
  .result-line { font-size:17px; }
  .page { padding:16px 14px 56px; }
  .greeting h1 { font-size:27px; }
  .hero { padding:var(--s4); }
  .hero-seal, .ekg { display:none; }
  .hero h2 { font-size:23px; }
  .q-card { padding:var(--s4) var(--s3) var(--s3); }
  .q-card .stem { font-size:19px; }
  .reasons { grid-template-columns:1fr; }
  .q-foot { justify-content:stretch; }
  .q-foot .btn { flex:1; }
  .score .num { font-size:44px; }
}

/* ── the generation screen (CP3D) ──────────────────────────────────────────
   Extends S4's .field vocabulary rather than starting a second form language:
   .field label and .field select are already defined above and are reused
   verbatim, so a control means the same thing on both screens. */
.gen-form { display:grid; gap:var(--s4); }
.field-hint { margin:0 0 var(--s2); font-size:13.5px; color:var(--muted); max-width:56ch; }
/* The hint sits ABOVE the control it describes for the textarea (where it sets
   an expectation before she starts typing) and below for the short fields
   (where it is an example, read after). Both are wired with aria-describedby,
   so the order on screen is a reading decision, not an accessibility one. */
.field textarea,
.field input[type="text"] {
  font:inherit; font-size:15px; color:var(--ink); width:100%;
  border:1px solid var(--line-2); border-radius:var(--r-ctl); background:var(--field);
  padding:var(--s2) var(--s3);
}
.field textarea { min-height:210px; resize:vertical; line-height:1.55; }
.field input[type="text"] { min-height:44px; }
.field textarea:focus-visible,
.field input[type="text"]:focus-visible { outline:3px solid var(--plum-ink); outline-offset:2px; }
.field select { max-width:22ch; }

.opt-tag {
  font-size:11px; font-weight:600; letter-spacing:.08em; color:var(--muted-sunk);
  background:var(--bg-2); border-radius:var(--r-pill); padding:2px 8px; margin-left:6px;
  text-transform:uppercase;
}

/* Quiet by default and never alarming: the "over" state is still information,
   not an error — the blocking message is raised on submit, next to the field. */
.field-note { margin:var(--s1) 0 0; font-size:13px; min-height:1.2em; color:var(--muted); }
.field-note[data-state="over"] { color:var(--miss); font-weight:600; }

/* ── "add a file" (CP3E E2+E3) ─────────────────────────────────────────────
   The file input is the REAL control and stays a native <input type="file">:
   it keeps the native keyboard contract (Tab to it, Enter/Space opens the
   picker) that a <button>+.click() shim would have to re-implement. It is
   visually hidden and its <label> is the visible skin, which is why the ring
   is drawn on the LABEL from the input's own :focus-visible — hiding the
   focusable element without moving its ring is how this pattern usually ships
   broken. Drag-and-drop is an addition on top, never the only way in. */
.import-row { display:flex; flex-wrap:wrap; align-items:center; gap:var(--s2); margin-bottom:var(--s2); }
/* ⚠ `.import-row` is on the selector to WIN, not for tidiness. `.field label`
   (line 401) is specificity 0-1-1 and paints every label in this form as a
   small uppercase caption — it outranked a bare `.file-btn` (0-1-0), so the
   button shipped as a 12px uppercase caption sitting ABOVE an empty rounded
   box. Every DOM and a11y assertion passed on it: the element existed, was
   named, was focusable, and measured 80x44. Only the screenshot showed it.
   Same class of miss as CP3D's `sample.meta.title`. */
.import-row .file-btn {
  font-family:var(--sans); font-size:14.5px; font-weight:600; cursor:pointer;
  /* Explicit resets, not inheritance: raising specificity only decides who
     wins a property BOTH rules set. `.field label` also sets text-transform
     and letter-spacing, which this rule never mentioned — so the button kept
     rendering in caps at the right size, which is the half-fixed state the
     computed-style gate caught. Override what you are overriding. */
  text-transform:none; letter-spacing:normal; margin-bottom:0;
  display:inline-flex; align-items:center; gap:8px;
  min-height:44px; padding:11px 18px;
  border:1.5px solid var(--line-2); border-radius:var(--r-ctl);
  background:var(--glass); color:var(--primary-ink);
  transition:border-color .16s, background .16s;
}
.import-row .file-btn:hover { border-color:var(--lavender); background:var(--raised); }
.file-pick:focus-visible + .file-btn { outline:3px solid var(--plum-ink); outline-offset:2px; }
.file-kinds { font-size:13px; color:var(--muted); }

/* The drop target is the textarea's own wrapper, so the thing that lights up
   is the thing the text lands in. */
.drop-wrap { position:relative; border-radius:var(--r-ctl); }
.drop-wrap.is-over { outline:3px dashed var(--plum-ink); outline-offset:3px; }
.drop-wrap.is-over::after {
  content:"Drop it here"; position:absolute; inset:0; display:grid; place-items:center;
  background:var(--glass-strong); color:var(--plum-ink);
  font-weight:600; border-radius:var(--r-ctl); pointer-events:none;
}

/* What we read out of her file. Bigger than a .field-note because it carries
   two sentences — what happened, and what to do next — and the no-text state
   is a destination rather than an error, so it must not look like one. */
.import-note { margin:0 0 var(--s2); font-size:13.5px; line-height:1.5; color:var(--ink-2); }
.import-note:empty { display:none; }
.import-note p { margin:0; }
.import-note .next { margin-top:6px; color:var(--muted); }
/* R-E18. Keyed off its OWN attribute rather than a state, because a machine-read
   file can be `ok`, `partial` or over the cap — the fact is orthogonal to the
   verdict. Without this the warning would land as unboxed grey text on the very
   case that needs it most: a clean-looking `ok` file whose words are wrong. */
.import-note .warn { margin-top:6px; color:var(--ink-2); }
.import-note[data-state="no-text"],
.import-note[data-state="partial"],
.import-note[data-recognized="true"] {
  background:var(--card-warm); border:1px solid var(--line-2);
  border-radius:var(--r-ctl); padding:var(--s3);
}
/* ⚖️ BOXED SINCE MULTI-FILE, where it stopped being a remark about the one file
   on screen and became a REFUSAL that has to name which of several files was
   turned away. Loose bold text was findable when there was exactly one possible
   subject; among four rows it has to be the thing the eye lands on. */
.import-note[data-state="over"] {
  color:var(--miss); font-weight:600;
  background:var(--miss-soft); border:1px solid var(--miss-line);
  border-radius:var(--r-ctl); padding:var(--s3);
}
.import-note[data-state="over"] .next { color:var(--ink-2); font-weight:400; }
.import-note[data-state="bad"] { color:var(--miss); }

/* Which pages come IN. An import control, not sectioning (R-E10) — nothing
   here generates or spends anything. */
.pages-pick {
  margin:0 0 var(--s3); padding:var(--s3);
  background:var(--bg-2); border:1px solid var(--line-2); border-radius:var(--r-ctl);
}
.pages-pick p { margin:0 0 var(--s2); font-size:13.5px; color:var(--ink-2); }
.pages-row { display:flex; flex-wrap:wrap; align-items:flex-end; gap:var(--s2); }
.pages-row label { font-size:13px; font-weight:600; color:var(--ink-2); display:block; margin-bottom:4px; }
.pages-row input[type="number"] {
  font:inherit; font-size:15px; width:6.5ch; min-height:44px; color:var(--ink);
  border:1px solid var(--line-2); border-radius:var(--r-ctl); background:var(--field);
  padding:var(--s2);
}
.pages-row input[type="number"]:focus-visible { outline:3px solid var(--plum-ink); outline-offset:2px; }
.pages-note { margin:var(--s2) 0 0 !important; font-size:13px; color:var(--muted); }
.pages-note[data-state="over"] { color:var(--miss); font-weight:600; }

/* ── the source list (multi-file import) ───────────────────────────────────
   Promoted verbatim out of app/mockups/multifile-import.html, which is the
   picture this was built to — same reason the token block at the top of this
   file was promoted out of the study-room comp: a component that lives only in
   a mockup drifts from the thing that shipped, and then neither is the record.

   WHAT THE LIST IS. Not a widget with its own state — a RENDERING of the
   textarea below it, parsed by app/js/nurseal-sources.js on every edit. Every
   row is a claim about a stretch of that box, which is why the row can never
   say something the box will not send. */
.fl-lead {
  font-size:12px; font-weight:700; letter-spacing:.09em; text-transform:uppercase;
  color:var(--muted); margin:0 0 var(--s2);
}
.fl { list-style:none; margin:0; padding:0; display:grid; gap:var(--s2); }
.fl-item {
  display:grid; grid-template-columns:auto 1fr; gap:var(--s1) var(--s3);
  background:var(--card); border:1px solid var(--line-2); border-radius:var(--r-ctl);
  padding:var(--s3);
}
/* The order number is not decoration: order is the order the material sits in
   the box, and therefore the order the generator reads it. A list you can
   reorder has to say what the order IS. */
.fl-n {
  font-family:var(--mono); font-size:12.5px; font-weight:700;
  width:24px; height:24px; border-radius:var(--r-pill);
  display:grid; place-items:center; margin-top:2px;
  background:var(--bg-2); color:var(--plum-ink);
}
.fl-body { min-width:0; }
.fl-name { margin:0; font-size:15px; font-weight:600; color:var(--ink); overflow-wrap:anywhere; }
.fl-meta { margin:2px 0 0; font-size:13px; color:var(--muted); }
.fl-meta b { color:var(--ink-2); font-weight:600; }
/* R-E18's machine-read warning, now per FILE rather than per page. Same warm
   treatment as .import-note[data-recognized] so it reads as the same fact — but
   it had to move onto the row, because section 12.12b measured her files at
   88.1% and 15.5% machine-read and one page-level sentence covering both would
   be wrong about one of them. */
.fl-warn {
  margin:var(--s2) 0 0; font-size:13px; line-height:1.5; color:var(--ink-2);
  background:var(--card-warm); border:1px solid var(--line-2);
  border-radius:var(--r-ctl); padding:var(--s2) var(--s3);
}
.fl-ctrls { grid-column:2; display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin-top:var(--s2); }
/* 36px in both axes with 8px gaps — above the house 32px floor and WCAG 2.5.8's
   spacing test, reached by padding rather than by shrinking to the glyph. */
.fl-btn {
  font-family:var(--sans); font-size:13.5px; font-weight:600; cursor:pointer;
  min-height:36px; padding:0 13px;
  display:inline-flex; align-items:center; gap:6px;
  border:1px solid var(--line-2); border-radius:var(--r-ctl);
  background:var(--glass); color:var(--primary-ink);
  transition:border-color .16s, background .16s;
}
.fl-btn:hover:not([disabled]) { border-color:var(--lavender); background:var(--raised); }
.fl-btn:focus-visible { outline:3px solid var(--plum-ink); outline-offset:2px; }
/* `.fl-btn.icon` was removed here 2026-08-11 with the reorder arrows, which
   were its only users. `[disabled]` stays: it is the arrows' old first/last-row
   state that is gone, not the ability to disable a control. */
.fl-btn[disabled] { opacity:.45; cursor:default; }
/* ⚠ Remove is separated by a FIXED 24px, not by a flex spacer. The mockup's
   first render used `flex:1` and it exiled Remove ~900px to the right edge on a
   1100px screen, where three of them stacked into a column of identical buttons
   whose only link to a filename was horizontal alignment. Separation is a
   distance, not "as far as possible" — and no assertion could see the
   difference. */
.fl-btn.rm { color:var(--miss); margin-left:var(--s4); }
/* ⚠ The bottom margin is not tidiness. #import-note sits directly below and is
   the same size and weight, so with no gap the total and the last import
   message rendered as ONE grey paragraph — "376 of 120,000 characters. Room for
   plenty more. recognized-mixed.pdf is now pages 1–1." Two unrelated
   sentences, read as one. Visible only in the screenshot; every assertion was
   already green. */
.fl-total { margin:var(--s3) 0 var(--s3); font-size:13.5px; color:var(--ink-2); }
.fl-total:empty { display:none; }
.fl-total b { color:var(--ink); font-weight:600; }
.fl-total[data-state="near"], .fl-total[data-state="over"] { color:var(--miss); }
/* The page picker, rendered INSIDE the row it belongs to. That placement is the
   whole of what "re-edit a range after it is already in" needed: a row already
   says which file it is about, so nothing has to be labelled or remembered.
   (A file that has NOT been added yet keeps the standalone #import-pages box
   below the list — the picker lives where the file lives.) */
.fl-pages {
  grid-column:2; margin-top:var(--s2); padding:var(--s3);
  background:var(--bg-2); border:1px solid var(--line-2); border-radius:var(--r-ctl);
}
.fl-pages p { margin:0 0 var(--s2); font-size:13px; color:var(--ink-2); }
@media (max-width:560px) {
  .fl-ctrls, .fl-pages { grid-column:1 / -1; }
  /* At 390px the control row wraps anyway, so the 24px separation is already
     carried by the line break; keeping it as well orphaned Remove onto a
     right-aligned row of its own. */
  .fl-btn.rm { margin-left:0; }
}

/* CP3C/T3 — the wheel-spin exit door. Boxed like the import warning rather than
   left as loose grey text: it is the one notice on this page that reports a
   pattern in HER work rather than a fact about the form, and it appears only
   when her log actually shows one. Warm, never red — "you have missed this ten
   times" is already the discouraging half, and learning-scientist's affect rule
   is that a bad run ends on the next winnable rep, not on an alarm. */
.stuck {
  margin:0 0 var(--s3); padding:var(--s3);
  background:var(--card-warm); border:1px solid var(--line-2);
  border-radius:var(--r-ctl);
}
.stuck[hidden] { display:none; }
.stuck-line { margin:0 0 var(--s2); font-size:13.5px; line-height:1.5; color:var(--ink-2); }

/* R-E19 — the free revisit row, under "How many questions?".

   ⚠ TWO ROWS WEAR THIS CLASS SINCE 2026-08-24, and the second is N10's "Add
   patient charts". Named here because the class is called `.revisit` and a
   reader editing it for the revisit row would otherwise move a case-study row
   without knowing it existed. The sharing is deliberate — both are "add
   something to this test" checkboxes in a form about this paste, and a second
   ground would make two answers to the same kind of question look like two
   kinds of thing — but it means every rule below is now a TWO-consumer rule.
   ⛔ The chart row is NOT hidden at load; only `.revisit#revisit` is, because
   only its label makes a claim about her data. So `[hidden]` below is a
   one-consumer rule inside a two-consumer block.

   Deliberately the SAME sunk-panel vocabulary as .stuck directly above it
   (--card-warm on --line-2): both are notices about HER history sitting inside
   a form about this paste, and giving the second one its own ground would make
   two things that answer the same kind of question look like two kinds of
   thing. It is the row's one visual decision.

   ⚠ --ok is the green, on --card-warm rather than on --bg-2, and that ground is
   why it can be --ok at all: A5 measured --mint-ink at 4.19:1 over --bg-2, under
   the 4.5 floor, which is the same class of miss that made --muted-sunk and
   --flag-ink exist. Gate 8 in a0-token-gate is the authority on all four cells,
   not this comment. */
.revisit {
  margin:var(--s2) 0 0; padding:var(--s3);
  background:var(--card-warm); border:1px solid var(--line-2);
  border-radius:var(--r-ctl);
  display:flex; gap:var(--s2); align-items:flex-start;
}
.revisit[hidden] { display:none; }
/* Not shrunk below the native box: the checkbox IS the control, and a 16px
   target beside a two-line label is the one thing on this row a thumb has to
   hit. `flex:none` stops the label's wrap from squeezing it. */
.revisit input[type="checkbox"] { flex:none; width:16px; height:16px; margin:2px 0 0; }
.revisit-txt { font-size:13px; line-height:1.5; color:var(--ink-2); }
/* ⚠ EVERY RESET HERE IS DELIBERATE AND THE FILE ALREADY RECORDS WHY, sixty
   lines up at `.import-row .file-btn`: `.field label` paints EVERY label in
   this form as a 12px uppercase caption, and it sets properties a shorter rule
   never mentions. The first version of this row set font-weight and cursor
   alone and shipped "ADD 3 QUESTIONS YOU GOT WRONG BEFORE — FREE, NO CREDITS"
   in shouting caps, reading as a section heading rather than as the label of
   the checkbox beside it. Every gate was green; only the screenshot showed it,
   which is the third time this exact rule has done this to this file.
   ⛔ Override what you are overriding — inheritance is not the question, and
   raising specificity only decides who wins a property BOTH rules set. */
.revisit-txt label {
  display:block; cursor:pointer;
  font-size:13px; font-weight:600; letter-spacing:normal; text-transform:none;
  color:var(--ink); margin-bottom:0; padding:0;
}
.revisit-sub { display:block; color:var(--muted); font-size:12px; margin-top:2px; }
/* One name for "this costs nothing", used in the count note (on --card) and in
   the row above (on --card-warm). A second spelling of the same claim is how
   two surfaces end up disagreeing about whether a thing is free. */
.free { color:var(--ok); font-weight:700; }

.tune { border-top:1px solid var(--line); padding-top:var(--s3); }
.tune > summary {
  cursor:pointer; font-size:14.5px; color:var(--plum-ink); font-weight:600;
  /* A summary is a native disclosure — keyboard-operable for free. Padding, not
     a min-height, so the hit target grows without the marker drifting. */
  padding:var(--s2) 0; list-style-position:inside;
}
.tune > summary:focus-visible { outline:3px solid var(--plum-ink); outline-offset:3px; border-radius:var(--r-ctl); }
.tune-now { color:var(--muted); font-weight:400; }

/* The two doors. Bordered in blush rather than the card line, because this is
   the one place on the page where the next tap decides whether she spends. */
.doors {
  border-color:var(--miss-line); background:var(--miss-soft);
  display:grid; gap:var(--s2); margin-top:var(--s4);
}
.doors-head { margin:0; font-size:15.5px; color:var(--ink); }
.doors-sub { margin:0 0 var(--s2); font-size:14px; color:var(--ink-2); max-width:60ch; }
.doors-note { margin:var(--s2) 0 0; font-size:13px; color:var(--muted); max-width:60ch; }

.gate p { margin:0; font-size:15px; color:var(--ink-2); max-width:56ch; }
.gate p b { color:var(--ink); }

.gen-status { margin-top:var(--s4); font-size:15px; border-radius:var(--r-ctl); }
/* NOT display:none, and this is the whole reason the rule looks odd. This is
   the page's live region: it must be present AND rendered at load, because a
   region that is display:none until its first message is a region most
   screen-reader/browser pairs never announce — the same failure as creating the
   container and its text together. An empty block with no padding is already
   zero-height, so collapsing the margin gives the identical visual result
   without taking the region out of the accessibility tree. */
.gen-status:empty { margin:0; }
.gen-status.is-info { background:var(--bg-2); color:var(--ink-2); padding:var(--s3) var(--s4); }
.gen-status.is-warn { background:var(--card-warm); color:var(--ink); padding:var(--s3) var(--s4); border:1px solid var(--line-2); }
.gen-status.is-bad  { background:var(--miss-soft); color:var(--miss); padding:var(--s3) var(--s4); border:1px solid var(--miss-line); }
.gen-status a { color:inherit; }

@media (max-width:560px) {
  .field textarea { min-height:170px; }
  .field select { max-width:100%; }
  .doors .btn { flex:1; }
}

/* ── your tests (step 33) ──────────────────────────────────────────────────
   Promoted verbatim out of app/mockups/your-tests.html once Sami approved that
   direction, the same way the comp's tokens were promoted into this file — so
   the mockup and the shipped page cannot drift apart.
   Tokens for every colour. This block once carried ONE stated exception —
   `color:#fff` on the solid pill, "there is no white token to point at yet" —
   named on purpose rather than claimed away, because the CP6 appearance audit
   had just found this stylesheet's header claiming "zero raw hex outside :root"
   while raw literals sat below it. ✅ Closed by appearance/A0 (2026-08-14):
   `--white` exists, and the header's claim is now enforced by a gate instead of
   asserted by a sentence. 🔑 Worth keeping the shape of what happened — the
   honest exception was written down, and it still grew, because a comment
   cannot count. What stopped it was a check that runs. */

.tl { list-style:none; margin:0; padding:0; display:grid; gap:var(--s3); }
.tl-item {
  background:var(--card); border:1px solid var(--line); border-left:4px solid var(--lavender-2);
  border-radius:var(--r-card); padding:var(--s4); box-shadow:var(--shadow-1);
}
/* A test she made and never opened is the only row asking for something. Warm
   card + a stronger edge is the vocabulary .hist-item.is-stuck already uses for
   "this row wants you", reused rather than reinvented. It is deliberately NOT a
   status colour scale: a low score must never wear an alarm edge, which is
   learning-scientist's standing rule on this product. */
.tl-item.is-new { background:var(--card-warm); border-left-color:var(--plum); }

.tl-head { display:flex; align-items:flex-start; justify-content:space-between; gap:var(--s3); flex-wrap:wrap; }
.tl-when { flex:1; min-width:0; }
.tl-date { display:block; font-family:var(--serif); font-size:17px; color:var(--ink); line-height:1.3; }
.tl-facts { display:block; font-size:13px; color:var(--muted); margin-top:2px; }

/* The number, and it is deliberately not a percentage. "4 of 5" is the thing
   she can check against the five questions in front of her; 80% is a derivation
   she cannot. Same rule as history.html's hand-checkable numbers. */
.tl-score {
  flex:none; text-align:right; font-family:var(--serif); font-size:22px; color:var(--ink);
  line-height:1.15;
}
.tl-score .tl-of { color:var(--muted); }
.tl-score small {
  display:block; font-family:var(--sans); font-size:11.5px; font-weight:700;
  letter-spacing:1.1px; text-transform:uppercase; color:var(--muted); margin-top:3px;
}
.tl-state {
  flex:none; font-size:12px; font-weight:700; letter-spacing:1.1px; text-transform:uppercase;
  border-radius:var(--r-pill); padding:5px 12px;
  color:var(--plum-ink); background:var(--bg-2); border:1px solid var(--line-2);
}
.tl-state.is-new { color:var(--on-primary); background:var(--primary); border-color:var(--primary); }

.tl-topics { margin:var(--s3) 0 0; font-size:14.5px; color:var(--ink-2); line-height:1.5; }
.tl-more { color:var(--muted); white-space:nowrap; }
.tl-line { margin:var(--s2) 0 0; font-size:13.5px; color:var(--muted); }
/* :empty is load-bearing here for the same reason it is on .chips — a paragraph
   with a margin and no text still reserves its margin, which renders as a dead
   band on every row whose redundant date line was suppressed. */
.tl-line:empty { display:none; }
.tl-doors { margin-top:var(--s3); }
.tl-doors .btn { padding:11px 18px; min-height:44px; font-size:14.5px; }

/* ── today's setting (CP3C / T4) ───────────────────────────────────────────
   The B+C graft: C's one-answer card on top, B's walk behind the fold, and the
   correction at the end of the walk where the disagreement forms.
   Tokens only — no raw hex, which is the precondition the CP6 appearance audit
   found this stylesheet already failing elsewhere. Do not add one here. */

.today {
  background:var(--card); border:1px solid var(--line); border-radius:var(--r-card);
  box-shadow:var(--shadow-2); padding:var(--s6) var(--s5); margin-bottom:var(--s4);
  text-align:center;
}
.today .eyebrow { margin:0 0 var(--s2); }
.today h2 { font-family:var(--serif); font-size:34px; line-height:1.15; margin:0 0 var(--s3); color:var(--ink); }
.today h2 em { font-style:normal; color:var(--primary-ink); }
.today .sub { margin:0 auto var(--s4); max-width:50ch; color:var(--ink-2); }
.today .shut-line {
  margin:calc(var(--s4) * -1 + var(--s2)) auto var(--s4); max-width:50ch;
  color:var(--miss); font-size:14.5px;
}
.today .cta-row { justify-content:center; }
.today .adjust { display:inline-block; margin-top:var(--s3); font-size:14.5px; color:var(--muted); }
.today .adjust button {
  font:inherit; background:none; border:0; padding:0; cursor:pointer;
  color:var(--primary-ink); font-weight:600;
  text-decoration:underline; text-underline-offset:3px;
  /* A 44px target without a 44px box: the link sits inside a sentence, so
     padding would break the line. Vertical hit area comes from the line-height
     of the paragraph it lives in, which is why the paragraph is not tightened. */
  min-height:24px;
}

.due-strip {
  display:flex; gap:var(--s3); align-items:center; justify-content:space-between; flex-wrap:wrap;
  background:var(--card-warm); border:1px solid var(--line-2); border-radius:var(--r-card);
  padding:var(--s4); margin-bottom:var(--s5);
}
.due-strip p { margin:0; color:var(--ink-2); max-width:52ch; }
.due-strip b { color:var(--ink); }

.fold { border-top:1px solid var(--line); padding-top:var(--s4); }
.fold > summary {
  cursor:pointer; list-style:none; font-family:var(--serif); font-size:21px;
  color:var(--ink); display:flex; align-items:center; gap:var(--s2);
  min-height:44px;
}
.fold > summary::-webkit-details-marker { display:none; }
.fold > summary::after { content:"›"; font-size:26px; color:var(--muted); transition:transform .15s ease; }
.fold[open] > summary::after { transform:rotate(90deg); }
.fold > summary:focus-visible { outline:3px solid var(--plum-ink); outline-offset:3px; border-radius:4px; }
.fold-note { color:var(--muted); font-size:14.5px; margin:var(--s2) 0 var(--s4); max-width:62ch; }
.fold-note b { color:var(--ink-2); }

/* Her own corrections, ahead of the walk they changed. */
.own-note {
  margin:0 0 var(--s4); padding:var(--s3); border-radius:var(--r-ctl);
  background:var(--bg-2); color:var(--ink-2); font-size:14.5px; max-width:62ch;
}
.own-note b { color:var(--ink); }

.walk { list-style:none; margin:0; padding:0; }
.walk li {
  display:grid; grid-template-columns:76px 1fr auto; gap:var(--s3);
  align-items:center; padding:var(--s3) 0; border-bottom:1px solid var(--line);
}
.walk li:last-child { border-bottom:0; }
.w-date { font-size:13px; color:var(--muted); }
.w-body { min-width:0; }
.w-score { font-family:var(--serif); font-size:19px; color:var(--ink); }
.w-score .of { font-family:var(--sans); font-size:13.5px; color:var(--muted); }
.w-note { font-size:14px; color:var(--muted); margin:2px 0 0; }
.w-tag { font-size:13px; font-weight:600; border-radius:var(--r-pill); padding:4px 12px; white-space:nowrap; }
.w-tag.up { background:var(--ok-soft); color:var(--ok); }
.w-tag.down { background:var(--miss-soft); color:var(--miss); }
.w-tag.hold { background:var(--bg-2); color:var(--plum-ink); }
.w-tag.skip { background:transparent; color:var(--muted); font-weight:400; }
.empty-walk { color:var(--muted); font-size:14.5px; max-width:56ch; }

.disagree {
  margin-top:var(--s4); padding:var(--s4);
  background:var(--card-warm); border:1px solid var(--line-2); border-radius:var(--r-card);
}
.disagree h3 { font-family:var(--serif); font-size:19px; margin:0 0 var(--s1); color:var(--ink); }
.disagree > p { margin:0 0 var(--s3); color:var(--ink-2); font-size:14.5px; max-width:54ch; }

/* The picker. Native radios inside labels: a div wearing a role has to
   hand-roll the whole keyboard contract, and half-kept ARIA is worse than
   none — the same call history.html's filter made. */
.picker { margin:var(--s4) 0 0; padding-top:var(--s3); border-top:1px solid var(--line-2); }
.picker-lead { margin:0 0 var(--s3); color:var(--ink-2); font-size:14.5px; max-width:54ch; }
.lvl-opts { display:grid; gap:var(--s2); margin-bottom:var(--s4); }
.lvl-opt {
  display:grid; grid-template-columns:auto 1fr auto; gap:var(--s3); align-items:center;
  min-height:44px; padding:var(--s2) var(--s3); cursor:pointer;
  background:var(--card); border:1px solid var(--line-2); border-radius:var(--r-ctl);
}
.lvl-opt:hover { border-color:var(--lavender); }
.lvl-opt:has(input:checked) { border-color:var(--primary); box-shadow:inset 0 0 0 1px var(--primary); }
.lvl-opt:has(input:focus-visible) { outline:3px solid var(--plum-ink); outline-offset:2px; }
.lvl-opt input { width:18px; height:18px; accent-color:var(--primary); }
.lvl-word { font-weight:600; color:var(--ink); }
.lvl-n { font-size:13.5px; color:var(--muted); }

/* Present and EMPTY at load, then only ever text-swapped — a live region
   created with its message already inside is silently dropped by most
   screen-reader pairings. Same pattern as take.html's save line. */
.warn-line { margin:var(--s3) 0 0; color:var(--miss); font-size:14.5px; min-height:1em; }
.warn-line:empty { margin:0; }

@media (max-width:640px) {
  .today { padding:var(--s5) var(--s4); }
  .today h2 { font-size:27px; }
  .walk li { grid-template-columns:1fr auto; }
  .w-date { grid-column:1 / -1; }
}

/* ── reduced motion ────────────────────────────────────────────────────────
   A global safety net rather than a per-effect opt-out: anything added later
   is covered by default instead of relying on the next editor remembering. */
@media (prefers-reduced-motion:reduce) {
  *, *::before, *::after {
    animation-duration:.001ms !important; animation-iteration-count:1 !important;
    transition-duration:.001ms !important; scroll-behavior:auto !important;
  }
  .ekg path { stroke-dashoffset:0; }
}

/* ══ WAVE 2 / RUNG 1 — THE STUDY BUDDY ══════════════════════════════════════
   Built from app/mockups/study-buddy-d-merged.html, direction D, accepted by
   Sami 2026-08-11. Tokens only — no raw hex enters this block, including the
   fab's lift, which reuses --shadow-2 rather than inventing an rgba.

   THREE THINGS HERE THAT ARE NOT COSMETIC:

   * ⛔ THE DOOR IS NOW ABSENT ON A LIVE QUESTION, NOT SHUT — changed
     2026-08-21 by Sami's ruling. This bullet used to end "a locked state that
     names its key teaches the rule once; A MISSING BUTTON READS AS A BUG."
     Measured against the first real user, that is backwards: the SHUT button
     read as a bug — she pressed it, nothing happened, and she reported the
     buddy as broken. §9a's REASON is unchanged and still binding: a buddy
     beside an unanswered question is an answer key with extra steps — it
     defeats the hint ladder in one message and poisons the adaptive model,
     because a handed answer is indistinguishable from a known one. Only the
     advert is gone. .bd-door-shut is KEPT below rather than deleted, because
     it still styles nothing and removing a rule is how a future reader loses
     the receipt; .bd-door-empty is the new, collapsed state.
   * The `?` explainer opens on TAP as well as hover, and the price
     (.bd-cost) is always-visible static text beside the door. She studies on a
     phone, where hover does not exist — a hover-only price is invisible to
     exactly the person it protects.
   * .bd-tip is NOT absolutely positioned over the next card. The mockup had to
     reserve 190px of padding for it, because a static page draws every state
     at once; here it opens on demand, in flow, so it pushes rather than
     covers. That overlap was the one thing reading the render caught that no
     assertion did — and the fix belongs in the built version, not the padding.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── the after-the-test invitation ───────────────────────────────────────── */
.bd-invite {
  display:flex; flex-wrap:wrap; gap:var(--s3); align-items:center; justify-content:space-between;
  padding:var(--s4); border-left:4px solid var(--lavender);
}
.bd-invite-body { flex:1; min-width:240px; }
.bd-invite-lead { margin:0 0 4px; font-family:var(--serif); font-size:19px; color:var(--ink); }
.bd-invite-sub { margin:0; font-size:13.5px; color:var(--muted); max-width:52ch; }
.bd-invite-act { display:flex; flex-direction:column; align-items:flex-end; gap:6px; }

/* ── the door ────────────────────────────────────────────────────────────── */
.bd-door {
  position:relative;
  display:flex; align-items:center; gap:var(--s2); flex-wrap:wrap;
  margin-top:var(--s3); padding-top:var(--s3); border-top:1px solid var(--line);
}
.bd-ask, .bd-why {
  display:inline-flex; align-items:center; gap:var(--s2);
  min-height:44px; padding:0 var(--s3);
  background:transparent; border:1px solid var(--line-2); border-radius:var(--r-pill);
  font-family:var(--sans); font-size:14.5px; font-weight:600; color:var(--primary-ink);
  cursor:pointer;
}
/* 44px in BOTH axes on the icon-only control (WCAG 2.5.5), not merely tall — a
   20px glyph inside a 44px-high pill is still a 20px-wide target. */
.bd-why { min-width:44px; justify-content:center; padding:0 var(--s2); }
.bd-ask:hover, .bd-why:hover { border-color:var(--lavender); background:var(--card-warm); }
.bd-ask:focus-visible, .bd-why:focus-visible { outline:2px solid var(--plum); outline-offset:2px; }
.bd-ico {
  width:20px; height:20px; border-radius:50%; background:var(--badge-soft);
  color:var(--plum-ink); font-weight:700; font-size:13px;
  display:inline-flex; align-items:center; justify-content:center; flex:none;
}
.bd-cost { font-size:12.5px; color:var(--muted); }

/* ⚠ THE LOCKED STATE RENDERS NOTHING AS OF 2026-08-21. mountBuddy returns an
   empty node (see nurseal-buddy.js) so callers can still hold a door per item
   without every one of them deciding what "locked" looks like. It must occupy
   NO space — a bare .bd-door would still contribute its border-top and padding,
   i.e. a mystery rule floating under the question, which is a fainter version
   of the defect this change removes. */
.bd-door-empty.is-empty {
  display:none;
}

/* The shut state. Muted, never alarmed — a closed door is the tool's rhythm,
   not her mistake, and the label carries the unlock condition.
   ⚠ UNUSED since 2026-08-21 and deliberately kept: it is the receipt for what
   the locked state looked like, and Gate 4's "zero raw hex outside :root" sweep
   still covers it. Delete it only with the reason above it. */
.bd-door-shut { border-top-color:var(--line-2); }
.bd-ask.is-shut {
  background:var(--bg-2); border-style:dashed; color:var(--muted-sunk);
  cursor:default; font-weight:600;
}
.bd-ask.is-shut .bd-ico { background:var(--line); color:var(--muted-sunk); }

/* The QUIET state — a door on a question she got RIGHT first try with no hints
   (Sami widened the door 2026-08-12; see take.html and mountBuddy).

   ⚖️ It is quieter, never hidden and never smaller. The weight comes off the
   BORDER and the ink, so the offer recedes on a card that needs no help while
   staying a full 44px target — shrinking the control on the majority of a
   perfect run's cards would trade one defect for an access-warden one, and a
   5/5 run is exactly when every card carries this state.

   ⛔ .bd-cost is deliberately NOT muted further here. The price is the one
   thing that must read identically on both tones: a cheaper-looking button
   that costs the same credit is the shape of a dark pattern, and the whole
   reason the door was widened is that she cannot tell in advance which
   questions she actually understood. */
.bd-door-quiet { border-top-color:var(--line-2); }
.bd-door-quiet .bd-ask { border-color:var(--line); color:var(--muted); font-weight:500; }
.bd-door-quiet .bd-ask:hover { border-color:var(--lavender); color:var(--primary-ink); }

/* ── the ? explainer ─────────────────────────────────────────────────────── */
.bd-tip {
  flex-basis:100%; order:99;
  width:min(380px, 100%);
  background:var(--card); border:1px solid var(--line-2); border-radius:var(--r-ctl);
  box-shadow:var(--shadow-2); padding:var(--s3); margin-top:var(--s2);
}
.bd-tip b { display:block; font-size:12px; font-weight:700; letter-spacing:1.2px;
            text-transform:uppercase; color:var(--plum-ink); margin-bottom:var(--s2); }
.bd-tip p { margin:0 0 var(--s2); font-size:13.5px; line-height:1.5; color:var(--ink-2); }
.bd-tip p:last-child { margin-bottom:0; }
.bd-tip p b { display:inline; font-size:inherit; letter-spacing:0; text-transform:none; color:var(--ink); }

/* ── the panel ───────────────────────────────────────────────────────────── */
.bd-panel { flex-basis:100%; order:100; }
/* ⚠ NO BORDER-TOP HERE, and the mockup had one. In the mockup the "Looking
   at:" line was the first thing in an already-open card, so it needed its own
   rule; in the built version `.bd-door` always sits directly above it and
   brings one — so the mockup's version painted TWO separators with a stranded
   `?` floating between them. Caught by reading the render, not by an
   assertion, which is the third time this batch. */
.bd-context {
  margin:var(--s2) 0 0;
  font-size:12.5px; color:var(--muted); display:flex; align-items:center; gap:6px;
}
/* An open panel leaves only the `?` on the door row, so the row should stop
   reserving button-sized height around it. */
.bd-door:has(.bd-panel:not([hidden])) { padding-bottom:0; gap:var(--s2); }
.bd-context b { color:var(--ink-2); font-weight:600; }
.bd-seal { color:var(--lavender); font-size:11px; }

.bd-thread { margin-top:var(--s3); display:grid; gap:var(--s3); }
.bd-turn p { margin:0 0 var(--s2); font-size:14.5px; line-height:1.55; }
.bd-turn p:last-child { margin-bottom:0; }
.bd-her {
  justify-self:end; max-width:80%;
  background:var(--bg-2); border-radius:var(--r-ctl) var(--r-ctl) 4px var(--r-ctl);
  padding:var(--s2) var(--s3); color:var(--ink-2);
}
.bd-buddy {
  max-width:92%;
  background:var(--card-warm); border:1px solid var(--line);
  border-radius:var(--r-ctl) var(--r-ctl) var(--r-ctl) 4px;
  padding:var(--s3); color:var(--ink-2);
}
.bd-buddy b { color:var(--ink); font-weight:600; }
.bd-record {
  background:var(--miss-soft); border-left:3px solid var(--miss-line);
  border-radius:0 var(--r-ctl) var(--r-ctl) 0;
  padding:var(--s2) var(--s3); font-size:13.5px; color:var(--miss);
}
.bd-record b { color:var(--miss); }
.bd-verify { font-size:12.5px; color:var(--muted); font-style:italic; }
.bd-empty p { margin:0; font-size:13.5px; line-height:1.55; color:var(--muted); }
.bd-empty b { color:var(--ink-2); }

.bd-compose { display:flex; gap:var(--s2); margin-top:var(--s3); }
.bd-input {
  flex:1; min-width:0; min-height:44px; padding:0 var(--s3);
  border:1px solid var(--line-2); border-radius:var(--r-ctl);
  background:var(--card); font-family:var(--sans); font-size:15px; color:var(--ink);
}
.bd-input::placeholder { color:var(--muted); }
.bd-input:focus { outline:2px solid var(--plum); outline-offset:1px; border-color:var(--plum); }
/* Present and EMPTY when there is nothing to say, then only ever text-swapped.
   :empty collapses it so a quiet panel carries no ghost gap. */
.bd-status { margin:var(--s2) 0 0; font-size:13px; color:var(--muted); min-height:1em; }
.bd-status:empty { margin:0; min-height:0; }
.bd-status[data-state="offline"] { color:var(--miss); }
.bd-foot { margin:var(--s2) 0 0; font-size:12px; color:var(--muted); }

/* ── history: the corner button ──────────────────────────────────────────── */
/* STICKY, NOT ABSOLUTE, and not fixed either. Sami drew it as a corner button
   over a long filtered list; the list here scrolls with the PAGE rather than
   inside a pane, so an absolutely-positioned corner would sit at the bottom of
   a very tall box and be off screen almost always. Sticky pins it to the
   viewport for exactly as long as the results are in view and then lets it go
   — a fixed one would follow her onto the footer and the legal copy.

   pointer-events is off on the row and back on for the button, so the strip of
   empty space beside it never eats a click meant for a row underneath. */
.bd-corner {
  position:sticky; bottom:var(--s4); z-index:3;
  display:flex; justify-content:flex-end; margin:0 0 var(--s3);
  pointer-events:none;
}
.bd-corner .bd-fab { pointer-events:auto; }
.bd-pickwrap { margin:0 0 var(--s4); }
.bd-pickwrap > h3 { margin:0 0 var(--s2); font-family:var(--serif); font-size:17px; color:var(--ink); }
.bd-pickwrap > p { margin:0 0 var(--s2); font-size:13.5px; color:var(--muted); }
.bd-fab {
  display:inline-flex; align-items:center; gap:var(--s2);
  min-height:44px; padding:0 var(--s4);
  background:var(--primary); color:var(--card); border:0; border-radius:var(--r-pill);
  box-shadow:var(--shadow-2);
  font-family:var(--sans); font-size:14.5px; font-weight:600; cursor:pointer;
}
.bd-fab:hover { background:var(--primary-ink); }
.bd-fab:focus-visible { outline:2px solid var(--plum); outline-offset:3px; }
.bd-fab .bd-seal { color:var(--lavender-2); }

/* The picker the corner button opens. "Any of these" means she picks ONE and
   asks about it — Sami's own overrule of a count, 2026-08-11, and it is also
   what keeps a message's context to one question rather than fourteen, which
   matters because a conversation re-sends its context every turn. */
.bd-pick { margin-top:var(--s3); display:grid; gap:var(--s2); }
.bd-pick-btn {
  text-align:left; width:100%; min-height:44px;
  background:var(--card-warm); border:1px solid var(--line); border-radius:var(--r-ctl);
  padding:var(--s2) var(--s3); font-family:var(--sans); cursor:pointer; color:var(--ink);
}
.bd-pick-btn:hover { border-color:var(--lavender); }
.bd-pick-btn:focus-visible { outline:2px solid var(--plum); outline-offset:2px; }
.bd-pick-btn b { display:block; font-size:14.5px; font-weight:600; }
.bd-pick-btn span { display:block; font-size:13px; color:var(--muted); }

@media (max-width:520px) {
  .bd-her, .bd-buddy { max-width:100%; }
  .bd-compose { flex-wrap:wrap; }
  .bd-compose .btn { width:100%; }
  .bd-invite-act { align-items:stretch; width:100%; }
  .bd-tip { width:100%; }
}

/* ═══════════════════════ CP6/R4 — the credit surfaces ═══════════════════════
   The buy page (credits.html), the balance strip on the two pages where she
   SPENDS, the return-from-Stripe banner, and the low-balance warning inside a
   conversation. Built to app/mockups/checkout.html §1-§3, which Sami approved
   and redlined 2026-08-13.

   ⛔ NOT ONE NEW TOKEN, AND THAT IS A RESULT RATHER THAN LUCK. The mockup was
   written before the appearance system existed and carries five raw literals —
   `#fff` on the primary buttons and three tints for the warn/wait panels. Every
   one of them already has a semantic name here: `--on-primary` (A5's fix for
   exactly this — white text on a primary that goes pale lavender at night,
   ~2.2:1), and `--miss-soft` / `--miss-line` / `--ok-soft` for the panels. So a
   page added AFTER the four token blocks were written costs the appearance
   system nothing, which is the test of whether A0-A5 actually worked.

   ⚠ 44px, not 22px, on every control here. access-warden's note on the mockup
   stands: a bare text link is the easiest control in any design to leave under
   the touch minimum, because nothing about it looks small — and this is the one
   screen in the product where a mis-tap costs money. */

.co-tiers {
  display:grid; grid-template-columns:repeat(auto-fit, minmax(260px, 1fr));
  gap:var(--s3); margin:var(--s4) 0 0; align-items:start;
}
.co-tier {
  background:var(--card); border:1px solid var(--line); border-radius:var(--r-ctl);
  padding:22px 20px; display:flex; flex-direction:column; gap:12px; position:relative;
}
/* De-emphasise the loser, never inflate the winner — no scale transform and no
   drop-shadow arms race. The featured tier gets a border and a word. */
.co-tier.is-featured { border-color:var(--primary); border-width:2px; padding:21px 19px; }
.co-flag {
  position:absolute; top:-11px; left:20px; background:var(--primary); color:var(--on-primary);
  font-size:11px; font-weight:700; letter-spacing:1.4px; text-transform:uppercase;
  padding:4px 10px; border-radius:var(--r-pill);
}
.co-name { font-family:var(--serif); font-size:20px; font-weight:600; color:var(--ink); margin:0; }
.co-price { display:flex; align-items:baseline; gap:8px; }
.co-price b { font-family:var(--serif); font-size:36px; font-weight:600; color:var(--ink); line-height:1; }
.co-price i { font-style:normal; color:var(--muted); font-size:13px; }
.co-credits { font-size:15px; color:var(--primary-ink); font-weight:600; }
.co-blurb { color:var(--ink-2); font-size:14px; line-height:1.55; margin:0; flex:1; }

.co-btn {
  display:inline-flex; align-items:center; justify-content:center; text-align:center;
  padding:12px 16px; border-radius:var(--r-ctl); font-weight:600; font-size:15px;
  font-family:var(--sans); text-decoration:none; cursor:pointer;
  border:1px solid var(--primary); color:var(--primary-ink); background:var(--card);
  min-height:44px; box-sizing:border-box; width:100%;
}
.co-btn.is-primary { background:var(--primary); color:var(--on-primary); }
.co-btn:hover { background:var(--raised); }
.co-btn.is-primary:hover { background:var(--primary-ink); }
.co-btn:focus-visible { outline:2px solid var(--primary); outline-offset:2px; }
/* A disabled buy button is a BUSY button here, never a refusal — the only time
   this class applies is the moment between the tap and the redirect. */
.co-btn[disabled] { opacity:.7; cursor:default; }
.co-foot { font-size:12px; color:var(--muted); text-align:center; margin:0; }

/* Shared features as ONE list, never a tick-matrix. The 08-12 ruling moved
   memory OUT of the tier line because the $65 tier already had it; three
   columns of identical ticks would silently re-imply the differentiation that
   ruling removed — the drawing arguing with the decision. */
.co-shared { margin:var(--s4) 0 0; padding:20px; background:var(--bg-2); border-radius:var(--r-ctl); }
.co-shared h3 { font-family:var(--serif); font-size:16px; margin:0 0 12px; color:var(--ink); font-weight:600; }
.co-shared ul {
  list-style:none; margin:0; padding:0; display:grid;
  grid-template-columns:repeat(auto-fit, minmax(230px, 1fr)); gap:8px 20px;
}
.co-shared li { display:flex; gap:9px; align-items:flex-start; font-size:14px; color:var(--ink-2); line-height:1.45; }
.co-tick { color:var(--ok); font-weight:700; flex:none; line-height:1.45; }

.co-ladder {
  margin:var(--s4) 0 0; padding:18px 20px; border:1px solid var(--line);
  border-radius:var(--r-ctl); background:var(--card);
}
.co-ladder h3 { font-family:var(--serif); font-size:16px; margin:0 0 6px; color:var(--ink); font-weight:600; }
.co-ladder p { margin:0 0 8px; font-size:14px; color:var(--ink-2); line-height:1.6; }
.co-ladder .co-btn { width:auto; min-width:220px; margin-top:var(--s2); }
/* CP6/R5 — the refund block reflects HER answer, so it gets a state hook. The
   accent is the only visual difference: an open window is good news and should
   read as such, while a closed one must stay calm rather than alarming — it is
   not an error, and a red-looking panel over "your credits do not expire" would
   contradict its own text. Both use existing tokens, so all four theme cells
   are answered without a new colour. */
.co-ladder[data-refund="eligible"] { border-left:3px solid var(--ok); }

/* The pre-purchase facts: tax, who handles the card, what the statement says,
   where the receipt lives. A list rather than a paragraph because each line is
   a separate thing she might go and check, and a reader scanning for one of
   them should not have to read the other three. */
.co-fine ul { list-style:none; margin:6px 0 0; padding:0; display:grid; gap:10px; }
.co-fine li { font-size:13px; color:var(--ink-2); line-height:1.55; padding-left:14px; position:relative; }
.co-fine li::before { content:"·"; position:absolute; left:2px; color:var(--muted); font-weight:700; }
.co-fine b { color:var(--ink); font-weight:600; }

.co-rungs { display:flex; gap:8px; flex-wrap:wrap; margin:10px 0 0; }
.co-rung {
  display:inline-flex; align-items:center; gap:6px; font-size:13px; background:var(--bg-2);
  border:1px solid var(--bg-2); border-radius:var(--r-ctl); padding:7px 11px; color:var(--ink-2);
}
.co-rung b { color:var(--primary-ink); }
/* A rung she can actually buy is a real button: same chip, plus the 44px floor
   and a focus ring. The informational one stays a span and is not a stop in the
   tab order — a control that does nothing must not be reachable. */
.co-rung.is-buy {
  min-height:44px; cursor:pointer; border-color:var(--primary); background:var(--card);
  font-family:var(--sans);
}
.co-rung.is-buy:hover { background:var(--raised); }
.co-rung.is-buy:focus-visible { outline:2px solid var(--primary); outline-offset:2px; }

/* ── the balance surface ──────────────────────────────────────────────────
   Three states of one component: healthy, low, empty. It is a STRIP, not a
   modal — a number she can glance at never has to interrupt her. */
.co-bal {
  display:flex; align-items:center; gap:14px; padding:14px 18px; background:var(--card);
  border:1px solid var(--line); border-radius:var(--r-ctl); flex-wrap:wrap;
}
.co-bal-n { font-family:var(--serif); font-size:26px; font-weight:600; color:var(--ink); line-height:1; }
.co-bal-l { font-size:13px; color:var(--muted); }
.co-meter { flex:1; min-width:140px; height:8px; background:var(--bg-2); border-radius:var(--r-pill); overflow:hidden; }
.co-meter i { display:block; height:100%; background:var(--primary); border-radius:var(--r-pill); }
.co-bal.is-low .co-meter i { background:var(--flag-ink); }
.co-bal.is-out .co-meter i { background:var(--line-2); }
.co-bal.is-low { border-color:var(--miss-line); }
.co-bal-hint { flex-basis:100%; margin:0; font-size:13px; color:var(--muted); line-height:1.5; }
.co-bal-a {
  font-size:14px; font-weight:600; color:var(--primary-ink); text-decoration:none;
  white-space:nowrap; min-height:44px; display:inline-flex; align-items:center; padding:0 14px;
  border-radius:var(--r-ctl); border:1px solid var(--line-2); background:var(--card);
}
.co-bal-a:hover { background:var(--raised); border-color:var(--primary); }
.co-bal-a:focus-visible { outline:2px solid var(--primary); outline-offset:2px; }
/* The compact form, for the two pages where she spends. Same component, one
   line, no meter — the full reading is one tap away on the credits page. */
.co-bal.is-strip { margin:0 0 var(--s3); padding:11px 14px; }
.co-bal.is-strip .co-bal-n { font-size:20px; }
.co-bal.is-strip .co-bal-hint { flex-basis:auto; flex:1; min-width:150px; }
.co-bal.is-strip .co-meter { display:none; }
/* The FIRST-TIME variant: no meter, no number, because she has never bought and
   a "0" would imply she spent something. Only the label survives from the
   balance layout, so it carries the weight the number used to. Existing tokens
   only - a0-token-gate forbids a raw hex here and is right to. */
.co-bal.is-first .co-bal-l { font-size:14px; font-weight:600; color:var(--ink); }
/* CP6/R7c — the FREE-TIER variant, and it is the meter's one honest home. The
   compact strip hides the meter above because a credit bank has no ceiling, so
   a bar would be a proportion of nothing. A free allowance is BOUNDED (40
   questions, 10 buddy messages), which is exactly what a bar is for.
   ⚠ MUST STAY BELOW `.co-bal.is-strip .co-meter`: both selectors are (0,3,0),
     so this only wins on source order. Moving it up silently re-hides the bar
     and the strip degrades to a number with an unexplained gap beside it. */
.co-bal.is-free .co-meter { display:block; }


/* ── the four states ──────────────────────────────────────────────────────
   Payment cancelled, paid-and-waiting, paid-and-landed, and out-of-credits
   mid-conversation. None of these are the happy path and all are reachable on
   day one. */
.co-msg {
  border-radius:var(--r-ctl); padding:16px; background:var(--card-warm);
  border:1px solid var(--line); margin:0 0 var(--s3);
}
.co-msg.is-warn { background:var(--miss-soft); border-color:var(--miss-line); }
.co-msg.is-wait { background:var(--ok-soft); border-color:var(--line); }
.co-msg p { margin:0 0 10px; font-size:14.5px; line-height:1.6; color:var(--ink-2); }
.co-msg p:last-child { margin:0; }
.co-msg strong {
  color:var(--ink); font-family:var(--serif); font-size:16px; font-weight:600;
  display:block; margin:0 0 6px;
}
.co-acts { display:flex; gap:9px; flex-wrap:wrap; margin:12px 0 0; }
.co-mini {
  font-size:13.5px; font-weight:600; padding:9px 14px; border-radius:var(--r-ctl);
  text-decoration:none; font-family:var(--sans); cursor:pointer;
  border:1px solid var(--primary); color:var(--primary-ink); background:var(--card);
  min-height:44px; box-sizing:border-box; display:inline-flex; align-items:center;
}
.co-mini.is-primary { background:var(--primary); color:var(--on-primary); }
.co-mini.is-quiet { border-color:var(--line-2); color:var(--ink-2); }
.co-mini:hover { background:var(--raised); }
.co-mini.is-primary:hover { background:var(--primary-ink); }
.co-mini:focus-visible { outline:2px solid var(--primary); outline-offset:2px; }

/* ── A11/F4 step 0.14 — the anon-recovery card, direction B ─────────────────
   `is-ask` IS A SCOPE, NOT A COLOUR. It paints no ground of its own: dropping
   `is-wait` is the point, so the card falls through to .co-msg's --card-warm
   instead of --ok-soft, which is this palette's SUCCESS ground and the wrong
   thing to say under a question about whose data this is.

   ⛔ IT EXISTS BECAUSE THE OBVIOUS EDIT LEAKS. The V6 rule below wants
   .co-mini.is-quiet, which is ALSO worn by nurseal-buddy.js:567 ("Back to my
   tests") and nurseal-credits.js:1091 (the return link). Neither surface was
   drawn for 0.13, photographed, or ruled on, and a global edit would have
   moved both silently. The comp scoped its own override to `.mk-v6` for
   exactly this reason and said so in its comment. */
.co-msg.is-ask .co-who { font-weight:600; color:var(--ink); }
.co-msg.is-ask .co-fine { font-size:12.5px; color:var(--muted); margin:10px 0 0; }
/* V6 — in NIGHT the quiet button's --line-2 border nearly disappears into the
   dark panel, and this is the SAFE action of the two. Direction B's whole
   claim is that neither action is recommended, so it takes the same border the
   claim button has rather than a merely-louder version of a quieter one. */
.co-msg.is-ask .co-mini.is-quiet { border-color:var(--primary); color:var(--primary-ink); }

/* ── CP6/R8 — the one-question survey card (nurseal-ask.js) ────────────────
   Its OWN scope, deliberately not `is-ask`. Like `is-ask` it paints no ground
   of its own, so the card falls through to .co-msg's --card-warm rather than
   --ok-soft (SUCCESS) or --miss-soft (which in this product means "you got
   this wrong"). What it must NOT inherit is the V6 rule above: that gives the
   quiet button the primary border because the recovery card's two actions are
   both irreversible and neither may be recommended. Here the quiet button is a
   decline among five answers, and it genuinely is the lesser action.

   ⚠ BUT NOT THE DEFAULT --line-2 EITHER. V6 measured that border nearly
   vanishing into the dark panel at night, and that finding is about legibility
   rather than about which card it was on. --muted is visible in both modes and
   still reads as secondary, which is the thing --line-2 could not do. */
.co-msg.is-survey .co-fine { font-size:12.5px; color:var(--muted); margin:10px 0 0; }
.co-msg.is-survey .co-mini.is-quiet { border-color:var(--muted); color:var(--ink-2); }
/* ⛔ THE DECLINE NEEDS ITS OWN GROUPING, AND THE NUMBER IS WHY. `.co-acts` is
   `margin:12px 0 0` with a 9px flex gap, so when the five options wrap the
   decline row lands just 3px further from the last answer than the answers are
   from each other — and at 375px "Somewhere else" and "No thanks" then stack as
   two same-shaped rows, so the decline reads as a SIXTH OPTION rather than as a
   refusal. Measured on the render at 375px; invisible at 820px, where all five
   sit on one row and the grouping is obvious. Geometry only, no colour. */
.co-msg.is-survey .co-acts + .co-acts { margin-top:18px; }
/* ⚠ The claim button carries an EMAIL ADDRESS, so its label is as long as the
   student's address — the comp used "genaya@example.com" and could not show
   this. Measured: a 51-character address is fine at 375px (313px button in a
   347px card) and OVERFLOWS THE CARD at 320px. It wraps rather than
   truncating, because the address is the one thing on this card that has to be
   recognisable — an ellipsised account name answers "is this yours?" with a
   shrug. `min-width:0` is load-bearing: a flex item's default `auto` floor
   refuses to shrink below its content and the wrap never happens. */
.co-msg.is-ask .co-mini { max-width:100%; min-width:0; overflow-wrap:anywhere; }

/* The low-balance banner. Sits ABOVE the transcript, spans it, and is visually
   quieter than a turn — it is information, not a participant in the chat. It
   persists while the condition holds and is never modal: it must not take the
   turn she is in the middle of. */
.co-warn {
  display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin:0 0 12px;
  padding:11px 14px; background:var(--card-warm); border:1px solid var(--line-2);
  border-left:3px solid var(--flag-ink); border-radius:var(--r-ctl);
  font-size:13.5px; color:var(--ink-2); line-height:1.5;
}
.co-warn b { color:var(--ink); font-weight:600; }
.co-warn .co-mini { margin-left:auto; }

@media (max-width:520px) {
  .co-tiers { grid-template-columns:1fr; }
  .co-ladder .co-btn { width:100%; min-width:0; }
  .co-warn .co-mini { margin-left:0; width:100%; justify-content:center; }
  .co-bal-a { width:100%; justify-content:center; }
}

/* ═══════════════ GROUP STUDY / T2 — the Study Table (table.html) ═══════════════
 *
 * Built to `app/mockups/group-study-t2.html`, the rule-3 comp Sami ruled off on
 * 2026-08-24: direction C (host-driven reveal, answered-counts mid-round,
 * scores only at the end) with TABLE TALK IN THE RAIL, always open — option 3a.
 * ⛔ 3b (chat behind a tab) was DECLINED and is not to be re-proposed as a
 *    "cleaner" refactor: it puts the remote person out of sight, which is the
 *    one thing map §2.5 says chat exists to fix.
 *
 * ⛔ ZERO RAW COLOUR OUTSIDE :root — Gate 4 sweeps every colour syntax, not just
 *    hex, so a `rgba(255,255,255,.7)` here would be caught too. Everything below
 *    resolves through the token layer, which is the only reason this surface
 *    arrives themed in all four cells instead of needing a second pass.
 * ══════════════════════════════════════════════════════════════════════════ */

.gs-wrap { max-width: 1080px; }

/* ⛔ A11/F2's FINDING, RE-CREATED ON A NEW PAGE AND CAUGHT BY LOOKING AT THE
 *    RENDER. At rest this page's status line has no text, and a bare 8px dot
 *    alone above the table reads as a broken loading element — on a surface
 *    whose whole job is to look like it is working. That cold-review finding
 *    is already in this project's record for take.html's fail() screens; the
 *    class transfers to any page that ships an empty live region.
 * ⛔ HIDE THE DOT, NEVER THE LINE. The <p> is an ARIA live region and must
 *    exist from page load — a region created together with its first message
 *    is silently dropped by most screen-reader pairs, so removing it to fix a
 *    visual defect trades a dot for an announcement she never hears.
 * ⚠ `visibility`, not `display`: the line keeps its height, so the table does
 *   not jump down the page the first time anything is said. */
.status-line[data-quiet="1"] .dot { visibility: hidden; }

/* ── the join card ─────────────────────────────────────────────────────────
 * ⛔ EVERY STRING SAYS "TABLE CODE", NEVER BARE "CODE". Measured: `code` is
 *    already this product's word for the 8-digit email sign-in code
 *    (signin.html, ~10 uses). The T0 board stated this rule in its own header
 *    and then broke it twice; `gs-t2-comp-shots` §A asserts it now. */
.gs-join { background: var(--card); border: 1px solid var(--line); border-radius: 16px; padding: 22px; max-width: 380px; }
.gs-join h2 { margin: 0 0 4px; font-size: 19px; }
.gs-join .gs-sub { font-size: 13.5px; color: var(--muted); margin: 0 0 18px; line-height: 1.55; }
.gs-field { margin: 0 0 14px; }
.gs-field label { display: block; font-size: 13px; font-weight: 600; color: var(--ink-2); margin: 0 0 6px; }
.gs-field input { width: 100%; box-sizing: border-box; min-height: 52px; border: 1px solid var(--line-2); border-radius: 10px;
  background: var(--paper, var(--card)); padding: 12px 13px; font: inherit; font-size: 16px; color: var(--ink); }
.gs-field input:focus-visible { outline: 3px solid var(--plum-ink); outline-offset: 2px; }
.gs-field input.is-bad { border-color: var(--miss-line); background: var(--miss-soft); }
.gs-code-input { letter-spacing: .28em; font-weight: 700; text-transform: uppercase; }
.gs-err { display: block; font-size: 12.5px; line-height: 1.45; color: var(--flag-ink); margin: 7px 0 0; }
.gs-fine { font-size: 12px; color: var(--muted); line-height: 1.55; margin: 14px 0 0; }

/* ── the table shell ──────────────────────────────────────────────────────── */
.gs-screen { background: var(--card); border: 1px solid var(--line); border-radius: 16px; overflow: hidden; }
.gs-top { display: flex; align-items: center; gap: 12px; padding: 12px 18px; border-bottom: 1px solid var(--line); background: var(--card-warm); flex-wrap: wrap; }
.gs-crumb { font-size: 13px; color: var(--muted); }
.gs-crumb b { color: var(--ink); font-weight: 600; }
.gs-spacer { flex: 1 1 auto; }
.gs-endctl { border:0; background:none; font:inherit; font-size:13px; color:var(--muted);
  text-decoration:underline; text-underline-offset:3px; cursor:pointer; padding:10px 6px; min-height:44px; }
.gs-code-pill { font-size: 13px; font-weight: 700; letter-spacing: .12em; padding: 5px 12px; border-radius: 999px;
  background: var(--bg-2); border: 1px solid var(--line-2); color: var(--ink-2); }
.gs-body { display: flex; align-items: stretch; }
.gs-main { flex: 1 1 auto; padding: 22px 24px; min-width: 0; }
.gs-rail { width: 236px; flex: 0 0 auto; border-left: 1px solid var(--line); background: var(--bg); display: flex; flex-direction: column; }
.gs-rail-pad { padding: 16px; }
.gs-rail-h { font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); margin: 0 0 10px; }

/* ── the question ─────────────────────────────────────────────────────────── */
.gs-meta { font-size: 12px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--muted); margin: 0 0 8px; }
.gs-stem { font-size: 17px; line-height: 1.5; color: var(--ink); margin: 0 0 16px; max-width: 62ch; }
.gs-opt { display: flex; gap: 10px; align-items: flex-start; width: 100%; box-sizing: border-box; text-align: left;
  padding: 13px 14px; border: 1px solid var(--line-2); border-radius: 12px; margin: 0 0 9px; background: var(--card);
  font: inherit; font-size: 15px; line-height: 1.45; color: var(--ink-2); min-height: 52px; cursor: pointer; position: relative; }
.gs-opt:hover { border-color: var(--lavender-2); }
.gs-opt:focus-visible { outline: 3px solid var(--plum-ink); outline-offset: 2px; }
.gs-opt[disabled] { cursor: default; }
.gs-opt .gs-key { flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%; border: 1px solid var(--line-2);
  background: var(--bg-2); color: var(--muted-sunk); font-size: 12px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; }
.gs-opt .gs-txt { flex: 1 1 auto; min-width: 0; }
.gs-opt.is-picked { border-color: var(--plum); background: var(--bg-2); }
.gs-opt.is-picked .gs-key { background: var(--plum); border-color: var(--plum); color: var(--on-primary); }

/* ── the reveal ───────────────────────────────────────────────────────────
 * ⛔ PINK MARKS YOUR OWN WRONG ANSWER AND NOTHING ELSE. The first comp draft
 *    painted a DISTRACTOR in miss-pink while the "your answer" pill sat on the
 *    correct option — accusing the reader of an answer they never gave, on a
 *    surface where pink already means *you got this wrong*. A distractor two
 *    strangers chose stays neutral and just carries its count. */
.gs-opt.is-right { border-color: var(--mint); background: var(--ok-soft); }
.gs-opt.is-right .gs-key { background: var(--mint-ink); border-color: var(--mint-ink); color: var(--on-primary); }
.gs-opt.is-yours-wrong { border-color: var(--miss-line); background: var(--miss-soft); }
.gs-opt.is-yours-wrong .gs-key { background: var(--miss-soft); border-color: var(--miss-line); color: var(--flag-ink); }
.gs-yours { position: absolute; right: 12px; top: -9px; font-size: 10px; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; background: var(--card); border: 1px solid var(--line-2); color: var(--muted); border-radius: 999px; padding: 2px 8px; }
.gs-tally { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; align-self: center; }
.gs-bar { width: 48px; height: 6px; border-radius: 999px; background: var(--line-2); overflow: hidden; }
.gs-bar i { display: block; height: 100%; background: var(--lavender); }
.gs-opt.is-right .gs-bar i { background: var(--mint-ink); }
.gs-tally .gs-n { font-size: 12px; font-weight: 700; color: var(--muted); font-variant-numeric: tabular-nums; min-width: 2.4em; text-align: right; }
.gs-why { margin: 16px 0 0; padding: 14px 16px; border-radius: 12px; background: var(--card-warm); border: 1px solid var(--line); }
.gs-why h3 { margin: 0 0 6px; font-size: 12px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--muted); }
.gs-why p { margin: 0; font-size: 15px; line-height: 1.55; color: var(--ink-2); max-width: 64ch; }
.gs-norank { margin: 13px 0 0; font-size: 12.5px; color: var(--muted); }

/* ── the roster ───────────────────────────────────────────────────────────── */
.gs-roster { list-style: none; margin: 0; padding: 0; }
.gs-roster li { display: flex; align-items: center; gap: 8px; padding: 8px 2px; font-size: 13.5px; color: var(--ink-2); border-bottom: 1px dashed var(--line); }
.gs-roster li:last-child { border-bottom: 0; }
.gs-roster .gs-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--mint-ink); flex: 0 0 auto; }
.gs-roster .gs-dot.is-away { background: var(--line-2); }
.gs-roster li.is-away { opacity: .55; }
.gs-roster .gs-pos { margin-left: auto; font-size: 12px; color: var(--muted); }
.gs-avatar { flex: 0 0 auto; width: 27px; height: 27px; border-radius: 50%; background: var(--badge-soft); color: var(--plum-ink);
  font-size: 11px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; }

/* ── table talk, IN THE RAIL (3a) ─────────────────────────────────────────
 * Hard rule 4: ≤280 characters, first names only, broadcast-only, live-state
 * only, never persisted, never sent to any model. There is deliberately no
 * scrollback store anywhere — the list below holds only what arrived while
 * this page has been open, and the copy says so. */
.gs-talk { border-top: 1px solid var(--line); display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
.gs-talk-h { padding: 13px 16px 8px; }
.gs-talk-h .gs-rail-h { margin: 0; }
.gs-talk-list { list-style: none; margin: 0; padding: 0 16px; display: flex; flex-direction: column; gap: 9px;
  max-height: 220px; overflow-y: auto; overflow-wrap: anywhere; }
.gs-talk-list li { font-size: 13px; line-height: 1.45; color: var(--ink-2); }
.gs-talk-list .gs-who { font-weight: 700; color: var(--plum-ink); }
.gs-talk-empty { font-size: 12.5px; color: var(--muted); line-height: 1.5; margin: 0; }
.gs-talk-form { display: flex; gap: 7px; margin: 12px 16px 8px; }
.gs-talk-form input { flex: 1 1 auto; min-width: 0; min-height: 44px; box-sizing: border-box; border: 1px solid var(--line-2);
  border-radius: 999px; background: var(--card); padding: 9px 14px; font: inherit; font-size: 13.5px; color: var(--ink); }
.gs-talk-form input:focus-visible { outline: 3px solid var(--plum-ink); outline-offset: 2px; }
.gs-talk-fine { margin: 0 16px 14px; font-size: 11.5px; line-height: 1.45; color: var(--muted); }

/* ── the host bar ─────────────────────────────────────────────────────────── */
.gs-hostbar { display: flex; align-items: center; gap: 14px; padding: 12px 18px; border-top: 1px solid var(--line);
  background: var(--card-warm); flex-wrap: wrap; }
.gs-note { font-size: 13px; color: var(--muted); }
.gs-alive { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ok); }
.gs-alive .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--mint-ink); flex: none; }
.gs-answered { font-size: 13px; font-weight: 700; color: var(--ok); background: var(--ok-soft); border: 1px solid var(--mint);
  border-radius: 999px; padding: 5px 12px; }

/* ── the lobby ────────────────────────────────────────────────────────────── */
.gs-between { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding: 12px 16px; margin: 0 0 18px;
  border-radius: 12px; background: var(--ok-soft); border: 1px solid var(--mint); color: var(--ink-2); }
.gs-between p { margin: 0; font-size: 13.5px; line-height: 1.5; }
.gs-lobby-code { text-align: center; padding: 20px 12px 6px; }
.gs-lobby-code .gs-big { font-size: 44px; font-weight: 700; letter-spacing: .3em; color: var(--ink); padding-left: .3em; line-height: 1.1; }
.gs-lobby-code .gs-lbl { font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); margin-top: 4px; }
/* ── the share control (T2c) ──────────────────────────────────────────────
 * Both halves of the invitation on one row: the address she sends, and the
 * control that puts it in her clipboard. `.gs-linkfield` is a READONLY INPUT
 * and not a styled <span>: when the clipboard is unavailable the fallback has
 * to be able to SELECT the text for her, and only a form control can. It also
 * makes the address focusable, so a keyboard user can take it without a mouse. */
.gs-share { padding: 16px 18px 2px; }
.gs-sharerow { display: flex; gap: 8px; align-items: stretch; max-width: 470px; margin: 0 auto; }
.gs-sharerow .btn { min-height: 44px; padding: 11px 18px; white-space: nowrap; }
.gs-linkfield {
  flex: 1 1 auto; min-width: 0; box-sizing: border-box; min-height: 44px;
  border: 1px solid var(--line-2); border-radius: 12px; background: var(--bg-2);
  padding: 0 14px; font: inherit; font-size: 13.5px; color: var(--ink-2);
  text-overflow: ellipsis; cursor: text;
}
.gs-linkfield:focus-visible { outline: 3px solid var(--plum-ink); outline-offset: 2px; }
.gs-share-fine { margin: 10px auto 0; max-width: 46ch; text-align: center; font-size: 12.5px; line-height: 1.5; color: var(--muted); }
.gs-copied { display: inline-block; font-size: 12px; font-weight: 700; color: var(--ok);
  background: var(--ok-soft); border: 1px solid var(--mint); border-radius: 999px; padding: 3px 11px; }

.gs-consent { margin: 15px 18px 0; padding: 11px 13px; border-radius: 10px; background: var(--bg-2); border: 1px solid var(--line);
  font-size: 13px; color: var(--ink-2); line-height: 1.55; }

/* ── the two states, and the end ──────────────────────────────────────────── */
.gs-plain { padding: 34px 24px; text-align: center; }
.gs-plain .gs-eyebrow { font-size: 13px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); margin: 0 0 8px; }
.gs-plain h2 { margin: 0 0 10px; font-size: 20px; color: var(--ink); }
.gs-plain p { margin: 0 auto; font-size: 14px; line-height: 1.6; color: var(--muted); max-width: 46ch; }
.gs-plain .gs-clock { display: inline-block; margin-top: 16px; font-size: 12.5px; font-weight: 700; color: var(--ink-2);
  background: var(--bg-2); border: 1px solid var(--line-2); border-radius: 999px; padding: 6px 14px; }
.gs-plain .btn { margin-top: 18px; }
.gs-res { display: flex; align-items: center; gap: 11px; padding: 11px 2px; border-bottom: 1px dashed var(--line); font-size: 15px; }
.gs-res:last-child { border-bottom: 0; }
.gs-res .gs-score { margin-left: auto; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ink); }
.gs-res.is-you { font-weight: 600; }
.gs-endcopy { margin: 14px 0 0; font-size: 13px; line-height: 1.6; color: var(--ink-2); }

/* The end confirm reuses the shipped .sp dialog shell. It stays outside
   #gs-root so a roster, answer or chat render cannot destroy it mid-decision. */
.gs-end-dialog { width: min(30rem, calc(100vw - 2rem)); }
.gs-end-dialog .sp-head { margin-bottom: 12px; }
.gs-end-copy { margin: 0 0 14px; font-size: 14px; line-height: 1.6; color: var(--ink-2); }
.gs-end-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.gs-end-actions .btn { min-height: 44px; }

/* ── T3 · keeping what you answered ───────────────────────────────────────
 * Sits under the scores on the results screen, so it is the last thing read
 * and never competes with the round itself. ⚖ A QUIET BUTTON FOR THE GUEST'S
 * account offer and a PRIMARY one for the save: the save is the action she is
 * on this screen for, and the signup is an invitation — promoting a signup on
 * a screen a guest reached by helping a friend study is the nag map §2.8 rules
 * out in as many words.
 * ⚠ 44px floor on both, because this screen is read on a phone more often than
 *   any other in the room — B20 asserts it. */
.gs-keep { margin: 18px 0 0; padding: 15px 0 0; border-top: 1px dashed var(--line); }
.gs-keep .gs-fine { margin: 0 0 12px; }
.gs-keep .gs-fine + .gs-fine { margin-top: -6px; }
.gs-keep .btn { min-height: 44px; }

/* ── the phone ────────────────────────────────────────────────────────────
 * The host is on a laptop with her notes; the CLASSMATE is on a phone. The
 * rail becomes a strip above the question and table talk becomes a block under
 * it, so the question never shares the fold with anything. */
@media (max-width: 720px) {
  .gs-body { flex-direction: column; }
  .gs-rail { width: auto; border-left: 0; border-top: 1px solid var(--line); }
  .gs-main { padding: 16px 15px; }
  .gs-stem { font-size: 16px; }
  .gs-talk-list { max-height: 180px; }
  .gs-lobby-code .gs-big { font-size: 34px; }
  /* The comp's §4 stack: Share… on top, the address under it, Copy link last.
     ⛔ 16px on the link field is NOT a type choice — iOS Safari zooms the whole
        page when a control smaller than 16px takes focus, and the fallback path
        focuses this field deliberately. A zoom on the failure branch would
        leave her looking at a magnified fragment of the address she was told
        to copy. */
  .gs-share { padding: 14px 15px 2px; }
  .gs-sharerow { flex-direction: column; max-width: none; }
  .gs-linkfield { font-size: 16px; }
}
