/* WEFT design tokens — single source (Constitution §5). No magic values in components. */

/* Self-hosted fonts. Loading these from fonts.googleapis.com sent every visitor's IP to
   Google, which LG Munchen I (3 O 17493/20, 2022) found unlawful without consent for German
   visitors — and this app is aimed at German learners. Serving them ourselves also drops a
   render-blocking third-party stylesheet and lets the service worker cache them offline,
   which it could not do while they were cross-origin.
   Archivo ships the `latin` subset (covers a o u umlauts and eszett). Noto Sans Georgian
   ships a Mkhedruli subset gated by unicode-range so non-Georgian visits do not download it.
   Archivo Turkish is the same trick for the five letters `latin` lacks (ğ Ğ İ ş Ş), subset
   out of Archivo's own latin-ext so Turkish glosses keep the same voice. Licence: fonts/OFL.txt. */

/* Archivo is a VARIABLE font: one file serves 400/500/600, so the browser interpolates real
   weights instead of synthesising them. Hence a weight RANGE, not three @font-face blocks. */
@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("./fonts/archivo-400-500-600.woff2") format("woff2");
}

/* IBM Plex Mono ships static instances, so each weight is its own file. */
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("./fonts/ibm-plex-mono-400.woff2") format("woff2");
}

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("./fonts/ibm-plex-mono-500.woff2") format("woff2");
}

/* Noto Sans Georgian — Mkhedruli (+ Mtavruli) only. unicode-range is the load gate: the browser
   fetches this file only when a Georgian codepoint is painted, so the ~100% of players who never
   see ქართული do not pay for it. Do NOT preload it from index.html. The --font stack names the
   family after the Latin faces so Archivo keeps Latin; this face is reached only by glyphs
   Archivo cannot draw. Subset + OFL: fonts/noto-sans-georgian-400.woff2, fonts/OFL.txt. */
@font-face {
  font-family: "Noto Sans Georgian";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("./fonts/noto-sans-georgian-400.woff2") format("woff2");
  unicode-range: U+10A0-10FF, U+1C90-1CBF;
}

/* Archivo Turkish — the same variable Archivo, subset to exactly the codepoints the `latin`
   file cannot draw (ğ Ğ İ ş Ş; dotless ı and ç/ö/ü are already in latin). unicode-range gates
   both the download and the claim: a page without those letters never fetches the file, and a
   codepoint outside the range can never be stolen from Archivo. OFL, same as the other faces. */
@font-face {
  font-family: "Archivo Turkish";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("./fonts/archivo-turkish-ext.woff2") format("woff2");
  unicode-range: U+011E-011F, U+0130, U+015E-015F;
}

