/* Vendored variable fonts — DESIGN-BRIEF §4.1, design-system.md §2.5.
 *
 * Figtree (admin) and Nunito (participant) are SIL OFL stand-ins for the
 * Mallory brand face, which has no licence (OD-09). All four families are
 * served from our own origin: `font-src 'self'` blocks a CDN fetch
 * structurally, so a missing file degrades to the system stack rather than
 * silently phoning out.
 *
 * Figtree and Nunito carry latin-only unicode-range. That is what makes the
 * Noto tail of the stack reachable: an Arabic or Ethiopic codepoint finds no
 * coverage in the latin faces and falls through. v1 finding 084-F01 recorded
 * that 5 of 9 locales were relying on the device having Noto installed, so the
 * Arabic and Ethiopic subsets are vendored too.
 */

@font-face {
  font-family: "Figtree";
  font-style: normal;
  font-weight: 300 900;
  font-display: swap;
  src: url("/assets/fonts/figtree-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

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

@font-face {
  font-family: "Nunito";
  font-style: normal;
  font-weight: 200 1000;
  font-display: swap;
  src: url("/assets/fonts/nunito-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

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

@font-face {
  font-family: "Noto Sans Arabic";
  font-style: normal;
  font-weight: 100 900;
  font-stretch: 100%;
  font-display: swap;
  src: url("/assets/fonts/noto-sans-arabic-arabic.woff2") format("woff2");
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1,
    U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF,
    U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4,
    U+10EFC-10EFF, U+1EE00-1EEFF;
}

@font-face {
  font-family: "Noto Sans Ethiopic";
  font-style: normal;
  font-weight: 100 900;
  font-stretch: 100%;
  font-display: swap;
  src: url("/assets/fonts/noto-sans-ethiopic-ethiopic.woff2") format("woff2");
  unicode-range: U+030E, U+1200-1399, U+2D80-2DDE, U+AB01-AB2E, U+1E7E0-1E7E6,
    U+1E7E8-1E7EB, U+1E7ED-1E7EE, U+1E7F0-1E7FE;
}

/* surveyPanelV2 semantic token layer — DESIGN-BRIEF §4.1, design-system.md §2/§3.
 *
 * This file is the ONLY place in the repo where a colour literal may appear;
 * `test/no-hardcoded-colours.test.ts` enforces that. Every light value below is
 * verbatim from the brief. Dark values marked (derived) are NOT in the brief:
 * the brief fixes 11 dark values, design-system.md §2.3 delegates the rest to
 * this sub-phase under the rule "propose value -> contrast CI must pass
 * mathematically -> record the computed ratio as a comment". The recorded
 * ratios are re-derived by the contrast test on every run; a drifted comment is
 * a comment, but a drifted value turns the build red.
 *
 * Block order is load-bearing. Custom properties cascade by specificity and
 * then source order, and several selectors here tie at (0,1,0):
 *   1  :root                                     light, complete, closed set
 *   2  [data-surface="participant"]              darker participant ground
 *   3  @media prefers-color-scheme: dark         system preference
 *   4  :root[data-theme="dark"]                  explicit choice at the root
 *   5  [data-theme="dark"]                       explicit choice on a subtree
 *   6  participant dark ground                   (0,2,0) beats blocks 3-5
 *   7  [data-theme="light"]                      light island inside dark
 *   8  participant light island                  (0,2,0) beats block 7
 * Blocks 3, 4 and 5 carry byte-identical bodies; block 7 restates block 1 for
 * exactly the same key set. `test/token-structure.test.ts` asserts both, so the
 * duplication cannot silently drift.
 *
 * Four blocks sit outside that palette machinery:
 *   9  @media prefers-reduced-motion            duration kill switch
 *  9b  :root[data-motion="reduce"]              the same kill switch, chosen
 *  10  [data-density="compact"]                 admin row rhythm
 *  11  high contrast, staff only (two forms)    an overlay, last
 *
 * DESIGN SYSTEM v4 FOUNDATIONS (DS-1, world-class W1). SP2-ADR-020 (ADR-DS4),
 * ratified as ONE package by the product owner on 2026-10-03 (OD-36, "2-evet"),
 * adds items DS4-a…j (design-system-v4 §17): neutral role slots, status solids
 * and the info pair, the dark rail, two high-contrast staff palettes, viz role
 * aliases, JetBrains Mono for codes (staff only), the script line height, the
 * display/title sizes and the 12px eyebrow, space snug/card, and the materials.
 * Every DS4 key is born in block 1 and restated, where it is a colour, in the
 * dark blocks 3-5 and the light island 7. NONE of them reaches the participant:
 * `test/participant-token-fence.test.ts` proves no participant-served rule
 * reads one, the participant blocks 2/6/8 declare none, and two v3 values the
 * participant does read and DS4 moved (the eyebrow size) are pinned back in
 * blocks 2 and 8.
 *
 * `color-scheme` IS SET HERE AND NOWHERE ELSE (MASTER-PLAN §3.2 ruling 11):
 * light in blocks 1 and 7, dark in blocks 3-5. It is the one declaration in this
 * file that is not a custom property, and it is what makes native controls —
 * the checkbox, the radio, the select list, the scrollbar — render in the
 * palette's own scheme instead of as UA-white squares on a dark card. It
 * reaches the participant through blocks 1 and 3-5 on purpose (the dark
 * participant checkbox, design-system-v4 §3.4); it changes no participant
 * token value. `test/color-scheme.test.ts` holds both claims.
 * Motion tokens (block 1, "Motion") are theme-independent — a duration is the
 * same number in dark — so blocks 3-8 never restate them; block 9 re-declares
 * ONLY the durations, and sits after block 1 so its (0,1,0) `:root` wins the
 * source-order tie wherever the media query matches. Block 10 ties with `:root`
 * at (0,1,0) too and therefore must also follow block 1; it re-declares ONLY
 * spacing and row-rhythm tokens, which no palette or motion block touches, so
 * its position relative to blocks 2-9 carries no meaning at all.
 */

/* ---------------------------------------------------------------- 1. LIGHT */
/* Every token in the system is born here. The dark blocks only redefine; a
 * token that exists only in dark is a structure-test failure. */
:root {
  color-scheme: light;

  /* Palette v3 (session 060, SP2-ADR-019): light SaaS chrome — white chrome,
   * neutral cool-grey ground, near-black ink, ONE vivid indigo. The participant
   * surface does not move: blocks 2/6/8 pin its pre-060 values explicitly and
   * `test/participant-palette-pin.test.ts` holds every resolved value. */

  /* Grounds */
  --sp-page: #F6F7F9;
  --sp-surface: #FFFFFF;
  --sp-surface-sunken: #EEF0F3;

  /* Ink */
  --sp-ink: #101828;
  --sp-ink-muted: #5A6477;              /* worst 5.22:1, on --sp-surface-sunken */

  /* Brand */
  --sp-primary: #4F46E5;                /* 6.29:1 with --sp-on-primary */
  --sp-primary-strong: #4338CA;         /* pressed/hover face — 7.94:1 with --sp-on-primary */
  --sp-on-primary: #FFFFFF;
  /* The accent IS the indigo in v3: it paints the rail's active marker, the
   * rail's focus ring and the brand square, all on the WHITE sidebar, where a
   * mint would vanish (1.4:1). One hue, one system. */
  --sp-accent: #4F46E5;
  --sp-on-accent: #FFFFFF;              /* 6.29:1 on --sp-accent */
  /* The progress VALUE. An alias, and mode-split on purpose: the fill must
   * clear 3:1 against the track (--sp-border), and no one hue does that on
   * both sides of the theme — light lavender primary against a border light
   * enough to clear the dark grounds lands at 2.3:1. Light fills with the
   * pressed brand face; dark fills with the accent, which reads as the same
   * "go" the CTA wears. Measured as its own pair in lib/pairs.ts. */
  /* v3: light fills with the INK. A vivid indigo light enough to be vivid
   * cannot sit 3:1 above a track that itself clears 3:1 on the sunken well. */
  --sp-progress-fill: var(--sp-ink);

  /* Feedback */
  --sp-error: #B4373D;
  --sp-error-surface: #FBE7E7;
  --sp-success-surface: #DCFBEF;
  --sp-on-success-surface: #056647;
  --sp-warning-surface: #FFF1CC;
  --sp-on-warning-surface: #8A6410;

  /* Focus — 3px solid ring at 2px offset, :focus-visible only */
  --sp-focus: #4F46E5;

  /* Lines. The border/rule split is a WCAG boundary, not a shade preference:
   * --sp-border bounds controls and is held to 1.4.11 (3:1); --sp-rule is
   * decorative, exempt, and must never bound a control. Merging them would
   * silently drop every control boundary to 1.22:1. */
  --sp-border: #7E8799;                 /* worst 3.16:1, on --sp-surface-sunken */
  --sp-rule: #E4E7EC;

  /* Depth. Decorative only, and deliberately outside the contrast matrix for
   * exactly the reason --sp-rule is: WCAG 1.4.11 exempts a purely decorative
   * boundary, and a shadow may never be the only thing separating a control
   * from its ground. These two are the only tokens in the file that carry an
   * alpha channel, which is also why `pair-coverage.test.ts` does not see them
   * — its colour-token test matches 6-digit hex, and 8-digit is not that. The
   * separation of surface from page is done by --sp-surface vs --sp-page,
   * which IS measured; the shadow only makes the step legible. */
  --sp-shadow: #10182814;
  --sp-shadow-strong: #10182826;

  /* Chrome — v3: a WHITE rail separated by a hairline, not a navy slab. */
  --sp-sidebar: #FFFFFF;
  --sp-sidebar-raised: #EEF0FF;         /* active/hover item — a whisper of the indigo */
  --sp-on-sidebar: #101828;
  --sp-on-sidebar-muted: #5A6477;       /* 5.26:1 on --sp-sidebar-raised */

  /* Dataviz categorical set — assigned by index in this order, stable per chart */
  --sp-viz-indigo: #5B57C7;
  --sp-viz-mint: #009466;
  --sp-viz-amber: #AB7810;
  --sp-viz-magenta: #B34A82;
  --sp-viz-cyan: #1A7F93;
  --sp-viz-olive: #6B7020;
  --sp-chart: var(--sp-viz-indigo);

  /* Dataviz ORDERED ramps — Phase09 09.7, SP2-ADR-012; design-system.md §3.
   * The full argument is in `lib/ramps.ts` (order, the missing midpoint) and
   * `lib/cvd.ts` (the floor and its derivation); this file is served verbatim
   * to every participant, so only what a reader of the STYLESHEET needs is
   * repeated here.
   *
   * seq-1 IS ALWAYS THE STEP NEAREST THE GROUND and seq-4 the furthest, light
   * or dark, so more ink always means more value and no component asks which
   * palette it is in. Same rule orders the diverging arms.
   *
   * BOTH RAMPS ARE FOUR STEPS AND THE SPEC ASKS FOR FIVE (PHASES/Phase09.md:384).
   * Deliberate and measured, under design-system.md §2.6 rule 4 — where the
   * floor cannot be met the RAMP shrinks, never the test. 3:1 walls each
   * palette at its WORST chart ground — light at L* 56.2 (the DARKEST light
   * ground, participant --sp-page), dark at L* 44.1 (the LIGHTEST dark ground,
   * --sp-surface) — so the shipped steps span ~44 L* and "pale" is what the
   * wall removes. Five steps in that room reach a worst adjacent CIEDE2000 of 10.4 under
   * colour-vision-deficiency simulation, four reach 13.2, and the house floor
   * is 12. THERE IS NO `--sp-viz-div-0`: a diverging midpoint that clears 3:1
   * can only be a mid-tone neutral, which is the tone the SUPPRESSED badge
   * owns, so zero divergence is the card ground plus the number — no fill.
   *
   * Re-measured by CI, never trusted from this comment. Worst contrast on
   * --sp-surface / --sp-page across all four palettes, worst adjacent dE00
   * across {normal, deuteranopia, protanopia, tritanopia}:
   *   sequential  light 3.12:1 / 14.54   dark 3.23:1 / 13.24
   *   diverging   light 3.99:1 / 24.75   dark 4.59:1 / 22.73 */
  --sp-viz-seq-1: #83819D;
  --sp-viz-seq-2: #5B5DAE;
  --sp-viz-seq-3: #2A317C;
  --sp-viz-seq-4: #1D1B31;
  --sp-viz-div-neg-2: #141F67;
  --sp-viz-div-neg-1: #6C66C8;
  --sp-viz-div-pos-1: #996900;
  --sp-viz-div-pos-2: #3C2200;

  /* Size and shape */
  --sp-touch-target: 44px;
  --sp-radius: 10px;
  --sp-radius-card: 14px;
  --sp-radius-pill: 999px;
  --sp-focus-ring-width: 3px;
  --sp-focus-ring-offset: 2px;

  /* Space */
  --sp-space-xsmall: 0.25rem;
  --sp-space-small: 0.5rem;
  --sp-space: 1rem;
  --sp-space-large: 2rem;
  --sp-space-xlarge: 3rem;

  /* Row rhythm — the DENSITY axis (Phase09 09.6; design-system.md §2.4).
   *
   * Two tokens rather than a hard-coded row height in each surface's sheet, so
   * the compact block at the end of this file can re-scope the rhythm of every
   * list in the admin shell without any component knowing that density exists.
   * Both values are today's rendered geometry written down: a question row's
   * min-block-size was `var(--sp-touch-target)` and a question list's gap was
   * `var(--sp-space-small)`, so comfortable renders byte-identically.
   *
   * --sp-row-block-size IS THE 44px FLOOR AS A ROW, and it is deliberately a
   * separate token from --sp-touch-target rather than an alias: the target
   * token is the WCAG contract (design-system §2.4) and may never move, while
   * a row height is a layout decision that a density could in principle grow.
   * It may never SHRINK below the contract, which is why the compact block
   * restates it unchanged and `test/token-structure.test.ts` measures it
   * against --sp-touch-target rather than trusting the comment. */
  --sp-row-block-size: 2.75rem;         /* 44px — the touch-target floor, as a row */
  --sp-row-gap: 0.5rem;

  /* Typography. Body stays at 1rem: below 16px mobile Safari focus-zooms
   * inputs, which breaks the 60-second registration budget on a phone. */
  --sp-line-height: 1.6;
  --sp-line-height-heading: 1.2;
  --sp-font-size-body: 1rem;
  --sp-font-size-meta: 0.8125rem;
  /* 12px since DS4-h (was 0.65625rem, 10.5px): the section label was the one
   * staff text below 12px. The participant keeps 10.5px (blocks 2 and 8). */
  --sp-font-size-eyebrow: 0.75rem;
  --sp-font-size-lede: 1.125rem;
  --sp-font-size-heading: 1.75rem;
  --sp-font-size-wordmark: 1.0625rem;
  /* DATA TYPOGRAPHY — Phase09 09.7; design-system.md §2.5. Asserted, with the
   * reasoning, in `test/data-typography.test.ts`.
   *
   * The stat numeral is larger than a page heading, because on an analytics
   * surface the number IS the heading, and it carries its OWN weight token
   * rather than borrowing --sp-font-weight-heading: a later revision that
   * lightens headings must not silently lighten every number in the product.
   * The caption — as-of stamp, denominator, chart annotation — is set LARGER
   * than --sp-font-size-meta because it is the apparatus that makes a number
   * honest, and deliberately below both WCAG large-text thresholds (24px, or
   * 18.66px bold) so SC 1.4.3's 4.5:1 binds it and no rule can argue a caption
   * onto a 3:1 non-text pair. */
  --sp-font-size-stat: 2.25rem;
  --sp-font-size-caption: 0.875rem;
  --sp-font-weight-stat: 800;
  --sp-font-weight-heading: 800;
  --sp-font-weight-eyebrow: 700;
  --sp-letter-spacing-eyebrow: 0.06em;
  --sp-letter-spacing-wordmark: -0.005em;
  --sp-measure: 40rem;
  /* ~62 characters of Dutch at --sp-font-size-body. --sp-measure is the ADMIN
   * measure and is nearly 90; a participant reading a consent purpose needs
   * the shorter line, and the difference is a token rather than a magic value
   * in one screen's stylesheet. */
  --sp-measure-prose: 34rem;
  --sp-font-admin: "Figtree", "Noto Sans Arabic", "Noto Sans Ethiopic", system-ui, sans-serif;
  --sp-font-participant: "Nunito", "Noto Sans Arabic", "Noto Sans Ethiopic", system-ui, sans-serif;

  /* Motion — SP2-ADR-012 (design/system-design.md §18), design-system.md §2.7.
   * Three durations and two curves are the WHOLE vocabulary; nothing above
   * 200ms, because motion here is confirmation, never spectacle. Theme-
   * independent: no dark block restates these (asserted by the structure
   * test), and block 9 at the end of this file zeroes the durations under
   * prefers-reduced-motion so every future animation inherits the kill switch.
   *
   * --sp-ease-standard is `ease` DELIBERATELY, not a custom cubic-bezier: the
   * primitives' previously hardcoded `120ms ease` transitions were migrated to
   * these tokens under a byte-equivalent-rendering contract (Phase09 09.1 —
   * the participant surface gains no new motion), and `ease` is the only value
   * that keeps that contract true. A nicer curve would be a visible change to
   * the participant surface and takes its own, later decision.
   *
   * --sp-ease-exit accelerates (Bézier easing that starts slow and leaves
   * fast) where `ease` decelerates: a control ENTERING the screen should land
   * gently where attention can follow it, but a control LEAVING must clear the
   * screen without holding attention on something already dismissed — so exits
   * read sharper and effectively shorter than entries at the same duration. */
  --sp-duration-instant: 80ms;
  --sp-duration-fast: 120ms;
  --sp-duration-base: 200ms;
  --sp-ease-standard: ease;
  --sp-ease-exit: cubic-bezier(0.4, 0, 1, 1);
  /* Carbon's productive entrance (design-system-v4 §9): STAFF OVERLAYS ONLY —
   * a menu, popover, dialog or drawer arriving. Decelerates harder than `ease`
   * so the overlay settles where the eye already is. Never read by a
   * participant-served sheet (the fence), so the participant gains no motion. */
  --sp-ease-entrance: cubic-bezier(0, 0, 0.38, 0.9);

  /* ======================================================================
   * DESIGN SYSTEM v4 FOUNDATIONS — SP2-ADR-020 (ADR-DS4), OD-36 RULED
   * 2026-10-03. STAFF ONLY: no participant-served rule reads any key below
   * (`test/participant-token-fence.test.ts`). Ratios are re-measured by the
   * contrast gate in six palettes on every run; the comments only say where
   * each value is used.
   * ==================================================================== */

  /* DS4-a — neutral role slots (design-system-v4 §3.1). Hover and selected
   * INCREASE contrast against rest; selected is never carried by ground
   * alone (a 3px --sp-primary bar and aria-selected carry it too). */
  --sp-surface-raised: #FFFFFF;         /* sticky bars, bulk bar, peek, toast */
  --sp-surface-overlay: #FFFFFF;        /* menu, popover, palette, dialog */
  --sp-surface-hover: #F2F4F7;          /* row and item hover */
  --sp-surface-selected: #EEF0FF;       /* selected row/item, current rail item */
  --sp-surface-pressed: #E4E7F5;        /* pressed row/item */
  --sp-border-strong: #4B5466;          /* hover boundary; the HC border */

  /* DS4-b — status solids and the info pair (design-system-v4 §3.3). The
   * solid is the 8px dot or the state glyph; the word is the signal. Info's
   * solid is --sp-on-info-surface, never --sp-primary (the colour diet). The
   * protected state has NO hue and therefore no token here. */
  --sp-success: #067647;
  --sp-warning: #B54708;
  --sp-info-surface: #EEF0FF;
  --sp-on-info-surface: #3730A3;

  /* The 1px inner halo inside the 3px ring (design-system-v4 §6): it keeps
   * the ring visible where it sits on an indigo face. An alias of the card
   * ground, so it follows the palette without a value of its own.
   *
   * EVERY ALIAS HERE (this one and the four viz roles below) IS RESTATED,
   * unchanged, in the dark blocks 3-5 and the light island 7, exactly as
   * --sp-progress-fill is. A custom property inherits its COMPUTED value and
   * its var() is substituted where it is declared, so an alias born only on
   * :root would reach a [data-theme] island (the gallery quadrants, the phone
   * preview) already resolved against the ROOT palette. Restated, it
   * re-resolves inside the island (VER-DS-1, W1; `ds4-foundations`). */
  --sp-focus-halo: var(--sp-surface);

  /* DS4-e — viz role ALIASES, no new hue (design-system-v4 §3.5). The grid
   * is horizontal gridlines only and inherits --sp-rule's decorative status
   * (and its high-contrast promotion to --sp-border, block 11). */
  --sp-viz-grid: var(--sp-rule);
  --sp-viz-axis: var(--sp-ink-muted);
  --sp-viz-context: var(--sp-border);
  --sp-viz-highlight: var(--sp-focus);

  /* DS4-f — the code face, STAFF ONLY. JetBrains Mono (SIL OFL 1.1) is
   * vendored under `fonts/staff/` and declared in `staff-foundations.css`,
   * a sheet the participant is never served; until a staff page loads that
   * sheet the stack falls through to the system monospace it replaces. It
   * separates 0/O and 1/l/I, which matters for a code read aloud. */
  --sp-font-code: "JetBrains Mono", "JetBrains Mono Fallback", ui-monospace, "SFMono-Regular", "Menlo", monospace;

  /* DS4-g — Arabic-script line height (ar, fa, ckb), design-system-v4 §4.1. */
  --sp-line-height-script: 1.8;

  /* DS4-h — the page-structure type roles (design-system-v4 §4.2). Display is
   * the page H1 (30/38, 800, -0.01em); title is a section, dialog or report
   * tile heading (20/28, 700). Neither is a FINDING/CONTEXT/PROVENANCE tier
   * and neither ever carries `data-weight`. */
  --sp-font-size-display: 1.875rem;
  --sp-line-height-display: 2.375rem;
  --sp-letter-spacing-display: -0.01em;
  --sp-font-size-title: 1.25rem;
  --sp-line-height-title: 1.75rem;
  --sp-font-weight-title: 700;

  /* DS4-i — the 4px soft grid's two added steps (design-system-v4 §5); the
   * compact block re-scopes both. */
  --sp-space-snug: 0.75rem;             /* 12px: field gap, cell padding, toolbar gap */
  --sp-space-card: 1.5rem;              /* 24px: card padding, header-to-content gap */

  /* DS4-j — materials, scrim, z-index, radius 6 (design-system-v4 §6). A
   * box-shadow reads one of these three or says none (`shadow-token-only`).
   * Flat (cards, tables, tiles) is the absence of a shadow, not a token. In
   * dark, separation is the surface step plus a 1px line; the shadows only
   * re-resolve --sp-shadow and are never relied on. The scrim is decorative
   * alpha, like --sp-shadow, and so is outside the contrast matrix. Overlays
   * live in the top layer; only sticky things take a z-index. */
  --sp-shadow-raised: 0 1px 2px var(--sp-shadow), 0 1px 3px var(--sp-shadow);
  --sp-shadow-overlay: 0 4px 6px -2px var(--sp-shadow), 0 12px 16px -4px var(--sp-shadow-strong);
  --sp-shadow-modal: 0 20px 24px -4px var(--sp-shadow-strong), 0 8px 8px -4px var(--sp-shadow);
  --sp-scrim: #10182859;
  --sp-z-sticky: 10;
  --sp-z-rail: 20;
  --sp-z-toast: 60;
  --sp-radius-small: 6px;               /* keycaps, cell chips, tooltips, inner elements */

  /* Skeleton policy (design-system-v4 §9, §11.11): a skeleton appears only
   * after the delay and, once shown, stays at least the minimum, so a fast
   * answer never flashes a placeholder. POLICY, not motion: not a
   * --sp-duration-*, not under the 200ms ceiling, and not zeroed by the
   * reduced-motion kill switch. */
  --sp-skeleton-delay: 300ms;
  --sp-skeleton-min-visible: 400ms;
}

/* -------------------------------------------- 2. PARTICIPANT GROUND (light) */
/* The participant surface sits on a darker ground. It used to re-tint
 * --sp-ink-muted and --sp-border because the base values dropped below their
 * floors here (#6E6C8A -> 4.19:1, #8B89A6 -> 2.81:1); palette v2's base values
 * cleared this ground on their own (#65637F -> 4.79:1, #817F9A -> 3.22:1).
 *
 * PALETTE V3 (session 060, SP2-ADR-019) MOVED THE STAFF PALETTE AND NOT THIS
 * ONE. Every block-1 token v3 changed is restated below at its pre-060 value,
 * so the participant resolves exactly what it resolved before; block 8 carries
 * the identical body (structure test) and the pin test holds every resolved
 * value against a pre-060 snapshot. */
[data-surface="participant"] {
  --sp-page: #E9EAF0;
  /* DS4-h moved the staff eyebrow to 12px. The participant's section labels
   * (`.sp-eyebrow` on the preference pages) keep their 10.5px until the
   * participant design track rules otherwise (ADR-PARTICIPANT-TRACK, OD-47); pinned here, read back by
   * `test/ds4-foundations.test.ts`. */
  --sp-font-size-eyebrow: 0.65625rem;
  /* Palette v3 (session 060, SP2-ADR-019) repainted the STAFF chrome only.
   * Every token the participant used to inherit from block 1 and that v3
   * moved is pinned here at its pre-060 value, so the participant renders
   * byte-identically; `test/participant-palette-pin.test.ts` holds it. */
  --sp-surface-sunken: #E4E6EF;
  --sp-ink: #211F49;
  --sp-ink-muted: #65637F;
  --sp-primary: #2D2A5E;
  --sp-primary-strong: #211F49;
  --sp-accent: #00E5A0;
  --sp-on-accent: #123B2E;
  --sp-progress-fill: var(--sp-primary-strong);
  --sp-focus: #2D2A5E;
  --sp-border: #817F9A;
  --sp-rule: #E7E8F0;
  --sp-shadow: #211F4914;
  --sp-shadow-strong: #211F4926;
  --sp-sidebar: #211F49;
  --sp-sidebar-raised: #2D2A5E;
  --sp-on-sidebar: #FFFFFF;
  --sp-on-sidebar-muted: #B9B8CC;
}

/* ------------------------------------------------- 3. DARK, system-preferred */
/* :not([data-theme="light"]) so an explicit light choice still wins here. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --sp-page: #0F1117;
    --sp-surface: #161A22;
    --sp-surface-sunken: #0B0D12;
    --sp-ink: #E7E9EE;
    --sp-ink-muted: #A3AAB8;
    --sp-primary: #8B90FF;
    --sp-primary-strong: #A5A9FF;
    --sp-on-primary: #0B0D12;
    --sp-accent: #B4BEFE;
    --sp-on-accent: #0B0D12;
    --sp-progress-fill: var(--sp-accent);
    --sp-error: #F2888D;
    --sp-error-surface: #3A1B1F;
    --sp-success-surface: #0E3328;
    --sp-on-success-surface: #6FE3B8;
    --sp-warning-surface: #3A2C0D;
    --sp-on-warning-surface: #F2C767;
    --sp-focus: #A5B4FC;
    --sp-border: #5F6678;
    --sp-rule: #232838;
    --sp-shadow: #00000059;
    --sp-shadow-strong: #0000008C;
    --sp-sidebar: #161A22;
    --sp-sidebar-raised: #22264A;
    --sp-on-sidebar: #E7E9EE;
    --sp-on-sidebar-muted: #A3AAB8;
    --sp-viz-indigo: #8B88E8;
    --sp-viz-mint: #00E5A0;
    --sp-viz-amber: #F2B33D;
    --sp-viz-magenta: #E88BBB;
    --sp-viz-cyan: #5ECFD3;
    --sp-viz-olive: #B9C15A;
    --sp-viz-seq-1: #696B8A;
    --sp-viz-seq-2: #7D86D2;
    --sp-viz-seq-3: #ACB3FF;
    --sp-viz-seq-4: #DFDEFF;
    --sp-viz-div-neg-2: #DFD7FF;
    --sp-viz-div-neg-1: #817DD1;
    --sp-viz-div-pos-1: #B47B24;
    --sp-viz-div-pos-2: #FFD574;
    --sp-surface-raised: #1C212B;
    --sp-surface-overlay: #222836;
    --sp-surface-hover: #1C2030;
    --sp-surface-selected: #22264A;
    --sp-surface-pressed: #2A2F45;
    --sp-border-strong: #8A92A6;
    --sp-success: #47CD89;
    --sp-warning: #F2B33D;
    --sp-info-surface: #22264A;
    --sp-on-info-surface: #C7CBFF;
    --sp-scrim: #000000A6;
    --sp-focus-halo: var(--sp-surface);
    --sp-viz-grid: var(--sp-rule);
    --sp-viz-axis: var(--sp-ink-muted);
    --sp-viz-context: var(--sp-border);
    --sp-viz-highlight: var(--sp-focus);
  }
}

/* ------------------------------------------------ 4. DARK, explicit at :root */
:root[data-theme="dark"] {
  color-scheme: dark;
  --sp-page: #0F1117;
  --sp-surface: #161A22;
  --sp-surface-sunken: #0B0D12;
  --sp-ink: #E7E9EE;
  --sp-ink-muted: #A3AAB8;
  --sp-primary: #8B90FF;
  --sp-primary-strong: #A5A9FF;
  --sp-on-primary: #0B0D12;
  --sp-accent: #B4BEFE;
  --sp-on-accent: #0B0D12;
  --sp-progress-fill: var(--sp-accent);
  --sp-error: #F2888D;
  --sp-error-surface: #3A1B1F;          /* derived — --sp-error 6.40:1, --sp-ink 13.11:1 */
  --sp-success-surface: #0E3328;        /* derived — --sp-ink 11.68:1 */
  --sp-on-success-surface: #6FE3B8;     /* derived — 8.77:1 on --sp-success-surface */
  --sp-warning-surface: #3A2C0D;        /* derived — --sp-ink 11.50:1 */
  --sp-on-warning-surface: #F2C767;     /* derived — 8.50:1 on --sp-warning-surface */
  --sp-focus: #A5B4FC;
  --sp-border: #5F6678;
  --sp-rule: #232838;
  --sp-shadow: #00000059;                /* derived — decorative; depth in dark comes
                                          * from --sp-surface sitting ABOVE --sp-page,
                                          * not from the shadow. */
  --sp-shadow-strong: #0000008C;
  --sp-sidebar: #161A22;
  --sp-sidebar-raised: #22264A;
  --sp-on-sidebar: #E7E9EE;
  --sp-on-sidebar-muted: #A3AAB8;
  --sp-viz-indigo: #8B88E8;
  --sp-viz-mint: #00E5A0;
  --sp-viz-amber: #F2B33D;
  --sp-viz-magenta: #E88BBB;
  --sp-viz-cyan: #5ECFD3;
  --sp-viz-olive: #B9C15A;
  --sp-viz-seq-1: #696B8A;
  --sp-viz-seq-2: #7D86D2;
  --sp-viz-seq-3: #ACB3FF;
  --sp-viz-seq-4: #DFDEFF;
  --sp-viz-div-neg-2: #DFD7FF;
  --sp-viz-div-neg-1: #817DD1;
  --sp-viz-div-pos-1: #B47B24;
  --sp-viz-div-pos-2: #FFD574;
  --sp-surface-raised: #1C212B;
  --sp-surface-overlay: #222836;
  --sp-surface-hover: #1C2030;
  --sp-surface-selected: #22264A;
  --sp-surface-pressed: #2A2F45;
  --sp-border-strong: #8A92A6;
  --sp-success: #47CD89;
  --sp-warning: #F2B33D;
  --sp-info-surface: #22264A;
  --sp-on-info-surface: #C7CBFF;
  --sp-scrim: #000000A6;
  --sp-focus-halo: var(--sp-surface);
  --sp-viz-grid: var(--sp-rule);
  --sp-viz-axis: var(--sp-ink-muted);
  --sp-viz-context: var(--sp-border);
  --sp-viz-highlight: var(--sp-focus);
}

/* ---------------------------------------------- 5. DARK, explicit on subtree */
/* A dark island inside a light page: phone previews, the gallery matrix, and
 * any embedded dark card. :root cannot cover these. */
[data-theme="dark"] {
  color-scheme: dark;
  --sp-page: #0F1117;
  --sp-surface: #161A22;
  --sp-surface-sunken: #0B0D12;
  --sp-ink: #E7E9EE;
  --sp-ink-muted: #A3AAB8;
  --sp-primary: #8B90FF;
  --sp-primary-strong: #A5A9FF;
  --sp-on-primary: #0B0D12;
  --sp-accent: #B4BEFE;
  --sp-on-accent: #0B0D12;
  --sp-progress-fill: var(--sp-accent);
  --sp-error: #F2888D;
  --sp-error-surface: #3A1B1F;
  --sp-success-surface: #0E3328;
  --sp-on-success-surface: #6FE3B8;
  --sp-warning-surface: #3A2C0D;
  --sp-on-warning-surface: #F2C767;
  --sp-focus: #A5B4FC;
  --sp-border: #5F6678;
  --sp-rule: #232838;
  --sp-shadow: #00000059;
  --sp-shadow-strong: #0000008C;
  --sp-sidebar: #161A22;
  --sp-sidebar-raised: #22264A;
  --sp-on-sidebar: #E7E9EE;
  --sp-on-sidebar-muted: #A3AAB8;
  --sp-viz-indigo: #8B88E8;
  --sp-viz-mint: #00E5A0;
  --sp-viz-amber: #F2B33D;
  --sp-viz-magenta: #E88BBB;
  --sp-viz-cyan: #5ECFD3;
  --sp-viz-olive: #B9C15A;
  --sp-viz-seq-1: #696B8A;
  --sp-viz-seq-2: #7D86D2;
  --sp-viz-seq-3: #ACB3FF;
  --sp-viz-seq-4: #DFDEFF;
  --sp-viz-div-neg-2: #DFD7FF;
  --sp-viz-div-neg-1: #817DD1;
  --sp-viz-div-pos-1: #B47B24;
  --sp-viz-div-pos-2: #FFD574;
  --sp-surface-raised: #1C212B;
  --sp-surface-overlay: #222836;
  --sp-surface-hover: #1C2030;
  --sp-surface-selected: #22264A;
  --sp-surface-pressed: #2A2F45;
  --sp-border-strong: #8A92A6;
  --sp-success: #47CD89;
  --sp-warning: #F2B33D;
  --sp-info-surface: #22264A;
  --sp-on-info-surface: #C7CBFF;
  --sp-scrim: #000000A6;
  --sp-focus-halo: var(--sp-surface);
  --sp-viz-grid: var(--sp-rule);
  --sp-viz-axis: var(--sp-ink-muted);
  --sp-viz-context: var(--sp-border);
  --sp-viz-highlight: var(--sp-focus);
}

/* --------------------------------------------- 6. PARTICIPANT GROUND (dark) */
/* The participant's own darker ground, plus (palette v3, session 060) every
 * token the v3 staff dark blocks 3-5 moved, pinned at its pre-060 value. Both
 * forms below carry the same body; `test/participant-palette-pin.test.ts`
 * asserts that and the resolved values. */
@media (prefers-color-scheme: dark) {
  :root[data-surface="participant"]:not([data-theme="light"]) {
    --sp-page: #0E0D1E;
    /* Palette v3 (session 060, SP2-ADR-019): the dark staff values moved; the
     * participant's did not. Pinned at their pre-060 values. */
    --sp-surface: #1C1B36;
    --sp-surface-sunken: #100F1F;
    --sp-ink: #ECEBF6;
    --sp-ink-muted: #A7A5C4;
    --sp-primary: #A9A6E8;
    --sp-primary-strong: #C6C4F2;
    --sp-on-primary: #131226;
    --sp-accent: #00E5A0;
    --sp-on-accent: #0B2A20;
    --sp-focus: #B8FFE4;
    --sp-border: #6A698A;
    --sp-rule: #2B2A4B;
    --sp-sidebar: #0E0D1E;
    --sp-sidebar-raised: #1C1B36;
    --sp-on-sidebar: #ECEBF6;
    --sp-on-sidebar-muted: #A7A5C4;
  }
}

[data-surface="participant"][data-theme="dark"] {
  --sp-page: #0E0D1E;                   /* derived — --sp-ink 16.24:1, --sp-ink-muted 8.06:1 */
  /* Palette v3 (session 060, SP2-ADR-019): the dark staff values moved; the
   * participant's did not. Pinned at their pre-060 values. */
  --sp-surface: #1C1B36;
  --sp-surface-sunken: #100F1F;
  --sp-ink: #ECEBF6;
  --sp-ink-muted: #A7A5C4;
  --sp-primary: #A9A6E8;
  --sp-primary-strong: #C6C4F2;
  --sp-on-primary: #131226;
  --sp-accent: #00E5A0;
  --sp-on-accent: #0B2A20;
  --sp-focus: #B8FFE4;
  --sp-border: #6A698A;
  --sp-rule: #2B2A4B;
  --sp-sidebar: #0E0D1E;
  --sp-sidebar-raised: #1C1B36;
  --sp-on-sidebar: #ECEBF6;
  --sp-on-sidebar-muted: #A7A5C4;
}

/* -------------------------------------------------- 7. LIGHT ISLAND IN DARK */
/* Same key set as the dark blocks, values identical to :root. Without this a
 * data-theme="light" subtree keeps whatever the root resolved to. */
[data-theme="light"] {
  color-scheme: light;
  --sp-page: #F6F7F9;
  --sp-surface: #FFFFFF;
  --sp-surface-sunken: #EEF0F3;
  --sp-ink: #101828;
  --sp-ink-muted: #5A6477;
  --sp-primary: #4F46E5;
  --sp-primary-strong: #4338CA;
  --sp-on-primary: #FFFFFF;
  --sp-accent: #4F46E5;
  --sp-on-accent: #FFFFFF;
  --sp-progress-fill: var(--sp-ink);
  --sp-error: #B4373D;
  --sp-error-surface: #FBE7E7;
  --sp-success-surface: #DCFBEF;
  --sp-on-success-surface: #056647;
  --sp-warning-surface: #FFF1CC;
  --sp-on-warning-surface: #8A6410;
  --sp-focus: #4F46E5;
  --sp-border: #7E8799;
  --sp-rule: #E4E7EC;
  --sp-shadow: #10182814;
  --sp-shadow-strong: #10182826;
  --sp-sidebar: #FFFFFF;
  --sp-sidebar-raised: #EEF0FF;
  --sp-on-sidebar: #101828;
  --sp-on-sidebar-muted: #5A6477;
  --sp-viz-indigo: #5B57C7;
  --sp-viz-mint: #009466;
  --sp-viz-amber: #AB7810;
  --sp-viz-magenta: #B34A82;
  --sp-viz-cyan: #1A7F93;
  --sp-viz-olive: #6B7020;
  --sp-viz-seq-1: #83819D;
  --sp-viz-seq-2: #5B5DAE;
  --sp-viz-seq-3: #2A317C;
  --sp-viz-seq-4: #1D1B31;
  --sp-viz-div-neg-2: #141F67;
  --sp-viz-div-neg-1: #6C66C8;
  --sp-viz-div-pos-1: #996900;
  --sp-viz-div-pos-2: #3C2200;
  --sp-surface-raised: #FFFFFF;
  --sp-surface-overlay: #FFFFFF;
  --sp-surface-hover: #F2F4F7;
  --sp-surface-selected: #EEF0FF;
  --sp-surface-pressed: #E4E7F5;
  --sp-border-strong: #4B5466;
  --sp-success: #067647;
  --sp-warning: #B54708;
  --sp-info-surface: #EEF0FF;
  --sp-on-info-surface: #3730A3;
  --sp-scrim: #10182859;
  --sp-focus-halo: var(--sp-surface);
  --sp-viz-grid: var(--sp-rule);
  --sp-viz-axis: var(--sp-ink-muted);
  --sp-viz-context: var(--sp-border);
  --sp-viz-highlight: var(--sp-focus);
}

/* -------------------------------------- 8. PARTICIPANT LIGHT ISLAND IN DARK */
[data-surface="participant"][data-theme="light"] {
  --sp-page: #E9EAF0;
  /* DS4-h moved the staff eyebrow to 12px. The participant's section labels
   * (`.sp-eyebrow` on the preference pages) keep their 10.5px until the
   * participant design track rules otherwise (ADR-PARTICIPANT-TRACK, OD-47); pinned here, read back by
   * `test/ds4-foundations.test.ts`. */
  --sp-font-size-eyebrow: 0.65625rem;
  /* Palette v3 (session 060, SP2-ADR-019) repainted the STAFF chrome only.
   * Every token the participant used to inherit from block 1 and that v3
   * moved is pinned here at its pre-060 value, so the participant renders
   * byte-identically; `test/participant-palette-pin.test.ts` holds it. */
  --sp-surface-sunken: #E4E6EF;
  --sp-ink: #211F49;
  --sp-ink-muted: #65637F;
  --sp-primary: #2D2A5E;
  --sp-primary-strong: #211F49;
  --sp-accent: #00E5A0;
  --sp-on-accent: #123B2E;
  --sp-progress-fill: var(--sp-primary-strong);
  --sp-focus: #2D2A5E;
  --sp-border: #817F9A;
  --sp-rule: #E7E8F0;
  --sp-shadow: #211F4914;
  --sp-shadow-strong: #211F4926;
  --sp-sidebar: #211F49;
  --sp-sidebar-raised: #2D2A5E;
  --sp-on-sidebar: #FFFFFF;
  --sp-on-sidebar-muted: #B9B8CC;
}

/* --------------------------------- 9. REDUCED-MOTION DURATION KILL SWITCH */
/* This is the TOKEN-LEVEL enforcement of `prefers-reduced-motion` that every
 * animation and transition written against the motion tokens inherits, now and
 * in every future phase — a rule that cannot be forgotten per component
 * because no component carries its own duration (SP2-ADR-012 motion clause;
 * design-system.md §2.7 rule 4).
 *
 * 0.01ms rather than 0s: a zero duration makes some engines skip
 * transition/animation EVENTS entirely, and a listener waiting for
 * `transitionend` would then wait forever. 0.01ms is imperceptible and still
 * fires them.
 *
 * Durations only. The easing tokens are left alone — a curve over 0.01ms
 * resolves no motion, and re-declaring it here would add a key the structure
 * test would have to exempt for no behavioural gain. Component-level
 * `transition: none` blocks (primitives.css) stay as a second, older layer of
 * the same answer; the Playwright reduced-motion check measures the result in
 * a real browser. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --sp-duration-instant: 0.01ms;
    --sp-duration-fast: 0.01ms;
    --sp-duration-base: 0.01ms;
  }
}

/* ------------------------------ 9b. THE SAME KILL SWITCH, CHOSEN (STAFF) */
/* `sp_staff_motion=reduce` makes the staff root layout write
 * `data-motion="reduce"` (lib/prefs-cookie.ts, world-class W0 SH-00). This
 * block is what that attribute DOES: the same three durations, the same
 * 0.01ms, so a person who asks for less motion in the account menu gets
 * exactly what the OS setting gives (shell-home spec §2.2). Its body is
 * asserted identical to block 9's by `test/token-structure.test.ts`. No
 * participant page writes the attribute. */
:root[data-motion="reduce"] {
  --sp-duration-instant: 0.01ms;
  --sp-duration-fast: 0.01ms;
  --sp-duration-base: 0.01ms;
}

/* ------------------------------------------- 10. COMPACT DENSITY (ADMIN ONLY)
 * Phase09 09.6, SP2-ADR-012's density clause; design-system.md §2.4.
 *
 * THE DARK-CASCADE PATTERN, APPLIED TO SPACE. Comfortable is not a value here:
 * it is block 1, complete and unqualified, exactly as light is the complete
 * palette that the dark blocks only redefine. Absence of the attribute IS
 * comfortable, so the default renders byte-identically to a build in which this
 * block does not exist — which is the claim `test/token-structure.test.ts` and
 * the 09.6 render diff both check rather than assume.
 *
 * ADMIN SHELL ONLY, AND STRUCTURALLY SO. The attribute is written in exactly
 * one place — admin-web's root layout, from the `sp_staff_density` cookie — and
 * nothing under `packages/panel_participant_ui/` sets it or can: the
 * participant surface has no density control, no cookie and therefore no
 * selector match, so every participant token resolves to block 1 as before.
 * Density is staff chrome; a survey a participant is answering is not a
 * workspace somebody tunes.
 *
 * SPACING AND ROW RHYTHM ONLY — A CLOSED SET. No colour, no motion, no radius,
 * no type scale and above all NO --sp-touch-target: a density that could shrink
 * the target contract would be a density that can fail WCAG 2.2 SC 2.5.8, and
 * the whole point of this block is that it cannot. --sp-row-block-size is
 * restated at its comfortable value for the same reason a contract is written
 * down rather than implied — the structure test reads it back and fails if it
 * ever drops below --sp-touch-target, and the real-browser target-size sweep
 * measures the rendered result in compact mode.
 *
 * --sp-space-xsmall is deliberately NOT re-declared: at 0.25rem it is already
 * the smallest gap the system draws, and a compact mode that halved it would be
 * removing a separation rather than tightening one.
 */
[data-density="compact"] {
  --sp-space-small: 0.375rem;           /* 6px  (comfortable 8px) */
  --sp-space: 0.75rem;                  /* 12px (comfortable 16px) */
  --sp-space-large: 1.5rem;             /* 24px (comfortable 32px) */
  --sp-space-xlarge: 2rem;              /* 32px (comfortable 48px) */
  --sp-row-gap: 0.25rem;                /* 4px  (comfortable 8px) */
  --sp-row-block-size: 2.75rem;         /* 44px — UNCHANGED. The floor never moves. */
  --sp-space-snug: 0.5rem;              /* 8px  (comfortable 12px) — DS4-i */
  --sp-space-card: 1rem;                /* 16px (comfortable 24px) — DS4-i */
}

/* ------------------------------------- 11. HIGH CONTRAST (STAFF ONLY, DS4-d)
 * design-system-v4 §3.4: under `prefers-contrast: more`, or an explicit
 * `data-contrast="more"` on the staff root, every quiet line and hint is
 * promoted one step: the hint ink becomes the ink, the decorative rule
 * becomes a control boundary (so the chart grid, an alias of the rule, is
 * drawn in the border too), and the focus ring grows to 4px. It is Linear's
 * contrast input as a MEASURED block, never a user theme generator (branding
 * UI is §1.9-3).
 *
 * AN OVERLAY OF ALIASES, NOT A THIRD PALETTE. Each value names another token,
 * so ONE body is correct over light and over dark and the contrast gate
 * measures the result as two more palettes, `light-hc` and `dark-hc`. The
 * specificity (0,3,0) beats every dark block (0,2,0), so the aliases resolve
 * against whichever palette is in force.
 *
 * STAFF ONLY, STRUCTURALLY. `:not([data-surface])` excludes the participant
 * root (`data-surface="participant"`, document.py), so a participant whose OS
 * asks for more contrast keeps the pinned participant palette until the
 * participant design track rules otherwise (ADR-PARTICIPANT-TRACK, OD-47). `data-contrast="standard"`
 * opts a staff root out of the OS preference. Nothing writes `data-contrast`
 * yet: the account-menu choice is the shell's (design-system-v4 §17 question
 * 5 is still open), so today the OS preference is the only way in.
 *
 * NOT DONE, ON PURPOSE: "border -> border-strong". Measured (DS-1,
 * 2026-10-03), it drops the progress pair ("progress value against its
 * track", lib/pairs.ts) to 2.33:1 in light-hc and 1.74:1 in dark-hc, because
 * the track IS --sp-border; the gate admits no waiver, so the border keeps its
 * own value here and --sp-border-strong stays a role a component asks for by
 * name. Aliasing the rule to the border is therefore exactly the "rule ->
 * border" design-system-v4 §3.4 names. */
@media (prefers-contrast: more) {
  :root:not([data-surface]):not([data-contrast="standard"]) {
    --sp-ink-muted: var(--sp-ink);
    --sp-on-sidebar-muted: var(--sp-on-sidebar);
    --sp-rule: var(--sp-border);
    --sp-focus-ring-width: 4px;
  }
}

:root:not([data-surface])[data-contrast="more"] {
  --sp-ink-muted: var(--sp-ink);
  --sp-on-sidebar-muted: var(--sp-on-sidebar);
  --sp-rule: var(--sp-border);
  --sp-focus-ring-width: 4px;
}

/* Base primitives — design-system.md §7 group A/C seed (Phase00.5).
 *
 * Tokens only: no colour literal, no length that a token already names. Only
 * logical properties, which is the whole RTL strategy — `test/logical-
 * properties.test.ts` fails the build on the first physical one.
 *
 * These carry no strings and import no localisation; every label in the
 * gallery is passed in by the caller (design-system.md §7 package rules).
 */

/* Border-box for everything inside a surface, and the reason is a measurement.
 *
 * `.sp-input` is `inline-size: 100%` with a token inline padding and a 1px
 * border. Under the default content-box that is 100% PLUS 18px, which at a
 * 320px viewport pushed the registration form 10px past the edge and gave the
 * Arabic page a horizontal scrollbar — found by the 320px reflow assertion in
 * e2e/registration-journey.spec.ts, not by reading the stylesheet.
 *
 * Scoped to the surface rather than declared on `*`, because this package is
 * embedded in pages it does not own and a global reset would reach them. */
.sp-root,
.sp-root *,
.sp-root *::before,
.sp-root *::after {
  box-sizing: border-box;
}

.sp-root {
  background-color: var(--sp-page);
  color: var(--sp-ink);
  font-family: var(--sp-font-admin);
  font-size: var(--sp-font-size-body);
  line-height: var(--sp-line-height);
  /* iOS inflates text in a rotated portrait column unless told not to. The
   * 16px input floor is pointless if the platform is rescaling underneath it. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

/* THE PARTICIPANT SURFACE IS DECLARED ON THE ROOT ELEMENT, NOT ON <body>, AND
 * THAT IS A CORRECTNESS REQUIREMENT RATHER THAN A PREFERENCE.
 *
 * tokens.css blocks 2-8 resolve by specificity on ONE element: block 6
 * (`:root[data-surface="participant"]`) is written to beat blocks 3-5 at
 * (0,2,0). Put `data-surface` on <body> and blocks 3-6 stop matching the same
 * element as block 2: <body> then keeps the LIGHT participant ground from
 * block 2 while inheriting the DARK --sp-ink from <html>, and the participant
 * journey renders the dark --sp-ink on the LIGHT participant --sp-page: the
 * two are within a twentieth of a stop of each other, which measures 1.06:1
 * and reads as a blank screen with a heading you can only find by selecting
 * it. That was the shipped state, on every participant screen, for every phone
 * whose system theme is dark. It was invisible to
 * every gate because lib/palettes.ts models the four palettes as one element's
 * cascade, which is exactly what this selector now makes true at runtime. */
.sp-root[data-surface="participant"] {
  font-family: var(--sp-font-participant);
  /* A 52-char opaque token once overflowed a 360px viewport by 36px. */
  overflow-wrap: anywhere;
  /* The gutter is a token, not the user agent's 8px body margin. The UA value
   * is applied as a physical margin, which on an RTL page with any overflow
   * shifts the whole document — the same 320px measurement that found the
   * border-box defect above. */
  margin: 0;
  padding: 0;
}

/* Arabic and Farsi need more optical size than Latin at the same nominal px:
 * Noto Sans Arabic's x-height-equivalent is smaller and its marks sit closer,
 * so 16px Nunito and 16px Noto Arabic are not the same reading experience.
 * The bump is applied to the TOKENS rather than to a list of selectors, so
 * every rule that reads --sp-font-size-* gets it for free — including the one
 * Arabic option on an otherwise Dutch language screen, which carries its own
 * `lang` and therefore its own bump. */
.sp-root[data-surface="participant"] :lang(ar),
.sp-root[data-surface="participant"] :lang(fa) {
  --sp-font-size-body: 1.0625rem;
  --sp-font-size-lede: 1.1875rem;
  --sp-font-size-heading: 1.9375rem;
  --sp-font-size-meta: 0.875rem;
  --sp-line-height: 1.85;
  --sp-line-height-heading: 1.45;
}

/* ==========================================================================
 * THE PARTICIPANT PAGE SHELL
 *
 * Six screens on a phone, no JavaScript, four scripts, two palettes. The shell
 * gives all of them the same shape so that a participant who has seen the
 * language screen recognises the consent screen: a machine strip at the very
 * top, the panel's mark, one column on a reading measure, and generous room
 * underneath so the last control is never flush with the bottom edge.
 * ========================================================================== */

.sp-participant {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space-large);
  min-block-size: 100vb;
  min-block-size: 100dvb;
  margin: 0;
  padding: 0;
  padding-block-end: var(--sp-space-xlarge);
  /* Body restates both, so the :lang() token bump above actually reaches the
   * text: `.sp-root` resolved them against the ROOT's token values and the
   * computed result would otherwise simply inherit. */
  font-size: var(--sp-font-size-body);
  line-height: var(--sp-line-height);
}

/* The one column every screen lives in. ~62 characters, centred, with a
 * gutter that survives 320px. */
.sp-shell {
  inline-size: 100%;
  max-inline-size: var(--sp-measure-prose);
  margin-inline: auto;
  padding-inline: var(--sp-space);
}

/* THE SYNTHETIC-DATA STRIP. An honesty control, not decoration (CLAUDE.md
 * invariant 4): every screenshot of this product must be impossible to mistake
 * for one of a live panel. It is therefore full-bleed rather than inset and
 * sits above the panel's own mark rather than below it.
 *
 * THERE IS NO CSS MODIFIER THAT QUIETS IT, AND THERE IS NOW ONE SERVER-SIDE
 * SETTING THAT DOES. This comment used to read "has no modifier that quiets
 * it", full stop. That sentence stopped being true in this file's own terms and
 * is corrected rather than left to contradict the code: `PANEL_PRESENTATION_MODE`
 * (OD-22 / SP2-ADR-010, design-system.md §5.2) is a single, named, DEFAULT-OFF
 * environment variable that a demonstration host may set. When it is on,
 * `panel_participant_ui.document.page()` emits this element with its
 * `data-synthetic-banner` attribute intact and its text absent; when it is
 * unset — development, CI, and any real deployment — the strip renders exactly
 * as it always has. There is still no CLASS a caller can add to quiet it, and
 * no modifier is added below: the decision is made once, server-side, by a host
 * that opted in, never per page and never in the browser.
 *
 * THE OD-02 CONSENT BANNER DOES NOT SHARE THIS. It stays unsuppressible in
 * every mode (CLAUDE.md invariant 5, brief §1.9 item 9): a consent screen that
 * stops saying its wording is unratified is a consent screen that looks like it
 * collected consent it did not collect. The two controls used to be governed by
 * one sentence here; they are two rules now, and only the weaker one moved.
 *
 * It is set in the ADMIN face at machine tracking while everything below it is
 * participant prose in Nunito. That typographic distance is the design: the
 * strip is the platform talking about itself, not the panel talking to a
 * participant, and it must never be read as part of the journey. */
.sp-synthetic {
  /* THERE IS NO ICON AND THAT IS THE SECOND ATTEMPT, NOT THE FIRST IDEA. A
   * 6px dot beside the label looked deliberate at 360px and looked like a
   * rendering fault at 320px in Arabic: the label wraps there, and a neutral
   * inline object next to a Latin run inside a right-to-left line reorders
   * away from the words it belongs to. The strip is already unmistakable
   * without it — full-bleed, above the panel's own mark, warning ground, a
   * 2px rule under it and the admin face at machine tracking — so the part
   * that could not be made to behave in four scripts was removed rather than
   * pinned down with a direction-specific rule. */
  display: block;
  margin: 0;
  padding-block: var(--sp-space-small);
  padding-inline: var(--sp-space);
  background-color: var(--sp-warning-surface);
  color: var(--sp-on-warning-surface);
  border-block-end: 2px solid var(--sp-on-warning-surface);
  font-family: var(--sp-font-admin);
  font-size: var(--sp-font-size-meta);
  font-weight: var(--sp-font-weight-eyebrow);
  letter-spacing: var(--sp-letter-spacing-eyebrow);
  line-height: var(--sp-line-height-heading);
  text-align: center;
}

/* ------------------------------------------------------------- the wordmark */
/* Drawn with text and one CSS ring, not with a raster logo: OpenEmbassy has
 * approved no mark for this panel, and inventing one in an image file is the
 * visual equivalent of inventing copy. The only string in it is the client's
 * own legal name, which the delivered copy already carries UNTRANSLATED in all
 * four locales (`journey.*.title`), so nothing here is authored content and
 * nothing here needs a translation cell.
 *
 * RTL: the lockup takes the page direction, so in Arabic and Farsi the ring
 * leads from the right. The ring is a circle and mirrors to itself; the name
 * is a Latin run and the bidi algorithm keeps it left-to-right inside the
 * right-to-left line, which is how a Latin brand name is set in Arabic
 * typography anyway. No rule in this block names a side. */
.sp-masthead {
  display: flex;
  align-items: center;
  gap: var(--sp-space);
  inline-size: 100%;
  max-inline-size: var(--sp-measure-prose);
  margin-inline: auto;
  padding-inline: var(--sp-space);
}

.sp-wordmark {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-space-small);
  margin: 0;
  color: var(--sp-ink);
  font-family: var(--sp-font-participant);
  font-size: var(--sp-font-size-wordmark);
  font-weight: var(--sp-font-weight-heading);
  letter-spacing: var(--sp-letter-spacing-wordmark);
  line-height: var(--sp-line-height-heading);
  text-align: start;
}

/* An open ring, because a filled dot reads as a bullet and a bullet reads as a
 * list item. The hole is punched with an inset shadow in the PAGE colour, so
 * it stays a ring in both palettes without a second token. */
.sp-wordmark__ring {
  flex: none;
  inline-size: 1.125rem;
  block-size: 1.125rem;
  /* A stroked ring, not a filled disc with a hole punched in it: an inset
   * shadow in the page colour only looks hollow while the mark sits on the
   * page, and the first thing anybody does with a mark is put it somewhere
   * else. A border is hollow everywhere. */
  border: 0.1875rem solid var(--sp-primary);
  border-radius: var(--sp-radius-pill);
}

/* --------------------------------------------------------------- focus ring */
/* :focus-visible only — a mouse click on a button must not paint a ring, but a
 * keyboard-only journey must never lose its position. */
.sp-button:focus-visible,
.sp-input:focus-visible,
.sp-choice__control:focus-visible,
.sp-check:focus-visible,
.sp-choice:focus-within {
  outline: var(--sp-focus-ring-width) solid var(--sp-focus);
  outline-offset: var(--sp-focus-ring-offset);
}

/* -------------------------------------------------------------------- button */
/* THE CONTROL FACE, RESTATED FOR A TOOL A RESEARCH TEAM PAYS FOR (session 058,
 * the skin pass — the owner's "neyle başladıysak hâlâ onu görüyorum").
 *
 * ONE RADIUS FOR EVERY CONTROL. `.sp-input`, `.sp-choice` and
 * `.sp-locale-option` already take --sp-radius; the button was the one control
 * still drawn as a pill, and a pill beside a 10px-radius field reads as two
 * products on one form. The journey CTA inherits the same radius: on every
 * participant screen it sits directly under a --sp-radius control, and the
 * two now agree.
 *
 * Height is the 44px contract in both densities (--sp-touch-target is never
 * re-scoped by the compact block); the inline padding IS density-aware, so a
 * compact toolbar's buttons tighten with the controls beside them. The
 * heading line-height keeps a two-line label from opening the box up. */
.sp-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-space-small);
  min-block-size: var(--sp-touch-target);
  min-inline-size: var(--sp-touch-target);
  padding-block: var(--sp-space-small);
  padding-inline: var(--sp-space);
  border: 1px solid transparent;
  border-radius: var(--sp-radius);
  font: inherit;
  font-weight: 600;
  line-height: var(--sp-line-height-heading);
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  /* Only properties that carry no meaning of their own: a state change a
   * participant can see, not motion they have to wait for. 120ms is under the
   * threshold at which a tap starts to feel laggy.
   *
   * Tokenised by Phase09 09.1 (SP2-ADR-012), value-matched to the previous
   * hardcoded `120ms ease`: --sp-duration-fast IS 120ms and --sp-ease-standard
   * IS `ease`, so the computed transition is byte-identical to what shipped —
   * the participant surface gains no new motion, only a name for the motion it
   * already had. What it does gain is the token-level reduced-motion kill
   * switch (tokens.css block 9), which the block at the end of this file
   * already answered locally with `transition: none`. */
  transition:
    background-color var(--sp-duration-fast) var(--sp-ease-standard),
    border-color var(--sp-duration-fast) var(--sp-ease-standard),
    color var(--sp-duration-fast) var(--sp-ease-standard),
    box-shadow var(--sp-duration-fast) var(--sp-ease-standard),
    translate var(--sp-duration-fast) var(--sp-ease-standard);
}

/* THE PRIMARY IS THE ONE ACCENTED CONTROL ON A SCREEN, and it earns its
 * weight with a filled face and a 1px rim in the pressed shade — not with a
 * resting shadow. A drop shadow under every primary button made each form read
 * as a stack of floating objects; the rim gives the face a crisp edge at rest,
 * and the shadow is kept for hover, where it says "this will move".
 *
 * THE RIM IS NOT THE CONTROL'S BOUNDARY, and `lib/grounds.ts` EXEMPT_USAGES
 * carries that claim where the usage gate can refuse it. What identifies the
 * control is the filled face: --sp-primary against the page, the card and the
 * well are registered text pairs at 4.5:1 or better, and the rim sits INSIDE
 * that silhouette — delete it and the button's extent is unchanged. The 4.5:1
 * that matters for the label is --sp-on-primary on --sp-primary, measured. */
.sp-button--primary {
  background-color: var(--sp-primary);
  color: var(--sp-on-primary);
  border-color: var(--sp-primary-strong);
}

/* Hover and active are a measured token, not a filter or an opacity: both of
 * those move the LABEL's contrast as well as the ground's, and a hovered
 * button that drops under 4.5:1 is a button a keyboard user sits on. */
.sp-button--primary:hover {
  background-color: var(--sp-primary-strong);
  box-shadow:
    0 1px 2px var(--sp-shadow),
    0 4px 12px var(--sp-shadow-strong);
}

.sp-button--primary:active {
  background-color: var(--sp-primary-strong);
  box-shadow: 0 1px 2px var(--sp-shadow);
  translate: 0 1px;
}

/* The quiet sibling: surface, ink, and the 3:1 control boundary. Hover sinks
 * the face one step and tightens the edge to the brand; nothing else moves. */
.sp-button--secondary {
  background-color: var(--sp-surface);
  color: var(--sp-ink);
  border-color: var(--sp-border);
}

.sp-button--secondary:hover {
  background-color: var(--sp-surface-sunken);
  border-color: var(--sp-primary);
}

.sp-button--secondary:active {
  translate: 0 1px;
}

/* A QUIET ACTION WITH A BUTTON'S HIT AREA: no face, no boundary, the hint
 * ink for its label, the full ink and an underline on hover. It is what a
 * "cancel", a "clear filters" or a row's delete wears beside a primary, so a
 * screen has one filled control and the rest step back.
 *
 * NOT INDIGO SINCE DS-1 (W1; design-system-v4 §3.2, the colour diet). Indigo
 * belongs to five roles — the primary face, link text, the focus ring,
 * current/selected markers and the single-series chart default — and a
 * tertiary BUTTON is none of them: painted in the link token it read as a
 * link and competed with the one primary. Its label is now --sp-ink-muted at
 * rest and --sp-ink on hover, both registered text pairs on the three grounds
 * a staff control sits on ("hint on app ground" / "hint in a card" /
 * "protected-badge label" on the well, and "body …" for hover). Staff only in practice: no participant page
 * renders the variant (`accent-diet.test.ts` holds the diet; `kit-skin`
 * holds the face). The inline padding is the small step so the label lines
 * up with running text beside it. */
.sp-button--tertiary {
  background-color: transparent;
  color: var(--sp-ink-muted);
  border-color: transparent;
  padding-inline: var(--sp-space-small);
}

.sp-button--tertiary:hover {
  color: var(--sp-ink);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

/* THE DESTRUCTIVE ACTION IS OUTLINED, NOT FILLED. A filled error face would
 * be the loudest object on any screen, and this product allows one loud
 * control per screen — the primary. The error token draws the label and the
 * boundary on the control's own surface ("field error in a card", a registered
 * text pair), and hover tints the face with the error surface ("error banner
 * text"). Text pairs both, which is the stricter floor for a boundary. */
.sp-button--danger {
  background-color: var(--sp-surface);
  color: var(--sp-error);
  border-color: var(--sp-error);
}

.sp-button--danger:hover {
  background-color: var(--sp-error-surface);
  color: var(--sp-error);
  border-color: var(--sp-error);
}

.sp-button--danger:active {
  translate: 0 1px;
}

/* An icon-only button is a square: the 44px floor on both axes with no inline
 * padding, so the glyph centres. The accessible name is the caller's
 * (`aria-label`); this rule only makes the box. */
.sp-button--icon {
  padding-inline: 0;
  inline-size: var(--sp-touch-target);
  flex: none;
}

.sp-button--accent {
  background-color: var(--sp-accent);
  color: var(--sp-on-accent);
}

.sp-button[disabled],
.sp-button[aria-disabled="true"] {
  cursor: not-allowed;
  opacity: 0.6;
  box-shadow: none;
  translate: none;
}

/* The journey's forward action. One per screen, full width on a phone, and
 * deliberately taller than the 44px floor: it is the control a participant on
 * a moving tram has to hit. */
.sp-button--journey {
  inline-size: 100%;
  min-block-size: calc(var(--sp-touch-target) + var(--sp-space-small));
  font-size: var(--sp-font-size-lede);
}

/* --------------------------------------------------------------------- field */
.sp-field {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space-small);
  margin-block-end: var(--sp-space);
}

/* Visible label is mandatory; placeholder-as-label is banned at the component
 * level, so no rule here ever hides .sp-field__label.
 *
 * THE LABEL IS CHROME, NOT COPY, on a staff form: meta size, 600, full ink —
 * the voice admin.css's toolbar and form-grid labels speak in, so the three
 * forms on one screen read as one family. (Meta, not caption: caption is a
 * 09.7 data-surface token and `test/participant-served.test.ts` proves this
 * served sheet reads none of those.) The participant surface restores reading
 * size below: a person filling in a consent form on a phone is READING the
 * label, not scanning a toolbar, and that surface renders exactly as before. */
.sp-field__label {
  color: var(--sp-ink);
  font-size: var(--sp-font-size-meta);
  font-weight: 600;
  text-align: start;
}

.sp-field__hint {
  color: var(--sp-ink-muted);
  font-size: var(--sp-font-size-meta);
  text-align: start;
}

.sp-field__error {
  color: var(--sp-error);
  font-size: var(--sp-font-size-meta);
  font-weight: 600;
  text-align: start;
}

.sp-root[data-surface="participant"] .sp-field__label,
.sp-root[data-surface="participant"] .sp-field__hint,
.sp-root[data-surface="participant"] .sp-field__error {
  font-size: var(--sp-font-size-body);
}

/* THE TEXT CONTROL. 44px tall in both densities, 1px --sp-border (the 3:1
 * control-boundary token, never --sp-rule), the card radius's smaller sibling,
 * surface ground, body-size text. The inline padding is a fixed 0.75rem rather
 * than a space token on purpose: the compact block re-scopes the space scale,
 * and a field whose text jumped 4px sideways at the density switch would be
 * the one control that looks broken by it.
 *
 * Hover darkens the edge to --sp-ink-muted — a registered text pair on every
 * ambient ground, so it clears the 3:1 boundary floor with room. Focus is the
 * shell ring declared once above and reused here, never a second ring. The
 * colour transition is the fast token, zeroed by the reduced-motion switch. */
.sp-input {
  min-block-size: var(--sp-touch-target);
  inline-size: 100%;
  padding-block: var(--sp-space-small);
  padding-inline: 0.75rem;
  background-color: var(--sp-surface);
  color: var(--sp-ink);
  border: 1px solid var(--sp-border);
  border-radius: var(--sp-radius);
  font: inherit;
  /* 1rem floor: below 16px mobile Safari focus-zooms the input. */
  font-size: var(--sp-font-size-body);
  /* No `box-shadow` in this list since DS-1 (W1): no rule ever gives an
   * `.sp-input` a shadow, so the entry animated nothing, and the motion
   * allow-list (design-system-v4 §9) admits colours, opacity and transforms
   * only. */
  transition:
    background-color var(--sp-duration-fast) var(--sp-ease-standard),
    border-color var(--sp-duration-fast) var(--sp-ease-standard);
}

.sp-input:hover {
  border-color: var(--sp-ink-muted);
}

/* A placeholder is a hint, never a label (the rule above), and it wears the
 * hint colour at full opacity: the user agent's default dims it under 4.5:1. */
.sp-input::placeholder {
  color: var(--sp-ink-muted);
  opacity: 1;
}

.sp-input:disabled {
  background-color: var(--sp-surface-sunken);
  color: var(--sp-ink-muted);
  cursor: not-allowed;
}

.sp-input[aria-invalid="true"] {
  border-color: var(--sp-error);
}

/* THE SELECT DRAWS ITS OWN CHEVRON. `appearance: none` removes the platform's
 * arrow — the one part of a native select no stylesheet can colour, and the
 * single detail that made every toolbar read as an operating-system dialog —
 * and two gradients paint a ∨ in `currentColor` at the inline end: each is a
 * 2px diagonal band through a 7px square, the first running from the top
 * corner on the start side down to the centre, the second back up to the top
 * corner on the end side. currentColor is paint the usage gate does not need
 * to see: it is whatever `color` the control already resolved to, so the
 * chevron is ink at rest and muted when the control is disabled, with no
 * colour literal and no SVG data URI anywhere. The inline-end padding leaves
 * the chevron its lane.
 *
 * `background-position` is physical — CSS has no flow-relative keyword for it
 * yet — so the mirrored position is stated for RTL in the two rules below: the
 * `[dir="rtl"]` precedent from `.sp-chart-kit__geometry` (data.css), and the
 * `:dir()` pseudo-class as a SEPARATE rule so a browser that lacks it drops one
 * rule and not both. A ∨ is symmetric about its own centre, so only its PLACE
 * mirrors, never its shape. */
select.sp-input {
  appearance: none;
  padding-inline-end: 2.5rem;
  background-image:
    linear-gradient(45deg, transparent calc(50% - 1px), currentColor calc(50% - 1px), currentColor calc(50% + 1px), transparent calc(50% + 1px)),
    linear-gradient(135deg, transparent calc(50% - 1px), currentColor calc(50% - 1px), currentColor calc(50% + 1px), transparent calc(50% + 1px));
  background-repeat: no-repeat;
  background-size: 0.4375rem 0.4375rem;
  background-position:
    calc(100% - 1rem) 50%,
    calc(100% - 0.5625rem) 50%;
}

[dir="rtl"] select.sp-input {
  background-position:
    0.5625rem 50%,
    1rem 50%;
}

select.sp-input:dir(rtl) {
  background-position:
    0.5625rem 50%,
    1rem 50%;
}

/* A list box has no closed state to open: no chevron, and no lane for it. */
select.sp-input[multiple],
select.sp-input[size]:not([size="1"]) {
  background-image: none;
  padding-inline-end: 0.75rem;
}

/* An email or phone value stays LTR even inside an RTL page. */
.sp-input--ltr-value {
  direction: ltr;
  text-align: start;
}

/* ------------------------------------------------------------- choice group */
.sp-choice-group {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space-small);
  border: 0;
  padding: 0;
  margin: 0;
}

