/* site.css — page-level layout for the built site.

   The three files before it are the design system, ported byte-for-byte from the signed-off
   prototype: fonts.css names the faces, tokens.css holds the custom properties, base.css is
   the reset and the document, components.css is the component library. None of them says
   anything about how a PAGE is laid out, because in the prototype every page carried its own
   <style> block and said it there.

   A built page cannot do that: a per-page <style> is inline CSS, and the site's CSP (Task 22)
   is meant to be strict. So page layout lives in a stylesheet, and this is it. Loaded LAST,
   after components.css.

   Same rules as everywhere else: tokens only (no literal colours, no literal lengths outside
   the scale) and logical properties only — never padding-top, margin-left or text-align:left.
   Arabic RTL depends on it, and check battery job 5 reads this directory.

   Task 7 brings the home page's own sections (hero, tool grid, proof strips) and Tasks 10–16
   bring the tool pages'; each of those is a section here, not a <style> block on the page. */

/* ┌─ RULE: a content-carrying flex/grid ITEM sets `min-inline-size: 0` ─────────────┐
   │ THE SAME CONTRACT components.css states in full above `.chip__name`; read the    │
   │ reasoning there. It applies to this file too — tool-page layout lives here now —  │
   │ and check battery job 5's `min-inline-size` sweep parses these two columns out of │
   │ THIS comment exactly as it parses that one, so a content class that arrives here  │
   │ unclassified is a finding rather than a blind spot.                               │
   │                                                                                  │
   │ GUARDED — carries content, siblings are trapped on the same line:                │
   │   .pk-slip__name        item of a .pk-slip row, beside the position badge         │
   │   .pk-hist__names       item of .pk-hist__row, beside the time and the count; it  │
   │                        ellipsises rather than wraps, which is what makes          │
   │                        shrinking safe                                             │
   │   .pk-hist__row         a grid item whose own min-content is the whole unbroken   │
   │                        run of names inside it                                     │
   │   .sp-tally__name       item of .sp-tally__row, beside the spin count             │
   │   .sp-tally__row        a grid item of .sp-tally, same shape as .pk-hist__row     │
   │   .chip__attr           the lobby's extra-detail pill: a flex item of .chip     │
   │                        beside the name, and stranger-typed (Task 13)            │
   │   .cl-row__name         a child's name in a register row, flex, trapped between  │
   │                        the roll number and the tick/away/remove column (Task 14) │
   │   .ss-roster__row       a wish-list row on the santa setup page: a grid item     │
   │                        whose own min-content is a name plus a text box          │
   │                        (Task 20a)                                               │
   │   .ss-roster__name       the name in that row, flex, trapped beside the box      │
   │   .ss-link__name         a participant's name on the manage page, flex, trapped  │
   │                        on the line with their URL and three buttons              │
   │   .ss-link__mail         the address their link is mailed to, on the same line,  │
   │                        and one long unbroken run (Task 18)                       │
   │   .dr-row__name         an entry on a draw record, flex, trapped between the      │
   │                        position badge and the winner pill (Task 17)              │
   │   .ac-event__name       a kept gift exchange's organiser on the account page:    │
   │                        a flex item trapped on the line with its retention        │
   │                        sentence and two controls (Task 19)                       │
   │   .ac-key__name         an API key's own label on the same page, in the same     │
   │                        shape of row, beside its dates and one control (Task 23)  │
   │                                                                                  │
   │  (The old header's controls row was listed here until the restyle, W5b. It was   │
   │   the phone header's second row — a `flex-basis: 100%` item whose min-content     │
   │   was the theme control plus six language links, which is what made the document │
   │   scroll sideways at 360 px, CP-C1 finding F3. The row is gone: the six          │
   │   language links live in a disclosure panel now, and nothing in the masthead     │
   │   is a content-carrying flex item any more.)                                     │
   │                                                                                  │
   │ EXEMPT — each with the reason, because "it looked fine" is not one:               │
   │   .wh-head__text       sole content item of a grid column; its siblings are the   │
   │                        projector buttons in the OTHER column, so nothing is       │
   │                        trapped beside it.                                         │
   │   .wh-console__notes   `flex-basis: 100%` — it is a full-width row of its own.    │
   │   .wh-step__n          a printed ordinal, two digits, never free text.            │
   │   .wh-chip-weight      `flex: none`, a fixed-measure number box like .chip__n.    │
   │   .wh-chip-colour      `flex: none`, a fixed square swatch — same shape, and it   │
   │                        holds a colour rather than text. (Task 16a.)               │
   │   .pk-slip__n          same shape: a fixed-size position badge, `flex: none`.     │
   │   .pk-hist__time       a clock reading, `flex: none`, `tabular-nums`.             │
   │   .pk-hist__count      a label and a number, not free text.                       │
   │   .sp-tally__n         a count, `flex: none`, `tabular-nums`.                     │
   │   .tm-pair__kind       `flex-basis: 100%` — it is a full-width row of its own.    │
   │   .tm-captain          a one-word badge on the tag pill's shape, `flex: none`.    │
   │   .sh-code__row       a join code and a Copy button: the code is six or eight    │
   │                        digits in a fixed measure and the button is a label, so    │
   │                        neither item can be squeezed by free text. (Task 12.)      │
   │   .hm-tool__name       the only thing on its row: .hm-tool is a single-column     │
   │                        grid, so nothing is trapped beside it. Its text is a tool  │
   │                        name from the string file, never user input — and the card │
   │                        itself carries the `min-inline-size: 0` that keeps the     │
   │                        grid track honest.                                         │
   │   .dr-check__row      a grid item of a single-column grid, so nothing sits       │
   │                        beside it; and its own two items are a `.field` that      │
   │                        carries `min-inline-size: 0` itself and a button whose    │
   │                        label is one word. (Task 17.)                             │
   │   .dr-kv__row         same: a row of the record's own key/value list, one per    │
   │                        line, whose `dd` carries the guard and wraps anywhere —   │
   │                        so a 64-character fingerprint in it has a min-content of  │
   │                        one character. (Task 17.)                                 │
   │   .cl-head__text       same shape as .wh-head__text: the sole content item of a   │
   │                        flex row whose only sibling is the projector button pair.  │
   │   .cl-row__n           a roll number, `flex: none`, two digits, `tabular-nums`.   │
   │   .cl-row__mark        the tick and the word beside it, `flex: none`, from the    │
   │                        string file — never a name. (Task 14.)                     │
   │   .cl-class-name       a teacher-typed class name, but the flex ITEM is the       │
   │                        .card__title around it; the <bdi> only isolates its bidi,  │
   │                        and it carries `overflow-wrap: anywhere` so a long one     │
   │                        breaks rather than widening the card. (Task 14.)           │
   │   .cl-expiry__days     a countdown, its own grid row, `tabular-nums`. (Task 14.)  │
   │   .ss-link__pin        a six-digit PIN, `flex: none`, `tabular-nums`, never     │
   │                        free text. (Task 20a; PIN_DIGITS is 6.)                  │
   │   .result-banner__name  components.css's own element, re-sized here for the      │
   │                        reveal card and classified there for the same reason:    │
   │                        sole content item of a single-column grid, with          │
   │                        `overflow-wrap: anywhere` already taking its min-content │
   │                        down to one character. (Task 20a.)                       │
   └──────────────────────────────────────────────────────────────────────────────────┘ */

/* The vertical rhythm of a content page: air under the header, more under the last section,
   and one gap between top-level sections so no section has to fight a neighbour's margin.
   Lifted from the prototype's `.hm` rule (docs/prototype/home.html), which set exactly this
   for the home page alone. */
.page { padding-block: var(--sp-7) var(--sp-8); }
.page > * + * { margin-block-start: var(--sp-8); }

/* ── the language suggestion banner ──────────────────────────────────────
   Built in the browser by the language script (build/lang-script.ts) and inserted between the
   header and <main>, so it never appears in the HTML a crawler reads. It is styled here
   anyway, for the same reason every other rule is: a page may not carry a <style> block, and
   the script may not carry inline styles.

   --brand-soft under inherited --ink is the tinted-surface pairing components.css already uses
   for .result-banner, so this introduces no new colour decision. It is measured rather than
   assumed: check battery job 9 runs axe over every page in light AND dark with this banner
   rendered — headless Chromium reports en-US, so the five non-English pages render it on every
   run — and reports zero contrast findings. The two controls are the ordinary .btn variants; a
   suggestion is not special enough to need a button of its own. */
/* ── the language suggestion (build/lang-script.ts) ───────────────────────
   A NON-REFLOWING OVERLAY, and that is the whole reason for `position: fixed` on an element
   that reads like a page banner.

   The script inserts it right after the header, in the DOM, once `DOMContentLoaded` has fired
   and `navigator.languages` has been consulted. In normal flow that insertion pushes the whole
   document down by the height of a banner — a layout shift of ~64 px (three rows at 393 px)
   landing after first paint, on every non-English visitor to an English page, which is CLS
   measured against the page they were already reading. Reserving the space instead is worse: the
   server cannot know whether this visitor will be offered anything, so the strip would sit empty
   on every page for everyone.

   So it floats: it takes no space, nothing moves when it appears or when it is dismissed, and it
   sits at the BLOCK END because the header is sticky above 46rem and a fixed strip at the top
   would cover it. The z-index is below `.ns-celebrate` (50) and above the sticky header (20) —
   it is a thing you dismiss, so it has to be reachable while the page scrolls under it. It is
   short, it carries its own dismiss button, and it is gone from the DOM when that is pressed. */
.lang-suggest {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 30;
  background: var(--brand-soft);
  border-block-start: 1px solid var(--line);
}

.lang-suggest__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  padding-block: var(--sp-3);
}

/* The text takes the slack, so the two controls sit together at the end of the row — and it
   is `margin-inline-end`, so in Arabic they sit at the other end without a second rule. */
.lang-suggest__text { margin-inline-end: auto; }

/* Nothing on paper: a printed page cannot be switched to another language. */
@media print {
  .lang-suggest { display: none; }
}

/* ── the celebration overlay ──────────────────────────────────────────────
   glue.js's `celebrate()` appends a <canvas class="ns-celebrate"> for about a second and a
   half and then removes it. The rules live here rather than in the script because an element
   styled from JavaScript is an inline style, and the CSP this site is heading for (Task 22)
   should not have to allow any. If this class is ever renamed, rename it in glue.js too — that
   is the one coupling, and it is stated in both files. */
.ns-celebrate {
  position: fixed;
  inset: 0;
  z-index: 50;
  inline-size: 100%;
  block-size: 100%;
  pointer-events: none;
}

/* ══ the site chrome on a phone (CP-C1-4's Option C collapse) ══════════════════════════
   base.css owns the chrome's LOOK — the masthead deck, the tool rail, the two disclosures and
   the three-column footer. What it does not own is what happens below 46rem, and that is a
   different design rather than the same one squeezed: **the rail folds into a pocket**.

   THE PROBLEM THIS REPLACES, kept on the record because the numbers are the reason the owner
   asked for a redesign at all. The ported header carried a brand, up to six tool links and SIX
   LANGUAGE LINKS — strings are baked per URL and changing language is changing page (ruling
   R15) — and at 393 px that wrapped to four rows and 251 px of header, 38 % of an iPhone's
   viewport before a word of the page (Task 6 review carry). Task 7 turned the two link groups
   into single-line scrolling strips, which brought it to 119 px; W1 then had to hand the
   controls row a `min-inline-size: 0` because its min-content width was still wider than a
   360 px phone and the whole DOCUMENT scrolled sideways (CP-C1 walk, finding F3).

   THE TREATMENT NOW: ONE row, and nothing on it is a strip (owner, 2026-09-01 — the earlier
   fold kept the theme control on a second row; the owner asked for one). The rail is hidden
   and its links move into a `<details>` pocket at the row's inline end — the last thing on
   the right in LTR, the language disclosure beside it — and the theme control moves INTO the
   pocket's panel, under the tool links (its masthead copy is hidden below). The six language
   links are in the language disclosure's panel where they are on a laptop too. The masthead
   is brand + two 44 px discs on one row, and the widest thing on the page is a panel that is
   positioned absolutely and therefore cannot widen the document at all.

   NOTHING IS REMOVED FROM THE HTML by any of it. The rail's links and the pocket's are the same
   hrefs rendered twice (pages/_layout.tsx says why the markup is duplicated), and `display: none`
   is what picks which one a reader and the accessibility tree get. A crawler reads both. */

@media (max-width: 45.999rem) {
  /* The two decks trade places: the rail goes, the pocket arrives. Exactly one "all tools"
     navigation is rendered at any viewport, which is what keeps the landmark labels unique. */
  .site-nav__rail { display: none; }
  .site-tools { display: inline-block; }

  /* …and the masthead takes over the rail's bottom hairline. Without this the chrome ends on a
     2 % tone step (--surface against --bg) and the header has no edge at all on a phone, which
     is the one place it most needs one: it is the only deck there. */
  .site-nav__mast { border-block-end: 1px solid var(--line); }

  /* The brand steps down a size so brand + language + pocket fit one row at 360 px in every
     locale. The wordmark and the longest autonym are the pair that decides it — "Namesome"
     plus "Português" — and the pocket's summary is a square glyph rather than the words
     `nav.all_tools` for the same reason (the label is there, visually hidden, in the DOM).
     Measured at 360 px in en, ar and ru: brand, language and pocket on one row. */
  .site-brand { font-size: var(--fs-1); }

  /* Below this width the chrome is being TOUCHED, so the footer's links become targets too —
     the same 2.75rem the discs and the theme buttons carry (spec §4.5, ≥ 44 px). Scoped to
     the narrow case on purpose: on a desktop the pointer is a mouse, and this would add
     18 px per link to a sixteen-link footer for nothing. */
  .site-foot__col a {
    display: flex;
    align-items: center;
    min-block-size: 2.75rem;
    padding-block: 0;
  }

  /* The masthead's copy of the theme control leaves the phone (owner, 2026-09-01: one row).
     The pocket panel renders the other copy — a device preference keeps its place in the
     chrome, one tap deep — and `display: none` takes this one out of the accessibility tree,
     so a reader meets exactly one theme group here as everywhere. The child combinator is
     what spares the panel's copy: it is inside the pocket, not a child of the mast row. */
  .site-nav__mast-inner > .site-seg { display: none; }

  /* A PANEL SPANS THE MASTHEAD instead of hanging off its own disc, and that is the one rule
     here that is about correctness rather than looks. An absolutely positioned box can still
     widen the DOCUMENT: the pocket's disc sits about 180 px in at 360 px, its panel wants
     12.5rem for the longest tool name, and 180 + 200 is wider than the phone — the sideways
     scroll of CP-C1 finding F3 all over again, from the other end. Anchoring to the masthead
     instead makes the panel exactly as wide as the page and never one pixel wider, at any
     viewport, in any language, which is a promise a measurement cannot make.

     `position: static` on the two disclosures is what hands the panels that containing block;
     `inset-block-start: calc(100% + …)` in base.css then drops them below the whole masthead
     rather than below one row of it, which is where a full-width panel belongs.

     `min-inline-size: 0` here is NOT the GUARDED/EXEMPT sweep's flex-item automatic-minimum
     guard above (job 5 only parses that pair off the two content classes there; a panel is
     absolutely positioned, never a flex item, so it is out of that sweep's remit entirely). It
     resets base.css's own floors instead — `.site-panel` sets 11rem and `.site-tools__panel`
     sets 12.5rem so neither shrinks below its longest label on a wide viewport — because a
     11rem/12.5rem floor is exactly what would fight `inset-inline: var(--sp-3)` on a 360 px
     phone: the box would either hold its floor and overflow the masthead, or the inset would
     be the one that quietly loses. Dropping the floor is what lets the inset win and the panel
     span edge to edge instead. */
  .site-nav__mast-inner { position: relative; }
  .site-lang,
  .site-tools { position: static; }
  .site-panel { inset-inline: var(--sp-3); min-inline-size: 0; }
}

/* ══ home (Task 7) ══════════════════════════════════════════════════════════════════════
   Ported from the signed-off prototype's page <style> block (docs/prototype/home.html), which
   a built page may not carry. Same composition and same class names, so the two stay diffable.

   The home page is the one marketing-register surface in the suite, and it makes its claim with
   the product's own vocabulary rather than with decoration: the disc is painted from the same
   six segment tokens the wheel engine paints, and the six names under it carry the same
   numbered chips the entries editor uses, in the same order. Chip 3 and slice 3 are the same
   colour here for the same reason they are on the wheel page.

   What is NOT ported: the prototype's `.hm-tool--soon` / `.hm-soon` badge pair. The grid renders
   from the registry now, so a tool that has no page has no card — there is nothing to label. */

/* One rhythm rule for the whole page, so no section has to fight a neighbour's margin.
   Sections set their own inner spacing and never their outer. */
.hm { padding-block: var(--sp-7) var(--sp-8); }
.hm > * + * { margin-block-start: var(--sp-8); }

/* ── hero ─────────────────────────────────────────────────────────────── */
.hm-hero { display: grid; gap: var(--sp-6); align-items: center; }
@media (min-width: 52rem) {
  .hm-hero {
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
    gap: var(--sp-8);
  }
}

.hm-hero__text { display: grid; justify-items: start; gap: var(--sp-5); min-inline-size: 0; }
/* One more step of the type scale's own 1.25 ratio, derived from the token rather than typed —
   the front door is the only display-sized line in the suite, and the clamp hands the extra
   size back on a phone. */
.hm-hero__text h1 { font-size: clamp(var(--fs-5), 5.4vw, calc(var(--fs-5) * 1.25)); }
.hm-hero__text .lede { max-inline-size: 46ch; }

.hm-stage { display: grid; justify-items: center; gap: var(--sp-4); min-inline-size: 0; }

/* The disc: the six slice colours in the wheel's own order, starting at twelve o'clock and
   turning clockwise — exactly what wheel-engine.js paints. Segment colours never invert, so
   this looks the same in both themes by construction; the rim, the hub and the pointer follow
   the theme, as they do on the canvas. */
.hm-disc {
  position: relative;
  inline-size: min(100%, 20rem);
  aspect-ratio: 1;
  border: 2px solid var(--line);
  border-radius: var(--r-round);
  background: conic-gradient(
    var(--seg-1) 0 60deg, var(--seg-2) 0 120deg, var(--seg-3) 0 180deg,
    var(--seg-4) 0 240deg, var(--seg-5) 0 300deg, var(--seg-6) 0 360deg);
}
/* Parked at twelve o'clock: `inset-inline: 0` with `margin-inline: auto` centres it whichever
   way the page runs. A wheel is not a directional icon and never mirrors (spec §4.3), so there
   is nothing here to flip. */
