/* 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.
   Only the `latin` subset is shipped; it already covers a o u umlauts and eszett.
   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");
}

: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 →. */
  --font: "Archivo", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
    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", 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;
  --footer-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: 560px;
  /* 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;
  --bg: oklch(14% 0.01 80);
  --surface: oklch(18% 0.01 80);
  --surface-raised: oklch(22% 0.012 80);
  --text: oklch(94% 0.02 85);
  --text-muted: oklch(72% 0.02 80);
  --line-ui: oklch(28% 0.015 80);
  --accent: oklch(76% 0.13 205);
  --accent-soft: oklch(28% 0.06 205);
  --accent-path: color-mix(in oklch, var(--accent) 22%, transparent);
  --danger: oklch(72% 0.16 25);
  --success: oklch(72% 0.12 155);
  --overlay: oklch(8% 0 0 / 0.85);
  --tile: oklch(20% 0.01 80);
  --tile-border: oklch(32% 0.015 80);
  --tile-hover: oklch(24% 0.012 80);
  --selected-ink: oklch(12% 0.01 80);
  --line: var(--accent);
  /* Glyph that rides on a coloured snake capsule (.cell.selected/.locked). Theme-INVARIANT:
     the capsule is always a saturated hue, so the letter stays white on both themes — hell
     does not override it. Paired with the black text-shadow outline that keeps it legible
     over the eyed head. */
  --snake-ink: #ffffff;

  /* 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: #ffce00;
  --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, and as a fill (dark --flag-ink on gold) in BOTH themes — but bright
     gold TEXT on the light theme's cream is ~1.4:1, invisible. 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=red, das=green).
     Filled pills with near-white ink; the light theme darkens them for contrast below.
     L deepened (der 55 / die 57 / das 53) so the near-white ink clears WCAG AA 4.5:1 at the
     pill's 12px-bold size; hue and chroma are untouched so the der=blue / die=red / das=green
     teaching signal is unchanged. */
  --gender-der: oklch(55% 0.16 250);
  --gender-die: oklch(57% 0.19 15);
  --gender-das: oklch(53% 0.15 150);
  --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;
  /* Elevation ladder. The page base is a warm PAPER, not white (hell != Weiss): cards need
     something to lift off. bg 95 -> surface 98.5 -> raised 100 gives two real ~3% steps the eye
     reads as depth, where the old 97/99/100 collapsed into one flat wash. Borders (--line-ui) drop
     to 83% so every card, chip and control gets a visible contour instead of a ghost edge. */
  --bg: oklch(95% 0.018 85);
  --surface: oklch(98.5% 0.01 85);
  --surface-raised: oklch(100% 0 0);
  --text: oklch(22% 0.02 80);
  --text-muted: oklch(46% 0.02 80);
  --line-ui: oklch(83% 0.022 85);
  --accent: oklch(42% 0.09 205);
  --accent-soft: oklch(90% 0.04 205);
  --accent-path: color-mix(in oklch, var(--accent) 18%, transparent);
  --danger: oklch(48% 0.18 25);
  --success: oklch(42% 0.1 155);
  --overlay: oklch(18% 0 0 / 0.72);
  /* Board tiles stay bright (near-white) so the grid reads as the active surface, but their edge
     goes to 80% — the board is the hero, so its cell contours must be the crispest thing on screen.
     On the deeper paper base the bright tiles now separate cleanly. */
  --tile: oklch(99% 0.008 85);
  --tile-border: oklch(80% 0.028 85);
  --tile-hover: oklch(95.5% 0.02 85);
  --selected-ink: oklch(99% 0.01 85);
  --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 (warm-cream) theme the flag's top band is TRUE BLACK — the real schwarz-rot-gold —
     which reads directly on the pale 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;

  /* Gold TEXT on this cream theme: bright --flag-gold is ~1.4:1 (invisible), so gold text drops to
     a deep amber that clears WCAG AA (~5.3:1 on surface). Same warm hue, just dark enough to read.
     --brand-pop stays bright for FILLS (dark ink on gold badges reads fine here). */
  --gold-text: oklch(52% 0.15 85);

  /* Gender pills stay filled with white ink, so on a light background they only need to be
     deep enough that white text clears contrast — the dark set at 62% L is too pale for that. */
  --gender-der: oklch(50% 0.17 250);
  --gender-die: oklch(52% 0.2 15);
  --gender-das: oklch(48% 0.15 150);

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