/* ============================================================================
   Whiteboard Chemistry — practice platform styles.

   Pairs with /styles.css, from which /practice/ now takes only the three font
   stacks (--font-body / --font-display / --font-mono), the two shared metrics
   (--radius, --gutter) and the brand mark. Every COLOUR is a --pr-* token
   owned by this file (§1 below), because the marketing site's palette is
   light-only: borrowing it made dark mode impossible and was itself a leak in
   the walled garden. Everything here is namespaced .pr-* so it can never
   collide with the .rn-* revision styles or the marketing site's classes.

   Deliberately NOT imported by any other page. /practice/ is a walled garden:
   it does not load revision.css, page.css, site.js or home.js, and no page
   outside it loads this file.
   ========================================================================= */

/* [hidden] must actually hide.
   The default UA rule is `[hidden] { display: none }` at author-origin
   weight, so ANY later rule that sets display — and this file sets
   display:flex on .pr-resume, .pr-strip-top and .pr-btn — silently beats it and the
   element stays on screen. Caught by a screenshot: an empty red "resume" box
   and a live "Next question" button on a wrong answer. One rule fixes the
   whole class of bug, so keep it first. */
[hidden] { display: none !important; }

/* ============================================================ 1. TOKENS === */

:root {
  color-scheme: light dark;

  /* --font-display / --font-body / --font-mono are NOT redeclared here.
     The prototype declared them so the mockups would open standalone; on the
     real pages /styles.css already defines them (and serves the @font-face
     rules), and fonts are shared with the site by design. Redeclaring would
     fork the stacks — /styles.css keeps an "EB Garamond" fallback this copy
     did not — for no gain. Same reason the prototype's @font-face block is
     dropped: re-declaring it double-downloads every face. */

  /* ---- paper -------------------------------------------------------- */
  --pr-paper:  oklch(0.972 0.006 80);   /* the desk */
  --pr-sheet:  oklch(1     0     0);    /* the question sits on white */
  --pr-sunk:   oklch(0.955 0.008 80);   /* table heads, chips, wells */
  --pr-rule:   oklch(0.900 0.008 80);   /* hairline */
  --pr-rule-2: oklch(0.800 0.010 80);   /* input borders, stronger edges */

  /* ---- ink ---------------------------------------------------------- */
  --pr-ink:      oklch(0.20 0.018 250);
  --pr-ink-2:    oklch(0.37 0.014 250); /* prose inside cards */
  --pr-ink-3:    oklch(0.52 0.010 250); /* mono micro-labels */
  --pr-ink-flip: oklch(0.98 0.004 80);  /* text on an --pr-ink field */

  /* ---- the pens ------------------------------------------------------
     ONE red, in three grammars, and nothing else may borrow it:
       · a DOT              = look here (brand grammar, as on the marketing site)
       · a 3px RULE or GLYPH = a mark has been made (marking grammar)
       · an ITALIC SERIF word in a title (brand grammar)
     Never a red fill bigger than a chip.  A red AREA is punishment;
     a red MARK is marking, and this platform only ever marks.            */
  --pr-red:        oklch(0.52 0.175 28);
  --pr-red-ink:    oklch(0.43 0.155 28);
  --pr-red-wash:   oklch(0.972 0.020 28);
  --pr-green:      oklch(0.47 0.105 152);   /* the tick — ink-green, never lime */
  --pr-green-ink:  oklch(0.38 0.090 152);
  --pr-green-wash: oklch(0.972 0.022 152);
  --pr-amber:      oklch(0.63 0.115 72);    /* "nearly" — method right, s.f. wrong */
  --pr-amber-ink:  oklch(0.44 0.095 72);
  --pr-amber-wash: oklch(0.975 0.028 72);

  --pr-focus: oklch(0.55 0.12 240);         /* focus ring, and nothing else */

  /* ---- the shell -------------------------------------------------------
     The platform had no navigation at all: five screens reachable only from
     a footer link, and no answer anywhere on screen to "where am I". The
     rail and the tab bar are that answer, and they are a DIFFERENT MATERIAL
     from the page on purpose — chrome is ink, content is paper. A shell
     painted in the page's own colours reads as a document with a menu on it
     rather than as a product.

     --pr-rail does NOT follow --pr-today. The due card inverts between the
     themes (ink slab in light, cream in dark) because it is a card ON the
     page; the shell is the frame AROUND it and stays dark in both, or the
     app changes shape at dusk. */
  --pr-rail:     oklch(0.190 0.016 250);   /* the chrome */
  --pr-rail-2:   oklch(0.255 0.018 250);   /* a well inside the chrome */
  --pr-ink-rail: oklch(0.760 0.014 250);   /* nav text at rest */
  /* The one red that is always ON ink and so never inverts: the rail's
     active mark, the hero's italic. --pr-today-em cannot do this job — it
     flips to the paper-tuned red in dark, because the card under IT flips. */
  --pr-red-on-ink: oklch(0.780 0.130 28);
  /* A control ON the chrome. --pr-ink-flip cannot do this job any more: it is
     the page's ink inverted, so in dark it is oklch(0.165) — and the hero it
     would sit on is --pr-rail at oklch(0.132). That is 1.04:1. At 11pm the
     one button the dashboard exists for was a black slab on a black card. */
  --pr-on-ink:      oklch(0.980 0.004 80);
  --pr-on-ink-hover: oklch(0.905 0.006 80);
  /* The mastery ramp, LIFTED for the same reason. --pr-m3..m5 are tuned
     against paper and flip with the theme; the arcs are drawn on the rail,
     which does not. In light, --pr-m5 on --pr-rail measured 1.21:1 — three
     rings a student could not read at all. These three are one scale, three
     steps, and they never invert. */
  --pr-arc-1: oklch(0.800 0.150 152);
  --pr-arc-2: oklch(0.720 0.135 152);
  --pr-arc-3: oklch(0.640 0.120 152);

  /* --pr-today-rule and --pr-today-em are GONE. They existed because the due
     card was the page inverted — an ink slab in light, a cream one in dark —
     so its ruling and its one red word had to carry the card's ground with
     them. The card is now part of the CHROME and does not invert at all, so
     its lattice is drawn by the ::after mask in §6 and its red word is
     --pr-red-on-ink. Two tokens, three theme blocks and a comment describing
     a card that no longer exists. */

  /* ---- mastery, the one SCALE in the system ---------------------------
     Red is the accent and it means "a mark has been made / look here".
     Mastery is not an accent — it is a scale, and it is drawn in the green
     that already means "right", because that is literally what it measures:
     answers the marker accepted. Six steps, so the specification map reads as
     a skyline at a glance and a single cell reads as a level.

     Level 0 (met it, not stuck) is deliberately NOT green — it is the rule
     colour, so an untouched topic and a topic you keep missing look the same
     from across the room, which is the truth. --pr-mx is the dashed "nothing
     written here yet" cell, which is ours to fix and not the student's.

     Colour is never the only carrier: every bar is accompanied by `4/7`, and
     every skill row by its level word. */
  --pr-m0: oklch(0.895 0.008 80);
  --pr-m1: oklch(0.870 0.038 152);
  --pr-m2: oklch(0.790 0.070 152);
  --pr-m3: oklch(0.680 0.098 152);
  --pr-m4: oklch(0.570 0.108 152);
  --pr-m5: oklch(0.470 0.105 152);     /* = --pr-green, the tick's own ink */
  --pr-mx: oklch(0.820 0.010 80);      /* the dashed edge: not written yet */

  /* ---- measure & rhythm --------------------------------------------- */
  --pr-measure: 34rem;                 /* the stem column ≈ 62ch at 19px */
  --pr-wide:    58rem;                 /* dashboard */
  --pr-gutter:  clamp(14px, 4.5vw, 28px);
  --pr-bar-h:   calc(66px + env(safe-area-inset-bottom, 0px));
  /* The sticky header's own height, so the sticky paper strip underneath it
     can be told where to stop. Measured on the real page at 375px: 52px of
     min-height plus 9px of padding top and bottom, less the 3px the brand row
     gives back. If the header's padding changes, change this with it —
     getting it wrong is not subtle, the strip slides UNDER the header and
     eats the top of the ladder. */
  --pr-head-h:  67px;

  /* ---- the spacing scale ---------------------------------------------
     Eight steps and nothing between them. Before this the file carried 30
     different margin values invented one at a time, which is what makes a
     screen read as a document rather than as a product: nothing lines up
     with anything, so nothing looks deliberate. Use a step or change the
     scale; never a number. */
  --pr-s1: 4px;
  --pr-s2: 8px;
  --pr-s3: 12px;
  --pr-s4: 16px;
  --pr-s5: 20px;
  --pr-s6: 26px;
  --pr-s7: 34px;
  --pr-s8: 46px;

  /* ---- the type scale -------------------------------------------------
     A ratio near 1.15 at the small end, opening up at the top. Every size on
     the platform is one of these nine, plus --pr-stem, which is its own
     clamp because the question is the one thing that must scale with the
     viewport. The old file had 27 distinct font-size values.

     --pr-t-num is for figures a student reads as data — a count, a
     percentage, a score. Tabular, tight, and never chemistry. */
  --pr-t-xs:   11.5px;                 /* mono labels ONLY — never chemistry */
  --pr-t-sm:   12.5px;
  --pr-t-base: 14px;
  --pr-t-md:   15px;
  --pr-t-lg:   16.5px;
  --pr-t-2xl:  23px;
  --pr-t-3xl:  28px;
  --pr-display: clamp(26px, 5.5vw, 34px);
  --pr-stem:    clamp(16.5px, 1rem + 0.45vw, 19px);
  --pr-body:    15.5px;
  --pr-small:   14px;
  --pr-micro:   11.5px;                /* alias of --pr-t-xs, kept: 40 uses */
  --pr-track:   0.075em;               /* the tracking that goes with --pr-micro */

  /* ---- form ---------------------------------------------------------- */
  --pr-r-sm: 6px;
  --pr-r:    9px;
  --pr-r-lg: 14px;
  --pr-r-pill: 999px;
  --pr-tap:  48px;                     /* every control, everywhere */
  /* Three elevations and no more. e0 is a hairline card that sits ON the
     page; e1 lifts; e2 is for the two things that float over it — the data
     sheet panel and the verdict bar. */
  --pr-e1: 0 1px 2px oklch(0.20 0.018 250 / 0.05),
           0 3px 10px oklch(0.20 0.018 250 / 0.05);
  --pr-e2: 0 2px 6px oklch(0.20 0.018 250 / 0.07),
           0 12px 34px oklch(0.20 0.018 250 / 0.10);
  /* e3 is for the ONE slab that has to read as lifted off the desk: the
     hero. Three elevations were enough while every screen was a column; a
     shell needs a fourth or the hero and the cards below it sit in the same
     plane. */
  --pr-e3: 0 3px 8px oklch(0.20 0.018 250 / 0.08),
           0 22px 50px -12px oklch(0.20 0.018 250 / 0.22);
  --pr-lift: var(--pr-e1);

  /* ---- the shell's metrics --------------------------------------------- */
  --pr-rail-w:   250px;
  --pr-content:  1160px;               /* the app uses the desk it is given */
  --pr-tabbar-h: calc(60px + env(safe-area-inset-bottom, 0px));

  /* ---- motion, three durations on one curve -----------------------------
     --pr-speed was the only one, and it was 140ms, which is right for a
     hover and far too fast for a reward. A tick that draws in 140ms has not
     been seen. Every one of these is collapsed to 1ms by the
     prefers-reduced-motion block at the end of the file. */
  --pr-speed: 140ms;                   /* hover, focus, colour  */
  --pr-mid:   240ms;                   /* something arriving    */
  --pr-slow:  420ms;                   /* a mark being made     */
  --pr-ease:  cubic-bezier(0.2, 0.7, 0.2, 1);


  /* ---- figure inks ---------------------------------------------------
     The bank packs hard-code the LIGHT-mode ink value into every SVG instead
     of currentColor — 154 strokes and 157 fills at oklch(0.20 0.018 250)
     alone — so in dark the skeletal formulae are near-black on a charcoal
     card. The whole figure palette is greyscale, so it is a lightness problem
     rather than a colour-mapping one, and a CSS stroke/fill PROPERTY overrides
     the corresponding presentation attribute. The rules are at the end of §6.

     Four tokens rather than one, because flattening every value to a single
     ink would darken the graph-paper rulings to the weight of the curve drawn
     on them.

     These four are declared here for the token gate — every --pr-* token must
     exist in all three theme states — and are UNUSED in light: the rules that
     read them are scoped to the two dark states, so light mode is untouched by
     construction. See the note above the rules for why that is not the same as
     the shape the plan carried. */
  --pr-fig-ink:   oklch(0.20 0.018 250);   /* the skeleton */
  --pr-fig-ink-2: oklch(0.30 0.014 250);   /* secondary lines */
  --pr-fig-guide: oklch(0.87 0.004 250);   /* graph-paper rulings, axes */
  --pr-fig-mask:  oklch(1 0 0);            /* knockouts behind a label */

  /* the whiteboard ruling, half the marketing site's 64px so it reads as
     squared paper rather than as a wall */
  --pr-grid:
    linear-gradient(to right,  oklch(0.20 0.018 250 / 0.045) 1px, transparent 1px),
    linear-gradient(to bottom, oklch(0.20 0.018 250 / 0.045) 1px, transparent 1px);
}

/* ---- dark: warm charcoal, not black.  A phone at 11pm is the design case. */
@media (prefers-color-scheme: dark) {
  :root:not([data-pr-theme="light"]) {
    --pr-paper:  oklch(0.180 0.010 75);
    --pr-sheet:  oklch(0.228 0.011 75);
    --pr-sunk:   oklch(0.272 0.011 75);
    --pr-rule:   oklch(0.325 0.010 75);
    --pr-rule-2: oklch(0.420 0.012 75);
    --pr-ink:      oklch(0.945 0.006 80);
    --pr-ink-2:    oklch(0.820 0.008 80);
    --pr-ink-3:    oklch(0.670 0.010 80);
    --pr-ink-flip: oklch(0.165 0.010 75);
    --pr-red:        oklch(0.705 0.150 28);
    --pr-red-ink:    oklch(0.775 0.130 28);
    --pr-red-wash:   oklch(0.285 0.050 28);
    --pr-green:      oklch(0.735 0.115 152);
    --pr-green-ink:  oklch(0.800 0.100 152);
    --pr-green-wash: oklch(0.278 0.042 152);
    --pr-amber:      oklch(0.790 0.105 72);
    --pr-amber-ink:  oklch(0.845 0.095 72);
    --pr-amber-wash: oklch(0.295 0.045 72);
    --pr-focus: oklch(0.74 0.11 240);
    --pr-rail:     oklch(0.132 0.010 75);
    --pr-rail-2:   oklch(0.205 0.011 75);
    --pr-ink-rail: oklch(0.720 0.010 80);
    --pr-red-on-ink: oklch(0.800 0.130 28);
    /* Identical in both themes on purpose — the ground does not change, so
       neither may these. They are declared in all three states because the
       token gate requires it, not because they differ. */
    --pr-on-ink:       oklch(0.980 0.004 80);
    --pr-on-ink-hover: oklch(0.905 0.006 80);
    --pr-arc-1: oklch(0.800 0.150 152);
    --pr-arc-2: oklch(0.720 0.135 152);
    --pr-arc-3: oklch(0.640 0.120 152);
    --pr-fig-ink:   oklch(0.945 0.006 80);   /* the skeleton,      = --pr-ink   */
    --pr-fig-ink-2: oklch(0.820 0.008 80);   /* secondary lines,   = --pr-ink-2 */
    --pr-fig-guide: oklch(0.420 0.012 75);   /* guides go DARKER,  = --pr-rule-2 */
    --pr-fig-mask:  oklch(0.228 0.011 75);   /* knockouts,         = --pr-sheet  */
    /* The mastery scale, inverted. In light a full cell is DARKER than the
       page; at 11pm it has to be LIGHTER, or a mastered skill reads as a hole
       in the map. Same six steps, same hue, lightness reversed. */
    --pr-m0: oklch(0.360 0.010 75);
    --pr-m1: oklch(0.420 0.045 152);
    --pr-m2: oklch(0.500 0.070 152);
    --pr-m3: oklch(0.590 0.092 152);
    --pr-m4: oklch(0.670 0.108 152);
    --pr-m5: oklch(0.760 0.118 152);
    --pr-mx: oklch(0.420 0.012 75);
    --pr-e1: 0 1px 2px oklch(0 0 0 / 0.34), 0 3px 10px oklch(0 0 0 / 0.30);
    --pr-e2: 0 2px 8px oklch(0 0 0 / 0.42), 0 14px 38px oklch(0 0 0 / 0.46);
    --pr-e3: 0 3px 10px oklch(0 0 0 / 0.46), 0 22px 54px -12px oklch(0 0 0 / 0.60);
    --pr-grid:
      linear-gradient(to right,  oklch(1 0 0 / 0.038) 1px, transparent 1px),
      linear-gradient(to bottom, oklch(1 0 0 / 0.038) 1px, transparent 1px);
  }
}
/* An explicit choice also pins color-scheme, which no rule of ours can reach:
   it is what paints the scrollbars, the form controls and — before this
   stylesheet has even arrived — the canvas itself. Bare :root says
   `light dark`, meaning "follow the system", which is right until somebody
   chooses. The inline <script> in each page's <head> sets the same thing as an
   inline style for the pre-stylesheet moment. */
:root[data-pr-theme="dark"]  { color-scheme: dark; }
:root[data-pr-theme="light"] { color-scheme: light; }

/* the explicit toggle wins in both directions */
:root[data-pr-theme="dark"] {
  --pr-paper:  oklch(0.180 0.010 75);
  --pr-sheet:  oklch(0.228 0.011 75);
  --pr-sunk:   oklch(0.272 0.011 75);
  --pr-rule:   oklch(0.325 0.010 75);
  --pr-rule-2: oklch(0.420 0.012 75);
  --pr-ink:      oklch(0.945 0.006 80);
  --pr-ink-2:    oklch(0.820 0.008 80);
  --pr-ink-3:    oklch(0.670 0.010 80);
  --pr-ink-flip: oklch(0.165 0.010 75);
  --pr-red:        oklch(0.705 0.150 28);
  --pr-red-ink:    oklch(0.775 0.130 28);
  --pr-red-wash:   oklch(0.285 0.050 28);
  --pr-green:      oklch(0.735 0.115 152);
  --pr-green-ink:  oklch(0.800 0.100 152);
  --pr-green-wash: oklch(0.278 0.042 152);
  --pr-amber:      oklch(0.790 0.105 72);
  --pr-amber-ink:  oklch(0.845 0.095 72);
  --pr-amber-wash: oklch(0.295 0.045 72);
  --pr-focus: oklch(0.74 0.11 240);
    --pr-rail:     oklch(0.132 0.010 75);
    --pr-rail-2:   oklch(0.205 0.011 75);
    --pr-ink-rail: oklch(0.720 0.010 80);
    --pr-red-on-ink: oklch(0.800 0.130 28);
    /* Identical in both themes on purpose — the ground does not change, so
       neither may these. They are declared in all three states because the
       token gate requires it, not because they differ. */
    --pr-on-ink:       oklch(0.980 0.004 80);
    --pr-on-ink-hover: oklch(0.905 0.006 80);
    --pr-arc-1: oklch(0.800 0.150 152);
    --pr-arc-2: oklch(0.720 0.135 152);
    --pr-arc-3: oklch(0.640 0.120 152);
  --pr-fig-ink:   oklch(0.945 0.006 80);   /* the skeleton,      = --pr-ink   */
  --pr-fig-ink-2: oklch(0.820 0.008 80);   /* secondary lines,   = --pr-ink-2 */
  --pr-fig-guide: oklch(0.420 0.012 75);   /* guides go DARKER,  = --pr-rule-2 */
  --pr-fig-mask:  oklch(0.228 0.011 75);   /* knockouts,         = --pr-sheet  */
  /* The mastery scale, inverted — see the note in the media-query block. */
  --pr-m0: oklch(0.360 0.010 75);
  --pr-m1: oklch(0.420 0.045 152);
  --pr-m2: oklch(0.500 0.070 152);
  --pr-m3: oklch(0.590 0.092 152);
  --pr-m4: oklch(0.670 0.108 152);
  --pr-m5: oklch(0.760 0.118 152);
  --pr-mx: oklch(0.420 0.012 75);
  --pr-e1: 0 1px 2px oklch(0 0 0 / 0.34), 0 3px 10px oklch(0 0 0 / 0.30);
  --pr-e2: 0 2px 8px oklch(0 0 0 / 0.42), 0 14px 38px oklch(0 0 0 / 0.46);
    --pr-e3: 0 3px 10px oklch(0 0 0 / 0.46), 0 22px 54px -12px oklch(0 0 0 / 0.60);
  --pr-grid:
    linear-gradient(to right,  oklch(1 0 0 / 0.038) 1px, transparent 1px),
    linear-gradient(to bottom, oklch(1 0 0 / 0.038) 1px, transparent 1px);
}

/* ===================================================== 2. THE CASE GUARD ===
   Every letter-spaced, uppercased label on this platform is a .pr-label, and a
   .pr-label may never contain chemistry.  The regions that DO carry chemistry
   are guarded structurally, so no future rule — a redesign, a copied snippet, a
   .rn-* class arriving inside a bank pack — can uppercase them by accident.
   Cl must never render CL, δ never Δ, and a <sub> must never detach from its
   atom.  This is the only !important in the file and it earns its keep.      */

.pr-stem, .pr-stem *,
.pr-opt-body, .pr-opt-body *,
.pr-sheetnote, .pr-sheetnote *,
.pr-tbl, .pr-tbl *,
/* Graph axis labels carry chemistry in the same way a table header does —
   "ln k", "pH", "1/T". k is the rate constant and pH is not PH, so an
   uppercase rule reaching them would corrupt the meaning exactly as it would
   corrupt Cl into CL. In the guard for the same reason .pr-tbl is. */
.pr-fig, .pr-fig *,
.pr-topic-name, .pr-skill-name, .pr-unitblock-sub,
.pr-answer, .pr-unit, .pr-verdict-val,
.pr-bar-txt, .pr-bar-txt *,
.pr-part-q, .pr-part-q *,
/* THE SELF-MARKED QUESTION ON A MOCK'S RESULT SCREEN. It reprints the STEM —
   every formula, every δ and every Cl of it — beside what the student wrote,
   and both are on the same page as a .pr-label. The mark scheme beside them is
   already covered as a .pr-sheetnote; these two would not have been, and that
   is exactly the accident this guard is structural to prevent. */
.pr-selfmark-q, .pr-selfmark-q *,
.pr-selfmark-a, .pr-selfmark-a *,
/* THE DATA SHEET. Its 43 .rn-chem spans are revision.css's opt-out and have no
   rule here, so they protect nothing — measured in a harness against a hostile
   th{text-transform:uppercase}: without this the NMR header renders "Δ / PPM"
   instead of "δ / ppm", and δ is the chemical shift while Δ means "change in".
   The enlargement overlay is in the list too: it lives on document.body,
   outside .pr-book, and is the one part of the sheet that would otherwise sit
   outside the guard. */
.pr-book, .pr-book *,
.pr-bookzoom, .pr-bookzoom * {
  text-transform: none !important;
  letter-spacing: normal !important;
  font-variant-caps: normal !important;
}

/* sub/sup that stay welded to their atom and never open the line */
.pr-stem sub, .pr-stem sup,
.pr-opt-body sub, .pr-opt-body sup,
.pr-sheetnote sub, .pr-sheetnote sup,
.pr-tbl sub, .pr-tbl sup,
.pr-unit sub, .pr-unit sup,
.pr-topic-name sub, .pr-topic-name sup,
.pr-skill-name sub, .pr-skill-name sup,
.pr-verdict-val sub, .pr-verdict-val sup,
.pr-bar-txt sub, .pr-bar-txt sup { font-size: 0.72em; line-height: 0; }

/* THE KICKER — a .pr-label that has to carry a value.

   Same mono voice, same size, same colour, and NO text-transform. It exists
   because §7.7's rule is that a .pr-label's text is a literal all the way to
   the screen: the moment a kicker has to say "Paper 3" or "12 questions" it
   can no longer be one, and the honest answer is a class that is safe by
   construction rather than an exemption in the gate. Nothing here can
   uppercase anything, so nothing here can corrupt a formula. */
.pr-kick {
  font-family: var(--font-mono);
  font-size: var(--pr-micro);
  font-weight: 500;
  letter-spacing: var(--pr-track);
  color: var(--pr-ink-3);
}

/* the sanctioned label.  Mono, tracked, uppercase — and prose only. */
.pr-label {
  font-family: var(--font-mono);
  font-size: var(--pr-micro);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: var(--pr-track);
  color: var(--pr-ink-3);
}

/* ==================================================== 3. PAGE FURNITURE ====
   design-proto.css §3, ported onto the class names the four pages already use.

   The prototype called the header .pr-head / .pr-head-in / .pr-head-r and the
   content column .pr-main. The pages have said .pr-header / .pr-header-inner /
   .pr-header-nav / .pr-page since they were written, and renaming them would
   churn four pages and six modules for no pixel. So the DECLARATIONS below are
   the prototype's and the NAMES are ours. The mapping, once, here:

     .pr-head    → .pr-header          .pr-head-in → .pr-header-inner
     .pr-head-r  → .pr-header-nav      .pr-main    → .pr-page
     .pr-brand b → .pr-brand-name      .pr-brand .pr-label → .pr-brand-tag

   Task 1 landed the two-declaration minimum of this section — body's colour and
   background-colour — so the token layer reached the page at all. The rest of
   it arrives here: the 32px ruling, the 34rem measure, the type scale.      */

/* ---- the page ground -------------------------------------------------------
   /styles.css paints `body` from its own light-only --paper / --ink, and it is
   loaded before this file, so without the colour and background-colour below
   the tokens above flip at 11pm and the sheet underneath them does not —
   --pr-ink-2 prose on a near-white page, which is what the first dark
   screenshot of Task 1 showed.

   Four of the prototype's §3 body declarations are deliberately NOT re-stated:
   `margin: 0`, `font-family`, `-webkit-font-smoothing` and `overflow-x: hidden`
   are all already set on body by /styles.css for the whole site, and forking
   them here would be the same mistake Task 1 avoided with the font stacks.
   font-size IS re-stated, because it genuinely differs: the site's body is 17px
   and this platform's is 16px, and --pr-body is the number every other size in
   the design was drawn against.

   background-COLOR and background-IMAGE, never the `background` shorthand. Two
   different concerns set them and the shorthand would silently reset whichever
   one it did not name. */
/* THE WHITEBOARD RULING IS OFF THE PAGE, and that is the one piece of the
   marketing site's language this redesign gives up.

   It was a 32px grid at 4.5% ink behind everything. It is beautiful on a
   page with three objects on it and it is noise on a page with forty: every
   hairline card edge, every 12px mono label and every mastery cell had to
   compete with a ruling running underneath it, and at 375px in daylight the
   cells lost. Joe's ruling of today — "the design can be totally separate to
   the static whiteboard chemistry website, whatever is most effective for
   this purpose" — is what makes dropping it allowed; density is what makes it
   right.

   --pr-grid is NOT deleted. The one object that still earns the ruling is the
   session card, which is the page inverted and has three things on it. */
body {
  color: var(--pr-ink);
  background-color: var(--pr-paper);
  background-image: none;
  font-size: var(--pr-body);
  line-height: 1.55;
  -webkit-text-size-adjust: 100%;
}

/* Links are ink, underlined in a hairline, and only go red on hover. §7.2
   spends the one red on marks and on the brand's dot; a page whose every link
   was red would spend the whole budget on navigation. */
a { color: var(--pr-ink); text-decoration-color: var(--pr-rule-2); text-underline-offset: 3px; }
a:hover { text-decoration-color: var(--pr-red); }

/* One focus ring for the platform, in the one token reserved for it. Everything
   that used to carry its own — .pr-btn had a hard-coded light-only blue — now
   inherits this, so the ring follows the theme. */
:focus-visible { outline: 2px solid var(--pr-focus); outline-offset: 2px; border-radius: 3px; }

/* --------------------------------------------------------------- structure */

.pr-page {
  max-width: var(--pr-wide);
  margin: 0 auto;
  padding: 0 var(--pr-gutter) 120px;
}

/* §7.4's measure: 34rem, not 980px. It is a modifier rather than the default
   because the two columns do different jobs — the dashboard, account and tutor
   pages are 54rem of cards, stats and tables, while the session runner is one
   column of prose you have to read a stem in without losing your place.
   session.html carries this class; nothing else should. */
.pr-page--measure {
  max-width: var(--pr-measure);
  /* The session runner is the one page with §8's sticky bar under it, so it is
     the one page that has to reserve room for it. --pr-bar-h has existed since
     the token layer and had nothing to measure until now; it carries
     env(safe-area-inset-bottom) as the bar's own padding does, which the flat
     120px on .pr-page does not — on a notched phone that was the difference
     between the last line of a marking sheet being readable and being under
     the bar. */
  padding-bottom: calc(var(--pr-bar-h) + 56px);
}

/* ================================================== 3a. THE SHELL =========
   The platform shipped with no navigation. Five screens, and the only route
   between any two of them was a link in the footer — so a student who wanted
   the specification map from a mock paper scrolled to the bottom of the page
   to find the way back. This is that missing frame.

   ONE navigation in two materials, and exactly one of them is ever on screen:
   a rail at or above 1024px, a bottom tab bar below it. The hide rule for
   each sits AFTER the other's `display`, at equal specificity, so the later
   rule wins — a `display:grid` written after a media query that hides it is
   the commonest way a responsive nav ends up on screen twice, and it is how
   the mockup for this failed first time.

   Sessions suppress both (.pr-app--focus). Nothing may compete with a
   question a student is being marked on.
   ========================================================================= */

.pr-app { min-height: 100vh; display: grid; grid-template-columns: var(--pr-rail-w) minmax(0, 1fr); }
.pr-app--focus { grid-template-columns: minmax(0, 1fr); }
/* §7.8's rule at the outermost level: a grid item's automatic minimum size is
   min-content, and this one contains the whole application — one wide table,
   one long unbreakable formula or one 90-mark paper card inside it would
   widen the column past the viewport and take the rail off the left edge with
   it. minmax(0, 1fr) on the track is only half of it; the ITEM needs the
   min-width too, because the track's minimum is what the item asks for. */
.pr-col { min-width: 0; display: flex; flex-direction: column; }
/* So the footer sits at the bottom of a short page rather than under the
   fold-line of whatever content there happened to be. */
.pr-col > .pr-page { flex: 1; }

/* Inside the shell the page no longer centres itself against the viewport —
   it centres against the column the rail leaves. --pr-wide was 58rem, which
   was a reasonable measure for a document and is not a layout for an app:
   at 1440px it left 60% of the desk empty. */
.pr-app .pr-page { max-width: var(--pr-content); padding-bottom: var(--pr-s8); }
/* The session runner is the one page with §8's sticky bar under it and so the
   one page that has to reserve room for it. The rule above is (0,2,0) and the
   original clearance on .pr-page--measure is (0,1,0), so this shell rule took
   the reservation away: 46px reserved under a bar that is 66px plus the
   safe-area inset, which puts the last lines of every marking sheet
   permanently underneath it.

   Restated here at (0,2,0) so it wins back, and stated on the class that
   NEEDS it rather than by weakening the shell rule for every page. Ordering
   was not the failure this time — specificity was. */
.pr-app .pr-page--measure {
  max-width: none;
  padding-bottom: calc(var(--pr-bar-h) + 56px);
}

/* ---------------------------------------------------------------- the rail */

.pr-rail {
  position: sticky; top: 0; align-self: start;
  height: 100vh;
  display: flex; flex-direction: column; gap: var(--pr-s5);
  padding: var(--pr-s5) var(--pr-s4) var(--pr-s4);
  background: var(--pr-rail);
  color: var(--pr-ink-rail);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.pr-rail-brand {
  display: flex; align-items: center; gap: 10px;
  padding: 0 var(--pr-s2) var(--pr-s4);
  text-decoration: none;
  border-bottom: 1px solid oklch(1 0 0 / 0.09);
}
.pr-rail-brand:hover { text-decoration: none; }
.pr-rail-mark {
  flex: none; width: 30px; height: 30px;
  display: grid; place-items: center;
  border-radius: 7px;
  background: oklch(1 0 0 / 0.10);
  color: oklch(0.98 0 0);
}
.pr-rail-mark svg { display: block; width: 19px; height: 19px; }
.pr-rail-names { display: grid; gap: 1px; min-width: 0; }
.pr-rail-name {
  font-family: var(--font-display); font-size: 17px; line-height: 1.12;
  color: oklch(0.98 0 0); white-space: nowrap;
}
/* .pr-label, so §7.7's one uppercase class stays the only one. */
.pr-rail-tag { font-size: 9.5px; color: oklch(0.70 0.020 60); }

.pr-nav { display: grid; gap: 2px; }
.pr-nav-item {
  display: flex; align-items: center; gap: 11px;
  min-height: 40px; padding: 8px 11px;
  border-radius: var(--pr-r);
  text-decoration: none;
  font-size: 14.5px; font-weight: 500;
  color: var(--pr-ink-rail);
  transition: background var(--pr-speed) var(--pr-ease),
              color var(--pr-speed) var(--pr-ease);
}
.pr-nav-item svg { flex: none; width: 17px; height: 17px; opacity: 0.85; }
.pr-nav-item:hover { background: oklch(1 0 0 / 0.07); color: oklch(0.97 0 0); text-decoration: none; }
.pr-nav-item[aria-current="page"] { background: oklch(1 0 0 / 0.12); color: oklch(0.99 0 0); }
.pr-nav-item[aria-current="page"] svg { opacity: 1; color: var(--pr-red-on-ink); }

/* The standing block: what the student is at, pinned where they can always
   see it. §7.5's ruling against a streak is kept and its REASON is what
   decides the contents — every number here only ever rises, so a week of flu
   costs nothing and there is nothing to protect by opening the app. */
.pr-standing {
  margin-top: auto;
  display: grid; gap: var(--pr-s3);
  padding: var(--pr-s4);
  border-radius: var(--pr-r-lg);
  background: var(--pr-rail-2);
}
.pr-standing-row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--pr-s3); }
.pr-standing-k { font-size: 10px; color: oklch(0.68 0.020 60); }
.pr-standing-v {
  font-family: var(--font-display); font-size: 22px; line-height: 1;
  color: oklch(0.98 0 0); font-variant-numeric: tabular-nums;
}
.pr-standing-v small { font-family: var(--font-body); font-size: 12.5px; color: oklch(0.70 0.020 60); }