.hm-disc::before {
  content: "";
  position: absolute;
  inset-block-start: calc(var(--sp-1) * -1);
  inset-inline: 0;
  margin-inline: auto;
  inline-size: var(--sp-4);
  block-size: var(--sp-5);
  background: var(--ink);
  clip-path: polygon(50% 100%, 0 0, 100% 0);
}
.hm-disc__hub {
  position: absolute;
  inset: 37%;
  background: var(--surface);
  border: 2px solid var(--line);
  border-radius: var(--r-round);
}

.hm-legend {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  /* --sp-3 is the chip list's own gap (components.css) — the same list, smaller. */
  gap: var(--sp-3);
  /* Held near the disc's own width so the six names fall into two even rows instead of
     four-and-two. */
  max-inline-size: 26rem;
}
.hm-caption { max-inline-size: 40ch; text-align: center; }

/* ── tools ────────────────────────────────────────────────────────────── */
.hm-tools { display: grid; gap: var(--sp-5); }
.hm-grid {
  display: grid;
  gap: var(--sp-4);
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
}

.hm-tool {
  position: relative;
  display: grid;
  align-content: start;
  min-inline-size: 0;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  transition: background-color var(--t-fast) ease-out, border-color var(--t-fast) ease-out;
}
.card-hero {
  display: block;
  inline-size: 100%;
  block-size: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-start-start-radius: var(--r-2);
  border-start-end-radius: var(--r-2);
}
.hm-tool__content { display: grid; gap: var(--sp-2); padding: var(--sp-5); }
.hm-tool__name { font-size: var(--fs-2); font-weight: 800; line-height: var(--lh-tight); }
/* A stretched link: the whole card is the target, but the accessible name is the tool's name
   alone — not the card's paragraph read out as a link. */
.hm-tool__name a { color: var(--ink); text-decoration: none; }
.hm-tool__name a::after { content: ""; position: absolute; inset: 0; border-radius: var(--r-2); }
.hm-tool__name a:hover { text-decoration: underline; }
.hm-tool:has(a:hover) { background: var(--surface-2); border-color: var(--ink-2); }
/* The ring belongs on the thing that looks clickable, which is the card. */
.hm-tool:has(a:focus-visible) { outline: 2px solid var(--brand); outline-offset: 2px; }
.hm-tool__name a:focus-visible { outline: none; }
.hm-tool__body { color: var(--ink-2); }
.hm-tools > .hint { max-inline-size: 66ch; }

/* ── proof strips ─────────────────────────────────────────────────────── */
.hm-proof { display: grid; gap: var(--sp-5); }
/* `auto-fit` rather than `repeat(3, …)`: the third strip is the API and the MCP server, and it
   renders only once they are live (manifest.homeStripIds). Two strips should fill the row, not
   leave a third of it empty. */
@media (min-width: 52rem) {
  .hm-proof { grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: var(--sp-6); }
}
.hm-proof__item {
  display: grid;
  align-content: start;
  gap: var(--sp-2);
  min-inline-size: 0;
  padding-block-start: var(--sp-4);
  /* a quiet rule, not an accent edge — no thick one-sided borders (owner, Checkpoint 2) */
  border-block-start: 1px solid var(--line);
}
.hm-proof__title { font-size: var(--fs-2); }
.hm-proof__item p { color: var(--ink-2); }
.hm-proof__item a { justify-self: start; }

/* ── the two promises ─────────────────────────────────────────────────── */
.hm-promises {
  display: grid;
  gap: var(--sp-6);
  padding-block-start: var(--sp-6);
  border-block-start: 1px solid var(--line);
}
@media (min-width: 52rem) {
  .hm-promises { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-8); }
}
.hm-promise { display: grid; align-content: start; gap: var(--sp-3); min-inline-size: 0; }
.hm-promise p { color: var(--ink-2); max-inline-size: 46ch; }

/* ── start ────────────────────────────────────────────────────────────── */
/* The same surface the picked name arrives on: the page ends where a spin ends. */
.hm-start {
  display: grid;
  justify-items: center;
  gap: var(--sp-4);
  padding-block: var(--sp-7);
  padding-inline: var(--sp-5);
  background: var(--brand-soft);
  border: 1px solid var(--brand);
  border-radius: var(--r-2);
  text-align: center;
}
.hm-start p { max-inline-size: 44ch; }

/* ══ the wheel page (Task 10) ═══════════════════════════════════════════════════════════
   Ported from the signed-off prototype's page <style> block (docs/prototype/wheel.html), which
   a built page may not carry. Same composition, same class names, so the two stay diffable.
   Every component it arranges (.card, .chip, .btn, .switch, .modal, .result-banner,
   .projector) is components.css's and is used unchanged.

   Task 11 gives the number wheel and the yes-or-no wheel the SAME stage — they are wheels, and
   a second copy of `.wh-stage`, `.wh-disc`, `.wh-canvas-box` and the projector block under
   other names would be a hundred lines of duplicate geometry that could drift. What differs on
   those two pages is the console above the disc, and that is `.sp-*` further down.

   The page has two registers and the CSS keeps them apart:
   • the WORKBENCH (masthead → stage → console) is an instrument — micro-labels in the
     .eyebrow treatment, tabular figures, dense spacing, one primary action;
   • the MANUAL below it is prose — margin headings, a reading measure, generous leading.
   The rule between them is the fairness strip.

   `.wh-main` replaces `.page` on <main> (see _layout.tsx's `mainClass`): this page brings its
   own vertical rhythm, section by section, and inheriting the content-page gap on top of it
   would be two rhythms fighting.

   The wheel section adds no content-carrying flex/grid item of its own — the two that carry
   free-form user text are components.css's `.chip__name` (GUARDED) and `.result-banner__name`
   (EXEMPT). `.wh-chip-weight` and `.wh-chip-colour` are a fixed-width number box and a fixed
   square swatch, both `flex: none`, the same shape as `.chip__n` and `.chip__x`. The picker
   section below DOES add some, and they are classified in this file's own GUARDED/EXEMPT
   columns at the top. */

.wh-main { padding-block-end: var(--sp-8); }

/* ── masthead ─────────────────────────────────────────────────────────
   A tool page, so the headline is fs-4 and not fs-5: the wheel is the hero and the type must
   not out-shout it. */
.wh-head {
  display: grid;
  gap: var(--sp-4);
  align-items: start;
  padding-block: var(--sp-6) var(--sp-5);
}
@media (min-width: 46rem) {
  .wh-head { grid-template-columns: minmax(0, 1fr) auto; column-gap: var(--sp-5); }
}
.wh-head__text { display: grid; gap: var(--sp-2); }
.wh-head h1 { font-size: var(--fs-4); }
.wh-head__actions { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.wh-lede-short, .wh-edit { display: none; }
#wh-entries { scroll-margin-block-start: 6rem; }
.ns-import-preview { margin-block-end: var(--sp-5); }
@media (max-width: 61.99rem) {
  .wh-head .wh-lede-full { display: none; }
  .wh-head .wh-lede-short { display: block; }
  .wh-head .wh-edit { display: inline-flex; }
}

/* ── stage: the wheel (60 %) and the editor (40 %) ────────────────────── */
.wh-stage { display: grid; gap: var(--sp-5); align-items: start; }
/* Grid children must be free to shrink below their content, or a long unbroken name pushes the
   whole page wider than a phone. */
.wh-stage > * { min-inline-size: 0; }
@media (min-width: 62rem) {
  .wh-stage {
    grid-template-columns: minmax(0, 3fr) minmax(19rem, 2fr);
    gap: var(--sp-6);
  }
}

/* ── the theme presets (Task 16a, spec §3.1) ──────────────────────────
   A preset is a SCOPED OVERRIDE and never an edit to the theme: `--seg-1 … --seg-6` keep the
   values tokens.css signed off, and one of these classes redefines them for this page's stage
   only. The palettes themselves are token families (`--pal-*`), so nothing here declares a
   colour — this file's own rule.

   ON `.wh-stage`, deliberately, and not on the canvas box. The chip list is the disc's LEGEND
   (components.css: "chip N and slice N share a colour"), and the chips live in the editor
   beside the disc; scoping to their common ancestor is what keeps that promise true under a
   preset without a second set of rules for the chips. The canvas re-reads the six values on
   the `themechange` event the page fires after the swap — a canvas cannot repaint itself from
   CSS. "Classic" is the absence of all three classes. */
.wh-pal-warm {
  --seg-1: var(--pal-warm-1); --seg-2: var(--pal-warm-2); --seg-3: var(--pal-warm-3);
  --seg-4: var(--pal-warm-4); --seg-5: var(--pal-warm-5); --seg-6: var(--pal-warm-6);
}
.wh-pal-cool {
  --seg-1: var(--pal-cool-1); --seg-2: var(--pal-cool-2); --seg-3: var(--pal-cool-3);
  --seg-4: var(--pal-cool-4); --seg-5: var(--pal-cool-5); --seg-6: var(--pal-cool-6);
}
.wh-pal-bright {
  --seg-1: var(--pal-bright-1); --seg-2: var(--pal-bright-2); --seg-3: var(--pal-bright-3);
  --seg-4: var(--pal-bright-4); --seg-5: var(--pal-bright-5); --seg-6: var(--pal-bright-6);
}

.wh-disc { display: grid; gap: var(--sp-4); justify-items: center; }

/* The canvas sits in a square box so the hub target can be a percentage of it and land exactly
   on the hub the engine draws. Capped by the viewport as well as by the page, so the Spin
   button — the one primary action — stays above the fold on a laptop. */
.wh-canvas-box {
  position: relative;
  inline-size: 100%;
  max-inline-size: min(34rem, 58dvh);
  aspect-ratio: 1;
}
.wh-canvas { inline-size: 100%; block-size: 100%; }

/* Click the hub to spin (spec §3.1). Geometry: with a hub word to hold (Checkpoint 2) the
   engine draws the hub at 22 % of the disc RADIUS, and that radius is 92 % of half the box —
   so the hub's diameter is 2 × 0.22 × 0.46 ≈ 20 % of the square's side. Below about a 220 px
   disc that falls under the 2.75rem touch minimum every control in components.css keeps, so
   the target stops shrinking there and grows a little past the painted hub, which is the right
   way round for a thumb. */
.wh-hub {
  position: absolute;
  inset-block: 0;
  inset-inline: 0;
  margin: auto;
  inline-size: 20%;
  block-size: 20%;
  min-inline-size: 2.75rem;
  min-block-size: 2.75rem;
  border-radius: var(--r-round);
}

/* The centre image (Task 16a, spec §3.1's "center logo upload"). A DOM overlay and never a
   canvas draw: the engine is frozen, and a picture painted into the disc would turn with it —
   a logo is not a slice.

   `pointer-events: none` IS THE WHOLE ACCESSIBILITY OF IT. This element sits directly over
   `.wh-hub`, which is the third spin trigger (spec §3.1), so an overlay that took the pointer
   would silently kill clicking the centre to spin — the one thing about this feature the matrix
   asserts by name. Same geometry as the hub, so the picture lands exactly where the engine
   draws it; the engine's hub word steps aside while an image is set (wheel-page.js). */
.wh-logo {
  position: absolute;
  inset-block: 0;
  inset-inline: 0;
  margin: auto;
  inline-size: 20%;
  block-size: 20%;
  min-inline-size: 2.75rem;
  min-block-size: 2.75rem;
  border-radius: var(--r-round);
  border: 1px solid var(--line);
  background: var(--surface);
  object-fit: cover;
  pointer-events: none;
}

.wh-spin {
  inline-size: 100%;
  max-inline-size: 22rem;
  min-block-size: 3.5rem;
  font-size: var(--fs-2);
}

.wh-note { text-align: center; max-inline-size: 42ch; }
.wh-overflow { inline-size: 100%; max-inline-size: 42ch; }
.wh-overflow .row { align-items: flex-start; }
.wh-overflow input[type="checkbox"] { flex: none; margin-block-start: .25em; }
.wh-partial-choice { cursor: pointer; min-block-size: 2.75rem; }
/* Reduced motion and the empty-roster error are states, not decoration: colour and
   start-aligned text — not a bar — are what make them read as the machine talking back. */
.wh-note--state {
  text-align: start;
  color: var(--ink);
}

/* ── picked so far ────────────────────────────────────────────────────
   The ledger. A wheel in a room produces a sequence, and the sequence is what makes
   remove-after-win and elimination legible — without it those modes have no visible product.
   Order is the information here, so these chips carry no segment colour: colour means "this
   slice", and these names have left it. */
.wh-ledger {
  inline-size: 100%;
  display: grid;
  gap: var(--sp-3);
  padding-block-start: var(--sp-4);
  border-block-start: 1px solid var(--line);
}
.wh-ledger__head { display: flex; align-items: center; gap: var(--sp-3); }
.wh-ledger__head .btn { margin-inline-start: auto; }
.wh-ledger .chip { min-block-size: 2.25rem; font-size: var(--fs-0); }

/* ── entries editor ───────────────────────────────────────────────────── */
.wh-limit {
  border: 1px solid var(--warn);
  border-radius: var(--r-1);
  padding: var(--sp-3) var(--sp-4);
  background: color-mix(in srgb, var(--warn) 6%, var(--surface));
  color: var(--ink);
}
/* The count and the duplicate pill are one line of status, tighter than a default row. Was an
   inline `--row-gap` override in the prototype; a built page carries no inline style. */
.wh-count { --row-gap: var(--sp-2); }

/* The per-entry weight (spec §3.1), shown inside its chip when the console's Weight switch is
   on. `flex: none` and a fixed measure, like .chip__n and .chip__x beside it: it is a number
   box, not free text, so it never squeezes the name. The spinner arrows are left alone —
   removing them takes vendor pseudo-elements and a keyboard user loses the step buttons. */
.wh-chip-weight {
  flex: none;
  inline-size: 4.25rem;
  min-block-size: 2.25rem;
  padding-inline: var(--sp-2);
  font-size: var(--fs-0);
  font-variant-numeric: tabular-nums;
}

/* The per-entry colour (spec §3.1), shown inside its chip when the console's Colour switch is
   on. A square of the same measure as the weight box beside it, `flex: none` for the same
   reason. The three vendor pseudo-elements are the only way to reach inside a native colour
   input: without them the swatch keeps a browser-drawn inset border that reads as a second,
   wrong-coloured edge on a chip that is already coloured.

   THE CHOSEN COLOUR ITSELF IS NOT HERE, and cannot be. `.chip-seg-1 … -6` (components.css)
   hold the six the design system knows; an override is one of sixteen million picked a second
   ago, so wheel-page.js sets `--chip-bg`/`--chip-edge` on the chip element through the CSSOM.
   That is the single element-level style in this codebase and the reason is written out beside
   it — see the note above `.ns-celebrate` at the top of this file for the rule it is the
   exception to. */
.wh-chip-colour {
  flex: none;
  inline-size: 2.25rem;
  block-size: 2.25rem;
  padding: var(--sp-1);
  background: var(--surface);
  border: 1px solid var(--ink-2);
  border-radius: var(--r-1);
  cursor: pointer;
}
.wh-chip-colour::-webkit-color-swatch-wrapper { padding: 0; }
.wh-chip-colour::-webkit-color-swatch { border: none; border-radius: var(--r-1); }
.wh-chip-colour::-moz-color-swatch { border: none; border-radius: var(--r-1); }

/* ── console ──────────────────────────────────────────────────────────
   The instrument panel: what the spin does, and what it sounds like. Ruled top and bottom so
   it reads as machine settings rather than as content. */
.wh-console {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--sp-4) var(--sp-6);
  margin-block-start: var(--sp-6);
  padding-block: var(--sp-4);
  border-block: 1px solid var(--line);
}
.wh-ctrl { display: grid; gap: var(--sp-2); }
/* .input is inline-size:100% for a form column; in the panel each control is only as wide as
   its own job. */
.wh-console .input { inline-size: auto; min-inline-size: 11rem; }
.wh-console .wh-num { min-inline-size: 6rem; }
/* Its own full-width line under the controls, like `.wh-console__notes` below it — starting at
   the same inline-start edge as the controls above (owner fix B2, 2026-09-03:
   `margin-inline-start: auto` used to push it to the far end instead, under the selects). */
.wh-switches {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-5);
  flex-basis: 100%;
  min-inline-size: 0;
}
.wh-switches .switch { flex-wrap: wrap; }
/* Full-width lines under the controls, tighter than the panel's own row gap so they read as one
   footnote block rather than stray sentences. */
.wh-console__notes { flex-basis: 100%; display: grid; gap: var(--sp-2); }

/* ── fairness strip: the rule between the machine and the manual ──────── */
.wh-fair {
  display: grid;
  gap: var(--sp-3);
  margin-block-start: var(--sp-6);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  padding: var(--sp-4) var(--sp-5);
}
.wh-fair p { max-inline-size: 72ch; }

/* ── the manual ───────────────────────────────────────────────────────
   Headings live in the margin, the way a spec sheet labels its blocks; the prose keeps a
   reading measure of its own. */
.wh-section {
  display: grid;
  gap: var(--sp-4);
  margin-block-start: var(--sp-7);
  padding-block-start: var(--sp-6);
  border-block-start: 1px solid var(--line);
  /* THESE SECTIONS ARE JUMPED TO, not only scrolled past: the "How to use this tool" line on
     each of the six tool pages links to `/help#help-<group>`. Above 46rem the site header is
     sticky (base.css), so a landing with no offset put the <h2> under it — measured at 1280×800
     before this line, the heading's top sat at y≈33 px with the header's bottom at y≈50 px.
     5rem is the offset `.wh-section__head`'s own sticky already uses, and the styleguide's
     precedent (`.sg-section`). Logical, so RTL and any future vertical writing get it free.
     Harmless on the wheel page's manual and the tool pages' FAQ sections, which nothing links
     into by fragment: scroll-margin costs nothing until a scroll targets the box. */
  scroll-margin-block-start: 5rem;
}
@media (min-width: 60rem) {
  .wh-section { grid-template-columns: 13rem minmax(0, 1fr); gap: var(--sp-8); }
  .wh-section__head { position: sticky; inset-block-start: 5rem; }
}
.wh-section__head { align-content: start; display: grid; gap: var(--sp-2); }
.wh-section__head h2 { font-size: var(--fs-3); }