:root {
  /* Spacing — 4px base */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;
  --space-12: 48px;

  /* Radii */
  --radius-xs: 4px;
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-pill: 999px;
  /* Board tile radius — softer than radius-sm so the hero grid reads as tactile keys, still
     crisp enough not to look bubbly at small tile sizes. */
  --radius-tile: 10px;

  /* Motion */
  --duration-fast: 120ms;
  --duration-normal: 200ms;
  --duration-slow: 300ms;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);

  /* Typography */
  /* Fallbacks are per-CHARACTER, not per-stack: the browser walks this list for every glyph. Our
     Archivo woff2 declares no unicode-range and carries no emoji, so ✓ → 🎉 🏆 🔥 always fall past
     it. The old tail ("Helvetica Neue", Helvetica, Arial) names nothing that exists on Android, so
     those glyphs landed on an arbitrary UA pick at a different baseline and weight — the "damaged
     symbols" in the Android simulation. Roboto (Android) and -apple-system (iOS) give Latin text a
     real second choice; the emoji families go LAST so they are only ever reached by glyphs no text
     font can draw. Do not move them earlier — an emoji font ahead of a text font hijacks ✓ and →.

     Georgian sits between the Latin faces and the generic, for the same reason emoji sit after it.
     Archivo ships the `latin` subset only, so Mkhedruli would otherwise resolve through whatever
     `sans-serif` happens to be, which on Windows is not a Georgian face at all. Naming the faces
     AFTER the Latin stack means they are reached only by glyphs Archivo cannot draw; naming them
     before it would let Noto Sans Georgian, which also carries Latin, take over the whole UI on any
     machine where the Archivo file fails to load. */
  --font: "Archivo", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
    "Noto Sans Georgian", "Archivo Turkish", "BPG Glaho", Sylfaen,
    sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji";
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
    "Roboto Mono", "Archivo Turkish", "Noto Sans Georgian", Sylfaen,
    monospace, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji";
  --text-xs: 12px;
  --text-sm: 14px;
  --text-base: 16px;
  --text-lg: 20px;
  --text-xl: 24px;
  --text-2xl: 32px;
  --text-3xl: 44px;
  /* Weight + tracking + leading scale — a single source so headings, labels and body lead with
     intent instead of every component picking a raw 600/700/800 and an ad-hoc letter-spacing. */
  --weight-regular: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;
  --weight-heavy: 800;
  --tracking-tight: -0.01em;
  --tracking-normal: 0;
  --tracking-wide: 0.02em;
  --tracking-caps: 0.08em;
  --leading-tight: 1.15;
  --leading-snug: 1.3;
  --leading-normal: 1.5;

  /* Layout */
  --header-h: 56px;
  --rail-w: 340px;
  --learn-w: 380px;
  /* The board is height-bound on a wide-but-short laptop (~680px), so a 1280px frame stranded a
     void on either flank of the board+rail cluster. Capping at 1100px sizes the frame TO the
     content: board + wider rail now fill the width and the outer margin reads as one clean
     symmetric gutter instead of accidental dead space. */
  --app-max: 1240px;
  /* The puzzle's visual ceiling on wide screens. Without it the board stretches to fill whatever
     grid track it lands in, so on a big monitor a 5x5 grid balloons to 120px tiles that read as a
     toy. Capping + centring the .grid-wrap at this value keeps tiles at a comfortable ~90px max and
     turns the leftover column space into deliberate framing instead of a starved-or-stretched
     board. resizeGrid() measures the (now-capped) grid-wrap box, so tile math needs no change. */
  --board-max: 600px;
  /* Legibility floor, NOT a tap-target floor. This was 48px and was enforced as a hard
     minimum, which forced a 7x7 grid wider than a 390px phone and put the last column out of
     reach entirely. A 42px cell you can touch beats a 48px cell you cannot. See fitTileSize()
     in lib/weft-core.js. */
  --tile-min: 28px;
  --touch-min: 44px;
  /* Elevation model is per-theme. On DARK, separation comes from the tonal lightness ladder
     (--bg 14% -> --surface 18% -> --surface-raised 22%, ~4% steps) plus a --line-ui hairline —
     a neutral drop shadow reads as muddy grime on a near-black surface, so there is none. The
     hell block below overrides this with a two-layer warm ambient shadow, because on near-white
     paper a contour alone is too subtle and a real shadow is what sells depth. */
  --shadow-raised: none;
  /* Elevation for the two hero surfaces (board tiles, cards). On the near-black dark theme a
     diffuse drop shadow reads as mud (hence --shadow-raised:none above), so depth is sold by an
     INSET TOP-HIGHLIGHT — a 1px lighter top edge, the modern dark-UI cue — plus only a whisper of
     drop shadow for grounding. The hell block overrides both with real warm shadows. */
  --elev-tile: inset 0 1px 0 oklch(40% 0.012 80 / 0.55), 0 1px 2px oklch(0% 0 0 / 0.35);
  --elev-card: inset 0 1px 0 oklch(100% 0 0 / 0.04), 0 6px 20px oklch(0% 0 0 / 0.22);

  /* —— BROWSER FLOOR: read before "adding color-mix() fallbacks" ——————————————————————
     The ENTIRE palette below is bare oklch() with no fallback — --bg, --text, --accent, --tile,
     in both themes. Modern colour syntax is therefore a hard dependency of this app, not an
     enhancement: on an engine that cannot parse oklch() there is no background and no text
     colour, and nothing else you do to the CSS matters.

     Consequence (G5): adding fallbacks to the 59 color-mix() sites *in isolation* buys nothing.
     color-mix() and oklch() belong to the same generation of colour syntax and land together in
     practice, so any engine that drops a color-mix() declaration has already dropped these token
     definitions — you would get correctly-mixed chips on a page with no background and no text.
     That work is a FALSE SAFETY: it looks like compatibility and delivers none.

     So the two are coupled. If the support matrix ever has to reach older WebViews, the fix is
     one @supports block redefining these tokens in hex FIRST, and only then the color-mix sites.
     Do not do half of it and record the app as compatible. minSdkVersion is currently 23
     (mobile/android/variables.gradle) which claims far more reach than this palette delivers —
     that mismatch is an open owner decision, not something to paper over in CSS. */

  /* Dark theme (default — Constitution: dark base retained).
     color-scheme has to be declared here, not only under [data-theme="dark"]: the toggle turns
     dark on by REMOVING the attribute, so that selector never matched in the default state and
     the browser kept painting scrollbars, form controls and the tap-highlight light-on-dark. */
  color-scheme: dark;
  /* Palette direction C "Nachtzug" (night indigo) — owner decision 2026-10-20. Exact hex from
     .claude/agent-out/palettes/palettes.html (dark column); see also hell block below for the
     light-mode direction A "Ostsee". */
  --bg: #090a1d;
  --surface: #12132a;
  --surface-raised: #181a35;
  --text: #eceef9;
  --text-muted: #a8acc8;
  --line-ui: #2a2e49;
  --accent: #3dd7cf;
  --accent-soft: color-mix(in srgb, var(--accent) 20%, var(--surface));
  --accent-path: color-mix(in oklch, var(--accent) 22%, transparent);
  --danger: #fa686a;
  --success: #62d286;
  --overlay: oklch(8% 0 0 / 0.85);
  --tile: #1a1c37;
  --tile-border: #313554;
  --tile-hover: color-mix(in srgb, var(--tile) 88%, var(--text) 12%);
  /* On-accent ink: dark navy, because C's dark-mode interactive teal (#3dd7cf) is a BRIGHT
     cyan — the opposite of the old warm-dark palette, where the dark-theme accent was the
     lighter colour and the light-theme accent the darker one. This still reads "light in dark
     mode, dark in hell mode" per the G-03 comment below — it is just that "light" is now a
     near-black ink because the fill itself got brighter, not darker. */
  --selected-ink: #0a0b1e;
  --line: var(--accent);
  /* Glyph on the LIVE (unsolved) drag trace only — .cell.selected, always painted with
     --accent. --selected-ink already carries "ink that sits on full-strength --accent" for
     the rest of the app (G-03 below), so the trace reuses it instead of a second invariant.
     Kept as an alias (not deleted) because index.html + tests still read --snake-ink. */
  --snake-ink: var(--selected-ink);
  /* Found-word ink: the single gold-family fill solved tiles use now (replacing the old
     randomised per-word hue). Fill is deliberately NOT full-strength --reward (that stays
     reserved for brand/reward chips) — see --found below. */
  --found: #664603;
  --found-ink: #ffedc0;

  /* Brand — the REAL German flag (schwarz-rot-gold), not evoked. --flag-ink is the top band of the
     per-letter logo gradient and is now TRUE BLACK on BOTH themes, so the wordmark is literally
     black-red-gold. Black would normally vanish on the near-black dark header; instead of a plate
     (the "awful white box" we killed twice) the dark theme lifts the glyphs with a faint light halo
     — see the drop-shadow on .wm-word in ui-layout.css. The light theme needs no halo: black reads
     on the cream header directly. --flag-paper is legacy only (nothing is painted behind the logo).
     --brand-pop (gold) is the single reusable accent — logo, streak, highlights — so teal (--accent)
     stays interactive-only and the two colour systems never compete for meaning. */
  --flag-ink: #111111;
  --flag-red: #dd0000;
  /* --flag-gold IS the palette's "reward" role (owner: gold = reward/brand only) — C's dark
     reward hex here, A's light reward hex (#fcc327) in the hell block below. */
  --flag-gold: #febf32;
  --flag-paper: #f4f1e8;
  --brand-pop: var(--flag-gold);
  /* Gold as TEXT vs gold as FILL are different a11y problems. Bright --flag-gold reads great as
     text on the DARK theme (reward-text == reward there), and as a fill (dark --flag-ink on
     gold) in BOTH themes — but bright gold TEXT on the light theme's pale sea-glass bg is too
     faint. So gold text uses --gold-text: bright on dark (= brand-pop), a deep readable amber on
     hell (set in the hell block). Fills keep using --brand-pop and stay bright everywhere. */
  --gold-text: var(--brand-pop);

  /* Word identity — OKLCH ~0.75 L */
  /* Six solved-word hues, each kept clear of the RESERVED bands so a solved word can never be
     mistaken for the live trace or the brand: teal 180 = --accent (the active selection colour)
     and gold 90 = --brand-pop. w1 (was 185, a near-teal that read identically to the selection)
     is now blue 240; w5 (was 90, the brand gold) is now red 20. */
  --word-1: oklch(75% 0.11 240);
  --word-2: oklch(75% 0.13 55);
  --word-3: oklch(75% 0.11 135);
  --word-4: oklch(75% 0.11 295);
  --word-5: oklch(75% 0.13 20);
  --word-6: oklch(75% 0.11 350);

  --chip-streak: oklch(32% 0.08 75);
  --chip-streak-ink: oklch(88% 0.12 90);

  /* German noun-gender pills — the classic teaching mnemonic (der=blue, die=rose, das=green).
     Filled pills with near-white ink. Owner decision 2026-10-20 pins the exact fills (deuteranopia
     research: old der/das sat ~5 oklab dE apart, nearly indistinguishable for that colour-vision
     type; this set raises the worst-case pair to ~10.7). die also moves off hue 15 (10deg from
     --danger's hue 25, misreadable as a destructive cue) to hue ~350, a raspberry/rose clearly not
     red. The light [data-theme="hell"] block below uses the SAME der/die/das hex the owner gave
     (#193ba1 / #ca358b / #267d30) — those were specified as the light-theme fills. For dark, die
     and das keep that identical fill (white ink stays >=4.5:1 AND the pill clears >=3:1 against
     --surface, so no change was needed); der does not (pill-vs-surface was 1.95:1) and is lightened
     to 55% L at the same hue/chroma until it clears 3:1 against --surface (checked below). */
  --gender-der: oklch(55% 0.17 265);   /* #3f69d3 — white ink 5.00:1, vs --surface 3.76:1 */
  --gender-die: oklch(58% 0.201 350);  /* #ca358b — white ink 4.78:1, vs --surface 3.94:1 */
  --gender-das: oklch(52.06% 0.14 145); /* #267d30 — white ink 5.17:1, vs --surface 3.64:1 */
  --gender-ink: oklch(98% 0 0);

  /* Part-of-speech accent per word class. Rendered as SOFT tinted chips (coloured text + a dot,
     faint tinted fill) so they read as secondary to the bold solid gender pill — a clear
     hierarchy: gender = loud, word-class = quiet. Hues kept clear of the reserved teal (180,
     interactive) and gold (90, brand). Bright for the dark theme; the hell block darkens them. */
  --pos-nomen: oklch(74% 0.14 290);     /* violet  */
  --pos-verb: oklch(76% 0.14 45);       /* amber   */
  --pos-adjektiv: oklch(76% 0.11 215);  /* cyan    */
  --pos-adverb: oklch(73% 0.16 335);    /* pink    */
  --pos-konnektor: oklch(75% 0.13 135); /* green   */
  --pos-sonstiges: var(--text-muted);   /* neutral */

  /* Grammatical case for the verb-Rektion pill (warten auf + Akk). SOLID white-ink pills so the
     governed case reads as a loud grammar fact, and so an Akk/Dat pill never blurs into the soft
     amber word-class chip that co-occurs on every verb card. Hues clear of reserved teal (180,
     interactive) and gold (90, brand). Dat sits at 55 (warmer than the 45 verb chip) to stay
     distinct. The hell block deepens them for white-ink contrast on cream. */
  --kasus-akk: oklch(55% 0.16 250);     /* blue   */
  --kasus-dat: oklch(60% 0.15 55);      /* amber  */
  --kasus-gen: oklch(56% 0.16 300);     /* violet */
  --kasus-ink: var(--gender-ink);

  /* Legacy aliases (Phase 1–2 migration) */
  --surface-elevated: var(--surface-raised);
  --ink: var(--text);
  --muted: var(--text-muted);
  --faint: oklch(58% 0.02 80);
  --error: var(--danger);
  --wall: transparent;
  --w1: var(--word-1);
  --w2: var(--word-2);
  --w3: var(--word-3);
  --w4: var(--word-4);
  --w5: var(--word-5);
  --w6: var(--word-6);
  --radius: var(--radius-sm);
  --radius-lg: var(--radius-md);
}