/* ------------------------------------------------------------- the top bar */

/* .pr-header keeps its name and its markup and becomes the content column's
   own bar: the brand has moved into the rail, so what is left is where you
   are and the two controls that belong to the student rather than to the
   page. */
.pr-app .pr-header {
  position: sticky; top: 0; z-index: 20;
  background: color-mix(in oklch, var(--pr-paper) 88%, transparent);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}
.pr-app .pr-header-inner { max-width: none; }
/* On a desk the brand is in the rail, so the header's copy of it goes — but
   ONLY where there is a rail. A session is .pr-app--focus and renders none,
   so this hid the brand on the one screen with no other route home: at
   1024px and up the session header had an empty left half, and the footer
   link that would have served instead is under the sticky verdict bar.

   ONE selector: a second, more specific one was written beside this and then
   out-specified the media query below that brings the brand back on a phone,
   leaving the mobile header empty on the left. */
.pr-app:not(.pr-app--focus) .pr-brand { display: none; }
.pr-here { font-family: var(--font-display); font-size: 19px; margin-right: auto; }

/* --------------------------------------------------------- the tab bar --- */

.pr-tabbar {
  position: fixed; inset: auto 0 0 0; z-index: 40;
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  height: var(--pr-tabbar-h);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  background: color-mix(in oklch, var(--pr-sheet) 94%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-top: 1px solid var(--pr-rule);
}
.pr-tab {
  /* min-width:0 for §7.8's reason: a grid item's automatic minimum is
     min-content, and five items whose labels cannot shrink pushed the bar to
     scrollWidth 323 in a 320px viewport — enough to rubber-band. */
  min-width: 0;
  display: grid; place-items: center; align-content: center; gap: 3px;
  position: relative;
  text-decoration: none;
  font-size: 9.5px;
  color: var(--pr-ink-3);
}
.pr-tab:hover { text-decoration: none; }
.pr-tab svg { width: 21px; height: 21px; }
.pr-tab .pr-label { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pr-tab[aria-current="page"] { color: var(--pr-ink); }
.pr-tab[aria-current="page"]::before {
  content: ""; position: absolute; top: 0; left: 50%;
  width: 24px; height: 2px; transform: translateX(-50%);
  background: var(--pr-red); border-radius: 0 0 2px 2px;
}

@media (max-width: 1023px) {
  .pr-app { grid-template-columns: minmax(0, 1fr); }
  .pr-rail { display: none; }
  .pr-app .pr-page { padding-bottom: calc(var(--pr-tabbar-h) + var(--pr-s7)); }
  /* …but not on the session, which has no tab bar and does have the verdict
     bar. Without this a phone reserved the height of furniture that is not on
     the page, and 28px too little for the furniture that is. */
  .pr-app .pr-page--measure { padding-bottom: calc(var(--pr-bar-h) + 56px); }
  .pr-app .pr-brand { display: flex; }        /* it comes back to the header */
  .pr-here { display: none; }
  /* The tab bar is fixed, and the padding that clears it is on .pr-page — but
     the footer is a SIBLING of <main> inside .pr-col, so it sat behind the bar
     entirely, taking the only Privacy link on the platform with it. */
  .pr-app .pr-footer { padding-bottom: calc(var(--pr-tabbar-h) + var(--pr-s4)); }
}
/* AFTER .pr-tabbar's own display:grid, at equal specificity. See the note at
   the head of this section. */
@media (min-width: 1024px) { .pr-tabbar { display: none; } }
/* A session does not RENDER a tab bar at all — session.html carries
   .pr-app--focus and simply has no <nav class="pr-tabbar">. There was a rule
   here to hide one; it was dead, and a rule for markup that cannot exist is
   the kind of thing that later gets read as evidence the markup does exist.
   The bottom of a session belongs to the verdict bar, which is the one thing
   a thumb reaches for on that screen. */

/* The private-beta strip, DEMOTED — a deliberate decision, not an inherited
   one.

   It used to be `background: var(--pr-ink)`, which is the correct token
   inversion and is exactly what .pr-btn--primary and .pr-toast do. In dark that
   makes it a light cream bar across the full width of every screen: the
   loudest object on the page is an administrative notice that never changes and
   that a student in a private beta reads once. §7.2's principle is that the
   loudest object is the thing that matters, and on the dashboard that is "Start
   a practice session", on the session runner the recovery action. So the strip
   becomes a quiet sunken rule: still read once, never again competing with the
   thing you came to do. Delete the whole element at launch. */
.pr-beta {
  background: var(--pr-sunk);
  color: var(--pr-ink-2);
  border-bottom: 1px solid var(--pr-rule);
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.02em;
  text-align: center;
  padding: 7px 16px;
}

/* A compact header of its own, so /practice/ needs no site JS for a drawer.

   Translucent, so the ruling underneath shows through as the page scrolls past
   — it is the one place the whiteboard reads as a surface rather than a
   texture. color-mix rather than an rgba() literal, because the ground it is
   mixing with is a token and changes at 11pm. */
.pr-header {
  border-bottom: 1px solid var(--pr-rule);
  background: color-mix(in srgb, var(--pr-paper) 88%, transparent);
  backdrop-filter: saturate(1.4) blur(8px);
  position: sticky;
  top: 0;
  z-index: 20;
}

.pr-header-inner {
  max-width: var(--pr-wide);
  margin: 0 auto;
  padding: 9px var(--pr-gutter);
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 52px;
}

.pr-brand {
  display: flex;
  align-items: center;
  gap: 9px;
  text-decoration: none;
  color: var(--pr-ink);
  font-weight: 600;
}

.pr-brand svg { display: block; }
/* The one class the practice header borrows from the site's stylesheet, and it
   leaked: /styles.css paints .brand-mark with its own light-only --ink, so on a
   dark page the mark was near-black on charcoal and all but disappeared. Its
   red dot is drawn with the site's --accent, which is the light-mode red at
   0.52 lightness — the same problem, one shade milder. Both are re-pointed at
   this platform's tokens here rather than by editing /styles.css, which the
   whole site depends on. */
.pr-brand .brand-mark {
  color: inherit;
  --accent: var(--pr-red);
}
.pr-brand-name { font-size: 14.5px; letter-spacing: -0.005em; white-space: nowrap; }
/* The tag takes its mono, its size, its tracking and its colour from .pr-label
   (§2) — the markup carries both classes. All this adds is the rule it hangs
   off. */
.pr-brand-tag {
  border-left: 1px solid var(--pr-rule);
  padding-left: 9px;
}

.pr-header-nav {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.pr-header-nav a { color: var(--pr-ink-2); text-decoration: none; font-size: 14px; }
.pr-header-nav a:hover { color: var(--pr-red); }

/* ---- the sign-in corner ----------------------------------------------------
   app/ui.mjs writes the slot's two children with no whitespace between them,
   and the slot itself was a bare inline <span>. Measured on the dashboard at
   375px signed in, the name's right edge and the Sign out button's left edge
   were BOTH x=199: not an overlap, but a flush abutment, which reads on screen
   as one run of text ("write-path-probeSign out"). A flex row with the nav's
   own 8px gap, so the corner spaces like everything beside it. */
.pr-header-nav [data-pr-authslot] {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.pr-whoami { font-family: var(--font-mono); font-size: 12px; color: var(--pr-ink-3) !important; }
/* The account link carries two words and shows one: the student's name where
   there is room for it, the word "Account" where there is not. Which of the
   two is a question about the VIEWPORT, so it is answered by the media query
   at the foot of this file and not by JavaScript — a phone turned on its side
   changes the answer and no re-render happens. */
.pr-whoami-short { display: none; }

/* Header controls read small but are NOT --sm: every control on this platform
   is --pr-tap tall, and the header is where a thumb reaches for "Save & exit"
   mid-question. Small type, full target. */
.pr-header-nav .pr-btn { font-size: 13px; padding: 4px 11px; }

/* ---- the theme control -----------------------------------------------------
   Three states — follow the system, light, dark — and the button always names
   the state the NEXT press moves to, never the one you are in. The word is
   written by app/theme.mjs; the before-paint read of localStorage is an inline
   <script> in each page's <head>, because a module is deferred and the page
   would paint in the system theme and then flip.

   min-width so that Dark → Light → System does not shuffle the header about as
   the word changes. */
.pr-theme-btn { min-width: 62px; }

/* The page hero on every screen that is not Today. It carried --pr-display,
   which tops out at 34px — a card title on a 1160px deck, not a page title.
   The same clamp the hero headline uses, so the two screens are the same
   product. */
.pr-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(30px, 2.2vw + 20px, 44px);
  line-height: 1.04;
  letter-spacing: -0.012em;
  margin: var(--pr-s2) 0 var(--pr-s3);
  text-wrap: balance;
}
/* the marketing site's second beat: one italic serif word, in the one red */
.pr-title i { font-style: italic; color: var(--pr-red); }

.pr-lede { color: var(--pr-ink-2); margin: 0 0 26px; max-width: 46ch; font-size: 16.5px; }
.pr-lede b { font-weight: 600; color: var(--pr-ink); font-variant-numeric: tabular-nums; }
.pr-lede a { color: var(--pr-red); }

/* A section is a CARD, not a rule with things under it.

   The old shape — a mono label over a hairline, then loose content — is
   correct typography for a document and is what made this screen read as one.
   In a shell, content that is not on a card floats: the eye has nothing to
   tell it where "worth your attention" ends and the specification map begins,
   because the only boundary is a 1px line that also appears inside both of
   them. The card is the boundary. */
.pr-section {
  margin: 0 0 var(--pr-s5);
  background: var(--pr-sheet);
  border: 1px solid var(--pr-rule);
  border-radius: var(--pr-r);
  box-shadow: var(--pr-e1);
  overflow: hidden;
}
.pr-section-title {                 /* markup carries .pr-label — see §2 */
  display: flex;
  align-items: center;
  gap: var(--pr-s3);
  font-size: var(--pr-t-sm);
  font-family: var(--font-mono);
  color: var(--pr-ink-3);
  margin: 0;
  padding: var(--pr-s4) var(--pr-s5);
  background: var(--pr-sunk);
  border-bottom: 1px solid var(--pr-rule);
}
/* The map's own blocks carry their rules to the card's edge, so the card
   gives it vertical room only. The weak-skill rows are full-bleed by design. */
.pr-section > .pr-map { padding: 0 var(--pr-s5) var(--pr-s2); }
.pr-section > .pr-mapsum {
  margin: 0; max-width: none;
  border: 0; border-bottom: 1px solid var(--pr-rule); border-radius: 0;
  padding: var(--pr-s5);
  background: transparent;
}
.pr-section > .pr-weaks > li > .pr-empty { padding: var(--pr-s5); margin: 0; }

/* =========================================================== 4. BUTTONS ====
   design-proto.css §4, with two name changes and nothing else: the prototype's
   .pr-btn--go is this platform's .pr-btn--primary (the pages and modules have
   emitted --primary since they were written), and --ghost is ours, a bordered
   quiet that predates the prototype and that the sign-in corner uses. --quiet
   and --sm come across as they are.

   min-height is --pr-tap now, not 44px. It was the one control on the platform
   that was not 48. */

.pr-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: var(--pr-tap);
  padding: 10px 18px;
  border: 1px solid var(--pr-rule-2);
  border-radius: var(--pr-r);
  background: var(--pr-sheet);
  color: var(--pr-ink);
  font: inherit;
  font-size: 15px;
  font-weight: 500;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--pr-speed) ease, border-color var(--pr-speed) ease;
}

.pr-btn:hover:not(:disabled) { background: var(--pr-sunk); border-color: var(--pr-ink-3); }
.pr-btn:disabled { opacity: 0.45; cursor: default; }

/* The loudest object on the page, and the reason .pr-beta above had to stop
   competing with it: ink, inverted, in both themes. */
.pr-btn--primary { background: var(--pr-ink); border-color: var(--pr-ink); color: var(--pr-ink-flip); }
.pr-btn--primary:hover:not(:disabled) { background: var(--pr-ink-2); border-color: var(--pr-ink-2); }

.pr-btn--ghost { border-color: var(--pr-rule); color: var(--pr-ink-2); }
.pr-btn--ghost:hover:not(:disabled) { border-color: var(--pr-ink-2); color: var(--pr-ink); }

.pr-btn--quiet { border-color: transparent; background: transparent; color: var(--pr-ink-2); }
.pr-btn--quiet:hover:not(:disabled) { background: var(--pr-sunk); border-color: var(--pr-rule); }

/* --sm is 34px, and that is BELOW --pr-tap on purpose in the prototype: it is
   for an in-flow secondary affordance sitting inside a card of prose ("See the
   method anyway"), not for a control you reach for with a thumb. Nothing in the
   header may use it — see .pr-header-nav .pr-btn above, which keeps the full
   target and only shrinks the type. */
.pr-btn--sm { min-height: 34px; padding: 4px 11px; font-size: 13px; }

/* -------------------------------------------------------------- dashboard */

/* WHAT DO I DO TODAY, in the first screenful.

   The page used to open with a 40px display-serif headline, a three-line
   sentence and a fourteen-bar chart; the button you came for was the fourth
   object down and, on a 667px phone, sat exactly on the fold. Measured at
   375px: 435px of page before the call to action.

   Now the session card is first and everything else is under it. The facts
   about the practice did not get quieter — they got shorter, and they moved
   to where a fact belongs, which is after the decision it informs. */

.pr-dash { padding-top: var(--pr-s5); }

/* ---- the three figures ---------------------------------------------------
   THE STREAK IS STILL DEAD (§7.5). What is here is the same information with
   none of the coercion: how many questions have been marked, how many were
   right first go, and how many of the last fourteen days had practice on
   them. A gap is a gap. Nothing can be taken away, so there is nothing to
   protect and nothing to lose by being ill for a week.

   The fourth tile the pre-redesign grid carried — "0 / 47 skills mastered" —
   is still gone from here and still forbidden by §7.6, because a denominator
   the student cannot move is not a measure of them. It exists exactly once,
   on the specification map, where the denominator IS moveable: skills with
   questions written. */
/* Separate tiles rather than one hairline-divided slab. The slab was right
   for three figures in a 34rem column; four tiles across a 1160px deck have
   to read as four things you could act on, and a shared border makes them
   read as one table. */
.pr-stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--pr-s4);
  margin: 0 0 var(--pr-s5);
}
/* SUNK, not raised. The row above this one is four LANES — doors, on white,
   lifted, with a hover — and the stats had the identical treatment: same
   ground, same radius, same border, same shadow, same 4-up grid. Eight
   identical cards, four of which do something when you press them and four of
   which do not, and nothing on screen saying which.

   Information sits IN the page; things you can press sit ON it. That is one
   distinction doing the work, and it costs no extra colour. */
.pr-stat {
  display: grid;
  align-content: start;
  gap: 2px;
  min-width: 0;
  padding: var(--pr-s5);
  background: var(--pr-sunk);
  border: 1px solid var(--pr-rule);
  border-radius: var(--pr-r);
}
/* The fortnight tile is the wide one: it carries a chart, and two equal
   columns would squeeze fourteen bars into 150px.

   It is ORDERED to the top of the stack on a phone as well as spanned. Left
   in source order it was the fourth of four in a two-column grid, so the
   third tile took the left cell of row 2 and the right cell was simply
   empty — a visible hole in the middle of the grid at 375px. */
.pr-stat--wide { grid-column: 1 / -1; order: -1; }
/* The figure is the display serif, at the size the marketing site uses for a
   headline. A number a student came to see should be the largest thing in its
   own tile — the old 27px sat below the topic titles under it. */
.pr-stat-n {
  display: block;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(30px, 2.4vw + 18px, 46px);
  line-height: 1;
  /* NO negative tracking on numerals. Instrument Serif's 1 has a short flag
     and at 46px with -0.005em "11" closes up and reads as "ll" — which on a
     tile whose whole job is a number is the worst possible failure. Figures
     get 0; the display serif keeps its tracking everywhere it sets words. */
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
  color: var(--pr-ink);
}
/* The denominator is part of the figure and must not compete with it. */
.pr-stat-n small { font-size: 0.44em; color: var(--pr-ink-3); }
.pr-stat-l { display: block; margin-top: 6px; }   /* markup carries .pr-label */
/* The line under the label: what the figure is out of, or which way it moved.
   A number with no direction is a fact; a number with a direction is
   progress, which is the whole point of the tile. */
.pr-stat-d { font-size: var(--pr-t-base); color: var(--pr-ink-2); }

/* the attendance rule — fourteen days, no streak to protect and none to lose.
   Momentum without a leash. */
.pr-days {
  display: flex;
  align-items: flex-end;
  gap: 3px;
  height: 20px;
  margin: var(--pr-s3) 0 0;
}
/* Each bar's height arrives as an inline --pr-day-h, because it is data and
   data does not belong in a stylesheet. Everything else about the bar —
   colour, radius, the floor an empty day sits at — is here. */
.pr-day {
  flex: 1 1 0;
  min-width: 0;
  height: var(--pr-day-h, 3px);
  min-height: 3px;
  border-radius: 1.5px;
  background: var(--pr-rule);
}
/* A day with practice on it is drawn in the mastery green, because that is
   what the scale means everywhere else on this page: work that counted. */
.pr-day.is-on { background: var(--pr-m4); }
/* Today, and it is NOT red any more.

   The argument for red was §7.2's first grammar — a dot means look here — and
   it is the same argument the due card made for a red wash before that card
   was changed for exactly this reason: on a screen where red is also the
   mark-wrong colour, a student who HAS practised today saw today drawn in the
   failure colour, sitting in a run of green days. Position is carried by an
   underline instead, which cannot be confused with a verdict, and the bar
   itself takes the darkest step of the mastery ramp like every other day that
   counted. */
.pr-day.is-today { background: var(--pr-m5); position: relative; }
.pr-day.is-today::after {
  content: "";
  position: absolute; left: 0; right: 0; bottom: -5px;
  height: 2px; border-radius: 1px;
  background: var(--pr-ink-3);
}
.pr-days { padding-bottom: 5px; }
.pr-days-cap { display: block; margin: 0 0 var(--pr-s6); }

/* The house for the two sentences that are not figures. It is a quieter
   surface than a section card — sunk rather than raised — because it is
   context, not a thing to act on, and a third card weight here would be a
   third grammar on one screen. Collapses to nothing when both children are
   hidden, so a signed-in student with progress never sees an empty box. */
.pr-notes { display: grid; gap: var(--pr-s3); margin: 0 0 var(--pr-s5); }
.pr-notes:not(:has(> :not([hidden]))) { display: none; }
.pr-notes > * { margin: 0; }
.pr-notes:has(> :not([hidden])) {
  padding: var(--pr-s5);
  background: var(--pr-sunk);
  border-radius: var(--pr-r);
}

.pr-lede { color: var(--pr-ink-2); margin: 0 0 var(--pr-s6); max-width: 46ch; font-size: var(--pr-t-lg); }
.pr-lede b { font-weight: 600; color: var(--pr-ink); font-variant-numeric: tabular-nums; }
.pr-lede a { color: var(--pr-red); }

/* The guest note sits under the figures rather than above them: the numbers
   are what a returning student came for, and "your progress lives on this
   device" is the caveat on them. */
.pr-guest { margin: 0 0 var(--pr-s6); font-size: var(--pr-t-base); color: var(--pr-ink-3); }
.pr-guest a { color: var(--pr-red); }

/* ---- resume: the unfinished session, unchanged by this task ------------- */
.pr-resume {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: center;
  justify-content: space-between;
  /* An unfinished session is STATE, not a mark. §7.2 keeps red for marks
     made — a dot, an italic serif word, a 3px rule or a ✗ — and never a fill
     larger than a chip. This card used to be a red 1px/4px frame over a 4%
     red wash, which is both a fill and a rule, and reads as "you got this
     wrong" on a screen where nothing has been marked at all. Ink instead:
     the 4px left edge still makes it the loudest card on the page. */
  border: 1px solid var(--pr-rule);
  border-left: 4px solid var(--pr-ink);
  border-radius: var(--pr-r);
  padding: var(--pr-s4);
  /* It is now the FIRST thing on the page when it exists, because an
     unfinished session is a more specific answer to "what do I do today" than
     a new one is. */
  margin: 0 0 var(--pr-s4);
  background: var(--pr-sunk);
}

.pr-resume-body { min-width: 240px; flex: 1; }
.pr-resume-kicker {                 /* markup carries .pr-label — see §2 */
  margin: 0 0 4px;
  font-family: var(--font-mono);
  font-size: 11px;
  /* colour comes from .pr-label (--pr-ink-3), not from --pr-red: an
     unfinished session is state, and §7.2 keeps red for marks made. */
}
.pr-resume-line { margin: 0 0 10px; font-size: 15px; }
.pr-resume-actions { display: flex; gap: 8px; flex-wrap: wrap; }

.pr-progress {
  height: 6px;
  background: var(--pr-sunk);
  border-radius: 3px;
  overflow: hidden;
  max-width: 320px;
}
/* Progress is state-but-not-a-mark, so the fill is ink (§7.2). A 320px-wide
   red bar is the largest red area on the platform and it is not marking
   anything. (Task 5 deletes this bar for the 12-rung ladder; until then it
   at least stops lying.) */
.pr-progress span { display: block; height: 100%; background: var(--pr-ink); transition: width 0.25s ease; }

/* ---- the due card: the one call to action --------------------------------
   design-proto.css §9's .pr-today, ported.

   Ink, not red. Due is STATE, not a mark (§7.2) — the old resume card was a
   red border over a red wash, which is both a fill and a rule, and read as
   "you got this wrong" on a screen where nothing had been marked at all. The
   card inverts the page instead: in light it is an ink slab, in dark it is
   the cream one, and either way it is the loudest object on the dashboard,
   which is what §7.2 asks of the thing that matters most. The design review's
   own dark mockup (shot-dark.png) is exactly this.

   The italic serif count is the third grammar — brand voice, in a title.   */
.pr-today {
  position: relative;
  overflow: hidden;
  display: grid;
  gap: var(--pr-s6);
  margin: 0 0 var(--pr-s5);
  padding: var(--pr-s6) var(--pr-s6);
  border-radius: var(--pr-r-lg);
  /* The shell's own ink, not the page's. The rail beside this card is
     --pr-rail in both themes, and a hero that inverted to cream at dusk while
     the rail stayed dark would break the frame the two of them make together.
     That is the one thing that changed here: the card used to be the PAGE
     inverted, and it is now part of the CHROME. */
  background-color: var(--pr-rail);
  color: oklch(0.962 0.004 80);
  box-shadow: var(--pr-e3);
  /* Its own ruling, not --pr-grid. --pr-grid is drawn in the PAGE's ink, and
     this card is the page inverted, so the shared token is the wrong polarity
     in both themes at once — and the prototype's fix,
     `:root:not([data-pr-theme="dark"]) .pr-today { --pr-grid: ... }`, also
     matches the commonest dark state there is (system dark, no attribute set),
     where it would paint white lines on a cream card. One token of its own,
     defined in all three theme states, has neither problem. */
}
/* The squared paper, masked to a radial fade rather than tiled edge to edge.
   Flat across the whole slab it competed with the headline; faded towards the
   corner the rings sit in, it reads as the marketing site's texture and the
   type stays the loudest thing on the card. A pseudo-element rather than a
   background-image so the mask cannot touch the card's own colour. */
.pr-today::after {
  content: "";
  position: absolute; inset: 0;
  pointer-events: none;
  background-image:
    linear-gradient(to right,  oklch(1 0 0 / 0.05) 1px, transparent 1px),
    linear-gradient(to bottom, oklch(1 0 0 / 0.05) 1px, transparent 1px);
  background-size: 34px 34px;
  -webkit-mask-image: radial-gradient(125% 105% at 80% 26%, #000 8%, transparent 72%);
          mask-image: radial-gradient(125% 105% at 80% 26%, #000 8%, transparent 72%);
}
.pr-today > * { position: relative; z-index: 1; }
.pr-today-b { min-width: 0; }
/* The kicker carries the page's identity so the h1 underneath can be about
   TODAY rather than about the product. .pr-label, so it is mono and tracked —
   and it may never carry chemistry, which "Your practice" does not. */
.pr-today-k {
  margin: 0 0 var(--pr-s2);
  color: inherit !important;      /* .pr-label is --pr-ink-3, wrong on this ground */
  opacity: 0.62;
}
.pr-today-n {
  font-family: var(--font-display);
  font-weight: 400;
  /* The brand's headline size. 28px was a card title; this is the line the
     student reads first and the only display type on the screen. */
  font-size: clamp(30px, 2.5vw + 18px, 50px);
  line-height: 1.04;
  margin: 0 0 var(--pr-s2);
  letter-spacing: -0.015em;
  text-wrap: balance;
}
/* The one red on the card, and it is a word rather than an area. Its own
   token because --pr-red is tuned against paper and this ground is ink. */
.pr-today-n em { font-style: italic; color: var(--pr-red-on-ink); }
.pr-today-s { margin: 0; font-size: var(--pr-t-lg); opacity: 0.78; line-height: 1.5; max-width: 46ch; }
/* The button is the card inverted again, so it reads as a control rather than
   as more card. NOT .pr-btn--primary, which is --pr-ink on --pr-ink here.

   Full width, and that is the whole point of the restructure: on a phone the
   thing you came to do is now a 48px target spanning the card, not a
   180px-wide button beside three lines of text. */
.pr-today-go {
  display: flex;
  width: 100%;
  margin-top: var(--pr-s5);
  min-height: 52px;
  font-size: var(--pr-t-lg);
  font-weight: 600;
}

/* ---- the rings: three arcs, one per unit of the specification -------------
   The platform could tell a student how many questions they had answered and
   nothing about how much of the COURSE they had. These are that, and they are
   the reason the hero is a grid rather than a stack.

   The arc is an SVG circle with a dash pattern, so the same element carries
   both the geometry and the number, and the fraction is written beside it in
   words — colour is never the only carrier (§7.6). */
.pr-arcs { display: flex; gap: var(--pr-s5); }
.pr-arc { display: grid; justify-items: center; gap: 6px; }
.pr-arc svg { display: block; width: 88px; height: 88px; }
.pr-arc-track { stroke: oklch(1 0 0 / 0.14); }
.pr-arc-fill {
  stroke: var(--pr-arc-c, var(--pr-m5));
  stroke-linecap: round;
  transform: rotate(-90deg);
  transform-origin: 50% 50%;
  transition: stroke-dashoffset var(--pr-slow) var(--pr-ease);
}
.pr-arc-n {
  font-family: var(--font-display); font-size: 22px;
  fill: var(--pr-on-ink); text-anchor: middle; dominant-baseline: central;
}
/* The denominator, under the numerator and deliberately quieter — it is the
   context, not the achievement. Both are on screen because §7.6 says colour
   is never the only carrier, and an aria-label does not satisfy that for the
   student looking at it. */
.pr-arc-d {
  font-family: var(--font-mono); font-size: 10px;
  fill: oklch(0.74 0.020 60); text-anchor: middle; dominant-baseline: central;
}
/* .pr-kick, NOT .pr-label — the unit title is a value, and §7.7 says a label's
   text is a literal all the way to the screen. (This comment said the
   opposite of what the markup does; dashboard.mjs has always emitted
   .pr-kick here.) */
.pr-arc-l { color: oklch(0.74 0.020 60); }
/* The card is the SHELL's ink now, not the page inverted, so the button on it
   cannot be the page inverted either — that is what made it invisible in
   dark. --pr-on-ink does not flip, because the ground under it does not. */
.pr-today .pr-btn {
  background: var(--pr-on-ink);
  border-color: var(--pr-on-ink);
  color: oklch(0.19 0.018 250);
}
.pr-today .pr-btn:hover:not(:disabled) {
  background: var(--pr-on-ink-hover);
  border-color: var(--pr-on-ink-hover);
}
/* The focus ring is --pr-focus, which is tuned against paper and flips with
   the theme while the rail does not — 3.3:1 on the standing block in light.
   On the chrome it is drawn in the same non-inverting ink as everything else
   here, so it is 15:1 in both themes instead of scraping past 3. */
.pr-rail :focus-visible,
.pr-today :focus-visible { outline-color: var(--pr-on-ink); }
.pr-start-note { margin: calc(-1 * var(--pr-s3)) 0 var(--pr-s6); font-size: var(--pr-t-sm); color: var(--pr-ink-3); }

/* ---- the lanes: practise a KIND of question -------------------------------
   A student revising for Paper 3 does not want a topic, they want the
   practical work or the maths. The server deals these free; they sit directly
   under the session card because they are the same kind of object — a way in
   — and burying a differentiator in a menu is how it goes unused.

   A lane with nothing behind it yet is NOT hidden. Half the specification is
   still uncovered, so "nothing here yet" is a common state and the honest
   thing is to say so at the moment of the tap, not to remove the door. */
.pr-lanes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--pr-s4);
  margin: 0 0 var(--pr-s5);
  padding: 0;
  list-style: none;
}
.pr-lane {
  display: grid;
  align-content: start;
  gap: var(--pr-s2);
  text-align: left;
  min-height: var(--pr-tap);
  padding: var(--pr-s5);
  border: 1px solid var(--pr-rule);
  border-radius: var(--pr-r);
  background: var(--pr-sheet);
  box-shadow: var(--pr-e1);
  transition: transform var(--pr-speed) var(--pr-ease),
              box-shadow var(--pr-speed) var(--pr-ease),
              border-color var(--pr-speed) var(--pr-ease);
  color: var(--pr-ink);
  font: inherit;
  text-decoration: none;
  cursor: pointer;
  width: 100%;
  /* (this block's transition is declared above, with the hover lift — a second
     one here silently replaced it and the lift snapped) */
}
.pr-lane:hover {
  border-color: var(--pr-rule-2);
  transform: translateY(-2px);
  box-shadow: var(--pr-e2);
}
/* A door needs a handle. Four tiles of two lines of text each read as a
   paragraph broken into boxes; the icon is what makes each one a place. */
.pr-lane-i {
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border-radius: var(--pr-r);
  background: var(--pr-sunk);
  color: var(--pr-ink-2);
}
.pr-lane-i svg { width: 19px; height: 19px; }
.pr-lane-n { font-size: var(--pr-t-lg); font-weight: 600; line-height: 1.25; }
.pr-lane-s { font-size: var(--pr-t-sm); color: var(--pr-ink-3); line-height: 1.4; }
/* The paid door reads as the same kind of object and a different offer: the
   ink edge is the platform's "this is the programme" mark. */
/* The one accented door. Red as a WASH at chip strength with a red icon
   plate — the mock is the thing a student in April actually needs, and it was
   previously distinguished by a slightly darker hairline nobody saw. */
.pr-lane--mock {
  background: var(--pr-red-wash);
  border-color: color-mix(in oklch, var(--pr-red) 30%, transparent);
}
.pr-lane--mock .pr-lane-i { background: var(--pr-red); color: oklch(0.99 0 0); }
/* A lane the platform cannot fill yet keeps its place in the row and stops
   pretending to be a door: dimmed, not removed, and it says why. */
.pr-lane.is-empty { background: var(--pr-paper); border-style: dashed; cursor: default; }
.pr-lane.is-empty .pr-lane-n { color: var(--pr-ink-3); }

/* ---- the specification map ------------------------------------------------
   design-proto.css §9, ported. §7.6: topic ROWS, not skill cells. Per topic a
   name, a red dot if something in it is due, a coverage bar and a fraction;
   tap opens the skills in place with a plain <details>.

   It replaces a list of 34 bordered cards, each with a five-segment mastery
   RING per skill. The ring is deleted rather than restyled: 47 rings is 47
   objects and no shape, where one bar per topic gives the skyline §7.6 is
   after — topic size and topic strength in the same object, readable at a
   glance down a phone.

   WHAT A CELL MEANS. One cell per WRITTEN skill, inked to that skill's
   mastery, then dashed cells making the row up to the topic's size. No skill
   in the taxonomy is tagged to a SUBTOPIC — every spec_ref is a topic code —
   so a per-subtopic map would be inventing a claim the data cannot support.
   The reasoning, and the join it rests on, is in app/spec-map.mjs.

   ONE COLUMN AT EVERY WIDTH, deliberately. A two-column grid leaves a hole the
   height of an open <details> beside it, and one column means the desktop and
   the phone are the same object rather than two designs to keep in step.

   The prototype puts a .pr-btn--sm "Drill" on every skill row. It is not
   ported: --sm is 34px against a --pr-tap of 48px, and this is the one screen
   that would have put 47 of them on a phone. The topic's own full-size
   "Practise this topic" is inside the same disclosure and covers the same
   intent, so --sm is still unused and Task 4's question about whether it
   should exist at all is still open.                                        */

/* 46rem was the right measure for a document column and is the wrong one for
   an app: inside a 1160px shell it left the skyline drawn at a third of the
   width available to it, which is exactly the thing the map is FOR. Full
   width, one column still (a two-column grid leaves a hole the height of an
   open <details> beside it), and the row re-lays itself at 900px so the name
   and the cells sit on one line instead of stacking. */
.pr-map { max-width: none; }

/* The folded first-run map: the three unit headings, and nothing under them.
   The rows are not removed, so nothing about the map's structure or its
   accessibility changes — they are simply not drawn until asked for. */
.pr-map.is-folded .pr-topics { display: none; }
.pr-map.is-folded .pr-unitblock { margin-bottom: var(--pr-s3); }
.pr-map-more { margin: 0 var(--pr-s5) var(--pr-s5); }

/* ---- the whole-course line, and the legend ------------------------------
   The map had no way in: 34 rows of cells with no scale beside them, so a
   shade meant nothing until you had read a paragraph above it and remembered
   it. The scale now sits next to the thing it scales. */