.wh-steps { display: grid; gap: var(--sp-5) var(--sp-6); }
@media (min-width: 40rem) { .wh-steps { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* The line under the steps — four on the wheel, three under the number and yes-or-no guides on
   /help (pages/help.tsx's STEPS). An inline margin in the prototype. */
.wh-steps__after { margin-block-start: var(--sp-5); }
.wh-step { display: grid; gap: var(--sp-2); align-content: start; }
/* A real sequence, so it is really numbered. A position is a number: LTR always. */
.wh-step__n {
  font-size: var(--fs-0);
  font-weight: 800;
  letter-spacing: .12em;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
  direction: ltr;
  unicode-bidi: isolate;
}
.wh-step h3 { font-size: var(--fs-2); }
.wh-step p { color: var(--ink-2); max-inline-size: 46ch; }

.wh-faq details { padding-block: var(--sp-4); border-block-end: 1px solid var(--line); }
.wh-faq details:first-child { padding-block-start: 0; }
.wh-faq summary {
  display: flex;
  align-items: baseline;
  gap: var(--sp-4);
  font-size: var(--fs-2);
  font-weight: 800;
  line-height: var(--lh-tight);
  cursor: pointer;
  list-style: none;
}
.wh-faq summary::-webkit-details-marker { display: none; }
/* A symmetric glyph: nothing to mirror, and it survives a mono print. */
.wh-faq summary::after {
  content: "+";
  margin-inline-start: auto;
  font-size: var(--fs-3);
  font-weight: 400;
  line-height: 1;
  color: var(--ink-2);
}
.wh-faq details[open] summary::after { content: "\2212"; }
.wh-faq details p {
  margin-block-start: var(--sp-3);
  max-inline-size: 68ch;
  color: var(--ink-2);
}

/* ── result card ──────────────────────────────────────────────────────── */
#wh-winner .card__actions {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--sp-3);
}

/* ── projector ────────────────────────────────────────────────────────
   .projector on <body> hides the nav, the skip link, the footer and everything marked
   data-no-projector (components.css) and steps the type scale up. What is left is centred in
   the screen and the disc is sized by the screen's height, so 1280×720 and 1920×1080 both fill
   without clipping. */
.wh-projector-only { display: none; }
.projector p.wh-projector-only { display: block; }
.projector .btn.wh-projector-only { display: inline-flex; }
/* Nothing to promote and nothing to interrupt: the language banner is chrome, and a projector
   is chrome-free (spec §3.1). */
.projector .lang-suggest { display: none; }
/* The screen is a fixed box, so the disc is not sized by a guessed fraction of it — it takes
   whatever height the exit button, the Spin button, the hint and the ledger have not claimed.
   A wheel that shrinks when the ledger grows always fits; 64dvh fits until somebody wins four
   times. The canvas drops its square aspect here and the engine centres the disc inside it on
   min(width, height), which is what makes the leftover box safe to hand over. */
.projector .wh-main {
  display: flex;
  flex-direction: column;
  justify-content: center;
  block-size: 100dvh;
  padding-block: var(--sp-4);
}
.projector .wh-head { flex: none; padding-block: 0 var(--sp-4); justify-items: center; }
.projector .wh-head__actions { justify-content: center; }
/* Every box in the chain has to be a DEFINITE height, or the canvas box's own `block-size:
   100%` has nothing to resolve against and falls back to its natural square. */
.projector .wh-stage {
  flex: 1 1 auto;
  min-block-size: 0;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  align-items: stretch;   /* the page pins the two columns to the top; here the disc fills */
}
.projector .wh-disc {
  min-block-size: 0;
  grid-template-rows: minmax(0, 1fr);   /* row 1 is the canvas; the rest stay auto */
  /* …and a DEFINITE column, for the same reason the rows are definite. The canvas box below
     is `inline-size: auto` with `aspect-ratio: 1`, so it takes its width from its height —
     and `max-inline-size: 100%` can only clamp that against a containing block whose width is
     already known. Under `justify-items: center` the grid area sizes itself to the item, the
     clamp resolves against a width that depends on the item it is clamping, and on a screen
     that is much taller than it is wide the disc comes out wider than the viewport: 631 px of
     canvas in a 412 px phone, 648 px of scrollWidth, and the exit button pushed out from under
     the pointer. A projector is a wide screen, but `#projector` is a URL now (spec §3.1) and
     somebody will open it on a phone. */
  grid-template-columns: minmax(0, 1fr);
}
/* The box stays square and takes the height the rest of the screen left it, so the disc is as
   big as the screen allows and the hub target keeps its percentage. */
.projector .wh-canvas-box {
  block-size: 100%;
  inline-size: auto;
  max-inline-size: 100%;
}
.projector .wh-spin { max-inline-size: 28rem; }
/* A long run of picked names must never squeeze the wheel off the screen. */
.projector .wh-ledger { max-block-size: 20dvh; overflow: auto; }

/* ══ the name picker (Task 11) ══════════════════════════════════════════════════════════
   Ported from the signed-off prototype's page <style> block (docs/prototype/name-picker.html).
   Same composition, same class names, so the two stay diffable. Every component it arranges
   (.card, .entries-editor, .chip, .btn, .switch, .field, .input) is components.css's and is
   used unchanged.

   The wheel page is the house style and this page keeps its workbench register — fs-4
   headline, .eyebrow micro-labels, ruled bands, one primary action shaped like the Spin bar.
   What it does NOT keep is the wheel's anatomy, because the instrument is different: the wheel
   has one hero object and a list beside it, so it is 60/40; the picker has an INTAKE and an
   OUTCOME and neither outranks the other, so the stage is two equal halves.

   TWO DIVERGENCES FROM THE PROTOTYPE, both forced by a rule a built page keeps and a
   prototype did not:
   1. The slip's arrival stagger was `--i` set per row from JavaScript — an inline style. It is
      `:nth-child()` here instead, for the first twelve rows, which is where the prototype's
      own STAGGER_CAP stopped anyway.
   2. The chip list and the slip sit in scroll boxes. The prototype rendered a fixed first 300
      chips and stopped; this page's list is allowed to be 200,000 names long and grows its
      window as you scroll (picker-state.js), so the list has to have an end. */

.pk-main { padding-block-end: var(--sp-8); }

/* ── masthead ─────────────────────────────────────────────────────────── */
.pk-head {
  display: grid;
  gap: var(--sp-2);
  padding-block: var(--sp-6) var(--sp-5);
}
.pk-head h1 { font-size: var(--fs-4); }

/* ── stage ────────────────────────────────────────────────────────────── */
.pk-stage { display: grid; gap: var(--sp-5); align-items: start; }
/* Grid children must be free to shrink below their content, or one unbroken name pushes the
   whole page wider than a phone. */
.pk-stage > * { min-inline-size: 0; }
@media (min-width: 62rem) {
  .pk-stage {
    grid-template-columns: minmax(0, 1fr) minmax(20rem, 1fr);
    gap: var(--sp-6);
  }
}

.pk-draw { display: grid; gap: var(--sp-4); align-content: start; }

/* The count and the duplicate pill are one line of status, tighter than a default row. Was an
   inline `--row-gap` override in the prototype; a built page carries no inline style. */
.pk-count { --row-gap: var(--sp-2); }

/* ── the windows ──────────────────────────────────────────────────────
   The scroll box the growing window lives in (picker-state.js's WINDOW_STEP / WINDOW_MAX). It
   is a focusable, named region on purpose: a scrollable area that a keyboard cannot reach is a
   WCAG failure, and `tabindex="0"` plus the group's aria-labelledby is the fix. */
.pk-scroll {
  max-block-size: 20rem;
  overflow: auto;
  overscroll-behavior: contain;
  padding-block: var(--sp-1);
}

/* ── the two settings ─────────────────────────────────────────────────
   Ruled off below, so they read as machine settings sitting on top of the action rather than
   as content. */
.pk-set {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--sp-3) var(--sp-5);
  padding-block-end: var(--sp-4);
  border-block-end: 1px solid var(--line);
}
.pk-ctrl { display: grid; gap: var(--sp-2); }
/* .input is inline-size:100% for a form column; here the field is only as wide as the number
   it holds. */
.pk-set .pk-num { inline-size: auto; min-inline-size: 6rem; }
.pk-set__note { flex-basis: 100%; }

/* The one primary action, the same bar the wheel gives Spin: full width, one step up. */
.pk-go {
  inline-size: 100%;
  min-block-size: 3.5rem;
  font-size: var(--fs-2);
}

/* States read as the machine talking back — the wheel page's treatment for its empty-roster
   and reduced-motion lines. */
.pk-note--state {
  color: var(--ink);
}
.pk-limit {
  border: 1px solid var(--warn);
  border-radius: var(--r-1);
  padding: var(--sp-3) var(--sp-4);
  background: color-mix(in srgb, var(--warn) 6%, var(--surface));
  color: var(--ink);
}

/* ── the slip ─────────────────────────────────────────────────────────
   The page's signature. A result here is a running order — who goes first, who goes next — so
   it is set like one: a numbered column with a hairline between rows, not a paragraph and not
   a bag of chips. */
.pk-slip-panel { display: grid; gap: var(--sp-3); }
.pk-slip-panel__head { display: flex; align-items: center; gap: var(--sp-3); }
/* Copy result and the panel's own Close travel together at the far end. The pair is what
   takes the `auto` margin — a rule on `.btn` would push each of them in turn and leave the
   title stranded between them. */
.pk-slip-panel__actions { margin-inline-start: auto; --row-gap: var(--sp-2); }

.pk-slip { display: grid; }
.pk-slip li {
  --slip-seg: var(--surface-2);
  --slip-ink: var(--ink-2);
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  padding-block: var(--sp-3);
  border-block-end: 1px solid var(--line);
  /* base.css switches every animation off under prefers-reduced-motion, and `animation: none`
     drops the fill mode with it, so a row that never animates simply renders — no query
     needed here. */
  animation: pk-arrive var(--t-base) ease-out both;
}
.pk-slip li:last-child { border-block-end: 0; }
@keyframes pk-arrive {
  from { opacity: 0; translate: 0 var(--sp-2); }
  to   { opacity: 1; translate: 0 0; }
}
/* The stagger, without an inline style: the first twelve rows arrive in sequence and
   everything after them arrives with the twelfth, which is exactly where the prototype's
   STAGGER_CAP stopped. */
.pk-slip li:nth-child(1)  { animation-delay: 0ms; }
.pk-slip li:nth-child(2)  { animation-delay: 45ms; }
.pk-slip li:nth-child(3)  { animation-delay: 90ms; }
.pk-slip li:nth-child(4)  { animation-delay: 135ms; }
.pk-slip li:nth-child(5)  { animation-delay: 180ms; }
.pk-slip li:nth-child(6)  { animation-delay: 225ms; }
.pk-slip li:nth-child(7)  { animation-delay: 270ms; }
.pk-slip li:nth-child(8)  { animation-delay: 315ms; }
.pk-slip li:nth-child(9)  { animation-delay: 360ms; }
.pk-slip li:nth-child(10) { animation-delay: 405ms; }
.pk-slip li:nth-child(11) { animation-delay: 450ms; }
.pk-slip li:nth-child(n + 12) { animation-delay: 495ms; }

/* A position is a number: tabular, LTR in every locale, and aligned toward the name it belongs
   to. */
.pk-slip__n {
  display: grid;
  place-items: center;
  flex: none;
  inline-size: var(--sp-6);
  block-size: var(--sp-6);
  border-radius: var(--r-round);
  background: var(--slip-seg);
  color: var(--slip-ink);
  font-size: var(--fs-0);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  direction: ltr;
  unicode-bidi: isolate;
}
.pk-slip__name {
  min-inline-size: 0;
  font-size: var(--fs-2);
  font-weight: 700;
  line-height: var(--lh-tight);
  overflow-wrap: anywhere;
}
/* Segment colours are identical in both themes, so anything drawn on one takes the fixed
   --seg-ink rather than --ink, which flips to near-white in dark. */
.pk-slip li[data-seg="1"] { --slip-seg: var(--seg-1); --slip-ink: var(--seg-ink); }
.pk-slip li[data-seg="2"] { --slip-seg: var(--seg-2); --slip-ink: var(--seg-ink); }
.pk-slip li[data-seg="3"] { --slip-seg: var(--seg-3); --slip-ink: var(--seg-ink); }
.pk-slip li[data-seg="4"] { --slip-seg: var(--seg-4); --slip-ink: var(--seg-ink); }
.pk-slip li[data-seg="5"] { --slip-seg: var(--seg-5); --slip-ink: var(--seg-ink); }
.pk-slip li[data-seg="6"] { --slip-seg: var(--seg-6); --slip-ink: var(--seg-ink); }

/* ── history ──────────────────────────────────────────────────────────
   Session-local, and ruled off from the draw above it so it reads as a record rather than
   another result. */
.pk-hist {
  display: grid;
  gap: var(--sp-3);
  padding-block-start: var(--sp-4);
  border-block-start: 1px solid var(--line);
}
.pk-hist__head { display: flex; align-items: center; gap: var(--sp-3); }
.pk-hist__head .btn { margin-inline-start: auto; }
.pk-hist__list { display: grid; gap: var(--sp-2); }
.pk-hist__row {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  font-size: var(--fs-0);
  /* A row is a grid item, so its automatic minimum size is its min-content — and the names
     inside it are `nowrap`, which makes that min-content the whole unbroken run. One long draw
     would claim the width of every name in it and stretch the page past a phone. The names
     cell already ellipsises, which is what makes shrinking safe. */
  min-inline-size: 0;
}
/* A clock reading is a machine value: tabular, and it never wraps. */
.pk-hist__time {
  flex: none;
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}
.pk-hist__count { flex: none; color: var(--ink-2); }
/* Names, clipped rather than wrapped: a row is one line of a log. */
.pk-hist__names {
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── seed strip ───────────────────────────────────────────────────────
   The claim the picker makes about itself, in the position and the treatment the wheel page
   gives its fairness strip. */
.pk-seed {
  display: grid;
  gap: var(--sp-3);
  margin-block-start: var(--sp-6);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  padding: var(--sp-4) var(--sp-5);
}
.pk-seed .field { max-inline-size: 30rem; }
/* A seed is a technical string: monospace, LTR, in every locale. */
.pk-seed__field {
  font-size: var(--fs-0);
  font-variant-numeric: tabular-nums;
  letter-spacing: .02em;
}
.pk-seed p { max-inline-size: 72ch; }

/* ══ the two fixed-set wheels (Task 11b) ════════════════════════════════════════════════
   `/number-wheel` and `/yes-no-wheel`. Everything geometric is the wheel section above —
   `.wh-main`, `.wh-stage`, `.wh-disc`, `.wh-canvas-box`, `.wh-spin`, `.wh-note`, `.wh-ledger`
   and the whole `.projector` block — used unchanged, because they ARE that wheel with a
   different set of slices on it. What is here is only what those two pages add: a console in
   the stage's second column instead of a roster editor, an answer banner, and a tally. */

/* The console. A card rather than the name wheel's full-width band, because this stage is a
   disc and a panel side by side rather than a disc and a roster editor. */
.sp-console { align-content: start; }
.sp-ctrl { display: grid; gap: var(--sp-2); }
/* .input is inline-size:100% for a form column; a number box is only as wide as its number. */
.sp-console .sp-num { inline-size: auto; min-inline-size: 7rem; }
.sp-range { display: flex; flex-wrap: wrap; gap: var(--sp-4); }
.sp-switches { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5); }
/* The wheel's ceiling, said out loud — the same warn rule the name wheel's limit note takes. */
.sp-cap {
  border: 1px solid var(--warn);
  border-radius: var(--r-1);
  padding: var(--sp-3) var(--sp-4);
  background: color-mix(in srgb, var(--warn) 6%, var(--surface));
  color: var(--ink);
}

/* The answer, under the disc rather than in a modal: these two wheels are spun over and over,
   and a card to dismiss between every spin is a card in the way. */
.sp-banner {
  inline-size: 100%;
  max-inline-size: 34rem;
}

/* The tally. One row per option INCLUDING the ones that never came up, in wheel order — a list
   that reorders itself while you read it is a list nobody can follow, and "No: 0 spins" is
   exactly the fact somebody is looking for. */
.sp-tally { inline-size: 100%; display: grid; gap: var(--sp-2); }
.sp-tally__row {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  padding-block: var(--sp-2);
  border-block-end: 1px solid var(--line);
  /* A grid item's automatic minimum size is its min-content, and the answer beside it can be
     one unbroken run in any script. */
  min-inline-size: 0;
}
.sp-tally__row:last-child { border-block-end: 0; }
.sp-tally__name {
  min-inline-size: 0;
  overflow-wrap: anywhere;
  font-weight: 700;
}
/* A count is a machine value: tabular, never wrapped, and pushed to the end in either
   direction by a logical margin rather than by a text-align. */
.sp-tally__n {
  flex: none;
  margin-inline-start: auto;
  color: var(--ink-2);
  font-size: var(--fs-0);
  font-variant-numeric: tabular-nums;
}

/* ── the roster boxes ─────────────────────────────────────────────────
   Every textarea a person pastes a LIST into. `unicode-bidi: plaintext` resolves each
   PARAGRAPH — which in a textarea is each line — from its own first strong character, so an
   Arabic name and a Latin one on consecutive lines each read the right way round. That is
   spec §4.3's per-entry rule (the one every chip already follows) applied to the box the
   entries are typed in, and it is not `dir="auto"`, which resolves ONE direction for the whole
   value from whichever line happens to be first.

   It is also the only treatment that is fast on both engines at the size this box is allowed
   to reach. Measured on the reference machine, 200,000 lines pasted, time to a painted frame:

                        Chromium EN   Chromium AR   WebKit EN   WebKit AR
     inherit (was)         553 ms        702 ms       281 ms    25,947 ms
     dir="auto"            614 ms*       607 ms*      282 ms       332 ms
     unicode-bidi          585 ms        588 ms       300 ms       398 ms

   * `dir="auto"` also puts Chromium's renderer into a ~76-second stall that no in-page clock
   sees: the DOM is updated in 35 ms and the tab stays unresponsive long after. `plaintext` has
   neither problem. The wheel's own roster box is deliberately NOT changed — it is capped at
   1,000 names, where the same layout costs about a tenth of a second. */
.ns-roster { unicode-bidi: plaintext; }

/* The separator sample every roster editor prints after its hint (pages/_controls.tsx). The
   characters come from roster.js, so this is only their treatment: a little air between them so
   ، and ； read as seven separate marks rather than as one smear, and the same monospace-ish
   tracking a technical token gets anywhere else on the site. */
.ns-seps {
  letter-spacing: .18em;
  font-weight: 700;
  color: var(--ink);
}

/* ══ the team maker (Task 11) ═══════════════════════════════════════════════════════════
   Ported from the signed-off prototype's page <style> block (docs/prototype/team-maker.html),
   which a built page may not carry. Same composition, same class names, so the two stay
   diffable. Every component it arranges (.card, .entries-editor, .chip, .btn, .switch, .field,
   .input, .team-card) is components.css's and is used unchanged.

   The house register is the wheel's — fs-4 headline, .eyebrow micro-labels on machine settings,
   ruled bands, one primary action shaped like the Spin bar — and the anatomy is this tool's
   own, because it is a THREE-BEAT page:

     the intake  — the names, and what the page read in them
     the plan    — how many teams, balanced or not, and the rules that must hold
     the outcome — the team sheet, full width, because it is the thing you carry into the
                   room, print out, or put on a screen

   Intake and plan sit side by side while you set them up; the outcome takes the whole page the
   moment it exists, and nothing above it competes for the width.

   The page adds ONE content-carrying flex/grid item of its own, and it is in the EXEMPT column
   at the top of this file: `.tm-pair__kind`, which is `flex-basis: 100%` — a full-width row.
   The names themselves are drawn by components.css's `.team-card__member-name` (GUARDED there)
   and by table cells, which are not flex items at all. */

.tm-main { padding-block-end: var(--sp-8); }

/* ── masthead ─────────────────────────────────────────────────────────── */
.tm-head {
  display: grid;
  gap: var(--sp-2);
  padding-block: var(--sp-6) var(--sp-5);
}
.tm-head h1 { font-size: var(--fs-4); }

/* ── stage: the intake and the plan ───────────────────────────────────── */
.tm-stage { display: grid; gap: var(--sp-5); align-items: start; }
/* Grid children must be free to shrink below their content, or one unbroken name pushes the
   whole page wider than a phone. */
.tm-stage > * { min-inline-size: 0; }
@media (min-width: 62rem) {
  .tm-stage {
    grid-template-columns: minmax(0, 1fr) minmax(21rem, 1fr);
    gap: var(--sp-6);
  }
}
.tm-col { display: grid; gap: var(--sp-5); align-content: start; }

/* The count and the duplicate pill are one line of status, tighter than a default row. Was an
   inline `--row-gap` override in the prototype; a built page carries no inline style. */
.tm-count { --row-gap: var(--sp-2); }

/* The format, shown rather than described: three lines exactly as they should be typed.
   It is a <pre>, so base.css has already given it the monospace face and the LTR direction a
   technical string wants (ruling R32) — no face is chosen here, which is also what keeps this
   file inside base.css's four sanctioned readers (check battery job 5).
   `unicode-bidi: plaintext` is the one thing it does need: these three lines are a sample of
   USER INPUT, and the Arabic one has to start at the right while the line under it starts at
   the left. Same treatment, and the same reason, as `.ns-roster` above. */
.tm-example {
  margin: 0;
  padding: var(--sp-3);
  background: var(--surface-2);
  border-radius: var(--r-1);
  font-size: var(--fs-0);
  line-height: var(--lh-body);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  unicode-bidi: plaintext;
  color: var(--ink-2);
}

.tm-limit {
  border: 1px solid var(--warn);
  border-radius: var(--r-1);
  padding: var(--sp-3) var(--sp-4);
  background: color-mix(in srgb, var(--warn) 6%, var(--surface));
  color: var(--ink);
}

/* ── the parse preview ────────────────────────────────────────────────── */
/* A table can always want more width than a phone has. */
.tm-scroll { overflow-x: auto; }
.tm-table { inline-size: 100%; font-size: var(--fs-0); }
.tm-table th,
.tm-table td {
  padding-block: var(--sp-2);
  padding-inline-end: var(--sp-3);
  border-block-end: 1px solid var(--line);
  text-align: start;
  vertical-align: baseline;
}
.tm-table th { white-space: nowrap; }
.tm-table tr:last-child td { border-block-end: 0; }
/* A level is a number: tabular, and isolated so it cannot reorder the cells beside it.
   NOT `direction: ltr`, which is what the prototype set — and which, measured on the Arabic
   page, is a bug: `text-align: start` follows the CELL's direction, so an LTR cell in an RTL
   table aligns its number to the LEFT of a 143 px column while every other column aligns right,
   and the level ends up sitting against the Tag column reading as one run with it. A level is
   one digit; there is no multi-part number here for `direction` to protect, and the isolate is
   the whole of what a number beside a name needs (spec §4.3). */
.tm-table .tm-num {
  font-variant-numeric: tabular-nums;
  unicode-bidi: isolate;
}

/* ── the plan ─────────────────────────────────────────────────────────── */
.tm-plan { gap: var(--sp-4); }
/* Native radios, so the keyboard behaviour is the browser's: arrow keys move inside the group,
   Tab leaves it. Only the accent is ours. */
.tm-group { display: grid; gap: var(--sp-2); }
.tm-choices { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5); }
.tm-choice {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-block-size: 2.75rem;
  cursor: pointer;
}
.tm-choice input { accent-color: var(--brand); inline-size: 1.1rem; block-size: 1.1rem; }
/* .input is inline-size:100% for a form column; this one is as wide as the number it holds. */
.tm-num-field { inline-size: auto; min-inline-size: 6rem; }