/* Hell — designed light theme (not inverted) */
[data-theme="hell"] {
  color-scheme: light;
  /* Palette direction A "Ostsee" (sea-glass) — owner decision 2026-10-20, exact hex from
     .claude/agent-out/palettes/palettes.html (light column).
     Owner follow-up 2026-10-20 ("too shiny, harmonize"): the first pass's near-white
     bg/surface/tile shone too hard next to the calm dark theme. Light now reads as ONE
     full-bleed sea-glass field like dark does — bg/surface/tile all sit in a tight ~92-96 L*
     band (bg L*93, surface L*94.5, tile L*94.6, surface-raised L*96 as the one "lifted" tier
     for cards) instead of jumping straight to #ffffff. Borders drop with them so edges stay
     soft, not ruled lines. Text/teal/gold/red roles and the found-gold pair are UNCHANGED —
     only the neutrals got quieter; re-verified below, all pairs still >=4.5:1. */
  --bg: #e3edef;
  --surface: #e8f1f2;
  --surface-raised: #edf5f6;
  --text: #101e2d;
  --text-muted: #495b67;
  --line-ui: #d2dee0;
  --accent: #076b72;
  --accent-soft: color-mix(in srgb, var(--accent) 18%, var(--surface));
  --accent-path: color-mix(in oklch, var(--accent) 18%, transparent);
  --danger: #be222a;
  --success: #017044;
  --overlay: oklch(18% 0 0 / 0.72);
  /* Board tiles are a TOUCH lighter than --bg (same ~1.5 L* step as --surface), not white — the
     grid still reads as the active surface, just without the glare. */
  --tile: #e9f1f2;
  --tile-border: #c9d7d9;
  --tile-hover: color-mix(in srgb, var(--tile) 94%, var(--text) 6%);
  /* On-accent ink for A's light teal (#076b72, still the dark member of the pair) is white —
     unchanged in direction from the old warm-light theme. */
  --selected-ink: #ffffff;
  /* Found-word ink: light-mode half of the single gold-family fill solved tiles use (see
     --found in :root). Pale sea-glass gold with a dark brown ink, per the palette page. */
  --found: #f7e09c;
  --found-ink: #462d0b;
  --faint: oklch(60% 0.02 80);
  --chip-streak: oklch(94% 0.08 90);
  --chip-streak-ink: oklch(42% 0.12 75);
  /* Two-layer shadow: a tight contact shadow for the edge + a soft ambient one for lift. A single
     diffuse shadow on a near-white theme reads as nothing; the contact layer is what sells depth. */
  --shadow-raised: 0 1px 2px oklch(30% 0.02 80 / 0.08),
    0 8px 24px oklch(30% 0.02 80 / 0.14);
  /* On warm paper a highlight is invisible; real (warm) shadows sell the lift. Tiles get a tight
     contact shadow; cards reuse the two-layer ambient shadow above. */
  --elev-tile: 0 1px 1px oklch(30% 0.02 80 / 0.10), 0 1px 3px oklch(30% 0.02 80 / 0.08);
  --elev-card: var(--shadow-raised);

  /* On the light theme the flag's top band is TRUE BLACK — the real schwarz-rot-gold — which
     reads directly on the pale sea-glass header with no plate. (Dark theme uses warm off-white
     instead; see :root.) --flag-paper is legacy only now that nothing sits behind the wordmark. */
  --flag-ink: #111111;
  --flag-paper: #ffffff;

  /* A's light reward hex (owner palette); bright --flag-gold from :root is ~invisible as text on
     this pale bg, so --flag-gold is reset here too (fills stay bright, same role as :root). */
  --flag-gold: #fcc327;

  /* Gold TEXT on this light theme: bright --flag-gold reads near-invisible, so gold text drops to
     a deep amber — the owner's exact "reward as text" hex (~5.6:1 on bg). --brand-pop stays
     bright for FILLS (dark ink on gold badges reads fine here). */
  --gold-text: #885700;

  /* Light theme: the owner's exact fills (der #193ba1 / die #ca358b / das #267d30), unlightened —
     white ink clears 9.63:1 / 4.78:1 / 5.17:1 here, all >= WCAG AA 4.5:1. */
  --gender-der: oklch(40% 0.17 265);   /* #193ba1 — white ink 9.63:1 */
  --gender-die: oklch(58% 0.201 350);  /* #ca358b — white ink 4.78:1 */
  --gender-das: oklch(52.06% 0.14 145); /* #267d30 — white ink 5.17:1 */

  /* POS chips carry COLOURED TEXT (not white ink), so on cream they must be dark enough to read
     as text — the 74% dark set washes out. Same hues dropped to ~50% L. */
  --pos-nomen: oklch(48% 0.16 290);
  --pos-verb: oklch(52% 0.15 45);
  --pos-adjektiv: oklch(50% 0.12 215);
  --pos-adverb: oklch(49% 0.18 335);
  --pos-konnektor: oklch(49% 0.14 135);
  --pos-sonstiges: var(--text-muted);

  /* Case pills are white-ink solids, so on cream they must be deep enough for white to clear
     contrast — the dark set is too pale. Same hues dropped to ~50% L. */
  --kasus-akk: oklch(50% 0.17 250);
  --kasus-dat: oklch(52% 0.16 55);
  --kasus-gen: oklch(50% 0.17 300);

  /* Word identity has to be redefined for light, not inherited. The dark set sits at 75%
     lightness, which is legible on a 14% background and nearly invisible on a 97% one — it
     took out the wordmark (a gradient with `color: transparent`) and, worse, the colour that
     tells the player WHICH word each rail chip belongs to. Same hues, dropped to 52% so they
     read as ink rather than as highlighter. */
  --word-1: oklch(52% 0.11 240);
  --word-2: oklch(52% 0.14 55);
  --word-3: oklch(52% 0.11 135);
  --word-4: oklch(52% 0.13 295);
  --word-5: oklch(52% 0.13 20);
  --word-6: oklch(52% 0.13 350);
}

/* Legacy: previous builds used data-theme="dark" on light default */
[data-theme="dark"] {
  color-scheme: dark;
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --duration-fast: 0ms;
    --duration-normal: 0ms;
    --duration-slow: 0ms;
  }
}