.pr-mapsum {
  margin: 0 0 var(--pr-s5);
  padding: var(--pr-s4);
  border: 1px solid var(--pr-rule);
  border-radius: var(--pr-r);
  background: var(--pr-sheet);
  max-width: 46rem;
}
.pr-mapsum-top { display: flex; align-items: baseline; gap: var(--pr-s3); flex-wrap: wrap; }
.pr-mapsum-n {
  font-size: var(--pr-t-2xl); font-weight: 600; letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums; line-height: 1;
}
.pr-mapsum-of { color: var(--pr-ink-3); font-weight: 400; }
.pr-mapsum-t { font-size: var(--pr-t-base); color: var(--pr-ink-2); flex: 1 1 200px; min-width: 0; }
.pr-mapsum-bar {
  height: 8px; border-radius: 4px; background: var(--pr-m0);
  overflow: hidden; margin: var(--pr-s3) 0 var(--pr-s3);
}
.pr-mapsum-bar span { display: block; height: 100%; background: var(--pr-m5); }
.pr-key-legend {
  list-style: none; margin: 0; padding: 0;
  display: flex; align-items: center; gap: 3px; flex-wrap: wrap;
}
.pr-key-legend li { display: flex; }
.pr-key-word {
  font-family: var(--font-mono); font-size: var(--pr-t-xs); color: var(--pr-ink-3);
  margin: 0 var(--pr-s4) 0 var(--pr-s2); white-space: nowrap;
}

.pr-unitblock { display: block; margin: 0 0 var(--pr-s6); }
.pr-unitblock-head {
  display: flex; align-items: baseline; gap: var(--pr-s3);
  padding: 0 0 var(--pr-s2); margin-bottom: 2px;
  border-bottom: 2px solid var(--pr-ink);
}
.pr-unitblock-name { font-size: var(--pr-t-lg); font-weight: 600; margin: 0; letter-spacing: -0.005em; }
.pr-unitblock-sub {
  font-family: var(--font-mono); font-size: var(--pr-micro);
  color: var(--pr-ink-3); margin-left: auto;
}
.pr-unitblock-n {
  font-family: var(--font-mono); font-size: var(--pr-t-sm); color: var(--pr-ink-2);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

.pr-topics { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: minmax(0, 1fr); }

.pr-topic { border-bottom: 1px solid var(--pr-rule); }

/* THE ROW IS TWO CONTROLS, NOT ONE.
   The wide one selects the topic; the narrow one opens it. They are siblings
   in a flex row rather than nested, because a button inside a button is not
   valid HTML and a <summary> containing a checkbox gives one click two
   meanings. See topicRow() in dashboard.mjs. */
.pr-topic-head { display: flex; align-items: stretch; gap: 0; }

.pr-topic-pick {
  flex: 1 1 auto;
  min-width: 0;
  /* Reset, because this is a <button> now and inherits none of the list's
     typography from its ancestors the way the old <summary> did. */
  appearance: none; background: none; border: 0; font: inherit; color: inherit;
  text-align: left;
  cursor: pointer;
  /* 10px block padding on a 26px two-line body clears --pr-tap at 46px, and
     the row was 52. Over 34 rows that is 200px of phone back. */
  padding: 10px var(--pr-s1);
  border-radius: var(--pr-r-sm);
  /* minmax(0, …) and never a bare 1fr (§7.8): a grid item's automatic minimum
     is min-content, and the longest heading here — "Chemical equilibria, Le
     Chatelier's principle and Kc" — would otherwise push the fraction off a
     375px screen rather than wrapping. */
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 4px var(--pr-s3);
  transition: background var(--pr-speed) ease;
}
.pr-topic-pick:hover { background: var(--pr-sunk); }
/* A topic with nothing written yet is not a control. It keeps the row's exact
   shape so the list stays a list, and loses only the pointer and the hover. */
.pr-topic-pick.is-none { cursor: default; }
.pr-topic-pick.is-none:hover { background: none; }

/* THE TICK. An outline square that fills with INK when checked — never with
   red. §7.2 allows red as a dot, a 3px rule or an italic serif word, and a
   selected row is none of those three: it is a choice the student has made,
   not a mark the platform has awarded. Filling it red would say "wrong".

   The check itself is two borders on a rotated pseudo-element rather than a
   glyph, so it cannot be re-flowed by a font stack that lacks it. */
.pr-tick {
  grid-column: 1; grid-row: 1 / 3; align-self: center;
  position: relative;
  flex: 0 0 auto; width: 20px; height: 20px;
  border: 1.5px solid var(--pr-rule-2);
  border-radius: var(--pr-r-sm);
  background: var(--pr-sheet);
  transition: background var(--pr-speed) ease, border-color var(--pr-speed) ease;
}
.pr-tick::after {
  content: ''; position: absolute;
  left: 6px; top: 2.5px; width: 5px; height: 10px;
  border: solid var(--pr-ink-flip);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg) scale(0.6);
  opacity: 0;
  transition: opacity var(--pr-speed) ease, transform var(--pr-speed) ease;
}
.pr-topic.is-picked .pr-tick { background: var(--pr-ink); border-color: var(--pr-ink); }
.pr-topic.is-picked .pr-tick::after { opacity: 1; transform: rotate(45deg) scale(1); }
/* The un-pickable row keeps the column so every name starts at the same x, and
   shows nothing in it. */
.pr-tick--none, .pr-topic-pick.is-none .pr-tick { visibility: hidden; }

/* Selection has to be legible while SCROLLING, not only when the row is under
   the thumb — a student ticks four topics down a 34-row list and needs to see
   which four on the way back up. A sunk field plus a 2px ink edge. Two, not
   three: a 3px rule is the marking grammar and means a mark has been made. */
.pr-topic.is-picked { background: var(--pr-sunk); box-shadow: inset 2px 0 0 var(--pr-ink); }

.pr-topic-name { grid-column: 2; grid-row: 1; font-size: var(--pr-t-md); line-height: 1.3; font-weight: 500; }
/* §7.2's first grammar: a DOT means look here. Never a fill. */
.pr-topic-dot { color: var(--pr-red); font-size: var(--pr-t-md); line-height: 1; }

.pr-cover { grid-column: 2; grid-row: 2; display: flex; gap: 2px; align-items: center; }

/* THE CELL IS THE ONE SCALE IN THE PRODUCT.
   It was three shades of grey mixed out of --pr-ink, which at 375px on a
   phone in daylight is one shade of grey: a mastered topic and a shaky one
   drew the same picture, so the map could not be read at the distance it is
   actually read from. Six steps of the mastery ramp instead — see §1 — and
   they are taller, because 9px of a 2px-gapped bar is a texture and not a
   chart.

   Colour is never the only carrier: the fraction sits beside every bar and
   the level word beside every skill. */
.pr-cell {
  flex: 0 0 auto; width: 13px; height: 12px; border-radius: 2px;
  background: var(--pr-m0);
}
.pr-cell[data-l="1"] { background: var(--pr-m1); }
.pr-cell[data-l="2"] { background: var(--pr-m2); }
.pr-cell[data-l="3"] { background: var(--pr-m3); }
.pr-cell[data-l="4"] { background: var(--pr-m4); }
.pr-cell[data-l="5"] { background: var(--pr-m5); }
/* not written yet — an outline, so it reads as a space left for something
   rather than as a skill you have scored nothing on */
.pr-cell[data-l="x"] { background: transparent; border: 1px dashed var(--pr-mx); }

.pr-topic-n {
  grid-column: 3; grid-row: 1; align-self: center;
  font-family: var(--font-mono); font-size: var(--pr-t-sm); color: var(--pr-ink-2);
  font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap;
}

/* The disclosure, now a control of its own beside the row rather than the
   whole row. --pr-tap wide so the caret is a real target, and no wider: every
   pixel it takes is a pixel off the name, which is what the student reads. */
.pr-topic-more {
  flex: 0 0 auto;
  appearance: none; background: none; border: 0; color: inherit;
  width: var(--pr-tap); align-self: stretch;
  display: grid; place-items: center;
  cursor: pointer;
  border-radius: var(--pr-r-sm);
}
.pr-topic-more:hover { background: var(--pr-sunk); }

.pr-topic-panel { padding-left: var(--pr-s1); }

/* A disclosure affordance the prototype does not have, and the one addition to
   its §9. The mockup's only "this opens" signal is :hover, which a phone does
   not have — and the phone is the design case. It is the SAME marker the
   marking sheet's worked solution already uses (.pr-worked > summary::before):
   the ▸ glyph in --pr-ink-3, rotated 90° when open. A second disclosure
   grammar on one platform would be one too many. aria-hidden, because
   <details> announces its own expanded state. */
.pr-topic-caret {
  color: var(--pr-ink-3); font-size: 12px; line-height: 1;
  transition: transform var(--pr-speed) ease;
}
/* The state now lives on the button's aria-expanded rather than on <details
   open>, so the rotation reads off the same attribute a screen reader does —
   there is only one source of truth for "this is open". */
.pr-topic-more[aria-expanded="true"] .pr-topic-caret { transform: rotate(90deg); }

/* The desktop topic row: name, then the skyline, then the fraction. Below
   900px they stack, which is the phone design and stays exactly as it was.
   The cells get their own column and grow into it — at 1160px a nine-skill
   topic finally draws nine legible cells instead of nine 13px stubs in a
   third of the page. */
@media (min-width: 900px) {
  .pr-topic-pick {
    /* The cells column is sized to its CONTENT, not given a fraction of the
       row. As a 1.1fr track it stretched to whatever was left, which at 1440px
       put ~430px of nothing between the last cell and the fraction — thirty-
       four rows of it. The name takes the slack instead, which is the column
       that can actually use it. */
    grid-template-columns: auto minmax(11rem, 1fr) auto auto;
    column-gap: var(--pr-s5);
    padding: 9px var(--pr-s3);
  }
  /* So a nine-skill topic and a one-skill topic still start their cells at the
     same x — a skyline you read down the page, not a ragged edge. */
  .pr-cover { min-width: 12rem; }
  .pr-topic-name { grid-column: 2; grid-row: 1; }
  .pr-cover { grid-column: 3; grid-row: 1; justify-content: flex-start; gap: 3px; }
  .pr-cell { width: 17px; height: 15px; border-radius: 3px; }
  .pr-topic-n { grid-column: 4; grid-row: 1; }
}

.pr-skills {
  list-style: none; margin: 0 0 var(--pr-s2); padding: var(--pr-s2) 0 var(--pr-s2) var(--pr-s3);
  display: grid; grid-template-columns: minmax(0, 1fr);
  border-left: 2px solid var(--pr-rule);
}
.pr-skill { display: block; }
/* The row IS the control — no extra button, and the whole 44px line is the
   tap target. Unstyled <button> reset, then the row's own layout. */
.pr-skill-go {
  display: flex; align-items: center; gap: var(--pr-s3);
  width: 100%; min-height: 44px; padding: 5px var(--pr-s2) 5px 0;
  border: 0; background: none; color: inherit; font: inherit;
  text-align: left; cursor: pointer; border-radius: var(--pr-r-sm);
}
.pr-skill-go:hover { background: var(--pr-sheet); }
.pr-skill-caret { margin-left: auto; color: var(--pr-ink-3); font-size: 11px; line-height: 1; }
/* ONE cell at the skill's own level, on the same six-step scale as the bar
   above it. It was five pips of a different shape carrying the same number,
   which is the map's scale said twice in two vocabularies. */
.pr-skill-cell { width: 10px; height: 10px; border-radius: 50%; }
.pr-skill-t { flex: 1; min-width: 0; }
.pr-skill .pr-skill-name { display: block; font-size: var(--pr-t-base); font-weight: 400; line-height: 1.35; }
.pr-skill-meta { display: block; font-family: var(--font-mono); font-size: 10.5px; color: var(--pr-ink-3); margin-top: 1px; }
.pr-due { color: var(--pr-red-ink); }

.pr-pending { font-size: var(--pr-t-sm); color: var(--pr-ink-3); padding: 2px 0 var(--pr-s2) var(--pr-s3); margin: 0; }
.pr-topic-go { margin: 0 0 var(--pr-s3); }

/* ---- the three skills worth your attention -------------------------------
   They were a name, a percentage in mono and two underlined text links that
   wrapped onto their own line at 375px — an arrangement in which the two
   ACTIONS were the smallest, palest things in the block. A student who has
   just been told what they are weakest at should be one thumb away from doing
   something about it, so "Practise" is a real button.

   The ring is the mastery level, on the map's own six-step scale, with the
   level word beside it — colour is never the only carrier. */
/* Rows in the section card, not three bordered boxes inside a bordered box.
   Three cards nested in a card is two frames saying the same thing, and it
   forced the three titles to three different heights. A row also lets the
   name have the width it needs — "Equilibrium constant Kp for homogeneous
   systems" was wrapping to three lines in a third of the page. */
.pr-weaks { list-style: none; margin: 0; padding: 0; display: grid; }
.pr-weak {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: var(--pr-s2) var(--pr-s4);
  padding: var(--pr-s4) var(--pr-s5);
  border-bottom: 1px solid var(--pr-rule);
}
.pr-weak:last-child { border-bottom: 0; }
.pr-weak-t { min-width: 0; }
.pr-weak-name { display: block; font-weight: 600; font-size: var(--pr-t-lg); line-height: 1.3; }
.pr-weak-meta {
  display: block; font-family: var(--font-mono); font-size: var(--pr-t-xs);
  color: var(--pr-ink-3); margin-top: 3px;
}
.pr-weak-links { grid-column: 1 / -1; display: flex; gap: var(--pr-s2); }
.pr-weak-links .pr-btn { flex: 1 1 auto; min-height: 40px; }

/* On a desktop the two actions come off their own line and sit at the end of
   the row, where a row full of free space already was. */
@media (min-width: 760px) {
  .pr-weak { grid-template-columns: auto minmax(0, 1fr) auto; }
  .pr-weak-links { grid-column: 3; grid-row: 1; }
  .pr-weak-links .pr-btn { flex: 0 0 auto; }
}

/* The mastery ring. A conic gradient and a knocked-out middle — no SVG, no
   dependency, and it inherits the card's own ground so it works in both
   themes without a second rule. --pr-ring-p is the percentage and arrives
   inline, because it is data. */
.pr-ring {
  --pr-ring-c: var(--pr-m0);
  flex: 0 0 auto;
  width: 40px; height: 40px; border-radius: 50%;
  display: grid; place-items: center;
  background: conic-gradient(var(--pr-ring-c) calc(var(--pr-ring-p, 0) * 1%), var(--pr-m0) 0);
}
.pr-ring::before {
  content: ''; grid-area: 1 / 1;
  width: 30px; height: 30px; border-radius: 50%;
  background: var(--pr-sheet);
}
.pr-ring b {
  grid-area: 1 / 1;
  font-family: var(--font-mono); font-size: var(--pr-t-sm); font-weight: 600;
  color: var(--pr-ink-2); font-variant-numeric: tabular-nums;
}
.pr-ring[data-l="1"] { --pr-ring-c: var(--pr-m1); }
.pr-ring[data-l="2"] { --pr-ring-c: var(--pr-m2); }
.pr-ring[data-l="3"] { --pr-ring-c: var(--pr-m3); }
.pr-ring[data-l="4"] { --pr-ring-c: var(--pr-m4); }
.pr-ring[data-l="5"] { --pr-ring-c: var(--pr-m5); }

/* ---- the gate: signing in, and the one screen with nothing behind it -----
   The sign-in box sat in the top-left corner of an otherwise empty 1440px
   viewport, which reads as a page that failed to load rather than as a door.
   A gate is centred, in a column its own content decides, with air above it —
   the shape every sign-in screen has, because it is the shape of a thing you
   are meant to walk through and then forget. */
.pr-gate {
  max-width: 34rem;
  margin-inline: auto;
  padding-top: clamp(var(--pr-s6), 7vh, var(--pr-s8));
}
.pr-gate .pr-card { max-width: none; }

/* The signed-in half of the account page. It was left in a 1160px column
   holding 34rem cards, which is the same empty-desk problem the gate above
   fixes and the redesign fixed everywhere else — the one screen it was
   missed on. Wider than the gate, because it is a settings page with tables
   on it, and left-aligned, because you come back to it. */
.pr-settings { max-width: 46rem; }
.pr-settings .pr-card { max-width: none; }
.pr-gate .pr-title { text-align: center; }
.pr-gate > .pr-lede { text-align: center; margin-inline: auto; }

/* ================================================ 4a. THE SESSION DESK =====
   The runner was one 34rem column centred in the viewport: at 1440px the
   question card occupied 23% of the width and the other 77% was empty paper.
   The measure was right — a stem is prose and 34rem is how prose is read —
   but a measure is not a layout, and the answer is not to stretch the column.
   It is to put something useful in the space beside it.

   So: the stem keeps its measure, and the desk beside it carries the three
   things a student in a session actually wants to know and previously could
   not — how many marks they have, which questions are left, and whether the
   skill they are on is moving. Below 1100px the desk folds away entirely and
   the phone design is exactly what it was.
   ========================================================================= */

.pr-sess {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--pr-s6);
  align-items: start;
}
.pr-sess-main { min-width: 0; }

/* The desk's track, collapsed when there is no desk. Set by paintSide(),
   finish() and finishMock(); see the note there for why hiding the item is
   not enough. */
.pr-sess--solo { grid-template-columns: minmax(0, 1fr) !important; }
/* With the desk gone the column is the whole 1000px, so the question goes
   back to its measure and centres in it — a mock paper set 1000px wide is
   not prose any more. */
.pr-sess--solo .pr-sheet { max-width: var(--pr-measure); margin-inline: auto; }
.pr-sess--solo .pr-strip,
.pr-sess--solo .pr-done,
.pr-sess--solo .pr-result,
.pr-sess--solo .pr-marking { margin-inline: auto; }
.pr-sess--solo .pr-strip { max-width: var(--pr-measure); }
/* And the verdict bar stops reserving room for a desk that is not there.
   :has() is the only way to reach an element that is a SIBLING of the grid's
   parent; where it is unsupported the fallback is a Back/Next pair sitting
   slightly left of centre, which is a cosmetic loss and not a broken one. */
.pr-app:has(.pr-sess--solo) .pr-bar-inner { padding-right: 0; }

@media (min-width: 1100px) {
  .pr-app .pr-page--measure.pr-sess {
    /* Not --pr-content: a session is not a dashboard. 1000px is the 34rem
       stem column plus the desk plus one gutter, and widening past it would
       only push the two apart. */
    max-width: 1000px;
    grid-template-columns: minmax(0, 1fr) 300px;
  }
}
.pr-side {
  position: sticky;
  /* Under the sticky page header, plus a gutter. */
  top: calc(var(--pr-head-h) + var(--pr-s4));
  display: grid;
  gap: var(--pr-s4);
}
.pr-side-card {
  background: var(--pr-sheet);
  border: 1px solid var(--pr-rule);
  border-radius: var(--pr-r);
  box-shadow: var(--pr-e1);
  overflow: hidden;
}
.pr-side-h {
  padding: var(--pr-s3) var(--pr-s4);
  background: var(--pr-sunk);
  border-bottom: 1px solid var(--pr-rule);
}
.pr-side-b { padding: var(--pr-s4); display: grid; gap: var(--pr-s3); }

/* AFTER .pr-side's own display:grid, at equal specificity — the same rule the
   tab bar needed and for the same reason. Written above it first, where it
   lost, and a phone got the desk it is explicitly not meant to pay for. */
@media (max-width: 1099px) { .pr-side { display: none; } }

/* Marks so far. The platform counted QUESTIONS right and never marks, which
   is not the currency the exam is in — a student who has taken 7 of 9 marks
   over four questions has done well and the old "2 / 4 right" said the
   opposite. */
.pr-side-score { display: flex; align-items: baseline; gap: var(--pr-s2); }
.pr-side-n {
  font-family: var(--font-display); font-size: 32px; line-height: 1;
  font-variant-numeric: tabular-nums;
}
.pr-side-n small { font-size: 0.5em; color: var(--pr-ink-3); }
.pr-side-t { font-size: var(--pr-t-base); color: var(--pr-ink-2); }

.pr-meter { height: 6px; border-radius: var(--pr-r-pill); background: var(--pr-m0); overflow: hidden; }
.pr-meter span {
  display: block; height: 100%; border-radius: var(--pr-r-pill);
  background: var(--pr-green);
  transition: width var(--pr-slow) var(--pr-ease);
}

/* The vertical ladder: one row per question, its topic, and the mark it took.
   The horizontal strip above the question still exists and still works on a
   phone; this is the same information with room for the names. */
/* Named once, at the top, when every question in the session is on it. */
.pr-side-one { margin: 0; font-size: var(--pr-t-lg); font-weight: 600; line-height: 1.25; }

.pr-sidelist { display: grid; gap: 1px; padding: 0; margin: 0; list-style: none; }
.pr-sidelist li { display: block; }
.pr-siderow {
  display: grid; grid-template-columns: 22px minmax(0, 1fr) auto;
  align-items: center; gap: var(--pr-s3);
  padding: 6px var(--pr-s3);
  border-radius: var(--pr-r-sm);
  font-size: var(--pr-t-base);
  color: var(--pr-ink-2);
}
.pr-siderow[aria-current="true"] { background: var(--pr-sunk); color: var(--pr-ink); font-weight: 600; }
.pr-siderow-n {
  font-family: var(--font-mono); font-size: var(--pr-t-xs); color: var(--pr-ink-3);
  font-variant-numeric: tabular-nums;
}
.pr-siderow-t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pr-siderow-m {
  width: 18px; height: 18px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: 11px; font-weight: 700; line-height: 1;
  border: 1.5px solid var(--pr-rule-2);
  color: transparent;
}
.pr-siderow-m.is-ok   { background: var(--pr-green); border-color: var(--pr-green); color: var(--pr-sheet); }
/* The "½" is 11px at weight 700, so it needs 4.5:1 and --pr-sheet on
   --pr-amber gives 3.57. The disc keeps the amber it shares with every other
   "nearly" on the platform and the glyph takes the dark amber ink instead —
   9.4:1, and the state colour is unchanged. */
.pr-siderow-m.is-near { background: var(--pr-amber); border-color: var(--pr-amber); color: oklch(0.22 0.048 72); }
.pr-siderow-m.is-err  { background: var(--pr-red);   border-color: var(--pr-red);   color: var(--pr-sheet); }

/* "Moving now" — the level the answered skill just went to. /api/check has
   returned the whole new mastery row since check.mjs was written and the
   client threw it away; this is it, put on screen. It is the single most
   motivating thing the product knows and it was invisible. */
.pr-move { display: grid; gap: var(--pr-s2); }
.pr-move-row { display: flex; align-items: center; gap: var(--pr-s3); }
.pr-rungs { display: flex; gap: 3px; }
.pr-rungs i {
  width: 15px; height: 7px; border-radius: 2px;
  background: var(--pr-m0);
  transition: background var(--pr-mid) var(--pr-ease);
}
.pr-rungs i[data-on="1"] { background: var(--pr-m2); }
.pr-rungs i[data-on="2"] { background: var(--pr-m3); }
.pr-rungs i[data-on="3"] { background: var(--pr-m4); }
.pr-rungs i[data-on="4"] { background: var(--pr-m5); }
/* The rung the answer just lit. A one-shot pulse, and the only place on the
   platform where a mastery cell moves under the student's eye. */
.pr-rungs i.is-new { animation: pr-pop var(--pr-slow) var(--pr-ease) both; }
@keyframes pr-pop {
  0%   { transform: scaleY(1);   filter: none; }
  45%  { transform: scaleY(2.1); filter: brightness(1.25); }
  100% { transform: scaleY(1);   filter: none; }
}
.pr-move-t { font-size: var(--pr-t-base); }
.pr-move-up { color: var(--pr-green-ink); font-weight: 700; }
.pr-move-dn { color: var(--pr-ink-3); font-weight: 700; }
.pr-move-s { margin: 0; font-size: var(--pr-t-sm); color: var(--pr-ink-3); }

@media (min-width: 1100px) {
  .pr-sess .pr-sheet { max-width: none; padding: var(--pr-s6) var(--pr-s6); }
  .pr-sess .pr-stem,
  .pr-sess .pr-ans,
  .pr-sess .pr-eq,
  .pr-sess .pr-opts,
  .pr-sess .pr-tbl-scroll,
  .pr-sess .pr-q-actions { max-width: var(--pr-measure); }
  /* The strip is the card's own header on a desk — same column, same edge. */
  .pr-sess .pr-strip { max-width: none; }
}

/* ===================================================== 5. THE PAPER STRIP ===
   design-proto.css §5, ported. Where you are, what skill this is, how many
   marks. The skill name is shown on purpose: it is the vocabulary the student
   needs, and it makes the notes link at the foot of the marking sheet mean
   something.

   It replaces the old .pr-hud — a thin progress bar, "0 of 0 marked" and a
   streak counter. The bar is DELETED rather than hidden (§7.4): a bar shows
   how far and nothing else, and the streak is the object §7.5 kills.        */

/* The strip is STICKY, under the header.

   Where you are in the session is the one fact a student loses when they
   scroll a long stem, and losing it is what makes a set of questions feel
   like a list instead of a paper. It was in the document and scrolled away
   at the first long stem; now the ladder, the skill, the marks and the data
   sheet stay on the desk for the whole question, which is where they are in
   a real exam. 52px of header + 62px of strip is 114px of a 812px phone,
   which is the price and it is worth it. */
.pr-strip {
  padding: var(--pr-s3) 0 10px;
  border-bottom: 1px solid var(--pr-rule);
  position: sticky;
  top: var(--pr-head-h);
  z-index: 15;
  background: var(--pr-paper);
}
.pr-strip-top { display: flex; align-items: center; gap: var(--pr-s3); }
/* No flex-wrap, unlike the prototype. Its mockup skill was "Titration
   calculations"; the real ones include "Period 3 elements — exam questions"
   and "Amino acids, proteins and DNA — exam questions", and at 375px a wrapping
   row put the marks chip alone on a second line with the title's last word
   beside a hole. Without wrapping, the title takes the two lines it needs and
   the chip stays where the mockup has it: first line, hard right. The chip and
   the specification reference are `flex: none` so the shrinking always happens
   to the title, which is the only one of the three that can wrap. */
/* flex-wrap, so the mock's section chip can take a line of its own without
   the skill name, the spec reference and the Data sheet button being pushed
   off the right-hand edge. On an ordinary session there is nothing to wrap —
   .pr-strip-bot's three items and the button have always fitted — so this
   changes nothing there. */
.pr-strip-bot { display: flex; align-items: baseline; gap: 10px; margin-top: 7px; flex-wrap: wrap; }
.pr-strip-bot .pr-spec, .pr-strip-bot .pr-marks { flex: none; }
/* The skill name takes what is left and the data-sheet button keeps its own
   width, so a long skill title truncates rather than pushing the control off
   the row. min-width: 0 because a flex item will not shrink below its content
   without it, which is how "Amino acids, proteins and DNA" would have done
   exactly that. */
/* THE SKILL NAME WRAPS, IT DOES NOT TRUNCATE.
   It used to be one nowrap line with an ellipsis, and the real titles are
   long: a student on a phone read "Titration calculat…", "Balancing equati…"
   and "Reactions of aqu…" — the vocabulary the marking sheet's notes link
   depends on, cut off at the exact point where it stops being a word. Two
   lines cost 18px and say the whole thing. */
.pr-strip-bot .pr-skill-name {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
}
.pr-strip-bot .pr-datasheet-btn { flex: 0 0 auto; margin-left: auto; align-self: center; }

/* Scoped to the strip, unlike the prototype's bare .pr-skill-name: the
   dashboard's skill list has used that class name since it was written and
   sets its own size further down this file. Both are chemistry-bearing and
   both are in the case guard's list at the top; only the SIZE differs. */
.pr-strip .pr-skill-name { font-size: var(--pr-t-base); font-weight: 600; color: var(--pr-ink); line-height: 1.3; }
.pr-spec { font-family: var(--font-mono); font-size: var(--pr-micro); color: var(--pr-ink-3); }
/* The marks are TEXT, not a chip. They were a bordered box sitting beside the
   data-sheet button, which is also a bordered box — so a phone offered two
   identical-looking objects of which exactly one was tappable, and the
   tappable one was on the right. A label should not dress as a control. */
.pr-marks {
  margin-left: auto; font-family: var(--font-mono); font-size: var(--pr-micro);
  color: var(--pr-ink-3); white-space: nowrap; letter-spacing: var(--pr-track);
}

/* the ladder — twelve marks along the top of the script.  Replaces the
   progress bar: it shows how far AND how it has gone, in one object, and it
   is legible at 320px because each rung is only 4px wide. */
.pr-ladder { display: flex; gap: 3px; flex: 1; align-items: center; }
/* 7px, not 5. At 5px on a 3px gap the ladder read as a dotted rule and a
   student could not tell four right from four missed without stopping to
   look — which is the entire thing it exists to say. */
.pr-rung { flex: 1 1 0; height: 7px; border-radius: 2px; background: var(--pr-rule); min-width: 4px; }
.pr-rung.is-ok  { background: var(--pr-green); }
.pr-rung.is-err { background: var(--pr-red); }
/* The fifth state. The prototype drew four — done-right, done-wrong, current,
   pending — because "nearly" did not exist when it was written; §7.4 names
   five and the verdict is live as of the third-verdict task, so the rung takes
   the same ochre the marking sheet's .is-near does. */
.pr-rung.is-near { background: var(--pr-amber); }
.pr-rung.is-now { background: var(--pr-ink); height: 13px; border-radius: 3px; }
.pr-count {
  font-family: var(--font-mono); font-size: var(--pr-t-sm); color: var(--pr-ink-2);
  white-space: nowrap; font-variant-numeric: tabular-nums;
}

/* ======================================================== 6. THE QUESTION ==
   design-proto.css §6, the chrome half: the sheet the question sits on, the
   stem, the given line, the data table and a figure. The five ANSWER LANES
   that follow it in the prototype (.pr-ans-row / .pr-unit / .pr-keys /
   .pr-opt*, and the two undrawn ones) are the next task's and are not here
   yet — the lane markup below still wears its pre-redesign classes.        */

.pr-q { margin: 0; }
.pr-q-note { display: inline-block; margin-top: var(--pr-s2); font-size: var(--pr-t-md); color: var(--pr-ink-2); }
.pr-q-actions { margin-top: var(--pr-s5); }

/* The ESCAPE VALUE on a chained part of a composed question — the stand-in a
   student uses when they could not do the part before this one. See
   practice/_engine/escape.mjs for why it exists.

   IT IS NOT A NOTE. .pr-q-note is data the question hands over as a matter of
   course (an M(r), a balanced equation); this is an instruction addressed to
   one student in one situation, and a student who DID answer the part before
   should be able to see at a glance that it is not for them. So it gets a
   rule down its leading edge and the quieter ink, which is how this platform
   already draws an aside — and no colour of its own, because it is neither a
   warning nor a correction.

   A BLOCK, not .pr-q-note's inline-block: it is a sentence or two and it wraps
   to three lines at 375px, where an inline-block would sit oddly against the
   line above it.

   NO CASE GUARD ENTRY IS NEEDED and that is worth saying rather than leaving
   to be rediscovered: the line is concatenated into the PART'S STEM by the
   template, so it renders inside .pr-stem and §2's `.pr-stem *` already
   immunises it — which it must, because the value it prints carries a unit
   (mol&nbsp;dm<sup>&minus;3</sup>) whose superscript must stay welded. */
.pr-q-escape {
  margin-top: var(--pr-s3);
  padding-left: var(--pr-s3);
  border-left: 2px solid var(--pr-rule);
  font-size: var(--pr-t-md);
  color: var(--pr-ink-2);
}
.pr-q-escape strong { color: var(--pr-ink); }

.pr-sheet {
  max-width: var(--pr-measure); margin: var(--pr-s4) auto 0;
  /* On the desk (§4a) the CARD fills its column and the STEM keeps the
     measure inside it. A 34rem card floating in a 660px column reads as a
     mistake; 34rem of prose inside a card that owns its column reads as a
     question paper, which is what it is. */
  background: var(--pr-sheet); border: 1px solid var(--pr-rule);
  border-radius: var(--pr-r-lg); box-shadow: var(--pr-e1);
  padding: clamp(16px, 4vw, 24px);
}

.pr-stem { font-size: var(--pr-stem); line-height: 1.62; color: var(--pr-ink); overflow-wrap: break-word; }
.pr-stem p { margin: 0 0 0.75em; }
.pr-stem p:last-child { margin-bottom: 0; }
.pr-stem b, .pr-stem strong { font-weight: 600; }
/* the given equation / condition line — the same object as .rn-eq on the notes */
.pr-given {
  display: block; margin: 12px 0 0; padding: 9px 12px;
  background: var(--pr-sunk); border-radius: var(--pr-r);
  font-size: 15.5px; text-align: center; color: var(--pr-ink-2);
}

/* ---- 6a. A COMPOSED QUESTION ---------------------------------------------
   One shared stem that owns 2-10 ordered parts — see paintQuestion() in
   session.mjs, and practice/_engine/question.mjs for why the atom exists.

   THE LAYOUT IS THE RULE. The stem sits once at the top of the sheet and stays
   there while the student works down the parts; nothing here may re-print it,
   collapse it or scroll it away. Everything below is therefore about the PARTS:
   a label gutter wide enough for "(j)", a hairline between one part and the
   next so three answer boxes do not read as one form, and no card of its own —
   they are all on the same sheet, because they are all one question.

   Names: .pr-mq-*, not .pr-q-*. .pr-q is already the standalone question's
   <article> and .pr-q-actions / .pr-q-note / .pr-q-src are already styled
   above; .pr-q-stem was deliberately deleted from this file and reusing it
   would resurrect a name that was removed on purpose.

   Nothing here carries text-transform. The two stems wear .pr-stem as well, so
   §2's case guard reaches their chemistry; the label and the tariff are
   interpolated values and so may never be .pr-label (§7.7), which is why they
   have plain classes of their own that cannot uppercase anything.          */

.pr-mq { margin: 0; }

/* THE OPENER — the frame sentence above the stem. It is set apart from the
   stem rather than merged into it, because the two do different jobs: the
   opener says what the question is ABOUT and the stem sets the scene. AQA
   prints the opener on its own line above everything else and so does this.

   Tighter beneath than the stem is (--pr-s4, not --pr-s6): the opener and the
   stem are one block of framing that the parts hang off together, and a full
   stem gap between them would read as two separate preambles. */
.pr-mq-opener {
  margin-block-end: var(--pr-s4);
  color: var(--pr-ink-2);
}

/* The stem holds its place. --pr-s6 rather than --pr-s5: it has to read as the
   thing the parts hang off, not as the first paragraph of part (a). */
.pr-mq-stem { margin-block-end: var(--pr-s6); }