.tm-rules { --stack-gap: var(--sp-3); }

/* Each rule kind is its own little form: a label, two names, one button. */
.tm-pair {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  padding-block-start: var(--sp-2);
}
.tm-pair__kind { flex-basis: 100%; }
.tm-pair .input { inline-size: auto; flex: 1 1 8rem; min-inline-size: 0; }

/* The kind of rule, said in the chip rather than drawn on it: colour is the wheel's language
   for "which slice", and a rule is not a slice. */
.tm-rule .eyebrow { flex: none; }

/* The primary action, the same bar the wheel gives Spin. */
.tm-go {
  inline-size: 100%;
  min-block-size: 3.5rem;
  font-size: var(--fs-2);
}

/* States read as the machine talking back — the wheel page's treatment for its empty-roster and
   reduced-motion lines. */
.tm-note--state {
  color: var(--ink);
}
/* A rule set that cannot hold is a refusal, not a warning: it carries the same glyph
   .pill-danger uses, so it survives a mono print and a colour-blind reader. */
.tm-alert {
  display: grid;
  gap: var(--sp-1);
  border: 1px solid var(--danger);
  border-radius: var(--r-1);
  padding: var(--sp-3) var(--sp-4);
  background: color-mix(in srgb, var(--danger) 6%, var(--surface));
  font-size: var(--fs-0);
}
.tm-alert__title { font-weight: 800; color: var(--ink); }
.tm-alert__title::before { content: "\2717"; margin-inline-end: var(--sp-2); color: var(--danger); }

/* ── the outcome ──────────────────────────────────────────────────────── */
.tm-results {
  display: grid;
  gap: var(--sp-4);
  margin-block-start: var(--sp-7);
  padding-block-start: var(--sp-5);
  border-block-start: 1px solid var(--line);
}
.tm-results__head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); }
.tm-results__head .btn { margin-inline-start: auto; }
.tm-grid {
  display: grid;
  gap: var(--sp-4);
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
}

/* The captain, said in a word rather than drawn as a star: a badge with no text is invisible to
   a screen reader and meaningless in a mono print. It takes the tag pill's shape (it sits in the
   same slot) and the brand tint, so the two never read as the same kind of thing. */
.tm-captain {
  border-color: var(--brand);
  background: var(--brand-soft);
  color: var(--ink);
}

/* ── print: the team sheet, and only the team sheet ──────────────────────
   base.css already turns the palette black-on-white and hides every button; components.css
   takes the segment colours off the cards. What is left here is the page deciding what belongs
   on paper: the teams, under a title, with the workbench that produced them left behind. */
@media print {
  .tm-results { margin-block-start: 0; padding-block-start: 0; border-block-start: 0; }
  .tm-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ══ the document pages: /fair, /about, /contact (Task 7) ═══════════════════════════════
   Ported from the signed-off prototype's fair.html <style> block, and shared by the three
   pages that are documents rather than instruments. Same class names as the prototype's, so the
   two stay diffable; about and contact reuse them rather than inventing a second reading column
   under other initials, because there is only one such column in the design.

   These pages are set as documents: a single reading measure, headings that carry the questions
   a reader actually asks, and exactly two objects with any weight to them — the seal, which
   shows the frozen record's real fingerprints, and the audit card. */

.fr { padding-block: var(--sp-7) var(--sp-8); }

/* Slightly wider than .wrap-narrow's 68ch, so the seal, the snippet and the audit table have
   room; the prose inside keeps its own measure. */
.fr-doc { max-inline-size: 74ch; margin-inline: auto; }
.fr-doc > * + * { margin-block-start: var(--sp-7); }

.fr-sec { display: grid; align-content: start; gap: var(--sp-4); min-inline-size: 0; }
.fr-sec p { max-inline-size: 66ch; }
.fr-sec h3 { padding-block-start: var(--sp-3); }

/* ── the seal ─────────────────────────────────────────────────────────── */
/* Commit, then reveal — a real sequence, so it is the one place on the page that earns a
   numbered marker. */
.fr-seal {
  display: grid;
  gap: var(--sp-5);
  padding: var(--sp-5);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
}

.fr-step {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--sp-2) var(--sp-3);
  align-items: baseline;
}
.fr-step__n {
  display: grid;
  place-items: center;
  inline-size: var(--sp-5);
  block-size: var(--sp-5);
  border-radius: var(--r-round);
  /* A wash of the page's own ink — the same recipe .chip__n uses on a chip. */
  background: color-mix(in srgb, var(--ink) 14%, transparent);
  font-size: var(--fs-0);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  direction: ltr;                     /* a position is a number, never reordered */
}
/* The value sits under the label, across both columns, so a 64-character fingerprint has the
   full width of the card to wrap into. */
.fr-step__v {
  grid-column: 1 / -1;
  display: block;
  padding: var(--sp-3);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-1);
  font-size: var(--fs-0);
  line-height: 1.6;
  overflow-wrap: anywhere;
  overflow-x: auto;                   /* for a run even `anywhere` will not break */
}

.fr-record { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2) var(--sp-3); }
.fr-record code { overflow-wrap: anywhere; }

/* ── the ns-draw-v1 snippet ───────────────────────────────────────────── */
.fr-code {
  padding: var(--sp-4);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-1);
  font-size: var(--fs-0);
  line-height: 1.6;
  /* Wraps rather than scrolls. Nothing here is meant to be scrolled past on a phone to be
     understood, and no line needs wrapping at the reading width — the longest is 86 characters
     inside a column that fits 93. `auto` stays for a run that even `anywhere` cannot break. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  overflow-x: auto;
}

/* The contact address: an address is read and typed, so it gets the page's own size rather than
   the smaller monospace a code span inherits, and it stays LTR inside an Arabic paragraph
   (the `dir="ltr"` is on the element itself, in the markup). */
.ct-address { font-size: var(--fs-2); }
.ct-address code { font-size: inherit; overflow-wrap: anywhere; }

/* ══ the fairness audit (Task 7) ════════════════════════════════════════════════════════
   Ported from the frozen rig page (docs/prototype/fairness-audit.html), which Phase B kept
   separate and spec §3.1 folds into /fair. Its `.fa-*` names are the rig's own, so the two stay
   diffable; what it arranges (.card, .btn, .pill-ok, .pill-danger) is components.css's. */

/* Numbers are technical strings: LTR and tabular wherever they appear. */
.fa-num,
.fa-table td + td {
  direction: ltr;
  unicode-bidi: isolate;
  font-variant-numeric: tabular-nums;
}

/* A grid item's automatic minimum size is its min-content width, and the results table's
   min-content is wider than a phone. Without these the table would stretch the card past the
   viewport instead of scrolling inside it. */
#fr-audit-results,
.fa-test,
.fa-scroll { min-inline-size: 0; }
.fa-scroll { overflow-x: auto; }

.fa-meta { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-3); }
.fa-progress { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); }
/* The bar is the browser's own <progress>: it needs no styling to be legible in either theme,
   and a bar drawn out of divs would need its ARIA written by hand. Sized, not painted. */
.fa-bar { inline-size: min(100%, 18rem); block-size: var(--sp-2); }

.fa-test { display: grid; gap: var(--sp-3); }
.fa-test + .fa-test {
  padding-block-start: var(--sp-5);
  border-block-start: 1px solid var(--line);
}
.fa-test__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2) var(--sp-3);
}
.fa-test__title { margin-inline-end: auto; font-size: var(--fs-1); font-weight: 800; }

.fa-table { inline-size: 100%; }
.fa-table th,
.fa-table td {
  padding-block: var(--sp-2);
  padding-inline: var(--sp-3);
  border-block-end: 1px solid var(--line);
  text-align: end;
}
.fa-table th:first-child,
.fa-table td:first-child { text-align: start; padding-inline-start: 0; }
.fa-table th:last-child,
.fa-table td:last-child { padding-inline-end: 0; }
.fa-table thead th {
  font-size: var(--fs-0);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.fa-table tbody tr:last-child td { border-block-end: 0; }

/* The verdict line: statistic, critical value, conclusion. */
.fa-verdict { font-size: var(--fs-0); color: var(--ink-2); max-inline-size: 66ch; }

/* "How the audit works" — the wheel page's FAQ disclosure at the size of a card's small print,
   so the two read as the same control rather than as two inventions. */
.fa-how { padding-block-start: var(--sp-4); border-block-start: 1px solid var(--line); }
.fa-how summary {
  display: flex;
  align-items: baseline;
  gap: var(--sp-4);
  font-weight: 700;
  cursor: pointer;
  list-style: none;
}
.fa-how summary::-webkit-details-marker { display: none; }
/* A symmetric glyph: nothing to mirror, and it survives a mono print. */
.fa-how summary::after {
  content: "+";
  margin-inline-start: auto;
  font-size: var(--fs-2);
  font-weight: 400;
  line-height: 1;
  color: var(--ink-2);
}
.fa-how[open] summary::after { content: "\2212"; }
.fa-how__body { padding-block-start: var(--sp-3); }
.fa-how__body p { font-size: var(--fs-0); color: var(--ink-2); max-inline-size: 66ch; }

/* ── print ────────────────────────────────────────────────────────────
   base.css hides every button on paper, which would leave the audit card as a heading over an
   empty box. A measured result PRINTS — somebody auditing a wheel for a school or a club is
   exactly the person who wants the table on paper — and the rig around it does not. */
@media print {
  .fa-progress, .fa-how { display: none; }
  .fa-scroll { overflow-x: visible; }
}

/* ═══ the trust pages' two data shapes (Task 21) ══════════════════════════
   `/privacy` prints a retention table and `/storage` prints two name→meaning lists, and both
   sit inside `.fr-doc` beside `/fair` and `/about`. They are `doc-*` rather than `pv-*`/`st-*`
   because two pages share them and a third (a DPA, when accounts hold school rosters — D-12)
   would share them too; everything else on those pages is `.fr-sec` prose and needs nothing. */

/* Same containment as `.fa-scroll`: a grid item's automatic minimum size is its min-content
   width, and a three-column table's min-content is wider than a 360 px phone. The table scrolls
   inside its own box rather than stretching the section. */
.doc-scroll { min-inline-size: 0; overflow-x: auto; }

/* No `font-size`: every cell is a sentence somebody has to read, so the table stays at the
   document's own size. `.fa-table` drops to --fs-0 because its cells are five-digit counts. */
.doc-table { inline-size: 100%; border-collapse: collapse; }
.doc-table caption {
  text-align: start;
  padding-block-end: var(--sp-3);
  font-size: var(--fs-0);
  color: var(--ink-2);
}
.doc-table th,
.doc-table td {
  padding-block: var(--sp-2);
  padding-inline: var(--sp-3);
  border-block-end: 1px solid var(--line);
  /* START, not END: `.fa-table` aligns to the end because every cell in it is a number. Every
     cell in this one is a sentence, and a sentence reads from the start edge in both directions. */
  text-align: start;
  vertical-align: top;
}
.doc-table th:first-child,
.doc-table td:first-child { padding-inline-start: 0; }
.doc-table th:last-child,
.doc-table td:last-child { padding-inline-end: 0; }
.doc-table thead th {
  font-size: var(--fs-0);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-2);
  white-space: nowrap;
}
/* A row header is the thing the row is ABOUT, so it carries the weight and the ink. */
.doc-table tbody th { font-weight: 600; }
.doc-table tbody tr:last-child th,
.doc-table tbody tr:last-child td { border-block-end: 0; }

/* A name and what it holds. Two columns where there is room, stacked where there is not — and
   the term keeps its own column rather than sitting inline, so eleven `ns:`-prefixed names read
   as a list somebody can scan for one of them. */
.doc-defs {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--sp-2) var(--sp-5);
  align-items: baseline;
}
.doc-defs dt { min-inline-size: 0; }
.doc-defs dd { margin: 0; min-inline-size: 0; color: var(--ink-2); }

/* ═══ the API reference's preformatted blocks (Task 23) ═══════════════════
   `/developers` renders one block per endpoint, generated from the route definitions, plus one
   worked `curl` example — column-aligned monospaced text whose widest line is about 120
   characters. Two rules and both are load-bearing:

   `min-inline-size: 0` is `.doc-scroll`'s, for `.doc-scroll`'s reason: a grid item's automatic
   minimum size is its min-content width, and a <pre> keeps its lines whole, so without this the
   block claims its full intrinsic width and stretches the whole document sideways (WCAG 2.1
   §1.4.10 Reflow — and the matrix measures it in every column).

   A SCROLLER rather than `.dr-json`'s wrap, which is the opposite choice to the one the draw
   record makes and is right here for the opposite reason: that block is a JSON document whose
   long lines are 64-character fingerprints and wrapping loses nothing, while this one is a TABLE
   whose columns are the whole of its readability. Wrapped, it would be worse than scrolled.

   On PAPER it wraps instead — `.doc-scroll`'s print counterpart, for its reason: there is no
   scrollbar on a sheet of paper, and what is past the edge of the box is simply gone. */
.dv-pre {
  min-inline-size: 0;
  overflow-x: auto;
  padding: var(--sp-4);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-1);
  font-size: var(--fs-0);
  line-height: 1.6;
}
@media print {
  .dv-pre { overflow-x: visible; white-space: pre-wrap; }
}

/* …AND THE PRINT COUNTERPART, the one `.fa-scroll` has had since Task 11 (Task 22, carry 17).
   `overflow-x: auto` is right on a screen — the retention table scrolls inside its own box
   rather than stretching the section — and WRONG on paper, where there is no scrollbar to
   scroll: whatever is past the edge of the box is simply cut off, and a privacy policy whose
   "counted from" column is missing from the printed copy is a promise the paper does not make.
   Nothing else changes: the table still wraps as it always would at the page's width. */
@media print {
  .doc-scroll { overflow-x: visible; }
}

@media (max-width: 34rem) {
  /* `max-content` on a narrow screen is what pushes a document sideways: below this the term
     sits on its own row above its meaning, which is what a definition list does by default. */
  .doc-defs { grid-template-columns: 1fr; gap: var(--sp-1) 0; }
  .doc-defs dd { padding-block-end: var(--sp-3); }
}

/* ═══ /w/{slug} — the page a shared link opens (Task 12) ══════════════════
   Ported from the signed-off prototype's `.sw-*` block (docs/prototype/share-wheel.html).
   Every component it arranges — .card, .chip, .btn, .result-banner — is components.css and is
   used unchanged; the whole design move here is SUBTRACTION. wheel.html has two registers, the
   workbench and the manual. This page has neither: it is the viewer, and what is left is the
   disc, the names it turns on, and one way out. */

.sw { padding-block: var(--sp-6) var(--sp-8); }

