/* ═══════════════════════════════════════════════════════════════════════════
   ZERIKO V4 — the shared component library   (spec: zeriko_v4_demo.html)
   ───────────────────────────────────────────────────────────────────────────
   WHY THIS FILE IS PLAIN CSS AND NOT A TAILWIND @layer
   The previous attempt (.zk4-*) lived in `@layer components`. Tailwind purges
   that layer — it only emits a component class when it finds a literal
   reference in a `content` file. Nothing referenced it, so the entire design
   system was deleted at build time and shipped as ZERO BYTES for weeks, with a
   "Done in 5s" success message. Plain CSS cannot be purged. Do not move these
   rules into a @layer, and do not "optimise" this file into the Tailwind build.

   COLOURS COME FROM TOKENS, NEVER LITERALS
   Every colour below is a var(--zk-*) already defined in app/app.html and
   games.html. This file must never declare a palette of its own — one palette,
   many consumers, so a theme change is a single edit.

   NAMING
   .v4-<component>[__<part>][--<variant>], mapping 1:1 onto the demo:
       demo .card → .v4-card    .btn → .v4-btn    .ib → .v4-ib
   The 1:1 mapping is deliberate: it lets the demo be diffed against the app.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════════════
   THE LAYERING SCALE  (roadmap 0.7)
   ───────────────────────────────────────────────────────────────────────────
   Before this, z-index values were picked ad-hoc and ranged from 202 to 99999.
   The consequence that actually bit: the tutorial tooltip sat at 202 while
   sheets sit at 9950 — so every coach-mark pointing at something inside a sheet
   or modal was rendered UNDERNEATH it and looked broken.

   Two ordering rules are load-bearing and easy to get wrong:
     · The TUTORIAL must sit ABOVE modals and sheets, because it points at
       elements inside them.
     · TOASTS must sit above sheets, or the feedback for an action taken in a
       sheet is invisible.

   Gaps of 1000 leave room to insert without renumbering. Use these tokens;
   never invent a new literal.
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
  /* ══ SEMANTIC TINTS — LIGHT  ·  PLANv3 WI-07.2 (#156) ═══════════════════════
     These exist in app.html's own <style> block, and the DARK counterparts exist
     further down this file — but the LIGHT ones did not, and this file is the
     only stylesheet games.html shares with the parent.

     games.html is an IFRAME: a separate document. CSS custom properties do not
     cross a document boundary any more than @font-face does (the same boundary
     that left the whole Practice tab in a fallback font). So inside games.html
     `var(--zk-tint-good-2)` resolved to NOTHING in light mode, which makes the
     whole declaration invalid — and

         .quiz-option-btn.correct { background: var(--zk-tint-good-2) !important; … }

     silently did nothing. That is the reported bug: answer a lesson quiz
     correctly and the right answer is never highlighted. `!important` does not
     help, because an invalid declaration is dropped before cascade.
     It "worked" in dark mode only, since those tokens are defined below.

     Defining them here fixes both documents at once and stops the pair drifting.
     Values are copied verbatim from app.html:316-320 — one palette, two
     consumers, never two palettes. */
  --zk-tint-good:   #f0fdf5;  --zk-tint-good-2: #d1fae6;  --zk-tint-good-ink: #15803e;
  --zk-tint-bad:    #fef2f3;  --zk-tint-bad-2:  #fee2e3;  --zk-tint-bad-ink:  #b91c1d;
  --zk-tint-warn:   #fffbec;  --zk-tint-warn-2: #fef3c8;  --zk-tint-warn-ink: #92400f;
  --zk-tint-info:   #eff6ff;  --zk-tint-info-2: #dbeaff;  --zk-tint-info-ink: #1d4ed9;
  --zk-tint-note:   #f5f3ff;  --zk-tint-note-2: #eef2ff;  --zk-tint-note-ink: #5b21b7;

  /* These are DESCRIPTIVE of what the app actually does, not aspirational.
     --z-nav is 50 because that is what .bottom-nav has shipped as; writing 500
     here would have made the scale a lie the first time anyone trusted it. */
  --z-nav:           50;   /* bottom nav (.bottom-nav)                */
  --z-sticky:       100;   /* sticky headers inside a scroller        */
  --z-legacy-modal: 500;   /* the ~40 hand-rolled centred overlays    */
  --z-fab:          600;   /* translator/notes FAB — above those      */
  --z-overlay:     1000;   /* generic full-screen views               */
  --z-scrim:       2000;   /* modal backdrop                          */
  --z-modal:       2100;   /* modal content                           */
  --z-sheet-scrim: 3000;   /* ZerikoSheet backdrop                    */
  --z-sheet:       3100;   /* ZerikoSheet panel                       */
  --z-coach:       4000;   /* tutorial — ABOVE what it points at      */
  --z-toast:       5000;   /* transient feedback                      */
  --z-banner:      6000;   /* update / install banners                */
  --z-onboard:     7000;   /* first-run takeover                      */
  --z-critical:    9000;   /* boot overlay, fatal error                */
}

/* ── Tactile button — presses DOWN into its own shadow ──────────────────────
   The travel distance and the shadow offset MUST match exactly (4px/4px). If
   they disagree the button appears to slide sideways rather than depress. */