.pr-mq-parts { list-style: none; margin: 0; padding: 0; display: grid; }

/* 2.25rem is measured, not chosen: "(j)" at --pr-t-md in the mono face is
   ~34px, and the gutter has to hold the widest label the atom allows rather
   than the widest one today's only composer uses. */
.pr-mq-part {
  display: grid; grid-template-columns: 2.25rem 1fr; align-items: start;
  padding-block-start: var(--pr-s6);
  margin-block-start: var(--pr-s6);
  border-block-start: 1px solid var(--pr-rule);
}
.pr-mq-part:first-child { padding-block-start: 0; margin-block-start: 0; border-block-start: 0; }

/* ON A PHONE THE GUTTER IS GIVEN BACK TO THE QUESTION, and this is a measured
   fix rather than a preference.

   A part's column is the sheet's width MINUS the label gutter, so on a 375px
   screen the marking sheet inside a part had 271px where a standalone
   question's has 311px — and the redox worked solution's ionic equation,
   `MnO4- + 8H+ + 5Fe2+ -> Mn2+ + 4H2O + 5Fe3+`, is one unbreakable ~300px run
   because every operator in it is an &nbsp;. Measured in a real 375px viewport:
   the page's scrollWidth was 404px and the final `3+` sat at x = 389, off the
   screen. A chemical equation must not be broken mid-token to make it fit —
   `Fe` / `2+` on two lines is worse than no equation — and the body must never
   scroll sideways, so the width has to come from somewhere else.

   It comes from the gutter. Below 480px the label sits on its own line above
   the part, which is what a printed paper does when the measure gets tight,
   and every part is then exactly as wide as a standalone question's sheet —
   the width the marking sheet was designed and read at. 480px, not 375px,
   because the part column has to reach ~311px before the equation fits and it
   does that at about 415px of viewport; the breakpoint is the measurement plus
   room. */
@media (max-width: 479px) {
  .pr-mq-part { grid-template-columns: 1fr; }
  .pr-mq-part-l { display: block; margin-block-end: 2px; }
  /* And the first part gets the hairline back, because here it is doing a
     second job: with no gutter to set the parts in from the stem, this is the
     only line that says where the shared stem ends and the parts begin. On a
     wider screen the indent says it and the rule would be noise. */
  .pr-mq-part:first-child {
    padding-block-start: var(--pr-s6);
    margin-block-start: var(--pr-s6);
    border-block-start: 1px solid var(--pr-rule);
  }
}

.pr-mq-part-l {
  font-family: var(--font-mono); font-size: var(--pr-t-md);
  font-variant-numeric: tabular-nums;
  line-height: 1.62; color: var(--pr-ink-2);
}
/* min-width: 0 so a wide data table or a long equation row inside a part
   scrolls in its own box instead of widening the grid column and pushing the
   sheet sideways at 375px. */
.pr-mq-part-b { min-width: 0; }

/* THE LEAD-IN — new data or a new scenario arriving immediately before the
   part that needs it. It sits above the part's task text and has to read as
   ARRIVING rather than as part of the question being asked, so it is given the
   rule-and-inset treatment a printed paper gives a mid-question data block. */
.pr-mq-lead {
  margin-block-end: var(--pr-s4);
  padding-inline-start: var(--pr-s4);
  border-inline-start: 2px solid var(--pr-rule);
  color: var(--pr-ink-2);
}

/* THE ANSWER LINES — the labelled lines a real paper prints after the task
   text and before the tally.

   Ruled lines, because that is what they are on paper and what tells a student
   these are the things the one answer has to cover rather than a list of
   facts. NO text-transform anywhere near them: a line label is very often
   chemistry ("Equation", "Role of V2O5") and §7.7's rule about interpolated
   values applies — these are the composer's strings, not a fixed .pr-label.

   The lines are not answer boxes and must not look like one: no field
   affordance, no border box, just the label and the rule under it. */
.pr-mq-lines {
  list-style: none;
  margin: 0 0 var(--pr-s4);
  padding: 0;
  display: grid;
  gap: var(--pr-s3);
}
.pr-mq-lines li {
  font-size: var(--pr-t-sm);
  color: var(--pr-ink-2);
  padding-block-end: var(--pr-s3);
  border-block-end: 1px solid var(--pr-rule);
}

.pr-mq-part-lane { margin: 0; }

/* The tariff. Small, quiet, and under the box it prices — the same voice the
   answer hints use, because it answers the same question ("how much does this
   one want?").

   IT IS .pr-hint, not a copy of it. This rule used to repeat .pr-hint's three
   declarations verbatim and add a margin, which is a second copy of a rule that
   already exists — the one thing the house rule about shared CSS forbids, and
   how the two would have drifted the first time either was touched. The element
   wears both classes (see paintQuestion) and what is left here is only the
   space around it. Nothing is lost: .pr-hint carries no text-transform, so the
   tariff still cannot be uppercased, and it brings its own sub/sup sizing.

   The margin matches .pr-ans-hint's, because the tariff sits directly under a
   lane whose own hints sit there on the standalone screen and the two must line
   up when a part has both. */
.pr-mq-part-m { margin: 8px 2px 0; }

/* The part's own marking sheet. Empty until submitQuestion() marks the whole
   question at once, so the margin is on the FILLED state only — an empty slot
   must not push the next part down. */
.pr-mq-part-fb:not(:empty) { margin-block-start: var(--pr-s4); }

/* A part the marker could not be reached for, a part the student chose to
   leave blank, and the line that explains a retry that came good. All three
   are the same voice — a plain sentence in the sheet's own ink, not a verdict
   — so they are one class rather than three that would drift apart. */
.pr-mq-nomark {
  margin: 0; font-size: var(--pr-t-md); color: var(--pr-ink-2);
}
.pr-mq-nomark + .pr-again { margin-block-start: 14px; }

/* The offer to leave a part blank. It appears only after a Check has actually
   been stopped by that empty box (session.mjs, offerBlank) — a "leave it
   blank" button standing beside an untouched box from the start would be an
   invitation rather than an escape.

   justify-items:start so the button is the width of its own words: it is the
   quieter of the two things the student can do here, and a full-width button
   would read as the way on. The <small> is the same 10.5px mono sub-label
   .pr-again's primary button uses — that rule is scoped to .pr-again, so the
   two declarations here are the whole of what this needs and there is no third
   copy of the button-with-a-sub-label pattern anywhere. */
.pr-mq-blank { display: grid; gap: 10px; justify-items: start; }
.pr-mq-blank .pr-btn { flex-direction: column; align-items: flex-start; gap: 1px; padding-block: 8px; }
.pr-mq-blank .pr-btn small {
  font-family: var(--font-mono); font-size: 10.5px; font-weight: 400;
  opacity: 0.72; letter-spacing: 0.03em;
}

/* data table: numerals line up, headers are NOT uppercased, and it scrolls
   inside its own box rather than widening the sheet.

   .pr-tbl, not the .pr-table it replaces: .pr-tbl is in the case guard's list
   at the top of this file and .pr-table never was, so until this port a
   dataTable's own column headers — "Cl2 / mol dm-3", "δ / ppm" — sat outside
   the one protection this platform has against being uppercased. */
.pr-tbl-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; margin: 16px -4px 0; padding: 0 4px; }

/* ---- a graph in a question stem ---------------------------------------
   practice/_engine/graph.mjs draws one for the four assessable statements
   that cannot be asked without it — reading a rate off a concentration-time
   curve, deducing an order, taking Ea from the gradient of ln k against 1/T,
   and choosing an indicator from a pH curve.

   width:100% AND a max-width, both. The viewBox scales the drawing to
   whatever box it is given, so width alone lets it grow to the full card and
   swallow the question — which is exactly what it did on the first render,
   the axis labels coming out larger than the stem. margin-inline:auto then
   needs the width to centre against, which is the same pairing CLAUDE.md
   records for .rn-img-figure > svg on the revision pages.

   .pr-fig's own rule is not here: it is one class shared with §6's figure
   slot, below, and lives there so there is exactly one place that sets it. */
.pr-graph {
  width: 100%;
  max-width: 400px;
  display: block;
  margin-inline: auto;
  color: var(--pr-ink);
}
.pr-fig-cap {
  font-size: 12.5px;
  color: var(--pr-ink-3);
  text-align: center;
  margin-top: 6px;
}

/* ---- the drawing answer ------------------------------------------------
   163 bank questions are excluded for being "answered by drawing", and they
   sit in the topics with the least content. See practice/app/draw.mjs.

   BOTH routes are always visible: the canvas and the textarea below it. A
   student with no stylus types the answer, a student with one draws it, and
   neither is machine-marked so neither is wrong. The canvas is not
   keyboard-operable and cannot sensibly be made so, which is exactly why the
   typing box is not optional. */
/* IT HAS TO LOOK LIKE PAPER YOU CAN DRAW ON.
   It was an empty rectangle in the sunken colour with a 1px edge — the exact
   look this stylesheet gives a DISABLED field — sitting between a row of
   small grey buttons and a real textarea. Nothing on screen said "draw here",
   so the object a student was meant to use looked like the one thing they
   could not. Three changes: a squared ruling so it reads as a sheet, a
   spelled-out label above it, and tools that look like tools. */
.pr-draw-k { margin: var(--pr-s5) 0 var(--pr-s2); }
.pr-draw {
  margin: 0;
  border: 1px solid var(--pr-rule-2);
  border-radius: var(--pr-r);
  overflow: hidden;
  background: var(--pr-sheet);
}
.pr-draw-bar {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  padding: var(--pr-s2) var(--pr-s2);
  background: var(--pr-sunk);
  border-bottom: 1px solid var(--pr-rule);
}
/* The spacer collapses below 420px. At 375 it pushed Undo and Clear onto a
   second row, so the toolbar was two rows of buttons over a drawing area —
   more chrome than surface. */
.pr-draw-sp { flex: 1 1 auto; }
@media (max-width: 420px) { .pr-draw-sp { display: none; } }
.pr-draw-tool, .pr-draw-act {
  font: inherit; font-size: var(--pr-t-sm); line-height: 1; font-weight: 500;
  padding: 8px 10px; min-height: 40px;
  border: 1px solid var(--pr-rule-2); border-radius: var(--pr-r-sm);
  background: var(--pr-sheet); color: var(--pr-ink-2); cursor: pointer;
}
.pr-draw-tool.is-on { background: var(--pr-ink); color: var(--pr-ink-flip); border-color: var(--pr-ink); }
.pr-draw-act[disabled] { opacity: 0.4; cursor: default; }
/* touch-action:none so a drag draws instead of scrolling the page — the same
   reason graph-tool.mjs sets it on the graph. The 16px ruling is the one
   place the whiteboard grid still earns its keep: it is a drawing surface,
   and squared paper is what a chemist draws a mechanism on. */
.pr-draw-canvas {
  display: block; width: 100%; height: 260px;
  border: 0;
  background-color: var(--pr-sheet);
  background-image: var(--pr-grid);
  background-size: 16px 16px;
  color: var(--pr-ink);
  touch-action: none; cursor: crosshair;
}
/* q.canvasHint — what the TEXT BOX is for on this question. It is not a
   caption on the canvas, so it sits with the box it describes, and it says
   which is which. A drawn answer is never replaceable by prose (Joe,
   2026-08-21): the box names and annotates, it does not substitute. */
.pr-draw-or {
  margin: var(--pr-s3) 0 6px;
  font-size: var(--pr-t-base); color: var(--pr-ink-2);
}

/* the gradient triangle — see practice/app/graph-tool.mjs for why it exists */
.pr-gt-tri { stroke: var(--pr-accent, oklch(0.55 0.13 250)); stroke-width: 1.4; stroke-dasharray: 4 3; }
.pr-gt-lab {
  font-size: 14px; font-family: var(--font-mono, ui-monospace, monospace);
  fill: var(--pr-accent, oklch(0.55 0.13 250)); text-anchor: middle;
}
.pr-gt-dy { text-anchor: start; }
.pr-gt-dot { fill: var(--pr-accent, oklch(0.55 0.13 250)); stroke: var(--pr-paper); stroke-width: 2; }
/* The grab target is the transparent r=13 circle, not the 5.5 dot — a 44px
   touch target at the sizes this graph renders at, which is the minimum a
   finger can hit reliably. */
.pr-gt-h { cursor: grab; }
.pr-gt-h:active { cursor: grabbing; }
.pr-gt-h:focus-visible { outline: none; }
.pr-gt-h:focus-visible .pr-gt-dot { stroke: var(--pr-ink); stroke-width: 3; }
.pr-tbl { width: 100%; border-collapse: collapse; font-size: 14.5px; font-variant-numeric: tabular-nums; }
.pr-tbl th, .pr-tbl td { padding: 7px 12px; text-align: left; border-bottom: 1px solid var(--pr-rule); white-space: nowrap; }
/* THE HEADER MAY WRAP AND THE NUMBERS MAY NOT, and the pair is the fix rather
   than either half.

   `nowrap` on both is right for a DATA cell — a number broken across two lines
   is unreadable — and wrong for a header, which is prose carrying a unit. At
   375 px the entropy table's header ran off the right edge of its column and
   printed

       S⦵ / J K⁻¹ mol

   with the trailing superscript minus one CUT OFF. That is a chemistry error
   rather than a cosmetic one: `J K⁻¹ mol` and `J K⁻¹ mol⁻¹` are different
   units, and the part below it asks for the answer in `J K⁻¹ mol⁻¹` — so a
   student who checks their unit against the table they read the data off finds
   a mismatch that does not exist in the source.

   It lands on CLAUDE.md's standing rule that table column headers must be
   rendered and read back as an image, including "nothing clipped, overlapping,
   cut off". That rule has always been cited for the uppercase transform
   corrupting δ and Cl; THIS IS THE SAME RULE FAILING BY CLIPPING AT A NARROW
   COLUMN, which nobody had recorded. Found by the lead reading the pixels.

   The table sits in `.pr-tbl-scroll`, so the header was technically reachable
   behind a horizontal swipe — with no affordance that there was anything to
   swipe to, which practice.css already records as the failure that put
   `Time / s` off-screen in the iodine-clock table. Wrapping removes the need
   for the scroll rather than relying on it.

   IT CANNOT BREAK A UNIT. The header is authored as
   `J&nbsp;K<sup>&minus;1</sup>&nbsp;mol<sup>&minus;1</sup>`, so the whole unit
   is one unbreakable run and the only break point is after the solidus. */
.pr-tbl thead th { background: var(--pr-sunk); font-weight: 600; font-size: 13.5px; color: var(--pr-ink); white-space: normal; }
.pr-tbl tbody tr:last-child td { border-bottom: 0; }
.pr-tbl caption { caption-side: top; text-align: left; font-size: 13px; color: var(--pr-ink-3); padding-bottom: 6px; }

/* a figure: capped in WIDTH so a skeletal formula can never eat a phone screen.
   Published by deal.mjs's `figure` field and painted by session.mjs's
   figureHTML() into the .pr-figslot below — a bank question's figure arrives
   wearing .wcqg-fig instead, styled further down, and is a separate thing.

   THERE IS NO HEIGHT CAP, AND THAT IS THE MEASUREMENT BELOW rather than an
   omission. `max-height: 42vh` stood here until 2026-08-25; see the wide-figure
   block that follows for what it did and why it could never have done anything
   else. */
.pr-fig { margin: 16px 0 0; text-align: center; }
.pr-fig svg, .pr-fig img { max-width: 100%; height: auto; display: inline-block; }
.pr-fig figcaption { font-size: 13px; color: var(--pr-ink-3); margin-top: 6px; }

/* A FIGURE render() HAS DECIDED SHOULD SCROLL. structure.mjs measures every
   drawing it makes and marks the ones that cannot be drawn inside the column at
   the smallest type it will set: past that it would rather scroll at full size
   than shrink, because shrinking a long chain takes its atom labels down to
   eight-pixel type and a figure a student cannot read is not a figure. It emits
   `overflow-x:auto` inline on an inner .pr-fig-scroll to say so.

   The rule above was quietly overruling that. `max-width: 100%` on every svg in
   a .pr-fig put back exactly the shrinking render() had left out, so the widest
   chain answers — 412 to 480px in a 307px column — came out at about 71%, atom
   labels at 15px and subscripts at 11px. Two rules disagreeing about one
   decision, and the one that knows the drawing's width lost. This is the CSS
   side of that agreement: where render() says scroll, the stylesheet stops
   capping the width.

   text-align matters as much as max-width here. .pr-fig centres, and an
   inline-block wider than its scroll container centres by overhanging both
   edges — the left overhang of a left-to-right scroller is unreachable, so a
   centred 480px drawing would lose its first 87px, which on a repeating unit is
   the opening bracket. Wide figures start at the left and scroll right.

   AND max-height IS GONE, which is the second half of the same bug arriving
   exactly where the comment that used to sit here said it would. That comment
   read "no wide figure comes near it… if a wide figure ever does grow tall
   enough to be shrunk by it, this is the second half of the same bug and the
   fix is the same — render() decides, the stylesheet obeys". It was true when
   it was measured. Two commits later it was not: raising MAX_WIDTH to three
   columns and dropping the shrink from scrolling figures let the acylation
   mechanisms out at their real size. Re-measured 2026-08-25 over the WHOLE
   pools — all 1,348 drawings the structure harness enumerates, which is the
   right instrument here and not a seed sweep: the optical-isomer pool alone
   holds 198 pairs and 2,000 seeds does not reach all of them. The readings are

     tallest figure of any kind   378.85 x 904.02px   draw-acylation-mechanism
     tallest that does NOT scroll 246.64 x 249.87px   draw-optical-isomers
     figures over the ~280px 42vh is on a 667px phone: 14, every one of them
     a wide draw-acylation-mechanism step 1, and none of them narrow

   against the 190.68px and 71.24px the old comment recorded. Both of those
   readings had gone stale, not just the wide one.

   WHAT A HEIGHT CAP DOES TO AN SVG HERE IS SQUASH IT, NOT SCALE IT, and that is
   why it goes rather than moves. render() writes width and height ATTRIBUTES on
   every drawing; in HTML those are presentation hints for the CSS width and
   height, and the inline `height:auto` neutralises only the second of them. So
   the used width stays pinned at the drawing's own — 904px — while max-height
   clamps the box to 280px, and preserveAspectRatio then letterboxes the drawing
   inside a box of the wrong shape: measured in a real 375x667 frame, the seed
   that draws 904.02 x 378.85 came out at 73.9% of its size with about 118px of
   dead margin down each side, so a student opening that mark scheme saw a
   benzene ring, one bond running off the edge, and a large blank rectangle
   under a caption reading "Step 1: the nitrogen attacks and the C=O opens".
   Shrunk AND scrolled: the worst of both, which is the pairing the commit
   before this one took from 225 figures down to 5.

   Making it scale instead would need `width:auto` on the svg, and that is not
   available: `.pr-fig svg` out-specifies `.pr-graph`, whose graphs carry no
   width or height attributes at all, so an `auto` width would drop every graph
   on the platform to the 300px default object size. And scaling is the blanket
   shrink render() exists to refuse anyway.

   So height is render()'s decision like width is, and today render() does not
   cap it. That is a real and stated limit, not a silent one: the tallest thing
   the platform can deal is 378.85px, which is 57% of a 667px phone and scrolls
   with the page like any other tall block. If a vertical cap is ever wanted it
   belongs in render(), which is the only code that can make a drawing shorter
   without distorting it.

   Nothing else on the platform is touched: graph.mjs's figures carry .pr-graph
   and its own max-width, a bank question's figure is .wcqg-fig and a different
   thing, and every structure narrower than the column still carries
   `max-width:100%` inline from render() itself.

   Both selectors double the class deliberately, so they beat `.pr-fig svg` on
   specificity rather than on being further down the file. */
.pr-fig.pr-fig-wide { text-align: left; }
.pr-fig.pr-fig-wide svg { max-width: none; }

/* AND THE STUDENT HAS TO BE TOLD THERE IS MORE TO THE RIGHT, which is the half
   of this that was missing and the half that was a chemistry bug.

   A scrolled-off CHAIN is obviously cut: it stops in the middle of a backbone
   with a bond running off the edge. A scrolled-off SHORT drawing is not, and
   that is what made this a chemistry bug rather than an inconvenience.
   Hexanedioic acid used to come out 337px wide against a column of 307; the
   overhang was exactly its terminal OH, and what was left on screen read
   HO—C—C—C—C—C—C—O — a structure, drawn cleanly, that is not the compound the
   mark scheme was about. A student checking their own answer against it had
   nothing to go on. Fitting to the real column takes that one back inside, and
   the twelve drawings that still cannot fit are all long chains and long
   repeating units — but a student has no more way of knowing where THEY stop.
   Overlay scrollbars are no help — iOS, and macOS on its default setting, draw
   nothing at all until a finger is already moving — so the cue has to be part
   of the figure.

   TWO CUES, because they do different jobs. The edge shading below is the
   accurate one: it is drawn only while there is something past that edge and it
   goes as the student reaches the end, on both sides, with no script. The line
   of text under it is the unmissable one: it is always there, it says what to
   do rather than hinting at it, and it is what a student who has never met a
   scrolling figure needs. It costs one short line under the twelve drawings
   that scroll, which reach a student in 103 of the two polymer templates' 520
   questions.

   THE SHADOW IS THE FOUR-LAYER BACKGROUND TRICK and it needs all four. Two
   `local` layers scroll WITH the content and paint the sheet colour over each
   end; two `scroll` layers stay put and paint a soft edge. At the left end the
   local layer covers the left shadow, so only the right one shows — which is
   exactly the state to signal. `--pr-sheet` and not `transparent`: the covers
   have to be opaque or they hide nothing, and the figure sits on the sheet.

   It paints BEHIND the drawing, so no stroke and no atom label is dimmed by it;
   what a reader sees is the paper darkening towards the edge the ink runs off.

   .pr-fig-scroll is the SCROLLER and the <figure> is not, which is deliberate:
   a figcaption is a block child of whatever scrolls, so a caption inside the
   scroll box takes the box's width and slides away to the left on the first
   swipe. CLAUDE.md's caption rule cannot be met by a caption that leaves the
   screen, and neither can the hint below. */
.pr-fig.pr-fig-wide .pr-fig-scroll {
  background:
    linear-gradient(to right, var(--pr-sheet) 60%, transparent) 0 0 / 28px 100% no-repeat local,
    linear-gradient(to left,  var(--pr-sheet) 60%, transparent) 100% 0 / 28px 100% no-repeat local,
    radial-gradient(farthest-side at 0    50%,
      color-mix(in oklch, var(--pr-fig-guide) 70%, transparent), transparent) 0 0 / 18px 100% no-repeat scroll,
    radial-gradient(farthest-side at 100% 50%,
      color-mix(in oklch, var(--pr-fig-guide) 70%, transparent), transparent) 100% 0 / 18px 100% no-repeat scroll;
}
/* A scroll container is not focusable in Chrome on its own, so render() gives
   this one tabindex="0" — without it a keyboard-only student has no way to
   reach the right-hand end of the drawing at all. The ring is §3's one
   :focus-visible rule; all this adds is somewhere for it to sit tidily. */
.pr-fig.pr-fig-wide .pr-fig-scroll:focus-visible { border-radius: var(--pr-r); }
/* The words. Quiet, but not so quiet it can be missed — it is the only thing on
   the page that distinguishes a drawing that continues from one that has
   finished. Right-aligned because that is the direction it is pointing. */
.pr-fig-more {
  margin: 4px 0 0;
  font-size: var(--pr-t-sm);
  color: var(--pr-ink-3);
  text-align: right;
}

/* THE SLOT a published `figure` is painted into — session.mjs's figureHTML().
   Three call sites use it: above the answer lane on a standalone question,
   above the parts on a composed one, and inside a part.

   It exists because .pr-fig carries a top margin and no bottom one, which is
   right for a figure that ends a stem and wrong for one that has an answer
   box, a part list or a tariff under it — without this the picture sits flush
   against the thing it is asking about. The space goes on the slot rather
   than on .pr-fig itself so the figures already concatenated INTO stems (
   draw-nucleophilic-substitution and the graph.mjs templates) keep the
   spacing they were read at.

   ONE CLASS, NOT A .pr-q-* AND A .pr-mq-* PAIR. The composed screen owns the
   .pr-mq-* namespace for what is peculiar to it — a label gutter, a hairline
   between parts — and the space under a picture is not peculiar to it. .pr-fig
   itself is already shared by both screens for the same reason, and it is
   already in §2's case guard, so nothing here has to repeat that protection.

   --pr-s5 is 20px, within a pixel of the 22px .pr-ans already puts above an
   answer box, so a figure between the stem and the box reads as one more
   block of the question rather than as something wedged in. */
.pr-figslot { margin-block-end: var(--pr-s5); }

/* ---- the answer, numeric with a unit ------------------------------------
   design-proto.css §6. The unit is printed INSIDE the field as a fixed chip,
   so the student never types it and never wonders whether they should — the
   commonest way a right answer was marked wrong on paper. It is the one place
   a formula sits next to an input, which is why .pr-unit is in the case guard
   at the top of this file: `mol dm⁻³` must never render `MOL DM⁻³`, and the
   ⁻³ must stay welded to the m.

   The border lives on the ROW, not on the field, so the chip and the input
   read as one control and a marked state can put a 3px rule down the whole
   thing rather than around half of it.                                     */
.pr-ans { margin-top: 22px; }
.pr-ans-row {
  display: flex; align-items: stretch;
  border: 1px solid var(--pr-rule-2); border-radius: var(--pr-r);
  background: var(--pr-sheet); overflow: hidden;
  transition: border-color var(--pr-speed) ease, box-shadow var(--pr-speed) ease;
}
/* The focus ring is the ROW's, because the field inside it has no border of
   its own to ring. --pr-focus, not the hard-coded teal that used to sit on
   .pr-answer:focus-visible: that value was light-only — the same one removed
   from .pr-btn with the page furniture — and in dark it was a loud blue that
   belongs to no part of this palette. */
.pr-ans-row:focus-within {
  border-color: var(--pr-focus);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--pr-focus) 18%, transparent);
}
.pr-answer {
  flex: 1; min-width: 0; font: inherit; font-family: var(--font-mono);
  font-size: 18px; font-variant-numeric: tabular-nums;
  padding: 12px 14px; min-height: var(--pr-tap);
  border: 0; background: transparent; color: var(--pr-ink);
}
.pr-answer:focus { outline: none; }
.pr-answer::placeholder { color: var(--pr-ink-3); opacity: 0.55; font-size: 15px; }
.pr-unit {
  display: flex; align-items: center; padding: 0 14px;
  background: var(--pr-sunk); border-left: 1px solid var(--pr-rule);
  font-size: 15px; color: var(--pr-ink-2); white-space: nowrap;
}
/* The wrapper inside the chip, and it is not decoration. A flex container
   BLOCKIFIES its children, and a blockified <sup> is no longer an inline box,
   so `vertical-align: super` stops applying and `mol dm<sup>−3</sup>` renders
   as a flat "mol dm-3" — the superscript quietly becomes a hyphen and a digit,
   which is a different unit. One wrapper makes the chip's contents ordinary
   inline boxes again. See unitHTML() in session.mjs. */
.pr-unit-t { display: block; }
/* The hint sits UNDER the field and stays there. It used to be the input's
   own `placeholder`, which vanishes at the first keystroke — the one moment
   the student stops being able to check how many figures to give. */
.pr-ans-hint { display: flex; flex-wrap: wrap; gap: 4px 12px; align-items: center; margin: 8px 2px 0; }
.pr-hint {
  font-family: var(--font-mono); font-size: var(--pr-micro);
  letter-spacing: 0.02em; color: var(--pr-ink-3);
}
.pr-hint sub, .pr-hint sup { font-size: 0.75em; line-height: 0; }

/* marked states — a RULE, never a flood (§7.2). The three answer-state fills
   still on .pr-answer below are the pre-redesign ones and are the NEXT task's
   to migrate; these are the row's own and are already on the token layer. */
.pr-ans-row.is-err  { border-color: var(--pr-red);   border-left-width: 3px; }
.pr-ans-row.is-ok   { border-color: var(--pr-green); border-left-width: 3px; }
.pr-ans-row.is-near { border-color: var(--pr-amber); border-left-width: 3px; }

/* ---- the chemistry key row, for the `text` lane -------------------------
   The characters a phone keyboard cannot reach in one page-flip. WHICH
   characters is a marking question, not a design one, and the answer is in
   practice/app/answer-lanes.mjs — a key that inserts something gradeText
   marks wrong would be our trap, not the student's mistake. */
.pr-keys { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 0; }
.pr-key {
  font-family: var(--font-mono); font-size: 16px; line-height: 1;
  min-height: 38px; min-width: 40px; padding: 0 9px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--pr-rule); border-radius: 4px;
  background: var(--pr-sunk); color: var(--pr-ink-2); cursor: pointer;
  /* the same protection .pr-answer has: these ARE chemistry, one glyph each */
  text-transform: none; letter-spacing: normal;
}
.pr-key:hover { background: var(--pr-sheet); border-color: var(--pr-ink-3); }
.pr-key:focus-visible { outline: 2px solid var(--pr-focus); outline-offset: 2px; }

/* ---- the typed field, marked ---------------------------------------------
   The mark itself is the 3px rule down the ROW, above; this is the tint that
   says WHICH half of the row was marked, and it is now on the token layer.

   It was two hard-coded light-mode fills, and in dark they were the worst
   defect on the runner: --pr-ink is near-white at 0.945 and the fill was a
   pale pink at 0.96, so a student could not read the answer they had just
   typed. Read back at 375px before and after.

   The washes are within 0.02 lightness of --pr-sunk in BOTH themes
   (light 0.972 vs 0.955, dark 0.278 vs 0.272), which is what makes a fill on
   half a control acceptable here: the seam where the tint meets the unit
   chip's own --pr-sunk is invisible, so the row still reads as one object.

   border-color is gone rather than re-tokenised. .pr-answer has had
   `border: 0` since the border moved onto .pr-ans-row, so it painted nothing
   — a dead declaration that read like a live one. */
.pr-answer.is-ok  { background: var(--pr-green-wash); }
.pr-answer.is-err { background: var(--pr-red-wash); }
/* And the third verdict. Without it a `nearly` drew an amber 3px rule down a
   row whose field was washed pink — two pens on one control for one mark,
   which is the opposite of §7.2's point. Read back at 375px: the ochre border
   and the pink fill were unmistakably arguing. */
.pr-answer.is-near { background: var(--pr-amber-wash); }

/* ============================ THE `recall` LANE ============================
   Self-marked, and the design has to say so. The student types, commits, and
   reports `got` / `partly` / `missed` BEFORE the mark scheme is revealed —
   the three buttons are the whole interaction, in place of a Check-answer
   button, because there is nothing for the server to check.

   THE BUTTONS ARE INK, NOT COLOUR. They are not marks: they are the
   student's own report of how they did, and §7.2 reserves red — and green —
   for a mark that has been made. A green "Got it" would be the platform
   agreeing with a claim it has not seen the evidence for. The verdict colour
   arrives afterwards, on the feedback line and the revealed mark scheme. */

/* The textarea. It is prose, not a number, so it reads in the body font
   rather than mono, and it takes the full card width rather than the numeric
   box's — a one-line box for a definition would need scrolling to re-read
   what you just wrote. font-size stays at 16px so iOS Safari does not zoom
   the page on focus, the same reason the numeric field is 17px.

   min-height is five whole lines — 5 × the line-height set two lines below,
   plus the 24px of padding — and not a round 96px, which was 3.9 lines and
   cut the last one in half along its x-height. A half line reads as a
   rendering fault rather than as "there is more below". In `em` rather than
   px so it tracks the font-size in the same rule, and not in `lh`, which is
   the exactly-right unit and not old enough: on a phone that does not have
   it the whole declaration is dropped and the box falls back to rows="3".
   Read back at 375px with a five-line answer typed in. */
.pr-answer--prose {
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.55;
  width: 100%;
  min-height: calc(5 * 1.55em + 24px);
  padding: 12px 14px;
  resize: vertical;
}

/* The self-report block (D1). A <fieldset> so the three buttons carry the
   legend as their group name, and the legend asks a PROSPECTIVE question —
   "do you think you've got it?" — because that is where the student actually
   is: they have not seen the answer yet.

   It is prose in --pr-ink-2, NOT a .pr-label. Two reasons, and the first is
   a rule: §7.7 allows exactly one uppercase class and practice-label-lint.mjs
   fails the build on any other. The second is that it is a question, not a
   label — "BEFORE YOU LOOK — DO YOU THINK YOU'VE GOT IT?" shouts, and a
   tracked micro-label cannot carry an em dash and an apostrophe legibly at
   11.5px. The plan's drawn CSS for this lane specified uppercase mono here;
   that part of it could not have shipped. */
.pr-report {
  border: 0;
  margin: 0;
  padding: 0;
  /* min-inline-size: a <fieldset> has a UA min-content minimum that stops it
     shrinking below its widest child, which on a 375px phone lets one long
     button push the card sideways. */
  min-inline-size: 0;
}
.pr-report-legend {
  width: 100%;
  margin: 0 0 10px;
  padding: 0;
  font-size: 15px;
  line-height: 1.45;
  color: var(--pr-ink-2);
}
/* Three equal columns, not three natural widths: they are one scale — got,
   partly, missed — and a scale whose steps are different sizes reads as
   three unrelated buttons. minmax(0, 1fr) and never a bare 1fr (§7.8).

   A wrapper rather than making the <fieldset> itself the grid: a <legend> is
   not an ordinary child of its fieldset, and how a UA slots it into a grid or
   flex formatting context is not something to depend on. */
.pr-report-btns {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
}
/* Two columns on a one-mark item, where there is no "Partly" to offer —
   floor(1 / 2) is 0, so it scored exactly what "Missed it" scored while
   holding the ladder higher. See the note in session.mjs. minmax(0, 1fr) and
   not a bare 1fr, per §7.8. */
.pr-report-btns--two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* Not one of the three — a plain text link under them, because it is not a
   fourth choice on the same scale, it is opting out of the question the
   three buttons ask. Ink, underlined, the same weight a "skip" affordance
   gets everywhere else on the platform rather than a button competing with
   the grid above it for the thumb's attention. */