.sp-choice-group__legend {
  font-weight: 600;
  padding: 0;
  margin-block-end: var(--sp-space-small);
  text-align: start;
}

/* The whole row is the target, not the 16px control: v1 found 18 sub-24px
 * targets in a real browser and the row-as-target is the carried fix. */
.sp-choice {
  display: flex;
  align-items: center;
  gap: var(--sp-space);
  min-block-size: calc(var(--sp-touch-target) + var(--sp-space-small));
  padding-block: var(--sp-space-small);
  padding-inline: var(--sp-space);
  background-color: var(--sp-surface);
  border: 1px solid var(--sp-border);
  border-radius: var(--sp-radius);
  cursor: pointer;
  /* Same tokenisation contract as .sp-button above: byte-equivalent to the
   * previous hardcoded `120ms ease`. */
  transition:
    background-color var(--sp-duration-fast) var(--sp-ease-standard),
    border-color var(--sp-duration-fast) var(--sp-ease-standard);
}

.sp-choice:hover {
  background-color: var(--sp-surface-sunken);
  border-color: var(--sp-primary);
}

/* A ticked row reads as ticked from across the screen, not only at the 24px
 * box. `:has()` does this with no script; a browser without it still shows the
 * native checkmark, so nothing is lost where it is unsupported. */