.v4-btn {
  display: block; width: 100%; border: 0; cursor: pointer; font: inherit;
  font-weight: 800; font-size: 15.5px; color: #fff;
  background: var(--zk-primary); padding: 14px;
  border-radius: var(--zk-radius-chip);
  box-shadow: 0 4px 0 var(--zk-primary-dark);
  transition: transform .08s, box-shadow .08s;
}
.v4-btn:active { transform: translateY(4px); box-shadow: 0 0 0 var(--zk-primary-dark); }
.v4-btn--teal { background: var(--zk-teal); box-shadow: 0 4px 0 var(--zk-teal-d); }
.v4-btn--teal:active { box-shadow: 0 0 0 var(--zk-teal-d); }
.v4-btn--violet { background: var(--zk-violet); box-shadow: 0 4px 0 #5B3494; }
.v4-btn--violet:active { box-shadow: 0 0 0 #5B3494; }
.v4-btn--ghost {
  background: var(--zk-card); color: var(--zk-ink);
  border: 1px solid var(--zk-line); box-shadow: 0 3px 0 var(--zk-line);
}
.v4-btn--ghost:active { transform: translateY(3px); box-shadow: 0 0 0 var(--zk-line); }
.v4-btn--sm { width: auto; font-size: 13px; padding: 10px 14px; border-radius: var(--zk-radius-pill); }

/* ── Sticker card ─────────────────────────────────────────────────────────
   Hard offset shadow, no blur. This single property is most of what separates
   "V2 soft-shadow app" from "V4 paper". */
.v4-card {
  background: var(--zk-card);
  border: 1.5px solid var(--zk-line);
  border-radius: var(--zk-radius-card);
  box-shadow: 0 3px 0 rgba(140,120,95,.13);
}

/* ── Section label with dashed trailing rule ──────────────────────────────
   The rule is what makes shelves read as sections without heavy headers. */
.v4-label {
  display: flex; align-items: center; gap: 7px;
  font-size: 11.5px; font-weight: 800; letter-spacing: .07em;
  text-transform: uppercase; color: var(--zk-ink-3); margin: 20px 3px 9px;
}
.v4-label::after {
  content: ''; flex: 1; height: 1.5px;
  background: repeating-linear-gradient(90deg, var(--zk-line) 0 5px, transparent 5px 10px);
}

/* ── Avatar ──────────────────────────────────────────────────────────────── */
.v4-av {
  border-radius: 50%; display: grid; place-items: center;
  color: #fff; font-weight: 800; flex: 0 0 auto; object-fit: cover;
}

/* ═══ THE DAILY PATH — the app's centrepiece ═══════════════════════════════ */
.v4-path { position: relative; margin-top: 16px; padding-left: 52px; }
/* Dashed spine drawn with a repeating gradient so it scales with content
   height — a border-dashed element cannot, because dash length is fixed. */
.v4-path__spine {
  position: absolute; left: 21px; top: 14px; bottom: 22px; width: 3px; border-radius: 2px;
  background-image: linear-gradient(var(--zk-line) 60%, transparent 0);
  background-size: 3px 11px;
}
.v4-path__fill {
  position: absolute; left: 21px; top: 14px; width: 3px; border-radius: 2px;
  background: linear-gradient(180deg, var(--zk-teal), var(--zk-primary));
  transition: height .75s cubic-bezier(.5,0,.2,1);
}
.v4-step { position: relative; padding: 9px 0 15px; transition: opacity .35s; }
.v4-step__knob {
  position: absolute; left: -52px; top: 4px; width: 44px; height: 44px;
  border-radius: 50%; display: grid; place-items: center; font-size: 19px;
  background: var(--zk-card-2); border: 2.5px dashed var(--zk-line);
  color: var(--zk-ink-3); transition: .4s;
}
.v4-step.is-done .v4-step__knob {
  background: var(--zk-teal); border: 2.5px solid var(--zk-teal); color: #fff;
}
.v4-step.is-now .v4-step__knob {
  background: var(--zk-card); border: 2.5px solid var(--zk-primary); color: var(--zk-primary);
  animation: v4Breathe 2.2s ease-in-out infinite;
}
.v4-step.is-pop .v4-step__knob { animation: v4Pop .55s cubic-bezier(.18,.89,.32,1.28); }
.v4-step.is-locked { opacity: .45; }
.v4-step__name { font-size: 15px; font-weight: 750; line-height: 1.25; color: var(--zk-ink); }
.v4-step__desc { font-size: 12px; color: var(--zk-ink-3); margin-top: 2px; }
.v4-step.is-done .v4-step__name {
  color: var(--zk-ink-3); text-decoration: line-through; text-decoration-color: var(--zk-line);
}
.v4-walker {
  position: absolute; left: -52px; width: 44px; height: 44px; z-index: 5;
  display: grid; place-items: center; font-size: 20px; pointer-events: none;
  transition: top .75s cubic-bezier(.5,0,.2,1);
  filter: drop-shadow(0 3px 4px rgba(0,0,0,.2));
}

/* Semantic step tags — meaning only, never decoration */
.v4-tag {
  display: inline-block; font-size: 10px; font-weight: 800; text-transform: uppercase;
  letter-spacing: .04em; padding: 2px 7px; border-radius: var(--zk-radius-pill); margin-top: 5px;
}
.v4-tag--chat   { background: rgba(225,85,10,.13);  color: var(--zk-primary); }
.v4-tag--lesson { background: rgba(124,77,188,.14); color: var(--zk-violet); }
.v4-tag--review { background: rgba(0,168,132,.14);  color: var(--zk-teal-d); }
.v4-tag--game   { background: rgba(245,183,47,.2);  color: #8A6100; }
/* Added when the path became configurable: it can now surface stops that only
   ever existed in the orphaned computeStepPath() catalogue. Without a variant
   the tag renders with .v4-tag's bare base and reads as a broken chip. */
.v4-tag--chill    { background: rgba(0,168,132,.14);  color: var(--zk-teal-d); }
.v4-tag--chillChat{ background: rgba(0,168,132,.14);  color: var(--zk-teal-d); }
.v4-tag--quest    { background: rgba(124,77,188,.14); color: var(--zk-violet); }
.v4-tag--story    { background: rgba(124,77,188,.14); color: var(--zk-violet); }
.v4-tag--world    { background: rgba(59,130,246,.16); color: #2563EB; }
.v4-tag--postcard { background: rgba(225,85,10,.13);  color: var(--zk-primary); }
.v4-tag--alphabet { background: rgba(0,168,132,.14);  color: var(--zk-teal-d); }
:root[data-zk-theme="dark"] .v4-tag--game  { color: var(--zk-sun); }
:root[data-zk-theme="dark"] .v4-tag--world { color: #93C5FD; }

/* Reward panel — dashed while pending, solid accent once earned, so completing
   the path is a visible STATE CHANGE and not just a disabled button. */
.v4-reward {
  margin-top: 6px; padding: 13px; border-radius: var(--zk-radius-chip);
  background: var(--zk-card-2); border: 1.5px dashed var(--zk-line); text-align: center;
}
.v4-reward.is-hit {
  background: var(--zk-primary-light); border-color: var(--zk-primary);
  border-style: solid; animation: v4Pop .6s;
}
.v4-reward__t { font-size: 13px; font-weight: 800; color: var(--zk-ink); }
.v4-reward__d { font-size: 11.5px; color: var(--zk-ink-3); margin-top: 2px; }

/* ═══ INBOX / PEOPLE ROWS ══════════════════════════════════════════════════
   Flat rows inside ONE card with hairline separators — not a stack of cards.
   That is what makes it read as a list of people rather than a grid of items. */
.v4-ib {
  display: flex; align-items: center; gap: 12px; padding: 12px 14px;
  border-bottom: 1.5px solid var(--zk-line); cursor: pointer; transition: .15s;
}
.v4-ib:last-child { border-bottom: 0; }
.v4-ib:active { transform: scale(.985); background: var(--zk-card-2); }
.v4-ib__t { flex: 1; min-width: 0; }
.v4-ib__n {
  font-weight: 750; font-size: 14.5px; color: var(--zk-ink);
  display: flex; align-items: center; gap: 6px;
}
.v4-ib__m {
  font-size: 12.5px; color: var(--zk-ink-2); margin-top: 1px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.v4-ib__time { font-size: 10.5px; color: var(--zk-ink-3); align-self: flex-start; margin-top: 3px; }
.v4-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--zk-teal); flex: 0 0 auto; }

/* ═══ LIST ROW (games, lessons, decks) ═════════════════════════════════════ */
.v4-g {
  display: flex; align-items: center; gap: 12px; padding: 12px 14px;
  border-bottom: 1.5px solid var(--zk-line); cursor: pointer;
}
.v4-g:last-child { border-bottom: 0; }
.v4-g:active { transform: scale(.985); }
.v4-g__i {
  width: 40px; height: 40px; border-radius: 13px; display: grid; place-items: center;
  flex: 0 0 auto; font-size: 17px; background: var(--zk-primary-light); color: var(--zk-primary);
}
.v4-g__t { flex: 1; min-width: 0; }
.v4-g__n { font-weight: 750; font-size: 14.5px; color: var(--zk-ink); }
.v4-g__d { font-size: 11.5px; color: var(--zk-ink-3); margin-top: 1px; }
.v4-g.is-locked { opacity: .55; }
.v4-g.is-locked .v4-g__i { background: var(--zk-card-2); color: var(--zk-ink-3); }
.v4-chev { color: var(--zk-ink-3); font-size: 17px; flex: 0 0 auto; }

/* Badge — locks are priced in WORDS, never XP (plan D5) */
.v4-bdg {
  font-size: 10.5px; font-weight: 800; color: var(--zk-ink-2); background: var(--zk-card-2);
  border: 1.5px solid var(--zk-line); padding: 3px 8px;
  border-radius: var(--zk-radius-pill); flex: 0 0 auto;
}
.v4-bdg--lock { color: var(--zk-primary); background: var(--zk-primary-light); border-color: transparent; }

/* ═══ LESSON CARD + STEP RAIL ══════════════════════════════════════════════ */
.v4-lesson { padding: 15px 16px; margin-top: 12px; position: relative; overflow: hidden; }
.v4-lesson::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 5px;
  background: var(--zk-violet);
}
.v4-lsteps { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; margin: 11px 0 13px; }
.v4-ls {
  font-size: 11px; font-weight: 750; color: var(--zk-ink-3); background: var(--zk-card-2);
  border: 1.5px solid var(--zk-line); border-radius: var(--zk-radius-pill); padding: 5px 9px;
}
.v4-ls.is-done { color: var(--zk-teal-d); background: rgba(0,168,132,.12); border-color: transparent; }
.v4-ls.is-now { color: #fff; background: var(--zk-violet); border-color: transparent; }

/* ═══ WORD ROW + MASTERY BAR ═══════════════════════════════════════════════ */
.v4-wr {
  display: flex; align-items: center; gap: 11px; padding: 11px 4px;
  border-bottom: 1.5px solid var(--zk-line);
}
.v4-wr:last-child { border-bottom: 0; }
.v4-wr__w { flex: 1; min-width: 0; }
.v4-wr__t { font-weight: 750; font-size: 15px; color: var(--zk-ink); }
.v4-wr__s { font-size: 11.5px; color: var(--zk-ink-3); }
.v4-mb {
  width: 48px; height: 7px; border-radius: 4px; background: var(--zk-card-2);
  overflow: hidden; flex: 0 0 auto;
}
.v4-mb > i { display: block; height: 100%; background: var(--zk-primary); }

/* ═══ JOURNEY LADDER ═══════════════════════════════════════════════════════ */
.v4-jline { position: absolute; left: 6px; top: 8px; bottom: 8px; width: 2px; background: var(--zk-line); }
.v4-jnode { position: relative; padding: 11px 0 11px 30px; }
.v4-jnode::before {
  content: ''; position: absolute; left: 0; top: 17px; width: 13px; height: 13px;
  border-radius: 50%; background: var(--zk-card); border: 2.5px solid var(--zk-line);
}
.v4-jnode.is-done::before { background: var(--zk-teal); border-color: var(--zk-teal); }
.v4-jnode.is-now::before {
  background: var(--zk-primary); border-color: var(--zk-primary);
  box-shadow: 0 0 0 4px var(--zk-primary-light);
}
/* Scope chips — "unlock once, forever" vs "adapts per language" (plan D6) */
.v4-scope {
  display: inline-block; font-size: 9.5px; font-weight: 800; text-transform: uppercase;
  padding: 2px 7px; border-radius: var(--zk-radius-pill); margin-top: 5px;
}
.v4-scope--account { background: rgba(59,111,224,.13); color: #3B6FE0; }
.v4-scope--lang { background: rgba(225,85,10,.13); color: var(--zk-primary); }

/* ═══ CHARACTER SHEET ══════════════════════════════════════════════════════ */
.v4-warm {
  margin-top: 13px; padding: 13px 15px; border-radius: var(--zk-radius-chip);
  background: var(--zk-primary-light); border: 1.5px solid var(--zk-primary);
}
.v4-warmbar { height: 7px; border-radius: 4px; background: rgba(0,0,0,.08); overflow: hidden; margin-top: 9px; }
.v4-warmbar > i { display: block; height: 100%; background: var(--zk-primary); border-radius: 4px; }
.v4-memo {
  font-size: 12.5px; color: var(--zk-ink-2); line-height: 1.55;
  padding: 9px 0; border-bottom: 1.5px solid var(--zk-line);
}
.v4-memo:last-of-type { border-bottom: 0; }
.v4-memo b { color: var(--zk-ink); }

/* ═══ COMPOSER ═════════════════════════════════════════════════════════════ */
.v4-composer {
  display: flex; align-items: center; gap: 9px; padding: 10px 12px;
  border-top: 1.5px solid var(--zk-line); background: var(--zk-card);
  flex: 0 0 auto; position: relative; z-index: 2;
  padding-bottom: calc(10px + env(safe-area-inset-bottom));
}
.v4-composer__fld {
  flex: 1; background: var(--zk-card-2); border: 1.5px solid var(--zk-line);
  border-radius: var(--zk-radius-pill); padding: 11px 15px;
  color: var(--zk-ink); font-size: 14px; outline: none;
}
.v4-ico {
  width: 38px; height: 38px; display: grid; place-items: center; flex: 0 0 auto;
  border-radius: var(--zk-radius-pill); background: var(--zk-card-2);
  border: 1.5px solid var(--zk-line); cursor: pointer; font-size: 15px; color: var(--zk-ink-2);
}
.v4-ico--plain { background: transparent; border: 0; }
.v4-snd {
  width: 41px; height: 41px; border-radius: 50%; flex: 0 0 auto;
  background: var(--zk-primary); color: #fff; display: grid; place-items: center;
  border: 0; cursor: pointer; box-shadow: 0 3px 0 var(--zk-primary-dark);
}
.v4-snd:active { transform: translateY(3px); box-shadow: 0 0 0 var(--zk-primary-dark); }

/* ═══ MOTION — the four primitives (see ZERIKO_V4_METHOD.md §3) ════════════
   Four curves, app-wide. Never `transition: all`; only transform/opacity in
   anything that repeats, so mid-range Android doesn't thrash layout. */
@keyframes v4Pop     { 0%{transform:scale(1)} 45%{transform:scale(1.32)} 100%{transform:scale(1)} }
@keyframes v4Breathe { 0%,100%{box-shadow:0 0 0 5px var(--zk-primary-light)} 50%{box-shadow:0 0 0 10px transparent} }
@keyframes v4Fall    { 0%{transform:translateY(0) rotate(0);opacity:1} 100%{transform:translateY(190px) rotate(540deg);opacity:0} }
@keyframes v4PopIn   { from{opacity:0;transform:scale(.92) translateY(9px)} to{opacity:1;transform:scale(1) translateY(0)} }

.v4-cf { position: absolute; width: 8px; height: 12px; border-radius: 2px; animation: v4Fall .95s ease-out forwards; }
.v4-confetti { position: absolute; inset: 0; pointer-events: none; z-index: 40; overflow: hidden; }
.v4-popin { animation: v4PopIn .3s cubic-bezier(.175,.885,.32,1.275) both; }

/* Reduced motion removes ANIMATION, never FUNCTION — every element above stays
   in its final state, it just arrives there instantly. */
@media (prefers-reduced-motion: reduce) {
  .v4-btn, .v4-step, .v4-step__knob, .v4-walker, .v4-path__fill, .v4-snd { transition: none !important; }
  .v4-cf, .v4-step.is-now .v4-step__knob, .v4-popin, .v4-reward.is-hit { animation: none !important; }
}

/* ══════════════════════════════════════════════════════════════════════════
   PAPER GRAIN  ·  GAP_ANALYSIS §5.4   (demo:49)
   ──────────────────────────────────────────────────────────────────────────
   The demo does NOT look like paper because of its hex values — the warm
   palette was already ported and the app still read as flat. It looks like
   paper because of a 14px dot lattice drawn over the whole screen.

   The demo paints it as `.screen::before` — an absolutely-positioned layer at
   z-index 1, with content above it at z-index 2. That does not port directly:
   our scrollers are the full-height elements, so an absolute overlay would
   cover only the first viewport and then slide away.

   So we paint the lattice as a BACKGROUND IMAGE on the scrolling surface
   itself. Same visual result, and strictly safer:
     · it sits behind every child by definition — no stacking-context fights,
       no `pointer-events` trap, nothing can end up drawn on top of the UI;
     · it repeats for the full scroll height instead of the first screen only.
   The colour must stay `rgba(140,120,95,.10)` — warm grey at 10%. Anything
   stronger stops reading as fibre and starts reading as a pattern.
   ══════════════════════════════════════════════════════════════════════════ */
.v4-paper {
  background-color: var(--zk-bg);
  background-image: radial-gradient(circle at 1px 1px, rgba(140,120,95,.10) 1px, transparent 0);
  background-size: 14px 14px;
}

/* ── "My languages" shortcut row (zeriko_revamp_demo.html:831) ──────────────
   Up to four flags at the top of Today. Lives here rather than in
   zeriko-today.js's private `.zkt-` block because §1.3 of ZERIKO_V4_METHOD.md
   is retiring those private stylesheets, and adding to them makes that worse.
   Chip geometry is the demo's pill: --zk-radius-pill, 1.5px warm line, and the
   active one carries the accent wash rather than a full accent fill — four
   saturated orange chips in a row is exactly the D8 violation this file keeps
   having to undo elsewhere. */
.zkt-langs { display: flex; gap: 7px; padding: 10px 2px 2px; }
.zkt-lang {
  display: inline-flex; align-items: center; gap: 5px; cursor: pointer;
  font: inherit; font-size: 11.5px; font-weight: 800;
  padding: 6px 11px; border-radius: var(--zk-radius-pill);
  background: var(--zk-card); border: 1.5px solid var(--zk-line);
  color: var(--zk-ink-2); transition: transform .08s, background .15s, border-color .15s;
}
.zkt-lang:active { transform: scale(.96); }
.zkt-lang.is-on {
  background: var(--zk-primary-light); border-color: var(--zk-primary); color: var(--zk-primary);
}
.zkt-lang__f { font-size: 15px; line-height: 1; }
/* The gear is pushed to the far end so the language chips read as one group and
   it reads as a separate affordance, not a fifth language. */
.zkt-lang--gear { margin-left: auto; padding: 6px 10px; font-size: 14px; }

/* Bottom clearance  ·  GAP §5.2 F2.
   The demo's scroller ends in `padding:4px 16px 96px` (demo:55) — 96px of air so
   the last card is never trapped against the nav. Ours ended in `p-4`'s 16px, so
   the final row sat hard against the bar, and the Aa→文 FAB (bottom:76px) covered
   it outright. Applied to the scroller rather than its inner `.p-4` wrapper, so a
   tab that doesn't use that wrapper still gets the clearance. */
#tab-home, #tab-review, #tab-notes { padding-bottom: 96px; }

/* ══════════════════════════════════════════════════════════════════════════
   SCROLLBARS  ·  GAP_ANALYSIS §5.5   (demo:54 — `.view::-webkit-scrollbar{width:0}`)
   ──────────────────────────────────────────────────────────────────────────
   Native scrollbars are the loudest "this is a web page" signal we still ship;
   three of five audit captures show one, including a grey track under the chat
   level-filter chips.

   Scoped to `.v4-scroll`, never applied globally: on a desktop mouse a hidden
   vertical scrollbar removes the only cue that a region scrolls at all. Put
   this on touch-first scrollers and horizontal chip rails — where the content
   itself (a half-visible next chip) is the affordance.
   ══════════════════════════════════════════════════════════════════════════ */
.v4-scroll { scrollbar-width: none; -ms-overflow-style: none; }
.v4-scroll::-webkit-scrollbar { width: 0; height: 0; }

/* `scrollbar-hide` is a Tailwind PLUGIN class (tailwind-scrollbar-hide). The
   plugin was never installed — `tailwind.config.js` has no `scrollbar` entry
   and `zeriko-tw.css` contains zero matches — yet five elements are marked with
   it, including the chat level-filter rail whose bare grey scrollbar shows up in
   the §4.3 audit capture. Rather than rewrite five call sites to `.v4-scroll`,
   supply the definition they have always assumed existed. Keep both names: the
   markup keeps reading in Tailwind idiom, and new code can use `.v4-scroll`. */
.scrollbar-hide { scrollbar-width: none; -ms-overflow-style: none; }
.scrollbar-hide::-webkit-scrollbar { width: 0; height: 0; }

/* ══════════════════════════════════════════════════════════════════════════
   SHEETIFY  ·  GAP_ANALYSIS §5.3   (demo:215-219 — `.ov` + `.cs`)
   ──────────────────────────────────────────────────────────────────────────
   The app has 45 fixed-inset overlays and only 5 go through ZerikoSheet. The
   other 40 are hand-rolled centred boxes: they appear instantly, in the middle
   of the screen, over a cold black scrim. The demo has exactly ONE overlay
   idiom — docked to the bottom, top corners rounded, sliding up over a warm
   scrim — and that difference is the whole of "all the popups look ugly".

   WHY CSS AND NOT 40 CONVERSIONS
   Converting each modal by hand means touching 40 open/close call sites, each
   with its own wired listeners — the highest-risk, lowest-information work in
   the project. But those 40 shells are near-identical Tailwind utility strings
   (`fixed inset-0 … flex items-center justify-center … bg-black/60`), so the
   *utility redefinition* mechanism applies (ZERIKO_V4_METHOD.md appendix, the
   same trick that fixed 250 cold greys). One rule converts all of them, and
   deleting the rule reverts all of them.

   WHY ONLY UNDER 640px
   Method §4.2: phone = "exactly the demo"; tablet/laptop keep a centred dialog,
   where a bottom-docked sheet would be absurd on a 1440px screen. So this is
   scoped to the band the demo actually describes, and desktop is left alone.

   Selector note: `.items-center.justify-center` is what marks a CENTRED modal.
   Overlays already written as `items-end` are sheets by hand and are skipped —
   they must not be double-styled. Full-screen viewers (no centring utilities)
   are untouched too, which is why the compound selector matters.
   ══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 640px) {
  .fixed.inset-0.items-center.justify-center {
    align-items: flex-end !important;
    padding: 0 !important;
    /* Warm scrim (demo:215 `rgba(30,20,10,.45)`). Tailwind's bg-black/NN paints
       a cold neutral that greys the paper behind it instead of warming it. */
    background: rgba(30,20,10,.45) !important;
  }
  /* The panel. `> *` rather than a named class because each modal names its own
     inner box differently — the one thing they share is being the only child. */
  .fixed.inset-0.items-center.justify-center > * {
    width: 100% !important;
    max-width: 100% !important;
    max-height: 88vh;
    border-radius: 24px 24px 0 0 !important;
    animation: v4SheetUp .3s cubic-bezier(.2,.8,.2,1);
  }
  /* Grab handle, drawn on the panel so no markup changes. `content` sits in the
     panel's own padding; panels with none still read correctly because the bar
     is only 4px tall. */
  .fixed.inset-0.items-center.justify-center > *::before {
    content: ''; display: block; width: 38px; height: 4px; border-radius: 2px;
    background: var(--zk-line); margin: 8px auto 10px;
  }
}
@keyframes v4SheetUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) {
  .fixed.inset-0.items-center.justify-center > * { animation: none !important; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   M1 · DARK MODE  —  token revaluation + utility redefinition
   ───────────────────────────────────────────────────────────────────────────
   WHY IT LOOKS LIKE THIS
   v7.2.2 shipped a half-dark theme: --zk-ink-1 went cream while every card
   stayed hardcoded white, producing invisible cream-on-white text. The lesson
   was not "dark mode is hard", it was "flip the SURFACE and the INK together,
   or neither". So this block does two things and only two things:

     1. Re-value the existing --zk-* tokens. Every site already reading
        var(--zk-card) / var(--zk-ink) re-skins for free.
     2. Redefine what the *literal* Tailwind utilities mean, scoped to the dark
        ancestor. There are ~100 bg-white and ~15 distinct grey text classes
        (text-gray-NNN, text-slate-NNN) in app.html that cannot follow a CSS
        variable because they hardcode a colour. Rewriting 180 sites by hand is how
        this milestone would fail halfway; redefining the class is the same
        mechanism that already carried the warm sweep (250 cold greys) and the
        purple sweep (38 indigo sites).

   WHY !important IS CORRECT HERE (and only here)
   Tailwind's .bg-white and ours are both single-class selectors — equal
   specificity, so source order decides, and the prebuilt Tailwind sheet may
   load either side of this file. The ancestor attribute selector already
   scopes the blast radius to dark mode only; !important just removes the
   load-order coin flip. It cannot leak into light mode.

   WHAT IT CANNOT REACH
   Inline style="background:#fff". No selector beats an inline style, and these
   rules target CLASSES. Those sites are edited individually in app.html to read
   var(--zk-card). If you add a new inline white background, dark mode breaks
   there and nowhere else.
   ═══════════════════════════════════════════════════════════════════════════ */

:root[data-zk-theme="dark"] {
  /* Warm dark, not neutral dark: the light theme is "paper", so the dark theme
     is the same paper at night, not a different material. Hues match. */
  --zk-bg:            #17130F;
  --zk-card:          #221C16;
  --zk-card-2:        #2C241C;   /* sunken surface — LIGHTER than card here,
                                    inverted from light mode, because "recessed"
                                    reads as lighter when the page is dark */
  --zk-line:          #3A3027;
  --zk-ink:           #F7F1E8;
  --zk-ink-2:         #BCAE9D;
  --zk-ink-3:         #827565;
  --zk-sec:           #BCAE9D;

  /* Accent: same hue, lifted for contrast. #E1550A on #17130F is ~3.4:1 — under
     AA for text. #F97316 clears it. --zk-primary-dark stays darker than the
     accent so the 4px "pressed" shadow still reads as depth. */
  /* #F97316 was picked to be legible AS TEXT on the dark page and never
     checked as a FILL: white button labels on it are 2.80:1, under the 3:1
     floor. #D9540B serves both uses — white on it is 4.02:1, and it is 4.60:1
     against #17130F. One token does two jobs, so it has to pass both.
     (check-contrast.js now fails the build on this.) */
  --zk-primary:       #D9540B;
  --zk-primary-dark:  #C4590C;
  --zk-primary-light: #3A2415;   /* accent-soft: a tint of the bg, not of white */

  --zk-teal:          #14B892;
  --zk-teal-d:        #0B7A5F;
  --zk-violet:        #9B72D9;
  --zk-sun:           #F5B72F;
  --zk-danger:        #F0705B;

  /* ══ THE LEGACY V2 PALETTE — the reason v7.5.0's dark mode was unusable ══
     Re-valuing --zk-* alone was not enough, and the miss was not subtle: the
     ENTIRE app column, #app-container, paints `background-color: var(--bg-app)`
     (app.html:744). --bg-app is a V2-era token that predates --zk-bg and was
     never folded into it. So the column stayed #FBF6EF cream while the ink
     tokens above flipped to cream — cream on cream, which is the exact failure
     v7.2.2 shipped and this milestone was supposed to prevent.

     The lesson is not "add --bg-app". It is: a theme must enumerate every token
     that paints a SURFACE, not every token in the design system you happen to be
     thinking about. These three are the complete set of pre-V4 survivors — see
     the :root block at app.html:258-268. */
  --bg-app:           #17130F;   /* == --zk-bg. #app-container, and ~2 other sites */
  --primary:          #F97316;   /* == --zk-primary */
  --primary-dark:     #C4590C;
  --text-main:        #F7F1E8;
  --text-sec:         #BCAE9D;

  /* The "desk" the 500px column rests on. app.html's <body> hardcoded #EDE5D9,
     one shade deeper than the paper, to make the column read as a sheet lying
     on a surface. That literal is now a token in both themes so the depth
     relationship survives the theme instead of being a light-mode-only idea.
     Only app.html uses it — games.html IS the column, it has no desk, so it
     stays on --zk-bg. That is why this is a token and not a `body` rule here:
     a `body` rule in this shared file would repaint Practice too. */
  --zk-page:          #0F0C09;

  /* Semantic tint panels — see the light block in app.html for why these exist.
     Each is the same hue as its light counterpart, dropped to roughly the
     lightness of --zk-card-2 so a "success" panel still reads as green without
     becoming a lamp. The -ink pair is lifted to clear AA on that background:
     #15803D on #14261C is unreadable, #4ADE80 is not. Getting the pair wrong is
     precisely the cream-on-cream failure, one hue at a time. */
  --zk-tint-good:   #14261C;  --zk-tint-good-2: #1B3527;  --zk-tint-good-ink: #4ADE80;
  --zk-tint-bad:    #2E1917;  --zk-tint-bad-2:  #3B1E1A;  --zk-tint-bad-ink:  #FCA5A5;
  --zk-tint-warn:   #2B2210;  --zk-tint-warn-2: #3A2E12;  --zk-tint-warn-ink: #FCD34D;
  --zk-tint-info:   #131F2E;  --zk-tint-info-2: #17293D;  --zk-tint-info-ink: #93C5FD;
  --zk-tint-note:   #1F1830;  --zk-tint-note-2: #241C3A;  --zk-tint-note-ink: #C4B5FD;

  /* The sticker shadow cannot be a darker brown on a dark page — it vanishes.
     Dark mode keeps the hard offset but paints it in black. */
  --zk-shadow:        0 3px 0 rgba(0,0,0,.45);
  --zk-shadow-hover:  0 3px 0 rgba(0,0,0,.6);

  color-scheme: dark;   /* scrollbars, form controls, and the UA's own chrome */
}

/* ── Surfaces ───────────────────────────────────────────────────────────── */
:root[data-zk-theme="dark"] .bg-app        { background-color: var(--zk-bg) !important; }
/* The paper grain. My first attempt targeted `body::before` and did NOTHING —
   the grain is a background-image on #app-container (app.html:748), and the
   comment directly above it explains they avoided a pseudo-element on purpose
   ("the app has hundreds of positioned children"). A dark page needs the dots
   to be light-on-dark rather than dark-on-light, at a much lower alpha: the
   same .10 brown that reads as texture on cream reads as dirt on #17130F.
   Same 14px grid, so the two themes have identical texture density. */
/* Both selectors are needed because the two documents attach the grain to
   different elements: app.html puts it on #app-container, games.html on
   `html, body`. Listing body also gives app.html's desk the same faint texture,
   which light mode does not have — a deliberate accept, not an oversight: at
   4.5% alpha on #0F0C09 it reads as the same material continuing past the edge
   of the sheet, and it is only visible on desktop beside the 500px column. */
:root[data-zk-theme="dark"] #app-container,
:root[data-zk-theme="dark"] body {
  background-image: radial-gradient(circle at 1px 1px, rgba(255,244,230,.045) 1px, transparent 0) !important;
}
:root[data-zk-theme="dark"] .bg-white,
:root[data-zk-theme="dark"] .bg-gray-50,
:root[data-zk-theme="dark"] .bg-slate-50,
:root[data-zk-theme="dark"] .bg-neutral-50 { background-color: var(--zk-card) !important; }
:root[data-zk-theme="dark"] .bg-gray-100,
:root[data-zk-theme="dark"] .bg-slate-100,
:root[data-zk-theme="dark"] .bg-gray-200,
:root[data-zk-theme="dark"] .bg-slate-200  { background-color: var(--zk-card-2) !important; }

/* ── Ink ────────────────────────────────────────────────────────────────── */
/* Attribute-substring match rather than 15 enumerated classes: greys added
   later are covered automatically, which is exactly the failure mode that
   produced the cream-on-white bug (a class nobody remembered to list). */
:root[data-zk-theme="dark"] [class*="text-gray-9"],
:root[data-zk-theme="dark"] [class*="text-gray-8"],
:root[data-zk-theme="dark"] [class*="text-slate-9"],
:root[data-zk-theme="dark"] [class*="text-slate-8"],
:root[data-zk-theme="dark"] [class*="text-neutral-9"] { color: var(--zk-ink) !important; }
:root[data-zk-theme="dark"] [class*="text-gray-7"],
:root[data-zk-theme="dark"] [class*="text-gray-6"],
:root[data-zk-theme="dark"] [class*="text-gray-5"],
:root[data-zk-theme="dark"] [class*="text-slate-7"],
:root[data-zk-theme="dark"] [class*="text-slate-6"],
:root[data-zk-theme="dark"] [class*="text-slate-5"],
:root[data-zk-theme="dark"] [class*="text-neutral-6"],
:root[data-zk-theme="dark"] [class*="text-neutral-5"] { color: var(--zk-ink-2) !important; }
:root[data-zk-theme="dark"] [class*="text-gray-4"],
:root[data-zk-theme="dark"] [class*="text-gray-3"],
:root[data-zk-theme="dark"] [class*="text-slate-4"],
:root[data-zk-theme="dark"] [class*="text-slate-3"] { color: var(--zk-ink-3) !important; }
/* text-white is deliberately NOT swept — it sits on coloured buttons where it
   is still correct. Same for text-*-600 accent inks. */

/* ── Borders ────────────────────────────────────────────────────────────── */
:root[data-zk-theme="dark"] [class*="border-gray-1"],
:root[data-zk-theme="dark"] [class*="border-gray-2"],
:root[data-zk-theme="dark"] [class*="border-slate-1"],
:root[data-zk-theme="dark"] [class*="border-slate-2"],
:root[data-zk-theme="dark"] .border-white { border-color: var(--zk-line) !important; }

/* ── Soft tinted panels ─────────────────────────────────────────────────────
   The pastel 50/100 tints (amber-50, green-50, indigo-50 …) are near-white and
   become glaring holes on a dark page. Knock them back to a dark tint of the
   same hue by compositing the panel's own ink over the card. One rule per
   family would be 30 rules; color-mix does it generically. */
@supports (background: color-mix(in srgb, red 10%, blue)) {
  :root[data-zk-theme="dark"] [class*="bg-amber-5"],
  :root[data-zk-theme="dark"] [class*="bg-amber-1"],
  :root[data-zk-theme="dark"] [class*="bg-yellow-5"],
  :root[data-zk-theme="dark"] [class*="bg-orange-5"],
  :root[data-zk-theme="dark"] [class*="bg-green-5"],
  :root[data-zk-theme="dark"] [class*="bg-emerald-5"],
  :root[data-zk-theme="dark"] [class*="bg-teal-5"],
  :root[data-zk-theme="dark"] [class*="bg-blue-5"],
  :root[data-zk-theme="dark"] [class*="bg-indigo-5"],
  :root[data-zk-theme="dark"] [class*="bg-violet-5"],
  :root[data-zk-theme="dark"] [class*="bg-purple-5"],
  :root[data-zk-theme="dark"] [class*="bg-pink-5"],
  :root[data-zk-theme="dark"] [class*="bg-rose-5"],
  :root[data-zk-theme="dark"] [class*="bg-red-5"] {
    background-color: color-mix(in srgb, currentColor 12%, var(--zk-card)) !important;
  }
}

/* ── Correction feedback panel (chat) ────────────────────────────────────────
   appendCorrection() in app.html renders spelling/grammar/naturalness feedback
   using literal Tailwind accent inks — text-green-600, text-amber-700,
   text-purple-700, text-blue-700, text-orange-600/700 — picked for a WHITE
   panel. The generic sweep above deliberately leaves text-*-600/700 alone
   (comment at :645: they're also used as text on solid coloured buttons,
   where darkening would be wrong). That correctly-scoped exception left THIS
   use uncovered: dark, saturated accent text on the color-mix'd dark panel
   the same block backgrounds now produce — "green above orange, hard to
   read" was this: a summary line and category labels both too dark for the
   surface under them. Scoped to `.zk-correction` (the hook added at the call
   site) rather than swept globally, because the button case above is real and
   a blanket rule would break it.

   EXPLICIT COLOURS, NOT color-mix(currentColor).
   The first version of this block wrote
       color: color-mix(in srgb, currentColor 62%, white)
   which is WRONG, and wrong in a way that looks right. Inside the `color`
   property itself, `currentColor` resolves to the INHERITED colour — not the
   element's own — because that is the value `color` is in the middle of
   defining. So every label rendered as one lightened copy of the parent ink
   and the green/blue/purple/amber coding was flattened into a single colour.
   (The rule at :675 uses the same function correctly: in `background-color`,
   `currentColor` really is this element's own text colour.)

   These are Tailwind's 400 ramp — the standard dark-mode step for a 600/700
   ink — measured against the panel these actually sit on (≈#302A22, the
   green-50/orange-50 fill after the color-mix at :675): green 8.1:1,
   amber 8.1:1, orange 5.9:1, blue 5.4:1, purple 5.2:1. All clear the 3:1
   floor for bold small text with room to spare, and the hue survives. */
:root[data-zk-theme="dark"] .zk-correction [class*="text-green-6"],
:root[data-zk-theme="dark"] .zk-correction [class*="text-green-7"]  { color: #4ADE80 !important; }
:root[data-zk-theme="dark"] .zk-correction [class*="text-orange-6"],
:root[data-zk-theme="dark"] .zk-correction [class*="text-orange-7"] { color: #FB923C !important; }
:root[data-zk-theme="dark"] .zk-correction [class*="text-amber-6"],
:root[data-zk-theme="dark"] .zk-correction [class*="text-amber-7"]  { color: #FBBF24 !important; }
:root[data-zk-theme="dark"] .zk-correction [class*="text-purple-6"],
:root[data-zk-theme="dark"] .zk-correction [class*="text-purple-7"] { color: #C084FC !important; }
:root[data-zk-theme="dark"] .zk-correction [class*="text-blue-6"],
:root[data-zk-theme="dark"] .zk-correction [class*="text-blue-7"]   { color: #60A5FA !important; }
/* border-green-200/orange-200 are pale light-mode hairlines (#BBF7D0 / #FED7AA)
   — a bright outline round a dark panel. Muted to the same hue at dark value. */
:root[data-zk-theme="dark"] .zk-correction [class*="border-green-2"]  { border-color: #2E5A3C !important; }
:root[data-zk-theme="dark"] .zk-correction [class*="border-orange-2"] { border-color: #6B3A18 !important; }

/* ── Chat bubbles ───────────────────────────────────────────────────────── */
/* The incoming bubble is the one surface where "just use --zk-card" is wrong:
   on dark it would be identical to the card it sits on and the conversation
   would lose its shape entirely. One step up from the card, same as light mode
   puts it one step down from white. */
:root[data-zk-theme="dark"] .bubble-ai { background: var(--zk-card-2) !important; }

/* ── The theme switch (rendered in the settings sheet, M4) ──────────────────
   Was two flex:1 column buttons with a 20px emoji on its own line — ~62px tall
   across half the sheet, for a BINARY setting, in a design system that already
   owns a control for exactly this. `.zk-segbar` (app.html:359) is that control
   and is used for chat display mode, Words sub-tabs and the Practice mode bar.
   Inventing a second one made the sheet look like two different products.
   No rules needed here: the switch now renders .zk-segbar markup. */

/* ═══════════════════════════════════════════════════════════════════════════
   THE SHARED GAME SHELL  —  #game-view
   ───────────────────────────────────────────────────────────────────────────
   THE PROBLEM
   The Practice LANDING screen is V4 (paper, warm hairlines, hard offset
   shadows, orange accent). Open any of the 37 games and you land in a different
   product: ~350 cold utility classes across the renderers — 30 bg-blue-50,
   25 bg-blue-100, 22 text-blue-600, 17 text-indigo-700, 72 bg-gray-*, plus 71
   rounded-2xl and 44 blurred shadow-md/lg. Indigo and blue are not in the V4
   palette at all.

   WHY THIS AND NOT 37 REWRITES
   Editing the renderers means touching 37 game implementations, each with its
   own markup, to change colours only — high risk, no functional gain, and the
   next game added would reintroduce the drift. This is the same UTILITY
   REDEFINITION that already carried the warm sweep (250 cold greys), the purple
   sweep (38 indigo sites) and dark mode: redefine what the class MEANS, in one
   place, and every current and future call site follows.

   WHY IT IS SAFE
   Every rule is scoped to `#game-view`, the container games render into. It
   cannot reach the Practice landing (already V4), the app shell, or app.html —
   games.html is a separate document and #game-view is hidden until a game
   opens. And because the values are tokens, this inherits dark mode for free:
   the games were the one area explicitly excluded from M1.

   SEMANTIC COLOUR IS KEPT, BUT NOT LEFT UNREADABLE
   Green = correct, red = wrong, amber = warning. Those carry MEANING inside a
   game and must never be flattened into the brand accent. But "keep the hue"
   is not the same as "leave the pair alone": a pale green-50 fill with
   green-800 text is a light-mode pairing, and in dark mode the fill turns into
   a glaring panel. So the HUE is preserved and the PAIR is re-balanced per
   theme, at the bottom of this block.

   Same for bg-gray-900/800: several games use a deliberately dark panel as a
   contrast surface. That is design, not drift — but on a dark page it stops
   being a contrast surface, so in dark mode it is lifted to --zk-card-2.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Shape: sticker, not soft card ─────────────────────────────────────────
   The V4 signature is a hard offset shadow with no blur. Blurred shadows are
   most of what makes the game screens read as "a different app". */
#game-view .rounded-2xl,
#game-view .rounded-xl        { border-radius: var(--zk-radius-card) !important; }
#game-view .shadow-sm,
#game-view .shadow-md,
#game-view .shadow-lg,
#game-view .shadow-xl         { box-shadow: var(--zk-shadow) !important; }

/* ── Surfaces ───────────────────────────────────────────────────────────── */
#game-view .bg-white          { background-color: var(--zk-card) !important; }
#game-view .bg-gray-50,
#game-view .bg-slate-50,
#game-view .bg-gray-100,
#game-view .bg-slate-100      { background-color: var(--zk-card-2) !important; }
#game-view .bg-gray-200,
#game-view .bg-slate-200      { background-color: var(--zk-line) !important; }

/* ── The accent: indigo/blue → brand orange ─────────────────────────────────
   These are "primary action" colours in the game renderers (buttons, selected
   states, headers). V4 has exactly one accent and it is not blue. */
#game-view .bg-blue-50,
#game-view .bg-indigo-50,
#game-view .bg-violet-50      { background-color: var(--zk-primary-light) !important; }
#game-view .bg-blue-100,
#game-view .bg-indigo-100     { background-color: var(--zk-primary-light) !important; }
#game-view .bg-blue-500,
#game-view .bg-blue-600,
#game-view .bg-indigo-500,
#game-view .bg-indigo-600     { background-color: var(--zk-primary) !important; }
#game-view .bg-blue-700,
#game-view .bg-indigo-700     { background-color: var(--zk-primary-dark) !important; }
#game-view .hover\:bg-blue-600:hover,
#game-view .hover\:bg-indigo-700:hover { background-color: var(--zk-primary-dark) !important; }

#game-view .text-blue-400,
#game-view .text-indigo-400   { color: var(--zk-ink-3) !important; }
#game-view .text-blue-500,
#game-view .text-blue-600,
#game-view .text-blue-700,
#game-view .text-indigo-500,
#game-view .text-indigo-600,
#game-view .text-indigo-700   { color: var(--zk-primary-dark) !important; }

#game-view [class*="border-blue-"],
#game-view [class*="border-indigo-"] { border-color: var(--zk-primary) !important; }
#game-view [class*="border-gray-1"],
#game-view [class*="border-gray-2"],
#game-view [class*="border-slate-1"],
#game-view [class*="border-slate-2"] { border-color: var(--zk-line) !important; }

/* ── Ink ────────────────────────────────────────────────────────────────── */
#game-view [class*="text-gray-9"],
#game-view [class*="text-gray-8"],
#game-view [class*="text-slate-8"] { color: var(--zk-ink) !important; }
#game-view [class*="text-gray-7"],
#game-view [class*="text-gray-6"],
#game-view [class*="text-gray-5"],
#game-view [class*="text-slate-5"] { color: var(--zk-ink-2) !important; }
#game-view [class*="text-gray-4"],
#game-view [class*="text-gray-3"] { color: var(--zk-ink-3) !important; }

/* ── Buttons get the tactile press ─────────────────────────────────────────
   A V4 button travels 4px into its own shadow. Without this the game buttons
   are the right colour but still feel like a different product. */
#game-view button.bg-blue-500,
#game-view button.bg-blue-600,
#game-view button.bg-indigo-500,
#game-view button.bg-indigo-600 {
  box-shadow: 0 4px 0 var(--zk-primary-dark) !important;
  transition: transform .08s, box-shadow .08s;
}
#game-view button.bg-blue-500:active,
#game-view button.bg-blue-600:active,
#game-view button.bg-indigo-500:active,
#game-view button.bg-indigo-600:active {
  transform: translateY(4px);
  box-shadow: 0 0 0 var(--zk-primary-dark) !important;
}

/* ── DECORATIVE TEAL / VIOLET / PURPLE → THE PALETTE ───────────────────────
   The games carry ~40 teal and ~30 violet/purple surfaces that are pure
   decoration — panel tints, chip fills, section accents. V4 has one accent
   (orange) plus two SEMANTIC colours it uses sparingly (teal = done, violet =
   lesson), so a screen full of independent teal and purple reads as a
   different product even after the blues were mapped.

   Careful distinction, and it is the whole reason this is a separate block
   from the semantic one below: GREEN and RED and AMBER are left to mean
   correct / wrong / warning. Teal and violet here are NOT carrying meaning —
   they are just colour someone reached for — so they join the palette. */
#game-view .bg-teal-50,
#game-view .bg-teal-100,
#game-view .bg-violet-50,
#game-view .bg-violet-100,
#game-view .bg-purple-50,
#game-view .bg-purple-100    { background-color: var(--zk-primary-light) !important; }
#game-view .bg-teal-500,
#game-view .bg-teal-600      { background-color: var(--zk-teal) !important; }
#game-view [class*="text-violet-"],
#game-view [class*="text-purple-"] { color: var(--zk-violet) !important; }
/* DARK: the accent tint these sit on is #3A2415 — dark. So the ink has to be
   the LIGHT end of the accent, not --zk-primary-dark, or you get 2.66:1.
   Pass 2 of check-contrast.js found this by resolving the class pairs the game
   markup actually uses; the CSS-only pass could not see it. */
:root[data-zk-theme="dark"] #game-view [class*="text-blue-"],
:root[data-zk-theme="dark"] #game-view [class*="text-indigo-"] { color: var(--zk-primary) !important; }
:root[data-zk-theme="dark"] #game-view [class*="text-violet-"],
:root[data-zk-theme="dark"] #game-view [class*="text-purple-"] { color: var(--zk-tint-note-ink) !important; }
/* Tailwind's green-500/emerald-500 with white is 2.28:1 — poor in Tailwind
   itself, not something the theme introduced. These are "correct"/"go" fills,
   so the hue stays and the value deepens to the one that carries white. */
#game-view .bg-green-500,
#game-view .bg-green-600,
#game-view .bg-emerald-500,
#game-view .bg-emerald-600 { background-color: var(--zk-teal-d) !important; }
/* White on a light grey fill is never readable. Those are disabled/muted
   buttons; give them ink instead of trying to keep the white. */
#game-view .bg-gray-200.text-white,
#game-view .bg-gray-300.text-white,
#game-view .bg-gray-400.text-white { color: var(--zk-ink) !important; }
/* …and in dark mode the FILL has to move too. A mid-grey (#9ca3af) with the
   dark theme's cream ink is 2.26:1 — swapping only the ink fixed light mode and
   left dark broken, which is exactly the half-fix this checker exists to catch. */
:root[data-zk-theme="dark"] #game-view .bg-gray-200,
:root[data-zk-theme="dark"] #game-view .bg-gray-300,
:root[data-zk-theme="dark"] #game-view .bg-gray-400 { background-color: var(--zk-line) !important; }
#game-view [class*="text-teal-"]   { color: var(--zk-teal-d) !important; }
:root[data-zk-theme="dark"] #game-view [class*="text-teal-"] { color: var(--zk-teal) !important; }
/* Emerald is used interchangeably with green for "correct" in some games and
   as plain decoration in others. Treated as decoration for FILLS at the 50/100
   steps (the tint that clashes) and left alone at 500+ where it is a state. */
#game-view .bg-emerald-50,
#game-view .bg-emerald-100   { background-color: var(--zk-tint-good) !important; }

/* ── SEMANTIC PAIRS, RE-BALANCED FOR DARK ──────────────────────────────────
   Correct/wrong/warning keep their hue in both themes — that meaning is not
   negotiable. What changes is the LIGHTNESS PAIRING: green-50 + green-800 is
   a light-mode combination, and on a dark page the pale fill becomes a lamp
   and the dark ink on it stops being the problem only because the whole panel
   is now wrong. Dark tint + light ink, same hue, same meaning. */
:root[data-zk-theme="dark"] #game-view [class*="bg-green-1"],
:root[data-zk-theme="dark"] #game-view [class*="bg-green-5"],
:root[data-zk-theme="dark"] #game-view [class*="bg-emerald-1"],
:root[data-zk-theme="dark"] #game-view [class*="bg-emerald-5"] { background-color: var(--zk-tint-good) !important; }
:root[data-zk-theme="dark"] #game-view [class*="text-green-7"],
:root[data-zk-theme="dark"] #game-view [class*="text-green-8"],
:root[data-zk-theme="dark"] #game-view [class*="text-green-9"],
:root[data-zk-theme="dark"] #game-view [class*="text-emerald-7"],
:root[data-zk-theme="dark"] #game-view [class*="text-emerald-8"] { color: var(--zk-tint-good-ink) !important; }

:root[data-zk-theme="dark"] #game-view [class*="bg-red-1"],
:root[data-zk-theme="dark"] #game-view [class*="bg-red-5"],
:root[data-zk-theme="dark"] #game-view [class*="bg-rose-1"],
:root[data-zk-theme="dark"] #game-view [class*="bg-rose-5"] { background-color: var(--zk-tint-bad) !important; }
:root[data-zk-theme="dark"] #game-view [class*="text-red-7"],
:root[data-zk-theme="dark"] #game-view [class*="text-red-8"],
:root[data-zk-theme="dark"] #game-view [class*="text-red-9"],
:root[data-zk-theme="dark"] #game-view [class*="text-rose-7"] { color: var(--zk-tint-bad-ink) !important; }

:root[data-zk-theme="dark"] #game-view [class*="bg-amber-1"],
:root[data-zk-theme="dark"] #game-view [class*="bg-amber-5"],
:root[data-zk-theme="dark"] #game-view [class*="bg-yellow-1"],
:root[data-zk-theme="dark"] #game-view [class*="bg-yellow-5"] { background-color: var(--zk-tint-warn) !important; }
:root[data-zk-theme="dark"] #game-view [class*="text-amber-7"],
:root[data-zk-theme="dark"] #game-view [class*="text-amber-8"],
:root[data-zk-theme="dark"] #game-view [class*="text-amber-9"],
:root[data-zk-theme="dark"] #game-view [class*="text-yellow-7"] { color: var(--zk-tint-warn-ink) !important; }

/* The deliberate dark panels stop being a contrast surface on a dark page. */
:root[data-zk-theme="dark"] #game-view .bg-gray-900,
:root[data-zk-theme="dark"] #game-view .bg-gray-800,
:root[data-zk-theme="dark"] #game-view .bg-slate-900,
:root[data-zk-theme="dark"] #game-view .bg-slate-800 { background-color: var(--zk-card-2) !important; }

/* LIGHT mode: the same panels carry light text, which must survive the fill
   staying dark — so ink on them is pinned, not swept by the grey rules above. */
#game-view .bg-gray-900 [class*="text-gray-"],
#game-view .bg-gray-800 [class*="text-gray-"],
#game-view .bg-slate-900 [class*="text-slate-"] { color: #F7F1E8 !important; }
:root[data-zk-theme="dark"] #game-view .bg-gray-900 [class*="text-gray-"],
:root[data-zk-theme="dark"] #game-view .bg-gray-800 [class*="text-gray-"] { color: var(--zk-ink) !important; }
