/* The token layer. Spec §7.1 (colour), §7.3 (type and spacing), §9.1 (light
 * theme is a token swap). NOTHING ELSE IN THE CLIENT MAY CONTAIN A HEX VALUE —
 * test/ui.base.test.mjs enforces that, because a host renaming a resource or a
 * consortium adjusting the palette must be one edit and not sixty.
 *
 * Every contrast ratio below was recomputed from its hex pair against the WCAG
 * 2.x definitions, not copied from the spec table. Two of the spec's stated
 * numbers are rounded wrong (player 6.9 -> 6.83, host 10.5 -> 10.73); the
 * numbers here are the computed ones and test/ui.tokens.test.mjs recomputes
 * them on every run, so they cannot drift back.
 */

:root {
  /* --- surfaces ---------------------------------------------------------- */
  --canvas: #0A0D0B;
  --surface: #121814;
  /* 1.27:1 on canvas. A decorative card edge and nothing more. Do not use it
   * to bound an input: WCAG 2.2 SC 1.4.11 wants 3:1 for anything that
   * identifies a control, and the surface fill is only 1.08:1 so it cannot
   * carry the boundary either. --line-strong is the one that can. */
  --line: #1E2721;
  --line-strong: #5A6A60;   /* 3.14:1 on surface, 3.41:1 on canvas */

  /* --- text -------------------------------------------------------------- */
  --ink: #E8EDE9;           /* 16.48:1 on canvas */
  --mute: #7C8A80;          /*  5.40:1 on canvas, 4.98:1 on surface */

  /* --- roles ------------------------------------------------------------- */
  /* Matched chroma (~0.19 OKLCH), which is what makes three very different
   * hues read as one family. A ~0.10-chroma moss green was rejected for
   * reading as a different palette beside a near-neon orange. */
  --role-player: #FF6A2B;   /*  6.83:1 on canvas */
  --role-host: #1FDB86;     /* 10.73:1 on canvas */
  --role-expert: #C56BFF;   /*  6.44:1 on canvas */

  /* --- states ------------------------------------------------------------ */
  /* Identical across all three roles, so meaning never shifts with context.
   * Each is always accompanied by a glyph AND a word — see ui/components.js's
   * GLYPHS table. The colour is reinforcement, never the signal. */
  --warn: #FFC94D;          /* 12.75:1 — dashed outline + the warn glyph */
  --risk: #FF5470;          /*  6.28:1 — fill + the risk glyph */
  /* Deliberately NOT a colour. Green belongs to the host role, and a finished
   * thing should stop competing for attention, so done recedes to mute with a
   * check. Same value as --mute, separate name so the intent is legible. */
  --done: #7C8A80;
  /* The QR quiet zone. A scanner needs the margin to contrast with the symbol, so
   * this is the one place a QR pins a colour rather than inheriting one — and it is
   * still a token, so the light theme swaps it like everything else. */
  --qr-quiet: #121814;

  /* The loading placeholder's fill. It CANNOT be --surface, which is what
   * base.css used to paint: an archetype F panel is itself --surface, so the
   * skeleton bars inside it were the same rgb(18,24,20) as the panel behind them
   * and a sighted user saw an empty box — measured in the Browser pane. A and B
   * sit on --canvas, where --surface is only 1.08:1, which is barely better.
   *
   * Derived from --ink and --surface rather than written twice, so the light
   * theme gets its own value for free from the swap and cannot be forgotten.
   * 20 % is bounded on both sides and ui.base.test.mjs recomputes both bounds:
   * above ~1.3:1 against BOTH backgrounds so the bar is perceptible on a card and
   * on the page, and below 3:1 in both themes so it can never read as the
   * boundary of a control — §7.7 forbids a tint or an edge from being the thing
   * that identifies one, and a placeholder is not a control at all. */
  --skeleton: color-mix(in srgb, var(--ink) 20%, var(--surface));

  /* --- the active role, set by the shell on <html data-role> -------------- */
  /* Role colour follows whose device it is. Every component reads --role and
   * never a specific role token, so one shell serves three themes. */
  --role: var(--role-player);

  /* --- type -------------------------------------------------------------- */
  /* GREEK IS DRAWN BY THE TAIL OF THIS LIST, not by the vendored face.
   * Hanken Grotesk publishes no Greek subset — measured 2026-09-21 from the
   * Google Fonts css2 endpoint and from google/fonts' own METADATA.pb, which
   * lists cyrillic-ext, latin, latin-ext, menu and vietnamese and nothing
   * else — and §7.3 names one typeface, so borrowing a Greek face from
   * another foundry is not on the table. A @font-face whose unicode-range
   * excludes a code point is skipped entirely, so every Greek character the
   * el-XA pseudo-locale draws arrives here instead. 'Noto Sans' is named for
   * that job: it is the one family in the list whose purpose is script
   * coverage, and it costs nothing because none of these is downloaded.
   * test/ui.base.test.mjs pins the list whole, and says plainly that pinning
   * the declaration is not the same as proving an installed face has the
   * glyph — that needs a device, and none of these four has had one. */
  --font-body: 'Hanken Grotesk', system-ui, -apple-system, 'Segoe UI', 'Noto Sans', sans-serif;
  --type-display: 34px;
  --type-h1: 26px;
  --type-h2: 19px;
  --type-body: 16px;        /* the floor, no exceptions — §9.2 */
  --type-meta: 13px;        /* chrome only, and only at >= 4.5:1 */
  --type-micro: 11px;
  /* Outlier 2's own scale (Task 17) — ruling 6: readable at ten metres, the
   * one screen whose viewing distance is fixed by its deployment rather than
   * by how close a phone is held. Signage guidance for text glanced at
   * rather than read closely is roughly 25 mm of character height per 3 m of
   * distance; at 10 m that is ~85 mm, which prints at roughly 88 CSS px on a
   * typical projector. Declared once, used once, at the one breakpoint this
   * screen is built for. */
  --type-jumbo: 88px;
  /* The same wall at fifteen rows a column. The frame leaves 908 px for the
   * list (see ui/base.css's budget), and fifteen rows plus their gaps have to
   * fit inside it: 15 x (56 x 0.86) + 14 x 8 = 834, which leaves 74 px — the
   * same spare the ten-row bands have. 60 px — the size the same signage rule
   * of thumb suggests for seven metres — comes to 15 x 51.6 + 112 = 886 and
   * leaves 22, and a projector whose real resolution nobody controls in
   * advance is not the place to spend the last pixel. 56 px reads at roughly
   * six and a half metres by that same rule, which is the price of showing all
   * thirty teams at once instead of ten of them at a time.
   *
   * RECOUNTED IN THE SPACING PASS and the answer did not move. That pass hid
   * the wall's two chrome links and gave the header's 16 px back, so the
   * budget went 892 -> 908 and 60 px went from 6 px of margin to 22. It also
   * corrected the arithmetic this comment used to carry: 60 px "lands at 892
   * exactly" was wrong — it lands at 886. 22 px of a 1,080 px frame is still
   * inside the error bar on whether the projector is really 1080p. */
  --type-jumbo-dense: 56px;
  --weight-body: 400;
  --weight-h2: 700;
  --weight-h1: 800;
  --weight-display: 800;
  --weight-meta: 600;
  --weight-micro: 800;
  --tracking-display: -0.042em;   /* §7.3: -4.2 % */
  --tracking-micro: 0.08em;
  --leading-display: 0.86;
  --leading-body: 1.5;

  /* --- space ------------------------------------------------------------- */
  /* THE 4 PT GRID, reconciled with the measurement in the spacing pass on
   * branch qa-playable-and-consistent. §7.3 said "8 pt throughout; 4 px
   * permitted only for hairlines and icon insets", and the client has never
   * been that: --hairline is used for the halo spread, the tile gap, the card
   * gap, the chip and badge padding, the focus offset and the untranslated
   * underline offset, and --tap is 44 px — WCAG 2.2 SC 2.5.8's floor, the most
   * repeated dimension in the product, and a number that can never move onto an
   * 8 pt step. A grid whose most common box violates it is not the grid the
   * codebase follows. Every declared value in the client is a multiple of 4, so
   * the description changed and NOT ONE VALUE DID: nothing became permissible
   * that was not already there, and the steps below are unchanged.
   * test/ui.tokens.test.mjs asserts the modulus. */
  --hairline: 4px;          /* step 0 of the scale, for hairlines and insets */
  --space-1: 8px;
  --space-2: 16px;
  --space-3: 24px;
  --space-4: 32px;
  --space-5: 48px;
  /* --space-6 (64px) WAS DECLARED HERE AND REMOVED IN THE SAME PASS: zero
   * consumers in the whole client since 0c, kept alive only by the test that
   * listed it. A scale step nothing reaches is a scale step nobody has decided
   * anything with. Add it back the day something needs 64. */

  /* --- controls ---------------------------------------------------------- */
  --tap: 44px;              /* §9.1 minimum, applied by .tap in base.css */
  --radius: 12px;
  --radius-pill: 999px;
  --focus-width: 2px;

  /* --- motion ------------------------------------------------------------ */
  --motion-fast: 120ms;
  --motion-base: 220ms;
  --easing: cubic-bezier(0.2, 0, 0, 1);

  /* --- depth ------------------------------------------------------------- */
  /* §7.7. The approved mockups carry a depth layer that §7.1 never recorded, so
   * nothing built it: an elevation shadow under a floating panel, a role-coloured
   * glow on the one primary action and on a selected control, and a radial wash at
   * the top of the page. Seven tokens, six uses, and every alpha below was chosen
   * by computing the COMPOSITED colour and re-checking the pair on top of it — a
   * gradient changes the effective background, so the base token's ratio is no
   * longer the ratio that holds. ui.tokens.test.mjs recomputes all of them.
   *
   * Geometry is deliberately NOT on the 8 pt grid: a blur radius is not a layout
   * dimension. Two exceptions, both load-bearing: the glow spread is --hairline so
   * it cannot cross --space-1, the gap between two cards, and reach a neighbour's
   * boundary; the accent glow's 24 px blur reaches 12 px, which stays inside the
   * --space-2 gap the shell footer puts between two buttons. Those two numbers are
   * why no --line-strong boundary anywhere sits in a tinted band. */
  --shadow-tint: #000000;   /* a true black key light, on a near-black canvas */
  --shadow-key: color-mix(in srgb, var(--shadow-tint) 32%, transparent);
  --shadow-key-deep: color-mix(in srgb, var(--shadow-tint) 60%, transparent);
  /* Resting elevation for something that floats over content: one shadow, one
   * layer. On this canvas it is visible where lighter content sits behind the
   * panel, which is exactly where the cue is needed; the panel's --line-strong rim
   * remains the boundary and the shadow never has to carry it. */
  --elev-1: 0 14px 38px var(--shadow-key);
  --elev-2: 0 20px 50px var(--shadow-key-deep);
  /* The single primary action. Blurred, because a lit CTA is what the mockup's
   * ignition button is, and offset downward so it reads as light from above.
   * The mockup's 0 8px 24px is tightened to 0 4px 16px, which is geometry doing
   * accessibility work: a blur reaches half its radius, so this band ends 8 px out
   * — the smallest gap anywhere in base.css — and no neighbouring label or
   * --line-strong boundary can sit in it. It could not survive one if it did:
   * --mute in this band is 4.40:1 and --line-strong 2.78:1. */
  --glow-accent: 0 4px 16px color-mix(in srgb, var(--role) 22%, transparent);
  /* Selected and pressed. Spread with ZERO blur — the cheapest shadow a GPU can
   * draw — and hard-edged, so it stops dead at --hairline instead of decaying
   * across a neighbour's border. */
  --glow-halo: 0 0 0 var(--hairline) color-mix(in srgb, var(--role) 16%, transparent);
  /* The top-of-page wash. 6 %, not the mockup's 13 %: at 13 % the page under the
   * host role takes --mute to 4.34:1 and --line-strong to 2.74:1. */
  --wash-page: radial-gradient(130% 48% at 50% -6%, color-mix(in srgb, var(--role) 6%, transparent), transparent 62%);
}

