/* ==========================================================================
   Sokra brand system — see docs/brand/sokrabrandguidelines.md
   ==========================================================================

   Sokra is the joint working brand for the AI Tutor built by PA Consulting and
   Developing Experts. It replaced the PA-only identity this file used to carry;
   the parents' own colours are no longer surface colours here, and appear only
   inside the co-brand lockup, which is a line of type rather than a palette.

   Colour is defined in two tiers, and the split is the whole point of this
   file's structure.

     PRIMITIVES (--sokra-*) are facts about the brand. Teal is #168C91 in
     every context, on every screen, under every theme. They are transcribed
     from the brand guide and never vary.

     SEMANTIC TOKENS (--surface, --text, --action-bg, …) say what a colour is
     being *used for*: the page ground, body text, the fill of an action
     button. Each is defined in terms of a primitive.

   Component rules below the :root block use the semantic tier and nothing
   else. No hex value, no rgba() literal, and no reference to a --sokra-
   primitive appears below :root. That is a mechanically checkable
   invariant, so it is checked rather than trusted. checks/css-tokens.test.ts
   enforces it — including named colours like `white`, which slip past a hex
   check and are just as unthemeable — and test/system/theme_tokens_test.rb
   proves the tokens actually reach the browser.

   WHY. The theme picker offers four authored palettes — the default, dark,
   warm and high contrast — and the set is open. A pupil with dyslexia or Irlen
   syndrome may need a cream or muted background, which is a SEND requirement
   here rather than a preference. Themes are [data-theme="<name>"] blocks that
   redefine the semantic tier and nothing else, for an open set of names — so
   nothing may assume there are exactly two. A component reaching past the
   semantic tier to a primitive, or to a literal, is invisible to that switch
   and silently breaks the theme.

   Every theme is authored and contrast-checked by us; pupils choose from the
   list rather than picking their own colours.

   These stay CSS custom properties rather than Sass variables for the same
   reason: Sass resolves at compile time and cannot express a palette chosen in
   the browser.

   ACCESSIBILITY. Contrast is a property of a palette, not of this file, so
   every palette is checked rather than the file being trusted:
   web/test/theme-contrast.test.ts computes the WCAG ratio for every pairing
   the product renders, in every theme and in every look that has a palette of
   its own, and fails the build below AA. The
   default palette's worst text pairing is 5.02:1 — a white label on the
   guide's strong pink, its own danger button carrying its own inverse text —
   and there is a test asserting that figure so this sentence cannot quietly
   go stale.

   That test checks flat pairings — a token on a token. It cannot check text on
   a *wash*, because a wash is a color-mix and has no hex to compute from, and
   the two failures found while building the first palette were both of that
   kind: the brand blue set on a ground already tinted with it. The axe pass in
   test/system/accessibility_test.rb is what catches those, on the rendered
   page where the wash actually exists. Neither check replaces the other.

   Type. Figtree for everything — the wordmark, headings and every word read
   at length — and Atkinson Hyperlegible Next for a pupil who cannot
   comfortably read it. Both are SIL Open Font Licence faces, so unlike the PA
   typefaces they can be bundled, and they are: the CSP allows fonts from
   :self only.

   AND TYPE IS AN AXIS, not a fixed decision — the second half of what a pupil
   can change about how this product reads. A palette redefines the colour tier;
   five data-type-* attributes redefine the type tier, independently of each
   other and of the palette. Face, size, spacing, line length and letter case.
   The full
   argument, including why colour is curated and type is not, is on Tier 3
   below; the short version is that a pupil who needs bigger text and a pupil
   who needs a different face are usually not the same pupil, and a product that
   makes them pick a bundle serves whichever of them the bundle was named after.

   The invariant the colour tier has, this one has too, and it is checked the
   same way: a type block sets the tokens of its own axis and nothing else, so
   "large" means the same thing under every face. checks/type-tokens.test.ts.

   AND A LOOK IS A THIRD KIND OF TOKEN BLOCK, the last thing a pupil chooses
   about how the pupil side reads and the only one that is decoration. A
   palette says what colour a role is, a type axis says how text is set, and a
   look is one of fifteen authored appearances: a palette, one display face,
   four shape tokens and a bounded amount of scenery (docs/plan/Pupil_Looks_Build_Plan.md).
   Pupils said the pupil screens read as functional and corporate; this is the
   answer that does not give up what the other two axes are for. Each look is
   one file in looks/, imported by looks.css, which the pupil's pages link
   straight after this one (the foot of this file says why it is not at the
   end of it), and carries two blocks. [data-theme="default"][data-look="k"] redefines the semantic
   tier, as a theme does, and matches nothing else; [data-look="k"] sets only
   the --look-* tokens declared on :root below, so that data-look="none" and a
   page with no attribute at all draw the product as it was.

   WHY A BLOCK, AND NOT RULES. The alternative is a look written as component
   rules with literal colours, and at fifteen of them it is exactly the failure
   the two tiers exist to stop: files the palette switch cannot see, a contrast
   test with nothing to measure, and a pupil who chose high contrast looking at
   a navy page with orange buttons because a look overpainted their palette
   without knowing it was there. As a token block it may name colours outright —
   it defines them, as a theme does — and the same two checks reach it: the
   literal ban holds for every other rule in a look file, and the contrast test
   measures each look's palette on every pairing it lists. What it adds beyond
   a palette is what no token expresses, and that is written as component rules
   scoped to its own key, in its own file, under the same ban.

   WHAT WINS WHEN A LOOK MEETS A SETTING. The palette: a look's colours apply
   only while the palette is Standard, because a palette is an access need (cream
   for Irlen syndrome, high contrast for low vision) and a look is a preference,
   so it gives up its colours and keeps its face, shapes and scenery. The face:
   a look's display face is reached only through the `sokra` option of the face
   axis, so a pupil who asked for the hyperlegible face has it in every look.
   Size, spacing, length and case are not a look's to touch at all. Nothing a
   look draws moves under reduced motion, and the scenery can be put away.

   AND STANDARD IS LIGHT UNDER EVERY LOOK. A look's palette is what Standard
   means while the pupil wears it, so a look drawn dark there leaves Standard
   and Dark as two dark pages, and the pupil who chose Dark for low light, or
   Standard to get away from it, sees nothing change. Five looks (cars, code,
   lab, music, space) were dark under Standard until 16 October 2026 and were
   redrawn light; under Dark each wears the dark palette below with its own
   face, shapes and scenery. checks/look-css.ts holds every look's palette block
   to `color-scheme: light`, and web/test/theme-contrast.test.ts to dark text on
   a light card and ground at least 3:1 from Dark's.
   ========================================================================== */

/* --- Faces ---------------------------------------------------------------

   Served from app/assets/fonts rather than from Google's CDN. The content
   security policy sets font-src :self, so a CDN link would be blocked and the
   page would silently fall through to Georgia and Arial — which is a legible
   page, but not this one.

   Latin and Latin Extended only. Polish and Romanian need the extended range;
   Arabic and Urdu are not covered by either face and fall to the Noto stack
   named on .language-endonym, which is a known gap recorded there.

   ⚠️ THIS SAID `font-display: swap`, AND THAT THE FALLBACKS WERE "metric-similar
   enough that the reflow is small". They are not, and the claim was never
   measured — it was reasoning about a wordmark, where it would have been true.
   On a question it is 3% of the width of every heading, which re-wraps a line
   and moves everything under it half a second after the page appears. See the
   note over the Figtree face below for what replaced it and what that costs. */

/* ⚠️ `optional` RATHER THAN `swap`, AND IT IS A TRADE TAKEN ON PURPOSE. With
   `swap` the browser paints the fallback, then repaints in the real face
   whenever it arrives — and under v1 the fallback for the display face was
   Palatino, about 3% wider than Marcellus, so every heading on the screen
   re-wrapped half a second after the page appeared. Measured off a screen
   recording: the practice stem drew at 859px and settled at 834px. A question
   that moves while somebody is reading it is worse than a question in the
   wrong typeface. Figtree's fallback is Arial, which is no closer a match, so
   the argument stands unchanged.

   `optional` gives the font a short block period and then STOPS: if it has not
   arrived, that page view keeps the fallback and never swaps. The file is
   still fetched and cached, so the next page has the real face. With the
   preload in shared/_font_preloads the first page usually has it too.

   What this costs is one page view in Arial on a cold cache over a slow link.
   What it buys is that a heading never moves under a reader.

   Atkinson below keeps `swap` deliberately: it is the face a pupil has gone
   into settings and ASKED for, it is not preloaded, and `optional` would mean
   they frequently did not get it. For that reader the face matters more than
   the stillness.

   Figtree is the brand's one face. One variable file per subset carries every
   weight from 300 to 900; the guide uses 400, 500, 700 and 800, and the range
   is declared so a weight is a choice rather than a synthesis. One file for
   everything also means one preload instead of the two v1 needed. */
@font-face {
  font-family: Figtree;
  font-style: normal;
  font-weight: 300 900;
  font-display: optional;
  src: url("/_web/assets/figtree-latin-D_ZTVpCC.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: Figtree;
  font-style: normal;
  font-weight: 300 900;
  font-display: optional;
  src: url("/_web/assets/figtree-latin-ext-DCwSJGxG.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* The third face, and the only one here chosen for a reader rather than for the
   brand. Atkinson Hyperlegible Next is the Braille Institute's, drawn so that
   the characters most often confused at low vision or with dyslexia — I l 1, O
   0, b d, p q — differ in shape rather than only in position. It is OFL, so it
   can be bundled on the same terms as Figtree, which font-src :self requires.

   It is never the default. A face helps some readers and not others, and the
   evidence for any single "dyslexia font" is thin; what is well supported is
   letting a reader change it. So it is one option on an axis rather than a
   replacement, and it is offered whole — a pupil who picks it gets it for the
   headings too, because a page set in two faces where one of them was chosen
   for legibility is a page that gave them half of what they asked for.

   Variable 400-700 in one file per subset, like Figtree, and the same two
   subsets with the same ranges. Arabic and Urdu are outside it, as they are
   outside Figtree — the gap recorded on .language-endonym. */
@font-face {
  font-family: "Atkinson Hyperlegible Next";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/_web/assets/atkinson-latin-BcXVPD7q.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Atkinson Hyperlegible Next";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/_web/assets/atkinson-latin-ext-C6vrW8VD.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  /* --- Tier 1: primitives ------------------------------------------------
     Brand facts. Identical under every theme; a theme redefines the semantic
     tier below, never these. */

  /* The palette, transcribed from the guide. Five hues — navy for structure,
     teal for doing, amber for "wait and watch", pink for what has genuinely
     gone wrong, purple for focus and key words — each with the variants the
     guide gives it. Which of them does which job is a property of the semantic
     tier below; these are only the facts.

     The variants are the guide's own, not darkenings picked here. Plain teal
     is 4.0:1 on white, under AA for text, so the guide carries a strong teal
     for a button fill and an ink teal for text, and the same pattern for pink
     and amber. Where this file needs a colour to read, it takes the ink; where
     it needs one to look at, the plain hue. */
  --sokra-navy: #17233c;
  --sokra-navy-2: #223152;

  --sokra-teal: #168c91;
  --sokra-teal-strong: #127a7f;
  --sokra-teal-ink: #0f6c70;
  --sokra-teal-soft: #e3f3f3;
  --sokra-teal-light: #9fe0e2;

  --sokra-green: #1a7f45;

  --sokra-pink: #ee2f66;
  --sokra-pink-strong: #d42358;
  --sokra-pink-ink: #b8184a;
  --sokra-pink-soft: #fde8ee;

  --sokra-purple: #5b35d5;
  --sokra-purple-ink: #3c2196;
  --sokra-purple-soft: #eeeafb;

  --sokra-amber: #e3a92c;
  --sokra-amber-ink: #8a6210;
  --sokra-amber-soft: #fcf3df;

  /* Greys, in the navy's temperature rather than a generic one. Surface is the
     page, raised is a card on it, grey-soft is the empty cell in a grid. */
  --sokra-surface: #f5f7f9;
  --sokra-surface-raised: #ffffff;
  --sokra-grey-soft: #eef1f4;
  --sokra-line: #e1e6ec;
  --sokra-ink: #26324a;
  --sokra-ink-muted: #5e6a80;
  --sokra-white: #ffffff;

  /* Amber at reading strength. The guide's amber-ink is 4.2:1 on amber-soft,
     which is the warning wash, so a warning's text would miss AA on its own
     ground. Same hue, darkened until it clears. The only colour in this block
     that is ours rather than the guide's. */
  --sokra-amber-deep: #7a560e;

  /* Amber at pressing strength. The guide gives amber no strong variant, and
     Review's buttons are amber — see the mode colours in the guide — so a
     hovered one needs somewhere darker to go that still carries navy
     lettering (5.9:1). Same hue, taken down one step; ours, like the one
     above. */
  --sokra-amber-strong: #cf9423;
  /* Challenge's colour (Revision_Sessions_And_Challenge_Plan, decision D9): a
     blue no mode had, because pink already means danger and green means right.
     Not in the brand guide's palette — the icon slot's sky, made a primitive
     so a mode can be built on it the way the other four are. */
  --sokra-sky: #2a63c9;
  --sokra-sky-ink: #1d4a9c;

  /* --- Type: the faces themselves ----------------------------------------

     Two faces, on different footings. Figtree is the brand's, and the guide
     sets everything in it. Atkinson Hyperlegible Next is not a brand decision
     at all: it is there so a pupil who cannot read the brand's face can read
     the product.

     The display and text stacks are kept as two names although they are now
     the same face. The type tier below answers "which stack does which job"
     in those terms, and a later brand that splits them again changes two
     lines here rather than every role.

     These are primitives, so they are stacks rather than roles. Which stack is
     doing which job is a semantic question, and it is answered below by
     --font-body, --font-display and --font-label — which the type axis
     redefines, the way a palette redefines --surface.

     Every fallback is locally available, because font-src is :self: if a
     woff2 fails the page lands on the next name in the list rather than on
     the browser's default serif. Arial, Helvetica, which is what the guide
     names. */

  --face-display: Figtree, Arial, Helvetica, sans-serif;
  --face-text: Figtree, Arial, Helvetica, sans-serif;
  --face-hyperlegible: "Atkinson Hyperlegible Next", "Atkinson Hyperlegible", Verdana, Tahoma, sans-serif;

  /* Mono is not a brand face and does not pretend to be one. It is for the
     things that are literally machine output — a pupil code, an OTP secret, a
     block of CSV — where reading character by character is the whole point, and
     it is deliberately outside the type axis: changing it would make those
     harder to read, not easier. */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;

  /* The weight a heading is set in. v1's display face had one weight and the
     guide forbade a faked bold, so headings were told apart by face and size
     alone and every display rule said 400. Figtree's guide sets titles bold,
     and a 400 Figtree heading at the old sizes reads as body copy that has
     been enlarged. A token rather than 700 written twenty times, so the
     rules that used to say 400 say what they mean. Every face on the axis
     carries 700, so it holds whichever face a pupil picks. */
  --weight-display: 700;

  /* The size scale, from the guide's pixels at a 16px root. In rem, so the
     size axis multiplies every one of them — a pixel figure here would be the
     WCAG 1.4.4 failure the size axis exists to avoid. Not every size in this
     file is on it: these are the roles the guide names, and the rest are
     component sizes that were never brand decisions. */
  --size-page-title: 1.625rem;
  --size-question: 1.3125rem;
  --size-card-title: 1.0625rem;
  --size-pupil-body: 1.0625rem;
  --size-body: .9375rem;
  --size-small: .8125rem;
  --size-eyebrow: .75rem;

  /* Shape, also from the guide. Five radii and nothing between them: a
     control, a card, a panel, a sheet, a pill. A radius that is not one of
     these is either a hairline detail (a code span, a meter's end) or a
     circle, and is written as itself. */
  --radius-sm: 6px;
  --radius-md: 8px;
  --radius-lg: 10px;
  --radius-xl: 14px;
  --radius-pill: 99px;

  /* The wordmark is set in caps with this much tracking, which the guide gives
     as "around 0.14em". It is a token because the mark appears at two sizes,
     and letterspacing that drifts between them is the sort of thing nobody
     notices and everybody feels.

     Not --tracking. The wordmark is a mark, not text a pupil reads, so the
     spacing axis leaves it alone — see the type tier below. */
  --wordmark-tracking: 0.14em;

  /* Clearspace: the guide asks for clear space around the wordmark of at least
     the height of the S, on all sides. */
  --clearspace: 1.75rem;

  /* The pupil's mark (components/PupilMark.tsx), the two of its tokens that
     are not a palette's to set. The wheel a pupil turns for a colour of their
     own is every hue at the palette's lightness and strength: --icon-own-l and
     --icon-own-c are each palette's, declared below, and this is computed on
     <html> beside them, so one declaration follows every palette. The size is
     what each place that draws a mark sets (the masthead 28px, the register
     36px, the picker 44px). */
  --icon-wheel: conic-gradient(in oklch longer hue, oklch(var(--icon-own-l) var(--icon-own-c) 0), oklch(var(--icon-own-l) var(--icon-own-c) 360));
  --mark-size: 28px;
}

/* --- The look axis: what a look may set ----------------------------------

   Ten tokens, declared with the product's present value so that a page with
   no data-look and a page with data-look="none" are the page as it was drawn
   before looks existed. A look sets these in [data-look="k"] and nothing else
   in that block (checks/css-tokens.test.ts); its colours go in the palette
   block beside it.

   Declared twice over, on :root and on [data-look="none"], for the reason the
   default palette and every type axis are: the Settings page draws one card
   per look, each carrying its own data-look, and the standard card has to
   match a block or it inherits whichever look the page is wearing and shows
   the pupil their present setting under two names.

   WHAT READS THEM. The rules below, which this file re-pointed at them without
   moving a pixel under `none`:

     --look-radius          the radius of a card, a bubble, a hint card, the
                            shelf; larger surfaces take this plus a fixed step
                            (two pixels for a card, six for a glossary tile),
                            so a look that squares the corners squares every
                            surface and keeps their order
     --look-radius-control  buttons, the answer options, the rungs, the answer
                            box; an option or a rung is this plus two pixels,
                            as it was
     --look-border          a card's and a bubble's border width
     --look-border-control  a button's, an option's and an input's, which are
                            half as heavy again on an option and a rung (1.5
                            times this) and twice on the rung being worked
     --look-ground          what the page is drawn on: a gradient or a paper is
                            a ground, not a surface, so it is not --surface-sunken.
                            It is the whole value of `background`, so a gradient
                            says how it sits — `linear-gradient(…) fixed
                            no-repeat` — or it tiles again below a short page
     --look-display-face    read by the `sokra` option of the face axis (Tier
                            3) and by nothing else, which is what lets a pupil's
                            own face outrank it
     --look-scenery-1..4    for a look's own scenery rules; no component reads
                            them

   The pill-shaped tools (read aloud, the shelf, Explain, the tutor's launcher)
   are 99px and not a token: a radius that reads 6px under `none` cannot also
   be a pill, and squaring the tools to fit a token would have redrawn the
   product. A look that wants them square writes a rule of its own. */
:root, [data-look="none"] {
  --look-display-face: var(--face-display);
  --look-radius: 8px;
  --look-radius-control: 6px;
  --look-border: 1px;
  --look-border-control: 1px;
  --look-ground: var(--surface-sunken);
  --look-scenery-1: transparent;
  --look-scenery-2: transparent;
  --look-scenery-3: transparent;
  --look-scenery-4: transparent;
}

/* THE PALETTE WINS OVER A LOOK'S GROUND, which is the one look token that carries
   colours outside a palette block. A look's colours are in play only while the
   palette is Standard (its palette block is scoped to the default palette), and
   a ground written as literal colours in [data-look="k"] is not scoped, so under
   dark, warm or high contrast it would sit behind text the palette set for
   another ground: a pupil who chose high contrast for low vision, reading black
   text on a pastel gradient. So under any other palette the ground is that
   palette's own. Specificity (0,2,0) is what beats a look's block (0,1,0), and
   being a token and not a rule on `body` it cannot beat a screen's own ground
   (the live wall's, which sets one on the body). The sample cards on the Look tab
   carry data-theme="default" and so are untouched: they show the look. */
:root:not([data-theme="default"]) {
  --look-ground: var(--surface-sunken);
}

/* --- Tier 2: semantic tokens ---------------------------------------------
   Roles, not colours. This block is the contract a theme author fills in, and
   every token here is one more thing each new palette has to get right, so it
   stays as small as the product's actual distinctions allow.

   The selector carries the default palette twice over: on :root, so a page
   with no data-theme at all still has colours, and on [data-theme="default"],
   so the default is a scope like any other theme rather than an absence of
   one. Without the second half the settings picker cannot show a light swatch
   on a dark page — the swatch would match no block and inherit the dark
   values. */

:root, [data-theme="default"] {

  /* Surfaces. Sunken is the page ground and any recessed fill; raised is what
     floats above the page. The guide's page is a white card on a pale grey,
     and that is exactly this: its surface is our ground, its raised surface
     is our card.

     ⚠️ NOT THE GUIDE'S NAMES MAPPED STRAIGHT ACROSS. "surface" here is the
     card — 150-odd rules paint a card with it — and the guide's surface is
     its page. Mapped by name, every card would be #f5f7f9 on a #eef1f4
     ground, 1.03:1, and with card shadows gone (see Depth) nothing would say
     where a card ends. */
  --surface: var(--sokra-surface-raised);
  --surface-sunken: var(--sokra-surface);
  --surface-raised: var(--sokra-surface-raised);

  /* Text. Inverse is text on a filled interactive surface; link is the
     interactive colour used *as* text or as a hover accent, which a dark theme
     has to be able to set separately from the button fill it sits next to.
     Links take teal-ink, not teal: teal is 4.0:1 on white and the ink is 6.2. */
  --text: var(--sokra-ink);
  --text-muted: var(--sokra-ink-muted);
  --text-inverse: var(--sokra-white);
  --text-link: var(--sokra-teal-ink);

  /* Lines. Strong is the emphasised or interactive edge, and it is navy
     rather than teal: teal is what the page uses to say "do this", so spending
     it on every input border would leave nothing to say it with.

     The focus ring is purple, which the guide keeps for focus and key words
     and nothing else. That is what makes it findable — no fill, edge or status
     on the page is the same hue. */
  --border: var(--sokra-line);
  --border-strong: var(--sokra-navy);
  --focus-ring: var(--sokra-purple);

  /* The word being read aloud. Amber is the guide's highlight colour, and
     navy on it is 7.4:1.

     It sits under 2:1 against the card, which is deliberate and is why the
     rule that uses it also underlines: a moving block of colour behind a word
     must not be the only thing saying which word it is, and a strong enough
     fill to carry that alone would strobe down the sentence. */
  --read-aloud-bg: var(--sokra-amber);
  --read-aloud-ink: var(--sokra-navy);

  /* A marked science word, when a pupil has asked for the words to be filled
     in as well as underlined — see settings/_key_words. The guide's own "key
     words" tag, purple-soft under purple-ink.

     ⚠️ IT MUST NOT BE THE READ-ALOUD FILL. Both can be on the same word at the
     same time, and if they were the same colour a pupil would have no way to
     tell "this word has a meaning you can look up" from "this is the word
     being spoken right now". So the highlight is cool where the read-aloud
     fill is warm, in every palette, and the two are never within reach of each
     other.

     A fill this soft cannot be the only thing saying which word it is either,
     so the dotted underline stays under it whether the fill is on or off. */
  --word-bank-bg: var(--sokra-purple-soft);
  --word-bank-ink: var(--sokra-purple-ink);

  /* An exam command word where it gives the order — "Explain", "Describe" — in
     a stem or the tutor's question back (web/app/lib/command-word-marks.ts).
     The pill over the question is the action teal, so the word in the sentence
     is the same teal, softly: the two say one thing. Taken from --action-bg
     rather than from a primitive so that a look's teal is the mark's teal too.

     ⚠️ NOT THE WORD-BANK PURPLE AND NOT THE READ-ALOUD AMBER. A command word
     says what kind of answer is wanted, a key word has a meaning to look up,
     and the amber is the word being spoken — three different things that can
     land on the same sentence, so three hues. The weight and the solid line
     carry it as well, so the fill is never the only thing saying which word. */
  --command-word-bg: color-mix(in srgb, var(--action-bg) 13%, transparent);
  --command-word-ink: var(--text);
  --command-word-line: var(--action-bg);

  /* How much a pupil used an access feature, in the Insight register: three
     steps of a hue no outcome cell uses (the guide's purple, "focus and key
     words"), so a heatmapped access cell can never be read as right or wrong.
     The count, "on" or a tick is written in every cell as well, so the step is
     never the only carrier. Not the word-bank tokens, whose meaning is "a
     marked science word". */
  --use-low: var(--sokra-purple-soft);
  --use-low-ink: var(--sokra-purple-ink);
  --use-mid: #c9bff0;
  --use-mid-ink: var(--sokra-navy);
  --use-high: var(--sokra-purple-ink);
  --use-high-ink: var(--sokra-white);

  /* Interactive fills. Action is the guide's primary button, teal-strong, and
     it carries almost everything — submit, send, the avatar. Primary is the
     two places the product asks for a second, heavier fill (`button.primary`
     and the glass box's open button), and the guide has no second fill, so it
     takes navy, which the guide gives as structure and authority. Danger is
     the guide's danger button. */
  --primary-bg: var(--sokra-navy);
  --primary-bg-hover: var(--sokra-navy-2);
  --action-bg: var(--sokra-teal-strong);
  --action-bg-hover: var(--sokra-teal-ink);
  /* The lettering on an action fill. White in every palette; a token of its own
     because inside a phase the fill can be amber, which needs navy — see
     main[data-phase]. */
  --action-ink: var(--sokra-white);
  --danger-bg: var(--sokra-pink-strong);
  --danger-bg-hover: var(--sokra-pink-ink);

  /* Accent — anything live or in progress: the rung being worked, a fragile
     idea, a bar. Amber, which the guide calls "wait and watch", and which is
     the right temperature for it: an open session is a thing part-done, not a
     thing gone wrong.

     Selection is the checked answer option, and it is navy. ⚠️ NOT TEAL. Teal
     is also --success, so a teal selection reads as "correct" before the pupil
     has submitted anything — the same fault the v1 palette had the other way
     round, when the selection was drawn in the caution colour.

     The wordmark accent is the O, in teal. It is not derived from --accent: a
     palette that retunes what "in progress" looks like must not move the logo
     with it. What a palette may do is set the whole word in a single colour
     where the context is one-colour — high contrast, below, does exactly
     that. */
  --accent: var(--sokra-amber);
  --wordmark-accent: var(--sokra-teal);
  --lockup: var(--sokra-ink-muted);
  --selected: var(--sokra-navy);
  --selected-surface: color-mix(in srgb, var(--sokra-navy) 7%, var(--sokra-white));

  /* The pupil's mark (components/PupilMark.tsx): six colour slots, each the
     ink a shape is drawn in; the disc behind it is that ink mixed into the
     surface at --icon-disc-mix. A pupil's own hue is set at --icon-own-l and
     --icon-own-c, the lightness and strength every slot here was chosen at, so
     any hue on the wheel reads on this palette. --icon-ring is the width of
     an inset ring of the ink, 0 here and 2px in high contrast, where a tint
     would not meet 3:1 against the page. Kept away from the red the status
     tokens use, so a mark never reads as an alert on a teacher's register. */
  --icon-teal: var(--sokra-teal-strong);
  --icon-violet: var(--sokra-purple);
  --icon-amber: #b07a12;
  --icon-rose: var(--sokra-pink-strong);
  --icon-sky: #2a63c9;
  --icon-moss: var(--sokra-green);
  --icon-own-l: 0.48;
  --icon-own-c: 0.15;
  --icon-disc-mix: 18%;
  --icon-ring: 0px;

  /* Status. Each is a triplet: the solid colour, the wash behind it, and text
     that meets contrast on that wash. All three slots are used by every state,
     so none of these collapse.

     Teal is secure, amber is everything that is warm rather than wrong — a
     fragile idea, an item a teacher is going to pick up, an answer that needs
     looking at again — because none of those is a fault in the pupil. Pink is
     kept for what has genuinely gone wrong or is genuinely blocking: a failed
     action, a flagged line, a foundational gap with the rest of the course
     built on it. A child reads red as a mark, and the argument this product
     is making is that a wrong answer is diagnostic rather than one. The guide
     says the same thing in words — "not quite yet", never "wrong" — and every
     one of these has a word beside it, never colour alone. */
  --success: var(--sokra-teal);
  --success-surface: var(--sokra-teal-soft);
  --success-text: var(--sokra-teal-ink);
  /* The badge a right answer gets, and nothing else. Not --success: that is
     teal, and so is Practice — inside main[data-phase="practice"] a teal tick
     read as the mode's colour rather than as the answer being right. A green
     no other state or phase uses, so the one moment that means "right" has a
     colour that means only that. The tick is cut out of it in --surface, so
     each palette holds that pairing at 4.5:1 (theme-contrast.test.ts).
     Since 14 October 2026 it is also the "Done" pill on a finished phase of
     the lesson home (.stage-done), asked for in green by name: "finished" is
     the same good news as "right", and teal there would have been lost on
     Practice's own band. The word is drawn in --surface, the same pairing. */
  --correct: var(--sokra-green);
  /* The sheet a supplier's figure sits on, under EVERY theme, which is why it
     is defined here and redefined in none of the blocks below. Oak's diagrams
     are drawn for paper: flat colour, black leader lines, a transparent or
     white background. Put one on the dark theme's surface and the lines go
     with it — and an <img> cannot read the theme, so the page has to hand it
     a ground rather than expect it to adapt. The pupil's own stem figure has
     the same problem and does not have this yet; that is the figure work in
     the Sparx plan, not this. */
  --figure-plate: var(--sokra-white);
  --warning: var(--sokra-amber);
  --warning-surface: var(--sokra-amber-soft);
  --warning-text: var(--sokra-amber-deep);
  --warning-ink: var(--sokra-navy);
  --error: var(--sokra-pink-strong);
  --error-surface: var(--sokra-pink-soft);
  --error-text: var(--sokra-pink-ink);
  /* A word-level diff (Prompt_Arena_Plan.md §6): an insertion and a deletion.
     Never by colour alone — an insertion is underlined, a deletion struck through. */
  --diff-added-bg: var(--sokra-teal-soft);
  --diff-added-text: var(--sokra-teal-ink);
  --diff-removed-bg: var(--sokra-pink-soft);
  --diff-removed-text: var(--sokra-pink-ink);
  /* A comparison's verdicts (Prompt_Arena_Plan.md §6): A better, B better, no
     difference, depends on order. Neither side is good or bad, so neither takes
     the success or the error hue; each verdict also carries a symbol. */
  --verdict-a: var(--sokra-navy);
  --verdict-a-text: var(--sokra-white);
  --verdict-b: var(--sokra-purple);
  --verdict-b-text: var(--sokra-white);
  --verdict-same: var(--sokra-grey-soft);
  --verdict-same-text: var(--sokra-ink);
  --verdict-order: var(--sokra-amber-soft);
  --verdict-order-text: var(--sokra-amber-ink);
  /* The class grid's five fills. One per state a teacher acts on, and none of
     them a status colour: the Class Struggles plan measured the success and
     warning surfaces edge to edge and found them the same colour for a
     red-green colour-blind reader, and a grid is nothing but edges. So secure
     is a teal tint against the warm slip and the two pinks — a blue-green
     against yellow and red is the pair that holds for that reader — and the
     two that share a hue are told apart by an edge and a glyph. Flat hex
     rather than color-mix, so theme_contrast_test can measure the glyph on
     each of them. */
  --cell-none: #eef1f4;
  --cell-secure: #c4e6e7;
  --cell-slip: #f6e2b3;
  --cell-open: #f9ccd9;
  --cell-persistent: #f29ab4;

  /* The mode colours — one per phase a pupil can be in, so which one they are
     in reads at a glance. The mapping is the guide's, under "Mode colours" in
     docs/brand/sokrabrandguidelines.md, and css_tokens_test holds this block
     to it: navy for where to start, purple for the recap, teal for practice,
     amber for review. The banner always says the phase in words as well; the
     colour is there to be recognised from across a classroom, not to carry
     the meaning alone.

     Three per phase: the fill, the ink on it, and the fill a hovered button
     goes to. The banner, the current step's edge, the pill over the question
     and the action buttons all take them — see .phase-banner. Amber needs navy
     ink where the other three take white. Every pairing, hovered or not, is
     measured by theme_contrast_test.

     ⚠️ THE SAME FOUR UNDER EVERY LOOK. The teacher's live screens (.live-grid,
     .live-wall, .live-peek, .live-pupil-status) draw where each pupil is in
     these colours, and a teacher who sees purple beside a name looks for the
     purple screen. Until 14 October 2026 each look drew its own four, so under
     Rocket ship the recap was blue and practice orange, and the teacher could
     not have known. A look now leaves these tokens alone, and checks/look-css.ts
     refuses any it sets. (A dark look copied the dark block's below until there
     were no dark looks; see "AND STANDARD IS LIGHT UNDER EVERY LOOK" at the top.) */
  --phase-baseline-bg: var(--sokra-navy);
  --phase-baseline-ink: var(--sokra-white);
  --phase-recap-bg: var(--sokra-purple);
  --phase-recap-ink: var(--sokra-white);
  --phase-practice-bg: var(--sokra-teal-strong);
  --phase-practice-ink: var(--sokra-white);
  --phase-review-bg: var(--sokra-amber);
  --phase-review-ink: var(--sokra-navy);
  --phase-challenge-bg: var(--sokra-sky);
  --phase-challenge-ink: var(--sokra-white);
  --phase-baseline-bg-hover: var(--sokra-navy-2);
  --phase-recap-bg-hover: var(--sokra-purple-ink);
  --phase-practice-bg-hover: var(--sokra-teal-ink);
  --phase-review-bg-hover: var(--sokra-amber-strong);
  --phase-challenge-bg-hover: var(--sokra-sky-ink);
  --phase-baseline-line: var(--sokra-navy);
  --phase-recap-line: var(--sokra-purple);
  --phase-practice-line: var(--sokra-teal-ink);
  --phase-review-line: var(--sokra-amber-deep);
  --phase-challenge-line: var(--sokra-sky-ink);

  /* Tints. color-mix with transparent is exactly an alpha of the primitive, so
     these carry the values the components used to write as rgba literals. The
     accent tints are amber, which is a wash the guide is happy with and a text
     colour it is not; nothing below sets type in one. The brand tints are
     teal, the colour the page already means "interactive" by.

     16%, not the 22% Honey had. Amber is heavier than Honey, and at 22% on the
     page ground it took muted text to 4.39:1 — a live marker on the teacher's
     pupil page, found by the axe pass, not by theme_contrast_test, which
     cannot see a wash. */
  --tint-accent-fill: color-mix(in srgb, var(--sokra-amber) 16%, transparent);
  --tint-accent-fill-soft: color-mix(in srgb, var(--sokra-amber) 11%, transparent);
  --tint-accent-line: color-mix(in srgb, var(--sokra-amber) 55%, transparent);
  --tint-accent-edge: color-mix(in srgb, var(--sokra-amber) 70%, transparent);
  --tint-brand-fill: color-mix(in srgb, var(--sokra-teal) 10%, transparent);
  --tint-brand-fill-soft: color-mix(in srgb, var(--sokra-teal) 7%, transparent);
  --tint-code: color-mix(in srgb, var(--sokra-teal) 7%, transparent);

  /* Depth. Whole box-shadow values, so a theme can change the geometry as well
     as the colour.

     ⚠️ CARDS CAST NO SHADOW. The guide's cards are flat and rely on their
     border, so --shadow-card is `none` in every palette but high contrast,
     which uses it for a 2px outline. Menus and dialogs keep theirs: they float
     over content, and a border alone does not lift something off the thing it
     is covering. Cast in navy rather than black, so a shadow stays in the
     palette's temperature instead of greying it. */
  --scrim: color-mix(in srgb, var(--sokra-navy) 48%, transparent);
  --shadow-menu: 0 6px 20px color-mix(in srgb, var(--sokra-navy) 14%, transparent);
  --shadow-card: none;
  --shadow-dialog: 0 18px 50px color-mix(in srgb, var(--sokra-navy) 24%, transparent);
  --shadow-open: 0 0 0 1px color-mix(in srgb, var(--sokra-amber) 55%, transparent);

  /* Tells the browser which way round the palette runs, so native form
     controls, scrollbars and spellcheck underlines follow it. This is a
     per-theme declaration: a dark palette must set `color-scheme: dark` or its
     native controls stay light against it. */
  color-scheme: light;
}

/* --- Themes --------------------------------------------------------------

   Each block redefines the semantic tier and nothing else. No selectors, no
   properties, no new rules — a theme is a palette, not a restyle, and
   test/assets/css_tokens_test.rb enforces that as well as checking every theme
   defines every token. A theme that forgets one silently inherits the default
   value, which is how you get white text on a white background.

   These are the palettes offered in Settings; Theme::ALL is the Ruby-side
   register and the tests hold the two in step. Contrast is checked per theme
   by test/assets/theme_contrast_test.rb, so a palette cannot ship below AA.

   Scoped by [data-theme], not :root[data-theme]. The page sets it on <html>,
   which is the normal case, but an element anywhere may carry it and everything
   inside repaints in that palette. The settings picker relies on this: each
   swatch renders in the theme it is advertising rather than in a hand-copied
   set of colours that could drift from the real one.

   The default palette is the base :root block above and needs no entry here.

   ⚠️ THE GUIDE DEFINES ONE PALETTE, AND THESE THREE ARE OURS. It gives the
   light theme and nothing else, so each block below is derived: the guide's
   hues kept wherever they hold, and taken lighter or darker only where a
   pupil's ability to read the screen requires it. Theme blocks may therefore
   name colours outright. */

[data-theme="dark"] {
  color-scheme: dark;

  /* Navy is the card, not a colour approximating one: the guide gives it as
     the colour of structure, and the ground is the same navy taken down until
     a card reads as lifted off it. */
  --surface: #17233c;
  --surface-sunken: #0f1829;
  --surface-raised: #223152;

  --text: #eef2f8;
  --text-muted: #c9d1e0;
  --text-inverse: #ffffff;
  --text-link: #9fe0e2;

  --border: #33456b;
  --border-strong: #9fe0e2;

  /* Amber again, and here it does double duty: the ink on it clears 7:1 AND
     it stands well off the card behind it, so the dark palette is the one
     where the highlight is unmissable on colour alone. */
  --read-aloud-bg: #e3a92c;
  --read-aloud-ink: #17233c;

  /* Cool where amber is warm, and lifted off the card rather than tinted into
     it: on a dark ground a fill has to be lighter than what it sits on or it
     reads as a hole in the sentence. */
  --word-bank-bg: #3a2f73;
  --word-bank-ink: #eeeafb;
  /* The action fill is too dark to read as a mark on navy; the light teal the
     dark selection uses is the same hue turned up. */
  --command-word-bg: color-mix(in srgb, #9fe0e2 16%, transparent);
  --command-word-ink: var(--text);
  --command-word-line: #9fe0e2;

  /* The access steps, lifted rather than darkened as the word bank is: the
     more a feature was used, the brighter the cell on a dark ground. */
  --use-low: #3a2f73;
  --use-low-ink: #eeeafb;
  --use-mid: #6a58c8;
  --use-mid-ink: #ffffff;
  --use-high: #d6ccff;
  --use-high-ink: #1a1240;
  /* Amber, not purple. The focus ring has to clear the page ground and the
     cards and the filled buttons, and purple on navy is barely there; the
     guide itself swaps to amber on a navy ground. */
  --focus-ring: #e3a92c;

  /* The teal and pink fills stay where the default palette has them. They
     carry white labels, and lightening either — the usual dark-theme move —
     drops that text under 4.5:1. Navy cannot be the primary fill on a navy
     page, so primary is a slate lifted off it — exactly as far as it can go
     and still carry a white label: 3.2:1 against the card, 4.95:1 for the
     label. The build copy's #3e4c6b was 1.8:1 against the card, a button
     with no edge. Measured by test/assets/theme_contrast_test.rb. */
  --primary-bg: #5f7096;
  --primary-bg-hover: #56688f;
  --action-bg: #127a7f;
  --action-bg-hover: #0f6c70;
  --action-ink: #ffffff;
  --danger-bg: #d42358;
  --danger-bg-hover: #b8184a;

  --accent: #e3a92c;
  --wordmark-accent: #9fe0e2;
  --lockup: #9fe0e2;
  --selected: #9fe0e2;
  --selected-surface: color-mix(in srgb, var(--selected) 18%, var(--surface));

  /* The mark's slots, lifted to read on navy; the own hue the same. */
  --icon-teal: #5cc9c4;
  --icon-violet: #b9a6ff;
  --icon-amber: #f0c25a;
  --icon-rose: #ff8fae;
  --icon-sky: #8fb4ff;
  --icon-moss: #7fd39a;
  --icon-own-l: 0.8;
  --icon-own-c: 0.11;
  --icon-disc-mix: 22%;
  --icon-ring: 0px;

  --success: #5cc9c4;
  --success-surface: #123a40;
  --success-text: #9fe0e2;
  --correct: #6fd68f;
  /* The same sheet under every theme — see the note in :root. Repeated
     rather than inherited because css_tokens_test refuses a theme that
     answers a token by silence: a plate that went dark here would be a
     diagram nobody can see, and that is worth saying four times. */
  --figure-plate: #ffffff;
  --warning: #e3a92c;
  --warning-surface: #3a2e12;
  --warning-text: #f5d48a;
  --warning-ink: #17233c;
  --error: #ff7fa0;
  --error-surface: #3a1826;
  --error-text: #ffb3c7;
  /* A word-level diff (Prompt_Arena_Plan.md §6): an insertion and a deletion.
     Never by colour alone — an insertion is underlined, a deletion struck through. */
  --diff-added-bg: #123a40;
  --diff-added-text: #9fe0e2;
  --diff-removed-bg: #3a1826;
  --diff-removed-text: #ffb3c7;
  /* A comparison's verdicts (Prompt_Arena_Plan.md §6): A better, B better, no
     difference, depends on order. Neither side is good or bad, so neither takes
     the success or the error hue; each verdict also carries a symbol. */
  --verdict-a: #8fb4ff;
  --verdict-a-text: #0f1829;
  --verdict-b: #c3a8ff;
  --verdict-b-text: #0f1829;
  --verdict-same: #26334f;
  --verdict-same-text: #eef2f8;
  --verdict-order: #3a2e12;
  --verdict-order-text: #f5d48a;
  /* The grid's five fills, on the dark ground — see the default block. */
  --cell-none: #223152;
  --cell-secure: #15565a;
  --cell-slip: #5c4714;
  --cell-open: #6e2440;
  --cell-persistent: #9a1f48;

  /* The phase fills — see the default block. Lifted off the navy card rather
     than sunk into it, so each carries navy ink here. */
  --phase-baseline-bg: #c9d1e0;
  --phase-baseline-ink: #17233c;
  --phase-recap-bg: #c9bdf5;
  --phase-recap-ink: #17233c;
  --phase-practice-bg: #9fe0e2;
  --phase-practice-ink: #17233c;
  --phase-review-bg: #e3a92c;
  --phase-review-ink: #17233c;
  --phase-challenge-bg: #8fb4ff;
  --phase-challenge-ink: #17233c;
  --phase-baseline-bg-hover: #b3bdd1;
  --phase-recap-bg-hover: #b3a3ef;
  --phase-practice-bg-hover: #84d3d6;
  --phase-review-bg-hover: #cf9423;
  --phase-challenge-bg-hover: #75a2fb;
  --phase-baseline-line: #c9d1e0;
  --phase-recap-line: #c9bdf5;
  --phase-practice-line: #9fe0e2;
  --phase-review-line: #e3a92c;
  --phase-challenge-line: #8fb4ff;

  --tint-accent-fill: color-mix(in srgb, var(--accent) 16%, transparent);
  --tint-accent-fill-soft: color-mix(in srgb, var(--accent) 11%, transparent);
  --tint-accent-line: color-mix(in srgb, var(--accent) 42%, transparent);
  --tint-accent-edge: color-mix(in srgb, var(--accent) 55%, transparent);
  --tint-brand-fill: color-mix(in srgb, var(--text-link) 18%, transparent);
  --tint-brand-fill-soft: color-mix(in srgb, var(--text-link) 13%, transparent);
  --tint-code: color-mix(in srgb, var(--text-link) 15%, transparent);

  /* Deeper and wider than the default palette: a shadow on a dark ground has
     far less luminance range to work with, so it needs more of it to read. */
  --scrim: color-mix(in srgb, #070b16 74%, transparent);
  --shadow-menu: 0 8px 24px color-mix(in srgb, #070b16 58%, transparent);
  --shadow-card: none;
  --shadow-dialog: 0 22px 60px color-mix(in srgb, #070b16 66%, transparent);
  --shadow-open: 0 0 0 1px color-mix(in srgb, var(--accent) 34%, transparent);
}

/* Warm. The cream grounds the old identity had, kept, with the new hues put on
   them. The default floats white cards above a pale grey; here the cards come
   down to meet the ground, because for a reader whose text shimmers or swims —
   dyslexia and Irlen syndrome among them — the glare is coming off the card,
   not the page. Contrast stays at AA; it is the harshness that comes down.

   Teal is taken one step darker than the default throughout, because cream is
   darker than white and the default's teal would drop under 4.5:1 on it. */
[data-theme="warm"] {
  color-scheme: light;

  --surface: #fbf3e3;
  --surface-sunken: #f3e7d0;
  --surface-raised: #fefaf1;

  --text: #26324a;
  --text-muted: #55607a;
  --text-inverse: #ffffff;
  --text-link: #0c6166;

  --border: #ddcaa6;
  --border-strong: #26324a;

  /* The underline carries this one here more than anywhere: amber on cream
     is a soft step, and that is the price of a palette with nothing bright in
     it. The ink on it is 7:1. */
  --read-aloud-bg: #e3a92c;
  --read-aloud-ink: #17233c;

  /* The one palette where the two fills could genuinely be confused — warm is
     amber throughout, and an amber highlight under an amber read-aloud would
     be one indistinguishable wash. Purple is as far from it as the guide
     goes, and it is the reason this token is authored per palette. */
  --word-bank-bg: #e9e2f7;
  --word-bank-ink: #3c2196;
  --command-word-bg: color-mix(in srgb, var(--action-bg) 13%, transparent);
  --command-word-ink: var(--text);
  --command-word-line: var(--action-bg);

  /* The access steps: purple is as far from warm's amber outcome cells as the
     guide goes, which is why it carries them here as well. */
  --use-low: #e9e2f7;
  --use-low-ink: #3c2196;
  --use-mid: #cbbff0;
  --use-mid-ink: #1f1442;
  --use-high: #3c2196;
  --use-high-ink: #ffffff;
  --focus-ring: #5b35d5;

  --primary-bg: #17233c;
  --primary-bg-hover: #223152;
  --action-bg: #0f6c70;
  --action-bg-hover: #0c585c;
  --action-ink: #ffffff;
  --danger-bg: #b8184a;
  --danger-bg-hover: #98133d;

  /* Amber taken down a little so it holds its own on cream. */
  --accent: #d9982a;
  --wordmark-accent: #0f6c70;
  --lockup: #55607a;
  --selected: #17233c;
  --selected-surface: color-mix(in srgb, var(--selected) 9%, var(--surface));

  /* The mark's slots on cream: a shade deeper than the light palette's. */
  --icon-teal: #0f6c70;
  --icon-violet: #4f2cc0;
  --icon-amber: #9a6a0e;
  --icon-rose: #b8184a;
  --icon-sky: #2358b5;
  --icon-moss: #166f3c;
  --icon-own-l: 0.45;
  --icon-own-c: 0.14;
  --icon-disc-mix: 16%;
  --icon-ring: 0px;

  --success: #127a7f;
  --success-surface: #ddefe9;
  --success-text: #0c5e62;
  --correct: #1b7d43;
  /* The same sheet under every theme — see the note in :root. Repeated
     rather than inherited because css_tokens_test refuses a theme that
     answers a token by silence: a plate that went dark here would be a
     diagram nobody can see, and that is worth saying four times. */
  --figure-plate: #ffffff;
  --warning: #c98a1a;
  --warning-surface: #f6e6c4;
  --warning-text: #6e4d0c;
  --warning-ink: #1b2538;
  --error: #b8184a;
  --error-surface: #f8dfe3;
  --error-text: #98133d;
  /* A word-level diff (Prompt_Arena_Plan.md §6): an insertion and a deletion.
     Never by colour alone — an insertion is underlined, a deletion struck through. */
  --diff-added-bg: #ddefe9;
  --diff-added-text: #0c5e62;
  --diff-removed-bg: #f8dfe3;
  --diff-removed-text: #98133d;
  /* A comparison's verdicts (Prompt_Arena_Plan.md §6): A better, B better, no
     difference, depends on order. Neither side is good or bad, so neither takes
     the success or the error hue; each verdict also carries a symbol. */
  --verdict-a: #17233c;
  --verdict-a-text: #ffffff;
  --verdict-b: #5b35d5;
  --verdict-b-text: #ffffff;
  --verdict-same: #f3e7d0;
  --verdict-same-text: #26324a;
  --verdict-order: #f6e6c4;
  --verdict-order-text: #6e4d0c;
  /* The grid's five fills — see the default block. */
  --cell-none: #f3e7d0;
  --cell-secure: #c1e0da;
  --cell-slip: #efd79e;
  --cell-open: #f4c4cc;
  --cell-persistent: #ec98ae;

  /* The phase fills — see the default block. Teal and amber one step down,
     as everywhere in this palette. */
  --phase-baseline-bg: #17233c;
  --phase-baseline-ink: #ffffff;
  --phase-recap-bg: #5b35d5;
  --phase-recap-ink: #ffffff;
  --phase-practice-bg: #0f6c70;
  --phase-practice-ink: #ffffff;
  --phase-review-bg: #d9982a;
  --phase-review-ink: #17233c;
  --phase-challenge-bg: #2358b5;
  --phase-challenge-ink: #ffffff;
  --phase-baseline-bg-hover: #223152;
  --phase-recap-bg-hover: #3c2196;
  --phase-practice-bg-hover: #0c585c;
  --phase-review-bg-hover: #c4861c;
  --phase-challenge-bg-hover: #1c4791;
  --phase-baseline-line: #17233c;
  --phase-recap-line: #5b35d5;
  --phase-practice-line: #0c6166;
  --phase-review-line: #6e4d0c;
  --phase-challenge-line: #2358b5;

  --tint-accent-fill: color-mix(in srgb, var(--accent) 13%, transparent);
  --tint-accent-fill-soft: color-mix(in srgb, var(--accent) 9%, transparent);
  --tint-accent-line: color-mix(in srgb, var(--accent) 36%, transparent);
  --tint-accent-edge: color-mix(in srgb, var(--accent) 48%, transparent);
  --tint-brand-fill: color-mix(in srgb, var(--text-link) 11%, transparent);
  --tint-brand-fill-soft: color-mix(in srgb, var(--text-link) 8%, transparent);
  --tint-code: color-mix(in srgb, var(--text-link) 9%, transparent);

  --scrim: color-mix(in srgb, #26324a 45%, transparent);
  --shadow-menu: 0 6px 20px color-mix(in srgb, #26324a 16%, transparent);
  --shadow-card: none;
  --shadow-dialog: 0 18px 50px color-mix(in srgb, #26324a 26%, transparent);
  --shadow-open: 0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent);
}

/* High contrast. Every surface is white, every edge is black, and the brand
   colours are darkened until each clears AA against white as text. Shadows are
   dropped in favour of borders, because a shadow is not a boundary to someone
   who needs this palette.

   This is the one-colour context: teal at any strength that would still read
   as teal next to black ink is a second colour competing with it, so the
   whole wordmark is set in the ink. */
[data-theme="high-contrast"] {
  color-scheme: light;

  --surface: #ffffff;
  --surface-sunken: #ffffff;
  --surface-raised: #ffffff;

  --text: #000000;
  --text-muted: #1c1c1c;
  --text-inverse: #ffffff;
  --text-link: #0a4a4e;

  --border: #000000;
  --border-strong: #000000;
  --focus-ring: #000000;

  /* INVERTED HERE, AND ONLY HERE. Every other palette lights the word with
     a warm fill under dark ink; on a pure white ground no fill is far
     enough from the page to be read as a highlight at a glance. Swapping ink
     and ground gives 21:1 against both the page and the words either side of
     it, which is what somebody who chose this palette came for. */
  --read-aloud-bg: #000000;
  --read-aloud-ink: #ffffff;

  /* The highlighter yellow, which is what this palette's users reach for a
     highlighter to get. Read-aloud is black on white here, so the two are as
     far apart as two fills can be, and 16.6:1 for the ink is the highest
     contrast of any pairing in the product. */
  --word-bank-bg: #ffe600;
  --word-bank-ink: #000000;
  /* No fill: high contrast says it with weight and a solid black line, and the
     yellow is the key words' and the black block the read-aloud's. */
  --command-word-bg: transparent;
  --command-word-ink: #000000;
  --command-word-line: #000000;

  /* The access steps, in this palette's way: white, grey, black. The grid's
     fills are all white here and the contrast is in the ink, so the three
     steps are told apart by their ink and edge as much as their fill. */
  --use-low: #ffffff;
  --use-low-ink: #000000;
  --use-mid: #d4d4d4;
  --use-mid-ink: #000000;
  --use-high: #000000;
  --use-high-ink: #ffffff;

  --primary-bg: #17233c;
  --primary-bg-hover: #000000;
  --action-bg: #0a4a4e;
  --action-bg-hover: #063033;
  --action-ink: #ffffff;
  --danger-bg: #8c1030;
  --danger-bg-hover: #690b24;

  --accent: #7a5200;
  --wordmark-accent: #000000;
  --lockup: #1c1c1c;
  --selected: #17233c;
  --selected-surface: color-mix(in srgb, var(--selected) 11%, var(--surface));

  /* The mark's slots, dark enough for 4.5:1 on white; no tint behind the
     shape (a tint is what this palette is chosen to avoid) and a 2px ring of
     the ink instead, so the disc still has an edge. */
  --icon-teal: #0a4a4e;
  --icon-violet: #3c2196;
  --icon-amber: #7a5200;
  --icon-rose: #8c1030;
  --icon-sky: #143f8a;
  --icon-moss: #0f5a2e;
  --icon-own-l: 0.35;
  --icon-own-c: 0.12;
  --icon-disc-mix: 0%;
  --icon-ring: 2px;

  --success: #0a4a4e;
  --success-surface: #ffffff;
  --success-text: #0a4a4e;
  --correct: #0b5d2b;
  /* The same sheet under every theme — see the note in :root. Repeated
     rather than inherited because css_tokens_test refuses a theme that
     answers a token by silence: a plate that went dark here would be a
     diagram nobody can see, and that is worth saying four times. */
  --figure-plate: #ffffff;
  --warning: #7a5200;
  --warning-surface: #ffffff;
  --warning-text: #5e3f00;
  --warning-ink: #ffffff;
  --error: #8c1030;
  --error-surface: #ffffff;
  --error-text: #750c28;
  /* A word-level diff (Prompt_Arena_Plan.md §6): an insertion and a deletion.
     Never by colour alone — an insertion is underlined, a deletion struck through. */
  --diff-added-bg: #ffffff;
  --diff-added-text: #0a4a4e;
  --diff-removed-bg: #ffffff;
  --diff-removed-text: #750c28;
  /* A comparison's verdicts (Prompt_Arena_Plan.md §6): A better, B better, no
     difference, depends on order. Neither side is good or bad, so neither takes
     the success or the error hue; each verdict also carries a symbol. */
  --verdict-a: #000000;
  --verdict-a-text: #ffffff;
  --verdict-b: #3c2196;
  --verdict-b-text: #ffffff;
  --verdict-same: #ffffff;
  --verdict-same-text: #000000;
  --verdict-order: #ffffff;
  --verdict-order-text: #5e3f00;
  /* The grid's five fills are all the page here, deliberately: this palette
     keeps every surface white and puts the contrast in the ink, so a cell
     says its state with its glyph and its edge and nothing else. */
  --cell-none: #ffffff;
  --cell-secure: #ffffff;
  --cell-slip: #ffffff;
  --cell-open: #ffffff;
  --cell-persistent: #ffffff;

  /* The phase fills — see the default block. Each darkened until white on it
     clears 6.9:1, and amber carries white here rather than navy. */
  --phase-baseline-bg: #17233c;
  --phase-baseline-ink: #ffffff;
  --phase-recap-bg: #3c2196;
  --phase-recap-ink: #ffffff;
  --phase-practice-bg: #0a4a4e;
  --phase-practice-ink: #ffffff;
  --phase-review-bg: #7a5200;
  --phase-review-ink: #ffffff;
  --phase-challenge-bg: #143f8a;
  --phase-challenge-ink: #ffffff;
  --phase-baseline-bg-hover: #000000;
  --phase-recap-bg-hover: #2a1670;
  --phase-practice-bg-hover: #063033;
  --phase-review-bg-hover: #5e3f00;
  --phase-challenge-bg-hover: #0d2c63;
  --phase-baseline-line: #17233c;
  --phase-recap-line: #3c2196;
  --phase-practice-line: #0a4a4e;
  --phase-review-line: #5e3f00;
  --phase-challenge-line: #0d2c63;

  --tint-accent-fill: color-mix(in srgb, var(--accent) 11%, transparent);
  --tint-accent-fill-soft: color-mix(in srgb, var(--accent) 8%, transparent);
  --tint-accent-line: color-mix(in srgb, var(--accent) 100%, transparent);
  --tint-accent-edge: color-mix(in srgb, var(--accent) 100%, transparent);
  --tint-brand-fill: color-mix(in srgb, var(--text-link) 11%, transparent);
  --tint-brand-fill-soft: color-mix(in srgb, var(--text-link) 8%, transparent);
  --tint-code: color-mix(in srgb, var(--text-link) 10%, transparent);

  --scrim: color-mix(in srgb, #000000 72%, transparent);
  --shadow-menu: 0 0 0 2px #000000;
  --shadow-card: 0 0 0 2px #000000;
  --shadow-dialog: 0 0 0 3px #000000;
  --shadow-open: 0 0 0 2px var(--accent);
}

/* --- Tier 3: type tokens -------------------------------------------------

   THE SECOND AXIS. A palette answers "what colour is this"; nothing above
   answered "how is it set", and for a good many readers that is the question
   that decides whether the page can be read at all.

   The colour tier is curated rather than composed, and the reason is written
   at the top of this file: contrast is a property of a *pairing*, so a free
   colour picker lets a pupil build something they cannot read. That argument
   does not transfer here, and it would be lazy to pretend it does. Bigger text
   is not made illegible by more leading; a narrower measure is not made
   illegible by a different face. Type has no pairings that fail — so type is
   composed, on five axes a pupil sets independently:

     face     which typeface, including one drawn for low vision
     size     a multiplier on the root, so everything sized in rem follows
     spacing  line height, letter spacing and word spacing together
     length   how long a line of prose is allowed to get
     case     whether the words are drawn as written, in capitals, in lower
              case or with each word capitalised

   Five attributes on <html>, not one. A pupil who needs the brand's face at
   twice the size, or the hyperlegible face at the default size, gets exactly
   that — which a single bundled "type theme" could not have given them without
   an option for every combination.

   The default column of every axis is the product exactly as it was before this
   tier existed, with one deliberate exception: --measure. Line length had no
   value at all, which is to say it was infinite, and an unbounded line on a
   wide monitor is the reading failure nobody files a bug about. The default is
   now 66ch. That is a change to every screen, and it is the point.

   WHAT A TYPE BLOCK MAY SET. Tokens, and only the tokens of its own axis. A
   face block that also set a size would make "large" mean something different
   depending on the face, which is the composability this tier exists to buy.
   test/assets/type_tokens_test.rb holds each block to its own axis, the way
   css_tokens_test.rb holds a palette to colour.

   The default is declared twice over — on :root and on its own axis value — for
   the same reason the default palette is: a preview swatch carries the axis
   value it is advertising, and without the second selector the "standard"
   swatch would match no block and silently inherit whatever is currently
   applied, showing the pupil their present setting under four different names.
   ------------------------------------------------------------------------- */

/* Face. All three roles move together. Offering the hyperlegible face for body
   text while leaving the headings in the brand face would be giving a pupil who
   asked for a legible page a page that is half legible — and the headings are
   the part they navigate by.

   THE DISPLAY ROLE OF THE `sokra` OPTION IS A LOOK'S FACE, and no other option
   reads it: --look-display-face is the brand face unless a look sets one, so a
   pupil with no look sees what they always saw, and a pupil who chose the
   hyperlegible face (or kept the retired plain one) gets that face in every
   look, because those blocks name their own and never reach for the look's.
   That is the whole precedence rule — a pupil's own setting outranks a look's
   — and checks/type-tokens.test.ts holds it. Body and label are not a look's:
   running text is read at length and no mockup measured a face for it. */
:root, [data-type-face="sokra"] {
  --font-display: var(--look-display-face);
  --font-body: var(--face-text);
  --font-label: var(--face-text);
}

/* ⚠️ RETIRED, AND KEPT. Under v1 this was the brand without its serif display
   face. Figtree has no serif to drop, so "plain" and "sokra" are now the same
   page and the picker no longer offers it — TypeStyle lists it as retired.
   The block stays because pupils saved it, and an attribute that matches no
   block would inherit whatever the page around it had; here that happens to
   be the same thing, but only by luck. */
[data-type-face="plain"] {
  --font-display: var(--face-text);
  --font-body: var(--face-text);
  --font-label: var(--face-text);
}

[data-type-face="hyperlegible"] {
  --font-display: var(--face-hyperlegible);
  --font-body: var(--face-hyperlegible);
  --font-label: var(--face-hyperlegible);
}

/* Size. A multiplier on the root font size and nothing else, which is the whole
   trick: this stylesheet sets exactly one font-size in px — on <html>, below —
   and 300-odd in rem. So every one of them scales together, and the ratios
   between a heading, a label and body copy are preserved instead of being
   re-picked four times.

   It multiplies 100% rather than replacing it with a pixel figure. 100% is
   whatever the reader set as their browser's default text size, so a pupil who
   has already turned text up in Chrome keeps that and this multiplies it —
   where a px root would have quietly thrown their setting away. That is WCAG
   1.4.4, and it is the reason no figure here is absolute. */
:root, [data-type-size="standard"] {
  --type-scale: 1;
}
[data-type-size="large"] {
  --type-scale: 1.125;
}
[data-type-size="larger"] {
  --type-scale: 1.25;
}
[data-type-size="largest"] {
  --type-scale: 1.5;
}

/* Spacing. Three properties on one axis because they are one decision: a reader
   who needs the lines further apart needs the words further apart too, and
   asking them the same question three times is how a settings screen stops
   being used.

   `normal` rather than 0 at the standard setting, so the default is the
   browser's own spacing and the faces' kerning is untouched — this axis adds
   nothing at all until it is asked to.

   The roomiest column is not a guess. WCAG 1.4.12 Text Spacing names the
   figures a page must survive being given: line height 1.5x, letter spacing
   0.12em, word spacing 0.16em. The product is already tested against them being
   applied from outside — see accessibility_test.rb — so this offers them from
   inside, where a pupil can actually reach them without a browser extension. */
:root, [data-type-spacing="standard"] {
  --line-height: 1.55;
  --tracking: normal;
  --word-spacing: normal;
}
[data-type-spacing="roomy"] {
  --line-height: 1.7;
  --tracking: 0.02em;
  --word-spacing: 0.08em;
}
[data-type-spacing="roomiest"] {
  --line-height: 1.9;
  --tracking: 0.12em;
  --word-spacing: 0.16em;
}

/* Length. In ch, so it is a count of characters rather than of pixels — which
   means it stays the right measure after the size axis has scaled everything
   and after the face axis has changed how wide a character is. A line length in
   rem would have been none of those things.

   66ch is the middle of the range typography has agreed on for a century and
   accessibility guidance repeats (WCAG 1.4.8 asks for no more than 80). `full`
   is kept as an option rather than removed: a teacher reading a wide table of a
   class's work is not reading prose, and the measure is not doing them a
   favour. */
:root, [data-type-length="comfortable"] {
  --measure: 66ch;
}
[data-type-length="narrow"] {
  --measure: 52ch;
}
[data-type-length="full"] {
  --measure: none;
}

/* Case. The fifth axis: letter case (docs/plan/Text_Case_Setting_Plan.md §3).
   These four blocks are the token set, like every other axis's; the rule that
   acts on them is just below the last of them, and is the one place in this
   file where a setting is applied by force rather than read through a token.
   Declared on :root and on its own axis value for the reason given at the top
   of this tier: the preview sample carries data-type-case="normal". */
:root, [data-type-case="normal"] {
  --text-case: none;
}
[data-type-case="upper"] {
  --text-case: uppercase;
}
[data-type-case="lower"] {
  --text-case: lowercase;
}
[data-type-case="capitalised"] {
  --text-case: capitalize;
}

/* THE RULE THAT APPLIES IT, AND THE ONLY !important A SETTING GETS. Around forty
   component rules above and below this one set `text-transform: uppercase` (rail
   labels, table heads, tags, chips, the wordmark) and a few set `none`. "Every
   word on the screen" means a pupil who chose lower case gets lower-case labels
   too, and without !important each of those rules wins on specificity. The
   alternative, editing forty rules to read --text-case, would be undone by the
   forty-first that somebody writes next month; this holds for that one too.
   checks/text-case.test.ts fails on any other text-transform that is !important,
   which is the one way a later rule could defeat it, and on an inline
   textTransform, which React cannot make !important and so would silently beat
   nothing.

   UNDER `normal` THE RULE DOES NOT EXIST, which is what the :not() is for.
   Normal means "as designed", and the design has capitals in it; forcing `none`
   there would have flattened every label the stylesheet sets in capitals.

   FOUR SELECTORS, because `*` does not reach a generated box or a placeholder:
   ::before and ::after carry content (counters, "required" marks, arrows made of
   words), and ::placeholder is neither a child nor a descendant. `body` itself
   is listed so text directly in it is covered. Every element is named, not left
   to inherit, because inheriting would let each of the forty rules above stop
   the value at its own element.

   THE EXEMPTION (§4.1) restates the same rule with `none`, at one more class of
   specificity, so it wins whatever order the two are in. sub, sup and .math come
   from the content markup; [data-case="keep"] is the marker for everything else
   whose case is its meaning: symbols found in prose (lib/sci-symbols.ts, drawn
   by SciText and Markup), equation tiles, join and demo codes, and what a pupil
   wrote where a teacher reads it (PupilWords). It does not go on people's names
   or on avatar initials. Its descendants are exempt too, and so are their
   generated boxes, which is why ::before and ::after appear in it as well.

   THE PREVIEW SAMPLE is a nested axis value: the Settings page draws one card per
   option, each carrying its own data-type-case, so --text-case on a card is that
   option's. The forced rule above is keyed on <html>, and would paint every card
   in the page's own case, so "Normal" would show capitals to a pupil who chose
   capitals. The card's own rule is one element more specific, applies under every
   root value, and reads the card's token (`none` included). It is the trap the
   default-declared-twice comment above describes, one level down.

   THE QUICK MENU'S CASE GLYPH is the same thing at one more place: each segment
   of the letter-case row is "Aa bb" carrying its own data-type-case, so it shows
   the case it saves and not the page's. It rides in this rule rather than a
   rule of its own so the one !important text-transform guard
   (checks/text-case.test.ts, item 3) still has a single shape to recognise.
   Specificity is html + body + one class + one attribute, (0,2,2), over the
   page rule's :root:not([…]) body * at (0,2,1); the glyph has no children, so
   it needs no `*` line. */
:root:not([data-type-case="normal"]) body,
:root:not([data-type-case="normal"]) body *,
:root:not([data-type-case="normal"]) body ::before,
:root:not([data-type-case="normal"]) body ::after,
:root:not([data-type-case="normal"]) body ::placeholder {
  text-transform: var(--text-case) !important;
}
:root:not([data-type-case="normal"]) body :is(sub, sup, .math, [data-case="keep"]),
:root:not([data-type-case="normal"]) body :is(sub, sup, .math, [data-case="keep"]) *,
:root:not([data-type-case="normal"]) body :is(sub, sup, .math, [data-case="keep"])::before,
:root:not([data-type-case="normal"]) body :is(sub, sup, .math, [data-case="keep"])::after,
:root:not([data-type-case="normal"]) body :is(sub, sup, .math, [data-case="keep"]) *::before,
:root:not([data-type-case="normal"]) body :is(sub, sup, .math, [data-case="keep"]) *::after {
  text-transform: none !important;
}
html body .type-sample[data-type-case],
html body .type-sample[data-type-case] *,
html body .quick-glyph--case[data-type-case] {
  text-transform: var(--text-case) !important;
}

*, *::before, *::after { box-sizing: border-box; }

/* The root carries the size axis, and it is the only element in this file given
   a font-size that is not a rem. Everything else is a ratio to this one figure,
   which is what lets a single token resize the whole product coherently.

   100%, never a pixel value: see the size axis above for why throwing away the
   reader's own browser default would be a WCAG 1.4.4 failure dressed up as a
   feature. */
html { font-size: calc(100% * var(--type-scale)); }

body {
  margin: 0;
  background: var(--look-ground);
  color: var(--text);
  font-family: var(--font-body);
  /* 1rem, not the 16px this used to be. That px figure was what made a size
     axis impossible: it pinned body copy at 16 while all 300-odd other sizes in
     the file were rems that would have scaled around it, so turning the text up
     made every label grow and the actual reading text stay put.

     1rem rather than either of the guide's body sizes. It gives 15px for a
     teacher and 17px for a pupil, and most screens here are neither one
     person's nor the other's by layout — so the teacher shell takes its 15
     and a pupil's answer options take their 17, each on the element that
     knows whose it is, and everything unmarked stays between them. */
  font-size: 1rem;
  line-height: var(--line-height);
  letter-spacing: var(--tracking);
  word-spacing: var(--word-spacing);
  -webkit-font-smoothing: antialiased;
}

/* --- The measure ---------------------------------------------------------

   Prose only, and named one by one rather than applied to every <p> in the
   product. A measure is a promise about *reading*: it belongs on the question
   stem, the tutor's reply, the sentence under a heading. Applied indifferently
   it would also narrow the inside of a table cell, a card in a grid and a form
   hint that is already inside a narrow column, and each of those would be a
   layout bug filed against an accessibility feature.

   max-inline-size rather than max-width, because Arabic and Urdu are offered
   and the constraint is on the line, not on the left-to-right axis.

   Anything added here should be something a pupil reads a sentence of. If it is
   a label, a number or a control, it does not belong. */
.lede,
.setting-help,
.question h1,
.question-stem,
.feedback p,
.coach-line,
.statement p,
.msg {
  max-inline-size: var(--measure);
}

/* --- Type styles ---------------------------------------------------------

   Figtree carries the headings, bold, at the guide's sizes. v1 set them in a
   one-weight serif a step larger than this to make up for having no bold;
   with a bold to lead the eye, the guide's smaller figures are enough, and
   they leave the question more of the screen.

   No letterspacing. v1 opened it slightly for a Palatino-lineage serif; a
   grotesque at bold wants none. */

h1, h2, h3 { font-family: var(--font-display); font-weight: var(--weight-display); }
h1 { font-size: var(--size-page-title); line-height: 1.25; margin: 0 0 .5rem; }
h2 { font-size: var(--size-card-title); line-height: 1.3; margin: 0 0 .75rem; }
h3 { font-size: 1rem; margin: 0 0 .5rem; }

/* THE LABEL FLOOR — about .82rem, and sentence case.

   Every label, phase name, exit and state in the pupil journey used to be set
   between .6rem and .68rem: 9.6 to 10.9 pixels, uppercase, letterspaced and
   muted. Contrast passed. Size and case did not, and neither is something a
   contrast test can see.

   Uppercase takes away the ascender-and-descender shape a word is recognised
   by, which is the cue a dyslexic reader leans on hardest — and this product
   ships four authored palettes BECAUSE dyslexia and Irlen syndrome are SEND
   requirements here. The same care had not reached the type. So the pupil
   journey's labels come up to about 13px and lose the transform, and the
   tracking goes with it: letterspacing is a fix for caps, and sentence case
   set with it just reads loose.

   The exception is a genuine chip — a word or two in a pill or a badge, where
   caps are the shape of the thing rather than a style. Those stay uppercase at
   about .78rem with the tracking cut back.

   Out of scope, deliberately: the glass box, the teacher app, the calendar
   internals and the brand straplines. A strapline is set the way the guide
   sets it, and the other three are not the journey a pupil walks. */

.muted { color: var(--text-muted); font-size: .9rem; }
a { color: var(--text-link); }
code {
  font-family: var(--font-mono); font-size: .88em;
  background: var(--tint-code); padding: .1em .35em; border-radius: 3px;
}

/* --- Masthead ------------------------------------------------------------

   The wordmark is set, not placed. SOKRA in Figtree 800, all caps, tracked
   to about 0.14em, the O in teal and the rest in the ink — and setting it in
   type is the only way one mark can follow four palettes: it has to go white
   on the dark ground and drop to a single colour in high contrast, neither of
   which a PNG does. The guide has no mark of its own, so this keeps v1's
   structure in the v2 face; docs/brand/sokrabrandguidelines.md says so.
   app/assets/images/sokra-mark.png is the O on navy, which is as much of the
   mark as a favicon has room for.

   Clearspace: the guide asks for the height of the S, kept clear on all sides.
   At this size that is what the masthead padding comes to. */

.masthead { background: var(--surface); border-bottom: 1px solid var(--border); }
.masthead-inner {
  max-width: 52rem; margin: 0 auto;
  padding: .85rem var(--clearspace);
  display: flex; align-items: center; gap: .9rem;
}
.brand { text-decoration: none; display: block; }
/* ⚠️ --face-display, NOT --font-display. The one place in this file that reaches
   past the type tier to the primitive, and the reason is the same one that gave
   the tracking below its own token: this is a MARK, not text somebody reads. It
   is set in Figtree, and an identity that changes shape with a reading
   preference has stopped being an identity — it would also mean two pupils
   comparing screens could not agree on what the product looks like.

   The line under it, the headings and every other word on the page do follow the
   face axis. The distinction is deliberate and narrow: five letters of a proper
   noun that is announced to a screen reader as "Sokra" regardless, against
   everything a pupil is actually reading.

   The size axis is NOT excluded — this is in rem and grows with everything else,
   because a mark that stayed small while the page grew would look like a bug. */
.wordmark {
  display: block; color: var(--text);
  font-family: var(--face-display); font-weight: 800;
  font-size: 1.45rem; line-height: 1.1;
  text-transform: uppercase; letter-spacing: var(--wordmark-tracking);
}
/* The one letter nothing else is allowed to recolour. */
.wordmark-o { color: var(--wordmark-accent); }
/* The co-brand lockup. Sokra travels with both parents or with neither, so
   this line belongs to the mark rather than being a strapline that could be
   dropped to save a row. Light teal on a dark ground, muted ink on a light
   one — which is what --lockup carries, per palette. */
.masthead .strap {
  font-family: var(--font-label); font-size: .58rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .1em;
  color: var(--lockup); display: block; margin-top: .2rem;
}
.masthead .spacer { flex: 1; }
.masthead .who-label { font-family: var(--font-label); font-size: .78rem;
                       text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); }

/* Profile disclosure. A <details> gives a keyboard-accessible menu with no
   JavaScript; a small Stimulus controller only adds click-away and Escape. */
.profile { position: relative; }
.profile > summary {
  display: flex; align-items: center; gap: .5rem;
  cursor: pointer; list-style: none; padding: .3rem .4rem .3rem .3rem;
  border: 1px solid transparent; border-radius: var(--radius-pill);
}
.profile > summary::-webkit-details-marker { display: none; }
.profile > summary:hover { border-color: var(--border); background: var(--surface-sunken); }
.profile[open] > summary { border-color: var(--border); background: var(--surface-sunken); }
.avatar {
  display: grid; place-items: center; width: 28px; height: 28px; border-radius: 50%;
  background: var(--action-bg); color: var(--action-ink);
  font-family: var(--font-display); font-size: .9rem; font-weight: var(--weight-display);
}
.profile-menu {
  position: absolute; right: 0; top: calc(100% + .4rem); z-index: 20;
  min-width: 13rem; padding: .3rem;
  background: var(--surface-raised); border: 1px solid var(--border);
  border-radius: var(--radius-md); box-shadow: var(--shadow-menu);
}
.profile-menu form.button_to { display: block; margin: 0; }
button.menu-item {
  width: 100%; background: none; border: 0; cursor: pointer;
  display: flex; align-items: center; gap: .6rem; text-align: left;
  padding: .5rem .6rem; border-radius: var(--radius-sm); color: var(--text);
  font-family: var(--font-body); font-size: .88rem;
}
button.menu-item:hover { background: var(--surface-sunken); color: var(--text-link); }
/* Sign out is the one item that ends something, so it picks up the error
   colour on hover rather than the interactive one — the same distinction the
   buttons make between .action and .danger. */
button.menu-item--leave:hover { color: var(--error-text); }
/* Stroked with currentColor, so an icon follows its label's colour into every
   theme and on hover, with no per-theme icon set to maintain. */
.icon {
  width: 1.05rem; height: 1.05rem; flex: none;
  opacity: .75;
}
.menu-item:hover .icon { opacity: 1; }

/* The separator above Sign out groups the three navigation items together and
   sets the one that leaves apart from them. */
.profile-menu form.button_to:last-child { border-top: 1px solid var(--border); margin-top: .25rem; padding-top: .25rem; }

/* The quick appearance rows (components/QuickAppearance.tsx): a pill of icon
   buttons per setting, the pressed one in the action colours. The menu is
   --surface-raised, so the pill is the sunken surface and reads as a well in it.
   Four segments at the largest text size still fit in 13rem because a segment
   holds a glyph and no word. */
.profile-menu form.quick-appearance { display: block; margin: 0 0 .3rem; }
/* The rows go before Settings, and the last of them sets the two apart from
   the navigation items below, as Sign out is set apart from them. */
.profile-menu form.quick-appearance + form.button_to {
  border-top: 1px solid var(--border); margin-top: .25rem; padding-top: .25rem;
}
.quick-segments {
  display: flex; gap: 2px; padding: 3px;
  border-radius: var(--radius-md); background: var(--surface-sunken);
}
.quick-segment {
  flex: 1; min-height: 2.25rem; border: 0; background: none; cursor: pointer;
  display: grid; place-items: center;
  border-radius: var(--radius-sm); color: var(--text);
  font-family: var(--font-body);
}
.quick-segment:hover { background: var(--surface); }
.quick-segment[aria-pressed="true"] { background: var(--action-bg); color: var(--action-ink); }
.quick-segment:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* Fixed steps rather than --type-scale: at icon size the real scale (1 to 1.5)
   is too subtle to tell the four apart, and the glyph is not text to be read. */
.quick-glyph { line-height: 1; font-weight: 600; }
.quick-glyph--standard { font-size: .8rem; }
.quick-glyph--large { font-size: .95rem; }
.quick-glyph--larger { font-size: 1.1rem; }
.quick-glyph--largest { font-size: 1.3rem; }
/* The case row's glyph is two words ("Aa bb", so Capitalised differs from
   Normal), and a segment is narrower than two words at body size: it wrapped to
   "Aa / bb". One line, a step smaller than the size row's smallest. */
.quick-glyph--case { font-size: .72rem; white-space: nowrap; }
/* A palette's segment: a card with a line of text and a button on it, every
   colour resolved under the swatch's own data-theme, so it shows the palette it
   saves and not the one the page is in. The border is the palette's own too,
   which is what tells Standard from High contrast at this size. */
.quick-swatch {
  display: grid; align-content: center; gap: 3px;
  width: 1.6rem; height: 1.25rem; padding: 0 4px;
  background: var(--surface); border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
}
.quick-swatch::before, .quick-swatch::after {
  content: ""; display: block; height: 3px; border-radius: 2px;
}
.quick-swatch::before { background: var(--text); }
.quick-swatch::after { width: 60%; background: var(--action-bg); }
/* The scenery row's glyph: a hill and a sun, struck through when it is off. The
   look itself has no row here: fifteen swatches in a menu thirteen rem wide
   were a wall of colour nobody could tell apart, and the Look tab in Settings
   is where a look is chosen, with a name and a miniature each. */
.quick-glyph--scenery, .quick-glyph--pictures { display: block; }
.quick-appearance-alert {
  margin: .3rem .2rem 0; font-size: .8rem; color: var(--error-text);
}

/* --- Shell --------------------------------------------------------------- */

.auth, .practice, .dashboard {
  max-width: 52rem; margin: 0 auto;
  padding: 2.5rem var(--clearspace) 4rem;
}

/* ⚠️ ONE COLUMN AT REST, SINCE 15 SEPTEMBER 2026. The chat used to sit here
   permanently, a 21rem rail beside the question holding a conversation that on
   most turns nobody had started — so a 656px question sat in a 1088px grid
   with a third of it given to an empty box. Every Sparx screen is one question
   on a card, and that is most of why theirs reads calm.

   The chat is not gone and nothing about it changed: same conversation, same
   modes, same monitoring notice, same place in the markup. It opens from a
   button now instead of being always open — see chat_panel_controller, which
   CLOSES a panel the page ships open, so that with JavaScript off the tutor is
   simply there as it always was.

   The grid keeps its second track, empty, for the panel to sit in when it
   opens: laid out that way the question does not reflow underneath the pupil
   at the moment they ask for help, which is the one moment reflowing it would
   be worst. */
/* ⚠️ THE RECAP IS ON THIS RULE TOO, AND USED TO HAVE ITS OWN. Its grid was
   permanently two columns — `minmax(0, 1fr) 21rem` inside 68rem — so the
   teaching sat pushed to the left of the window with a 21rem strip of nothing
   beside it whether or not the tutor was open, and the screen read as
   off-centre because it was. It is the same screen shape as a question with
   the same panel on it, so it is the same rule: centred and alone at rest,
   growing a second track when somebody asks for the tutor. */
.practice-grid,
.recap-grid {
  max-width: 52rem; margin: 0 auto;
  padding: 2.5rem var(--clearspace) 4rem;
  display: grid; gap: 2.5rem;
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
}
/* ⚠️ THE QUESTION KEEPS ITS WIDTH WHEN THE PANEL OPENS, and that is what the
   two numbers here are for. One column at 68rem looked like the room being
   used and was not: it put a written answer in a box a thousand pixels wide,
   which is twice a readable measure and nothing anybody writes a sentence in.
   So the question column is 52rem in both states and the page simply grows a
   second track beside it — the pupil asking for help does not watch the
   question they are asking about reflow underneath them. The first screenshot
   is what found this. */
.practice-grid:has(.practice-side:not([hidden])),
.practice-grid:has(> .chat:not([hidden])),
.recap-grid:has(> .chat:not([hidden])) {
  max-width: 75.5rem;
  grid-template-columns: minmax(0, 52rem) 21rem;
}
@media (max-width: 78rem) {
  .practice-grid:has(.practice-side:not([hidden])),
  .practice-grid:has(> .chat:not([hidden])),
  .recap-grid:has(> .chat:not([hidden])) {
    max-width: 68rem; grid-template-columns: minmax(0, 1fr) 21rem;
  }
}
@media (max-width: 62rem) {
  .practice-grid:has(.practice-side:not([hidden])),
  .practice-grid:has(> .chat:not([hidden])),
  .recap-grid:has(> .chat:not([hidden])) {
    grid-template-columns: minmax(0, 1fr); gap: 2rem;
  }
}
/* The way in. Quiet rather than an action colour: asking is offered, not
   urged, and a loud button next to Check my answer would compete with the
   thing the pupil came to do. */
/* The way in and the way out, ON the question's own tools row beside Read
   aloud, Key words and Explain it differently — which is why it joins their
   rule above rather than carrying its own margins. It began under Check my
   answer, which put "I am stuck" after "I am finished"; then on a line of its
   own under the row, because a plain button next to three inline-flex ones is
   not on the same row, it is merely near it.

   ⚠️ IT IS A TOGGLE, and says which way it will go. A button that opens a
   panel and then goes on saying "Ask the tutor" while the panel is open is a
   button whose label describes the room rather than the door — see
   chat_panel_controller, which swaps the word and the aria-expanded together
   because a reader who is told "expanded" and reads "Ask the tutor" has been
   told two different things. */
.chat-launcher {
  /* The same square as report and the data sheet beside it — it is a picture
     with a name now rather than a word. See practice/_ask_the_tutor. */
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.1rem; height: 2.1rem; padding: 0;
  border: 1.5px solid var(--border-strong); border-radius: var(--radius-pill);
  background: var(--surface); color: var(--text);
  font-size: 1rem; line-height: 1; cursor: pointer;
}
.chat-icon { display: inline-flex; font-size: 1rem; line-height: 1; }
.chat-launcher:hover { background: var(--surface-sunken); }
.chat-launcher[aria-expanded="true"] { background: var(--surface-sunken); }
/* The way out, in the panel's own head beside the mode chip. */
.chat-close {
  margin-inline-start: auto; padding: .2rem .6rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text-muted);
  font-family: var(--font-label); font-size: .78rem; cursor: pointer;
}
.chat-close:hover { color: var(--text); border-color: var(--border-strong); }
/* A screen with no tutor on it uses the room. The review check shuts the chat
   deliberately — it is measuring what a pupil can do unaided. Now that one
   column is the resting state this only narrows the page, which is the half of
   the old rule that still applies: a question with no tutor coming has no
   reason to leave room for one. Keyed off the panel rather than off the screen,
   so any screen that drops the tutor gets the same treatment without anyone
   remembering this rule. */
/* A screen with no tutor coming is already at the resting width, so this rule
   has nothing left to do — kept as the statement of intent it always was, and
   as the thing that would still narrow the page if the resting width ever
   grew. */
.practice-grid:not(:has(.chat)) { max-width: 52rem; }

/* The reference shelf — the periodic table or the equations, as the exam puts
   them on the desk. Quiet like the report line beside it: it is a thing to
   reach for, not a thing to be urged towards. See shared/_reference_shelf. */
.shelf { display: inline-flex; }
/* The same square as the report button beside it — see .report-open. The
   command words' button is the same square, under its own name: a page that
   draws one draws no data sheet unless it has one. */
.shelf-open, .commands-open {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.1rem; height: 2.1rem; padding: 0; border: 1.5px solid var(--border-strong); border-radius: var(--radius-pill);
  background: var(--surface); color: var(--text);
  font-family: var(--font-label); font-size: .85rem; cursor: pointer;
}
.shelf-open:hover, .commands-open:hover { background: var(--surface-sunken); }
.shelf-dialog, .commands-dialog {
  /* Wider than the report dialog and allowed to use the window: eighteen
     groups across is the one thing on this screen that genuinely needs room. */
  max-width: 68rem; width: calc(100% - 2rem);
  padding: 1.5rem; border: var(--look-border) solid var(--border); border-radius: calc(var(--look-radius) + 2px);
  background: var(--surface); color: var(--text);
}
.shelf-dialog::backdrop, .commands-dialog::backdrop { background: var(--scrim); }
.shelf-head { display: flex; align-items: baseline; gap: 1rem; margin-bottom: 1rem; }
.shelf-heading { margin: 0; font-size: 1.25rem; }
.shelf-head .quiet { margin-inline-start: auto; }
/* The table is the one thing in this product allowed to scroll sideways: at
   eighteen columns there is no width at which it wraps and stays a table. */
.shelf-scroll { overflow-x: auto; }
.shelf-table { border-collapse: collapse; font-family: var(--font-label); }
.shelf-table th {
  padding: .2rem .35rem; font-size: .7rem; font-weight: 600; color: var(--text-muted);
}
.shelf-table td { padding: 1px; }
.el {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  min-width: 3rem; min-height: 3rem; padding: .15rem;
  border: 1px solid var(--border); border-radius: 4px; background: var(--surface-sunken);
}
.el-mass { font-size: .58rem; color: var(--text-muted); line-height: 1.1; }
.el-symbol { font-size: .95rem; font-weight: 700; line-height: 1.2; }
.el-z { font-size: .58rem; color: var(--text-muted); line-height: 1.1; }
.shelf-note { margin: .8rem 0 0; font-size: .82rem; color: var(--text-muted); }
.shelf-subheading { margin: 1.2rem 0 .4rem; font-family: var(--font-label); font-size: .85rem; }
.shelf-equations { margin: 0; padding: 0; list-style: none; }
.shelf-equations li {
  display: flex; flex-wrap: wrap; gap: .3rem 1rem; align-items: baseline;
  padding: .4rem 0; border-bottom: 1px solid var(--border);
}
.eq-symbols { min-width: 11rem; font-weight: 600; }
.eq-words { flex: 1; font-size: .88rem; color: var(--text-muted); }
@media (scripting: none) {
  .shelf-dialog, .commands-dialog { display: block; position: static; margin-top: 1rem; }
}

/* The command words — the board's list, beside the data sheet because it is
   the same kind of thing: what the exam itself gives a pupil to go on, never a
   hint. The button is the shelf's square, and the dialog the shelf's panel at
   a reading width, since this one is words and not a table. */
.commands { display: inline-flex; }
.commands-dialog { max-width: 40rem; }
.commands-intro { margin: 0 0 1rem; color: var(--text-muted); }
.commands-group { margin: 0 0 1.1rem; }
.commands-group:last-child { margin-bottom: 0; }
.commands-group h3 {
  margin: 0 0 .4rem; font-family: var(--font-label); font-size: .85rem;
  color: var(--text-muted);
}
.commands-list { margin: 0; }
.commands-list > div {
  display: grid; grid-template-columns: minmax(6.5rem, max-content) 1fr; gap: .2rem 1rem;
  padding: .45rem 0; border-bottom: 1px solid var(--border);
}
.commands-list > div:last-child { border-bottom: 0; }
.commands-list dt { font-weight: 700; }
.commands-list dd { margin: 0; }
/* The words this screen's question uses, first and set apart: the reason the
   pupil pressed the button is nearly always the word in front of them. Drawn in
   the mark's own colours, so the dialog and the sentence say they are the same
   words. */
.commands-group--here {
  padding: .6rem .9rem; border-radius: var(--radius-sm);
  background: var(--command-word-bg); border: 1px solid var(--command-word-line);
}
.commands-group--here dt { text-decoration: underline solid var(--command-word-line) 2px; text-underline-offset: 3px; }
/* One word per row on a phone: a definition column eleven characters wide
   would be a word a line. */
@media (max-width: 30rem) {
  .commands-list > div { grid-template-columns: 1fr; }
}

/* "Something wrong with this question?" — the defect channel. Quiet and last on
   the column: it is not what the pupil came to do, and a button competing with
   Check my answer invites somebody who is stuck to decide the question is
   broken rather than that they are stuck. See shared/_report_a_problem. */
/* One square, not a sentence. It was a whole line under every question on
   every screen; a warning triangle says the same thing in 36px.
   ⚠️ THE COLOUR IS NOT THE MESSAGE. The triangle is, which is what keeps this
   inside the brand rule that nothing may be said by colour alone — a reader
   who cannot tell the red from the page still has a warning shape and a name
   on the button. Restrained rather than filled: this marks a thing a pupil MAY
   do, not a thing that has gone wrong, and a solid red block under a question
   reads as the question having failed. */
.report { display: inline-flex; }
.report-open {
  display: inline-flex; align-items: center; justify-content: center;
  /* Square, and the same height as the pill-shaped controls it now sits
     between, so the row has one baseline rather than five. */
  width: 2.1rem; height: 2.1rem; padding: 0;
  /* The row's own grey, not the error red it wore until 9 October 2026: red
     on this screen means something has gone wrong, and a red-edged triangle
     beside every question read as a warning about the question rather than
     a way to report one. The dialog behind it is where the seriousness goes. */
  border: calc(var(--look-border-control) * 1.5) solid var(--border-strong); border-radius: calc(var(--look-radius-control) + 2px);
  background: var(--surface); color: var(--text); cursor: pointer;
}
.report-open:hover { background: var(--surface-sunken); }
/* ⚠️ THIS TIP IS RIGHT-ALIGNED, NOT CENTRED ON ITS BUTTON, BECAUSE THE BUTTON IS
   THE LAST THING IN THE ROW. The tip is drawn at opacity 0 until hover, but it
   still has a box, and a box wider than the room beside the button makes the page
   scroll sideways for the sake of words nobody can see. "Report question" fits
   at the size it was drawn for and not in capitals: with the letter case on
   `upper` and every type axis turned up the page was 5px too wide, and at 320
   pixels, in any case, 15px. The sweep (viewport.ts) named it. */
.report-open.icon-button .tip { left: auto; right: 0; transform: none; }
.report-icon { width: 1.15rem; height: 1.15rem; }
.shelf-icon, .commands-icon { width: 1.15rem; height: 1.15rem; }
.report-dialog {
  max-width: 32rem; width: calc(100% - 2rem);
  padding: 1.5rem; border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--surface); color: var(--text);
}
.report-dialog::backdrop { background: var(--scrim); }
.report-heading { margin: 0 0 .4rem; font-size: 1.25rem; }
.report-note { margin: 0 0 1rem; font-size: .9rem; color: var(--text-muted); }
.report-reasons { margin: 0 0 1rem; padding: 0; border: 0; }
.report-reasons legend { padding: 0; font-family: var(--font-label); font-size: .82rem; }
.report-reason { display: flex; gap: .5rem; align-items: baseline; padding: .35rem 0; cursor: pointer; }
.report-label { display: block; font-family: var(--font-label); font-size: .82rem; margin-bottom: .3rem; }
.report-form textarea { width: 100%; }
.report-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; margin-top: 1rem; }
/* The dialog is rendered inline with no JavaScript — see report_controller —
   so it needs to read as a panel rather than as a modal that failed to open. */
@media (scripting: none) {
  .report-dialog { display: block; position: static; margin-top: 1rem; }
}

/* --- Login --------------------------------------------------------------- */

.auth { max-width: 26rem; padding-top: 3rem; text-align: center; }
/* The full lockup, at the one size in the product where the mark is the
   subject of the screen rather than the way back to the top of it. On the
   chooser and the doors this element is the h1, so the heading margins are
   cleared here rather than being fought with further down. */
.auth .brandmark { display: block; margin: 0 auto 1.5rem; }
.auth .brandmark .wordmark { font-size: 2.6rem; line-height: 1.05; }
.auth .brandmark .strap {
  font-family: var(--font-label); font-size: .6rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .1em;
  color: var(--lockup); display: block; margin-top: .6rem;
}
/* The tagline, on the one screen that is the front door. It is the brand's own
   sentence about what the product does, so it says it where somebody is
   deciding whether to come in. */
.auth .tagline {
  font-family: var(--font-display); font-size: 1.25rem; line-height: 1.35;
  color: var(--text); margin: 0 0 .35rem;
}
.auth-form, .password-form { display: grid; gap: .3rem; margin-top: 1.75rem; text-align: left; }
.auth-form label, .password-form label {
  font-family: var(--font-label); font-size: .68rem; text-transform: uppercase;
  letter-spacing: .08em; color: var(--text-muted); margin-top: .8rem;
}
input[type=text], input[type=password], input[type=email], input[type=date], textarea {
  width: 100%; padding: .65rem .75rem;
  font: inherit; font-family: var(--font-body);
  border: var(--look-border-control) solid var(--border); border-radius: var(--look-radius-control);
  background: var(--surface); color: var(--text);
}
input:focus-visible, textarea:focus-visible, button:focus-visible, summary:focus-visible {
  outline: 2px solid var(--focus-ring); outline-offset: 2px;
}

/* ⚠️ THE RING GOES ON THE WRAPPER, NOT ON THE DATE INPUT. A date field is
   several tab stops inside one element — Chrome puts the day, month and year in
   the shadow DOM and Tab moves between them — and the outline is not painted at
   every one of them, so somebody tabbing through loses sight of where they are
   partway across the control. :focus-within on a wrapper is true at every
   internal stop, and lights the whole field rather than one segment of it.
   Found by keyboard_flow_test.rb, which compares each stop focused against
   unfocused; the failure read "textbox 'Wanted by' shows nothing when it has
   focus". The input's own outline is suppressed so there is one ring rather
   than two nested inside each other. */
.date-field { display: block; border-radius: var(--radius-sm); }
.date-field:focus-within { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.date-field input[type=date]:focus { outline: none; }

.demo-hint { margin-top: 2.5rem; font-size: .82rem; }

/* A one-time code, meant to be read aloud or copied onto a slip. Big, spaced,
   and in the mono face so 0 and O are told apart — the alphabet already leaves
   out the pairs that cannot be, see StaffIssuedReset. */
.reset-code {
  font-family: var(--font-mono); font-size: 2rem; letter-spacing: .18em;
  margin: 1rem 0; color: var(--text); user-select: all;
}
.pupil-reset summary { cursor: pointer; font-weight: 600; }
.pupil-reset .password-form { max-width: 22rem; }

/* Who can see a pupil's record, and the control that changes it. Reuses the
   reset panel's shape on purpose: both are a staff act that needs the teacher's
   own password, and a second visual language for the same kind of thing would
   suggest a difference that is not there. */
.pupil-guardians .guardian-name { margin: 0 0 .35rem; }
.pupil-guardians .status { margin-bottom: .35rem; }
.pupil-guardians .muted { margin: .2rem 0 0; }
.guardian-suspend { margin-top: .6rem; }
.guardian-suspend summary { cursor: pointer; font-weight: 600; }
.guardian-suspend .password-form { max-width: 22rem; margin-top: .6rem; }

/* The enrolment QR and the printed codes. Both exist to be read off a screen
   by someone holding a phone, so they are larger and looser than body text. */
.qr { margin: 1rem 0; max-width: 15rem; }
.qr svg { width: 100%; height: auto; display: block; }
.otp-secret { font-family: var(--font-mono); letter-spacing: .12em; user-select: all; }
.recovery-codes {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), 1fr));
  gap: .4rem 1rem; list-style: none; margin: 1rem 0; padding: 0;
  font-family: var(--font-mono); font-size: 1.05rem; letter-spacing: .1em;
  user-select: all;
}
.auth input[type=submit].primary { width: 100%; }
/* The same control as a link: a door that sends people to an identity
   provider's page has nothing to submit, and cannot be a button — the content
   security policy's form-action would stop the redirect in a browser. It has
   to look exactly like the button it stands in for, or the two kinds of door
   stop reading as one product. */
.auth a.primary { display: block; text-align: center; text-decoration: none; }
/* A way in BESIDE the password form — "Sign in with MyLogin" at the teacher
   and pupil doors — rather than instead of it. Outlined, not filled: signing
   in with the password keeps the screen's one primary action, and the second
   way in sits under it without competing. A link for the reason above. */
.auth-or { margin: 1.5rem 0 0; text-align: center; font-size: .82rem; color: var(--text-muted); }
.auth a.provider {
  display: block; margin-top: .6rem; padding: .75rem 1.25rem;
  text-align: center; text-decoration: none;
  border: 1px solid var(--border-strong); border-radius: var(--radius-md);
  color: var(--text); font-family: var(--font-body); font-size: 1rem; font-weight: 600;
}
.auth a.provider:hover { border-color: var(--primary-bg); color: var(--text-link); }

/* --- Buttons -------------------------------------------------------------

   Four roles, and the brand decides which colour carries each. The guide gives
   the proportions outright — "Aegean leads, Amphora supports" — and that is a
   statement about how often each appears, which makes it a statement about
   which button gets which:

     Aegean is the primary. It carries Continue, Start and Send, which is the
     most common action in the product and therefore the colour the page is
     mostly made of.

     Amphora is the support: warm emphasis, used once. Signing in is the one
     screen where a single button is the whole point of the screen, so it is
     the one that gets it.

   The functional red is neither. A destructive button is not a brand moment —
   ending a session is a warning — and the guide has nothing for that, PA Red
   and DE Coral being the parents' and not Sokra's. So --danger-bg is the
   functional red declared in the primitive block, kept clear of Amphora so
   "this ends something" is not read as "this is important".

     .primary  --primary-bg   the one significant CTA (signing in)
     .action   --action-bg    the workhorse: Continue, Start, Send
     .danger   --danger-bg    destructive or progress-ending
     .quiet    outline        minor, and never destructive

   .action and .danger share their box metrics deliberately: they appear side
   by side, and a smaller End session next to a larger Continue reads as the
   lesser of two options rather than the more serious one. */

input[type=submit].primary, button.primary, .primary button, a.primary {
  margin-top: 1.25rem;
  background: var(--primary-bg); color: var(--text-inverse); border: 0; border-radius: calc(var(--look-radius-control) + 2px);
  padding: .8rem 1.25rem;
  font-family: var(--font-body); font-size: 1rem; font-weight: 600; cursor: pointer;
}
input[type=submit].primary:hover, button.primary:hover, .primary button:hover, a.primary:hover {
  background: var(--primary-bg-hover);
}

button.action, .action button, input[type=submit].action,
button.danger, .danger button, input[type=submit].danger {
  border: 0; border-radius: var(--look-radius-control); padding: .6rem 1.15rem;
  font-family: var(--font-body); font-size: .92rem; font-weight: 600;
  line-height: 1.2; cursor: pointer; color: var(--text-inverse);
}
button.action, .action button, input[type=submit].action { background: var(--action-bg); color: var(--action-ink); }
button.action:hover, .action button:hover,
input[type=submit].action:hover { background: var(--action-bg-hover); }

button.danger, .danger button, input[type=submit].danger { background: var(--danger-bg); }
button.danger:hover, .danger button:hover,
input[type=submit].danger:hover { background: var(--danger-bg-hover); }

form.button_to { display: inline-block; margin-right: .5rem; }
/* input[type=submit].quiet is the third selector and it was missing, which the
   .action rule above has carried all along. A `form.submit … class: "quiet"`
   matched none of these and fell through to the browser's own button — grey,
   square and in the wrong face. It had been doing that on the teacher's recap
   Save for a while; the calendar's Search is what made it obvious. */
button.quiet, .quiet button, input[type=submit].quiet {
  background: none; border: var(--look-border-control) solid var(--border); color: var(--text-muted);
  border-radius: var(--look-radius-control); padding: .45rem .85rem;
  font-family: var(--font-label); font-size: .82rem; cursor: pointer;
}
button.quiet:hover, .quiet button:hover, input[type=submit].quiet:hover {
  border-color: var(--border-strong); color: var(--text-link);
}

/* --- Confirmation dialog -------------------------------------------------
   Stands in for window.confirm, so it has to carry the brand rather than the
   browser's chrome. The <dialog> element supplies the modality and the
   backdrop; everything here is presentation. */

dialog.confirm {
  border: 0; border-radius: var(--radius-xl); padding: 0;
  max-width: 30rem; width: calc(100% - 2rem);
  background: var(--surface-raised); color: var(--text);
  box-shadow: var(--shadow-dialog);
}
dialog.confirm::backdrop { background: var(--scrim); }
.confirm-panel { padding: 1.5rem var(--clearspace) 1.25rem; }
.confirm-panel h2 { font-size: 1.2rem; margin-bottom: .5rem; }
.confirm-panel p { margin: 0 0 1.25rem; color: var(--text-muted); font-size: .92rem; }
.confirm-actions {
  display: flex; justify-content: flex-end; align-items: center; gap: .6rem;
  flex-wrap: wrap;
}
/* Cancel is the safe default and gets the same footprint as confirm, so the
   destructive button is not the only large target on the panel. */
.confirm-actions button.quiet {
  padding: .6rem 1.15rem; font-family: var(--font-body); font-size: .92rem;
  text-transform: none; letter-spacing: normal; color: var(--text);
}

@media (prefers-reduced-motion: no-preference) {
  dialog.confirm[open] { animation: confirm-in .14s ease-out; }
  @keyframes confirm-in {
    from { opacity: 0; transform: translateY(6px); }
    to { opacity: 1; transform: none; }
  }
}

/* --- Statutory pages -----------------------------------------------------
   The accessibility statement and the footer that reaches it. Plain, narrow and
   readable: this is a legal document somebody may be reading precisely because
   the rest of the service is not working for them. */

.site-footer {
  max-width: 52rem; margin: 3rem auto 2rem; padding: 1.25rem var(--clearspace) 0;
  border-top: 1px solid var(--border);
  font-family: var(--font-label); font-size: .78rem;
  display: flex; flex-wrap: wrap; gap: .5rem 1.5rem;
  align-items: baseline; justify-content: space-between;
}
.site-footer a { color: var(--text-link); }
/* The co-brand lockup, on every page that has a footer — which is every page.
   Sokra travels with both parents or with neither. */
.site-footer .strap {
  font-size: .58rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .1em; color: var(--lockup);
}

.statement {
  max-width: 44rem; margin: 2.5rem auto; padding: 0 var(--clearspace);
  line-height: 1.6;
}
.statement h1 { margin-bottom: .75rem; }
.statement h2 { margin: 2.25rem 0 .6rem; font-size: 1.15rem; }
.statement .lede { color: var(--text-muted); font-size: 1.05rem; }
.statement ul { padding-left: 1.25rem; }
.statement li { margin-bottom: .35rem; }

/* Wide, and allowed to scroll inside itself rather than pushing the page
   sideways — reflow is one of the things the table is reporting on. */
.statement-table { display: block; overflow-x: auto; max-width: 100%; }
.statement-table th[scope="row"] { font-weight: 600; white-space: normal; min-width: 12rem; }
.statement-table td { vertical-align: top; }

/* --- Not found -----------------------------------------------------------
   The page behind a mistyped id, and the only page in the product that renders
   inside all three shells: the rail on one side of it, the pupil masthead on
   another. So it sets its own width and centres itself rather than borrowing
   .teacher or .lessons, either of which would bring a grid it has no rows for.

   Narrower than .statement above. That page is a document somebody reads; this
   is a sentence and a way out, and a sentence set 44rem wide reads as an
   apology. */
.not-found {
  max-width: 32rem; margin: 3rem auto; padding: 0 var(--clearspace);
  line-height: 1.6;
}
.not-found h1 { margin-bottom: .75rem; }
.not-found .lede { color: var(--text-muted); font-size: 1.05rem; }
/* The primary door and the second one on one row, wrapping on a phone rather
   than shrinking — a button that has lost its words is not a shortcut. */
.not-found-back {
  display: flex; flex-wrap: wrap; align-items: center; gap: .9rem;
  margin-top: 1.75rem;
}

/* --- Skip link -----------------------------------------------------------
   The first thing in the tab order on every page, and invisible until it has
   focus. Someone working by keyboard alone would otherwise tab through the
   masthead, the nav and the profile menu on every page before reaching the
   content; this is the way past that (WCAG 2.4.1).

   Moved off-canvas rather than `display: none`, which would take it out of the
   tab order altogether and leave nothing to skip with. */

.skip-link {
  position: absolute; top: 0; left: -9999px; z-index: 100;
  padding: .7rem 1.1rem;
  background: var(--surface-raised); color: var(--text-link);
  border: 2px solid var(--focus-ring); border-radius: 0 0 8px 0;
  font-family: var(--font-label); font-size: .8rem; font-weight: 600;
  text-decoration: none;
}
.skip-link:focus { left: 0; }

/* The landmark it lands on carries tabindex="-1", so that activating the link
   actually moves focus there. Without it the browser moves only its own
   navigation point, activeElement stays on <body>, and the next Tab starts
   again from the top of the page — the skip link would look like it worked and
   would not have. No ring on it: it is never in the tab order itself, and a
   box drawn round the whole page is noise rather than an indicator. */
main:focus { outline: none; }

/* --- Flash --------------------------------------------------------------- */

.flash {
  max-width: 52rem; margin: 1rem auto -.5rem;
  padding: .7rem 1rem; border-radius: var(--radius-md); font-size: .92rem; border-left: 4px solid;
}
.flash--notice { background: var(--success-surface); border-color: var(--success); color: var(--success-text); }
.flash--alert { background: var(--error-surface); border-color: var(--error); color: var(--error-text); }

/* --- Can't reach Sokra ---------------------------------------------------
   components/ConnectionBanner.tsx: a live screen that has lost Sokra, or the
   browser's connection, says so here. Fixed to the foot of the window and over
   the page, never in its flow, so arriving or leaving it moves no row of the
   register under the teacher's eye. The warning's colours, not the error's:
   nothing the reader did is wrong, and the screen above it is still right as
   of the time it gives. Empty, the status region draws nothing at all. */
.connection-banner { position: fixed; inset-inline: 0; bottom: 1rem; z-index: 60; display: flex; justify-content: center; pointer-events: none; }
.connection-banner.is-up > * { pointer-events: auto; }
.connection-banner-words {
  margin: 0; padding: .6rem 1rem; max-width: 40rem;
  background: var(--warning-surface); color: var(--warning-text);
  border: 1px solid var(--warning); border-inline-end: 0;
  border-start-start-radius: var(--radius-md); border-end-start-radius: var(--radius-md);
  font-size: .92rem; box-shadow: var(--shadow-menu);
}
.connection-banner button {
  margin: 0; padding: .6rem 1rem;
  background: var(--warning-surface); color: var(--warning-text);
  border: 1px solid var(--warning);
  border-start-end-radius: var(--radius-md); border-end-end-radius: var(--radius-md);
  border-start-start-radius: 0; border-end-start-radius: 0;
  font-weight: 700; box-shadow: var(--shadow-menu);
}

/* --- Phase banner --------------------------------------------------------
   Full width, straight under the masthead, in the phase's own fill — see the
   phase tokens in :root and shared/_phase_banner. Capitals by transform rather
   than in the string, so the translations keep their own casing and the lesson
   page can go on using the same keys in sentence case. Chrome does pass the
   capitals to the accessibility tree, as it does for the nav links — see
   test/accessibility/announcements. */
.phase-banner {
  margin: 0; padding: .5rem var(--clearspace);
  font-family: var(--font-label); font-size: .88rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .12em; text-align: center;
}
.phase-banner--baseline { background: var(--phase-baseline-bg); color: var(--phase-baseline-ink); }
.phase-banner--recap { background: var(--phase-recap-bg); color: var(--phase-recap-ink); }
.phase-banner--practice { background: var(--phase-practice-bg); color: var(--phase-practice-ink); }
.phase-banner--review { background: var(--phase-review-bg); color: var(--phase-review-ink); }
.phase-banner--challenge { background: var(--phase-challenge-bg); color: var(--phase-challenge-ink); }

/* --- Scenery -------------------------------------------------------------
   The layer a look draws its decoration in (components/PupilLayout.tsx,
   components/LookScenery.tsx): one fixed box behind the page that takes no
   pointer, holds no text and is aria-hidden, so a screen reader hears the same
   page under every look and nothing a look draws can be tabbed to or clicked.
   Its three spans are for CSS-only scenery (a gradient, a halftone, a stave)
   and are positioned here so a look sets where and how big, not how; the
   inline drawing a look needs (a plant, a rocket) goes in as `.look-scenery-svg`.

   THE PAGE IS LIFTED ABOVE IT, and only when a look is on. A fixed box at
   z-index 0 paints over every static element in the flow, so the masthead, the
   main landmark and the footer are given a position and a level of their own;
   the masthead is the highest of them because the profile menu opens out of it
   over the page. Under `none`, and on every screen with no pupil layout, none
   of this applies: giving the main landmark a position would change what an
   absolutely positioned child is placed against, which is a change to the
   product for a feature that is not in use.

   Off is the calm variant: the layer goes, and the palette, face and shapes
   stay. `display: none` is allowed here and only here (checks/css-tokens.test.ts
   lets a look file do it for its own scenery and nothing else). */
.look-scenery { position: fixed; inset: 0; z-index: 0; pointer-events: none; }
.look-scenery > span, .look-scenery-svg { position: absolute; display: block; }
[data-scenery="off"] .look-scenery { display: none; }

/* A SYMBOL KEEPS THE BODY FACE UNDER EVERY LOOK. A display face is chosen for
   how a heading looks, and several of the looks' faces take liberties a
   science symbol cannot survive: Bangers draws every lower-case letter as a
   capital, so a stem set in it reads MgO as MGO and Co (cobalt) as CO (carbon
   monoxide); Orbitron's 8 is a B at a glance. The product already marks every
   place case is meaning — [data-case="keep"] and .math, the same marker the
   letter-case setting respects — so under a look those are set in the body
   face, which is the brand's or the pupil's own and was chosen to be read.
   Scoped to a look other than none so the standard product is untouched. The
   Comic strip's author found this on the first screenshot; one rule here
   rather than fifteen. */
[data-look]:not([data-look="none"]) :is([data-case="keep"], .math),
[data-look]:not([data-look="none"]) :is([data-case="keep"], .math) * {
  font-family: var(--font-body);
}
[data-look]:not([data-look="none"]) .masthead { position: relative; z-index: 2; }
[data-look]:not([data-look="none"]) main,
[data-look]:not([data-look="none"]) .site-footer { position: relative; z-index: 1; }

/* --- The warning before the lesson ends ------------------------------------
   components/LessonEnding.tsx: less than five minutes, then less than one, on
   every pupil screen while a lesson governs them. Sticky to the top of the
   page, so a pupil scrolled down a long question still sees it, and as wide
   as the flash below it, so the two read as one column.

   QUIET, THEN CLEAR, NEVER ALARMING. The first warning is a raised card with
   an amber edge — the pupil's work stays the brightest thing on the screen.
   The last minute takes the warning fill, the colour of "do this now", and
   still not the error colour: nothing has gone wrong. More time is the
   success pair. The bar drains over the five minutes and repeats the sentence
   in a shape; it is hidden from a screen reader, which hears the sentence.

   Token pairs only (each surface with its own text), every one of which
   web/test/theme-contrast.test.ts already measures in every palette. */
.lesson-ending {
  position: sticky; top: 0; z-index: 5;
  max-width: 52rem; margin: 1rem auto -.5rem; padding: 0 var(--clearspace);
}
.lesson-ending-inner {
  display: grid; grid-template-columns: 1fr auto; column-gap: 1rem; align-items: center;
  padding: .7rem 1rem; border-radius: var(--radius-md); border-left: 4px solid var(--warning);
  background: var(--surface-raised); color: var(--text); box-shadow: var(--shadow-card);
}
.lesson-ending-heading { margin: 0; grid-column: 1; font-weight: 700; }
.lesson-ending-body { margin: .15rem 0 0; grid-column: 1; font-size: .92rem; }
.lesson-ending-inner .lesson-ending-bar { grid-column: 1; margin-top: .55rem; }
.lesson-ending-hide { grid-column: 2; grid-row: 1 / span 3; }
.lesson-ending--last .lesson-ending-inner {
  background: var(--warning-surface); color: var(--warning-text); border-left-color: var(--warning);
}
.lesson-ending--more_time .lesson-ending-inner {
  background: var(--success-surface); color: var(--success-text); border-left-color: var(--success);
}
.lesson-ending-bar {
  display: block; block-size: .3rem; border-radius: var(--radius-pill);
  background: var(--surface-sunken); overflow: hidden;
}
.lesson-ending-bar > span {
  display: block; block-size: 100%; border-radius: inherit; background: var(--warning);
}
.lesson-ending--more_time .lesson-ending-bar > span { background: var(--success); }
.lesson-ending-hide,
.lesson-ending-show {
  font: inherit; font-size: .88rem; cursor: pointer;
  border: 1px solid var(--border-strong); border-radius: var(--radius-pill);
  background: var(--surface); color: var(--text); padding: .3rem .8rem;
}
.lesson-ending-hide:focus-visible,
.lesson-ending-show:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; }
/* Hidden, the first warning folds to a small button at the end of the line:
   the time it ends, and the bar, which keeps draining. */
.lesson-ending--folded { display: flex; justify-content: flex-end; }
.lesson-ending-show {
  display: inline-flex; align-items: center; gap: .5rem; box-shadow: var(--shadow-card);
}
.lesson-ending-show .lesson-ending-bar { inline-size: 3rem; }
@media (prefers-reduced-motion: no-preference) {
  .lesson-ending-inner { animation: lesson-ending-in .4s ease-out; }
  .lesson-ending-bar > span { transition: inline-size 1s linear; }
  @keyframes lesson-ending-in {
    from { opacity: 0; transform: translateY(-.4rem); }
    to { opacity: 1; transform: none; }
  }
}

/* INSIDE A PHASE, THE PHASE'S COLOUR IS THE INTERACTIVE COLOUR. The screen
   wears the banner's colour from the top of the page to the button at the foot
   of it, in every palette — the user's call, 18 September 2026.

   Done by re-pointing tokens rather than by restyling components, because the
   first attempt restyled components: the pill, Next, the strip — and in the
   dark palette Back, the three round tools, the lens picker and every link
   were still teal, because they draw with --border-strong and --text-link,
   which dark sets to teal. Chasing elements one at a time leaves whichever one
   nobody photographed. A token re-pointed on <main> reaches everything drawn
   with it, including the next control somebody adds.

   Five tokens, and what each one now means inside a phase:
     --action-bg, -hover   the filled button, the pill, the chosen lens
     --action-ink          the lettering on those — white, except on amber
     --border-strong       every outlined control: Back, the tools, an option
     --text-link           links, and outlined buttons' lettering

   --phase-*-line is the phase's colour at a strength that holds as text and as
   a control's edge (theme_contrast_test measures both). It is the fill itself
   wherever the fill is dark enough — navy, purple — and the ink variant where
   it is not, the rule the guide gives for teal. Review's amber cannot be text
   at any lightness that still reads as amber, so its line is amber-deep.

   What this costs: teal was the one colour the product said "press this"
   with. Inside a phase it is now that phase's colour instead, and what still
   makes a button a button is its shape and its label.

   Left alone on purpose: --selected (a chosen answer is never teal, because
   teal says "correct"), --accent (in-progress markers that are not the
   phase's), the status colours, and the masthead, which is outside <main>. */
main[data-phase="baseline"] {
  --action-bg: var(--phase-baseline-bg); --action-bg-hover: var(--phase-baseline-bg-hover);
  --action-ink: var(--phase-baseline-ink);
  --border-strong: var(--phase-baseline-line); --text-link: var(--phase-baseline-line);
}
main[data-phase="recap"] {
  --action-bg: var(--phase-recap-bg); --action-bg-hover: var(--phase-recap-bg-hover);
  --action-ink: var(--phase-recap-ink);
  --border-strong: var(--phase-recap-line); --text-link: var(--phase-recap-line);
}
main[data-phase="practice"] {
  --action-bg: var(--phase-practice-bg); --action-bg-hover: var(--phase-practice-bg-hover);
  --action-ink: var(--phase-practice-ink);
  --border-strong: var(--phase-practice-line); --text-link: var(--phase-practice-line);
}
main[data-phase="review"] {
  --action-bg: var(--phase-review-bg); --action-bg-hover: var(--phase-review-bg-hover);
  --action-ink: var(--phase-review-ink);
  --border-strong: var(--phase-review-line); --text-link: var(--phase-review-line);
}
main[data-phase="challenge"] {
  --action-bg: var(--phase-challenge-bg); --action-bg-hover: var(--phase-challenge-bg-hover);
  --action-ink: var(--phase-challenge-ink);
  --border-strong: var(--phase-challenge-line); --text-link: var(--phase-challenge-line);
}

/* The step strip, which takes the phase's FILL rather than its line: worked
   steps are filled with it and the step being worked is outlined in it, so
   shape still says done-or-here and colour says which phase. They were navy in
   every phase until 18 September 2026 — Where to start's colour, so a recap
   strip read as a baseline one. A step both worked and current takes the
   outline: where the pupil is matters more on the strip than what they have
   finished, and with one colour for both a fill could not say it. */
main[data-phase="baseline"] .rung-tab--here { border-color: var(--phase-baseline-bg); }
main[data-phase="baseline"] .rung-tab--done {
  background: var(--phase-baseline-bg); border-color: var(--phase-baseline-bg); color: var(--phase-baseline-ink);
}
main[data-phase="recap"] .rung-tab--here { border-color: var(--phase-recap-bg); }
main[data-phase="recap"] .rung-tab--done {
  background: var(--phase-recap-bg); border-color: var(--phase-recap-bg); color: var(--phase-recap-ink);
}
main[data-phase="practice"] .rung-tab--here { border-color: var(--phase-practice-bg); }
main[data-phase="practice"] .rung-tab--done {
  background: var(--phase-practice-bg); border-color: var(--phase-practice-bg); color: var(--phase-practice-ink);
}
main[data-phase="review"] .rung-tab--here { border-color: var(--phase-review-bg); }
main[data-phase="review"] .rung-tab--done {
  background: var(--phase-review-bg); border-color: var(--phase-review-bg); color: var(--phase-review-ink);
}
/* After the four above, and one class deeper than them, so it wins. */
main[data-phase] .rung-tab--done.rung-tab--here { background: var(--surface); color: var(--text); }
main[data-phase="baseline"] .rung-tab--done.rung-tab--here { border-color: var(--phase-baseline-bg); }
main[data-phase="recap"] .rung-tab--done.rung-tab--here { border-color: var(--phase-recap-bg); }
main[data-phase="practice"] .rung-tab--done.rung-tab--here { border-color: var(--phase-practice-bg); }
main[data-phase="review"] .rung-tab--done.rung-tab--here { border-color: var(--phase-review-bg); }

/* --- Progress strip ------------------------------------------------------
   Pips only fill forwards: the strip never moves backwards during an episode,
   so stepping down reads as building rather than demotion. */

/* The fades live on the wrapper, not as a mask over the track: a mask would
   fade the focus ring on the first and last stop, and a ring half-drawn is
   worse than no fade. Drawn only when there is something past that edge — see
   strip_controller, which is also what scrolls the pupil to where they are. */
.strip { margin-bottom: 2rem; position: relative; }
.strip::before, .strip::after {
  content: ""; position: absolute; top: 0; bottom: .55rem; width: 1.6rem;
  pointer-events: none; opacity: 0; transition: opacity .15s ease;
}
.strip::before { left: 0; background: linear-gradient(to right, var(--surface), transparent); }
.strip::after { right: 0; background: linear-gradient(to left, var(--surface), transparent); }
.strip[data-more-start="true"]::before, .strip[data-more-end="true"]::after { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .strip::before, .strip::after { transition: none; }
}
.strip-track {
  display: flex; gap: .3rem; margin-bottom: .55rem;
  /* The fixed width IS the viewport's: the strip takes the column it is in and
     no more, and everything past that edge is a scroll rather than a new line. */
  max-width: 100%;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x proximity;
  /* Room for the focus ring, which is outside the border box and would
     otherwise be clipped by the overflow that makes this scroll at all. */
  padding: 3px; margin-inline: -3px;
  scrollbar-width: thin;
  /* A scrollbar that appears on hover and leaves no gap otherwise: a 15px
     trough under seven numerals is most of the strip's height. */
  scrollbar-color: var(--border-strong) transparent;
}
/* ⚠️ THE LADDER'S STRIP IS TABS NOW, AND THE PIPS BELOW ARE STILL THE PIPS.
   Practice is the one phase where the pupil may CHOOSE which rung to work —
   rungs are independent misconceptions, so the ladder's order is advice — and a
   tab you can press is what says so. The baseline, the review and the recap run
   a fixed sequence of checks where there is nothing to choose, and a numbered
   tab that does not move is a pip with a number on it: a control that lies
   about being one. Different behaviour, drawn differently, on purpose.

   38px targets, and they SCROLL rather than wrap — which is the opposite of
   what this said, and the reason is the letters. One tab per rung fits: three
   to eight rungs, and eight sit on a 320px screen with room over. But a rung
   that goes wrong grows up to three lettered stops of its own, so a seven-rung
   ladder answered badly is nineteen, and nineteen wrapped is four ragged lines
   of numerals above the question — a block of furniture where there was a line.
   One line that moves is easier to read than four that do not.

   What was worth avoiding about Sparx clipping Q13 off the right edge is that
   nothing says it is there. So: scroll-snap to the stops, the fades at each end
   that say there is more, and `scroll-margin` so keyboard focus never lands on
   a tab half under a fade. The pupil is scrolled to where they are on load —
   see strip_controller — rather than left at stop one on a strip whose live end
   is off-screen.

   The states are a SHAPE first — see the argument in practice/show. Worked is
   filled ink with an inverse numeral, here is a honey edge, not yet is a dashed
   outline. Nothing is green: green would say the answer was right, and this
   strip says only where the pupil is. */
.rung-tab {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 38px; min-height: 38px; padding: 0 .35rem;
  border: calc(var(--look-border-control) * 1.5) dashed var(--border); border-radius: calc(var(--look-radius-control) + 2px);
  background: var(--surface); color: var(--text-muted);
  font-family: var(--font-label); font-size: .9rem; text-decoration: none;
}
.rung-tab {
  flex: 0 0 auto;
  scroll-snap-align: start;
  /* Never scrolled to flush against the clipped edge — see the note above. */
  scroll-margin-inline: .6rem;
}
.rung-tab:hover { border-color: var(--border-strong); color: var(--text); }
/* Begun and not finished: solid, not filled. A dashed square here said the
   pupil had never seen this rung, on the rung they were in the middle of. */
.rung-tab--started { border-style: solid; color: var(--text); }
.rung-tab--done {
  border-style: solid; border-color: var(--border-strong);
  background: var(--border-strong); color: var(--text-inverse);
}
/* Last, so that the rung being worked reads as the one being worked even when
   it is also one already worked — the pupil has come back to it. The same
   ordering argument .pip--here carries below. */
.rung-tab--here {
  border-style: solid; border-width: calc(var(--look-border-control) * 2); border-color: var(--accent);
  font-weight: 700; color: var(--text);
  /* It is a <span> rather than a link — see practice/_strip — so it must not
     offer the hand a link offers, and there is nothing to hover. */
  cursor: default;
}
.rung-tab--done.rung-tab--here { color: var(--text-inverse); }

.pip { flex: 1; max-width: 3rem; height: 5px; border-radius: 3px; background: var(--border); }
.pip--done { background: var(--success); }
/* An item that left the queue without clearing. Amber, matching .idea--flagged
   and for the same reason: it means a person will help. */
.pip--flagged { background: var(--warning); }
/* A right answer given while the pupil said they were unsure — carried forward
   rather than handed to anyone. It shared .pip--flagged, which told a pupil a
   teacher was coming for an item nobody had raised; accent is what the rest of
   the product uses for work still in progress, which is what a carry is. */
.pip--carried { background: var(--accent); }
/* Honey, not the functional red: this marks where the pupil is, and red on
   this screen should mean something has gone wrong or is about to be lost. It
   also matches the accent used for "in progress" everywhere else.

   LAST OF THE FOUR, AND THAT IS LOAD-BEARING. A pip carries several of these
   at once — cleared, flagged and here are three separate facts about the same
   item — and they are all one class deep, so source order is the whole of the
   cascade. .pip--flagged used to be declared two thousand lines below this
   rule and quietly won, which painted the pupil's own position in the colour
   that means somebody else is dealing with it. */
.pip--here { background: var(--accent); }
.why {
  font-family: var(--font-label); font-size: .82rem;
  color: var(--text-muted); margin: 0;
}

/* --- Question ------------------------------------------------------------ */

.question { margin-bottom: 1.75rem; }
/* The stem is a heading by structure and a sentence by nature: it is read
   through rather than scanned. So it is set nearer reading size than the
   display sizes the other h1s take, and given the line spacing the guide asks
   for in body copy rather than the tight leading a title wants. */
.question h1 { font-size: var(--size-question); line-height: 1.35; }
.question .tags { display: flex; align-items: center; gap: .65rem; margin: 0 0 .75rem; }
/* ⚠️ THE QUESTION BACK WEARS THIS TOO, and deliberately in the same colour.
   A question back is still a question; it used to sit under a muted grey label
   where every other ask carries a filled pill, so the two screens read as two
   different products and the eye had to re-find where the question started.
   What differs is the word in it — a question has an exam command word and a
   question back has none, so it says what kind of ask this is instead. */
/* ⚠️ THE ROW ITSELF, WHICH IT DID NOT HAVE. `p.tags` was a bare paragraph, so
   the pill inside it was an inline box whose height came from whatever
   line-height the screen around it happened to set — and the two screens that
   carry one set different ones, so the same pill came out two heights. A flex
   row with the gap and the space under it stated here is what makes the slot
   the same slot on both. */
.tags {
  display: flex; flex-wrap: wrap; align-items: center; gap: .4rem;
  margin: 0 0 .6rem;
}
.command {
  /* inline-block with a line-height of its own, for the reason above: the box
     is then the font plus its padding and nothing else, on every screen. */
  display: inline-block; line-height: 1.2;
  background: var(--action-bg); color: var(--action-ink);
  border-radius: 4px; padding: .25rem .6rem; cursor: help;
  font-family: var(--font-label); font-size: .78rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em;
}
.tags .muted { font-family: var(--font-label); font-size: .66rem; letter-spacing: .06em; text-transform: uppercase; }

/* A <fieldset>, so the options are one question rather than four loose
   choices. Fieldset carries browser defaults that a grid does not want: a
   border, padding, and min-inline-size: min-content, which stops the track
   shrinking. All three are cleared here rather than the element being changed
   back to a div, because the grouping is the point. */
.options {
  display: grid; gap: .55rem; margin: 1.5rem 0;
  border: 0; padding: 0; min-inline-size: 0;
}
.option {
  position: relative; display: block;
  padding: .9rem 1.1rem; padding-inline-start: 2.9rem;
  font-size: var(--size-pupil-body);
  background: var(--surface);
  border: calc(var(--look-border-control) * 1.5) solid var(--border); border-radius: calc(var(--look-radius-control) + 2px); cursor: pointer;
  transition: border-color .12s ease, background-color .12s ease;
}
.option:hover { border-color: var(--border-strong); }

/* The dot, drawn rather than native. Chosen was a tint, an edge colour and a
   font-weight, and the first two are colour: on a palette picked for being
   low-contrast the whole state came down to bolder text, which is not a cue
   unless you have the unbolded version beside it to compare against. A radio
   that fills is the one convention every reader already has, and it is a
   shape rather than a colour.

   Logical properties throughout: Arabic and Urdu put it on the right. */
.option::before {
  content: ""; position: absolute;
  inset-inline-start: 1.1rem; top: 1.15rem;
  width: 1.1rem; height: 1.1rem; border-radius: 50%;
  border: 2px solid var(--border-strong); background: var(--surface);
  transition: border-color .12s ease, background-color .12s ease;
}

/* The native control is hidden but not removed: it stays focusable and screen
   readers still announce the group, while the bar itself carries the state. */
.option-input {
  position: absolute; width: 1px; height: 1px;
  opacity: 0; margin: 0; pointer-events: none;
}
/* Parked under the drawn dot, and that is the whole reason it is given a
   position at all. The browser anchors a required-field bubble to the control
   it belongs to, and a 1px control at the label's start corner put "Please
   select one of these options" on top of the first answer and hanging off the
   leading edge of the page. Pointed at the dot, it opens where the pupil is
   already looking.
   Scoped to .option: the theme and language pickers share .option-input and
   lay their cards out differently, so moving it globally would move it on
   two screens that never asked. */
.option > .option-input { inset-inline-start: 1.6rem; top: 1.7rem; }

.option:has(.option-input:focus-visible) { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* An inset ring rather than a wider border, so the chosen bar thickens without
   the row reflowing under the pointer that just chose it. */
.option:has(.option-input:checked) {
  border-color: var(--selected);
  background: var(--selected-surface);
  box-shadow: inset 0 0 0 1px var(--selected);
}
.option:has(.option-input:checked)::before {
  border-color: var(--selected); background: var(--selected);
  box-shadow: inset 0 0 0 2px var(--surface);
}
.option:has(.option-input:checked) .option-text { font-weight: 600; }

/* A choose-all-that-apply draws a box where the radio draws a dot — the one
   shape convention every reader has for "more than one of these" — and the
   line above the group says so in words for anyone the shape is lost on. */
.option--multi::before { border-radius: 4px; }
.choose-all { margin: 1.5rem 0 -.9rem; font-size: .9rem; color: var(--text-muted); }
/* The second tier's own question, a legend over the same bars — see
   practice/_reason. Set like a heading, since to the pupil it is one. */
.reason-heading { font-family: var(--font-display); font-size: 1.25rem; margin: 0; padding: 0; }
.reason-options { margin-top: 1.75rem; }
.reason-options .choose-all { margin-top: .4rem; }
.choose-all + .options { margin-top: 1.4rem; }

/* --- The probe screen ----------------------------------------------------
   A wrong answer, put back as a question, on a page of its own — see
   practice/probe.

   ONE COLUMN AND NOTHING ELSE IN IT. The shape is the summaries' shape and it
   shares their rule: this is a page a pupil reads and answers, not a panel
   beside something. It began as a block at the foot of the feedback screen,
   ruled off from the correction above it; the rule went with the screen,
   because there is nothing left up there to be ruled off from.

   The question carries the display face at heading size because it IS the
   heading — the only thing being asked on the page. */
.probe-screen {
  max-width: 40rem; margin: 0 auto;
  padding: 3.5rem var(--clearspace) 4rem;
}
/* The rung's question back sits in the practice grid with the tutor beside
   it, so the grid does the spacing and the column takes its width. */
.probe-screen--beside-the-tutor { max-width: none; margin: 0; padding: 0; }
.probe-label {
  font-family: var(--font-label); font-size: .82rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em;
  color: var(--text-muted); margin: 0 0 .6rem;
}
.probe-question { font-size: 1.6rem; line-height: 1.3; margin: 0; }
.probe-help { margin: 1.25rem 0 0; font-size: .95rem; color: var(--text-muted); }
.probe-form[aria-busy="true"] textarea { opacity: .7; }
/* The tutor's "Not quite" above the next question back: the chat's own
   bubble shape, so it reads as the tutor speaking — but in the reframe's
   tint and a size up, because on a page that is otherwise one heading and
   one box it has to be seen to be the tutor cutting in and not furniture.
   The speaker label is the accent, for the same reason. */
.probe-said { margin: 0 0 1.25rem; }
.probe-said .msg {
  display: inline-block; max-width: 100%;
  padding: .8rem 1rem; font-size: 1rem; font-weight: 600;
  background: var(--warning-surface); border: 1px solid var(--warning);
  border-left-width: 4px; color: var(--warning-text);
}
.probe-said .who { color: var(--warning-text); font-size: .68rem; }
/* The questions back already answered — see practice/_thread. The chat's
   bubbles in the chat's tokens, but not its classes: tests count those as
   what a pupil asked the tutor. The pupil's reply is set in from the left so
   the two voices read as an exchange rather than a list. */
.thread { margin: 0 0 1.5rem; }
.thread-turns { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }
.thread-turn { display: grid; gap: .5rem; }
.thread-said {
  max-inline-size: var(--measure); padding: .6rem .75rem; border-radius: var(--look-radius);
  background: var(--surface-sunken); border: var(--look-border) solid var(--border); color: var(--text);
}
.thread-said p { margin: 0; }
/* The question back while it is being written: the chat's own dots and stage
   label, in the question's place. The size is the question's own so that the
   page does not jump when the question lands in the same spot. */
.thread-pending { display: flex; align-items: center; gap: .6rem; min-height: 2rem; color: var(--text-muted); }
.thread-pending .pending-stage { margin-left: 0; font-size: 1rem; }
.thread-gave-up { color: var(--text-muted); }
.thread-said--pupil {
  margin-inline-start: 2rem;
  background: var(--tint-accent-fill-soft); border-color: var(--tint-accent-line);
}
/* The turn being answered, told from the record above it by its box rather
   than by its type: raised off the page ground (the answered bubbles sit
   flush in it, outlined) with the reading edge in --accent, the colour the
   page already means "the thing being worked" by — .resume's open sessions,
   the rung being worked. Not a tint: the pupil's bubbles are the amber wash,
   and a tutor bubble in the same wash would read as theirs. Under high
   contrast every surface is white, so the 4px edge and the full-strength
   accent border are the whole difference, which is why there are two of
   them. Logical edge, so it stays on the reading side in Arabic and Urdu. */
.thread-turn--open { margin-top: .5rem; }
.thread-turn--open .thread-said--tutor {
  background: var(--surface); border-color: var(--tint-accent-edge);
  border-inline-start: 4px solid var(--accent);
  padding: .9rem 1.1rem;
}
/* The record, one guide size quieter than the turn being answered: body (15)
   against pupil body (17) — two sizes the guide already names — rather than a
   muted ink, which on the pupil's amber wash is the pairing the axe pass once
   failed (see the tints in :root). The whole exchange steps down together,
   the pupil's picks included, so the open turn is the one thing at reading
   size. */
.thread-turn:not(.thread-turn--open) .thread-said { font-size: var(--size-body); }
/* The line over a closed step's statements ("Choose one."): the page
   talking, not the tutor, so it is outside the tutor's bubble and directly on
   the statements it is about. Until 7 Oct 2026 it sat
   at the top of the bubble, above the tutor's own message, and pupils read
   the instruction, then the tutor, then the statements, with no way to tell
   which was which. Lifted a half-step rather than bolded: 500 is carried by
   every face on the axis (Figtree 300–900, Atkinson 400–700), so it is a real
   weight under both. In the record it sits over the pupil's bubble, set in
   with it, because that is where the statements are drawn again. */
.thread-lead-in { font-weight: 500; margin: 0; }
.thread-turn--open .thread-lead-in { margin: 1rem 0 0; font-size: var(--size-pupil-body); }
.thread-turn--open .thread-lead-in + .options { margin-top: .5rem; }
.thread-turn:not(.thread-turn--open) .thread-lead-in {
  margin-inline-start: 2rem; font-size: var(--size-body);
}
/* What the pupil typed, line breaks and all. */
.thread-reply { white-space: pre-wrap; overflow-wrap: anywhere; }
/* A question back as the tutor laid it out: the opening line, the case to
   picture and the question each a paragraph, a blank line between
   (ai/tutor/pupil-typography.ts). pre-line and not pre-wrap: the line breaks
   are the layout, and a run of spaces from the model is not. The open one is
   the heading below, by its place rather than a class of its own: tests read
   that heading's class attribute whole. */
.tutor-wrote,
.thread-turn--open .probe-question { white-space: pre-line; }
.thread-reply--blank { color: var(--text-muted); font-style: italic; }
/* The question again: a heading a step down from the question's own h1, which
   is still on the page above it. The open question back shared this rule
   until it stopped being one line — see the next rule. */
.question-again-stem { font-size: 1.3rem; line-height: 1.3; margin: .3rem 0 0; }

/* THE OPEN QUESTION BACK IS A MESSAGE, NOT A TITLE. Since #418 the tutor
   writes three or four sentences here — a line on the pupil's thinking, a
   line of setting, the question at the end — and this rule set the lot in the
   display weight at 1.3rem because the element is an <h2>. A paragraph in
   bold is slower to read, not easier, and for a dyslexic reader the heavy
   strokes close the counters. The element stays: it is the thread's focus
   target, a heading somebody navigates to, and what the reply form is
   labelled by. The type goes: body face, body weight, the pupil body size the
   answer options are set in, the reading line height (the spacing axis's,
   not a title's 1.3). What says "answer this one" is the bubble round it —
   .thread-turn--open .thread-said--tutor — not the weight. The bubble already
   holds the measure. */
.thread-turn--open .probe-question {
  font-family: var(--font-body); font-weight: 400;
  font-size: var(--size-pupil-body); line-height: var(--line-height);
  margin: .35rem 0 0;
}
.thread-turn--open .probe-form textarea { margin-top: .25rem; }
.question-again { margin: 1.5rem 0 1rem; }
/* What the pupil wrote to the question itself, as their side of the
   conversation: the thread's pupil bubble, with the record's own spacing. */
.chose.chose--bubble { margin: 1.25rem 0 1rem 2rem; font-size: 1rem; }
/* How it ended, said by the tutor at the bottom of the thread. The answer in
   it keeps the record's label, in the bubble's measure. */
.thread-outcome { margin: 1rem 0; }
.thread-outcome p { margin: 0; }
.thread-outcome .chose { margin: .6rem 0 0; }
/* The card that ends a rung whose answer is shown (PracticeParts.tsx,
   `AnswerShown`): the question again over the answer, as a whole sentence,
   in the reframe panel's amber — the product's own voice, where the bubbles
   above it were the tutor's. Two small labels in the label face, the question
   in the question-again heading's type, the answer at pupil body size with
   its key point in `<strong>`, and the teacher line a step quieter. */
.answer-shown { margin: 1.5rem 0; }
.answer-shown p { margin: 0; }
.answer-shown-label {
  font-family: var(--font-label); font-size: .78rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted);
  margin: 0 0 .3rem;
}
.answer-shown-question { font-size: 1.2rem; line-height: 1.35; margin: 0 0 .9rem; }
.answer-shown-answer { font-size: var(--size-pupil-body); line-height: var(--line-height); }
.answer-shown .closing-line { margin: 0 0 .8rem; }
.answer-shown-next { margin: .8rem 0 0; color: var(--text-muted); font-size: var(--size-body); }
.answer-shown .answered-actions { margin-top: 1rem; }
.question-again .thread-said p { margin: 0; }
/* Focused by thread_controller.js so a screen reader starts where the page
   moved to — and the question, by Continue and a rung's tab when they bring it
   into the page (lib/thread-motion.ts, useArrivalFocus). A ring round a heading nobody tabbed to reads as a fault; a
   keyboard user who does tab there still gets one. */
.thread [tabindex="-1"]:focus:not(:focus-visible),
.question-again [tabindex="-1"]:focus:not(:focus-visible),
.feedback[tabindex="-1"]:focus:not(:focus-visible),
.question[tabindex="-1"]:focus:not(:focus-visible),
.practice-summary h1[tabindex="-1"]:focus:not(:focus-visible) { outline: none; }
/* The tutor's mark beside "Tutor" wherever the scaffold speaks: an outline
   robot in the colour of the text it stands beside (so every palette and every
   look colours it), where it was the robot emoji and then a ring — TutorBot.tsx
   says why each went. The label around it is set at .6rem, where a glyph is a
   smudge; this is sized off the root. */
.tutor-mark { display: inline-block; font-size: 1rem; line-height: 1; vertical-align: -.15em; margin-inline-end: .15rem; }
.tutor-mark svg, .chat-icon svg { display: block; width: 1.05em; height: 1.05em; }
.probe-form textarea { margin: .7rem 0 0; }
/* A closed question back's statements are the reason form's own bars (the shared
   OptionRadios), set closer under the stem than that form's legend wants: here
   the stem is the heading directly above, not a legend inside the group. */
.thread-turn--open .probe-form .options { margin: 1rem 0 0; }
/* An answered closed question back: the statements again, in the pupil's
   bubble, with theirs picked out — see AnsweredChoice in PracticeParts.tsx.
   Not .option, which is a control with a dot and a hover, and not
   .marked-option, whose green is "the right answer": this green is "yours",
   right or not, and the tag says so in words for anyone the colour is lost on.
   The others go quiet, as the marked record's do. */
.thread-picks { list-style: none; margin: .35rem 0 0; padding: 0; display: grid; gap: .4rem; }
.thread-pick {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .2rem .8rem;
  padding: .55rem .8rem; border: calc(var(--look-border-control) * 1.5) solid var(--border); border-radius: calc(var(--look-radius-control) + 2px);
  background: var(--surface); color: var(--text-muted);
}
.thread-pick--yours {
  border-color: var(--success); background: var(--success-surface);
  color: var(--text); font-weight: 600;
}
.thread-pick-tag {
  margin-inline-start: auto; font-family: var(--font-label); font-size: .78rem;
  font-weight: 400; white-space: nowrap; color: var(--success-text);
}
.thread-picks + .thread-reply--blank { margin-top: .4rem; }
/* THE ONE PICTURE A QUESTION BACK CARRIES (PracticeParts.tsx, TurnPicture;
   docs/plan/Pictures_In_Questions_Back_Plan.md §2). Under the tutor's opening
   line and over the case to picture, inside the bubble, so the words that
   follow have something to hang on. The question's own figure (.stem-figure)
   is the model: a button stripped back to the picture it wraps, the frame on
   the <img>, a cap on the height so the question stays above the fold on a
   laptop — lower than the stem's 22rem, because this sits inside a message
   that has to be read whole. Full width on a phone, where the bubble is the
   column. In the record of an answered turn it steps down with the type
   around it. */
.tutor-lead { margin: 0 0 .75rem; }
.turn-picture { margin: 0 0 .5rem; }
.turn-picture-open {
  position: relative; display: inline-block; padding: 0; border: 0;
  background: none; cursor: zoom-in; max-width: 100%; font: inherit;
}
.turn-picture-image {
  display: block; max-width: 100%; max-height: 15rem; height: auto;
  border-radius: var(--radius-md); border: 1px solid var(--border); background: var(--figure-plate);
}
.turn-picture--record .turn-picture-image { max-height: 9rem; }
/* The zoom affordance, drawn rather than written: it is never a word for the
   message's read-aloud button to read, and never a letter for a pupil to
   puzzle over. Over the corner of the picture, where a frame of Oak's
   recording has nothing a chip could cover (the recap draws them the same). */
.turn-picture-open::after {
  content: "⤢"; position: absolute; right: .45rem; bottom: .45rem;
  width: 1.7rem; height: 1.7rem; border-radius: 50%; display: grid; place-items: center;
  background: var(--border-strong); color: var(--text-inverse); font-size: 1rem; line-height: 1;
  opacity: .85;
}
.turn-picture-open:hover .turn-picture-image,
.turn-picture-open:focus-visible .turn-picture-image { border-color: var(--focus-ring); }
/* The disclosure: a quiet button, not a link (the design review of 15 October
   2026), because it changes the bubble rather than leaving the page. The
   marker is the stylesheet's chevron so the control reads the same in every
   browser; open, the ground fills so the pressed state is seen as well as
   said. */
.turn-picture-words { margin: 0 0 .9rem; }
.turn-picture-what {
  display: inline-flex; align-items: center; gap: .4rem; list-style: none; cursor: pointer;
  background: var(--surface); border: var(--look-border-control) solid var(--border);
  border-radius: var(--look-radius-control); padding: .45rem .8rem;
  font-family: var(--font-body); font-size: .9rem; font-weight: 600; color: var(--text);
}
.turn-picture-what::-webkit-details-marker { display: none; }
.turn-picture-what::before { content: "▸"; font-size: .75rem; color: var(--text-muted); }
.turn-picture-words[open] > .turn-picture-what::before { content: "▾"; }
.turn-picture-what:hover { border-color: var(--action-bg); }
.turn-picture-words[open] > .turn-picture-what { background: var(--surface-sunken); }
.turn-picture-alt {
  margin: .5rem 0 0; max-width: 34rem; padding: .6rem .75rem; border-radius: var(--look-radius);
  background: var(--surface-sunken); font-size: .95rem;
}
.turn-picture-dialog-alt { margin: .75rem 0 0; max-width: 40rem; }
.turn-picture--record + .turn-picture-words { font-size: var(--size-body); }
@media (max-width: 40rem) {
  .turn-picture-open, .turn-picture-image { width: 100%; }
  .turn-picture-image { max-height: none; }
}
/* The pupil's own switch (pupil_settings.pictures, the quick row): off, the
   picture and its disclosure are put away and the message reads as it did
   before pictures. On <html> before first paint, so nothing flashes. */
[data-pictures="off"] .turn-picture, [data-pictures="off"] .turn-picture-words { display: none; }
/* The answer leads and the decline is quiet beside it — but quiet is not
   hidden. A pupil who genuinely has nothing to say and cannot find the way to
   say so is a pupil stuck on a text box, which is the one outcome this tier
   must never produce. */
.probe-actions {
  margin-top: 1.5rem; display: flex; gap: 1rem; align-items: center; flex-wrap: wrap;
}

/* The answer form's own validation message, rendered hidden and shown when a
   pupil says how sure they are without having chosen anything. In the page and
   above the options, because the native bubble it replaces cannot be styled,
   speaks the browser's language rather than the one the pupil chose, and
   vanishes on the next click. .option-input above is the other half of it.

   Amber, not the functional red. Nothing has gone wrong; a step is missing. */
.answer-error {
  margin: 1.5rem 0 0; padding: .6rem .8rem; border-radius: var(--radius-md);
  background: var(--warning-surface); color: var(--warning-text);
  border: 1px solid var(--warning);
  font-size: .9rem; font-weight: 600;
}
/* Only while the message is actually up. The element ships `hidden` and is
   revealed by the controller, and a plain adjacent-sibling rule matches a
   hidden element too — so this tightened the gap above the answers on every
   load, for a message that was not there. */
.answer-error:not([hidden]) + .options { margin-top: .6rem; }

textarea { margin: 1.5rem 0; }

.confidence-label {
  font-family: var(--font-label); font-size: .82rem;
  color: var(--text-muted); margin: 1.5rem 0 .6rem;
}
.confidence { display: flex; gap: .5rem; flex-wrap: wrap; }
.confidence button {
  flex: 1 1 8rem; padding: .8rem 1rem; cursor: pointer;
  font-family: var(--font-body); font-size: .95rem; font-weight: 600;
  border: calc(var(--look-border-control) * 1.5) solid var(--border-strong); border-radius: calc(var(--look-radius-control) + 2px);
  background: var(--surface); color: var(--text-link);
}
.confidence button:hover { background: var(--action-bg); color: var(--action-ink); }

/* What the pupil actually chose, restated above the panel.

   The options are off the screen by the time the feedback is on it, so "Not
   quite" arrived with nothing to be not-quite about: a pupil who misread the
   stem could not tell which answer they had given, only that it was the wrong
   one. Set outside the tone colours and quieter than the panel below — it is
   the evidence, not the verdict. The negative bottom margin pulls the two
   together so they read as one block rather than two notices. */
.chose {
  margin: 1.5rem 0 -.75rem; color: var(--text); font-size: .95rem;
}
/* Table completion — see practice/_grid. Statements down, categories across,
   a tick in every cell that applies. */
.grid-table {
  width: 100%; margin: 1rem 0 0; border-collapse: collapse; font-size: .95rem;
}
.grid-table th, .grid-table td {
  border: 1px solid var(--border); padding: .6rem .7rem; text-align: start;
}
.grid-table thead th, .grid-table .grid-corner {
  background: var(--surface-sunken); font-family: var(--font-label);
  font-size: .82rem; font-weight: 600; color: var(--text-muted);
}
/* The categories are narrow and the statements are not: a column sized to its
   heading leaves the sentence three words wide. */
.grid-table thead th:not(.grid-corner) { width: 6.5rem; text-align: center; }
.grid-table tbody th { font-weight: 400; color: var(--text); }
.grid-table td { text-align: center; }
/* ⚠️ THE WHOLE CELL IS THE TARGET, AND THE BOX SITS IN THE MIDDLE OF IT. A tick
   grid is the one control here where the thing being pressed is a table cell,
   and a 44px row of empty space beside a tiny checkbox is a miss waiting to
   happen on a phone. The first cut stretched the checkbox itself to fill the
   cell — width 100%, min-height 2.75rem — which gives the right target and puts
   the glyph wherever the browser feels like, which was the top. A flex cell
   with a checkbox centred in it is the same target and looks like a table. */
.grid-table td:has(.grid-tick) {
  padding: 0; height: 2.75rem;
}
.grid-table .grid-cell {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; min-height: 2.75rem; margin: 0; cursor: pointer;
}
.grid-tick {
  width: 1.15rem; height: 1.15rem; margin: 0;
  accent-color: var(--selected); cursor: pointer;
}
/* The category name inside each cell. Hidden on the wide layout, where the
   column heading above says it, and brought back below — see the reflow. */
.grid-cell-category {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}

/* ⚠️ AT 320px A FOUR-BY-THREE GRID HAS TO BECOME SOMETHING READABLE WITHOUT
   BECOMING A DIFFERENT QUESTION, which is the real design constraint on this
   format and the reason it is not just a table.

   Scrolling it sideways was the first thought and is wrong: the statement is
   the row header, so scrolling to reach the third category takes the sentence
   you are answering off the screen. What a pupil needs is the statement and
   its categories together.

   So below 34rem the table stops being a table and becomes a stack of cards,
   one per statement, with the categories as full-width rows under it. The
   header row goes — each cell carries its own visually-hidden label naming
   both the statement and the category, so nothing is lost when the column
   heading is not on screen. Same question, same controls, same answer. */
@media (max-width: 34rem) {
  .grid-table, .grid-table tbody, .grid-table tr, .grid-table td, .grid-table th { display: block; }
  .grid-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  .grid-table tr {
    margin-bottom: .9rem; border: 1.5px solid var(--border); border-radius: var(--radius-md); overflow: hidden;
  }
  .grid-table tbody th {
    border: 0; border-bottom: 1px solid var(--border); background: var(--surface-sunken);
  }
  .grid-table td { border: 0; border-bottom: 1px solid var(--border); text-align: start; padding: 0; }
  .grid-table tr td:last-child { border-bottom: 0; }
  /* The category name comes back as the cell's own text, since the column
     heading it used to sit under is off the screen. The STATEMENT stays
     hidden: the card header above is already it, and repeating it in every row
     is the same sentence three times. */
  .grid-table td .grid-cell-category {
    position: static; width: auto; height: auto; margin: 0; clip: auto;
    overflow: visible; white-space: normal;
  }
  .grid-table .grid-cell {
    justify-content: flex-start; gap: .6rem; padding: .7rem .8rem; height: auto;
  }
  .grid-table td:has(.grid-tick) { height: auto; }
}
/* A question with more than one answer — see practice/_parts. No border and no
   ground: a fieldset drawing a box round three text fields makes the page look
   like a form to fill in, and the stem above it is the question. What it needs
   is the SPACING that says these belong together and each is its own ask. */
.parts { margin: 1.25rem 0 0; padding: 0; border: 0; display: grid; gap: 1.1rem; }
/* The legend names the act for a screen reader and is not drawn: the <h1>
   above already says what the question is, and a second heading over the boxes
   would be the same sentence twice to everyone who can see it. Clipped rather
   than display:none, which removes it from the accessibility tree too. */
.parts-legend {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.part-label { display: block; margin-bottom: .4rem; color: var(--text); font-size: .95rem; }
/* A calculation's box and the unit printed beside it — never typed into it,
   which is what Sparx do and what keeps the marker out of unit parsing. Baseline
   alignment so the unit sits on the text's line rather than the box's middle,
   which is where it sits on a worksheet. */
.part-answer { display: flex; align-items: baseline; gap: .5rem; }
.part-answer .short-answer { flex: 1; min-width: 0; }
.part-unit { font-family: var(--font-label); font-size: .95rem; color: var(--text-muted); white-space: nowrap; }
/* The a) b) c). In the label AND in the marked record, so the two read as the
   same question — a pupil looking at what they wrote should not have to count
   rows to find which box b) was. */
.part-letter { font-family: var(--font-label); color: var(--text-muted); margin-right: .15rem; }
/* The spelling under a forgiven misspelling — see practice/_marked_options and
   Practice::NearMiss. Small and italic because it is an aside and not the
   verdict: the verdict is the tick above it, and a line the same weight as the
   answer would read as a correction of an answer that was marked right.

   --success-text rather than --success. The token the marked states use is a
   border and a ground colour, and pistachio as INK on paper is 2.11:1 — this
   is prose a pupil has to read at .85rem, so it takes the ink token that was
   mixed to be read: 7.85:1 on the light theme and 10.77:1 on the dark. */
.spelling {
  margin: .35rem 0 -.75rem; color: var(--success-text);
  font-size: .85rem; font-style: italic;
}
/* The options again, marked — see practice/_marked_options. Not .option: that
   is a control with a drawn dot and a hover, and these are a record. Each
   state is a colour AND a tag, so a reader who cannot tell the green from the
   red still reads which was theirs and which was right; the rest go quiet. */
.marked { display: grid; gap: .55rem; margin: 1.5rem 0 -.5rem; padding: 0; list-style: none; }
.marked-option {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .3rem .8rem;
  padding: .8rem 1.1rem; border: calc(var(--look-border-control) * 1.5) solid var(--border); border-radius: calc(var(--look-radius-control) + 2px);
  background: var(--surface);
}
.marked-option--other { color: var(--text-muted); }
.marked-option--right { border-color: var(--success); background: var(--success-surface); }
.marked-option--wrong { border-color: var(--error); background: var(--error-surface); }
.marked-tag { margin-inline-start: auto; font-family: var(--font-label); font-size: .78rem; white-space: nowrap; }
/* A look's own word for the outcome, beside the tag and never in place of it
   ("a wrong turn" after "Not quite yet"). Drawn only when the look has one
   (components/SittingPanels.tsx, MarkedRow); an outcome word and never a count. */
.look-word { font-family: var(--font-label); font-size: .78rem; font-weight: 700; white-space: nowrap; color: var(--text-muted); }
.marked-option--right .marked-tag { color: var(--success-text); }
.marked-option--wrong .marked-tag { color: var(--error-text); }
/* Their order or their groups, marked — the ordering and matching branches of
   practice/_marked_options. An ordered list keeps the numbers; a group is a
   heading over a list of what they put there. */
ol.marked { counter-reset: place; }
ol.marked > .marked-option { counter-increment: place; }
ol.marked > .marked-option::before { content: counter(place) "."; font-family: var(--font-label); font-size: .82rem; color: var(--text-muted); min-width: 1.4rem; }
/* A question with more than one answer, marked box by box — see
   practice/_marked_options.

   ⚠️ NO DRAWN COUNTER, THOUGH IT IS AN <ol> LIKE THE ORDERING ABOVE. The parts
   already carry a) b) c) IN THE TEXT: those are the question's own labels, not
   a presentation of the order, and the rule above would draw "1." over "a)" and
   put two numbering systems on one row. It has to come after that rule and
   match `ol` too — the same two class names with one fewer element loses on
   specificity, which is how this shipped wrong the first time and was caught by
   looking at the screen rather than by a test. The list stays ordered because
   the parts are; only the marker goes.

   And the row stacks: a box's answer, its verdict, and either the spelling it
   was read as or the word it missed, each on its own line — which the single
   row an option is drawn as has no space for. */
ol.marked--parts > .marked-option::before { content: none; }
.marked--parts .marked-option { flex-direction: column; align-items: stretch; }
.marked--parts .marked-tag { align-self: flex-start; }
.marked--parts .chose, .marked--parts .spelling { margin-top: .35rem; margin-bottom: 0; }
.marked--groups { gap: 1rem; }
.marked-group { display: grid; gap: .45rem; }
.marked-heading { margin: 0; font-family: var(--font-label); font-size: .85rem; font-weight: 600; color: var(--text-muted); }
.marked-list { display: grid; gap: .45rem; margin: 0; padding: 0; list-style: none; }

/* --- Put in order — see practice/_ordering and ordering_controller ---------
   The select is the answer; the buttons and the drag are ways of moving it.
   Items are numbered by CSS from their place in the list, so the number a
   sighted pupil reads is always the number the select is about to post. */
.arrange { display: grid; gap: .55rem; margin: 1.5rem 0; padding: 0; list-style: none; counter-reset: place; }
.arrange-item {
  display: flex; align-items: center; gap: .8rem;
  padding: .8rem 1rem; background: var(--surface);
  border: 1.5px solid var(--border); border-radius: var(--radius-md);
  counter-increment: place; cursor: grab;
}
.arrange-item::before { content: counter(place); font-family: var(--font-label); font-size: .82rem; color: var(--text-muted); min-width: 1.4rem; }
.arrange-item--dragging { opacity: .5; }
.arrange-item--over { border-color: var(--selected); box-shadow: inset 0 0 0 1px var(--selected); }
/* Drawn, never read: the grip says "this moves" to a sighted pupil and is
   aria-hidden for everyone else, who have the buttons. */
.arrange-grip { color: var(--text-muted); letter-spacing: -.25em; user-select: none; }
.arrange-text { flex: 1; }
.arrange-place { margin-inline-start: auto; }
.arrange-place[hidden] { display: none; }
.arrange-select {
  font: inherit; padding: .35rem .5rem; border: calc(var(--look-border-control) * 1.5) solid var(--border-strong);
  border-radius: var(--look-radius-control); background: var(--surface); color: var(--text);
}
.arrange-select:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.arrange-moves { display: flex; gap: .3rem; margin-inline-start: auto; }
.arrange-moves[hidden] { display: none; }
/* 44px each way, because these are pressed on a school laptop's trackpad and a
   tablet as often as with a keyboard. */
.arrange-move {
  min-width: 2.75rem; min-height: 2.75rem; border: calc(var(--look-border-control) * 1.5) solid var(--border-strong);
  border-radius: var(--look-radius-control); background: var(--surface); color: var(--text);
  font-size: 1.1rem; cursor: pointer;
}
.arrange-move:hover:not(:disabled) { border-color: var(--selected); background: var(--selected-surface); }
.arrange-move:disabled { opacity: .35; cursor: default; }
.arrange-move:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* --- Match up — see practice/_matching and matching_controller ------------
   Two columns of bars: the key words, then their explanations. A pair shares
   a number, and the number is the state; the colour only repeats it. The bar
   is the .option recipe over again — the same edge, radius and padding — so
   a pupil who has just answered an MCQ meets the same shape. Amber is the
   bar that is waiting for its other half, for the reason .answer-error is
   amber: nothing is wrong, a step is missing. */
.pair {
  display: grid; gap: 1rem 1.25rem; margin: 1.5rem 0; align-items: start;
  grid-template-columns: minmax(11rem, 2fr) 3fr;
}
.pair-side--keys[hidden] { display: none; }
/* Without a script the key words are the choices in every select, and the
   explanations take the whole width. */
.pair:has(.pair-side--keys[hidden]) { grid-template-columns: 1fr; }
@media (max-width: 40rem) { .pair { grid-template-columns: 1fr; } }
.pair-heading {
  display: flex; align-items: center; min-height: 1.9rem; margin: 0 0 .6rem;
  font-family: var(--font-label); font-size: .72rem; letter-spacing: .08em;
  text-transform: uppercase; color: var(--text-muted);
}
.pair-heading[hidden] { display: none; }
.pair-prompt {
  display: flex; align-items: center; min-height: 1.9rem; margin: 0 0 .6rem;
  padding: .25rem .7rem; border-radius: var(--radius-sm);
  background: var(--warning-surface); color: var(--warning-text);
  font-size: .9rem; font-weight: 600;
}
.pair-prompt[hidden] { display: none; }
.pair-list { display: grid; gap: .55rem; margin: 0; padding: 0; list-style: none; }
.pair-bar {
  display: flex; align-items: center; gap: .8rem; width: 100%; text-align: start;
  padding: .8rem 1rem; background: var(--surface); color: var(--text);
  border: 1.5px solid var(--border); border-radius: var(--radius-md); cursor: pointer;
  font: inherit; line-height: 1.35;
  transition: border-color .12s ease, background-color .12s ease;
}
.pair-bar[hidden] { display: none; }
.pair-bar:hover { border-color: var(--border-strong); }
.pair-bar:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* Dashed and empty until paired, then filled with the pair's number. A key
   word shows its own number from the start, since the number is its. */
.pair-badge {
  flex: none; width: 1.5rem; height: 1.5rem; border-radius: 50%;
  display: grid; place-items: center;
  border: 2px dashed var(--border-strong); color: var(--text-muted);
  font-family: var(--font-label); font-size: .78rem; font-weight: 600;
}
.pair-bar--paired { border-color: var(--selected); box-shadow: inset 0 0 0 1px var(--selected); }
.pair-bar--paired .pair-badge { border: 2px solid var(--selected); background: var(--selected); color: var(--surface); }
.pair-bar--paired .pair-text, .pair-bar--pending .pair-text { font-weight: 600; }
.pair-bar--pending { border-color: var(--warning); background: var(--warning-surface); box-shadow: inset 0 0 0 1px var(--warning); }
.pair-bar--pending .pair-badge { border-color: var(--warning); color: var(--warning-text); }
.pair-text { flex: 1; }
/* Without a script: the explanation, and beside it the list of key words. */
.pair-place {
  display: flex; flex-wrap: wrap; align-items: center; gap: .6rem .9rem;
  padding: .8rem 1rem; background: var(--surface);
  border: calc(var(--look-border-control) * 1.5) solid var(--border); border-radius: calc(var(--look-radius-control) + 2px);
}
.pair-place[hidden] { display: none; }
.pair-select {
  font: inherit; padding: .35rem .5rem; border: calc(var(--look-border-control) * 1.5) solid var(--border-strong);
  border-radius: var(--look-radius-control); background: var(--surface); color: var(--text);
}
.pair-select:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* --- The tray and its groups — see practice/_equation and equation_controller.
   The matching screen was this until it became two columns paired by tapping
   (PR #122); the equation keeps the tray, because its groups are the gaps in
   a line of formulae and a tile is dragged into its gap. Both are drop zones;
   the dashed edge says "put things here" and fills when something is over it. */
.match { display: grid; gap: 1rem; margin: 1.5rem 0; }
.match-tray, .match-slot {
  display: flex; flex-wrap: wrap; gap: .55rem; margin: 0; padding: .6rem;
  list-style: none; min-height: 3.6rem;
  border: calc(var(--look-border-control) * 1.5) dashed var(--border-strong); border-radius: calc(var(--look-radius-control) + 2px); background: var(--surface-sunken);
}
.match-zone--over { border-color: var(--selected); background: var(--selected-surface); }
.match-item {
  display: flex; align-items: center; gap: .3rem .5rem; padding: .55rem .8rem;
  background: var(--surface); border: calc(var(--look-border-control) * 1.5) solid var(--border); border-radius: calc(var(--look-radius-control) + 2px); cursor: grab;
}
/* Under a heading the group is narrow, so the select drops beneath the words
   rather than squeezing them into a column three words tall — seen on the
   first screenshot. In the tray there is room, and the row stays a row. */
.match-slot .match-item { flex-wrap: wrap; }
.match-slot .match-text { flex: 1 1 auto; min-width: 0; }
/* The select behind the drag — see practice/_matching. Clipped, not hidden:
   it keeps its place in the tab order and the accessibility tree, and comes
   back the moment its item is hovered or anything in it has focus. The class
   is put on by the controller, so with no script every select is plain. */
.match-place--behind {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
.match-item { position: relative; }
.match-item:hover .match-place--behind, .match-item:focus-within .match-place--behind {
  position: static; width: auto; height: auto; overflow: visible;
  clip: auto; clip-path: none; white-space: normal;
}
/* "Drop here", drawn only while the group is empty. */
.match-empty { color: var(--text-muted); font-size: .85rem; padding: .3rem .2rem; list-style: none; }
.match-slot:has(.match-item) .match-empty { display: none; }
.match-item--dragging { opacity: .5; }
.match-groups { display: grid; gap: .8rem; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); }
.match-groups[hidden] { display: none; }
.match-group { display: grid; gap: .4rem; }
.match-heading { margin: 0; font-family: var(--font-label); font-size: .9rem; font-weight: 600; }

/* --- Balance the equation — see practice/_equation. The formulae sit in
   one line, each with the slot its number drops into in front of it, and the
   tiles wait in a tray below. The slot is a match-slot and the tile a
   match-item, so the drag, the hover-to-choose select and the marking of the
   drop zone are the matching's; what is set here is only the shape. */
.match--equation .equation {
  display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .6rem;
  margin: .4rem 0 1rem; font-size: 1.4rem;
}
.match--equation .equation[hidden] { display: none; }
.equation-term { display: inline-flex; align-items: center; gap: .35rem; }
.equation-formula { font-weight: 500; }
.equation-joiner { color: var(--text-muted); }
.match--equation .equation-slot {
  min-width: 3.4rem; min-height: 2.8rem; padding: .2rem; gap: .2rem;
  align-items: center; justify-content: center;
}
.match--equation .equation-slot .match-empty { padding: 0; font-size: 1.1rem; }
.equation-tray { gap: .6rem; }
.equation-tile {
  flex: 0 0 auto; min-width: 3.2rem; justify-content: center; text-align: center;
  font-size: 1.3rem; font-weight: 600; padding: .35rem .6rem;
}
.equation-tile .arrange-grip { display: none; }
.match--equation .equation-slot .equation-tile { border: 0; background: transparent; padding: .1rem .3rem; min-width: 0; }
/* The marked record: the same line, each number coloured by whether it is
   the one that belongs there. */
.marked--equation {
  display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .6rem;
  margin: .4rem 0 .8rem; font-size: 1.4rem;
}
.equation-number {
  display: inline-block; min-width: 2.2rem; text-align: center; padding: .1rem .45rem;
  border-radius: var(--look-radius-control); border: calc(var(--look-border-control) * 1.5) solid var(--border-strong); font-weight: 600;
}
.equation-number--right { border-color: var(--success); background: var(--success-surface); color: var(--success-text); }
.equation-number--wrong { border-color: var(--error); background: var(--error-surface); color: var(--error-text); }

/* A publisher's maths, rendered — see Content::Markup. Scripts are set a
   touch smaller and never change the line height, so a formula in an option
   does not push its neighbours apart. */
.math sub, .math sup { font-size: .78em; line-height: 0; }

/* --- The picture on the question — see practice/_stem_image. Never wider than
   the column, never taller than a laptop's viewport leaves for the answer. */
.stem-figure { margin: 1rem 0 0; }
/* The figure is a button so it can be opened from the keyboard — see
   practice/_stem_image — and a button is stripped back to the picture it wraps:
   no chrome of its own, because the frame belongs to the <img> and a second
   border round it would read as two figures. Inline-block so the hit area is
   the picture and not the width of the column. */
.stem-open {
  display: inline-block; padding: 0; border: 0;
  background: none; cursor: zoom-in; max-width: 100%; font: inherit;
}
.stem-image {
  display: block; max-width: 100%; max-height: 22rem; height: auto;
  border-radius: var(--radius-md); border: 1px solid var(--border); background: var(--figure-plate);
}
/* The affordance, UNDER the picture rather than over its corner. It began over
   the corner, which is where an image viewer usually puts it, and the first
   screenshot showed it sitting on top of the figure's own label — Oak letter
   the corners of their diagrams, and a chip over a letter is the one part of
   the picture a pupil most needs. Under it, nothing can be covered.

   aria-hidden and decorative: the button already says what it does, and this
   is for the pupil who can see the figure and would otherwise have no reason
   to think it opens. */
.stem-open-hint {
  display: block; margin-top: .3rem; text-align: end;
  color: var(--text-muted); font-family: var(--font-label); font-size: .7rem;
}
.stem-open:hover .stem-image, .stem-open:focus-visible .stem-image { border-color: var(--focus-ring); }
/* The picture at its own size. Bounded by the viewport rather than by a fixed
   number: the point of opening it is to see it bigger, and a figure drawn at
   1600px wide has more to give than one drawn at 200px. */
.figure-dialog {
  max-width: 92vw; max-height: 92vh; padding: 1rem; border: 0; border-radius: var(--radius-lg);
  background: var(--surface); color: var(--text); box-shadow: var(--shadow-dialog);
}
.figure-dialog::backdrop { background: var(--scrim); }
.figure-dialog-image {
  display: block; max-width: 100%; max-height: 80vh; height: auto;
  border-radius: var(--radius-sm); background: var(--figure-plate);
}
.figure-dialog form { margin: .75rem 0 0; display: flex; justify-content: flex-end; }

/* --- Fill the gap — see practice/_short_answer and PracticeHelper#question_stem.
   The blank in the stem is a drawn line with a hidden word for readers; the
   box is a single line under it, wide enough for a phrase and no wider. */
.stem-gap { display: inline-block; min-width: 5ch; border-bottom: 2px solid var(--border-strong); vertical-align: baseline; }

/* --- Read aloud ---------------------------------------------------------

   The question read out, with the word being spoken lit as it is said. The
   highlight is the feature and the audio is the delivery — see
   read_aloud_controller.js — so most of what is here is about making one word
   in a sentence unmistakable without making the sentence jump.

   NOTHING MOVES. No transform, no scale, no weight change on the current word:
   every one of those reflows the line, and a line that reflows under a reader
   who is following it word by word has undone the thing it was helping with.
   Only colour and an underline change, both of which are painted rather than
   laid out. */

.read-aloud-word {
  /* Painted through the padding so the fill has room to be a block rather than
     a tight outline of the glyphs, without the box taking up more space. */
  padding: .08em .04em;
  border-radius: 3px;
}

.read-aloud-word--past { color: var(--text-muted); }

.read-aloud-word--now {
  background: var(--read-aloud-bg);
  color: var(--read-aloud-ink);
  /* NEVER COLOUR ALONE — the brand guide's rule, and here it is load-bearing
     rather than a formality: the default and warm palettes put the fill only
     1.5:1 and 2.4:1 from the page, which is legible as a highlight but is not
     something a reader with a colour deficiency should have to depend on. */
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}

/* The gap in a short-answer stem is a visually-hidden word inside a drawn box —
   see read_aloud/tokenise.js. Highlighting the hidden word paints nothing, so
   the box it sits in takes the highlight instead and the cursor does not appear
   to skip a beat where the answer goes. */
.stem-gap:has(.read-aloud-word--now) { background: var(--read-aloud-bg); }

/* Transitions are a nicety here and a hazard for anyone who asked for less
   motion, so they are opt-in rather than opt-out. */
@media (prefers-reduced-motion: no-preference) {
  .read-aloud-word { transition: background-color 90ms linear, color 90ms linear; }
}

/* Room on both sides, because this control sits between two things it is not
   part of — the stem above it and, on four different screens, the answers, the
   translate button or the marked list below. At .75rem above and nothing below
   it read as though it belonged to whichever of those it happened to be nearest,
   which on the answered screen was the marked options. */
/* --- The reading supports, on one line ------------------------------------
   Read aloud, Key words and the translation are three ways into the same
   question, and stacked they were three rows of chrome between the stem and
   the answer — the objection the 25 August tab-order note makes about stops,
   in pixels rather than keystrokes.

   They are laid out here rather than wrapped in a container in the templates,
   because which of the three a screen carries differs: a recap step has no
   word bank, a baseline no translation. Inline-flex siblings flow onto one
   line and wrap when they must, whichever are present, with no change to the
   DOM and so none to the tab order. The translation panel between them is a
   block and stays one: revealed, it belongs on its own line under the row. */
.read-aloud-controls,
.word-bank,
.translate-form {
  display: inline-flex;
  vertical-align: top;
  margin: 0 .5rem .5rem 0;
}

/* ⚠️ THE DISCLOSURE IS A DROPDOWN, NOT A PANEL THAT PUSHES THE ROW APART, and
   it has to be once the row exists. As a block it opened downwards and the
   things beside it stayed where they were; laid out in a row, an opening
   details either lays its definitions out sideways — inline-flex putting the
   list next to its own summary, which is a balloon across the page — or grows
   the row and moves the button the pupil just pressed. Neither is a thing to do
   to somebody who is looking up a word. Floated over the page it costs the
   layout nothing, which is what .profile-menu does a few hundred lines up. */
.word-bank { position: relative; display: inline-block; }

.question-tools-row { margin: 1.25rem 0 .5rem; }

.read-aloud-controls { margin-top: 0; }

/* The line that says a read failed. Empty almost always, and then it takes no
   room; it is never `hidden`, because a live region has to be in the page
   before its words arrive to be heard saying them. */
.read-aloud-failed { align-self: center; margin-left: .5rem; font-size: .85rem; color: var(--text-muted); }

.read-aloud-failed:empty { margin: 0; }

.read-aloud-button {
  display: inline-flex; align-items: center; gap: .45rem;
  padding: .4rem .8rem;
  font: inherit; font-size: .9rem; font-weight: 500;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  cursor: pointer;
}

.read-aloud-button:hover { background: var(--surface-sunken); }

/* Pressed is a state a pupil reads at a glance while the voice is running, so
   it is the filled version of the same control rather than a subtler edge. */
.read-aloud-button[aria-pressed="true"] {
  background: var(--read-aloud-bg);
  color: var(--read-aloud-ink);
  border-color: var(--read-aloud-ink);
}

.read-aloud-icon { width: 1.15rem; height: 1.15rem; }

/* The same control, one per message of the tutor's, on the speaker's line
   beside "Tutor" (ReadMessageAloud). Smaller than the question's, because
   there can be a dozen in a chat and they are not the screen's main control —
   but no smaller than WCAG 2.2's 24px target, which a pupil on a tablet is
   pressing with a finger. The label is the button's own, so it drops the
   speaker line's capitals.

   ⚠️ THE LABEL SITS BESIDE THE BUTTON, NOT UNDER IT. Under it, as every other
   icon button's is, it covered the first line of the very message the pupil
   had just asked to hear, and stayed there while it was read, because the
   button keeps focus. The rest of the speaker line is empty, so it goes there.
   Centred under the button it had also run off the edge of the chat panel,
   which scrolls and so clips; beside it, it could run off the other edge, so
   the wrapper fills the rest of the line and is measured as a container, and
   the label wraps rather than pass its end. Its foot is level with the
   button's, so a label that wraps grows upwards, into the gap between this
   message and the one before it, and never down over the words.
   `.read-message-button.icon-button`, two classes: `.icon-button .tip`, further
   down, is as specific as one and later, and won while this said `.read-message
   .tip` — the label was centred under the button the whole time it claimed to be
   anchored at the left. */
.who:has(.read-message) { display: flex; align-items: center; gap: .35rem; }
.read-message { flex: 1 1 0; container-type: inline-size; margin: 0 0 0 .15rem; }
.read-message-button {
  min-inline-size: 1.6rem; min-block-size: 1.6rem;
  padding: .1rem .35rem; justify-content: center;
}
.read-message-button .read-aloud-icon { width: .9rem; height: .9rem; }
.read-message-button.icon-button .tip {
  top: auto; bottom: 0; left: calc(100% + .4rem); transform: none;
  inline-size: max-content; max-inline-size: calc(100cqi - 100% - .4rem);
  white-space: normal; text-transform: none; letter-spacing: normal;
}

/* --- Saying an answer instead of typing it --------------------------------

   The microphone lives INSIDE the field, bottom right, where a send button
   lives in every messaging app. It began as a labelled button on a row of its
   own and that was a fourth row of chrome between the question and the answer;
   as an icon in the corner it reads as part of the box it fills. */
.answer-box, .chat-composer { display: block; }

.field-with-mic { position: relative; display: block; }

/* A textarea is inline-level by default, so its line box left a few pixels of
   descender under it and the buttons sat astride the border rather than inside
   the box. */
.field-with-mic > textarea,
.field-with-mic > input[type="text"] { display: block; width: 100%; margin: 0; }

/* Room for the icon, so a long answer never runs under it. */
.field-with-mic > textarea { padding-right: 5.5rem; }

.field-with-mic--line > input[type="text"] { padding-right: 5rem; }

/* The chat composer: a box that wraps, one line tall until the words need
   more and then growing to a few lines before it scrolls — see
   composer_controller.js, which does the growing where field-sizing is not
   supported. The microphone keeps to the bottom corner, where the last line
   is, and the padding keeps every line clear of it. */
.field-with-mic--chat > textarea {
  resize: none; overflow-y: auto;
  min-height: 2.4rem; max-height: 8.5rem;
  field-sizing: content;
}
.field-with-mic--chat .dictation-buttons { bottom: .3rem; right: .35rem; }

.dictation-controls { display: contents; }

/* The cross and the tick sit together, bottom right, in that order: discard is
   to the left of keep, which is the order every dialog in this product puts
   them in. */
.dictation-buttons {
  position: absolute;
  right: .6rem;
  bottom: .6rem;
  display: flex; align-items: center; gap: .35rem;
}

.field-with-mic--line .dictation-buttons { bottom: 50%; transform: translateY(50%); }

.dictation-button {
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .35rem;
  min-width: 2rem; min-height: 2rem;
  justify-content: center;
  color: var(--text-muted);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  cursor: pointer;
}

.dictation-button:hover { background: var(--surface-sunken); color: var(--text); }

.dictation-button:disabled { opacity: .45; cursor: default; }

/* Try again is words, not an icon (see the component), so it takes a pill's
   padding rather than a circle's, and the status line beside it gives up the
   room it needs so the two never overlap. */
.dictation-button--retry { padding: .35rem .8rem; font-size: .85rem; color: var(--text); }

.dictation-controls:has(.dictation-button--retry:not([hidden])) .dictation-status { padding-right: 11rem; }

/* Listening is a state a pupil reads at a glance across a room, so it is the
   filled version of the same control — read-aloud's argument, and the same
   fill, because both mean "this is running now". */
.dictation-button[aria-pressed="true"] {
  background: var(--read-aloud-bg);
  color: var(--read-aloud-ink);
  border-color: var(--read-aloud-ink);
}

.dictation-icon { width: 1.15rem; height: 1.15rem; }

/* Which icon the one button wears. In the stylesheet because `hidden` is an
   HTMLElement property and does nothing at all on an SVG — see the partial. */
.dictation-icon--accept { display: none; }

.dictation-button--accept .dictation-icon--mic { display: none; }

.dictation-button--accept .dictation-icon--accept { display: block; }

/* Keeping what was said is the affirmative action on the field, so it is the
   filled control — the same treatment Check my answer gets, one size down. */
/* Written to out-specify .dictation-button[aria-pressed="true"] above, which it
   would otherwise lose to: while it listens the control is both running AND the
   way to keep what was said, and the second is what a pupil is about to press. */
.dictation-button.dictation-button--accept[aria-pressed="true"],
.dictation-button.dictation-button--accept {
  background: var(--action-bg);
  color: var(--action-ink);
  border-color: var(--action-bg);
}

.dictation-button.dictation-button--accept:hover { background: var(--action-bg-hover); }

/* ⚠️ WHAT WAS HEARD, IN THE BOX AND VISIBLY NOT KEPT YET. Never in the box's
   own value: a recogniser that streams revises the whole utterance as it goes,
   and a box being rewritten under a pupil's caret — undo stack, maxlength,
   input events and all — is not a thing to do to them. So it is an overlay that
   looks like the box, which needs two halves.

   FIRST, IT IS THE WHOLE FIELD AND NOT A CORNER OF IT. Pinned to the top left,
   a second dictation drew its words across the first sentence instead of after
   it. The controller copies the field's font, padding and wrapping onto
   .dictation-preview-text and renders the text either side of the caret into it
   as well, so the browser puts every character exactly where the field would.

   SECOND, THE FIELD'S OWN TEXT GOES TRANSPARENT WHILE IT IS UP (.field--
   dictating below), so what is on screen is this and not two nearly-aligned
   copies of the same words. */
.dictation-preview {
  position: absolute;
  inset: 0;
  margin: 0;
  overflow: hidden;
  pointer-events: none;
}

/* Sized as the field is, borders included, because the window above is the
   whole border box and the text has to start where the field's text starts. */
.dictation-preview-text {
  display: block;
  box-sizing: border-box;
  width: 100%;
  border-style: solid;
  border-color: transparent;
  color: var(--text);
}

/* ⚠️ THE ONE THING ON SCREEN THAT SAYS THESE WORDS ARE NOT KEPT YET, and so the
   one thing that makes the cross safe to press: grey and italic is what every
   editor uses for text that is provisional, and a pupil can see at a glance
   which half of the box the cross would take and which half it would not. */
.dictation-preview-words {
  color: var(--text-muted);
  font-style: italic;
}

.dictation-preview[hidden] { display: none; }

/* The field, while the overlay above is standing in for it. Its text is drawn
   transparent rather than emptied — the value is untouched, so a dictation
   thrown away costs nothing to put back — and its placeholder goes with it:
   "Write your answer here" otherwise reads out from underneath the sentence a
   pupil is halfway through saying, telling them to do the thing they are doing. */
input.field--dictating, textarea.field--dictating { color: transparent; }

input.field--dictating::placeholder,
textarea.field--dictating::placeholder { color: transparent; }

/* Empty for all but the first press ever, and an empty flex item still takes
   its gap — which is what pushed the icon off the centre of its own button. */
.dictation-progress { font-size: .75rem; font-variant-numeric: tabular-nums; }

.dictation-progress:empty { display: none; }

/* ⚠️ OVER THE BOX, AND NOT BESIDE IT. What the microphone is doing is a fact
   about the box the words are going into, so it is drawn on the box: centred,
   over a wash of the field's own background so the placeholder behind it does
   not read through. It takes no pointer events, so it can never come between a
   pupil and the thing they are writing in, and it is empty and invisible
   whenever there is nothing to say. */
.dictation-status {
  gap: .45rem;
  position: absolute;
  inset: auto .5rem .6rem .75rem;
  margin: 0;
  display: flex; align-items: center;
  padding-right: 5rem;
  font-size: .9rem;
  color: var(--text);
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  border-radius: var(--radius-md);
  pointer-events: none;
}

/* :has, because the region is never empty now — it holds the dots and a span
   for the words — and a wrapper with nothing in it must still not draw. */
.dictation-status:not(:has(span:not([hidden]):not(:empty))) { display: none; }

.dictation-status .dots { padding-top: 0; }

/* The settings section. Two options side by side, matching the palette and type
   pickers on the same page — see settings/_read_aloud. */
.read-aloud-options {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: .75rem; margin-bottom: 1rem;
}

.read-aloud-option {
  display: flex; flex-direction: column; gap: .2rem;
  padding: .8rem .9rem;
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--surface); cursor: pointer;
}

/* ⚠️ :checked ALONE, AND NO SERVER-RENDERED --current CLASS BESIDE IT.
   .option-input is visually hidden and pointer-events: none, so without a
   :checked rule a pupil clicks an option and nothing on the screen changes —
   this picker has no preview controller repainting behind it the way the
   palette has, so the click looked broken and the setting looked stuck on.

   The first fix added :checked and kept a --current class for the saved value,
   which is the shape the palette and language pickers use. Here that lit BOTH
   cards as soon as a pupil chose the other one: --current stayed on the saved
   option while :checked moved to the new one. The radio is already rendered
   with `checked`, so :checked describes the saved state and the just-chosen one
   with a single rule, and exactly one card can ever match it. */
.read-aloud-option:has(.option-input:checked) {
  border-color: var(--selected);
  background: var(--selected-surface);
}

.read-aloud-option:has(.option-input:focus-visible) { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

.read-aloud-name { font-weight: 600; }

.read-aloud-detail { font-size: .88rem; color: var(--text-muted); }

/* The voice picker: the same card, with the radio's label filling it and the
   Listen button beside it rather than inside it — a button in a label is two
   controls answering one press (SettingsPage.tsx, VoicePicker). The label
   fills the card so a press anywhere but the button still chooses. */
.voice-option {
  flex-direction: row; align-items: center; justify-content: space-between;
  gap: .75rem; padding: 0 .6rem 0 0;
}
.voice-choice {
  flex: 1; display: flex; align-items: center;
  padding: .8rem .9rem; cursor: pointer;
}

/* --- The word bank ------------------------------------------------------

   The science words on a recap slide, marked where they are, and what they
   mean behind one disclosure on the slide's tools row. A recap slide only:
   on a question the marks gave the answer away (ruled 7 October 2026). Two
   paths to one thing: the mark is for the eye and carries no tabindex, no
   role and no aria — see web/app/lib/word-bank-marks.ts — and the disclosure
   is the keyboard and screen-reader path.

   NOTHING MOVES, on the same grounds the read-aloud block gives at greater
   length: a pupil is reading the sentence these words are in, and a mark that
   changed the line's height or the word's width would reflow the thing they
   are following. A dotted underline is painted, not laid out. */

.word-bank-term {
  text-decoration: underline dotted var(--border-strong);
  /* The same offset as .read-aloud-word--now, so that when read-aloud lights a
     marked word the two underlines land on one line rather than two. */
  text-underline-offset: 3px;
  cursor: help;
}

/* ⚠️ OPT IN, AND THE UNDERLINE STAYS UNDER IT. A pupil who asked for the words
   to be filled in gets the fill AS WELL AS the dotted line, never instead of
   it: colour is never the only thing saying which words these are — the
   brand guide's rule, and the same one the read-aloud highlight is built on.

   Scoped to an attribute on the column rather than to a class on each mark,
   because the marks are written by the server and then taken apart by
   read_aloud_controller.js; a class on the mark would have to survive that,
   and an attribute on the element enclosing everything simply does. */
[data-word-bank-highlight="true"] .word-bank-term {
  background: var(--word-bank-bg);
  color: var(--word-bank-ink);
  padding: .08em .12em;
  border-radius: 3px;
}

/* Both fills on one word: the word being spoken that also has a meaning behind
   it. Read-aloud wins, because it is the one that is moving and the one the
   pupil is following — the highlight is still there under it as the dotted
   line, and comes back the moment the voice passes on. */
[data-word-bank-highlight="true"] .word-bank-term .read-aloud-word--now,
[data-word-bank-highlight="true"] .read-aloud-word--now .word-bank-term {
  background: var(--read-aloud-bg);
  color: var(--read-aloud-ink);
}

/* An exam command word where it gives the order, in a stem or a question back
   (web/app/lib/command-word-marks.ts). Always on, unlike the key words' fill:
   which word is the instruction is the first thing a pupil needs from the
   question, not a reading aid they opt into, and the exam board's papers print
   it in bold for the same reason.

   ⚠️ NOTHING MOVES, as for the marks above: the fill is a box-shadow and the
   line an underline, both painted rather than laid out, so a sentence reads at
   the width it was set at. The weight does change the word's width, once, as
   the server drew it — never while the pupil is reading. */
.command-word {
  font-weight: 700;
  color: var(--command-word-ink);
  background: var(--command-word-bg);
  box-shadow: 0 0 0 .12em var(--command-word-bg);
  border-radius: 3px;
  text-decoration: underline solid var(--command-word-line);
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
  cursor: help;
}
/* The word being spoken wins here too, for the reason it wins over a key word. */
.command-word .read-aloud-word--now,
.read-aloud-word--now .command-word {
  background: var(--read-aloud-bg);
  color: var(--read-aloud-ink);
}

/* The disclosure, drawn as the read-aloud pill beside it — they sit under the
   same question and do the same kind of job, and two different-looking small
   controls in one place read as two different kinds of thing. */
.word-bank > summary { white-space: nowrap; }

.word-bank > summary {
  display: inline-flex; align-items: center; gap: .45rem;
  width: fit-content;
  padding: .4rem .8rem;
  font: inherit; font-size: .9rem; font-weight: 500;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  cursor: pointer;
  list-style: none;
}

.word-bank > summary::-webkit-details-marker { display: none; }
.word-bank > summary:hover { background: var(--surface-sunken); }
.word-bank > summary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* ⚠️ A DRAWN ARROW, NEVER `content: "\25B8"`. The announcement baseline caught
   that once already, on the teaching disclosure: a character in `content` is a
   character in the accessibility tree, and the control announced itself as
   "▾GO OVER IT AGAIN". A border triangle has no text to announce. */
.word-bank > summary::after {
  content: ""; width: 0; height: 0;
  border-left: .3rem solid transparent;
  border-right: .3rem solid transparent;
  border-top: .35rem solid currentColor;
}

.word-bank[open] > summary::after { transform: rotate(180deg); }

/* ⚠️ --surface, NOT --surface-sunken. The question column sits ON the sunken
   ground, so a panel tinted with it is invisible — the definitions ran loose
   down the page with nothing holding them. The screenshot caught it; no test
   would have. */
.word-bank > dl {
  position: absolute; left: 0; top: calc(100% + .4rem); z-index: 20;
  width: max-content; max-width: min(30rem, 80vw);
  max-height: 22rem; overflow-y: auto;
  margin: 0; padding: .75rem .9rem;
  box-shadow: var(--shadow-menu);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  font-size: .92rem; line-height: 1.5;
}

.word-bank dt { font-weight: 600; }
.word-bank dd { margin: 0 0 .6rem; color: var(--text-muted); }
.word-bank dd:last-child { margin-bottom: 0; }

/* The board's own word, where the school follows one that uses it. Set apart
   from the meaning rather than run on from it: it answers a different question,
   and a pupil who does not need it should be able to stop reading at the end of
   the definition. */
.word-bank-board { display: block; font-size: .85rem; color: var(--text); }

.word-bank-empty { margin: 0; font-size: .92rem; color: var(--text-muted); }

.short-answer-label { display: block; margin: 1.5rem 0 .4rem; font-family: var(--font-label); font-size: .9rem; color: var(--text-muted); }
.short-answer {
  font: inherit; width: 100%; max-width: 24rem; padding: .7rem .9rem;
  border: calc(var(--look-border-control) * 1.5) solid var(--border-strong); border-radius: calc(var(--look-radius-control) + 2px);
  background: var(--surface); color: var(--text);
}
.short-answer:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* Continue, and the Explain bubble beside it — see practice/_explain. */
.answered-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem .8rem; margin-top: 1.5rem; }
.answered-actions form.button_to { margin: 0; }
.chose-label {
  display: block; font-family: var(--font-label); font-size: .82rem;
  color: var(--text-muted);
}

/* --- Feedback ------------------------------------------------------------
   THE GROUND SAYS RIGHT OR WRONG. THE EDGE ADDS THE NUANCE, AND NEVER
   CONTRADICTS IT.

   Two channels, and they do not do each other's job. The surface carries the
   verdict and is the only thing that does; the border says what kind of right
   or wrong it was.

   Splitting them is what the old rule was missing. `fragile` is emitted for a
   CORRECT answer given with "I think so" or "I'm guessing" — see
   Practice::SubmitAnswer#carry and Review::Verdict#held, which are the only
   two things that produce it — and it shared a rule with `stuck`, a question
   being taken off the pupil and handed to a teacher. So getting it right while
   unsure was drawn pixel for pixel like being stopped. It sits on the success
   ground now with the honey accent as its edge: right, and carried forward,
   which is what accent means everywhere else in this product.

   `reframe` is amber and not the functional red. It is the tone for a
   misconception the product has actually identified, which is the most useful
   thing that can happen on this screen — and a child reads red as a mark. Red
   stays for what has genuinely gone wrong.

   `stuck` takes the plain surface and a strong edge, because it is not a
   verdict on the pupil: it is a hand-off. (It used to share the rule with the
   Challenge phase's not_yet, which had taken the sunken surface — the same
   colour as the body background in all four palettes, so that panel drew no
   panel at all. Measured on the page; worth remembering before the sunken
   surface is used for a panel again.)

   A full 1px border rather than the 4px rule alone. Under high contrast the
   success, warning and error surfaces are all the same white, and that rule
   was the entire difference between a right answer and a wrong one; outlining
   the panel gives the tone a shape as well as an edge. Logical, so the rule
   stays on the reading edge in Arabic and Urdu. */

.feedback {
  margin: 1.5rem 0; padding: 1.1rem 1.2rem; border-radius: var(--look-radius);
  border: var(--look-border) solid; border-inline-start-width: 4px;
}
.feedback p { margin: 0; }
.feedback--secure, .feedback--cleared, .feedback--floor, .feedback--climb {
  background: var(--success-surface); border-color: var(--success);
}
.feedback--fragile {
  background: var(--success-surface); border-color: var(--accent);
}
.feedback--reframe {
  background: var(--warning-surface); border-color: var(--warning);
}
.feedback--stuck {
  background: var(--surface); border-color: var(--border-strong);
}

/* THE MOMENT A RIGHT ANSWER GETS. An outcome, not a sticker: a tick drawn, a
   handful of specks, the chosen option settling — and the kicker line, which
   stays when the motion does not. The line and the colours are in the server's
   markup; everything that moves hangs off `data-arrived`/`--arrived`, which
   only the browser sets, and only after a submit from inside the turn
   (lib/thread-motion.ts), so a reload, a tab on the strip or a poll never
   replays it. CSS only, no sound, no focus change, and all of it under
   no-preference motion: a pupil who asked for stillness gets the line and the
   colour and nothing that travels. Never a count — there is no streak here. */
p.outcome-kicker {
  display: flex; align-items: center; gap: .6rem; margin: 0 0 .5rem;
  font-family: var(--font-label); font-weight: 700; font-size: 1.25rem;
  color: var(--success-text);
}
/* A filled badge rather than a bare stroke: at the size of a line of text a
   thin tick read as decoration, not as the answer being right. The circle is
   --correct, the green kept for this alone, and the tick is cut out of it in
   --surface, which holds in every theme including high contrast. */
.outcome-tick { width: 2rem; height: 2rem; flex: 0 0 auto; color: var(--surface); }
.outcome-tick circle { fill: var(--correct); }
.outcome-burst { display: none; }
.feedback--arrived, .thread-outcome--arrived { position: relative; overflow: visible; }
/* ⚠️ PACED, NOT QUICK. The first cut ran in 0.6s and the user read it as
   "a bit fast and not celebratory enough": over before the eye had left the
   option it chose. Now it runs as a sequence — the option, then the badge,
   then the tick, the confetti and the words — and is over in about 1.5s. It
   never holds anything up: Continue is live from the first frame. */
@media (prefers-reduced-motion: no-preference) {
  .outcome[data-arrived] .marked-option--right,
  .outcome[data-arrived] .equation-number--right {
    animation: outcome-pop .8s cubic-bezier(.3, 1.5, .5, 1) both;
  }
  .feedback--arrived, .thread-outcome--arrived {
    animation: outcome-rise .5s cubic-bezier(.2, .9, .3, 1.2) .15s both,
      outcome-glow 1.2s ease-out .45s both;
  }
  .feedback--arrived .outcome-tick, .thread-outcome--arrived .outcome-tick {
    animation: outcome-badge .6s cubic-bezier(.3, 1.6, .5, 1) .3s both;
  }
  .feedback--arrived .outcome-tick path, .thread-outcome--arrived .outcome-tick path {
    stroke-dasharray: 18; stroke-dashoffset: 18;
    animation: outcome-draw .4s ease-out .65s forwards;
  }
  .feedback--arrived .outcome-kicker-text, .thread-outcome--arrived .outcome-kicker-text {
    display: inline-block;
    animation: outcome-word .55s cubic-bezier(.3, 1.6, .5, 1) .55s both;
  }
  .feedback--arrived .outcome-burst, .thread-outcome--arrived .outcome-burst {
    display: block; position: absolute; inset-block-start: 1.9rem; inset-inline-start: 2rem;
    width: 0; height: 0; pointer-events: none;
  }
  /* Strips and dots, in three colours, thrown two distances: one ring of
     identical specks read as a pattern, not as confetti. */
  .outcome-burst i {
    position: absolute; inset-block-start: -.15rem; inset-inline-start: -.35rem;
    width: .7rem; height: .3rem; border-radius: var(--radius-sm);
    background: var(--success); opacity: 0;
    animation: outcome-fly-far 1.1s cubic-bezier(.15, .75, .3, 1) .6s forwards;
  }
  .outcome-burst i:nth-child(even) {
    animation-name: outcome-fly-near; animation-delay: .68s;
  }
  .outcome-burst i:nth-child(3n) { background: var(--accent); }
  .outcome-burst i:nth-child(3n+1) {
    background: var(--phase-practice-bg); width: .45rem; height: .45rem; border-radius: 50%;
  }
  /* Each piece points its own way by the individual `rotate`, which composes
     outside `transform`: the flight is then one keyframe per distance, with no
     custom property to carry the angle. */
  .outcome-burst i:nth-child(1) { rotate: 0deg; }
  .outcome-burst i:nth-child(2) { rotate: 20deg; }
  .outcome-burst i:nth-child(3) { rotate: 40deg; }
  .outcome-burst i:nth-child(4) { rotate: 60deg; }
  .outcome-burst i:nth-child(5) { rotate: 80deg; }
  .outcome-burst i:nth-child(6) { rotate: 100deg; }
  .outcome-burst i:nth-child(7) { rotate: 120deg; }
  .outcome-burst i:nth-child(8) { rotate: 140deg; }
  .outcome-burst i:nth-child(9) { rotate: 160deg; }
  .outcome-burst i:nth-child(10) { rotate: 180deg; }
  .outcome-burst i:nth-child(11) { rotate: 200deg; }
  .outcome-burst i:nth-child(12) { rotate: 220deg; }
  .outcome-burst i:nth-child(13) { rotate: 240deg; }
  .outcome-burst i:nth-child(14) { rotate: 260deg; }
  .outcome-burst i:nth-child(15) { rotate: 280deg; }
  .outcome-burst i:nth-child(16) { rotate: 300deg; }
  .outcome-burst i:nth-child(17) { rotate: 320deg; }
  .outcome-burst i:nth-child(18) { rotate: 340deg; }
  @keyframes outcome-pop {
    0% { transform: scale(1); }
    30% { transform: scale(1.07); box-shadow: 0 0 0 .5rem var(--success-surface); }
    60% { transform: scale(.99); }
    100% { transform: scale(1); box-shadow: 0 0 0 0 transparent; }
  }
  @keyframes outcome-rise { from { opacity: 0; transform: translateY(12px) scale(.97); } }
  @keyframes outcome-glow {
    0% { box-shadow: 0 0 0 0 var(--success); }
    100% { box-shadow: 0 0 0 .9rem transparent; }
  }
  @keyframes outcome-badge {
    0% { transform: scale(0) rotate(-90deg); }
    100% { transform: scale(1) rotate(0); }
  }
  @keyframes outcome-draw { to { stroke-dashoffset: 0; } }
  @keyframes outcome-word {
    0% { opacity: 0; transform: translateX(-.5rem) scale(.8); }
    100% { opacity: 1; transform: none; }
  }
  @keyframes outcome-fly-far {
    0% { opacity: 1; transform: translateY(0) rotate(0) scale(.6); }
    15% { opacity: 1; }
    100% { opacity: 0; transform: translateY(-6.5rem) rotate(540deg) scale(1); }
  }
  @keyframes outcome-fly-near {
    0% { opacity: 1; transform: translateY(0) rotate(0) scale(.6); }
    15% { opacity: 1; }
    100% { opacity: 0; transform: translateY(-4rem) rotate(-420deg) scale(.9); }
  }
}
/* The three states a baseline question can be read back in — see
   baselines/answered. There were two class names here before and NEITHER had a
   rule, so the verdict on a baseline drew a border in currentColor and said
   nothing in colour at all. That was invisible rather than deliberate.

   ⚠️ --warning AND NOT --error FOR A WRONG ANSWER. The red is for something
   that has gone wrong — a failed save, a refused form. A wrong answer on a
   diagnostic is the product working: it is the thing the sitting was run to
   find, and the whole argument of this repository is that it is evidence
   rather than a mark. Colouring it like a failure states the opposite in the
   one channel a child reads before any of the words.

   Each one carries its word as well as its colour, in the markup. */
.feedback--correct {
  background: var(--success-surface); border-color: var(--success);
}
.feedback--incorrect {
  background: var(--warning-surface); border-color: var(--warning);
}
.feedback--unanswered {
  background: var(--surface-sunken); border-color: var(--border-strong);
}

/* The answer, given straight away rather than withheld. Practice holds it back
   because the next check has to still be a check; there is nothing to protect
   here, and the pupil came to be shown something. */
.reveal {
  margin: 1.2rem 0 0; padding: .9rem 1.1rem; border-radius: var(--radius-md);
  border: 1px dashed var(--border-strong); background: var(--surface-sunken);
}
.reveal-label {
  margin: 0 0 .2rem; font-family: var(--font-label); font-size: .82rem;
  color: var(--text-muted);
}
.reveal-text { margin: 0; font-weight: 600; }

/* --- Chat ---------------------------------------------------------------- */

/* As tall as what is in it, and no taller.

   It used to `align-self: stretch` so the two columns always ended level with
   each other. Measured on a real question, that came to a 788-pixel panel with
   nothing in the log: a box most of the page high, inviting a pupil to type
   into it and demonstrating at that size that nobody has. Ending level is
   tidiness. An empty panel drawn at the size of a conversation is a claim
   about the screen that is not true.

   Still sticky, and still capped at the viewport so a very long question
   cannot push the composer off the bottom of the screen. */
/* Stretched to the row, so the two columns always end level.

   ⚠️ THE STRETCH IS LOAD-BEARING AND WAS BRIEFLY TAKEN OUT. The panel is a
   large empty box on a screen where nobody has asked anything yet, and the
   obvious reading of that is wasted room. It is not: `align-self: stretch` is
   what stops the panel resizing as the thread grows, and two tests defend the
   two halves of that — question_translation_test, where revealing a
   translation makes the question column taller and a chat that kept its own
   height stopped short of it, and pupil_practice_test, where the panel must be
   the same size before and after a message lands so the page does not move
   under a pupil mid-sentence. The answer to an empty panel is to put something
   in it — see .chat-starters — not to shrink it and take the stability with
   it.

   Capped at the viewport so a very long question cannot push the composer off
   the bottom of the screen, and still sticky within whatever room that leaves. */
.chat {
  position: sticky; top: 1.5rem;
  align-self: stretch;
  max-height: calc(100vh - 3rem);
  display: flex; flex-direction: column;
  background: var(--surface); border: var(--look-border) solid var(--border);
  border-radius: calc(var(--look-radius) + 2px); padding: 1rem;
}
@media (max-width: 62rem) { .chat { position: static; max-height: none; align-self: auto; } }
/* Wraps. The heading and the mode chip share this row, and the chip is a
   readable label now rather than a 9.6px one, which is more than a 21rem
   column always has room for on one line. */
.chat-head {
  display: flex; align-items: center; flex-wrap: wrap; gap: .6rem;
  padding-bottom: .75rem; margin-bottom: .25rem; border-bottom: 1px solid var(--border);
}
.chat-head h2 { margin: 0; font-size: .95rem; text-transform: none; letter-spacing: -0.01em; }
.chat-empty { color: var(--text-muted); font-size: .85rem; margin: 0; }

/* Two ways in, offered rather than waited for. An empty log with a text box
   under it asks a pupil to already know what to ask, which is the hardest part
   of using a tutor and most of the reason the log stays empty on the screens
   where it would help most. These put words in the box; they do not send
   anything on their own, so a pupil can read what they are about to ask.

   Pills rather than links: they are things to press, and a row of blue
   underlined sentences in a panel of blue underlined sentences is not. */
.chat-starters { display: flex; flex-wrap: wrap; gap: .4rem; margin: .7rem 0 0; }
/* Each bubble is a form of its own — see shared/_chat_starter — and the form is
   the flex item, so it must not bring a margin of its own to the row. */
.chat-starter-form { margin: 0; }
.chat-starter {
  flex: 0 1 auto; max-width: 100%; text-align: start;
  padding: .5rem .8rem; min-height: 2.5rem;
  border: 1px solid var(--border); border-radius: var(--radius-pill);
  background: var(--surface); color: var(--text-link);
  font-family: var(--font-body); font-size: .82rem; line-height: 1.3;
  cursor: pointer;
}
.chat-starter:hover { border-color: var(--border-strong); background: var(--surface-sunken); }
.chat-starter:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* The Tell-the-tutor bubble is a link to the composer, drawn as the bubble
   beside it — see practice/_explain for why a link. */
.chat-starter--tell { display: inline-block; text-decoration: none; }

/* The log takes whatever height the panel has left, rather than fixing it, with
   a floor so an empty chat beside a short question still looks like a place you
   can type into.

   ⚠️ THE FLOOR IS UNCONDITIONAL ON PURPOSE. Making it :has(.msg) — so an empty
   log collapsed and only opened out once a turn landed — is the same mistake as
   dropping the stretch above: it resizes the panel under a pupil at the exact
   moment they have just asked something. The panel is the same size empty as it
   is full, and what fills the empty one is the starters, not a smaller box. */
.chat-log {
  flex: 1 1 auto; min-height: 14rem; overflow-y: auto; margin: .9rem 0;
  display: grid; gap: .55rem; align-content: start;
}
@media (max-width: 62rem) { .chat-log { flex: 0 0 auto; height: 14rem; min-height: 0; } }
.msg { margin: 0; padding: .6rem .75rem; border-radius: var(--look-radius); font-size: .88rem;
       background: var(--surface-sunken); border: var(--look-border) solid var(--border); }
.msg--pupil { background: var(--tint-accent-fill-soft); border-color: var(--tint-accent-line); }
/* The tutor's reply as it was laid out: a closing question stands on a line of
   its own (ai/tutor/pupil-typography.ts), and without this the break between
   them was drawn as a space. pre-line, as .tutor-wrote and for its reason. */
.msg--tutor { white-space: pre-line; }

/* Thinking indicator while the reply is being generated. Reduced-motion turns
   the animation off but keeps the dots, so the state is still legible. */
.dots { display: inline-flex; gap: .25rem; padding-top: .2rem; }
.dots span {
  width: .35rem; height: .35rem; border-radius: 50%;
  background: var(--text-muted); opacity: .35;
  animation: chat-dot 1.2s infinite ease-in-out;
}
.dots span:nth-child(2) { animation-delay: .15s; }
.dots span:nth-child(3) { animation-delay: .3s; }
@keyframes chat-dot {
  0%, 60%, 100% { opacity: .25; }
  30% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .dots span { animation: none; opacity: .5; }
}
/* Which stage the reply is at, beside the dots — see shared/_chat_log. Muted
   because it is status rather than speech: the tutor has not said anything
   yet, and a label in the message colour would read as if it had. */
.pending-stage { margin-left: .4rem; color: var(--text-muted); font-size: .8rem; }
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.who {
  display: block; margin-bottom: .2rem; color: var(--text-muted);
  font-family: var(--font-label); font-size: .6rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em;
}
.chat-form { display: flex; gap: .4rem; }
/* ⚠️ THE GROWTH BELONGS ON THE FLEX CHILD, AND THE COMPOSER IS NOW TWO
   ELEMENTS DEEP. The microphone needs a positioned wrapper around the input,
   and the dictation controller needs to enclose both, so what used to be the
   form's own input is now .chat-composer > .field-with-mic > input. `flex: 1`
   on the input, or on the middle one, leaves .chat-composer shrinking to its
   content and the box stopping short of the panel — which is exactly what it
   did. min-width: 0 because a flex item will not shrink below its content
   otherwise, and a long placeholder would push Send off the edge. */
.chat-form textarea,
.chat-form .chat-composer,
.chat-form .field-with-mic { flex: 1; min-width: 0; }

.chat-form .chat-composer { display: flex; flex-direction: column; }
/* ⚠️ THE RIGHT PADDING IS THE MICROPHONE'S, and is written here as well as on
   .field-with-mic because this rule is the later of two equal selectors: when
   it said `padding: .5rem .6rem` alone it quietly won, and the pupil's words
   ran on under the microphone. */
.chat-form textarea {
  font-size: .88rem; line-height: 1.4; padding: .5rem 3rem .5rem .6rem;
}
.chat-form input[type=submit] { align-self: flex-end; }
.chat-form input[type=submit] {
  background: var(--action-bg); color: var(--action-ink); border: 0; border-radius: var(--look-radius-control);
  padding: .5rem .9rem; font-family: var(--font-body); font-size: .85rem;
  font-weight: 600; cursor: pointer;
}

/* --- Lesson picker -------------------------------------------------------
   The pupil's home. Cards carry state, not scores: what an idea has got to,
   never how well they did. Mastery colour follows the same semantic set as
   feedback — green for secure, accent for work in progress, never red. */

.lessons {
  max-width: 68rem; margin: 0 auto;
  padding: 2.5rem var(--clearspace) 4rem;
}
.lessons-head { max-width: 40rem; }
.lessons-head .lede { color: var(--text-muted); font-size: 1.02rem; }
.lessons-head .back {
  display: inline-block; margin-bottom: .5rem;
  font-family: var(--font-label); font-size: .82rem;
  text-decoration: none;
}
.lessons-head .back:hover { text-decoration: underline; }

/* Subject cards. The whole card is the link, so there is no button competing
   with it — a subject is navigation, not an action. */
/* auto-fit tracks give way below their own minimum.
   `minmax(19rem, 1fr)` holds a 304px track whatever the viewport is, so at 320px
   the grid is wider than the screen and the page scrolls sideways — WCAG 1.4.10
   Reflow, and in practice a pupil on a small phone dragging the page back and
   forth for every line. `min(19rem, 100%)` keeps the same layout everywhere it
   fits and lets the track shrink where it does not. Held by
   test/system/accessibility_test.rb. */

.subject-grid {
  display: grid; gap: 1rem; margin-top: 2rem;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
}
.subject-card {
  display: block; text-decoration: none; color: inherit;
  padding: 1.5rem 1.35rem;
  background: var(--surface); border: var(--look-border) solid var(--border);
  border-top: 3px solid var(--border-strong); border-radius: calc(var(--look-radius) + 2px);
  transition: box-shadow .12s ease, transform .12s ease;
}
.subject-card:hover {
  box-shadow: var(--shadow-card); transform: translateY(-2px);
}
.subject-card:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.subject-card--biology { border-top-color: var(--success); }
.subject-card--chemistry { border-top-color: var(--border-strong); }
.subject-card--physics { border-top-color: var(--warning); }
.subject-eyebrow {
  font-family: var(--font-label); font-size: .82rem; font-weight: 600;
  color: var(--text-muted);
}
.subject-card h2 { font-size: 1.4rem; margin: .1rem 0 .35rem; }
.subject-meta { font-size: .85rem; color: var(--text-muted); margin: 0 0 .75rem; }
/* The gap below it is a gap before the status line, so it goes when that does
   — a subject with nothing open and nothing secure says nothing there. */
.subject-meta:last-child { margin-bottom: 0; }
.subject-status {
  display: flex; align-items: center; gap: .45rem; margin: 0;
  font-family: var(--font-label); font-size: .82rem; color: var(--text-muted);
}
.dot { width: .5rem; height: .5rem; border-radius: 50%; background: var(--border); }
.dot--open { background: var(--accent); }
.dot--secure { background: var(--success); }

/* Open sessions. A list, not a banner: a pupil can leave several lessons on
   the go, and each needs its own Continue and End session. */
.resume {
  margin: 1.75rem 0 0;
  padding: 1rem 1.25rem;
  background: var(--surface); border: var(--look-border) solid var(--border);
  border-left: 4px solid var(--accent); border-radius: var(--look-radius);
}
.resume h2 {
  font-family: var(--font-label); font-size: .82rem; font-weight: 600;
  color: var(--text-muted);
  margin: 0 0 .75rem;
}
.resume-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  padding: .6rem 0; border-top: 1px solid var(--border);
}
.resume-row:first-of-type { border-top: 0; padding-top: 0; }
.resume-row strong { display: block; font-family: var(--font-display); font-weight: var(--weight-display); font-size: 1.15rem; }
.resume-meta { font-family: var(--font-label); font-size: .82rem; color: var(--text-muted); }
.resume-actions { display: flex; align-items: center; gap: .5rem; }
.resume-actions form.button_to { margin: 0; }
/* Sections on the overview that are not live sessions: same shape, no accent
   edge, so "still open" stays the one thing that draws the eye. */
.resume--quiet { border-left-color: var(--border); }

/* Hub: a one-line pointer to the overview, never the list itself. */
.hub-open {
  display: flex; align-items: center; gap: .45rem;
  margin: 1.5rem 0 0;
  font-family: var(--font-label); font-size: .7rem; color: var(--text-muted);
}
.empty { margin-top: 2rem; }

.unit { margin-top: 2.75rem; }
.unit h2 {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  font-size: 1.25rem; margin-bottom: 1rem;
}

/* The unit heading and its glossary link, on one line where there is room and
   stacked where there is not. The link is deliberately quiet: it is a way in
   for somebody who wants it, not a second thing competing with the lessons
   under it, which are what the pupil came for. */
.unit-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: .75rem; flex-wrap: wrap;
}
.unit-head h2 { margin-bottom: .5rem; }
.unit-glossary {
  font-family: var(--font-label); font-size: .82rem;
  color: var(--text-link); text-decoration: none; margin-bottom: .5rem;
}
.unit-glossary:hover { text-decoration: underline; }
.unit-glossary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-radius: 4px; }

/* --- The glossary -------------------------------------------------------

   The words as a page, rather than as marks inside one question — the same
   definitions the "Key words" disclosure shows, read without a question around
   them. See app/views/glossaries.

   NOTHING ON THIS PAGE IS FOCUSABLE EXCEPT THE WAY BACK, and that is a
   decision rather than an omission: a glossary has as many entries as the
   curriculum has words, so a control per term would make the number of tab
   stops a function of how much this pupil's class has been taught, and a
   baseline cannot hold that. */

.glossary-card {
  display: flex; flex-direction: column; gap: .15rem;
  margin-top: 1.5rem; padding: .9rem 1.1rem;
  border: var(--look-border) solid var(--border); border-radius: calc(var(--look-radius) + 6px);
  background: var(--surface); color: var(--text); text-decoration: none;
}
.glossary-card:hover { background: var(--surface-sunken); }
.glossary-card:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.glossary-card-title { font-weight: 600; }
.glossary-card-detail { font-size: .88rem; color: var(--text-muted); }

/* ⚠️ THE PAGE'S OWN INSET, because <main> has none. Every pupil screen supplies
   its own — .lessons, .practice-grid — and a screen that forgot ran flush to
   the left edge of the viewport with the headings touching it. Caught by
   looking at the screenshot, which is the only thing that would have. */
.glossary {
  max-width: 52rem; margin: 0 auto;
  padding: 2.5rem var(--clearspace) 4rem;
}
.glossary-head { margin-bottom: 1.5rem; }
.glossary-head .back {
  display: inline-block; margin-bottom: .75rem;
  font-family: var(--font-label); font-size: .82rem; color: var(--text-link); text-decoration: none;
}
.glossary-head .back:hover { text-decoration: underline; }
.glossary-head .lede { color: var(--text-muted); }

/* A term and its meaning on one row where the screen is wide enough to read
   an index that way, and stacked on a phone — where a two-column list of long
   definitions is a column of single words. */
.glossary-list { display: grid; gap: .1rem 1.25rem; }
.glossary-list dt {
  font-weight: 600; padding-top: .7rem;
  border-top: 1px solid var(--border);
}
.glossary-list dd {
  margin: 0; padding-bottom: .7rem; color: var(--text-muted); line-height: 1.55;
}
@media (min-width: 48rem) {
  .glossary-list { grid-template-columns: minmax(8rem, 14rem) 1fr; }
  .glossary-list dd { padding-top: .7rem; border-top: 1px solid var(--border); }
}
.subject {
  font-family: var(--font-label); font-size: .62rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .07em;
  padding: .18rem .55rem; border-radius: var(--radius-pill);
  color: var(--text-link); background: var(--tint-brand-fill);
}
.subject--biology { color: var(--success-text); background: var(--success-surface); }
.subject--physics { color: var(--warning-text); background: var(--warning-surface); }

/* auto-fit, not auto-fill: a unit with two concepts would otherwise leave an
   empty third track and the cards would sit bunched to the left. */
.lesson-grid {
  display: grid; gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(19rem, 100%), 1fr));
}
/* The whole card is the link — see subjects/_lesson, and .subject-card above,
   which makes the same bargain for the same reason. */
.lesson {
  display: flex; flex-direction: column; justify-content: space-between;
  text-decoration: none; color: inherit;
  background: var(--surface); border: var(--look-border) solid var(--border);
  border-top: 3px solid var(--border); border-radius: calc(var(--look-radius) + 2px);
  padding: 1.15rem 1.25rem 1.25rem;
  transition: box-shadow .12s ease, transform .12s ease;
}
.lesson:hover { box-shadow: var(--shadow-card); transform: translateY(-2px); }
.lesson:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.lesson--secure { border-top-color: var(--success); }
.lesson--working { border-top-color: var(--accent); }
.lesson--fragile { border-top-color: var(--warning); }
.lesson h3 { font-size: 1.05rem; margin: .15rem 0 .4rem; }
.lesson-state {
  font-family: var(--font-label); font-size: .78rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted);
  margin: 0;
}
.lesson--secure .lesson-state { color: var(--success-text); }
.lesson-summary { font-size: .88rem; color: var(--text-muted); margin: 0 0 .5rem; }
.lesson-misconception {
  font-size: .82rem; margin: .5rem 0 0;
  padding: .5rem .65rem; border-radius: var(--radius-sm);
  color: var(--text-link); background: var(--tint-accent-fill);
}
.lesson-foot { margin-top: 1.1rem; }
.bar {
  height: 5px; border-radius: var(--radius-pill); background: var(--border); overflow: hidden;
}
.bar span { display: block; height: 100%; background: var(--accent); }
.lesson--secure .bar span { background: var(--success); }
.lesson-count {
  font-family: var(--font-label); font-size: .82rem; color: var(--text-muted);
  margin: .45rem 0 .75rem;
}

/* A lesson with a session open. Accent, matching the "still open" list above,
   so the two read as the same fact stated twice rather than two states. */
.lesson--open {
  border-color: var(--tint-accent-edge); border-top-color: var(--accent);
  box-shadow: var(--shadow-open);
}
.lesson--open .lesson-state::after {
  content: "· session open"; margin-left: .35rem; color: var(--success-text);
}


.lessons-foot { margin-top: 3rem; }
.lessons-foot .muted {
  font-family: var(--font-label); font-size: .62rem;
  text-transform: uppercase; letter-spacing: .08em; margin-bottom: .6rem;
}


/* --- Lesson home ---------------------------------------------------------
   The four phases, in the order the journey runs. Two of them are greyed and
   the greys mean different things: locked is waiting for the pupil, unbuilt is
   waiting for us. They are drawn the same on purpose — both are unavailable,
   and inventing a second grey would suggest a difference the pupil can act on.
   An unbuilt one says so in a sentence; a locked one has said nothing since
   14 October 2026, when "Opens once you've…" went as an explanation of the
   journey the numbers already give. */

.lesson-home {
  max-width: 68rem; margin: 0 auto;
  padding: 2.5rem var(--clearspace) 4rem;
}
/* THE WHOLE JOURNEY, IN THE HEADER, above the fold on a phone.

   The four cards below say all of this and more, but they arrive one at a
   time: masthead, nav, title, unit, summary, and then one card with the second
   clipped. The four-phase shape is the thing this product most wants a pupil
   to be holding in their head, and it cannot be the thing they have to scroll
   to find. It is the same partial the subject cards and the calendar carry —
   see shared/_phase_bar — drawn heavier here, because on this screen it is the
   subject of the header rather than a footnote to a link. */
.stage-map { margin-top: 1.15rem; max-width: 32rem; }
.stage-map .phase-bar { margin-top: 0; gap: 4px; }
.stage-map .phase { height: 10px; border-radius: 5px; }

/* TWO COLUMNS, NEVER FOUR — and the four is what this used to be asking for.
   `minmax(min(19rem, 100%), 1fr)` reads as "as many as fit", and four of them
   need about 79rem in a container that renders about 64rem wide. So auto-fit
   laid three and dropped the fourth onto its own row, which put the card
   labelled "Last" underneath the card labelled "First": the one thing an
   ordered list of phases must not do.

   23rem is the width at which a second column stops fitting, so the same
   declaration gives two columns on a laptop and one on a phone, and reading
   order and phase order agree at every width. min() keeps the track from
   overflowing 320px — see .subject-grid, which explains that at length. */
.stages {
  list-style: none; margin: 2rem 0 0; padding: 0;
  display: grid; gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(23rem, 100%), 1fr));
}
/* EACH CARD IS HEADED IN ITS PHASE'S COLOUR, AND NUMBERED (14 October 2026).

   The band is the banner the pupil will see across the top of the page once
   they are inside that phase (.phase-banner), so the card is a preview of
   where the button takes them, and the four colours they meet inside the
   phases are introduced here, in order. The number replaced "First / Then /
   Next / Later": words that said the order in a way a pupil had to read,
   where a numeral is read at a glance — and it counts what is drawn, so the
   lesson in the room, which has no Review, counts to three.

   Every pairing is one theme-contrast.test.ts already holds in all four
   palettes: the band's words are --phase-*-ink on --phase-*-bg, the numeral's
   disc is the same pair inverted, a locked card's outline is --phase-*-line
   on the sunken ground, and the Done pill is --surface on --correct. Nothing
   new is measured because nothing new is paired.

   --stage-bg, -ink and -line are this card's phase, set once per key so the
   rules below are written once. The card's buttons take the phase too, by the
   same re-pointing main[data-phase] does for a whole page — see there. */
.stage--baseline {
  --stage-bg: var(--phase-baseline-bg);
  --stage-ink: var(--phase-baseline-ink);
  --stage-line: var(--phase-baseline-line);
  --action-bg: var(--phase-baseline-bg);
  --action-bg-hover: var(--phase-baseline-bg-hover);
  --action-ink: var(--phase-baseline-ink);
}
.stage--recap {
  --stage-bg: var(--phase-recap-bg);
  --stage-ink: var(--phase-recap-ink);
  --stage-line: var(--phase-recap-line);
  --action-bg: var(--phase-recap-bg);
  --action-bg-hover: var(--phase-recap-bg-hover);
  --action-ink: var(--phase-recap-ink);
}
.stage--practice {
  --stage-bg: var(--phase-practice-bg);
  --stage-ink: var(--phase-practice-ink);
  --stage-line: var(--phase-practice-line);
  --action-bg: var(--phase-practice-bg);
  --action-bg-hover: var(--phase-practice-bg-hover);
  --action-ink: var(--phase-practice-ink);
}
.stage--review {
  --stage-bg: var(--phase-review-bg);
  --stage-ink: var(--phase-review-ink);
  --stage-line: var(--phase-review-line);
  --action-bg: var(--phase-review-bg);
  --action-bg-hover: var(--phase-review-bg-hover);
  --action-ink: var(--phase-review-ink);
}
/* The body takes the slack, so the buttons sit at the foot of a card that a
   taller neighbour in its row has stretched, and the words stay under the
   band rather than floating to the middle (space-between did that, once the
   band made three children of two). */
.stage {
  display: flex; flex-direction: column;
  overflow: hidden;
  background: var(--surface); border: var(--look-border) solid var(--border);
  border-radius: calc(var(--look-radius) + 2px);
}
/* The phase the pupil is part-way through: the card outlined in its colour,
   the one place on the page to look. */
.stage--now { border-color: var(--stage-line); box-shadow: 0 0 0 1px var(--stage-line); }
.stage--locked, .stage--unbuilt { background: var(--surface-sunken); }

.stage-band {
  display: flex; align-items: center; gap: .75rem;
  padding: .7rem 1.25rem;
  background: var(--stage-bg); color: var(--stage-ink);
}
.stage-band h2 { margin: 0; font-size: 1.15rem; color: inherit; }
.stage-number {
  flex: none; display: grid; place-items: center;
  width: 2.1rem; height: 2.1rem; border-radius: 50%;
  background: var(--stage-ink); color: var(--stage-bg);
  border: 2px solid var(--stage-ink);
  font-family: var(--font-label); font-weight: 800; font-size: 1.05rem;
  font-variant-numeric: tabular-nums; line-height: 1;
}
/* Not open yet, or not built: the phase's colour as an outline only. Hollow
   is "not now"; the two kinds stay drawn alike, as they always have been —
   see the top of this section. */
.stage--locked .stage-band, .stage--unbuilt .stage-band {
  background: none; color: var(--text-muted);
  border-bottom: 2px dashed var(--stage-line);
}
.stage--locked .stage-number, .stage--unbuilt .stage-number {
  background: none; color: var(--stage-line); border: 2px dashed var(--stage-line);
}
/* ⚠️ A WORD AS WELL AS THE TICK. The tick alone was the first draft and is a
   picture; "Done" is what a screen reader says and what a pupil who cannot
   tell the green from the band behind it reads.

   THE RING IS FOR THE LOOKS. Every look redefines --correct, and Rocket ship's
   is the blue of its own Recap band, so on that card the pill had no edge at
   all. A ring in the band's ink separates it from any band in any palette or
   look, because ink on band is a pairing every one of them already holds. */
.stage-done {
  margin-inline-start: auto; flex: none;
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .2rem .6rem .2rem .45rem; border-radius: var(--radius-pill);
  background: var(--correct); color: var(--surface);
  box-shadow: 0 0 0 1.5px var(--stage-ink);
  font-family: var(--font-label); font-size: .78rem; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase;
}
.stage-done svg { width: .95rem; height: .95rem; }

.stage-body { flex: 1; padding: 1rem 1.25rem 0; }
.stage-actions { padding: 1rem 1.25rem 1.25rem; }
.stage-actions:empty { padding-top: 0; }
.stage-blurb { margin: 0; color: var(--text-muted); font-size: .95rem; }
.stage-state {
  margin: .7rem 0 0; font-size: .9rem; color: var(--text-muted);
  border-left: 2px solid var(--border); padding-left: .6rem;
}
.stage-count {
  margin: .4rem 0 0; font-family: var(--font-label);
  font-size: .82rem; color: var(--text-muted);
}
/* ONE CONTINUOUS BAR, AND THE ONLY COUNT ON THE CARD. "Working on 2 of 3"
   above it and "1 of 3 done" below said the same thing three times, and went.
   A bar with a segment per item was tried on the design board and turned to
   noise past half a dozen; a plan can hold twenty.

   The track is hollow — the sunken ground ringed in --border, as the header's
   phase bar draws an empty phase — and not a filled grey: in high contrast
   --border is black, and a black track read as the full part of the bar. */
.stage-progress {
  height: 10px; margin-top: 1rem; border-radius: var(--radius-pill); overflow: hidden;
  background: var(--surface-sunken); box-shadow: inset 0 0 0 1px var(--border);
}
.stage-progress span {
  display: block; height: 100%; border-radius: var(--radius-pill);
  background: var(--stage-bg); box-shadow: inset 0 0 0 1.5px var(--stage-line);
}

/* At phone width the header is the whole of what stands between a pupil and
   the first phase, so it gives up what it can: the page's top padding, a step
   off the title, and the air around the lede. The bar keeps its room — it is
   the thing being made room for. */
@media (max-width: 40rem) {
  .lesson-home { padding-top: 1.5rem; }
  .lesson-home .lessons-head h1 { font-size: 1.5rem; }
  .lesson-home .lessons-head .lede { font-size: .95rem; margin: 0 0 .35rem; }
  .lesson-home .stage-map { margin-top: .85rem; }
  .lesson-home .stages { margin-top: 1.25rem; }
}

/* The controls on a phase card are a set: one primary, one destructive, one
   quiet. They only read as a set if they share their box metrics, so .quiet —
   normally a small bordered label — takes the shape of the two beside it here.

   form.button_to carries its own right margin for the places it sits in a run
   of them; inside a flex row with a gap that is the gap twice. */
.stage-actions { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
.stage-actions form.button_to { margin: 0; }
.stage-actions a.quiet, .stage-actions button.quiet {
  display: inline-block;
  padding: .6rem 1.15rem; border-radius: var(--look-radius-control);
  border: var(--look-border-control) solid var(--border); background: none; color: var(--text-muted);
  font-family: var(--font-body); font-size: .92rem; font-weight: 600;
  line-height: 1.2; letter-spacing: 0; text-transform: none; text-decoration: none;
}
.stage-actions a.quiet:hover, .stage-actions button.quiet:hover {
  border-color: var(--border-strong); color: var(--text-link);
}

/* The phase bar. One segment per phase of the lesson, in the order the journey
   runs — see shared/_phase_bar for why it replaced a bar counting questions.

   Colour follows the same semantic set as everything else a pupil reads:
   success for finished, accent for in progress, and two greys for the two
   kinds of unavailable. The greys differ from each other only slightly, and
   deliberately: locked and not-built are both "not now", and inventing a
   strong distinction between them would suggest one is a problem.

   ⚠️ THE COLUMN COUNT FOLLOWS LessonStages::KEYS AND IS NOT WRITTEN HERE. It
   was `repeat(4, 1fr)` and went stale the moment a fifth phase landed: the bar
   drew five segments into four columns and the last one wrapped onto a second
   row, on every card on the subject hub. `auto-fit` counts them instead, so
   the next phase costs nothing here. */
.phase-bar {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 3px;
  margin-top: .9rem;
}
.phase {
  height: 6px; border-radius: 3px;
  background: var(--surface-sunken); border: 1px solid var(--border);
}
.phase--done { background: var(--success); border-color: var(--success); }
.phase--doing { background: var(--accent); border-color: var(--accent); }
.phase--open { background: var(--surface-sunken); border-color: var(--border-strong); }
.phase--locked, .phase--unbuilt { background: var(--surface-sunken); border-color: var(--border); }

/* --- The placement -------------------------------------------------------
   The last screen of a baseline: one route marked, one authored sentence, and
   a line saying the rest of the lesson is open anyway.

   Deliberately narrow and quiet. Every impulse on a screen that follows a set
   of marked questions is to make it look like a result — a big number, a band,
   a colour that means how it went — and there is nothing here to be a result
   OF. The route is the content, so the route gets the type. */
/* ⚠️ THE PAGE GUTTER IS THE SCREEN'S OWN, NOT the layout's. `main` carries no
   padding at all — every screen brings its own container, and .lessons,
   .lesson-home and .settings-page all use this same three-value shape. Without
   it the heading, the card and the closing line sit flush against the left edge
   of the viewport, which is what this screen shipped as until somebody looked
   at it. */
/* The container is the same width as every other screen's, and the narrow
   column is inside it — the inner width is .lessons-head's trick, one step
   narrower.

   ⚠️ AND THE COLUMN IS CENTRED IN IT, WHICH IT WAS NOT UNTIL 11 SEPTEMBER
   2026. The argument for leaving it against the left edge of the container was
   that the screen would then start where the one before it started, and a
   column that jumped to the middle on the last screen of five would read as a
   page from another product. That held while the screen was a heading, one
   card and a line: a short block low on the left of a wide page reads as a
   column that has been left there.

   It stopped holding when the list of questions arrived. The page is now tall
   enough to be the whole window, and a tall narrow column pinned left with
   half the viewport empty beside it reads as a layout that has failed rather
   than as one that matches the screen before. Centring is the change; the
   container width is untouched, so nothing else about the page moved.

   `margin-inline` and not the `margin` shorthand, because .placement-route and
   .placement-answers set their own block margins below and a shorthand here
   would silently win. */
.placement {
  max-width: 68rem; margin: 0 auto;
  padding: 2.5rem var(--clearspace) 4rem;
}
.placement .lessons-head, .placement-answers, .placement-route, .placement-others {
  max-width: 34rem; margin-inline: auto;
}
/* The eyebrow is a span and `.lessons-head .back` is inline-block, so a header
   carrying BOTH puts them on one line. Every other screen has one or the
   other; this one names where the pupil came from and which lesson it was. */
.placement .cal-eyebrow { display: block; }
.placement-route {
  margin-top: 1.4rem; padding: 1.2rem;
  background: var(--surface-raised); border: var(--look-border) solid var(--border);
  border-top: 3px solid var(--action-bg); border-radius: calc(var(--look-radius) + 2px);
}
.placement-mark {
  margin: 0; font-size: .78rem; letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-muted);
}
.placement-route h2 { margin: .2rem 0 .6rem; }
.placement-reason { margin: 0 0 1rem; color: var(--text); }
.placement-others { margin-top: 1.2rem; color: var(--text-muted); }

/* The list of what was asked, above the advice.

   ⚠️ THE ROWS ARE NOT A BAR CHART AND MUST NOT BECOME ONE. Every row is the
   same height and the same width whichever way the question went; the only
   thing that varies is the edge and the word. A list whose wrong rows were
   taller, redder or indented would be a score drawn in furniture, which is the
   same number this screen refuses to print.

   The mark sits at the end of the row rather than in front of the position, so
   that the column of positions reads straight down and the eye has to go
   looking for the verdicts rather than meeting them first. */
.placement-answers { margin-bottom: 1.6rem; }
.placement-answers h2 { margin: 0 0 .2rem; }
.answer-list { list-style: none; margin: .9rem 0 0; padding: 0; display: grid; gap: .5rem; }
.answer-row { margin: 0; }
.answer-link {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: .8rem 1rem; border-radius: var(--look-radius); text-decoration: none;
  border: var(--look-border) solid var(--border); border-inline-start-width: 4px;
  background: var(--surface); color: var(--text);
}
.answer-link:hover { background: var(--surface-sunken); }
.answer-row--correct .answer-link { border-inline-start-color: var(--success); }
.answer-row--incorrect .answer-link { border-inline-start-color: var(--warning); }
.answer-row--unanswered .answer-link { border-inline-start-color: var(--border-strong); }
.answer-step { font-family: var(--font-label); }
.answer-mark { color: var(--text-muted); font-size: .93rem; }


/* --- Recap ---------------------------------------------------------------
   The same two-column shape as Practice, and deliberately so: the chat sits in
   the same place on both screens, because it is the same panel doing the same
   job and a pupil should not have to find it twice. */

/* The recap's grid is the practice grid's — see the rule it shares up there.
   What is left here is only what a recap does differently, which is nothing
   about the columns. */
/* Back beside Next, which is the pair every reader expects.
   ⚠️ AN ANCHOR, NOT A BUTTON, AND `.action` DOES NOT REACH ONE. Every selector
   in the .action rule names `button`, `.action button` or `input[type=submit]`
   — so `link_to class: "action"` matched none of them and Back came out as an
   underlined link sitting in a focus outline, which is what the first
   screenshot of this showed. `.action-link` is the anchor that already existed
   for exactly this; these two ride on it.
   Back is quiet because forward is the ordinary direction through a recap and
   the two should not compete for the eye — see recaps/show for why there are
   two at all. */
.recap-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; margin-top: 1rem; }
.recap-actions form.button_to { margin: 0; }
/* Both classes in the selector on purpose: `.action-link` is declared further
   down this file, so a single-class rule here loses on source order and Back
   came out the same blue as Next. Specificity rather than moving the rule,
   because the modifier belongs beside the thing it modifies. */
.action-link.action-link--quiet {
  background: var(--surface); color: var(--text);
  border: 1.5px solid var(--border-strong);
}
.action-link.action-link--quiet:hover { background: var(--surface-sunken); color: var(--text); }
.recap-step h1 { margin: .3rem 0 .8rem; font-size: var(--size-question); line-height: 1.25; }
/* Continue moves focus to the next slide's heading, as a new page would have
   put a reader at the top of it (lib/recap-in-place.ts). It is a place to
   land, never in the tab order, so no ring — the same reasoning as
   main:focus above; after a keyboard press a black box round the title read
   as a fault. */
.recap-step h1[tabindex="-1"]:focus { outline: none; }
.recap-body { color: var(--text); line-height: 1.6; }
.recap-body p { margin: 0 0 .8rem; }
/* A Key words slide, drawn as a table (`Definitions` in routes/recap.tsx).
   The head is the recap's mode colour, the same purple as the banner and the
   current step's edge, so the table reads as part of this phase rather than
   as a teacher's grid dropped into it. The rows are banded and the word is
   in the ordinary ink: the words are what the slide is for, and colouring
   them as well would be the highlight this table replaced. Nothing is
   coloured that the words do not also say.

   EVERYTHING ELSE IS THE LOOK'S. The heads and the words are set in the
   display face, as the slide's title is, so a look that letters its headings
   letters these too — and a pupil who chose the plain or hyperlegible face
   gets that, because --font-display follows the face axis. The corners and
   the edge's weight are the look's tokens; each look then draws the table as
   it draws a .glossary-card, which holds the same thing (looks/none.css). */
.definitions {
  width: 100%; margin: 0 0 .8rem;
  border-collapse: separate; border-spacing: 0;
  border: max(1.5px, var(--look-border)) solid var(--phase-recap-line);
  border-radius: var(--look-radius);
  overflow: hidden;
}
.definitions th, .definitions td {
  padding: .6rem .9rem; text-align: start; vertical-align: top;
}
.definitions thead th {
  background: var(--phase-recap-bg); color: var(--phase-recap-ink);
  font-family: var(--font-display); font-size: var(--size-small);
  font-weight: var(--weight-display); letter-spacing: .02em;
}
.definitions tbody th {
  width: 30%; color: var(--text);
  font-family: var(--font-display); font-weight: var(--weight-display);
  border-inline-end: 1px solid var(--border);
  /* break-word, NOT anywhere: `anywhere` lets the column shrink below a whole
     word, and on a phone at the largest size "alkali" came out as "alkal" over
     "i". break-word keeps the word whole and only breaks one too long to fit. */
  overflow-wrap: break-word;
}
.definitions tbody tr > * { background: var(--surface); }
.definitions tbody tr:nth-child(even) > * { background: var(--surface-sunken); }
.definitions tbody tr + tr > * { border-top: 1px solid var(--border); }
.recap-image {
  display: block; max-width: 100%; height: auto;
  margin: 1rem 0; border: 1px solid var(--border); border-radius: var(--radius-md);
}
/* A frame of the lesson recording under a slide's point: the column's full
   width, as the player it came from would be. */
.recap-picture { width: 100%; }
.recap-media { margin: 1rem 0 0; }
.recap-media-note {
  margin-inline-start: .4rem; font-family: var(--font-label);
  font-size: .82rem; color: var(--text-muted);
}
.recap-leave { margin-top: 1.5rem; }

/* Media on a recap step. A player where the file is ours to serve, and a frame
   the same size and shape where it is not — see app/views/recaps/_media for why
   the second case exists at all. Both sit in a 16:9 box so a step does not jump
   about as the pupil moves through it. */
.media-frame {
  display: block; margin: 1.25rem 0;
  aspect-ratio: 16 / 9; width: 100%;
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--surface-sunken); overflow: hidden;
}
.media-player { width: 100%; height: 100%; display: block; background: var(--surface-sunken); }

/* A video shut behind a summary (routes/recap.tsx, `Media`, says why). The
   word bank's pill and its drawn arrow, so the two small disclosures on a
   slide read as one kind of control; the frame keeps its own margin once
   open. */
.media-optional { margin: 1.25rem 0 0; }
.media-optional > summary {
  display: inline-flex; align-items: center; gap: .45rem;
  width: fit-content;
  padding: .4rem .8rem;
  font: inherit; font-size: .9rem; font-weight: 500;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  cursor: pointer;
  list-style: none;
}
.media-optional > summary::-webkit-details-marker { display: none; }
.media-optional > summary:hover { background: var(--surface-sunken); }
.media-optional > summary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.media-optional > summary::after {
  content: ""; width: 0; height: 0;
  border-left: .3rem solid transparent;
  border-right: .3rem solid transparent;
  border-top: .35rem solid currentColor;
}
.media-optional[open] > summary::after { transform: rotate(180deg); }
.media-optional > .media-frame { margin-top: .75rem; }

/* The stand-in. A link drawn as a player, so a step whose video lives
   elsewhere still reads as a step with a video on it.

   It was a dashed outline on the page ground, and a dashed rectangle is the
   universal sign for "nothing here yet" — drawn around the richest thing in
   the recap. Solid, filled and lifted off the page now, with the glyph in a
   disc, so whatever it turns out to be it is obviously something to press. */
.media-frame--link {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .5rem; text-decoration: none; color: var(--text);
  background: var(--surface); border-color: var(--border-strong);
  box-shadow: var(--shadow-card);
  transition: background .12s ease, transform .12s ease;
}
.media-frame--link:hover { background: var(--surface-sunken); transform: translateY(-2px); }
.media-frame--link:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* The disc is tinted rather than filled. The glyph is an emoji on most
   platforms and paints in its own colours whatever this rule says, so a solid
   Aegean fill behind it would be a clash rather than a play button. */
.media-glyph {
  display: inline-flex; align-items: center; justify-content: center;
  width: 3.25rem; height: 3.25rem; border-radius: 50%;
  background: var(--tint-brand-fill); color: var(--text-link);
  font-size: 1.6rem; line-height: 1;
}
.media-label { font-weight: 600; font-size: 1.02rem; }
.media-note {
  font-family: var(--font-label); font-size: .82rem; color: var(--text-muted);
}

/* A list of ideas, never a mark — see the note in each template. Shared by the
   recap summary and by the two practice screens built on the same shape: the
   plan a sitting is about to work, and what it did. Each names itself, so a
   test can tell them apart. */
/* Every phase's summary is the same shape: one narrow column, centred, with
   room above and below. Named per phase so a screen can be found by its class,
   and sharing one rule so they cannot drift apart — which the review's did, by
   being called `.summary` and matching nothing at all. */
.recap-summary,
.review-summary,
.practice-plan,
.practice-summary {
  max-width: 44rem; margin: 0 auto;
  padding: 2.5rem var(--clearspace) 4rem;
}
.summary-score {
  font-family: var(--font-label); font-size: .8rem;
  text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted);
}
.summary-section { margin-top: 2rem; }
.idea-list { list-style: none; margin: 1rem 0 0; padding: 0; display: grid; gap: .75rem; }
.idea {
  border: var(--look-border) solid var(--border); border-left: 3px solid var(--border);
  border-radius: var(--look-radius); padding: .9rem 1.1rem; background: var(--surface);
}
/* Foundational is the one that blocks everything built on it, so it is the one
   that reads as urgent. Peripheral is warm rather than alarming — it has a
   ladder waiting in Practice and nothing is wrong with the pupil. */
.idea--foundational { border-left-color: var(--error); }
.idea--peripheral { border-left-color: var(--warning); }
/* Reinforce is something the pupil already had right, so it gets the neutral
   edge rather than a colour that implies a problem. Done is the only green on
   these lists — an idea that cleared is the one thing here that is finished. */
.idea--reinforce { border-left-color: var(--border-strong); }
.idea--done { border-left-color: var(--success); }
/* Flagged is amber and never red. It means a person is going to help, which is
   a relief rather than a fault — the wording in the template says so and the
   colour must not contradict it. */
.idea--flagged { border-left-color: var(--warning); }
.idea-statement { margin: 0; font-weight: 600; }
.idea-next { margin: .35rem 0 0; color: var(--text-muted); font-size: .93rem; }
.summary-actions {
  margin-top: 2rem; display: flex; gap: 1rem; align-items: center; flex-wrap: wrap;
}
/* A note under a section heading, before the list it introduces. Sized between
   the heading and the list so it reads as part of the heading rather than as
   the first item. */
.section-note { margin: .4rem 0 0; color: var(--text-muted); font-size: .93rem; }

/* THE PLAN, DRAWN AS A ROUTE. Not shared with the two summary lists above: a
   summary is a set of outcomes and has no order worth showing, while a plan is
   a sequence a pupil is about to walk. See practice/plan for the argument.

   The mark first — how far. It is the whole decision on this screen, so it is
   read before the route rather than found inside it. A second mark stood
   beside it giving a duration, and came off deliberately: see practice/plan.
   The row is still a flex row because one chip and two lay out the same, and
   the next fact about a plan should not have to rebuild it. */
.plan-marks {
  display: flex; flex-wrap: wrap; gap: .5rem; margin: 1.25rem 0 0;
}
.plan-mark {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .35rem .75rem; border-radius: var(--radius-pill);
  border: 1px solid var(--border); background: var(--surface-sunken);
  font-family: var(--font-label); font-size: .78rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted);
}

.plan-route { list-style: none; margin: 1.5rem 0 0; padding: 0; }
.stop {
  position: relative; display: grid;
  grid-template-columns: 2.25rem 1fr; gap: 1rem;
  padding-bottom: 1.6rem;
}
/* The line from one stop to the next, behind the nodes and stopping at the
   last one. A route that runs off the bottom of the list promises more of it
   than the plan has. */
.stop::before {
  content: ""; position: absolute; left: 1.125rem; top: 2.4rem; bottom: -.15rem;
  width: 2px; margin-left: -1px; background: var(--border);
}
.stop:last-child { padding-bottom: 0; }
.stop:last-child::before { content: none; }

.stop-node {
  width: 2.25rem; height: 2.25rem; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  border: 2px solid var(--border-strong); background: var(--surface);
  font-family: var(--font-label); font-size: .85rem; font-weight: 600;
  color: var(--text);
}
.stop-tick { width: 1.15rem; height: 1.15rem; }

/* The node is the only thing on the stop that is coloured, and the colour is
   never the message — .stop-kind says the same thing in a word, because a
   pupil cannot be expected to know that a red ring means "foundational".

   Foundational is the one that blocks everything built on it, so it is the one
   that reads as urgent. Peripheral is warm rather than alarming. Reinforce is
   something they already had right and gets the neutral ring, because a colour
   implying a problem would contradict the label beside it. */
.stop--foundational .stop-node { border-color: var(--error); }
.stop--peripheral .stop-node { border-color: var(--warning); }
/* Quieter than the ring around a named idea, not louder. Drawn at full
   strength these outnumbered the one stop that mattered and pulled the eye
   straight past it. */
.stop--reinforce .stop-node { border-color: var(--border); color: var(--text-muted); }
.stop--win .stop-node { border-color: var(--success); }
/* An answer nothing could place is the same warm amber as peripheral, which is
   what it is. A probe is quiet for the opposite reason: nothing has gone wrong
   on this lesson yet and nothing has been asked, so the loudest ring on the
   route would be the one stop that has seen no evidence at all. */
.stop--unmatched .stop-node { border-color: var(--warning); }
.stop--probe .stop-node { border-color: var(--border); color: var(--text-muted); }
/* Done is the only stop that fills, and the only green: it is the one thing on
   this screen that is finished. */
.stop--done .stop-node {
  border-color: var(--success); background: var(--success-surface); color: var(--success-text);
}

.stop-body { padding-top: .3rem; min-width: 0; }
.stop-kind {
  margin: 0; font-family: var(--font-label); font-size: .78rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted);
}
.stop-idea { margin: .3rem 0 0; font-weight: 600; }
.stop-why { margin: .25rem 0 0; color: var(--text-muted); font-size: .93rem; }
/* A stop with no idea of its own is one line under its label, so it sits up
   next to it rather than floating in a card's worth of space. */
.stop-body > .stop-kind + .stop-why { margin-top: .15rem; }

/* Something true about the plan that the plan does not otherwise show: an
   unfinished recap behind it, or a sitting that overran its estimate. Warm
   rather than alarming — both are statements of fact, and neither is a fault. */
.plan-caveat {
  margin-top: 1rem; padding: .8rem 1rem; border-radius: var(--radius-md);
  background: var(--warning-surface); border-left: 3px solid var(--warning);
  font-size: .93rem;
}

/* What is still open on the plan, on the summary: "one thing still open" is
   an invitation where a greyed-out card is a wall. */
.gate {
  margin: 0; padding: .9rem 1.1rem; border-radius: var(--look-radius);
  border-left: 3px solid var(--border-strong); background: var(--surface-sunken);
}

/* The second action in a pair, where the first is the one being offered.
   Deferring should cost nothing and should not look like failing at
   something, so it reads as a link rather than as a button competing with
   Start. button_to renders a form, hence the pair. */
.quiet-form { margin: 0; }
.quiet-button {
  background: none; border: 0; padding: 0; cursor: pointer; color: var(--text-link);
  font-family: var(--font-label); font-size: .82rem;
}
.quiet-button:hover { text-decoration: underline; }

/* --- Settings ------------------------------------------------------------
   The palette picker. Swatches are painted by the real themes rather than by
   copies of their colours: each swatch carries its own data-theme, so a change
   to a palette shows here without anyone remembering to update a preview. */

/* Settings is the same screen in all three apps, so it sets its own width
   rather than inheriting whichever container the surrounding app uses. The
   pupil app is 68rem wide, the teacher's 76rem and the parent's 48rem, and an
   auto-fit grid inside those reflows to a different column count in each —
   which is how one shared partial ended up looking like three designs. */
/* Wider than it was, because it now holds two columns. The content column is
   still the 46rem it always was — the sidebar and the gap are what the extra
   16rem buys, so the pickers inside are the same width in all three apps as
   they were before the sidebar existed. */
.settings-page {
  max-width: 62rem; margin: 0 auto;
  padding: 2.5rem var(--clearspace) 4rem;
}

/* --- The settings sidebar ------------------------------------------------

   A fixed column rather than a fraction, so the content column is the same
   width on all three settings pages and does not jump as you move between
   them. align-items: start keeps the nav at the top of a long page instead of
   centring it against four screens' worth of pickers.

   Not sticky. It would be pleasant on the reading page, which is long — but the
   masthead is already sticky, and a second sticky element is how a control ends
   up focusable while sitting underneath something else. That is WCAG 2.4.11 and
   there is a test for it; this is not worth spending it on. */
.settings-layout {
  display: grid; gap: 2.5rem; align-items: start; margin-top: 2rem;
  grid-template-columns: 13rem minmax(0, 1fr);
}

.settings-nav { display: flex; flex-direction: column; gap: .15rem; }
.settings-nav-link {
  display: block; padding: .5rem .7rem; border-radius: var(--radius-md);
  border-inline-start: 2px solid transparent;
  text-decoration: none; color: var(--text-muted); font-size: .95rem;
}
.settings-nav-link:hover { background: var(--surface); color: var(--text); }
.settings-nav-link:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* The page you are on, said twice: the ring for anyone looking at it, and
   aria-current="page" in the markup for anyone not. */
.settings-nav-link--current {
  background: var(--selected-surface); color: var(--text); font-weight: 600;
  border-inline-start-color: var(--selected);
}

/* ⚠️ AFTER the rules it overrides, not before them. A media query carries no
   extra specificity, so the same selector declared later wins whether or not a
   query is wrapped round it — this block sat above .settings-nav and did
   nothing at all, and the only way to see that was to look at a narrow screen.

   Below this width the sidebar cannot be a column without squeezing the pickers
   into one card per row, so it becomes a row of links above the content: the
   same navigation, folded rather than dropped. The current marker moves to the
   underside for the same reason a tab strip puts it there — an inline start
   border on a row reads as a divider between the links rather than as a mark on
   one of them. */
@media (max-width: 54rem) {
  .settings-layout { grid-template-columns: minmax(0, 1fr); gap: 1.25rem; }
  .settings-nav { flex-direction: row; flex-wrap: wrap; gap: .3rem; }
  .settings-nav-link { border-inline-start: 0; border-bottom: 2px solid transparent; }
  .settings-nav-link--current { border-bottom-color: var(--selected); }
}
.settings-head { margin-bottom: .5rem; }
.settings-head .lede { color: var(--text-muted); font-size: 1.02rem; }
.settings-head .back {
  display: inline-block; margin-bottom: .5rem;
  font-family: var(--font-label); font-size: .68rem;
  text-transform: uppercase; letter-spacing: .07em; text-decoration: none;
}
.settings-head .back:hover { text-decoration: underline; }

.settings-page .setting { margin-top: 2.5rem; }
/* The first one sits at the top of its column, level with the sidebar. */
.settings-content > .setting:first-child,
.settings-content > form:first-child .setting { margin-top: 0; }
.setting h2 { font-size: 1.15rem; margin-bottom: .25rem; }
/* No max-width of its own any more: the measure above is the one place a line
   length is decided, and 42rem here used to quietly overrule it. */
.setting-help { color: var(--text-muted); font-size: .9rem; margin: 0 0 1.25rem; }

/* Fixed two columns rather than auto-fit. auto-fit makes the layout a function
   of the container, which is exactly what let three pages disagree; two columns
   is two columns wherever this lands. It collapses to one on a narrow screen,
   which is the only reflow worth having. */
.theme-grid {
  display: grid; gap: 1rem; max-width: 42rem;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (max-width: 40rem) { .theme-grid { grid-template-columns: minmax(0, 1fr); } }
/* A column with the preview pushed to the bottom, so previews line up across a
   row however many lines each description runs to. Grid stretches the cards to
   equal height already; this is what makes that visible. */
.theme-option {
  position: relative; cursor: pointer;
  display: flex; flex-direction: column;
  padding: 1.1rem 1.2rem 1.2rem;
  background: var(--surface); border: calc(var(--look-border) * 1.5) solid var(--border);
  border-radius: calc(var(--look-radius) + 2px);
  transition: border-color .12s ease;
}
.theme-option:hover { border-color: var(--border-strong); }
.theme-option:has(.option-input:focus-visible) {
  outline: 2px solid var(--focus-ring); outline-offset: 2px;
}
/* Saved, and selected-right-now. The palette cards got away with only the
   first because theme_preview_controller repaints the whole page on click, so
   there was always *some* feedback — but that feedback is JavaScript's, and
   without it these cards were as silent as the language ones. */
.theme-option--current,
.theme-option:has(.option-input:checked) {
  border-color: var(--selected);
  background: var(--selected-surface);
}
.theme-name {
  display: block; font-family: var(--font-display); font-weight: var(--weight-display); font-size: 1.1rem;
}
.theme-description {
  display: block; font-size: .85rem; color: var(--text-muted); margin: .2rem 0 .9rem;
  flex: 1;
}

/* A miniature of a real screen. Everything inside resolves the palette named
   by its own data-theme, so what a pupil sees here is what they will get —
   there is no second copy of the colours to fall out of step. */
.theme-preview {
  display: block; margin-top: auto; padding: .6rem;
  background: var(--surface-sunken);
  border: 1px solid var(--border); border-radius: var(--radius-md);
}
.theme-preview-card {
  display: block; padding: .55rem .6rem;
  background: var(--surface);
  border: 1px solid var(--border); border-top: 2px solid var(--accent);
  border-radius: var(--radius-sm);
}
.theme-preview-heading {
  display: block; height: .5rem; width: 55%; border-radius: 2px;
  background: var(--text); margin-bottom: .38rem;
}
.theme-preview-line {
  display: block; height: .3rem; border-radius: 2px;
  background: var(--text-muted); margin-bottom: .28rem;
}
.theme-preview-line--short { width: 65%; }
.theme-preview-buttons { display: flex; gap: .3rem; margin-top: .5rem; }
.theme-preview-button { height: .85rem; width: 2.6rem; border-radius: 3px; }
.theme-preview-button--action { background: var(--action-bg); }
.theme-preview-button--danger { background: var(--danger-bg); }

/* --- The look picker (settings/look) ---------------------------------------

   A tile per look: its emoji over its name, in equal columns, so sixteen looks
   are four rows of four rather than a page of cards. The emoji goes on top
   because a name beside it did not fit a quarter of the column: "Mission
   control" broke over two lines and left its row taller than the rest, and
   chips sized to their names instead lined up with nothing. The columns are
   auto-fill rather than fixed, against .theme-grid's rule, because a pupil on
   a larger text size needs fewer, wider tiles; the max-width is what holds it
   at four, which is what makes sixteen a square. Nothing on a tile is painted
   by the look it names — pressing it paints the whole page in that look, which
   is the preview, and sixteen tiles each in its own colours were the wall of
   colour the quick menu dropped them for.

   ⚠️ :checked ALONE, as .read-aloud-option, for the reason given there: a
   server-rendered --current class stays on the saved tile while :checked moves,
   and without JavaScript that lit two. The class's tile is a row of its own,
   emoji beside the name, because its name is a sentence and it is a different
   kind of choice. */
.look-grid {
  display: grid; gap: .6rem; max-width: 42rem;
  grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
}
.look-option {
  position: relative; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .4rem; padding: .85rem .5rem .75rem; text-align: center;
  background: var(--surface); border: calc(var(--look-border) * 1.5) solid var(--border);
  border-radius: calc(var(--look-radius) + 2px);
  transition: border-color .12s ease;
}
.look-option--class {
  grid-column: 1 / -1;
  flex-direction: row; justify-content: flex-start; gap: .6rem;
  padding: .6rem .9rem; text-align: start;
}
.look-option:hover { border-color: var(--border-strong); }
.look-option:has(.option-input:focus-visible) {
  outline: 2px solid var(--focus-ring); outline-offset: 2px;
}
.look-option:has(.option-input:checked) {
  border-color: var(--selected);
  background: var(--selected-surface);
}
.look-emoji { font-size: 1.75rem; line-height: 1; }
.look-option--class .look-emoji { font-size: 1.3rem; }
.look-name { font-weight: 600; line-height: 1.2; }
/* The look's own line about what Scenery off puts away, under the switch. */
.look-off-line { margin: -.75rem 0 1rem; }

.setting-actions {
  display: flex; align-items: center; gap: .6rem; margin-top: 1.5rem; flex-wrap: wrap;
}
.setting-actions input[type=submit].action { margin: 0; }
.preview-note {
  font-family: var(--font-label); font-size: .66rem;
  text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted);
}
[hidden] { display: none !important; }


/* --- The type axes -------------------------------------------------------

   Three columns rather than the palette picker's two: the widest axis has four
   options, and a card here holds a line of sample text instead of a miniature
   screen, so it needs less room. Fixed columns for the reason .theme-grid gives
   — auto-fit makes the layout a function of the container, which is what let
   three pages disagree about the same partial. */

.type-grid {
  display: grid; gap: .8rem; max-width: 42rem;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media (max-width: 46rem) { .type-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 30rem) { .type-grid { grid-template-columns: minmax(0, 1fr); } }

.type-option {
  position: relative; cursor: pointer;
  display: flex; flex-direction: column;
  padding: .9rem 1rem 1rem;
  background: var(--surface); border: calc(var(--look-border) * 1.5) solid var(--border);
  border-radius: calc(var(--look-radius) + 2px);
  transition: border-color .12s ease;
}
.type-option:hover { border-color: var(--border-strong); }
.type-option:has(.option-input:focus-visible) {
  outline: 2px solid var(--focus-ring); outline-offset: 2px;
}
.type-option--current,
.type-option:has(.option-input:checked) {
  border-color: var(--selected);
  background: var(--selected-surface);
}
.type-name { display: block; font-size: 1rem; font-weight: 600; }
.type-description {
  display: block; font-size: .82rem; color: var(--text-muted); margin: .2rem 0 .8rem;
  flex: 1;
}

/* The sample, and the whole reason these cards are markup rather than pictures.

   It restates every type property explicitly instead of inheriting them. That
   is the point: the card carries one data-type-* attribute of its own, so the
   token for that axis resolves to the option being advertised while the other
   three resolve to what the reader is actually using. Inheriting would have
   taken the computed values from the page and shown four identical samples.

   The size sample is in em against a base under 1, so the four options differ
   from each other correctly while none of them grows the card out of its
   column at the largest applied setting. */
.type-sample {
  display: block; margin-top: auto; padding: .5rem .6rem;
  background: var(--surface-sunken);
  border: 1px solid var(--border); border-radius: var(--radius-md);
  font-size: calc(.78em * var(--type-scale));
  line-height: var(--line-height);
  letter-spacing: var(--tracking);
  word-spacing: var(--word-spacing);
  max-inline-size: var(--measure);
  /* `anywhere`, not `break-word`: only `anywhere` lowers the min-content width,
     which is what the grid sizes a column from. With every type setting turned
     up, "Electrolysis" set in capitals is wider than a card, and as one
     unbreakable word it pushed the Settings page 7px past a 1280px window
     (the viewport sweep, under the letter-case setting). */
  overflow-wrap: anywhere;
}
/* Both roles, because two of the three faces differ only in the display one.
   Each is named explicitly rather than inherited, for the same reason the
   sample restates the other properties: the card carries its own data-type-*,
   and only a declaration re-resolves the token under it. */
.type-sample-heading {
  display: block; font-family: var(--font-display); font-weight: var(--weight-display);
  font-size: 1.15em; margin-bottom: .15em;
}
.type-sample-body { display: block; font-family: var(--font-body); }

/* Line length is the one axis a sample at reading size cannot show: 66ch and
   52ch are both wider than a card in a three-column grid, so all three options
   would clip to the same width and look identical. Set small, the measure is
   narrower than the card again and the difference is visible as what it
   actually is — the shape of a paragraph. It is aria-hidden, so nobody is being
   asked to read it. */
.type-form[data-axis="length"] .type-sample { font-size: .34em; }
.type-form[data-axis="length"] .type-sample-heading { display: none; }

.profile-facts { margin: 0; }
.profile-facts > div {
  display: flex; justify-content: space-between; gap: 1rem;
  padding: .6rem 0; border-bottom: 1px solid var(--border);
}
.profile-facts > div:last-child { border-bottom: 0; }
.profile-facts dt {
  font-family: var(--font-label); font-size: .68rem; text-transform: uppercase;
  letter-spacing: .07em; color: var(--text-muted);
}
.profile-facts dd { margin: 0; font-family: var(--font-label); font-size: .8rem; }

/* --- The pupil's mark (components/PupilMark.tsx) --------------------------

   A disc in a tint of one colour and a shape drawn in it, at --mark-size: the
   masthead's 28px, the register's 36px, the picker's 44px. --ink is the slot's
   token, set by data-colour below, or — for a pupil's own hue — written on the
   element by the component as an oklch() of the palette's lightness and
   strength with only the hue the pupil's; it is written there and not here
   because a colour function below :root is what css-tokens.test.ts refuses,
   and that one is built from the tokens above. --disc is the tint, and the
   shape's cut-outs (an eye, a window) are painted in it, so a mark is one hue
   and a tint of it on every palette. High contrast sets the mix to nothing
   and the ring to 2px (the tokens say why). */
.mark {
  --ink: var(--text);
  --disc: color-mix(in srgb, var(--ink) var(--icon-disc-mix), var(--surface));
  display: inline-grid; place-items: center; flex: none;
  width: var(--mark-size); height: var(--mark-size); border-radius: 50%;
  background: var(--disc); color: var(--ink);
  box-shadow: inset 0 0 0 var(--icon-ring) var(--ink);
}
.mark--teal { --ink: var(--icon-teal); }
.mark--violet { --ink: var(--icon-violet); }
.mark--amber { --ink: var(--icon-amber); }
.mark--rose { --ink: var(--icon-rose); }
.mark--sky { --ink: var(--icon-sky); }
.mark--moss { --ink: var(--icon-moss); }
.mark-shape { width: 68%; height: 68%; display: block; }
/* Beside the speaker's word in a chat, which is set small and in capitals. */
.mark--chat { vertical-align: middle; margin-top: -2px; }

/* --- The icon picker (routes/settings-profile.tsx) -------------------------

   Two fieldsets and nothing else on purpose (design review, 15 October 2026):
   a row of colour tiles, then one grid of thirty shape tiles, six to a row at
   the content column's width. Pressing a tile saves — the form posts on
   change, and the masthead's mark changing is the whole confirmation — so
   there is no Save but the one inside <noscript>. The tiles follow
   .look-option: :checked alone, since the server-rendered state and the
   pressed state are one here. The wheel is a ring of every hue at the
   palette's lightness, --icon-wheel on :root. */
.colour-row { display: flex; gap: .6rem; flex-wrap: wrap; }
.colour-option {
  position: relative; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; gap: .35rem;
  width: 4.9rem; padding: .55rem .25rem .5rem;
  background: var(--surface); border: calc(var(--look-border) * 1.5) solid var(--border);
  border-radius: calc(var(--look-radius) + 2px);
  transition: border-color .12s ease;
}
.colour-option:hover { border-color: var(--border-strong); }
.colour-option:has(.option-input:focus-visible) { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.colour-option:has(.option-input:checked) { border-color: var(--selected); background: var(--selected-surface); }
.colour-swatch { width: 28px; height: 28px; border-radius: 50%; background: var(--ink); box-shadow: inset 0 0 0 var(--icon-ring) var(--ink); }
.colour-swatch--teal { --ink: var(--icon-teal); }
.colour-swatch--violet { --ink: var(--icon-violet); }
.colour-swatch--amber { --ink: var(--icon-amber); }
.colour-swatch--rose { --ink: var(--icon-rose); }
.colour-swatch--sky { --ink: var(--icon-sky); }
.colour-swatch--moss { --ink: var(--icon-moss); }
/* The wheel's own tile: the ring's gradient, set inline by the component. */
.colour-swatch--wheel { background: var(--icon-wheel); }
.colour-name { font-size: .78rem; font-weight: 600; line-height: 1.2; text-align: center; }

.wheel-row {
  display: flex; flex-direction: column; align-items: center; gap: .75rem;
  margin-top: .75rem; max-width: 42rem; padding: 1.1rem;
  background: var(--surface); border: calc(var(--look-border) * 1.5) solid var(--border);
  border-radius: calc(var(--look-radius) + 2px);
}
.wheel { position: relative; width: 140px; height: 140px; display: grid; place-items: center; border-radius: 50%; cursor: pointer; touch-action: none; }
.wheel:has(.wheel-input:focus-visible) { outline: 2px solid var(--focus-ring); outline-offset: 4px; }
.wheel-ring {
  position: absolute; inset: 0; border-radius: 50%; background: var(--icon-wheel);
  -webkit-mask: radial-gradient(circle, transparent 46px, var(--text) 47px);
  mask: radial-gradient(circle, transparent 46px, var(--text) 47px);
}
.wheel-handle {
  position: absolute; width: 26px; height: 26px; border-radius: 50%;
  transform: translate(-50%, -50%); pointer-events: none;
  background: var(--ink); border: 3px solid var(--surface); box-shadow: 0 0 0 1.5px var(--border-strong);
}
/* The range input the wheel stands on: present for the keyboard and a screen
   reader, drawn as nothing (.visually-hidden's rule, kept inside the ring so
   its focus ring lands on the ring). */
.wheel-input { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.wheel-name { font-size: 1rem; font-weight: 600; }

.icon-grid {
  display: grid; gap: .5rem; max-width: 42rem;
  grid-template-columns: repeat(auto-fill, minmax(6.25rem, 1fr));
}
.icon-option {
  position: relative; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; gap: .35rem;
  padding: .6rem .3rem .5rem; text-align: center;
  background: var(--surface); border: calc(var(--look-border) * 1.5) solid var(--border);
  border-radius: calc(var(--look-radius) + 2px);
  transition: border-color .12s ease;
}
.icon-option:hover { border-color: var(--border-strong); }
.icon-option:has(.option-input:focus-visible) { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.icon-option:has(.option-input:checked) { border-color: var(--selected); background: var(--selected-surface); }
.icon-name { font-size: .74rem; line-height: 1.2; font-weight: 600; overflow-wrap: anywhere; }
.icon-form legend {
  padding: 0; margin-bottom: .6rem;
  font-family: var(--font-label); font-size: .68rem; text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted);
}
.icon-form fieldset { border: 0; padding: 0; margin: 0 0 1.5rem; min-width: 0; }
.icon-form noscript .action { margin-top: .5rem; }

/* --- Teacher app ---------------------------------------------------------
   A different product wearing the same brand: wider, denser, and built around
   tables rather than cards, because a teacher is comparing rows where a pupil
   is reading one thing at a time. */

.masthead--teacher { border-bottom-width: 2px; border-bottom-color: var(--border-strong); }
.masthead-inner--wide { max-width: 76rem; }

/* The app nav — teacher, parent and pupil. It was `.teacher-nav` while two of
   the three used it, which was already one register too many for the name.

   margin-inline-start, not margin-left: Arabic and Urdu put the wordmark on the
   right, and the nav belongs beside it rather than across the masthead. */
.app-nav { display: flex; gap: .2rem; margin-inline-start: 1.25rem; }
.app-nav-link {
  padding: .4rem .7rem; border-radius: var(--look-radius-control); text-decoration: none;
  font-family: var(--font-label); font-size: .78rem;
  text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted);
}
.app-nav-link:hover { background: var(--surface-sunken); color: var(--text-link); }
.app-nav-link.active { background: var(--tint-brand-fill); color: var(--text-link); }
/* A queue with somebody in it, in the nav on every teacher screen. Colour is
   the second signal and not the only one — the count in the label is the first,
   so this still reads as "3 waiting" with no colour at all. */
.app-nav-link--alert { color: var(--error); }
.app-nav-link--alert:hover { color: var(--error); }

/* --- The class grid ---------------------------------------------------------
   One row per pupil, one column per idea, every cell one of eight states drawn
   three ways at once: a fill from the five --cell-* tokens, a glyph, and the
   words a screen reader gets. The fill is the least of the three. In the
   high-contrast palette every fill is white and the grid still reads; that is
   the test of whether the glyph and the edge are doing their job.

   Cells are separated by a 2px gap in the surface colour rather than a border,
   which is the cheapest secondary encoding there is: two abutting fills of
   similar lightness read as one shape without it. */

.picture-layout {
  display: grid; gap: 1.25rem; align-items: start;
  grid-template-columns: minmax(0, 1fr) minmax(16rem, 22rem);
}
@media (max-width: 62rem) { .picture-layout { grid-template-columns: minmax(0, 1fr); } }
/* The scope picker: one select that goes on change, dressed as the roll's
   search box is — the same border, radius and surface — so it reads as one of
   the page's own controls rather than the browser's. The Show button lives in
   <noscript>; with script, choosing is the request. */
.picture-scope {
  display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .75rem;
  margin-bottom: .75rem;
}
.picture-scope-label { font-family: var(--font-label); font-size: .72rem; text-transform: uppercase;
                       letter-spacing: .06em; color: var(--text-muted); }
.picture-scope-select {
  max-width: min(28rem, 100%); padding: .45rem .65rem;
  font: inherit; font-family: var(--font-body); font-size: .9rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text);
}
.picture-showing { color: var(--text-muted); font-size: .92rem; margin: 0 0 1.25rem; }
.scroll-x { overflow-x: auto; max-width: 100%; }
/* The grid's own frame scrolls both ways, so thirty rows do not push the roll
   off the class page, and the heads stay put while the rows go by. */
.grid-scroll { max-height: 32rem; }
table.grid thead th { position: sticky; top: 0; z-index: 1; background: var(--surface); }
.picture-grid--class, .heat-panel { margin-bottom: 1.25rem; }
.panel-head-link { margin-inline-start: auto; }

/* --- The class breakdown --------------------------------------------------
   A group per lesson, in teaching order: the lesson as a heading line across
   the panel, and beneath it equal tiles, one per idea somebody in the class
   has met, most held first, the fill deepening with how much of the class
   holds it: nobody, under a quarter, a quarter, half, three quarters. Every
   group lays its tiles on the same column grid, so they line up from lesson
   to lesson and a lesson with one idea is one aligned tile rather than a
   short row beside a label. The count is what a reader reads; the fill is
   what a reader scans, and in the high-contrast palette — where the four
   fills are all white — the count does the whole job. The top band is the
   error red with the surface's ink on it, because "three quarters of the
   class" is the one band that should stop a glance. */
.heat-group { padding: .8rem 0 .2rem; border-top: 1px solid var(--border); }
.heat-group:first-child { border-top: 0; padding-top: .1rem; }
.heat-lesson {
  display: flex; align-items: baseline; gap: .6rem; margin: 0 0 .55rem;
  font-family: var(--font-body); font-size: .92rem; font-weight: 600;
}
.heat-lesson a { color: var(--text); }
.heat-lesson-count { font-family: var(--font-label); font-size: .68rem; font-weight: 500;
                     text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); }
.heat-tiles {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: .5rem; grid-template-columns: repeat(auto-fill, minmax(11.5rem, 1fr));
}
.heat-tile {
  display: flex; flex-direction: column; gap: .25rem; height: 100%; min-height: 5.6rem;
  padding: .6rem .7rem; border-radius: var(--radius-md); text-decoration: none;
  color: var(--text); background: var(--cell-none);
}
.heat-tile:hover, .heat-tile:focus-visible { outline: 2px solid var(--text-link); outline-offset: 2px; }
.heat-tile-statement {
  font-size: .82rem; line-height: 1.3;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.heat-tile-count { margin-top: auto; font-family: var(--font-label); font-size: .82rem; font-weight: 600; }
.heat-band--0 { color: var(--text-muted); }
.heat-band--1 { background: var(--cell-slip); }
.heat-band--2 { background: var(--cell-open); }
.heat-band--3 { background: var(--cell-persistent); }
.heat-band--4 { background: var(--error); color: var(--surface); }
.heat-legend .legend-swatch { width: 2rem; height: 1.2rem; border-radius: 4px; background: var(--cell-none); }
[data-theme="high-contrast"] .heat-tile, [data-theme="high-contrast"] .heat-legend .legend-swatch { border: 2px solid var(--border); }
/* The words behind a column head, and the meaning of a marked science word —
   both fixed to the viewport by their own controller from the element's own
   rectangle, inverted so they sit on top of anything, and hidden until hovered,
   focused or tapped. Neither is in the accessibility tree: the head's own label
   carries its words, and the "Key words" list carries the definitions. One rule
   because they are one thing drawn twice. */
.grid-tip, .word-bank-tip, .command-word-tip {
  position: fixed; z-index: 30; max-width: 22rem; padding: .5rem .7rem;
  background: var(--text); color: var(--surface);
  border-radius: var(--radius-sm); font-size: .85rem; line-height: 1.35; pointer-events: none;
}
[data-theme="high-contrast"] .grid-tip,
[data-theme="high-contrast"] .word-bank-tip,
[data-theme="high-contrast"] .command-word-tip { border: 2px solid var(--surface); }
.command-word-tip strong { display: block; font-family: var(--font-label); }
table.grid { border-collapse: separate; border-spacing: 0; font-size: .85rem; }
table.grid th, table.grid td { padding: 0; vertical-align: middle; }
table.grid .grid-name {
  text-align: start; padding: .3rem .75rem .3rem 0; white-space: nowrap;
  font-family: var(--font-body); font-weight: 500; min-width: 11rem;
}
table.grid thead .grid-name { font-family: var(--font-label); font-size: .68rem; text-transform: uppercase;
                              letter-spacing: .06em; color: var(--text-muted); vertical-align: bottom; }
.grid-col { width: 2.4rem; vertical-align: bottom; }
.grid-col-link {
  display: flex; flex-direction: column; align-items: center; gap: .1rem;
  padding: .3rem 0; text-decoration: none; border-radius: 6px 6px 0 0;
}
.grid-col-link:hover { background: var(--surface-sunken); }
.grid-col--current .grid-col-link { background: var(--tint-brand-fill); }
.grid-col-n { font-family: var(--font-label); font-size: .78rem; font-weight: 600; color: var(--text-link); }
.grid-col-count { font-family: var(--font-label); font-size: .68rem; color: var(--text-muted); }
.grid-count { text-align: center; padding: 0 .5rem; font-family: var(--font-label); font-size: .78rem;
              color: var(--text-muted); width: 3.5rem; }
thead .grid-count { text-transform: uppercase; letter-spacing: .06em; font-size: .62rem; vertical-align: bottom; }
.cell {
  width: 2.4rem; height: 1.75rem; text-align: center;
  border: 2px solid var(--surface); border-radius: 4px;
  background: var(--cell-none); color: var(--text);
}
.cell-glyph { font-family: var(--font-label); font-size: .85rem; line-height: 1; font-weight: 600; }
.cell--not-attempted { background: var(--cell-none); color: var(--text-muted); }
.cell--secure, .cell--resolving, .cell--resolved { background: var(--cell-secure); }
.cell--slip { background: var(--cell-slip); }
.cell--open, .cell--lapsed { background: var(--cell-open); }
/* Persistent shares a warm hue with open, so it carries an edge the way the
   design gave it a stripe: the one state on the grid that has stopped being
   the tutor's problem. */
.cell--persistent { background: var(--cell-persistent); box-shadow: inset 3px 0 0 var(--error); }
[data-theme="high-contrast"] .cell { border-color: var(--border); }
.grid-totals th, .grid-totals td { border-top: 1px solid var(--border); padding-top: .4rem; }
.grid-totals .grid-name { color: var(--text-muted); font-weight: 400; }
.legend { list-style: none; padding: 0; margin: .9rem 0 .4rem; display: flex; flex-wrap: wrap; gap: .5rem 1.1rem; }
.legend-item { display: flex; align-items: center; gap: .45rem; font-size: .8rem; color: var(--text-muted); }
.legend-swatch { display: inline-flex; align-items: center; justify-content: center; width: 1.5rem; height: 1.3rem; }
.legend-count { font-family: var(--font-label); font-size: .72rem; }
.picture-detail-statement { font-size: 1.05rem; line-height: 1.45; margin: 0 0 .3rem; }
.picture-stats { margin: .75rem 0 1rem; }
.picture-detail h3 { font-size: .95rem; margin: 1.1rem 0 .4rem; }
.picture-quote {
  margin: 0; padding: .1rem 0 .1rem .75rem; border-inline-start: 3px solid var(--border);
  font-style: italic; color: var(--text);
}
/* The day, on Today. Three columns rather than the mini-list's row — the
   period and time, the lesson, the one act a period invites — so the eye can
   run down the periods without the lesson titles drifting to the centre and
   the times wrapping under them. The period column is as wide as its widest
   entry and no wider. Doubled up with .mini-list because that rule sits
   later in this file at the same specificity and would otherwise win. */
.mini-list.day-list > li {
  display: grid; grid-template-columns: max-content minmax(0, 1fr) max-content;
  gap: .3rem .9rem; align-items: baseline; justify-content: start;
}
.day-list .mini-meta { white-space: nowrap; text-align: start; }
.day-list .panel-link { margin: 0; white-space: nowrap; }

/* --- The term view ------------------------------------------------------------
   One row per unit, one column per week, a count in every cell with the class
   size beside it, and a fill behind the count that deepens with how much of
   the class that is. One hue — the open tint, the same the grid uses — at five
   depths via opacity, so intensity always means the same thing and no second
   colour has to be told apart from it. The count is what a reader reads; the
   fill is what a reader scans. */

table.term { border-collapse: separate; border-spacing: 0; font-size: .82rem; width: 100%; }
table.term th, table.term td { padding: .35rem .5rem; vertical-align: middle; }
table.term .term-unit { text-align: start; min-width: 12rem; font-family: var(--font-body); font-weight: 500; }
table.term thead th { font-family: var(--font-label); font-size: .64rem; text-transform: uppercase;
                      letter-spacing: .06em; color: var(--text-muted); text-align: center; vertical-align: bottom; }
table.term thead .term-unit { text-align: start; }
.term-cell { position: relative; text-align: center; min-width: 3.4rem; border: 2px solid var(--surface); border-radius: 4px; }
.term-cell::before {
  content: ""; position: absolute; inset: 0; border-radius: 4px;
  background: var(--cell-open); opacity: 0; z-index: 0;
}
/* Lifted above the fill — every child but the hidden words, which must keep
   the position the visually-hidden recipe gives them or they stop being
   hidden. */
.term-cell > :not(.visually-hidden) { position: relative; z-index: 1; }
/* Five depths of the one fill. Opacity rather than five tokens: the hue is
   the grid's open tint and only the depth varies, which is what makes the
   scale readable — intensity always means the same thing. */
.term-cell--1::before { opacity: .18; }
.term-cell--2::before { opacity: .36; }
.term-cell--3::before { opacity: .58; }
.term-cell--4::before { opacity: .8; }
.term-cell--5::before { opacity: 1; }
.term-count { font-family: var(--font-label); font-size: .78rem; white-space: nowrap; }
.term-none { color: var(--text-muted); }
.term-direction { text-align: start; white-space: nowrap; }

/* --- The teacher rail ------------------------------------------------------
   The teacher app's navigation, down the side rather than across the top. A
   masthead holds five words; the teacher side now has three sections every
   teacher sees and two that only some do, and a row is the wrong shape for a
   list that grows by role. The parent app keeps its masthead — two links do not
   need a column.

   The fold is a column on the teacher (teachers.rail_collapsed), written onto
   <html> as data-rail before the first paint, so every rule below keys off the
   root rather than off a class a script would add after load. The width is one
   custom property so the grid, the rail and the fold agree by construction.

   Logical properties throughout: Arabic and Urdu put the rail on the right, and
   the grid flips with `direction` on its own. */

.teacher-shell {
  --rail-width: 15rem;
  font-size: var(--size-body);
  display: grid; grid-template-columns: var(--rail-width) minmax(0, 1fr);
  min-height: 100vh;
}
.rail {
  position: sticky; top: 0; height: 100vh; overflow-y: auto; overflow-x: hidden;
  display: flex; flex-direction: column; gap: 1.2rem;
  padding: 1.1rem 0 .9rem;
  background: var(--surface); border-inline-end: 2px solid var(--border-strong);
}
.rail-brand { padding: 0 1rem; }
.rail-strap {
  display: block; margin-top: .2rem;
  font-family: var(--font-label); font-size: .58rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .1em; color: var(--lockup);
}
.rail-nav { display: flex; flex-direction: column; gap: 1rem; margin: 0; }
/* A heading over a group, not a stop: a <p> named by the group's
   aria-labelledby, so the Tab key skips it and a screen reader still says it. */
.rail-section-label {
  margin: 0 0 .25rem; padding: 0 1.1rem;
  font-family: var(--font-label); font-size: .62rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .1em; color: var(--text-muted);
}
.rail-section-links { display: flex; flex-direction: column; gap: .1rem; padding: 0 .5rem; }
.rail-link {
  display: flex; align-items: center; gap: .65rem;
  padding: .5rem .6rem; border-radius: var(--radius-sm);
  border-inline-start: 3px solid transparent;
}
/* The active entry carries the same tint the masthead's did, plus an edge — the
   edge is what still reads when the rail is folded to icons and the tint is a
   28px square. */
.rail-link.active { border-inline-start-color: var(--text-link); }
.rail-icon { width: 1.15rem; height: 1.15rem; opacity: .8; }
.rail-link:hover .rail-icon, .rail-link.active .rail-icon { opacity: 1; }
.rail-foot {
  margin-top: auto; padding: .75rem .5rem 0; border-top: 1px solid var(--border);
  display: flex; flex-direction: column; gap: .4rem;
}
.rail-collapse-form { margin: 0; }
button.rail-collapse {
  width: 100%; display: flex; align-items: center; gap: .65rem;
  padding: .5rem .6rem; border: 0; border-radius: var(--radius-sm); background: none; cursor: pointer;
  color: var(--text-muted);
  font-family: var(--font-label); font-size: .72rem;
  text-transform: uppercase; letter-spacing: .04em; text-align: start;
}
button.rail-collapse:hover { background: var(--surface-sunken); color: var(--text-link); }
button.rail-collapse .icon { width: 1.15rem; height: 1.15rem; }
.rail .who-label {
  font-family: var(--font-label); font-size: .78rem;
  text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted);
}
/* The profile menu opens upward here: the summary sits at the foot of the
   column, and a menu dropping below it would open into the bottom of the
   viewport. */
.rail .profile-menu { top: auto; bottom: calc(100% + .4rem); inset-inline: 0 auto; }
.rail-main { min-width: 0; display: flex; flex-direction: column; }
.rail-main > main { flex: 1; }

/* Folded. Only where there is a column to fold: under the breakpoint below the
   rail is a strip and the fold does nothing, so the rule must not apply there
   or a teacher who folded it on a desk arrives at a tablet with no labels. */
@media (min-width: 62.01rem) {
  html[data-rail="collapsed"] .teacher-shell { --rail-width: 4rem; }
  html[data-rail="collapsed"] .rail-label,
  html[data-rail="collapsed"] .rail-section-label,
  html[data-rail="collapsed"] .rail-strap,
  html[data-rail="collapsed"] .rail .who-label,
  html[data-rail="collapsed"] .rail-collapse-label {
    position: absolute; width: 1px; height: 1px; margin: -1px;
    padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }
  html[data-rail="collapsed"] .rail-brand { padding: 0 .35rem; text-align: center; }
  html[data-rail="collapsed"] .rail .wordmark { font-size: .82rem; letter-spacing: .06em; }
  /* A hairline where each heading was, so the groups still read as groups. */
  html[data-rail="collapsed"] .rail-section + .rail-section { border-top: 1px solid var(--border); padding-top: .6rem; }
  html[data-rail="collapsed"] .rail-link { justify-content: center; padding: .6rem 0; gap: 0; }
  html[data-rail="collapsed"] button.rail-collapse { justify-content: center; padding: .6rem 0; gap: 0; }
  html[data-rail="collapsed"] button.rail-collapse .icon { transform: rotate(180deg); }
  html[data-rail="collapsed"] .rail .profile > summary { justify-content: center; padding: .3rem; }
}

/* Narrow: a strip across the top, the way the masthead was. Not a folded rail,
   because a 4rem column on a 40rem screen is a tenth of the page spent on
   icons, and the fold control is hidden so the state cannot be changed from a
   screen where it has no effect. */
@media (max-width: 62rem) {
  .teacher-shell { grid-template-columns: minmax(0, 1fr); }
  .rail {
    position: static; height: auto; overflow: visible;
    flex-direction: row; flex-wrap: wrap; align-items: center; gap: .6rem 1rem;
    padding: .7rem var(--clearspace);
    border-inline-end: 0; border-bottom: 2px solid var(--border-strong);
  }
  .rail-nav { flex-direction: row; flex-wrap: wrap; gap: .3rem; }
  .rail-section-label { display: none; }
  .rail-section-links { flex-direction: row; flex-wrap: wrap; padding: 0; }
  .rail-link { border-inline-start: 0; }
  .rail-icon { display: none; }
  .rail-foot { margin-top: 0; margin-inline-start: auto; padding: 0; border-top: 0; flex-direction: row; align-items: center; }
  .rail-collapse-form { display: none; }
  .rail .profile-menu { top: calc(100% + .4rem); bottom: auto; inset-inline: auto 0; }
}

/* The teacher assistant (docs/plan/Teacher_Assistant_Plan.md; the board's
   frames 1–6). Open, its panel is the shell's third column, so the page beside
   it narrows rather than being covered — a teacher reads an answer about the
   class next to the class. On a narrow screen there is no room for a column,
   and it is a sheet over the page instead.

   ⚠️ THE ANSWER IS NOT A BUBBLE. The teacher's line is a quiet box on the end
   side; the assistant's is drawn as the page's own prose, with no avatar and
   no name, because it is software and is not to look like a person talking
   (CLAUDE.md, "The tutor's voice"). */
.teacher-shell.teacher-shell--assistant {
  grid-template-columns: var(--rail-width) minmax(0, 1fr) minmax(20rem, 26rem);
}
/* In a live lesson: narrower, so the lesson's grid keeps its room (frame 7). */
.teacher-shell.teacher-shell--assistant-slim {
  grid-template-columns: var(--rail-width) minmax(0, 1fr) minmax(18rem, 22rem);
}
.ax-dock--slim .ax-turns { padding: .8rem; gap: .75rem; }
.ax-dock {
  position: sticky; top: 0; height: 100vh;
  display: flex; flex-direction: column; min-width: 0;
  background: var(--surface); color: var(--text);
  border-inline-start: 1px solid var(--border);
}
/* Shut, the landmark is a corner holding the launcher, out of the grid. */
.ax-dock--shut {
  position: fixed; top: auto; inset-inline-end: 1.2rem; bottom: 1.2rem; z-index: 40;
  height: auto; background: transparent; border: 0;
}
.ax-panel { display: flex; flex-direction: column; flex: 1; min-height: 0; }
.ax-panel[hidden] { display: none; }
.ax-icon { width: 1.05rem; height: 1.05rem; flex: none; }
.ax-launcher {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .65rem 1.05rem; border-radius: var(--radius-pill);
  background: var(--action-bg); color: var(--action-ink);
  box-shadow: var(--shadow-menu); font-weight: 600; text-decoration: none;
}
.ax-launcher:hover { background: var(--action-bg-hover); }
.ax-head { display: flex; align-items: center; gap: .6rem; padding: .8rem 1rem; border-bottom: 1px solid var(--border); }
.ax-head h2 { margin: 0; font-size: 1rem; display: flex; align-items: center; gap: .45rem; }
.ax-head h2 .ax-icon { color: var(--text-link); }
.ax-head-tools { margin-inline-start: auto; display: flex; gap: .15rem; }
.ax-tool {
  width: 2.1rem; height: 2.1rem; display: grid; place-items: center;
  border: 0; border-radius: var(--radius-sm); background: transparent; color: var(--text-muted);
  cursor: pointer;
}
.ax-tool:hover { background: var(--surface-sunken); color: var(--text); }
.ax-context {
  margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: .35rem;
  padding: .5rem 1rem; border-bottom: 1px solid var(--border);
  background: var(--surface-sunken); font-size: .8rem; color: var(--text-muted);
}
.ax-chip {
  padding: .1rem .55rem; border-radius: var(--radius-pill);
  background: var(--surface); border: 1px solid var(--border);
  color: var(--text); font-weight: 600; font-size: .78rem;
}
/* column-reverse around one inner column keeps the newest line in view, as a
   chat opens, without a script to scroll it. */
.ax-thread { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column-reverse; }
.ax-turns { list-style: none; margin: 0; padding: 1rem; display: flex; flex-direction: column; gap: 1rem; }
.ax-turns > * { flex: none; }
.ax-me {
  align-self: flex-end; max-width: 88%;
  padding: .55rem .85rem; border-radius: var(--radius-xl) var(--radius-xl) var(--radius-sm) var(--radius-xl);
  background: var(--tint-brand-fill); font-size: .92rem; line-height: 1.45;
}
.ax-me p { margin: 0; white-space: pre-wrap; }
.ax-it { display: flex; flex-direction: column; gap: .5rem; font-size: .92rem; line-height: 1.5; }
.ax-answer p { margin: 0 0 .55rem; }
.ax-answer p:last-child { margin-bottom: 0; }
.ax-answer ul { margin: .2rem 0 .55rem; padding-inline-start: 1.1rem; }
.ax-answer li { margin-bottom: .2rem; }
.ax-it--failed .ax-answer { color: var(--text-muted); }
.ax-again { display: grid; justify-items: start; gap: .4rem; margin-top: .4rem; }
.ax-writing { margin: 0; color: var(--text-muted); font-style: italic; }
.ax-trail { border: 1px solid var(--border); border-radius: var(--radius-md); font-size: .8rem; color: var(--text-muted); }
.ax-trail summary { cursor: pointer; display: flex; align-items: center; gap: .45rem; padding: .4rem .65rem; list-style: none; }
.ax-trail summary::-webkit-details-marker { display: none; }
.ax-trail summary .ax-icon { width: .85rem; height: .85rem; }
.ax-trail-more { margin-inline-start: auto; color: var(--text-link); }
.ax-trail-more::after { content: attr(data-shut); }
.ax-trail[open] .ax-trail-more::after { content: attr(data-open); }
.ax-trail ol { list-style: none; margin: 0; padding: 0 .65rem .5rem 1.9rem; display: grid; gap: .25rem; }
.ax-trail li { position: relative; }
.ax-trail li::before {
  content: ""; position: absolute; inset-inline-start: -1rem; top: .45em;
  width: .45rem; height: .45rem; border-radius: var(--radius-pill); background: var(--success);
}
.ax-trail li.ax-step--refused::before { background: var(--border-strong); }
.ax-foot { border-top: 1px solid var(--border); padding: .75rem 1rem .9rem; display: grid; gap: .6rem; }
.ax-starters ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .4rem; }
.ax-starters form { margin: 0; }
.ax-starters button {
  width: 100%; text-align: start; font: inherit; font-size: .88rem; cursor: pointer;
  padding: .55rem .75rem; border-radius: var(--radius-md);
  border: 1px solid var(--border); background: var(--surface); color: var(--text);
}
.ax-starters button:hover { border-color: var(--border-strong); }
.ax-group-label {
  margin: 0 0 .35rem; font-size: .7rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; color: var(--text-muted);
}
.ax-compose { margin: 0; display: grid; gap: .4rem; }
.ax-compose-box {
  display: flex; align-items: flex-end; gap: .4rem;
  border: 1px solid var(--border-strong); border-radius: var(--radius-lg);
  padding: .45rem .5rem .45rem .7rem; background: var(--surface);
}
.ax-compose-box:focus-within { outline: 3px solid var(--focus-ring); outline-offset: 2px; }
.ax-compose-box textarea {
  flex: 1; min-width: 0; border: 0; resize: none; font: inherit; font-size: .92rem;
  color: var(--text); background: transparent; padding: .2rem 0; margin: 0;
  min-height: 2.6rem; max-height: 10rem; field-sizing: content; box-shadow: none; outline: none;
}
.ax-send {
  flex: none; width: 2.2rem; height: 2.2rem; border: 0; border-radius: var(--radius-pill);
  display: grid; place-items: center; cursor: pointer;
  background: var(--action-bg); color: var(--action-ink);
}
.ax-send:hover { background: var(--action-bg-hover); }
.ax-send:disabled, .ax-starters button:disabled { opacity: .5; cursor: not-allowed; }
.ax-refusal { margin: 0; padding: .45rem .65rem; border-radius: var(--radius-md); background: var(--error-surface); color: var(--error-text); font-size: .86rem; }
.ax-off { margin: .8rem 1rem 0; padding: .55rem .75rem; border-radius: var(--radius-md); background: var(--warning-surface); color: var(--warning-text); font-size: .88rem; }

/* A proposal: steps waiting for the teacher's press (the board's frame 3),
   then what became of them (frame 4). Who each step reaches is on every step;
   home is the alarming colour, because a parent reading it cannot be undone. */
.ax-plan { border: 2px solid var(--accent); border-radius: var(--radius-lg); background: var(--surface); overflow: hidden; }
.ax-plan--settled { border-width: 1px; border-color: var(--border); border-inline-start: 4px solid var(--success); }
.ax-plan-head {
  margin: 0; padding: .55rem .85rem; font-size: .76rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .05em;
  background: var(--warning-surface); color: var(--warning-text);
}
.ax-plan--settled .ax-plan-head { background: var(--success-surface); color: var(--success-text); }
.ax-plan form { margin: 0; }
.ax-plan-steps { list-style: none; margin: 0; padding: 0; }
.ax-plan-step { display: flex; align-items: flex-start; gap: .6rem; padding: .7rem .85rem; border-bottom: 1px solid var(--border); }
.ax-plan-step:last-child { border-bottom: 0; }
.ax-plan-step label { display: flex; gap: .65rem; align-items: flex-start; cursor: pointer; flex: 1; }
.ax-plan-step input[type="checkbox"] { margin-top: .2rem; width: 1.05rem; height: 1.05rem; accent-color: var(--action-bg); flex: none; }
.ax-plan-body { display: flex; flex-direction: column; gap: .3rem; flex: 1; min-width: 0; font-size: .88rem; }
.ax-plan-status { font-size: .8rem; color: var(--text-muted); }
.ax-plan-step--failed .ax-plan-status { color: var(--error-text); }
.ax-plan-step form { flex: none; }
.ax-who {
  align-self: flex-start; padding: .08rem .5rem; border-radius: var(--radius-pill);
  font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
}
.ax-who--pupils { background: var(--tint-brand-fill); color: var(--success-text); }
.ax-who--home { background: var(--error-surface); color: var(--error-text); }
.ax-who--you { background: var(--surface-sunken); color: var(--text-muted); }
.ax-plan-foot {
  display: flex; align-items: center; flex-wrap: wrap; gap: .5rem;
  padding: .65rem .85rem; background: var(--surface-sunken); border-top: 1px solid var(--border);
}
.ax-fine { margin-inline-start: auto; font-size: .74rem; color: var(--text-muted); }

/* The assistant's own page: the chats down the side, one chat beside them.
   It is a workspace, not a document: the column is held to the window
   (TeacherLayout's `fill`) and the chat takes whatever the flash, a paused
   lesson's banner and the footer leave. It was 100vh inside a column that also
   held those, so the footer sat a screen down, under a chat that had already
   filled the window, and the page scrolled to reach it. */
.rail-main--fill { height: 100vh; }
.rail-main--fill > main { display: flex; flex-direction: column; min-height: 0; }
.rail-main--fill > main > * { flex: none; }
.rail-main--fill > main > .ax-ws { flex: 1; height: auto; min-height: 0; }
/* The footer is the column's last line here: still the contentinfo landmark,
   still on every page, but a rule under the workspace rather than a block
   with a 3rem margin after it. */
.rail-main--fill > .site-footer {
  max-width: none; margin: 0; padding: .45rem var(--clearspace);
  background: var(--surface-sunken);
}
.ax-ws { display: grid; grid-template-columns: 16rem minmax(0, 1fr); height: 100vh; }
.ax-ws-threads { border-inline-end: 1px solid var(--border); background: var(--surface-sunken); padding: 1rem .6rem; overflow-y: auto; }
.ax-ws-threads ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .1rem; }
.ax-ws-threads a { display: block; padding: .5rem .6rem; border-radius: var(--radius-sm); color: var(--text); text-decoration: none; font-size: .88rem; }
.ax-ws-threads a small { display: block; color: var(--text-muted); font-size: .74rem; }
.ax-ws-threads a:hover { background: var(--surface); }
.ax-ws-threads a.on { background: var(--surface); box-shadow: inset 3px 0 0 var(--text-link); }
.ax-ws-top { display: flex; gap: .4rem; align-items: center; margin-bottom: .6rem; }
.ax-search {
  flex: 1; min-width: 0; margin: 0; display: flex; align-items: center; gap: .4rem;
  padding: .4rem .55rem; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface);
}
.ax-search:focus-within { outline: 3px solid var(--focus-ring); outline-offset: 2px; }
.ax-search .ax-icon { width: .9rem; height: .9rem; color: var(--text-muted); }
.ax-search input { flex: 1; min-width: 0; border: 0; padding: 0; margin: 0; font: inherit; font-size: .86rem; background: transparent; color: var(--text); box-shadow: none; outline: none; }
.ax-ws-new {
  flex: none; width: 2.2rem; height: 2.2rem; display: grid; place-items: center;
  border-radius: var(--radius-md); background: var(--action-bg); color: var(--action-ink);
}
.ax-ws-empty { margin: .5rem .6rem; color: var(--text-muted); font-size: .88rem; }
.ax-ws-chat { display: flex; flex-direction: column; min-width: 0; min-height: 0; background: var(--surface); }
.ax-ws-chat .ax-turns { padding: 1.4rem 1.6rem; max-width: 46rem; width: 100%; margin-inline: auto; }
.ax-ws-chat .ax-foot { max-width: 46rem; width: 100%; margin-inline: auto; border-top: 0; }
/* The notice that the assistant is off lines up with the chat it is about. */
.ax-ws-chat .ax-off { max-width: 44rem; width: calc(100% - 2rem); margin: 1.4rem auto 0; box-sizing: border-box; }

@media (max-width: 62rem) {
  .teacher-shell.teacher-shell--assistant { grid-template-columns: minmax(0, 1fr); }
  .ax-dock:not(.ax-dock--shut) { position: fixed; inset: 0; z-index: 60; height: auto; border-inline-start: 0; }
  .rail-main--fill { height: auto; }
  .ax-ws { grid-template-columns: minmax(0, 1fr); height: auto; }
  .ax-ws-threads { border-inline-end: 0; border-bottom: 1px solid var(--border); max-height: 14rem; }
  .ax-ws-chat { min-height: 70vh; }
}

/* One flag, and the decision a teacher can record about it. A list rather than
   a table: the pupil's own words are a block of prose, and a table cell is the
   wrong shape for the thing this screen exists to show. */
.flag-queue { list-style: none; margin: 0; padding: 0; display: grid; gap: .9rem; }
.flag-queue-item {
  padding: .8rem .9rem; border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface);
}
/* The whole row, as one target. Block rather than inline so the hit area is
   the card and not the words in it — a teacher reaching for this on a laptop
   trackpad in a corridor should not have to aim. */
.flag-queue-link {
  display: block; text-decoration: none; color: inherit;
  margin: -.8rem -.9rem .2rem; padding: .8rem .9rem; border-radius: var(--radius-md);
}
.flag-queue-link:hover { background: var(--surface-sunken); }
.flag-queue-link:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -2px; }
.flag-queue-link strong { color: var(--text-link); }
.flag-queue-who { display: block; }
/* `quiet` rather than `action`, deliberately. These are the two decisions a
   teacher can record, and neither is the thing the screen wants them to do —
   the screen wants them to read what is above the buttons first. A pair of
   filled primary buttons under a child's own words reads as a form to clear. */
.flag-queue-actions { display: flex; gap: .5rem; margin-top: .7rem; flex-wrap: wrap; }
.flag-queue-actions form.button_to { margin: 0; }
.flag-queue-note { margin: .5rem 0 0; font-size: .8rem; color: var(--text-muted); }

/* The conversation a flagged line sits inside. Numbered, because the order is
   the argument: a refusal reads differently when the three turns before it are
   a pupil being told for the third time they will not be given the answer. */
.flag-thread { list-style: none; margin: 0; padding: 0; display: grid; gap: .55rem; }
.flag-turn {
  padding: .55rem .7rem; border-radius: var(--radius-md); background: var(--surface-sunken);
  border-inline-start: 3px solid transparent;
}
.flag-turn--pupil { border-inline-start-color: var(--border-strong); }
.flag-turn--tutor { border-inline-start-color: var(--action-bg); }
/* The flagged line, marked in place rather than lifted out. Three signals, not
   one: the word "flagged" beside the speaker, the red edge, and the tint. No
   new token for the fill — the accent one already exists in all three themes,
   and the red edge beside it is what says which kind of mark this is. */
.flag-turn--flagged { border-inline-start-color: var(--error); background: var(--tint-accent-fill); }
.flag-turn-who {
  margin: 0 0 .25rem; display: flex; gap: .5rem; align-items: baseline;
  font-family: var(--font-label); font-size: .64rem;
  text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted);
}
/* --error-text, not --error: the word sits on the accent wash, and the solid
   pink is a colour to look at rather than to read — 4.29:1 there. */
.flag-turn-mark { color: var(--error-text); font-weight: 700; }
.flag-turn-text { margin: 0; font-size: .92rem; }
.flag-turn-note { margin: .35rem 0 0; font-size: .78rem; color: var(--text-muted); font-style: italic; }

.flag-setting { margin: 0; display: grid; gap: .6rem; }
.flag-setting dt {
  font-family: var(--font-label); font-size: .64rem;
  text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted);
}
.flag-setting dd { margin: .15rem 0 0; font-size: .92rem; }
.flag-facts { margin: 0; padding-inline-start: 1.1rem; }
.flag-history { list-style: none; margin: 0; padding: 0; display: grid; gap: .4rem; font-size: .9rem; }

/* The case list, and one case as a row.

   THE SAME SHAPE AS THE QUEUE IT REPLACES, on purpose. A school that has been
   using the flag queue should not have to relearn where to click: the whole row
   is the target, the band is first on the meta line because the list is sorted
   by it, and everything else reads left to right in the order somebody triages
   in. What changed is what a row IS — a concern about a child rather than a
   chat message a detector stopped. */
.case-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .6rem; }
.case-row {
  padding: .7rem .85rem; border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface);
}
.case-row-link {
  display: block; text-decoration: none; color: inherit;
  margin: -.7rem -.85rem; padding: .7rem .85rem; border-radius: var(--radius-md);
}
.case-row-link:hover { background: var(--surface-sunken); }
.case-row-link:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -2px; }
.case-row-link strong { color: var(--text-link); }
.case-row-who { display: block; }
.case-row-state {
  display: block; margin-top: .3rem;
  font-family: var(--font-label); font-size: .68rem;
  text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted);
}
/* Which school the child is at. On every row, including where the school has
   only one — see the partial. Set in the label face rather than as another
   badge: it is provenance, not a state, and a second pill beside the urgency
   band would compete with the one thing the row is sorted by. */
.case-school {
  font-family: var(--font-label); font-size: .66rem;
  letter-spacing: .05em; color: var(--text-muted);
}
/* A case somebody wearing the safeguarding hat has acted on, which is what
   decides who may close it. Marked rather than coloured: it is not a worse
   case, it is a case with a different owner. */
.case-lead { font-weight: 700; color: var(--text); }

/* Narrowing the list. Links rather than a control, so every combination is a
   URL somebody can keep — and so the whole thing works with JavaScript off,
   which no filter on a safeguarding screen should need. */
.case-filters {
  display: flex; align-items: center; gap: .45rem; flex-wrap: wrap;
  margin: 0 0 1.4rem; padding: .6rem .75rem;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
}
.case-filter-label {
  font-family: var(--font-label); font-size: .64rem;
  text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted);
}
.case-filter-label:not(:first-child) { margin-inline-start: .8rem; }
.case-filter {
  padding: .3rem .7rem; border-radius: var(--radius-pill); text-decoration: none;
  font-family: var(--font-label); font-size: .72rem;
  border: 1px solid var(--border); background: var(--surface); color: var(--text);
}
.case-filter:hover { border-color: var(--border-strong); }
/* The one in force, and it has to be legible without colour: a filter somebody
   cannot see the state of is a list they cannot trust the length of. */
.case-filter.active {
  background: var(--action-bg); color: var(--action-ink);
  border-color: var(--action-bg); font-weight: 600;
}
.case-filter:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* The evidence on a case: every flagged line, with what was said. The words are
   on the page rather than behind the link, because this is the screen somebody
   decides from and a decision cannot be taken from a count. */
.case-evidence { list-style: none; margin: 0; padding: 0; display: grid; gap: .6rem; }
.case-evidence > li {
  padding: .7rem .85rem; background: var(--surface-sunken); border-radius: var(--radius-md);
}
.case-evidence-link { display: block; text-decoration: none; color: inherit; }
/* Each part on its own line. They are spans because an anchor may not contain a
   paragraph, and without this the reason, the time and "The pupil wrote" run
   together into one sentence that reads as though the lesson title were part of
   the attribution. */
.case-evidence-link > span { display: block; }
.case-evidence-link .flag-idea { color: var(--text-link); font-weight: 600; margin: 0; }
.case-evidence-link .flag-meta { margin-top: .15rem; }
.case-evidence-link:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* What the reference is for, said once beside it. Set down to the label face so
   the line reads as name, school, code — and not as a sentence with a code in
   the middle of it. */
.case-ref-help {
  font-family: var(--font-label); font-size: .66rem;
  letter-spacing: .05em; color: var(--text-muted);
}

/* The case's own record: what the school did about the concern, in order.

   NUMBERED AND RULED, because the order is the claim being made. A trail read
   out of order is a list of things that happened; read in order it is an
   account, and the one line that matters most — a decision reversed sitting
   after the decision it reversed — only reads correctly as a sequence. Same
   argument .flag-thread makes about the conversation two panels up. */
/* The queue's own triage band. Three of them, and the colour is doing the same
   job the count chips do elsewhere: telling a scanning eye where to start.

   ⚠️ NOT A GRADING OF THE CHILD, and the palette is chosen so it cannot be read
   as one. `error` is deliberately not used — that token marks the flagged line
   itself, and reusing it here would put the same red on "read this first" as on
   a child's own words. The bands lean on the accent tints instead, which are
   about attention rather than alarm. */
.urgency-badge {
  display: inline-block; padding: .05rem .4rem; border-radius: var(--radius-pill);
  font-family: var(--font-label); font-size: .6rem;
  text-transform: uppercase; letter-spacing: .07em;
  background: var(--surface-sunken); color: var(--text-muted);
  border: 1px solid var(--border);
}
.urgency-badge--prompt {
  background: var(--tint-accent-fill-soft); color: var(--text);
  border-color: var(--tint-accent-line);
}
.urgency-badge--urgent {
  background: var(--tint-accent-fill); color: var(--text);
  border-color: var(--tint-accent-edge); font-weight: 700;
}
/* The band a case is already in, shown as a state rather than as a fourth
   button. A row of three buttons where one of them does nothing is a row that
   makes somebody click to find out which. */
.urgency-badge--current { padding: .3rem .6rem; font-size: .64rem; }

.grading-heading {
  margin: 1.2rem 0 .3rem; font-size: .95rem;
}
.grading-heading:first-of-type { margin-top: .2rem; }
/* Whose view a grading is. Not muted into the background: it is the sentence
   that stops a class teacher's opinion being read as a determination. */
.grading-caveat {
  margin: 0 0 .6rem; padding: .5rem .7rem; border-radius: var(--radius-md);
  background: var(--surface-sunken); border-inline-start: 3px solid var(--border-strong);
  font-size: .85rem;
}
.grading-who { margin: 0 0 .6rem; font-size: .85rem; color: var(--text-muted); }
/* The picker takes the column and the button does not. A submit stretched to
   the width of the form it sits under reads as the primary act of the page,
   which this is not — deciding is, two panels up. */
.grading-form select { max-width: 28rem; }
.grading-form input[type=submit] { justify-self: start; }

.case-clocks {
  margin: -.5rem 0 .9rem; font-family: var(--font-label); font-size: .64rem;
  text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted);
}
.case-trail {
  list-style: none; margin: 0 0 1.4rem; padding: 0 0 0 .9rem;
  display: grid; gap: .7rem; border-inline-start: 2px solid var(--border);
}
.case-event { position: relative; }
.case-event-what { margin: 0; font-size: .92rem; }
.case-event-kind { font-weight: 600; }
.case-event-who {
  margin: .15rem 0 0; font-family: var(--font-label); font-size: .64rem;
  text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted);
}
/* What somebody wrote, quoted rather than restated — the same treatment
   .flag-said gives a pupil's own words, and for the same reason: it is the
   record itself and not a description of it. */
.case-note {
  margin: .4rem 0 0; padding: .5rem .7rem; border-radius: var(--radius-md);
  background: var(--surface-sunken); border-inline-start: 3px solid var(--border-strong);
  font-size: .92rem;
}
.case-note-form { display: grid; gap: .3rem; margin: 0 0 1rem; }
/* What has gone to the school's own safeguarding system, on a case and on the
   settings page: one line a send, its state first because that is the question
   somebody opens the panel with. A failure says why under the line, in the
   error text colour and in words, so it is legible without the colour. */
.case-sends { list-style: none; margin: 0 0 1rem; padding: 0; display: grid; gap: .45rem; }
.case-send { font-size: .9rem; }
.case-send-state { font-weight: 600; }
.case-send--failed .case-send-state { color: var(--error-text); }
.case-send-failure { display: block; font-size: .85rem; color: var(--error-text); }
/* A tick box beside its label, on the settings page's form. The label is a
   sentence — a flag's reason, as the case list words it — so it is read as one
   rather than in the form's small capitals, which suit a field's name. */
.check-row { display: flex; align-items: baseline; gap: .5rem; margin: .2rem 0; }
.check-row input[type=checkbox] { flex: none; }
.authoring-form .check-row label {
  font-family: inherit; font-size: .92rem; text-transform: none; letter-spacing: normal;
  color: var(--text); margin-top: 0;
}
/* As the grading form's: the button is not the width of the form above it. */
.integration-form input[type=submit], .case-send-form input[type=submit] { justify-self: start; }
.integration-form fieldset { border: 1px solid var(--border); border-radius: var(--radius-md); }
.case-note-label {
  font-family: var(--font-label); font-size: .68rem; text-transform: uppercase;
  letter-spacing: .08em; color: var(--text-muted);
}
.case-note-form textarea { width: 100%; margin: 0 0 .6rem; font-size: .95rem; }
.case-note-form input[type=submit] { justify-self: start; }
/* The way back down, ruled off from the decisions above it. It is the one
   control on this page that undoes an escalation rather than recording one, and
   a box sitting flush under "handled it myself" reads as a third resolution. */
.case-hand-back {
  margin-top: 1.4rem; padding-top: 1.1rem; border-top: 1px solid var(--border);
}
/* The reference in the history table. It is what somebody quotes on the phone,
   so it is set in the label face rather than the body one — a code read back
   character by character is easier in something that is not prose. */
.flag-ref-link { font-family: var(--font-label); font-size: .8rem; }

/* The pupil masthead was pinned at 52rem under 68rem of content, which a nav
   would have made obvious. It takes the width of what it sits above rather
   than the teacher's 76rem, which is wider than a pupil page. */
.masthead-inner--app { max-width: 68rem; }

/* At phone width three items, the wordmark and the profile will not share a
   row, so the nav takes its own: full-width thirds, each a 44px target, and
   sitting where a thumb already is. */
@media (max-width: 46rem) {
  .masthead-inner--app { flex-wrap: wrap; }
  .masthead-inner--app .app-nav {
    order: 3; flex: 0 0 100%; margin-inline-start: 0; margin-top: .6rem;
    display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0;
    border-top: 1px solid var(--border);
  }
  .masthead-inner--app .app-nav-link {
    text-align: center; padding: .8rem .3rem; border-radius: 0;
  }
  .masthead-inner--app .app-nav-link.active { border-bottom: 2px solid var(--action-bg); }
}

.teacher { max-width: 76rem; margin: 0 auto; padding: 2.5rem var(--clearspace) 4rem; }
.teacher-head { max-width: 46rem; margin-bottom: 2rem; }
.teacher-head .lede { color: var(--text-muted); font-size: .98rem; }
.teacher-head .back {
  display: inline-block; margin-bottom: .5rem;
  font-family: var(--font-label); font-size: .68rem;
  text-transform: uppercase; letter-spacing: .07em; text-decoration: none;
}
.teacher-section { margin-bottom: 2.75rem; }
.teacher-section > h2 { font-size: 1.15rem; margin-bottom: .9rem; }

/* A class heading. The class code leads, because that is what a teacher calls
   it; the subject and size follow as context rather than as the label. */
.group-head { display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap; }
.group-name { font-family: var(--font-label); letter-spacing: .02em; }
.group-meta {
  font-family: var(--font-label); font-size: .66rem; font-weight: 400;
  text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted);
}
/* Classes as cards, so the page a teacher lands on is a short list of things
   they recognise rather than a roll of every name they teach. */
.group-grid {
  display: grid; gap: 1rem; margin-top: 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
}
.group-card {
  display: block; text-decoration: none; color: inherit;
  padding: 1.15rem 1.25rem;
  background: var(--surface); border: 1px solid var(--border);
  border-top: 3px solid var(--border-strong); border-radius: var(--radius-lg);
  transition: box-shadow .12s ease, transform .12s ease;
}
.group-card:hover { box-shadow: var(--shadow-card); transform: translateY(-2px); }
.group-card:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.group-card--attention { border-top-color: var(--error); }
.group-card-head { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.group-card-meta {
  display: block; margin-top: .3rem;
  font-family: var(--font-label); font-size: .66rem; color: var(--text-muted);
}
.group-card-stats { display: flex; gap: 1.5rem; margin-top: 1rem; }
.group-stat-figure {
  display: block; font-family: var(--font-display); font-size: 1.55rem; font-weight: var(--weight-display);
  line-height: 1;
}
.group-stat-label {
  display: block; margin-top: .15rem;
  font-family: var(--font-label); font-size: .6rem;
  text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted);
}
.group-stat--alert .group-stat-figure { color: var(--error-text); }

/* Sortable headings and the search box above a roll. */
.roll-tools { display: flex; align-items: center; gap: .75rem; margin-bottom: .9rem; }
.roll-search {
  width: 100%; max-width: 20rem; padding: .5rem .7rem;
  font: inherit; font-family: var(--font-body); font-size: .9rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text);
}
.roll-count {
  margin: 0; font-family: var(--font-label); font-size: .66rem; color: var(--text-muted);
}
.sort-header {
  display: inline-flex; align-items: center; gap: .3rem;
  text-decoration: none; color: inherit;
}
.sort-header:hover { color: var(--text-link); }
.sort-header.active { color: var(--text-link); }
.sort-arrow { opacity: .45; font-size: .75rem; }
.sort-header.active .sort-arrow { opacity: 1; }

/* Parent app. Plainer than the teacher's: fewer figures, more sentences, and
   nothing comparative. */
.flag-meta-plain { margin: .4rem 0 0; font-size: .88rem; color: var(--text-muted); }

/* Authoring forms. Narrower than a table page: these are read top to bottom,
   and a 70rem-wide text field is harder to read, not easier. */
.teacher--narrow { max-width: 48rem; }
.head-actions { display: flex; align-items: center; gap: .75rem; margin-top: .9rem; flex-wrap: wrap; }
.action-link {
  display: inline-block; padding: .5rem 1rem; border-radius: var(--look-radius-control);
  background: var(--action-bg); color: var(--action-ink); text-decoration: none;
  font-family: var(--font-body); font-size: .88rem; font-weight: 600;
}
.action-link:hover { background: var(--action-bg-hover); }
.quiet-link {
  font-family: var(--font-label); font-size: .82rem; text-decoration: none;
}
.quiet-link:hover { text-decoration: underline; }
.head-actions form.button_to { margin: 0; }

.authoring-form { display: grid; gap: .3rem; margin-bottom: 2rem; }
.authoring-form label {
  font-family: var(--font-label); font-size: .68rem; text-transform: uppercase;
  letter-spacing: .08em; color: var(--text-muted); margin-top: .9rem;
}
.authoring-form select, .authoring-form input[type=number] {
  padding: .55rem .7rem; font: inherit; font-family: var(--font-body); font-size: .92rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text);
}
/* What counts as done, on the set-work form. Stacked and sentence-cased,
   against the grain of .authoring-form label above — these are three statements
   about what a class has to do rather than three field names, and rendered as
   labels they came out inline, uppercase and unreadable as the sentences they
   are. The fieldset groups them for the same reason: they are one decision. */
.asked-for { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: .4rem 1rem 1rem; }
.asked-for legend {
  font-family: var(--font-label); font-size: .68rem; text-transform: uppercase;
  letter-spacing: .08em; color: var(--text-muted); padding: 0 .4rem;
}
.asked-check {
  display: flex; align-items: baseline; gap: .5rem; margin: .55rem 0 0;
  font-family: var(--font-body); font-size: .92rem; text-transform: none;
  letter-spacing: normal; color: var(--text);
}
.asked-check input[type=checkbox] { flex: none; }
/* The note under the last check is about that check, and without this it
   sat hard against it and read as a second line of its label. */
/* A term, one row per week. A list rather than a grid: a half-term is six or
   seven weeks of two or three lessons each, and a calendar grid of that is
   mostly empty cells — the pupil calendar draws a grid because it answers
   "where am I on Tuesday", and this answers "how is the term going". */
.seq-paging { display: flex; gap: 1rem; margin-top: .75rem; }
.seq-weeks { list-style: none; margin: 1.25rem 0 0; padding: 0; display: grid; gap: 1rem; }
.seq-week-head {
  font-family: var(--font-label); font-size: .68rem; text-transform: uppercase;
  letter-spacing: .08em; color: var(--text-muted); margin: 0 0 .4rem;
}
.seq-lessons { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
.seq-lesson {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-md); padding: .75rem 1rem;
}
/* A lesson still to teach is drawn quieter than one that has happened. It is a
   plan rather than a record, and the difference should be legible before any
   of the words are read. */
.seq-lesson--ahead { background: none; border-style: dashed; }
.seq-lesson-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: .5rem; }
.seq-title { font-weight: 600; }
.seq-lesson p { margin: .3rem 0 0; }
.seq-ground { margin: .3rem 0 0; padding-left: 1.1rem; }
.seq-empty { margin: 0; color: var(--text-muted); font-size: .88rem; }

/* What a class's answers showed. Set above the roll and reading as prose
   rather than as another table: these are findings about the material, and a
   fifth grid of numbers on this screen would be read as a sixth column of the
   one below it. */
.panel--read .read-subhead {
  font-family: var(--font-label); font-size: .68rem; text-transform: uppercase;
  letter-spacing: .08em; color: var(--text-muted); margin: 1.1rem 0 .3rem;
}
.read-ideas { margin: 0; padding-left: 1.1rem; }
.read-ideas li { margin: .3rem 0; }
.read-items { margin: 0; padding-left: 1.2rem; }
.read-items > li { margin: .7rem 0; }
.read-stem { margin: 0; }
.read-choices { list-style: none; margin: .3rem 0 0; padding: 0; }
.read-choices li {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: .45rem; margin: .2rem 0;
}
/* The number leads, because how many chose it is the finding and the wording
   of the option is what explains it. */
.read-count {
  font-family: var(--font-label); font-weight: 600; color: var(--text);
  min-width: 1.4rem; text-align: right;
}
.read-choice-text { color: var(--text); }

/* A family's messages. Each one rendered whole rather than as a subject line
   to click into — they are one or two sentences from a fixed list, so a page
   per message would be a click to read what the list already showed. */
.messages { list-style: none; margin: 0; padding: 0; display: grid; gap: .75rem; }
.message {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-md); padding: 1rem 1.15rem;
}
/* Unread is a marked edge rather than a different background: the list is read
   top to bottom either way, and a colour block per unread message turns an
   inbox with a term in it into stripes. */
.message--unread { border-left: 3px solid var(--action-bg); }
.message-title { margin: 0; font-size: 1rem; }
.message-new {
  font-family: var(--font-label); font-size: .62rem; text-transform: uppercase;
  letter-spacing: .05em; color: var(--text-link); margin-left: .4rem;
}
.message-body { margin: .4rem 0 0; }
.message-meta { margin: .5rem 0 0; font-size: .82rem; color: var(--text-muted); }

/* The shortcuts under "Wanted by". A row of two-line buttons: what it means on
   top, the date it works out as underneath — without the date, two of them a
   week apart read as the same button. They wrap rather than scroll, because
   there are at most five and a teacher should see all of them at once. */
.due-date { display: grid; gap: .5rem; }
.due-date-options {
  display: flex; flex-wrap: wrap; gap: .4rem; margin: 0; padding: 0; list-style: none;
}
.due-date-option {
  display: grid; gap: .1rem; text-align: left; cursor: pointer;
  padding: .4rem .7rem; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: none; color: var(--text-muted); font-family: var(--font-label);
}
.due-date-option:hover { border-color: var(--border-strong); color: var(--text-link); }
.due-date-option:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* Pressed, not selected: the field beside it holds the answer, and this only
   says which shortcut happens to match what it currently holds. */
.due-date-option[aria-pressed="true"] {
  border-color: var(--action-bg); color: var(--text-link); background: var(--surface);
}
.due-date-when { font-size: .82rem; }
.due-date-on { font-size: .72rem; color: var(--text-muted); }

/* Who has not done it, and what is still wanted from each. A list rather than
   a sentence: the names are what a teacher acts on one at a time. */
.outstanding-list { margin: .35rem 0 0; padding-left: 1.1rem; }
/* A row per name: the reason and the button sit on the line with it rather
   than pushing it about. form.button_to carries a right margin for the runs of
   buttons it sits in elsewhere, and here the gap does that job. */
.outstanding-list li {
  display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; margin: .35rem 0;
}
.outstanding-list form.button_to { margin: 0; }
/* Inline with the name — .row-ref is a block elsewhere, and stacked under
   each name this read as a caption rather than as the other half of the
   sentence it is. */
.outstanding-list .row-ref { display: inline; }

.asked-for .panel-help { margin: .35rem 0 0 1.55rem; }

.field-row { display: grid; gap: .75rem; grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr)); }
.field-row label { margin-top: .9rem; }
.form-heading { margin-top: 2rem; font-size: 1.05rem; }
.form-actions { display: flex; align-items: center; gap: .75rem; margin-top: 1.5rem; flex-wrap: wrap; }
.form-actions form.button_to { margin: 0; margin-left: auto; }
.form-errors { max-width: none; margin: 0 0 1rem; }
.form-errors ul { margin: .4rem 0 0; padding-left: 1.2rem; }
/* Focus lands on the sentence after a refused save (components/FormErrors.tsx),
   so it shows a ring like anything else that holds focus. */
.form-errors:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* A field's own messages, under the field, and the field marked to match. */
.field-error { margin: .3rem 0 0; color: var(--error-text); font-size: .86rem; font-weight: 600; }
.field-error > span { display: block; }
.authoring-form [aria-invalid="true"] { border-color: var(--error); }

.option-fields {
  display: grid; gap: .5rem; align-items: center;
  grid-template-columns: minmax(0, 2fr) auto minmax(0, 1.4fr) auto;
  padding: .5rem 0; border-bottom: 1px solid var(--border);
}
.option-correct, .option-destroy {
  display: flex; align-items: center; gap: .35rem; margin: 0;
  font-family: var(--font-label); font-size: .62rem;
  text-transform: uppercase; letter-spacing: .05em; color: var(--text-muted);
}
@media (max-width: 52rem) { .option-fields { grid-template-columns: minmax(0, 1fr); } }

/* Asking Developing Experts to change a library entry, from inside a row that
   is already about something else — a misconception card, a flagged idea, a
   class's reading of one lesson. Closed by default and quiet, because it is
   never what the teacher came to the screen to do; the same shape as
   .guardian-suspend, on the same argument that a second visual language for
   "a secondary act inside a row" would suggest a difference that is not there.
   Reuses .authoring-form outright rather than restyling its fields: it is the
   same kind of form as the one on the lesson editor, and only its margins
   differ inside a disclosure. */
.propose-change { margin-top: .75rem; }
.propose-change summary {
  cursor: pointer; font-family: var(--font-label); font-size: .68rem;
  text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted);
}
.propose-change .authoring-form { max-width: 34rem; margin: .6rem 0 0; }
.propose-change .setting-help { margin: .35rem 0 0; }
.propose-change .form-actions { margin-top: 1rem; }

.question-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 1rem 1.15rem; margin-bottom: .85rem;
}
.question-card-head { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.question-card-head .spacer { flex: 1; }
.question-stem { margin: .6rem 0 .5rem; font-size: .98rem; }
.option-list { list-style: none; padding: 0; margin: 0; display: grid; gap: .3rem; }
.option-list li {
  display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap;
  font-size: .9rem; padding: .3rem .5rem; border-radius: var(--radius-sm);
}
.option-row--correct { background: var(--success-surface); }
.option-mark { font-family: var(--font-label); color: var(--text-muted); }
.option-reveals {
  font-family: var(--font-label); font-size: .62rem;
  text-transform: uppercase; letter-spacing: .05em; color: var(--text-link);
}
.option-reveals--none { color: var(--text-muted); }

.code-area, .code-block {
  font-family: var(--font-label); font-size: .78rem; line-height: 1.5;
}
.code-block {
  background: var(--surface-sunken); border: 1px solid var(--border);
  border-radius: var(--radius-md); padding: .9rem 1rem; overflow-x: auto; white-space: pre;
}
.danger-zone { border-top: 1px solid var(--border); padding-top: 1.5rem; }
.cell-actions { display: flex; align-items: center; gap: .6rem; white-space: nowrap; }
.cell-actions form.button_to { margin: 0; }

/* Two panes, each scrolling on its own. Thirty pupils in one long table would
   push the lessons off the bottom of the screen, and the two questions — who is
   stuck, and what on — are asked together. Fixed height so the page itself does
   not grow with the class. */
.pane-row {
  display: grid; gap: 1.25rem;
  grid-template-columns: repeat(auto-fit, minmax(min(26rem, 100%), 1fr));
}
.pane {
  display: flex; flex-direction: column; min-width: 0;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 1.1rem 1.2rem 1.2rem;
}
.pane-head { display: flex; align-items: center; gap: .6rem; margin-bottom: .75rem; }
.pane-head h2 { margin: 0; font-size: 1.05rem; }
.pane-scroll {
  overflow: auto; max-height: 26rem;
  border: 1px solid var(--border); border-radius: var(--radius-md);
}
/* The header stays put while the body scrolls, so a column is still labelled
   forty rows down. */
.pane-scroll .teacher-table { border: 0; border-radius: 0; }
.pane-scroll thead th { position: sticky; top: 0; z-index: 1; }
@media (max-width: 60rem) { .pane-scroll { max-height: 20rem; } }

/* Mid-sitting. Aqua and a dot, matching "in progress" on the pupil side, so
   the same fact looks the same to both people looking at it. */
.live {
  display: inline-flex; align-items: center; gap: .3rem;
  font-family: var(--font-label); font-size: .62rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted);
}
.live-dot {
  width: .45rem; height: .45rem; border-radius: 50%; background: var(--accent);
}
.live-lesson {
  display: block; margin-top: .15rem;
  font-family: var(--font-label); font-size: .62rem; color: var(--text-muted);
}
.live-banner {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  margin: .5rem 0 0; padding: .5rem .8rem;
  background: var(--tint-accent-fill); border-radius: var(--radius-md);
  font-size: .88rem;
}
.mini-head {
  margin: .8rem 0 .3rem;
  font-family: var(--font-label); font-size: .6rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted);
}
.mini-head:first-of-type { margin-top: 0; }

.row-note {
  display: block; margin-top: .15rem;
  font-family: var(--font-label); font-size: .6rem;
  text-transform: uppercase; letter-spacing: .05em; color: var(--text-muted);
}

/* Panels ---------------------------------------------------------------- */

.panel {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 1.25rem 1.35rem; margin-bottom: 1.25rem;
}
/* Error red, and the only place on the teacher side that uses it: this is the
   one panel that means a person has to do something. */
.panel--attention { border-left: 4px solid var(--error); }
/* Same weight as attention rather than louder. A pupil swearing needs a person
   to look, not a klaxon — and the panel sits above the other one, which is the
   ordering this screen uses to say what comes first. */
.panel--safety { border-left: 4px solid var(--error); }
/* Folded (components/Safeguarding.tsx, SafeguardingFold). The line says how
   many and how long and names nobody; the panel opens on a press. One summary,
   one target: the whole line is the button, as a case row is the whole link. */
.sg-fold-line { display: flex; align-items: center; gap: .95rem; }
.sg-fold > summary { list-style: none; cursor: pointer; }
.sg-fold > summary::-webkit-details-marker { display: none; }
.sg-fold > summary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 4px; border-radius: var(--radius-sm); }
.sg-fold-mark {
  flex: none; width: 2.3rem; height: 2.3rem; border-radius: var(--radius-pill);
  display: grid; place-items: center; background: var(--error-surface); color: var(--error-text);
}
.sg-fold-mark .icon { width: 1.15rem; height: 1.15rem; }
.sg-fold[open] .sg-fold-shut, .sg-fold:not([open]) .sg-fold-open { display: none; }
.sg-fold-text { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.sg-fold-head { display: flex; flex-wrap: wrap; align-items: center; gap: .45rem; }
.sg-fold-head :is(h2, h3, h4) { margin: 0; }
.sg-fold-head h2 { font-size: 1.05rem; }
.sg-fold-summary { color: var(--text-muted); font-size: .88rem; }
.sg-fold-toggle {
  flex: none; margin-inline-start: auto; padding: .4rem .95rem;
  border: 1px solid var(--border); border-radius: var(--look-radius-control);
  font-family: var(--font-label); font-size: .85rem; font-weight: 600; color: var(--text-link);
}
.sg-fold > summary:hover .sg-fold-toggle { background: var(--surface-sunken); }
.sg-fold[open] .sg-fold-show, .sg-fold:not([open]) .sg-fold-hide { display: none; }
.sg-fold-body { margin-top: 1rem; }
/* Locked (the safeguarding lock, lib/safeguarding-lock.server.ts): the line and
   an Unlock link, nothing behind it — the API sent nothing to put there. The
   link is the panel's one press, so it looks like one. */
.sg-fold-toggle.sg-fold-unlock {
  border-color: var(--action-bg); background: var(--action-bg); color: var(--action-ink);
  text-decoration: none;
}
.sg-fold-toggle.sg-fold-unlock:hover { background: var(--action-bg-hover); }
.sg-fold-toggle.sg-fold-unlock:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* Unlocked: when it locks again, and Lock now, above what it opened. */
.sg-lock-now { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; margin: 0 0 .9rem; }
.sg-unlocked {
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .2rem .65rem; border-radius: var(--radius-pill);
  background: var(--success-surface); color: var(--success-text);
  font-family: var(--font-label); font-size: .74rem; font-weight: 600;
}
.sg-unlocked .icon, .sg-lock-now button .icon { width: .9rem; height: .9rem; }
.sg-lock-now button { display: inline-flex; align-items: center; gap: .4rem; padding: .35rem .8rem; }
/* What a transcript holds back while locked (6A, 6C): that a concern's words
   were here, and the way to read them, never whose. */
.sg-withheld {
  display: inline-flex; flex-wrap: wrap; align-items: center; gap: .45rem; margin: 0;
  color: var(--text-muted); font-size: .86rem; font-style: normal;
}
.sg-withheld .icon { width: .95rem; height: .95rem; color: var(--error-text); }
.ch-msg--withheld .fu-av .icon { width: .85rem; height: .85rem; color: var(--error-text); }
.ch-msg--withheld .sg-withheld {
  padding: .45rem .7rem; border: 1px dashed var(--border); border-radius: var(--radius-md);
}

/* The unlock screen (routes/teacher-unlock.tsx; 2A, 2B, 2E): a card, narrow,
   the form and nothing else. */
.panel.unlock { max-width: 30rem; margin: 2rem auto; padding: 1.6rem 1.7rem 1.2rem; border-radius: var(--radius-xl); }
.unlock-head { display: flex; gap: .9rem; align-items: flex-start; margin-bottom: 1.2rem; }
.unlock-head h1 { margin: 0; font-size: 1.3rem; }
.unlock-lede { margin: .3rem 0 0; color: var(--text-muted); }
.unlock-mark--stop { background: var(--error); color: var(--text-inverse); }
.unlock-alert {
  margin: 0 0 1rem; padding: .55rem .75rem; border-radius: var(--radius-md);
  background: var(--error-surface); color: var(--error-text); font-size: .9rem;
}
.unlock-field { display: flex; flex-direction: column; gap: .35rem; margin: 0 0 1rem; }
.unlock-field label { font-weight: 600; font-size: .9rem; }
.unlock-field input { font-size: 1rem; padding: .55rem .7rem; }
.unlock-code { font-family: var(--font-mono); font-size: 1.4rem; letter-spacing: .3em; max-width: 12rem; }
.unlock-subhead { margin: 0; font-size: 1.05rem; }
/* The passkey press, first (2A): one wide button, the device named under it. */
.unlock-passkey { margin: 0 0 .4rem; }
.unlock-passkey-button {
  width: 100%; display: flex; align-items: center; gap: .9rem; text-align: start; padding: .9rem 1.1rem;
}
.unlock-passkey-button .icon { width: 1.6rem; height: 1.6rem; flex: none; }
.unlock-passkey-button span { display: flex; flex-direction: column; }
.unlock-passkey-button small { font-weight: 400; font-size: .8rem; }
.unlock-or { display: flex; align-items: center; gap: .7rem; margin: .9rem 0; color: var(--text-muted); font-size: .78rem; }
.unlock-or::before, .unlock-or::after { content: ""; flex: 1; border-top: 1px solid var(--border); }
.unlock-form > .unlock-lede { margin: .25rem 0 1rem; }
.unlock-note { margin: 0 0 1rem; font-size: .84rem; color: var(--text-muted); }
/* The lead's list of PIN holders (routes/teacher-safeguarding.tsx). */
.pin-holders { list-style: none; margin: 0; padding: 0; display: grid; gap: .4rem; }
.pin-holder {
  display: flex; align-items: center; gap: .8rem; padding: .5rem .7rem;
  border: 1px solid var(--border); border-radius: var(--radius-md);
}
.pin-holder form { margin-inline-start: auto; }
.unlock-actions { display: flex; justify-content: flex-end; align-items: center; gap: .9rem; margin: 0; }
.unlock-foot {
  margin: 1.1rem 0 0; padding-top: .8rem; border-top: 1px solid var(--border);
  font-size: .8rem; color: var(--text-muted);
}
/* What the pupil actually typed, on their own page and nowhere else. Set as a
   quotation because that is what it is: their words, reproduced, not the
   product's account of them. */
.flag-said-by {
  margin: .55rem 0 0; font-family: var(--font-label); font-size: .66rem;
  text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted);
}
.flag-said {
  margin: .45rem 0 0; padding: .5rem .7rem;
  border-left: 2px solid var(--border); background: var(--surface-sunken);
  font-size: .92rem; color: var(--text);
}
/* Worth a look rather than needing a person: amber, and the one panel on the
   teacher side that uses it. A pasted answer is a conversation about
   somebody's work — see ClipboardEvent — and painting it the safety colour
   would put it in the same register as a child in trouble. */
.panel--clipboard { border-left: 4px solid var(--warning); }
.panel--win { border-left: 4px solid var(--success); }
.panel-head { display: flex; align-items: center; gap: .7rem; margin-bottom: .5rem; }
.panel-head h2 { margin: 0; font-size: 1.05rem; }
.panel-help { color: var(--text-muted); font-size: .88rem; margin: 0 0 .9rem; max-width: 48rem; }
.panel-empty { color: var(--text-muted); font-size: .9rem; margin: 0; }
.panel-link {
  display: inline-block; margin-top: .4rem;
  font-family: var(--font-label); font-size: .68rem;
  text-transform: uppercase; letter-spacing: .07em;
}
.count {
  display: inline-grid; place-items: center; min-width: 1.6rem; height: 1.6rem;
  padding: 0 .4rem; border-radius: var(--radius-pill);
  background: var(--surface-sunken); color: var(--text-muted);
  font-family: var(--font-label); font-size: .74rem; font-weight: 600;
}
.count--alert { background: var(--error-surface); color: var(--error-text); }
.count--calm { background: var(--success-surface); color: var(--success-text); }
.count--note { background: var(--warning-surface); color: var(--warning-text); }

.panel-row { display: grid; gap: 1.25rem; grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr)); }
.panel-row .panel { margin-bottom: 0; }

.flag-list { list-style: none; padding: 0; margin: 0; display: grid; gap: .9rem; }
.flag-list > li { padding: .75rem .9rem; background: var(--surface-sunken); border-radius: var(--radius-md); }
.flag-list a { text-decoration: none; }
.flag-ref, .row-ref {
  display: inline-block; margin-left: .4rem;
  font-family: var(--font-label); font-size: .64rem; color: var(--text-muted);
}
.row-ref { display: block; margin: .1rem 0 0; }
.flag-idea { margin: .3rem 0 0; font-size: .92rem; }
.flag-meta {
  margin: .25rem 0 0; font-family: var(--font-label); font-size: .64rem;
  text-transform: uppercase; letter-spacing: .05em; color: var(--text-muted);
}
.flag-model { margin: .5rem 0 0; font-size: .88rem; color: var(--text-muted); }

/* One probe response on the pupil report — the question, the pupil's words, and
   where it happened. The pupil's own sentence is the thing to read, so it is
   the only one of the three at full weight; the question above it is set in the
   display face because it is a question, and the placing line under it is the
   quietest. All three in sentence case: the small-caps .flag-meta beside them
   carries codes and counts, and a whole authored statement set that way is a
   paragraph of shouting. */
.thought-asked {
  margin: 0; font-family: var(--font-display); font-size: .92rem; color: var(--text-muted);
}
/* The picture the pupil saw under a question back, small, between the question
   and what they said: the teacher reads the turn as it was. */
.thought-picture {
  display: block; margin: .4rem 0 0; max-height: 7rem; max-width: 100%; height: auto;
  border-radius: var(--radius-sm); border: 1px solid var(--border); background: var(--figure-plate);
}
.thought-said { margin: .35rem 0 0; font-size: 1rem; color: var(--text); }
.thought-where { margin: .35rem 0 0; font-size: .8rem; color: var(--text-muted); }

.mini-list { list-style: none; padding: 0; margin: 0 0 .5rem; display: grid; gap: .35rem; }
.mini-list > li { display: flex; justify-content: space-between; gap: .75rem; font-size: .9rem; }
.mini-meta { font-family: var(--font-label); font-size: .68rem; color: var(--text-muted); text-align: right; }

/* Headline figures ------------------------------------------------------- */

.stat-row {
  display: grid; gap: 1rem; margin-bottom: 2.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr));
}
.stat {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 1.1rem 1.2rem;
}
.stat-figure { display: block; font-family: var(--font-display); font-size: 2.1rem; font-weight: var(--weight-display); line-height: 1; }
.stat-label { display: block; margin-top: .35rem; font-size: .92rem; }
.stat-note {
  display: block; margin-top: .2rem;
  font-family: var(--font-label); font-size: .64rem; color: var(--text-muted);
}

/* Tables ----------------------------------------------------------------- */

.teacher-table {
  width: 100%; border-collapse: collapse; background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden;
}
.teacher-table th {
  text-align: left; padding: .6rem .85rem;
  background: var(--surface-sunken); border-bottom: 1px solid var(--border);
  font-family: var(--font-label); font-size: .62rem;
  text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted);
}
.teacher-table td {
  padding: .75rem .85rem; border-bottom: 1px solid var(--border);
  vertical-align: top; width: auto; font-weight: normal;
}
.teacher-table tr:last-child td { border-bottom: 0; }
.teacher-table .row--attention { background: var(--error-surface); }
.cell-quiet { font-family: var(--font-label); font-size: .72rem; color: var(--text-muted); }
.lesson-idea { margin: 0; font-size: .88rem; }

/* The spread of a class across one idea. A shape, because "how is this class
   spread" is a shape question — the numbers are underneath it in words. */
.spread { display: flex; gap: 2px; height: 8px; border-radius: var(--radius-pill); overflow: hidden; min-width: 9rem; }
.spread-part--secure { background: var(--success); }
.spread-part--working { background: var(--accent); }
.spread-part--fragile { background: var(--warning); }
.spread-part--none { background: var(--border); }
.spread-key {
  margin: .35rem 0 0; font-family: var(--font-label); font-size: .64rem; color: var(--text-muted);
}

.mastery {
  display: inline-block; padding: .2rem .55rem; border-radius: var(--radius-pill);
  font-family: var(--font-label); font-size: .78rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em;
  color: var(--text-muted); background: var(--surface-sunken);
}
.mastery--secure, .mastery--retained { color: var(--success-text); background: var(--success-surface); }
.mastery--fragile { color: var(--warning-text); background: var(--warning-surface); }
.mastery--misconception { color: var(--text-link); background: var(--tint-accent-fill); }

/* Sign-in role chooser ---------------------------------------------------- */

.role-choice { display: grid; gap: .75rem; margin-top: 1.75rem; text-align: left; }
.role-card {
  display: grid; grid-template-columns: auto 1fr; column-gap: .9rem; row-gap: .1rem;
  align-items: center; text-decoration: none; color: inherit;
  padding: 1rem 1.1rem;
  background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--radius-lg);
  transition: border-color .12s ease, transform .12s ease;
}
.role-card:hover { border-color: var(--border-strong); transform: translateY(-1px); }
.role-card:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.role-icon { width: 1.75rem; height: 1.75rem; grid-row: span 2; color: var(--text-link); }
.role-name { font-family: var(--font-display); font-weight: var(--weight-display); font-size: 1.12rem; }
.role-detail { font-size: .85rem; color: var(--text-muted); }
.auth-switch { margin-top: 1.25rem; font-size: .85rem; }

/* The recap builder. A numbered list rather than cards: the order is the
   content here, and a grid would let it wrap into something that no longer
   reads top to bottom. */
.recap-status { display: flex; align-items: baseline; gap: .75rem; flex-wrap: wrap; }
.recap-status .setting-help { margin: 0; }
.step-list { list-style: none; margin: 1rem 0 1.5rem; padding: 0; display: grid; gap: .6rem; }
.step-row {
  border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: .8rem 1rem; background: var(--surface);
}
.step-head { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.step-head .spacer { flex: 1 1 auto; }
.step-body { margin: .5rem 0 0; color: var(--text-muted); font-size: .93rem; }
.step-question { margin: .5rem 0 0; font-size: .93rem; }
.step-retired { margin: .35rem 0 0; font-size: .9rem; color: var(--text-muted); }

/* --- Tables and status badges -------------------------------------------
   These began on the ops page, which is gone; the curriculum screens use them. */

table {
  width: 100%; border-collapse: collapse; background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden;
}
td { text-align: left; padding: .65rem .85rem; border-bottom: 1px solid var(--border); vertical-align: top; }
tr:last-child td { border-bottom: 0; }
td:first-child { width: 12rem; font-weight: 600; }

.status {
  display: inline-block; padding: .15rem .55rem; border-radius: var(--radius-pill);
  font-family: var(--font-label); font-size: .62rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .05em;
}
.status--ok, .status--approved { color: var(--success-text); background: var(--success-surface); }
.status--down { color: var(--error-text); background: var(--error-surface); }
.status--submitted { color: var(--warning-text); background: var(--warning-surface); }
.status--draft { color: var(--text-muted); background: var(--tint-brand-fill-soft); }

footer { border-top: 1px solid var(--border); padding-top: 1rem; font-size: .82rem; color: var(--text-muted); }

@media (max-width: 34rem) {
  :root { --clearspace: 1.1rem; }
  h1 { font-size: 1.45rem; }
  .confidence button { flex: 1 1 100%; }
  .masthead img { width: 44px; height: 44px; }
}

/* --- Language picker -------------------------------------------------------
   Appended at the end of the file deliberately: this landed alongside another
   branch editing the settings and sign-in rules above, and additions at
   opposite ends of a file merge cleanly where adjacent edits do not.

   Narrower cards than .theme-grid because the content is two short lines rather
   than a description and a preview, and a four-column row of names scans faster
   than a two-column one when you are looking for your own language. */
.language-grid {
  display: grid; gap: .75rem; max-width: 42rem;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media (max-width: 40rem) { .language-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 26rem) { .language-grid { grid-template-columns: minmax(0, 1fr); } }

.language-option {
  position: relative; cursor: pointer;
  display: flex; flex-direction: column; gap: .1rem;
  padding: .8rem .9rem;
  background: var(--surface); border: calc(var(--look-border) * 1.5) solid var(--border);
  border-radius: calc(var(--look-radius) + 2px);
  transition: border-color .12s ease;
}
.language-option:hover { border-color: var(--border-strong); }
.language-option:has(.option-input:focus-visible) {
  outline: 2px solid var(--focus-ring); outline-offset: 2px;
}
/* Two different states, and the card needs both.

   `--current` is what is *saved*. `:checked` is what is *selected right now*.
   Only the first was styled, and because the radio is visually hidden — the
   whole card is the affordance — clicking a language changed nothing on screen
   at all. The control read as broken until you pressed Save and the page came
   back. The answer options already solve this exactly this way. */
.language-option--current,
.language-option:has(.option-input:checked) {
  border-color: var(--selected);
  background: var(--selected-surface);
}
.language-option:has(.option-input:checked) .language-endonym { font-weight: 700; }

/* The endonym leads: a pupil is scanning for the word in their own language.
   Sized up a little because several of these are scripts the reader may be
   picking out by shape, and Devanagari and Arabic need the extra leading.

   ⚠️ FONT COVERAGE IS NOT SETTLED. Figtree is bundled here in Latin and Latin
   Extended only, and has no glyphs for Gurmukhi, Arabic or Devanagari at all,
   so the Noto faces are named first and Figtree is the fallback for the Latin
   endonyms only. Named
   is not the same as present: these are system fonts here, not served by us,
   and a device without them shows tofu boxes rather than a language. That is
   exactly what a headless container does, which is how it was found.

   School Chromebooks and iPads generally do ship Noto, so this is likely fine
   in production — but "likely" is not a basis for shipping a language picker
   whose entire job is to be readable. Self-hosting the subsets belongs with the
   RTL and typography work; until that lands, verify on a real school device
   before adding any non-Latin language to Language::ALL. */
.language-endonym {
  display: block; font-weight: 600; font-size: 1.05rem; line-height: 1.5;
  font-family:
    "Noto Sans", "Noto Sans Gurmukhi", "Noto Sans Arabic", "Noto Nastaliq Urdu",
    "Noto Sans Devanagari", "Noto Sans Bengali", var(--font-body);
}
.language-label { display: block; font-size: .8rem; color: var(--text-muted); }

/* --- Question translation --------------------------------------------------
   Shown only once the pupil asks. English stays where it is and the panel
   arrives underneath it, so the eye keeps its place on the question rather than
   having the layout rearranged around it. */
.translate-button {
  display: inline-flex; align-items: center; gap: .45rem;
  margin-top: .9rem; padding: .45rem .85rem;
  background: var(--surface); color: var(--text);
  border: 1.5px solid var(--border-strong); border-radius: var(--radius-pill);
  font-family: var(--font-body); font-weight: 600; font-size: .92rem;
  cursor: pointer;
}
.translate-button:hover { border-color: var(--selected); }
.translate-button:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

.translation {
  margin-top: 1rem; padding: .9rem 1rem 1rem;
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  /* A leading edge rather than a full border: the panel is an aid attached to
     the English above it, not a second question of equal standing. */
  border-inline-start: 3px solid var(--selected);
  border-radius: var(--radius-md);
}
.translation-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: .75rem; margin-bottom: .5rem;
}
.translation-label {
  font-family: var(--font-label); font-size: .82rem; font-weight: 600;
  color: var(--text-muted);
}
.translation-hide { margin: 0; font-size: .8rem; }
.translation-stem { margin: 0; font-size: 1.05rem; line-height: 1.6; }
.translation-hints { margin: .6rem 0 0; padding-inline-start: 1.1rem; font-size: .92rem; }
.translation-waiting { margin: 0; color: var(--text-muted); font-size: .92rem; }

/* Secondary line under the English option, never a replacement — the radio
   still posts the English option's id. */
.option-translation {
  display: block; margin-top: .2rem;
  font-size: .88rem; color: var(--text-muted);
}
/* One is rendered under every option before there is anything to put in it, so
   a translation arriving while the pupil waits has somewhere to land. Empty, it
   must take up no room at all — the margin above would otherwise loosen every
   option on the page of a pupil who has never asked for a translation. */
.option-translation:empty { display: none; }

/* Non-Latin scripts need more leading than the Latin type scale allows. */
.translation-body, .option-translation {
  font-family:
    "Noto Sans", "Noto Sans Arabic", "Noto Nastaliq Urdu", "Noto Sans Devanagari",
    "Noto Sans Bengali", var(--font-body);
  line-height: 1.7;
}

/* --- Translation review ----------------------------------------------------
   English and the candidate side by side, because the judgement being asked for
   is a comparison. Stacked on a narrow screen rather than shrunk: two columns
   of prose at half width is worse than one after the other. */
.review-row {
  display: grid; gap: 1rem; margin-bottom: 1.25rem;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: start;
}
@media (max-width: 52rem) { .review-row { grid-template-columns: minmax(0, 1fr); gap: .4rem; } }

.review-label {
  display: block; margin-bottom: .3rem;
  font-family: var(--font-label); font-size: .64rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted);
}
/* The English is reference, not an input — it reads as a quotation rather than
   as something else to fill in. */
.review-english p {
  margin: 0; padding: .55rem .7rem;
  background: var(--surface-sunken); border-inline-start: 3px solid var(--border-strong);
  border-radius: var(--radius-sm); font-size: .95rem; line-height: 1.5;
}
.review-candidate textarea { width: 100%; margin: 0; font-size: .95rem; }
.translation-review .form-heading { margin-top: 2rem; }

/* --- The teach card ------------------------------------------------------
   What the pupil reads before the check, above the question it is about.

   Every word in it is Developing Experts' own, looked up from the escalation
   table — see Practice::TeachCard. It is set apart from the question rather
   than styled as part of it, because a pupil has to be able to tell what they
   are being told from what they are being asked.

   Deliberately NOT coloured by which column of the table it came from. A card
   that got redder as a pupil escalated would tell them how badly it is going,
   on the screen where that is least useful to know. */

.teach {
  margin-bottom: 1.75rem; padding: 1.1rem 1.2rem;
  background: var(--surface-sunken); border-radius: var(--look-radius);
  border-left: 3px solid var(--accent);
}
.teach-heading {
  margin: 0 0 .6rem; font-family: var(--font-label); font-size: .82rem;
  color: var(--text-muted);
}
.teach-part { margin: 0 0 .6rem; }
.teach-part:last-of-type { margin-bottom: 0; }
/* A worked example is something to look at rather than another sentence
   addressed to the pupil, so it is set as a quotation — and captioned, because
   read aloud it otherwise lands as a second question. */
.teach-example { margin: .8rem 0 0; }
.teach-example figcaption {
  font-family: var(--font-label); font-size: .82rem;
  color: var(--text-muted); margin-bottom: .3rem;
}
.teach-example blockquote {
  margin: 0; padding: .7rem 0 .7rem 1rem;
  border-left: 2px solid var(--border-strong); color: var(--text);
  font-style: italic;
}
.teach-resource { margin: .8rem 0 0; font-size: .93rem; }
.teach-example-answer { margin: .55rem 0 0; padding-left: 1rem; }
.teach-example-label {
  font-family: var(--font-label); font-size: .82rem; font-weight: 600;
  color: var(--text-muted); margin-inline-end: .4rem;
}

/* THE COMPACT NOTE ABOVE THE QUESTION. What used to be the whole teach card.
   Three lines instead of a hundred and ten words, because the card itself is
   read on the feedback screen before this one — see practice/show.

   Quieter than the question below it in every way that can be measured: no
   background, no heading, smaller type. It is a reminder, and a reminder that
   competes with the question for attention is the thing being fixed. */
/* The reminder, and it sits BELOW the answer now — see practice/show, where the
   question used to start 428px down the page behind eight other things.

   Which is why it carries a top margin it never needed before: following the
   confidence buttons with only the flow gap between them, it read as a fourth
   thing in that row rather than as a card the pupil may drop into.

   Logical properties, because the bar has to be on the reading edge and this
   app renders right-to-left for Arabic and Urdu. */
.coach-line { margin: 0 0 .6rem; color: var(--text); }

/* Marks, not a bar and not a number. Columns of the teaching table, filling in
   the direction of more help — see practice/_support_level for the argument,
   and note that nothing here reddens as it fills. */
.coach-level { display: flex; align-items: center; gap: .45rem; margin: 0 0 .35rem; }
.coach-marks { display: inline-flex; gap: .2rem; }
.coach-mark {
  width: .95rem; height: .2rem; border-radius: 2px; background: var(--border);
}
.coach-mark--on { background: var(--accent); }
.coach-level-label {
  font-family: var(--font-label); font-size: .82rem; font-weight: 600;
  color: var(--text-muted);
}

.coach-more { margin-top: .5rem; }
.coach-more > summary {
  display: inline-block; cursor: pointer; list-style: none;
  font-family: var(--font-label); font-size: .82rem; font-weight: 600;
  color: var(--text-link);
  padding: .3rem 0;
}
.coach-more > summary::-webkit-details-marker { display: none; }
/* ⚠️ THE ARROW IS DRAWN, NOT WRITTEN, and the announcement baseline is what
   caught that. As `content: "\25B8"` it was a character in the accessibility
   tree: the transcript read `text "▾"` and the disclosure announced itself as
   "▾GO OVER IT AGAIN". Generated content with a glyph in it is text as far as
   a screen reader is concerned. An empty string is not, so the shape is a
   border triangle instead and the control is announced by its words alone. */
.coach-more > summary::before {
  content: ""; display: inline-block; margin-right: .4rem;
  border-style: solid; border-width: .28rem 0 .28rem .4rem;
  border-color: transparent transparent transparent currentColor;
  transition: transform .12s ease;
}
.coach-more[open] > summary::before { transform: rotate(90deg); }
.coach-more > summary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* The card inside the disclosure keeps its own inset, so it does not sit
   flush against the rule the note already draws. */
.coach-more .teach { margin: .5rem 0 .2rem; }


/* --- Calendar ------------------------------------------------------------

   A RECORD, not a ledger. Nothing below draws a completion state: a month grid
   marked not-done thirty cells at a time is the wall of failure this product
   refuses everywhere else, and it would land hardest on the pupil furthest
   behind. The single exception is .cal-cell--open, which is Honey — the same
   accent the subject pages use for a sitting left running, and an invitation
   rather than a mark.

   MONDAY TO FRIDAY, five columns. Nothing is taught at a weekend, and dropping
   the two columns buys back a fifth of the width — which at tablet sizes is
   the difference between a readable grid and an unreadable one.

   LOGICAL PROPERTIES THROUGHOUT. Arabic and Urdu are RTL and the layout has to
   flip with them; one `border-left` here and Monday lands on the wrong side of
   the week for two of the five languages we ship. */

.calendar { max-width: 68rem; margin: 0 auto; padding: 2.5rem var(--clearspace) 4rem; }
.cal-eyebrow {
  font-family: var(--font-label); font-size: .82rem; font-weight: 600;
  color: var(--text-muted);
}

/* Filters. Subjects are a dropdown and not a row of chips — see the note in
   _filters: a chip per subject grows the bar with the curriculum, and a bar
   that grows stops fitting on a phone first. */
.cal-filters {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  margin-top: 1.75rem; padding: .75rem .85rem;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
}
.cal-search { display: flex; align-items: center; gap: .5rem; flex: 1 1 18rem; min-width: 0; }
.cal-search input[type=search] {
  flex: 1; min-width: 0; padding: .45rem .7rem;
  font-family: var(--font-body); font-size: .88rem;
  color: var(--text); background: var(--surface-sunken);
  border: 1px solid var(--border); border-radius: var(--radius-md);
}
.cal-search input[type=search]:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 1px; }

.cal-drop { position: relative; }
.cal-drop > summary,
.cal-zoom-on, .cal-zoom-off, .cal-step, .cal-picker > summary {
  display: inline-flex; align-items: center; gap: .35rem;
  font-family: var(--font-label); font-size: .72rem;
  cursor: pointer; list-style: none;
}
.cal-drop > summary::-webkit-details-marker { display: none; }
.cal-drop > summary {
  padding: .4rem .8rem; border-radius: var(--radius-pill);
  border: 1px solid var(--border); background: var(--surface); color: var(--text);
}
.cal-drop > summary:hover { border-color: var(--border-strong); }
.cal-drop[open] > summary { border-color: var(--border-strong); }
.cal-drop-menu, .cal-picker-panel {
  position: absolute; inset-inline-start: 0; margin-top: .35rem; z-index: 20;
  min-width: 12rem; padding: .35rem;
  background: var(--surface-raised); border: 1px solid var(--border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-menu);
}
.cal-drop-menu a, .cal-picker-months a {
  display: block; padding: .38rem .6rem; border-radius: var(--radius-sm);
  font-family: var(--font-label); font-size: .74rem;
  text-decoration: none; color: var(--text);
}
.cal-drop-menu a:hover, .cal-picker-months a:hover { background: var(--surface-sunken); }

/* Navigation. The arrows stay for one week either side — the commonest move,
   and one that should not need a panel. */
.cal-nav { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-top: 1.5rem; }
.cal-nav-spacer { flex: 1; }
.cal-step { text-decoration: none; color: var(--text-link); }
.cal-step:hover { text-decoration: underline; }
.cal-chevron { width: .7rem; height: .7rem; flex: none; }
.cal-zoom { display: flex; border: 1px solid var(--border); border-radius: var(--radius-pill); overflow: hidden; }
.cal-zoom-on { padding: .38rem .9rem; background: var(--border-strong); color: var(--text-inverse); }
.cal-zoom-off { padding: .38rem .9rem; background: var(--surface); color: var(--text); text-decoration: none; }
.cal-zoom-off:hover { background: var(--surface-sunken); }

/* The period picker. A row is the target, not a day — the week number in the
   gutter is what makes five dates read as one thing to click. */
.cal-picker { position: relative; }
.cal-picker > summary {
  padding: .25rem .7rem .3rem; border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface);
  font-family: var(--font-display); font-size: 1.2rem; line-height: 1.3; color: var(--text);
}
.cal-picker > summary::-webkit-details-marker { display: none; }
.cal-picker-panel { min-width: 20rem; padding: .85rem .8rem; }
.cal-picker-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: .5rem; margin-bottom: .7rem;
}
.cal-picker-month { font-family: var(--font-label); font-size: .74rem; font-weight: 600; }
.cal-picker-term {
  font-family: var(--font-label); font-size: .58rem;
  text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted);
}
.cal-picker-weeks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .25rem; }
.cal-pweek {
  display: grid; grid-template-columns: 2rem minmax(0, 1fr); gap: .3rem; align-items: center;
  padding: .4rem .35rem; border-radius: var(--radius-sm); text-decoration: none; color: inherit;
  background: var(--surface); border: 1px solid var(--border);
}
.cal-pweek:hover { border-color: var(--border-strong); }
.cal-pweek--here { background: var(--tint-brand-fill); border-color: var(--focus-ring); }
.cal-pweek--holiday {
  border-color: var(--border);
  background-color: var(--surface-sunken);
  background-image: repeating-linear-gradient(135deg, var(--border) 0 1px, transparent 1px 8px);
}
.cal-pweek-num { font-family: var(--font-label); font-size: .58rem; color: var(--text-muted); }
.cal-pweek-days { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: .15rem; }
.cal-pweek-days span {
  text-align: center; font-family: var(--font-display); font-size: .8rem; color: var(--text-muted);
}
.cal-picker-foot {
  margin-top: .7rem; padding-top: .6rem; border-top: 1px solid var(--border);
  font-family: var(--font-label); font-size: .72rem;
}
.cal-picker-months {
  margin-top: .5rem; padding-top: .5rem; border-top: 1px solid var(--border);
  max-height: 11rem; overflow-y: auto;
}
.cal-pmonth--here { background: var(--action-bg); color: var(--action-ink); }

/* Filtered. Days are never hidden, only what is inside them — so this line is
   what stops an emptied Tuesday reading as a day with no lesson. */
.cal-filtered {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  margin: .8rem 0 0; padding: .5rem .8rem; border-radius: var(--radius-md);
  background: var(--tint-brand-fill);
  font-family: var(--font-label); font-size: .72rem; color: var(--text-link);
}

/* The week. Five day columns with lessons stacked inside, never periods as
   rows: we know about science only, and an empty period row would draw a free
   period we cannot actually see. */
/* Five day columns, lessons stacked inside. Deliberately NOT aligned into
   period rows — see the note in _week: an empty row under a labelled time reads
   as a free period, and science is the only thing this calendar can see. */
/* align-items: start, so a column is as tall as its own lessons. Grid stretches
   children by default, which left the tinted today column running down to the
   bottom of the busiest day as a long empty band. */
.cal-week {
  display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: .75rem; margin-top: 1rem; align-items: start;
}
/* Padding on every column so that tinting one does not shift it out of line
   with the other four. */
.cal-day {
  display: flex; flex-direction: column; gap: .55rem;
  padding: .4rem .45rem .7rem; border-radius: calc(var(--look-radius) + 2px);
}
.cal-day-head {
  display: flex; align-items: baseline; gap: .4rem; margin: 0;
  padding-bottom: .45rem; border-bottom: 1px solid var(--border);
  font-family: var(--font-body); font-weight: 400;
}

/* TODAY. A 2px rule and a grey label was a mark you had to go looking for, on
   the one column somebody opening this page is looking for first.

   The tint is the same one the picker gives the week you are on and the filter
   bar gives the filter in force — in this feature it already means "this is the
   one", so today borrows a vocabulary rather than inventing a fifth. Honey was
   the other candidate and is wrong: it means a session left open, and a Tuesday
   is not in progress. */
.cal-day--today { background: var(--tint-brand-fill); }
.cal-day--today .cal-day-head { padding-top: .1rem; border-bottom: 2px solid var(--action-bg); }
.cal-daynum { font-family: var(--font-display); font-size: 1.15rem; line-height: 1; color: var(--text-muted); }
.cal-day--today .cal-daynum { font-size: 1.35rem; color: var(--text-link); }
.cal-day--today .cal-dayname { color: var(--text-link); }
.cal-dayname {
  font-family: var(--font-label); font-size: .58rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted);
}
.cal-today {
  margin-inline-start: auto;
  padding: .15rem .4rem; border-radius: var(--radius-pill);
  background: var(--action-bg); color: var(--action-ink);
  font-family: var(--font-label); font-size: .55rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .08em;
}
.cal-day-body { display: flex; flex-direction: column; gap: .55rem; }

/* A lesson that happened, and a place to be. The dashed, unfilled one carries
   no title because the object behind it has none — see app/models/timetable.rb. */
.cal-cell {
  display: block; text-decoration: none; color: inherit;
  padding: .6rem .7rem .7rem; border-radius: var(--radius-md);
  background: var(--surface); border: 1px solid var(--border);
  border-top: 3px solid var(--border);
}
a.cal-cell:hover { box-shadow: var(--shadow-card); }
a.cal-cell:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.cal-cell--biology { border-top-color: var(--success); }
.cal-cell--chemistry { border-top-color: var(--border-strong); }
.cal-cell--physics { border-top-color: var(--warning); }
.cal-cell--scheduled { background: transparent; border-style: dashed; border-top-style: dashed; }
.cal-cell--open { border-color: var(--tint-accent-edge); border-top-color: var(--accent); box-shadow: var(--shadow-open); }
.cal-cell-title {
  display: block; margin: .3rem 0 .35rem;
  font-family: var(--font-display); font-size: .92rem; line-height: 1.25;
}
.cal-cell-meta { display: block; font-family: var(--font-label); font-size: .62rem; color: var(--text-muted); }
.cal-cell-open {
  display: flex; align-items: center; gap: .35rem; margin-top: .45rem;
  font-family: var(--font-label); font-size: .62rem; color: var(--success-text);
}

/* The month: for finding a lesson you half remember, where a week is for
   reading one. A real table, so the weekday headers are column headers. */
/* ONE DAY, AND IT IS WHERE THE SCREEN OPENS. The week's five columns squeeze a
   cell to about a fifth of the page; with one day the same cell has room for the
   meta line and the phase bar to sit out rather than wrap.

   Capped AND CENTRED rather than run to the full 68rem. A column of full-width
   cards reads as a table with one column — the measure is what makes a stack
   read as a list of lessons, and it is the same measure the week's busiest
   column has. Left where the week's first column starts, that measure put the
   whole day against one edge with half the page empty beside it; the week has
   four more columns to balance it and a day has nothing.

   The cap is on the wrapper, so the empty-state sentence below the card sits
   under the day it is about rather than back at the far edge of the page. */
.cal-solo { max-width: 34rem; margin: 1rem auto 0; }
.cal-day--solo { padding: .6rem .7rem 1rem; }

/* The head says the date the way a person says it — weekday, then the date
   itself. See the note in _day: the week's "31 MON" is a column label and only
   works with four more beside it. */
.cal-day-head--solo { gap: .6rem; padding-bottom: .6rem; }
.cal-daydate { font-family: var(--font-display); font-size: 1.35rem; line-height: 1; }
.cal-day--today .cal-daydate { color: var(--text-link); }

/* The date as the way into its day, from a week column. Underlined on hover
   only: five permanently underlined headings across the top of the week would
   read as five links before they read as five days. */
.cal-day-open {
  display: inline-flex; align-items: baseline; gap: .4rem;
  color: inherit; text-decoration: none;
}
.cal-day-open:hover .cal-daynum { text-decoration: underline; }
.cal-day-open:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 3px; border-radius: 4px; }

.cal-month-scroll { overflow-x: auto; margin-top: 1.25rem; }
.cal-month { border-collapse: separate; border-spacing: .6rem; width: 100%; min-width: 36rem; table-layout: fixed; }
.cal-month th {
  padding: 0 0 .2rem; text-align: start;
  font-family: var(--font-label); font-size: .58rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted);
}
.cal-month th abbr { text-decoration: none; }
.cal-mcell {
  vertical-align: top; height: 6.5rem; padding: .5rem .55rem;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md);
}
.cal-mcell--today { border: 2px solid var(--action-bg); background: var(--tint-brand-fill); }
.cal-mcell--today .cal-mnum { color: var(--text-link); font-size: 1rem; }
/* A day belonging to the month either side. The cell says so by having no
   ground and no edge — the NUMBER stays readable.

   It was set in --border to fade it, which came out at 1.32:1 against the card
   and is a WCAG 1.4.3 failure: a date nobody can read is not a quiet date, it
   is a broken one. The axe pass caught it where the token test could not, since
   both colours are perfectly legitimate tokens and it is the pairing that
   fails. */
.cal-mcell--outside { background: transparent; border-color: transparent; }
.cal-mnum { font-family: var(--font-display); font-size: .85rem; color: var(--text-muted); }
.cal-mstack { display: flex; flex-direction: column; gap: .3rem; margin-top: .4rem; }
.cal-chip {
  display: flex; align-items: flex-start; gap: .3rem;
  text-decoration: none; color: inherit;
  font-family: var(--font-label); font-size: .62rem; line-height: 1.3;
}
a.cal-chip:hover .cal-chip-text { text-decoration: underline; }
.cal-chip--scheduled { color: var(--text-muted); }
.cal-chip-dot { width: .4rem; height: .4rem; border-radius: 50%; flex: none; margin-top: .3rem; background: var(--border); }
.cal-chip-dot--biology { background: var(--success); }
.cal-chip-dot--chemistry { background: var(--border-strong); }
.cal-chip-dot--physics { background: var(--warning); }
.cal-chip-dot--hollow { background: transparent; border: 1px solid currentColor; }
.cal-chip-text { overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }

/* A week nobody was taught in. Hatched rather than blank, because a blank week
   and a week the import missed are the same picture and must not be — which is
   the entire reason school_terms is a table. */
.cal-week--holiday .cal-day-body,
.cal-solo--holiday .cal-day-body,
.cal-row--holiday .cal-mcell {
  background-color: var(--surface-sunken);
  background-image: repeating-linear-gradient(135deg, var(--border) 0 1px, transparent 1px 8px);
  border-radius: var(--radius-md);
  min-height: 5rem;
}
.cal-row--holiday .cal-mcell { border-color: var(--border); }

.cal-note { margin-top: 1.25rem; font-family: var(--font-label); font-size: .72rem; color: var(--text-muted); }

/* The fault state, and it is the only one here that is ours. Amphora rather
   than the functional red: the palette keeps red for a misconception the
   product has actually identified, and a timetable that never arrived is not
   the pupil being wrong about anything. */
.cal-fault {
  margin-top: 2rem; padding: 1.25rem 1.35rem;
  background: var(--surface); border: 1px solid var(--border);
  border-inline-start: 4px solid var(--warning); border-radius: var(--radius-md);
}
.cal-fault h2 { margin-bottom: .4rem; }
.cal-fault p { margin: 0 0 .6rem; color: var(--text-muted); font-size: .92rem; }
.cal-fault p:last-child { margin-bottom: 0; }

/* A phone gets one column, and the week template becomes the agenda rather
   than a second template written to say the same thing. */
@media (max-width: 52rem) {
  .cal-week { grid-template-columns: minmax(0, 1fr); gap: 1.4rem; }
  /* The day view is already one column; what has to go is the measure, which
     below this width is wider than the window and so does nothing but stop the
     cards using it. */
  .cal-solo { max-width: none; }
  .cal-nav { gap: .6rem; }
  .cal-nav-spacer { flex: 0 0 100%; }
}

/* --- Your work -----------------------------------------------------------
   The landing page. Two additions to the bands that were already here: what a
   teacher asked for, and what is on today.

   Overdue is Amphora and not the functional red. The palette keeps red for a
   misconception the product has actually identified — a date that has passed is
   a fact about a calendar, not a pupil being wrong about anything. */

.overdue { color: var(--warning-text); }

/* Nothing waiting, and it has to read as good news rather than as a page that
   failed to load. Pistachio, used once and quietly: it is the palette's word
   for secure, and this is the state of having nothing outstanding. */
.all-clear {
  margin: 1.75rem 0 0;
  padding: 1.15rem 1.25rem;
  background: var(--surface); border: 1px solid var(--border);
  border-inline-start: 4px solid var(--success); border-radius: var(--radius-md);
}
.all-clear h2 {
  display: flex; align-items: center; gap: .5rem;
  margin: 0 0 .35rem; font-size: 1.25rem; color: var(--success-text);
}
.all-clear p { margin: 0 0 .5rem; font-size: .9rem; color: var(--text-muted); }
.all-clear p:last-child { margin-bottom: 0; }

/* Search terms, held as pills. Several apply at once and narrow each other, so
   each needs its own way off — "gravity, Priya" minus Priya is a different
   thing from starting again. The row is a flex child at full width, which is
   why the bar above it wraps. */
.cal-terms { flex: 0 0 100%; display: flex; flex-wrap: wrap; gap: .4rem; }
.cal-term {
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .3rem .4rem .3rem .7rem; border-radius: var(--radius-pill);
  background: var(--tint-brand-fill); color: var(--text-link);
  font-family: var(--font-label); font-size: .7rem;
}
.cal-term-off {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.1rem; height: 1.1rem; border-radius: 50%;
  color: var(--text-link); text-decoration: none;
}
.cal-term-off svg { width: .65rem; height: .65rem; }
.cal-term-off:hover { background: var(--surface); }
.cal-term-off:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 1px; }

/* Clear all, sitting with the controls rather than beside the result count.
   Quieter than a term pill — it is the way out rather than one of the things
   applied, so it reads as an action and not as another filter. */
.cal-clear {
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .3rem .7rem; border-radius: var(--radius-pill);
  border: 1px solid var(--border); background: var(--surface);
  color: var(--text-muted); text-decoration: none;
  font-family: var(--font-label); font-size: .7rem;
}
.cal-clear svg { width: .7rem; height: .7rem; }
.cal-clear:hover { border-color: var(--border-strong); color: var(--text-link); }
.cal-clear:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* --- Curriculum app --------------------------------------------------------
   The misconception library and the screens that review it.

   Appended at the end of the file for the reason the language picker above
   already gives: this landed alongside other branches editing the teacher and
   calendar rules, and additions at opposite ends of a file merge cleanly where
   adjacent edits do not.

   ⚠️ WHAT THE THREE FUNCTIONAL COLOURS MEAN HERE, because this app has more
   states than any other teacher surface and getting them wrong would make the
   whole page shout. Amphora (--warning-*) is warm rather than wrong: unread,
   waiting, stale, thin. Pistachio (--success-*) is somebody has approved it.
   Red (--error-*) is kept for the two states that actually stop something — a
   record a person has said is wrong, which is withdrawn from every pupil's
   practice until it is settled, and a record no distractor points at, which
   cannot be detected at all however true it is. Everything else that is merely
   over — retired, declined — is muted, because an ending is not a fault. */

/* The four statuses a misconception holds, joining .status--approved above,
   which already carries the pistachio and is not redeclared here. `derived` is
   labelled "unread" on screen: the column records where the words came from,
   and what a reader wants to know is whether anybody has looked at them. */
.status--derived, .status--proposed, .status--stale {
  color: var(--warning-text); background: var(--warning-surface);
}
.status--flagged { color: var(--error-text); background: var(--error-surface); }
.status--applied { color: var(--success-text); background: var(--success-surface); }
/* ⚠️ AN OUTLINE, BECAUSE --surface-sunken IS ALMOST THE ROW IT SITS ON. Muted
   is the right reading for a withdrawn record — it is not wrong, it is gone —
   but without an edge the pill disappears into the table and the cell reads as
   a badge somebody forgot rather than a quiet one. Every other state renders as
   a pill and this has to as well. */
.status--retired, .status--declined {
  color: var(--text-muted); background: var(--surface-sunken);
  box-shadow: inset 0 0 0 1px var(--border);
}

/* ⚠️ THE HEADLINE FIGURE HAS A "NO ANSWER YET" RENDERING, and it is a size
   rather than a colour. approval_rate returns nil rather than zero when nobody
   has read anything, because "nobody has looked" and "everybody rejected
   everything" are opposite findings and 0% says the second one. The words are
   longer than a percentage and would otherwise wrap out of the tile. */
.stat--headline .stat-figure { color: var(--text-link); }
.stat-figure--none { font-size: 1.4rem; color: var(--text-muted); }

/* Four selects and a submit, wrapping on a narrow window. Labels are visible
   here rather than hidden: this is one form, not a control repeated down a
   table, and a filter bar whose labels only exist for a screen reader is one
   where nobody sighted knows what the third dropdown does either. */
.filter-bar { display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-end; }
.filter-field { display: grid; gap: .25rem; }
.filter-field label {
  font-family: var(--font-label); font-size: .62rem;
  text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted);
}
.filter-field select {
  padding: .5rem .65rem; font: inherit; font-size: .9rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text); max-width: 16rem;
}
.filter-actions { display: flex; align-items: center; gap: 1rem; }

/* One heading and one table per lesson, because the derivation ran per lesson
   and its failures — overlap and gaps — are properties of a set. */
/* The curriculum index: subject, then its units. A heavier rule above each
   subject than between its units, because the thing a reviewer is scanning for
   at this altitude is "which subject has work in it" — three headings, not
   thirty rows. Sits above .lesson-group, which is the same idea one level down
   and is what the filtered view still uses. */
.subject-group { margin-bottom: 2.5rem; }
.subject-group-head {
  margin: 0 0 .6rem; padding-bottom: .4rem; font-size: 1.15rem;
  border-bottom: 2px solid var(--border);
  display: flex; align-items: baseline; gap: .75rem; flex-wrap: wrap;
}
.subject-group-head .row-ref { font-weight: normal; margin: 0; }

/* The subject a unit belongs to, on the unit's own screen, where there is no
   heading above it to say so. Not one of the status colours: a subject is not a
   state, and colouring it like one would make chemistry look approved. */
.status--subject { color: var(--text-muted); background: var(--tint-brand-fill-soft); }

.lesson-group { margin-bottom: 2rem; }
.lesson-group-head { margin: 0 0 .6rem; font-size: 1.05rem; }
.lesson-group-head a { text-decoration: none; }
.lesson-group-head a:hover { text-decoration: underline; }
.lesson-group-head .row-ref { font-weight: normal; }

/* How much a record rests on. Thin is amphora because it is where the errors
   will be — 44 of the 71 mapped misconceptions rest on a single question — and
   none is red because a misconception no distractor points at cannot be
   detected by anything in practice, which no amount of editing its text fixes. */
.evidence { font-family: var(--font-label); font-size: .72rem; }
.evidence--thin { color: var(--warning-text); font-weight: 600; }
.evidence--none { color: var(--error-text); font-weight: 600; }

/* ⚠️ CARDS RATHER THAN ROWS, AND THE WIDTH IS THE ARGUMENT. Judging whether two
   entries are the same idea in different words means reading two claims beside
   each other, so the minimum is wide enough for a statement and a correct model
   to sit on the card without either being truncated — narrower columns would
   turn the set back into a list of links, which is the thing this screen exists
   not to be. */
.set-grid {
  display: grid; gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(24rem, 100%), 1fr));
}
.set-card { margin-bottom: 0; display: flex; flex-direction: column; }
.set-card-meta {
  margin: .6rem 0 0; font-family: var(--font-label); font-size: .72rem;
  color: var(--text-muted);
}
/* Pushed to the foot so the link sits in the same place on every card whatever
   the statement above it does. */
.set-card-open { margin: auto 0 0; padding-top: .75rem; }

/* Oak's own codes for the lesson, held back from the derivation prompt so that
   recovering one means something. A plain row of references, not a scoreboard. */
.oak-codes { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: .4rem; }
.oak-codes li {
  padding: .2rem .6rem; border-radius: var(--radius-pill);
  background: var(--surface-sunken); color: var(--text-muted);
  font-family: var(--font-label); font-size: .7rem;
}

/* What a record's standing means for a pupil, said under the badge. Misconception.live
   is approved-only, so three of the four statuses mean the tutor is not drawing
   on it — and a badge on its own leaves the reader to know that by heart. */
.standing {
  margin: .6rem 0 0; padding: .55rem .8rem;
  border-inline-start: 3px solid var(--warning); background: var(--surface-sunken);
  font-size: .9rem; color: var(--text-muted);
}

/* The ten context-pack fields. .authoring-form sets a .3rem grid gap, and the
   global `textarea { margin: 1.5rem 0 }` fights it — the same collision the
   translation review already had to undo on .review-candidate. */
.pack-field { display: grid; gap: .25rem; margin-top: 1.25rem; }
.pack-field label { margin-top: 0; }
.pack-field textarea { margin: 0; font-size: .95rem; }
.pack-help { margin: 0 0 .2rem; font-size: .82rem; color: var(--text-muted); max-width: 48rem; }

/* ⚠️ THE TWO FIELDS THE MODEL OBEYS, MARKED. approved_analogies and avoid_list
   are instructions rather than reference material: they go into the prompt as
   things the tutor may and may not say. Honey rather than amphora, because this
   is "read this differently", not "something is wrong with it" — the palette
   keeps the warm functional colour for a state somebody has to act on. */
.pack-field--instruction {
  padding-inline-start: .9rem;
  border-inline-start: 3px solid var(--accent);
}
.pack-badge {
  justify-self: start; padding: .15rem .55rem; border-radius: var(--radius-pill);
  background: var(--tint-accent-fill); color: var(--text-link);
  font-family: var(--font-label); font-size: .6rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em;
}

/* The verdict, in the same form as the edit because they are one act. A
   fieldset rather than three loose radios: it is one decision, and a legend is
   what says so to somebody hearing the form. */
.verdict-set {
  margin-top: 1.75rem; padding: .4rem 1rem 1rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.verdict-set legend {
  font-family: var(--font-label); font-size: .68rem; text-transform: uppercase;
  letter-spacing: .08em; color: var(--text-muted); padding: 0 .4rem;
}
.verdict-choice {
  display: grid; grid-template-columns: auto 1fr; gap: .2rem .6rem;
  align-items: baseline; margin-top: .7rem;
}
.verdict-choice label {
  margin-top: 0; font-family: var(--font-body); font-size: .95rem;
  text-transform: none; letter-spacing: normal; color: var(--text);
}
.verdict-help { grid-column: 2; font-size: .82rem; color: var(--text-muted); }

/* Which of the ten a revision moved. Inline pills rather than a list, because
   in a table cell a stacked list of four field names is taller than the row it
   is describing. */
.moved-fields { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: .3rem; }
.moved-fields li {
  padding: .15rem .5rem; border-radius: var(--radius-pill);
  background: var(--tint-brand-fill); color: var(--text-link);
  font-family: var(--font-label); font-size: .64rem;
}

/* Apply, and decline with its reason. The two sit in one column so the decline
   box does not stretch a table row's height across the whole queue. */
.proposal-actions { display: grid; gap: .5rem; justify-items: start; }
.decline-form { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; margin: 0; }
.decline-form input[type=text] { width: auto; min-width: 12rem; flex: 1 1 12rem; }
/* Stale is refused rather than warned about, so where the Apply button would
   have been there is a sentence instead. Amphora: the proposal is not wrong,
   it was written against text that has since moved. */
.stale-note {
  margin: 0; max-width: 26rem;
  font-size: .82rem; color: var(--warning-text);
}

/* The way back from a detection. Numbered because the rungs are ordered and the
   order is the whole mechanism — rung 1 is the floor, the target is the top. */
.rung-list { list-style: none; padding: 0; margin: .75rem 0 0; display: grid; gap: .5rem; }
.rung-list li {
  display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap;
  padding: .5rem .75rem; border-radius: var(--radius-md);
  background: var(--surface-sunken); font-size: .9rem;
}
.rung-list .row-note { margin: 0; flex: 0 0 4rem; }

/* Flag and retire, each with the box the reason goes in — both write a revision
   that changes no text, so the note is the entire content of the entry and is
   required rather than optional. Neither is .danger: nothing here destroys
   anything, and a control drawn as destructive invites a confirmation dialog
   for an act that does not need one. */
.settle-forms { display: flex; flex-wrap: wrap; gap: 1.5rem; margin-top: 1rem; }
.reason-form { display: grid; gap: .3rem; max-width: 22rem; margin: 0; }
.reason-form label {
  font-family: var(--font-label); font-size: .62rem;
  text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted);
}
.reason-form input[type=submit] { justify-self: start; margin-top: .4rem; }

/* The distractor that names the record being read, against the ones that name
   somebody else's. .option-reveals is already the brand link colour, so this
   only has to carry weight — a second colour here would compete with the
   correct answer's pistachio row in the same list. */
.option-reveals--here { font-weight: 700; }

@media (max-width: 46rem) {
  /* One column, and the filter bar stops being a bar. Selects go full width
     because a 16rem cap on a 20rem screen leaves them looking half-filled. */
  .filter-bar { display: grid; }
  .filter-field select { max-width: none; }
  .set-grid { grid-template-columns: minmax(0, 1fr); }
}

/* --- The open form under a multiple-choice question -----------------------
   The stem the ladder asks without its options, and a reviewer's three
   buttons in a row. Nothing here paints a colour: the chip is .status and the
   buttons are .action-link, both of which already know the palette. */
.open-form { margin-top: .75rem; padding-top: .5rem; border-top: 1px solid var(--border); }
.open-form .question-stem { margin: .25rem 0 .5rem; }
.open-form-actions { display: flex; gap: .5rem; flex-wrap: wrap; }
.open-form-actions form { margin: 0; }

/* The picture under review, beside the words that stand in for it — see
   teachers/lessons/_figure_description. Bounded rather than natural size:
   Oak's figures run from a 200px icon to a 1600px wide schematic, and a lesson
   page with forty questions on it cannot give each one a full-width plate. The
   cap is a height so a wide diagram stays legible along its long edge, and
   max-width keeps a tall one inside the card at 320px.

   On its own ground, not the card's. A flat-colour diagram with a white or
   transparent background vanishes into the dark theme, and an <img> cannot
   read the theme — so it is given a light plate to sit on under both, which
   is the same argument .stem-figure makes on the pupil's screen. */
.figure-description {
  margin-top: .75rem; padding-top: .5rem; border-top: 1px solid var(--border);
}
.figure-review-image {
  display: block; max-height: 220px; max-width: 100%; margin: .5rem 0;
  padding: .4rem; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--figure-plate);
}
.figure-alt { margin: .25rem 0 .5rem; color: var(--text); font-size: .95rem; }
/* What the supplier wrote, which is what a pupil reads until this is
   approved. Quieter than the description, because it is the thing being
   replaced and not the thing being judged. */
.figure-supplied { font-style: italic; }

/* --- The rung's right column ---------------------------------------------
   The scaffold above the chat, in one column so the two share the grid's
   second track. The chat keeps its own box; the scaffold takes the panel's.
   Nothing here paints a colour by name: the stop the pupil stands on takes the
   accent, which is what "you are here" means on the strip above it, and a
   stop the rung never needed is quieter than one it did. */
/* The side column is the chat alone, at a height of its own and pinned while
   the question column scrolls past it.

   ⚠️ NOT STRETCHED TO THE ROW. It was, to match the question column — and a
   wrong answer's thread makes that column as long as the conversation, so the
   panel grew with it and a pupil's own chat ran on down the page with no end,
   the composer somewhere below the fold. A fixed box that scrolls inside
   itself keeps the composer where the pupil left it. Capped at the viewport
   so the composer is never off the bottom of a short screen. */
.practice-side {
  align-self: start; position: sticky; top: 1.5rem;
  display: flex; flex-direction: column; min-width: 0;
}
.practice-side .chat {
  position: static; flex: none;
  height: min(38rem, calc(100vh - 3rem)); max-height: none;
}
.practice-side .chat-log { min-height: 0; }
@media (max-width: 62rem) {
  .practice-side { position: static; align-self: auto; }
  .practice-side .chat { height: auto; }
}
.answer-actions { margin-top: 1rem; }
.teach-next { margin: 1rem 0 0; color: var(--text-muted); }


/* The help behind one Hint button — see hints_controller.js. The cards keep
   the coach's own rule; the button is a quiet button beside them. */
.hints { margin: 0 0 1.25rem; display: grid; gap: .75rem; }
.hint-button { justify-self: start; cursor: pointer; padding: .55rem 1rem; border: var(--look-border-control) solid var(--border); border-radius: calc(var(--look-radius-control) + 2px); background: var(--surface); color: var(--text); font-weight: 600; }
.hint-button:hover:not(:disabled), .hint-button:focus-visible { border-color: var(--accent); }
/* Every card is open: the button stays where the pupil expects it and says
   there is nothing more behind it — see hints_controller.js. */
.hint-button:disabled { opacity: .5; cursor: default; }


/* Marking a written answer: the form is locked, the button is busy and a
   status line with the chat's dots says so — see marking_controller.js. */
.answer-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .75rem 1rem; }
.marking-status { display: inline-flex; align-items: center; gap: .5rem; margin: 0; color: var(--text-muted); }
.action--busy { opacity: .6; cursor: progress; }
form.answer[aria-busy="true"] textarea { opacity: .7; }
/* The choices are held while the answer goes up (holdChoices in
   lib/thread-motion.ts), and the drawn card hides the native control that
   says so: the pointer says it instead, over every card and the button. */
form.answer[aria-busy="true"] .option,
form.answer[aria-busy="true"] .action--busy { cursor: progress; }
/* A press that never arrived (PracticeParts' UnsentLine, the chat composer's
   line). Amber, as .answer-error is: nothing the pupil did is wrong, a step is
   missing. The live region is always in the page so its words are announced,
   and takes no room until it has some — not display:none, which would take it
   out of the accessibility tree and silence it. */
.unsent:empty { margin: 0; padding: 0; border: 0; }
.unsent:not(:empty) {
  margin: .75rem 0 0; padding: .5rem .75rem; border-radius: var(--radius-md);
  background: var(--warning-surface); color: var(--warning-text);
  border: 1px solid var(--warning);
  font-size: .9rem; font-weight: 600;
}

/* --- The lens picker -------------------------------------------------------

   "Explain it differently": the third tool on the question's row, beside Read
   aloud and Key words, and shaped like Key words because it is the same kind of
   thing — a disclosure whose summary reads as a button.

   The chips inside are radio inputs with their own label, so the whole visible
   chip is the hit area and the keyboard behaviour is the platform's rather than
   ours. The input is hidden from sight but NOT from the accessibility tree — no
   display:none, no visibility:hidden — because a radio removed from the tree
   takes the group's semantics with it and leaves seven unrelated labels.

   Nothing below reaches for a primitive or a literal: see the note at the top
   of this file for why the semantic tier is the only one anything here may
   name. */

.lenses {
  position: relative;
  display: inline-block;
  vertical-align: top;
  margin: 0 .5rem .5rem 0;
}

.lenses > summary {
  display: inline-flex; align-items: center; gap: .45rem;
  width: fit-content;
  white-space: nowrap;
  padding: .4rem .8rem;
  font: inherit; font-size: .9rem; font-weight: 500;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  cursor: pointer;
  list-style: none;
}

.lenses > summary::-webkit-details-marker { display: none; }

/* The chosen lens, on the closed control. The action colour, because it is the
   one thing on the row that reports a choice the pupil made. */
.lenses-chosen {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  padding: .05rem .45rem;
  border-radius: var(--radius-pill);
  background: var(--action-bg);
  color: var(--action-ink);
  font-size: .8rem;
}
.lenses > summary:hover { background: var(--surface-sunken); }
.lenses > summary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* A drawn arrow, never `content: "\25B8"` — the word bank's rule, and for its
   reason: a character in `content` is a character in the accessibility tree,
   and the control announces itself with the glyph read out. */
.lenses > summary::after {
  content: ""; width: 0; height: 0;
  border-left: .3rem solid transparent;
  border-right: .3rem solid transparent;
  border-top: .35rem solid currentColor;
}

.lenses[open] > summary::after { transform: rotate(180deg); }

/* --surface, not --surface-sunken: the question column sits ON the sunken
   ground, so a panel tinted with it is invisible. The word bank learned this
   from a screenshot; so did the two-boxes-on-top-of-each-other this replaced. */
.lenses-panel {
  position: absolute; left: 0; top: calc(100% + .4rem); z-index: 20;
  width: max-content; max-width: min(32rem, 82vw);
  margin: 0; padding: .75rem .9rem;
  box-shadow: var(--shadow-menu);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}

.lenses-footnote {
  margin: .7rem 0 0;
  padding-block-start: .6rem;
  border-block-start: 1px solid var(--border);
  font-size: .8rem;
  color: var(--text-muted);
}

.lenses-hint {
  margin: 0 0 .6rem;
  color: var(--text-muted);
  font-size: .85rem;
}

.lens-chips {
  display: flex;
  flex-wrap: wrap;
  gap: .45rem;
  margin: 0;
}

.lens-chip {
  display: inline-flex;
  align-items: baseline;
  gap: .4rem;
  padding: .35rem .8rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--text-muted);
  cursor: pointer;
}

/* Clipped rather than hidden, so it keeps its place in the accessibility tree
   and in the tab order. */
.lens-chip-input {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  white-space: nowrap;
  clip-path: inset(50%);
}

.lens-chip-label { font-size: .92rem; }
.lens-chip-note { font-size: .78rem; color: var(--text-muted); }

.lens-chip.is-on {
  border-color: var(--action-bg);
  background: var(--action-bg);
  color: var(--action-ink);
}

.lens-chip.is-on .lens-chip-note { color: var(--text-inverse); }

/* The focus ring goes on the chip, because the input it belongs to is clipped
   to a pixel and a ring drawn there would be invisible. */
.lens-chip:has(.lens-chip-input:focus-visible) {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

/* The lens sprite: the chip's own colour painted through a shape.

   ⚠️ A MASK RATHER THAN AN <img>, because the sprite has to be dark on a chip
   at rest and light on the chosen one. An <img> is a separate document and its
   `currentColor` resolves to its own black, so the chosen chip would carry a
   black glyph on a dark ground. The URL is set inline per lens — style-src
   allows that deliberately, see the note in content_security_policy.rb. */
.lens-sprite {
  /* Declared here as well as set inline, so the name exists in the stylesheet —
     a var() that is only ever set from markup fails silently if the name is
     ever mistyped, and nothing on the page says so. */
  --lens-sprite: none;
  inline-size: 1.05em;
  block-size: 1.05em;
  flex: none;
  align-self: center;
  background: currentColor;
  mask-image: var(--lens-sprite);
  mask-repeat: no-repeat;
  mask-position: center;
  mask-size: contain;
}

/* --- The lens panel -------------------------------------------------------
   A catalogue to edit rather than a queue to clear, so it borrows the teacher
   table and adds only what a lens has that nothing else does: a sprite beside
   its name, and a key under it. */

.lens-row-name {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
}

/* The key is what every rendering references and what a person debugging one
   reads in a trace, so it is set as the identifier it is rather than as prose. */
.lens-row-key {
  display: block;
  font-family: var(--font-mono);
  font-size: .8rem;
  color: var(--text-muted);
}

.lens-editor .field { margin-block-end: 1rem; }
.lens-editor label { display: block; font-weight: 600; margin-block-end: .35rem; }

/* A prompt is read and edited as a block of instructions, not as prose: a
   monospaced box makes a stray indent or a doubled line visible, which on the
   one field in the product that steers a model is worth the plainer look. */
.lens-editor textarea {
  inline-size: 100%;
  font-family: var(--font-mono);
  font-size: .88rem;
  line-height: 1.5;
}

.lens-editor input[type="text"] { inline-size: 100%; }
.lens-editor .field-help { margin: .35rem 0 0; font-size: .85rem; color: var(--text-muted); }

/* The prompt as it stood, read rather than edited. Monospaced for the reason
   the editor's textarea is: a stray indent or a doubled line is the kind of
   thing that changes what a model does and nothing else would show it. */
.lens-prompt {
  margin: 0 0 1rem;
  padding: .9rem 1rem;
  background: var(--surface-sunken);
  border-radius: .5rem;
  font-family: var(--font-mono);
  font-size: .86rem;
  line-height: 1.55;
  white-space: pre-wrap;
  overflow-x: auto;
}

.diff-picker {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  margin-block-end: 1rem;
}

/* The diff. Colour is the second signal, never the first: the marker column
   carries +, − or a space as text, so the change is legible to somebody reading
   in greyscale, to somebody who cannot tell red from green, and to a screen
   reader — which is the one that fails silently. WCAG 1.4.1. */
.diff {
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--border);
  border-radius: .5rem;
  overflow: hidden;
  font-family: var(--font-mono);
  font-size: .86rem;
  line-height: 1.6;
}

.diff-line {
  display: flex;
  gap: .6rem;
  padding: .1rem .7rem;
  white-space: pre-wrap;
}

.diff-mark {
  flex: none;
  inline-size: 1ch;
  color: var(--text-muted);
  user-select: none;
}

.diff-line--added { background: var(--success-surface); }
.diff-line--removed { background: var(--error-surface); }
.diff-line--added .diff-mark { color: var(--success-text); }
.diff-line--removed .diff-mark { color: var(--error-text); }

.diff-text { min-inline-size: 0; }

/* Read aloud and "Explain it differently" under a recap slide, on one line —
   the same pair the question screen carries above its answer box, and the same
   pair the question put back carries under it. */
/* ⚠️ ONE ROW, ONE PLACE, ON ALL FOUR SCREENS A PUPIL ANSWERS A QUESTION ON.
   Read aloud, Key words, Explain it differently and Ask the tutor sat under the
   stem on practice, the review and the baseline, and under the prose on the
   recap — so the control that READS a passage came after the passage, and a
   pupil who could not get through the words had to get through them to reach
   the button that would have read them. Putting them above the question on
   their own was worse: a row floating between the strip and the question reads
   as part of neither.
   They ride on the header row the question already had, opposite the command
   word, in the same place every time. A control that moves between screens is a
   control a pupil has to find again on each of them. */
.head-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  /* Pushed to the far end of the header row, so the pill stays where the eye
     looks for it and the controls read as a set rather than as a continuation
     of the label. */
  margin-inline-start: auto;
}
/* The four tools carry their own margins for the flowing layout they used to
   sit in; on this row the gap does the spacing. */
.head-tools > *, .head-tools form.button_to { margin: 0; }
/* ⚠️ ONE HEIGHT FOR THE WHOLE ROW. Five controls drawn by five different rules
   came out 37px, 34px and 31px, which reads as a row somebody assembled rather
   than designed. The border widths differ between them too, so the box is
   stated here rather than left to each. The icons are squares of the same
   height; Explain is a pill because it carries a word. */
.head-tools > * { box-sizing: border-box; min-height: 2.15rem; }
.head-tools .read-aloud-button { min-height: 2.15rem; border-width: 1.5px; }
/* The picker carries a sprite chip, which is taller than the word beside it and
   pushed the summary past the row's height. The box is stated rather than left
   to whatever is in it. */
.head-tools > .lenses > summary {
  height: 2.15rem; padding-block: 0; border-width: 1.5px;
}
.head-tools .lenses-chosen { line-height: 1; }
.head-tools .icon-button:not(.read-aloud-button) { width: 2.15rem; height: 2.15rem; }
.head-tools .read-aloud-button { width: 2.15rem; padding: 0; justify-content: center; }
/* The picker is a <details>, which is a block and was three pixels taller than
   the summary inside it. Shrink-wrapped, it is the summary. */
.head-tools > .lenses { display: inline-flex; }

/* ⚠️ A TOOLTIP THAT IS ALSO THE ACCESSIBLE NAME, which is what makes it safe to
   draw these controls as pictures. `title` was the obvious spelling and does
   none of the job: it waits a second before appearing, cannot be styled, is
   announced inconsistently, never shows for somebody on the keyboard, and does
   nothing whatever on a touch screen. One span carries the words instead — read
   by a screen reader as the button's name, and shown under it on hover or
   focus. There is one string, so the two cannot disagree.
   `opacity` rather than `display` or `visibility`, because both of those take
   the element out of the accessibility tree and the name with it. */
.icon-button { position: relative; }
.icon-button .tip {
  position: absolute; top: calc(100% + .4rem); left: 50%; transform: translateX(-50%);
  z-index: 5; padding: .25rem .55rem; border-radius: var(--radius-sm);
  background: var(--text); color: var(--surface);
  font-family: var(--font-label); font-size: .72rem; line-height: 1.3;
  white-space: nowrap; pointer-events: none; opacity: 0;
}
/* ⚠️ `:focus` AS WELL AS `:hover`, AND NOT `:focus-visible` ALONE. A tap
   focuses the button, which is the only way somebody on a tablet ever sees
   these words — and `:focus-visible` deliberately does not fire for a pointer,
   so the touch case would have been the one it missed. A keyboard user gets it
   from the same rule.
   The first version of this drew the label under the icon permanently on
   `(hover: none)`, which named the control and made the buttons half as tall
   again on exactly the screens with the least room. The name is in the
   accessibility tree either way; what a tap now buys is seeing it. */
.icon-button:hover .tip,
.icon-button:focus .tip { opacity: 1; }
/* ⚠️ ONE TIP AT A TIME WHILE THERE IS A POINTER. `:focus` above means a button
   keeps its tip after it is pressed — and both dialogs on this row hand focus
   back to the button that opened them when they close (ReportProblem,
   moveChatFocus). So a pupil who shut the report dialog and moved the mouse one
   button along had two tips up at once, the focused button's and the hovered
   one's, overlapping on a row with .4rem between them. While a pointer is over
   any icon button, that one's tip is the only one drawn; the focused button's
   comes back the moment the pointer leaves, which is what the keyboard and a
   tablet (where hover and focus land on the same button) still rely on. Not
   `:focus-visible`: the comment above says why a tap needs `:focus`. */
:root:has(.icon-button:hover) .icon-button:not(:hover) .tip { opacity: 0; }
@media (prefers-reduced-motion: no-preference) {
  .icon-button .tip { transition: opacity .12s ease; }
}
/* ⚠️ HIDDEN LIKE A SKIP LINK, NOT REMOVED. The dotted marks in a stem give the
   definitions on hover or tap and carry no tabindex by design — see
   Content::WordBankMarks — so this disclosure is the only keyboard and
   screen-reader route to the same words. It comes off the tools row without
   coming out of the page: clipped out of the layout, still in the reading
   order, and back in view the moment it takes focus. */
.word-bank--quiet:not(:focus-within) {
  position: absolute;
  width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.word-bank--quiet:focus-within { position: static; }



/* The tools under the question put back: Read aloud and the lens picker, on one
   line, the same pair the question screen carries above its answer box. */
.probe-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: .5rem;
  margin-block: .6rem 1.2rem;
}

.probe-tools .read-aloud-controls { margin: 0; }
.probe-tools .chat-launcher { margin: 0; }
.probe-tools .lenses { margin: 0; }

/* What a pupil sees while a re-telling is being written.

   Beside the tools, never in place of anything: the question and the teaching
   stay where they were, so somebody who stops caring reads what is already
   there and answers. Empty when nothing is in flight, which is why it collapses
   to nothing rather than holding a gap. */
.lens-waiting {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin-block: .5rem 0;
  color: var(--text-muted);
  font-size: .88rem;
}

.lens-waiting:empty { display: none; }

/* What is said when a re-telling did not come. Quiet: it is information, not a
   warning, and nothing has gone wrong for the pupil — the authored text they
   are already reading is the answer. */
.lens-gave-up { color: var(--text-muted); }

/* Where the re-telling went once the wait is over. The same quiet register as
   the waiting line: it points, it does not celebrate. */
.lens-ready { color: var(--text); }

.hint-actions { display: flex; flex-wrap: wrap; gap: .5rem; }

/* --- The folding box ------------------------------------------------------
   A hint, and inside it the same hint told through a lens: one component,
   nested, rather than two that look alike. See practice/_folding_box.

   ⚠️ THE FRAME IS WHAT SAYS WHOSE WORDS THESE ARE. Before this, a re-telling
   was set as a coloured rail down the side of the card it had replaced, and a
   pupil reading "A HINT · FOOTBALL" had no way to tell where the publisher's
   sentence stopped and a model's began — because the publisher's sentence was
   not on the page at all. Same frame, nested, each with its own name on it. */

.fold {
  border: var(--look-border) solid var(--border);
  border-radius: calc(var(--look-radius) * 1.2);
  background: var(--surface);
  overflow: hidden;
}

/* The label IS the control, so it takes the whole width of the box: a target
   the size of the thing it opens, rather than a word to aim at. */
.fold-head {
  display: flex;
  align-items: center;
  gap: .5rem;
  inline-size: 100%;
  appearance: none;
  border: 0;
  border-block-end: 1px solid var(--border);
  background: var(--surface-sunken);
  padding: .55rem .8rem;
  font-family: var(--font-label);
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--text-muted);
  text-align: start;
  cursor: pointer;
}

.fold-head:hover { color: var(--text); }
.fold-head:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -2px; }
/* Folded, the rule under the label would be a line under nothing. */
.fold-head[aria-expanded="false"] { border-block-end-color: transparent; }

.fold-body { padding: .9rem .95rem; }
.fold-body > :last-child { margin-block-end: 0; }
/* The box inside the box: enough air to read as nested rather than as the next
   thing down the page. */
.fold-body > .fold { margin-block-start: 1rem; }

/* ⚠️ THE AUTHORED CARD LOSES ITS OWN FRAME INSIDE ONE. `.teach` carries a
   ground and a rail of its own, which is right on the feedback screen where it
   stands alone and is a third box in here — and three nested frames is the
   thing the pupil said was unclear. */
.fold-body > .teach {
  margin: 0; padding: 0; background: none; border: 0;
}

/* A lens's box, told apart from the app's own by its colour and by the sprite
   on its label. Not louder — it is an alternative, not an improvement. */
.fold--lens { border-color: var(--action-bg); }
.fold--lens > .fold-head { color: var(--text-link); border-block-end-color: var(--action-bg); }
.fold--lens > .fold-head[aria-expanded="false"] { border-block-end-color: transparent; }
.fold--lens > .fold-head:hover { color: var(--text-link); }

/* Where a re-telling of the question goes. Collapses to nothing when there is
   none, which is most of the time — see practice/_lens_slot. */
.lens-slot:empty { display: none; }
.lens-slot > .fold { margin-block: 1rem 1.25rem; }

/* A drawn triangle, never `content: "\25B8"` — the word bank's rule, for its
   reason: a character in `content` is a character in the accessibility tree,
   and the control announces itself with the glyph read out. */
.fold-chevron {
  inline-size: 0;
  block-size: 0;
  flex: none;
  border-left: .3rem solid transparent;
  border-right: .3rem solid transparent;
  border-top: .35rem solid currentColor;
  transition: transform .12s ease;
}

.fold-head[aria-expanded="false"] .fold-chevron { transform: rotate(-90deg); }

/* ─── The glass box window ────────────────────────────────────────────────
   A developer sign-in as a tree (GlassBox::TracesController). Dense on purpose:
   it is a development tool read beside the pupil screen, so a row is one line
   of monospace and indentation is the structure. The actor colours keep the old
   inline screen's argument — content the success pair, a model the warning pair
   because it is the row to look at, code the neutral sunken surface — and
   nothing else here is coloured, so a model call is findable at a glance. */

.gbt-body { background: var(--surface); color: var(--text); }
.gbt { padding: 1rem clamp(1rem, 3vw, 2rem) 4rem; font-family: var(--font-mono); font-size: .85rem; }

.gbt-header {
  position: sticky; top: 0; z-index: 1;
  background: var(--surface); border-bottom: 1px solid var(--border);
  padding: .5rem 0; margin-bottom: .75rem;
}
.gbt-title { font-family: var(--font-mono); font-size: 1rem; margin: 0; display: flex; gap: .75rem; flex-wrap: wrap; }
.gbt-id { color: var(--text-muted); }
.gbt-totals { color: var(--text-muted); }
.gbt-totals:empty { display: none; }
.gbt-totals-root { margin: .25rem 0 0; }
.gbt-header form { margin: 0; }
.gbt-actions { display: flex; gap: .5rem; margin-top: .35rem; }

/* The prompts, in a panel over the right of the window rather than inside the
   boxes of the steps that sent them. Full width on a phone. */
.gbt-prompts {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 10;
  box-sizing: border-box; width: min(44rem, 100%); overflow-y: auto; overflow-x: hidden; padding: 1rem;
  background: var(--surface); border-left: 1px solid var(--border); box-shadow: var(--shadow-dialog);
}
.gbt-prompts-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: .75rem; }
.gbt-prompts-title { font-family: var(--font-mono); font-size: 1rem; margin: 0; }
.gbt-prompt-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .4rem; }
/* No indent under an entry: there is no row above it to indent from, and on a
   phone the indent pushed the text past the panel's edge. */
.gbt-prompts .gbt-payload-body { margin-left: 0; }
/* The panel scrolls; a prompt inside it does not scroll again. */
.gbt-value.gbt-prompt-text { max-height: none; overflow: visible; }
.gbt-prompt-link {
  padding: 0; border: 0; background: transparent; color: var(--text-link);
  font: inherit; text-decoration: underline; cursor: pointer;
}
.gbt-prompt-link:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 1px; }
.gbt-clear {
  padding: .1rem .6rem; border: 1px solid var(--border-strong); border-radius: .2rem;
  background: transparent; color: var(--text); font: inherit; font-size: .8rem; cursor: pointer;
}
.gbt-clear:hover { background: var(--surface-sunken); }
.gbt-clear:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 1px; }
.gbt-empty, .gbt-stopped { color: var(--text-muted); }
.gbt-stopped { border: 1px solid var(--warning); background: var(--warning-surface); color: var(--warning-text); padding: .5rem; }

.gbt-children { list-style: none; margin: 0; padding: 0 0 0 1.1rem; border-left: 1px solid var(--border); }
.gbt-tree { padding-left: 0; border-left: 0; }
.gbt-node { margin: 0; }
.gbt-orphan > .gbt-row,
.gbt-waiting > .gbt-row { outline: 1px dashed var(--warning); outline-offset: -1px; }

.gbt-row {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .15rem .6rem;
  padding: .1rem .35rem; border-radius: .2rem; min-height: 1.6rem;
}
.gbt-row:hover { background: var(--surface-sunken); }

/* Always the same width, drawn or not, so keys line up whether a row has
   children or not. A class and not the hidden attribute, which took the width
   with it and staggered every leaf a toggle to the left. */
.gbt-toggle {
  flex: none; width: 1.1rem; padding: 0; border: 0; background: transparent;
  color: var(--text-muted); font: inherit; cursor: pointer;
}
.gbt-toggle--none { visibility: hidden; }
.gbt-toggle span { display: inline-block; transition: transform .1s ease; }
.gbt-toggle[aria-expanded="true"] span { transform: rotate(90deg); }
.gbt-toggle:focus-visible, .gbt-payload-toggle:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 1px; }

.gbt-actor, .gbt-status { padding: 0 .35rem; border-radius: .2rem; font-size: .75rem; }
.gbt-actor-code { background: var(--surface-sunken); color: var(--text); }
.gbt-actor-content { background: var(--success-surface); color: var(--success-text); }
.gbt-actor-model { background: var(--warning-surface); color: var(--warning-text); }
.gbt-actor-service { background: var(--selected-surface); color: var(--text); }
.gbt-status { border: 1px solid var(--border-strong); color: var(--text); }
.gbt-status-failed, .gbt-status-held { background: var(--error-surface); color: var(--error-text); border-color: var(--error); }
.gbt-status-orphan { border-style: dashed; }
.gbt-status-running { border-style: dotted; color: var(--text-muted); }
.gbt-repeats { padding: 0 .35rem; border-radius: .2rem; background: var(--surface-sunken); color: var(--text); font-size: .75rem; }
.gbt-elapsed { color: var(--text-muted); }

.gbt-gap { margin: .15rem 0 .15rem .35rem; color: var(--text-muted); font-style: italic; }

.gbt-payload-toggle {
  padding: 0 .3rem; border: 1px solid var(--border); border-radius: .2rem; background: transparent;
  color: var(--text-link); font: inherit; font-size: .75rem; cursor: pointer;
}
.gbt-payload-toggle[aria-expanded="true"] { background: var(--surface-sunken); }
.gbt-payload-body {
  margin: .2rem 0 .4rem 1.45rem; padding: .5rem .75rem;
  background: var(--surface-sunken); border: 1px solid var(--border); border-radius: .25rem;
}
.gbt-payload-heading { font-family: var(--font-mono); font-size: .8rem; margin: .75rem 0 .25rem; color: var(--text-muted); }
.gbt-facts, .gbt-fields { display: grid; grid-template-columns: max-content 1fr; gap: .15rem .75rem; margin: 0; }
.gbt-facts dd, .gbt-fields dd { margin: 0; min-width: 0; }
.gbt-facts code { overflow-wrap: anywhere; }
.gbt-value {
  margin: 0; white-space: pre-wrap; overflow-wrap: anywhere;
  max-height: 24rem; overflow: auto; font-size: .8rem;
}
.gbt-withheld { margin: 0; padding-left: 1.1rem; }

@media (max-width: 40rem) {
  .gbt-facts, .gbt-fields { grid-template-columns: 1fr; }
  .gbt-totals { margin-left: 0; }
}

/* The pupil screen's way into the window. Fixed to a corner rather than placed
   in the page, so it never moves the screen it is watching. */
.gbt-open {
  position: fixed; right: 1rem; bottom: 1rem; z-index: 50;
  padding: .4rem .8rem; border-radius: .3rem;
  background: var(--primary-bg); color: var(--text-inverse);
  font-family: var(--font-mono); font-size: .85rem; text-decoration: none;
  box-shadow: var(--shadow-menu);
}
.gbt-open:hover { background: var(--primary-bg-hover); }
.gbt-open:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; }

/* --- Tester tools ----------------------------------------------------------
   The development site's panel for testers (Testers::PanelController). Never
   drawn on a school instance, and styled as a plain document for that reason:
   it is tooling, and the teacher-dashboard design freeze does not reach it
   (docs/plan/Tester_Panel_Plan.md, D5). */
.tester-panel {
  max-width: 44rem; margin: 2.5rem auto; padding: 0 var(--clearspace);
  line-height: 1.6;
}
.tester-panel .lede { color: var(--text-muted); }
.tester-panel section { margin-top: 1.75rem; }
.tester-facts {
  display: grid; grid-template-columns: max-content 1fr; gap: .35rem 1.25rem;
}
.tester-facts dt { color: var(--text-muted); }
.tester-facts dd { margin: 0; overflow-wrap: anywhere; }

/* The `</>` is the handle of the tray: a `summary`, so it is a button to
   assistive technology and opens on Enter and Space with no script. Fixed to
   the corner the glass box's button used to have to itself. Its box is stated
   rather than inherited, because the column's foot is measured from it: 1rem
   up plus 2.25rem tall is where the bottom cube stands (plan §5). One above
   the column in z-index, so the bottom cube slides out from behind it. */
.tester-open {
  position: fixed; right: 1rem; bottom: 1rem; z-index: 51;
  box-sizing: border-box; height: 2.25rem; padding: 0 .8rem;
  display: inline-flex; align-items: center; border-radius: .3rem;
  background: var(--primary-bg); color: var(--text-inverse);
  font-family: var(--font-mono); font-size: .85rem; text-decoration: none;
  box-shadow: var(--shadow-menu);
  list-style: none; cursor: pointer;
}
.tester-open::-webkit-details-marker { display: none; }
.tester-open:hover, .tester-tray[open] > .tester-open { background: var(--primary-bg-hover); }
.tester-open:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; }
/* Never both for one person (a tester never gets the glass box's own button),
   but a developer-door session on a laptop can, and then this one moves up a row. */
body:has(.tester-tray) .gbt-open { bottom: 3.5rem; }
/* No box round the column. The first build put the cubes and their caption in
   a grid shelf, and the shelf grew to the caption's width: a pale slab four
   times the column with its left edge well past the `</>`. The cubes carry
   their own shadow now, and the caption is a box of its own beside the foot,
   so nothing it says can widen or move the column. 3.65rem is the handle's
   1rem, its 2.25rem, and one cube gap. */
.tester-cubes {
  position: fixed; right: 1rem; bottom: 3.65rem; z-index: 50;
  display: flex; flex-direction: column; gap: .4rem;
}
/* The children are the things that animate, the `form.button_to` wrappers
   included, and a later sibling paints over an earlier one. */
.tester-cubes > * { position: relative; margin: 0; }
.tester-tray-caption {
  position: fixed; right: calc(1rem + 3.5rem + .6rem); bottom: 3.65rem; z-index: 50;
  max-width: 12rem; display: flex; flex-direction: column;
  align-items: flex-end; justify-content: flex-end; gap: .3rem; text-align: right;
}
.tester-tray-who, .tester-tray-notice {
  margin: 0; padding: .2rem .5rem; background: var(--surface-raised);
  border: 1px solid var(--border); border-radius: .4rem;
  font-family: var(--font-mono); font-size: .75rem; color: var(--text-muted);
}
.tester-tray-who strong { color: var(--text); }
/* One slide per cube, from the `</>` upward and back down into it: "the
   buttons slide out from each other one by one". The animated element is
   the column's CHILD — the door cubes are buttons inside a form — and a
   later sibling paints over an earlier one, so each cube arrives from
   behind the one below it, and the bottom one from behind the handle.
   Position in the stagger comes from the markup order (`nth-child`) rather
   than an inline index the JSX would have to count across a map and two
   conditionals; the rules go to eleven (Sandbox is the eighth, the Reviewer's
   makes nine, and the two cubes drawn only for a tester with a tier on the
   bench, the bench's and the API reference's, make eleven), and a twelfth
   child would arrive with the first. Each
   declaration starts its own line
   because checks/css-tokens.test.ts finds a declared custom property by
   its line start, and these two are set here, not in :root. */
.tester-cubes > :nth-last-child(1) {
  --tray-out: 0; }
.tester-cubes > :nth-child(1) {
  --tray-in: 0; }
.tester-cubes > :nth-last-child(2) {
  --tray-out: 1; }
.tester-cubes > :nth-child(2) {
  --tray-in: 1; }
.tester-cubes > :nth-last-child(3) {
  --tray-out: 2; }
.tester-cubes > :nth-child(3) {
  --tray-in: 2; }
.tester-cubes > :nth-last-child(4) {
  --tray-out: 3; }
.tester-cubes > :nth-child(4) {
  --tray-in: 3; }
.tester-cubes > :nth-last-child(5) {
  --tray-out: 4; }
.tester-cubes > :nth-child(5) {
  --tray-in: 4; }
.tester-cubes > :nth-last-child(6) {
  --tray-out: 5; }
.tester-cubes > :nth-child(6) {
  --tray-in: 5; }
.tester-cubes > :nth-last-child(7) {
  --tray-out: 6; }
.tester-cubes > :nth-child(7) {
  --tray-in: 6; }
.tester-cubes > :nth-last-child(8) {
  --tray-out: 7; }
.tester-cubes > :nth-child(8) {
  --tray-in: 7; }
.tester-cubes > :nth-last-child(9) {
  --tray-out: 8; }
.tester-cubes > :nth-child(9) {
  --tray-in: 8; }
.tester-cubes > :nth-last-child(10) {
  --tray-out: 9; }
.tester-cubes > :nth-child(10) {
  --tray-in: 9; }
.tester-cubes > :nth-last-child(11) {
  --tray-out: 10; }
.tester-cubes > :nth-child(11) {
  --tray-in: 10; }
/* Animations rather than transitions: a details child goes from display:none
   to shown, and a transition has nothing to run from. `backwards` on the way
   out, or a cube waiting on its delay stands in place and then jumps;
   `forwards` on the way in, or a cube that has landed pops back while the
   last is still moving. The way in needs the script's help — a details hides
   its content the instant it closes, so the tray is marked `data-closing`
   first and closed when every animation under it has finished
   (TesterTools.tsx, slideShut). */
@media (prefers-reduced-motion: no-preference) {
  .tester-tray[open] .tester-cubes > * {
    animation: tester-cube-out .14s cubic-bezier(.2, .7, .2, 1) backwards;
    animation-delay: calc(var(--tray-out, 0) * 35ms);
  }
  .tester-tray[open] .tester-tray-caption { animation: tester-fade .14s ease-out 210ms backwards; }
  .tester-tray[data-closing] .tester-cubes > * {
    animation: tester-cube-in .12s cubic-bezier(.5, 0, .8, .4) forwards;
    animation-delay: calc(var(--tray-in, 0) * 30ms);
  }
  .tester-tray[data-closing] .tester-tray-caption { animation: tester-fade .12s ease-in reverse forwards; }
  .tester-cube-flyout[open] .tester-flyout { animation: tester-flyout-out .14s ease-out; }
}
/* 3.9rem is one cube and one gap: collapsed exactly onto the neighbour below. */
@keyframes tester-cube-out { from { opacity: 0; transform: translateY(3.9rem); } }
@keyframes tester-cube-in { to { opacity: 0; transform: translateY(3.9rem); } }
@keyframes tester-fade { from { opacity: 0; } }
@keyframes tester-flyout-out { from { opacity: 0; transform: translateX(.5rem); } }
/* The group's aria-busy is the one mark of a write in flight. */
.tester-cubes[aria-busy="true"] { pointer-events: none; cursor: progress; }
.tester-cube-flyout { position: relative; }
.tester-cube {
  box-sizing: border-box; width: 3.5rem; height: 3.5rem; position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .15rem; padding: 0; cursor: pointer; text-decoration: none; list-style: none;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: .4rem;
  font-family: var(--font-mono); font-size: .6rem; line-height: 1; text-transform: none;
  white-space: nowrap; box-shadow: var(--shadow-menu);
}
.tester-cube::-webkit-details-marker { display: none; }
.tester-cube .icon { width: 1.25rem; height: 1.25rem; }
.tester-cube:hover { background: var(--surface-sunken); }
.tester-cube:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; }
.tester-cube[aria-current="true"] {
  background: var(--selected-surface); border-color: var(--selected); color: var(--text);
  box-shadow: inset 0 -3px 0 var(--selected);
}
.tester-cube[aria-pressed="true"] {
  background: var(--selected-surface); border-color: var(--selected); color: var(--text);
  box-shadow: inset 0 -3px 0 var(--selected);
}
.tester-cube[disabled] { opacity: .45; cursor: not-allowed; }
/* Reset is the one destructive cube, and says so by colour as well as by name. */
.tester-cube--reset {
  background: var(--warning-surface); color: var(--warning-text); border-color: var(--warning);
}
.tester-cube--reset:hover { background: var(--warning-surface); }
/* The pressed cube is a flyout's summary; aria-busy sits on the group, so the
   open flyout's is the one dimmed while its write is out. */
.tester-cubes[aria-busy="true"] .tester-cube-flyout[open] > .tester-cube { opacity: .55; cursor: progress; }
.tester-cube-dot {
  position: absolute; top: .25rem; right: .25rem; width: .4rem; height: .4rem;
  border-radius: 50%; background: var(--success);
}
/* To the left of its cube, foot level with it: above would cover the cubes
   stacked over it. */
.tester-flyout {
  position: absolute; bottom: 0; right: calc(100% + .4rem); z-index: 1; min-width: 14rem;
  padding: .6rem; background: var(--surface-raised); border: 1px solid var(--border);
  border-radius: var(--radius-md); box-shadow: var(--shadow-menu);
}
.tester-flyout .tester-form { margin: 0; }
/* The Pupil flyout's rows (a pupil per follow-up flow): one sign-in button each,
   the signed-in one marked as a pressed cube is, by colour and by the bar. */
/* Anchored by its top, not its foot like the others: it is the tallest flyout and
   the Pupil cube is near the top of the column, so growing upward from the
   cube's foot would run off the top of a short window. */
#tester-flyout-pupil .tester-flyout { top: 0; bottom: auto; }
.tester-profiles { list-style: none; margin: .3rem 0 .6rem; padding: 0; display: grid; gap: .3rem; }
.tester-profiles .button_to { margin: 0; }
.tester-profile { width: 100%; text-align: start; }
.tester-profile[aria-current="true"] {
  background: var(--selected-surface); border-color: var(--selected); color: var(--text);
  box-shadow: inset 3px 0 0 var(--selected);
}
.tester-tray-notice { padding: .3rem .5rem; font-size: .8rem; color: var(--text); }
.tester-tray-notice:empty { display: none; }
.tester-tray-notice--alert { color: var(--error-text); }
.tester-dialog {
  max-width: 46rem; width: calc(100% - 2rem); max-height: calc(100dvh - 2rem);
  padding: 1rem; border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--surface); color: var(--text); box-shadow: var(--shadow-dialog);
}
.tester-dialog::backdrop { background: var(--scrim); }
.tester-dialog .tester-panel { margin: 0; padding: 0; }
/* Above the frame, which is positioned (for the busy bar below) and so paints
   over anything merely floated: without the z-index the heading inside the
   frame took every click meant for Close, and the dialog would not shut. */
.tester-close { float: right; position: relative; z-index: 3; }
.tester-accounts { list-style: none; margin: 0; padding: 0; }
.tester-account { padding: .9rem 0; border-top: 1px solid var(--border); }
.tester-account-name { display: flex; flex-wrap: wrap; gap: .25rem .75rem; align-items: baseline; margin: 0 0 .5rem; }
.tester-account-name span { color: var(--text-muted); }
.tester-form { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: .4rem 0; }
.tester-form label { flex-basis: 100%; font-size: .85rem; color: var(--text-muted); }
.tester-form-label { flex-basis: 100%; font-size: .85rem; color: var(--text-muted); }
.tester-lessons { list-style: none; margin: 0 0 .5rem; padding: 0; }
.tester-lessons li { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; padding: .2rem 0; }
/* The Access screen (docs/plan/Access_Management_Plan.md §2). It is a tester
   page, so it is NOT held to the site's column width (the user, 30 Sep 2026):
   the article takes the viewport less the page gutter, and the table inside it
   is wide enough for seven columns and a row of actions on one line. The short
   columns are fixed, the email takes the rest, and the action forms sit inline
   in the last cell. Below 75.5rem (the sum of the column minimums, so the
   email column never collapses) the table scrolls sideways inside its own
   container rather than wrapping; a tester page is not read on a phone.
   Prose stays at a readable measure. Semantic tokens only. */
.tester-panel.access-page { max-width: none; }
.access-page > p, .access-page > section > p { max-width: 44rem; }
.access-scroll { overflow-x: auto; max-width: 100%; }
.access-table { border-collapse: collapse; table-layout: fixed; width: 100%; min-width: 75.5rem; font-size: .9rem; margin: .5rem 0 1rem; }
.access-table caption { text-align: start; color: var(--text-muted); font-size: .85rem; padding-bottom: .4rem; }
.access-table th, .access-table td { padding: .55rem .6rem; border-bottom: 1px solid var(--border); text-align: start; vertical-align: middle; }
.access-table thead th { font-family: var(--font-label); font-size: .78rem; font-weight: 600; color: var(--text-muted); }
.access-table tbody th { font-weight: 600; overflow-wrap: anywhere; }
.access-table .access-site { width: 11rem; }
.access-table .access-flag { width: 4.5rem; }
.access-table .access-tier { width: 6.5rem; }
.access-table .access-since { width: 7rem; white-space: nowrap; }
.access-table .access-invited { width: 12.5rem; overflow-wrap: anywhere; }
.access-table .access-who { width: 14rem; }
.access-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem .6rem; }
.access-actions p { margin: 0; flex-basis: 100%; }
.access-actions .button_to { margin: 0; }
.access-bench { display: flex; flex-wrap: nowrap; align-items: center; gap: .4rem; margin: 0; }
.access-bench fieldset { border: 0; margin: 0; padding: 0; display: flex; flex-wrap: nowrap; align-items: center; gap: .2rem .6rem; }
.access-bench legend { float: inline-start; font-size: .78rem; color: var(--text-muted); padding: 0; margin-inline-end: .4rem; }
.access-bench fieldset > label { display: inline-flex; align-items: center; gap: .25rem; }
.access-password { margin: 1rem 0; padding: .75rem 1rem; border: 1px solid var(--border); border-inline-start: 4px solid var(--action-bg); background: var(--surface); }
.access-password h2 { margin: 0 0 .4rem; font-size: 1.1rem; }
.access-password code { font-size: 1.1rem; overflow-wrap: anywhere; }
/* Each section folded behind its summary, which reads as the heading it
   replaces; each account folded to one line. The marker stays — it is what
   says a line opens. */
.tester-section { margin-top: 1rem; border-top: 1px solid var(--border); padding-top: .75rem; }
.tester-section > summary {
  cursor: pointer; font-family: var(--font-display); font-size: 1.2rem; font-weight: 700;
}
.tester-section[open] > summary { margin-bottom: .5rem; }
.tester-count { margin-left: .4rem; font-family: var(--font-body); font-size: .9rem; font-weight: 400; color: var(--text-muted); }
.tester-account > summary { cursor: pointer; margin: 0; }
.tester-account[open] > summary { margin-bottom: .4rem; }
.tester-glass-box { margin-top: 1.25rem; }
.tester-glass-box h2 { font-size: 1.2rem; margin: 0 0 .25rem; }
.tester-glass-box p { margin: 0 0 .25rem; }
/* An account's summary stays a list item, not a flex row: a flex summary loses
   its disclosure marker, and the marker is what says the line opens. */
.tester-account > summary > span { margin-left: .5rem; color: var(--text-muted); }

/* ⚠️ SOMETHING HAS TO HAPPEN THE MOMENT A BUTTON IS PRESSED. The people using
   this are not developers, and a press that changes nothing for a second reads
   as a broken button and gets pressed again — which, for a reset or a stage,
   starts the same job twice. So, with no script of our own: Turbo marks the
   frame `busy` for the length of the request, and while it is the panel dims,
   stops taking clicks, and a bar runs across its top; the button pressed says
   "Working…" and is disabled (data-turbo-submits-with). */
turbo-frame#tester_panel { display: block; position: relative; }
turbo-frame#tester_panel[busy] { cursor: progress; }
turbo-frame#tester_panel[busy] .tester-panel { opacity: .55; pointer-events: none; }
/* Sticky, not absolute: the dialog scrolls, and a bar at the top of a panel
   scrolled down to its last account is a bar nobody sees. The negative margin
   gives back the four pixels it takes, so nothing below it jumps. */
turbo-frame#tester_panel[busy]::before {
  content: ""; display: block; position: sticky; top: 0; height: 4px; margin-bottom: -4px; z-index: 2;
  background: linear-gradient(90deg, transparent, var(--action-bg), transparent);
  background-size: 50% 100%; background-repeat: no-repeat;
  animation: tester-busy 1s linear infinite;
}
@keyframes tester-busy { from { background-position: -50% 0; } to { background-position: 150% 0; } }
@media (prefers-reduced-motion: reduce) {
  turbo-frame#tester_panel[busy]::before { animation: none; background: var(--action-bg); }
}
.tester-panel button[disabled], .tester-panel input[type=submit][disabled] { cursor: progress; }
/* What happened, where it can be seen: the dialog scrolls, and a notice at the
   top of a panel scrolled to its last account is a notice nobody reads. */
.tester-panel .flash { position: sticky; top: 0; z-index: 1; }
/* Sign in as: the usual way into a sandbox account, so it is the one full
   button on the account; everything else is quiet. */
.tester-sign-in { margin: .25rem 0 .6rem; }
.tester-password { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: .4rem 0; }
.tester-password .tester-form-label { flex-basis: auto; }
.tester-password summary { cursor: pointer; }
.tester-password code { margin-left: .4rem; }
.tester-note { color: var(--text-muted); font-size: .9rem; }
.tester-real { margin-top: .5rem; }
.tester-real > summary { cursor: pointer; color: var(--text-muted); font-size: .9rem; }

/* Live lessons ----------------------------------------------------------
   The teacher's lesson in the room and the pupil's side of it. Built from the
   pieces every other screen uses — panels, stat tiles, the teacher table, the
   three buttons — and coloured only through the semantic tokens, so a theme
   reaches these screens with nothing to add here. The lesson's own Overview,
   its header and the clock and controls in the rail are the next block's. */

.panel--start form { display: inline-flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: 0; }

/* A slot on the start panel: when, then what the timetable says about the
   class (its subject and room), then the lesson planned for it. The two
   buttons sit at the end, Start first. The slot running now carries the
   success edge, the same one a running thing carries elsewhere. */
.start-slots { list-style: none; margin: 0 0 1rem; padding: 0; display: grid; gap: .5rem; }
.start-slot {
  display: grid; grid-template-columns: 9.5rem minmax(0, 1fr); gap: .25rem 1rem; align-items: start;
  padding: .8rem 1rem; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface);
}
.start-slot--now { border-inline-start: 4px solid var(--success); }
.start-slot p { margin: 0; }
.start-slot-when { grid-row: 1 / span 3; font-family: var(--font-label); font-size: .78rem; color: var(--text-muted); line-height: 1.35; }
.start-slot-when strong { display: block; color: var(--text); font-size: .9rem; }
.start-slot-class { display: flex; flex-wrap: wrap; align-items: center; gap: .45rem; }
.start-slot-room { font-family: var(--font-label); font-size: .78rem; color: var(--text-muted); }
.start-slot-plan { grid-column: 2; font-size: .9rem; color: var(--text-muted); }
/* Under the plan rather than beside it: a Start button names the lesson and
   the class, and beside the plan it left the plan a column two words wide. */
.start-slot-actions { grid-column: 2; display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-top: .35rem; }
@media (max-width: 48rem) {
  .start-slot { grid-template-columns: minmax(0, 1fr); }
  .start-slot-when, .start-slot-plan, .start-slot-actions { grid-column: 1; grid-row: auto; }
}

/* Today's switch, Lesson | Revision session (Revision_Sessions_And_Challenge_Plan
   §2.1): two buttons of one GET form, joined into a segmented control. The
   pressed one is filled — navy for a lesson, Challenge's blue for a revision
   session, which every revision control on the panel then wears too, so the
   switch and the button it changed read as one thing. */
.panel-head .start-switch { margin-inline-start: auto; }
.start-switch {
  display: inline-flex; border: 1.5px solid var(--border-strong);
  border-radius: var(--look-radius-control); overflow: hidden;
}
.start-switch-option {
  border: 0; padding: .45rem .9rem; background: var(--surface); color: var(--text);
  font-family: var(--font-body); font-size: .88rem; font-weight: 600; cursor: pointer;
}
.start-switch-option + .start-switch-option { border-inline-start: 1.5px solid var(--border-strong); }
.start-switch-option:hover { background: var(--surface-sunken); }
.start-switch-option[aria-pressed="true"] { background: var(--primary-bg); color: var(--text-inverse); }
.start-switch-option--revision[aria-pressed="true"] {
  background: var(--phase-challenge-bg); color: var(--phase-challenge-ink);
}
.start-switch-option:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -3px; }
button.action.action--revision { background: var(--phase-challenge-bg); color: var(--phase-challenge-ink); }
button.action.action--revision:hover { background: var(--phase-challenge-bg-hover); }
button.action.action--revision:disabled { opacity: .5; cursor: not-allowed; }

/* A Challenge question on the curriculum lesson page, read before release
   (Revision_Sessions_And_Challenge_Plan §3.4): the worked answer's lines
   numbered, the one carrying the misconception edged in the error colour
   and named in words beside it, so the edge is never the only signal. */
.challenge-lines { margin: .5rem 0; padding-inline-start: 1.4rem; }
.challenge-line { padding: .2rem .4rem; border-inline-start: 3px solid transparent; }
.challenge-line.is-wrong { border-inline-start-color: var(--error); background: var(--error-surface); }
.challenge-line-mark {
  display: block; font-size: .78rem; font-weight: 600; color: var(--error-text);
}
.challenge-points { margin: .2rem 0 .6rem; padding-inline-start: 1.2rem; }

/* Setting up a revision session (routes/teacher-live-revision-new.tsx): the
   form down the left, where pupils start down the right. The units are rows
   with thin rules, as the register's are; a unit with nothing taught is
   muted and cannot be ticked. The two counts are the biggest thing in the
   aside, each with the roll beside it, and the bar under them is two
   flex-grow values — Review's amber and Challenge's blue — never a share. */
.revision-setup-grid {
  display: grid; grid-template-columns: minmax(0, 1fr) 18rem; gap: 1.25rem; align-items: start;
}
.revision-setup-main { display: flex; flex-direction: column; gap: 1.1rem; }
.revision-setup fieldset { margin: 0; min-width: 0; }
/* Floated, so a legend sits inside its panel like a heading instead of on
   the panel's border, where a fieldset puts it. */
.revision-setup legend {
  float: left; width: 100%; padding: 0; margin-bottom: .35rem;
  font-size: 1.05rem; font-weight: 700; color: var(--text);
}
.revision-setup legend + * { clear: both; }
.revision-unit {
  display: grid; grid-template-columns: 1.4rem minmax(0, 1fr) auto; gap: .65rem; align-items: center;
  padding: .65rem .1rem; border-bottom: 1px solid var(--border); cursor: pointer;
}
.revision-unit:last-child { border-bottom: 0; }
.revision-unit input { width: 1.1rem; height: 1.1rem; accent-color: var(--primary-bg); }
.revision-unit-words { display: flex; flex-direction: column; }
.revision-unit-meta { font-size: .86rem; color: var(--text-muted); }
.revision-unit-due { font-size: .82rem; font-weight: 600; color: var(--warning-text); }
.revision-unit--none { cursor: default; }
.revision-unit--none .revision-unit-words { color: var(--text-muted); }
.revision-mode-list { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .6rem; }
.revision-mode {
  display: flex; gap: .5rem; align-items: center; padding: .75rem .85rem; cursor: pointer;
  border: 1.5px solid var(--border); border-radius: var(--radius-lg); background: var(--surface);
  font-weight: 600;
}
.revision-mode:has(input:checked) { border-color: var(--border-strong); box-shadow: inset 0 0 0 1px var(--border-strong); }
.revision-mode input { accent-color: var(--primary-bg); }
.revision-setup-actions { display: flex; gap: .75rem; align-items: center; }
.revision-setup-actions button.action { padding: .8rem 1.5rem; font-size: 1rem; }
.revision-summary dl { margin: 0; }
.revision-summary dt { font-size: .88rem; color: var(--text-muted); }
.revision-summary dd { margin: 0 0 .85rem; color: var(--text-muted); font-size: .88rem; }
.revision-summary-count {
  font-size: 1.7rem; font-weight: 750; color: var(--text); font-variant-numeric: tabular-nums;
}
.revision-summary-bar {
  display: flex; height: .5rem; border-radius: var(--radius-pill); overflow: hidden;
  background: var(--surface-sunken);
}
.revision-summary-review { background: var(--phase-review-bg); }
.revision-summary-challenge { background: var(--phase-challenge-bg); }
@media (max-width: 62rem) {
  .revision-setup-grid { grid-template-columns: minmax(0, 1fr); }
  .revision-mode-list { grid-template-columns: minmax(0, 1fr); }
}

.live-links { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; margin: 1.5rem 0 0; }

/* A word in a table cell that is a state rather than prose. */
.live-tag {
  display: inline-block; padding: .15rem .5rem; border-radius: 999px;
  font-family: var(--font-label); font-size: .64rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em;
  background: var(--surface-sunken); color: var(--text-muted);
}
.live-tag--present { background: var(--success-surface); color: var(--success-text); }
.live-roll-table td { vertical-align: middle; }
.live-roll-table form { margin: 0; }
/* A table cell stays a table cell: the shared .cell-actions makes it a flex
   box, which parts it from its row's border. */
.live-cell-action { text-align: end; white-space: nowrap; }

/* The register. It is taken at the front of a room, often on the projector,
   so it is built to be read from a step back: the count largest, then a grid
   of names, because a class of thirty read down one column is a page and a
   half of scrolling. Each name is a tile the whole of which is the tickbox, a
   thumb's width tall. The state is drawn from the box itself (:has) rather
   than from a class the script keeps, so a page with no JavaScript shows what
   it will send. Absent is the amber of something the teacher should look at
   twice — not the error red, because a pupil off sick is not a fault. */
.live-register-figures { grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr)); margin-bottom: 1.25rem; }
.live-register-figures .stat { margin: 0; }
.live-register-present { border-color: var(--success); background: var(--success-surface); }
.live-register-present .stat-figure { color: var(--success-text); }
.live-register-absent .stat-figure { color: var(--text-muted); }
.live-register-absent--some { border-color: var(--warning); border-style: dashed; }
.live-register-absent--some .stat-figure { color: var(--warning-text); }
/* The end time is a detail of the lesson, set in the figure's face, that
   happens to be editable: it looks like the other figures until it is
   pointed at, and then like the field it is. */
.live-register-ends .stat-figure {
  width: auto; max-width: 100%; margin: -.2rem 0 0 -.35rem; padding: .1rem .35rem;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  background: transparent; color: var(--text);
  font-variant-numeric: tabular-nums;
}
.live-register-ends .stat-figure:hover { border-color: var(--border); }
.live-register-ends .stat-figure:focus { border-color: var(--border-strong); background: var(--surface-sunken); }
.live-register-ends label.stat-label { color: var(--text); }

.live-register-roll .panel-head { justify-content: space-between; flex-wrap: wrap; }
.live-register-tools { display: flex; flex-wrap: wrap; gap: .5rem; }
ul.live-roll {
  list-style: none; padding: 0; margin: 1rem 0 0;
  display: grid; gap: .6rem; grid-template-columns: repeat(auto-fill, minmax(min(15rem, 100%), 1fr));
}
ul.live-roll > li > label {
  display: flex; align-items: center; gap: .75rem; min-height: 3.75rem; cursor: pointer;
  padding: .6rem .8rem; border: 1px solid var(--success); border-radius: var(--radius-md);
  background: var(--surface);
}
ul.live-roll > li > label:hover { background: var(--surface-sunken); }
ul.live-roll > li > label:has(input:focus-visible) { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
ul.live-roll .avatar { flex: none; width: 2.25rem; height: 2.25rem; font-size: .82rem; }
.live-pupil { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: .1rem; }
.live-pupil-name { font-weight: 600; line-height: 1.25; overflow-wrap: anywhere; }
.live-pupil-state {
  font-family: var(--font-label); font-size: .62rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .08em; color: var(--success-text);
}
.live-pupil-absent { display: none; }
ul.live-roll input[type=checkbox] {
  flex: none; width: 1.35rem; height: 1.35rem; margin: 0; accent-color: var(--action-bg); cursor: pointer;
}
/* The box's own outline stands in for this one: the tile is the target. */
ul.live-roll input[type=checkbox]:focus-visible { outline: none; }
ul.live-roll > li > label:has(input:not(:checked)) {
  border-style: dashed; border-color: var(--warning); background: var(--warning-surface);
}
ul.live-roll > li > label:has(input:not(:checked)) .avatar {
  background: var(--surface); color: var(--text-muted); box-shadow: inset 0 0 0 1px var(--border);
}
ul.live-roll > li > label:has(input:not(:checked)) .live-pupil-name { color: var(--text-muted); }
ul.live-roll > li > label:has(input:not(:checked)) .live-pupil-state { color: var(--warning-text); }
ul.live-roll > li > label:has(input:not(:checked)) .live-pupil-present { display: none; }
ul.live-roll > li > label:has(input:not(:checked)) .live-pupil-absent { display: inline; }

/* The two ways out, held at the bottom of the window while the grid scrolls
   under them: with thirty names the button that opens the lesson is otherwise
   off the foot of the projector. It names the count it opens for, so the last
   check against the room is the press itself. */
.live-register-foot {
  position: sticky; bottom: 0; z-index: 2;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: .75rem;
  margin: 0 0 1.25rem; padding: .9rem 1.35rem;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
}
button.action.live-register-open { padding: .8rem 1.5rem; font-size: 1rem; }

/* Cancel is quiet until it is wanted: under a rule, after everything else,
   outlined rather than filled. Still the danger colour, so it is never
   mistaken for a way back — but not a red block beside the lesson's own
   buttons, where a hurried press would land on it. */
.live-cancel {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1rem;
  margin-top: 2rem;
}
.live-cancel h2 { margin: 0 0 .25rem; font-size: 1rem; }
.live-cancel .panel-help { margin: 0; }
.live-cancel form { margin: 0; }
button.danger.live-cancel-button {
  background: transparent; color: var(--error-text); border: 1px solid currentColor;
  padding: .5rem 1rem; font-size: .88rem;
}
button.danger.live-cancel-button:hover { background: var(--error-surface); }

/* On a phone the three tiles would stack into a screenful before the first
   name, so the two counts share a row and the end time takes the next. */
@media (max-width: 40rem) {
  .live-register-figures { grid-template-columns: 1fr 1fr; gap: .6rem; }
  .live-register-figures .stat { padding: .8rem .9rem; }
  .live-register-ends { grid-column: 1 / -1; }
  .live-register-foot { padding: .75rem; }
  .live-register-foot button { flex: 1 1 100%; }
  .live-register-foot .live-register-open { order: -1; }
}

/* Choosing the lesson: the class first, then the library by subject and unit
   (lib/lesson-pick.server.ts). The line under the heading is what the
   timetable says; the search sits above the tabs because it reaches every
   subject whichever tab is open. */
.live-class-pick { display: flex; flex-wrap: wrap; align-items: center; gap: .75rem; }
.live-class-pick select { width: auto; }

.teacher-head .pick-context {
  display: flex; flex-wrap: wrap; align-items: center; gap: .4rem .9rem; margin: .3rem 0 0;
  font-family: var(--font-label); font-size: .82rem; color: var(--text-muted);
}

/* The glyph sits in a slot as tall as the field and as wide as the field's
   left padding, centred both ways in it. Placed by `top: 50%` on the form it
   sat low whenever anything else was in the form's box. */
.pick-search { position: relative; max-width: 40rem; margin: 0 0 1rem; }
.pick-search input[type="search"] {
  width: 100%; box-sizing: border-box; padding: .7rem 4.5rem .7rem 2.6rem;
  font: inherit; font-family: var(--font-body); font-size: 1rem;
  border: 1px solid var(--border-strong); border-radius: var(--radius-md);
  background: var(--surface); color: var(--text);
}
.pick-search-glyph {
  position: absolute; inset-block: 0; inset-inline-start: 0; width: 2.6rem;
  display: flex; align-items: center; justify-content: center;
  color: var(--text-muted); pointer-events: none;
}
.pick-search-glyph .icon { display: block; width: 1.15rem; height: 1.15rem; }
/* Chromium draws a clear cross of its own in a search field; the field has a
   Clear in words already, and two controls doing one thing is one too many. */
.pick-search input[type="search"]::-webkit-search-cancel-button { appearance: none; }
.pick-search-clear {
  position: absolute; inset-block: 0; inset-inline-end: .4rem; margin: auto 0; height: max-content;
  padding: .3rem .5rem; background: none; border: 0; cursor: pointer;
  color: var(--text-link); font-family: var(--font-label); font-size: .78rem;
}

.pick-tabs ul { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 1.2rem; padding: 0; list-style: none; }
.pick-tab {
  display: inline-flex; align-items: baseline; gap: .4rem;
  padding: .45rem .9rem; border-radius: var(--radius-pill);
  border: 1px solid var(--border); background: var(--surface); color: var(--text);
  font-family: var(--font-label); font-size: .85rem; text-decoration: none;
}
.pick-tab:hover { border-color: var(--border-strong); }
.pick-tab[aria-current="true"] { background: var(--action-bg); border-color: var(--action-bg); color: var(--action-ink); font-weight: 600; }
.pick-tab small { font-size: .72rem; }

.pick-count { margin: 0 0 .9rem; font-family: var(--font-label); font-size: .85rem; color: var(--text-muted); }
.pick-count strong { color: var(--text); }
.pick-subject + .pick-subject, .pick-units + .pick-subject { margin-top: 1.6rem; }
.pick-subject-heading { display: flex; align-items: baseline; gap: .6rem; margin: 0 0 .5rem; font-size: 1.05rem; }
.pick-subject-heading small { font-family: var(--font-label); font-size: .78rem; font-weight: 400; color: var(--text-muted); }
.pick-units { display: grid; gap: .5rem; }
.pick-more { margin: .6rem 0 0; }

.pick-unit { margin: 0; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--surface); }
.pick-unit > summary {
  display: grid; grid-template-columns: 2.2rem minmax(0, 1fr) auto; align-items: center; gap: .25rem .9rem;
  padding: .85rem 1.1rem; cursor: pointer; list-style: none;
}
.pick-unit > summary::-webkit-details-marker { display: none; }
.pick-unit[open] > summary { border-bottom: 1px solid var(--border); }
.pick-unit[open] .pick-unit-title { color: var(--text-link); }
.pick-unit-no, .pick-lesson-no { font-family: var(--font-label); font-size: .78rem; color: var(--text-muted); }
.pick-unit-title { font-family: var(--font-display); font-size: 1.02rem; font-weight: var(--weight-display); }
.pick-unit-title .subject { vertical-align: middle; }
.pick-unit-meta { font-family: var(--font-label); font-size: .78rem; color: var(--text-muted); text-align: end; }
.pick-unit-meta--taught { color: var(--success-text); }

.pick-lessons { margin: 0; padding: .35rem 0; list-style: none; }
.pick-lesson {
  display: grid; grid-template-columns: 2.2rem minmax(0, 1fr) 11rem auto; align-items: center; gap: .9rem;
  padding: .5rem 1.1rem;
}
.pick-lesson + .pick-lesson { border-top: 1px solid var(--border); }
.pick-lesson:hover { background: var(--surface-sunken); }
.pick-lesson form { margin: 0; }
.pick-lesson-taught { font-family: var(--font-label); font-size: .78rem; color: var(--text-muted); }
.pick-lesson-taught--taught { color: var(--success-text); }
.pick-lesson-taught--last { color: var(--success-text); font-weight: 600; }
.pick-hit { background: var(--warning-surface); color: inherit; border-radius: 2px; }

@media (max-width: 48rem) {
  .pick-unit > summary { grid-template-columns: 2.2rem minmax(0, 1fr); }
  .pick-unit-meta { grid-column: 2; text-align: start; }
  .pick-lesson { grid-template-columns: 2.2rem minmax(0, 1fr); }
  .pick-lesson-taught, .pick-lesson form { grid-column: 2; }
}
.live-pick-unit { color: var(--text-muted); font-size: .82rem; }

/* The pupil's side: one card saying what is happening, one button. */
.live-state {
  max-width: 40rem; margin: 2rem 0 0; padding: 1.75rem 1.75rem 1.5rem;
  background: var(--surface); border: var(--look-border) solid var(--border);
  border-inline-start: 5px solid var(--border); border-radius: calc(var(--look-radius) + 2px);
}
.live-state h1 { margin: .35rem 0 .5rem; }
.live-state .lede { margin: 0; }
.live-state--waiting { border-inline-start-color: var(--accent); }
.live-state--not_registered { border-inline-start-color: var(--warning); }
.live-state--done { border-inline-start-color: var(--success); }
/* Neutral: not the amber that means "ask your teacher", nor the accent that
   means "waiting to start". */
.live-state--paused { border-inline-start-color: var(--border-strong); }
.live-state-count {
  margin: 1rem 0 0; font-family: var(--font-label); font-size: .72rem;
  text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted);
}
.live-state-actions { margin: 1.5rem 0 0; }
/* Paused on an idea: the label small, the words at reading size under a rule,
   so the card's own heading stays the first thing read. Quoted words (a
   question about the wrong idea, never a statement of it) sit indented under
   their question, as a quotation does. */
.live-state-idea { margin: 1.25rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); }
.live-state-idea-label {
  margin: 0 0 .4rem; font-family: var(--font-label); font-size: .78rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted);
}
.live-state-idea-text { margin: 0; font-size: 1.15rem; line-height: 1.5; color: var(--text); }
blockquote.live-state-idea-text { padding-inline-start: 1rem; border-inline-start: 3px solid var(--border-strong); }
blockquote.live-state-idea-text p { margin: 0; }

/* THE END CARD (14 October 2026): the end of the lesson as a lesson card
   (.stage's shape: a band, then a body) rather than a sentence on a plain card,
   because the user asked for it to look like the cards the pupil has just
   worked through, and to say what to work on next.

   The band is --correct with its words in --surface: the "Done" pill's pairing
   on the lesson home (.stage-done), which theme-contrast.test.ts already holds
   in every palette, and the green that means finished there. Not a phase's
   colour: the end is no phase, and the teacher's live screens name a pupil's
   mode by those four. The tick's disc is the same pair inverted.

   THE NUMBERS ARE COLOURED BY PLACE, NOT BY MEANING: 1 is Practice's fill, 2
   Recap's, 3 Review's, each with the ink its own band uses — pairings the
   lesson cards' numerals already hold. Never by what the idea is, so a pupil
   cannot learn "amber means a check" from them; the colour carries nothing
   the words do not. The thin rule joining them is --border, so they read as
   an order. */
.end-card {
  padding: 0; overflow: hidden; display: flex; flex-direction: column;
  border-inline-start-width: var(--look-border); border-inline-start-color: var(--border);
}
.end-card-band {
  display: flex; align-items: center; gap: .75rem;
  padding: .8rem 1.25rem;
  background: var(--correct); color: var(--surface);
}
.end-card-band h1 { margin: 0; font-size: 1.35rem; color: inherit; }
.end-card-tick {
  position: relative; flex: none; display: grid; place-items: center;
  width: 2.1rem; height: 2.1rem; border-radius: 50%;
  background: var(--surface); color: var(--correct);
}
.end-card-tick svg { width: 1.25rem; height: 1.25rem; fill: none; stroke: currentColor; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
.end-card-body { padding: 1.15rem 1.25rem 1.4rem; }
.end-card-wait { margin: 0 0 1rem; font-weight: 600; color: var(--text); }
.end-card-from {
  display: flex; align-items: center; gap: .45rem; margin: 0 0 .45rem;
  font-family: var(--font-label); font-size: .78rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted);
}
.end-card-from .tutor-mark { margin: 0; }
.end-card-opening { margin: 0; font-size: 1.08rem; line-height: 1.55; color: var(--text); }
.end-card-writing { margin: 0; color: var(--text-muted); }
.end-card-next { margin: 1.15rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); }
.end-card-body > .end-card-next:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.end-card-label {
  margin: 0 0 .85rem; font-family: var(--font-label); font-size: .78rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted);
}
.end-card-steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 1.1rem; }
.end-card-step {
  position: relative; display: grid; grid-template-columns: 2rem 1fr; gap: .1rem .75rem; align-items: start;
}
.end-card-step p { margin: 0; padding-top: .2rem; font-size: 1.05rem; line-height: 1.45; color: var(--text); }
.end-card-step:not(:last-child)::before {
  content: ""; position: absolute; inset-inline-start: calc(1rem - 1px); inset-block: 1.6rem -1.2rem;
  width: 2px; background: var(--border);
}
.end-card-number {
  position: relative; z-index: 1;
  display: grid; place-items: center; width: 2rem; height: 2rem; border-radius: 50%;
  font-family: var(--font-label); font-weight: 800; font-size: .95rem;
  font-variant-numeric: tabular-nums; line-height: 1;
  box-shadow: 0 0 0 3px var(--surface);
}
.end-card-step:nth-child(3n+1) .end-card-number { background: var(--phase-practice-bg); color: var(--phase-practice-ink); }
.end-card-step:nth-child(3n+2) .end-card-number { background: var(--phase-recap-bg); color: var(--phase-recap-ink); }
.end-card-step:nth-child(3n) .end-card-number { background: var(--phase-review-bg); color: var(--phase-review-ink); }

/* A revision session's finished card lists what was reviewed, with how many
   checks — counts of checks, never of right answers. */
.end-card-reviewed { margin-top: 1rem; }
.end-card-reviewed-list { list-style: none; margin: .35rem 0 0; padding: 0; }
.end-card-reviewed-list li {
  display: flex; justify-content: space-between; gap: 1rem;
  padding: .45rem 0; border-bottom: 1px solid var(--border);
}
.end-card-reviewed-list li:last-child { border-bottom: 0; }
.end-card-reviewed-checks { color: var(--text-muted); white-space: nowrap; }

/* *Your revision* (routes/overview.tsx, board frame 4A): a lesson card's
   shape, with Review's band across the top of its list — the colour the
   teacher's register draws Review in — the next lesson marked with Review's
   soft wash, and one button in Review's colour under it. */
.your-revision h1 { margin-bottom: 1rem; }
.your-revision-part {
  border: 1px solid var(--border); border-radius: var(--look-radius);
  overflow: hidden; background: var(--surface);
}
.your-revision-head {
  margin: 0; padding: .6rem 1rem; font-size: .95rem;
  background: var(--phase-review-bg); color: var(--phase-review-ink);
}
.your-revision-list { list-style: none; margin: 0; padding: 0; }
.your-revision-item {
  display: flex; justify-content: space-between; align-items: center; gap: 1rem;
  padding: .75rem 1rem; border-top: 1px solid var(--border);
}
.your-revision-item:first-child { border-top: 0; }
.your-revision-item.is-next { background: color-mix(in srgb, var(--phase-review-bg) 16%, var(--surface)); font-weight: 700; }
.your-revision-words { display: flex; flex-direction: column; }
.your-revision-unit { font-size: .82rem; font-weight: 500; color: var(--text-muted); }
.your-revision-checks { white-space: nowrap; }
.your-revision-start { margin: 1.25rem 0 0; }
button.action.your-revision-go {
  width: 100%; padding: .9rem 1.4rem; font-size: 1.05rem;
  background: var(--phase-review-bg); color: var(--phase-review-ink);
}
button.action.your-revision-go:hover { background: var(--phase-review-bg-hover); }
/* Challenge after Review, in Challenge's blue; muted while Review is not done,
   with "After Review" in its band, so the order reads without a sentence. */
.your-revision-part + .your-revision-part { margin-top: 1rem; }
.your-revision-head { display: flex; justify-content: space-between; gap: 1rem; }
.your-revision-part--challenge .your-revision-head {
  background: var(--phase-challenge-bg); color: var(--phase-challenge-ink);
}
.your-revision-part--challenge .your-revision-item.is-next {
  background: color-mix(in srgb, var(--phase-challenge-bg) 12%, var(--surface));
}
.your-revision-part--challenge.is-later .your-revision-list { color: var(--text-muted); }
button.action.your-revision-go--challenge {
  background: var(--phase-challenge-bg); color: var(--phase-challenge-ink);
}
button.action.your-revision-go--challenge:hover { background: var(--phase-challenge-bg-hover); }

/* A Challenge sitting (routes/challenge.tsx): the question as a card, a
   worked answer's lines as choices, the box to write in; then what was
   written, the line verdict, the points of a full answer each ticked or
   ringed — words beside each mark, so the mark is never the only signal —
   and the tutor's one line. */
.challenge-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.challenge-step { margin: 0; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.challenge-question, .challenge-answered, .challenge-done {
  margin-top: 1rem; padding: 1.25rem; border: 1px solid var(--border);
  border-radius: var(--look-radius); background: var(--surface);
  display: flex; flex-direction: column; gap: .9rem;
}
.challenge-kicker {
  margin: 0; font-family: var(--font-label); font-size: .78rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .07em; color: var(--text-link);
}
.challenge-marks { font-weight: 600; color: var(--text-muted); text-transform: none; letter-spacing: 0; }
.challenge-stem { margin: 0; font-size: 1.12rem; font-weight: 600; line-height: 1.45; }
.challenge-data { border-collapse: collapse; background: var(--surface-sunken); border-radius: var(--radius-md); }
.challenge-data th, .challenge-data td { padding: .4rem .8rem; text-align: start; }
.challenge-lines-pick { margin: 0; padding: 0; border: 0; display: flex; flex-direction: column; gap: .4rem; }
.challenge-lines-pick legend { font-weight: 700; margin-bottom: .2rem; }
.challenge-prompt { margin: 0 0 .3rem; color: var(--text-muted); }
.challenge-line-option {
  display: grid; grid-template-columns: 1.3rem 4.5rem minmax(0, 1fr); gap: .5rem; align-items: start;
  padding: .6rem .8rem; border: 1.5px solid var(--border); border-radius: var(--radius-lg); cursor: pointer;
}
.challenge-line-option:has(input:checked) { border-color: var(--border-strong); background: var(--selected-surface); }
.challenge-line-label { font-size: .82rem; font-weight: 600; color: var(--text-muted); }
.challenge-write { display: flex; flex-direction: column; gap: .35rem; }
.challenge-write-label { font-weight: 700; }
.challenge-write textarea { width: 100%; box-sizing: border-box; margin: 0; }
.challenge-question button.action, .challenge-answered button.action, .challenge-done button.action { align-self: flex-start; }
.challenge-label {
  margin: 0; font-family: var(--font-label); font-size: .72rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted);
}
.challenge-yours { margin: 0; padding: .7rem .9rem; border-radius: var(--radius-md); background: var(--surface-sunken); }
.challenge-verdict { margin: 0; font-weight: 600; }
.challenge-points-made ul { list-style: none; margin: .35rem 0 0; padding: 0; }
.challenge-points-made li { display: flex; gap: .55rem; padding: .25rem 0; }
.challenge-point-mark { flex: none; width: 1.1rem; font-weight: 800; }
.challenge-points-made li.is-made .challenge-point-mark { color: var(--success-text); }
.challenge-points-made li.is-missing { color: var(--text-muted); }
.challenge-tutor { display: flex; gap: .65rem; align-items: flex-start; }
.challenge-tutor p { margin: 0; padding: .8rem 1rem; border: 1px solid var(--border); border-radius: var(--radius-xl); background: var(--surface); }
/* The arrival, once, as the card is drawn: the tick spins in and is drawn, the
   confetti a right answer throws comes off it, the heading slides in, then the
   message and each step rise in turn. About two seconds, and nothing waits on
   it. A step that arrives later — the message landing after "Writing your next
   steps" — rises as it is drawn, because it is a new element. Each look adds
   its own moment beside it (data-moment on <main>). */
@media (prefers-reduced-motion: no-preference) {
  .end-card-tick { animation: outcome-badge .6s cubic-bezier(.3, 1.6, .5, 1) .15s both; }
  .end-card-tick path {
    stroke-dasharray: 22; stroke-dashoffset: 22;
    animation: outcome-draw .45s ease-out .55s forwards;
  }
  .end-card .outcome-burst {
    display: block; position: absolute; inset-block-start: 50%; inset-inline-start: 50%;
    width: 0; height: 0; pointer-events: none;
  }
  .end-card .outcome-burst i { animation-delay: .5s; }
  .end-card .outcome-burst i:nth-child(even) { animation-delay: .58s; }
  .end-card-band h1 { animation: outcome-word .55s cubic-bezier(.3, 1.6, .5, 1) .35s both; }
  .end-card-from, .end-card-opening, .end-card-wait {
    animation: outcome-rise .5s cubic-bezier(.2, .9, .3, 1.2) .7s both;
  }
  .end-card-step { animation: outcome-rise .5s cubic-bezier(.2, .9, .3, 1.2) .95s both; }
  .end-card-step:nth-child(2) { animation-delay: 1.15s; }
  .end-card-step:nth-child(3) { animation-delay: 1.35s; }
  .end-card-number { animation: outcome-badge .5s cubic-bezier(.3, 1.6, .5, 1) 1.05s both; }
  .end-card-step:nth-child(2) .end-card-number { animation-delay: 1.25s; }
  .end-card-step:nth-child(3) .end-card-number { animation-delay: 1.45s; }
}
/* The question the teacher asked everyone, on the pupil's card: the stem at
   reading size, each option a whole-width target to tap, and Send. */
.live-hinge-form { margin: 1.25rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); }
.live-hinge-form fieldset { margin: 0; padding: 0; border: 0; }
.live-hinge-question { margin: 0 0 .75rem; font-size: 1.15rem; line-height: 1.5; color: var(--text); }
.live-hinge-answers { display: grid; gap: .5rem; margin: 0 0 1rem; padding: 0; list-style: none; }
.live-hinge-answer {
  display: flex; align-items: baseline; gap: .6rem; padding: .7rem .9rem; cursor: pointer;
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); color: var(--text);
}
.live-hinge-answer:has(input:checked) { border-color: var(--action-bg); box-shadow: 0 0 0 1px var(--action-bg); }

/* The rail's Live lesson entry: "none" beside it when nothing is open, and the
   lesson's own screens under it while one is. The sub-entries hang off the
   parent's label rather than its icon, and go when the rail folds — the
   parent's icon already leads to the same lesson. */
.rail-note {
  margin-inline-start: auto;
  font-family: var(--font-label); font-size: .62rem; text-transform: uppercase;
  letter-spacing: .06em; color: var(--text-muted);
}
.rail-sublinks {
  display: flex; flex-direction: column; gap: .05rem;
  margin: .1rem 0 .25rem; padding-inline-start: 2.35rem;
}
.rail-sublink {
  padding: .3rem .6rem; border-radius: var(--radius-sm);
  border-inline-start: 2px solid var(--border);
  font-family: var(--font-label); font-size: .72rem; letter-spacing: .03em;
  color: var(--text-muted); text-decoration: none;
}
.rail-sublink:hover { color: var(--text-link); background: var(--surface-sunken); }
.rail-sublink.active { color: var(--text-link); border-inline-start-color: var(--text-link); font-weight: 600; }
/* The badge that says a lesson is in progress: red, with a play mark, on the
   right of Live lesson while questions are running, so a teacher glancing at
   the rail can tell a lesson in the room from one that is not. Red is the
   danger colour on purpose — the one the site keeps for things that are
   happening and matter — and it pulses gently unless the reader has asked for
   less motion. Before Start the badge is amber and still. Folded, the mark
   alone sits on the icon's corner. */
.rail-link { position: relative; }
.rail-badge {
  display: inline-flex; align-items: center; gap: .3rem;
  margin-inline-start: auto; padding: .15rem .45rem .15rem .4rem; border-radius: 999px;
  font-family: var(--font-label); font-size: .6rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .08em;
}
.rail-badge-mark {
  width: 0; height: 0;
  border-block: .28rem solid transparent; border-inline-start: .42rem solid currentColor;
}
.rail-badge--live { background: var(--danger-bg); color: var(--text-inverse); }
.rail-badge--waiting { background: var(--warning-surface); color: var(--warning-text); }
.rail-badge--paused { background: var(--warning-surface); color: var(--warning-text); }
/* A pause mark, two bars in currentColor, and no pulse: held, not happening. */
.rail-badge--paused .rail-badge-mark {
  width: .42rem; height: .42rem; border: 0;
  border-inline: .14rem solid currentColor;
}
.rail-badge--waiting .rail-badge-mark {
  width: .4rem; height: .4rem; border: 0; border-radius: 50%; background: currentColor;
}
@media (prefers-reduced-motion: no-preference) {
  .rail-badge--live { animation: rail-live-pulse 2s ease-in-out infinite; }
}
@keyframes rail-live-pulse { 50% { opacity: .65; } }
@media (min-width: 62.01rem) {
  html[data-rail="collapsed"] .rail-badge {
    position: absolute; inset-block-start: .2rem; inset-inline-end: .2rem; padding: .2rem;
  }
  html[data-rail="collapsed"] .rail-badge-label { display: none; }
}
@media (min-width: 62.01rem) {
  html[data-rail="collapsed"] .rail-note,
  html[data-rail="collapsed"] .rail-sublinks { display: none; }
}

/* Live lessons: the Overview and the rail's lesson controls ---------------
   The Overview is read in a glance from the front of a room, so it says
   three things and each once: where the class is, what to do next, and who
   needs the teacher. The class is one bar rather than four equal tiles —
   four tiles weigh "0 Done" the same as "15 Practice", where the bar shows
   the shape of the room before any figure is read. The figures sit under it
   for the exact count and for a screen reader, which is not shown the bar.

   Calm when nothing needs the teacher: one card, no colour but the class's
   own. Amber appears only with a pupil waiting to be let in — amber, not the
   error red, because somebody arriving late is a press to make, not a fault.

   The time left, the end time and End lesson are in the rail, under Live
   lesson, on every teacher screen: one place for each, and not in the way of
   the class. */

.live-head { margin-bottom: 1.5rem; }
.live-head h1 { margin: 0; }
.live-head .lede { margin: .3rem 0 0; }

.live-card {
  margin: 0 0 1.25rem; padding: 1.5rem 1.6rem;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
}
.live-card-title { margin: 0 0 .35rem; font-size: 1.15rem; }
.live-card-help { margin: 0; color: var(--text-muted); }
.live-card-note { margin-top: 1rem; }
.live-card-foot { margin: 1.25rem 0 0; }

/* Start: the one press the waiting room is for, under a rule and larger than
   an ordinary button, with what it does beside it. */
.live-start {
  display: flex; flex-wrap: wrap; align-items: center; gap: .75rem 1.25rem;
  margin: 1.5rem 0 0; padding-top: 1.25rem; border-top: 1px solid var(--border);
}
.live-start button.action { padding: .8rem 1.6rem; font-size: 1rem; }
.live-start .live-card-help { flex: 1 1 18rem; }

@media (max-width: 40rem) {
  .live-card { padding: 1.1rem 1rem; }
  .live-start button.action { flex: 1 1 100%; }
}

/* The rail's time left and End lesson: two more lines under Live lesson,
   sized and indented like Overview and Pupils so the four read as one list.
   The time is the figure and the word, tabular so it does not jitter as it
   ticks, and opens the Time screen; amber in the last five minutes and red
   once it is up, the whole line, so it is seen from the corner of an eye.
   End lesson is the same line in the danger red; it opens to one sentence
   and a solid button rather than posting at once. */
.live-clock { margin: 0; line-height: 1.1; }
.live-clock-figure {
  font-family: var(--font-display); font-weight: var(--weight-display);
  font-size: 1.75rem; font-variant-numeric: tabular-nums;
}
.live-clock-word {
  font-family: var(--font-label); font-size: .72rem; color: var(--text-muted);
  white-space: pre;
}
.live-clock--soon .live-clock-figure { color: var(--warning-text); }
.live-clock--over .live-clock-figure { color: var(--error-text); font-size: 1.1rem; }

.rail-sublink--time .live-clock-figure {
  font-family: var(--font-label); font-size: .72rem; font-weight: 600; color: inherit;
}
.rail-sublink--time .live-clock-word { font-size: .72rem; color: inherit; }
.rail-sublink--time:has(.live-clock--soon) {
  color: var(--warning-text); background: var(--warning-surface); border-inline-start-color: var(--warning);
}
.rail-sublink--time:has(.live-clock--over) {
  color: var(--error-text); background: var(--error-surface); border-inline-start-color: var(--danger-bg);
}
.rail-end > summary { list-style: none; cursor: pointer; }
.rail-end > summary::-webkit-details-marker { display: none; }
.rail-sublink--end { display: block; color: var(--error-text); }
.rail-sublink--end:hover, .rail-end[open] > .rail-sublink--end {
  color: var(--error-text); background: var(--error-surface); border-inline-start-color: var(--danger-bg);
}
.rail-end-form { display: flex; flex-direction: column; gap: .45rem; margin: .35rem 0 .3rem; padding-inline-start: .6rem; }
.rail-end-form button.danger { width: 100%; }

/* Pause, one line between the time and End lesson: the action colour, never
   the danger red End lesson wears, so the two cannot be mistaken from a step
   back. No confirm step, because it is reversible in one press; while paused
   the same line is Resume, in weight, and the banner in <main> carries a
   second one. */
.rail-pause { margin: 0; }
.rail-sublink--pause, .rail-sublink--resume {
  display: block; width: 100%; text-align: start; background: transparent;
  border-block: 0; border-inline-end: 0; cursor: pointer; color: var(--text-link);
}
.rail-sublink--pause:hover, .rail-sublink--pause:focus-visible,
.rail-sublink--resume:hover, .rail-sublink--resume:focus-visible {
  background: var(--surface-sunken); border-inline-start-color: var(--action-bg);
}
.rail-sublink--resume { font-weight: 600; border-inline-start-color: var(--action-bg); }
/* Every rail line is a 44px target on a touch screen, Pause included. */
@media (pointer: coarse) {
  .rail-sublink { min-height: 2.75rem; display: flex; align-items: center; }
}

/* While paused, on every teacher screen: at the top of <main>, amber with a
   heavy start edge, and the Resume beside the words. */
.live-paused-banner {
  display: flex; flex-wrap: wrap; align-items: center; gap: .75rem;
  margin: 0 0 1.25rem; padding: .7rem 1rem;
  border: 1px solid var(--warning); border-inline-start-width: 4px;
  border-radius: var(--radius-md);
  background: var(--warning-surface); color: var(--warning-text);
}
.live-paused-banner p { margin: 0; }
.live-paused-on { display: block; margin-top: .2rem; font-weight: 600; }
.live-paused-resume { margin: 0; margin-inline-start: auto; }

/* The Time screen: the clock large, then the presses, then a set time. */
.live-time { display: grid; gap: 1.5rem; }
.live-time-clock .live-clock-figure { font-size: 3.2rem; }
.live-time-clock .live-card-help { margin-top: .35rem; }

/* The wall (/teacher/live/:id/wall): the class-level half of the grid, for the
   projector at the front of the room. Read from the back row, through a
   projector that washes colour out, so size and contrast carry it: every
   figure scales with the viewport rather than the type setting, because the
   screen is the room's and not the teacher's, and every colour is the theme's
   strongest pairing — ink on the plain surface, a heavy border, the grid's
   phase colours only on the figures. Nothing per pupil exists to style.
   Paused, the screen is "Look up" and nothing else: the one thing the room
   should do is the one thing on the wall. */
body.live-wall-shell { background: var(--surface); }
.live-wall-open { margin: -.75rem 0 1rem; }
.live-wall {
  min-height: 100vh; box-sizing: border-box;
  display: flex; flex-direction: column; gap: 3vh;
  padding: 3vh 4vw;
  background: var(--surface); color: var(--text);
}
.live-wall-head { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: flex-start; gap: 1rem 3vw; }
.live-wall-head h1 { margin: 0; font-size: clamp(1.6rem, 3.6vw, 4rem); line-height: 1.1; }
.live-wall-lesson { margin: .4rem 0 0; font-size: clamp(1.1rem, 2.2vw, 2.4rem); color: var(--text-muted); }
.live-wall-clock { text-align: end; }
.live-wall-clock .live-clock-figure { font-size: clamp(3rem, 8vw, 9rem); line-height: 1; }
.live-wall-clock .live-clock-word { font-size: clamp(1.1rem, 2.2vw, 2.4rem); }
.live-wall-clock .live-clock--over .live-clock-figure { font-size: clamp(2rem, 5vw, 5.5rem); }
.live-wall-ended { margin: 0; font-size: clamp(1.6rem, 3.6vw, 4rem); font-weight: 700; }

.live-wall-counts {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: 2vw;
}
.live-wall-col {
  display: flex; flex-direction: column; align-items: center; gap: 1vh;
  padding: 2.5vh 1vw;
  border: 3px solid var(--border-strong); border-radius: var(--radius-lg);
}
.live-wall-count {
  font-family: var(--font-display); font-weight: var(--weight-display);
  font-size: clamp(4rem, 13vw, 16rem); line-height: 1; font-variant-numeric: tabular-nums;
}
.live-wall-count--none { color: var(--text-muted); }
.live-wall-col--baseline .live-wall-count:not(.live-wall-count--none) { color: var(--phase-baseline-line); }
.live-wall-col--recap .live-wall-count:not(.live-wall-count--none) { color: var(--phase-recap-line); }
.live-wall-col--practice .live-wall-count:not(.live-wall-count--none) { color: var(--phase-practice-line); }
.live-wall-col--done .live-wall-count:not(.live-wall-count--none) { color: var(--success-text); }
.live-wall-col--in .live-wall-count:not(.live-wall-count--none) { color: var(--text-link); }
.live-wall-label { font-family: var(--font-label); font-weight: 600; font-size: clamp(1rem, 2.2vw, 2.6rem); text-align: center; }

.live-wall-hinge, .live-wall-rightnow { border-top: 3px solid var(--border-strong); padding-top: 2.5vh; }
.live-wall-section-label, .live-wall-idea-label {
  margin: 0; font-family: var(--font-label); font-weight: 600;
  font-size: clamp(1rem, 2vw, 2.2rem); color: var(--text-muted);
}
.live-wall-idea { margin: .3em 0 0; font-size: clamp(1.6rem, 3.6vw, 4.4rem); line-height: 1.2; }
.live-wall-rightnow-counts { display: flex; flex-wrap: wrap; gap: .4em 2em; margin: .6em 0 0; font-size: clamp(1.2rem, 2.4vw, 2.8rem); }
.live-wall-hinge-stem { margin: .3em 0 0; font-size: clamp(1.4rem, 3vw, 3.6rem); line-height: 1.2; }
.live-wall-hinge-options {
  list-style: none; margin: 2vh 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 1.5vw;
}
.live-wall-hinge-option {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 1.2vw;
  padding: 1.5vh 1.5vw;
  border: 3px solid var(--border-strong); border-radius: var(--radius-lg);
  font-size: clamp(1.1rem, 2.2vw, 2.6rem);
}
.live-wall-hinge-letter { font-weight: 700; }
.live-wall-hinge-count {
  font-family: var(--font-display); font-weight: var(--weight-display);
  font-size: clamp(2.6rem, 6.5vw, 8rem); line-height: 1; font-variant-numeric: tabular-nums;
}
.live-wall-hinge-answered { margin: 1.5vh 0 0; font-size: clamp(1.2rem, 2.4vw, 2.8rem); }

.live-wall--paused { justify-content: center; text-align: center; }
.live-wall-pause { flex: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 3vh; }
.live-wall-look-up { margin: 0; font-size: clamp(4rem, 17vw, 20rem); line-height: 1; }
.live-wall--paused .live-wall-idea { max-width: 40ch; }
.live-wall-back { margin: auto 0 0; }
.live-time-add, .live-time-set { padding-top: 1.25rem; border-top: 1px solid var(--border); }
.live-time-options { display: flex; flex-wrap: wrap; gap: .6rem; margin: .6rem 0 .5rem; }
.live-time-options form { margin: 0; }
.live-time-options button.action { min-width: 6.5rem; padding: .75rem 1.2rem; font-size: 1rem; }
.live-time-set-row { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; margin: .6rem 0 0; }
.live-time-set-row label { font-family: var(--font-label); font-size: .78rem; color: var(--text-muted); }
.live-time-set-row input[type=time] { width: auto; }

/* Folded to icons: the clock alone, small, under the icon. The two controls
   need words and a field, which a 4rem column has no room for; unfolding the
   rail is one press, and the lesson's own screens are the icon's link. */
@media (min-width: 62.01rem) {
  /* Folded to icons the sub-entries go, but the time left stays, small, under
     the icon: a 4rem column has room for "31:05" and not for the rest. */
  html[data-rail="collapsed"] .rail-sublinks:has(.rail-sublink--time) {
    display: flex; margin: 0 0 .3rem; padding: 0; align-items: center;
  }
  html[data-rail="collapsed"] .rail-sublinks > :not(.rail-sublink--time) { display: none; }
  html[data-rail="collapsed"] .rail-sublink--time { border: 0; padding: .2rem 0; }
  html[data-rail="collapsed"] .live-clock-word {
    position: absolute; width: 1px; height: 1px; margin: -1px;
    padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }
}

/* A strip across the top on a narrow screen: the lesson takes a line of its
   own under the links, clock then end time then End lesson, and an opened
   control drops below the line rather than pushing it apart. */
@media (max-width: 62rem) {
  /* Overview, Pupils, the time and End lesson on the strip's own line. */
  .rail-sublinks { flex-direction: row; flex-wrap: wrap; gap: .3rem; margin: 0; padding-inline-start: 0; }
  .rail-sublink { border-inline-start: 0; border-bottom: 2px solid transparent; border-radius: 0; }
  .rail-sublink.active { border-bottom-color: var(--text-link); }
  .rail-end[open] { flex-basis: 100%; }
}

/* Live lessons: the class and one pupil -------------------------------------
   The live screen is a register grid, read from a step back, often while
   walking the room: every pupil is a row that never moves, the modes run
   across the top, and each column's head is its count, so the shape of the
   class is the first thing read. The count wears the phase's own colour, the
   one the pupil's screen wears. Progress is a fraction drawn (a bar per
   phase, done and left as flex-grow) with its count beside it, never a
   percentage; the one-pupil page keeps marks (components/LivePupil.tsx), where
   each phase has room. A row that needs the teacher wears an amber edge and a
   mark, because it is a conversation to have, not a fault, and it does not
   move. One
   pupil's page is a column of what is happening now and a narrower one of how
   the lesson has gone. */

.live-card--start { padding: 1.1rem 1.25rem; }
.live-card--start .live-start { margin: 0; padding-top: 0; border-top: 0; }

/* The register: pupils down, the modes across, one row per pupil under thin
   rules (v3, 9 October 2026). A real table, so a reader hears the name and
   where they are, then the column. Columns: the name, the marks, and the five
   modes, each headed by its count in the stat tile's figure style and the
   phase's colour, every label on one line so the five counts share one
   baseline. Fixed layout, so the bars sit exactly under their headers and
   every column is on screen at a laptop's width. Its own wrapper scrolls
   sideways only where five columns cannot fit (the container query below);
   wider than that the head sticks to the page's own scroll. */
/* The head is a line shorter than other teacher pages': every row it gives
   back is a pupil on the first screen of a laptop. */
.teacher.live-register { padding-top: 1.5rem; }
.live-register-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; margin: 0 0 .75rem; }
.live-register-head .live-head { margin: 0; }
.live-register-head .live-head h1 { margin: 0 0 .1rem; }
.live-register-head .live-head .lede { margin: 0; }
.live-register-head .live-wall-open { margin: 0; }
.live-grid-scroll { margin: 0 0 1rem; }
.live-grid {
  width: 100%; border-collapse: separate; border-spacing: 0; overflow: visible; table-layout: fixed;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md);
}
.live-grid th, .live-grid td { box-sizing: border-box; }
.live-grid-pupil, .live-grid-name { width: 11rem; text-align: start; }
.live-grid-marks-head, .live-grid-marks { width: 4.6rem; }
.live-grid-col--not_signed_in { width: 7.5rem; }
.live-grid-col--done { width: 6.75rem; }
.live-grid-col--in { width: auto; }
.live-grid thead { position: sticky; top: 0; z-index: 2; }
.live-grid thead th {
  padding: .5rem .6rem .45rem; vertical-align: bottom; text-align: start;
  background: var(--surface); border-bottom: 2px solid var(--border);
}
.live-grid thead th:first-child { border-start-start-radius: var(--radius-md); }
.live-grid thead th:last-child { border-start-end-radius: var(--radius-md); }
.live-grid-pupil {
  font-family: var(--font-label); font-size: .74rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted);
}
/* The count is the type: the stat tile's own figure, set as a column head. */
.live-grid-count {
  display: block; font-family: var(--font-display); font-weight: var(--weight-display);
  font-size: 2rem; line-height: 1; font-variant-numeric: tabular-nums; color: var(--text);
}
.live-grid-count--none { color: var(--text-muted); }
.live-grid-label {
  display: block; margin-top: .15rem; white-space: nowrap;
  font-family: var(--font-label); font-size: .74rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted);
}
.live-grid-col--baseline .live-grid-count:not(.live-grid-count--none) { color: var(--phase-baseline-line); }
.live-grid-col--recap .live-grid-count:not(.live-grid-count--none) { color: var(--phase-recap-line); }
.live-grid-col--practice .live-grid-count:not(.live-grid-count--none) { color: var(--phase-practice-line); }
.live-grid-col--done .live-grid-count:not(.live-grid-count--none) { color: var(--success-text); }
.live-grid-col--in .live-grid-count:not(.live-grid-count--none) { color: var(--text-link); }
.live-grid thead .live-grid-col--baseline { border-bottom-color: var(--phase-baseline-line); }
.live-grid thead .live-grid-col--recap { border-bottom-color: var(--phase-recap-line); }
.live-grid thead .live-grid-col--practice { border-bottom-color: var(--phase-practice-line); }
.live-grid thead .live-grid-col--done { border-bottom-color: var(--success); }

/* Above the register: who needs you, and the Right-now line beside it. The
   figures are counts of pupils, never a share of them, in the warm wash with
   their marks, readable from the door; nought is drawn quiet, never hidden.
   The Right-now line is the class's one idea, nobody named — "6 of 24 on",
   the idea quoted (it is the wrong idea the class is on), the recent wrong
   answers muted after it, and Pause on this at the end. It keeps its height
   while the lesson runs, so an idea arriving on a poll moves no row. */
.live-glance { display: flex; align-items: stretch; gap: 1rem; margin: 0 0 .75rem; }
.live-figures { display: flex; flex: none; gap: .5rem; margin: 0; padding: 0; list-style: none; }
.live-figure {
  display: flex; flex-direction: column; justify-content: flex-end; gap: .2rem;
  min-width: 6.25rem; padding: .4rem .8rem;
  border: 1px solid var(--warning); border-radius: var(--radius-md); background: var(--warning-surface);
}
.live-figure-count {
  display: flex; align-items: center; gap: .35rem;
  font-family: var(--font-display); font-weight: var(--weight-display);
  font-size: 2.1rem; line-height: 1; font-variant-numeric: tabular-nums; color: var(--warning-text);
}
.live-figure-count svg { flex: none; width: 1.5rem; height: 1.5rem; }
.live-figure-label {
  font-family: var(--font-label); font-size: .74rem; font-weight: 600; white-space: nowrap;
  text-transform: uppercase; letter-spacing: .08em; color: var(--warning-text);
}
.live-figure--none { border-color: var(--border); background: var(--surface); }
.live-figure--none .live-figure-count, .live-figure--none .live-figure-label { color: var(--text-muted); }
.live-rightnow {
  display: flex; flex: 1 1 auto; align-items: center; gap: 1rem; min-width: 0; min-height: 3.75rem;
  padding: .3rem .9rem; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface);
}
.live-rightnow-label {
  flex: none; font-family: var(--font-label); font-size: .78rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted);
}
.live-rightnow-what {
  flex: 1 1 auto; min-width: 0; margin: 0; display: flex; flex-wrap: wrap;
  align-items: baseline; gap: .2rem .6rem; font-size: .95rem; line-height: 1.35; color: var(--text);
}
.live-rightnow-count { font-family: var(--font-label); font-weight: 600; font-variant-numeric: tabular-nums; }
.live-rightnow-idea { font-weight: 600; }
.live-rightnow-wrong { font-size: .9rem; font-variant-numeric: tabular-nums; color: var(--text-muted); }
.live-rightnow-pause { flex: none; margin: 0; }
/* Empty, it keeps its place so an idea arriving moves no row, and draws
   nothing in it: an empty outline read as something missing (review C25). */
.live-rightnow--empty { visibility: hidden; }
/* The safeguarding chip at the end of the glance strip (Safeguarding_Lock_Plan
   5A): that something came in and when, as tall as the figures beside it, and
   red-edged because it is the one thing on this screen that is not about
   learning. No name and no reason: this screen is the one on the board. */
.live-sg {
  display: flex; flex: none; align-items: center; gap: .7rem;
  padding: .4rem .9rem; border: 2px solid var(--error); border-radius: var(--radius-md);
  background: var(--surface); color: var(--text); text-decoration: none;
}
.live-sg-icon { width: 1.4rem; height: 1.4rem; color: var(--error-text); }
.live-sg-text { display: flex; flex-direction: column; }
.live-sg-text strong { font-size: .95rem; }
.live-sg-text span { font-size: .8rem; color: var(--text-muted); }
.live-sg-go {
  margin-inline-start: .4rem; padding: .35rem .8rem; border-radius: var(--look-radius-control);
  background: var(--action-bg); color: var(--action-ink); font-weight: 600; font-size: .85rem;
}
.live-sg:hover .live-sg-go { background: var(--action-bg-hover); }
.live-sg:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* Unlocked, within the live window (5B): who and why above the register, red
   at the start as the chip was, and a shield in the marks column of the row. */
.live-sg-open {
  margin: 0 0 .75rem; padding: .75rem .9rem; border: 1px solid var(--border);
  border-inline-start: 4px solid var(--error); border-radius: var(--radius-md); background: var(--surface);
}
.live-sg-open .sg-list { margin: 0; }
.live-mark-sign--concern { background: var(--error); color: var(--text-inverse); }
.live-mark-sign--concern .icon { width: 1rem; height: 1rem; }
.live-grid-row--concern > .live-grid-name { border-inline-start-color: var(--error); }
.live-rightnow-pause button { padding: .3rem .8rem; font-size: .82rem; }

/* The hinge's options on a session's Insight, the right one ticked: drawn
   here, not written; the words beside it are for screen readers. */
.live-hinge-option--correct .live-hinge-letter::after { content: " \2713"; }
.live-hinge-letter { font-family: var(--font-label); font-weight: 600; }
.live-hinge-options { margin: 0 0 .5rem; padding: 0; list-style: none; font-size: .95rem; line-height: 1.4; color: var(--text); }
.live-hinge-option--correct { color: var(--success-text); font-weight: 600; }

/* The hinge questions on a session's Insight: each as asked, its counts beside
   its options, and how many answered of how many it asked. */
.insight-hinges { display: grid; gap: 1rem; margin: 0; padding: 0; list-style: none; }
.insight-hinge-stem { margin: 0 0 .4rem; font-weight: 600; color: var(--text); }
.insight-hinge-when { font-weight: 400; font-size: .9rem; color: var(--text-muted); }
.insight-hinge-answered { margin: 0; font-size: .9rem; font-variant-numeric: tabular-nums; color: var(--text-muted); }

/* A row: thin rules, never a zebra, never a box. The row lifts under the
   pointer or when its link has focus. The whole row is the link: the name's
   box is stretched over the row, and Let in and the card sit above that.
   Needs you is a thin amber edge on the name and the marks — no wash: a
   quarter of the page turning amber was the loudest thing on it. Finished is
   the success surface with the name in its ink; not signed in and off the
   register are muted, and off the register sunken. */
.live-grid-row { position: relative; }
.live-grid-row > th, .live-grid-row > td {
  height: 2.3rem; padding: .25rem .6rem; vertical-align: middle;
  background: var(--surface); border-bottom: 1px solid var(--border);
}
.live-grid tbody tr:last-child > th, .live-grid tbody tr:last-child > td { border-bottom: 0; }
.live-grid-row:hover > th, .live-grid-row:hover > td,
.live-grid-row:focus-within > th, .live-grid-row:focus-within > td,
.live-grid-row.is-open > th, .live-grid-row.is-open > td { background: var(--surface-sunken); }
.live-grid-name { font-weight: 400; border-inline-start: 4px solid transparent; }
.live-grid-link {
  font-size: 1rem; font-weight: 600; line-height: 1.25; color: var(--text);
  text-decoration: none; overflow-wrap: anywhere;
}
.live-grid-link:hover { text-decoration: underline; }
.live-grid-link::after { content: ""; position: absolute; inset: 0; }
.live-grid-link:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.live-grid-link:focus-visible::after { outline: 2px solid var(--focus-ring); outline-offset: -2px; }
.live-grid-row--needs > .live-grid-name { border-inline-start-color: var(--warning); }
.live-grid-row--done > th, .live-grid-row--done > td { background: var(--success-surface); }
.live-grid-row--done .live-grid-link { color: var(--success-text); }
.live-grid-row--not_signed_in .live-grid-link,
.live-grid-row--absent .live-grid-link,
.live-grid-row--waiting_to_join .live-grid-link { color: var(--text-muted); font-weight: 500; }
.live-grid-row--absent > th, .live-grid-row--absent > td,
.live-grid-row--waiting_to_join > th, .live-grid-row--waiting_to_join > td { background: var(--surface-sunken); }
.live-grid-row--waiting_to_join > .live-grid-name { border-inline-start-color: var(--border-strong); }
/* Pupils ticked but not signed in, once the lesson runs: under the class, a
   group of their own with its name and count, not a column of dots. */
.live-grid-divider > th {
  padding: .9rem .9rem .4rem; text-align: start; background: var(--surface-sunken);
  border-top: 2px solid var(--border); border-bottom: 1px solid var(--border);
  font-family: var(--font-label); font-size: .72rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted);
}
.live-grid-divider-count {
  display: inline-block; margin-inline-start: .35rem; padding: 0 .45rem; border-radius: var(--radius-pill);
  background: var(--surface); letter-spacing: 0; font-variant-numeric: tabular-nums;
}
.live-grid-cell--off { white-space: nowrap; }
.live-off-words { font-size: .86rem; color: var(--text-muted); vertical-align: middle; }
.live-grid-add { display: inline-block; position: relative; z-index: 1; margin: 0 0 0 .6rem; vertical-align: middle; }
.live-grid-add button { padding: .2rem .65rem; font-size: .78rem; }

/* The marks: a hand, a clock. Big enough to find across the room; a solid disc
   for the hand, an amber ring for the clock, so the two differ in shape as
   well as in drawing. They sit above the row's link so a tooltip can show. */
.live-grid-marks { position: relative; padding-inline: .35rem; white-space: nowrap; }
.live-mark-sign {
  display: inline-flex; position: relative; z-index: 1; align-items: center; justify-content: center;
  width: 1.75rem; height: 1.75rem; border-radius: 50%; vertical-align: middle;
}
.live-mark-sign + .live-mark-sign { margin-inline-start: .25rem; }
.live-mark-sign svg { width: 1.1rem; height: 1.1rem; }
.live-mark-sign--hand { background: var(--primary-bg); color: var(--text-inverse); }
.live-mark-sign--stuck { background: var(--surface); color: var(--warning-text); box-shadow: inset 0 0 0 2px var(--warning); }

/* The bars: one segment per phase column, under its header. Passed is one
   quiet grey, full; the phase they are in takes its colour and fills done:left
   (two flex-grow values, never a share); to come is empty; Done is teal to
   the end. A count slot of fixed width ends every bar cell, empty in all but
   the pupil's own, so the three tracks keep the same width. */
.live-grid-cell { padding-inline: .5rem; }
.live-track { display: flex; align-items: center; gap: .45rem; }
.live-seg {
  display: flex; flex: 1 1 auto; height: .5rem; overflow: hidden; border-radius: 3px;
  background: var(--surface-sunken); box-shadow: inset 0 0 0 1px var(--border);
}
.live-seg-done, .live-seg-left { display: block; }
.live-seg--past .live-seg-done { background: var(--text-muted); opacity: .35; }
.live-seg--finished { box-shadow: none; }
.live-seg--finished .live-seg-done { background: var(--success); }
.live-grid-cell--baseline .live-seg--here .live-seg-done { background: var(--phase-baseline-line); }
.live-grid-cell--recap .live-seg--here .live-seg-done { background: var(--phase-recap-line); }
.live-grid-cell--practice .live-seg--here .live-seg-done { background: var(--phase-practice-line); }
.live-grid-cell--review .live-seg--here .live-seg-done { background: var(--phase-review-line); }
.live-grid-cell--challenge .live-seg--here .live-seg-done { background: var(--phase-challenge-line); }
.live-count-slot {
  flex: none; width: 2.6rem; font-size: .76rem; line-height: 1;
  font-variant-numeric: tabular-nums; color: var(--text-muted); white-space: nowrap;
}
.live-grid-row--needs .live-count-slot { color: var(--text); }
.live-grid-cell--not_signed_in, .live-grid-cell--in { text-align: center; }
.live-dot {
  display: inline-block; flex: none; width: .6rem; height: .6rem; border-radius: 50%;
  background: var(--text-muted); vertical-align: middle;
}
.live-grid-cell--in .live-dot { background: var(--action-bg); }
.live-finished { display: inline-flex; align-items: center; gap: .35rem; vertical-align: middle; color: var(--success-text); white-space: nowrap; }
.live-finished svg { width: 1.3rem; height: 1.3rem; }
.live-finished-time { font-size: .8rem; font-variant-numeric: tabular-nums; }

/* The hover card: one pupil, now. Beside the marks, never over the name or
   them, and moved to fit the window (components/LiveRegisterCard.tsx places
   it). One label column, one order, no tinted boxes. */
.live-peek {
  position: absolute; z-index: 6; left: 100%; top: 0; width: min(34rem, calc(100vw - 2rem)); overflow-y: auto;
  border: 1px solid var(--border-strong); border-radius: var(--radius-lg);
  background: var(--surface-raised); color: var(--text); box-shadow: var(--shadow-menu);
  font-size: .86rem; line-height: 1.38; white-space: normal; text-align: start; cursor: default;
}
.live-peek-head { padding: .6rem 1rem .55rem; border-bottom: 1px solid var(--border); }
.live-peek-name { margin: 0; font-family: var(--font-display); font-weight: var(--weight-display); font-size: 1.15rem; line-height: 1.2; }
.live-peek-where { margin: .1rem 0 0; font-size: .9rem; font-weight: 600; }
.live-peek-muted { color: var(--text-muted); font-weight: 400; }
.live-peek-phases { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .75rem; margin: .55rem 0 0; padding: 0; list-style: none; }
.live-peek-phase { display: grid; gap: .25rem; }
.live-peek-phase-line { display: flex; justify-content: space-between; align-items: baseline; gap: .5rem; }
.live-peek-phase-word { font-weight: 600; font-size: .82rem; }
.live-peek-phase--baseline .live-peek-phase-word { color: var(--phase-baseline-line); }
.live-peek-phase--recap .live-peek-phase-word { color: var(--phase-recap-line); }
.live-peek-phase--practice .live-peek-phase-word { color: var(--phase-practice-line); }
.live-peek-phase-count { font-size: .82rem; font-variant-numeric: tabular-nums; color: var(--text-muted); white-space: nowrap; }
.live-seg--card { height: .45rem; }
.live-peek-phase--baseline .live-seg-done { background: var(--phase-baseline-line); }
.live-peek-phase--recap .live-seg-done { background: var(--phase-recap-line); }
.live-peek-phase--practice .live-seg-done { background: var(--phase-practice-line); }
.live-peek-body { margin: 0; padding: .3rem 1rem .2rem; }
.live-peek-row { display: grid; grid-template-columns: 8.5rem minmax(0, 1fr); gap: .5rem; padding: .32rem 0; border-bottom: 1px solid var(--border); }
.live-peek-row:last-child { border-bottom: 0; }
.live-peek-row dt { margin: 0; font-weight: 600; color: var(--text-muted); }
.live-peek-row dd { margin: 0; overflow-wrap: anywhere; }
.live-peek-row--needs dt { color: var(--warning-text); }
.live-peek-needs { display: grid; gap: .2rem; margin: 0; padding: 0; list-style: none; }
.live-peek-needs li { display: flex; align-items: flex-start; gap: .45rem; font-weight: 600; color: var(--warning-text); }
.live-peek-needs svg { flex: none; width: 1.05rem; height: 1.05rem; margin-top: .15rem; }
.live-peek-note { display: block; color: var(--text-muted); font-weight: 400; }
.live-peek q { quotes: "\201C" "\201D"; }
.live-peek-recent { display: grid; gap: .1rem; margin: 0; padding: 0; list-style: none; }
.live-peek-verdict { font-weight: 600; }
.live-peek-verdict--right { color: var(--success-text); }
.live-peek-verdict--not_right, .live-peek-verdict--near_miss { color: var(--warning-text); }
.live-peek-ideas { margin: 0; padding: 0 0 0 1rem; }
.live-peek-times { color: var(--text-muted); font-variant-numeric: tabular-nums; }
.live-peek-foot { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: .5rem 1rem .6rem; border-top: 1px solid var(--border); }
.live-peek-open { display: inline-flex; align-items: center; gap: .2rem; font-weight: 600; }
/* A button, not a link in the text (review C24): the card's way on. */
.live-peek-open.action-link { padding: .4rem .85rem; font-size: .84rem; }
.live-peek-open svg { width: 1rem; height: 1rem; }
.live-peek-visited { margin: 0; }
.live-peek-visited button { padding: .3rem .8rem; font-size: .82rem; }

/* Only where five columns cannot fit beside the names: the grid keeps a
   readable width and scrolls sideways inside its own box, the names pinned.
   Wider than this it is part of the page, and its header pins to the page's
   own scroll — a second scrollbar inside a laptop screen hid rows for nothing.
   The hover card is off here: the scroll box would clip it, and the name is
   still the link to the pupil's page.

   ⚠️ A CONTAINER QUERY, NOT A MEDIA QUERY, AND THE REASON IS TEXT SIZE. A
   media query's rem is the browser's 16px whatever the teacher chose under
   Appearance; a container query's rem is the root's computed size. At the
   largest text size 48rem is 1152px, so a 1280px laptop with the rail open
   is "too narrow" and scrolls, where a media query said it fitted and the
   column labels were cut off mid-word ("BASELIN", "PRACTIC"). It also sees
   the rail: folded, the grid has the room and stops scrolling. */
.live-register { container: live-register / inline-size; }
@container live-register (max-width: 52rem) {
  .live-glance { flex-wrap: wrap; }
  .live-grid-scroll { overflow-x: auto; }
  .live-grid { min-width: 52rem; }
  .live-grid thead .live-grid-pupil, .live-grid-name { position: sticky; left: 0; z-index: 1; }
  .live-grid thead .live-grid-pupil { z-index: 3; }
  .live-grid-link { display: block; }
  .live-grid-link::after { display: none; }
  .live-grid-row { position: static; }
  .live-peek { display: none; }
}

/* The pupil's hand, in the masthead while they work in a lesson in the room
   (components/LiveHand.tsx). The teacher reads this button off the pupil's
   screen from across the room, so the two states have to differ at that
   distance, not just up close. Down is the solid primary fill — the dark pill
   every screen in the room shows, so a teacher knows where to look. Up is the
   amber the product uses for "needs you" (the stuck ring on the register, the
   hand-offs on the class page): a dark pill turning amber is a change of
   colour, which carries across a room; the earlier design, a white pill on the
   white masthead for down, was the quieter state yet looked pressed, and a
   navy pill for up was the same dark as the teacher's register disc — a
   different screen, and not what the teacher was looking at. The register
   keeps its navy disc: there the hand is a mark among marks, not a signal
   across a room. The text is --warning-ink, the ink for an amber fill;
   --warning-text is deep amber, for words on a pale warning surface, and
   would not read on amber. */
.live-hand { display: inline-flex; align-items: center; gap: .5rem; margin: 0 .75rem 0 0; }
/* A press that never left the browser (components/LiveHand.tsx): beside the
   button, in the alert's colours, so the page under it is untouched. */
.live-hand-unsent { padding: .2rem .5rem; border-radius: var(--radius-md); background: var(--error-surface); color: var(--error-text); font-size: .86rem; }
.live-hand-button {
  display: inline-flex; align-items: center; gap: .4rem; padding: .35rem .8rem; font-family: inherit;
  border: 1px solid var(--primary-bg); border-radius: var(--radius-pill);
  background: var(--primary-bg); color: var(--text-inverse); font-size: .9rem; font-weight: 600; cursor: pointer;
}
.live-hand-button svg { flex: none; width: 1.15rem; height: 1.15rem; }
.live-hand-button:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.live-hand--up .live-hand-button { border-color: var(--warning); background: var(--warning); color: var(--warning-ink); }

/* One mark per step: filled for done. */
.live-marks { display: inline-flex; flex-wrap: wrap; gap: 3px; }
.live-mark {
  width: .55rem; height: .55rem; border-radius: 50%;
  border: 1px solid var(--border-strong); background: transparent;
}
.live-mark--done { background: var(--text-muted); border-color: var(--text-muted); }
/* In the phase's own line colour, so the marks are the phase's and not the
   amber that means "handed to you". */
.live-phase--baseline .live-mark--done { background: var(--phase-baseline-line); border-color: var(--phase-baseline-line); }
.live-phase--recap .live-mark--done { background: var(--phase-recap-line); border-color: var(--phase-recap-line); }
.live-phase--practice .live-mark--done { background: var(--phase-practice-line); border-color: var(--phase-practice-line); }

/* One pupil. The desk briefing first: the name, then the lines read on the
   walk to the desk, set at reading size rather than as a display heading so
   that on a 390px phone the name, all of the brief and Visited sit inside the
   first screen. The heading the other live screens open with (the class, 2rem)
   is one quiet line under it here; it had pushed the brief below the fold. */
.live-desk-name { margin: 0 0 .5rem; font-size: 1.35rem; line-height: 1.2; overflow-wrap: anywhere; }
.live-brief {
  display: flex; flex-direction: column; gap: .45rem;
  margin-bottom: 1.25rem; padding: .8rem .9rem;
  border: 1px solid var(--border); border-inline-start: 4px solid var(--action-bg);
  border-radius: var(--radius-md); background: var(--surface);
}
.live-brief p { margin: 0; overflow-wrap: anywhere; }
.live-brief-where { font-weight: 600; }
.live-brief-label {
  font-family: var(--font-label); font-size: .72rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted);
}
.live-brief-words { font-style: normal; }
/* Amber, as on the Go-to line: it is the same reason, and amber means "needs you". */
.live-brief-why { font-weight: 600; color: var(--warning-text); }
.live-brief-question { font-weight: 600; }
.live-brief-visited { margin: .15rem 0 0; }
.live-brief-visited button { padding: .35rem .9rem; font-size: .88rem; }
.live-pupil-head { margin-bottom: 1rem; }
.live-desk-lesson { margin: 0; color: var(--text-muted); font-size: .92rem; }
.live-back { margin: .5rem 0 0; }
/* The phase as the pupil's own banner wears it: its fill and its ink. */
.live-pupil-status {
  display: inline-block; margin: .4rem 0 0; padding: .2rem .65rem;
  border-radius: var(--radius-pill); font-size: .88rem; font-weight: 500;
  background: var(--surface-sunken); color: var(--text-muted);
}
.live-pupil-status--baseline { background: var(--phase-baseline-bg); color: var(--phase-baseline-ink); }
.live-pupil-status--recap { background: var(--phase-recap-bg); color: var(--phase-recap-ink); }
.live-pupil-status--practice { background: var(--phase-practice-bg); color: var(--phase-practice-ink); }
.live-pupil-status--done { background: var(--success-surface); color: var(--success-text); }
.live-pupil-figures { margin-bottom: 1.25rem; }
.live-pupil-figures .stat-figure { font-size: 1.8rem; }

.live-pupil-grid {
  display: grid; gap: 0 1.25rem; align-items: start;
  grid-template-columns: minmax(0, 1.65fr) minmax(0, 1fr);
}
.live-pupil-grid .panel-head h3 { margin: 0; font-size: 1rem; }

.live-now-block { margin-top: .9rem; padding-top: .8rem; border-top: 1px solid var(--border); }
.live-now .panel-head + .live-now-block { margin-top: 0; padding-top: 0; border-top: 0; }
.live-now-label {
  margin: 0 0 .3rem;
  font-family: var(--font-label); font-size: .68rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted);
}
.live-now-stem { margin: 0 0 .3rem; font-weight: 500; }
/* A pupil's own words, set apart from ours. */
.live-words {
  margin: .2rem 0 .6rem; padding: .5rem .8rem;
  border-inline-start: 3px solid var(--border-strong); border-radius: var(--radius-sm);
  background: var(--surface-sunken); font-style: normal; overflow-wrap: anywhere;
}
/* Right is green; not right is amber rather than red — a wrong answer here
   is the evidence the page exists to show, not an error. */
.live-outcome {
  display: inline-block; margin-inline-start: .25rem; padding: .05rem .45rem;
  border-radius: var(--radius-pill); text-transform: none; letter-spacing: 0;
  font-family: var(--font-label); font-size: .7rem; font-weight: 600;
}
.live-outcome--right { background: var(--success-surface); color: var(--success-text); }
.live-outcome--not-right { background: var(--warning-surface); color: var(--warning-text); }
.live-idea {
  display: inline-block; margin: .1rem 0 0; padding: .25rem .6rem;
  border-radius: var(--radius-sm); background: var(--tint-accent-fill);
  font-size: .85rem; font-weight: 500;
}

.live-handed { border-inline-start: 4px solid var(--warning); }
.live-ideas { list-style: none; margin: 0; padding: 0; }
.live-ideas li {
  display: flex; flex-direction: column; gap: .15rem;
  padding: .55rem 0; border-top: 1px solid var(--border);
}
.live-ideas li:first-child { border-top: 0; padding-top: 0; }
.live-why { font-size: .82rem; color: var(--text-muted); }

.live-answers { list-style: none; margin: 0; padding: 0; }
.live-answers li { padding: .8rem 0; border-top: 1px solid var(--border); }
.live-answers li:first-child { border-top: 0; padding-top: 0; }
.live-answer-meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: .3rem .75rem; margin: 0 0 .3rem;
  font-family: var(--font-label); font-size: .7rem; color: var(--text-muted);
}
.live-answer-meta .live-outcome { margin: 0; }
.live-answer-stem { margin: 0 0 .25rem; font-weight: 500; }
.live-answer-reason { margin: 0 0 .4rem; font-size: .88rem; }

.live-phases { list-style: none; margin: 0; padding: 0; }
.live-phase {
  display: grid; grid-template-columns: 6rem minmax(0, 1fr); gap: .15rem .6rem; align-items: baseline;
  padding: .55rem .6rem; border-inline-start: 3px solid transparent; border-radius: var(--radius-sm);
}
.live-phase .live-marks { grid-column: 2; }
.live-phase-label { font-weight: 600; }
.live-phase-state { color: var(--text-muted); font-size: .9rem; }
.live-phase--current { background: var(--surface-sunken); border-inline-start-color: var(--accent); }
.live-phase--current .live-phase-state { color: var(--text); }

.live-plan { list-style: none; margin: 0; padding: 0; }
.live-plan-item { display: flex; gap: .65rem; padding: .55rem 0; border-top: 1px solid var(--border); }
.live-plan-item:first-child { border-top: 0; padding-top: 0; }
.live-plan-n {
  flex: none; width: 1.6rem; height: 1.6rem; display: grid; place-items: center;
  border-radius: 50%; border: 1px solid var(--border-strong);
  font-family: var(--font-label); font-size: .72rem; font-weight: 600; color: var(--text-muted);
}
.live-plan-body { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.live-plan-idea { overflow-wrap: anywhere; }
.live-plan-state {
  font-family: var(--font-label); font-size: .66rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .05em; color: var(--text-muted);
}
.live-plan-note { font-size: .8rem; color: var(--text-muted); }
.live-plan-item--cleared .live-plan-n,
.live-plan-item--worked .live-plan-n { background: var(--success-surface); border-color: var(--success); color: var(--success-text); }
.live-plan-item--cleared .live-plan-state,
.live-plan-item--worked .live-plan-state { color: var(--success-text); }
.live-plan-item--on_now .live-plan-n { background: var(--accent); border-color: var(--accent); color: var(--text-inverse); }
.live-plan-item--on_now .live-plan-idea { font-weight: 600; }
.live-plan-item--on_now .live-plan-state { color: var(--text); }
.live-plan-item--flagged .live-plan-n { background: var(--warning-surface); border-color: var(--warning); color: var(--warning-text); }
.live-plan-item--flagged .live-plan-state { color: var(--warning-text); }

@media (max-width: 62rem) {
  .live-pupil-grid { grid-template-columns: 1fr; }
}
@media (max-width: 40rem) {
  .live-pupil-figures { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .5rem; }
  .live-pupil-figures .stat { padding: .7rem .75rem; }
  .live-pupil-figures .stat-figure { font-size: 1.4rem; }
  .live-pupil-figures .stat-label { font-size: .78rem; }
}
/* --- The Insight view --------------------------------------------------------
   What one session showed (routes/teacher-live-insight.tsx and its pupil page).
   It is built from parts the teacher already knows: the headline figures are
   .stat, the grid is the class picture's table.grid, and a cell is the same
   .cell said three ways — fill, glyph, and a screen reader's words.

   The three lights borrow the picture's own fills rather than adding tokens:
   right first time is its secure teal, right after help its amber, not yet
   its open pink. They were validated per palette there (the note above
   .cell), and a fourth set of near-identical colours would be a fourth thing
   to keep legible in dark and high contrast. The glyph is what a projector or
   a colour-blind reader is left with, so it is never the fill alone. */
.insight-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .35rem 1rem; margin-bottom: 1rem; }
.insight-head h2 { margin: 0; font-size: 1.5rem; }
.insight-date { margin: 0; color: var(--text-muted); }
.insight-sessions { margin-inline-start: auto; }
.insight-sessions ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }
.insight-sessions a {
  display: inline-block; padding: .3rem .7rem; border: 1px solid var(--border);
  border-radius: var(--radius-sm); font-family: var(--font-label); font-size: .78rem; text-decoration: none;
}
.insight-sessions a[aria-current="page"] { background: var(--tint-brand-fill); border-color: var(--border-strong); color: var(--text); font-weight: 600; }
.insight-sessions a.quiet-link { border-color: transparent; }
.insight-note { margin-bottom: 1rem; }
.insight-stats { margin-bottom: 1.25rem; grid-template-columns: repeat(auto-fit, minmax(min(10.5rem, 100%), 1fr)); }
.insight-stats .stat { margin: 0; }
.insight .panel-head h3 { margin: 0; font-size: 1.05rem; }

.insight-idea-held { font-family: var(--font-label); font-size: .74rem; color: var(--text-muted); white-space: nowrap; }

.cell-link { display: grid; place-items: center; width: 100%; height: 100%; min-height: 1.75rem; color: inherit; text-decoration: none; border-radius: 3px; }
.cell-link:hover { box-shadow: inset 0 0 0 2px var(--border-strong); }
.cell--first-time { background: var(--cell-secure); }
.cell--with-help { background: var(--cell-slip); }
.cell--not-yet { background: var(--cell-open); }
.cell--not-asked { background: var(--cell-none); color: var(--text-muted); }
.insight-absent { font-family: var(--font-label); font-size: .66rem; color: var(--text-muted); white-space: nowrap; }

.insight-access, .insight-pupil-ideas { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
.insight-pupil-ideas li { display: flex; gap: .6rem; align-items: flex-start; }
.insight-pupil-ideas .legend-swatch { flex: none; }
.insight-pupil-ideas .insight-idea-held { display: block; white-space: normal; }
.insight-pupil-top { margin-bottom: 1.25rem; }
.insight-pupil-ideas + .panel-help { margin: .9rem 0 0; }

/* What I did next: the note above the five presses, so it is written before
   the press that sends it. */
.insight-recorded { margin: 0 0 .9rem; padding: .6rem .8rem; background: var(--success-surface); color: var(--success-text); border-radius: var(--radius-sm); }
.insight-recorded-note { display: block; margin-top: .2rem; }
/* The line under "What I did next" is saved in place (components/InsightFollowUp.tsx),
   so it has to say "Saving…" and a refusal as well as the record. The wrapper holds the
   line's place whether or not it has words, so the form does not jump when it arrives. */
.insight-status { min-height: 2.6rem; }
.insight-recorded--saving { background: var(--surface-sunken); color: var(--text-muted); }
.insight-recorded--refused { background: var(--error-surface); color: var(--error-text); }
.insight-next-form { display: grid; gap: .5rem; }
.insight-next-form label { font-size: .88rem; color: var(--text-muted); }
.insight-next-form textarea { width: 100%; }
.insight-next-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .25rem; }

/* ===== The class page: the band, Handed to you, the Follow up. =====
   The shape first, names after: the band carries no name, and the page reads
   band, then the tutor's own hand-offs, then the register's evidence, then what
   to do. Semantic tokens only; the bar's fills are the class grid's own five
   (`--cell-*`), so a bar and a cell never disagree. */
.insight { container: insight / inline-size; }
.insight .muted { color: var(--text-muted); }
.insight .h-small { font-size: .95rem; margin: 0; }
.insight .h-sub { font-weight: 400; font-size: .84rem; }
.insight .small { font-size: .8rem; }
.insight .panel-head { flex-wrap: wrap; }
.band { display: grid; grid-template-columns: minmax(19rem, 2fr) minmax(0, 3fr); gap: 1.5rem; align-items: start; }
.band > * { min-width: 0; }
@container insight (max-width: 64rem) { .band { grid-template-columns: minmax(0, 1fr); } }
.band-ideas .panel-help { margin-bottom: .4rem; }
.idea-lines { list-style: none; margin: 0; padding: 0; }
.idea-line { display: grid; grid-template-columns: 1.3rem minmax(0, 1fr) 7.5rem max-content; align-items: center; gap: .3rem .6rem; padding: .3rem 0; border-top: 1px solid var(--border); }
.idea-line:first-child { border-top: 0; }
.idea-line-what { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.idea-line .idea-full { font-weight: 400; color: var(--text-muted); font-size: .84rem; }
.seg--line { height: .5rem; }
.idea-line-count { font-family: var(--font-label); font-size: .78rem; white-space: nowrap; color: var(--text); text-align: end; }
.idea-line-count strong { font-family: var(--font-display); font-size: 1.05rem; color: var(--error-text); }
.idea-lines-more { margin: .4rem 0 0 1.9rem; font-size: .8rem; }
.idea-lines-more a { color: var(--text-muted); }
.idea-line.is-lit { background: var(--surface-sunken); }
.idea-n { font-family: var(--font-label); font-size: .78rem; font-weight: 600; color: var(--text-link); }
.idea-short { font-weight: 700; }
.idea-full { font-weight: 500; }
.idea-label { font-family: var(--font-label); font-size: .68rem; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); margin-inline-end: .3rem; }
.seg { display: flex; height: .55rem; border-radius: var(--radius-pill); overflow: hidden; background: var(--cell-none); }
.seg-part { display: block; min-width: 2px; }
.seg-not-yet { background: var(--cell-persistent); }
.seg-with-help { background: var(--cell-slip); }
.seg-first-time { background: var(--cell-secure); }
.seg-not-asked { background: var(--cell-none); }
.band .stat { background: var(--surface-sunken); }
.insight-stats--compact { grid-template-columns: 1fr 1fr; gap: .6rem; margin: 0; }
.insight-stats--compact .stat--wide { grid-column: 1 / -1; }
.insight-stats--compact .stat { padding: .7rem .85rem; }
.insight-stats--compact .stat-figure { font-size: 1.5rem; }
.stat-figure-sub { font-size: .92rem; font-weight: 500; font-family: var(--font-body); }
.stat--wide .seg { margin: .35rem 0 .2rem; height: .7rem; }
.stat--quiet .stat-figure { color: var(--text-muted); font-size: 1.2rem; }
.stat-note a { color: var(--text-link); }

/* The red mark after a name: a person has to do something. --error is the
   product's "a person has to do something" fill (.panel--attention); the glyph
   carries the meaning and the accessible name says how many times and when. It
   sits on the name's line so no row grows. */
.mark-handed { display: inline-flex; align-items: center; justify-content: center; min-width: 1.05rem; height: 1.05rem; padding: 0 .3rem; margin-inline-start: .35rem; border-radius: var(--radius-pill); background: var(--error); color: var(--surface); font-family: var(--font-label); font-size: .74rem; font-weight: 700; line-height: 1; vertical-align: middle; cursor: help; box-sizing: border-box; }
.mark-n { font-size: .62rem; font-weight: 700; margin-inline-start: .2rem; letter-spacing: .02em; }
.name-with-mark, .see-who, .handoff-who-line { white-space: nowrap; }

/* Handed to you: one row per hand-off. Four columns at width, one when narrow. */
.handoffs { list-style: none; margin: 0; padding: 0; }
.handoff { display: grid; grid-template-columns: 9.5rem minmax(0, 1.2fr) minmax(0, 1.6fr) 10.5rem; gap: .25rem 1rem; padding: .6rem 0; border-top: 1px solid var(--border); align-items: start; }
.handoff:first-child { border-top: 0; }
.handoff p { margin: 0; font-size: .86rem; line-height: 1.45; }
.handoff-name { font-weight: 600; color: var(--text); text-decoration: none; }
.handoff-name:hover { color: var(--text-link); text-decoration: underline; }
.handoff-when { display: block; font-family: var(--font-label); font-size: .82rem; font-variant-numeric: tabular-nums; cursor: help; }
.handoff-idea .idea-full { color: var(--text-muted); font-weight: 400; font-size: .84rem; }
.handoff-ask q, .handoff-wrote q { font-style: italic; }
.handoff-ask q { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; cursor: help; }
.handoff-end p { font-size: .84rem; }
.handoff-seen { color: var(--success-text); font-weight: 600; cursor: help; }
.handoff-end--unseen .handoff-seen { color: var(--warning-text); }
.handoff-outcome { cursor: help; }
.handoff-outcome--right_later { color: var(--success-text); }
.handoff-outcome--still_not_right { color: var(--error-text); font-weight: 600; }
.handoff-outcome--not_asked_again { color: var(--text-muted); }
.handoff.is-lit { background: var(--surface-sunken); }
@container insight (max-width: 64rem) { .handoff { grid-template-columns: minmax(0, 1fr); gap: .15rem; } .handoff-when { display: inline; margin-inline-start: .5rem; } }
.qchip { display: inline-block; padding: 0 .45rem; border: 1px solid var(--border); border-radius: var(--radius-sm); font-family: var(--font-label); font-size: .76rem; font-weight: 600; text-decoration: none; color: var(--text-link); line-height: 1.5; }
.qchip:hover, .qchip.is-lit { background: var(--tint-brand-fill); border-color: var(--text-link); }

/* The Follow up: the ideas in full beside Pupils to see. */
.followup { display: grid; grid-template-columns: minmax(0, 3fr) minmax(16rem, 2fr); gap: 1.5rem; }
.followup > section { min-width: 0; }
@container insight (max-width: 64rem) { .followup { grid-template-columns: minmax(0, 1fr); } }
.ideas { list-style: none; margin: 0; padding: 0; }
.follow-idea { padding: .8rem 0; border-top: 1px solid var(--border); }
.ideas > .follow-idea:first-child { border-top: 0; padding-top: .2rem; }
.idea-head { display: grid; grid-template-columns: 1.6rem minmax(0, 1fr) auto; align-items: baseline; gap: .4rem .6rem; }
.follow-idea-title { margin: 0; font-size: 1rem; }
.idea-held { font-family: var(--font-label); font-size: .82rem; white-space: nowrap; color: var(--text); }
.idea-held strong { font-family: var(--font-display); font-size: 1.25rem; color: var(--error-text); }
.idea-of { color: var(--text-muted); font-size: .76rem; }
/* The session before, counted as "who held it" is: a line of its own under the count, so neither the band's line nor the Follow up's head grows wider. */
.idea-before { display: block; color: var(--text-muted); font-size: .72rem; font-weight: 400; }
.idea-track { margin: .15rem 0 0 2.2rem; font-size: .84rem; }
.idea-bar { display: grid; grid-template-columns: minmax(0, 1fr); gap: .3rem; margin: .4rem 0 .35rem 2.2rem; }
.idea-counts { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: .2rem 1rem; font-size: .78rem; color: var(--text-muted); font-family: var(--font-label); }
.idea-counts li { display: inline-flex; align-items: center; gap: .3rem; }
.swatch { display: inline-flex; align-items: center; justify-content: center; width: 1.1rem; height: 1rem; border-radius: 3px; font-size: .7rem; font-weight: 700; color: var(--text); }
.swatch--not-yet { background: var(--cell-persistent); }
.swatch--with-help { background: var(--cell-slip); }
.swatch--first-time { background: var(--cell-secure); }
.swatch--not-asked { background: var(--cell-none); color: var(--text-muted); }
.idea-names, .idea-questions { margin: .15rem 0 0 2.2rem; font-size: .9rem; line-height: 1.6; }
.idea-names a { color: var(--text); }
.idea-names a:hover { color: var(--text-link); }
.idea-group { margin-inline-start: .6rem; padding: .2rem .6rem; font-size: .76rem; }
.follow-idea.is-lit { background: var(--surface-sunken); margin: 0 -.5rem; padding-inline: .5rem; border-radius: var(--radius-sm); }
/* The fold: the ideas held by fewer, their heads in one line, opened in full on a press. */
.more-ideas { border-top: 1px solid var(--border); margin-top: .4rem; }
.more-ideas > summary { cursor: pointer; padding: .6rem 0; font-size: .84rem; color: var(--text-muted); list-style: none; display: block; }
.more-ideas > summary::before { content: "▸ "; color: var(--text-link); }
.more-ideas[open] > summary::before { content: "▾ "; }
.more-ideas-label { font-weight: 600; color: var(--text-link); }
.more-ideas-list { display: block; margin-top: .15rem; }
.more-ideas-list { font-size: .8rem; }
.more-ideas[open] > summary .more-ideas-list { display: none; }
.see-list { list-style: none; margin: 0; padding: 0; }
.see-row { display: grid; grid-template-columns: minmax(0, 1fr); padding: .45rem 0; border-top: 1px solid var(--border); }
.see-row:first-child { border-top: 0; }
.see-name { font-weight: 600; color: var(--text); text-decoration: none; }
.see-name:hover { text-decoration: underline; color: var(--text-link); }
.see-reason { font-size: .82rem; color: var(--warning-text); }
.see-foot { margin: .9rem 0 0; font-size: .84rem; color: var(--text-muted); }
.insight-next-heading { margin: 1.25rem 0 .4rem; padding-top: 1rem; border-top: 1px solid var(--border); }

/* ===== The register: one table, three column groups. =====
   Everything is scoped to `.register-table`, because `.cell`, `.cell--open`
   and `table.grid` mean other things on the class picture (the pink "open"
   there is not "asked, not answered" here). Outcome cells use the class
   picture's own five fills; access has its own purple scale (--use-*) so a
   heatmapped access cell can never be read as right or wrong. */
.register-tools { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem 1rem; margin-bottom: .75rem; }
.register-tools .tools-label { font-family: var(--font-label); font-size: .72rem; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); }
.register-tools button[aria-pressed="true"] { border-color: var(--border-strong); color: var(--text); background: var(--surface-sunken); }
.access-warn { font-size: .8rem; color: var(--warning-text); }
/* Visible only when focused: a way past some 500 cells for a keyboard. */
.register-skip { position: absolute; left: -999rem; }
.register-skip:focus { position: static; display: inline-block; margin-bottom: .5rem; }
.register-scroll { margin: 0 0 .75rem; }
/* Wide: measured, not a breakpoint (components/InsightRegister.tsx). contain: paint as well: Chromium otherwise lets the clipped table's width leak into the document's scrollable overflow (55px of empty page to the right, measured 9 Oct 2026). */
.register-scroll.is-wide { overflow: auto; max-height: 80vh; contain: paint; }
.register-table {
  border-collapse: separate; border-spacing: 0; font-size: .85rem; width: auto;
  /* The base table rule clips; a clipping table is the scroll container its own sticky heads stick to. */
  overflow: visible;
  /* The first head row's height, replaced by script with the measured one. */
  --groups-h: 1.6rem;
}
.register-table th, .register-table td { padding: 0; vertical-align: middle; }
.register-table thead th { position: sticky; z-index: 2; background: var(--surface); }
.register-table thead tr.groups th { top: 0; border-bottom: 1px solid var(--border); }
/* The second head row sticks under the first at the first row's MEASURED height (--groups-h, set by script), not a guess. */
.register-table thead tr.heads th { top: var(--groups-h); }
.register-table thead th[id] { scroll-margin-top: 1rem; }
.register-table .reg-group { padding: .25rem .4rem .2rem; text-align: start; font-family: var(--font-label); font-size: .68rem; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); white-space: nowrap; }
.register-table .reg-group--i, .register-table .reg-group--a { border-inline-start: 2px solid var(--border); }
.register-table .grid-name { text-align: start; padding: .1rem .6rem .1rem 0; font-weight: 500; min-width: 10rem; max-width: 12rem; line-height: 1.1; white-space: nowrap; }
.register-table thead .grid-name { font-family: var(--font-label); font-size: .68rem; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); vertical-align: bottom; }
.register-table .grid-name a { color: var(--text); text-decoration: none; }
.register-table .grid-name a:hover, .register-table .grid-name a:focus-visible { color: var(--text-link); text-decoration: underline; }
.register-table .reg-row--see > .grid-name { border-inline-start: 4px dotted var(--warning); padding-inline-start: .5rem; }
.register-table .reg-row--absent > .grid-name a { color: var(--text-muted); font-weight: 400; }
.register-table .reg-col { width: 2.2rem; min-width: 2.2rem; vertical-align: bottom; }
.register-table [data-group="i"]:is(th, td):first-of-type { border-inline-start: 2px solid var(--border); }
.register-table .reg-col--i:first-of-type, .register-table .reg-col--a:first-of-type { border-inline-start: 2px solid var(--border); }
/* Heads: one height for every column, content at the foot, so the ideas' rotated names and the questions' numbers sit on one baseline. */
.register-table .col-head { display: grid; place-items: center; align-content: end; gap: .05rem; width: 100%; min-height: 8rem; padding: .3rem 0 .2rem; border: 0; border-radius: var(--radius-sm) var(--radius-sm) 0 0; background: var(--cell-none); color: var(--text); cursor: help; font: inherit; }
.register-table .col-head:hover, .register-table .col-head:focus-visible, .register-table th.is-lit .col-head { outline: 2px solid var(--text-link); outline-offset: -2px; }
.register-table .col-n { font-family: var(--font-label); font-size: .76rem; font-weight: 700; color: var(--text-link); line-height: 1; }
.register-table .col-count { font-family: var(--font-label); font-size: .72rem; font-weight: 600; line-height: 1; font-variant-numeric: tabular-nums; }
.register-table .col-of { font-weight: 400; color: var(--text-muted); font-size: .62rem; }
/* The short name, read upwards; ellipsis at the head's height rather than a second line, so the column stays 2.2rem whatever the words. */
.register-table .col-short { writing-mode: vertical-rl; transform: rotate(180deg); font-family: var(--font-label); font-size: .72rem; font-weight: 600; line-height: 1; max-height: 5.9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); margin-bottom: .3rem; }
.register-table .heat-1 .col-head { background: var(--cell-slip); }
.register-table .heat-2 .col-head { background: var(--cell-open); }
.register-table .heat-3 .col-head { background: var(--cell-persistent); }
.register-table .heat-4 .col-head { background: var(--error); color: var(--surface); }
.register-table .heat-4 .col-n, .register-table .heat-4 .col-of, .register-table .heat-4 .col-short { color: var(--surface); }
.register-table .reg-col--hinge .col-head { background: var(--surface-sunken); }
.register-table .reg-col--a .col-head { background: var(--use-low); color: var(--use-low-ink); }
.register-table .reg-col--a .col-n, .register-table .reg-col--a .col-short { color: var(--use-low-ink); }
.register-table .grid-count { text-align: center; padding: 0 .35rem; font-family: var(--font-label); font-size: .78rem; color: var(--text-muted); width: 2.6rem; }
.register-table .cell { width: 2.2rem; min-width: 2.2rem; height: 1.7rem; text-align: center; border: 2px solid var(--surface); border-radius: 4px; background: var(--cell-none); color: var(--text); }
/* The outcome fills, scoped: `.register-table .cell` above sets the not-asked grey at two classes, so the unscoped `.cell--first-time` family lost to it and every answered cell drew grey (seen in the built page, 9 Oct 2026). */
.register-table .cell--first-time { background: var(--cell-secure); }
.register-table .cell--with-help { background: var(--cell-slip); }
.register-table .cell--not-yet { background: var(--cell-open); }
.register-table .cell--not-asked { background: var(--cell-none); color: var(--text-muted); }
.register-table .cell--unanswered { background: var(--cell-none); color: var(--text); box-shadow: inset 0 0 0 1px var(--border-strong); }
.register-table .cell--mark { background: var(--surface-sunken); }
.register-table .cell--mark-none { background: var(--surface-sunken); color: var(--text-muted); }
.register-table .cell--blank { background: var(--surface); }
/* Access use: its own hue, three steps, the count written in the cell. */
.register-table .cell--use-1, .swatch.cell--use-1 { background: var(--use-low); color: var(--use-low-ink); }
.register-table .cell--use-2, .swatch.cell--use-2 { background: var(--use-mid); color: var(--use-mid-ink); }
.register-table .cell--use-3, .swatch.cell--use-3 { background: var(--use-high); color: var(--use-high-ink); }
.register-table .cell--use .cell-glyph, .register-table [class*="cell--use-"] .cell-glyph { font-size: .78rem; }
.register-table .cell-link { min-height: 1.5rem; }
.register-table .cell-link:focus-visible { outline: 2px solid var(--text-link); outline-offset: -1px; }
.register-table .reg-row:hover > th, .register-table .reg-row:hover > td, .register-table .reg-row:focus-within > th, .register-table .reg-row:focus-within > td { background-image: linear-gradient(var(--tint-brand-fill), var(--tint-brand-fill)); }
/* Hidden groups: a toggle re-measures the table (components/InsightRegister.tsx). */
.register-table.hide-q [data-group="q"] { display: none; }
.register-table.hide-i [data-group="i"] { display: none; }
.register-table.hide-a [data-group="a"] { display: none; }
.register-scroll:has(.register-table.hide-a) ~ .legends .legend-access { display: none; }
.register-scroll.is-wide .register-table .grid-name { position: sticky; left: 0; z-index: 1; background: var(--surface); }
.register-scroll.is-wide .register-table thead .grid-name { z-index: 3; }
[data-theme="high-contrast"] .register-table .cell { border-color: var(--border); }
.legends { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: .4rem 1.5rem; margin: .5rem 0 0; padding: 0; }
.legends .legend { margin: 0; }
.legend-title { font-family: var(--font-label); font-size: .68rem; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); margin: 0 0 .2rem; }
.legend-item--text { display: block; }
.swatch--cell-first-time { background: var(--cell-secure); }
.swatch--cell-with-help { background: var(--cell-slip); }
.swatch--cell-not-yet { background: var(--cell-open); }
.swatch--cell-unanswered { background: var(--cell-none); box-shadow: inset 0 0 0 1px var(--border-strong); }
.swatch--cell-not-asked { background: var(--cell-none); color: var(--text-muted); }
.swatch--cell-mark { background: var(--surface-sunken); }
.swatch--rule-dotted { background: transparent; border-inline-start: 4px dotted var(--warning); }
.mark-handed--legend { margin-inline-start: 0; }
.heat-scale { display: inline-flex; flex: none; gap: 2px; vertical-align: middle; }
.legends .legend-item { align-items: flex-start; }
.heat-scale span { width: .9rem; height: .9rem; border-radius: 3px; display: inline-block; }
.heat-swatch-0 { background: var(--cell-none); } .heat-swatch-1 { background: var(--cell-slip); } .heat-swatch-2 { background: var(--cell-open); } .heat-swatch-3 { background: var(--cell-persistent); } .heat-swatch-4 { background: var(--error); }

/* ===== The hover card (components/InsightCards.tsx): the live register's card (.live-peek) after the lesson. =====
   The same values, token for token, so a teacher who has learned one has learned the other; the one difference is
   that it is fixed to the window, because the register's frame would clip anything inside it. */
.peek {
  position: fixed; z-index: 30; left: 0; top: 0; width: min(34rem, calc(100vw - 2rem)); overflow-y: auto;
  border: 1px solid var(--border-strong); border-radius: var(--radius-lg);
  background: var(--surface-raised); color: var(--text); box-shadow: var(--shadow-menu);
  font-size: .86rem; line-height: 1.38; white-space: normal; text-align: start; cursor: default;
}
.peek-head { padding: .6rem 1rem .55rem; border-bottom: 1px solid var(--border); }
.peek-name { margin: 0; font-family: var(--font-display); font-weight: var(--weight-display); font-size: 1.15rem; line-height: 1.2; }
.peek-where { margin: .1rem 0 0; font-size: .9rem; font-weight: 600; }
.peek-muted { color: var(--text-muted); font-weight: 400; }
.peek-q { font-family: var(--font-label); font-weight: 600; color: var(--text-link); }
.peek-bar { display: flex; flex-wrap: wrap; gap: .15rem .9rem; margin: .4rem 0 0; padding: 0; list-style: none; font-family: var(--font-label); font-size: .78rem; color: var(--text-muted); }
.peek-bar li { display: inline-flex; align-items: center; gap: .3rem; }
.peek-phase { display: grid; gap: .25rem; margin: .55rem 0 0; }
.peek-phase-line { display: flex; justify-content: space-between; align-items: baseline; gap: .5rem; }
.peek-phase-word { font-weight: 600; font-size: .82rem; color: var(--phase-practice-line); }
.peek-phase-count { font-size: .82rem; font-variant-numeric: tabular-nums; color: var(--text-muted); white-space: nowrap; }
.peek-phase .live-seg-done { background: var(--phase-practice-line); }
.peek-body { margin: 0; padding: .3rem 1rem .2rem; }
.peek-row { display: grid; grid-template-columns: 8.5rem minmax(0, 1fr); gap: .5rem; padding: .32rem 0; border-bottom: 1px solid var(--border); }
.peek-row:last-child { border-bottom: 0; }
.peek-row dt { margin: 0; font-weight: 600; color: var(--text-muted); }
.peek-row dd { margin: 0; overflow-wrap: anywhere; }
.peek-row--needs dt, .peek-row--needs dd { color: var(--error-text); font-weight: 600; }
.peek-items { display: grid; gap: .15rem; margin: 0; padding: 0; list-style: none; }
.peek-note { display: block; color: var(--text-muted); font-weight: 400; }
.peek q { quotes: "\201C" "\201D"; }
.peek-verdict { font-weight: 600; }
.peek-verdict--right { color: var(--success-text); }
.peek-verdict--not_right { color: var(--warning-text); }
.peek-foot { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: .4rem 1rem; padding: .5rem 1rem .6rem; border-top: 1px solid var(--border); }
.peek-open { font-weight: 600; }
.peek-close { margin-inline-start: auto; }
/* What an open card is about: its column(s) outlined in the register, the pupil's row shaded. */
.register-table .is-col-lit { box-shadow: inset 0 0 0 2px var(--text-link); }
.register-table tr.is-lit > th, .register-table tr.is-lit > td { background-image: linear-gradient(var(--tint-brand-fill), var(--tint-brand-fill)); }
.idea-link { background: none; border: 0; padding: 0; margin: 0; font: inherit; color: var(--text); text-align: start; cursor: default; border-bottom: 1px dotted transparent; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.idea-link:hover, .idea-link:focus-visible { border-bottom-color: var(--text-link); color: var(--text-link); }
@media print {
  .peek, .register-tools, .register-skip, .legend-access, .register-table [data-group="a"] { display: none; }
  .register-scroll, .register-scroll.is-wide { overflow: visible; max-height: none; contain: none; }
  .register-table thead th, .register-scroll.is-wide .register-table .grid-name { position: static; }
}
.follow-idea-title .idea-link { white-space: normal; overflow: visible; }

/* One pupil: each question, its answers, and the conversation under it. */
.insight-anchor { display: block; scroll-margin-top: 5rem; }
.insight-attempts { list-style: none; margin: .5rem 0 0; padding: 0 0 0 .9rem; border-inline-start: 2px solid var(--border); display: grid; gap: .6rem; }
.insight-attempts li { padding: 0; border: 0; }
.insight-chat-block { margin-top: .75rem; }
.insight-chat { list-style: none; margin: .3rem 0 0; padding: 0; display: grid; gap: .45rem; }
.insight-chat li { display: grid; gap: .1rem; padding: .5rem .7rem; border: 1px solid var(--border); border-radius: var(--radius-sm); }
.insight-line--pupil { background: var(--surface-sunken); }
.insight-line-who { font-family: var(--font-label); font-size: .68rem; font-weight: 600; color: var(--text-muted); }
.insight-line-clock { font-weight: 400; }
.insight-line-text { overflow-wrap: anywhere; white-space: pre-wrap; }
/* A line that is evidence in a concern, and the reply to it: marked where it
   sits, the way the single-flag page marks a turn (.flag-turn--flagged). */
.insight-line--flagged { border-inline-start: 3px solid var(--error); background: var(--tint-accent-fill); }
.insight-line-concern, .ch-concern { font-size: .78rem; color: var(--text-link); }
.ch-msg--flagged .ch-said { background: var(--tint-accent-fill); box-shadow: inset 3px 0 0 var(--error); }

/* Past sessions. */
.insight-session-list { list-style: none; margin: 0; padding: 0; }
.insight-session-list li {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: .25rem 1.25rem; align-items: baseline;
  padding: .75rem 0; border-top: 1px solid var(--border);
}
.insight-session-list li:first-child { border-top: 0; }
.insight-session-meta, .insight-session-next { font-family: var(--font-label); font-size: .76rem; color: var(--text-muted); }

/* The design review of 15 October 2026. Every section folds from its head;
   every band figure is a card with a gauge; the counts live in the bars; the
   Follow up reteaches by idea or by pupil. What each rule is for is beside it. */

/* A section folds from its head: the summary is the head, the chevron says
   which way. Nothing interactive is in the summary (it is a button); a
   section's tools are lifted onto its line from the body instead. */
.panel--fold { position: relative; }
.panel-fold > summary { list-style: none; cursor: pointer; }
.panel-fold > summary::-webkit-details-marker { display: none; }
.panel-fold > summary:hover h3 { color: var(--text-link); }
.panel-fold > summary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 4px; border-radius: var(--radius-sm); }
.panel-fold:not([open]) > summary { margin-bottom: 0; }
.panel-fold-chev {
  margin-inline-start: auto; flex: none; width: 1.75rem; height: 1.75rem;
  display: grid; place-items: center; border-radius: 50%;
}
.panel-fold > summary:hover .panel-fold-chev { background: var(--surface-sunken); }
.panel-fold-chev::before {
  content: ""; width: .5rem; height: .5rem;
  border-right: 2px solid var(--text-muted); border-bottom: 2px solid var(--text-muted);
  transform: translateY(-2px) rotate(45deg);
}
.panel-fold:not([open]) .panel-fold-chev::before { transform: translateY(1px) rotate(-45deg); }
.panel-fold > summary .h-sub + .panel-fold-chev, .panel-fold > summary .demo-tag + .panel-fold-chev { margin-inline-start: auto; }

/* The session tabs: "Sessions" once, then the numbers, then a button for the
   list of every session, set apart by a rule. */
.insight-sessions { display: flex; align-items: center; gap: .6rem; }
.insight-sessions-label {
  font-family: var(--font-label); font-size: .7rem; letter-spacing: .08em;
  text-transform: uppercase; color: var(--text-muted);
}
.insight-sessions a { min-width: 2rem; text-align: center; }
.insight-sessions-all { margin-inline-start: .4rem; padding-inline-start: .9rem; border-inline-start: 1px solid var(--border); }
.insight-sessions .insight-sessions-all a.fu-btn { min-width: 0; font-size: var(--size-small); }

/* Export: a button in the same quiet register, opening a short menu of the
   three files under it (components/InsightExport.tsx). A <details>, so the
   marker the browser draws is taken off and the button's own chevron says it
   opens. */
.insight-export { position: relative; }
.insight-export > summary { list-style: none; display: inline-flex; align-items: center; gap: .4rem; }
.insight-export > summary::-webkit-details-marker { display: none; }
.insight-export > summary::after {
  content: ""; width: .4rem; height: .4rem; border: solid currentColor; border-width: 0 2px 2px 0;
  transform: translateY(-2px) rotate(45deg);
}
.insight-export[open] > summary::after { transform: translateY(1px) rotate(-135deg); }
.insight-export-menu {
  position: absolute; inset-inline-end: 0; top: calc(100% + .4rem); z-index: 20;
  width: min(20rem, 90vw); padding: .4rem; background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius-md); box-shadow: var(--shadow-menu);
}
.insight-export-menu ul { list-style: none; margin: 0; padding: 0; }
.insight-export-menu a {
  display: grid; gap: .1rem; padding: .5rem .65rem; border-radius: var(--radius-sm);
  color: var(--text); text-decoration: none;
}
.insight-export-menu a:hover { background: var(--surface-sunken); }
.insight-export-menu a:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -2px; }
.insight-export-format { font-weight: 600; font-size: var(--size-small); }
.insight-export-note { color: var(--text-muted); font-size: .78rem; }
.insight-export-menu a[aria-busy="true"] { cursor: progress; opacity: .7; }
/* A file that could not be made (components/InsightExport.tsx), said where
   the press was rather than in the browser's downloads. */
.insight-export-failed {
  margin: .35rem .4rem 0; padding: .45rem .65rem; border-radius: var(--radius-md);
  background: var(--error-surface); color: var(--error-text); font-size: .8rem;
}
.insight-export-leaves {
  margin: .35rem .65rem .25rem; padding-top: .45rem; border-top: 1px solid var(--border);
  color: var(--text-muted); font-size: .74rem;
}

/* A button in the page's quiet register: outlined, the text colour. Shared by
   Small group, Across the term, Open and All past sessions. */
.fu-btn {
  display: inline-block; box-sizing: border-box; padding: .45rem .9rem; text-align: center;
  border: 1px solid var(--border-strong); border-radius: var(--radius-md); background: var(--surface);
  font: inherit; font-size: var(--size-small); font-weight: 600; color: var(--text);
  text-decoration: none; white-space: nowrap; cursor: pointer;
}
.fu-btn:hover { background: var(--surface-sunken); }
.fu-btn:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.fu-btn--quiet { border-color: var(--border); font-weight: 500; color: var(--text-muted); }

/* Segmented control: the register's column groups, Reteach by, What I did
   next. The pressed or checked one is raised. */
.seg-toggle {
  display: inline-flex; flex-wrap: wrap; gap: 2px; padding: 3px;
  border: 1px solid var(--border); border-radius: var(--radius-pill); background: var(--surface-sunken);
}
.seg-toggle button, .seg-toggle label {
  display: inline-flex; align-items: center; gap: .4rem; margin: 0; padding: .35rem .9rem;
  border: 0; border-radius: var(--radius-pill); background: transparent; box-shadow: none;
  font: inherit; font-size: var(--size-small); font-weight: 500; color: var(--text-muted); cursor: pointer;
}
.seg-toggle button:hover, .seg-toggle label:hover { color: var(--text); background: transparent; }
.seg-toggle button[aria-pressed="true"], .seg-toggle label:has(input:checked) {
  background: var(--surface); color: var(--text); box-shadow: var(--shadow-card);
}
.seg-toggle label:has(input:focus-visible) { outline: 2px solid var(--focus-ring); outline-offset: 1px; }
.seg-toggle input[type="radio"] { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }

/* The band: six figures as cards, each a title, a gauge and its count. */
.band-figures .gauges { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; }
.gauge-card {
  display: flex; flex-direction: column; align-items: center; gap: .3rem; text-align: center;
  padding: .85rem .75rem .9rem; border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface-sunken);
}
.gauge-title { font-weight: 700; font-size: .95rem; line-height: 1.25; color: var(--text); }
.gauge-dial { position: relative; display: block; width: 6.5rem; }
.gauge { display: block; width: 100%; height: auto; overflow: visible; }
.gauge path { fill: none; stroke-width: 10; stroke-linecap: round; }
.gauge-track { stroke: var(--cell-none); }
.gauge-card--right_first_time .gauge-fill { stroke: var(--correct); }
.gauge-card--with_hint .gauge-fill, .gauge-card--needed_help .gauge-fill { stroke: var(--warning); }
.gauge-card--took_part .gauge-fill { stroke: var(--phase-practice-line); }
.gauge-card--not_reached .gauge-fill { stroke: var(--text-muted); }
.gauge-card--asked_tutor .gauge-fill { stroke: var(--phase-recap-line); }
.gauge-inside {
  position: absolute; left: 0; right: 0; bottom: .1rem;
  font-family: var(--font-display); font-weight: 700; font-size: 1.3rem; line-height: 1; color: var(--text);
}
.gauge-foot { font-size: .8rem; color: var(--text-muted); }
.band-right { display: grid; gap: 1.4rem; align-content: start; min-width: 0; }

/* A misconception's line: the statement in one weight, the bar carrying its
   own count, and the column after it only what changed since last session. */
.idea-line { grid-template-columns: 1.3rem minmax(0, 1fr) 14rem 5rem; }
.idea-line .idea-short, .idea-line .idea-full,
.fu-statement .idea-short, .fu-statement .idea-full { font-weight: inherit; color: inherit; font-size: inherit; }
.idea-line .idea-link { font-weight: 500; font-size: .9rem; }
.seg--counted { height: 1.4rem; border-radius: var(--radius-sm); gap: 2px; background: transparent; }
.seg--counted .seg-part { display: flex; align-items: center; min-width: 0; overflow: hidden; }
.seg-count { padding: 0 .5rem; white-space: nowrap; font-size: .75rem; color: var(--text); font-variant-numeric: tabular-nums; }
.idea-line-trend { display: flex; }
.idea-trend {
  font-size: .68rem; font-weight: 600; padding: .05rem .45rem; border-radius: var(--radius-pill);
  background: var(--surface-sunken); color: var(--text-muted); white-space: nowrap;
}
.idea-trend--new { background: var(--warning-surface); color: var(--warning-text); }

/* The register: its toggles on the head's line, every column head one height
   and full to the top, the access heads as icons, and one cell saying so when
   nobody used any access feature. */
.register-panel .register-tools { position: absolute; top: 1rem; inset-inline-end: 3.6rem; margin: 0; }
.register-panel .register-tools .seg-toggle button[aria-pressed="true"]::before,
.register-panel .register-tools .seg-toggle button[aria-pressed="false"]::before {
  content: ""; width: .5rem; height: .5rem; border-radius: 50%; box-sizing: border-box;
}
.register-panel .register-tools .seg-toggle button[aria-pressed="true"]::before { background: var(--accent); }
.register-panel .register-tools .seg-toggle button[aria-pressed="false"]::before { border: 1.5px solid var(--border-strong); }
.register-table thead tr.heads th { vertical-align: bottom; }
.register-table thead tr.heads .col-head { height: 9.25rem; min-height: 0; box-sizing: border-box; }
.register-table .col-icon { display: grid; place-items: center; margin-bottom: .2rem; color: var(--use-low-ink); }
.register-table .reg-col--unused .col-icon, .register-table .reg-col--unused .col-count { opacity: .5; }
.register-table .reg-group-none { font-weight: 400; text-transform: none; letter-spacing: 0; }
.register-table td.access-none { background: var(--surface-sunken); border-radius: var(--radius-sm); text-align: center; vertical-align: middle; padding: .5rem; }
.register-table td.access-none span { display: inline-block; max-width: 7.5rem; font-size: var(--size-small); line-height: 1.35; color: var(--text-muted); }
.legends--one { display: block; }
.legends--one .legend { margin: .75rem 0 0; }

/* The Follow up. Reteach by idea or by pupil: two radios, and the view whose
   radio is checked is the one drawn. */
.fu-top { display: flex; align-items: center; gap: 1rem; margin-bottom: .9rem; }
.fu-h { margin: 0; font-size: 1rem; font-weight: 600; }
.panel:has(input[name="reteach-by"][value="pupil"]:checked) .fu-view--idea,
.panel:not(:has(input[name="reteach-by"][value="pupil"]:checked)) .fu-view--pupil { display: none; }
.fu-ideas { list-style: none; margin: 0; padding: 0; display: grid; gap: .75rem; }
.fu-idea {
  display: grid; grid-template-columns: 2rem minmax(0, 1fr) auto; gap: 1rem; align-items: center;
  padding: 1rem 1.1rem; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface);
}
.fu-n {
  width: 1.75rem; height: 1.75rem; border-radius: 50%; display: grid; place-items: center; flex: none;
  background: var(--cell-persistent); color: var(--text); font-weight: 600; font-size: var(--size-small);
}
.fu-n--sm { width: 1.4rem; height: 1.4rem; font-size: .72rem; margin-bottom: .3rem; }
.fu-statement { margin: 0 0 .6rem; font-size: 1rem; font-weight: 500; line-height: 1.4; }
.fu-statement .idea-link { white-space: normal; text-align: start; font-weight: 500; }
.fu-meta { display: flex; align-items: center; gap: .9rem; margin-bottom: .7rem; }
.fu-meta .seg { width: 16rem; flex: none; }
.fu-qs { display: inline-flex; gap: .3rem; }
.fu-people { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: .3rem; }
.fu-nobody { margin: 0; font-size: var(--size-small); }
.fu-av {
  width: 1.75rem; height: 1.75rem; border-radius: 50%; display: inline-grid; place-items: center; flex: none;
  background: var(--tint-accent-fill); color: var(--text); font-size: .64rem; font-weight: 700; text-decoration: none;
}
a.fu-av:hover, a.fu-av:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.fu-actions { display: flex; flex-direction: column; align-items: stretch; gap: .45rem; }
.fu-ptable-frame { overflow-x: auto; }
.fu-ptable { width: 100%; border-collapse: separate; border-spacing: 0; }
.fu-ptable th, .fu-ptable td { padding: .45rem .5rem; border-bottom: 1px solid var(--border); text-align: start; vertical-align: middle; }
.fu-ptable thead th { vertical-align: bottom; font-size: .72rem; font-weight: 600; color: var(--text-muted); border-bottom-width: 2px; }
.fu-pt-idea { width: 8.5rem; }
.fu-pt-short { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; font-weight: 500; line-height: 1.3; color: var(--text); }
.fu-pt-who { display: flex; align-items: center; gap: .55rem; }
.fu-pt-name { font-weight: 600; color: var(--text); text-decoration: none; white-space: nowrap; }
.fu-pt-name:hover { text-decoration: underline; }
.fu-open { font-size: .7rem; padding: .05rem .45rem; border: 1px dashed var(--border-strong); border-radius: var(--radius-pill); color: var(--text-muted); white-space: nowrap; }
.fu-pt-cell .cell { display: inline-grid; place-items: center; width: 1.9rem; height: 1.6rem; border-radius: var(--radius-sm); }
.fu-pt-sum { font-size: var(--size-small); color: var(--text); white-space: nowrap; }
.fu-pt-go { text-align: end; }
.fu-pt-go .fu-btn { padding: .3rem .8rem; }

/* Who took no part, under the register, by why: never signed in, or signed in
   and answered nothing, which is the one to act on and wears the concern
   colour. */
.reg-absent-row th, .reg-absent-row td { padding: .55rem .4rem; vertical-align: middle; }
.reg-absent tr:first-child th, .reg-absent tr:first-child td { border-top: 2px solid var(--border); padding-top: .9rem; }
.reg-absent-row th { line-height: 1.5; padding-inline-end: 1rem; }
.absent-label { font-family: var(--font-label); font-size: .68rem; font-weight: 600; letter-spacing: .07em; text-transform: uppercase; color: var(--text-muted); }
.absent-n { display: inline-block; padding: 0 .45rem; border-radius: var(--radius-pill); background: var(--surface-sunken); font-size: .72rem; color: var(--text-muted); }
.absent-people { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: .4rem; }
.absent-people a {
  display: inline-flex; align-items: center; gap: .4rem; padding: .2rem .7rem .2rem .2rem; border-radius: var(--radius-pill);
  background: var(--surface-sunken); color: var(--text-muted); text-decoration: none; font-size: var(--size-small);
}
.absent-people a:hover { color: var(--text); background: var(--border); }
.fu-av--muted { background: var(--surface); color: var(--text-muted); box-shadow: inset 0 0 0 1px var(--border); }
.reg-absent-row--concern .absent-label { color: var(--error-text); }
.reg-absent-row--concern .absent-n, .reg-absent-row--concern .absent-people a { background: var(--error-surface); color: var(--error-text); }
.reg-absent-row--concern .fu-av--muted { color: var(--error-text); box-shadow: inset 0 0 0 1px var(--error); }

/* The concerns raised in the session, under the misconceptions: at most four
   rows, open first, each with what it says in place of a press. */
.band-sg { border-top: 1px solid var(--border); padding-top: 1rem; }
.band-sg-head { display: flex; flex-wrap: wrap; align-items: center; gap: .45rem; margin-bottom: .6rem; }
.band-sg-head h4 { margin: 0 .3rem 0 0; }
/* Folded in the band's corner: a smaller padlock, and the rows a step below
   the line rather than a panel's width below it. */
.band-sg .sg-fold-mark { width: 1.75rem; height: 1.75rem; }
.band-sg .sg-fold-mark .icon { width: .95rem; height: .95rem; }
.band-sg .sg-fold-body { margin-top: .6rem; }
.sg-tally { font-size: .72rem; font-weight: 600; padding: .1rem .5rem; border-radius: var(--radius-pill); background: var(--surface-sunken); color: var(--text-muted); }
.sg-tally--open { background: var(--error-surface); color: var(--error-text); }
.sg-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .45rem; }
.sg-row {
  display: grid; grid-template-columns: 1.75rem minmax(0, 1fr) auto; gap: .7rem; align-items: center;
  padding: .55rem .7rem; border: 1px solid var(--border); border-inline-start: 3px solid var(--border); border-radius: var(--radius-md);
}
.sg-row--open { border-inline-start-color: var(--error); }
.sg-row--onward { border-inline-start-color: var(--warning); }
.sg-who { margin: 0; display: flex; align-items: center; gap: .45rem; font-size: var(--size-small); }
.sg-who a { font-weight: 600; color: var(--text); text-decoration: none; }
.sg-who a:hover { text-decoration: underline; }
.sg-at { color: var(--text-muted); }
.sg-urgency { font-size: .66rem; font-weight: 600; padding: .02rem .45rem; border-radius: var(--radius-pill); background: var(--surface-sunken); color: var(--text-muted); }
.sg-urgency--raised { background: var(--error-surface); color: var(--error-text); }
.sg-reason { margin: .15rem 0 0; font-size: var(--size-small); color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sg-act { display: flex; align-items: center; gap: .4rem; }
.sg-act .fu-btn { padding: .3rem .7rem; font-size: .76rem; }
.sg-send-form { margin: 0; }
.fu-btn.sg-send { background: var(--danger-bg); border-color: var(--danger-bg); color: var(--text-inverse); }
.fu-btn.sg-send:hover { background: var(--danger-bg-hover); }
.sg-flag { font-size: .72rem; font-weight: 600; padding: .25rem .6rem; border-radius: var(--radius-pill); white-space: nowrap; }
.sg-flag--onward { background: var(--warning-surface); color: var(--warning-text); }
.sg-flag--closed { background: var(--surface-sunken); color: var(--text-muted); }
.fu-btn.sg-all { margin-top: .75rem; }

/* What the class asked the tutor: by question, each message with the
   pupil, the time and what they asked for, the reply on hover or focus. */
.ch-kinds { list-style: none; display: flex; flex-wrap: wrap; gap: .6rem; margin: 0 0 1rem; padding: 0; }
.ch-kind { display: inline-block; padding: .1rem .5rem; border-radius: var(--radius-pill); font-size: .72rem; font-weight: 600; background: var(--surface-sunken); color: var(--text-muted); }
.ch-kind--hint_request { background: var(--phase-recap-bg); color: var(--phase-recap-ink); }
.ch-kind--clarify, .ch-kind--why { background: var(--phase-practice-bg); color: var(--phase-practice-ink); }
.ch-kind--stuck { background: var(--warning-surface); color: var(--warning-text); }
.ch-kind.ch-kind--total { display: inline-flex; align-items: center; gap: .55rem; padding: .25rem .3rem .25rem .8rem; font-size: .8rem; }
.ch-kind-n {
  min-width: 1.45rem; height: 1.45rem; padding: 0 .35rem; box-sizing: border-box; border-radius: var(--radius-pill);
  display: inline-grid; place-items: center; background: var(--surface); color: var(--text); font-size: .78rem; font-weight: 700;
}
.ch-groups { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }
.ch-q { margin: 0 0 .5rem; display: flex; align-items: baseline; gap: .5rem; font-size: var(--size-small); font-weight: 600; line-height: 1.4; }
.ch-qn { margin-inline-start: auto; font-weight: 400; color: var(--text-muted); white-space: nowrap; }
.ch-msgs { list-style: none; margin: 0; padding: 0 0 0 .6rem; border-inline-start: 2px solid var(--border); display: grid; gap: .7rem; }
.ch-msg { display: flex; align-items: flex-start; gap: .55rem; }
.ch-body { flex: 1; min-width: 0; }
.ch-meta { margin: 0; display: flex; align-items: center; gap: .45rem; font-size: .78rem; color: var(--text-muted); }
.ch-meta a { font-weight: 600; color: var(--text); text-decoration: none; }
.ch-meta a:hover { text-decoration: underline; }
.ch-said {
  display: inline-block; margin: .2rem .5rem 0 0; padding: .45rem .75rem; font-size: var(--size-small);
  background: var(--surface-sunken); border-radius: 0 var(--radius-md) var(--radius-md) var(--radius-md);
}
.ch-noreply { font-size: .74rem; color: var(--text-muted); }
.ch-reply { position: relative; display: inline-block; }
.ch-reply-btn {
  padding: .2rem .65rem; border: 1px solid var(--border-strong); border-radius: var(--radius-pill);
  background: var(--surface); color: var(--text); font: inherit; font-size: .74rem; font-weight: 600; cursor: help;
}
.ch-reply-btn:hover, .ch-reply-btn:focus-visible { background: var(--surface-sunken); }
.ch-reply-pop {
  position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.ch-reply:hover .ch-reply-pop, .ch-reply:focus-within .ch-reply-pop {
  z-index: 30; left: 0; top: calc(100% + .35rem); width: 22rem; height: auto; overflow: visible; clip-path: none; white-space: normal;
  padding: .7rem .85rem; border: 1px solid var(--border); border-inline-start: 3px solid var(--tint-accent-line); border-radius: var(--radius-md);
  background: var(--surface); box-shadow: var(--shadow-menu); font-size: var(--size-small); line-height: 1.45; color: var(--text);
}
.ch-reply-who { display: block; margin-bottom: .25rem; font-family: var(--font-label); font-size: .64rem; letter-spacing: .07em; text-transform: uppercase; color: var(--text-muted); }

/* What I did next: the five presses as one control, the note beside them. */
.insight-next-row { display: flex; flex-wrap: wrap; align-items: center; gap: .75rem; }
.insight-next-row .insight-next-actions { margin: 0; }
.insight-next-row .insight-next-actions button.action { background: var(--surface); color: var(--text); box-shadow: var(--shadow-card); }
.insight-next-row textarea {
  flex: 1; min-width: 16rem; resize: vertical; padding: .55rem .8rem;
  border: 1px solid var(--border-strong); border-radius: var(--radius-md);
  font: inherit; font-size: var(--size-small); background: var(--surface); color: var(--text);
}

@media (max-width: 40rem) {
  .insight-sessions { margin-inline-start: 0; flex-basis: 100%; }
  .insight-idea-held { grid-column: 2; }
  .insight-session-list li { grid-template-columns: 1fr; }
}
@media print {
  .insight details > * { display: block; }
  .insight-next-form, .insight-sessions, .insight-export, .idea-group { display: none; }
}
/* --- The model bench --------------------------------------------------------
   Staff-only screens in the public layout (docs/plan/Model_Bench_Plan.md §11).
   Figures per variant, never a ranking: nothing here is coloured "best". Colour
   carries a state and only a state: a pill's tone comes from the loader
   (`toneOf`), never from the word it holds, and the two-tone chips (`--right`,
   `--wrong`) say whether one answer matched its label. Everything else is the
   page's own text and borders. `main` brings no gutter in this app, so `.bench`
   is its own container. */
.bench { max-width: 72rem; margin: 0 auto; padding: 1.5rem var(--clearspace) 3rem; min-width: 0; }
.bench .lede { color: var(--text-muted); }
.bench-head { margin-bottom: .75rem; }
.bench-crumbs { font-size: .9rem; color: var(--text-muted); }
.bench-crumbs a { color: var(--text-link); }
.bench-read-only { border-left: 4px solid var(--warning); background: var(--warning-surface); color: var(--warning-text);
  padding: .5rem .75rem; margin: .75rem 0; border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }

/* Page head: title and its one-line fact on the left, state and actions on the right. */
.bench-title { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: .75rem 1.5rem; margin-bottom: 1rem; }
.bench-title h1 { margin-bottom: .2rem; }
.bench-sub { margin: 0; color: var(--text-muted); font-size: .92rem; }
.bench-title-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
.bench-title-actions form.button_to { margin: 0; }
/* Redoc draws the API reference (components/BenchApiReference.tsx) with its
   own colours and its own markup, and axe finds two things in its JSON
   samples that its options cannot reach. The samples are opened in full, so
   the 15px collapse control is taken out rather than left a target too small
   to press; and a boolean's red on Redoc's dark panel is 4.2:1, so it takes
   the sample's own text colour. Both are `.redoc-json`, Redoc's own stable
   class, because every other class it makes is a hash. */
.bench-api-reference .redoc-json .collapser { display: none; }
.bench-api-reference .redoc-json .token.boolean { color: inherit; }
/* The API reference takes the whole window. Redoc lays out three columns of
   its own — a 260px menu, the description, and a dark panel of samples at 40%
   — and inside `.bench`'s 72rem the middle one came out about 400px wide with
   the samples cut off, beside 170px of nothing either side on a laptop. It is
   a developer's page, read by operation and not as prose, so the measure the
   rest of the bench keeps does not apply to it. The heading keeps the bench's
   gutter; Redoc goes edge to edge, its menu flush with the window. */
.bench--wide { max-width: none; }
.bench--wide .bench-api-reference { margin-inline: calc(-1 * var(--clearspace)); }
.bench-help { color: var(--text-muted); font-size: .88rem; margin: .35rem 0; max-inline-size: var(--measure); }
.bench-muted { color: var(--text-muted); }
/* The prompt register (Prompt_Arena_Plan.md §6.1): a call site's task under its
   id, and its evidence over the digest it was read from. */
.bench-table--register th[scope="row"] { font-weight: 400; }
.bench-table--register th code, .bench-register-task, .bench-table--register td > span, .bench-table--register td > a { display: block; }
.bench-register-task, .bench-register-detail { font-size: .88rem; }

/* A call site's prompt (Prompt_Arena_Plan.md §6.1): the template at reading
   size, each {{slot}} a chip, and a version as a word diff. An insertion is
   underlined and a deletion struck through as well as coloured, so the
   difference never rests on colour alone. */
.bench-prompt { font-size: .86rem; line-height: 1.55; max-height: 32rem; overflow-y: auto; }
.bench-slot { display: inline-block; padding: 0 .35rem; margin: 0 .05rem; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); font-family: var(--font-label); font-size: .78rem; color: var(--text); background: var(--surface); }
.bench-slot::before { content: "{"; color: var(--text-muted); }
.bench-slot::after { content: "}"; color: var(--text-muted); }
.bench-slots .bench-slot { margin: .1rem .15rem; }
.bench-diff-added { background: var(--diff-added-bg); color: var(--diff-added-text); text-decoration: underline; text-decoration-thickness: 2px; }
.bench-diff-removed { background: var(--diff-removed-bg); color: var(--diff-removed-text); text-decoration: line-through; }
.bench-versions { list-style: none; padding: 0; margin: .5rem 0; display: grid; gap: 1rem; }
.bench-version { padding: .5rem 0 .5rem .9rem; border-left: 3px solid var(--border); }
.bench-version h3 { margin: 0 0 .15rem; font-size: 1rem; }
/* A version's depth in its family, as a step in from the edge, deepest at six:
   classes rather than an inline margin, which the CSP refuses (StyleRule.tsx). */
.bench-version--depth-1 { margin-inline-start: 1.5rem; }
.bench-version--depth-2 { margin-inline-start: 3rem; }
.bench-version--depth-3 { margin-inline-start: 4.5rem; }
.bench-version--depth-4 { margin-inline-start: 6rem; }
.bench-version--depth-5 { margin-inline-start: 7.5rem; }
.bench-version--depth-6 { margin-inline-start: 9rem; }
.bench-fragments { display: grid; gap: .5rem; margin: .5rem 0 0; }
.bench-fragments dd { margin: .15rem 0 0; }

/* A compared pair (Prompt_Arena_Plan.md §6.2, §6.3). A bar per question: A's
   wins grow left from the centre, B's right, no difference sits between them,
   and depends-on-order and split are set apart at the end. A weak win is an
   outline, not a fill, and every segment carries its symbol and its count. */
.bench-sides { list-style: none; padding: 0; margin: .5rem 0 1rem; display: flex; flex-wrap: wrap; gap: .5rem 1.5rem; }
.bench-side-letter { display: inline-block; min-inline-size: 1.6rem; text-align: center; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); font-family: var(--font-label); }
.bench-side--rejected { color: var(--text-muted); }
.bench-legend { list-style: none; padding: 0; margin: .25rem 0 .75rem; display: flex; flex-wrap: wrap; gap: .35rem 1rem; font-size: .85rem; }
.bench-swatch { display: inline-flex; justify-content: center; min-inline-size: 1.6rem; border-radius: var(--radius-sm); }
.bench-bars { display: grid; gap: .45rem; }
.bench-bar { display: grid; grid-template-columns: minmax(8rem, 13rem) minmax(0, 1fr) 3.5rem minmax(0, 1fr) auto; grid-template-areas: "q a same b apart"; align-items: center; gap: .5rem; }
.bench-bar-question { grid-area: q; overflow-wrap: anywhere; font-size: .9rem; }
.bench-bar-track { display: flex; min-inline-size: 0; }
.bench-bar-track--a { grid-area: a; justify-content: flex-end; }
.bench-bar-track--b { grid-area: b; }
.bench-bar-middle { grid-area: same; text-align: center; font-size: .85rem; }
.bench-bar-apart { grid-area: apart; display: flex; flex-wrap: wrap; gap: .5rem; font-size: .85rem; white-space: nowrap; }
.bench-seg { display: inline-flex; justify-content: center; align-items: center; min-inline-size: 2.6rem; padding: .15rem .3rem; font-size: .8rem; text-decoration: none; white-space: nowrap; box-sizing: border-box; }
.bench-seg--a { background: var(--verdict-a); color: var(--verdict-a-text); }
.bench-seg--b { background: var(--verdict-b); color: var(--verdict-b-text); }
.bench-seg--a_weak { background: var(--surface); color: var(--text); border: 2px dashed var(--verdict-a); }
.bench-seg--b_weak { background: var(--surface); color: var(--text); border: 2px dashed var(--verdict-b); }
.bench-seg--same { background: var(--verdict-same); color: var(--verdict-same-text); }
.bench-seg--order, .bench-seg--split { background: var(--verdict-order); color: var(--verdict-order-text); }
.bench-seg--unread { background: var(--surface-sunken); color: var(--text-muted); }
.bench-seg--current { outline: 3px solid var(--focus-ring); outline-offset: 1px; }
.bench-compare-cases { list-style: none; padding: 0; margin: .5rem 0; display: grid; }
.bench-compare-cases li { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: .25rem .75rem; padding: .4rem 0; border-bottom: 1px solid var(--border); }
.bench-compare-line { overflow-wrap: anywhere; }
.bench-compare-cells { display: inline-flex; gap: .2rem; }
.bench-cell { display: inline-flex; justify-content: center; min-inline-size: 1.7rem; padding: .05rem .3rem; border-radius: var(--radius-sm); }
.bench-judges { list-style: none; padding: 0; display: grid; gap: .75rem; }
.bench-judge-line { display: block; font-size: .9rem; }
.bench-outputs { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr)); }
.bench-output h3 { margin: 0 0 .5rem; font-size: 1rem; }
.bench-output-text { white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; }
.bench-table--verdicts td { vertical-align: top; }
.bench-orders { font-family: var(--font-label); letter-spacing: .1em; }
.bench-th-note { display: block; font-weight: 400; font-size: .75rem; }
.bench-reasons dl { margin: .25rem 0 0; font-size: .85rem; }
.bench-reasons dd { margin: 0 0 .25rem; }
/* A generated batch (Prompt_Arena_Plan.md §6.7): one row per version in the
   order written, its change line over its bar against production. */
.bench-batch { list-style: none; padding: 0; margin: .5rem 0 1rem; display: grid; gap: .9rem; }
.bench-batch-row { padding: .5rem 0 .6rem; border-bottom: 1px solid var(--border); }
.bench-batch-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .25rem .5rem; margin-bottom: .35rem; }
.bench-batch-head input[type="checkbox"] { inline-size: 1.1rem; block-size: 1.1rem; }
.bench-section-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .5rem 1rem; margin: 2rem 0 .5rem; }
.bench-section-head h2, .bench-section-head p { margin: 0; }

/* Pills: a state, at the label floor and in sentence case. Tone is the loader's, never the word's. */
.bench-pill { display: inline-block; font-family: var(--font-label); font-size: .78rem; font-weight: 600; line-height: 1.3;
  padding: .12rem .6rem; border-radius: var(--radius-pill); white-space: nowrap; vertical-align: middle; }
.bench-pill--good { background: var(--success-surface); color: var(--success-text); }
.bench-pill--warn { background: var(--warning-surface); color: var(--warning-text); }
.bench-pill--bad { background: var(--error-surface); color: var(--error-text); }
.bench-pill--plain { background: var(--surface-sunken); color: var(--text-muted); }
.bench-pill--prod { background: var(--tint-brand-fill); color: var(--text-link); }
.bench-pill--label { background: var(--tint-code); color: var(--text); font-family: var(--font-mono); font-weight: 500; }
.bench-pill + .bench-pill { margin-inline-start: .3rem; }

/* Tables. Sentence-case headers at the label floor; figures right-aligned and tabular. */
.bench-scroll { overflow-x: auto; max-width: 100%; -webkit-overflow-scrolling: touch; margin: .5rem 0 1rem; }
.bench-table { border-collapse: collapse; width: 100%; font-size: .9rem; margin: .5rem 0 1rem; }
.bench-scroll > .bench-table { margin: 0; }
.bench-table th, .bench-table td { padding: .55rem .6rem; border-bottom: 1px solid var(--border); text-align: start; vertical-align: top; }
.bench-table thead th { font-family: var(--font-label); font-size: .78rem; font-weight: 600; color: var(--text-muted);
  border-bottom: 1px solid var(--border-strong); white-space: nowrap; vertical-align: bottom; }
.bench-table thead .bench-th-group th { border-bottom: 0; padding-bottom: 0; font-weight: 700; color: var(--text); text-align: center; }
.bench-table tbody th { font-weight: 600; }
.bench-num { text-align: end; font-variant-numeric: tabular-nums; white-space: nowrap; }
.bench-zero { color: var(--text-muted); }
.bench-share { color: var(--text-muted); font-size: .8rem; }
.bench-table--results th[scope="row"], .bench-table--grid th[scope="row"] { position: sticky; left: 0; background: var(--surface); z-index: 1; min-width: 9rem; }
.bench-table--results th, .bench-table--results td { padding-inline: .4rem; }
.bench-table--results th[scope="row"] { min-width: 12rem; }
.bench-table--results tbody tr:hover td, .bench-table--results tbody tr:hover th { background: var(--surface-sunken); }
.bench-row--prod th[scope="row"] { box-shadow: inset 3px 0 0 var(--action-bg); }
.bench-row--rejected th[scope="row"] { box-shadow: inset 3px 0 0 var(--error); }
.bench-row--rejected .bench-num { color: var(--text-muted); }
.bench-variant-name { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem; }
.bench-settings { display: block; margin-top: .2rem; font-family: var(--font-mono); font-size: .72rem; font-weight: 400;
  color: var(--text-muted); max-width: 16rem; overflow-wrap: anywhere; }
.bench-gate-note { display: block; margin-top: .2rem; font-size: .8rem; color: var(--error-text); }
.bench-count { font-weight: 600; }

/* One chip per repeat in the grid and on the case page. */
.bench-cell { vertical-align: middle; }
.bench-rep { display: inline-block; font-family: var(--font-mono); font-size: .75rem; padding: .08rem .45rem; border-radius: var(--radius-sm);
  margin: .1rem .25rem .1rem 0; }
.bench-rep--right { background: var(--success-surface); color: var(--success-text); }
.bench-rep--wrong { background: var(--error-surface); color: var(--error-text); }
.bench-rep--none { background: var(--surface-sunken); color: var(--text-muted); }

/* Results: chart left, "About this run" right. */
.bench-results-top { display: grid; grid-template-columns: minmax(0, 1fr) 22rem; gap: 1.25rem; align-items: start; margin: 1.25rem 0; }
.bench-about { margin: 0; }
.bench-about h2 { font-size: 1rem; }
.bench-notes { list-style: none; padding: 0; margin: 0 0 1rem; display: grid; gap: .45rem; }
.bench-note { position: relative; padding-inline-start: 1.1rem; font-size: .88rem; color: var(--text); }
.bench-note::before { content: ""; position: absolute; left: 0; top: .45em; width: .5rem; height: .5rem; border-radius: 50%; background: var(--border-strong); }
.bench-note--warn::before { background: var(--warning); }
.bench-key { margin: 0; }
.bench-key div { margin-bottom: .5rem; }
.bench-key dt { font-family: var(--font-label); font-size: .8rem; color: var(--text-muted); }
.bench-key dd { margin: .1rem 0 0; font-size: .88rem; color: var(--text-muted); }
/* A criteria judgement and the fixed rules' row on the case page, and the
   judge-context box on the suite (Bench_Judge_Panel_Plan.md §3.1, §3.2). */
.bench-criteria-answers { list-style: none; margin: .25rem 0 .5rem; padding: 0; }
.bench-criteria-answers li { margin: .2rem 0; font-size: .88rem; }
.bench-review textarea { display: block; width: 100%; min-width: 16rem; margin: .25rem 0; }
/* The panel (Bench_Judge_Panel_Plan.md §3.3, §3.4). Cases are links in a row
   that wraps, because "split on 40 of 400" is forty of them; the case page's
   table puts each judge in a column of its own, wide enough to read a reason
   in, and scrolls sideways before it squeezes one. */
.bench-panel h3 { font-size: .95rem; margin: 1.25rem 0 .25rem; }
.bench-case-links { display: flex; flex-wrap: wrap; gap: .1rem .5rem; margin-top: .2rem; font-family: var(--font-mono); font-size: .78rem; }
/* The builder's judge rows. A select is as wide as its longest option, and a
   catalogue's model names are long: uncapped, the model column pushed the
   optional name field out of the card, where it had to be scrolled to. */
.bench-table--builder.bench-table--judges select { max-width: 9rem; min-width: 0; }
.bench-table--builder.bench-table--judges input { width: 6rem; min-width: 0; }
.bench-table--panel td { min-width: 13rem; vertical-align: top; font-size: .88rem; }
.bench-table--panel td:first-of-type { min-width: 6rem; }
/* A person's answer under the panel's: said in a line, asked for in a row of
   two buttons that wraps under a narrow column. */
.bench-person { margin-top: .4rem; }
.bench-person-form { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem; }
.bench-keybox { background: var(--surface-sunken); border-radius: var(--radius-md); padding: .75rem 1rem; margin: .75rem 0 1rem; display: grid; gap: .35rem; font-size: .88rem; }
.bench-keybox div { display: grid; grid-template-columns: max-content 1fr; gap: .6rem; align-items: baseline; }

/* The scatter: inline SVG, tokens only. */
.bench-scatter { margin: 0; }
.bench-scatter figcaption { font-size: .95rem; font-weight: 600; margin-bottom: .5rem; }
.bench-scatter figcaption .bench-muted { font-weight: 400; }
.bench-chart { background: var(--surface-sunken); border-radius: var(--radius-md); padding: .75rem; }
.bench-chart svg { width: 100%; height: auto; display: block; }
.bench-axis { stroke: var(--border-strong); stroke-width: 1; }
.bench-grid-line { stroke: var(--border); stroke-width: 1; }
.bench-tick { fill: var(--text-muted); font-size: 11px; font-family: var(--font-label); }
.bench-axis-label { fill: var(--text-muted); font-size: 12px; font-family: var(--font-label); }
.bench-point-dot { fill: var(--text); }
.bench-point-prod { fill: var(--action-bg); }
.bench-point--rejected line { stroke: var(--error); stroke-width: 2.2; }
.bench-point-label { fill: var(--text); font-size: 11.5px; font-family: var(--font-mono); }
.bench-leader { stroke: var(--border-strong); stroke-width: 1; }
.bench-legend { list-style: none; padding: 0; margin: .5rem 0 0; display: flex; flex-wrap: wrap; gap: .4rem 1.25rem; font-size: .8rem; color: var(--text-muted); }
.bench-legend li { display: flex; align-items: center; gap: .4rem; }
.bench-swatch { display: inline-block; width: .75rem; height: .75rem; border-radius: 50%; position: relative; }
.bench-swatch--prod { background: var(--action-bg); }
.bench-swatch--dot { background: var(--text); }
.bench-swatch--cross { border-radius: 0; }
.bench-swatch--cross::before, .bench-swatch--cross::after { content: ""; position: absolute; left: -1px; top: 45%; width: .9rem; height: 2px; background: var(--error); transform: rotate(45deg); }
.bench-swatch--cross::after { transform: rotate(-45deg); }

/* Filters (suite sources, grid split/all). */
.bench-filters { display: flex; flex-wrap: wrap; gap: .4rem; margin: .5rem 0; }
.bench-filter { padding: .2rem .7rem; border: 1px solid var(--border); border-radius: var(--radius-pill); color: var(--text); text-decoration: none; font-size: .88rem; }
.bench-filter:hover { border-color: var(--border-strong); }
.bench-filter--current { background: var(--selected-surface); border-color: var(--selected); font-weight: 600; }
.bench-count { color: var(--text-muted); }

/* Home: one row per task. */
.bench-tasks { border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface); margin: 1rem 0; }
.bench-task { display: grid; grid-template-columns: 1.4fr 1fr 1fr 1.6fr auto; gap: .75rem 1.25rem; padding: 1rem 1.25rem; border-bottom: 1px solid var(--border); align-items: start; }
.bench-task:last-child { border-bottom: 0; }
.bench-task h2 { font-size: var(--size-card-title); margin: 0 0 .2rem; }
.bench-task-key { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: .35rem; font-size: .85rem; }
.bench-lbl { display: block; font-family: var(--font-label); font-size: .78rem; color: var(--text-muted); margin-bottom: .15rem; }
.bench-standing { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.bench-task-actions { align-self: center; }

/* Suite. */
.bench-input { max-width: 40rem; width: 45%; min-width: 16rem; }
.bench-quote { font-weight: 400; quotes: "\201C" "\201D"; }
.bench-review { display: flex; flex-wrap: wrap; gap: .35rem; }
.bench-review form.button_to { margin: 0; }
.bench-review button { white-space: nowrap; }
.bench-plant { border: 1px solid var(--border); border-radius: var(--radius-md); padding: .75rem 1rem; margin: 1rem 0; background: var(--surface); }
.bench-plant > summary { cursor: pointer; font-weight: 600; }
.bench-plant > summary .bench-muted { font-weight: 400; font-size: .88rem; }
.bench-faults { list-style: none; padding: 0; margin: .75rem 0; display: grid; gap: .5rem; font-size: .88rem; }
.bench-faults label { display: grid; grid-template-columns: auto 1fr; gap: .5rem; align-items: start; }

/* Case: the input, then one card per variant, one chip per repeat. */
.bench-input-full { border-left: 3px solid var(--action-bg); margin: .5rem 0 1rem; padding: .25rem .9rem; font-size: 1.05rem; }
.bench-fold > summary { cursor: pointer; color: var(--text-muted); font-size: .88rem; }
.bench-pre { white-space: pre-wrap; overflow-wrap: anywhere; font-size: .8rem; background: var(--surface-sunken); padding: .6rem; border-radius: var(--radius-sm); }
.bench-figure { margin: 0 0 .75rem; }
/* A simulated class's run page (components/BenchClassSim.tsx): its sections
   spaced as the bench's are, a pupil's reference on a line of its own, its
   faults as a list worst first, and a strip of screenshots per person, which
   scrolls sideways rather than pushing the page wide. */
.class-sim-section { margin-top: 2rem; }
.class-sim-ref { display: block; color: var(--text-muted); font-weight: 400; font-size: .82rem; white-space: nowrap; }
.class-sim-faults { padding-inline-start: 1.25rem; }
.class-sim-faults > li { margin-bottom: 1rem; }
.class-sim-faults p { margin: .25rem 0; }
.class-sim-strip { display: flex; gap: .5rem; overflow-x: auto; padding: .5rem 0; }
/* The runner's viewport (1280 × 900), so a thumbnail has its width before it
   loads: a lazy image with none is a link nobody can hit (axe target-size). */
.class-sim-strip a { display: block; flex: none; }
.class-sim-strip img { display: block; height: 9rem; width: auto; aspect-ratio: 1280 / 900; object-fit: contain; object-position: top; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); }
.bench-figure img { display: block; max-width: 100%; height: auto; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); }
.bench-answers { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr)); }
.bench-variant { margin: 0; }
.bench-variant .panel-head { justify-content: space-between; }
.bench-repeats { list-style: none; padding: 0; margin: 0; display: grid; gap: .5rem; }
.bench-repeat { padding: .35rem 0 .35rem .75rem; border-left: 3px solid var(--border); }
.bench-repeat--right { border-left-color: var(--success); }
.bench-repeat--wrong { border-left-color: var(--error); }
.bench-repeat-head { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
.bench-repeat-meta { font-family: var(--font-mono); font-size: .72rem; color: var(--text-muted); }

/* Builder, finding, send-to-bench: fields with the label above the control. */
.bench-builder .panel, .bench-form .panel { margin-bottom: 1rem; }
.bench-builder fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }
.bench-builder legend { font-family: var(--font-display); font-weight: var(--weight-display); font-size: var(--size-card-title); margin-bottom: .5rem; }
.bench-builder input, .bench-builder select { max-width: 100%; }
.bench-table--builder input { width: 100%; min-width: 9rem; }
.bench-fields { display: grid; align-items: start; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr)); margin: .5rem 0 1rem; }
.bench-field { display: grid; gap: .25rem; }
.bench-field > .bench-lbl { margin: 0; color: var(--text); font-weight: 600; font-size: .88rem; }
.bench-field input, .bench-field select, .bench-field textarea { width: 100%; }
.bench-money { display: flex; align-items: center; gap: .3rem; }
.bench-money input { width: 7rem; }
.bench-form { max-width: 44rem; }
.bench-form form { display: grid; gap: 1rem; }
.bench-form button.action { justify-self: start; }
.bench-dl { display: grid; grid-template-columns: 9rem 1fr; gap: .75rem 1.25rem; max-width: 52rem; margin: 1rem 0; }
.bench-dl dt { font-family: var(--font-label); font-size: .8rem; color: var(--text-muted); padding-top: .2rem; }
.bench-dl dd { margin: 0; }
.bench-decision { padding: .75rem 1rem; border-left: 3px solid var(--action-bg); background: var(--surface); border-radius: 0 var(--radius-sm) var(--radius-sm) 0; font-size: 1.05rem; }

/* The builder, to the wireframe (Part 2 of the UX pass): a two-column layout
   whose right column is the sticky estimate, the route as a segmented control of
   links (each profile has its own model ids, so the page is made again for it),
   and the model x reasoning grid, one real checkbox per cell the model accepts. */
.bench-builder-cols { display: grid; grid-template-columns: minmax(0, 2fr) minmax(18rem, 1fr); gap: 1.25rem; align-items: start; }
.bench-builder-main { min-width: 0; }
.bench-builder-side { position: sticky; top: 1rem; background: var(--surface-sunken); display: grid; gap: .75rem; margin: 0; min-width: 0; }
.bench-builder-side .bench-fields { margin: 0; }
.bench-fields--two { grid-template-columns: 1fr 1fr; }
.bench-builder-side button.action { justify-self: stretch; }
.bench-estimate { display: grid; gap: .35rem; margin: .25rem 0; font-size: .9rem; font-variant-numeric: tabular-nums; }
.bench-estimate div { display: flex; justify-content: space-between; gap: 1rem; align-items: baseline; }
.bench-estimate dt { color: var(--text-muted); }
.bench-estimate dd { margin: 0; text-align: end; }
.bench-estimate-sub { font-size: .8rem; color: var(--text-muted); }
.bench-estimate-sub dd:only-child { text-align: start; }
.bench-estimate-total { border-top: 1px solid var(--border-strong); padding-top: .4rem; font-weight: 600; }
.bench-estimate-total dt, .bench-estimate-total dt label { color: var(--text); }
.bench-hand { margin-top: 0; }
.panel > .bench-section-head { margin-top: 0; }
.bench-static { font-size: .95rem; }
.bench-field--route { grid-column: 1 / -1; }
.bench-route { display: grid; grid-template-columns: auto auto 1fr; gap: .75rem 1.5rem; align-items: end; background: var(--surface-sunken); border-radius: var(--radius-md); padding: .75rem 1rem; }
.bench-seg { display: inline-flex; flex-wrap: wrap; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; background: var(--surface); }
.bench-seg-item { white-space: nowrap; padding: .3rem .7rem; font-size: .85rem; color: var(--text-muted); text-decoration: none; border-inline-start: 1px solid var(--border-strong); }
.bench-seg-item:first-child { border-inline-start: 0; }
.bench-seg-item--on { background: var(--action-bg); color: var(--action-ink); font-weight: 600; }
.bench-catalogue { display: flex; flex-wrap: wrap; gap: .5rem 1.25rem; align-items: center; margin: .75rem 0 0; padding-top: .6rem; border-top: 1px dashed var(--border-strong); font-size: .8rem; color: var(--text-muted); }
.bench-filters--families { align-items: center; }
.bench-filters button.bench-filter { background: transparent; font: inherit; font-size: .88rem; cursor: pointer; }
.bench-filters button.bench-filter--current { background: var(--selected-surface); }
.bench-search { margin-inline-start: auto; min-width: 12rem; }
.bench-table--catalogue { overflow: visible; } /* a hidden overflow here makes the table its own scroller and the sticky model column never sticks */
.bench-table--catalogue th[scope="row"] { position: sticky; left: 0; background: var(--surface); z-index: 1; min-width: 11rem; }
.bench-table--catalogue th.bench-cellbox { text-align: center; }
.bench-table--catalogue tr[hidden] { display: none; }
.bench-cellbox { text-align: center; vertical-align: middle; }
.bench-avail { font-size: .8rem; white-space: nowrap; line-height: 1.35; }
.bench-dot { display: inline-block; width: .55rem; height: .55rem; border-radius: 50%; margin-inline-end: .35rem; vertical-align: middle; }
.bench-dot--yes { background: var(--success); }
.bench-dot--no { border: 1.5px solid var(--error); }
.bench-row--off th, .bench-row--off td { color: var(--text-muted); }
.bench-row--prod th[scope="row"] { box-shadow: inset 3px 0 0 var(--action-bg); }
.bench-box { position: relative; display: inline-grid; place-items: center; width: 1.15rem; height: 1.15rem; border: 1.5px solid var(--border-strong); border-radius: 4px; background: var(--surface); cursor: pointer; vertical-align: middle; }
.bench-box input { appearance: none; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.bench-box:has(input:checked) { background: var(--action-bg); border-color: var(--action-bg); }
.bench-box:has(input:checked)::after { content: ""; position: absolute; width: .3rem; height: .6rem; margin-top: -.08rem; border: solid var(--action-ink); border-width: 0 2px 2px 0; transform: rotate(45deg); pointer-events: none; }
.bench-box:has(input:focus-visible) { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.bench-box--prod { outline: 2px solid var(--action-bg); outline-offset: 2px; }
.bench-box--na { border-style: dotted; opacity: .45; background: repeating-linear-gradient(45deg, transparent 0 3px, var(--border) 3px 5px); cursor: not-allowed; }

/* The scatter's controls: presets as links, the two axes as selects in a GET form. */
.bench-chart-controls { display: grid; gap: .5rem; margin: 0 0 .6rem; }
.bench-presets { margin: 0; }
.bench-axes { display: flex; flex-wrap: wrap; gap: .5rem 1.25rem; align-items: center; font-size: .85rem; color: var(--text-muted); }
.bench-axes label { display: inline-flex; align-items: center; gap: .35rem; }
.bench-axes select { font-size: .85rem; color: var(--text); background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: .2rem .4rem; max-width: 14rem; }
.bench-axes-plot { padding: .25rem .6rem; }

/* Phone. The page never scrolls sideways: wide tables scroll inside .bench-scroll,
   the suite's rows become cards, and the two-column blocks stack. */
@media (max-width: 56rem) {
  .bench-builder-cols { grid-template-columns: 1fr; }
  .bench-builder-side { position: static; }
  .bench-results-top { grid-template-columns: 1fr; }
  .bench-task { grid-template-columns: 1fr 1fr; }
  .bench-task-main, .bench-task > div:nth-child(4), .bench-task-actions { grid-column: 1 / -1; }
}
@media (max-width: 40rem) {
  .bench { padding: 1rem 1rem 2rem; }
  .bench-route { grid-template-columns: 1fr; }
  .bench-fields--two { grid-template-columns: 1fr 1fr; }
  .bench-search { min-width: 0; width: 100%; margin: 0; }
  .bench-axes { flex-direction: column; align-items: stretch; }
  .bench-axes select { max-width: none; flex: 1; }
  .bench-task { grid-template-columns: 1fr; }
  .bench-th-group { display: none; }
  .bench-settings { display: none; }
  .bench-dl { grid-template-columns: 1fr; gap: .15rem; }
  .bench-dl dd { margin-bottom: .75rem; }
  .bench-input { width: auto; }
  .bench-table--suite thead { display: none; }
  .bench-table--suite tr { display: grid; grid-template-columns: 1fr 1fr; gap: .25rem .75rem; padding: .75rem 0; border-bottom: 1px solid var(--border); }
  .bench-table--suite td { display: block; border: 0; padding: 0; }
  .bench-table--suite td.bench-input, .bench-table--suite td.bench-review { grid-column: 1 / -1; }
  .bench-table--suite td[data-label]::before { content: attr(data-label); display: block; font-family: var(--font-label); font-size: .74rem; color: var(--text-muted); }
  .bench-table--register thead { display: none; }
  .bench-table--register tr { display: grid; grid-template-columns: 1fr auto; gap: .25rem .75rem; padding: .75rem 0; border-bottom: 1px solid var(--border); }
  .bench-table--register th, .bench-table--register td { display: block; border: 0; padding: 0; }
  .bench-table--register td:last-child { grid-column: 1 / -1; }
  .bench-bar { grid-template-columns: minmax(0, 1fr) 3rem minmax(0, 1fr); grid-template-areas: "q q q" "a same b" "apart apart apart"; padding-bottom: .4rem; border-bottom: 1px solid var(--border); }
}

/* --- The looks come after this file, in looks.css ---------------------------

   A look is a file in looks/ (docs/plan/Pupil_Looks_Build_Plan.md §3), and the
   sixteen of them are imported by looks.css, which the pupil's pages link right
   after this stylesheet. They must come after every rule above, not before:
   `[data-look="k"]` is one attribute and `:root` is one pseudo-class, and a
   look's token block loses to the defaults declared on :root if it comes first.
   Their component rules are scoped to their own key and so outweigh the rules
   here, but a tie goes to whatever is later, and a look must win ties.

   ⚠️ THE @IMPORTS WERE FIRST WRITTEN AT THE END OF THIS FILE, which is where
   their order says they belong, and Vite does not take them there: CSS requires
   an @import to precede every other rule, postcss-import warns "@import
   statements must precede all other statements" and leaves a late one alone,
   and the built stylesheet had none of the look files in it. At the top of this
   file they would inline, and be overridden by :root. So they are a stylesheet
   of their own, which has nothing before them, and the page links it second.
   One request more on a pupil's pages (none on a teacher's or a parent's), and
   a look's edit no longer changes the hash of this file. */