/* fs-4, not fs-5: the disc is the hero here as it is on the wheel page. */
.sw-head {
  display: grid;
  gap: var(--sp-2);
  padding-block-end: var(--sp-5);
}
.sw-head h1 {
  font-size: var(--fs-4);
  overflow-wrap: anywhere;
}
.sw-head__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2) var(--sp-4);
  color: var(--ink-2);
  font-size: var(--fs-0);
}

/* The same 3fr/2fr split as the wheel page, so a visitor who has seen one recognises the
   other. Grid children must be free to shrink below their content or one long unbroken name
   pushes the page wider than a phone. */
.sw-stage { display: grid; gap: var(--sp-5); align-items: start; }
.sw-stage > * { min-inline-size: 0; }
@media (min-width: 62rem) {
  .sw-stage {
    grid-template-columns: minmax(0, 3fr) minmax(17rem, 2fr);
    gap: var(--sp-6);
  }
}

.sw-disc { display: grid; gap: var(--sp-4); justify-items: center; }

/* Square, capped by the viewport as well as by the column, so the Spin button stays above the
   fold on a laptop. Slightly smaller than the wheel page's disc: there is no console under it,
   so the result banner has to fit instead. */
.sw-canvas-box {
  position: relative;
  inline-size: 100%;
  max-inline-size: min(32rem, 56dvh);
  aspect-ratio: 1;
}
.sw-canvas { inline-size: 100%; block-size: 100%; }

/* Click the hub to spin — the same affordance and the same contract the wheel page carries:
   transparent, untabbable, aria-hidden, because the named Spin button below is the accessible
   path. 20 % of the square ≈ the 22 %-of-radius hub the engine draws; never under the 2.75rem
   touch floor. */
.sw-hub {
  position: absolute;
  inset-block: 0;
  inset-inline: 0;
  margin: auto;
  inline-size: 20%;
  block-size: 20%;
  min-inline-size: 2.75rem;
  min-block-size: 2.75rem;
  border-radius: var(--r-round);
}

.sw-spin {
  inline-size: 100%;
  max-inline-size: 22rem;
  min-block-size: 3.5rem;
  font-size: var(--fs-2);
}

.sw-note { text-align: center; max-inline-size: 42ch; }
/* A state reads as the machine talking back — the same colour-and-alignment treatment the
   wheel and join pages give theirs. */
.sw-note--state {
  text-align: start;
  color: var(--ink);
}

.sw-result { inline-size: 100%; }

/* The editor's chip list with the textarea and the × taken out: badge N still carries slice N's
   colour, so the list is the disc's legend and nothing more. */
.sw-list { gap: var(--sp-3); }
.sw-list__items {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  list-style: none;
}

/* A shared team sheet: the same grid the team maker's own outcome uses (.tm-grid above), so
   the sheet a host sends looks like the sheet they made. */
.sw-teams { gap: var(--sp-4); }

/* Below the rule: what you do ABOUT this page rather than with it. Actions and small print
   share one quiet register, because none of them is about the wheel — they are about the link
   that brought you here. */
.sw-foot {
  display: grid;
  gap: var(--sp-4);
  margin-block-start: var(--sp-6);
  padding-block-start: var(--sp-5);
  border-block-start: 1px solid var(--line);
}
.sw-actions { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: center; }
.sw-small { display: grid; gap: var(--sp-2); }

/* ═══ /join — six digits, typed (Task 12) ════════════════════════════════
   Ported from the signed-off prototype's `.jn-*` block (docs/prototype/join.html) on its
   checkpoint-2 composition (C2-6). It borrows the wheel page's structural idea: the INSTRUMENT
   is the hero and the type steps down to make room for it. There the instrument is the disc, so
   the headline drops to fs-4; here it is six digits, so the field is set at fs-5 — larger than
   the h1 above it, because the thing you are copying is a number on a screen across the room,
   and the field should look like the place that number goes. */

.jn { padding-block: var(--sp-6) var(--sp-8); }

.jn-head { display: grid; gap: var(--sp-2); padding-block-end: var(--sp-6); }
.jn-head h1 { font-size: var(--fs-4); }

/* The instrument and the manual side by side at the page's own width — the same two-register
   split every tool page keeps. The card keeps an instrument's measure: wider and the code field
   starts to look like a search box. */
.jn-stage { display: grid; gap: var(--sp-5); align-items: start; }
.jn-stage > * { min-inline-size: 0; }
@media (min-width: 56rem) {
  .jn-stage { grid-template-columns: minmax(0, 34rem) minmax(16rem, 1fr); gap: var(--sp-7); }
}
.jn-stack { --stack-gap: var(--sp-5); }
.jn-aside { display: grid; gap: var(--sp-5); align-content: start; }

.jn-card { gap: var(--sp-5); }

/* The code as it looks on the projector: big, tabular, spaced out a digit at a time, centred.
   LTR on the element itself — a code is a technical string and must not reorder inside an
   Arabic page. */
.jn-code {
  min-block-size: 4.5rem;
  font-size: var(--fs-5);
  font-weight: 800;
  letter-spacing: .16em;
  font-variant-numeric: tabular-nums;
  text-align: center;
}
/* An empty field must not read as a filled one: the format hint keeps the field's placeholder
   colour and loses its weight. */
.jn-code::placeholder { font-weight: 400; }

/* One action, full width, the width of the thing above it — the wheel page's Spin bar. */
.jn-go {
  inline-size: 100%;
  min-block-size: 3.5rem;
  font-size: var(--fs-2);
}

/* States read as the machine talking back. The glyph carries the status as well as the colour
   (components.css sets the same one on .pill-danger), so nothing here is signalled by colour
   alone. */
.jn-error {
  display: flex;
  gap: var(--sp-2);
  border: 1px solid var(--danger);
  border-radius: var(--r-1);
  padding: var(--sp-3) var(--sp-4);
  background: color-mix(in srgb, var(--danger) 6%, var(--surface));
  color: var(--ink);
  font-size: var(--fs-0);
}
.jn-error::before { content: "\2717"; font-weight: 800; color: var(--danger); }

.jn-note {
  display: grid;
  gap: var(--sp-2);
  padding-block-start: var(--sp-5);
  border-block-start: 1px solid var(--line);
}
.jn-note p:not(.eyebrow) { color: var(--ink-2); }

/* ═══ the share sheet (Task 12) ══════════════════════════════════════════
   Ported from the signed-off prototype's `.wh-linkrow` / `.wh-qr` / `.wh-code` block
   (docs/prototype/wheel.html), renamed `.sh-*` because it is now ONE sheet shared by the wheel,
   the picker and the team maker rather than three copies of a wheel-page block. The dialog it
   sits in is components.css's `.modal`, unchanged. */

.sh-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.sh-row .input { flex: 1 1 14rem; min-inline-size: 0; }
/* The prototype set a monospace stack on this box. It is not repeated here: base.css is the
   only file in this tree allowed to choose a face (check battery sweep 5, "sanctioned
   font-family"), and its `code, kbd, samp, pre` rule cannot reach an <input>. A readonly URL is
   legible in the UI face, and what actually matters — that it stays LTR inside an Arabic page —
   is `dir="ltr"` on the element itself. */
.sh-url { font-size: var(--fs-0); }

.sh-qr { display: grid; gap: var(--sp-4); align-items: center; }
@media (min-width: 26rem) { .sh-qr { grid-template-columns: auto minmax(0, 1fr); } }
/* A QR is black on white in both themes or it does not scan, so the plinth takes --seg-wash:
   the token set's one fixed, non-inverting white. The padding is the quiet zone the generator
   is asked to leave out (margin: 0) — four modules at cellSize 4 is exactly --sp-4. */
.sh-qr__code {
  display: grid;
  place-items: center;
  inline-size: 11rem;
  block-size: 11rem;
  padding: var(--sp-4);
  background: var(--seg-wash);
  border: 1px solid var(--line);
  border-radius: var(--r-1);
}
.sh-qr__code svg { display: block; inline-size: 100%; block-size: auto; }
.sh-qr__text { display: grid; gap: var(--sp-2); min-inline-size: 0; }

/* The other way in: digits typed on any device, for the desktop in the room with no camera.
   Ruled off from the QR so the sheet reads as two routes to the same wheel rather than one
   block of small print. */
.sh-code {
  display: grid;
  gap: var(--sp-2);
  padding-block-start: var(--sp-4);
  border-block-start: 1px solid var(--line);
}
.sh-code__row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); }
/* A code is read aloud a digit at a time, so it is set like one: tabular, spaced, LTR in every
   locale. */
.sh-code__value {
  font-size: var(--fs-3);
  font-weight: 800;
  letter-spacing: .12em;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* While the link is being made, and when it could not be. Both replace the sheet's body rather
   than sitting above it: there is nothing to copy yet, and a Copy button beside an error is a
   button that copies the last person's link. */
.sh-state { display: grid; gap: var(--sp-3); }
.sh-state--error {
  border: 1px solid var(--danger);
  border-radius: var(--r-1);
  padding: var(--sp-3) var(--sp-4);
  background: color-mix(in srgb, var(--danger) 6%, var(--surface));
  color: var(--ink);
}
/* The Turnstile widget's own box. It has a fixed size of its own and we do not style inside it;
   this only stops the sheet jumping when it appears. */
.sh-verify { min-block-size: 4.5rem; }
.sh-body { --stack-gap: var(--sp-5); display: grid; gap: var(--stack-gap); }
.sh-small { display: grid; gap: var(--sp-2); }

/* ══ the lobby (Task 13, spec §3.7) ══════════════════════════════════════
   Two pages that are opposites of each other, ported from the signed-off prototypes
   (docs/prototype/lobby-host.html and lobby-join.html) with their reasoning intact and their
   <style> blocks moved here, because a built page may not carry one.

   THE HOST PAGE IS PROJECTOR-FIRST. Every other page in the suite is a workbench that can be
   put on a projector; this one has no other job — its whole reason to exist is being the screen
   a room reads from the back row. So the display half carries the `.projector` type scale
   PERMANENTLY (components.css defines it as a set of --fs-* overrides, which cascade from any
   element, not just <body>), and the "Projector view" button only does the second half of that
   class's job: strip the chrome and let the sign fill the screen.

   THE JOIN PAGE IS PHONE-FIRST AND STAYS PHONE-FIRST, on a desktop too (cross-cutting decision
   4): one column that a wide window does not stretch, one field, one button the width of the
   field, and nothing else competing. It is held at arm's length by one person for about eight
   seconds, standing up, one-handed, while a screen at the front waits. */

/* ── the host's page ──────────────────────────────────────────────────── */

.lb-main { padding-block-end: var(--sp-8); }

.lb-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  padding-block: var(--sp-5) var(--sp-4);
}
.lb-head__actions { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-inline-start: auto; }

.lb-stage { display: grid; gap: var(--sp-4); align-content: start; }

/* The sign is one framed object rather than a stack of page sections, because at three metres a
   frame is what turns a web page into a sign on a wall. 2px and the brand colour: the border is
   doing work here, not decoration. */
.lb-sign,
.lb-closed {
  padding: var(--sp-5);
  background: var(--surface);
  border: 2px solid var(--brand);
  border-radius: var(--r-2);
}
.lb-sign { display: grid; gap: var(--sp-4); align-content: start; }

/* The invitation and the count of who has taken it, on one line: a board's header. The tally is
   repeated in the closed panel, because closing the list must not take the number away from the
   host who is about to send it somewhere. */
.lb-sign__top {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2) var(--sp-4);
}
.lb-sign__h1,
.lb-closed__h1 { font-size: var(--fs-4); margin-inline-end: auto; }

.lb-tally { display: inline-flex; align-items: baseline; gap: var(--sp-2); }
/* The number the room watches: tabular, so it never jitters as it ticks, and a step under the
   headline, which is the thing they should read first. */
.lb-count {
  font-size: var(--fs-3);
  font-weight: 800;
  line-height: var(--lh-tight);
  font-variant-numeric: tabular-nums;
}

/* Single-line labels, not prose: they take the tight leading rather than the 1.75 base.css gives
   Arabic body copy, which on a sign is 40px of empty screen. */
.lb-sign .eyebrow,
.lb-sign .hint,
.lb-scan__hint { line-height: 1.35; }

.lb-sign__routes { display: grid; gap: var(--sp-5); }
/* Wide enough for the two ways in to sit side by side, separated by the grid's own gap — no
   divider needed, and a vertical rule under a stacked column would divide nothing anyway. */
@media (min-width: 52rem) {
  .lb-sign__routes { grid-template-columns: minmax(0, 1fr) auto; gap: var(--sp-6); }
}

.lb-typed { display: grid; gap: var(--sp-3); align-content: start; }
.lb-route { display: grid; gap: var(--sp-1); }
/* The code is the third route, so it sits under the link and behind a rule rather than beside
   it: a peer in kind, not in weight. */
.lb-route--code { padding-block-start: var(--sp-3); border-block-start: 1px solid var(--line); }

/* The prototype carried a `.lb-sign code { font-family: … }` rule here, to win the URL and the
   join code back from the Arabic text face: its base.css put a face on `:lang(ar)`, which is a
   class-weight selector matching every element inside an Arabic page, `<code>` included.
   PRODUCTION HAS NO SUCH RULE — the Task 4 port replaced it with `[lang]` plus
   `code[lang], kbd[lang], samp[lang], pre[lang]` (base.css's header, ruling R32), so a code
   element already keeps the monospace stack in every locale. Restating it here would be a fifth
   face declaration outside the four base.css sanctions, which check battery job 5 refuses — and
   it would be refusing the right thing: two files choosing one face is how they start
   disagreeing. */

/* The thing the room copies. Sized by the viewport rather than the type scale, and capped where
   21 monospace characters still fit the column the page gutter leaves them. */
.lb-url {
  font-size: clamp(1.5rem, 4.6vw, 3.5rem);
  font-weight: 700;
  line-height: 1.1;
  color: var(--ink);
  overflow-wrap: anywhere;
}
/* Six digits read aloud a digit at a time: tabular, spaced, LTR in every locale — the same
   treatment the share sheet and /join give the same number. */
