/* 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.
 */

/* ---------------------------------------------------------------- 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 {
  /* Grounds */
  --sp-page: #F4F5F8;
  --sp-surface: #FFFFFF;
  --sp-surface-sunken: #F7F7FB;

  /* Ink */
  --sp-ink: #211F49;
  --sp-ink-muted: #6E6C8A;              /* 5.03:1 on --sp-surface */

  /* Brand */
  --sp-primary: #2D2A5E;
  --sp-primary-strong: #211F49;         /* pressed/hover face — 15.48:1 with --sp-on-primary */
  --sp-on-primary: #FFFFFF;
  --sp-accent: #00E5A0;
  --sp-on-accent: #123B2E;              /* 7.52:1 on --sp-accent */

  /* 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: #2D2A5E;

  /* 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: #8B89A6;
  --sp-rule: #E7E8F0;

  /* 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: #211F4914;
  --sp-shadow-strong: #211F4926;

  /* Chrome */
  --sp-sidebar: #211F49;
  --sp-sidebar-raised: #2D2A5E;
  --sp-on-sidebar: #FFFFFF;
  --sp-on-sidebar-muted: #B9B8CC;

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

  /* 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;

  /* 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;
  --sp-font-size-eyebrow: 0.65625rem;
  --sp-font-size-lede: 1.125rem;
  --sp-font-size-heading: 1.75rem;
  --sp-font-size-wordmark: 1.0625rem;
  --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;
}

/* -------------------------------------------- 2. PARTICIPANT GROUND (light) */
/* The participant surface sits on a darker ground, on which two base tokens
 * drop below their floor (#6E6C8A -> 4.19:1, #8B89A6 -> 2.81:1). */
[data-surface="participant"] {
  --sp-page: #E9EAF0;
  --sp-ink-muted: #5A5878;              /* 5.64:1 on --sp-page */
  --sp-border: #83819E;                 /* 3.12:1 on --sp-page */
}

/* ------------------------------------------------- 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"]) {
    --sp-page: #131226;
    --sp-surface: #1C1B36;
    --sp-surface-sunken: #16152D;
    --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-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: #B8FFE4;
    --sp-border: #56547A;
    --sp-rule: #2B2A4B;
    --sp-shadow: #00000059;
    --sp-shadow-strong: #0000008C;
    --sp-sidebar: #0E0D1E;
    --sp-sidebar-raised: #1C1B36;
    --sp-on-sidebar: #ECEBF6;
    --sp-on-sidebar-muted: #A7A5C4;
    --sp-viz-indigo: #8B88E8;
    --sp-viz-mint: #00E5A0;
    --sp-viz-amber: #F2B33D;
    --sp-viz-magenta: #E88BBB;
    --sp-viz-cyan: #5ECFD3;
    --sp-viz-olive: #B9C15A;
  }
}

/* ------------------------------------------------ 4. DARK, explicit at :root */
:root[data-theme="dark"] {
  --sp-page: #131226;
  --sp-surface: #1C1B36;
  --sp-surface-sunken: #16152D;
  --sp-ink: #ECEBF6;
  --sp-ink-muted: #A7A5C4;
  --sp-primary: #A9A6E8;
  --sp-primary-strong: #C6C4F2;         /* derived — 11.01:1 with --sp-on-primary */
  --sp-on-primary: #131226;             /* derived — 8.15:1 on --sp-primary */
  --sp-accent: #00E5A0;
  --sp-on-accent: #0B2A20;
  --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: #B8FFE4;
  --sp-border: #56547A;
  --sp-rule: #2B2A4B;                   /* derived — decorative, deliberately unchecked */
  --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: #0E0D1E;                /* derived */
  --sp-sidebar-raised: #1C1B36;         /* derived */
  --sp-on-sidebar: #ECEBF6;             /* derived — 16.24:1 on --sp-sidebar */
  --sp-on-sidebar-muted: #A7A5C4;       /* derived — 8.06:1 on --sp-sidebar */
  --sp-viz-indigo: #8B88E8;
  --sp-viz-mint: #00E5A0;
  --sp-viz-amber: #F2B33D;
  --sp-viz-magenta: #E88BBB;
  --sp-viz-cyan: #5ECFD3;
  --sp-viz-olive: #B9C15A;
}

/* ---------------------------------------------- 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"] {
  --sp-page: #131226;
  --sp-surface: #1C1B36;
  --sp-surface-sunken: #16152D;
  --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-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: #B8FFE4;
  --sp-border: #56547A;
  --sp-rule: #2B2A4B;
  --sp-shadow: #00000059;
  --sp-shadow-strong: #0000008C;
  --sp-sidebar: #0E0D1E;
  --sp-sidebar-raised: #1C1B36;
  --sp-on-sidebar: #ECEBF6;
  --sp-on-sidebar-muted: #A7A5C4;
  --sp-viz-indigo: #8B88E8;
  --sp-viz-mint: #00E5A0;
  --sp-viz-amber: #F2B33D;
  --sp-viz-magenta: #E88BBB;
  --sp-viz-cyan: #5ECFD3;
  --sp-viz-olive: #B9C15A;
}

/* --------------------------------------------- 6. PARTICIPANT GROUND (dark) */
/* Only the ground moves: the dark --sp-ink-muted and --sp-border from blocks
 * 3-5 already clear their floors against this darker ground. */