.pr-report-skip {
  display: block;
  margin: 10px 0 0;
  padding: 4px 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 13.5px;
  color: var(--pr-ink-3);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.pr-report-skip:hover:not(:disabled) { color: var(--pr-ink-2); }
.pr-report-skip:focus-visible { outline: 2px solid var(--pr-focus); outline-offset: 2px; }
.pr-report-skip:disabled { opacity: 0.5; cursor: default; text-decoration: none; }
.pr-report-btn {
  font: inherit;
  font-size: 15px;
  /* 6px, not the 18px this had when the buttons were three natural widths:
     in three equal columns at a 320px fold-out phone a column is 78px and
     "Missed it" measures 60, so the horizontal padding is the whole margin
     against a two-line button — at 18px there would have been 40px of room
     for 60px of word. Measured with a Range, not guessed. */
  padding: 10px 6px;
  min-height: var(--pr-tap);      /* every control, everywhere */
  border: 1px solid var(--pr-rule-2);
  border-radius: var(--pr-r);
  background: var(--pr-sheet);
  color: var(--pr-ink);
  cursor: pointer;
  transition: background var(--pr-speed) ease, border-color var(--pr-speed) ease;
}
.pr-report-btn:hover:not(:disabled) { background: var(--pr-sunk); border-color: var(--pr-ink-3); }
.pr-report-btn:focus-visible { outline: 2px solid var(--pr-focus); outline-offset: 2px; }
/* `Got it` and `Partly` are disabled until something is typed, so this is the
   state a recall question OPENS in — it has to read as "not yet", not as
   "broken". 0.5 rather than the 0.38 the plan drew: at 0.38 on a --pr-sheet
   card in dark the words are barely there at all. */
.pr-report-btn:disabled { opacity: 0.5; cursor: default; }
/* Which one was pressed. Ink, at double weight — the same "you picked this"
   grammar .pr-opt.is-sel uses, and deliberately not a verdict colour.
   It was a hard-coded pale blue at 0.95 lightness, which in dark put
   near-white ink on a near-white slab: the word inside the button the student
   had just pressed disappeared completely.

   The second edge is an inset shadow rather than border-width: 2px so that
   pressing a button cannot move the row it sits in by a pixel. */
.pr-report-btn.is-sel {
  border-color: var(--pr-ink);
  box-shadow: inset 0 0 0 1px var(--pr-ink);
  background: var(--pr-sunk);
  font-weight: 500;
}
/* A report locks all three buttons, the pressed one included — one report per
   question and there is no path back. Dimmed with the other two it reads as
   "all three are off" rather than as "this is what you said", and the word
   inside it is the only record on the card of which button that was. It is a
   record, not an unavailable control, so it keeps full contrast. Read back at
   375px in both themes: at 0.5 the selected button and its two disabled
   neighbours are the same grey. */
.pr-report-btn.is-sel:disabled { opacity: 1; }

/* THE SECOND SELF-REPORT — the one that scores.

   It lives inside the marking sheet, under the mark scheme it is a question
   about, and it is the reason that card exists before anything has been
   marked. Separated from the reveal by the same hairline the reveal itself
   uses, because "here is the mark scheme" and "so how did you do" are two
   moves and running them together is what makes a two-stage flow read as one
   long block.

   The legend is heavier than the prediction's (ink, not --pr-ink-2): at this
   point it is the only outstanding action on the screen, and the verdict bar
   is deliberately empty until it is answered. Still prose and still not a
   .pr-label — a label inside .pr-sheetnote loses its uppercase to the case
   guard anyway, and this is a question rather than a label. */
.pr-report--after {
  margin: 20px 0 0;
  padding: 17px 0 0;
  border-top: 1px solid var(--pr-rule);
}
.pr-report--after .pr-report-legend { color: var(--pr-ink); font-size: 15.5px; }
/* Says that the prediction is already picked, so "tap the one that is already
   selected" is a visible instruction rather than something to be guessed. The
   buttons also carry aria-pressed, which is the same fact for a screen
   reader. */
.pr-report-hint {
  margin: 9px 0 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--pr-ink-3);
}

/* ========================== THE `equation` LANE ============================

   [ ] CH4  +  [ ] O2  →  [ ] CO2  +  [ ] H2O

   Coefficients only — the species are given (§5.4). A flex row of TERMS,
   where a term is "the operator in front of me, my number box, my formula":
   the operator lives inside the term that follows it, so wrapping can never
   leave a "+" or a "→" dangling at the end of a line with nothing after it.
   At 375px a five-species half-equation takes two or three lines and every
   one of them starts with a species.

   row-gap is larger than column-gap for the same reason: once it wraps, the
   lines need to read as separate lines rather than as one crowded block.

   THE ROW IS A CONTROL, so it is drawn as one — a bordered box on the sheet,
   the same object .pr-ans-row is for the numeric lane. That is what gives a
   marked answer somewhere to put §7.2's 3px rule: the answer here is the SET
   of numbers, not any one of them, so the mark belongs to the group. Before
   this the row was bare and the only marked state was a fill inside each
   box, which is a red area rather than a red mark. */
.pr-eq {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 8px;
  margin: 22px 0 0;
  padding: 14px 12px;
  border: 1px solid var(--pr-rule-2);
  border-radius: var(--pr-r);
  background: var(--pr-sheet);
  transition: border-color var(--pr-speed) ease;
}
.pr-eq-term { display: inline-flex; align-items: center; gap: 7px; }

/* The + and the → . The glyph is aria-hidden in the markup: screen readers
   announce "→" inconsistently. It is not simply dropped, though — each one
   is paired with a `.pr-eq-op-sr` span (below) carrying the spoken word, so
   a screen-reader user still hears where the arrow falls. */
.pr-eq-op {
  font-size: 17px;
  color: var(--pr-ink-3);
  padding-right: 1px;
  user-select: none;
}

/* The spoken stand-in for an aria-hidden operator glyph ("plus", "reacts to
   form", "is in equilibrium with") — visible to a screen reader, invisible
   on screen. Standard clip-to-1px technique, same one `.skip-link` in
   styles.css uses before it gains focus; restated here rather than shared
   because /practice/ loads styles.css only for its design tokens (CLAUDE.md)
   and .skip-link bakes its own always-hidden rules into one class rather
   than exposing a reusable utility. */
.pr-eq-op-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The box. A SUNK well inside the row's box — the same depth model the unit
   chip and the data table's headers use, so a hole in a printed equation
   reads as a hole. Wide enough for two digits with room to spare (the
   largest coefficient any template here produces is 19), --pr-tap tall so it
   is a thumb target, and 17px so iOS Safari does not zoom the page on focus.
   tabular-nums so a 1 and a 15 sit on the same centre.

   The spinner arrows are removed: they are unusable at this width and invite
   clicking a number up one at a time instead of working the chemistry out. */
.pr-eq-n {
  flex: 0 0 auto;
  width: 50px;
  font: inherit;
  font-family: var(--font-mono);
  font-size: 17px;
  font-variant-numeric: tabular-nums;
  text-align: center;
  min-height: var(--pr-tap);
  padding: 8px 2px;
  border: 1px solid var(--pr-rule-2);
  border-radius: var(--pr-r);
  background: var(--pr-sunk);
  color: var(--pr-ink);
  -moz-appearance: textfield;
  appearance: textfield;
  transition: border-color var(--pr-speed) ease, background var(--pr-speed) ease;
}
.pr-eq-n::-webkit-outer-spin-button,
.pr-eq-n::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
/* The same ring .pr-ans-row:focus-within wears, so the two lanes focus alike.
   :focus rather than :focus-visible because the box you are typing in has to
   be obvious however you got there — a five-box half-equation is the one
   place on this platform where losing your place is easy. It replaces a
   hard-coded teal that was light-only and off-palette in both themes.
   The well lifts to sheet white while it holds the caret. */
.pr-eq-n:focus {
  outline: none;
  background: var(--pr-sheet);
  border-color: var(--pr-focus);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--pr-focus) 18%, transparent);
}
/* Locked once the answer is right. opacity is NOT the lever — it would fade
   the border and the number together and make a correct answer harder to
   read back than a wrong one. */
.pr-eq-n:disabled { opacity: 1; color: var(--pr-ink-2); }

/* Marked. §7.2's grammar: a 3px rule down the edge of the CONTROL, which for
   this lane is the whole row, because the answer is the set of numbers. */
.pr-eq.is-ok   { border-color: var(--pr-green); border-left-width: 3px; }
.pr-eq.is-err  { border-color: var(--pr-red);   border-left-width: 3px; }
/* There is no .pr-eq.is-near, and there should not be. The lane port kept one
   for parity with .pr-ans-row.is-near and flagged it for this task to rule on:
   a `nearly` is the RIGHT VALUE at the wrong precision, and a set of
   coefficients has no precision to get wrong — gradeEquation returns no
   verdict field at all, so the class could never be applied. A state rule
   nothing can reach reads like a live one and invites a future edit to "fix"
   the missing toggle. */

/* And a tint in the wells, exactly as .pr-answer.is-ok tints the typed field
   inside .pr-ans-row: the CONTROL carries the coloured border, the FIELDS
   carry the wash. The wells keep their neutral --pr-rule-2 edge — a red
   border on the box as well as on the row was tried and read back at 375px
   as six red outlines for one mark, which is a red area by accumulation even
   though no single one of them is bigger than a chip (§7.2). One border, one
   3px rule, and a tint.

   It was two hard-coded light-mode fills at 0.96 lightness, and in dark they
   put near-white numerals on near-white: every coefficient the student had
   typed went invisible the moment it was marked, four boxes at a time. */
.pr-eq.is-ok  .pr-eq-n { background: var(--pr-green-wash); }
.pr-eq.is-err .pr-eq-n { background: var(--pr-red-wash); }

/* The species itself. NEVER uppercased or letter-spaced: a transform here
   turns Cl2 into CL2 and detaches a subscript from its atom — the same
   reasoning the case guard applies to .pr-opt-body, restated because this is
   a second place formulae are printed and the rule has to hold in both. */
.pr-eq-sp {
  font-size: 17px;
  color: var(--pr-ink);
  white-space: nowrap;
  text-transform: none;
  letter-spacing: normal;
}
.pr-eq-sp sub, .pr-eq-sp sup { line-height: 0; }

/* The balanced equation inside a worked solution. Not an input — just the
   answer, printed once, big enough to read back against what was typed. */
.pr-eq-answer { font-size: 18px; }
.pr-eq-answer sub, .pr-eq-answer sup { line-height: 0; }

/* ---- multiple choice ---------------------------------------------------
   design-proto.css §6. minmax(0, 1fr) and not a bare 1fr (§7.8): a grid
   item's automatic minimum size is min-content, and one option that is an
   &nbsp;-joined formula or a traced SVG widens the whole column and scrolls
   the page sideways on a phone. Every grid on this platform states minmax. */
.pr-opts {
  list-style: none; margin: 22px 0 0; padding: 0;
  display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px;
}
.pr-opt {
  width: 100%; text-align: left; font: inherit; font-size: 16px;
  min-height: var(--pr-tap); padding: 11px 13px;
  border: 1px solid var(--pr-rule); border-left-width: 3px; border-radius: var(--pr-r);
  background: var(--pr-sheet); color: var(--pr-ink); cursor: pointer;
  /* flex-start, not centre: an option can be two lines of prose, a structural
     formula, or a formula with a correction printed under it, and the letter
     must stay level with the FIRST line of whichever it is. */
  display: flex; align-items: flex-start; gap: 12px;
  transition: background var(--pr-speed) ease, border-color var(--pr-speed) ease;
}
.pr-opt:hover:not(:disabled) { background: var(--pr-sunk); }
.pr-opt:focus-visible { outline: 2px solid var(--pr-focus); outline-offset: 2px; }
/* The letter is shown, not just used as an id — a bank question's mark scheme
   reveals its answer as "D  Cl₂O", which means nothing on unlettered options. */
.pr-opt-key {
  flex: 0 0 auto; font-family: var(--font-mono); font-size: 12.5px; font-weight: 500;
  color: var(--pr-ink-3); border: 1px solid var(--pr-rule); border-radius: 3px;
  width: 24px; line-height: 22px; text-align: center;
}
/* The option's own content. In the case guard's immunised list at the top of
   this file — these are formulae, and a transform here ships Cl2O as CL2O.
   .pr-option-body, which this replaces, was NOT in that list and carried its
   own local opt-out instead; the rename is what puts it under the guard. */
/* overflow-wrap is the pair to min-width: 0 and it is load-bearing, not polish.
   min-width: 0 lets this flex item shrink below its min-content width — §7.8's
   house rule, and without it one long formula pushes the whole row past the
   viewport. But a formula is one unbreakable "word": shrink the BOX and the
   TEXT stays its own width and simply paints outside it. At 320px
   [Cr(H2NCH2CH2NH2)3]3+ measures 173px in a 159px box and ran straight under
   the ✓ beside it — the verdict glyph landed on top of the charge, which reads
   as a different ion. Measured, not guessed: the gap from the end of the
   formula to .pr-opt-mark was −2px.

   break-word only breaks a word that cannot fit a line ON ITS OWN, so nothing
   changes at 375px, where the same option has 48px to spare. When it does fire
   Chrome takes the bracket boundary — "[Cr(H2NCH2CH2NH2)3" / "]3+" — which is
   a complete formula on two lines rather than a truncated one on one. */
.pr-opt-body { min-width: 0; flex: 1; padding-top: 1px; overflow-wrap: break-word; }
/* A structural formula travels as inline SVG. It must shrink to the button,
   not push the button wider than the card. The height cap stops a reaction
   scheme drawn down the page from turning one option into most of the screen.
   It does NOT even up the type size between options: these were traced from
   the papers at whatever scale each source image happened to be, so a
   two-atom option can be lettered three times the size of the skeletal
   formula above it. That is inherited from the drawings and is the same on
   the revision pages; evening it up means redrawing them, not styling them. */
.pr-opt-body svg { max-width: 100%; max-height: 130px; width: auto; height: auto; }
.pr-opt-body p { margin: 0; }
.pr-opt-body p + p { margin-top: 4px; }
.pr-opt-mark { flex: 0 0 auto; font-size: 17px; line-height: 22px; font-weight: 600; }

.pr-opt.is-sel { border-color: var(--pr-ink); background: var(--pr-sunk); }
.pr-opt.is-sel .pr-opt-key { border-color: var(--pr-ink); color: var(--pr-ink); }
.pr-opt.is-ok  { border-color: var(--pr-green); background: var(--pr-green-wash); }
.pr-opt.is-ok  .pr-opt-mark { color: var(--pr-green-ink); }
.pr-opt.is-err { border-color: var(--pr-red); background: var(--pr-red-wash); }
.pr-opt.is-err .pr-opt-mark { color: var(--pr-red-ink); }
/* The option that WAS right, revealed beside the one they picked — §7.3 tier 0
   for this lane, since the verdict card names a letter and this is what makes
   the letter mean something without counting rows.

   A 3px rule and a ✓, and deliberately NOT .is-ok's wash: .is-ok paints a
   whole row green and belongs to a screen where the student got it right and
   there is one mark on it. Here there are two marked rows, and a red slab
   beside a green slab is most of a phone screen filled with colour for one
   multiple-choice question. The base .pr-opt already carries
   border-left-width: 3px, so this is one property doing §7.2's marking
   grammar exactly. */
.pr-opt.is-answer { border-color: var(--pr-green); }
.pr-opt.is-answer .pr-opt-mark { color: var(--pr-green-ink); }
/* Ported with the rest of §6 and not emitted yet: the prototype dims the
   options that were not picked once an answer is marked, which the runner
   does not do because a wrong MCQ here is still retryable and the other
   three options are still live. */
.pr-opt.is-out { opacity: 0.42; }

/* The per-option correction, attached to the thing they actually clicked.
   45% of the bank's multiple-choice options carry an authored, examiner-
   derived one, and until this rule existed every word of it was printed as a
   generic paragraph under the card instead of against the option it is about.

   It sits INSIDE .pr-opt-body, which is deliberate and not incidental: the
   corrections are full of chemistry ("Calcium forms 2+ ions…", "Each H−Cl
   bond…"), and inside the body they are covered by the case guard. As a
   sibling of the body they would not be. */
.pr-opt-why { display: block; margin-top: 6px; font-size: 14px; color: var(--pr-red-ink); line-height: 1.5; }

/* ---- figures in dark: the packs' hard-coded ink, remapped ----------------
   Every SVG the bank packs carry paints itself with a literal light-mode grey
   rather than currentColor, so on a --pr-sheet card at 0.228 lightness the
   skeletal formulae are near-black on charcoal. A CSS stroke/fill PROPERTY
   beats the corresponding presentation attribute, so the fix is 27 attribute
   selectors and four tokens. Generated from the packs, not transcribed.

   SCOPED TO DARK, and that is a correction to the block this was generated as.
   That version left the rules unconditional and relied on each token's LIGHT
   value being "exactly the string already in the packs", so that they would be
   a no-op until a dark block redefined them. That property does not hold: the
   guide bucket alone collapses 15 distinct greys — from oklch(0.45), a mid
   grey, up to oklch(0.945), a near-white — onto one value, and 18 of the 23
   distinct source values differ from the light value of the token they map to.
   Unconditional, it would have re-coloured light mode: a mid-grey axis would
   have gone pale and a near-white fill would have gone grey. Scoping the rules
   instead makes "light renders byte-identical" true by construction rather
   than by 23 strings staying in step, and it is what the finding these tokens
   came from actually called for. Asserted by sha256 on a graph-paper figure
   rendered light before and after: unchanged.

   FOUR REGIONS, not the two the block was generated with. .pr-stem and
   .pr-opt-body were counted; .pr-worked was not, and 40 of 1850 sampled
   questions across ten packs carry an <svg> inside their worked solution, so a
   mark-scheme figure would have stayed invisible in dark. .pr-sheetnote is
   there for the marking sheet the next task nests the reveal inside.

   currentColor is left alone — two fills and one stroke already follow the
   theme correctly, and that is exactly why filter: invert(1) is the wrong
   tool: it would flip the only marks that were already right. The url(#…)
   fills are pattern references (wcqg-*-grid, wcqg-*-mm) and are not colours;
   the rulings inside those <pattern> elements are reached by the descendant
   selector like anything else in the same <svg>.

   A figure that ever gains a genuinely chromatic colour falls through these
   rules unchanged. That is the right default — a colour that is not grey
   usually means something — but it needs its own decision, not a fifth
   bucket.                                                                  */
@media (prefers-color-scheme: dark) {
  /* the skeleton — 403 of the 442 coloured marks in the packs */
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="oklch(0.20 0.018 250)"] { fill: var(--pr-fig-ink); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.20 0.018 250)"] { stroke: var(--pr-fig-ink); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="#1a1a1a"] { fill: var(--pr-fig-ink); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="#1a1a1a"] { stroke: var(--pr-fig-ink); }

  /* secondary lines */
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.30 0.014 250)"] { stroke: var(--pr-fig-ink-2); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.28 0.015 250)"] { stroke: var(--pr-fig-ink-2); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.32 0.015 250)"] { stroke: var(--pr-fig-ink-2); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="oklch(0.42 0.012 250)"] { fill: var(--pr-fig-ink-2); }
  /* the Data Sheet's own secondary grey — the traced structures on page 4.
     It carries the FULL selector like every other rule in this block, not a
     .pr-book-only one: answer-lanes.test.mjs asserts that every figure rule
     names the stem, the options AND the reveal, because the defect that test
     was written for was a mark-scheme figure left near-black on charcoal. A
     rule shaped differently to dodge that assertion would dodge the check as
     well as the shape. No pack uses this value, so widening it costs
     nothing. */
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="oklch(0.50 0.012 250)"] { fill: var(--pr-fig-ink-2); }

  /* graph-paper rulings, axes and other guides — 15 distinct greys */
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.885 0.004 250)"] { stroke: var(--pr-fig-guide); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="#bcbcbc"] { stroke: var(--pr-fig-guide); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.72 0.006 250)"] { stroke: var(--pr-fig-guide); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.79 0.006 250)"] { stroke: var(--pr-fig-guide); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.45 0.012 250)"] { stroke: var(--pr-fig-guide); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="#eaeaea"] { fill: var(--pr-fig-guide); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="#c2c2c2"] { fill: var(--pr-fig-guide); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="#cacaca"] { fill: var(--pr-fig-guide); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="#bcbcbc"] { fill: var(--pr-fig-guide); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="#ebebeb"] { fill: var(--pr-fig-guide); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.88 0.004 250)"] { stroke: var(--pr-fig-guide); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.87 0.004 250)"] { stroke: var(--pr-fig-guide); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="oklch(0.945 0.003 250)"] { fill: var(--pr-fig-guide); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="oklch(0.885 0.005 250)"] { fill: var(--pr-fig-guide); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="oklch(0.72 0.006 250)"] { fill: var(--pr-fig-guide); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="#d2d2d2"] { stroke: var(--pr-fig-guide); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="#a6a6a6"] { stroke: var(--pr-fig-guide); }

  /* a white knockout behind a label */
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="#ffffff"] { fill: var(--pr-fig-mask); }
  :root:not([data-pr-theme="light"]) :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="#fff"] { fill: var(--pr-fig-mask); }
}

/* the explicit toggle wins in both directions, same as the tokens above */
/* the skeleton — 403 of the 442 coloured marks in the packs */
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="oklch(0.20 0.018 250)"] { fill: var(--pr-fig-ink); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.20 0.018 250)"] { stroke: var(--pr-fig-ink); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="#1a1a1a"] { fill: var(--pr-fig-ink); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="#1a1a1a"] { stroke: var(--pr-fig-ink); }

/* secondary lines */
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.30 0.014 250)"] { stroke: var(--pr-fig-ink-2); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.28 0.015 250)"] { stroke: var(--pr-fig-ink-2); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.32 0.015 250)"] { stroke: var(--pr-fig-ink-2); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="oklch(0.42 0.012 250)"] { fill: var(--pr-fig-ink-2); }
  /* the Data Sheet's own secondary grey — the traced structures on page 4.
     It carries the FULL selector like every other rule in this block, not a
     .pr-book-only one: answer-lanes.test.mjs asserts that every figure rule
     names the stem, the options AND the reveal, because the defect that test
     was written for was a mark-scheme figure left near-black on charcoal. A
     rule shaped differently to dodge that assertion would dodge the check as
     well as the shape. No pack uses this value, so widening it costs
     nothing. */
  :root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="oklch(0.50 0.012 250)"] { fill: var(--pr-fig-ink-2); }

/* graph-paper rulings, axes and other guides — 15 distinct greys */
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.885 0.004 250)"] { stroke: var(--pr-fig-guide); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="#bcbcbc"] { stroke: var(--pr-fig-guide); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.72 0.006 250)"] { stroke: var(--pr-fig-guide); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.79 0.006 250)"] { stroke: var(--pr-fig-guide); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.45 0.012 250)"] { stroke: var(--pr-fig-guide); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="#eaeaea"] { fill: var(--pr-fig-guide); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="#c2c2c2"] { fill: var(--pr-fig-guide); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="#cacaca"] { fill: var(--pr-fig-guide); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="#bcbcbc"] { fill: var(--pr-fig-guide); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="#ebebeb"] { fill: var(--pr-fig-guide); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.88 0.004 250)"] { stroke: var(--pr-fig-guide); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="oklch(0.87 0.004 250)"] { stroke: var(--pr-fig-guide); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="oklch(0.945 0.003 250)"] { fill: var(--pr-fig-guide); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="oklch(0.885 0.005 250)"] { fill: var(--pr-fig-guide); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="oklch(0.72 0.006 250)"] { fill: var(--pr-fig-guide); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="#d2d2d2"] { stroke: var(--pr-fig-guide); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [stroke="#a6a6a6"] { stroke: var(--pr-fig-guide); }

/* a white knockout behind a label */
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="#ffffff"] { fill: var(--pr-fig-mask); }
:root[data-pr-theme="dark"] :is(.pr-stem, .pr-opt-body, .pr-worked, .pr-sheetnote, .pr-book, .pr-bookzoom) svg [fill="#fff"] { fill: var(--pr-fig-mask); }

/* ===================================================== 7. THE MARKING SHEET ==
   design-proto.css §7, ported, and §7.3's five tiers in order — because the
   ORDER IS THE DESIGN. A card that opens with 200px of algebra buries the one
   sentence that mattered; a card that opens with the verdict and the value
   lets a student check their own working in two seconds and then choose how
   much more they want.

     tier 0  .pr-verdict   the verdict AND the correct value  (except `nearly`)
     tier 1  .pr-named     the NAMED misconception, display serif  + .pr-fix
     tier 2  .pr-worked    the method, COLLAPSED even when wrong
     tier 3  .pr-margin    the examiner's note, inside tier 2
     tier 4  .pr-read      the notes link, a line and not a button
             .pr-again     the recovery action — the reason this exists

   TWO NAMES ARE OURS RATHER THAN THE PROTOTYPE'S, the same policy §3 sets out
   for the header:

     .pr-work   → .pr-worked      .pr-work-n → .pr-recall-marks

   The prototype's .pr-work-n is the chip at the far end of the summary line —
   "3 steps" in the mockup. This platform already had one, .pr-recall-marks,
   carrying the marks fraction on a self-marked reveal; two classes for one
   slot is how a summary ends up with both. So .pr-work-n is not ported and
   .pr-recall-marks takes its place, with margin-left: auto doing the work.

   .pr-worked has been the reveal's class since the runner was written, it is
   named in all 54 dark-mode figure rules (and in the gate that checks they are
   dark-scoped), in .pr-worked .wcqg-msline below, and in the generated bank
   harness. Renaming it would churn every one of those for no pixel. The
   DECLARATIONS below are the prototype's.

   NESTING IS LOAD-BEARING. Everything here renders INSIDE .pr-sheetnote, which
   §2's case guard immunises: `.pr-sheetnote *` carries text-transform: none
   !important. That is where every δ on this platform actually lives — no live
   stem contains one, but the worked solutions and mark-scheme notes are full
   of them, along with Cl, Na and Co. A reveal rendered as a SIBLING of the
   sheet rather than a child would sit outside the guard, and any uppercase
   rule added later would ship Δ and CL. Nest it.                            */

/* The container is the page's aria-live region (session.html/session.mjs), so
   it must exist even when empty — :empty keeps it from adding 22px of space
   above an unanswered question. */
.pr-feedback { margin-top: 22px; }
.pr-feedback:empty { display: none; }

.pr-sheetnote {
  max-width: var(--pr-measure); margin: 14px auto 0;
  background: var(--pr-sheet); border: 1px solid var(--pr-rule);
  border-left: 3px solid var(--pr-red);
  border-radius: var(--pr-r-lg); box-shadow: var(--pr-lift);
  padding: clamp(16px, 3.5vw, 24px);
}
/* §7.2: a 3px RULE is a mark that has been made. The card is never flooded —
   the pen colour is on one edge and on the glyph, and nowhere else. */
.pr-sheetnote.is-ok   { border-left-color: var(--pr-green); }
.pr-sheetnote.is-near { border-left-color: var(--pr-amber); }
/* recall, between its two self-report questions: the mark scheme is on screen
   and NOTHING HAS BEEN MARKED YET. §7.2's grammar is that a 3px rule down the
   edge means a mark has been made, so this card's edge is ink until the second
   report arrives and paintFeedback replaces it with a pen. */
.pr-sheetnote.is-open { border-left-color: var(--pr-rule-2); }

/* ---- tier 0: the verdict, and the value they need to check their working
   FIVE TIERS HAVE TO READ AS A HIERARCHY, NOT A STACK.
   They were five blocks of 15–16px text separated by 8–16px margins, in three
   greys, and the card read top to bottom at one volume: the verdict, the
   named error, the method, the margin note and the notes link all arrived
   with the same weight, so the most valuable sentence on the screen — the
   name of the mistake — had to be found rather than seen.

   The hierarchy is now carried by three things at once, so it survives being
   read at arm's length: SIZE (tier 1 is display serif at up to 26px, tier 0
   is 17px, tiers 2–4 are 15px), COLOUR (tier 0 and 1 are --pr-ink, 2–4 are
   --pr-ink-2/3) and SEPARATION (tier 2 opens with a hairline across the whole
   card; nothing above it has a rule). */
.pr-verdict { display: flex; align-items: baseline; gap: var(--pr-s2) var(--pr-s3); flex-wrap: wrap; }
.pr-verdict-mk { font-size: 22px; line-height: 1; font-weight: 600; color: var(--pr-red-ink); }

/* ---- the mark, drawn --------------------------------------------------
   The one reward moment on the platform. A tick that ARRIVES the way a
   pen makes one — 26px, stroked in the verdict's own colour, drawn over
   --pr-slow — where before it was a ✓ character that was simply already
   there. Everything else about marking is deliberately undramatic; this is
   the single place the product is allowed to feel like something happened.

   Stroke, not fill, and currentColor, so the three states need no rule of
   their own: .pr-verdict-mk is already green, amber or red by the sheet's
   state class and the path inherits it. */
.pr-verdict-svg {
  display: block; width: 26px; height: 26px;
  align-self: center;
  overflow: visible;
}
.pr-ink-draw {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  /* 30 comfortably exceeds the longest path here (the tick is ≈24 units), so
     one dasharray serves both glyphs and neither can start part-drawn. */
  stroke-dasharray: 30;
  stroke-dashoffset: 30;
  animation: pr-draw var(--pr-slow) var(--pr-ease) 60ms forwards;
}
/* The cross is two strokes and they are made one after the other, because
   that is how a cross is written. */
.pr-ink-draw--2 { animation-delay: 200ms; }
@keyframes pr-draw { to { stroke-dashoffset: 0; } }

/* The verdict line itself arrives rather than appearing. 8px is enough to
   read as movement and small enough that nothing below it jumps. */
.pr-verdict { animation: pr-rise var(--pr-mid) var(--pr-ease) both; }
@keyframes pr-rise {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
.is-ok   .pr-verdict-mk { color: var(--pr-green-ink); }
.is-near .pr-verdict-mk { color: var(--pr-amber-ink); }
.pr-verdict-txt { font-size: var(--pr-t-lg); color: var(--pr-ink-2); font-weight: 500; }
/* The value is chemistry — 0.119 mol dm⁻³, [CuCl₄]²⁻, 2 (CH₃)₂CHOH + 9O₂ →
   6CO₂ + 8H₂O — so .pr-verdict-val is in the case guard's list at the top of
   this file, welding included. It is NOT a flex or grid container, which is
   the other half of keeping a <sup> raised: a blockified <sup> loses
   vertical-align and prints flat. See unitHTML() in session.mjs. */
.pr-verdict-val {
  font-family: var(--font-mono); font-size: var(--pr-t-lg); color: var(--pr-ink); font-weight: 600;
  background: var(--pr-sunk); border-radius: var(--pr-r-sm); padding: 2px 8px;
}

/* ---- tier 1: the NAMED misconception -----------------------------------
   Display serif, and the strongest type on the card, because naming the error
   is the most valuable sentence on the screen: it turns "I'm bad at
   titrations" into "I made a known, named, fixable mistake."

   It is omitted rather than faked wherever no title was authored — an
   invented name for someone else's error is worse than none. Three of the
   seventy-four templates carry misconceptions at all, and all eleven of their
   entries are titled; the other lanes diagnose through options[].feedback and
   answer.nearMiss, which land in .pr-fix below with no name above them. */
.pr-named {
  font-family: var(--font-display); font-size: clamp(21px, 4.6vw, 26px);
  line-height: 1.18; margin: var(--pr-s4) 0 0; color: var(--pr-ink); font-weight: 400;
}
/* §7.2's third grammar: an italic serif word in a title is brand voice.
   template-contract.test.mjs fails the build on a title with no <em>. */
.pr-named em { font-style: italic; color: var(--pr-red-ink); }
.pr-fix { margin: 8px 0 0; font-size: 15.5px; line-height: 1.6; color: var(--pr-ink-2); }
.pr-fix sub, .pr-fix sup { line-height: 0; }
/* The gap between the prediction and the mark, on a recall answer, and only
   when the two disagree. Set apart from the correction above it because it is
   a different KIND of diagnosis — it is about the student rather than about
   the chemistry — and it is the one thing the two-stage self-mark exists to
   make visible.

   INK, not a pen. §7.2 reserves red for a mark that has been made, and nothing
   here marks anything: the mark is the verdict at the top of the card. */
.pr-fix-calib {
  margin-top: 12px; padding: 1px 0 1px 12px;
  border-left: 2px solid var(--pr-rule-2);
  font-size: 14.5px;
}

/* ---- tier 2: the method, COLLAPSED even when wrong ----------------------
   Auto-opening 200px of algebra buries tier 1 and trains skimming. It opens
   from the second attempt on (§7.3, "progressive by attempt"), and always for
   recall, whose reveal IS the point of the type.

   The border/background this rule used to carry are gone: inside .pr-sheetnote
   a second bordered box inside a bordered box reads as a modal. A hairline
   across the card is the prototype's object and it is enough. */
.pr-worked { margin: 16px 0 0; border-top: 1px solid var(--pr-rule); }
.pr-worked > summary {
  list-style: none; cursor: pointer; padding: 13px 0 0;
  display: flex; align-items: center; gap: 9px;
  font-size: 15px; font-weight: 500; color: var(--pr-ink);
}
.pr-worked > summary::-webkit-details-marker { display: none; }
.pr-worked > summary::before {
  content: "\25B8"; color: var(--pr-ink-3); font-size: 12px;
  transition: transform var(--pr-speed) ease;
}
.pr-worked[open] > summary::before { transform: rotate(90deg); }
.pr-worked p { margin: 12px 0 0; line-height: 1.6; font-size: 15.5px; color: var(--pr-ink-2); }
.pr-worked p b, .pr-worked p strong { color: var(--pr-ink); font-weight: 600; }
/* The prototype's numbered step, ported whole. Nothing emits it yet — a
   template's workedSolution is authored HTML and arrives as <p> — but it is
   the shape a generated step-by-step solution takes, and it is part of §7. */
.pr-step { display: flex; gap: 12px; margin: 12px 0 0; font-size: 15.5px; line-height: 1.6; color: var(--pr-ink-2); }
.pr-step-n {
  flex: 0 0 auto; font-family: var(--font-mono); font-size: 12px; color: var(--pr-ink-3);
  width: 20px; line-height: 20px; text-align: center;
  border: 1px solid var(--pr-rule); border-radius: 50%;
}
.pr-step b { color: var(--pr-ink); font-weight: 600; }

/* Recall's reveal is always open — check.mjs guarantees reveal === true for
   every self-report — and carries a marks fraction the numeric reveal does
   not. margin-left: auto pushes it to the far end of the summary line, which
   is already a flex row, so "Mark scheme" and "1 / 1" both read at a glance
   instead of running together as one phrase. */
/* THE TARIFF, beside the scheme it came from.

   It was a grey mono fraction on self-marked questions only. It is now on
   every question and it is a CHIP, because it is the one number on this
   screen that says what the question was worth — and the sheet it sits on is
   the only place in the category where a student sees an award and the mark
   scheme that produced it in the same object.

   It takes the sheet's own state colour: green when every mark was taken,
   amber when some were, and the plain rule otherwise. .pr-sheetnote's state
   class is on the ancestor, so no JavaScript decides this. */
.pr-recall-marks {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--pr-t-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--pr-ink-2);
  white-space: nowrap;
  background: var(--pr-sheet);
  border: 1px solid var(--pr-rule-2);
  border-radius: var(--pr-r-pill);
  padding: 2px 10px;
}
.is-ok .pr-recall-marks {
  color: var(--pr-green-ink);
  border-color: color-mix(in oklch, var(--pr-green) 45%, transparent);
  background: var(--pr-green-wash);
}
.is-near .pr-recall-marks {
  color: var(--pr-amber-ink);
  border-color: color-mix(in oklch, var(--pr-amber) 45%, transparent);
  background: var(--pr-amber-wash);
}