/* Role themes. The shell writes data-role on <html>; nothing else selects a
 * role, so a shared surface resolves its colour by whose screen it is. */
:root[data-role="player"] { --role: var(--role-player); }
:root[data-role="host"]   { --role: var(--role-host); }
:root[data-role="expert"] { --role: var(--role-expert); }

/* The light theme, for bright venues and for users who need it. §9.1 calls it a
 * token swap and this is what makes that true: the same names, different values,
 * and not one selector anywhere else changes. Twenty-one declarations — fourteen
 * colour tokens and the seven depth tokens §7.7 added below. It said "the same
 * eleven names" from 0c until phase 5 counted them; --skeleton is the only
 * colour-valued token in :root with no line here, and that is deliberate (it is
 * a color-mix over --ink and --surface, which both swap, so writing it once is
 * what makes it resolve differently in each theme).
 *
 * The role colours CANNOT survive the swap unchanged — #FF6A2B on #F4F6F4 is
 * 2.63:1, which fails body text badly. These are darkened mirrors in the same
 * hue family, each recomputed against #F4F6F4.
 *
 * Driven by data-theme and NOT by prefers-color-scheme: dark is the canonical
 * identity, a venue projector reads better dark, and the host chooses. Phase 5
 * owns the settings control and the full light pass; 0c owns only the seam. */