@media (prefers-color-scheme: dark) {
  :root[data-surface="participant"]:not([data-theme="light"]) {
    --sp-page: #0E0D1E;
  }
}

[data-surface="participant"][data-theme="dark"] {
  --sp-page: #0E0D1E;                   /* derived — --sp-ink 16.24:1, --sp-ink-muted 8.06:1 */
}

/* -------------------------------------------------- 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"] {
  --sp-page: #F4F5F8;
  --sp-surface: #FFFFFF;
  --sp-surface-sunken: #F7F7FB;
  --sp-ink: #211F49;
  --sp-ink-muted: #6E6C8A;
  --sp-primary: #2D2A5E;
  --sp-primary-strong: #211F49;
  --sp-on-primary: #FFFFFF;
  --sp-accent: #00E5A0;
  --sp-on-accent: #123B2E;
  --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: #2D2A5E;
  --sp-border: #8B89A6;
  --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;
  --sp-viz-indigo: #5B57C7;
  --sp-viz-mint: #00A874;
  --sp-viz-amber: #B07C10;
  --sp-viz-magenta: #B34A82;
  --sp-viz-cyan: #1A7F93;
  --sp-viz-olive: #6B7020;
}

/* -------------------------------------- 8. PARTICIPANT LIGHT ISLAND IN DARK */
[data-surface="participant"][data-theme="light"] {
  --sp-page: #E9EAF0;
  --sp-ink-muted: #5A5878;
  --sp-border: #83819E;
}

/* 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, sits
 * above the panel's own mark rather than below it, and has no modifier that
 * quiets it — the same rule the OD-02 consent banner lives under.
 *
 * 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-choice:focus-within {
  outline: var(--sp-focus-ring-width) solid var(--sp-focus);
  outline-offset: var(--sp-focus-ring-offset);
}

/* -------------------------------------------------------------------- button */
.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-pill);
  font: inherit;
  font-weight: 600;
  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. */
  transition:
    background-color 120ms ease,
    border-color 120ms ease,
    box-shadow 120ms ease,
    translate 120ms ease;
}

/* A filled control is the one thing on the screen that must read as pressable
 * before it is pressed, so the primary gets the only real elevation in the
 * system. --sp-shadow is decorative and carries alpha; the 4.5:1 that matters
 * is --sp-on-primary on --sp-primary, which the contrast matrix measures. */
.sp-button--primary {
  background-color: var(--sp-primary);
  color: var(--sp-on-primary);
  box-shadow:
    0 1px 2px var(--sp-shadow),
    0 4px 12px var(--sp-shadow-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);
}

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

.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;
}

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

.sp-button[disabled] {
  cursor: not-allowed;
  opacity: 0.6;
}

/* 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. */
.sp-field__label {
  font-weight: 600;
  text-align: start;
}

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

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

.sp-input {
  min-block-size: var(--sp-touch-target);
  inline-size: 100%;
  padding-block: var(--sp-space-small);
  padding-inline: var(--sp-space-small);
  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);
}

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

/* 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;
  transition:
    background-color 120ms ease,
    border-color 120ms ease;
}

.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);
}

.sp-choice__control {
  inline-size: 1.5rem;
  block-size: 1.5rem;
  accent-color: var(--sp-primary);
  flex: none;
}

.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, it clears that against every ground here,
   * and --sp-primary clears 3:1 against IT — both measured by the matrix. */
  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-primary);
}

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

/* 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);
}

/* --------------------------------------------------------------------- badge */
.sp-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-space-small);
  padding-block: 0.125rem;
  padding-inline: var(--sp-space-small);
  border-radius: var(--sp-radius-pill);
  font-size: var(--sp-font-size-body);
  font-weight: 600;
}

.sp-badge--neutral {
  background-color: var(--sp-surface-sunken);
  color: var(--sp-ink);
}

.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);
}

/* 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. */
.sp-badge--protected {
  background-color: var(--sp-surface-sunken);
  color: var(--sp-ink-muted);
  border: 1px solid var(--sp-border);
}

/* Rendered on every aggregate view whether or not a cell is currently
 * suppressed: copy that appears only when suppression fires is a signal. */
.sp-suppression-legend {
  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. */
.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 10px 28px 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);
}

/* ------------------------------------------------------ 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. */
.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;
  }
}