/* ---- tier 3: the examiner's margin note, INSIDE tier 2 ------------------
   The one piece of warmth in the design, and the same object as the "Joe —
   your tutor" annotation on the marketing site: display serif italic with a
   red hairline, handwritten in feel without a single script font. The copy it
   carries is already the strongest brand asset the platform has ("never stop
   working because a number looks ugly"). */
.pr-margin {
  margin: 20px 0 0; padding: 2px 0 2px 15px;
  border-left: 2px solid var(--pr-red);
  font-size: 15px; color: var(--pr-ink-2);
}
/* A REAL HEADING, not an inline "Where marks get lost —" lead-in run into the
   first sentence. Same display-serif italic object as before; what changed is
   that the label and the prose are no longer one block of text, which is half
   of why the note read as an undifferentiated slab. */
.pr-margin-h {
  font-family: var(--font-display); font-style: italic; font-weight: 400;
  font-size: 18px; line-height: 1.25; color: var(--pr-red-ink);
  margin: 0 0 7px;
}
/* The note as one string — all 189 in the bank are strings today. Median 93
   words, longest 207, and at 375px that is 25-odd lines. The leading is opened
   up and the measure is capped so a desk browser does not run it to 90
   characters; what actually breaks the wall is the list below, which arrives
   when a note is re-authored as an array. Nothing splits a string here — a
   sentence cut by a machine ends mid-clause. */
.pr-margin-body { line-height: 1.7; max-width: 58ch; }
/* One entry per way to lose a mark. */
.pr-margin-list { margin: 0; padding-left: 20px; max-width: 58ch; }
.pr-margin-list li { line-height: 1.65; margin: 0 0 9px; }
.pr-margin-list li:last-child { margin-bottom: 0; }

/* ---- tier 4: the exit ramp ---------------------------------------------
   A line, not a button: it must not out-shout the recovery action below it. */
.pr-read { margin: 16px 0 0; font-size: 14.5px; }
.pr-read a { color: var(--pr-ink-2); }

/* ---- tier 5: report a problem ---------------------------------------------
   QUIET BY CONSTRUCTION, and that is the design rather than modesty. This sits
   under a mark, below the recovery action, and it must never compete with it:
   a student who has just got something wrong should be reaching for "try a
   similar one", because the platform is usually right. So link-weight text in
   --pr-ink-3, no button chrome at all until the form is open, and no colour —
   §7.2 reserves the red for marks, and a dispute is not a mark.

   It renders inside .pr-sheetnote, so §2's case guard reaches it. Nothing here
   is chemistry and nothing here may become chemistry: .pr-label on the
   textarea's own label is uppercased by §7.7, and "WHAT HAPPENED" is safe
   precisely because it is not a formula. */
.pr-dispute { margin: 18px 0 0; }
.pr-dispute-links { display: flex; flex-wrap: wrap; gap: 16px; margin: 0; }
.pr-dispute .pr-link {
  background: none; border: 0; padding: 0;
  font: inherit; font-size: 13px; color: var(--pr-ink-3);
  text-align: left; text-decoration: underline; text-underline-offset: 2px;
  cursor: pointer;
}
.pr-dispute .pr-link:hover { color: var(--pr-ink); }
.pr-dispute-form { margin: 14px 0 0; }
.pr-textarea { width: 100%; resize: vertical; font: inherit; font-size: 16px; line-height: 1.45; }
.pr-dispute-thanks { font-size: 13px; color: var(--pr-ink-3); margin: 14px 0 0; line-height: 1.5; }

/* ---- the recovery action, the reason the platform exists ---------------- */
/* scroll-margin-bottom so scrollIntoView({block:'end'}) stops SHORT of the
   sticky bar rather than tucking this row underneath it — see clearTheBar()
   in session.mjs, and the 375x667 measurement in its comment. */
.pr-again { display: flex; gap: 10px; flex-wrap: wrap; margin: 18px 0 0; align-items: center;
            scroll-margin-bottom: calc(var(--pr-bar-h) + 14px); }
/* flex-basis 220px so the primary takes the row on a phone and shares it on a
   desk, and column so the sub-label sits under the verb rather than beside it
   — .pr-btn is a flex row by default. */
.pr-again .pr-btn--primary { flex: 1 1 220px; flex-direction: column; gap: 1px; padding-block: 8px; }
.pr-again .pr-btn--primary small {
  font-family: var(--font-mono); font-size: 10.5px; font-weight: 400;
  opacity: 0.72; letter-spacing: 0.03em;
}

/* ---- the three dev harnesses ------------------------------------------
   _preview.html, _text-input_harness.html and the generated _bank-harness.html
   render a feedback card without a .pr-sheetnote around it — they are marking
   previews, not the runner — and they link this stylesheet. These four rules
   are what they use and the session runner no longer emits: the flat verdict
   paragraph the marking sheet replaces, its notes link, its button row, and
   the mark-scheme note tier 3 replaces. Do not delete them without editing
   _scripts/practice-bank-harness.mjs, which is the required pixel read before
   an authored MCQ topic can be imported.

   The three verdict colours were hard-coded light-mode values — a green, a
   red and an ochre written out in oklch — so at 11pm they were dark ink on a
   dark card. They read the pens now, like everything else. */
.pr-fb { font-size: 16px; line-height: 1.55; margin: 0 0 10px; }
.pr-fb.is-ok      { color: var(--pr-green-ink); font-weight: 500; }
.pr-fb.is-err     { color: var(--pr-red-ink); }
.pr-fb.is-partial { color: var(--pr-amber-ink); }
.pr-fb-help { margin: 0 0 14px; font-size: 15px; }
.pr-fb-help a { color: var(--pr-red); }
.pr-fb-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 6px; }
.pr-ms-note {
  border-left: 3px solid var(--pr-red);
  padding-left: 12px;
  font-size: 15px;
  color: var(--pr-ink-2);
}

/* ==================================================== 8. THE VERDICT BAR ====
   design-proto.css §8. Sticky, one line, thumb height. While the question is
   unanswered it holds the Check button; once the answer is marked it holds the
   mark, whichever of the two numbers fit beside it, and the recovery action.

   This is what makes the phone case work, and it is the reason §7.4 forbids a
   modal: the full marking sheet stays INLINE in the document, where it can be
   read at whatever length it needs, and the one action never leaves the thumb.

   It is also the only place the Check button exists. It used to live inside
   the sheet in .pr-q-actions and simply stay there after the answer came back,
   so a marked question offered "Check answer" under a verdict — the state
   machine in session.mjs owns the whole bar and can only paint one state at a
   time, which is what makes that unreachable rather than merely fixed.       */

.pr-bar {
  position: sticky; bottom: 0; z-index: 25;
  margin: 0; width: 100%;
  padding: 10px var(--pr-gutter) calc(10px + env(safe-area-inset-bottom, 0px));
  background: color-mix(in srgb, var(--pr-paper) 92%, transparent);
  backdrop-filter: saturate(1.4) blur(10px);
  border-top: 1px solid var(--pr-rule);
  transition: opacity var(--pr-speed) ease, transform var(--pr-speed) ease;
}

/* THE BAR STANDS DOWN WHEN THE REAL ACTIONS ARE ON SCREEN.

   Measured on a 375 × 812 phone after marking a wrong answer: the student saw
   "Try a similar one" TWICE — once as a full-width ink button inside the
   marking sheet, and once, 40px below it, in the sticky bar. Two identical
   primary buttons, one screen, same job. The bar exists for the case where
   the student has scrolled back up to re-read the question and the action has
   gone off the bottom; when the action is right there, the bar is noise.

   session.mjs watches .pr-again with an IntersectionObserver and adds this
   class while it is visible. If the browser has no IntersectionObserver the
   class is never added and the bar simply stays, which is the old behaviour
   and is safe. It is opacity and a slide rather than `hidden`, so the bar
   never reflows the page under a thumb mid-tap. */
.pr-bar.is-stood-down {
  opacity: 0;
  transform: translateY(100%);
  pointer-events: none;
}
.pr-bar-inner { max-width: var(--pr-measure); margin: 0 auto; display: flex; align-items: center; gap: 10px; }
/* The bar spans the viewport, because it is the thumb zone and a thumb zone
   that stops short of the screen edge is a smaller target for no reason. Its
   CONTENTS line up with the question column: 1000px of session grid, less the
   300px desk and the gap, so the action sits under the question the student
   is looking at and not under the desk beside it. */
@media (min-width: 1100px) {
  .pr-bar-inner { max-width: 1000px; padding-right: calc(300px + var(--pr-s6)); }
}
/* Unanswered: the Check button IS the bar and takes the whole width. */
.pr-bar-inner .pr-btn { flex: 1; }
/* Marked: the mark and the two numbers take the room and the action keeps its
   natural width, so "You gave 0.104 · the answer is 0.119" is not squeezed
   into one column by a button that never needed it. */
.pr-bar--marked .pr-btn { flex: 0 0 auto; }
.pr-bar-mk { font-size: 18px; font-weight: 600; color: var(--pr-red-ink); flex: 0 0 auto; }
.pr-bar-mk.is-ok   { color: var(--pr-green-ink); }
.pr-bar-mk.is-near { color: var(--pr-amber-ink); }
/* min-width: 0 for §7.8's reason at one remove: a flex item's automatic
   minimum is min-content, so one long unbreakable value would push the button
   off a 375px screen rather than wrapping. */
.pr-bar-txt { font-size: 13.5px; color: var(--pr-ink-2); min-width: 0; line-height: 1.35; }
.pr-bar-txt b { font-family: var(--font-mono); font-weight: 500; color: var(--pr-ink); }
/* "You gave 0.302" and "the answer is 0.207 mol dm⁻³" are two clauses, and at
   375px beside a button they do not fit on one line. inline-block makes each
   one an atomic wrap unit, so the break lands at the separator between them
   instead of inside "the answer is B" — which read back as a stray letter
   alone on the second line. It is NOT white-space: nowrap: a clause wider than
   the bar would then overflow the page sideways rather than wrap, and §7.8's
   whole lesson is that a min-content minimum is how that happens. */
.pr-bar-part { display: inline-block; max-width: 100%; }
/* The fold-out phone. At 320px the text column beside the ✗ and the button is
   about 110px, and "the answer is 0.172" alone needs two lines of it — so with
   the echo as well the bar ran to three and became a block rather than a bar.
   The echo is the half that goes: the student's own answer is still in the
   field a thumb's width above it, with the mark down its edge, and the value
   is the one thing the bar was carrying that is not otherwise in the thumb
   zone. Measured at 320, not guessed. */
@media (max-width: 360px) {
  .pr-bar-given { display: none; }
}
/* The echoed value is the student's own input and the correct one is
   chemistry, so .pr-bar-txt is in the case guard's list at the top of this
   file — welding included. It is not a flex container, for the same reason
   .pr-verdict-val is not: a blockified <sup> loses vertical-align and prints
   flat, which on a concentration is a different unit. */

/* ------------------------------------------ AQA-verbatim exam question markup

   The bank packs carry the published question HTML unchanged, and it arrives
   wearing the class names the revision pages give it: wcqg-msline, wcqg-fig,
   rn-table and so on. /practice/ does NOT load revision.css — that wall is the
   point — so those class names are styled HERE, from scratch, sized for a
   practice card rather than a notes page.

   The one rule that matters: NOTHING in this block may uppercase or letter-space
   its text. These are AQA's own formulae and table headers, and a transform here
   silently ships Cl2O as CL2O and δ as Δ. revision.css has to opt out of its own
   uppercase table headers for exactly this reason; practice.css never opts in.
   ------------------------------------------------------------------------- */

/* a figure in a stem or a mark scheme */
.wcqg-fig { position: relative; width: fit-content; max-width: 100%; margin: 14px auto; }
.wcqg-fig svg { display: block; width: auto; max-width: 100%; height: auto; margin-inline: auto; }
.wcqg-fig figcaption { font-size: 13px; color: var(--pr-ink-3); text-align: center; margin-top: 4px; }

/* a figure that IS an option — a structure the student picks between */
.wcqg-optfig { display: block; width: fit-content; max-width: 100%; }
.wcqg-optfig svg { display: block; width: auto; max-width: 100%; height: auto; }

/* stem paragraphs, and mark-scheme lines in the reveal.

   One selector now, not two. The runner's stem is .pr-stem; the .pr-q-stem
   half of this rule existed for the three dev harnesses, and each of them
   carries its own copy of it in its own <style> as of the answer-lane port —
   _preview.html, _text-input_harness.html and the generated _bank-harness.html
   (via _scripts/practice-bank-harness.mjs). None of them ships, and four real
   pages were downloading the rule for them. */
.pr-stem .wcqg-line { margin: 0 0 8px; }
.pr-stem .wcqg-line:last-child { margin-bottom: 0; }
/* A recall mark scheme is TWO DIFFERENT THINGS, and they used to be set as
   one. First a <ul class="wcqg-ul"> of what earns each mark — one <li> per
   mark, which is what makes self-marking a judgement rather than a feeling.
   Then one to three <p class="wcqg-msline"> paragraphs of ACCEPTANCE RULES:
   what else is allowed, which word carries the mark, what is neither credited
   nor penalised. Measured across all 189 items in the bank: 189 of 189 open
   with the list, 126 carry one acceptance paragraph, 58 carry two, 5 carry
   three; median 33 words, longest 98.

   The second was set at the same weight as the first, directly underneath it,
   competing with it. So the marks are the answer now — full ink, at reading
   size — and the acceptance detail sits behind its own labelled line, smaller
   and quieter, where a student can find it without reading it first.

   The label is a ::before rather than markup because this is presentation and
   the strings are authored content this file must not rewrite. It is
   deliberately NOT a .pr-label: §7.7 allows exactly one uppercase class, and
   in any case a label INSIDE .pr-sheetnote loses its uppercase to the case
   guard's !important, which is the right precedence and the reason a label
   belongs beside an immunised region rather than within one. Sentence-case
   mono is what it is, and what it should be. */
.pr-worked .wcqg-ul { margin: 0 0 4px; padding-left: 22px; }
.pr-worked .wcqg-ul li {
  font-size: 15.5px; line-height: 1.6; color: var(--pr-ink); margin: 0 0 8px;
}
.pr-worked .wcqg-ul li:last-child { margin-bottom: 0; }

/* ---- M1, M2, M3 — NUMBERED HERE, NEVER AUTHORED INTO THE STRINGS ----------

   Joe, an AQA examiner, reading a composed question on 2026-09-02: "mark
   schemes would be better if each M1/M2/M3 etc. was clearly identified."

   RENDERED RATHER THAN GENERATED, and the argument is the one three paragraphs
   above: the label is presentation, the strings are authored content this file
   must not rewrite. Same reasoning as the "Marking detail" ::before, and the
   same place.

   WHY A COUNTER IS SAFE HERE SPECIFICALLY. `explain/card.mjs` keeps marking
   points and rulings in SEPARATE arrays and derives the tariff as
   `markPoints.length`, emitting one <li> per mark and putting every "Allow…" /
   "Ignore…" line in a <p class="wcqg-msline"> OUTSIDE the list. So every <li>
   in this list is a mark by construction, and numbering by position cannot
   label a ruling as a mark — which was the only real hazard. Measured at the
   dealt layer before this shipped: over every template at 5 seeds, 525 LIST
   worked solutions, the <li> count inside the <ul> equals the question's marks
   on 525 of 525, with the check shown reporting a mismatch when fed marks+1.
   That equality is asserted per SHA in mark-label-counter.test.mjs, because the
   bank importer regenerates the 32 packs that also emit this list.

   `> li` AND NOT `li`: a nested list inside a marking point would otherwise be
   numbered as though its items were marks of their own.

   IT DOES NOT REACH THE NOTE. "Where marks get lost" renders as
   <ul class="pr-margin-list"> inside .pr-margin — a different class in a
   different block — and it MUST NOT be numbered, because that list mixes
   marking points with ways to lose them and nothing can tell them apart by
   position. That is the whole reason the five prose notes label their marks in
   the authored text instead.

   The label replaces the bullet rather than sitting beside it, hung in the
   margin the way a real scheme prints it. */
.pr-worked .wcqg-ul { counter-reset: pr-mark; list-style: none; padding-left: 34px; }
.pr-worked .wcqg-ul > li { counter-increment: pr-mark; position: relative; }
.pr-worked .wcqg-ul > li::before {
  content: "M" counter(pr-mark);
  position: absolute; left: -34px; top: 0; width: 30px;
  font-weight: 600; color: var(--pr-ink-3);
  font-variant-numeric: tabular-nums;
}

.pr-worked .wcqg-msline {
  font-size: 14px; line-height: 1.6; color: var(--pr-ink-3); margin: 0 0 8px;
}
.pr-worked .wcqg-msline:last-child { margin-bottom: 0; }
/* the seam between the marks and the fine print, and only there — a mark
   scheme with no list has nothing to be fine print BELOW */
.pr-worked .wcqg-ul + .wcqg-msline { margin-top: 16px; padding-top: 13px; border-top: 1px solid var(--pr-rule); }
.pr-worked .wcqg-ul + .wcqg-msline::before {
  content: "Marking detail";
  display: block;
  margin: 0 0 5px;
  font-family: var(--font-mono);
  font-size: var(--pr-micro);
  font-weight: 500;
  color: var(--pr-ink-3);
}

/* "1 mark" — a rule across the line with the chip at the end, as printed */
.wcqg-msmark { display: flex; align-items: center; gap: 10px; margin: 0 0 14px; padding: 0; }
.wcqg-msmark::before { content: ""; flex: 1; height: 1px; background: var(--pr-rule); }
.rn-exam-mk {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.04em;
  color: var(--pr-ink-3);
  border: 1px solid var(--pr-rule);
  border-radius: 3px;
  padding: 1px 5px;
  white-space: nowrap;
}

/* which paper the question came from */
.pr-q-src {
  margin: 14px 0 0;
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.04em;
  color: var(--pr-ink-3);
}

/* a data table inside a question */
.rn-table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; margin: 16px 0; }
.rn-table-scroll > .rn-table { margin: 0; }
.rn-table {
  width: 100%;
  border-collapse: collapse;
  margin: 16px 0;
  font-size: 14.5px;
  border: 1px solid var(--pr-rule);
}
.rn-table th {
  background: var(--pr-ink);
  color: var(--pr-paper);
  font-weight: 500;
  font-size: 12.5px;
  padding: 8px 12px;
  text-align: left;
  vertical-align: top;
}
.rn-table td {
  padding: 8px 12px;
  border-bottom: 1px solid var(--pr-rule);
  vertical-align: top;
  line-height: 1.55;
  color: var(--pr-ink-2);
}
.rn-table tr:last-child td { border-bottom: 0; }

/* Phone width: the same table, with less air around the numbers.

   The five-column iodine-clock table in the rate-equations pack laid out at
   383 px inside a 305 px scroller, which put `Time / s` — the column that
   question actually turns on — off-screen, with no visual affordance that the
   table scrolled at all. The cell CONTENT was already at its minimum (the
   headers wrap, the numbers cannot); the 24 px of horizontal padding per cell
   was not. Twelve columns' worth of padding is comfortable on a laptop and is
   simply the reason a phone cannot see the data. */
@media (max-width: 480px) {
  .rn-table th, .rn-table td { padding-left: 8px; padding-right: 8px; }
}

/* THE END OF A SESSION IS A RESULT, NOT A HEADLINE.
   It was "Session finished" in 34px display serif with the score underneath
   it in 17px mono — the largest object on the screen said the least. The
   score is the thing the student came back for, so the score is the object
   and "session finished" is the label on it. */
.pr-done {
  text-align: center;
  margin: var(--pr-s6) auto 0;
  max-width: var(--pr-measure);
  box-shadow: var(--pr-e2);
  padding: var(--pr-s7) var(--pr-s5);
  border: 1px solid var(--pr-rule);
  border-radius: var(--pr-r-lg);
  background: var(--pr-sheet);
}
.pr-done-k { margin: 0 0 var(--pr-s3); }
.pr-done-score {
  font-family: var(--font-display); font-size: var(--pr-display);
  line-height: 1.1; margin: 0 0 var(--pr-s3); color: var(--pr-ink);
  font-variant-numeric: tabular-nums;
}
.pr-done-note { color: var(--pr-ink-2); max-width: 42ch; margin: 0 auto var(--pr-s4); font-size: var(--pr-t-md); }

/* ---- the payoff -------------------------------------------------------
   The session ended on one sentence. These are the two things a student who
   has just done twelve questions actually wants: what it was worth in marks,
   and what it moved. */
.pr-done-marks {
  display: flex; align-items: baseline; justify-content: center; gap: 6px;
  margin: 0 0 var(--pr-s2);
  font-family: var(--font-display);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.pr-done-marks b { font-size: clamp(48px, 9vw, 76px); font-weight: 400; }
.pr-done-marks > span { font-size: clamp(22px, 4vw, 32px); color: var(--pr-ink-3); }
.pr-done-marks-l {
  font-family: var(--font-mono); font-size: var(--pr-t-sm);
  color: var(--pr-ink-3);
  /* Aligned to the DENOMINATOR's baseline, not the big numeral's — sitting it
     on the 76px figure's baseline drops it below the card's optical centre
     and reads as a caption that has come loose. */
  align-self: baseline;
  letter-spacing: 0.06em;
}
/* The sentence under it drops to a supporting size: it is now the second
   number on the screen, not the first. */
.pr-done-marks + .pr-done-score { font-size: var(--pr-t-lg); font-family: var(--font-body); color: var(--pr-ink-2); }

.pr-moved {
  text-align: left;
  margin: var(--pr-s5) 0 var(--pr-s4);
  padding: var(--pr-s4) var(--pr-s5);
  background: var(--pr-sunk);
  border-radius: var(--pr-r);
}
.pr-moved-k { margin: 0 0 var(--pr-s3); }
.pr-moved-l { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--pr-s2); }
.pr-moved-l li { display: flex; align-items: baseline; gap: var(--pr-s3); }
.pr-moved-n { flex: 1; min-width: 0; font-size: var(--pr-t-md); }
.pr-moved-v {
  font-family: var(--font-mono); font-size: var(--pr-t-sm); font-weight: 600;
  color: var(--pr-green-ink); font-variant-numeric: tabular-nums; white-space: nowrap;
}
.pr-moved-back { margin: var(--pr-s3) 0 0; font-size: var(--pr-t-base); color: var(--pr-ink-3); }
.pr-moved-l + .pr-moved-back { padding-top: var(--pr-s3); border-top: 1px solid var(--pr-rule); }
/* WHY THOSE TWELVE QUESTIONS — the server's own `planKind`, printed. */
.pr-plan {
  margin: 0 auto var(--pr-s5); max-width: 42ch;
  padding-top: var(--pr-s4); border-top: 1px solid var(--pr-rule);
  font-size: var(--pr-t-base); color: var(--pr-ink-3);
}
.pr-plan-up { display: block; margin-top: var(--pr-s2); color: var(--pr-ink-2); }
.pr-done-actions { display: flex; gap: var(--pr-s2); justify-content: center; flex-wrap: wrap; }
.pr-done-actions .pr-btn { flex: 1 1 auto; }

/* ============================================ 9. THE TIMED MOCK PAPER ======
   A mock is the session runner with a clock, a marks budget and its marking
   held to the end. Everything below is the furniture those three things need;
   the question itself, in all five lanes, is §6's and is not duplicated. */

/* ---- the chooser --------------------------------------------------------- */
.pr-page-k { margin: var(--pr-s5) 0 var(--pr-s2); }
/* The mock page's own h1. It carried --pr-display, which tops out at 34px,
   while .pr-title was raised to the hero's clamp with the note "so the two
   screens are the same product" — and this was the screen that change was
   for. Same clamp, same line-height, same balance. */
.pr-mock-t {
  font-family: var(--font-display); font-weight: 400;
  font-size: clamp(30px, 2.2vw + 20px, 44px);
  line-height: 1.04; letter-spacing: -0.012em;
  margin: 0 0 var(--pr-s3);
  text-wrap: balance;
}
.pr-mock-t i { font-style: italic; color: var(--pr-red); }
.pr-mock-b { color: var(--pr-ink-2); margin: 0 0 var(--pr-s6); max-width: 46ch; }
.pr-mock-n { color: var(--pr-ink-3); font-size: var(--pr-t-base); max-width: 46ch; margin: var(--pr-s6) 0 0; }

/* align-items:stretch so the row has ONE bottom edge. Paper 3's description
   runs to three lines where the others take two, and the cards measured
   182/182/203 — a ragged row that reads as a rendering accident. */
.pr-papers { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--pr-s4); align-items: stretch; }
.pr-papers > li { display: flex; }
.pr-paper {
  display: grid; align-content: start; gap: var(--pr-s3);
  padding: var(--pr-s6);
  border: 1px solid var(--pr-rule); border-radius: var(--pr-r);
  background: var(--pr-sheet); color: var(--pr-ink); text-decoration: none;
  box-shadow: var(--pr-e1);
  transition: transform var(--pr-speed) var(--pr-ease),
              box-shadow var(--pr-speed) var(--pr-ease),
              border-color var(--pr-speed) var(--pr-ease);
}
.pr-paper { flex: 1; }
.pr-paper:hover {
  border-color: var(--pr-rule-2);
  transform: translateY(-3px);
  box-shadow: var(--pr-e2);
  text-decoration: none;
}
/* The verb, pushed to the card's foot so the three sit on one line however
   long the description above them runs. */
.pr-paper-go {
  margin-top: auto;
  padding-top: var(--pr-s4);
  display: inline-flex; align-items: center; gap: 7px;
  font-size: var(--pr-t-lg); font-weight: 600;
  color: var(--pr-ink);
}
.pr-paper-go svg { width: 16px; height: 16px; transition: transform var(--pr-speed) var(--pr-ease); }
.pr-paper:hover .pr-paper-go svg { transform: translateX(3px); }
.pr-paper--accent .pr-paper-go { color: var(--pr-red-ink); }
/* The tariff and the duration were a mono run inline with the title, and at
   1280px "105 marks · 120 min" was clipped by its own card. They are their
   own row now and each is a chip, which also makes them the two facts a
   student scans for when deciding whether there is time tonight. */
.pr-paper-h { display: grid; gap: var(--pr-s3); }
.pr-paper-n {
  font-family: var(--font-display); font-weight: 400;
  /* "Paper 1" — same reason as .pr-stat-n: the 1 needs its space. */
  font-size: 30px; line-height: 1; letter-spacing: 0;
}
.pr-paper-m {
  /* No margin-left:auto and no nowrap on a shared line: that pair is what
     pushed "105 marks · 120 min" off the edge of its own card. It sits under
     the title in its own row, as a chip, and cannot be clipped by anything. */
  justify-self: start;
  font-family: var(--font-mono); font-size: var(--pr-t-sm);
  color: var(--pr-ink-2);
  background: var(--pr-sunk);
  border: 1px solid var(--pr-rule);
  border-radius: var(--pr-r-pill);
  padding: 3px 10px;
  font-variant-numeric: tabular-nums;
}
.pr-paper-b { display: block; margin-top: 5px; color: var(--pr-ink-2); font-size: var(--pr-t-base); line-height: 1.5; }

/* ---- the clock -----------------------------------------------------------
   Tabular, so the minute digit does not shuffle the row every second. Under
   five minutes it goes red AND gets heavier — colour is never the only
   carrier of a state a student has to act on. */
.pr-clock { flex: 0 0 auto; }
.pr-clock-t {
  font-family: var(--font-mono); font-size: var(--pr-t-lg); font-weight: 600;
  font-variant-numeric: tabular-nums; color: var(--pr-ink);
  letter-spacing: -0.01em;
}
.pr-clock-t.is-low { color: var(--pr-red-ink); font-weight: 700; }
/* A mock's strip carries one more row than a session's, so it gets a little
   more air rather than crowding the clock against the ladder. */
/* A REAL PAPER DOES NOT TELL YOU WHICH SKILL THE QUESTION TESTS.

   The session strip names the skill and its specification reference on
   purpose — that is the vocabulary a student needs, and it is what makes the
   notes link at the foot of the marking sheet mean anything. In a mock both
   are a hint: "Titration calculations, 3.1.2" above a stem tells you what to
   reach for before you have read it, and no exam does that. They come back on
   the result screen, where every question is listed by name, which is where
   the vocabulary is actually useful.

   It is also what makes the strip fit. A mock carries a clock and a section
   chip a session does not, and with all five items the row wrapped twice and
   the strip stood at 156px — nearly a fifth of a phone, permanently, above
   the stem the student is trying to read. */
.pr-strip--mock .pr-spec,
.pr-strip--mock .pr-skill-name { display: none; }

/* THE QUESTION NUMBER, on a mock only.

   AQA prints it in the left margin as one boxed digit per character —
   `0` `1` `.` `1` — on the ruled answer booklet. A row of single-character
   boxes on a screen would be costume: there is no booklet, nothing is being
   written in the boxes, and at 375px four of them eat the row. What carries
   across is that the number is SET APART from the question rather than
   running into it, monospaced and two digits from the start, which is what
   this is. */
.pr-qno {
  flex: none;
  font-family: var(--font-mono); font-size: var(--pr-t-sm); font-weight: 600;
  letter-spacing: 0.06em; color: var(--pr-ink);
  border: 1px solid var(--pr-rule); border-radius: 3px;
  padding: 2px 7px; line-height: 1.25;
}
/* With the skill name gone there is nothing left for the marks to be pushed
   away FROM, and the auto margin left it stranded in the middle of the row. */
.pr-strip--mock .pr-marks { margin-left: 0; }
.pr-strip--mock { padding-bottom: var(--pr-s2); }
/* ITS OWN LINE, and it truncates. "Practical techniques and data analysis ·
   40 marks" is 47 characters; on the skill row at 375px it pushed the skill
   name, the specification reference and the Data sheet button clean off the
   right-hand edge. Measured, not guessed. */
.pr-mocksec {
  flex: 1 1 100%; min-width: 0; max-width: 100%;
  margin-bottom: 5px;
  font-family: var(--font-mono); font-size: var(--pr-micro);
  letter-spacing: var(--pr-track); color: var(--pr-ink-2);
  background: var(--pr-sunk); border-radius: var(--pr-r-pill); padding: 3px 9px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  align-self: flex-start; width: max-content;
}
/* A section the platform could not fill. Said on the paper, not only on the
   result: a student is entitled to know while they are sitting it. */
.pr-mocksec.is-short { color: var(--pr-amber-ink); background: var(--pr-amber-wash); }
/* The rung you have been past, in a mock. No verdict — see MOCK_RUNG in
   session.mjs for why a mock's ladder is never coloured. */
.pr-rung.is-seen { background: var(--pr-ink-3); }

/* the mock's own bar: Back and the one that moves you on */
.pr-bar-back { flex: 0 0 auto !important; }

/* ---- marking the paper --------------------------------------------------- */
.pr-marking { text-align: center; padding: var(--pr-s8) var(--pr-s5); max-width: var(--pr-measure); margin: 0 auto; }
.pr-marking-k { margin: 0 0 var(--pr-s3); }
.pr-marking-t {
  font-family: var(--font-display); font-weight: 400;
  font-size: var(--pr-t-2xl); margin: 0 0 var(--pr-s5);
}
.pr-marking-bar { height: 8px; border-radius: 4px; background: var(--pr-sunk); overflow: hidden; }
.pr-marking-bar span { display: block; height: 100%; background: var(--pr-ink); transition: width 0.2s linear; }
.pr-marking-n { font-family: var(--font-mono); font-size: var(--pr-t-sm); color: var(--pr-ink-3); margin: var(--pr-s2) 0 0; }

/* ---- the result ---------------------------------------------------------- */
.pr-result {
  max-width: var(--pr-measure); margin: var(--pr-s5) auto 0;
  padding: var(--pr-s6) var(--pr-s5);
  border: 1px solid var(--pr-rule); border-radius: var(--pr-r-lg);
  background: var(--pr-sheet); box-shadow: var(--pr-e1);
}
.pr-result-k { margin: 0 0 var(--pr-s2); }
/* Marks out of marks, because that is what an exam gives you. The percentage
   is inside the grade block below, at a quarter of this size. */
.pr-result-score {
  margin: 0 0 var(--pr-s5); line-height: 1;
  font-variant-numeric: tabular-nums; letter-spacing: -0.03em;
}
.pr-result-score b { font-size: clamp(44px, 13vw, 60px); font-weight: 600; }
.pr-result-score span { font-size: clamp(22px, 6vw, 28px); color: var(--pr-ink-3); }
.pr-result-warn {
  margin: 0 0 var(--pr-s4); padding: var(--pr-s3);
  border-left: 3px solid var(--pr-amber);
  background: var(--pr-amber-wash); color: var(--pr-ink-2);
  border-radius: 0 var(--pr-r-sm) var(--pr-r-sm) 0;
  font-size: var(--pr-t-base); line-height: 1.5;
}
.pr-result-h { margin: var(--pr-s7) 0 var(--pr-s3); padding-bottom: var(--pr-s2); border-bottom: 1px solid var(--pr-rule); }