.sp-choice:has(.sp-choice__control:checked) {
  background-color: var(--sp-surface-sunken);
  border-color: var(--sp-primary);
}

/* THE BOX ITSELF: 20px, brand-accented, and never the target — the row around
 * it is (above), or the `.sp-cell-choice` label in a table (reach.css).
 * `.sp-check` is the same face for a bare checkbox or radio that has no
 * `.sp-choice` row to live in, so a screen never ships a user-agent-coloured
 * box beside a brand-coloured one. The participant's consent ticks stay 24px
 * (below): a thumb on a tram wants the larger mark, and that surface renders
 * exactly as it did. */
.sp-choice__control,
.sp-check {
  inline-size: 1.25rem;
  block-size: 1.25rem;
  margin: 0;
  accent-color: var(--sp-primary);
  flex: none;
  cursor: pointer;
}

.sp-root[data-surface="participant"] .sp-choice__control {
  inline-size: 1.5rem;
  block-size: 1.5rem;
}

.sp-choice__label {
  text-align: start;
}

/* --------------------------------------------------------- progress indicator */
/* Native <progress>: it is the only element that reports progress to AT
 * without a JS shim, and the no-JS runner has no shim. It carries no
 * percentage text — Intl numeral systems would need a locale the CSS layer
 * does not have (design-system.md §2.5). */