.lb-code {
  font-size: var(--fs-3);
  font-weight: 800;
  line-height: var(--lh-tight);
  letter-spacing: .12em;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.lb-scan { display: grid; gap: var(--sp-2); justify-items: center; align-content: start; }
/* A QR is black on white in both themes or it does not scan, so the plinth takes --seg-wash: the
   token set's one fixed, non-inverting white. The padding is the quiet zone the generator is
   asked to leave out (margin: 0). */
.lb-qr {
  display: grid;
  place-items: center;
  inline-size: clamp(8rem, 16vw, 10.5rem);
  block-size: clamp(8rem, 16vw, 10.5rem);
  padding: var(--sp-4);
  background: var(--seg-wash);
  border: 1px solid var(--line);
  border-radius: var(--r-1);
}
.lb-qr svg { display: block; inline-size: 100%; block-size: auto; }
.lb-scan__hint { color: var(--ink-2); max-inline-size: 18ch; text-align: center; }


/* A sign that has stopped inviting people has to stop saying "Add your name", so the routes are
   not dimmed — they are gone, and this takes their place. */
.lb-closed { display: grid; gap: var(--sp-3); border-color: var(--warn); }
.lb-closed__h1 { color: var(--warn); }

/* ── the roll ─────────────────────────────────────────────────────────── */

.lb-roll { display: grid; gap: var(--sp-3); align-content: start; }
.lb-waiting { color: var(--ink-2); }
.lb-names { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-content: start; }
/* The arrival, and the whole of it: a chip lands rather than blinking in. base.css switches
   every animation off under prefers-reduced-motion. */
@keyframes lb-land {
  from { opacity: 0; translate: 0 var(--sp-3); }
  to   { opacity: 1; translate: 0 0; }
}
.lb-names .chip { animation: lb-land var(--t-base) ease-out; }
/* The host asked for one extra detail, and it rides on the chip that carries the name. It is
   STRANGER-TYPED text up to 24 characters — twenty-four Chinese characters is most of a phone's
   width — so it takes the same `min-inline-size: 0` the contract at the top of this file asks of
   any content-carrying flex item, and breaks rather than pushing the chip past the viewport. */
.lb-names .chip__attr {
  min-inline-size: 0;
  overflow-wrap: anywhere;
  padding-inline: var(--sp-2);
  border: 1px solid currentColor;
  border-radius: var(--r-round);
  font-size: var(--fs-0);
  opacity: .85;
}

/* ── the console (the host's own laptop) ──────────────────────────────── */

.lb-console {
  display: grid;
  gap: var(--sp-3);
  justify-items: start;
  padding-block-start: var(--sp-5);
  margin-block-start: var(--sp-5);
  border-block-start: 1px solid var(--line);
}
.lb-send { display: grid; gap: var(--sp-2); }

.lb-note {
  display: grid;
  gap: var(--sp-2);
  padding-block-start: var(--sp-5);
  margin-block-start: var(--sp-5);
  border-block-start: 1px solid var(--line);
  color: var(--ink-2);
  max-inline-size: 62ch;
}

/* ── projector ────────────────────────────────────────────────────────
   `.projector` on <body> hides the nav, the skip link, the footer and everything marked
   data-no-projector (components.css). What is left is one screenful: the sign at its natural
   height, the roll taking whatever is over, and the console as a strip along the bottom. The
   roll scrolls rather than growing, so a long list can never push the join link off the screen
   it exists to advertise. */
.lb-projector-only { display: none; }
.projector .btn.lb-projector-only { display: inline-flex; }

.projector .lb-main {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  block-size: 100dvh;
  padding-block: var(--sp-3);
}
.projector .lb-head { flex: none; padding-block: 0; }
.projector .lb-stage {
  flex: 1 1 auto;
  min-block-size: 0;
  grid-template-rows: auto minmax(0, 1fr);
  align-content: stretch;
}
.projector .lb-roll { min-block-size: 0; grid-template-rows: minmax(0, 1fr); }
/* A screen has whatever height it has, and a list of names does not. Once the roll is taller
   than the room it sits in, it becomes a window on the newest arrivals — the count above it is
   what carries the total. The fade is the affordance that says so. */
.projector .lb-names { overflow: auto; align-content: start; }
/* Only when it really is cut off — the page sets the class from scrollHeight, so a roll that
   fits keeps its top row at full strength. */
.projector .lb-names.is-clipped {
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 var(--sp-4));
  mask-image: linear-gradient(to bottom, transparent 0, #000 var(--sp-4));
}
.projector .lb-console { flex: none; padding-block-start: var(--sp-2); margin-block-start: 0; }

/* ── print: the join sign is a poster ─────────────────────────────────
   LAST IN THE SECTION ON PURPOSE. A media query carries no specificity of
   its own, so every rule below has to be one this block already outranks or
   one it comes after — `.lb-roll { display: grid }` is the second kind, and
   a print block written higher up simply lost to it.
   The one production surface that carries a QR code on PAPER (Secret Santa's
   printable cards are C2's; components.css already holds their sheet rules,
   ported from the prototype). A host prints this to put on a wall, so the code
   is sized the way that sheet sizes its own — in MILLIMETRES, because paper is
   the target and `16vw` of a printed page is not a length anybody chose.

   45 mm carries a join URL comfortably: the encoder draws at most 33 modules
   across, so a 39 mm code inside a 3 mm quiet zone is over a millimetre a
   module — read from across a classroom, not just at arm's length.

   The sign is kept whole (`break-inside: avoid`): a QR split over a page break
   is a QR nobody can scan, and it is the only element here where that is fatal.

   AND THE ROLL DOES NOT GO ON THE WALL (controller ruling R37). What is printed
   here is a POSTER — it is sized in millimetres and it exists to be stuck up in a
   room — and a printed list of first names leaves that room, outlives the lobby's
   own 24-hour TTL, survives the host closing the list (which deletes the names
   server-side, `lobby.host_privacy`), and answers to no expiry rule this product
   publishes. That is out of step with everything the classroom posture promises:
   first names only, auto-expiry, local-first. The names stay ON SCREEN, where the
   host is looking at them and where they still disappear; the deliberate way to
   take a roster away is the pour into a tool and that tool's own CSV. Default,
   not a decision: if the owner wants a printable roll at CP-C1 it becomes a
   control with its own copy, rather than a thing paper does silently. */
@media print {
  .lb-qr {
    inline-size: 45mm;
    block-size: 45mm;
    padding: 3mm;
    border-color: var(--ink-2);
  }
  .lb-sign,
  .lb-closed { break-inside: avoid; border-color: var(--ink-2); }
  .lb-roll { display: none; }
}

/* ── the dialog that opens a list ─────────────────────────────────────── */
/* Built in the DOM by static/js/lobby-start.js — two questions that can only be asked before the
   list exists, and the two sentences that say what a list is. */
.lb-start { display: grid; gap: var(--sp-4); }
.lb-start__check { display: flex; align-items: center; gap: var(--sp-3); }
.lb-start__notes { display: grid; gap: var(--sp-2); }
/* The one state line under the controls. The glyph carries the failure as well as the colour,
   so nothing here is signalled by colour alone. */
.lb-start__error {
  color: var(--ink);
  border: 1px solid var(--danger);
  border-radius: var(--r-1);
  padding: var(--sp-3) var(--sp-4);
  background: color-mix(in srgb, var(--danger) 6%, var(--surface));
}

/* ── the phone page ───────────────────────────────────────────────────── */

.lj { padding-block: var(--sp-6) var(--sp-8); }

/* One column, phone-width, centred — on a desktop as well (cross-cutting decision 4). */
.lj-col { display: grid; gap: var(--sp-5); max-inline-size: 26rem; margin-inline: auto; }

.lj-head { display: grid; gap: var(--sp-2); }
.lj-head h1 { font-size: var(--fs-4); }
.lj-head .lede { font-size: var(--fs-2); }
.lj-out { margin-block-start: var(--sp-3); }

.lj-card { gap: var(--sp-5); }

/* It borrows /join's structural idea — the INSTRUMENT is the hero and the type steps down to
   make room for it. There the instrument is six digits at fs-5; here it is your own name, which
   you are about to see on a wall, so the field is fs-3: big enough to check a spelling at a
   glance, not so big that "عبد الرحمن" runs out of field. */
.lj-name { min-block-size: 3.5rem; font-size: var(--fs-3); font-weight: 600; }
/* An empty field must not read as a filled one: the placeholder keeps the field's colour
   (components.css sets --ink-2 on every placeholder) and loses its weight. */
.lj-name::placeholder { font-weight: 400; }

/* One action, full width, the width of the thing above it — the same bar the wheel page's Spin
   button and /join's Join button are. */
.lj-go { inline-size: 100%; min-block-size: 3.5rem; font-size: var(--fs-2); }

/* States read as the machine talking back — the same boxed treatment /join gives its unknown-code
   line. The glyph carries the status as well as the colour, so nothing here is signalled by
   colour alone. */
.lj-error {
  display: grid;
  gap: var(--sp-1);
  border: 1px solid var(--danger);
  border-radius: var(--r-1);
  padding: var(--sp-3) var(--sp-4);
  background: color-mix(in srgb, var(--danger) 6%, var(--surface));
  font-size: var(--fs-0);
}
.lj-error__title { font-weight: 800; }
.lj-error__title::before { content: "\2717"; color: var(--danger); margin-inline-end: var(--sp-2); }
.lj-error__body { color: var(--ink-2); }

/* The attribute the host asked for, echoed back beside the name so the person can see that the
   second field went somewhere too. */
.lj-tag {
  justify-self: center;
  /* Also stranger-typed, also up to 24 characters, also in a column narrower than that run can
     be: it wraps inside the panel rather than widening it. */
  max-inline-size: 100%;
  overflow-wrap: anywhere;
  padding-inline: var(--sp-3);
  border: 1px solid var(--brand);
  border-radius: var(--r-round);
  font-size: var(--fs-0);
  font-weight: 700;
}
/* The whole of the "transition": the panel arrives rather than blinking into place. */
@keyframes lj-arrive {
  from { opacity: 0; translate: 0 var(--sp-3); }
  to   { opacity: 1; translate: 0 0; }
}
.lj-done { animation: lj-arrive var(--t-base) ease-out; }

.lj-closed h1 { color: var(--warn); }

/* ══ classroom mode (Task 14, spec §3.8) ═════════════════════════════════
   PORTED from the signed-off prototype's page <style> block
   (docs/prototype/classroom.html), which is why the class names, the grid areas and the
   projector treatment are its ones. Every component it arranges (.card, .btn, .input,
   .switch, .modal, .result-banner, .projector) is components.css, used unchanged.

   THE SIGNATURE HERE IS THE REGISTER. Every other page draws names as chips — a bag of equals
   you pull one out of. A class is a numbered list a teacher works down, so the roster is set as
   a class register: a rule per row, a tick against the ones who have been up, a struck line
   through the ones who are away. On the projector it folds into columns, which is how a
   register is printed.

   One deliberate omission, kept from the prototype: there is no progress BAR under the counter.
   The register already shows the round filling up and the counter already says the number; a
   bar would be the third telling of one fact. */

.cl-main { padding-block-end: var(--sp-8); }

/* ── masthead ─────────────────────────────────────────────────────────── */
.cl-head {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: var(--sp-4);
  padding-block: var(--sp-5) var(--sp-4);
}
.cl-head__text { display: grid; gap: var(--sp-2); }
.cl-head__actions { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-inline-start: auto; }

/* ── the promise ──────────────────────────────────────────────────────
   Not a card: a note stamped in the margin of the page. This is the one sentence on the page a
   competitor cannot write, so it is set once, plainly, with the small print folded away behind
   it rather than shouted alongside. */
.cl-promise {
  display: grid;
  gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-5);
  background: var(--brand-soft);
  border-radius: var(--r-1);
}
.cl-promise__note {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  font-size: var(--fs-2);
  font-weight: 800;
  line-height: var(--lh-tight);
  /* --brand-soft never inverts its ROLE, but --ink does, so the note reads in both themes
     without a second token. */
  color: var(--ink);
}
.cl-promise__note .icon { stroke-width: 2.5; color: var(--brand); }
.cl-promise summary {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-block-size: 2.75rem;
  color: var(--ink);
  font-weight: 600;
  cursor: pointer;
  list-style: none;
}
.cl-promise summary::-webkit-details-marker { display: none; }
.cl-promise summary::after {
  content: "+";
  font-weight: 800;
  font-size: var(--fs-2);
  line-height: 1;
  color: var(--ink-2);
}
.cl-promise details[open] summary::after { content: "\2212"; }   /* − */
.cl-promise details p { max-inline-size: 68ch; padding-block-end: var(--sp-2); }

/* ── the two columns ──────────────────────────────────────────────────
   Grid areas follow the writing direction, so the console lands on the far side in Arabic with
   no mirrored rule of its own. */
.cl-grid { display: grid; gap: var(--sp-5); align-items: start; padding-block-start: var(--sp-5); }
@media (min-width: 64rem) {
  .cl-grid {
    grid-template-columns: minmax(0, 1fr) 22rem;
    /* The console spans both rows; without this the grid shared its height between them and
       the register card started a screen below the pick card (owner, 2026-09-03). */
    grid-template-rows: auto 1fr;
    grid-template-areas: "pick console" "register console";
  }
  .cl-pick { grid-area: pick; }
  .cl-register { grid-area: register; }
  .cl-console { grid-area: console; }
}

/* ── the pick panel ───────────────────────────────────────────────────── */
.cl-pick { border-color: var(--brand); }
.cl-class-name { overflow-wrap: anywhere; }

.cl-tally { display: inline-flex; align-items: baseline; gap: var(--sp-2); }
/* The number the room watches: tabular, so it never jitters as it ticks. */
.cl-count {
  font-size: var(--fs-2);
  font-weight: 800;
  line-height: var(--lh-tight);
  font-variant-numeric: tabular-nums;
}

/* The instrument. Everything else on this page is arm's-length; this is the one control pressed
   from three metres away, so it takes the whole column. */
.cl-pick__btn {
  inline-size: 100%;
  min-block-size: 4rem;
  font-size: var(--fs-3);
}

/* The name that came up, and — underneath it, when there is one — the reason there is nobody
   left to come up next. Both can be on screen at once: the last student of a round must not
   vanish the instant the round ends. */
.cl-result { display: grid; gap: var(--sp-3); }
.cl-banner.is-celebrating { animation: cl-pop var(--t-base) ease-out; }
@keyframes cl-pop {
  from { opacity: 0; scale: .94; }
  60%  { scale: 1.02; }
  to   { opacity: 1; scale: 1; }
}
/* An outline, not a filled panel: it is the ABSENCE of a name, and it should not compete with
   the banner that carries one. */
.cl-state {
  padding: var(--sp-4);
  border: 1px dashed var(--line);
  border-radius: var(--r-2);
  color: var(--ink-2);
  text-align: center;
}

.cl-pick__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-4);
}
.cl-pick__foot .hint { max-inline-size: 42ch; margin-inline-end: auto; }

.cl-scopes { display: grid; gap: var(--sp-3); }
@media (min-width: 30rem) { .cl-scopes { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* ── the register ─────────────────────────────────────────────────────── */
.cl-add { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.cl-add .input { flex: 1 1 12rem; }
.cl-hints { display: grid; gap: var(--sp-1); }

/* KI-2 / PC-A11Y-2 — the skip-the-register link. The same behaviour as the document's own skip
   link (base.css `.skip-link`) with none of its geometry: this one belongs INSIDE the card, in
   flow, so it pushes the register down by its own height when it appears rather than flying to
   the corner of the window. Invisible until focused, and then a real, hit-sized target.

   `:focus`, not `:focus-visible`, exactly as base.css's own `.skip-link` has it: a skip control
   that has focus must be VISIBLE, whichever way the focus arrived — an engine that withholds
   `:focus-visible` from a programmatic focus would otherwise leave a focused control nobody can
   see. */
.cl-skip {
  display: block;
  block-size: 0;
  overflow: hidden;
  color: var(--brand);
  font-weight: 600;
}
.cl-skip:focus {
  block-size: auto;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-1);
  background: var(--brand);
  color: var(--brand-ink);
  text-decoration: none;
}

/* One column of full-width rows on the page — a register sheet, readable and fully labelled. It
   folds into columns only on the projector, where the labels come off and the room needs the
   whole class in one screen. */
.cl-list { display: grid; grid-template-columns: minmax(0, 1fr); }

.cl-row {
  display: flex;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  min-block-size: 2.75rem;
  padding-block: var(--sp-1);
  border-block-end: 1px solid var(--line);
  break-inside: avoid;                  /* never split a name over two printed pages */
}
/* A roll number is a position: tabular, LTR in every locale, parked at the start edge so the
   column of them lines up down the sheet. */
.cl-row__n {
  flex: none;
  inline-size: 2.5ch;
  font-size: var(--fs-0);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  direction: ltr;
  unicode-bidi: isolate;
  text-align: end;
  color: var(--ink-2);
}
/* The cell, not the name. An inline-end auto margin is resolved against the ELEMENT'S OWN
   direction, so putting it on the <bdi> (which takes its direction from the name inside it)
   shoved every Arabic name to the far side of the row, away from its own roll number. The cell
   inherits the page's direction and carries the margin; the <bdi> inside it only isolates the
   name's bidi, which is all it was ever for. */
.cl-row__name {
  min-inline-size: 0;
  margin-inline-end: auto;
  overflow-wrap: anywhere;
  text-align: start;
}

/* Had a turn: quiet ink and a tick. Never the colour alone — the tick is a glyph and the row
   carries the word beside it. */
.cl-row.is-picked .cl-row__name { color: var(--ink-2); }
.cl-row__mark {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  flex: none;
  font-size: var(--fs-0);
  font-weight: 700;
  color: var(--brand);
}
.cl-row__tick { font-size: var(--fs-1); line-height: 1; }   /* ✓ — never mirrored */

/* Away: struck through, which is what a register does to an absentee, and legible on a
   projector at the back of a room. The checkbox beside it is the second signal, and the only
   one that can be operated. */
.cl-row.is-away .cl-row__name {
  color: var(--ink-2);
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}

/* A column, not a label repeated twenty-four times: the box is on every row, the WORD only on
   the rows that are away — where it is the state, and worth reading. The cell keeps its width
   either way, so ticking one does not shunt the row. */
.cl-away {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  flex: none;
  min-inline-size: 7.5rem;
  min-block-size: 2.75rem;
  padding-inline: var(--sp-2);
  border-radius: var(--r-1);
  font-size: var(--fs-0);
  color: var(--ink-2);
  white-space: nowrap;
  cursor: pointer;
}
.cl-away:hover { background: var(--surface-2); color: var(--ink); }
.cl-away input {
  inline-size: 1.125rem;
  block-size: 1.125rem;
  margin: 0;
  accent-color: var(--brand);
  cursor: pointer;
}

/* Same target and same behaviour as a chip's ×, so removing a student and removing a name
   anywhere else in the suite are one gesture. */
.cl-row__x {
  display: grid;
  place-items: center;
  flex: none;
  inline-size: var(--sp-6);
  block-size: var(--sp-6);
  border-radius: var(--r-round);
  color: var(--ink-2);
  font-size: var(--fs-2);
  line-height: 1;
  transition: background-color var(--t-fast) ease-out, color var(--t-fast) ease-out;
}
.cl-row__x:hover { background: var(--surface-2); color: var(--ink); }

/* ── the console ──────────────────────────────────────────────────────── */
.cl-console { display: grid; gap: var(--sp-5); align-content: start; }
/* The console is the skip link's target, so it takes focus — without an outline of its own,
   because a container that has been jumped TO is not a control that has been chosen. */
.cl-console:focus { outline: none; }
.cl-expiry {
  display: grid;
  gap: var(--sp-2);
  justify-items: start;
  padding-block-start: var(--sp-3);
  border-block-start: 1px solid var(--line);
  font-size: var(--fs-0);
  color: var(--ink-2);
}
/* The countdown spec §3.8 asks for, beside the date the line already gives. Its own element and
   its own line — two sentences separated by a layout gap, never joined inside a string. */
.cl-expiry__days { font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
.cl-switches { display: grid; gap: var(--sp-1); }
/* The three things Quiet mode switches off: the indent — not a rule down the side — is what
   marks them as a group it governs, rather than three peers beside it. */
.cl-switches--governed {
  padding-inline-start: var(--sp-5);
}
.cl-use { display: grid; gap: var(--sp-3); }

/* ── quiet mode ───────────────────────────────────────────────────────
   One tap has to reach further than three checkboxes: a classroom that asks for quiet is asking
   for a still screen too. Same shape as base.css's prefers-reduced-motion block, applied by a
   class instead of a media query. */
body.cl-quiet *,
body.cl-quiet *::before,
body.cl-quiet *::after {
  animation: none !important;
  transition: none !important;
}

/* ── projector ────────────────────────────────────────────────────────
   `.projector` on <body> hides the nav, the skip link, the footer and everything marked
   data-no-projector (components.css) and steps the type scale up. What is left is the class
   name, the Pick button, the name that came up and the whole register — one screenful, the
   register taking whatever is left and scrolling inside itself rather than pushing the button
   off the screen.

   The register's controls go with the chrome: a projector is a display, not a control surface,
   and a room does not need to see 24 checkboxes. What the room DOES get is the arrow cursor
   (ruling R23) — see `.projector .cl-row:focus` at the end of this block. */
.cl-projector-only { display: none; }
.projector .btn.cl-projector-only { display: inline-flex; }
.projector p.cl-projector-only { display: block; }
/* Nothing to promote and nothing to interrupt: the language banner is chrome, and a projector
   is not a place to offer one. */
.projector .lang-suggest { display: none; }

.projector .cl-main {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  block-size: 100dvh;
  padding-block: var(--sp-3);
}
.projector .cl-head { flex: none; padding-block: 0; }
.projector .cl-grid {
  flex: 1 1 auto;
  min-block-size: 0;
  padding-block-start: 0;
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "pick" "register";
  grid-template-rows: auto minmax(0, 1fr);
  align-items: stretch;
}
/* A projector is landscape and a class list is a column, so on any screen wide enough the two
   go side by side: the instrument keeps a fixed measure at the start edge and the register
   takes the whole height for itself. Stacked, a 1280×720 screen showed twelve of twenty-four
   names; beside it, all of them. */
@media (min-width: 64rem) {
  .projector .cl-grid {
    grid-template-columns: minmax(0, 26rem) minmax(0, 1fr);
    grid-template-areas: "pick register";
    grid-template-rows: minmax(0, 1fr);
    gap: var(--sp-4);
  }
  /* Its own height is the column's height, so the instrument sits at eye level rather than
     pinned to the ceiling above a field of nothing. */
  .projector .cl-pick { align-content: center; }
  /* A stat, not a sentence: the label over the number. Side by side they need 380 px and the
     column has 368 — and a counter that wraps mid-phrase is not something a room can read at a
     glance. */
  .projector .cl-tally { display: grid; justify-items: start; gap: 0; }
  .projector .cl-count { font-size: var(--fs-3); }
}
.projector .cl-register { min-block-size: 0; align-content: stretch; grid-template-rows: auto minmax(0, 1fr); }
.projector .cl-list {
  grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
  gap: 0 var(--sp-5);
  overflow: auto;
  align-content: start;
}
/* Only when it really is cut off — the page sets this from scrollHeight, so a register that
   fits keeps its last row at full strength. Without it, a class too long for the screen simply
   stops mid-list and reads as broken. The `#000` in the gradient is a mask's alpha channel, not
   a colour anybody sees — the same literal the lobby's roll uses for the same reason. */
.projector .cl-list.is-clipped {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - var(--sp-5)), transparent 100%);
  mask-image: linear-gradient(to bottom, #000 calc(100% - var(--sp-5)), transparent 100%);
}
.projector .cl-away,
.projector .cl-row__x,
.projector .cl-row__word { display: none; }

/* THE ARROW CURSOR (ruling R23). In projector view the rows are the navigable content and the
   arrow keys walk them, so the row that holds focus has to be readable from the back of a room
   — an outline alone is a hairline at that distance. The wash is the same `--surface-2` the
   away control uses on hover, so no new colour decision enters the page, and the outline
   base.css draws on `:focus-visible` sits on top of it.

   `:focus` and not `:focus-visible`: the page moves focus to a row in response to an arrow key,
   and the row is a plain <li> rather than a control — engines disagree about whether that earns
   `:focus-visible`, and a cursor that is invisible on one of them is not a cursor. */
.projector .cl-row:focus { background: var(--surface-2); border-radius: var(--r-1); }

/* ── print ────────────────────────────────────────────────────────────
   A class register is a thing teachers print. base.css turns the palette black on white and
   drops every button; this takes the columns as well, because a register on paper is a
   column. */
@media print {
  .cl-grid { grid-template-columns: minmax(0, 1fr); grid-template-areas: "pick" "register" "console"; }
  .cl-promise { border: 1px solid var(--ink); }
}

/* ══ Secret Santa (Task 20a, spec §3.4) ══════════════════════════════════
   Three surfaces, one prefix. `.ss-*` is the organiser's two — the setup page
   (`/secret-santa`, an SSG page in six locales) and the manage URL the draw mints
   (`/s/{token}`, rendered by the Worker) — and `.rv-*` is the one a participant
   ever sees, their sealed reveal link (`/r/{token}`).

   Ported from the signed-off prototypes (docs/prototype/secret-santa-create.html
   and -reveal.html, frozen reference), with two deliberate structural changes the
   templates' own headers argue: the setup page is a STACK of three numbered beats
   rather than a tablist (a form may not hide two thirds of itself behind a
   script), and the links, the cards and the print sheet moved off it onto the
   manage URL (the event is a row in D1 now, and the URL is the credential).

   Every component this arranges — .card, .field, .input, .btn, .chip, .switch,
   .pill-*, .qr-card, .qr-sheet — is components.css, used unchanged. */

.ss-main { padding-block-end: var(--sp-8); }

/* ── masthead ─────────────────────────────────────────────────────────── */
.ss-head {
  display: grid;
  gap: var(--sp-2);
  padding-block: var(--sp-6) var(--sp-5);
}
.ss-head h1 { font-size: var(--fs-4); }

/* ── the three beats ──────────────────────────────────────────────────
   The prototype's stepper, unrolled. The numeral survives it and still means what
   it meant there — this one's position in a sequence — so it is drawn like the
   entry chips' index badge, which says the same thing on every other page. */
.ss-work { display: grid; gap: var(--sp-6); }
.ss-step { display: grid; gap: var(--sp-4); }
.ss-step__head {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  font-size: var(--fs-2);
  padding-block-end: var(--sp-3);
  border-block-end: 1px solid var(--line);
}
.ss-step__n {
  display: grid;
  place-items: center;
  flex: none;
  inline-size: var(--sp-5);
  block-size: var(--sp-5);
  border-radius: var(--r-round);
  background: var(--brand);
  color: var(--brand-ink);
  font-size: var(--fs-0);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  direction: ltr;                     /* a position is a number, never reordered */
}

/* ── the workbench inside a beat ──────────────────────────────────────── */
.ss-cols { display: grid; gap: var(--sp-5); align-items: start; }
/* Grid children must be free to shrink below their content, or one unbroken name
   pushes the whole page wider than a phone. */
.ss-cols > * { min-inline-size: 0; }
@media (min-width: 62rem) {
  .ss-cols { grid-template-columns: minmax(0, 1fr) minmax(21rem, 1fr); gap: var(--sp-6); }
}
.ss-count { --row-gap: var(--sp-2); }

/* ── one person, one wish list ────────────────────────────────────────── */
.ss-roster { display: grid; }
.ss-roster__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  padding-block: var(--sp-2);
  border-block-end: 1px solid var(--line);
  min-inline-size: 0;
}
.ss-roster__row:last-child { border-block-end: 0; }
.ss-roster__name {
  flex: 1 1 7rem;
  min-inline-size: 0;
  overflow-wrap: anywhere;
  font-weight: 600;
}
/* .input is inline-size:100% for a form column; here it shares a row. */
.ss-roster__row .input { flex: 2 1 12rem; inline-size: auto; min-inline-size: 0; }

/* ── the rules ────────────────────────────────────────────────────────── */
.ss-pair { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.ss-pair .input { inline-size: auto; flex: 1 1 8rem; min-inline-size: 0; }
/* The arrow between the two names is the rule itself, so it is never colour: it
   is a shape, and .icon-chevron mirrors it under RTL where "from → to" reads the
   other way. */
.ss-pair__arrow { flex: none; color: var(--ink-2); }
/* The same relation inside a rule chip, as a glyph rather than an icon: ⇄ for a
   couple, → for a one-way rule. Both are bidi-neutral and would flip inside an
   Arabic chip, which is right — the rule reads in the direction the page reads. */
.ss-rule__arrow { padding-inline: var(--sp-2); color: var(--ink-2); }

/* ── how people find out ──────────────────────────────────────────────── */
.ss-mode { gap: var(--sp-4); }
/* The retention promise, set as a promise: the same hairline-card treatment the fairness and
   seed asides give a claim worth setting apart, because it is the same kind of statement. */
.ss-retention {
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  padding: var(--sp-3) var(--sp-4);
  color: var(--ink);
  font-size: var(--fs-0);
}
/* The primary action, the same bar the wheel gives Spin. */
.ss-go { inline-size: 100%; min-block-size: 3.5rem; font-size: var(--fs-2); }
/* The challenge draws itself here, beside the button, and only after it is pressed
   (ruling R31 — static/js/turnstile.js). Empty until then, so it takes no room. */
.ss-verify:not(:empty) { margin-block-start: var(--sp-3); }

/* States read as the machine talking back — the wheel page's treatment for its
   empty-roster line. */
.ss-note--state {
  color: var(--ink);
}
/* A draw that cannot be made is a refusal, not a warning: it carries the same
   glyph .pill-danger uses, so it survives a mono print and a colour-blind reader. */
.ss-alert {
  display: grid;
  gap: var(--sp-1);
  border: 1px solid var(--danger);
  border-radius: var(--r-1);
  padding: var(--sp-3) var(--sp-4);
  background: color-mix(in srgb, var(--danger) 6%, var(--surface));
  font-size: var(--fs-0);
}
.ss-alert__title { font-weight: 800; color: var(--ink); }
.ss-alert__title::before { content: "\2717"; margin-inline-end: var(--sp-2); color: var(--danger); }

/* ══ the manage URL (`/s/{token}`) ═══════════════════════════════════════
   The organiser's own page: what the draw produced, and how to hand it out. Its
   signature is the LINK ROW — one line per person carrying their name, their own
   unguessable URL and the ways to send it. It is the thing this tool has that no
   competitor does, so it is set like a document rather than as a grid of cards. */

.ss-manage { display: grid; gap: var(--sp-6); padding-block: var(--sp-6) var(--sp-8); }

/* THE KEY NOTE. Louder than a hint, because losing this URL is losing the event:
   the warn box, and the address itself in code type under it. */
.ss-key {
  display: grid;
  gap: var(--sp-2);
  border: 1px solid var(--warn);
  border-radius: var(--r-1);
  padding: var(--sp-3) var(--sp-4);
  background: color-mix(in srgb, var(--warn) 6%, var(--surface));
}
.ss-key code { overflow-wrap: anywhere; }

/* The closing notes: the retention promise, the date it works out to, and the way back. */
.ss-small { display: grid; gap: var(--sp-2); }

.ss-out__head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); }
.ss-out__head .ss-out__actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-inline-start: auto; }