/* THE GRADE, WITH ITS RANGE AND ITS ASSUMPTION.
   Both are required and neither is decoration. AQA publishes boundaries for
   the whole subject and never for one paper, so `assumption` says what the
   grade is an estimate OF and is printed in full. And the A* boundary has
   moved 13 marks in four sittings — wider than some gaps between adjacent
   grades — so when the sittings disagree the RANGE is shown and the headline
   is the cautious end of it. */
.pr-grade {
  margin: 0 0 var(--pr-s5); padding: var(--pr-s4);
  border: 1px solid var(--pr-rule); border-radius: var(--pr-r);
  background: var(--pr-sunk);
}
.pr-grade-band { display: flex; align-items: baseline; gap: var(--pr-s2); flex-wrap: wrap; }
.pr-grade-g {
  font-family: var(--font-display); font-size: var(--pr-t-3xl); line-height: 1;
  color: var(--pr-ink);
}
.pr-grade-to { font-size: var(--pr-t-base); color: var(--pr-ink-3); }
.pr-grade-pc {
  margin-left: auto; font-family: var(--font-mono); font-size: var(--pr-t-lg);
  color: var(--pr-ink-2); font-variant-numeric: tabular-nums;
}
/* The actionable half gets the weight: "four marks off a B" is a target, a
   percentage is only a score. */
.pr-grade-next { margin: var(--pr-s3) 0 0; font-size: var(--pr-t-md); color: var(--pr-ink); }
.pr-grade-next b { font-weight: 600; }
.pr-grade-note {
  margin: var(--pr-s3) 0 0; padding-top: var(--pr-s3);
  border-top: 1px solid var(--pr-rule);
  font-size: var(--pr-t-sm); color: var(--pr-ink-3); line-height: 1.5;
}

.pr-mocksecs { margin: 0 0 var(--pr-s5); }

/* ============================================================================
   MARKING THE PARTS ONLY THE STUDENT CAN MARK.

   Everything a person needs to score themselves honestly, in the order the
   work happens: the question, what they wrote, the mark scheme, then the
   three buttons. It is a <details> so a fifteen-question list is still a list
   — collapsed once marked, open until then, which is the state that says
   "this one is still yours to do" without a badge to say it.
   ========================================================================= */
.pr-selfmark { margin-top: 8px; }
.pr-selfmark > summary {
  cursor: pointer; list-style: none;
  font-family: var(--font-mono); font-size: var(--pr-micro);
  letter-spacing: var(--pr-track); color: var(--pr-ink-2);
  padding: 4px 0;
}
.pr-selfmark > summary::-webkit-details-marker { display: none; }
.pr-selfmark > summary::before {
  content: '▸'; display: inline-block; width: 1em; color: var(--pr-ink-3);
}
.pr-selfmark[open] > summary::before { content: '▾'; }
.pr-selfmark-b {
  border-left: 2px solid var(--pr-rule); padding-left: 12px; margin: 6px 0 4px;
}
/* The stem, reprinted. Slightly quieter than it was on the paper — it is here
   to be checked against, not answered again — but the same size, because a
   formula shrunk to a caption is a formula that gets misread. */
.pr-selfmark-q { color: var(--pr-ink-2); margin-bottom: 10px; }
.pr-selfmark-q svg { max-width: 100%; height: auto; }
/* What they actually typed, in their own words and preserved as they typed
   it: a mark scheme with three points on it is often answered on three lines,
   and reflowing that into a paragraph makes it harder to mark, not tidier. */
.pr-selfmark-a {
  white-space: pre-wrap; margin: 2px 0 12px;
  padding: 8px 10px; border-radius: var(--pr-r-sm);
  background: var(--pr-sunk); color: var(--pr-ink);
}
.pr-selfmark-a.is-blank { color: var(--pr-ink-3); font-style: italic; }

/* The one-pass control. Sticky at the top of the list on a phone, because the
   list is long and the button is the next thing to do after the last mark
   scheme rather than after the first. */
.pr-selfbar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin: 0 0 var(--pr-s5); padding: 12px;
  border: 1px solid var(--pr-rule); border-radius: var(--pr-r-md);
  background: var(--pr-card);
  position: sticky; top: 0; z-index: 3;
}
.pr-selfbar-n {
  font-family: var(--font-mono); font-size: var(--pr-micro);
  letter-spacing: var(--pr-track); color: var(--pr-ink-2);
}
.pr-selfbar .pr-btn { margin-left: auto; }
@media (max-width: 420px) {
  /* At 375px the button and the count share a row only if the count is short;
     it is not, so the button takes its own and goes full width, which is also
     the easier tap target. */
  .pr-selfbar .pr-btn { margin-left: 0; width: 100%; }
}
.pr-qlist { list-style: none; margin: 0; padding: 0; }
.pr-qrow {
  display: flex; align-items: center; gap: var(--pr-s3);
  padding: var(--pr-s2) 0; border-bottom: 1px solid var(--pr-rule);
}
.pr-qrow:last-child { border-bottom: 0; }
.pr-qrow-n {
  flex: 0 0 auto; width: 26px; text-align: right;
  font-family: var(--font-mono); font-size: var(--pr-t-sm); color: var(--pr-ink-3);
  font-variant-numeric: tabular-nums;
}
.pr-qrow-t { flex: 1; min-width: 0; }
.pr-qrow-m { display: block; font-family: var(--font-mono); font-size: 10.5px; color: var(--pr-ink-3); margin-top: 1px; }
.pr-qrow-mk { flex: 0 0 auto; font-size: var(--pr-t-lg); font-weight: 600; }
.pr-qrow.is-ok   .pr-qrow-mk { color: var(--pr-green-ink); }
.pr-qrow.is-part .pr-qrow-mk { color: var(--pr-amber-ink); }
.pr-qrow.is-err  .pr-qrow-mk { color: var(--pr-red-ink); }
.pr-qrow.is-none .pr-qrow-m { color: var(--pr-amber-ink); }
/* A recall question in a mock is waiting for the student, not for us: the
   mark scheme can be shown now the clock has stopped, and the report they
   make IS the mark. Its row grows the three buttons the recall lane already
   uses, at the smaller size a review list can carry. */
.pr-qrow.is-self { align-items: flex-start; }
.pr-qrow.is-self .pr-qrow-n { padding-top: 3px; }
.pr-qself { display: flex; gap: var(--pr-s2); flex-wrap: wrap; margin-top: var(--pr-s2); }
.pr-report-btn--sm { min-height: 40px; padding: 6px 12px; font-size: var(--pr-t-base); flex: 1 1 auto; }

/* ---- the offer, when the server has said no ------------------------------
   402 is a price, not a fault, so this is not the error card. It states the
   line exactly as it is — every question is free, the programme adds the
   review schedule and the timed papers — because on a sixteen-year-old the
   honest sentence is the only one worth making. */
.pr-offer {
  max-width: var(--pr-measure); margin: var(--pr-s6) auto 0;
  padding: var(--pr-s7) var(--pr-s5);
  border: 1px solid var(--pr-rule-2); border-radius: var(--pr-r-lg);
  background: var(--pr-sheet); box-shadow: var(--pr-e1);
}
.pr-offer-k { margin: 0 0 var(--pr-s3); }
.pr-offer-t {
  font-family: var(--font-display); font-weight: 400;
  font-size: var(--pr-t-2xl); line-height: 1.2; margin: 0 0 var(--pr-s3);
}
.pr-offer-t em { font-style: italic; color: var(--pr-red); }
.pr-offer-b { color: var(--pr-ink-2); margin: 0 0 var(--pr-s4); }
.pr-offer-list { margin: 0 0 var(--pr-s4); padding-left: var(--pr-s5); color: var(--pr-ink-2); }
.pr-offer-list li { margin: 0 0 var(--pr-s2); line-height: 1.55; }
.pr-offer-list b { color: var(--pr-ink); font-weight: 600; }
.pr-offer-n { font-size: var(--pr-t-base); color: var(--pr-ink-3); margin: 0 0 var(--pr-s5); }
.pr-offer-n em { font-style: italic; }
.pr-offer-go { display: flex; gap: var(--pr-s2); flex-wrap: wrap; }
.pr-offer-go .pr-btn { flex: 1 1 auto; }

/* ---- nothing here yet ----------------------------------------------------
   A content state, not an error: about half the specification has no
   questions behind it, and two of the three lanes deal nothing today. It has
   to say what was asked for, why it is empty, and where to go instead — and
   it must not be the red error card, or a student will read "broken", try
   again, and get the same screen. */
.pr-nothing {
  margin: var(--pr-s6) auto 0;
  max-width: var(--pr-measure);
  padding: var(--pr-s7) var(--pr-s5);
  border: 1px dashed var(--pr-rule-2);
  border-radius: var(--pr-r-lg);
  background: var(--pr-sheet);
  text-align: center;
}
.pr-nothing-k { margin: 0 0 var(--pr-s3); }
.pr-nothing-t {
  font-family: var(--font-display); font-weight: 400;
  font-size: var(--pr-t-2xl); line-height: 1.2; margin: 0 0 var(--pr-s3);
}
/* Left-aligned inside a centred card: the heading is one line to be seen and
   the body is four lines to be read, and centred body copy makes a reader
   hunt for the start of every line. */
.pr-nothing-b {
  color: var(--pr-ink-2); max-width: 44ch; margin: 0 auto var(--pr-s5);
  font-size: var(--pr-t-md); text-align: left;
}
.pr-nothing-go { display: flex; gap: var(--pr-s2); justify-content: center; flex-wrap: wrap; }
.pr-nothing-go .pr-btn { flex: 1 1 auto; }

/* --------------------------------------------------------------- account */

/* A card is a SURFACE, not an outline. It carried the page's own ground
   inside a hairline, so on the account page the form and the paper it sits on
   were the same colour and the border was doing all the work; at 375px in
   daylight that reads as a stray rule rather than as a panel. */
.pr-card {
  border: 1px solid var(--pr-rule);
  border-radius: var(--pr-r);
  background: var(--pr-sheet);
  box-shadow: var(--pr-e1);
  padding: var(--pr-s5);
  margin: 0 0 var(--pr-s4);
  max-width: 34rem;
}

.pr-field { display: block; margin: 0 0 16px; }
/* THE LABEL, and only the label.
   This was `.pr-field > span`, which is (0,1,1) and therefore beat .pr-readonly
   at (0,1,0) — so the two READ-ONLY values in this form, the student's email
   address and their plan, were painted in the label's own 11px mono
   --pr-ink-3 and rendered as a second label under the first. Measured on the
   rendered page: `write-path-probe@example.invalid` computed to font-size 11px,
   colour oklch(0.52 0.01 250) — pixel for pixel the "EMAIL" above it.
   Targeting .pr-label instead means the rule can only ever style a label, and
   the size, the mono, the tracking and the uppercase all come from §2's one
   sanctioned class rather than being restated here. */
.pr-field > .pr-label { display: block; margin-bottom: 5px; }
/* 16px is not a style choice: any smaller and iOS Safari zooms the page on
   focus, which on a sign-in form throws the layout sideways at the exact
   moment a student is typing. --pr-rule-2, not --pr-rule: a field has to look
   like something you type into, and the hairline is the colour of a divider.
   min-height is the full tap target. */
.pr-input, .pr-select {
  font: inherit;
  font-size: 16px;
  width: 100%;
  min-height: var(--pr-tap);
  padding: 11px 13px;
  border: 1px solid var(--pr-rule-2);
  border-radius: var(--pr-r);
  background: var(--pr-paper);
  color: var(--pr-ink);
}
/* The last hard-coded colour in this file was here: a light-only teal that
   belonged to no palette, loud in dark, and the one focus ring on the platform
   that did not follow the theme. --pr-focus is the token reserved for it. */
.pr-input:focus-visible, .pr-select:focus-visible {
  outline: 2px solid var(--pr-focus);
  outline-offset: 1px;
}
.pr-input--code { font-family: var(--font-mono); font-size: 22px; letter-spacing: 0.3em; text-align: center; }

/* ---- the six-cell code entry ----------------------------------------------
   ONE input, six cells, and the input is the only focusable thing on the card.
   The reasoning is in account.html: six real inputs breaks one-time-code
   autofill, breaks pasting six digits in one go, and gives a screen reader six
   unlabelled fields where there is exactly one value.

   NOT called .pr-code — that class is taken, by the join code tutor.mjs prints
   on the tutor page, and a grid declaration under that name would have quietly
   restyled a screen nothing in this phase touches.

   The input is painted invisible rather than moved off-screen or given
   opacity 0: an opacity-0 input is skipped by some password managers. So the
   text is transparent and the cells behind carry the digits that mirrorCode()
   writes into them.

   THE CARET IS TRANSPARENT TOO, and that is the one thing here that was got
   wrong first and measured second. The obvious build keeps the caret visible
   so the student sees where they are typing. Measured at 375 px, the caret x
   went 188 → 194 → 207 → 221 as the value grew from nothing to five digits,
   while the six cells span 38 → 337: a centred input's caret sits near the
   middle of the ROW whatever is in it, so it pointed at cell 4 when the
   student was filling cell 1, and still pointed at cell 4 when they were
   filling cell 6. It is wrong in every position but one. A screenshot taken
   while it happened to be blinked off shows nothing at all, which is why this
   was measured rather than read.

   So the caret is hidden and the CELL is the cursor: mirrorCode() puts
   [data-next] on the first empty cell and the rule below lights it. That is
   also what every other six-digit code entry on a phone does.

   The focus ring is on the wrapper: an outline on a transparent input would
   draw one box around all six cells and say nothing about which is live. */
.pr-codewrap { position: relative; margin: 0 0 14px; }
.pr-codewrap .pr-input--code {
  position: relative; z-index: 2;
  letter-spacing: 0; text-align: center;
  background: transparent; border-color: transparent;
  color: transparent; caret-color: transparent;
}
.pr-codewrap .pr-input--code:focus-visible { outline: none; }
.pr-codewrap:focus-within {
  outline: 2px solid var(--pr-focus); outline-offset: 1px; border-radius: var(--pr-r);
}
.pr-codecells {
  position: absolute; inset: 0; z-index: 1;
  display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px;
  pointer-events: none;
}
.pr-codecells span {
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono); font-size: 22px; color: var(--pr-ink);
  background: var(--pr-paper); border: 1px solid var(--pr-rule-2);
  border-radius: var(--pr-r-sm);
}
.pr-codecells span[data-empty] { color: var(--pr-ink-3); }
/* The cell being filled — the cursor, since the real caret cannot be trusted
   to land in it. Only while the field has focus: an unfocused card should not
   look like it is waiting for a keystroke. */
.pr-codewrap:focus-within .pr-codecells span[data-next] {
  border-color: var(--pr-focus);
  box-shadow: inset 0 0 0 1px var(--pr-focus);
}

/* A step's name inside a card — "Step 1 · your email".

   NOT .pr-section-title, which is what this first used and which is a CARD
   HEADER: it carries a --pr-sunk band and a bottom rule to the edges of a
   .pr-section, which has overflow:hidden to clip them. Inside a .pr-card,
   whose padding the band does not escape, it rendered as a grey rectangle
   floating in the middle of the sign-in card. Same words, different job. The
   markup still carries .pr-label, which is where the uppercase and the letter
   spacing come from (§7.7) — and this heading is exactly why no chemistry may
   ever be written into one. */
.pr-step-title { display: block; margin: 0 0 var(--pr-s4); }

/* Small print under a control: the alternative route, and what a field is for.
   .pr-note is the same colour and is the card's own prose; this is quieter and
   sits UNDER a control it is about, which is why it is its own class rather
   than a modifier. */
.pr-fine { font-size: 13px; color: var(--pr-ink-3); margin: 10px 0 12px; line-height: 1.45; }

/* A primary action that owns its row. The sign-in card's "Sign in" is the only
   thing on that line and a hugged button reads as one of several options. */
.pr-btn--wide { width: 100%; }

/* A VALUE, not a label. Its own display:block now that the rule above no
   longer supplies one, and --pr-ink so it reads as the answer to the label
   rather than as more label. An email address is prose belonging to a person:
   §7.7 sanctions uppercase on .pr-label alone, and this is emphatically not
   one — ELLIE.MACLEOD@STANDREWS.SCH.UK is not a heading. */
.pr-readonly {
  display: block;
  font-family: var(--font-mono);
  font-size: 14px;
  color: var(--pr-ink);
  overflow-wrap: anywhere;
}
.pr-note { font-size: 14px; color: var(--pr-ink-3); margin: 0 0 14px; }
.pr-note--warn { color: var(--pr-red-ink); }

/* ---- the recent-answers list ----------------------------------------------
   ONE COLUMN, not four in a row.

   It was a single flex row carrying the skill, the answer, the self-mark and
   the day, with the skill pinned at `flex: 0 0 auto` so it could not shrink.
   At 320px that put the page's scrollWidth at 356 — the fold-out-phone
   overflow the screen harness found — and it was the wrong shape anyway: the
   thing you scan this list for is the SKILL, and it was competing for width
   with a raw float like 0.07094017094017095.

   So the skill gets the line, and the three pieces of metadata get a mono line
   under it that wraps. minmax(0, 1fr) rather than a bare 1fr, per §7.8: a grid
   item's automatic minimum is min-content, which is exactly how a long
   unbroken answer string widens a column past the viewport. */
.pr-recent {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 2px;
  padding: 7px 0;
  /* The rows are separated so that two consecutive rows in the SAME state do
     not merge their 3px rules into one continuous bar. Read off the 320px
     render: two amber rows in a row looked like a single block covering both,
     which is a mark spanning two answers rather than one mark each. */
  margin: 0 0 5px;
  font-size: 14px;
}
.pr-recent.is-ok  { border-left: 3px solid var(--pr-green); padding-left: 10px; }
.pr-recent.is-err { border-left: 3px solid var(--pr-red);   padding-left: 10px; }
/* The third state: a recall answer the STUDENT marked. Green and red are
   verdicts we reached; this row is a report they made, and painting it red
   would tell them we marked wrong an answer nobody marked. The same --pr-amber
   the "nearly" verdict and the ladder's ochre rung use, so "self-reported"
   reads the same way everywhere — and it flips for dark now, which the
   hard-coded oklch it replaces did not. */
.pr-recent.is-self { border-left: 3px solid var(--pr-amber); padding-left: 10px; }
/* The skill's own name, in the platform's chemistry class so that §2's case
   guard reaches it: these titles are M<sub>r</sub>, K<sub>c</sub> and ΔG, and
   the list used to print the raw skill id instead. */
.pr-recent .pr-skill-name { font-size: 14.5px; color: var(--pr-ink); }
.pr-recent-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 3px 10px;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--pr-ink-3);
}
.pr-recent-ans { min-width: 0; overflow-wrap: anywhere; }
.pr-recent-self { color: var(--pr-amber-ink); white-space: nowrap; }
.pr-recent-when { margin-left: auto; }

/* ---- deleting your data ----------------------------------------------------
   §7.2 allows no red fill bigger than a chip, and a destructive action still
   has to look destructive. So: a red OUTLINE on the card and a red WORD on the
   buttons, and no slab anywhere. Both tokens flip with the theme.

   The buttons wrap their own text here — .pr-btn is white-space: nowrap
   everywhere else, and "Clear progress on this device" is 29 characters inside
   a card inside a 320px gutter, which is the one place on the platform where a
   label genuinely cannot fit on one line. */
.pr-danger { border-color: var(--pr-red); }
.pr-danger .pr-btn { border-color: var(--pr-red); color: var(--pr-red-ink); white-space: normal; }
.pr-danger-actions { display: flex; flex-wrap: wrap; gap: 10px; }

/* ----------------------------------------------------------------- tutor */

.pr-student {
  border: 1px solid var(--pr-rule);
  border-radius: var(--pr-r);
  padding: 16px 18px;
  margin: 0 0 12px;
}
.pr-student-head { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; flex-wrap: wrap; }
.pr-student-head h3 { margin: 0; font-size: 17px; }
.pr-student-meta { font-family: var(--font-mono); font-size: 12px; color: var(--pr-ink-3); }
.pr-student-week { margin: 8px 0 0; font-size: 15px; }
.pr-quiet { color: var(--pr-ink-3); }
.pr-student-weak { margin-top: 12px; }
.pr-student-weak h4 {               /* markup carries .pr-label — see §2 */
  margin: 0 0 6px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--pr-ink-3);
}
/* The three skills a student is weakest at, and the skills a cohort is stuck
   on. Both lists carry chemistry — "Moles from mass and M<sub>r</sub>",
   "K<sub>c</sub> calculations" — so both wrap the title in .pr-skill-name,
   which §2's case guard already immunises and already welds the sub/sup to its
   atom. Nothing new in the guard; the class it already has is the right one. */
.pr-student-weak ul,
.pr-reteach ul { margin: 0; padding-left: 18px; font-size: 15px; }
.pr-student-weak li,
.pr-reteach li { margin: 0 0 4px; line-height: 1.45; }
.pr-reteach li:last-child, .pr-student-weak li:last-child { margin-bottom: 0; }
.pr-code { font-family: var(--font-mono); font-size: 20px; letter-spacing: 0.2em; }

/* --------------------------------------------------------------- utility */

/* Two shapes the account and tutor pages were writing as inline `style=`
   attributes — `style="margin-top:14px"` on three notes and
   `style="list-style:none;margin:0;padding:0"` on the recent-answers list. All
   reusable CSS belongs in this file, and an inline style is also the one thing
   a stylesheet cannot theme, restyle or override. */
.pr-after { margin: 14px 0 0; }
.pr-plainlist { list-style: none; margin: 0; padding: 0; }

.pr-empty { color: var(--pr-ink-3); font-size: 15px; padding: 14px 0; }
.pr-empty--error { color: var(--pr-red-ink); }

.pr-toast {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translate(-50%, 20px);
  background: var(--pr-ink);
  color: var(--pr-paper);
  font-size: 14px;
  padding: 11px 18px;
  border-radius: var(--pr-r);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease, transform 0.18s ease;
  z-index: 60;
  max-width: min(440px, calc(100vw - 32px));
  text-align: center;
}
.pr-toast.is-on { opacity: 1; transform: translate(-50%, 0); }
/* An error toast — "could not reach the marker", "that code has expired". It
   was a hard-coded dark red carrying --pr-paper text, which in dark meant
   near-black text on a dark red slab and the message could not be read at
   all. Both halves are on the token layer now and both flip with the theme.

   Recorded rather than glossed: this is a red area bigger than a chip, which
   §7.2 forbids for MARKS — the second one on the platform, after
   .pr-opt.is-err, which Task 6a flagged for the same ruling. A toast is not a
   mark: it says the SYSTEM failed, never that the student did, and it is
   never shown against an answer. Task 12 may still want it demoted to the
   neutral ink slab with a 3px red rule; the reason it is not is that an
   error a student has to act on should not look like "Saved.". */
/* AN ERROR TOAST IS NOT A MARK, so it does not get marking's colour.

   This filled the whole pill with --pr-red — oklch(0.52 0.175 28), the fully
   saturated one — measured at 188x87, so 16,324px² of solid red, and in dark
   mode a bright salmon block that was the only saturated colour on the page.
   §7.2 allows one red and never a fill larger than a chip.

   It also claimed the wrong thing. Red on this platform means a mark has been
   made; a toast saying the connection dropped is not a verdict on the student.
   So the pill keeps the ordinary ink background and takes §7.2's third
   grammar instead — a 3px rule — which says "look at this" without saying
   "you were marked". The sentence carries the meaning; the edge points at it.

   NOT to be confused with .pr-opt.is-err, which was reported alongside this
   and is NOT a breach: its background is --pr-red-wash, oklch(0.972 0.020 28)
   against paper at oklch(0.972 0.006 80) — identical lightness and a chroma of
   0.020. That is a tint, not a fill, and the file argues for it two hundred
   lines up. Area was measured there; saturation was not. */
.pr-toast--error {
  border-left: 3px solid var(--pr-red);
  padding-left: 15px;   /* 18 − 3, so the text does not shift when the rule appears */
}

.pr-footer {
  border-top: 1px solid var(--pr-rule);
  margin-top: 60px;
  padding: 20px var(--pr-gutter);   /* the page's gutter, not the site's */
  font-size: 13px;
  color: var(--pr-ink-3);
  text-align: center;
}
.pr-footer a { color: var(--pr-ink-2); }

@media (prefers-reduced-motion: reduce) {
  /* The two disclosure markers rotate on open, which is the same 140ms and the
     same reason. .pr-worked's was missed when this block was written; adding
     .pr-topic-caret beside it and leaving its sibling out would have been an
     arbitrary line. */
  /* Everything this redesign added that moves is here too, and the list is
     kept explicit rather than becoming `* { transition: none }` — a blanket
     rule also kills the transitions a reduced-motion user still benefits from
     (a colour crossfade is not motion) and hides the next one that is added.

     .pr-bar is the one that matters most: the verdict bar slides out of the
     way when the marking sheet's own actions are on screen, and a slide under
     a thumb is exactly the kind of movement this setting exists to stop. It
     still stands down — it simply does it instantly. */
  .pr-toast, .pr-progress span, .pr-btn,
  .pr-topic-caret, .pr-worked > summary::before,
  .pr-bar, .pr-lane, .pr-paper, .pr-marking-bar span { transition: none; }

  /* The shell and the rebuilt screens, added to the same explicit list rather
     than to a blanket `*`. A colour crossfade is not motion and a
     reduced-motion student still benefits from it; a bar that grows, an arc
     that sweeps and a rung that pops are motion, and they stop. */
  .pr-meter span, .pr-arc-fill, .pr-rungs i, .pr-cell { transition: none; }

  /* The drawn mark is DRAWN INSTANTLY, not removed. The tick is the verdict's
     only glyph — suppressing the animation without settling the dashoffset
     would leave a student who asked for less motion with no mark at all. */
  .pr-ink-draw { animation: none; stroke-dashoffset: 0; }
  .pr-verdict, .pr-rungs i.is-new { animation: none; }
}

/* The header sheds the brand in two steps and the sign-in corner in one, and
   every breakpoint is measured rather than guessed. Rendered widths, read off
   the pages themselves at 375px:

     brand, mark + name + tag ....... 278px
     brand, mark + tag .............. 119px
     brand, mark alone ...............  38px
     theme control ...................  68px  ("System" — the widest word)
     one site link ................... 91px  ("Revision notes"; "Dashboard" 68)
     the sign-in corner, SIGNED OUT ..  64px  ("Sign in")
     the sign-in corner, SIGNED IN ... 214px  (a 16-char name 134 + gap 8 +
                                               "Sign out" 72)
     gap between brand and nav ....... 12px (8 below 640)
     gutter .......................... clamp(16px, 5vw, 32px), both sides

   THE SECOND-TO-LAST LINE IS THE ONE THAT WAS MISSING. This block used to read
   "the nav's three controls ... 233px", and 233 is the SIGNED-OUT total. Signed
   in the corner alone is 214, the nav is 389, and there are 337 to put it in —
   so the nav wrapped, and because every control is --pr-tap tall a wrapped nav
   is 104px. Measured on all four pages at 375px signed in: a 123px sticky
   header, the theme control and the site link on one line, the brand mark
   alone level with the line below it, and the name flush against Sign out.
   Signed out the same header is 67px and one line. Signed in on a phone is the
   state a paying student is in every day, so the breakpoints are set from THAT
   total from here on.

   278 + 12 + 233 + gutter needs ~590px, so the NAME goes below 640. 119 + 12 +
   233 + gutter needs ~405px, so the TAG goes below 420. Between the two the tag
   does the prototype's job: it says where you are in a third of the width.

   Below 640 the corner sheds too, and it sheds down to what the design review's
   own verified header carries: `Dark | Notes | Account` — no name, no Sign out
   (mock-progress.html, read back at 375px in shot-progress-phone.png). The name
   becomes the word "Account", 59px in mono against 134, and Sign out goes
   because it is the one control here that is administrative rather than
   navigational — it is a once-a-term action, and account.html has carried its
   own Sign out button since it was written, which is where the header now sends
   you.

   Re-measured after the change rather than predicted: the nav is 234px on the
   dashboard (theme 68 + 8 + "Revision notes" 91 + 8 + "Account" 59) and 211px
   where the link is "Dashboard". With the mark and the two gaps that is 318 of
   375, and 312 of 320 — eight pixels spare at the narrowest, which is why
   flex-wrap stays on .pr-header-nav as the safety valve: a wrapped nav is ugly
   and a horizontal scrollbar on a phone is a defect the screen harness fails
   on. All four pages read headerH 67 and one line at both widths, signed in and
   signed out.

   The mark still links home and the anchor carries an aria-label either way, so
   the accessible name stays "Whiteboard Chemistry Practice" at every width. It
   is only pixels that go. */
@media (max-width: 640px) {
  .pr-header-inner { gap: 8px; }
  .pr-brand-name { display: none; }
  .pr-whoami-name { display: none; }
  .pr-whoami-short { display: inline; }
  /* Scoped to the header so it beats .pr-btn's own display, which is set at
     the same specificity earlier in the file. */
  .pr-header-nav [data-pr-signout] { display: none; }
}

@media (max-width: 560px) {
  /* The stem's own step-down used to live here as `.pr-q-stem { font-size:
     17px }`. §6's .pr-stem takes its size from --pr-stem, which is
     clamp(17px, 1rem + 0.5vw, 19.5px) — 17.9px at 375px and 17px at the
     narrowest width there is — so the breakpoint would only ever fight the
     clamp. Removed rather than re-pointed at .pr-stem. */
  .pr-resume-actions { width: 100%; }
  .pr-resume-actions .pr-btn { flex: 1; }
}

@media (max-width: 420px) {
  .pr-brand-tag { display: none; }
}

/* ---- the wider screens ---------------------------------------------------
   The phone is the design case and everything above is drawn for it, so these
   are the two places where a laptop's extra width buys something rather than
   just stretching a column: the three figures line up as three, and the three
   skills worth your attention sit side by side instead of as a stack a
   desktop student has to scroll past to reach the map.

   The specification map stays ONE COLUMN at every width, deliberately (§7.6):
   two columns leave a hole the height of an open <details> beside them, and
   one column means the desktop and the phone are the same object rather than
   two designs to keep in step. */