/* SIX SEGMENTS, ONE ELEMENT.
 *
 * The journey has six steps and a participant should see which one they are on
 * without counting, but the thing that reports progress to a screen reader has
 * to stay the native <progress> — six <span>s would be six decorations and no
 * announcement, and there is no JavaScript here to fix that up.
 *
 * So the segmentation is a MASK over the native element: the bar and its value
 * are painted by the user agent exactly as before, and the mask cuts five gaps
 * through both. The gradient is stated at 90deg and is symmetric about its own
 * centre, so it renders identically in a mirrored layout and needs no
 * direction; `currentColor`/`transparent` are alpha stops, not paint, so no
 * colour enters the rule. A browser without mask support shows the continuous
 * bar this replaced, which is a degradation and not a break. */
.sp-progress {
  inline-size: 100%;
  block-size: 0.5rem;
  appearance: none;
  border: 0;
  border-radius: var(--sp-radius-pill);
  /* THE TRACK IS --sp-border, AND IT USED TO BE --sp-rule BECAUSE OF A
   * MISREADING THIS FILE'S OWN COMMENT WARNS ABOUT.
   *
   * --sp-rule and the participant --sp-page are the same grey to within one
   * unit per channel: 1.01:1. On a phone the rail showed one filled block
   * floating on nothing,
   * and "six steps, and you can see which one you are on" was true only for
   * the step you were on. --sp-rule is the DECORATIVE token and tokens.css
   * says in as many words that it must never bound a control; a progress track
   * is part of a control, so it was never --sp-rule's to carry. --sp-border is
   * the token held to 1.4.11's 3:1 against every ground here.
   *
   * THE FILL IS --sp-progress-fill, AND IT USED TO BE --sp-primary — which
   * "cleared 3:1 against the track, both measured by the matrix". That sentence
   * was true only because the dark track was itself failing its grounds
   * (OD-10 F-01): a dark border light enough to clear the darkest dark ground at 3:1 can never
   * also sit 3:1 UNDER the light-lavender dark primary — the two constraints
   * exclude each other, which palette v2 surfaced the moment the border was
   * repaired. The fill token is mode-split in tokens.css (pressed brand face in
   * light, accent in dark) and its pair is measured like every other. */
  background-color: var(--sp-border);
  overflow: hidden;
  -webkit-mask-image: repeating-linear-gradient(
    90deg,
    currentColor 0 calc(100% / 6 - 4px),
    transparent calc(100% / 6 - 4px) calc(100% / 6)
  );
  mask-image: repeating-linear-gradient(
    90deg,
    currentColor 0 calc(100% / 6 - 4px),
    transparent calc(100% / 6 - 4px) calc(100% / 6)
  );
}