.ss-links { display: grid; }
.ss-link {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  padding-block: var(--sp-3);
  border-block-end: 1px solid var(--line);
  /* A grid item's automatic minimum size is its min-content width, and the row of
     buttons at the end of the line is wider than a phone — so without this the row
     pushes the panel past a 393 px viewport (WCAG 2.1 §1.4.10 Reflow). The url
     below already truncates, which is what makes shrinking safe here. */
  min-inline-size: 0;
}
.ss-link:last-child { border-block-end: 0; }
.ss-link__name { flex: 0 1 8rem; min-inline-size: 0; overflow-wrap: anywhere; font-weight: 700; }
/* A URL is a technical string: LTR in every locale, and truncated rather than
   wrapped — the copy button is what actually carries it anywhere. */
.ss-link__url {
  flex: 1 1 13rem;
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink-2);
}
/* A PIN is read off a message a digit at a time, so it is set like one: six
   digits (PIN_DIGITS, src/santa-manage.ts), tabular and spaced, LTR in every
   locale because it is a technical string. */
.ss-link__pin {
  flex: none;
  font-variant-numeric: tabular-nums;
  letter-spacing: .15em;
  direction: ltr;
  font-weight: 700;
}
.ss-link__actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-inline-start: auto; }

/* The word beside a link: whether it has been opened. A WORD and not a dot —
   this has to survive a mono print and a reader who is not looking at colour —
   and `--ink-2` until it happens, so the page reads as a list of things still
   to do rather than a wall of green. */
.ss-link__seen {
  flex: none;
  font-size: var(--fs-0);
  color: var(--ink-2);
}
.ss-link__seen.is-seen { color: var(--ok); font-weight: 700; }

/* THE ADDRESS A LINK IS MAILED TO (Task 18, email mode). `flex: 1 1 12rem` and
   `overflow-wrap: anywhere` for the same reason `.ss-link__name` has them: an
   address is a long unbroken run, and a flex item's automatic minimum size is
   its min-content width — without both, one long address pushes the row wider
   than a phone (the `min-inline-size` contract at the top of this file). */
.ss-link__mail {
  flex: 1 1 12rem;
  min-inline-size: 0;
  overflow-wrap: anywhere;
  font-size: var(--fs-0);
  color: var(--ink-2);
}

/* Last year's event file, on the setup page: a file input and the sentence it
   answers with. `<input type="file">` keeps the browser's own button, which is
   the one control on this site whose words we do not choose — it is localized by
   the browser rather than by us, and a custom one would need a script. */
.ss-prev { display: grid; gap: var(--sp-2); }

/* ── the organiser's dashboard (unit 20b) ─────────────────────────────
   Under the list it acts on, and made of the same cards the setup page is made
   of — this is the same person doing more of the same job, not a second product
   with a console. Every control in it is a real <form>, so the whole dashboard
   works with no script at all. */
.ss-dash { display: grid; gap: var(--sp-4); }
.ss-verb { display: contents; }
/* A verb with fields in it is a small stack; one that is a lone button keeps
   `display: contents` above, so the button sits in the row it belongs to. */
.ss-verb--stack { display: grid; gap: var(--sp-3); justify-items: start; }
.ss-verb--stack .field { inline-size: 100%; }

/* THE SENTENCE A VERB LEAVES BEHIND — and, since Task 18, the second one about
   the email under it. The same boxed treatment `.ss-key` uses, in the colour
   of what happened; the second line is a `.hint`, so it is quieter without being
   a different kind of thing. */
.ss-notice {
  display: grid;
  gap: var(--sp-1);
  border: 1px solid var(--ok);
  border-radius: var(--r-1);
  padding: var(--sp-3) var(--sp-4);
  background: color-mix(in srgb, var(--ok) 6%, var(--surface));
  font-weight: 600;
}
.ss-notice > p { margin: 0; }
.ss-notice--warn {
  border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 6%, var(--surface));
}

/* The one that ends the event, behind a native disclosure: the button is not
   under the cursor until the promise beside it has been opened and read. */
.ss-danger { border-color: color-mix(in oklab, var(--danger) 35%, var(--line)); }
.ss-danger__step { display: grid; gap: var(--sp-2); }
.ss-danger__step > summary {
  cursor: pointer;
  font-weight: 700;
  padding-block: var(--sp-2);
}

/* ── the printable sheet ──────────────────────────────────────────────── */
.ss-sheet { display: grid; gap: var(--sp-4); }
.ss-sheet__head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); }
.ss-sheet__head .btn { margin-inline-start: auto; }
.ss-sheet__head .btn + .btn { margin-inline-start: 0; }

/* ── print: the cards, and nothing else ──────────────────────────────
   The paper output of this page is the QR sheet; everything that made it is
   marked data-no-print and stays behind. base.css turns the palette black on
   white and hides every button; components.css sizes .qr-card and .qr-sheet in
   millimetres so the cards land several to a page. */
@media print {
  @page { margin: 10mm; }
  .ss-manage { gap: 0; padding-block: 0; }
  .ss-main { padding-block-end: 0; }
}

/* ══ the reveal link (`/r/{token}`) ══════════════════════════════════════
   The most-shared surface in the suite and the only one most of its visitors will
   ever see: somebody is handed a link or a printed card, opens it on a phone,
   learns one name and closes it again. So it is a sibling of /join — ONE narrow
   column, phone-width, nothing beside it — and the instrument is an ENVELOPE.

   Not decoration: a sealed envelope is the promise the whole tool is making, and
   the flap turning back is the only moment of theatre a gift draw has. Under
   prefers-reduced-motion base.css switches the transition off and the card is
   simply there. */

.rv { padding-block: var(--sp-6) var(--sp-8); }
/* Wider than /join's code column: this one carries a name that might be four
   Chinese characters or twenty-eight Cyrillic ones. */
.rv-col { display: grid; gap: var(--sp-5); max-inline-size: 34rem; margin-inline: auto; }
.rv-head { display: grid; gap: var(--sp-2); }
.rv-head h1 { font-size: var(--fs-4); }

/* Budget and date: the two facts you need before you go shopping, as one quiet
   line under the title — the row's own gap is what separates them. */
.rv-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-4);
  color: var(--ink-2);
  font-size: var(--fs-0);
}

/* ── the envelope ─────────────────────────────────────────────────────
   The button IS the envelope: the whole thing is the target, at any size a thumb
   can find, and it says what it does underneath. */
.rv-env {
  display: grid;
  justify-items: center;
  gap: var(--sp-4);
  inline-size: 100%;
  padding: var(--sp-5) var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  color: var(--ink);
  text-align: center;
  transition: border-color var(--t-fast) ease-out;
}
.rv-env:hover { border-color: var(--brand); }
/* The drawing takes its colours from the token set like everything else: a
   presentation attribute would put a literal in the markup, and the envelope has
   to be legible in both themes. */
.rv-body { fill: var(--surface-2); stroke: var(--ink-2); }
.rv-flap { fill: var(--brand-soft); stroke: var(--brand); }
.rv-env__art { inline-size: 100%; max-inline-size: 17rem; }
.rv-env__art svg { inline-size: 100%; block-size: auto; }
.rv-env__label { font-size: var(--fs-2); font-weight: 800; }
/* One part moves, and it moves around its own edge. `transform-box: fill-box`
   makes the origin the shape's own box rather than the whole SVG viewport, so the
   flap turns on the line where it is glued. Flipping a downward triangle about
   its top edge IS an open flap — no perspective, nothing an engine can disagree
   about. */
.rv-flap { transform-box: fill-box; transform-origin: 50% 0; transition: transform var(--t-base) ease-out; }
.rv-stage.is-open .rv-flap { transform: scaleY(-1); }
.rv-stage.is-open .rv-env { opacity: 0; }

.rv-result { display: grid; gap: var(--sp-4); }
@keyframes rv-arrive { from { opacity: 0; translate: 0 var(--sp-4); } }
.rv-result { animation: rv-arrive var(--t-base) ease-out; }
/* The name is the largest thing on the page — larger than the h1, because it is
   what the visitor came for. .result-banner__name is already --fs-4; this is the
   one place in the suite that takes it up a step. */
.rv-result .result-banner__name { font-size: var(--fs-5); }
.rv-result:focus-visible { outline: 2px solid var(--brand); outline-offset: 4px; }

/* THE PARTICIPANT'S OWN WISH LIST (unit 20b). A card like the giftee's above it,
   with a form in it — quieter than the card that carries the name, because it is
   the thing to do next rather than the thing that was come for. */
.rv-own { display: grid; gap: var(--sp-3); justify-items: start; }
.rv-own .field { inline-size: 100%; }
.rv-own__saved { color: var(--ok); }

/* ── the gates ────────────────────────────────────────────────────────
   A PIN is read off a message a digit at a time, so it is set like one: tabular,
   spaced, centred, and LTR in every locale. */
.rv-pin {
  min-block-size: 3.5rem;
  font-size: var(--fs-3);
  font-weight: 800;
  letter-spacing: .3em;
  font-variant-numeric: tabular-nums;
  text-align: center;
}
.rv-go { inline-size: 100%; min-block-size: 3.5rem; font-size: var(--fs-2); }
/* The same boxed treatment /join gives a code it does not know. The glyph carries
   the status as well as the colour. */
.rv-error {
  display: flex;
  gap: var(--sp-2);
  border: 1px solid var(--danger);
  border-radius: var(--r-1);
  padding: var(--sp-3) var(--sp-4);
  background: color-mix(in srgb, var(--danger) 6%, var(--surface));
  color: var(--ink);
  font-size: var(--fs-0);
}
.rv-error::before { content: "\2717"; font-weight: 800; color: var(--danger); }
.rv-claimed { border-color: var(--danger); }

/* ── the manual ───────────────────────────────────────────────────────── */
.rv-note {
  display: grid;
  gap: var(--sp-2);
  padding-block-start: var(--sp-5);
  border-block-start: 1px solid var(--line);
}
.rv-note p:not(.eyebrow) { color: var(--ink-2); }

/* ═══ /d/{id} — a verifiable draw record (Task 17) ════════════════════════
   Ported from the signed-off prototype's `.dr-*` block (docs/prototype/draw-record.html),
   which states the design in three moves and they are kept exactly:

     1. THE VERDICT IS THE INSTRUMENT. It sits above the tabs, before any of the evidence,
        because the question a visitor arrives with is "can I trust this?" and the answer is a
        button they press themselves. It is the one place on the page that changes colour.
     2. THE RESULT IS THE TOP OF THE ORDER. The winners are not a separate fact — they are the
        first N rows of the shuffled list, shown twice on purpose, so the layout teaches the rule
        without a sentence explaining it.
     3. FINGERPRINTS ARE SET AS OBJECTS, NOT AS TEXT. Each one is a labelled block of monospace
        on the page's second surface — the three of them read as a seal.

   Everything else is deliberately quiet: one measure, ruled sections, `.eyebrow` micro-labels,
   and no colour anywhere except the verdict and the winners' band. Three blocks are NOT the
   prototype's, because production has three things the demo did not: the share surface, the
   creator's own controls, and the raw record as a disclosure rather than a tab panel. */