@media (min-width: 620px) {
  .pr-stats { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  /* Four across: no span and no reordering — the row is full either way. */
  .pr-stat--wide { grid-column: auto; order: 0; }
  /* The action stops spanning the card and sits at its natural width, where
     there is now room for it. */
  .pr-today-go { width: auto; }
  .pr-lanes { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* The rings go beside the headline rather than under it. 900px is where the
   text column still holds its 46ch after 88px of arc and its gutter come out
   of the width; below it they stack, and below 420 they shrink rather than
   wrap, because three arcs on one line IS the reading. */
@media (min-width: 900px) {
  .pr-today { grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
  .pr-today { padding: var(--pr-s7) var(--pr-s7); }
}
@media (max-width: 419px) {
  .pr-arcs { gap: var(--pr-s4); }
  .pr-arc svg { width: 70px; height: 70px; }
}

@media (min-width: 760px) {
  /* The three skills used to become three side-by-side CARDS here. They are
     rows in a section card now, so the only thing width buys them is the
     actions coming off their own line — which is done where .pr-weak is
     defined, beside the rest of the row. Three columns of one-line rows would
     be a table pretending to be tiles. */
  .pr-nothing-go, .pr-offer-go, .pr-done-actions { justify-content: flex-start; }
  .pr-nothing-go .pr-btn, .pr-offer-go .pr-btn, .pr-done-actions .pr-btn { flex: 0 0 auto; }
  .pr-papers { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}


/* ===================================================== 18. THE DATA SHEET ===
   The one exact copy permitted anywhere on this platform (Joe, 2026-08-20).
   ONE shared asset — assets/exam-questions/aqa-a-level-chemistry/
   data-booklet.html — fetched on demand by app/data-sheet.mjs and styled here,
   because it arrives as bare markup carrying .wcqg-* and .rn-* classes.

   That is not a breach of the walled garden: practice.css already styles ten
   question-bank classes for the same reason. The rule forbids LOADING site.js
   and revision.css, not rendering shared content.                            */

.pr-sheetpanel {
  position: fixed;
  inset: 0;
  z-index: 80;
  background: var(--pr-paper);
  display: flex;
  flex-direction: column;
}
.pr-sheetpanel[hidden] { display: none; }
.pr-sheetpanel-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 10px var(--pr-gutter);
  border-bottom: 1px solid var(--pr-rule);
  background: var(--pr-card);
  flex: 0 0 auto;
}
/* Not a .pr-label — §7.7 allows one uppercase class and it may never hold
   chemistry, and the tabs right below it carry 1H and 13C. */
.pr-sheetpanel-title { font-size: 15px; font-weight: 600; color: var(--pr-ink); }
.pr-sheetpanel-body {
  flex: 1;
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  padding: 12px var(--pr-gutter) 40px;
}

/* ── the tabs ─────────────────────────────────────────────────────────────
   Six of them; they wrap onto three rows at 375px. Scrolling them sideways
   would hide half the sheet behind a gesture nobody discovers. */
/* ONE ROW THAT SCROLLS, not three rows that wrap.
   Six tabs wrapped onto three lines at 375px and took 230px of the panel
   before the sheet itself began — on the screen a student opens mid-question
   to look one number up. A scroller keeps the whole switch to 44px and puts
   the periodic table above the fold.

   -webkit-overflow-scrolling and the negative gutter so the row can bleed to
   the panel's edges, which is what tells a thumb there is more to the right. */
.pr-book .wcqg-book-tabs {
  display: flex;
  flex-wrap: nowrap;
  gap: 6px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  margin: 0 calc(-1 * var(--pr-gutter)) var(--pr-s3);
  padding: 0 var(--pr-gutter) var(--pr-s3);
  border-bottom: 1px solid var(--pr-rule);
}
.pr-book .wcqg-book-tabs::-webkit-scrollbar { display: none; }
.pr-book .wcqg-book-tabs .rn-cap-btn {
  font: inherit;
  font-size: var(--pr-t-base);
  line-height: 1.2;
  padding: 8px 12px;
  min-height: 40px;
  flex: 0 0 auto;
  white-space: nowrap;
  border: 1px solid var(--pr-rule);
  border-radius: var(--pr-r-pill);
  background: var(--pr-sheet);
  color: var(--pr-ink-2);
  cursor: pointer;
}
.pr-book .wcqg-book-tabs .rn-cap-btn[aria-selected="true"] {
  background: var(--pr-ink);
  border-color: var(--pr-ink);
  color: var(--pr-paper);
}
/* a sup in a tab ("Table B · 1H NMR") must not open the line */
.pr-book .wcqg-book-tabs sup { font-size: .7em; line-height: 0; vertical-align: super; }

.pr-book .wcqg-book-pane[hidden] { display: none; }
.pr-book figure { margin: 0; }
.pr-book img, .pr-book svg { max-width: 100%; height: auto; display: block; }

/* ── the tables ───────────────────────────────────────────────────────────── */
/* The booklet is a PAGE, and a page has a measure. Unconstrained, its
   two-column shift tables put "Type of proton" at x=270 and "δ / ppm" at
   x=1080 with 800px of nothing between them — which is not what the sheet on
   the desk looks like and not readable as a table. */
.pr-book .wcqg-book-pane { max-width: 960px; margin-inline: auto; }
.pr-book .wcqg-book-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.pr-book .wcqg-book-table caption {
  text-align: left; font-size: 14px; font-weight: 600;
  color: var(--pr-ink); padding-bottom: 8px;
}
.pr-book .wcqg-book-table th,
.pr-book .wcqg-book-table td {
  border-bottom: 1px solid var(--pr-rule);
  padding: 7px 8px; text-align: left; vertical-align: top;
}
.pr-book .wcqg-book-table th { font-weight: 600; color: var(--pr-ink); }
.pr-book .wcqg-book-table td { color: var(--pr-ink-2); }
/* All three of these tables are two columns and the second is always a numeric
   range — 3300–3500, 0.5–5.0, 5–40. At 375px "10.0–12.0" broke across two
   lines and read as two numbers. Checked across all 34 data rows; scoped to
   this class so it cannot reach the amino-acid or structures panes. */
.pr-book .wcqg-book-table td:last-child { white-space: nowrap; }

/* ── Enlarge ──────────────────────────────────────────────────────────────
   The periodic table is A4 landscape. It fits a 375px column only by being
   scaled to about 5px element symbols, so this is not a refinement: without it
   a student cannot read an A_r off the pane at all. The booklet's own button
   hands off to window.rnZoomFigure, which is site.js.

   No pinch-zoom implementation — the image is rendered at a fixed large width
   inside `overflow: auto` and the browser pans and pinches it. No gesture
   arithmetic, identical with a finger, a mouse and a keyboard.               */
/* ENLARGE IS THE POINT OF THE PERIODIC TABLE PANE ON A PHONE.
   The table renders at 375px wide, where a relative atomic mass is about four
   pixels tall — the pane is unusable without this button, and it was a 13px
   ghost control below the fold. It is now the loudest thing in the pane and it
   sits ABOVE the image, where a thumb finds it before scrolling past the very
   thing it cannot read. */
.pr-book .wcqg-book-zoom {
  order: -1;
  display: inline-flex; align-items: center; gap: 7px;
  margin: 0 0 var(--pr-s3);
  font: inherit; font-size: var(--pr-t-base); font-weight: 500;
  padding: 10px 16px; min-height: var(--pr-tap);
  border: 1px solid var(--pr-ink); border-radius: var(--pr-r);
  background: var(--pr-ink); color: var(--pr-ink-flip);
  cursor: pointer;
}
/* order:-1 only bites inside a flex container, and the button lives in the
   <figure> beside the image (data-sheet.mjs finds it with closest('figure'),
   so it must stay there). The figure becomes the flex column.

   align-items STRETCH, not flex-start: an <img> with max-width:100% and no
   width in a flex column shrink-wraps to its intrinsic size, and the first
   attempt at this shrank the periodic table to 185px — smaller than the
   problem it was added to solve. The button gets align-self instead. */
.pr-book .wcqg-book-pane figure { display: flex; flex-direction: column; }
.pr-book .wcqg-book-zoom { align-self: flex-start; }
.pr-bookzoom {
  position: fixed; inset: 0; z-index: 90;
  background: var(--pr-paper);
  display: flex; flex-direction: column;
}
.pr-bookzoom[hidden] { display: none; }
.pr-bookzoom-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 10px var(--pr-gutter);
  border-bottom: 1px solid var(--pr-rule);
  background: var(--pr-card);
}
.pr-bookzoom-hint { font-size: 13px; color: var(--pr-ink-3); }
.pr-bookzoom-scroll { flex: 1; overflow: auto; -webkit-overflow-scrolling: touch; }
/* The intrinsic width is what makes it legible; the container scrolls. */
.pr-bookzoom-scroll img { display: block; width: 1100px; max-width: none; height: auto; }

/* The control that opens it, on the paper strip. Quiet, and on every question:
   always available, never forced — which is what a desk does. */
/* The Data Sheet is on the desk in the exam, so it is on the desk here — and
   it has to look like the one thing on the strip you can press. It was a
   26px mono outline identical to the marks chip beside it; now it is a
   filled, 34px control with a rule icon, and the marks chip is plain text.
   Still small: it is a reference you reach for, not the action of the page. */
.pr-datasheet-btn {
  display: inline-flex; align-items: center; gap: 6px;
  font: inherit; font-size: var(--pr-t-sm);
  font-weight: 500;
  padding: 5px 11px; min-height: 34px;
  border: 1px solid var(--pr-rule-2); border-radius: var(--pr-r-pill);
  background: var(--pr-sheet); color: var(--pr-ink-2);
  cursor: pointer; white-space: nowrap;
}
.pr-datasheet-btn::before {
  content: ''; flex: 0 0 auto; width: 11px; height: 13px;
  border: 1.5px solid currentColor; border-radius: 1.5px;
  /* three ruled lines inside the little page */
  background-image: linear-gradient(currentColor 1px, transparent 1px);
  background-size: 100% 3.2px; background-position: 0 2px;
  background-repeat: repeat-y; opacity: 0.85;
}
.pr-datasheet-btn:hover { color: var(--pr-ink); border-color: var(--pr-ink-3); }


/* ================================================ 19. THE TOPIC PICKER =====

   The screen's job changed and the furniture had to follow it. It was a
   specification AUDIT that permitted practice from inside an accordion — the
   only control that started revision was a ghost button three taps deep, which
   made the least prominent object on the page the one everybody came for.

   It is now a PICKER that shows progress: the row selects, the caret opens,
   and a bar at the bottom says what has been chosen and starts it. The
   progress cells did not move and did not change; they are what tells a
   student which topics are worth ticking.
   ========================================================================= */

/* The platform's general visually-hidden class. The equation lane had its own
   (.pr-eq-op-sr) written before there was a shared one; that stays where it is
   rather than being renamed under a lane this section has no business
   touching, and everything new uses this. */
.pr-sr {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.pr-mapnote { margin: 0 0 var(--pr-s3); }

/* THE SEARCH. Thirty-four rows is more than a thumb should scroll to reach
   Thermodynamics, and the list is in specification order — which is the right
   order and is not the order anyone looks things up in. */
.pr-mapsearch { display: block; margin: 0 0 var(--pr-s3); }
.pr-mapsearch .pr-input { width: 100%; }
/* Safari draws its own clear affordance and then a second one from the
   appearance reset, which lands two crosses on top of each other. */
.pr-mapsearch input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; }

/* ---- the selection bar ---------------------------------------------------

   Fixed ABOVE the tab bar, never over it: the tab bar is how a student leaves,
   and a control that covered the way out would trap anyone who ticked a row by
   accident. Hidden at zero selected, because a bar that permanently reads
   "0 topics" is 90px of nothing at the bottom of a phone.                   */
.pr-selbar {
  position: fixed;
  inset: auto 0 var(--pr-tabbar-h) 0;
  z-index: 41;                 /* one above .pr-tabbar's 40 */
  background: color-mix(in oklch, var(--pr-sheet) 94%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-top: 1px solid var(--pr-rule);
  /* Lifts it off the tab bar so the two read as two objects rather than one
     tall one. */
  box-shadow: 0 -6px 20px -12px oklch(0 0 0 / 0.45);
}
.pr-selbar-in {
  max-width: var(--pr-measure-wide, 60rem);
  margin: 0 auto;
  padding: var(--pr-s2) var(--pr-s4);
  display: grid;
  gap: var(--pr-s2);
}
/* Line one: what is chosen, and the way out of it. */
.pr-selbar-say { display: flex; align-items: center; justify-content: space-between; gap: var(--pr-s3); }
.pr-selbar-n {
  margin: 0;
  font-family: var(--font-mono); font-size: var(--pr-t-sm); color: var(--pr-ink-2);
  font-variant-numeric: tabular-nums;
}

/* The length ladder. Four taps rather than a <select>, which on a phone opens
   a full-screen wheel for a choice between four numbers. */
.pr-lens { display: flex; gap: var(--pr-s1); }
.pr-len {
  appearance: none; cursor: pointer;
  min-width: 40px; height: 34px; padding: 0 var(--pr-s2);
  border: 1px solid var(--pr-rule-2); border-radius: var(--pr-r-sm);
  background: var(--pr-sheet); color: var(--pr-ink-2);
  font-family: var(--font-mono); font-size: var(--pr-t-sm);
  font-variant-numeric: tabular-nums;
  transition: background var(--pr-speed) ease, color var(--pr-speed) ease,
              border-color var(--pr-speed) ease;
}
.pr-len:hover { border-color: var(--pr-ink-3); color: var(--pr-ink); }
/* AFTER the hover rule and at higher specificity, deliberately: a pressed
   button that is also hovered must stay pressed. This platform has been bitten
   by an [aria-pressed] rule losing to a later one before. */
.pr-len[aria-pressed="true"],
.pr-len[aria-pressed="true"]:hover {
  background: var(--pr-ink); border-color: var(--pr-ink); color: var(--pr-ink-flip);
}

/* Line two: the two actions. Start takes the room, because it is what the bar
   is for; Clear is a ghost and sized to its words. */
.pr-selbar-do { display: flex; gap: var(--pr-s2); align-items: center; }
.pr-selbar-do .pr-btn--primary { flex: 1 1 auto; }
.pr-selbar-clear { flex: 0 0 auto; }

/* On a phone the two lines stack as written. From 560px there is room to put
   the ladder and the buttons on ONE line, which takes the bar from ~96px to
   ~60px and gives four more rows of the list back. */
@media (min-width: 560px) {
  .pr-selbar-in {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
  }
  .pr-selbar-say { justify-content: flex-start; gap: var(--pr-s4); }
  .pr-selbar-do .pr-btn--primary { flex: 0 0 auto; min-width: 8rem; }
}

/* With the tab bar gone at 1024px the bar sits on the floor. */
@media (min-width: 1024px) {
  .pr-selbar { bottom: 0; }
}

/* NOTHING MUST SIT UNDER THE BAR, AND THE CLEARANCE GOES ON THE FOOTER.

   The obvious fix is to pad .pr-page while anything is selected, and it is the
   wrong one twice over: it opens a visible gap between the last topic row and
   the footer, and it still leaves the FOOTER — which comes after the page —
   underneath the bar. Measured with two topics ticked, the footer sat at
   631–748 with the bar's top edge at 645.

   Padding the last element in the flow instead clears everything above it and
   costs nothing visible, because the space it adds is below the last thing
   anyone reads. The tab bar's own clearance is already there; this adds the
   bar's height on top of it while the bar exists. */
body.has-selbar .pr-app .pr-footer { padding-bottom: calc(var(--pr-tabbar-h) + 108px); }
@media (min-width: 560px) {
  body.has-selbar .pr-app .pr-footer { padding-bottom: calc(var(--pr-tabbar-h) + 72px); }
}

/* ---- the level control ---------------------------------------------------

   Beside the list rather than on the start bar, because what it can deliver
   depends on which topics are ticked — see the markup in index.html. It reuses
   .pr-len so a pressed level and a pressed length are the same object in two
   places; .pr-lvl only relaxes the width, because "Standard" is not a number
   and must not be squeezed into a number's box.                             */
.pr-level { margin: 0 0 var(--pr-s4); display: grid; gap: var(--pr-s2); }
.pr-level-lab { color: var(--pr-ink-3); }
.pr-level-opts { display: flex; flex-wrap: wrap; gap: var(--pr-s1); }
.pr-lvl { min-width: 0; padding: 0 var(--pr-s3); font-family: var(--font-body); }

/* The honest line. It is prose and not a warning: on more than half the course
   the answer is "some of these topics have nothing harder yet", which is a
   fact about what has been written rather than a fault to flag in red. */
.pr-level-note { margin: 0; }

/* ---- Tick the points (launch design L17), built to the TickPoints artboard ---
   .pr-pt, not .pr-tick: .pr-tick is the topic row's tick face and was taken.
   The box is drawn at 22px inside a 44px target; the row is the tap target
   too, wired in session.mjs. The point's HTML sits inside .pr-sheetnote, so
   the case guard reaches it. */
.pr-pts { list-style: none; margin: 0 0 var(--pr-s3); padding: 0; display: grid; gap: var(--pr-s2); }
.pr-pt {
  display: grid; grid-template-columns: 44px minmax(0, 1fr) auto; align-items: start; gap: var(--pr-s2);
  padding: 6px 10px 6px 4px; border: 1px solid var(--pr-rule); border-radius: var(--pr-r-sm);
  background: var(--pr-sheet); cursor: pointer;
}
.pr-pt--on { border-color: var(--pr-green); background: var(--pr-green-wash); }
.pr-pt-box {
  width: 44px; height: 44px; margin: -6px 0; padding: 0; border: 0; background: transparent;
  display: grid; place-items: center; cursor: pointer; color: transparent;
}
.pr-pt-box::before {
  content: ""; grid-area: 1 / 1; width: 22px; height: 22px; box-sizing: border-box;
  border: 1.5px solid var(--pr-rule-2); border-radius: var(--pr-r-sm); background: var(--pr-paper);
}
.pr-pt-box[aria-checked="true"]::before { border-color: var(--pr-green); background: var(--pr-green-wash); }
.pr-pt-box[aria-checked="true"] { color: var(--pr-green); }
.pr-pt-glyph { grid-area: 1 / 1; font-size: 15px; line-height: 1; font-weight: 700; }
.pr-pt-box:focus-visible { outline: 2px solid var(--pr-focus); outline-offset: 2px; }
.pr-pt-box:disabled { cursor: default; opacity: 0.7; }
.pr-pt-text { font-size: var(--pr-t-base); line-height: 1.45; padding-top: 4px; }
.pr-pt-m { font-family: var(--font-mono); font-size: var(--pr-t-sm); color: var(--pr-ink-3); padding-top: 6px; }
.pr-pts-lead { margin: 0 0 var(--pr-s2); }
.pr-pts-go { display: grid; gap: var(--pr-s2); justify-items: start; padding: var(--pr-s3) 0 0; }
/* The mark-count prediction row (L26): 0..N as equal buttons. */
.pr-predict { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: var(--pr-s2); }
.pr-predict-n { min-height: 44px; padding: 8px 4px; }

/* ---- The free meter (launch design §3), built to the canvas's meterCard ------
   .pr-quota, not .pr-meter: that name is the mock's marks bar. */
.pr-quota { display: grid; gap: var(--pr-s2); margin: 0 0 var(--pr-s5); padding: var(--pr-s4); background: var(--pr-sheet); border: 1px solid var(--pr-rule); border-radius: var(--pr-r); }
.pr-quota-lead { margin: 0; font-size: var(--pr-t-base); }
.pr-quota-t { margin: 0; font-size: var(--pr-t-md); font-weight: 600; }
.pr-quota-bar { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 3px; height: 8px; }
.pr-quota-c { border-radius: 2px; background: var(--pr-m0); }
.pr-quota-c--on { background: var(--pr-m4); }
.pr-quota-f { margin: 0; }

/* ---- The Full card, built to the canvas's fullCard ---------------------------
   The rail's ink, in both themes; the red on ink is --pr-red-on-ink. */
.pr-full {
  display: grid; gap: var(--pr-s4); margin: 0 0 var(--pr-s5); padding: var(--pr-s6);
  border-radius: var(--pr-r-lg); background: var(--pr-rail); color: oklch(0.962 0.004 80);
  box-shadow: var(--pr-e2);
}
.pr-full-k { margin: 0; color: var(--pr-ink-rail); }
.pr-full-h { margin: 0; font-family: var(--font-display); font-size: 28px; line-height: 1.1; font-weight: 400; }
.pr-full-h em { font-style: italic; color: var(--pr-red-on-ink); }
.pr-full-list { margin: 0; padding: 0; list-style: none; display: grid; gap: var(--pr-s2); font-size: var(--pr-small); color: oklch(0.86 0.008 80); }
.pr-full-list li { padding-left: var(--pr-s4); position: relative; }
.pr-full-list li::before { content: ""; position: absolute; left: 0; top: 0.6em; width: 5px; height: 5px; border-radius: 50%; background: var(--pr-red-on-ink); }
.pr-full-go { display: flex; flex-wrap: wrap; gap: var(--pr-s3); align-items: center; }
.pr-full-or { color: var(--pr-ink-rail); }
.pr-btn--onink { background: var(--pr-on-ink); border-color: var(--pr-on-ink); color: var(--pr-rail); }
.pr-btn--onink:hover:not(:disabled) { background: var(--pr-on-ink); border-color: var(--pr-on-ink); color: var(--pr-rail); opacity: 0.92; }

/* The trial line under the start button (L29). */
.pr-today-trial { margin: var(--pr-s3) 0 0; font-size: var(--pr-t-sm); opacity: 0.85; }

/* A chip: "Full plan" on a locked paper or lane. */
.pr-chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: var(--pr-r-pill); background: var(--pr-sunk); border: 1px solid var(--pr-rule); font-size: var(--pr-t-sm); color: var(--pr-ink-2); vertical-align: middle; }
.pr-chip--red { background: var(--pr-red-wash); border-color: var(--pr-red); color: var(--pr-red-ink); }

/* The cap screens: the "still yours" card and the papers gate. */
.pr-capped { display: grid; }
.pr-capped--end { margin-top: var(--pr-s5); }
.pr-still-list { margin: 0 0 var(--pr-s3); padding-left: 1.2em; display: grid; gap: var(--pr-s2); font-size: var(--pr-small); color: var(--pr-ink-2); }
.pr-still-go { display: flex; flex-wrap: wrap; gap: var(--pr-s3); }
.pr-papers-lead { margin: 0 0 var(--pr-s4); }
.pr-paper--locked { background: var(--pr-sunk); }
.pr-paper--locked .pr-paper-go { opacity: 0.45; }
.pr-plan-up { display: flex; flex-wrap: wrap; gap: var(--pr-s3); align-items: center; margin: 0 0 var(--pr-s4); }
.pr-plan-up .pr-fine { flex-basis: 100%; margin: 0; }

/* ---- Pills (the canvas's .pr-pills), the scope bar and the lane filter step --- */
.pr-pills { display: flex; gap: var(--pr-s2); flex-wrap: wrap; }
.pr-pill {
  appearance: none; cursor: pointer; min-height: 34px; padding: 6px 14px;
  border: 1px solid var(--pr-rule-2); border-radius: var(--pr-r-pill);
  background: var(--pr-sheet); color: var(--pr-ink-2); font: inherit; font-size: var(--pr-t-sm);
}
.pr-pill:hover { border-color: var(--pr-ink-3); color: var(--pr-ink); }
.pr-pill.is-on, .pr-pill.is-on:hover { background: var(--pr-ink); border-color: var(--pr-ink); color: var(--pr-ink-flip); }
.pr-pill:focus-visible { outline: 2px solid var(--pr-focus); outline-offset: 2px; }
.pr-pill--add { border-style: dashed; color: var(--pr-ink-3); }
.pr-scope { display: grid; gap: var(--pr-s2); margin: 0 0 var(--pr-s4); padding: var(--pr-s4); background: var(--pr-sheet); border: 1px solid var(--pr-rule); border-radius: var(--pr-r); }
.pr-scope-row { display: flex; align-items: center; gap: var(--pr-s3); flex-wrap: wrap; }
.pr-scope-f { margin: 0; }
/* The tile is a box now, with a button face and an optional filter panel. */
.pr-lane-face { display: grid; align-content: start; gap: var(--pr-s2); text-align: left; padding: 0; border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; width: 100%; }
.pr-lane-face:focus-visible { outline: 2px solid var(--pr-focus); outline-offset: 4px; }
.pr-lane--open { grid-column: 1 / -1; border-color: var(--pr-ink-3); box-shadow: var(--pr-e2); }
.pr-lane-filter { display: grid; gap: var(--pr-s3); justify-items: start; border-top: 1px solid var(--pr-rule); padding-top: var(--pr-s4); margin-top: var(--pr-s2); }
.pr-lane-filter .pr-fine { margin: 0; }

/* ---- Your sets (launch design L25) -------------------------------------------- */
.pr-sets { display: grid; gap: var(--pr-s2); margin: 0 0 var(--pr-s4); }
.pr-sets-f { margin: 0; }
.pr-set { display: inline-flex; align-items: center; gap: 2px; }
.pr-set-x { appearance: none; border: 0; background: transparent; color: var(--pr-ink-3); font: inherit; font-size: 18px; line-height: 1; width: 32px; height: 34px; cursor: pointer; border-radius: var(--pr-r-sm); }
.pr-set-x:hover { color: var(--pr-ink); background: var(--pr-sunk); }
.pr-set-x:focus-visible { outline: 2px solid var(--pr-focus); outline-offset: 2px; }
.pr-selbar-name { display: flex; gap: var(--pr-s2); align-items: center; padding: var(--pr-s2) 0 0; flex-basis: 100%; }
.pr-selbar-name .pr-input { flex: 1 1 auto; min-height: 40px; }

/* ===================================================== 20. THE PUBLIC FACE ==
   Phase 4a (launch design L2, L8, L13). Three screens a visitor reaches
   before there is a rail or a tab bar to navigate by: the landing page, the
   sign-in page signed out, and the taster's last screen. Every colour is a
   token, so dark needs no block of its own; nothing here uppercases (§7.7:
   .pr-label is the only uppercase class and the label lint holds it).      */

/* ---- the way back to the site --------------------------------------------
   The garden carries no site nav, so a parent who tapped Practice arrived
   somewhere with the footer 2,800px down a phone page (Joe, 2026-09-03).
   Three plain anchors; the garden rule forbids loading the site's CSS and
   JS, not linking to its pages. */
.pr-backbar {
  display: flex; flex-wrap: wrap; gap: var(--pr-s4); align-items: center;
  padding: var(--pr-s2) 0 var(--pr-s3); margin: 0 0 var(--pr-s4);
  border-bottom: 1px solid var(--pr-rule);
  font-size: var(--pr-t-sm);
}
.pr-backbar a { color: var(--pr-ink-2); text-decoration: none; }
.pr-backbar a:hover { color: var(--pr-ink); text-decoration: underline; text-underline-offset: 2px; }
.pr-backbar a:focus-visible { outline: 2px solid var(--pr-focus); outline-offset: 2px; border-radius: 2px; }

/* ---- the taster's last screen (canvas TasterEnd) ---------------------------- */
.pr-tend { display: grid; gap: var(--pr-s6); max-width: 480px; margin: var(--pr-s5) auto 0; }
.pr-tend .pr-backbar { margin: 0; }
.pr-tend-hero { display: grid; gap: var(--pr-s3); }
.pr-tend-h {
  margin: 0; font-family: var(--font-display); font-weight: 400;
  font-size: 32px; line-height: 1.1; letter-spacing: -0.015em; color: var(--pr-ink);
}
.pr-tend-h em { font-style: italic; color: var(--pr-red); }
.pr-tend-offer { margin: 0; }
.pr-tend-list {
  margin: 0; padding: 0; list-style: none; display: grid; gap: var(--pr-s2);
  font-size: var(--pr-small); color: var(--pr-ink-2);
}
.pr-tend-list li { padding-left: var(--pr-s4); position: relative; }
.pr-tend-list li::before {
  content: ""; position: absolute; left: 0; top: 0.6em; width: 5px; height: 5px;
  border-radius: 50%; background: var(--pr-ink-3);
}
.pr-tend-card { display: grid; gap: var(--pr-s4); }

/* ---- the landing page (canvas Main / LandingDeskLight), /practice/ ---------
   Numbers from the Phase D kit, tokens from §1. The page is a single column
   at 375px and a 900px measure at 1024px and up; the three sample cards and
   the two tier cards go into columns there. */
.pr-ld {
  display: grid; gap: var(--pr-s7);
  max-width: 900px; margin: 0 auto;
  padding: var(--pr-gutter) var(--pr-gutter) var(--pr-s8);
  color: var(--pr-ink);
}
.pr-ld-header { display: flex; align-items: center; gap: var(--pr-s3); }
.pr-ld-brand { display: flex; align-items: center; gap: var(--pr-s3); color: var(--pr-ink-2); text-decoration: none; }
.pr-ld-brand:hover { color: var(--pr-ink); }
.pr-ld-brand:focus-visible { outline: 2px solid var(--pr-focus); outline-offset: 4px; border-radius: 4px; }
/* L28: the brand on one line, the product under it, one lockup. */
.pr-lockup { display: grid; gap: 1px; line-height: 1.05; }
.pr-ld-brand .pr-brand-name { color: var(--pr-ink); }
.pr-ld-header-sp { margin-left: auto; }
.pr-ld .pr-backbar { margin: calc(-1 * var(--pr-s5)) 0 0; }
.pr-ld-main { display: grid; gap: var(--pr-s7); }
.pr-ld-hero { display: grid; gap: var(--pr-s4); }
.pr-pill--course {
  min-height: 44px; padding: 8px 18px; display: inline-flex; align-items: center;
  font-size: var(--pr-t-md); font-weight: 500; cursor: default;
}
.pr-ld-h {
  margin: 0; font-family: var(--font-display); font-weight: 400;
  font-size: 36px; line-height: 1.05; letter-spacing: -0.015em; color: var(--pr-ink);
}
.pr-ld-h em { font-style: italic; color: var(--pr-red); }
.pr-ld-sub { margin: 0; font-size: 17px; line-height: 1.5; color: var(--pr-ink-2); max-width: 30rem; }
.pr-ld-go { display: flex; flex-wrap: wrap; gap: var(--pr-s3); }
.pr-ld-hero .pr-fine { max-width: 34rem; }
.pr-ld-hero .pr-fine a { color: var(--pr-ink-2); white-space: nowrap; }
.pr-ld-section { display: grid; gap: var(--pr-s4); }
.pr-ld-k { margin: 0; }
.pr-ld-section > .pr-note { max-width: 34rem; }

/* the three sample cards — the platform's marked question, compact */
.pr-ld-samples { display: grid; gap: var(--pr-s4); }
.pr-ld-q {
  background: var(--pr-sheet); border: 1px solid var(--pr-rule); border-radius: var(--pr-r);
  box-shadow: var(--pr-e1); overflow: hidden; min-width: 0;
}
.pr-ld-qh {
  display: flex; align-items: center; gap: var(--pr-s3);
  padding: var(--pr-s3) var(--pr-s5); background: var(--pr-sunk); border-bottom: 1px solid var(--pr-rule);
}
.pr-ld-marks { margin-left: auto; white-space: nowrap; }
.pr-ld-qb { padding: var(--pr-s5); display: grid; gap: var(--pr-s4); }
.pr-ld-qb .pr-stem { margin: 0; }
.pr-ld-field {
  display: flex; align-items: stretch; border: 1px solid var(--pr-rule-2); border-radius: var(--pr-r);
  background: var(--pr-sheet); overflow: hidden;
}
.pr-ld-field input {
  flex: 1 1 auto; min-width: 0; border: 0; background: transparent; color: var(--pr-ink);
  font: inherit; font-size: var(--pr-t-md); padding: 10px var(--pr-s4);
}
.pr-ld-field .pr-unit { display: flex; align-items: center; padding: 0 var(--pr-s4); background: var(--pr-sunk); border-left: 1px solid var(--pr-rule); }
.pr-ld-opts { margin: 0; padding: 0; list-style: none; display: grid; gap: var(--pr-s2); }
.pr-ld-opt {
  display: grid; grid-template-columns: 1.6em minmax(0, 1fr); gap: 0 var(--pr-s3); align-items: start;
  padding: 10px var(--pr-s4); border: 1px solid var(--pr-rule); border-radius: var(--pr-r-sm); background: var(--pr-sheet);
}
.pr-ld-optk { font-family: var(--font-mono); font-size: var(--pr-t-sm); color: var(--pr-ink-3); padding-top: 2px; }
.pr-ld-opt .pr-opt-body { font-size: var(--pr-t-md); }
.pr-ld-opt--right { border-color: var(--pr-green); box-shadow: inset 3px 0 0 var(--pr-green); }
.pr-ld-opt--wrong { border-color: var(--pr-red); box-shadow: inset 3px 0 0 var(--pr-red); }
.pr-ld-optfb { grid-column: 2; margin: 4px 0 0; font-size: var(--pr-t-sm); color: var(--pr-ink-2); }
.pr-ld-verdict {
  display: grid; gap: var(--pr-s2); padding: var(--pr-s4) var(--pr-s5);
  border-top: 3px solid var(--pr-rule); background: var(--pr-sunk);
}
.pr-ld-verdict--wrong { border-top-color: var(--pr-red); background: var(--pr-red-wash); }
.pr-ld-vh { display: flex; align-items: baseline; gap: var(--pr-s3); flex-wrap: wrap; }
.pr-ld-vg { font-size: 20px; line-height: 1; font-weight: 600; color: var(--pr-red-ink); }
.pr-ld-vt { font-size: var(--pr-t-md); color: var(--pr-ink-2); }
.pr-ld-vm { margin-left: auto; white-space: nowrap; font-family: var(--font-mono); font-size: var(--pr-t-sm); color: var(--pr-ink-2); }
.pr-ld-verdict .pr-named { margin: var(--pr-s2) 0 0; font-size: clamp(20px, 4.4vw, 24px); }
.pr-ld-verdict .pr-note { margin: 0; }

/* the two tiers */
.pr-tiers { display: grid; gap: var(--pr-s4); }
.pr-tier {
  display: grid; gap: var(--pr-s3); align-content: start; padding: var(--pr-s6);
  border: 1px solid var(--pr-rule); border-radius: var(--pr-r-lg); background: var(--pr-sheet); box-shadow: var(--pr-e1);
}
.pr-tier--full { border-color: var(--pr-ink); }
.pr-tier > .pr-label, .pr-tier > .pr-note { margin: 0; }
.pr-price { margin: 0; font-family: var(--font-display); font-size: 40px; line-height: 1; color: var(--pr-ink); }
.pr-price small { font-size: 15px; font-family: var(--font-body); color: var(--pr-ink-3); }
.pr-tier ul { margin: 0; padding: 0; list-style: none; display: grid; gap: var(--pr-s2); font-size: var(--pr-small); color: var(--pr-ink-2); }
.pr-tier li { padding-left: var(--pr-s4); position: relative; }
.pr-tier li::before { content: ""; position: absolute; left: 0; top: 0.6em; width: 5px; height: 5px; border-radius: 50%; background: var(--pr-ink-3); }
.pr-tier .pr-btn { margin-top: var(--pr-s2); }

.pr-ld-foot { border-top: 1px solid var(--pr-rule); padding-top: var(--pr-s4); font-size: var(--pr-t-sm); color: var(--pr-ink-3); }
.pr-ld-foot a { color: var(--pr-ink-2); }

@media (min-width: 1024px) {
  .pr-ld-h { font-size: 54px; }
  .pr-ld-sub { font-size: 19px; }
  .pr-ld-samples { grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: start; }
  .pr-tiers { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* The landing page's lockup shows both lines at every width — the app header
   hides .pr-brand-name below 640px because a signed-in header also carries a
   name and two controls, and the landing header carries neither. The mark is
   re-inked for the same reason .pr-brand .brand-mark is: /styles.css paints
   it in the site's light-only --ink, which vanishes on the dark ground. */
.pr-ld-brand .pr-brand-name, .pr-ld-brand .pr-brand-tag { display: block; }
.pr-ld-brand .brand-mark { color: var(--pr-ink); display: grid; }
.pr-ld-brand .brand-mark svg { display: block; }
/* The verdict's three parts share one row: glyph, sentence, mark. */
.pr-ld-vh { flex-wrap: nowrap; align-items: flex-start; }
.pr-ld-vt { flex: 1 1 auto; min-width: 0; }
/* The header never overflows: the lockup may wrap its name onto two lines
   before the row grows past the viewport (it measured 383 at 375 with the name
   on one line beside the two controls). */
.pr-ld-header { gap: var(--pr-s2); }
.pr-ld-brand { min-width: 0; flex: 0 1 auto; }
.pr-ld-brand .pr-brand-name { white-space: normal; line-height: 1.1; }
.pr-ld-header .pr-btn { flex: none; }
.pr-ld-vt .pr-verdict-val { white-space: nowrap; }

/* ---- the states (canvas States): slow, failed, expired, offline -------------- */
.pr-state {
  display: grid; gap: var(--pr-s3); justify-items: start;
  max-width: var(--pr-measure); margin: var(--pr-s6) auto 0; padding: var(--pr-s5);
  background: var(--pr-sheet); border: 1px solid var(--pr-rule); border-radius: var(--pr-r-lg); box-shadow: var(--pr-e1);
}
.pr-state-h { margin: 0; font-family: var(--font-display); font-weight: 400; font-size: var(--pr-t-2xl); line-height: 1.2; }
.pr-state .pr-note, .pr-state .pr-fine { margin: 0; }
.pr-state-go { display: flex; gap: var(--pr-s3); flex-wrap: wrap; }
.pr-skel { display: grid; gap: 10px; width: 100%; }
.pr-skel span { display: block; height: 12px; border-radius: 6px; background: var(--pr-sunk); animation: pr-skel 1.2s ease-in-out infinite alternate; }
@keyframes pr-skel { from { opacity: 0.55; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .pr-skel span { animation: none; } }
.pr-offline { margin: var(--pr-s3) 0 0; font-size: var(--pr-t-sm); color: var(--pr-amber-ink); }
.pr-ld-h, .pr-tend-h { text-wrap: balance; }
/* The week on Full, said under the buttons where it is read (Joe, 2026-09-05). */
.pr-ld-trial { margin: 0; font-size: var(--pr-t-md); color: var(--pr-ink-2); max-width: 32rem; }
.pr-ld-trial strong { color: var(--pr-ink); font-weight: 600; }