.sp-progress::-webkit-progress-bar {
  background-color: var(--sp-border);
}

.sp-progress::-webkit-progress-value {
  background-color: var(--sp-progress-fill);
}

.sp-progress::-moz-progress-bar {
  background-color: var(--sp-progress-fill);
}

/* The label above the rail. Meta size and weight: it names the rail, it is not
 * something to read. */
.sp-progress-label {
  color: var(--sp-ink-muted);
  font-size: var(--sp-font-size-meta);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: start;
}

/* Label and rail are one unit and must never be split by a stack gap. */
.sp-progress-group {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space-small);
}

/* ---------------------------------------------------------------------- chip */

/* A CLASS SIXTEEN ELEMENTS WORE AND NO RULE APPLIED TO — gap G-21, closed.
 *
 * `.sp-chip` is written on sixteen elements across three route files
 * (`app/surveys/page.tsx`, `.../[versionId]/page.tsx`,
 * `.../translations/page.tsx`) to mark a duration, a lifecycle state, a locale
 * and a translation state. Its ONLY appearance in any stylesheet was
 * `app/surveys/builder.css:112` — `.builder__locales .sp-chip { overflow-wrap:
 * anywhere }` — a contextual modifier inside one block. Every chip outside
 * `.builder__locales` was a bare inline `<span>` wearing the name of a
 * component that did not exist, and a recursive grep for the name returned a
 * positive number, which is why it survived two rounds of review.
 *
 * IT IS A CHIP AND NOT A BADGE, which is why it is its own rule rather than an
 * alias. `.sp-badge` below is the STATE vocabulary — `sp-badge--protected` is
 * the brand moment of brief §4.3 and carries weight 600 so a withheld cell
 * announces itself, and a lifecycle state ("draft", "Concept") is a
 * `sp-badge--neutral`. A chip states a routine FACT about a row: this locale is
 * `nl`, this estimate is eleven minutes, this holder's role is RESEARCHER.
 * Giving them one class would make every fact as loud as a state, which is the
 * uniform-weight defect §2.5.1 exists to prevent.
 *
 * SO IT IS A FACT IN A WORD, NOT IN A PILL — the integration-round critique
 * measured why. A list screen showed fifty grey pills (25 `draft`, 25 `nl`) and
 * a settings screen a wall of nineteen, all on `--sp-surface-sunken`, the same
 * lavender the row hover, the state frames and the stepper ordinal paint: a
 * grey wash in which a pill carried no information a plain word would not. A
 * ground is reserved for a STATE (the badge tones below); a fact is set as text
 * — full ink, weight 500 so it does not go limp beside a 600 code, no ground,
 * no inline padding, so it sits exactly where a word sits under its column
 * heading. What survives from the pill is the BASELINE: the badge's
 * min-block-size and line-height, so a chip and a badge in one cell still line
 * up. `--sp-ink` on the three ambient grounds is the registered "body …" trio.
 *
 * A TAG LIST MAY OUTLINE ITS WORDS (`.sp-chip--outlined`, below): a 1px
 * `--sp-border` ring and the pill's inline padding, no ground. It exists for
 * the one case where bare words run together — a wrapping list of locale
 * codes or capability names — and is the MOST a chip may wear: the protected
 * badge is the one bordered GREY pill on the surface and must stay
 * unmistakable, so no chip ever takes a sunken ground again
 * (`test/kit-skin.test.ts` refuses it).
 *
 * `overflow-wrap: anywhere` MOVES HERE FROM `builder.css`. A chip holds a locale
 * tag or a state code, which is unbreakable text that used to overflow its
 * track — the builder discovered that and fixed it for its own locale list
 * only. It is a property of the component, not of one screen. */
.sp-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-space-xsmall);
  min-block-size: 1.5rem;
  padding-block: 0;
  padding-inline: 0;
  color: var(--sp-ink);
  font-size: var(--sp-font-size-meta);
  font-weight: 500;
  line-height: var(--sp-line-height-heading);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

.sp-chip--outlined {
  padding-inline: var(--sp-space-small);
  border: 1px solid var(--sp-border);
  border-radius: var(--sp-radius-pill);
}

/* --------------------------------------------------------------------- badge */
/* A STATE, IN A PILL: meta size, 600, a tinted ground from the feedback set.
 * NEVER BODY SIZE — a badge at body size in a 44px row was the loudest object
 * on the register and said the least (one state, repeated twenty-five times).
 * Meta rather than the caption token because this sheet is served to the
 * participant and `test/participant-served.test.ts` proves it reads no 09.7
 * data-surface token; the staff sheet (data.css) restates badge, chip and save
 * chip ONCE at caption, which is the tier of the row they sit in. The
 * min-block-size and line-height are the chip's exactly, so a chip and a badge
 * in one cell share a baseline; what differs is weight, the ground and the
 * inline padding a ground needs.
 *
 * THE BORDER IS DECLARED ON THE BASE, TRANSPARENT. Two of the six tones draw a
 * 1px ring (neutral, protected) and four do not; without a border on the base
 * those two would be 2px taller than their siblings and a row holding a good
 * badge beside a neutral one would show two baselines. `transparent` is not a
 * colour the usage gate measures — it paints nothing — and each ringed tone
 * sets only `border-color`. */
.sp-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-space-xsmall);
  min-block-size: 1.5rem;
  padding-block: var(--sp-space-xsmall);
  padding-inline: var(--sp-space-small);
  border: 1px solid transparent;
  border-radius: var(--sp-radius-pill);
  font-size: var(--sp-font-size-meta);
  font-weight: 600;
  line-height: var(--sp-line-height-heading);
  overflow-wrap: anywhere;
}