:root[data-theme="light"] {
  --canvas: #F4F6F4;
  --surface: #FFFFFF;
  --line: #D5DCD7;          /* 1.28:1 on canvas-light, 1.40:1 on white — decorative,
                               same role as dark --line, and both now recomputed */
  --line-strong: #7B8880;   /* 3.41:1 on canvas-light, 3.70:1 on white */
  --ink: #141815;           /* 16.51:1 */
  --mute: #4A574E;          /*  6.99:1 */
  --role-player: #B23C00;   /*  5.46:1 */
  --role-host: #0A6B41;     /*  6.06:1 */
  --role-expert: #6B2FA8;   /*  7.44:1 */
  --warn: #7A5300;          /*  6.31:1 */
  --risk: #C2183A;          /*  5.55:1 */
  --done: #4A574E;          /* still --mute's value, still not a colour */
  --qr-quiet: #FFFFFF;      /* the canvas colour: an opaque margin, still a token */

  /* Depth cannot survive the swap either, and it fails in the opposite direction
   * from the palette: a shadow tuned for a near-black canvas reads as DIRT on a
   * bright one. Every value below is derived, not reused.
   *
   * The shadow is ink-tinted rather than black, because a pure-black shadow on a
   * warm-white canvas greys the page instead of shading it. The alphas drop hard:
   * --line-strong is only 3.41:1 on this canvas, so a dark band over it at more
   * than ~6 % takes a control's boundary below SC 1.4.11's 3:1. A drop shadow's
   * peak sits UNDER the element casting it, which is opaque, and the band visible
   * beside it is at most half the peak — so the ceiling on the declared alpha is
   * twice that, and 12 % is where --elev-2 lands.
   *
   * The glows go the other way and get MORE alpha: the light role colours are
   * darkened mirrors, and a dark tint at 16 % over white is barely a tint. The
   * wash gets less: on a light canvas it darkens rather than lifts, and --risk
   * (5.55:1) is what runs out first. */
  --shadow-tint: #141815;   /* --ink's value: a shaded page, not a grey one */
  --shadow-key: color-mix(in srgb, var(--shadow-tint) 8%, transparent);
  --shadow-key-deep: color-mix(in srgb, var(--shadow-tint) 12%, transparent);
  --elev-1: 0 4px 12px var(--shadow-key);
  --elev-2: 0 10px 28px var(--shadow-key-deep);
  --glow-accent: 0 4px 12px color-mix(in srgb, var(--role) 30%, transparent);
  --glow-halo: 0 0 0 var(--hairline) color-mix(in srgb, var(--role) 22%, transparent);
  --wash-page: radial-gradient(130% 48% at 50% -6%, color-mix(in srgb, var(--role) 5%, transparent), transparent 62%);
}