.dr { padding-block: var(--sp-6) var(--sp-8); }

/* One column with a reading measure. A record is read, not operated, so it never goes wider
   than this even on a projector. */
.dr-col {
  display: grid;
  gap: var(--sp-5);
  max-inline-size: 54rem;
  margin-inline: auto;
}

.dr-head { display: grid; gap: var(--sp-2); }
.dr-head h1 { font-size: var(--fs-4); }

/* ── the letterhead ───────────────────────────────────────────────────
   Who this record is, in four fields. Ruled top and bottom so it reads as part of the page's
   masthead rather than as the first section of the record. */
.dr-letterhead {
  display: grid;
  gap: var(--sp-2);
  padding-block: var(--sp-4);
  border-block: 1px solid var(--line);
}
.dr-meta { display: flex; flex-wrap: wrap; gap: var(--sp-4) var(--sp-6); }
.dr-meta__item { display: grid; gap: var(--sp-1); min-inline-size: 0; }
.dr-meta__item dd { font-weight: 600; }

/* What the last request left behind — the metered refusal, and nothing else uses it. The boxed
   treatment /join and the reveal page already give the machine talking back: a glyph, a colour
   and a box, and it WRAPS (a pill sets `white-space: nowrap` and a sentence in one pushes a
   390 px page sideways). */
.dr-notice {
  display: flex;
  gap: var(--sp-2);
  border: 1px solid var(--warn);
  border-radius: var(--r-1);
  padding: var(--sp-3) var(--sp-4);
  background: color-mix(in srgb, var(--warn) 6%, var(--surface));
  color: var(--ink);
  font-size: var(--fs-0);
}
.dr-notice::before { content: "!"; font-weight: 800; color: var(--warn); }

/* ── the verdict ──────────────────────────────────────────────────────
   Neutral until somebody presses it; then the border, the glyph and the word all change
   together, so the state survives a colour-blind reader and a black-and-white print. */
.dr-verdict {
  display: grid;
  gap: var(--sp-4);
  padding: var(--sp-5);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
}
@media (min-width: 46rem) {
  .dr-verdict {
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    column-gap: var(--sp-5);
  }
}
.dr-verdict__btn { min-block-size: 3.25rem; font-size: var(--fs-1); }
.dr-verdict__say { display: grid; gap: var(--sp-2); min-inline-size: 0; }
.dr-verdict__msg { font-weight: 600; }
.dr-verdict--pass { border-color: var(--ok); }
.dr-verdict--fail { border-color: var(--danger); }
.dr-verdict--pass .dr-verdict__msg { color: var(--ok); }
.dr-verdict--fail .dr-verdict__msg { color: var(--danger); }
/* The glyph carries the state as well as the colour — the same pair components.css puts on
   .pill-ok and .pill-danger. Never mirrored in RTL. */
.dr-verdict--pass .dr-verdict__msg::before,
.dr-verdict--fail .dr-verdict__msg::before {
  font-weight: 800;
  padding-inline-end: var(--sp-2);
}
.dr-verdict--pass .dr-verdict__msg::before { content: "\2713"; }
.dr-verdict--fail .dr-verdict__msg::before { content: "\2717"; }

/* ── the result ───────────────────────────────────────────────────────
   The banner holds every winner, not only the first: a draw for two is as ordinary as a draw
   for one, and a component that shows one name lies the moment somebody asks for three. */
.dr-winners { display: grid; gap: var(--sp-2); }
.dr-winners .result-banner__name { font-size: var(--fs-3); }
/* Masked, a winner is a fingerprint: it drops to body size and breaks, because a 64-character
   run set at fs-3 is a horizontal scrollbar on every phone. */
.dr-winners .dr-winners__hash {
  font-size: var(--fs-0);
  font-weight: 600;
  overflow-wrap: anywhere;
}

/* One row shape, two lists: the full order and the entries. The badge is a position, so it is
   tabular and never reordered. */
.dr-rows { display: grid; }
.dr-row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding-block: var(--sp-3);
  border-block-end: 1px solid var(--line);
}
.dr-row:last-child { border-block-end: 0; }
.dr-row__n {
  display: grid;
  place-items: center;
  flex: none;
  inline-size: var(--sp-6);
  block-size: var(--sp-6);
  border-radius: var(--r-round);
  background: var(--surface-2);
  color: var(--ink-2);
  font-size: var(--fs-0);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  direction: ltr;                 /* a position is a number, never reordered */
}
.dr-row__name { min-inline-size: 0; overflow-wrap: anywhere; }
/* Pushes the badge to the far end in either direction. */
.dr-row__tag { margin-inline-start: auto; flex: none; }
/* A winner's row, in the order list: a wash of the brand rather than a second banner, so the
   eye reads one list with a top to it. */
.dr-row--win { background: var(--brand-soft); }
.dr-row--win .dr-row__n { background: var(--brand); color: var(--brand-ink); }
.dr-row--win .dr-row__name { font-weight: 800; }
/* The pill's --ok is drawn to clear 4.5:1 on --bg and --surface, not on the wash; standing the
   pill back on the card surface restores it (the prototype measured 4.22:1 on --brand-soft in
   light, a WCAG 2.1 §1.4.3 failure, and 5.07:1 once it stands on --surface). */
.dr-row--win .pill-ok { background: var(--surface); }
/* The wash needs air around the text it carries, so the row borrows a little of the card's
   padding back on both sides — logical, so it borrows the right side in RTL. */
.dr-row--win {
  margin-inline: calc(var(--sp-3) * -1);
  padding-inline: var(--sp-3);
  border-radius: var(--r-1);
}
/* A masked entry: a fingerprint where the name was. Monospace, LTR, and allowed to break
   anywhere — 64 hex characters do not fit on a phone in one run. */
.dr-row__hash { font-size: var(--fs-0); overflow-wrap: anywhere; min-inline-size: 0; }

/* ── was I included? ──────────────────────────────────────────────────── */
.dr-mask { display: grid; gap: var(--sp-3); }
.dr-check { display: grid; gap: var(--sp-3); }
@media (min-width: 34rem) {
  .dr-check__row { display: flex; gap: var(--sp-3); align-items: end; }
  .dr-check__row .field { flex: 1 1 auto; min-inline-size: 0; }
}
/* Two answers, and neither of them is an error: yes takes the ok colour and the same glyph the
   pills carry, no is simply quiet. */
.dr-check__out { font-weight: 600; }
.dr-check__out--yes { color: var(--ok); }
.dr-check__out--yes::before { content: "\2713"; font-weight: 800; padding-inline-end: var(--sp-2); }
.dr-check__out--no { color: var(--ink-2); }

/* ── the seal ─────────────────────────────────────────────────────────
   Three fingerprints, one shape. The value is an object on the page's second surface:
   selectable, copyable, wrapped rather than truncated — a hash you cannot see all of is a hash
   you cannot check. */
.dr-seal { display: grid; gap: var(--sp-4); }
.dr-hash { display: grid; gap: var(--sp-2); }
.dr-hash__v {
  display: block;
  padding: var(--sp-3);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-1);
  font-size: var(--fs-0);
  line-height: 1.6;
  overflow-wrap: anywhere;
  overflow-x: auto;          /* for a run even `anywhere` will not break */
}

/* Label above, value beside — the record's own fields, in the record's own voice. */
.dr-kv { display: grid; gap: var(--sp-3); }
.dr-kv__row { display: grid; gap: var(--sp-1); }
@media (min-width: 34rem) {
  .dr-kv__row { grid-template-columns: 12rem minmax(0, 1fr); gap: var(--sp-4); align-items: baseline; }
}
.dr-kv__row dd { min-inline-size: 0; overflow-wrap: anywhere; }

/* ── the share surface (spec §3.6) ────────────────────────────────────
   The lobby sign's two routes at a document's scale: what to type on one side, what to point a
   camera at on the other. The QR is fixed at a size a phone reads from arm's length and does
   not grow with the column — this is a page somebody scans off their own screen, not a wall. */
.dr-share__grid { display: grid; gap: var(--sp-5); align-items: start; }
@media (min-width: 40rem) {
  .dr-share__grid { grid-template-columns: minmax(0, 1fr) auto; column-gap: var(--sp-6); }
}
.dr-share__text { display: grid; gap: var(--sp-2); min-inline-size: 0; }
.dr-share__url { overflow-wrap: anywhere; }
.dr-share__code {
  font-size: var(--fs-3);
  font-weight: 800;
  letter-spacing: .12em;
  font-variant-numeric: tabular-nums;
}
.dr-share__scan { display: grid; gap: var(--sp-2); justify-items: center; }
.dr-qr {
  inline-size: 8.5rem;
  block-size: 8.5rem;
  display: grid;
  place-items: center;
}
/* The encoder writes an SVG with no size of its own. */
.dr-qr svg { inline-size: 100%; block-size: 100%; }

/* ── the creator's own controls ───────────────────────────────────────
   A card with no head: it is a sentence and two buttons, and a heading would name a landmark
   nobody needs to walk to (src/pages/draw.tsx says the same). */
.dr-manage { gap: var(--sp-4); border-color: var(--warn); }

/* ── the raw record ───────────────────────────────────────────────────
   base.css already sets direction: ltr on <pre>, so this only has to give it a surface and
   somewhere for a long line to go.

   That "somewhere" has to be a WRAP, not a scroller: a <pre> keeps its lines whole, and the
   widest line here is a 64-character fingerprint, so `overflow-x: auto` alone let the block
   claim ~1100px of intrinsic width and stretched the whole page grid around it (WCAG 2.1
   §1.4.10 Reflow). Wrapping is also what the page already does with the same fingerprints in
   .dr-hash__v, for the same reason. */
/* The summary is `/fair`'s disclosure treatment (`.fa-how`), repeated rather than shared: two
   page blocks with the same five rules is honest duplication, and a `.disclosure` component in
   components.css is the right move on the THIRD one, not the second. The glyph is symmetric —
   nothing to mirror in RTL, and it survives a mono print. */
.dr-raw { padding-block-start: var(--sp-4); border-block-start: 1px solid var(--line); }
.dr-raw summary {
  display: flex;
  align-items: baseline;
  gap: var(--sp-4);
  font-weight: 700;
  cursor: pointer;
  list-style: none;
}
.dr-raw summary::-webkit-details-marker { display: none; }
.dr-raw summary::after {
  content: "+";
  margin-inline-start: auto;
  font-size: var(--fs-2);
  font-weight: 400;
  line-height: 1;
  color: var(--ink-2);
}
.dr-raw[open] summary::after { content: "\2212"; }
.dr-raw__body { --stack-gap: var(--sp-3); padding-block-start: var(--sp-3); }
.dr-json {
  padding: var(--sp-4);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-1);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  overflow-x: auto;
  font-size: var(--fs-0);
  line-height: 1.6;
}

.dr-notes {
  display: grid;
  gap: var(--sp-2);
  padding-block-start: var(--sp-5);
  border-block-start: 1px solid var(--line);
}

/* ═══ the account (Task 19, spec §3.10) ══════════════════════════════════
   Two Worker-rendered pages — the sign-in form and the account behind it (src/pages/account.tsx)
   — and no page script at all: every control here is a real `<form>`. The shapes are borrowed
   rather than invented, because both pages are made of things this site already has: `.ac-stage`
   is `/join`'s instrument-and-manual split, `.ac-notice` is the santa dashboard's notice, and
   `.ac-danger` is its cancel card. Tokens only, logical properties only. */

.ac { padding-block: var(--sp-6) var(--sp-8); }

.ac-head { display: grid; gap: var(--sp-2); padding-block-end: var(--sp-6); }
.ac-head h1 { font-size: var(--fs-4); }
/* The address, and the button that ends the session, on one line that wraps on a phone. */
.ac-head__meta { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2); }
.ac-head__meta code { overflow-wrap: anywhere; }

/* The form and the two sentences beside it — `/join`'s split at the same breakpoint, because it
   is the same shape of page: one instrument, one manual. */
.ac-stage { display: grid; gap: var(--sp-5); align-items: start; }
.ac-stage > * { min-inline-size: 0; }
@media (min-width: 56rem) {
  .ac-stage { grid-template-columns: minmax(0, 30rem) minmax(14rem, 1fr); gap: var(--sp-7); }
}
.ac-stack { --stack-gap: var(--sp-5); }
.ac-aside { display: grid; gap: var(--sp-3); align-content: start; }
.ac-note { color: var(--ink-2); }

.ac-card { gap: var(--sp-4); }
/* Six digits, read off a phone and typed once: tabular and spaced, the join page's own code box
   at a smaller measure because this one is not on a projector. */
.ac-code {
  font-size: var(--fs-3);
  font-weight: 700;
  letter-spacing: .12em;
  font-variant-numeric: tabular-nums;
}

/* What the last verb did. The santa dashboard's notice, to the character: the border carries the
   tone as well as the colour, so nothing here is signalled by colour alone. */
.ac-notice {
  display: grid;
  gap: var(--sp-1);
  border: 1px solid var(--ok);
  border-radius: var(--r-1);
  padding: var(--sp-3) var(--sp-4);
  background: color-mix(in srgb, var(--ok) 6%, var(--surface));
  margin-block-end: var(--sp-5);
  font-weight: 600;
}
.ac-notice > p { margin: 0; }
.ac-notice--warn {
  border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 6%, var(--surface));
}

.ac-block { display: grid; gap: var(--sp-4); margin-block-end: var(--sp-6); }
.ac-block__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-3); }
.ac-empty { color: var(--ink-2); }

/* One saved wheel: a name, its names, its link and the two verbs. A card of its own, because
   each is a separate form and a separate thing to delete. */
.ac-wheel { display: grid; gap: var(--sp-4); }
.ac-wheel__edit { display: grid; gap: var(--sp-4); justify-items: start; }
.ac-wheel__names { min-block-size: 9rem; resize: vertical; }
.ac-wheel__link { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2); }
.ac-wheel__link code { overflow-wrap: anywhere; }

.ac-events { display: grid; gap: var(--sp-3); }
.ac-event { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); }
.ac-event__name { min-inline-size: 0; font-weight: 600; overflow-wrap: anywhere; }

/* The API keys (Task 23, spec §3.12). The events list's shape, for the same reason it has that
   shape: a name, some dates, and one control, on a line that wraps on a phone. A key is never
   in this list — only its label, its dates and the first characters of its hash, which is what a
   person needs to tell two of them apart. */
.ac-keys { display: grid; gap: var(--sp-3); }
.ac-key { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); }
.ac-key__name { min-inline-size: 0; font-weight: 600; overflow-wrap: anywhere; }
/* The one moment a key exists in the clear. Boxed and full-measure, because it is long, it must
   be selectable in one gesture, and it will not be shown again. */
.ac-key__secret {
  display: block;
  overflow-wrap: anywhere;
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  background: var(--surface-2);
  padding: var(--sp-3);
  font-size: var(--fs-2);
}

/* A verb with nothing around it: the form is a wrapper, not a layout. */
.ac-verb { display: grid; gap: var(--sp-2); justify-items: start; }

/* The one that ends the account, behind a native disclosure — the santa dashboard's cancel card,
   for its reason: the button is not under the cursor until the promise beside it has been opened
   and read, and `<details>` is what a confirm dialog would have been. */
.ac-danger { border-color: color-mix(in oklab, var(--danger) 35%, var(--line)); }
.ac-danger__step { display: grid; gap: var(--sp-2); }
.ac-danger__step > summary {
  cursor: pointer;
  font-weight: 700;
  padding-block: var(--sp-2);
}

/* ═══ the two controls at the foot of a shared page (Task 25, spec §3.11) ══════════════════
   The report button and the gallery offer. Both are native `<details>` and both sit BELOW the
   small print, which is where their register already is: `.sw-foot` says the block under the
   rule is about the link rather than about the wheel, and these are the two things a reader can
   do about the link. The summary styling is `.ac-danger__step`'s, to the character — this site
   has exactly one look for "a disclosure holding a form", and a second one would be a second
   thing to keep in step. */
.sw-mod { display: grid; gap: var(--sp-3); }
.sw-mod__block { display: grid; gap: var(--sp-3); }
.sw-mod__block > summary {
  cursor: pointer;
  font-weight: 700;
  padding-block: var(--sp-2);
}
.sw-mod__block form { display: grid; gap: var(--sp-4); justify-items: start; }
.sw-mod__block .input { max-inline-size: 44ch; }
.sw-mod__block textarea.input { min-block-size: 6rem; }

/* What the last submission did. `.ac-notice`'s shape without its grid: one sentence, and the
   border carries the tone as well as the colour so nothing is signalled by colour alone. */
.sw-notice {
  border: 1px solid var(--ok);
  border-radius: var(--r-1);
  padding: var(--sp-3) var(--sp-4);
  background: color-mix(in srgb, var(--ok) 6%, var(--surface));
  font-weight: 600;
}

/* ═══ /gallery — the curated shared wheels (Task 25, spec §3.11) ═══════════════════════════
   A content page's head over a grid of cards. The head is `.ga-head` rather than `.fr-doc`
   because this page is not a document: it is a heading, two lines of explanation and then a
   list, so it takes the account page's head measure and none of its prose measure.

   THE GRID IS `auto-fill`, NOT `auto-fit`, and the difference is the empty case one card away:
   `auto-fit` collapses the empty tracks and stretches a single card across the whole page, which
   makes the first approved wheel look like a banner. `auto-fill` leaves the tracks, so one card
   is card-sized and the gallery grows into its shape. */
.ga { padding-block: var(--sp-6) var(--sp-8); }

.ga-head { display: grid; gap: var(--sp-2); padding-block-end: var(--sp-6); }
.ga-head h1 { font-size: var(--fs-4); }
.ga-head .lede { max-inline-size: 60ch; }
.ga-head .hint { max-inline-size: 66ch; }
.ga-section { display: grid; gap: var(--sp-4); margin-block-end: var(--sp-7); }
.ga-starter .ga-card__body { grid-template-rows: auto auto 1fr auto; }
.ga-options { list-style: disc; padding-inline-start: var(--sp-4); line-height: 1.6; }

.ga-empty { color: var(--ink-2); max-inline-size: 60ch; }

.ga-grid {
  display: grid;
  gap: var(--sp-4);
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
}

.ga-card {
  display: grid;
  grid-template-rows: auto 1fr;
  align-content: start;
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  background: var(--surface);
  min-inline-size: 0;
}
.ga-card__body { display: grid; gap: var(--sp-2); padding: var(--sp-4); align-content: start; }
/* Somebody else's title, at whatever length they chose: it wraps inside its own card rather
   than widening the track, which is what `min-inline-size: 0` above and this line are for. */
.ga-card__link { font-weight: 700; overflow-wrap: anywhere; }