/* THE FIVE TONES, each a registered text pair in lib/pairs.ts. Good, warning
 * and serious are the three tinted feedback surfaces under their own ink;
 * critical is the one FILLED tone — the error token as a ground under the
 * primary label token, the "critical badge label" pair — for the state a
 * reader must not scroll past (a failed import batch, a breached retention
 * clock).
 *
 * NEUTRAL IS OUTLINED, NOT FILLED. It was the sunken well, and on a list where
 * twenty-five rows share one lifecycle state that is twenty-five grey pills in
 * the same lavender as the row hover and the state frames — the "grey wash" the
 * integration critique measured. A tinted, filled ground now means exactly one
 * thing: a good / warning / serious / critical STATE. A routine state is the
 * surface it sits on, a 1px --sp-border ring and full ink at 600 — present,
 * scannable, and carrying no colour that would compete with the one row that is
 * actually amber. `--sp-ink` on `--sp-surface` is "body in a card";
 * `--sp-border` on `--sp-surface` is "input border in a card". The protected
 * badge keeps its meaning, its muted ink, its SUNKEN ground and its border
 * (brief §4.3), unchanged below — it is now the only bordered grey pill on the
 * surface, which is what makes it unmistakable. */
.sp-badge--neutral {
  background-color: var(--sp-surface);
  color: var(--sp-ink);
  border-color: var(--sp-border);
}

.sp-badge--good {
  background-color: var(--sp-success-surface);
  color: var(--sp-on-success-surface);
}

.sp-badge--warning {
  background-color: var(--sp-warning-surface);
  color: var(--sp-on-warning-surface);
}

.sp-badge--serious {
  background-color: var(--sp-error-surface);
  color: var(--sp-error);
}

.sp-badge--critical {
  background-color: var(--sp-error);
  color: var(--sp-on-primary);
}

/* The SUPPRESSED brand moment. Grey by design: it must read as "withheld",
 * never as a value and never as zero. The badge carries no arithmetic — "n=4"
 * would itself be the disclosure. The staff sheet (data.css) lets this one
 * badge wrap at SPACES into a rounded rectangle, because its label is a
 * sentence and a pill is a one-line shape. */
.sp-badge--protected {
  background-color: var(--sp-surface-sunken);
  color: var(--sp-ink-muted);
  border: 1px solid var(--sp-border);
}

/* Text for assistive technology only. NOT `display: none` and not
 * `visibility: hidden` — both remove the node from the accessibility tree,
 * which is the one consumer this class exists for. The clip-rect pattern keeps
 * it announced and off the screen.
 *
 * Used by the command palette's live region and by form labels whose visible
 * text would be redundant beside the control they label. A label that is only
 * a placeholder is not a label (WCAG 2.2 AA, 3.3.2), so the label exists and
 * this is where it goes. */
.sp-visually-hidden {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Rendered on every aggregate view whether or not a cell is currently
 * suppressed: copy that appears only when suppression fires is a signal.
 *
 * A CAPTION LINE, NOT A PARAGRAPH (round 059b). At body size the legend was
 * the loudest sentence on Analyse — louder than the figures it explains. It is
 * apparatus, so it takes the small tier and the muted ink, and it states its
 * own leading so a two-line legend reads as running text whatever container it
 * lands in, rather than inheriting one. Still unconditional and still in the
 * open: nothing here can hide it.
 *
 * META, NOT CAPTION, AND THAT IS THE FENCE: this sheet is served to the
 * participant (`stylesheet.py` `_SHEETS`), and `test/participant-served.test.ts`
 * allows only `data.css` to read the 09.7 caption token. The same reason the
 * pills sit at meta here (handoff 052, decision 2). */
.sp-suppression-legend {
  font-size: var(--sp-font-size-meta);
  line-height: var(--sp-line-height);
  color: var(--sp-ink-muted);
  text-align: start;
}

/* --------------------------------------------------------------------- stack */
/* The whole participant journey is one column of blocks on a phone. A single
 * stack primitive is what stops each screen inventing its own vertical rhythm
 * with a margin — and a margin is where a physical property gets in. */
.sp-stack {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space);
}

.sp-stack--tight {
  gap: var(--sp-space-small);
}

/* ----------------------------------------------------------- language choice */
/* The first screen of the participant journey (brief §4.2 surface 1). Each
 * option is a full-width link, not a <select>: a select on a low-end Android
 * opens a system picker whose options render in the PAGE's direction, which
 * would show every Arabic name left-to-right. Separate elements each carrying
 * their own `dir` cost one tap and get it right.
 *
 * `justify-content: space-between` rather than any inline offset — the code
 * chip lands at the reading END in both directions with no rule of its own. */
/* A CHOICE, NOT AN ACTION, AND THE SHAPE SAYS SO. The pill radius belongs to
 * the buttons that move the journey forward; the four languages are a list
 * being chosen from, so they take the card radius that `.sp-choice` already
 * uses for the consent tick rows. One rule, two places, same meaning. */
.sp-locale-option {
  justify-content: space-between;
  inline-size: 100%;
  /* Endonyms are long in some scripts; a single line would ellipsis them. */
  white-space: normal;
  border-radius: var(--sp-radius);
  padding-block: var(--sp-space);
  padding-inline: var(--sp-space);
  /* Comfortably past the 44px floor — four of these are the first thing a
   * participant ever touches, sometimes one-handed on a bus. */
  min-block-size: calc(var(--sp-touch-target) + var(--sp-space));
  /* THE ROW IS SUNKEN INTO THE CARD, AND IN DARK THAT IS THE ONLY THING THAT
   * SEPARATES IT. `.sp-button--secondary` paints --sp-surface, and the card it
   * sits in is also --sp-surface: in light a shadow rescues it, and in dark a
   * shadow is invisible, so the four language rows were a 1px border and
   * nothing else. --sp-surface-sunken is a real step in both palettes and is
   * measured against --sp-ink, --sp-border and --sp-focus already. */
  background-color: var(--sp-surface-sunken);
  /* The endonym is the content of this control, so it is set at reading size
   * and not at chrome size. On the ar/fa options the :lang() bump above makes
   * this bigger again, which is the whole point of that rule. */
  font-size: var(--sp-font-size-lede);
  font-weight: 700;
  box-shadow: 0 1px 2px var(--sp-shadow);
}

/* Hover inverts the row out of the card instead of pushing it further in —
 * the sunken direction is already spent on the resting state. */
.sp-locale-option:hover {
  background-color: var(--sp-surface);
  border-color: var(--sp-primary);
}

.sp-locale-option:active {
  translate: 0 1px;
}

/* The tag chip. Set as a bordered pill so it reads as metadata attached to the
 * row rather than as a second, smaller label competing with the endonym. */
.sp-locale-option__code {
  flex: none;
  padding-block: var(--sp-space-xsmall);
  padding-inline: var(--sp-space-small);
  /* Raised out of the sunken row, so the chip reads as attached to the row
   * rather than as a hole in it. */
  background-color: var(--sp-surface);
  color: var(--sp-ink-muted);
  border: 1px solid var(--sp-border);
  border-radius: var(--sp-radius-pill);
  font-family: var(--sp-font-admin);
  font-size: var(--sp-font-size-meta);
  font-weight: 600;
  line-height: var(--sp-line-height-heading);
  text-transform: uppercase;
  /* Direction-neutral: a language TAG is Latin script in every locale. */
  direction: ltr;
}

/* ---------------------------------------------------------------------- card */
/* The card is lifted off the page ground by THREE things, in this order of
 * importance: --sp-surface against --sp-page (a measured pair, and the one
 * that still works with shadows disabled), a 1px --sp-rule edge, and only then
 * the shadow. The shadow is never the separation — it is what makes the step
 * read as depth rather than as a colour change. */
/* QUIETER THAN IT WAS. The second layer read `0 10px 28px` — a soft halo that,
 * on a register with one card, made the card float over the page rather than
 * sit on it, and multiplied across the tiles of a landing screen into a haze.
 * 2px/6px keeps the step legible and lets the hairline and the measured
 * ground pair do the separating; `.sp-chart-tile` (data.css) wears the same
 * two layers so a figure and a card on one screen have one elevation. */
.sp-card {
  background-color: var(--sp-surface);
  color: var(--sp-ink);
  border: 1px solid var(--sp-rule);
  border-radius: var(--sp-radius-card);
  padding: var(--sp-space);
  box-shadow:
    0 1px 2px var(--sp-shadow),
    0 2px 6px var(--sp-shadow);
}

/* More room on the participant surface than on a dense admin table: this is
 * one thought per card, read once, on a phone. */
.sp-root[data-surface="participant"] .sp-card {
  padding: var(--sp-space-large) var(--sp-space);
}

/* ---------------------------------------------------------- rules, dividers */
/* A HAIRLINE BETWEEN TWO PASSIVE REGIONS, as an element: `<hr class="sp-rule-h">`
 * between a card's head and its body, `<span class="sp-divider">` between two
 * groups in a toolbar row. Rules instead of boxes is the whole composition
 * grammar of the staff surface, and a screen that wants one line should not
 * have to invent a bordered wrapper to get it. Both are --sp-rule, and both are
 * argued in `lib/grounds.ts` EXEMPT_USAGES as decorative: neither bounds a
 * control, and a screen that needs a line to IDENTIFY a control may not reach
 * for these — that line is --sp-border and belongs on the control. */
.sp-rule-h {
  display: block;
  inline-size: 100%;
  block-size: 0;
  margin: 0;
  border: 0;
  border-block-start: 1px solid var(--sp-rule);
}

.sp-divider {
  display: inline-block;
  align-self: stretch;
  inline-size: 0;
  min-block-size: 1.5rem;
  margin: 0;
  border: 0;
  border-inline-start: 1px solid var(--sp-rule);
}

/* ------------------------------------------------------ typographic levels */
/* Three levels and no more: the heading a participant lands on, the sentence
 * that supports it, and the body. The user agent's <h1> is 2em/bold with a
 * block margin the stack would then double; both are replaced here so the
 * rhythm comes from one place. */
.sp-root[data-surface="participant"] h1 {
  margin: 0;
  color: var(--sp-ink);
  font-size: var(--sp-font-size-heading);
  font-weight: var(--sp-font-weight-heading);
  line-height: var(--sp-line-height-heading);
  letter-spacing: var(--sp-letter-spacing-wordmark);
  text-align: start;
  /* Keeps a two-line heading from leaving one orphan word on line two. */
  text-wrap: balance;
}

.sp-root[data-surface="participant"] p {
  margin: 0;
}

/* The sentence under the heading. Larger than body and muted rather than
 * bold — the step between levels is size and colour, never weight, because
 * weight is what the heading already uses. */
.sp-lede {
  color: var(--sp-ink-muted);
  font-size: var(--sp-font-size-lede);
  line-height: var(--sp-line-height);
  text-align: start;
}

/* Running participant prose. Full ink, body size, 1.6 — this is the text
 * somebody actually has to read and understand, so it gets the strongest
 * contrast pair in the system rather than the hint colour. */
.sp-prose {
  color: var(--sp-ink);
  font-size: var(--sp-font-size-body);
  line-height: var(--sp-line-height);
  text-align: start;
}

.sp-eyebrow {
  font-size: var(--sp-font-size-eyebrow);
  font-weight: var(--sp-font-weight-eyebrow);
  letter-spacing: var(--sp-letter-spacing-eyebrow);
  text-transform: uppercase;
  color: var(--sp-ink-muted);
  text-align: start;
}

.sp-metric {
  font-variant-numeric: tabular-nums;
}

/* The OD-02 unapproved-consent banner.
 *
 * Rendered wherever a consent text is shown — participant screen, staff
 * preview, demo capture — and there is no modifier that hides it. A flag that
 * could suppress it is the one thing that would let an unapproved text reach
 * somebody without saying so, which is why the component has no such flag and
 * this stylesheet has no `--quiet` variant of it.
 *
 * Warning surface rather than error: the text is not broken, it is unratified.
 * The pair carries 8.50:1 in dark and is contrast-checked in both palettes by
 * the design-tokens contrast suite.
 *
 * The rule is drawn with a logical inline-start edge, so the marker sits at
 * the reading edge in Arabic and Farsi for free. */
.sp-consent-unapproved {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: var(--sp-space-small);
  background-color: var(--sp-warning-surface);
  color: var(--sp-on-warning-surface);
  border-inline-start: 4px solid var(--sp-on-warning-surface);
  border-radius: var(--sp-radius);
  padding: var(--sp-space);
  margin-block-end: var(--sp-space);
  font-size: var(--sp-font-size-body);
  text-align: start;
}

/* The machine token gets a chip of its own. It is not a word in any locale and
 * must not be read as the first words of the sentence beside it.
 *
 * THE PARTICIPANT SURFACE NO LONGER USES THIS (04.5). `PLACEHOLDER_PENDING_DPO`
 * is a state, not a string to print, and printing a database enum to somebody
 * filling in a consent form explained nothing to them. The chip survives on the
 * STAFF consent screen (`/privacy/consent`), where the machine token is the
 * right vocabulary for the person governing the machine, and the participant
 * banner now carries the state in `data-consent-text-status` alone. */
.sp-consent-unapproved__status {
  display: inline-block;
  padding-block: var(--sp-space-xsmall);
  padding-inline: var(--sp-space-small);
  border: 1px solid var(--sp-on-warning-surface);
  border-radius: var(--sp-radius-pill);
  font-family: var(--sp-font-admin);
  font-size: var(--sp-font-size-meta);
  font-variant-numeric: tabular-nums;
  font-weight: var(--sp-font-weight-eyebrow);
  letter-spacing: var(--sp-letter-spacing-eyebrow);
  /* The participant surface sets `overflow-wrap: anywhere` for opaque tokens
   * in prose. This IS an opaque token, but it is a chip: at 320px it broke as
   * "PLACEHOLDER_PENDING_DP / O" across two lines inside its own pill, which
   * reads as a rendering fault rather than as a status. It fits on one line at
   * 320px with the padding this rule sets, so it is allowed to keep it. */
  overflow-wrap: normal;
  white-space: nowrap;
}

/* THE CONSENT TEXT ITSELF, AND WHY IT IS NO LONGER STYLED AS MACHINERY.
 *
 * This paragraph used to render in the admin face at --sp-ink-muted, on the
 * argument that `vault.consent_text_version.body` holds a content-tracker
 * marker rather than words. That argument was right about the STORED value and
 * wrong about the RENDERED one: `resolve_marker` dereferences the marker
 * against the translation store, so what a participant sees here is approved,
 * plain-language consent copy in their own language — and it was the faintest,
 * smallest text on the screen they were being asked to tick a box about.
 *
 * The "not ratified" signal was never carried by this paragraph's colour. It
 * is carried by the banner directly above it, which states it as a sentence in
 * the participant's own locale, and by PLACEHOLDER_PENDING_DPO beside it,
 * which no locale softens. Both are unsuppressible. So the text a consent
 * decision is made on is set as prose, and the honesty control stays where it
 * can actually be read. */
.sp-consent-placeholder-body {
  color: var(--sp-ink);
  font-size: var(--sp-font-size-body);
  line-height: var(--sp-line-height);
  text-align: start;
}

/* ------------------------------------------------------------- the runner */
/* Phase02 02.10. Five classes, all logical, all token-coloured.
 *
 * The runner is one question per screen on a 360px phone, so the layout work
 * is a max inline size and a reading-edge alignment; there is no grid here and
 * there is deliberately no matrix grid anywhere (CP-05). */
.sp-runner {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space);
  max-inline-size: var(--sp-measure-prose);
  margin-inline: auto;
  text-align: start;
}

/* The TEST banner (02.11 step 3). Persistent on every screen of a test
 * session: a banner that appears once is a banner a participant scrolled past.
 * Warning surface rather than error, because a test session is not broken. */
.sp-runner-test-banner {
  background-color: var(--sp-warning-surface);
  color: var(--sp-on-warning-surface);
  border-inline-start: 4px solid var(--sp-on-warning-surface);
  border-radius: var(--sp-radius);
  padding: var(--sp-space-small);
  font-weight: 600;
  text-align: start;
}

/* Back and Next side by side, both full-height touch targets. `wrap-reverse`
 * so that at 320px the primary action stays on the first line the thumb
 * reaches, and `flex: 1 1 auto` so neither control shrinks below the target. */
.sp-runner-nav {
  display: flex;
  flex-wrap: wrap-reverse;
  gap: var(--sp-space-small);
}

.sp-runner-nav .sp-button {
  flex: 1 1 auto;
  min-block-size: var(--sp-touch-target);
}

/* CP-05: the statements a participant has already answered, collapsed into a
 * native <details>. No JavaScript, keyboard-reachable, and announced by a
 * screen reader as a disclosure — which a div with a click handler is not. */
.sp-runner-accordion {
  border: 1px solid var(--sp-rule);
  border-radius: var(--sp-radius);
  padding: var(--sp-space-small);
  background-color: var(--sp-surface-sunken);
}

.sp-runner-accordion > summary {
  min-block-size: var(--sp-touch-target);
  display: flex;
  align-items: center;
  cursor: pointer;
  color: var(--sp-ink-muted);
}

@media (prefers-reduced-motion: reduce) {
  .sp-button,
  .sp-input,
  .sp-choice,
  .sp-locale-option {
    transition: none;
  }

  /* The 1px press displacement is motion too. The state still changes — it
   * changes by colour alone, which is what the hover pair is measured for. */
  .sp-button:active,
  .sp-locale-option:active {
    translate: none;
  }
}

/* PARTICIPANT SHELL — lane B of the participant spec (PT01b, 09.75; spec P0,
 * F03 and F04). Served to the participant only, after the kit (`_SHEETS` in
 * `panel_participant_ui/stylesheet.py`); the staff `index.css` never imports it.
 *
 * ---------------------------------------------- F03 color-scheme: NOT HERE
 * The dark participant checkbox (an UNCHECKED box drawn UA-white on the dark
 * card, which reads as ticked — BASE-00's `ar-390-js-dark` consent capture) is
 * fixed by `color-scheme`, and `color-scheme` has ONE writer: tokens.css
 * (MASTER-PLAN §3.2 ruling 11, critique C-30: "DS W1 should own it, and the
 * participant drops its copy"). The participant root matches tokens.css
 * blocks 1 and 3-5 itself, so it is drawn in the palette's own scheme with no
 * declaration in this file. `test/participant-color-scheme.test.ts` proves
 * that from the participant's side — it resolves the cascade on the served
 * root in every system-scheme × `data-theme` state and requires the scheme
 * to equal the participant palette there — and refuses a `color-scheme` in
 * any participant sheet.
 *
 * The CHECKED face needs nothing either: every participant checkbox and radio
 * is `.sp-choice__control`, which primitives.css accents with the mode-split
 * `--sp-primary` (`tests/participant_contract/test_participant_dark_controls.py`
 * holds every rendered control to that class). */

/* ------------------------------------------------------- F04 the [hidden] strip
 * `.sp-synthetic` declares `display: block` (primitives.css), and an author
 * `display` beats the user agent's `[hidden] { display: none }`. So when a
 * presentation-mode host renders the strip EMPTY and `hidden`
 * (`document.page`, OD-22), it still drew a cream band with a 2px rule across
 * the top of every participant page (PA-31). Only the empty, hidden strip is
 * affected: the labelled strip carries no `hidden`, and the OD-02 consent
 * banner, the content markers and the refusal bodies are never `hidden` and are
 * not matched here. */
.sp-synthetic[hidden] {
  display: none;
}

/* ------------------------------------------- F05 the skip link (PT04, W2)
 * First in the body, rendered by `document.page` once `chrome.skip` has words
 * (words-gated; never a marker). Out of sight until it takes focus, then a
 * real, legible control at the inline start of the page. Its target is the
 * page's one `id="main"`. */
.sp-skip:not(:focus) {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

a.sp-skip:focus {
  display: inline-block;
  margin-block: var(--sp-space-small);
  margin-inline: var(--sp-space);
  padding-block: var(--sp-space-small);
  padding-inline: var(--sp-space);
  background-color: var(--sp-surface);
  color: var(--sp-primary);
  border-radius: var(--sp-radius);
  font-weight: 700;
}

/* ------------------------------------------- F06 the footer (PT04, W2)
 * ONE DOM position on every page that has it (WCAG 3.2.6; spec P0): after the
 * shell, the page's `contentinfo`. Help line, then help and privacy. Quiet —
 * meta size, muted ink on the page ground — but every link is a 44px target
 * and keeps the link colour (--sp-primary on --sp-page, a measured pair). The
 * page the footer sits on marks its own link `aria-current="page"`: ink, not
 * a link colour, because it goes nowhere new. */
.sp-footer {
  inline-size: 100%;
  max-inline-size: var(--sp-measure-prose);
  margin-inline: auto;
  margin-block-start: var(--sp-space-xlarge);
  padding-inline: var(--sp-space);
  color: var(--sp-ink-muted);
  font-size: var(--sp-font-size-meta);
  text-align: start;
}

.sp-footer__help {
  margin-block: 0 var(--sp-space-small);
  margin-inline: 0;
  color: var(--sp-ink);
  font-size: var(--sp-font-size-body);
  font-weight: 600;
}

.sp-footer__links {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--sp-space);
  margin: 0;
  padding: 0;
  list-style: none;
}

.sp-footer__links a {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--sp-touch-target);
  color: var(--sp-primary);
  text-underline-offset: 0.2em;
}

.sp-footer__links a[aria-current="page"] {
  color: var(--sp-ink);
  font-weight: 600;
  text-decoration: none;
}

/* PARTICIPANT JOURNEY — lane A of the participant spec (PT02, W2: the journey
 * law; spec P1–P6, F14, F16, F18, F21, F24). Served to the participant only,
 * after the kit (`_SHEETS` in `panel_participant_ui/stylesheet.py`); the staff
 * `index.css` never imports it. v3 tokens only (`participant-token-fence`),
 * logical properties only, registered colour pairs only, no new colour. The
 * markup is `panel_api/journey_pages.py` and `registration_pages.py`.
 *
 * GOV.UK's back-link, error-summary and interruption-card patterns, re-built
 * in these tokens (OD-35: patterns, never copied code). The error summary
 * itself is the runner's `.sp-error-summary` (participant-runner.css): one
 * look for an error anywhere on the participant surface. */

/* ------------------------------------------------------ F14 the back link
 * A quiet text control at the reading start, above the card: 44px tall (the
 * kit's tertiary button), never competing with Continue. The chevron is drawn,
 * not a glyph (no font carries one in every script), and it points back in
 * the reading direction, so RTL turns it the other way. */
.sp-back-link,
.sp-journey-privacy {
  align-self: flex-start;
  color: var(--sp-ink);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

.sp-back-link {
  gap: var(--sp-space-small);
  padding-inline-start: 0;
}

.sp-back-link::before {
  content: "";
  flex: none;
  inline-size: 0.5em;
  block-size: 0.5em;
  border-block-start: 2px solid currentColor;
  border-inline-start: 2px solid currentColor;
  transform: rotate(-45deg);
}

/* In RTL the inline-start border is the right one, so the two borders meet
 * at the top-right corner; a quarter-turn the other way points that corner
 * at the reading start, which is the right. */
.sp-root[dir="rtl"] .sp-back-link::before {
  transform: rotate(45deg);
}

/* The privacy link sits under the call to action: a way to read before
 * trusting, never a second call to action. */
.sp-journey-privacy {
  padding-inline: 0;
  font-weight: 600;
}

/* ---------------------------------------------- spec P2 the browser hint
 * One quiet sentence under the call to action, shown to everyone. */
.sp-journey-hint {
  margin: 0;
  color: var(--sp-ink-muted);
  font-size: var(--sp-font-size-meta);
  text-align: start;
}

/* --------------------------------------------- F14 the onboarding recap
 * "In short: …" on the last card: a sunken well inside the card, so it reads
 * as the summary of what came before rather than one more paragraph. */
.sp-journey-recap {
  margin: 0;
  padding: var(--sp-space-small) var(--sp-space);
  border-inline-start: 4px solid var(--sp-border);
  border-radius: var(--sp-radius);
  background-color: var(--sp-surface-sunken);
  color: var(--sp-ink);
  font-weight: 600;
  text-align: start;
}

/* ------------------------------------------- F16, F21 a field in error
 * The error edge GOV.UK draws on a group in error, on the tick row and on the
 * address field; the message itself is the summary's and the inline line's. */
.sp-choice--invalid,
.sp-field--invalid {
  padding-inline-start: var(--sp-space-small);
  border-inline-start: 4px solid var(--sp-error);
}

/* ----------------------------------------------- F18 the interruption
 * The consent text changed under the person: not an error of theirs, so no
 * error colour — a sunken well with a strong edge, focused on load (autofocus
 * + tabindex=-1, no script), above the new texts it introduces. */
.sp-journey-interruption {
  padding: var(--sp-space);
  border-inline-start: 4px solid var(--sp-ink);
  border-radius: var(--sp-radius);
  background-color: var(--sp-surface-sunken);
  color: var(--sp-ink);
  text-align: start;
}

.sp-journey-interruption:focus-visible {
  outline: var(--sp-focus-ring-width) solid var(--sp-focus);
  outline-offset: var(--sp-focus-ring-offset);
}

.sp-journey-interruption .sp-lede {
  margin: 0;
}

/* The one sentence of an interruption or a refusal IS the content of the
 * card, so it is set in full ink, never the kit lede's muted hint colour
 * (spec §2.2: "Lede … ink, never muted"; PT11 moves the kit itself). */
.sp-journey-interruption .sp-lede,
.sp-journey-state .sp-lede {
  color: var(--sp-ink);
}

/* --------------------------------------------- F18, AC-16 the refusal card
 * One way on, and it is the obvious thing on the card: the action takes the
 * card's width, like every journey action. */
.sp-journey-state .sp-button--primary {
  inline-size: 100%;
}

/* ------------------------------------------ F18 the refusal's way on
 * "Choose your language": each language that HAS the text, in its own name,
 * as the same sunken rows the language gate uses. A list without bullets. */
.sp-journey-state__choose {
  margin: 0;
  font-size: var(--sp-font-size-lede);
  font-weight: var(--sp-font-weight-heading);
  line-height: var(--sp-line-height-heading);
  text-align: start;
}

.sp-journey-locales {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space-small);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* ======================================================================
 * PT03 — registration (participant spec P5, P6; F22, F23, F25). Appended
 * as one block so lane A's two packages never edit the same rules.
 * ====================================================================== */

/* ----------------------------------------------- F22 the typo suggestion
 * A status, not an error: the participant may well be right. A sunken well
 * with a quiet edge between the hint and the EMPTY field; the allow-listed
 * domain is the one emphasised line, kept in LTR order by its <bdi>. */
.sp-typo {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space-xsmall);
  margin: 0;
  padding: var(--sp-space-small) var(--sp-space);
  border-inline-start: 4px solid var(--sp-ink-muted);
  border-radius: var(--sp-radius);
  background-color: var(--sp-surface-sunken);
  color: var(--sp-ink);
  text-align: start;
}

.sp-typo p {
  margin: 0;
}

.sp-typo__title {
  font-weight: var(--sp-font-weight-heading);
}

.sp-typo__domain {
  font-size: var(--sp-font-size-lede);
  font-weight: var(--sp-font-weight-heading);
  overflow-wrap: anywhere;
}

.sp-typo__ack {
  margin-block-start: var(--sp-space-small);
}

/* --------------------------------------------------- F23 the profile fold
 * One closed <details> under the field: a full-width row with a touch-target
 * height, so the 60-second path passes it without a second look. Native
 * marker kept (no script, no icon font). */
.sp-profile {
  border: 1px solid var(--sp-border);
  border-radius: var(--sp-radius-card);
  background-color: var(--sp-surface);
  color: var(--sp-ink);
}

.sp-profile__summary {
  display: list-item;
  min-block-size: var(--sp-touch-target);
  padding: var(--sp-space-small) var(--sp-space);
  font-weight: var(--sp-font-weight-heading);
  line-height: var(--sp-line-height-heading);
  text-align: start;
  cursor: pointer;
}

.sp-profile__summary:focus-visible,
.sp-profile__why summary:focus-visible {
  outline: var(--sp-focus-ring-width) solid var(--sp-focus);
  outline-offset: var(--sp-focus-ring-offset);
}

.sp-profile__optional {
  color: var(--sp-ink-muted);
  font-weight: normal;
}

.sp-profile__body {
  padding: 0 var(--sp-space) var(--sp-space);
}

.sp-profile__why {
  margin-block-end: var(--sp-space-small);
  font-size: var(--sp-font-size-meta);
  text-align: start;
}

.sp-profile__why summary {
  min-block-size: var(--sp-touch-target);
  padding-block: var(--sp-space-xsmall);
  color: var(--sp-ink);
  text-decoration: underline;
  text-underline-offset: 0.15em;
  cursor: pointer;
}

.sp-profile__why .sp-prose {
  margin: 0;
}

/* ------------------------------------------- F25 what happens next, receipt
 * After the closure panel: the owner's three steps as a numbered list, then
 * "Your choices" as label/value rows. Each row stacks at 320px and sits side
 * by side from a phone held sideways up. */
.sp-next,
.sp-choices {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space-small);
  text-align: start;
}

.sp-next__heading,
.sp-choices__heading {
  margin: 0;
  font-size: var(--sp-font-size-lede);
  font-weight: var(--sp-font-weight-heading);
  line-height: var(--sp-line-height-heading);
}

.sp-next__list {
  margin: 0;
  padding-inline-start: var(--sp-space-large);
}

.sp-choices__list {
  margin: 0;
  border-block-start: 1px solid var(--sp-border);
}

.sp-choices__row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-space-xsmall) var(--sp-space);
  padding-block: var(--sp-space-small);
  border-block-end: 1px solid var(--sp-border);
}

.sp-choices__row dt {
  flex: 1 1 12em;
  font-weight: var(--sp-font-weight-heading);
}

.sp-choices__row dd {
  flex: 1 1 12em;
  margin: 0;
}

.sp-choices__version {
  color: var(--sp-ink-muted);
  font-size: var(--sp-font-size-meta);
  overflow-wrap: anywhere;
}

.sp-choices__print {
  margin: 0;
  color: var(--sp-ink-muted);
  font-size: var(--sp-font-size-meta);
}

/* Printing needs no button (a print button is script): the page prints as
 * the closure and the receipt, without the skip link, the progress bar or the
 * hint that says it can be printed. The footer's help line stays — on paper
 * it is the one way back to a person. */
@media print {
  .sp-root[data-surface="participant"] .sp-skip,
  .sp-root[data-surface="participant"] .sp-progress-group,
  .sp-choices__print {
    display: none;
  }

  .sp-closure,
  .sp-choices__row {
    break-inside: avoid;
  }
}

/* PARTICIPANT RUNNER — lane C of the participant spec (PT05, W2: the runner
 * frame; spec R3, R7, F27–F31, F42, F48). Served to the participant only,
 * after the kit (`_SHEETS` in `panel_participant_ui/stylesheet.py`); the staff
 * `index.css` never imports it. v3 tokens only (`participant-token-fence`),
 * logical properties only, registered colour pairs only, and indigo only
 * where the accent diet allows it (focus-visible here). The markup is
 * `panel_participant_ui/runner_frame.py`, which the staff preview draws too.
 *
 * GOV.UK's question-page and error-summary patterns, re-built in these tokens
 * (OD-35: patterns, never copied code). */

/* ------------------------------------------------------------ the form
 * The question form is a column: the error sentence, the group, the help,
 * then the controls 24px-ish below the answers (spec R3, "24px above"). */
.sp-runner [data-question-form] {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space);
}

/* ------------------------------------------------- F28 the question <h1>
 * The question IS the page heading, inside its legend or label, so the
 * heading a screen-reader user lands on is the control's own name. */
.sp-question-heading {
  margin: 0;
  color: var(--sp-ink);
  font-size: var(--sp-font-size-heading);
  font-weight: var(--sp-font-weight-heading);
  line-height: var(--sp-line-height-heading);
  text-align: start;
}

/* "(optional)" under the heading, quieter than it, never smaller than body. */
.sp-question-optional {
  display: block;
  margin-block-start: var(--sp-space-xsmall);
  color: var(--sp-ink-muted);
  font-size: var(--sp-font-size-body);
  font-weight: 400;
}

/* ----------------------------------------- F31 the refusal inside its group
 * The kit's hairline (`.sp-rule-h`, decorative by argument), then "prefer not
 * to say" as the group's last row, the same size as every answer (spec R3,
 * "same group, same size"). Only the spacing is the runner's. */
.sp-choice-divider {
  margin-block: var(--sp-space-xsmall);
}

/* ------------------------------------------------------ F30 the rejection
 * The summary is focused on load (`autofocus` + `tabindex=-1`, no script), so
 * its outline is the focus ring. Its link goes to the first control. */
.sp-error-summary {
  padding: var(--sp-space);
  border: 2px solid var(--sp-error);
  border-radius: var(--sp-radius);
  background-color: var(--sp-surface);
  color: var(--sp-ink);
  text-align: start;
}

.sp-error-summary:focus-visible {
  outline: var(--sp-focus-ring-width) solid var(--sp-focus);
  outline-offset: var(--sp-focus-ring-offset);
}

.sp-error-summary__title {
  margin-block: 0 var(--sp-space-small);
  font-size: var(--sp-font-size-lede);
  font-weight: var(--sp-font-weight-heading);
  line-height: var(--sp-line-height-heading);
}

.sp-error-summary__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.sp-error-summary__list a {
  color: var(--sp-error);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

.sp-error-summary__list a:focus-visible {
  outline: var(--sp-focus-ring-width) solid var(--sp-focus);
  outline-offset: var(--sp-focus-ring-offset);
}

/* The same sentence beside the field, with the error edge GOV.UK draws on a
 * group in error. */
.sp-field__error[data-answer-error] {
  margin: 0;
  padding-inline-start: var(--sp-space-small);
  border-inline-start: 4px solid var(--sp-error);
}

/* ----------------------------------------------------- F29 the saved line
 * Quiet, under the progress bar, from screen 2. The tick is drawn, not a
 * glyph (no font carries it in every script), and it is not mirrored in RTL:
 * a tick is not directional. */
.sp-saved-receipt {
  display: flex;
  align-items: center;
  gap: var(--sp-space-small);
  margin: 0;
  color: var(--sp-ink-muted);
  font-size: var(--sp-font-size-meta);
  text-align: start;
}

.sp-saved-receipt::before {
  content: "";
  flex: none;
  inline-size: 0.35em;
  block-size: 0.7em;
  margin-block-start: -0.2em;
  border-block-end: 2px solid currentColor;
  border-inline-end: 2px solid currentColor;
  transform: rotate(45deg);
}

/* In RTL the inline-end border is the left one; mirror the box back first,
 * then turn it, so the tick reads the same way in every script. */
.sp-root[dir="rtl"] .sp-saved-receipt::before {
  transform: rotate(45deg) scaleX(-1);
}

/* --------------------------------------------------- F48 "moved on" notice
 * A status, not an error: the participant did nothing wrong. A sunken well
 * with a quiet edge, above the progress bar. */
.sp-runner-notice {
  margin: 0;
  padding: var(--sp-space-small) var(--sp-space);
  border-inline-start: 4px solid var(--sp-ink-muted);
  border-radius: var(--sp-radius);
  background-color: var(--sp-surface-sunken);
  color: var(--sp-ink);
  font-weight: 600;
  text-align: start;
}

/* ------------------------------------------------- F28, F42 the controls
 * Next is FIRST in the document (a form's implicit submission presses the
 * first submit button, and it used to be Back); it is drawn at the reading
 * end, Back at the reading start. On the first screen there is no Back and
 * Next takes the row. At 320px each takes its own row, Next on top. */
.sp-runner-nav--question {
  flex-direction: row-reverse;
  flex-wrap: wrap;
  margin-block-start: var(--sp-space-small);
}

/* "Stop for now": secondary, on its own line under Back and Next, never
 * competing with Next for the thumb. */
.sp-runner-pause {
  display: flex;
}

.sp-runner-pause .sp-button {
  min-block-size: var(--sp-touch-target);
}

/* --------------------------------------- F42, R7 stop page and welcome back
 * One closure panel: the heading (focused on load), one sentence, one action. */
.sp-closure {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space);
  padding: var(--sp-space-large) var(--sp-space);
  border: 1px solid var(--sp-border);
  border-radius: var(--sp-radius-card);
  background-color: var(--sp-surface);
  color: var(--sp-ink);
  text-align: start;
}

.sp-closure__heading:focus-visible {
  outline: var(--sp-focus-ring-width) solid var(--sp-focus);
  outline-offset: var(--sp-focus-ring-offset);
}

.sp-closure__heading {
  margin: 0;
  font-size: var(--sp-font-size-heading);
  font-weight: var(--sp-font-weight-heading);
  line-height: var(--sp-line-height-heading);
}

.sp-closure .sp-runner-nav .sp-button {
  min-block-size: var(--sp-touch-target);
}

/* ======================================================== PT06 the types
 * Spec R4, F32 F33 F35 F36 F37 (W2). The markup is
 * `panel_participant_ui/runner_types.py`, drawn by the runner and the staff
 * preview alike. Layout only: the cards and tiles' own faces are PT13's
 * (W4); these rules make each reworked type usable and stable at 320px. */

/* ---------------------------------------------- F32 a slider's points
 * Eleven or fewer points are one wrapping row of equal tiles, low to high
 * in reading order (an RTL page mirrors it); each tile is the target. */
.sp-tiles {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-space-small);
}

.sp-tiles > .sp-choice {
  flex: 1 0 calc(var(--sp-touch-target) + var(--sp-space));
  justify-content: center;
  padding-inline: var(--sp-space-small);
}

.sp-number-bounds {
  display: block;
}

/* ------------------------------------------------ F33 the date's parts
 * One row, the locale's order, each part labelled above its field; the
 * row wraps rather than squeezing a label at 320px. */
.sp-date__parts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-space);
}

.sp-date__part {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space-xsmall);
}

.sp-date__part--day .sp-date__input {
  inline-size: 4.5em;
}

.sp-date__part--year .sp-date__input {
  inline-size: 6.5em;
}

.sp-date__select {
  min-block-size: var(--sp-touch-target);
}

/* --------------------------------------------------- F35 image tiles
 * Two to a row, the picture 4:3 on top and the label below it. The `<img>`
 * carries its own width/height, so with `block-size: auto` the browser
 * reserves the box before the bytes arrive (CLS 0); it never overflows the
 * tile. A tile without its picture keeps the same 4:3 frame, alt as text. */
.sp-image-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-space-small);
}

.sp-image-choice {
  flex-direction: column;
  align-items: stretch;
  gap: var(--sp-space-small);
  padding: var(--sp-space-small);
}

.sp-image-choice__media {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  border-radius: var(--sp-radius);
  background-color: var(--sp-surface-sunken);
}

.sp-image-choice__image {
  display: block;
  inline-size: 100%;
  block-size: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

.sp-image-choice__missing {
  padding: var(--sp-space-small);
  overflow-wrap: anywhere;
  text-align: center;
}

.sp-image-choice__row {
  display: flex;
  align-items: center;
  gap: var(--sp-space-small);
  min-inline-size: 0;
}

.sp-image-choice__row .sp-choice__label {
  overflow-wrap: anywhere;
}

/* ------------------------------------------------- F36 the open text
 * The notice is a block of its own above the box (inside its label), and
 * wraps at 320px instead of overflowing (AC-26). */
.sp-open-text__notice {
  display: block;
  margin-block-start: var(--sp-space-xsmall);
  color: var(--sp-ink-muted);
  font-size: var(--sp-font-size-meta);
  font-weight: normal;
  overflow-wrap: anywhere;
}

.sp-open-text__box {
  inline-size: 100%;
  min-block-size: 9rem;
  resize: vertical;
}

/* ------------------------------------------------ F37 the matrix legend
 * The stem small above (an eyebrow), the statement as the `<h1>`, the
 * position "Statement 2 of 5" under it. */
.sp-matrix-stem,
.sp-matrix-position {
  display: block;
  color: var(--sp-ink-muted);
  font-size: var(--sp-font-size-meta);
  font-weight: normal;
}

.sp-matrix-stem {
  margin-block-end: var(--sp-space-xsmall);
}

.sp-matrix-position {
  margin-block-start: var(--sp-space-xsmall);
}

/* ======================================================= PT07 the thank-you
 * Participant spec R8, F51, AC-37. A full bar with the saved tick, then
 * exactly four elements: the closure panel (`--sp-success-surface`, R-19: no
 * new colour), what happens next, how to change your mind, and the one-tap
 * experience question. Untitled UI's "success" empty state and GOV.UK's
 * confirmation page, re-built in these tokens (OD-35: patterns only). No
 * reward, badge or confetti (OD-17): the panel is calm, not celebratory. */

/* The closure panel on the success surface. `--sp-ink` on it is a registered
 * pair ("success banner body"); the heading takes no focus here, so no focus
 * ring is ever drawn on this ground. */
.sp-closure--done {
  border-color: var(--sp-success-surface);
  background-color: var(--sp-success-surface);
  color: var(--sp-ink);
}

/* Elements 2 and 3, and the experience question: a heading-sized label only
 * when the owner's words exist (D-16), then one paragraph. */
.sp-done-part {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space-xsmall);
  margin: 0;
  text-align: start;
}

.sp-done-part > .sp-prose {
  margin: 0;
}

.sp-done-label,
.sp-done-question {
  margin: 0;
  color: var(--sp-ink);
  font-size: var(--sp-font-size-body);
  font-weight: 700;
  line-height: var(--sp-line-height);
}

/* The experience question: options as equal-weight secondary buttons — no
 * filled primary, so no answer is nudged. One column on a phone (each a full
 * thumb target), five across from 40rem. "Prefer not to say" sits under a
 * hairline, on its own row. */
.sp-experience__form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space-small);
  margin-block-start: var(--sp-space-xsmall);
}

.sp-experience__options {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-space-small);
}

.sp-experience__refusal {
  display: flex;
}

.sp-experience__option {
  min-block-size: var(--sp-touch-target);
  inline-size: 100%;
  white-space: normal;
}

.sp-experience__refusal .sp-experience__option {
  inline-size: auto;
}

@media (min-width: 40rem) {
  .sp-experience__options {
    grid-template-columns: repeat(5, minmax(0, 1fr));
  }
}

/* After the tap: the thanks replaces the options and takes focus (no script:
 * `tabindex=-1` + `autofocus`). It sits on the page ground, where the focus
 * ring is a registered pair. */
.sp-experience-thanks {
  margin: 0;
  color: var(--sp-ink);
  font-weight: 600;
}

.sp-experience-thanks:focus-visible {
  outline: var(--sp-focus-ring-width) solid var(--sp-focus);
  outline-offset: var(--sp-focus-ring-offset);
}

/* PARTICIPANT STATES — lane B of the participant spec (PT04, W2; spec §2.2
 * "StateCard", §3 X, P8, P9; F59–F61). Served to the participant only, after
 * the kit (`_SHEETS` in `panel_participant_ui/stylesheet.py`); the staff
 * `index.css` never imports it. v3 tokens only (the participant token fence),
 * logical properties only, `min-width` queries only, no new colour pair: every
 * foreground/ground below is a pair `lib/pairs.ts` already measures in both
 * participant palettes (ink, ink-muted and primary on surface and page;
 * on-primary on primary through `.sp-button--primary`).
 *
 * --------------------------------------------------------------- StateCard
 * Glyph, one short <h1>, one or two sentences, exactly one action or none, the
 * endonym switch, the help line, the help-desk reference. One card, start-
 * aligned (centred text is harder to read in a second language and in RTL),
 * no progress bar, no retry. */
/* The card's face — surface, hairline, radius, shadow — is the kit's own
 * `.sp-card` (the element carries both classes); this rule adds only the
 * StateCard's column. */
.sp-state {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-space);
  margin-block: var(--sp-space-large);
  text-align: start;
}

.sp-state__glyph {
  flex: none;
  inline-size: 3rem;
  block-size: 3rem;
  color: var(--sp-ink);
}

/* The one directional glyph points along the reading direction. */
[dir="rtl"] .sp-state__glyph--arrow {
  scale: -1 1;
}

.sp-state h1 {
  margin: 0;
  font-size: var(--sp-font-size-heading);
  font-weight: var(--sp-font-weight-heading);
  line-height: var(--sp-line-height-heading);
}

/* Until a state's short heading has words, its approved sentence is the <h1>
 * (the "lead with delivered words" rule). A sentence is not a heading in
 * length, so it is set at lede size: still the page's one heading, never a
 * three-line shout. */
.sp-state[data-lead="sentence"] h1 {
  font-size: var(--sp-font-size-lede);
  line-height: var(--sp-line-height-title);
}

.sp-state p {
  margin: 0;
  max-inline-size: var(--sp-measure);
}

.sp-state__action {
  margin-block-start: var(--sp-space-small);
}

.sp-state__action .sp-button {
  min-block-size: var(--sp-touch-target);
}

/* A step only the person can take: words, no control — so it must not look
 * like one. Full ink, the weight of an instruction. */
.sp-state__next {
  font-size: var(--sp-font-size-lede);
  font-weight: 700;
}

.sp-state__languages ul,
.sp-privacy__toc ul {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--sp-space-small);
  margin: 0;
  padding: 0;
  list-style: none;
}

.sp-state__languages a,
.sp-privacy__toc a,
.sp-privacy__back a,
a.sp-door__link {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--sp-touch-target);
  color: var(--sp-primary);
  text-underline-offset: 0.2em;
}

.sp-state__languages a {
  padding-inline: var(--sp-space-xsmall);
}

.sp-state__languages a[aria-current="true"] {
  color: var(--sp-ink);
  font-weight: 700;
  text-decoration: none;
}

.sp-state__help {
  inline-size: 100%;
  padding-block-start: var(--sp-space-small);
  font-weight: 600;
}

.sp-state__ref {
  color: var(--sp-ink-muted);
  font-size: var(--sp-font-size-meta);
}

.sp-state__ref > summary,
.sp-door > summary {
  display: flex;
  align-items: center;
  gap: var(--sp-space-small);
  min-block-size: var(--sp-touch-target);
  cursor: pointer;
}

/* A disclosure must look like one: `display: flex` drops the UA triangle, so
 * the summary draws its own chevron (two borders, no glyph, no image). It
 * points along the reading direction when closed and down when open. */
.sp-state__ref > summary::-webkit-details-marker,
.sp-door > summary::-webkit-details-marker {
  display: none;
}

.sp-state__ref > summary::before,
.sp-door > summary::before {
  content: "";
  flex: none;
  inline-size: 0.5em;
  block-size: 0.5em;
  border-block-end: 2px solid currentColor;
  border-inline-end: 2px solid currentColor;
  rotate: -45deg;
}

[dir="rtl"] .sp-state__ref > summary::before,
[dir="rtl"] .sp-door > summary::before {
  rotate: 135deg;
}

.sp-state__ref[open] > summary::before,
.sp-door[open] > summary::before {
  rotate: 45deg;
}

.sp-state__ref code {
  font-family: var(--sp-font-admin);
  font-size: var(--sp-font-size-body);
  font-weight: 700;
  letter-spacing: var(--sp-letter-spacing-eyebrow);
  color: var(--sp-ink);
}

/* ----------------------------------------------------- P9 the help page
 * "A person can help you": the help line is the hero, set at lede size in
 * full ink on its own card; then
 * the five doors as disclosures (one open at a time where `name` is
 * supported, all openable where it is not). No form, no chatbot, no session. */
.sp-support,
.sp-privacy {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space);
  margin-block: var(--sp-space-large);
  text-align: start;
}

.sp-support h1,
.sp-privacy h1 {
  margin: 0;
  font-size: var(--sp-font-size-heading);
  font-weight: var(--sp-font-weight-heading);
  line-height: var(--sp-line-height-heading);
}

.sp-support p,
.sp-privacy p {
  margin: 0;
}

/* Face from the kit's `.sp-card`, like the StateCard. */
.sp-support__person {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space-small);
}

.sp-support__line {
  font-size: var(--sp-font-size-lede);
  font-weight: 700;
}

.sp-support__doors,
.sp-privacy__sections {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space-small);
  margin: 0;
  padding: 0;
  list-style: none;
}

.sp-door {
  background-color: var(--sp-surface);
  color: var(--sp-ink);
  border: 1px solid var(--sp-border);
  border-radius: var(--sp-radius);
}

.sp-door > summary {
  padding-inline: var(--sp-space);
  font-weight: 600;
}

.sp-door__body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-space-small);
  padding-block: var(--sp-space-small) var(--sp-space);
  padding-inline: var(--sp-space);
}

/* ----------------------------------------------------- P8 the privacy notice
 * Back to where you were, the heading, the version line, "On this page", and
 * the sections as disclosures. The notice's own prose keeps the reading
 * measure and full ink — it is the text a reader has a right to understand. */
.sp-privacy__version {
  color: var(--sp-ink-muted);
  font-size: var(--sp-font-size-meta);
}

.sp-privacy__notice {
  max-inline-size: var(--sp-measure);
  color: var(--sp-ink);
}

@media (min-width: 40rem) {
  .sp-root[data-surface="participant"] .sp-state,
  .sp-root[data-surface="participant"] .sp-support__person {
    padding-inline: var(--sp-space-large);
  }
}
