/* ═══════════════════════════════════════════════════
   Tokens — Warm Minimal (Japandi) + Editorial
═══════════════════════════════════════════════════ */
:root {
  /* ── DEFAULT LOOK = the themed "energy" redesign (warm-dark, glowing).
     This replaces the old washi light palette; because the whole app is
     token-driven, changing these VALUES retints navbar/cards/buttons/text
     everywhere. Scene themes (:root[data-scene]) shift the accent family on
     top of this. The old washi values live on only as a light-mode variant. */
  --paper:      #181410;   /* page — warm near-black ground */
  --paper-card: #262019;   /* raised card surface */
  --paper-2:    #2f2820;   /* deeper inset / pressed state */

  --ink:   #f2ecdf;   /* primary text — warm off-white */
  --ink-2: #c8bfa9;   /* secondary text */
  --ink-3: #8f8571;   /* tertiary / placeholder */

  --line:   #3a3125;  /* hairline border */
  --line-2: #4d4132;  /* stronger / focus border */

  /* Accent system — vivid-but-earthy amber/coral primary with a cool teal
     focus, and brighter feedback colours tuned for a dark ground. */
  --accent:      #f2a83c;
  --accent-ink:  #22150a;   /* text on accent fill */
  --accent-text: var(--accent);  /* accent-COLORED text on --paper-card; scenes recompute it (theme_scenes) to --ink where --accent fails AA on the scene's card. Base pairing passes. */
  --on-ink:      #1a1206;   /* text on a solid --ink fill (base: --ink is near-white → dark ink). Recomputed per-scene in theme_scenes mount() */
  --accent-wash: #3a2c14;
  --accent-wash-2:#54401c;

  --clay:      #f2683c;   /* streak / energy */
  --clay-wash: #3a1f14;

  --moss:      #7cc86a;   /* success / correct / mastered */
  --moss-wash: #1f3a1c;

  --brick:      #ff6f5e;  /* error / incorrect */
  --brick-wash: #3f231e;

  --ochre:      #f5c542;  /* warn / in-progress */
  --ochre-wash: #3d301a;

  /* Signature reward gradient — coral → amber → gold. */
  --grad-warm:      linear-gradient(135deg, #f2683c 0%, #f2a83c 55%, #f5c542 100%);
  --grad-warm-soft: linear-gradient(135deg, rgba(242,104,60,.18), rgba(242,168,60,.15), rgba(245,197,66,.12));
  --grad-warm-ink:  #22150a;

  /* Floating layers — a soft dark-ground glow shadow (not the flat washi one). */
  --sh-float: 0 20px 40px -16px rgba(0,0,0,.55), 0 0 22px -10px var(--accent);

  --r-sm: 3px;
  --r-md: 5px;
  --r-lg: 8px;
  /* base corner shape for the themed look (scenes override via data-shape) */
  --rBtn: 12px;
  --rCard: 16px;

  /* Scene-var aliases: the themed-look rules (glow/pills/quiz) below read
     --a1/--a2/--a3/--gold/--reward/--gk/--bg2/--card/--card2/--edge/--edge2/--txt.
     Default them to the base palette so the themed look is the DEFAULT; an
     active scene (:root[data-scene]) overrides --a1..--gk with its own accents. */
  --a1:     var(--accent);
  --a2:     var(--clay);
  --a3:     var(--ochre);
  --gold:   var(--ochre);
  --reward: var(--grad-warm);
  --gk:     1;
  --stage:  var(--paper);
  --bg2:    var(--paper-2);
  --card:   var(--paper-card);
  --card2:  var(--paper-2);
  --edge:   var(--line);
  --edge2:  var(--line-2);
  --txt:    var(--ink);
  --txt2:   var(--ink-2);
  --txt3:   var(--ink-3);
  --onr:    var(--grad-warm-ink);

  /* One font, everywhere — Noto Sans JP covers Latin and Japanese in the
     same face, so headings/body/UI/Japanese glyphs/numbers all read as one
     consistent typeface instead of the previous four-family mix. The
     --serif/--sans/--mono names are kept (hundreds of existing var(...)
     references) but all resolve to the same family now; tabular-nums
     still keeps score/stat digits aligned without a separate mono font. */
  --jp:    'Noto Sans JP', 'Hiragino Sans', sans-serif;
  --serif: 'Noto Sans JP', 'Hiragino Sans', sans-serif;
  --sans:  'Noto Sans JP', 'Hiragino Sans', -apple-system, sans-serif;
  --mono:  'Noto Sans JP', 'Hiragino Sans', ui-monospace, monospace;
  --ui:    var(--sans);

  --sidebar: 260px;
  --header:  56px;
  --ease:    cubic-bezier(.22,1,.36,1);
  --ease-island: cubic-bezier(.22,1.12,.32,1);   /* the island's overshoot */
  --ease-bounce: cubic-bezier(.34,1.56,.64,1);

  /* Legacy aliases — a few inline styles in app.js reference the old token
     names directly; keep them resolving to the new palette. */
  --t1: var(--ink);
  --t2: var(--ink-2);
  --t3: var(--ink-3);
  --ok: var(--moss);
  --err: var(--brick);
  --warn: var(--ochre);

  color-scheme: dark;
}

/* ── BREAKPOINT POLICY ───────────────────────────────────────────────────────
   Written down rather than consolidated, on purpose. `@media (max-width: var(--bp))`
   is invalid CSS — custom properties do not work in a media prelude — and there
   is no CSS build step, so a "scale" can only ever be a documented allowlist.
   test/responsive.test.js enforces this table: adding a 17th value fails the
   build until you either pick an existing tier or amend both.

   Consolidating the existing ones was considered and rejected: it would move
   ~200 rules across bands that have no visual test coverage (below 1024 the
   only machine-checked widths are 390 and 768, via qa/crawl.js).

   STRUCTURAL — moves the app grid or the navigation. FROZEN.
     1480   .app-grid → 2 columns, left rail out
     1120 + min-721   the ☰ nav-collapse band (was 1024 — raised because the
            nav wrapped to 3-4 rows and a 95-131px header between 1025 and 1120)
     1121 + max-1559  reserves the header height where the nav still wraps to
            two rows, so it cannot grow when the nav renders (CLS)
      980   .app-grid → 1 column, both rails out
     1120 and 721 are load-bearing against shell/mobile.js's
     matchMedia('(max-width:720px)') drawer gate: widen the phone tier without
     it and an iPad gets the ☰ collapse AND the drag drawer at the same time.
     1559/1121 come from measurement, not taste: the nav fits one row in BOTH
     Noto and the fallback face at >=1560, and wraps below it.

   HEADER BUDGET — a monotone squeeze chain, header only. FROZEN; each step was
   tuned against a real window size, and the comments at each explain which.
     1820 · 1500 · 1300 · 1120

   PHONE — the two real tiers. Everything new goes here.
      720   the phone package (9 blocks): drawer, sticky header, safe-area
            insets, dvh modals, scroll handed to the document
      520   the small-phone tier (10 blocks)

   COMPONENT ONE-OFFS — a closed set. A component may pick one of these; do not
   introduce a 17th value.
      900 · 860 · 640 · 560 · 480 · 440 · 380

   ALSO: @media (hover:none) and (pointer:coarse) — touch ergonomics and the
   tap-target floors. Prefer this over a width whenever the thing you are fixing
   is about the INPUT rather than the layout: it reaches tablets, and it cannot
   regress a mouse.
   ── */
/* ═══════════════════════════════════════════════════
   Dark mode IS the default now (:root carries the dark themed palette), so
   data-theme="dark" is just an explicit selector for it — no token overrides
   needed. (The old cyan dark palette lived here; it's been retired along with
   the washi default. Light mode restores washi via [data-theme="light"] above.)
═══════════════════════════════════════════════════ */
:root[data-theme="dark"] { color-scheme: dark; }

/* ── Day ⇄ night crossfade ────────────────────────────────────────────────────
   Switching theme rewrites the custom properties above, and custom properties
   don't animate — every surface repainted on the same frame, so the flip landed
   as a hard snap. Transitioning the *consuming* properties instead gives one
   coordinated crossfade.

   Scoped to a .theme-anim class that toggleTheme() adds for the duration of the
   switch and removes afterwards, NOT left on permanently: a standing global
   `transition:background-color` would lag every hover/active state in the app
   and fight the per-component transitions that already exist.

   It applies to EVERY element (* below), deliberately. The first version listed
   ~18 selectors by hand — but ~710 rules in this file paint a themed surface,
   so the listed handful faded while the N5 panel, leaderboard, dropdowns, game
   boards, badges and the rest snapped instantly. A partial fade reads worse
   than none: the eye tracks the parts that jumped. Enumerating surfaces was
   never going to hold as the app grows, so the rule is inverted — everything
   fades, for the ~1s the class is on.

   Only colour-ish properties are listed, never `transform`/`opacity`/`width`,
   so animations in flight (combo meter, caret, cameos, scene drift) are not
   disturbed. `transition-property` is set rather than the `transition`
   shorthand so a component's own duration/easing for OTHER properties survives.

   --theme-fade is the SINGLE source of truth for the duration: app.js reads it
   off the computed style rather than keeping its own copy, so the class is
   always removed after the transition actually finishes. Deliberately slow
   (1.6s): dark→light is a big luminance jump and the eye needs time to adapt.
   The ease keeps most of the change in the middle of the window, so it leaves
   and lands gently instead of ramping hard at either end. */
:root { --theme-fade:1.6s; --theme-ease:cubic-bezier(.45,.05,.35,1); }

/* `background-image` is deliberately NOT here: gradients don't interpolate
   between different colour stops, so listing it buys nothing and hides the
   problem. Where a wash has to change between themes it's built as a stacked
   layer faded by `opacity` instead (see #sceneLayer #bg::after).
   `opacity` is NOT in this universal list either — 18 rules in this file use it
   for show/hide (dropdowns, modals, the lookup panel), and overriding those to
   1.6s for the duration of a theme switch would make the UI feel broken. It's
   opted into per-surface below, only where a theme change actually moves it. */
.theme-anim, .theme-anim *, .theme-anim *::before, .theme-anim *::after {
  transition-property:background-color, border-color, color, box-shadow,
                      text-shadow, fill, stroke, outline-color !important;
  transition-duration:var(--theme-fade) !important;
  transition-timing-function:var(--theme-ease) !important;
  transition-delay:0s !important;
}
/* The scene backdrop: the only place opacity/filter carry the theme change
   rather than a show/hide. Without these the light wash and the artwork
   brightness would land on frame 1 — the "snaps to white, then eases" flash. */
.theme-anim #sceneLayer #bg, .theme-anim #sceneLayer #bg::after,
.theme-anim #sceneLayer .scene svg, .theme-anim #sceneLayer .aurora {
  transition-property:background-color, opacity, filter, fill, stroke !important;
}
/* The header also changes size between themes (--header: 64px dark / 60px
   light). It sets `min-height`, not `height`, so that's the property to
   animate — transitioning `height` would do nothing here. */
.theme-anim .app-header {
  transition-property:background-color, border-color, color, box-shadow,
                      text-shadow, min-height !important;
}


@media (prefers-reduced-motion:reduce) {
  .theme-anim, .theme-anim *, .theme-anim *::before, .theme-anim *::after {
    transition:none !important;
  }
}

/* ═══════════════════════════════════════════════════
   Light mode — the original washi/sand palette, kept as the LIGHT VARIANT of
   the toggle now that the default (:root) is the dark themed look. Selected
   when the user (or their OS) chooses light.
═══════════════════════════════════════════════════ */
:root[data-theme="light"] {
  --paper:      #f1ead9;
  --paper-card: #f8f4ea;
  --paper-2:    #e7ddc4;
  --ink:   #211c15;
  --ink-2: #5c5346;
  --ink-3: #948a78;
  --line:   #ddd0b4;
  --line-2: #c2b090;
  --accent:      #1d6478;
  --accent-ink:  #fbf6ea;
  --accent-text: var(--accent);  /* see dark-theme note; light base pairing passes AA */
  --on-ink:      #fff8f0;   /* light theme: --ink is near-black → light ink on an --ink fill */
  --accent-wash: #d7e8ea;
  --accent-wash-2:#b3d4d8;
  --clay:      #dd5f35;  --clay-wash: #fbe0d1;
  --moss:      #3f8f4f;  --moss-wash: #d9efdc;
  --brick:      #d33f30; --brick-wash: #fbdcd6;
  --ochre:      #d99a2b; --ochre-wash: #faead0;
  --grad-warm:      linear-gradient(135deg, #e2603a 0%, #ef8a3d 55%, #f0b429 100%);
  --grad-warm-soft: linear-gradient(135deg, rgba(226,96,58,.16), rgba(239,138,61,.13), rgba(240,180,41,.11));
  --grad-warm-ink:  #241c11;
  --sh-float: 0 16px 34px -16px rgba(33,28,21,.32), 0 2px 5px -2px rgba(33,28,21,.14);
  color-scheme: light;
}

body::before { content:none; }   /* no tiled pattern — see .bg-scenes below */

/* ═══════════════════════════════════════════════════
   Background scenery — hand-drawn pixel-art panoramas (Tokyo Tower over a
   city skyline, Mt. Fuji on a forested plain with a torii, a map of Japan
   on a wave-flecked sea, two blossoming sakura on a meadow), one at a
   time, slowly crossfading (initBgScenes in app.js). Sprites live in
   public/img/bg-*.png, regenerated by tools/gen-bg-pixel-art.py;
   image-rendering:pixelated + big upscales keep the pixels crisp. Each
   scene also carries a soft matching sky tint from the bottom of the
   viewport (dusk amber for the city, sky-blue-to-green for Fuji, sea teal
   for the map, pink for the sakura) so the backdrop isn't bare paper.
═══════════════════════════════════════════════════ */
#bgScenes {
  position:fixed;
  inset:0;
  z-index:-1;
  overflow:hidden;
  pointer-events:none;
}
.bg-scene {
  position:absolute;
  inset:0;
  opacity:0;
  transition:opacity 2.5s ease;
}
.bg-scene.active { opacity:1; }
.bgs-img {
  /* Full-bleed: every scene is a 280×100 panorama stretched edge to edge,
     with its landmarks drawn at the left/right ends of the sprite — the
     middle stays low and quiet since the app's content column covers it. */
  position:absolute; bottom:0; left:0;
  width:100vw;
  image-rendering:pixelated;
  opacity:.9;
}
:root[data-theme="dark"] .bgs-img { opacity:.6; }

/* Scene-matched ambient sky, rising from the horizon — fades in/out with
   the scene itself since it sits on the crossfading .bg-scene layer. */
.bgs-tower  { background:linear-gradient(180deg, transparent 42%, rgba(238,150,92,.16) 100%); }
.bgs-fuji   { background:linear-gradient(180deg, transparent 38%, rgba(122,170,199,.13) 76%, rgba(122,160,112,.17) 100%); }
.bgs-map    { background:linear-gradient(180deg, transparent 45%, rgba(102,162,172,.14) 100%); }
.bgs-sakura { background:linear-gradient(180deg, transparent 45%, rgba(229,151,171,.15) 100%); }
:root[data-theme="dark"] .bgs-tower  { background:linear-gradient(180deg, transparent 42%, rgba(255,140,80,.09) 100%); }
:root[data-theme="dark"] .bgs-fuji   { background:linear-gradient(180deg, transparent 38%, rgba(120,180,210,.07) 76%, rgba(130,180,120,.09) 100%); }
:root[data-theme="dark"] .bgs-map    { background:linear-gradient(180deg, transparent 45%, rgba(110,180,190,.08) 100%); }
:root[data-theme="dark"] .bgs-sakura { background:linear-gradient(180deg, transparent 45%, rgba(255,160,180,.08) 100%); }

@media (max-width:720px) { #bgScenes { display:none; } }
@media (prefers-reduced-motion:reduce) { .bg-scene { transition:none; } }

/* ═══════════════════════════════════════════════════
   Sakura — a slow drift of falling petals across the whole app (login
   screen included). Ambient only: low opacity, behind every real UI layer,
   never intercepts clicks. JS (spawnSakura in app.js) creates the petals;
   this just defines how one falls.
═══════════════════════════════════════════════════ */
.sakura-layer {
  /* Negative z-index, same tier as #bgScenes — guaranteed to stay behind
     every normal-flow/positioned UI element in the app regardless of that
     element's own z-index, since negative-z always paints under level-0+
     content. DOM order (this sits right after #bgScenes) puts it above
     the landmark silhouettes so the petals are visible. */
  position:fixed;
  inset:0;
  z-index:-1;
  overflow:hidden;
  pointer-events:none;
}
.sakura-petal {
  position:absolute;
  top:-12%;
  left:var(--x, 50%);
  font-size:var(--size, 1.2rem);
  opacity:0;
  line-height:1;
  animation:sakuraFall var(--dur, 14s) linear infinite;
  animation-delay:var(--delay, 0s);
  will-change:transform, opacity;
}
@keyframes sakuraFall {
  0%   { transform:translate3d(0,-10vh,0) rotate(0deg); opacity:0; }
  8%   { opacity:var(--op, .55); }
  50%  { transform:translate3d(calc(var(--drift, 40px) * .55), 54vh, 0) rotate(165deg); }
  92%  { opacity:var(--op, .55); }
  100% { transform:translate3d(var(--drift, 40px), 112vh, 0) rotate(330deg); opacity:0; }
}
@media (max-width:720px) { .sakura-layer { display:none; } }
@media (prefers-reduced-motion:reduce) { .sakura-layer { display:none; } }

/* ═══════════════════════════════════════════════════
   Shared entrance animation
═══════════════════════════════════════════════════ */
@keyframes viewIn {
  from { opacity:0; transform:translateY(10px); }
  to   { opacity:1; transform:translateY(0); }
}

/* ═══════════════════════════════════════════════════
   Reset
═══════════════════════════════════════════════════ */
*,*::before,*::after { box-sizing:border-box; margin:0; padding:0; }
html { font-size:18px; }   /* bumped from 16px — scales all rem-sized text/UI app-wide, less eye strain */

/* The page GROUND, on <html> as well as on the scene layer. Two reasons, and the
   second one is a bug fix.
   1. Safari 26 stopped using <meta name="theme-color"> for browser-UI tinting. It
      now reads the background-color of <body>, falls back to <html>, and if
      NEITHER is set defaults to white in light mode and black in dark. The app
      set neither: `body.scene-on { background:transparent !important }` (see the
      scene section) deliberately clears body's so #sceneLayer shows through, and
      html had none — so every scene, measured, gave html=rgba(0,0,0,0) and
      body=rgba(0,0,0,0), and Safari painted its own flat dark. That is why the
      dashboard had no coloured tab while /admin and /review (which never load
      theme_scenes.js, so body keeps its ground) tinted correctly.
   2. It also retires a hazard the scene section documents: with body transparent,
      #sceneLayer #bg's var(--stage) was the ONLY opaque surface in the chain, so
      anything that hid or failed to cover with that layer exposed the UA default
      WHITE behind a dark scene.
   Invisible on screen: the root background is the canvas, painted BELOW negative
   z-index, and #sceneLayer (z-index:-1) carries the same --stage over it. Verified
   pixel-identical at 1440 and 390 across scenes and both themes. */
html { background-color:var(--stage); }
/* The whole app — games included — stays at the calm 18px landing size. A global
   `html.kq-authed { font-size:15px }` used to shrink the entire logged-in app just
   to make the oversized DASHBOARD fit, but it shrank the games with it. The
   dashboard is now compacted on its own via `zoom` (see .dashboard-view below),
   so games keep their full size. `.kq-authed` stays on <html> for other scoping. */
body {
  font-family:var(--sans);
  background:var(--paper);
  color:var(--ink);
  min-height:100dvh;
  -webkit-font-smoothing:antialiased;
  overflow:hidden;
  /* A long unbroken string — an AI-generated gloss, a pasted URL, a 30-character
     username — has nothing to break on, and only two rules in 8,000+ lines said
     so. This is `break-word` deliberately, NOT `anywhere` and NOT
     word-break:break-all:
       • break-all breaks ordinary English mid-word.
       • `anywhere` ALSO lets the intrinsic min-content width shrink, which
         silently changes how every flex and grid track in the app sizes
         itself — the riskiest single thing you could do to this stylesheet.
       • break-word only breaks a word that would otherwise OVERFLOW, and
         leaves min-content sizing alone, so it cannot move any existing layout.
     It inherits, and it is a no-op wherever white-space:nowrap is set — which
     is all 58 of those sites, i.e. every chrome pill and game-head slot. It
     also composes correctly with the `word-break:keep-all` used for CJK: that
     forbids breaks BETWEEN Japanese characters, and this still permits an
     emergency break. */
  overflow-wrap:break-word;
}
a { color:inherit; text-decoration:none; }
button { font-family:var(--sans); cursor:pointer; border:none; background:none; color:inherit; }
ul { list-style:none; }
.hidden { display:none !important; }
/* Furigana baseline — browsers default <rt> to ~50% of the parent's font
   size, which reads as illegibly tiny over kanji. Raise the floor for any
   ruby/rt this stylesheet doesn't already give a bigger, context-specific
   size to (see the per-component ruby rt rules for the primary reading
   displays, which are sized larger still). */
rt { font-size:.7em; }

/* ═══════════════════════════════════════════════════
   Login
═══════════════════════════════════════════════════ */
/* The signed-OUT landing is a long document (hero, demo, features, CTA, legal),
   unlike the app shell which manages its own scroll panes. Two things trapped it:
   `body{overflow:hidden}` above, and centring a taller-than-viewport child with
   `align-items:center` — which overflows equally off BOTH edges, so the top is
   unreachable no matter what. Scroll the page while unauthenticated, and centre
   with auto margins instead: still centred when it fits, never clipped when it
   doesn't. `html.kq-authed` is set at sign-in (app.js), so the app is untouched. */
html:not(.kq-authed) body { overflow-y:auto; }
.login-screen {
  min-height:100dvh;
  display:flex;
  align-items:flex-start;
  justify-content:center;
  padding:24px;
  background:var(--paper);
}
.login-screen > * { margin-block:auto; }

.login-card {
  width:100%;
  max-width:420px;
  background:var(--paper-card);
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  padding:36px 32px;
  display:flex;
  flex-direction:column;
  gap:24px;
  animation:viewIn .5s var(--ease);
}

.login-logo {
  display:flex;
  align-items:center;
  gap:14px;
}

.logo-mark {
  font-family:var(--jp);
  font-size:2rem;
  color:var(--grad-warm-ink);
  width:54px;
  height:54px;
  background:var(--grad-warm);
  border-radius:var(--r-md);
  display:flex;
  align-items:center;
  justify-content:center;
  flex-shrink:0;
  /* OPTICAL CENTRING. align-items:center centres the LINE BOX, and Noto Sans
     JP's em box is asymmetric — measured 42 up / 10 down at 36px — while あ's
     ink is nearly symmetric. So the box centres and the glyph inside it does
     not: it renders 0.0725em low (2.61px at 36px, from canvas TextMetrics).
     border-box padding shrinks the content box from the bottom, so the centred
     glyph rises by half the padding. Hence 2 × 0.0725em. In em so it holds at
     every size, and as padding rather than a transform because .logo-mark-sm
     animates transform on hover. */
  padding-bottom:.145em;
}

.login-title {
  font-family:var(--serif);
  font-size:1.5rem;
  /* 700, not 800. The font URL loads wght@400;500;600;700, so 700 is the
     heaviest face available — 800 and 900 measure byte-identical to it (canvas
     TextMetrics: 127.73px for all three at 27px) and adding 800 to the URL
     would cost another download. This is the whole weight budget there is
     without paying for it, so the wordmark takes it. */
  font-weight:700;
  letter-spacing:-.03em;
  line-height:1;
  color:var(--ink);
  background:var(--grad-warm);
  -webkit-background-clip:text; background-clip:text;
  -webkit-text-fill-color:transparent;
}

.login-tagline {
  font-size:.75rem;
  /* --ink-2, not --ink-3. --ink-3 is the placeholder tier and measures 4.31:1
     on daylight Terrace in light theme — under AA for text this size. It went
     unnoticed while the landing could only ever render Shinjuku; now that it
     honours the visitor's scene, all 22 reach this label. */
  color:var(--ink-2);
  margin-top:4px;
  font-weight:500;
}

.login-modes {
  display:flex;
  flex-direction:column;
  gap:10px;
}

.login-mode {
  padding:14px;
  background:var(--paper);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  transition:border-color .18s, transform .18s var(--ease);
}
.login-mode:hover { border-color:var(--line-2); transform:translateY(-2px); }

.login-mode-header {
  display:flex;
  align-items:center;
  gap:10px;
  margin-bottom:6px;
}

.lm-kana {
  font-family:var(--jp);
  font-size:1.35rem;
  color:var(--accent-text);
  min-width:28px;
  text-align:center;
}

.login-mode-header strong {
  font-size:.85rem;
  font-weight:700;
  color:var(--ink);
}

.login-mode p {
  font-size:.78rem;
  color:var(--ink-2);
  line-height:1.55;
  padding-left:38px;
}

.btn-google {
  display:flex;
  align-items:center;
  justify-content:center;
  gap:10px;
  padding:11px 18px;
  /* Scene-INDEPENDENT colours. The login screen carries the scene tokens
     (data-scene/data-theme apply before login), and under a scene --paper is
     `transparent` and --ink a pale scene tint — so var(--ink)/var(--paper) made
     the label transparent on near-white (docs/TODO.MD: invisible sign-in text,
     the first thing every new user sees). Fixed dark button, white text works
     in both themes and all scenes. */
  background:#1a1206;
  color:#fff8f0;
  font-size:.875rem;
  font-weight:700;
  border-radius:var(--r-sm);
  transition:opacity .15s;
}

.btn-google:hover { opacity:.88; }

.login-note {
  text-align:center;
  font-size:.7rem;
  color:var(--ink-3);
}

/* ═══════════════════════════════════════════════════
   Landing (pre-auth) — the product-first welcome page.
   Reuses the login tokens/surfaces; .btn-google keeps its fixed
   scene-independent colours (see the note on .btn-google above).
═══════════════════════════════════════════════════ */
.landing {
  width:100%;
  max-width:960px;
  display:flex;
  flex-direction:column;
  gap:24px;
  animation:viewIn .5s var(--ease);
}

/* The landing's <main> landmark (see index.html). .landing is a flex column with
   gap:24px, so wrapping hero + features + CTA in one element would have turned
   three flex items into one and silently dropped two 24px gaps. Repeating the
   column + gap here keeps the rendered spacing identical. */
.landing-main {
  display:flex;
  flex-direction:column;
  gap:24px;
}

.landing-top {
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:16px;
}

.landing-signin-link {
  flex-shrink:0;
  font-size:.8rem;
  font-weight:700;
  color:var(--ink-2);
  background:var(--paper-card);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  padding:8px 14px;
  transition:border-color .18s, transform .18s var(--ease);
}
.landing-signin-link:hover { border-color:var(--line-2); transform:translateY(-1px); }

.landing-hero {
  display:grid;
  grid-template-columns:1.15fr .85fr;
  gap:16px;
  align-items:stretch;
}
@media (max-width:860px) {
  .landing-hero { grid-template-columns:1fr; }
}

.landing-pitch {
  background:var(--paper-card);
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  padding:26px 28px;
  display:flex;
  flex-direction:column;
  gap:14px;
}

.landing-headline {
  font-family:var(--serif);
  font-size:1.6rem;
  font-weight:600;
  letter-spacing:-.01em;
  line-height:1.2;
  color:var(--ink);
}

.landing-sub {
  font-size:.88rem;
  color:var(--ink-2);
  line-height:1.6;
}

.landing-points {
  list-style:none;
  display:flex;
  flex-direction:column;
  gap:9px;
  margin-top:2px;
}
.landing-points li {
  display:flex;
  align-items:baseline;
  gap:10px;
  font-size:.8rem;
  color:var(--ink-2);
  line-height:1.5;
}
.lp-icon {
  font-family:var(--jp);
  font-size:.95rem;
  color:var(--accent-text);
  flex-shrink:0;
  min-width:20px;
  text-align:center;
}

/* The playable no-account demo (wired by initLandingDemo in app.js). */
.demo-card {
  background:var(--paper-card);
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  padding:22px 24px;
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:12px;
  text-align:center;
}

.demo-head {
  width:100%;
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:10px;
}
.demo-head strong { font-size:.8rem; font-weight:700; color:var(--ink); }
.demo-streak { font-size:.8rem; font-weight:700; color:var(--accent-text); }

.demo-kana {
  font-family:var(--jp);
  font-size:4.2rem;
  line-height:1.15;
  color:var(--ink);
  padding:6px 0 2px;
}
.demo-kana.demo-hit { animation:demoPop .3s var(--ease); }
@keyframes demoPop { from { transform:scale(1.3); } to { transform:scale(1); } }

.demo-card.demo-miss { animation:demoShake .3s; }
@keyframes demoShake {
  25% { transform:translateX(-5px); }
  75% { transform:translateX(5px); }
}
@media (prefers-reduced-motion: reduce) {
  .demo-kana.demo-hit, .demo-card.demo-miss { animation:none; }
}

.demo-input {
  width:100%;
  max-width:230px;
  text-align:center;
  font-size:1rem;
  padding:10px 14px;
  background:var(--paper);
  color:var(--ink);
  border:1px solid var(--line-2);
  border-radius:var(--r-sm);
}
.demo-input:focus { outline:2px solid var(--accent-text); outline-offset:1px; }

.demo-msg {
  font-size:.74rem;
  color:var(--ink-3);
  line-height:1.5;
  min-height:2.3em;
}

/* Demo tabs + the three multiple-choice mini-games. Verdict colours are
   fixed mid-tones (not scene tokens) so right/wrong read on every scene. */
.demo-tabs {
  width:100%;
  display:flex;
  gap:5px;
}
.demo-tab {
  flex:1;
  font-size:.68rem;
  font-weight:700;
  padding:7px 2px;
  background:var(--paper);
  color:var(--ink-2);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  cursor:pointer;
  white-space:nowrap;
  transition:border-color .15s, color .15s;
}
.demo-tab:hover { border-color:var(--line-2); }
.demo-tab.active { border-color:var(--accent-text); color:var(--accent-text); }

/* The slider: four panes on one track, auto-advanced by initLandingDemo. */
.demo-viewport { width:100%; overflow:hidden; }
.demo-track {
  display:flex;
  align-items:stretch;
  transition:transform .5s var(--ease);
}
@media (prefers-reduced-motion: reduce) {
  .demo-track { transition:none; }
}
.demo-pane {
  flex:0 0 100%;
  min-width:100%;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:11px;
  padding:2px 1px;
}

.demo-q { font-size:.78rem; color:var(--ink-2); line-height:1.5; text-align:center; }

/* The listening player: circular transport button + seek bar + clock. */
.demo-player {
  width:100%;
  display:flex;
  align-items:center;
  gap:12px;
  background:var(--paper);
  border:1.5px solid var(--line-2);
  border-radius:99px;
  padding:7px 16px 7px 7px;
}
.demo-play {
  flex-shrink:0;
  width:38px;
  height:38px;
  border-radius:50%;
  border:2px solid var(--accent-text);
  color:var(--accent-text);
  background:var(--paper-card);
  font-size:.9rem;
  line-height:1;
  cursor:pointer;
  display:flex;
  align-items:center;
  justify-content:center;
  transition:transform .15s var(--ease);
}
.demo-play:hover { transform:scale(1.06); }
.demo-seek {
  flex:1;
  height:6px;
  border-radius:3px;
  background:var(--line);
  cursor:pointer;
  position:relative;
  overflow:hidden;
}
.demo-seek-fill {
  position:absolute;
  top:0; left:0; bottom:0;
  width:0%;
  border-radius:3px;
  background:var(--accent-text);
}
.demo-time {
  flex-shrink:0;
  font-size:.68rem;
  color:var(--ink-3);
  font-variant-numeric:tabular-nums;
  white-space:nowrap;
}

.demo-opts {
  width:100%;
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:6px;
}
.demo-opts-4  { grid-template-columns:repeat(4, 1fr); }
.demo-opts-col { grid-template-columns:1fr; }

.demo-opt {
  font-size:.76rem;
  font-weight:600;
  line-height:1.4;
  padding:10px 10px;
  background:var(--paper);
  color:var(--ink);
  border:1.5px solid var(--line-2);
  border-radius:var(--r-sm);
  cursor:pointer;
  text-align:center;
  transition:border-color .15s, transform .15s var(--ease);
}
.demo-opts-4 .demo-opt { font-family:var(--jp); font-size:1.05rem; padding:8px 2px; }
.demo-opt:hover:not(:disabled) { border-color:var(--accent-text); transform:translateY(-1px); }
.demo-opt:disabled { cursor:default; }
.demo-opt-right { border-color:#3d9963 !important; color:#3d9963; font-weight:700; }
.demo-opt-wrong { border-color:#c0564a !important; color:#c0564a; opacity:.55; }

/* The manga pane draws the game's real panel art (mg-panel is square). */
.demo-manga { width:min(190px, 62%); }
.demo-manga .mg-panel { width:100%; }
.demo-manga .mg-bubble { font-size:.72rem; }
.demo-manga .mg-num { display:none; }

.demo-script {
  width:100%;
  text-align:left;
  background:var(--paper);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  padding:10px 13px;
  display:flex;
  flex-direction:column;
  gap:3px;
}
.demo-jp { font-family:var(--jp); font-size:.95rem; color:var(--ink); line-height:1.6; }
.demo-romaji { font-size:.7rem; color:var(--ink-3); }
.demo-en { font-size:.74rem; color:var(--ink-2); }
.demo-part-q { font-size:1.05rem; text-align:center; }
.demo-slot { color:var(--accent-text); font-weight:700; }
.demo-slot-filled { text-decoration:underline; }

.feat-grid {
  display:grid;
  grid-template-columns:repeat(auto-fit, minmax(230px, 1fr));
  gap:12px;
}

.feat-card {
  background:var(--paper-card);
  border:1px solid var(--line);
  border-radius:var(--r-md);
  padding:16px;
  display:flex;
  flex-direction:column;
  gap:6px;
  transition:border-color .18s, transform .18s var(--ease);
}
.feat-card:hover { border-color:var(--line-2); transform:translateY(-2px); }

.feat-icon {
  font-family:var(--jp);
  font-size:1.15rem;
  font-weight:600;
  color:var(--accent-text);
}
.feat-card h3 { font-size:.85rem; font-weight:700; color:var(--ink); margin:0; }
.feat-card p { font-size:.76rem; color:var(--ink-2); line-height:1.55; }

.landing-cta {
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:10px;
  padding:6px 0 2px;
}
.landing-cta .btn-google { min-width:260px; }

/* Legal footer — deliberately quiet: smallest type on the page, ink-3. */
.landing-legal {
  display:flex;
  flex-wrap:wrap;
  align-items:center;
  justify-content:center;
  gap:8px;
  font-size:.68rem;
  color:var(--ink-3);
  padding-top:4px;
}
.landing-legal a { color:var(--ink-3); text-decoration:none; }
.landing-legal a:hover { color:var(--ink-2); text-decoration:underline; }

/* ═══════════════════════════════════════════════════
   App shell
═══════════════════════════════════════════════════ */
#appShell {
  display:flex;
  flex-direction:column;
  height:100dvh;
  overflow:hidden;
}

/* ── Header ── */
.app-header {
  height:auto;                 /* grows if the nav wraps to a second row */
  min-height:var(--header);
  background:var(--paper-card);
  border-bottom:1px solid var(--line);
  display:flex;
  align-items:center;
  gap:14px;
  padding:0 20px;
  z-index:50;
}

/* Reserve the header's real height where the nav still wraps to two rows, so
   it does not GROW when the nav renders.
   The nav is built by JS from the static GROUPS literal, so the header paints
   at var(--header) (64px) and then jumps to 69-79px once the nav exists. That
   single shift was most of the app's CLS: 0.14 measured on the live desktop
   app, over the 0.1 Core Web Vitals budget.
   79px is the FALLBACK font's requirement, not Noto's (77px). Reserving the
   fallback figure is what makes this hold while the webfont is still in flight
   — the nav needs 589px of row in the fallback against 558px in Noto, so it
   wraps a little more eagerly, and reserving only Noto's height would let the
   header grow during the swap. Costs 2px at rest, and removes the shift in
   both fonts.
   One rule for the whole 1121-1559 band rather than a reservation per
   header-budget tier (71px at 1121-1300, 79px at 1301-1519, 69px at
   1520-1559): the extra 8-10px at rest in two sub-bands is worth not adding
   four more breakpoints to the table below. Above 1560 the nav fits one row in
   both fonts, so nothing is reserved there. */
@media (min-width:1121px) and (max-width:1559px) {
  .app-header { min-height:79px; }
}

.header-left {
  display:flex;
  align-items:center;
  gap:8px;
  flex-shrink:0;
  padding:4px 8px 4px 4px;
  border-radius:var(--r-md);
  transition:background .15s;
}
.header-left:hover { background:var(--paper-2); }

.logo-mark-sm {
  font-family:var(--jp);
  font-size:1.2rem;
  color:var(--grad-warm-ink);
  width:32px;
  height:32px;
  background:var(--grad-warm);
  border-radius:var(--r-sm);
  display:flex;
  align-items:center;
  justify-content:center;
  transition:transform .25s var(--ease-bounce);
  padding-bottom:.145em;        /* same optical centring as .logo-mark */
}
.logo-mark-sm:hover { transform:rotate(-8deg) scale(1.08); }

.app-name {
  font-family:var(--serif);
  font-size:1rem;
  font-weight:600;
  letter-spacing:-.01em;
  color:var(--ink);
  background:var(--grad-warm);
  -webkit-background-clip:text; background-clip:text;
  -webkit-text-fill-color:transparent;
}

/* Mobile tabs — hidden on desktop */
.mob-tabs {
  display:none;
  flex:1;
  overflow-x:auto;
  scrollbar-width:none;
  align-items:center;
  gap:4px;
}
.mob-tabs::-webkit-scrollbar { display:none; }

.mob-tab {
  flex-shrink:0;
  padding:4px 11px;
  border-radius:var(--r-sm);
  font-size:.72rem;
  font-weight:600;
  color:var(--ink-3);
  border:1px solid var(--line);
  cursor:pointer;
  white-space:nowrap;
  transition:color .12s, background .12s, border-color .12s;
}
.mob-tab:hover { color:var(--ink-2); }
.mob-tab.active { color:var(--accent-text); background:var(--accent-wash); border-color:transparent; }

.mob-tab-sep {
  flex-shrink:0;
  width:1px;
  height:18px;
  background:var(--line);
  margin:0 4px;
}

.header-right {
  /* No margin-left:auto. The N5 bar is a header child now and carries auto
     margins on BOTH sides; a third auto here would split the free space three
     ways and put the bar nearer the nav than the island. With the bar's pair
     of autos taking everything, this cluster still lands flush right. */
  display:flex;
  align-items:center;
  /* Just the island and your controls — one tight cluster, so a fixed gap.
     NOT a flexible spacer: the nav next door needs every pixel (see the header
     space budget above), and a spreading right cluster is exactly what starved
     it before. */
  gap:16px;
  flex-shrink:0;
}
.header-actions { display:flex; align-items:center; gap:7px; }

.header-right img {
  width:26px;
  height:26px;
  border-radius:50%;
  border:1px solid var(--line);
  object-fit:cover;
}

/* ── Top-bar nested-menu nav ── */
.top-nav {
  display:flex;
  align-items:center;
  gap:4px;
  margin-left:10px;
  /* The nav + right cluster together can exceed the header width on mid-size
     desktops. IMPORTANT: never make this a scroll container (overflow-x:auto)
     — a scroll box CLIPS its absolutely-positioned descendants, which made
     every dropdown open invisibly (opacity 1 but clipped to the 40px nav box).
     Instead the menus WRAP to a second header row when they don't fit; nothing
     is ever clipped or out of reach. */
  min-width:0;
  /* Shrink-and-wrap, but do NOT grow. The wrapping this comment describes needs
     flex-wrap and flex-shrink; the grow was doing nothing for it and quietly
     absorbed every spare pixel in the header — at 1920px the nav measured
     1096px for ~700px of menus. That padding is invisible, but it is the free
     space the readiness pill's auto margins need to sit evenly between the nav
     and the island, so the pill was pinned at the bare 14px header gap on both
     sides at every width. Narrow widths are unaffected: there is no free space
     to grow into there, and shrinking is what wraps the menus. */
  flex:0 1 auto;
  flex-wrap:wrap;
  row-gap:0;
}
.nav-menu { position:relative; flex-shrink:0; }
.nav-trigger {
  display:flex;
  align-items:center;
  gap:5px;
  padding:6px 9px;
  border-radius:var(--r-sm);
  font-size:.82rem;
  font-weight:600;
  color:var(--ink-2);
  white-space:nowrap;
  transition:background .15s, color .15s;
}
.nav-trigger .caret { font-size:.55rem; opacity:.6; transition:transform .2s var(--ease); }
/* The decorative Japanese prefix on a group label ("漢字" in "漢字 Kanji").
   Six bilingual labels need ~837px, but a 1728px window leaves the nav only
   ~710px once the right-hand HUD is up — so the bar wrapped onto a second
   header row after Kanji. Drop the prefix before that happens: it is flourish,
   and the English half is what the label is FOR. Full labels return on screens
   wide enough to hold them. Font-size is untouched — these labels are already
   at the small end, and Japanese glyphs need more size than Latin to stay
   distinguishable (qa/legibility.js), so width comes from padding and the
   prefix, never from shrinking the type. */
.nav-jp { margin-right:5px; }
@media (max-width:1820px) { .nav-jp { display:none; } }
.nav-trigger:hover { background:var(--paper-2); color:var(--ink); }
/* The fill here is --ink (the TEXT colour), so the label must be the PAGE
   ground, not --accent-ink: under scenes --accent-ink is also dark, which made
   this a dark-on-dark solid block with the label invisible (measured 1:1).
   --paper-card is the guaranteed inverse of --ink in every theme/scene. */
/* --on-ink not --paper-card: this is text on an --ink FILL. Under a scene
   --paper-card doesn't track --ink's per-scene luminance, so it collided
   (nav label 1.61:1 on dark/yumepark, docs/TODO.MD). --on-ink is computed to
   contrast --ink on every scene (theme_scenes.js), same as #dashCorrect. */
.nav-trigger.active { color:var(--on-ink); background:var(--ink); }
.nav-menu:hover .nav-trigger .caret,
.nav-menu:focus-within .nav-trigger .caret,
.nav-menu.open .nav-trigger .caret { transform:rotate(180deg); }

.nav-dropdown {
  position:absolute;
  top:calc(100% + 8px);
  left:0;
  min-width:240px;
  max-height:min(70vh, 520px);
  overflow-y:auto;
  padding:6px;
  background:var(--paper-card);
  border:1px solid var(--line-2);
  border-radius:var(--r-md);
  box-shadow:var(--sh-float);
  opacity:0;
  visibility:hidden;
  transform:translateY(-6px);
  transition:opacity .16s var(--ease), transform .16s var(--ease), visibility .16s;
  z-index:60;
  scrollbar-width:thin;
  scrollbar-color:var(--line-2) transparent;
}
.nav-dropdown::-webkit-scrollbar { width:7px; }
.nav-dropdown::-webkit-scrollbar-thumb { background:var(--line-2); border-radius:99px; border:2px solid transparent; background-clip:padding-box; }
.nav-menu:hover .nav-dropdown,
.nav-menu:focus-within .nav-dropdown,
.nav-menu.open .nav-dropdown {          /* click/tap toggle (works on touch, where hover doesn't) */
  opacity:1;
  visibility:visible;
  transform:translateY(0);
}
.nav-menu::after {
  content:'';
  position:absolute;
  top:100%; left:0; right:0;
  height:10px;
}
.nav-dropdown .nav-list { padding:0; }

/* ── Nav dropdown items ── */
.nav-list { padding:0 6px 4px; }

/* Non-interactive in-list section header (breaks the tall Vocabulary group into
   Survival / Core / Practice). Not a nav-item: no hover, no click, no tab stop. */
.nav-sep {
  list-style:none;
  padding:9px 10px 3px;
  font-size:.6rem;
  font-weight:700;
  letter-spacing:.08em;
  text-transform:uppercase;
  color:var(--ink-3);
  pointer-events:none;
}
.nav-sep:first-child { padding-top:2px; }

.nav-item {
  position:relative;
  display:flex;
  align-items:center;
  justify-content:space-between;
  padding:9px 10px 9px 13px;
  border-radius:var(--r-sm);
  cursor:pointer;
  font-size:.82rem;
  color:var(--ink-2);
  transition:background .18s var(--ease), color .18s, transform .18s var(--ease);
  user-select:none;
  gap:8px;
}
.nav-item::before {
  content:'';
  position:absolute;
  left:4px; top:50%;
  width:3px; height:0;
  border-radius:99px;
  background:var(--accent);
  transform:translateY(-50%);
  transition:height .2s var(--ease);
}
.nav-item:hover { background:var(--paper-2); color:var(--ink); transform:translateX(2px); }
.nav-item.active {
  background:var(--accent-wash);
  color:var(--ink);
  font-weight:600;
}
.nav-item.active::before { height:58%; }

.nav-item-left {
  display:flex;
  align-items:center;
  gap:9px;
  min-width:0;
}

/* ── house icons (icons.js) ─────────────────────────────────────────────────
   The svg inherits currentColor for its lines and var(--a1) for its one accent
   wash, so scenes and themes tint icons the way they tint text — nothing here
   is per-icon. kq-kchip is the type chip for the kana categories, where the
   glyph IS the subject (あ/が/ぱ/ア): a consistent frame so a drawn icon and a
   glyph sit as equals on the same row. */
.kq-ico { display:inline-block; vertical-align:-.18em; flex-shrink:0; }

/* ── Hover explanations (kernel/tooltip.js) ────────────────────────────────
   Replaces the native `title` bubble, which rendered every explanation as one
   unstyled OS-chrome line and never appeared for keyboard users. Above z-index
   90 (the highest the app otherwise uses) so it is never covered by the header
   or a modal, and pointer-events:none so it can never swallow a click.
   Deliberately OPAQUE: scene art is painted behind translucent surfaces, so a
   see-through tooltip would put explanation text over illustration. */
.kq-tip {
  position:fixed; z-index:95; pointer-events:none;
  max-width:min(300px, calc(100vw - 24px));
  padding:9px 11px;
  background:var(--paper-card);
  border:1px solid var(--line);
  border-radius:var(--r-md);
  box-shadow:0 8px 24px -10px rgba(0,0,0,.6), 0 2px 6px -3px rgba(0,0,0,.45);
  color:var(--ink-2);
  font-size:.78rem; line-height:1.45;
  opacity:0; transform:translateY(3px);
  transition:opacity .14s var(--ease), transform .14s var(--ease);
}
.kq-tip.kq-tip-on { opacity:1; transform:none; }
.kq-tip.kq-tip-still { transition:none; }

.kq-tip-body { display:flex; flex-direction:column; gap:5px; }
/* The structured form, for the number explanations: name, the timescale it
   covers, what it means — and, set apart by a rule because it is the half
   people get wrong, what it does NOT mean. See kernel/numbers.js. */
.kq-tip-head { display:flex; align-items:baseline; gap:6px; flex-wrap:wrap; }
.kq-tip-head b { color:var(--ink); font-size:.82rem; font-weight:700; }
.kq-tip-scope {
  font-style:normal; font-size:.58rem; font-weight:700;
  text-transform:uppercase; letter-spacing:.06em;
  color:var(--accent-text); background:var(--accent-wash);
  border-radius:99px; padding:2px 6px;
}
.kq-tip-what { color:var(--ink-2); }
.kq-tip-not  { color:var(--ink-3); border-top:1px solid var(--line); padding-top:5px; }

.kq-tip-arrow {
  position:absolute; width:9px; height:9px;
  transform:translateX(-50%) rotate(45deg);
  background:var(--paper-card); border:1px solid var(--line);
}
.kq-tip[data-side="above"] .kq-tip-arrow { bottom:-5px; border-top:0;    border-left:0;  }
.kq-tip[data-side="below"] .kq-tip-arrow { top:-5px;    border-bottom:0; border-right:0; }

@media (prefers-reduced-motion: reduce) {
  .kq-tip { transition:none; transform:none; }
}
.nav-ico { display:inline-flex; align-items:center; justify-content:center; }
.kq-kchip {
  display:inline-flex; align-items:center; justify-content:center;
  min-width:22px; height:22px; padding:0 3px;
  font-weight:700; font-size:.85rem;
  border:1px solid var(--line-2); border-radius:5px;
  background:var(--paper-2);
}
.chip-val .kq-ico { margin-right:3px; }
.lp-icon .kq-ico { vertical-align:-.22em; }

/* ── header stats island (shell/statsbox.js) ────────────────────────────────
   One object that changes shape: three cells on the dashboard, four in a game.
   A surviving cell keeps its node and FLIPS its face, so a number travels
   rather than being swapped underneath you — the cell is a clipped window with
   perspective, and faces move through it.

   It REPLACES the per-category rail card, which was display:none below 980px,
   so this has to hold at 360px where it is the only stats readout there is. */
.statsbox {
  position:relative; display:flex; gap:2px; padding:2px;
  background:var(--paper-2); border:1px solid var(--line);
  border-radius:var(--r-md); overflow:hidden;
  /* will-change is applied from JS only while the width actually animates
     (shell/statsbox.js). Declaring it here permanently promoted nothing —
     `width` is a layout property the compositor cannot animate — while telling
     the browser to expect layout on this element forever. */
  transition:width .44s var(--ease-island);
}
/* The per-answer XP mark. Says what the answer just earned — never a running
   total, which is the cell underneath and is the server's number.

   It stays INSIDE the cell on purpose. Both .sb-cell and .statsbox set
   overflow:hidden (the box needs it for its width animation), so anything that
   rises out of the cell is simply clipped. The motion is therefore a short lift
   within the cell's own bounds rather than a float above it. */
.sb-xp-float {
  position: absolute; inset: 0;
  display: flex; align-items: flex-start; justify-content: center;
  padding-top: 1px;
  font-family: var(--mono); font-size: .66rem; font-weight: 700;
  color: var(--moss); pointer-events: none; white-space: nowrap;
  animation: sb-xp-rise 1.1s ease-out forwards;
}
@keyframes sb-xp-rise {
  0%   { opacity: 0; transform: translateY(7px); }
  20%  { opacity: 1; transform: translateY(0); }
  70%  { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(-5px); }
}
@media (prefers-reduced-motion: reduce) { .sb-xp-float { display: none; } }

.sb-cell {
  position:relative; overflow:hidden; min-width:0; perspective:420px;
  background:var(--paper-card); border:0; border-radius:calc(var(--r-md) - 4px);
  font:inherit; color:inherit; cursor:pointer;
}
.sb-cell:hover { background:var(--paper); }
.sb-cell:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
.sb-face {
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:1px; padding:5px 10px 4px; white-space:nowrap;
  backface-visibility:hidden; transform-origin:50% 0%;
}
.sb-val {
  display:flex; align-items:center; justify-content:center; gap:3px;
  font-size:.98rem; font-weight:700; line-height:1.1; color:var(--ink);
  font-variant-numeric:tabular-nums; white-space:nowrap;
}
.sb-val .kq-ico { flex:none; }
.sb-sub { font-style:normal; font-size:.72rem; font-weight:600; color:var(--ink-3); }
.sb-key { font-size:.62rem; letter-spacing:.04em; color:var(--ink-3); white-space:nowrap; }
/* the one cell allowed an accent: something is actually due */
/* The rotating outcome cell, coloured apart from the two standing facts beside
   it. Renamed from .sb-due — nothing has been "due" since that slot stopped
   being a review queue, and a dead name on a live rule is how it ended up
   orphaned and colourless in the first place.

   A WASH behind the cell, not accent-coloured text. --accent-text is not a
   reliable way to colour anything: scenes recompute it to --ink wherever the
   scene accent fails AA on that scene's card (see its definition), so in
   shinjuku and every other night scene it collapsed to exactly the colour of
   the cells it was meant to differ from. A fill carries the hue at full
   strength in all 22 scenes while the text stays at full ink contrast — the
   same reasoning as .dash-focus-kana, which also warns against pairing an
   accent glyph WITH the wash. */
.sb-cell:has(.sb-face.sb-accent) { background:var(--accent-wash); }
.sb-cell:has(.sb-face.sb-accent):hover { background:var(--accent-wash-2); }
/* The key label steps up one rank on the wash. .sb-key is .6rem, so it needs
   the full 4.5:1 — and --ink-3 on --accent-wash misses it in 12 of the 44
   scene/theme pairs (mahou dark 3.77, terrace dark 3.78, …). --ink-2 clears it
   in all 44, worst case 5.33. */
.sb-face.sb-accent .sb-key { color:var(--ink-2); }

/* The flip. Written out in eight and eleven steps rather than two: a
   two-keyframe flip is a straight interpolation and is over before the eye can
   follow it, so the steps are bunched where the movement should be readable. */
.sb-face.sb-out {
  position:absolute; inset:0; transform-origin:50% 100%;
  animation:sbFaceOut .37s var(--ease) both;
}
.sb-face.sb-in { animation:sbFaceIn .46s cubic-bezier(.24,.9,.28,1) both; }
@keyframes sbFaceOut {
  0%{opacity:1;transform:translateY(0) rotateX(0deg)}
  12%{opacity:1;transform:translateY(-4%) rotateX(7deg)}
  26%{opacity:.97;transform:translateY(-16%) rotateX(20deg)}
  42%{opacity:.88;transform:translateY(-36%) rotateX(35deg)}
  58%{opacity:.7;transform:translateY(-58%) rotateX(48deg)}
  74%{opacity:.45;transform:translateY(-82%) rotateX(60deg)}
  88%{opacity:.2;transform:translateY(-104%) rotateX(69deg)}
  100%{opacity:0;transform:translateY(-122%) rotateX(75deg)} }
@keyframes sbFaceIn {
  0%{opacity:0;transform:translateY(122%) rotateX(-75deg)}
  12%{opacity:.22;transform:translateY(100%) rotateX(-63deg)}
  24%{opacity:.45;transform:translateY(78%) rotateX(-51deg)}
  36%{opacity:.66;transform:translateY(56%) rotateX(-39deg)}
  48%{opacity:.82;transform:translateY(36%) rotateX(-27deg)}
  60%{opacity:.93;transform:translateY(19%) rotateX(-16deg)}
  71%{opacity:1;transform:translateY(6%) rotateX(-6deg)}
  81%{transform:translateY(-4%) rotateX(4deg)}
  90%{transform:translateY(-2.5%) rotateX(2deg)}
  96%{transform:translateY(-.8%) rotateX(.7deg)}
  100%{opacity:1;transform:translateY(0) rotateX(0deg)} }
.sb-cell.sb-enter { animation:sbCellIn .46s cubic-bezier(.24,.9,.28,1) both; }
@keyframes sbCellIn {
  0%{opacity:0;transform:scale(.66) translateY(14px)}
  18%{opacity:.35;transform:scale(.78) translateY(10px)}
  36%{opacity:.66;transform:scale(.9) translateY(6px)}
  54%{opacity:.88;transform:scale(1) translateY(2px)}
  70%{opacity:1;transform:scale(1.06) translateY(-2px)}
  84%{transform:scale(.98) translateY(1px)}
  100%{opacity:1;transform:none} }
.sb-cell.sb-leaving {
  position:absolute; top:2px; bottom:2px;
  animation:sbCellOut .34s var(--ease) both;
}
@keyframes sbCellOut {
  0%{opacity:1;transform:none}
  100%{opacity:0;transform:scale(.72) translateY(8px)} }

/* ── level-up ───────────────────────────────────────────────────────────────
   How loud this is comes from the SCENE. Every scene already sets --gk, the
   constant its own art uses to decide how much it moves (0.25 terrace → 1.9
   shinjuku); statsbox.js reads it and stamps data-energy. So a scene brings its
   own celebration and nothing here keeps a second list of the lively ones. */
.sb-levelup {
  position:absolute; inset:0; display:flex; flex-direction:column;
  align-items:center; justify-content:center; overflow:hidden;
  background:var(--reward); color:var(--onr);
  opacity:0; pointer-events:none; border-radius:calc(var(--r-md) - 2px); z-index:2;
}
.sb-lu-lv { font-size:1.05rem; font-weight:800; line-height:1.1; position:relative; z-index:1; }
.sb-lu-rung { font-style:normal; font-size:.66rem; font-weight:600; opacity:.92; position:relative; z-index:1; }
.statsbox.sb-celebrating .sb-levelup { animation:sbLuSeq 2.9s var(--ease) both; }
@keyframes sbLuSeq {
  0%{opacity:0;transform:scale(.55) rotate(-3deg)}
  7%{opacity:1;transform:scale(1.12) rotate(1.5deg)}
  13%{transform:scale(.97) rotate(-.5deg)}
  18%{transform:scale(1.03)}
  24%{transform:scale(1) rotate(0)}
  76%{opacity:1;transform:scale(1)}
  88%{opacity:.7;transform:translateY(-14%) scale(1.04)}
  100%{opacity:0;transform:translateY(-42%) scale(.9)} }
.statsbox.sb-celebrating .sb-lu-lv { animation:sbLuPop .8s var(--ease-island) .06s both; }
.statsbox.sb-celebrating .sb-lu-rung { animation:sbLuRise .6s var(--ease-island) .3s both; }
@keyframes sbLuPop {
  0%{opacity:0;transform:scale(.35) rotate(-6deg)}
  40%{opacity:1;transform:scale(1.32) rotate(2deg)}
  62%{transform:scale(.94) rotate(-1deg)}
  100%{transform:scale(1) rotate(0)} }
@keyframes sbLuRise { 0%{opacity:0;transform:translateY(12px) scale(.9)} 100%{opacity:.92;transform:none} }

.sb-flash { position:absolute; inset:0; background:#fff; opacity:0; }
.sb-ring {
  position:absolute; left:50%; top:50%; width:18px; height:18px; margin:-9px 0 0 -9px;
  border-radius:99px; border:2.5px solid color-mix(in srgb,#fff 82%,var(--accent)); opacity:0;
}
.sb-spark {
  position:absolute; left:50%; top:50%; width:6px; height:6px; margin:-3px 0 0 -3px;
  border-radius:99px; background:color-mix(in srgb,#fff 88%,var(--accent));
  box-shadow:0 0 6px color-mix(in srgb,#fff 70%,transparent); opacity:0;
}
.sb-conf {
  position:absolute; left:50%; top:50%; width:6px; height:9px; border-radius:1px;
  background:var(--c); opacity:0; will-change:transform;
}
.sb-bloom {
  position:absolute; inset:0; opacity:0;
  background:radial-gradient(ellipse at 50% 50%, color-mix(in srgb,#fff 55%,transparent) 0%, transparent 62%);
}
/* active and bright scenes: the full thing, paper included */
.statsbox.sb-celebrating[data-energy="high"] .sb-flash,
.statsbox.sb-celebrating[data-energy="mid"]  .sb-flash { animation:sbFlash .5s var(--ease) both; }
.statsbox.sb-celebrating[data-energy="high"] .sb-ring,
.statsbox.sb-celebrating[data-energy="mid"]  .sb-ring { animation:sbRing .8s var(--ease) .04s both; }
.statsbox.sb-celebrating[data-energy="high"] .sb-ring.r2 { animation-delay:.18s; animation-duration:.9s; }
.statsbox.sb-celebrating[data-energy="mid"]  .sb-ring.r2 { animation:none; }
.statsbox.sb-celebrating[data-energy="high"] .sb-spark,
.statsbox.sb-celebrating[data-energy="mid"]  .sb-spark {
  animation:sbSpark var(--dur,.85s) cubic-bezier(.15,.85,.3,1) var(--del,.05s) both; }
.statsbox.sb-celebrating[data-energy="mid"] .sb-spark:nth-of-type(2n) { animation:none; }
.statsbox.sb-celebrating[data-energy="high"] .sb-conf {
  animation:sbConf var(--dur,1.1s) cubic-bezier(.12,.62,.3,1) var(--del,.05s) both; }
@keyframes sbFlash { 0%{opacity:0} 8%{opacity:.85} 100%{opacity:0} }
@keyframes sbRing { 0%{opacity:.85;transform:scale(.2)} 100%{opacity:0;transform:scale(14)} }
@keyframes sbSpark {
  0%{opacity:0;transform:rotate(var(--a)) translateX(2px) scale(.3)}
  18%{opacity:1;transform:rotate(var(--a)) translateX(26px) scale(1.15)}
  100%{opacity:0;transform:rotate(var(--a)) translateX(var(--far,120px)) scale(.25)} }
@keyframes sbConf {
  0%{opacity:0;transform:translate(-50%,-50%) rotate(0deg) scale(.4)}
  10%{opacity:1}
  45%{opacity:1;transform:translate(calc(-50% + var(--dx) * .62), calc(-50% + var(--dy) * .55)) rotate(calc(var(--rot) * .5)) scale(1)}
  100%{opacity:0;transform:translate(calc(-50% + var(--dx)), calc(-50% + var(--dy) + 26px)) rotate(var(--rot)) scale(.85)} }
/* calm scenes get the opposite: nothing is thrown, one slow bloom */
.statsbox.sb-celebrating[data-energy="calm"] .sb-bloom { animation:sbBloom 1.5s var(--ease) .06s both; }
.statsbox.sb-celebrating[data-energy="calm"] .sb-levelup { animation:sbLuSeqCalm 3s var(--ease) both; }
.statsbox.sb-celebrating[data-energy="calm"] .sb-lu-lv { animation:sbLuPopCalm .9s var(--ease-island) .1s both; }
@keyframes sbBloom { 0%{opacity:0;transform:scale(.72)} 32%{opacity:.85;transform:scale(1)} 100%{opacity:0;transform:scale(1.12)} }
@keyframes sbLuSeqCalm {
  0%{opacity:0;transform:scale(.9)} 12%{opacity:1;transform:scale(1.02)} 22%{transform:scale(1)}
  76%{opacity:1;transform:scale(1)} 100%{opacity:0;transform:translateY(-22%) scale(.97)} }
@keyframes sbLuPopCalm { 0%{opacity:0;transform:scale(.82)} 55%{opacity:1;transform:scale(1.06)} 100%{transform:scale(1)} }

@media (prefers-reduced-motion: reduce) {
  .statsbox { transition:none; }
  .sb-face.sb-out, .sb-face.sb-in, .sb-cell.sb-enter, .sb-cell.sb-leaving,
  .statsbox.sb-celebrating .sb-levelup, .statsbox.sb-celebrating .sb-lu-lv,
  .statsbox.sb-celebrating .sb-lu-rung { animation:none; }
  .statsbox.sb-celebrating .sb-levelup { opacity:1; }
  .sb-flash, .sb-ring, .sb-spark, .sb-conf, .sb-bloom { display:none; }
}

.nav-kana {
  font-family:var(--jp);
  font-size:.95rem;
  min-width:22px;          /* min not fixed: a multi-char label (ラーメン, てんぷら) in a
                              22px box wrapped one glyph per line — "logo going up to
                              down". Let it grow horizontally instead. */
  white-space:nowrap;
  text-align:center;
  flex-shrink:0;
  opacity:.6;
  transition:opacity .18s, color .18s;
}
.nav-item:hover .nav-kana { opacity:.85; }
.nav-item.active .nav-kana { opacity:1; color:var(--accent-text); }

.nav-label { white-space:nowrap; }

/* Mastery level badge */
.nav-lvl {
  font-family:var(--mono);
  font-size:.58rem;
  font-weight:700;
  letter-spacing:.02em;
  color:var(--ink-3);
  font-variant-numeric:tabular-nums;
  flex-shrink:0;
  padding:2px 7px;
  border-radius:99px;
  background:var(--paper-2);
  transition:background .18s, color .18s;
  white-space:nowrap;
}
.nav-item.active .nav-lvl { color:var(--accent-text); background:var(--accent-wash-2); }
.nav-lvl.maxed { color:var(--ochre); background:var(--ochre-wash); }
.nav-lvl:empty { display:none; }   /* categories with no single level band (Memory, Draw) show no pill */

/* Goal progress bar pinned to the item's bottom edge */
.nav-prog {
  position:absolute;
  left:10px; right:10px; bottom:3px;
  height:2px;
  border-radius:99px;
  background:var(--paper-2);
  overflow:hidden;
}
.nav-prog-fill {
  display:block;
  height:100%;
  width:0%;
  border-radius:99px;
  background:var(--accent);
  opacity:.55;
  transition:width .5s var(--ease);
}
.nav-item.active .nav-prog-fill { opacity:1; }

/* ── Session stat chips ── */
.chip {
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:1px;
  padding:4px 12px;
  min-width:44px;
  position:relative;
}
.chip + .chip::before {
  content:'';
  position:absolute;
  left:0; top:20%; bottom:20%;
  width:1px;
  background:var(--line);
}
.chip-val {
  font-family:var(--mono);
  font-size:.85rem;
  font-weight:700;
  color:var(--ink);
  font-variant-numeric:tabular-nums;
  line-height:1.1;
  transition:transform .15s var(--ease);
}
.chip:first-child .chip-val { color:var(--accent-text); }
.chip-key {
  font-size:.5rem;
  font-weight:700;
  text-transform:uppercase;
  letter-spacing:.06em;
  color:var(--ink-3);
}

/* ── User menu ── */
.user-menu { position:relative; }
.user-trigger {
  display:flex;
  align-items:center;
  gap:8px;
  padding:4px 8px 4px 4px;
  border-radius:99px;
  transition:background .15s;
}
.user-trigger:hover { background:var(--paper-2); }
.user-menu::after { content:''; position:absolute; top:100%; left:0; right:0; height:10px; }
.user-dropdown {
  position:absolute;
  top:calc(100% + 8px);
  right:0;
  min-width:170px;
  padding:6px;
  display:flex;
  flex-direction:column;
  background:var(--paper-card);
  border:1px solid var(--line-2);
  border-radius:var(--r-md);
  box-shadow:var(--sh-float);
  opacity:0;
  visibility:hidden;
  transform:translateY(-6px);
  transition:opacity .16s var(--ease), transform .16s var(--ease), visibility .16s;
  z-index:60;
}
.user-menu:hover .user-dropdown,
.user-menu:focus-within .user-dropdown {
  opacity:1; visibility:visible; transform:translateY(0);
}
.menu-link {
  text-align:left;
  padding:9px 11px;
  border-radius:var(--r-sm);
  font-size:.82rem;
  font-weight:600;
  color:var(--ink-2);
  transition:background .12s, color .12s;
}
.menu-link:hover { background:var(--paper-2); color:var(--ink); }
.menu-signout { color:var(--brick); opacity:.85; }
.menu-signout:hover { opacity:1; background:var(--brick-wash); color:var(--brick); }

/* ── Body ── */
.app-body {
  display:flex;
  flex:1;
  overflow:hidden;
}
.main-content { width:100%; }

/* Three-column app layout — rails ride near the viewport edges, the centre
   column (the actual game) absorbs whatever width that leaves. */
.app-grid {
  display:grid;
  grid-template-columns: 240px minmax(0, 1fr) 300px;
  gap:24px;
  align-items:start;
  max-width:min(2040px, 97vw);
  margin:0 auto;
}
.app-main { min-width:0; }
.side-left, .side-right {
  position:sticky; top:0;
  display:flex; flex-direction:column; gap:14px;
  max-height:calc(100vh - var(--header) - 32px);
  overflow-y:auto;
  scrollbar-width:none;
  padding-bottom:4px;
}
.side-left::-webkit-scrollbar, .side-right::-webkit-scrollbar { display:none; }

#quizView { max-width:1120px; margin:0 auto; width:100%; animation:viewIn .4s var(--ease); }

@media (max-width:1480px) {
  .app-grid { grid-template-columns: minmax(0,1fr) 280px; }
  .side-left { display:none; }
}
/* Header space budget: the full nav (7 menus) + the right cluster don't both
   fit on a typical laptop, and the nav — the PRIMARY navigation — was getting
   starved and its later menus (Grammar, Games, Reading, Kanji) clipped out of
   reach. The chips this rule used to shed are gone: the stats box replaced
   them AND the right-rail card, and unlike either it must survive every width
   (the rail card was hidden below 980px, leaving phones with no per-game
   numbers at all). So the nav sheds its padding here instead. */
/* Header space budget. 7 nav menus (~900–990px) can't coexist with the full
   right cluster on a laptop, and the nav is the PRIMARY navigation, so it wins
   the space. What the cluster used to shed here — the duplicated stat chips,
   then the N5 bar — no longer exists: both folded into the stats box, which
   stays at every width by design. Nav padding tightens instead. This keeps all
   7 menus visible and clickable down to ~1000px without a scroll. */
/* The chips are the first thing the header sheds (they duplicate the right
   rail), so they must go BEFORE the nav is squeezed — not after. At the old
   1500px line they were still on screen while the nav had already been pushed
   onto a second row: a 1536px window left the nav 518px to seat 560px of
   labels, which is the "two rows after Kanji" wrap. 1600px is where the nav,
   the chips and the rest genuinely stop fitting together. */
@media (max-width:1500px) {
  .top-nav { gap:2px; margin-left:6px; }
  .nav-trigger { padding:7px 9px; font-size:.88rem; }
}
@media (max-width:1300px) {
  .nav-trigger { padding:6px 7px; font-size:.82rem; }
}
@media (max-width:1120px) {
  .nav-trigger { padding:5px 5px; font-size:.78rem; }
  .header-right { gap:5px; }
}
/* Below the mobile nav breakpoint (720px) the nav is replaced entirely (see
   that rule); above it, menus that don't fit WRAP to a second header row
   (.top-nav flex-wrap) — never scrolled or clipped, so dropdowns always work. */
@media (max-width:980px) {
  .app-grid { grid-template-columns: 1fr; max-width:760px; }
  .side-left, .side-right { display:none; }
}

/* ── The header cluster, 721–900px ────────────────────────────────────────────
   `.header-right` is flex-shrink:0 — deliberately, so the nav next door cannot
   starve it. But below ~900px there is no longer room for the cluster at its
   natural width, and shrink:0 means it does not compress, it OVERFLOWS: at
   721px its right edge sits at 816px against a 721px viewport, 95px outside.
   `#appShell` is overflow:hidden, so that 95px is not scrolled off — it is
   gone, and with it #headerUser and the dropdown holding Profile · Leaderboard
   · Sign out. A document-level overflow check reports zero the whole time,
   because the clip happens inside the shell.

   iPad portrait (768) sits in this band and loses 48px, truncating the name to
   "Smo". DESIGN.md's named rule is "The Header Grows, Never Clips", so let it
   grow: allow the cluster to wrap to a second row, and drop the user's NAME
   (never the avatar, and only where an avatar exists to stand in for it).
   Below 721 the mobile header takes over and none of this applies. */
@media (min-width:721px) and (max-width:900px) {
  .header-right {
    flex-wrap:wrap;
    min-width:0;
    flex-shrink:1;
    justify-content:flex-end;
    row-gap:6px;
  }
  .header-actions { min-width:0; flex-wrap:wrap; justify-content:flex-end; }
  /* Only when the avatar is there to identify the account in its place —
     with no avatar the span is the whole button and hiding it leaves a
     nameless target. */
  #headerUser img + span { display:none; }
  /* The dropdown is absolutely positioned; pin it to the cluster's right edge
     so it opens INTO the viewport rather than off the same edge. */
  .user-dropdown { right:0; left:auto; }
}

/* Narrow-laptop nav collapse (721–1024px). Above this the full nav fits (with
   the space-budget trims above); below 720px the mobile tab row takes over. In
   this band the 7 menus used to WRAP to a second header row, making the header
   tall — instead, collapse them behind a ☰ button that drops the nav as an
   absolute panel (never clipped: it's positioned off the header, not inside a
   scroll box). The .nav-menu dropdowns still work inside the panel. */
.nav-collapse-btn {
  display:none;
  flex-shrink:0;
  font-size:1.1rem; line-height:1;
  padding:8px 11px; margin-left:6px;
  color:var(--ink);
  background:var(--paper-2);
  border:1px solid var(--line);
  border-radius:var(--r-md);
  cursor:pointer;
  transition:background .15s, border-color .15s;
}
.nav-collapse-btn:hover { border-color:var(--accent-text); background:var(--accent-wash); }
/* Collapse the nav to ☰ below 1121, not 1025.
   Measured on the live app: the six triggers need 468-589px of row and the nav
   box only gets 202-512px below ~1560, so they WRAP — and the header is
   `min-height:var(--header)` with `height:auto`, so it grows with them:

     width   header (Noto / fallback font)
     >=1560    64 / 64     one row, fits
     1121-1559 69-77 / 69-79   two rows
     1050-1100 95 / 98     THREE rows
     1025-1049 95 / 131    FOUR rows in the fallback

   1025-1120 was the worst of it — a 95-131px header, a third of a small
   laptop's vertical space spent on a wrapped nav. The ☰ collapse already
   existed and was already the answer below 1025; it just stopped too early.
   Extending it to 1120 deletes the 3-and-4-row cases outright.
   min-width:721 is unchanged: below that shell/mobile.js's drawer takes over
   (it gates on matchMedia('(max-width:720px)')), and the two must not overlap. */
@media (max-width:1120px) and (min-width:721px) {
  .nav-collapse-btn { display:inline-flex; align-items:center; justify-content:center; }
  /* Collapsed by default: nav hidden until ☰ opens it. */
  .top-nav { display:none; }
  .app-header.nav-open .top-nav {
    display:flex;
    flex-wrap:wrap;
    position:absolute;
    top:100%; left:0; right:0;
    margin:0;
    padding:8px 20px 12px;
    gap:4px;
    background:var(--paper-card);
    border-bottom:1px solid var(--line);
    box-shadow:var(--sh-float, 0 12px 28px rgba(0,0,0,.28));
    z-index:49;
    animation:navDrop .18s var(--ease);
  }
  .app-header { position:relative; }
}
@keyframes navDrop { from { opacity:0; transform:translateY(-6px); } to { opacity:1; transform:none; } }

/* ═══════════════════════════════════════════════════
   Left rail — Kanji of the Day · Phrase of the Day
═══════════════════════════════════════════════════ */
.sl-card {
  position:relative; overflow:hidden;
  background:var(--paper-card);
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  padding:18px 16px;
  text-align:center;
  animation:viewIn .4s var(--ease);
}
/* Every rail/dashboard card gets a thin colour-coded top edge instead of a
   uniformly flat border — same "which card is which" cue at a glance, a lot
   less gray. Colours cycle through the palette; no card is left plain. */
.sl-card::before, .sr-card::before, .dash-card::before {
  content:''; position:absolute; top:0; left:0; right:0; height:3px;
}
#kotdCard::before { background:var(--accent); }
#potdCard::before { background:var(--clay); }
.sl-eyebrow {
  font-size:.62rem; font-weight:700; text-transform:uppercase; letter-spacing:.1em;
  color:var(--ink-3); margin-bottom:10px;
}
.sl-kanji-glyph { font-family:var(--jp); font-size:3.2rem; line-height:1; color:var(--ink); margin-bottom:6px; }
.sl-kanji-read { font-size:.78rem; color:var(--accent-text); font-weight:700; margin-bottom:4px; }
.sl-kanji-meaning { font-family:var(--serif); font-style:italic; font-size:.9rem; color:var(--ink-2); margin-bottom:10px; }
.sl-kanji-sentence {
  font-family:var(--jp); font-size:.8rem; line-height:1.5; color:var(--ink-2);
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-sm);
  padding:8px 10px; margin-bottom:12px; text-align:left;
}
.sl-phrase-jp { font-family:var(--jp); font-size:1.3rem; color:var(--ink); margin-bottom:4px; }
.sl-phrase-rom { font-family:var(--mono); font-size:.78rem; font-weight:600; color:var(--accent-text); margin-bottom:4px; }
.sl-phrase-en { font-family:var(--serif); font-style:italic; font-size:.9rem; color:var(--ink-2); margin-bottom:14px; }
.sl-card .btn-ghost-sm { width:100%; border-radius:var(--r-sm); }

/* ═══════════════════════════════════════════════════
   Right rail — mastery · stats · trend · leaderboard · badges · recent
═══════════════════════════════════════════════════ */
.sr-card {
  position:relative; overflow:hidden;
  /* flex-shrink:0 — the side rail is a flex column with overflow-y:auto; without
     this every card is squeezed below its content height when the stack exceeds
     the viewport, and overflow:hidden then shears text mid-glyph (BEST STREAK /
     LAST PLAYED labels went missing, mastery/leaderboard rows sliced). Keeping
     each card at its natural height makes the RAIL scroll instead. */
  flex-shrink:0;
  background:var(--paper-card);
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  padding:14px 16px;
  animation:viewIn .4s var(--ease);
}
.sr-header::before  { background:var(--accent); }
#srMasteryCard::before { background:var(--clay); }
#srStats::before     { background:var(--moss); }
#srTrendCard::before { background:var(--ochre); }
#srLbCard::before    { background:var(--accent); }
#srBadgesCard::before { background:var(--clay); }
#srRecentCard::before { background:var(--moss); }
.sr-card-title {
  font-size:.68rem; font-weight:700; text-transform:uppercase; letter-spacing:.08em;
  color:var(--ink-3); display:block; margin-bottom:10px;
}
.sr-card-hint { text-transform:none; letter-spacing:0; font-weight:600; opacity:.7; margin-left:4px; }

.sr-header { display:flex; align-items:center; gap:12px; }
.sr-kana {
  font-family:var(--jp); font-size:1.5rem; color:var(--accent-text);
  background:var(--accent-wash); border-radius:var(--r-md); width:44px; height:44px;
  display:flex; align-items:center; justify-content:center; flex-shrink:0;
  /* Most badges are ONE glyph, but the word categories use 2–4 (ねこ, ごはん,
     てんぷら, ラーメン). At 1.5rem those wrapped to two lines inside the fixed
     44px box and spilled out the top and bottom. Keep them on one line and let
     the glyphs shrink to fit instead of overflowing. */
  white-space:nowrap;
  overflow:hidden;
  padding:0 3px;
  line-height:1;
}
/* Progressive shrink by label length — set from the kana string in renderCatSidePanel. */
.sr-kana[data-len="2"] { font-size:1.05rem; letter-spacing:-.02em; }
.sr-kana[data-len="3"] { font-size:.78rem;  letter-spacing:-.03em; }
.sr-kana[data-len="4"] { font-size:.56rem;  letter-spacing:-.05em; }
.sr-header-text { min-width:0; }
.sr-title { font-family:var(--serif); font-size:1rem; font-weight:600; color:var(--ink); line-height:1.25; }
.sr-sub { font-size:.72rem; color:var(--ink-3); margin-top:2px; line-height:1.4; }

.sr-mastery-top { display:flex; align-items:baseline; justify-content:space-between; margin-bottom:8px; }
.sr-mastery-top .sr-card-title { margin-bottom:0; }
/* --ink not --accent: "Lv 3" is a value to read, and --accent(=--a1 under a
   scene) measured 2.43:1 on dark/edo + light/terrace — accent isn't safe as text
   (docs/QA.md §2.20). --ink always contrasts with the card. */
.sr-lvl-val { font-family:var(--mono); font-size:.82rem; font-weight:700; color:var(--ink); }
.sr-bar { height:6px; border-radius:99px; background:var(--paper-2); overflow:hidden; }
.sr-bar > span { display:block; height:100%; width:0%; border-radius:99px; background:var(--grad-warm); transition:width .5s var(--ease); }
.sr-lvl-note { font-size:.72rem; color:var(--ink-3); margin-top:8px; }

.sr-stat { display:flex; flex-direction:column; align-items:center; gap:2px; padding:6px 4px; background:var(--paper); border:1px solid var(--line); border-radius:var(--r-sm); }

/* Improvement sparkline */
/* The trend chart. Was a flex row of bars on a 0–100 domain, which could not
   show variance (see srSpark in shell/header.js); it is an area + line on the
   observed range now, with the range printed under it. The stroke takes
   --accent-text rather than --grad-warm: the gradient means "reward" in this
   system and a trend line is not one. */
.sr-spark { margin-bottom:6px; }
.sr-spark svg { display:block; width:100%; height:46px; overflow:visible; }
.sr-spark-scale {
  display:flex; justify-content:space-between;
  font-size:.62rem; font-weight:700; letter-spacing:.04em;
  color:var(--ink-3); margin-top:1px;
  font-variant-numeric:tabular-nums;
}
.sr-trend-note { font-size:.72rem; color:var(--ink-2); line-height:1.4; }
.sr-trend-note.up   { color:var(--moss); }
.sr-trend-note.down { color:var(--brick); }
/* Light theme: --moss/--brick are tuned for the dark ground and drop to ~1.84:1
   on the light --paper-card under a scene (theme-sweep top offender, 13 scenes).
   Darker on-brand variants on light only; dark keeps the vivid tokens. The
   up/down HUE still reads (green vs red), just legibly. */
:root[data-theme="light"] .sr-trend-note.up   { color:#2f7d32; }   /* ~5:1 */
:root[data-theme="light"] .sr-trend-note.down { color:#b23a2e; }   /* ~5:1 */
.sr-trend-empty, .sr-trend-loading { font-family:var(--serif); font-style:italic; font-size:.8rem; color:var(--ink-3); }

.sr-lb-list { display:flex; flex-direction:column; gap:5px; list-style:none; }
.sr-lb-row {
  display:flex; align-items:center; gap:8px; padding:6px 8px;
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-sm); font-size:.8rem;
}
.sr-lb-row.me { background:var(--accent-wash); border-color:var(--accent-text); }
.sr-lb-rank { flex-shrink:0; width:22px; text-align:center; font-family:var(--mono); font-weight:700; color:var(--ink-2); }
.sr-lb-name { flex:1; min-width:0; color:var(--ink-2); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.sr-lb-row.me .sr-lb-name { color:var(--ink); font-weight:700; }
/* (you) — --ink not --accent: accent-as-text was 2.08:1 on dark titans/temple/
   office. The .me row already has the --accent-wash highlight, so the label just
   needs to be legible, not accent-coloured. Real class (.sr-lb-you) so a token
   audit can find it — it was styled only by inheritance before. */
.sr-lb-name b, .sr-lb-you { color:var(--ink); font-weight:700; }
.sr-lb-val { font-family:var(--mono); font-weight:700; color:var(--ink); font-variant-numeric:tabular-nums; flex-shrink:0; }
/* --ink-2 not --ink-3: the "Loading…"/empty placeholder dropped to 2.19-3.10:1
   on dark/terrace + dark/catcafe (docs/TODO.MD contrast tail). --ink-2 is a
   strictly higher-contrast tier, so this only ever improves the ratio. */
.sr-lb-empty, .sr-lb-loading { font-family:var(--serif); font-style:italic; font-size:.8rem; color:var(--ink-2); padding:4px 2px; }

.sr-badge-list { display:flex; flex-direction:column; gap:6px; }
.sr-badge { display:flex; align-items:center; gap:8px; padding:6px 8px; background:var(--paper); border:1px solid var(--line); border-radius:var(--r-sm); font-size:.78rem; }
.sr-badge.locked { opacity:.5; }
.sr-badge-emoji { font-size:1rem; flex-shrink:0; }
.sr-badge-name { flex:1; min-width:0; color:var(--ink-2); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.sr-badge.earned .sr-badge-name { color:var(--ink); font-weight:600; }
.sr-badge-lvl { font-size:.68rem; font-weight:700; color:var(--ink-3); flex-shrink:0; }
.sr-badge.earned .sr-badge-lvl { color:var(--accent-text); }

#srRecentCard .recent-list { display:flex; flex-direction:column; gap:5px; }

/* ═══════════════════════════════════════════════════
   Dashboard — landing screen (first entry + logo click)
   Asymmetric bento grid, not equal-weight tiles.
═══════════════════════════════════════════════════ */
.dashboard-view {
  display:flex; flex-direction:column; gap:18px;
  max-width:1120px; margin:0 auto; width:100%;
  padding:8px 0 40px;
  animation:viewIn .4s var(--ease);
}

.dash-hero {
  display:flex; align-items:center; justify-content:space-between; gap:20px; flex-wrap:wrap;
  background:var(--paper-card);
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  padding:28px 30px;
}
/* Screen titles get the signature gradient — a bit of personality on each
   game's entry point, kept off everyday labels/stat numbers so it doesn't
   turn into "everything is a gradient". */
.dash-greeting, .game-name, .pt-title, .rush-title, .kj-title, .mem-title, .rd-title {
  background:var(--grad-warm);
  -webkit-background-clip:text; background-clip:text;
  -webkit-text-fill-color:transparent;
}

.dash-greeting {
  font-family:var(--serif);
  /* 1.9rem was sized against the 0.85 zoom that used to sit over it. */
  font-size:1.55rem; font-weight:600; letter-spacing:-.01em;
  color:var(--ink);
}
.dash-sub { font-size:.92rem; color:var(--ink-2); margin-top:6px; max-width:46ch; line-height:1.5; }
.dash-continue { flex:0 0 auto; width:auto; padding:12px 22px; white-space:nowrap; }

/* First stat ("Total correct") reads as the headline number; the rest trail
   as smaller supporting figures — deliberate size contrast, not a grid of
   identical tiles. */
.dash-stats-grid {
  display:grid; grid-template-columns:repeat(8, 1fr); gap:12px;
}
.dash-stat-card {
  display:flex; flex-direction:column; align-items:center; gap:4px;
  background:var(--paper-card); border:1px solid var(--line); border-radius:var(--r-md);
  padding:16px 8px;
  /* 2 (hero) + 3 + 3 = 8 per row — fills both rows exactly, no dead columns */
  grid-column:span 3;
  transition:transform .15s var(--ease), border-color .15s;
}
.dash-stat-card:first-child {
  grid-column:span 2;
  grid-row: span 2;
  background:var(--ink);
  border-color:var(--ink);
  justify-content:center;
  padding:16px 10px;
}
/* Hero card fills with --ink (= the scene's --txt under a scene). Foreground uses
   --on-ink, computed per-scene from --ink's OWN luminance in theme_scenes mount()
   → guaranteed AA on every scene. (Earlier --paper-card/--paper-2 tries collided
   at 1.8-2.0:1 because a surface token isn't guaranteed to contrast with --ink.)
   QA: npm run theme-sweep. */
.dash-stat-card:first-child .dash-stat-val { color:var(--on-ink); font-size:1.9rem; }
.dash-stat-card:first-child .dash-stat-key { color:var(--on-ink); opacity:.8; }
.dash-stat-card:hover { transform:translateY(-2px); border-color:var(--line-2); }
.dash-stat-card:first-child:hover { border-color:var(--ink); }
.dash-stat-val { font-family:var(--mono); font-size:1.35rem; font-weight:700; color:var(--ink); font-variant-numeric:tabular-nums; }
/* .72rem is the Label role in DESIGN.md. These were .66/.68 and then scaled by
   the zoom, which is how the words naming the five biggest numbers on the
   screen ended up the smallest text on it. */
.dash-stat-key { font-size:.72rem; font-weight:700; text-transform:uppercase; letter-spacing:.05em; color:var(--ink-3); text-align:center; }

/* One 2-row/3-column grid: the calendar is tall enough to be worth a
   dedicated left column spanning both rows (align-self:stretch so it
   actually fills that height rather than leaving a gap below it); Focus +
   Almost-there sit side by side up top, Jump-back-in spans under both of
   them. Replaces the old two-grid layout (a separate Time-spent card up
   top, the calendar off in its own row below) that left the calendar
   fighting for a sliver of width and a lot of dead space around it. */
.dash-grid {
  display:grid;
  grid-template-columns:300px 1fr 1fr;
  grid-template-rows:auto auto;
  gap:22px;
  align-items:start;
}
.dash-activity-card { grid-column:1; grid-row:1 / 3; align-self:stretch; }
#dashFocusCard       { grid-column:2; grid-row:1; }
#dashBadgeCard       { grid-column:3; grid-row:1; }
/* Row 2: Jump-back-in at half width (col 2) with Daily Quest beside it (col 3). */
.dash-shortcuts-card { grid-column:2; grid-row:2; }
#dashQuestCard       { grid-column:3; grid-row:2; }
/* When there's no active quest the card is .hidden, so let Jump-back-in take the
   full row-2 width again rather than leaving an empty column. */
.dash-grid:has(#dashQuestCard.hidden) .dash-shortcuts-card { grid-column:2 / 4; }

/* The dashboard reads oversized at the app's 18px base, so the desktop grid
   opens out — the wider column gap pushes the side panels (activity left,
   badges/quest right) away from the centre for the airy, "zoomed-out" look.
   Desktop only: below 901px the grid is a single column, where there is no
   side-vs-centre to separate.

   This used to be `zoom:0.85` on the whole view, and the air was never coming
   from the zoom — it comes from the gap on the next line. What the zoom DID do
   was shrink every label by 15% after the cascade had finished: .dash-stat-key
   computed 11.88px and painted 10.10px, .dash-card-title 12.24 → 10.40, the
   weekday initials 11.16 → 9.49. All under the 11px Latin floor PRODUCT.md
   calls "a legibility requirement that outranks layout convenience", and all
   invisible to qa/legibility.js, which reads getComputedStyle().fontSize — the
   PRE-zoom number. The oversized elements are trimmed explicitly below
   instead, which the harness can see. */
@media (min-width:901px) {
  .dash-grid { gap:36px; }
}

.dash-card {
  position:relative; overflow:hidden;
  background:var(--paper-card);
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  padding:14px 16px;
}
.dash-activity-card::before { background:var(--moss); }
#dashFocusCard::before { background:var(--clay); }
#dashBadgeCard::before { background:var(--ochre); }
.dash-shortcuts-card::before { background:var(--grad-warm); }
.dash-quest-card::before { background:var(--accent); }
.dash-mastery-card::before { background:var(--accent); }
.dash-card-title {
  font-size:.72rem; font-weight:700; text-transform:uppercase; letter-spacing:.08em;
  color:var(--ink-3); margin-bottom:12px;
}


/* Focus suggestion / badge progress */
.dash-focus-empty { font-family:var(--serif); font-style:italic; font-size:.82rem; color:var(--ink-3); line-height:1.5; }
.dash-focus-top { display:flex; align-items:center; gap:12px; margin-bottom:12px; }
.dash-focus-kana {
  /* --ink not --accent: an --accent glyph on --accent-wash is a same-hue,
     low-separation pair (2.08:1 on dark/titans). The wash + shape already cue
     "focus"; the glyph just needs to be legible. */
  font-family:var(--jp); font-size:1.4rem; color:var(--ink);
  background:var(--accent-wash); border-radius:var(--r-md); width:42px; height:42px;
  display:flex; align-items:center; justify-content:center; flex-shrink:0;
}
.dash-badge-emoji { font-size:1.8rem; line-height:1; flex-shrink:0; width:42px; text-align:center; }
.dash-focus-label { font-size:.9rem; font-weight:700; color:var(--ink); }
.dash-focus-acc { font-size:.76rem; color:var(--ink-3); margin-top:2px; }

/* D2 Daily Quest — three tappable legs, each with its own progress rail.
   Tokens only (theme-aware); a completed leg dims and shows a check. */
#dashQuestBody { display:flex; flex-direction:column; gap:10px; }
.dq-leg {
  display:grid; grid-template-columns:auto 1fr auto; grid-template-rows:auto auto;
  column-gap:10px; row-gap:6px; align-items:center; width:100%; text-align:left;
  background:transparent; border:0; padding:6px 4px; cursor:pointer; color:var(--ink);
}
.dq-emoji {
  grid-row:1 / span 2; font-family:var(--jp); font-size:1.2rem; color:var(--ink);
  background:var(--accent-wash); border-radius:var(--r-sm);
  width:36px; height:36px; display:flex; align-items:center; justify-content:center; flex-shrink:0;
}
.dq-text  { font-size:.86rem; font-weight:700; color:var(--ink); min-width:0; }
.dq-count { font-size:.74rem; font-weight:700; color:var(--ink-3); white-space:nowrap; }
.dq-rail  { grid-column:2 / span 2; height:6px; border-radius:999px; background:var(--line); overflow:hidden; }
.dq-fill  { display:block; height:100%; background:var(--accent); border-radius:999px; transition:width .35s var(--ease, ease); }
.dq-leg:hover .dq-text { color:var(--accent-text); }
.dq-done { opacity:.6; }
.dq-done .dq-emoji { background:var(--moss-wash, var(--accent-wash)); color:var(--moss, var(--accent)); }
.dq-done .dq-fill { background:var(--moss, var(--accent)); }
@media (prefers-reduced-motion: reduce) { .dq-fill { transition:none; } }
.dash-focus-btn { width:100%; }
.dash-badge-bar { height:6px; border-radius:99px; background:var(--paper-2); overflow:hidden; }
.dash-badge-bar > span { display:block; height:100%; border-radius:99px; background:var(--grad-warm); transition:width .5s var(--ease); }

/* Lifetime mastery tiles (words/kanji/grammar/letters — GET /api/mastery) */
.dash-mastery-row { display:flex; flex-wrap:wrap; gap:10px; margin-bottom:10px; }
.dash-mastery-item {
  flex:1; min-width:90px; display:flex; flex-direction:column; align-items:center; gap:2px;
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-md);
  padding:12px 8px;
}
.dash-mastery-emoji { font-size:1.1rem; line-height:1; }
.dash-mastery-val { font-family:var(--mono); font-size:1.3rem; font-weight:700; color:var(--ink); font-variant-numeric:tabular-nums; }
.dash-mastery-key { font-size:.68rem; color:var(--ink-3); text-transform:uppercase; letter-spacing:.04em; }
.dash-mastery-note { font-size:.72rem; color:var(--ink-3); line-height:1.4; }

.dash-activity-card .act-cell { font-size:.68rem; }
.dash-activity-card .act-grid { gap:3px; }
.dash-activity-card .act-weekdays { margin-bottom:3px; }
.act-head-mini { display:flex; align-items:center; justify-content:center; gap:10px; margin-bottom:8px; }
.act-head-mini .act-month { min-width:auto; }

.dash-shortcuts-card { text-align:left; }
/* 2×2: the card is half width now (Daily Quest beside it) and holds exactly 4
   tiles, so two columns keeps each tile comfortably sized rather than cramped. */
.dash-shortcuts { display:grid; grid-template-columns:repeat(2, 1fr); gap:10px; }
.dash-tile {
  display:flex; flex-direction:column; align-items:center; gap:6px;
  padding:16px 10px; background:var(--paper); border:1px solid var(--line); border-radius:var(--r-md);
  transition:transform .18s var(--ease-bounce), border-color .15s, background .15s;
  text-align:center;
}
.dash-tile:hover { transform:translateY(-3px); border-color:var(--accent-text); background:var(--accent-wash); }
.dash-tile:active { transform:translateY(0); }
/* --ink not --accent: the tile's kana/emoji glyph is content that must stay
   legible. --accent (=--a1 under a scene) measured 1.58-2.75:1 on 6 dark scenes
   (theme-sweep) — the accent isn't guaranteed to contrast with the tile surface.
   The glyph doesn't need to BE the accent colour to read as a tile icon. */
.dash-tile-kana { font-family:var(--jp); font-size:1.3rem; color:var(--ink); }
.dash-tile-label { font-size:.76rem; font-weight:600; color:var(--ink-2); line-height:1.3; }
.dash-tile:hover .dash-tile-label { color:var(--ink); }

@media (max-width:900px) {
  .dash-stats-grid { grid-template-columns:repeat(4, 1fr); }
  .dash-stat-card { grid-column:span 2; grid-row:auto; }
  .dash-stat-card:first-child { grid-column:1 / -1; grid-row:auto; }
  .dash-grid { grid-template-columns:1fr; grid-template-rows:none; }
  .dash-activity-card, #dashFocusCard, #dashBadgeCard, .dash-shortcuts-card, #dashQuestCard,
  .dash-grid:has(#dashQuestCard.hidden) .dash-shortcuts-card {
    grid-column:auto; grid-row:auto;
  }
  /* full-width card on mobile, but only 4 tiles — keep 2×2, not 3+1. */
  .dash-shortcuts { grid-template-columns:repeat(2, 1fr); }
}
@media (max-width:560px) {
  .dash-hero { flex-direction:column; align-items:stretch; }
  .dash-continue { width:100%; }
  .dash-stats-grid { grid-template-columns:repeat(2, 1fr); }
  .dash-stat-card:first-child { grid-column:1 / -1; }
  .dash-shortcuts { grid-template-columns:repeat(2, 1fr); }
}

/* ═══════════════════════════════════════════════════
   Main content
═══════════════════════════════════════════════════ */
.main-content {
  flex:1;
  overflow-y:auto;
  padding:22px 18px 40px;
  scrollbar-width:none;
  scrollbar-color:var(--line-2) transparent;
}
.main-content::-webkit-scrollbar { width:8px; }
.main-content::-webkit-scrollbar-track { background:transparent; }
.main-content::-webkit-scrollbar-thumb {
  background:transparent;
  border-radius:99px;
  border:2px solid transparent;
  background-clip:padding-box;
  transition:background .2s;
}
.main-content:hover { scrollbar-width:thin; }
.main-content:hover::-webkit-scrollbar-thumb { background:var(--line-2); background-clip:padding-box; }

/* Modal panels center their own content */
#lbView, #refView { margin:0 auto; }
#refView { max-width:max-content; }

/* ═══════════════════════════════════════════════════
   Game head — one head, one slot order.   docs/GAME_UI.md R1

     [icon + name]  [level]  [count]  ——— gap ———  [guide] [reference] [settings]

   Left cluster is *what and where*, right cluster is *what I can do about it*.
   Ten containers used to answer this ten different ways (.quiz-header, .rd-head,
   .kj-head, .pt-head, .mem-head, .rush-head, .cl-head, .cmp-head, .ev-head,
   .mg-head) — which is why the level pill sat second from the left in Reading
   and fourth in Kanji. They are kept here as ALIASES of this one rule for a
   release; new markup uses .game-head and nothing else.
═══════════════════════════════════════════════════ */
.game-head,
.quiz-header, .rd-head, .kj-head, .pt-head, .mem-head,
.rush-head, .cl-head, .cmp-head, .ev-head, .mg-head {
  display:flex; align-items:center; gap:10px 8px; flex-wrap:wrap; width:100%;
  /* The spacer absorbs row 1's slack, so the left cluster still sits left; a
     row that WRAPS carries only right-cluster controls, and flex-end keeps
     those against the right edge instead of stranding them mid-row. */
  justify-content:flex-end;
}
/* The views that lay their children out with `gap` need no margin here or the
   two stack; the ones that are plain blocks do. */
#quizView > .game-head, .particles-view > .game-head, .conj-lab-view > .game-head,
.composition-view > .game-head, .eavesdrop-view > .game-head, .manga-view > .game-head,
.quiz-header, .pt-head, .cl-head, .cmp-head, .ev-head, .mg-head { margin-bottom:14px; }

/* Identity — house icon + name, always leftmost. */
.game-name {
  display:inline-flex; align-items:center; gap:8px; margin:0;
  font-family:var(--serif); font-size:1.3rem; font-weight:600;
  letter-spacing:-.01em; line-height:1.2; color:var(--ink);
}
/* The category blurb is not a head slot — it sits under the head, full width.
   --ink-2, not the --ink-3 the old .quiz-sub used: measured against the real
   pixels behind it, --ink-3 bottoms out at 4.26:1 on mahou/light, just under
   AA, and this line carries the sentence that says what the game is. */
.game-sub { font-size:.75rem; color:var(--ink-2); margin:-6px 0 12px; }

/* Level — R5. --ink on the wash, accent carried by the border and fill, the
   pairing .btn-ghost-sm already settled on: accent-on-accent-wash is same-hue
   and drops to 2.56-3.00:1 under scenes. This one measures 7.61:1 worst case
   across all 44 scene/theme pairs, and does it STATICALLY — it does not depend
   on deriveInks() having run, the way --accent-text does.
   `—` when the data cannot stand behind a level (docs/CONTENT_SOURCING.md);
   never absent, because a missing pill and an unnoticed one look the same. */
.game-level,
.rd-level, .kj-level, .cmp-level, .ev-level, .mg-level {
  font-family:var(--mono); font-size:.75rem; font-weight:700;
  color:var(--ink);
  background:var(--accent-wash);
  border:1px solid var(--accent-wash-2);
  border-radius:999px; padding:2px 10px; white-space:nowrap;
  flex-shrink:0;
}

/* Count — R7. One format: `7 / 20` for a finite set, `24 answered` for endless
   practice, the clock instead of either when the game is timed. Never nothing. */
.game-count,
.kj-progress, .cmp-progress, .ev-progress, .mg-progress {
  font-family:var(--mono); font-size:.8rem; color:var(--ink-2);
  white-space:nowrap; font-variant-numeric:tabular-nums; flex-shrink:0;
}

/* Deck — R9. What is IN the practice (which set, which script, which version)
   is chosen on the entry screen and shown here read-only, so which deck you are
   in is never a mystery mid-run. Quieter than the level pill on purpose: it is
   context, not the headline. */
/* A level pill that is also the way to the ladder. Same pill, plus the one
   affordance that says it can be pressed. */
.game-level-btn { cursor:pointer; padding-right:8px; transition:border-color .15s, background .15s; }
.game-level-btn::after { content:" ▾"; font-size:.8em; color:var(--ink-2); }
.game-level-btn:hover { border-color:var(--accent-text); background:var(--accent-wash-2); }

.game-deck {
  font-size:.72rem; font-weight:700; color:var(--ink-2);
  background:var(--paper-2); border:1px solid var(--line);
  border-radius:999px; padding:2px 10px;
  max-width:34ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex-shrink:0;
}
/* A deck pill that must NOT be truncated away — the difficulty choice R3 asks
   the head to keep visible. It shares the pill look but never ellipsises. */
.game-deck-key { max-width:none; color:var(--ink); }

/* Where a kana glyph IS the subject, it stands in for the house icon. */
.game-glyph { font-family:var(--jp); font-size:1.15em; line-height:1; }

/* Nav — leftmost in the head. A LABEL, not a chevron: on a screen of numbered
   question cards a bare `<` reads as "previous question", and there is no
   previous question to go to (every question renders at once). Outline only —
   leaving is never the accented action.   docs/GAME_UI_LONGFORM.md §4 */
/* The arrow is the mark, the LABEL is the control (R1): a chevron alone reads
   as a stepper on a screen of numbered questions. */
.game-back { display:inline-flex; align-items:center; gap:6px; }
.game-back .kq-ico { display:block; }
.game-back {
  font-size:.73rem; font-weight:600; color:var(--ink-2);
  background:transparent; border:1px solid var(--line-2);
  border-radius:99px; padding:4px 11px;
  white-space:nowrap; flex-shrink:0; cursor:pointer;
  transition:color .15s, border-color .15s, background .15s;
}
.game-back:hover { color:var(--ink); border-color:var(--ink-3); background:var(--paper-2); }

/* Reference — R4. Two lookups, ONE object: the border means "these belong
   together" instead of decorating each item. Do NOT merge two different
   functions into one button because they share an analytics flag; what is
   shared here is the container, not the control. */
.game-ref {
  display:inline-flex; align-items:stretch; flex-shrink:0;
  border:1px solid var(--line-2); border-radius:var(--r-md);
  overflow:hidden; background:var(--paper-card);
}
.game-ref > button {
  display:inline-flex; align-items:center; gap:5px;
  font:inherit; font-size:.73rem; font-weight:600; color:var(--ink-2);
  background:transparent; border:0; padding:6px 11px;
  white-space:nowrap; cursor:pointer; transition:color .15s, background .15s;
}
.game-ref > button + button { border-left:1px solid var(--line-2); }
.game-ref > button:hover:not(:disabled) { color:var(--ink); background:var(--paper-2); }
/* Locked reads as locked, not as a faded pill — a faded pill reads as broken
   or as a paywall. --ink-3 plus a padlock, and no hover surface. */
.game-ref > button:disabled { color:var(--ink-3); cursor:default; }
/* §9.5 — a visible focus ring per button. The segment is `overflow:hidden` so
   the two halves share one border, and the global :focus-visible ring is drawn
   at outline-offset:2px, which that clip would EAT: keyboard users would tab
   into an invisible focus. Drawn inside the button instead. (The locked half is
   `disabled`, so it is skipped rather than focusable-but-dead.) */
.game-ref > button:focus-visible,
.game-script-tabs > button:focus-visible,
.lf-script-tabs > button:focus-visible { outline-offset:-2px; border-radius:0; }
/* …and while it is locked it carries its REASON (§5), which needs a second
   line, so the button stacks rather than putting the reason beside the label. */
.game-ref > button.is-locked { flex-direction:column; align-items:flex-start; gap:0; }
.game-ref > button.is-locked .game-cost { color:var(--ink-2); white-space:normal; max-width:40ch; line-height:1.35; }
/* The locked reason needs the width to stay two lines; at 26ch it wrapped to
   three and pushed the whole reference segment onto its own row. */

/* A quiet action: available, never inviting. Used for anything that costs
   mastery (Skip) or is the non-default of a pair (Listen again). */
.game-quietlink {
  font-size:.73rem; font-weight:600; color:var(--ink-2);
  background:none; border:0; padding:2px 0; cursor:pointer;
  text-decoration:underline; text-underline-offset:3px;
  text-decoration-color:var(--line-2);
}
.game-quietlink:hover { color:var(--ink); text-decoration-color:currentColor; }

/* The single flex:1 spacer that splits the two clusters. */
.game-head-gap, .rd-spacer { flex:1; min-width:0; }

/* R4's disclosure for a reference SEGMENT — said once, under the pair, instead
   of once inside each button. Two buttons carrying the same sentence made each
   of them 188x50 (larger than the game's name) and said the same thing twice.
   Full-width so it lands on its own line under the segment it describes, and
   right-aligned so it stays attached to it rather than drifting to the title. */
.game-ref-cost {
  flex:1 0 100%; order:9;
  margin:-4px 0 0; text-align:right;
  font-size:.68rem; font-weight:500; line-height:1.4; color:var(--ink-2);
}

/* Skip's own layout lives with the shared .game-skip-row rules further down —
   it was renamed out of .rd-skip-row when it moved from the head to the foot of
   the passage. */
/* The entry screen has no passage, so nothing that acts on one appears there.
   This hid nothing for a while: the selector still said `.rd-skip-row`, a class
   that no longer exists anywhere in the markup, so "Skip this passage ⇧↵"
   rendered under the entry card with no passage to skip (user-reported). Match
   the class the markup actually carries. */
.rd-entry-open #rdKanjiBtn,
.rd-entry-open #rdVocabBtn,
.rd-entry-open .game-ref-cost,
.rd-entry-open .game-skip-row { display:none; }
/* An empty deck pill rendered as a 22x6 grey dash beside the level — a genre
   only exists once a passage is loaded. Applies to every game's deck slot. */
.game-deck:empty { display:none; }

/* Prompt — R6. Every question says what kind of answer it wants, in the same
   slot and the same style, which is what lets the mode switch come off (R2).
   --ink-2, NEVER --ink-3: at this size --ink-3 measures 2.84:1 on --paper-card,
   well under AA, and a small uppercase label slides into placeholder grey by
   default. */
/* R6 — the one line that says what the screen is asking for. It is a SENTENCE,
   and it is read, so it is set like one.
   It used to be .72rem (13px) uppercase with .09em tracking in --ink-2: the
   smallest, palest, hardest-to-scan text on the screen, in a corner, carrying
   the information the mode BUTTONS used to carry (R2/R3 moved it here). All-caps
   at 13px strips word shape, which is what a reader skims by, so the line that
   tells you what to do was the one nobody noticed — reported, from the Kanji
   game, as "too small and in the corner, difficult to notice what it is asking".
   R6's own contrast note existed because of that size; at 17px the worry goes
   away and --ink is affordable, which is right for the second-most-important
   text on a play screen after the item itself. */
.game-prompt {
  font-size:1.05rem; font-weight:600; letter-spacing:0;
  color:var(--ink); margin:0 0 12px;
}
@media (max-width:520px) { .game-prompt { font-size:1rem; } }
/* Assistance disclosure — R4. What a control costs is stated BEFORE the tap;
   by the time a panel is open the rep is already marked, and a consequence
   revealed on the way out is a receipt, not a choice. */
.game-cost { display:block; font-size:.68rem; font-weight:500; color:var(--ink-2); margin-top:1px; }
.btn-ghost-sm .game-cost { color:inherit; opacity:.75; }

/* R11 — the guide, as a sheet under the head.
   It was a full-page card that replaced the game (`.lesson-card`, still used by
   grammar's own lesson, which IS its guide). A learner who asked to play was
   given 3-7 paragraphs and nothing to press: Real Listening's Start measured
   y=1642 on an 844px screen. This is the same content sized to sit under the
   head of a game that is already on screen — so it is capped, not full-bleed,
   and the accent keyline ties it to the reference segment above it rather than
   making it a modal. Dismissing it is what records that it has been seen.

   NOT an overlay: on a phone an overlay would cover the question the guide was
   opened to explain. It is in the flow, and a click anywhere outside closes it. */
.game-guide {
  max-width:46rem; margin:0 0 14px;
  background:var(--paper-card);
  border:1px solid var(--line-2); border-left:3px solid var(--accent);
  border-radius:var(--r-md);
  padding:13px 16px;
  animation:viewIn .22s var(--ease);
}
.game-guide-h {
  font-family:var(--serif); font-size:.95rem; font-weight:700; color:var(--ink);
  margin:0 0 9px; line-height:1.3;
}
/* Long guides scroll INSIDE the sheet rather than pushing their own button off
   the screen — Real Listening's is seven rows (its own four plus Shadow's
   three) and ran past 900px on a phone, which is the bug this rule replaced in
   a different shape. The cap only bites when the content is actually long. */
.game-guide-rows {
  margin:0; padding:0 0 0 18px; display:flex; flex-direction:column; gap:7px;
  max-height:46vh; overflow-y:auto; overscroll-behavior:contain;
}
.game-guide-row { font-size:.84rem; line-height:1.5; color:var(--ink-2); }
/* The heading of each row is the scannable part — someone reopening the guide
   is looking for one of these, not reading the whole thing again. */
.game-guide-row b { color:var(--ink); font-weight:700; margin-right:.35em; }
.game-guide-foot { display:flex; margin-top:12px; }
/* .btn-primary is flex:1 by default (it is built for a card's button row); here
   it is one control under a paragraph and should be its own width. */
.game-guide-foot .btn-primary { flex:0 0 auto; padding:9px 18px; }
@media (prefers-reduced-motion:reduce) { .game-guide { animation:none; } }

/* R1 on a phone — two fixed rows, so the way out and the identity do not trade
   places between games.
   The head is one wrapping row with `justify-content:flex-end` (see the base
   rule): whichever items happen to fit share line 1, and the LAST line is
   pushed right. That is fine at desktop width, where nothing wraps. At 390px it
   means Real Listening (a 20-character name) puts [All settings] beside the
   title while Long-form (a 21-character name plus a deck pill) pushes the title
   onto its own line and right — so the two screens of the same game family have
   their back button and their title in different places, and the eye has to
   find them again on every screen.
   Fixed by ordering rather than by shrinking anything: line 1 is the way out
   and the tools, line 2 is identity + level + deck + count, left-aligned. The
   ::after is a real flex item and is what forces the break. */
@media (max-width:720px) {
  .game-head,
  .quiz-header, .rd-head, .kj-head, .pt-head, .mem-head,
  .rush-head, .cl-head, .cmp-head, .ev-head, .mg-head {
    justify-content:flex-start;
  }
  /* Line 1 is WHERE YOU ARE and the way out of it; line 2 is what you can do
     about it. The first version of this rule had those the other way round —
     tools first, identity last — so a phone showed two rows of buttons before
     it said which game you were in, and the screen you had just opened named
     itself below the fold of its own head. Nav still comes first within line 1
     (docs/GAME_UI_LONGFORM.md §2): the way out is the first thing on the row. */
  .game-head > [data-role="nav"], .game-head > .game-back { order:1; }
  .game-head > * { order:2; }                       /* identity, level, deck, count */
  .game-head > .game-head-gap { order:3; }          /* eats the slack on line 1 */
  .game-head::after { content:''; order:4; flex-basis:100%; height:0; }
  .game-head > [data-role="guide"],
  .game-head > [data-role="reference"],
  .game-head > [data-role="presentation"],
  .game-head > [data-role="settings"],
  .game-head > .game-ref { order:5; }
  /* No tools, no second row — the break must not leave an empty one behind. */
  .game-head:not(:has(> [data-role="guide"], > [data-role="reference"],
                      > [data-role="presentation"], > [data-role="settings"],
                      > .game-ref))::after { display:none; }
}

/* ── Quiz header (legacy names, still styled while markup migrates) ── */
.quiz-title {
  font-family:var(--serif);
  font-size:1.2rem;
  font-weight:600;
  letter-spacing:-.01em;
  line-height:1.2;
}
.quiz-sub {
  font-size:.75rem;
  color:var(--ink-3);
  margin-top:3px;
}

.btn-ghost-sm {
  font-size:.73rem;
  /* --ink not --accent: accent-on-accent-wash is a same-hue, low-separation pair
     that drops to 2.56-3.00:1 under scenes (theme-sweep). --ink on the wash is
     always legible; the accent still reads via the border + wash fill. */
  color:var(--ink);
  padding:5px 11px;
  border-radius:99px;
  border:1px solid var(--accent-wash-2);
  background:var(--accent-wash);
  white-space:nowrap;
  transition:color .15s, border-color .15s, background .15s, transform .15s var(--ease);
  flex-shrink:0;
}
.btn-ghost-sm:hover { color:var(--ink); border-color:var(--accent-text); background:var(--accent-wash); transform:translateY(-1px); }
/* A head control the current item cannot offer (KQHead.btn's `disabled`) reads
   as unavailable, not as another live button — the house marks for that are
   .voc-node[disabled] and .jn-crosslink[disabled]. It keeps its label and its
   cost line, which is what says WHY it is out. */
.btn-ghost-sm[disabled] { opacity:.55; cursor:not-allowed; }
.btn-ghost-sm[disabled]:hover { color:var(--ink-2); border-color:var(--line-2); background:transparent; transform:none; }

/* The two daily-widget CTAs carry a DATA-DRIVEN label — `Practice “Greetings &
   Basic Phrases” →`, built from a Word Group name — inside a 240px rail whose
   card is 208px across the padding. `.btn-ghost-sm` is `white-space:nowrap`,
   which is right for the chips in a game head and wrong for a sentence: the
   longest label measures ~252px, and `.sl-card` clips its own overflow (it has
   to, for the rounded colour edge), so the tail of the name and the arrow were
   simply cut off. 47 of the 244 group names are over the line, i.e. about one
   day in five. The label wraps instead. */
.sl-cta {
  display:inline-block; max-width:100%;
  white-space:normal; overflow-wrap:break-word;
  line-height:1.4; padding:7px 12px; text-align:center;
}

/* R9 — the shared quiz's entry screen. Same card as the standalone games' start
   panels, so "choose your deck, then start" looks the same everywhere. */
.quiz-entry {
  display:flex; flex-direction:column; align-items:center; gap:4px;
  background:var(--paper-card); border:1px solid var(--line); border-radius:var(--r-lg);
  padding:28px 26px; margin:0 auto 18px; max-width:560px; width:100%;
  animation:viewIn .35s var(--ease);
}
.quiz-entry .btn-primary { width:100%; max-width:30rem; margin-top:4px; }
.quiz-entry .wp-picker-row { justify-content:center; }

/* Answering the other direction — a nudge, not a verdict. It must not look like
   the wrong-answer treatment, because the model does not count it as one. */
.kj-wrongdir {
  font-size:.85rem; color:var(--ink-2); line-height:1.5; text-align:center;
  margin:8px auto 0; padding:8px 12px; max-width:30rem;
  border:1px dashed var(--line); border-radius:var(--r-md); background:var(--paper-2);
}
.kj-wrongdir b { color:var(--ink); }

/* R3's rescue, in the verdict. Deliberately quiet — it is an offer made at a
   low moment, not a scolding — but it is a real control, so it looks like one. */
.rescue-offer {
  display:flex; flex-direction:column; align-items:center; gap:8px;
  margin:14px auto 4px; padding:12px 16px; max-width:32rem;
  border:1px dashed var(--line-2, var(--line)); border-radius:var(--r-md);
  background:var(--paper-2);
}
.rescue-text { font-size:.85rem; color:var(--ink-2); line-height:1.5; margin:0; text-align:center; }
.rescue-text b { color:var(--ink); }

/* R4 — the hint that replaced Reveal mode. It marks the row, so it says so on
   the control, before the tap, and it looks like the other assistance controls
   rather than like a neutral action. */
.reveal-hint-btn {
  display:inline-flex; flex-direction:column; align-items:center; gap:0;
  margin:8px auto 0; line-height:1.25;
}

/* ── Progress bar ── */
.progress-bar {
  height:3px;
  background:var(--line);
  border-radius:99px;
  margin-bottom:16px;
  overflow:hidden;
}
.progress-fill {
  height:100%;
  background:var(--grad-warm);
  border-radius:99px;
  transition:width .5s var(--ease);
  width:0%;
}

/* ═══════════════════════════════════════════════════
   Quiz card
═══════════════════════════════════════════════════ */
.quiz-card {
  background:var(--paper-card);
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  position:relative;
  overflow:hidden;
  margin-bottom:18px;
  animation:viewIn .4s var(--ease);
  transition:border-color .2s;
}

/* Top accent stripe — changes with result state */
.quiz-card::before {
  content:'';
  position:absolute;
  top:0; left:0; right:0;
  height:3px;
  background:var(--line-2);
  transition:background .25s;
}
.quiz-card.st-ok::before   { background:var(--moss); }
.quiz-card.st-err::before  { background:var(--brick); }
.quiz-card.st-warn::before { background:var(--ochre); }

/* Clean (first-try) correct — quick flat colour flash, no glow */
@keyframes cleanPop {
  0%,100% { background-color:var(--paper-card); }
  35%     { background-color:var(--moss-wash); }
}
.quiz-card.clean-pop { animation:cleanPop .6s var(--ease); }

/* ── Clean-streak badge (top-right of card) ── */
.streak-badge {
  position:absolute;
  top:12px; right:12px;
  z-index:3;
  display:flex;
  align-items:center;
  gap:4px;
  padding:4px 10px 4px 8px;
  border-radius:999px;
  font-family:var(--mono);
  font-weight:700;
  font-size:.8rem;
  color:var(--clay);
  background:var(--clay-wash);
  border:1px solid var(--clay);
  letter-spacing:.02em;
  transition:transform .2s var(--ease), opacity .2s;
}
.streak-badge .streak-flame { display:inline-flex; align-items:center; line-height:1; }
.streak-badge .streak-flame .kq-ico { display:block; }
.streak-badge.bump { animation:badgeBump .45s var(--ease); }
@keyframes badgeBump {
  0%{transform:scale(1);} 40%{transform:scale(1.28);} 100%{transform:scale(1);}
}
/* Hotter look at high streaks — solid fill, still no glow */
.streak-badge.hot {
  color:var(--grad-warm-ink);
  background:var(--grad-warm);
  border-color:transparent;
}

/* ── Streak banner (milestones) ── */
/* Not just the streak toast — this is the app's ERROR CHANNEL. showBanner()
   has 14 callers, and the longest strings are the failures: app.js's "Could not
   load this game's data. Check your connection and try again." is 70
   characters, plus the paywall's checkout/billing errors, the mock exam's
   "That paper isn't available yet." and adaptive's "Nothing to drill right
   now".

   It used to be `white-space:nowrap` on a box centred with `left:50%` and NO
   max-width. At 1rem serif that 70-character string lays out ~600px wide, so on
   a 390px phone it ran ~105px off BOTH edges — and a position:fixed element
   contributes nothing to document scrollable overflow, so it was clipped with
   no way to reach it in either direction. The message a phone user most needs
   to read was the one guaranteed to be unreadable.

   `top` was the hardcoded DESKTOP dark-theme header height while --header is
   56/60/64px depending on theme, so it also sat wrong on every other
   combination — and on a phone, where the header wraps to two rows, it painted
   over the header (z-index 60 beats the header's 50).

   left:50% + translate(-50%) + max-width is self-centring and cannot overflow,
   so .streak-banner.show's own translate(-50%,0) below still works. Do NOT
   switch to left/right insets: that rule would snap the banner back off-screen. */
.streak-banner {
  position:fixed;
  top:calc(var(--header) + 8px); left:50%;
  z-index:60;
  transform:translate(-50%,-18px);
  /* width:max-content is load-bearing, not tidying. A position:fixed box with
     `left:50%` and no width is shrink-to-fit against the space from 50% to the
     RIGHT EDGE — 195px of a 390px phone — so the max-width below never got a
     chance and a wrapped error laid out as a 195px column five lines tall.
     max-content sizes to the text and lets max-width (which resolves against
     the viewport, not the available space) do the capping. */
  width:max-content;
  max-width:min(92vw, 34rem);
  padding:11px 22px;
  border-radius:999px;
  font-family:var(--serif);
  font-weight:600;
  font-size:1rem;
  line-height:1.4;
  color:var(--on-ink);
  background:var(--ink);
  box-shadow:var(--sh-float);
  opacity:0;
  pointer-events:none;
  white-space:normal;
  text-align:center;
  text-wrap:balance;
  transition:opacity .3s var(--ease), transform .3s var(--ease);
}
.streak-banner.show {
  opacity:1;
  transform:translate(-50%,0);
}
/* Phone: the box is now allowed to wrap, so give it the width and tighten the
   type. `top` is set from JS against the MEASURED header (see showBanner) —
   the wrapped phone header is ~157px, which no static value tracks. */
@media (max-width:720px) {
  .streak-banner {
    max-width:calc(100vw - 24px);
    padding:9px 16px;
    font-size:.92rem;
  }
}

/* ── Confetti layer ── */
.celebrate-layer {
  position:fixed;
  inset:0;
  z-index:90;
  pointer-events:none;
  overflow:hidden;
}
.confetti-piece {
  position:absolute;
  top:-12px;
  width:9px; height:14px;
  border-radius:2px;
  opacity:0;
  will-change:transform, opacity;
  animation:confettiFall var(--dur,1.2s) var(--ease) forwards;
}
@keyframes confettiFall {
  0%   { opacity:0; transform:translateY(-10px) rotate(0deg); }
  10%  { opacity:1; }
  100% { opacity:0; transform:translateY(92vh) rotate(var(--spin,720deg)); }
}

.card-face {
  padding:34px 42px 30px;
  display:flex;
  flex-direction:column;
  gap:18px;
  animation:fadein .18s ease;
}

/* ── Kana / word display ── */
.kana-display {
  font-family:var(--jp);
  text-align:center;
  color:var(--ink);
  line-height:1.05;
  padding:16px 0 8px;
  min-height:140px;
  display:flex;
  align-items:center;
  justify-content:center;
}

/* Loading affordance — a quiet spinner + label, so a brief content fetch reads
   as "working" not "stuck". Theme-aware; honors reduced motion. */
.kq-loading { display:flex; flex-direction:column; align-items:center; gap:14px; color:var(--ink-3); }
.kq-spinner {
  width:34px; height:34px; border-radius:50%;
  border:3px solid var(--line); border-top-color:var(--accent-text);
  animation:kq-spin .8s linear infinite;
}
.kq-loading-label { font-size:.8rem; font-weight:700; letter-spacing:.04em; }
@keyframes kq-spin { to { transform:rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .kq-spinner { animation-duration:2s; }
}

/* Loading skeletons — shimmer placeholders for dashboard panels awaiting their
   API (docs/TODO.MD empty-frame gap). Theme-aware via --line/--paper-card. */
.kq-sk { display:block; border-radius:var(--r-sm); background:
  linear-gradient(100deg, var(--line) 30%, var(--paper-card) 50%, var(--line) 70%);
  background-size:200% 100%; animation:kq-shimmer 1.3s ease-in-out infinite; }
.kq-sk-grid { display:grid; grid-template-columns:repeat(7, 1fr); gap:4px; }
.kq-sk-cell { aspect-ratio:1; }
.kq-sk-row  { display:flex; gap:10px; }
.kq-sk-tile { flex:1; height:56px; }
@keyframes kq-shimmer { to { background-position:-200% 0; } }
@media (prefers-reduced-motion: reduce) {
  .kq-sk { animation:none; opacity:.5; }
}

/* ── "NEW feature" marker + diagonal shine (time-boxed; see NEW_FEATURES) ── */
.kq-new {
  display:inline-block; vertical-align:middle;
  font-size:.64rem; font-weight:700; letter-spacing:.06em;
  line-height:1; padding:2px 5px; border-radius:999px;
  background:var(--accent-wash); color:var(--accent-text);
  border:1px solid var(--accent);
  margin-left:6px;
}
/* A shiny diagonal line sweeps across the flagged element every few seconds.
   Uses a clipped ::after overlay so it never affects layout or the element's
   own colours. `position` is set defensively (nav-item/buttons/cards are all
   fine to be positioned). */
.kq-shine { position:relative; overflow:hidden; }
.kq-shine::after {
  content:''; position:absolute; top:0; bottom:0;
  left:-60%; width:40%;
  background:linear-gradient(105deg, transparent 0%, rgba(255,255,255,.05) 40%, rgba(255,255,255,.35) 50%, rgba(255,255,255,.05) 60%, transparent 100%);
  transform:skewX(-18deg);
  pointer-events:none;
  animation:kq-shine-sweep 3.6s ease-in-out infinite;
  /* `left` is animated by the keyframes below as a TRANSFORM, so this element
     never relayouts. Seven of these ran at once on the nav (each new-feature
     badge adds one), continuously, and animating `left` relayouts the parent
     on every frame of all seven. */
}
/* Pixel-identical to the old `left:-60% -> 130%` sweep, but composited.
   The element is width:40% of its parent and starts at left:-60%, so the old
   travel was 190% of the PARENT = 475% of the element's own width. skewX is
   restated because a transform declaration replaces the base one wholesale —
   drop it and the shine becomes an unskewed rectangle. */
@keyframes kq-shine-sweep {
  0%   { transform:translateX(0) skewX(-18deg); }
  /* sweep across quickly, then a long pause before the next pass */
  22%  { transform:translateX(475%) skewX(-18deg); }
  100% { transform:translateX(475%) skewX(-18deg); }
}
@media (prefers-reduced-motion: reduce) {
  .kq-shine::after { animation:none; opacity:0; }
}

.kana-display.mode-letter {
  font-size: clamp(5.5rem, 16vw, 9rem);
  font-weight:400;
}

.kana-display.mode-word {
  font-size: clamp(2.8rem, 9vw, 5.2rem);
  font-weight:500;
  letter-spacing:.04em;
}

/* Sentence context block */
.sentence-ctx {
  margin: 2px 0 10px;
  padding: 10px 18px 11px;
  border-radius: var(--r-sm);
  background: var(--paper);
  border: 1px solid var(--line);
  text-align: center;
}
.sent-jp {
  font-family: var(--jp);
  font-size: 1rem;
  color: var(--ink);
  line-height: 1.7;
  letter-spacing: .03em;
}
.sent-word {
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-underline-offset: 3px;
  color: var(--accent);
  font-weight: 600;
}
.sent-tr {
  font-family: var(--serif);
  font-size: .82rem;
  color: var(--ink-2);
  margin-top: 3px;
  font-style: italic;
  letter-spacing: .01em;
}

/* Furigana */
.kana-display ruby { display:inline-block; text-align:center; }
.kana-display rt {
  display:block;
  font-family:var(--jp);
  font-size:clamp(13px, 0.42em, 28px);
  font-weight:600;
  color:var(--ink-2);
  letter-spacing:.04em;
  line-height:1.35;
  margin-bottom:.12em;
}

/* Pronunciation hint */
.kana-display ruby.hint {
  background:var(--accent-wash);
  border-radius:var(--r-sm);
  padding:0 .08em;
  margin:0 .02em;
}
.kana-display ruby.hint rt {
  font-family:var(--sans);
  font-size:.32em;
  font-weight:700;
  color:var(--accent-text);
  letter-spacing:.02em;
  text-transform:lowercase;
}

/* ── Attempt pips ── */
.attempt-track {
  display:flex;
  justify-content:center;
  gap:7px;
}
.pip {
  width:32px;
  height:4px;
  border-radius:2px;
  background:var(--line);
  transition:background .18s;
}
.pip.used { background:var(--brick); }

.attempt-label {
  text-align:center;
  font-size:.7rem;
  font-weight:500;
  color:var(--ink-3);
  margin-top:-10px;
}

/* ── Input ── */
.field-wrap { display:flex; flex-direction:column; gap:5px; }

.ans-input {
  width:100%;
  padding:11px 14px;
  background:var(--paper);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  font-size:1rem;
  font-weight:500;
  color:var(--ink);
  font-family:var(--sans);
  outline:none;
  caret-color:var(--accent-text);
  transition:border-color .15s, box-shadow .15s;
}
.ans-input::placeholder { color:var(--ink-3); font-weight:400; }
.ans-input:focus {
  border-color:var(--line-2);
  box-shadow:0 0 0 3px var(--accent-wash);
}
.ans-input.shake {
  animation:shake .28s ease;
  border-color:var(--brick) !important;
  box-shadow:0 0 0 3px var(--brick-wash) !important;
}
.ans-input.flash-ok {
  border-color:var(--moss) !important;
  box-shadow:0 0 0 3px var(--moss-wash) !important;
  animation:flashOk .3s ease;
}

.field-hint {
  font-size:.72rem;
  font-weight:500;
  color:var(--brick);
  min-height:15px;
  padding-left:2px;
}

/* ── Buttons ── */
.card-btns { display:flex; gap:8px; }

.btn-primary {
  flex:1;
  display:flex;
  align-items:center;
  justify-content:center;
  gap:7px;
  padding:12px 18px;
  background:var(--grad-warm);
  background-size:160% 160%;
  background-position:0% 50%;
  color:var(--grad-warm-ink);
  font-size:.875rem;
  font-weight:700;
  border-radius:var(--r-sm);
  transition:transform .15s var(--ease-bounce), filter .15s, background-position .35s;
}
.btn-primary:hover { transform:translateY(-2px); filter:brightness(1.06); background-position:100% 50%; }
.btn-primary:active { transform:translateY(0) scale(.98); }

.btn-primary:disabled, .btn-ghost:disabled {
  opacity:.5;
  cursor:default;
  transform:none !important;
}

.btn-primary kbd, .btn-next kbd {
  display:inline-flex;
  align-items:center;
  justify-content:center;
  width:17px;
  height:17px;
  border-radius:3px;
  /* Tint of the button's own text colour, not a hardcoded shade — .btn-primary
     and .btn-next have opposite fill/text pairings (and both flip again in
     dark mode), so this is the only way both stay legible in every case. */
  background:color-mix(in srgb, currentColor 16%, transparent);
  font-size:.62rem;
  font-weight:700;
  border:1px solid color-mix(in srgb, currentColor 22%, transparent);
  font-family:var(--mono);
}

.btn-ghost {
  padding:11px 14px;
  font-size:.8rem;
  font-weight:600;
  color:var(--accent-text);
  background:var(--accent-wash);
  border:1px solid var(--accent-wash-2);
  border-radius:var(--r-sm);
  transition:color .15s, border-color .15s, background .15s, transform .15s var(--ease);
}
.btn-ghost:hover { color:var(--brick); border-color:var(--brick); background:var(--brick-wash); transform:translateY(-1px); }
.btn-ghost:active { transform:translateY(0); }

/* ── Result face ── */
#resultFace {
  align-items:center;
  text-align:center;
}

.result-status {
  display:inline-flex;
  align-items:center;
  gap:6px;
  padding:5px 14px;
  border-radius:var(--r-sm);
  font-size:.8rem;
  font-weight:700;
  letter-spacing:.01em;
}
.result-status.ok   { background:var(--moss-wash);  color:var(--moss);  border:1px solid var(--moss); }
.result-status.err  { background:var(--brick-wash); color:var(--brick); border:1px solid var(--brick); }
.result-status.warn { background:var(--ochre-wash); color:var(--ochre); border:1px solid var(--ochre); }

.result-reading { display:flex; flex-direction:column; gap:3px; }

.result-field-label {
  font-size:.6rem;
  font-weight:700;
  text-transform:uppercase;
  letter-spacing:.1em;
  color:var(--ink-3);
}

.result-romaji {
  font-family:var(--mono);
  font-size:1.9rem;
  font-weight:700;
  color:var(--accent-text);
  letter-spacing:.01em;
}

.result-meaning { display:flex; flex-direction:column; gap:3px; }
.result-meaning-text {
  font-family:var(--serif);
  font-size:1.05rem;
  font-weight:500;
  color:var(--ink-2);
  font-style:italic;
}

.result-your-ans {
  font-size:.78rem;
  color:var(--ink-3);
  min-height:18px;
}
.result-your-ans .wrong-ans { color:var(--brick); }
.result-your-ans .right-ans { color:var(--moss); }

.btn-next {
  width:100%;
  display:flex;
  align-items:center;
  justify-content:center;
  gap:7px;
  padding:11px 18px;
  background:var(--accent-wash);
  border:1px solid var(--accent-wash-2);
  color:var(--ink);
  font-size:.875rem;
  font-weight:600;
  border-radius:var(--r-sm);
  transition:background .15s, transform .15s var(--ease), border-color .15s;
  cursor:pointer;
}
.btn-next:hover { background:var(--accent-wash-2); border-color:var(--accent-text); transform:translateY(-1px); }
.btn-next:active { transform:translateY(0); }

/* ═══════════════════════════════════════════════════
   Recent answers
═══════════════════════════════════════════════════ */

.recent-label {
  font-size:.6rem;
  font-weight:700;
  text-transform:uppercase;
  letter-spacing:.1em;
  color:var(--ink-3);
  margin-bottom:7px;
}

.recent-list { display:flex; flex-direction:column; gap:3px; }

.recent-empty {
  font-family:var(--serif);
  font-style:italic;
  font-size:.8rem;
  color:var(--ink-3);
  padding:10px 12px;
  background:var(--paper);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
}

.recent-item {
  display:flex;
  align-items:center;
  gap:10px;
  padding:8px 12px;
  background:var(--paper);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  font-size:.78rem;
  animation:fadein .15s ease;
  transition:border-color .15s, transform .15s var(--ease);
}
.recent-item:hover { border-color:var(--line-2); transform:translateX(-2px); }
.recent-icon { font-size:.8rem; width:14px; flex-shrink:0; }
.recent-kana {
  font-family:var(--jp);
  font-size:1rem;
  min-width:24px;
  text-align:center;
  flex-shrink:0;
}
.recent-arr { color:var(--ink-3); flex-shrink:0; font-size:.7rem; }
.recent-reading { font-weight:600; color:var(--ink-2); }
.recent-meaning { color:var(--ink-3); font-style:italic; }
.recent-wrong { color:var(--ink-3); font-size:.7rem; }

/* ═══════════════════════════════════════════════════
   Leaderboard
═══════════════════════════════════════════════════ */
.table-wrap {
  max-width:100%;
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  overflow:hidden;
  overflow-x:auto;
  background:var(--paper-card);
}
.data-table {
  width:100%;
  border-collapse:collapse;
  font-size:.82rem;
}
.data-table th {
  padding:11px 16px;
  text-align:left;
  font-size:.6rem;
  font-weight:700;
  text-transform:uppercase;
  letter-spacing:.08em;
  color:var(--ink-3);
  background:var(--paper-2);
  border-bottom:1px solid var(--line);
  font-variant-numeric:tabular-nums;
  user-select:none;
}
.data-table th.sortable { cursor:pointer; transition:color .15s, background .15s; }
.data-table th.sortable:hover { color:var(--ink); background:var(--paper-2); }
.data-table th .sort-caret {
  display:inline-block;
  margin-left:5px;
  opacity:0;
  transform:translateY(1px);
  transition:opacity .15s, transform .2s var(--ease);
  color:var(--accent-text);
}
.data-table th[aria-sort] .sort-caret { opacity:1; }
.data-table th[aria-sort="ascending"] .sort-caret { transform:rotate(180deg); }
.data-table th[aria-sort] { color:var(--ink); }

.data-table td {
  padding:11px 16px;
  color:var(--ink-2);
  border-bottom:1px solid var(--line);
  font-variant-numeric:tabular-nums;
}
.data-table tbody tr {
  transition:background .15s;
  animation:fadein .25s ease both;
}
.data-table tr:last-child td { border-bottom:none; }
.data-table tbody tr:hover td { background:var(--paper-2); }

/* Podium tint for top 3 — flat, no glow */
.lb-row-1 td { background:var(--ochre-wash); }
.lb-row-2 td { background:var(--paper-2); }
.lb-row-3 td { background:var(--clay-wash); }

/* Your own row — always findable */
.lb-row-me td { background:var(--accent-wash) !important; box-shadow:inset 3px 0 0 var(--accent); }
.lb-row-me:hover td { background:var(--accent-wash) !important; }

/* Rank medal — an actual metallic shine on the podium, flat everywhere else */
.lb-rank { width:1%; white-space:nowrap; }
.lb-medal {
  display:inline-flex;
  align-items:center;
  justify-content:center;
  width:26px; height:26px;
  border-radius:50%;
  font-family:var(--mono);
  font-size:.7rem;
  font-weight:700;
  font-variant-numeric:tabular-nums;
  color:var(--ink-3);
  background:var(--paper-2);
}
.lb-medal.m1 { background:linear-gradient(135deg, #f5cf6b, #d99a2b); color:#3a2a06; font-size:.95rem; }
.lb-medal.m2 { background:linear-gradient(135deg, #eee7d8, #b3a684); color:#33291a; font-size:.95rem; }
.lb-medal.m3 { background:linear-gradient(135deg, #e5a874, #a85c33); color:#331c0d; font-size:.95rem; }

.lb-player { display:flex; align-items:center; gap:9px; }
.lb-avatar { width:26px; height:26px; border-radius:50%; object-fit:cover; border:1px solid var(--line-2); }
/* Generated from the player's handle (lbAvatar in panels/leaderboard.js) — the
   public board no longer carries anyone's Google photo. Only the HUE varies,
   which is what makes a player recognisable between visits; saturation and
   lightness are fixed so the initials are legible on every hue the generator
   can pick, in both themes. 52%/28% was chosen by measuring all 360: white on
   the worst hue (60, yellow) is 5.44:1. The obvious 42% lightness fails there
   at 2.68:1 — bright yellow is exactly where a fixed-lightness palette breaks,
   so this is measured rather than eyeballed. */
.lb-avatar-gen {
  flex:none;
  width:26px; height:26px; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  font-family:var(--mono); font-size:.6rem; font-weight:700; letter-spacing:.01em;
  color:#fff;
  background:hsl(var(--lb-hue, 210) 52% 28%);
  border:1px solid rgba(255,255,255,.16);
}
.lb-avatar-ph {
  width:26px; height:26px; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  font-size:.7rem; font-weight:700; color:var(--accent-ink);
  background:var(--accent);
}
.lb-name { font-weight:600; color:var(--ink); }
.lb-you {
  margin-left:6px;
  font-size:.58rem;
  font-weight:700;
  text-transform:uppercase;
  letter-spacing:.06em;
  color:var(--accent-text);
  padding:1px 6px;
  border-radius:99px;
  background:var(--accent-wash);
}

/* Total — number + proportional bar */
.lb-total { display:flex; align-items:center; gap:10px; min-width:130px; }
.lb-total-num { font-family:var(--mono); font-weight:700; color:var(--ink); min-width:46px; }
.lb-totbar { flex:1; height:6px; background:var(--paper-2); border-radius:99px; overflow:hidden; }
.lb-totbar i { display:block; height:100%; background:var(--grad-warm); border-radius:99px; transition:width .6s var(--ease); }

/* Best-streak pill */
.lb-streak {
  display:inline-flex;
  align-items:center;
  gap:3px;
  font-family:var(--mono);
  font-weight:700;
  font-variant-numeric:tabular-nums;
  color:var(--clay);
  padding:3px 9px;
  border-radius:99px;
  background:var(--clay-wash);
  border:1px solid var(--clay);
}
.lb-streak.zero { color:var(--ink-3); background:var(--paper-2); border-color:transparent; }

/* ═══════════════════════════════════════════════════
   Reference chart
═══════════════════════════════════════════════════ */
.gojuon, .gojuon-flat { --gj: clamp(30px, min(5.8vh, 11.5vw), 56px); }
.gojuon {
  display:inline-flex;
  flex-direction:column;
  gap:5px;
  max-width:100%;
  margin:0 auto;
}
.gj-row { display:flex; gap:5px; }
.gj-corner,
.gj-rlabel {
  flex:0 0 calc(var(--gj) * 0.72);
  display:flex;
  align-items:center;
  justify-content:center;
}
.gj-rlabel {
  font-family:var(--mono);
  font-weight:700;
  color:var(--accent-text);
  text-transform:uppercase;
  font-size:calc(var(--gj) * 0.3);
  letter-spacing:.05em;
}
.gj-vlabel {
  flex:0 0 var(--gj);
  display:flex;
  align-items:center;
  justify-content:center;
  padding-bottom:2px;
  font-family:var(--mono);
  font-weight:700;
  color:var(--ink-2);
  text-transform:uppercase;
  font-size:calc(var(--gj) * 0.26);
  letter-spacing:.1em;
}
.gj-cell {
  flex:0 0 var(--gj);
  height:var(--gj);
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:1px;
  background:var(--paper-card);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  cursor:pointer;
  position:relative;
  transition:border-color .12s, background .12s, transform .12s;
}
.gj-cell:hover { border-color:var(--accent-text); background:var(--paper-2); transform:translateY(-2px); z-index:1; }
.gj-cell:active { transform:translateY(0); }
.gj-empty { background:transparent; border:none; cursor:default; }
.gj-empty:hover { transform:none; }
.gj-kana { font-family:var(--jp); font-size:calc(var(--gj) * 0.5); line-height:1; }
.gj-rom  { font-family:var(--mono); font-size:calc(var(--gj) * 0.2); color:var(--ink-3); font-weight:700; letter-spacing:.03em; }

/* Flat grid for dakuten / handakuten */
.gojuon-flat {
  display:grid;
  grid-template-columns:repeat(auto-fill, var(--gj));
  gap:5px;
  justify-content:center;
}

/* ── Kana detail popup ── */
.kana-pop-overlay {
  position:fixed;
  inset:0;
  z-index:80;
  display:flex;
  align-items:center;
  justify-content:center;
  padding:24px;
  background:rgba(33,28,21,.5);
  opacity:0;
  transition:opacity .18s var(--ease);
}
.kana-pop-overlay.open { opacity:1; }
.kana-pop {
  position:relative;
  width:100%;
  max-width:360px;
  background:var(--paper-card);
  border:1px solid var(--line-2);
  border-radius:var(--r-lg);
  box-shadow:var(--sh-float);
  padding:26px 24px 24px;
  transform:translateY(10px) scale(.96);
  transition:transform .22s var(--ease);
}
.kana-pop-overlay.open .kana-pop { transform:translateY(0) scale(1); }

.kp-head { display:flex; align-items:flex-start; gap:16px; margin-bottom:18px; }
.kp-kana {
  font-family:var(--jp);
  font-size:4rem;
  line-height:1;
  color:var(--ink);
}
.kp-sound { display:flex; flex-direction:column; gap:4px; padding-top:4px; }
.kp-rom {
  font-family:var(--mono);
  font-size:1.5rem;
  font-weight:700;
  color:var(--accent-text);
}
.kp-say { font-size:.82rem; color:var(--ink-2); line-height:1.5; }
.kp-say b { color:var(--accent-text); font-weight:700; }
.kp-say i { color:var(--ink-2); font-style:normal; font-weight:600; }

.kp-mnemonic {
  display:flex;
  align-items:center;
  gap:14px;
  padding:14px;
  background:var(--accent-wash);
  border:1px solid var(--line);
  border-radius:var(--r-md);
  margin-bottom:12px;
}
.kp-emoji { font-size:2.2rem; line-height:1; flex-shrink:0; }
.kp-mnemonic p { font-size:.85rem; line-height:1.5; color:var(--ink); }
.kp-mnemonic b { color:var(--accent-text); font-weight:700; }
.kp-mnemonic i { color:var(--ink-2); font-style:normal; font-weight:600; }

.kp-example {
  display:flex;
  align-items:center;
  gap:8px;
  flex-wrap:wrap;
  padding:11px 13px;
  background:var(--paper);
  border:1px solid var(--line);
  border-radius:var(--r-md);
}
.kp-ex-label {
  font-size:.55rem; font-weight:700; text-transform:uppercase;
  letter-spacing:.1em; color:var(--ink-3);
}
.kp-ex-kana { font-family:var(--jp); font-size:1.15rem; color:var(--ink); }
.kp-ex-rom  { font-family:var(--mono); font-weight:700; color:var(--accent-text); font-size:.85rem; }
.kp-ex-mean { font-family:var(--serif); font-style:italic; font-size:.82rem; color:var(--ink-2); }

/* ═══════════════════════════════════════════════════
   Word Lookup — persistent floating tool
═══════════════════════════════════════════════════ */
/* --on-ink, not --accent-ink: this is text on an --ink FILL. --accent-ink is the
   ink for an ACCENT fill, and under a scene it resolves to --onr, which several
   scenes set light (temple #faf4e6) while their --ink/--txt is also light — so
   the two collided and the control rendered white-on-white. --on-ink is computed
   from --ink's own luminance per scene (theme_scenes mount), so it contrasts by
   construction. Same pairing as .nav-trigger.active and the hero stat card. */
.lookup-toggle {
  position:fixed; right:20px; bottom:20px; z-index:75;
  /* 52px with a 1.3rem glyph read as undersized for a floating action button
     (56px is the common floor, and the icon sat small inside it). */
  width:60px; height:60px; border-radius:50%; border:1px solid var(--line-2);
  display:flex; align-items:center; justify-content:center;
  background:var(--ink); color:var(--on-ink); font-size:1.7rem; line-height:1;
  box-shadow:var(--sh-float); cursor:pointer; transition:transform .15s var(--ease);
}
.lookup-toggle:hover { transform:scale(1.08); }
.lookup-panel {
  position:fixed; right:20px; bottom:82px; z-index:76;
  width:min(360px, calc(100vw - 40px)); max-height:min(70vh, 560px);
  display:flex; flex-direction:column; gap:10px;
  background:var(--paper-card);
  border:1px solid var(--line-2); border-radius:var(--r-lg); box-shadow:var(--sh-float);
  padding:14px 16px 16px; opacity:0; transform:translateY(10px) scale(.97);
  transition:opacity .18s var(--ease), transform .18s var(--ease);
}
.lookup-panel.open { opacity:1; transform:translateY(0) scale(1); }
.lookup-head { display:flex; align-items:center; justify-content:space-between; }
.lookup-title { font-family:var(--serif); font-weight:600; color:var(--ink); font-size:1.05rem; }
.lookup-input {
  width:100%; padding:10px 12px; border-radius:var(--r-md); border:1px solid var(--line);
  background:var(--paper); color:var(--ink); font-size:.95rem; font-family:var(--jp);
}
.lookup-input:focus { outline:none; border-color:var(--accent-text); }
.lookup-results { overflow-y:auto; display:flex; flex-direction:column; gap:10px; }
.lk-result { background:var(--paper); border:1px solid var(--line); border-radius:var(--r-md); padding:10px 12px; }
.lk-top { display:flex; align-items:baseline; justify-content:space-between; gap:8px; }
.lk-jp  { font-family:var(--jp); font-size:1.3rem; color:var(--ink); }
.lk-jp ruby rt { font-size:clamp(13px, .8rem, 16px); color:var(--ink-2); }
.lk-rom { font-family:var(--mono); font-size:.8rem; font-weight:700; color:var(--accent-text); flex-shrink:0; }
.lk-en  { font-family:var(--serif); font-size:.88rem; color:var(--ink-2); margin:4px 0 0; }
.lk-kana-tag {
  display:inline-block; margin-top:6px; font-size:.65rem; font-weight:700; color:var(--ink-3);
  text-transform:uppercase; letter-spacing:.05em;
}
.lk-kanji { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
.lk-kchip {
  display:flex; align-items:baseline; gap:5px; background:var(--paper-card); border:1px solid var(--line);
  border-radius:var(--r-sm); padding:3px 8px; font-family:var(--jp); cursor:default;
}
.lk-kchip b { font-size:1.05rem; color:var(--accent-text); font-weight:700; }
.lk-kchip i { font-size:.7rem; color:var(--ink-2); font-style:normal; font-family:var(--sans); }
.lk-kchip-plain { color:var(--ink-2); font-size:1.05rem; padding:3px 8px; }

/* Conjugated-verb match */
.lk-conj { border-color:var(--accent-text); background:var(--accent-wash); }
.lk-conj-tag {
  font-size:.68rem; font-weight:700; color:var(--accent-text); background:var(--paper-card);
  border:1px solid var(--line); border-radius:999px; padding:2px 9px; flex-shrink:0;
}
.lk-conj-root { font-size:.82rem; color:var(--ink-2); margin-top:6px; }
.lk-conj-root .lk-jp { font-size:1.05rem; margin:0 4px; }
.lk-conj-root .lk-rom { font-weight:700; color:var(--accent-text); }

/* Full conjugation table under a looked-up verb */
.lk-conjtable { margin-top:10px; border-top:1px dashed var(--line-2); padding-top:8px; }
.lk-conjtable summary {
  cursor:pointer; font-size:.8rem; font-weight:700; color:var(--accent-text);
  list-style:none; display:flex; align-items:center; gap:8px;
}
.lk-conjtable summary::-webkit-details-marker { display:none; }
.lk-conjtable summary::before { content:'▸'; font-size:.7rem; transition:transform .12s; }
.lk-conjtable[open] summary::before { transform:rotate(90deg); }
.lk-cf-grp { font-size:.66rem; font-weight:700; color:var(--ink-3); background:var(--paper-2); border-radius:999px; padding:1px 8px; }
.lk-cf { width:100%; border-collapse:collapse; margin-top:8px; font-size:.82rem; }
.lk-cf td { padding:3px 6px; border-bottom:1px solid var(--line); vertical-align:baseline; }
.lk-cf tr:last-child td { border-bottom:none; }
.lk-cf-head td {
  font-size:.68rem; font-weight:700; text-transform:uppercase; letter-spacing:.03em;
  color:var(--ink-3); padding-top:10px; border-bottom:1px solid var(--line-2);
}
.lk-cf-en  { color:var(--ink-2); }
.lk-cf-jp  { font-family:var(--mono); font-size:1rem; color:var(--ink); font-weight:600; white-space:nowrap; }
.lk-cf-rom { color:var(--ink-3); font-family:var(--mono); font-size:.76rem; text-align:right; white-space:nowrap; }

@media (max-width:480px) {
  .lookup-panel { right:12px; left:12px; width:auto; bottom:78px; }
  .lookup-toggle { right:16px; bottom:16px; }
}

/* ═══════════════════════════════════════════════════
   Animations
═══════════════════════════════════════════════════ */
@keyframes fadein {
  from { opacity:0; transform:translateY(5px); }
  to   { opacity:1; transform:translateY(0); }
}
@keyframes shake {
  0%,100% { transform:translateX(0); }
  20%     { transform:translateX(-6px); }
  40%     { transform:translateX(6px); }
  60%     { transform:translateX(-4px); }
  80%     { transform:translateX(3px); }
}
@keyframes flashOk {
  0%,100% { transform:scale(1); }
  40%     { transform:scale(1.015); }
}

/* ═══════════════════════════════════════════════════
   Header trophy / badges button
═══════════════════════════════════════════════════ */
.header-icon-btn {
  width:32px; height:32px;
  flex-shrink:0;
  display:flex; align-items:center; justify-content:center;
  font-size:1rem;
  border-radius:var(--r-md);
  background:var(--accent-wash);
  border:1px solid var(--accent-wash-2);
  transition:background .15s, border-color .15s, transform .2s var(--ease-bounce);
}
.header-icon-btn:hover { background:var(--accent-wash-2); border-color:var(--accent-text); transform:translateY(-2px) scale(1.06); }
.header-icon-btn:active { transform:none; }
/* Reviewer-only door — a filled accent pill so it stands out from the icon row. */
.header-review-btn {
  flex-shrink:0; display:inline-flex; align-items:center; gap:5px;
  padding:6px 13px; border-radius:999px; font-size:.82rem; font-weight:700; letter-spacing:.01em;
  color:var(--on-ink); background:var(--ink); border:1px solid transparent; text-decoration:none; white-space:nowrap;
  transition:transform .2s var(--ease-bounce), opacity .15s;
}
.header-review-btn:hover { transform:translateY(-2px) scale(1.04); opacity:.92; }
/* Sun ⇄ moon swap: spin the glyph so it reads as one object turning over
   rather than a flicker. Animating an inner pseudo-free rotation on the button
   itself would fight the :hover transform above, so this animates rotation
   only, from a class toggled for the duration (see syncThemeIcon). */
#themeToggleBtn.theme-spin { animation:themeSpin .38s var(--ease); }
@keyframes themeSpin {
  from { transform:rotate(-160deg) scale(.6); opacity:.35; }
  to   { transform:rotate(0) scale(1); opacity:1; }
}
@media (prefers-reduced-motion:reduce) { #themeToggleBtn.theme-spin { animation:none; } }

/* ── JLPT N5 readiness bar (navbar button + modal detail panel) ── */
.n5-bar-track {
  display:inline-block;
  width:106px; height:9px;      /* 10% longer than the original 96px */
  border-radius:99px;
  background:var(--paper-2);
  overflow:hidden;
}
.n5-bar-fill {
  display:block; height:100%; width:0%;
  border-radius:99px;
  transition:width .5s var(--ease), background .3s;
}
.n5-tier-low  { background:var(--brick); }
.n5-tier-mid  { background:var(--ochre); }
.n5-tier-high { background:var(--moss); }

/* The pill itself. This block lived inside `@media (max-width:1300px)` for a
   day — restored to the wrong side of a brace — so above 1300px the button had
   no padding, no background and no border, and read as loose text dropped next
   to the island rather than a control. Base scope, and deliberately placed
   AFTER .n5-bar-track above: media queries add no specificity, so the narrow
   -width `display:none` has to come later in the file than the base
   `display:inline-block` or it loses the cascade and never applies. */
.n5-bar-btn {
  display:flex; align-items:center; gap:9px;
  padding:6px 12px 6px 6px;
  border-radius:var(--r-md);
  background:var(--accent-wash);
  border:1px solid var(--accent-wash-2);
  transition:background .15s, border-color .15s, transform .2s var(--ease-bounce);
  /* Docked to the island. One auto margin now, not two: the left one takes all
     the free space so the pill travels with the island instead of floating in
     the middle of the header. The negative right margin is deliberate — the
     header's own `gap:14px` is the floor a margin cannot go under, so -5px is
     what buys a 9px gap. Readiness and the island are one group; the nav is the
     other. Zeroed on phones, where the header wraps (see the MOBILE block). */
  margin-left:auto;
  margin-right:-5px;
  flex-shrink:0;
}
.n5-bar-btn:hover { background:var(--accent-wash-2); border-color:var(--accent-text); transform:translateY(-2px); }
.n5-bar-btn:active { transform:none; }
.n5-bar-label { font-size:.8rem; font-weight:700; color:var(--ink-2); white-space:nowrap; }

/* Narrow desktops keep the % label and drop the bar graphic — the label alone
   still answers "how ready am I", and the 96px track is the cheapest thing in
   the header to give back to the nav. */
@media (max-width:1300px) {
  .n5-bar-track { display:none; }
}

/* Cleaner "artifact" N5 look: each section is a soft card with a glowing bar
   and a bold % readout; the remaining items read as neat rows/chips. Full
   drill-down preserved (JS still lists every item behind the expander). */
.n5-pillar {
  margin:14px 0; padding:14px 16px;
  background:color-mix(in srgb, var(--card) 84%, transparent);
  border:1px solid var(--edge2); border-radius:var(--rCard);
  box-shadow:0 0 calc(20px * var(--gk)) -14px var(--a1);
}
/* The head is a disclosure BUTTON now (progressive disclosure — see
   renderN5Panel): collapsed it shows label + how many are left + the
   percentage, so the panel opens as five scannable lines instead of ~30 rows
   of detail nobody asked for yet. */
.n5-pillar-head {
  display:flex; align-items:center; gap:10px; width:100%;
  font-family:inherit; font-size:.9rem; font-weight:700; color:var(--txt);
  margin-bottom:9px; padding:2px 0; cursor:pointer;
  background:none; border:0; text-align:left;
}
.n5-pillar-head:hover .n5-pillar-label { color:var(--a1); }
.n5-pillar-head:focus-visible { outline:2px solid var(--a1); outline-offset:3px; border-radius:var(--r-sm); }
.n5-pillar-caret {
  flex:none; font-size:.7rem; color:var(--ink-3); line-height:1;
  transition:transform .16s var(--ease);
}
.n5-pillar.is-open .n5-pillar-caret { transform:rotate(90deg); }
.n5-pillar-label { flex:1 1 auto; min-width:0; }
.n5-pillar-left {
  flex:none; font-size:.7rem; font-weight:700; color:var(--ink-3);
  font-variant-numeric:tabular-nums; white-space:nowrap;
}
.n5-pillar-done { color:var(--moss); }
.n5-pillar-pct { flex:none; color:var(--gold); font-weight:700; font-variant-numeric:tabular-nums; }
/* --gold is tuned for dark panels; on the light+scene N5 panel it measured
   1.24:1 (bright yellow on near-white — effectively invisible). Deepen it here
   only; the dark theme keeps the bright gold. */
:root[data-theme="light"][data-scene] .n5-pillar-pct { color:#6f4f00; }
/* the body only exists in the DOM when open, so this is just its spacing */
.n5-pillar-body { margin-top:2px; }
@media (max-width:520px) {
  /* the "N to go" count is the first thing to go when space is tight — the
     percentage and the bar already convey progress */
  .n5-pillar-left { display:none; }
}
.n5-pillar .n5-bar-track { width:100%; height:10px; background:var(--track); border:1px solid var(--edge); }
.n5-pillar .n5-bar-fill { box-shadow:0 0 calc(10px * var(--gk)) -2px currentColor; }
.n5-pillar-remaining {
  font-family:var(--jp); font-size:.82rem; color:var(--moss); font-weight:700; line-height:1.6;
  margin-top:9px;
}

/* Per-item "still needs work" rows: the word/kanji/particle + per-direction
   status chip (✓ mastered / N/5 in progress / not started), closest-to-done
   first (n5mDetailedRemaining). Only ~6 show by default; rest behind the toggle. */
.n5-remaining-list { list-style:none; margin-top:10px; display:flex; flex-direction:column; gap:6px; }
.n5-remaining-row {
  display:flex; justify-content:space-between; align-items:center; gap:10px;
  padding:7px 11px;
  background:color-mix(in srgb, var(--bg2) 70%, transparent);
  border:1px solid var(--edge); border-radius:calc(var(--rBtn) - 2px);
}
.n5-remaining-item { font-family:var(--jp); font-size:.9rem; font-weight:600; color:var(--txt); }
.n5-remaining-chips { display:flex; gap:6px; flex-shrink:0; flex-wrap:wrap; justify-content:flex-end; }
.n5-dir-chip {
  font-size:.68rem; font-weight:700; padding:2px 8px; border-radius:99px;
  background:var(--track); color:var(--ink-3); white-space:nowrap; border:1px solid var(--edge);
}
.n5-dir-chip.n5-dir-done { background:color-mix(in srgb,var(--moss) 22%,transparent); color:var(--moss); border-color:transparent; }
/* the level-2 "show N more" expander → a tidy pill button.
   (This was declared twice, the second block silently overriding the first;
   merged into one.) */
.n5-remaining-toggle {
  display:block; width:100%; margin-top:8px; padding:7px 10px; cursor:pointer;
  font-family:var(--jp); font-size:.76rem; font-weight:700; color:var(--a1);
  background:color-mix(in srgb, var(--a1) 12%, transparent);
  border:1px solid color-mix(in srgb, var(--a1) 30%, transparent);
  border-radius:var(--rBtn);
  transition:background .15s, border-color .15s;
}
.n5-remaining-toggle:hover {
  background:color-mix(in srgb, var(--a1) 22%, transparent);
  border-color:var(--a1);
}
.n5-remaining-extra { margin-top:8px; }
/* honest note when the expanded list is capped (see N5_EXPAND_CAP) — a silent
   truncation would read as "that's everything" when it isn't */
.n5-remaining-note {
  margin-top:8px; padding:7px 11px;
  font-size:.72rem; font-weight:600; color:var(--ink-3); line-height:1.5;
  background:color-mix(in srgb, var(--bg2) 55%, transparent);
  border:1px dashed var(--edge); border-radius:calc(var(--rBtn) - 2px);
}

/* ═══════════════════════════════════════════════════
   Per-question speed timer bar
═══════════════════════════════════════════════════ */
/* Hidden unless systems/combo_fx.js turns its speedBar flag on. The timer is
   NOT disabled by this: stopTimer() still returns the speed factor and
   momentum still scales with it. A visible countdown on every question is
   pressure on someone who is mid-thought, so it is off by default. */
html:not([data-fx~="speedBar"]) .speed-bar { display:none; }
.speed-bar {
  position:absolute;
  top:3px; left:0; right:0;
  height:3px;
  background:transparent;
  z-index:2;
  overflow:hidden;
}
.speed-fill {
  height:100%;
  width:100%;
  background:var(--grad-warm);
  opacity:.7;
}

/* ═══════════════════════════════════════════════════
   Escalating streak feedback + fever flash on the card
   (flat colour/border escalation — no glow, no blur)
═══════════════════════════════════════════════════ */
.quiz-card.glow-1 { border-color:var(--accent-text); }
.quiz-card.glow-2 { border-color:var(--ochre); }
.quiz-card.glow-3 { border-color:var(--clay); }
.quiz-card.glow-2::before { background:var(--ochre); }
.quiz-card.glow-3::before { background:var(--clay); }
@keyframes feverFlash {
  0%,100% { background-color:var(--paper-card); }
  30%     { background-color:var(--ochre-wash); }
}
.quiz-card.fever { animation:feverFlash .7s var(--ease); }

/* ═══════════════════════════════════════════════════
   Floating "+score" pops
═══════════════════════════════════════════════════ */
.scorepop-layer { position:fixed; inset:0; z-index:58; pointer-events:none; }
.score-pop {
  position:fixed;
  transform:translate(-50%,0);
  font-family:var(--mono);
  font-size:1.1rem;
  font-weight:700;
  color:var(--accent-text);
  font-variant-numeric:tabular-nums;
  white-space:nowrap;
  animation:scorePop 1.1s var(--ease) forwards;
}
.score-pop.fast { color:var(--moss); }
@keyframes scorePop {
  0%   { opacity:0; transform:translate(-50%,6px) scale(.85); }
  18%  { opacity:1; transform:translate(-50%,-4px) scale(1.05); }
  100% { opacity:0; transform:translate(-50%,-46px) scale(1); }
}

/* ═══════════════════════════════════════════════════
   Modal (leaderboard / reference)
═══════════════════════════════════════════════════ */
.modal-overlay {
  position:fixed;
  inset:0;
  z-index:70;
  display:flex;
  align-items:center;
  justify-content:center;
  padding:24px;
  background:rgba(33,28,21,.5);
  opacity:0;
  transition:opacity .2s var(--ease);
}
.modal-overlay.open { opacity:1; }
.modal {
  position:relative;
  width:100%;
  max-width:600px;
  /* dvh, not vh: on iOS `vh` is the PRE-toolbar viewport, so an 86vh sheet's
     bottom edge can sit under the browser chrome. On desktop dvh == vh, so
     this is a no-op there. (The ≤720 rule already uses dvh.) */
  max-height:86dvh;
  overflow:auto;
  background:var(--paper-card);
  border:1px solid var(--line-2);
  border-radius:var(--r-lg);
  box-shadow:var(--sh-float);
  padding:24px 24px 26px;
  transform:translateY(12px) scale(.98);
  transition:transform .25s var(--ease);
  scrollbar-width:thin;
  scrollbar-color:var(--line-2) transparent;
}
.modal-overlay.open .modal { transform:translateY(0) scale(1); }
.modal::-webkit-scrollbar { width:8px; }
.modal::-webkit-scrollbar-thumb { background:var(--line-2); border-radius:99px; border:2px solid transparent; background-clip:padding-box; }
.modal-close {
  position:absolute;
  top:14px; right:14px;
  width:30px; height:30px;
  display:flex; align-items:center; justify-content:center;
  border-radius:var(--r-sm);
  font-size:.8rem;
  color:var(--ink-2);
  background:var(--paper-2);
  border:1px solid var(--line);
  transition:background .15s, color .15s, transform .15s var(--ease);
  z-index:2;
}
.modal-close:hover { background:var(--accent-wash); color:var(--ink); transform:rotate(90deg); }
.modal-head { margin-bottom:16px; padding-right:36px; }
.modal-title {
  font-family:var(--serif);
  font-size:1.25rem;
  font-weight:600;
  letter-spacing:-.01em;
}
.modal-sub { font-size:.78rem; color:var(--ink-3); margin-top:4px; }
.ref-scroll { overflow-x:auto; }

/* ═══════════════════════════════════════════════════
   Center combo HUD (in the play area)
═══════════════════════════════════════════════════ */
.combo-hud {
  display:flex;
  align-items:center;
  gap:11px;
  margin-bottom:16px;
  padding:8px 12px;
  background:var(--paper-card);
  border:1px solid var(--line);
  border-radius:99px;
  transition:border-color .25s;
}
.ch-mult {
  font-family:var(--mono);
  font-size:1rem;
  font-weight:700;
  font-variant-numeric:tabular-nums;
  color:var(--ink-2);
  min-width:34px;
  transition:color .2s, transform .25s var(--ease);
}
.ch-bar {
  position:relative;               /* the anticipation sheen is a ::after on this */
  flex:1;
  height:10px;
  border-radius:99px;
  background:var(--paper-2);
  overflow:hidden;
}
.ch-fill {
  height:100%;
  width:0%;
  border-radius:99px;
  background:linear-gradient(90deg, var(--accent), var(--clay));
  transition:width .35s var(--ease), background .2s;
}
.ch-tag {
  font-size:.62rem;
  font-weight:700;
  text-transform:uppercase;
  letter-spacing:.06em;
  /* --ink-2, not --ink-3: at 10px the placeholder token measures 4.42:1 on the
     card — just under AA — and this is the label that tells you what the combo
     bar beside it means. */
  color:var(--ink-2);
  white-space:nowrap;
}
.ch-score {
  font-family:var(--mono);
  font-size:.95rem;
  font-weight:700;
  font-variant-numeric:tabular-nums;
  min-width:46px;
  text-align:right;
  color:var(--accent-text);
}
/* HUD heat tiers — flat colour escalation, no glow */
.combo-hud[data-mult="2"] { border-color:var(--accent-text); }
.combo-hud[data-mult="3"] .ch-fill,
.combo-hud[data-mult="4"] .ch-fill,
.combo-hud[data-mult="5"] .ch-fill { background:var(--ochre); }
.combo-hud[data-mult="3"] { border-color:var(--ochre); }
.combo-hud[data-mult="4"] { border-color:var(--clay); }
.combo-hud[data-mult="4"] .ch-fill { background:var(--clay); }
.combo-hud[data-mult="5"] {
  border-color:var(--brick);
  animation:comboPulse 1s ease-in-out infinite;
}
.combo-hud[data-mult="5"] .ch-fill { background:var(--grad-warm); }
.combo-hud[data-mult="3"] .ch-mult,
.combo-hud[data-mult="4"] .ch-mult { color:var(--clay); }
.combo-hud[data-mult="5"] .ch-mult {
  background:var(--grad-warm); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent;
}
@keyframes comboPulse { 0%,100% { opacity:1; } 50% { opacity:.7; } }

/* ── The long ladder: 6, 7, 8 (systems/combo_fx.js ladder8) ────────────────
   No new hues — past ×5 the meter escalates with the reward gradient it
   already owns, so the top of the ladder feels earned rather than recoloured. */
.combo-hud[data-mult="6"], .combo-hud[data-mult="7"], .combo-hud[data-mult="8"] {
  border-color:var(--brick); animation:comboPulse 1s ease-in-out infinite;
}
.combo-hud[data-mult="6"] .ch-fill,
.combo-hud[data-mult="7"] .ch-fill,
.combo-hud[data-mult="8"] .ch-fill { background:var(--grad-warm); }
.combo-hud[data-mult="6"] .ch-mult,
.combo-hud[data-mult="7"] .ch-mult,
.combo-hud[data-mult="8"] .ch-mult {
  background:var(--grad-warm); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent;
}
.combo-hud[data-mult="7"] { box-shadow:0 0 14px -2px color-mix(in srgb, var(--a1) 60%, transparent); }
.combo-hud[data-mult="8"] {
  animation:comboPulse .7s ease-in-out infinite;
  box-shadow:0 0 22px 0 color-mix(in srgb, var(--a1) 70%, transparent);
}
@media (prefers-reduced-motion:reduce) {
  .combo-hud[data-mult="6"], .combo-hud[data-mult="7"], .combo-hud[data-mult="8"] { animation:none; }
}

/* ── Anticipation: the meter reacts as the next tier comes into reach ──────
   data-brink is stamped 0–3 by updateComboMeter(); 3 means one fast clean
   answer would finish the tier. Every stage is gated on [data-fx~="…"] so
   systems/combo_fx.js can retire any one of them, and so a calm scene simply
   never receives the loud ones. Reports real progress, fakes nothing, adds no
   penalty — and see the evidence note in combo_fx.js before betting on it. */
.ch-brink {
  flex-shrink:0; font-size:.62rem; font-weight:700; letter-spacing:.06em;
  text-transform:uppercase; color:var(--accent-text); white-space:nowrap;
  opacity:0; transition:opacity .2s var(--ease);
}
[data-fx~="callout"] .combo-hud[data-brink="2"] .ch-brink,
[data-fx~="callout"] .combo-hud[data-brink="3"] .ch-brink { opacity:1; }

[data-fx~="sheen"] .combo-hud[data-brink="1"] .ch-bar::after,
[data-fx~="sheen"] .combo-hud[data-brink="2"] .ch-bar::after,
[data-fx~="sheen"] .combo-hud[data-brink="3"] .ch-bar::after {
  content:''; position:absolute; inset:0; border-radius:99px; pointer-events:none;
  background:linear-gradient(100deg, transparent 30%, rgba(255,255,255,.5) 50%, transparent 70%);
  background-size:45% 100%; background-repeat:no-repeat;
  animation:comboSheen var(--sheen, 1.6s) linear infinite;
}
.combo-hud[data-brink="2"] { --sheen:1s; }
.combo-hud[data-brink="3"] { --sheen:.55s; }
@keyframes comboSheen { from { background-position:-45% 0; } to { background-position:145% 0; } }

[data-fx~="pulse"] .combo-hud[data-brink="2"] { animation:comboBrink 1.1s ease-in-out infinite; }
[data-fx~="pulse"] .combo-hud[data-brink="3"] { animation:comboBrink .55s ease-in-out infinite; }
@keyframes comboBrink {
  0%,100% { box-shadow:0 0 0 0 color-mix(in srgb, var(--a1) 50%, transparent); }
  50%     { box-shadow:0 0 16px 3px color-mix(in srgb, var(--a1) 50%, transparent); }
}
[data-fx~="shake"] .combo-hud[data-brink="3"] { animation:comboShake .16s linear infinite; }
[data-fx~="shake"][data-fx~="pulse"] .combo-hud[data-brink="3"] {
  animation:comboShake .16s linear infinite, comboBrink .55s ease-in-out infinite;
}
@keyframes comboShake {
  0%,100% { transform:translateX(0); }
  25%     { transform:translateX(-1.5px) rotate(-.25deg); }
  75%     { transform:translateX(1.5px) rotate(.25deg); }
}
[data-fx~="glow"] .combo-hud[data-brink="3"] .ch-mult { text-shadow:0 0 12px var(--a1); }
[data-fx~="glow"] .combo-hud[data-brink="3"] .ch-fill { filter:brightness(1.25) saturate(1.2); }

/* ── Tier-up extras, both gated ───────────────────────────────────────────── */
.fx-shock {
  position:absolute; left:50%; top:42%; width:80px; height:80px; margin:-40px 0 0 -40px;
  border-radius:50%; border:3px solid var(--accent-text); opacity:0; pointer-events:none;
  animation:fxShock .7s var(--ease) forwards;
}
.fx-shock.tier-3 { border-color:var(--ochre); }
.fx-shock.tier-4 { border-color:var(--clay); }
.fx-shock.tier-5 { border-color:var(--brick); }
@keyframes fxShock { 0% { opacity:.9; transform:scale(.2); } 100% { opacity:0; transform:scale(4.2); } }
/* The calm-scene gesture: a slow bloom instead of an explosion. */
.fx-bloom {
  position:absolute; left:50%; top:42%; width:60px; height:60px; margin:-30px 0 0 -30px;
  border-radius:50%; background:var(--grad-warm); opacity:0; pointer-events:none;
  animation:fxBloom 1.5s var(--ease) forwards;
}
@keyframes fxBloom { 0% { opacity:0; transform:scale(.3); } 25% { opacity:.5; } 100% { opacity:0; transform:scale(6); } }
.quiz-card.kick { animation:fxKick .38s var(--ease-bounce); }
@keyframes fxKick { 0% { transform:none; } 30% { transform:translateY(-5px) scale(1.018); } 100% { transform:none; } }
@media (prefers-reduced-motion:reduce) {
  .fx-shock, .fx-bloom { display:none; }
  .quiz-card.kick { animation:none; }
}
/* Max combo is the one place in the app that runs an INFINITE animation on a
   piece of chrome, and it sat outside every reduced-motion block — so it kept
   pulsing for someone who has asked the OS to stop exactly this. The brick
   border and the gradient numeral already say "max" on their own, so dropping
   the pulse costs no meaning. (It also stops one perpetual compositor job on a
   phone, which is the same thing the animations-at-rest pass was after.) */
@media (prefers-reduced-motion:reduce) {
  .combo-hud[data-mult="5"] { animation:none; }
}

/* ═══════════════════════════════════════════════════
   Big multiplier effects (center burst · particles · flash)
   Flat colour, hard edges — no rays, no blur, no glow.
═══════════════════════════════════════════════════ */
.center-burst {
  position:absolute;
  left:50%; top:42%;
  transform:translate(-50%,-50%);
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:4px;
  pointer-events:none;
}
.cb-mult {
  font-family:var(--serif);
  font-size:clamp(3.5rem, 14vw, 7.5rem);
  font-weight:600;
  font-style:italic;
  letter-spacing:-.02em;
  line-height:.9;
  color:var(--accent-text);
  animation:burstPop 1.1s var(--ease) forwards;
}
.cb-label {
  font-family:var(--mono);
  font-size:clamp(.9rem, 2.6vw, 1.3rem);
  font-weight:700;
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--ink);
  animation:labelPop 1.1s var(--ease) forwards;
}
.center-burst.tier-3 .cb-mult { color:var(--ochre); }
.center-burst.tier-4 .cb-mult { color:var(--clay); }
.center-burst.tier-5 .cb-mult { color:var(--brick); }
@keyframes burstPop {
  0%   { opacity:0; transform:scale(.5); }
  35%  { opacity:1; transform:scale(1.08); }
  55%  { transform:scale(.98); }
  80%  { opacity:1; transform:scale(1); }
  100% { opacity:0; transform:scale(1.04); }
}
@keyframes labelPop {
  0%,18% { opacity:0; transform:translateY(10px); }
  40%    { opacity:1; transform:translateY(0); }
  80%    { opacity:1; }
  100%   { opacity:0; transform:translateY(-6px); }
}

.particle {
  position:absolute;
  left:50%; top:42%;
  border-radius:50%;
  pointer-events:none;
  opacity:0;
  animation:particleBurst var(--dur,1s) var(--ease) forwards;
}
@keyframes particleBurst {
  0%   { opacity:0; transform:translate(-50%,-50%) scale(.4); }
  12%  { opacity:1; }
  100% { opacity:0; transform:translate(calc(-50% + var(--tx)), calc(-50% + var(--ty))) scale(1); }
}

/* Full-screen tint on big combos — flat fade, no radial blur blob */
.screen-flash {
  position:fixed;
  inset:0;
  pointer-events:none;
  opacity:0;
  background:var(--accent-wash);
  animation:flashFade .5s var(--ease) forwards;
}
.screen-flash.tier-3 { background:var(--ochre-wash); }
.screen-flash.tier-4 { background:var(--clay-wash); }
.screen-flash.tier-5 { background:var(--brick-wash); }
@keyframes flashFade {
  0%   { opacity:0; }
  22%  { opacity:.8; }
  100% { opacity:0; }
}

/* Bigger confetti pieces */
.confetti-piece { width:9px; height:14px; }

/* ═══════════════════════════════════════════════════
   Badges
═══════════════════════════════════════════════════ */
.header-icon-btn { position:relative; }
.header-icon-btn.has-badges::after {
  content:attr(data-count);
  position:absolute;
  top:-5px; right:-5px;
  min-width:15px; height:15px;
  padding:0 3px;
  display:flex; align-items:center; justify-content:center;
  font-family:var(--mono);
  font-size:.55rem; font-weight:700;
  color:var(--accent-ink);
  background:var(--clay);
  border-radius:99px;
  box-shadow:0 0 0 2px var(--paper);
}

/* Profile header */
.profile-head { display:flex; align-items:center; gap:14px; margin-bottom:20px; }
.profile-av {
  width:54px; height:54px; border-radius:50%; object-fit:cover;
  border:2px solid var(--line-2); flex-shrink:0;
}
.profile-av-ph {
  display:flex; align-items:center; justify-content:center;
  font-size:1.4rem; font-weight:700; color:var(--grad-warm-ink);
  background:var(--grad-warm); border-color:transparent;
  box-shadow:0 0 calc(18px * var(--gk)) -8px var(--a1);
}
.profile-section-label {
  font-size:.6rem; font-weight:700; text-transform:uppercase; letter-spacing:.12em;
  color:var(--ink-3); margin-bottom:10px;
}
/* The public handle, under the account's own name on the profile. Reads as the
   secondary identity it is — the board shows this, never the real name. */
.profile-handle {
  display:flex; align-items:baseline; gap:8px; flex-wrap:wrap;
  margin:2px 0 4px;
  font-family:var(--mono); font-size:.9rem; font-weight:700;
  color:var(--accent-text);
}
.profile-handle .ph-note {
  font-family:var(--sans); font-size:.68rem; font-weight:600;
  letter-spacing:.04em; text-transform:uppercase; color:var(--ink-3);
}

.profile-empty {
  text-align:center;
  padding:30px 20px;
  border:1px dashed var(--line-2);
  border-radius:var(--r-md);
  background:var(--paper);
}
.pe-emoji { font-size:2.4rem; opacity:.6; }
.pe-title { font-family:var(--serif); font-size:1rem; font-weight:600; color:var(--ink); margin-top:8px; }
.pe-sub { font-size:.8rem; color:var(--ink-3); line-height:1.5; margin-top:5px; max-width:300px; margin-left:auto; margin-right:auto; }

/* Badges grid */
.badge-grid {
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(140px, 1fr));
  gap:10px;
}
.badge-card {
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:7px;
  padding:16px 12px 13px;
  border-radius:var(--rCard);
  border:1px solid var(--edge);
  background:color-mix(in srgb, var(--card) 80%, transparent);
  text-align:center;
  transition:transform .15s var(--ease), border-color .15s, box-shadow .15s;
}
.badge-card:hover { transform:translateY(-3px); border-color:var(--edge2); }
.badge-card.earned {
  border-color:var(--edge2);
  background:color-mix(in srgb, var(--a1) 8%, var(--card));
  box-shadow:0 0 calc(22px * var(--gk)) -14px var(--a1);
}
.badge-emoji { font-size:2.2rem; line-height:1; }
.badge-card.locked .badge-emoji { filter:grayscale(1); opacity:.4; }
.badge-name { font-size:.82rem; font-weight:700; color:var(--ink); }
.badge-prog {
  font-family:var(--mono);
  font-size:.64rem; font-weight:700; color:var(--ink-3);
  font-variant-numeric:tabular-nums;
}
.badge-card.earned .badge-prog { color:var(--accent-text); }
/* Milestone cards carry a sentence ("Practice 7 days in a row") where a word-set
   card carries a "Lv 2 / 3" counter — so drop the tabular-mono treatment and let
   it wrap as prose. Distinguished by a gold edge: milestones are one-shot
   achievements, not a tier ladder you keep climbing. */
.badge-card-ms { border-color:color-mix(in srgb, var(--gold, #d8a13a) 45%, var(--edge2)); }
.badge-card-ms .badge-prog {
  font-family:inherit;
  font-size:.66rem; font-weight:600; line-height:1.3;
  color:var(--ink-3); text-wrap:balance;
}
.badge-card.earned.badge-card-ms .badge-prog { color:var(--ink-2); }
/* Sibling-milestone rows in the detail view: icon + name, clickable. */
.badge-word.ms-other { cursor:pointer; align-items:center; }
.badge-word.ms-other:hover { background:color-mix(in srgb, var(--a1) 7%, transparent); }
.badge-bar {
  width:100%; height:4px; margin-top:2px;
  background:var(--paper-2); border-radius:99px; overflow:hidden;
}
.badge-bar i { display:block; height:100%; background:var(--grad-warm); border-radius:99px; transition:width .5s var(--ease); }

/* Badge detail (word list) */
.badge-back {
  font-size:.78rem; font-weight:600; color:var(--ink-2);
  padding:5px 0; margin-bottom:6px;
  transition:color .15s;
}
.badge-back:hover { color:var(--ink); }
.badge-detail-head { display:flex; align-items:center; gap:14px; }
.badge-detail-emoji { font-size:2.6rem; line-height:1; }
.badge-detail-emoji.locked { filter:grayscale(1); opacity:.45; }
.badge-words { display:flex; flex-direction:column; gap:4px; }
.badge-medals { display:flex; gap:3px; }
.badge-medals .bm { font-size:.9rem; filter:grayscale(1); opacity:.3; transition:filter .15s, opacity .15s; }
.badge-medals .bm.on { filter:none; opacity:1; }

/* Flavour line explaining a themed badge's name/pun (I2a) — the reward half of
   the detail, above the progress table. */
.badge-blurb {
  margin:2px 0 16px; padding:10px 14px;
  border-left:3px solid var(--accent);
  background:var(--paper-2); border-radius:0 var(--r-sm,8px) var(--r-sm,8px) 0;
  color:var(--ink-2); font-size:.95rem; font-style:italic; line-height:1.5;
}

.badge-tiers { display:flex; flex-direction:column; gap:5px; margin-bottom:14px; }
.bt-row {
  display:flex; align-items:center; gap:10px;
  padding:8px 12px;
  background:var(--paper);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
}
.bt-row.on { border-color:var(--accent-text); background:var(--accent-wash); }
.bt-medal { font-size:1.1rem; filter:grayscale(1); opacity:.4; }
.bt-row.on .bt-medal { filter:none; opacity:1; }
.bt-label { font-weight:700; color:var(--ink); flex:1; }
.bt-prog { font-family:var(--mono); font-size:.72rem; font-weight:700; color:var(--ink-3); font-variant-numeric:tabular-nums; }
.bt-row.on .bt-prog { color:var(--accent-text); }

.badge-word {
  display:grid;
  grid-template-columns:auto auto 1fr auto;
  align-items:baseline;
  gap:10px;
  padding:8px 12px;
  background:var(--paper);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  font-size:.82rem;
}
.bw-kana { font-family:var(--jp); font-size:1rem; color:var(--ink); }
.bw-rom  { font-family:var(--mono); font-weight:700; color:var(--accent-text); }
.bw-mean { color:var(--ink-2); font-style:italic; }
.bw-pips { display:flex; gap:4px; align-self:center; }
.bw-pips .wp {
  width:8px; height:8px; border-radius:50%;
  background:var(--paper-2);
}
.bw-pips .wp.on { background:var(--accent); }

/* Badge-earned popup */
.badge-pop-overlay {
  position:fixed; inset:0; z-index:85;
  display:flex; align-items:center; justify-content:center;
  padding:24px;
  background:rgba(33,28,21,.55);
  opacity:0; transition:opacity .2s var(--ease);
}
.badge-pop-overlay.open { opacity:1; }
.badge-pop {
  width:100%; max-width:340px;
  padding:30px 26px 24px;
  text-align:center;
  background:var(--paper-card);
  border:1px solid var(--line-2);
  border-radius:var(--r-lg);
  box-shadow:var(--sh-float);
  transform:translateY(12px) scale(.94);
  transition:transform .25s var(--ease);
}
.badge-pop-overlay.open .badge-pop { transform:translateY(0) scale(1); }
.bp-emoji {
  position:relative; display:inline-block;
  font-size:4.5rem; line-height:1; margin-bottom:8px;
  animation:badgePopIn .6s var(--ease) both;
}
.bp-medal { position:absolute; bottom:-4px; right:-10px; font-size:2rem; }
.bp-tiers { display:flex; justify-content:center; gap:8px; margin:6px 0 4px; }
.bp-tier { font-size:1.3rem; filter:grayscale(1); opacity:.35; }
.bp-tier.on { filter:none; opacity:1; }
@keyframes badgePopIn {
  0% { transform:scale(.4) rotate(-10deg); opacity:0; }
  60%{ transform:scale(1.12) rotate(4deg); opacity:1; }
  100%{ transform:scale(1) rotate(0); }
}
.bp-eyebrow {
  font-size:.68rem; font-weight:700; letter-spacing:.14em; text-transform:uppercase;
  color:var(--accent-text);
}
.bp-name { font-family:var(--serif); font-size:1.5rem; font-weight:600; letter-spacing:-.01em; margin:3px 0 8px; color:var(--ink); }
.bp-sub { font-size:.8rem; color:var(--ink-2); line-height:1.5; margin-bottom:18px; }
.bp-btns { display:flex; flex-direction:column; gap:8px; }

/* ═══════════════════════════════════════════════════
   Game-mode switch + English prompt
═══════════════════════════════════════════════════ */
.mode-switch {
  display:flex;
  gap:5px;
  width:fit-content;
  margin:0 auto 16px;
  padding:4px;
  background:var(--paper-2);
  border:1px solid var(--line);
  border-radius:var(--r-md);
}
.mode-btn {
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:2px;
  padding:6px 16px;
  border-radius:var(--r-sm);
  font-size:.84rem;
  font-weight:700;
  color:var(--ink-2);
  transition:background .15s, color .15s;
}
.mode-btn .ms-sub {
  font-size:.54rem;
  font-weight:700;
  text-transform:uppercase;
  letter-spacing:.07em;
  color:var(--ink-3);
}
.mode-btn:hover { color:var(--ink); background:var(--paper-card); }
.mode-btn.active { background:var(--grad-warm); color:var(--grad-warm-ink); }
.mode-btn.active .ms-sub { color:var(--grad-warm-ink); opacity:.7; }

/* English prompt shown in EN → JP (recall) mode */
.kana-display.mode-en {
  font-family:var(--serif);
  font-size:clamp(1.5rem, 5vw, 2.4rem);
  font-weight:600;
  line-height:1.3;
  color:var(--ink);
  padding:14px 12px;
  letter-spacing:-.01em;
}

/* ── Memory Match (concentration) ── */
.memory-view { display:flex; flex-direction:column; align-items:center; gap:16px; padding:8px 0 40px; animation:viewIn .4s var(--ease); }
/* .mem-head is the shared .game-head family now. */
.mem-title { font-family:var(--serif); font-size:1.3rem; font-weight:600; color:var(--ink); }
.mem-stat { font-size:.85rem; color:var(--ink-2); }
.mem-status { color:var(--moss); font-weight:600; }
.mem-grid {
  display:grid;
  grid-template-columns:repeat(4, 1fr);
  gap:10px;
  width:100%; max-width:480px;
}
.mem-card {
  aspect-ratio:1 / 1;
  font-family:var(--jp);
  font-size:clamp(1.3rem, 6vw, 2rem);
  font-weight:600;
  color:var(--ink);
  background:var(--paper-card);
  border:2px solid var(--line);
  border-radius:var(--r-md);
  cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  transition:background .12s, border-color .12s, transform .1s;
}
.mem-card:not(.flipped):not(.matched) { background:var(--paper-2); color:transparent; }
.mem-card:not(.flipped):not(.matched):hover { border-color:var(--accent-text); transform:translateY(-2px); }
.mem-card.flipped { border-color:var(--accent-text); background:var(--accent-wash); color:var(--accent-text); }
.mem-card.matched { border-color:var(--moss); background:var(--moss-wash); color:var(--moss); cursor:default; }
@media (max-width:520px) { .mem-grid { gap:7px; } }

/* ── Word Groups picker ── */
.word-groups-view { display:flex; flex-direction:column; align-items:center; gap:16px; padding:8px 0 40px; animation:viewIn .4s var(--ease); width:100%; }
.wg-title { font-family:var(--serif); font-size:1.7rem; font-weight:600; color:var(--ink); }
/* --ink-2 not --ink-3: this hint sits on the PAGE ground, which under a scene
   is the illustration itself -- no card, no plate. --ink-3 measured 4.38:1
   there. It's instruction copy at 17px, so secondary is the right weight. */
.wg-picker-hint { font-size:.95rem; color:var(--ink-2); margin:-8px 0 0; text-align:center; }
.wg-group-grid {
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(180px, 1fr));
  gap:14px;
  width:100%; max-width:1100px;
}
.wg-group-card {
  position:relative;
  display:flex; flex-direction:column; align-items:center; gap:8px;
  padding:22px 16px 18px;
  border-radius:var(--r-md);
  border:1px solid var(--line);
  background:var(--paper);
  text-align:center;
  cursor:pointer;
  transition:transform .15s var(--ease), border-color .15s;
}
.wg-group-card:hover { transform:translateY(-3px); border-color:var(--line-2); }
.wg-group-card.cleared { border-color:var(--moss); background:var(--moss-wash); }
.wg-group-card.selected { outline:3px solid var(--accent); outline-offset:2px; }
.wg-group-selected-badge {
  position:absolute; top:8px; right:8px;
  width:22px; height:22px; border-radius:50%;
  background:var(--accent); color:#fff;
  font-size:.8rem; font-weight:700;
  display:flex; align-items:center; justify-content:center;
}
.wg-group-emoji { font-size:2.8rem; line-height:1; }
/* wordgroup_art covers: block display so the tile centres like the emoji did;
   in titles the art rides the text baseline. */
.wg-group-emoji .kq-grpart { display:block; margin:0 auto; }
.wg-title-art, h2 .kq-grpart, h3 .kq-grpart { vertical-align:-.32em; }
.wg-group-name { font-size:1.05rem; font-weight:700; color:var(--ink); }
/* --ink-2 not --ink-3: the "0/8 learned · N5" line is real info at 15px (needs
   AA 4.5:1); --ink-3 (tertiary) measured 2.49–2.92:1 on the card under light
   scenes. --ink-2 (secondary) clears it. QA: npm run legibility. */
.wg-group-sub  { font-size:.85rem; color:var(--ink-2); }
.wg-group-check { font-family:var(--mono); font-size:.75rem; font-weight:700; color:var(--moss); }

/* Completion "practice meter" (#6) — grows every finished round, decoupled
   from mastery. Primary bar fills to full over WG_BOX_FULL rounds; past that
   a second (accent) bar overfills on top in a different color, and a small
   count/medal label rides along so re-earning a full box stays satisfying. */
.wg-group-fill {
  position:relative; display:block;
  width:100%; height:10px; margin-top:2px;
  border-radius:999px;
  background:var(--line);
  overflow:hidden;
}
.wg-group-fill-bar, .wg-group-fill-over {
  position:absolute; top:0; left:0; height:100%;
  border-radius:999px;
  transition:width .4s var(--ease);
}
.wg-group-fill-bar  { background:var(--moss); }
.wg-group-fill-over { background:var(--accent); }
.wg-group-fill.has-lap .wg-group-fill-bar { opacity:.55; }   /* dim the "full" base so the overfill color reads clearly */
.wg-group-fill-label {
  position:absolute; inset:0;
  display:flex; align-items:center; justify-content:center;
  font-family:var(--mono); font-size:.62rem; font-weight:700;
  color:var(--ink); mix-blend-mode:luminosity; opacity:.85;
  pointer-events:none; letter-spacing:.02em;
}
.wg-done-box { font-weight:700; color:var(--moss); }
.wg-done-cooldown {
  margin-top:10px; padding:10px 14px;
  border-radius:var(--r-sm); border:1px solid var(--line-2);
  background:var(--paper-2, var(--paper)); color:var(--ink-2);
  font-size:.9rem; line-height:1.4;
}

.wg-start-bar {
  position:sticky; bottom:12px;
  display:flex; align-items:center; justify-content:space-between; gap:16px;
  width:100%; max-width:1100px;
  padding:14px 20px;
  border-radius:var(--r-md);
  background:var(--accent-wash);
  border:1px solid var(--accent);
  font-weight:600; color:var(--ink);
}

.wg-start-info { display:flex; flex-direction:column; gap:2px; min-width:0; margin-right:auto; }
/* B5 combo-badge hint — a quiet second line that shapes the choice being made. */
.wg-start-hint { font-size:.72rem; font-weight:700; color:var(--ink-3); }
/* B1 "Study the kanji" — outlined secondary beside the filled Start; Start stays
   the path, studying the selection's kanji is the branch. Carries the distinct-
   kanji count (漢字 18), which is the whole argument for the feature. */
.wg-start-kanji {
  padding:12px 18px; border-radius:var(--r-md);
  background:transparent; border:1px solid var(--accent);
  color:var(--accent-text); font-weight:700; white-space:nowrap; cursor:pointer;
  font-family:var(--jp); transition:background .12s;
}
.wg-start-kanji:hover { background:var(--accent-wash); }

/* ── Word Groups: Learn screen (kanji + romaji + meaning preview) ── */
.wg-learn-wrap { display:flex; flex-direction:column; align-items:center; gap:14px; width:100%; max-width:640px; }
.wg-learn-hint { font-size:1rem; color:var(--ink-3); text-align:center; margin:0; }
.wg-learn-list.rd-vlist { width:100%; gap:14px; font-size:1.15rem; }
.wg-learn-list .rd-vw { font-size:1.6rem; }

/* ── Word Groups: the match recap is the literal card grid left as-is (every
   card already flipped/matched) with a Continue button below it — no CSS
   needed for that. Flashcard refresher (tap to flip) that follows it: ── */
.wg-flash-wrap { display:flex; flex-direction:column; align-items:center; gap:14px; width:100%; max-width:640px; }
.wg-flash-hint { font-size:.95rem; color:var(--ink-3); text-align:center; margin:0; }
.wg-flashcard {
  width:100%; min-height:220px;
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:16px;
  padding:28px 20px;
  border-radius:var(--r-md);
  border:2px solid var(--line);
  background:var(--paper-card);
  cursor:pointer;
  transition:border-color .15s, transform .1s;
}
.wg-flashcard:hover { border-color:var(--accent-text); }
.wg-flashcard:active { transform:scale(.98); }
.wg-flashcard.flipped { border-color:var(--moss); background:var(--moss-wash); cursor:default; }
.wg-flash-front { font-family:var(--jp); font-size:2.6rem; color:var(--ink); text-align:center; }
.wg-flash-back { display:flex; flex-direction:column; align-items:center; gap:6px; width:100%; }
.wg-flash-back .voc-prompt-rom { font-family:var(--mono); font-size:1.1rem; font-weight:700; color:var(--accent-text); }
.wg-flash-back .voc-prompt-en  { font-family:var(--serif); font-size:1.3rem; font-weight:600; color:var(--ink); }
.wg-flash-kanji-hint-title {
  margin-top:10px; padding-top:10px; width:100%; max-width:480px;
  border-top:1px solid var(--line);
  font-size:.8rem; font-weight:700; color:var(--accent-text); text-align:center;
}
.wg-flash-kanji-notes {
  list-style:none; margin:6px 0 0; padding:0 14px; width:100%; max-width:480px;
  font-size:.85rem; color:var(--ink-2); line-height:1.55; text-align:left;
}
.wg-flash-kanji-notes li { margin-bottom:8px; }
.wg-flash-kanji-notes strong { font-family:var(--jp); font-size:1.1rem; color:var(--ink); }
.wg-flash-kanji-notes em { color:var(--ink-3); font-style:italic; }

/* ── Word Groups: Match — an OPEN tap-to-pair board (game_wordgroups.js).
   Two columns — Japanese on the left, meanings on the right, every word
   visible from the start: tap a word, then its match. This replaced a
   face-down memory grid whose opening screen of blank grey tiles was the
   "boring / discouraging" first impression. The two sides are tinted as
   distinct families (warm JP / cool EN) so a pair reads as "one from each
   side"; picked, matched and missed each get their own state + motion. ── */
.wg-match-wrap { width:100%; max-width:760px; margin-inline:auto; }
.wg-match-grid { display:flex; gap:14px; align-items:start; width:100%; }
.wg-col { flex:1 1 0; min-width:0; display:flex; flex-direction:column; gap:10px; }

.wg-card {
  width:100%; min-height:60px; padding:12px 14px;
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:3px;
  text-align:center; cursor:pointer; color:var(--ink);
  border:1.5px solid var(--edge, var(--line)); border-radius:var(--rCard, 14px);
  background:var(--card, var(--paper-card));
  box-shadow:0 2px 0 color-mix(in srgb, var(--ink-3) 20%, transparent);
  transition:transform .16s var(--ease-bounce), border-color .14s, background .14s, box-shadow .16s;
  animation:wgCardIn .32s var(--ease) both;
}
/* the two sides read as distinct families — warm Japanese, cool meanings */
.wg-card-jp { background:color-mix(in srgb, var(--accent) 9%, var(--card, var(--paper-card))); }
.wg-card-en { background:color-mix(in srgb, var(--ink-3) 9%, var(--card, var(--paper-card))); }
.wg-card:not([disabled]):hover {
  transform:translateY(-2px);
  border-color:color-mix(in srgb, var(--accent) 60%, var(--edge, var(--line)));
  box-shadow:0 6px 15px -7px color-mix(in srgb, var(--accent) 45%, transparent);
}
.wg-card-word { font-family:var(--jp); font-size:1.5rem; font-weight:600; line-height:1.2; }
.wg-card-en .wg-card-word { font-family:var(--serif); font-size:1.06rem; font-weight:600; line-height:1.3; }
.wg-card-rom { font-family:var(--mono); font-size:.9rem; font-weight:700; color:var(--accent-text); }

/* picked — the card you tapped, waiting for its pair */
.wg-card.sel {
  border-color:var(--accent); background:var(--accent-wash);
  box-shadow:0 0 0 2px var(--accent), 0 7px 18px -8px color-mix(in srgb, var(--accent) 60%, transparent);
  transform:translateY(-1px) scale(1.015);
}
.wg-card.sel .wg-card-word { color:var(--accent-text); }
/* matched — settles green; the board visibly fills with progress */
.wg-card.matched {
  border-color:var(--moss); background:var(--moss-wash); cursor:default; box-shadow:none;
  animation:wgPop .34s var(--ease-bounce);
}
.wg-card.matched .wg-card-word { color:var(--moss); }
.wg-card.matched .wg-card-rom { color:color-mix(in srgb, var(--moss) 85%, transparent); }
/* wrong — a red flash + shake, then it deselects */
.wg-card.wrong {
  border-color:var(--brick); background:var(--brick-wash); animation:wgShake .34s var(--ease);
}
.wg-card.wrong .wg-card-word { color:var(--brick); }

@keyframes wgCardIn { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:none; } }
@keyframes wgPop { 0%{ transform:scale(1); } 45%{ transform:scale(1.06); } 100%{ transform:scale(1); } }
@keyframes wgShake { 0%,100%{ transform:translateX(0); } 20%{ transform:translateX(-5px); }
  40%{ transform:translateX(5px); } 60%{ transform:translateX(-4px); } 80%{ transform:translateX(3px); } }

@media (max-width:520px) {
  .wg-match-grid { gap:9px; }
  .wg-col { gap:8px; }
  .wg-card { min-height:52px; padding:9px 10px; border-radius:12px; }
  .wg-card-word { font-size:1.3rem; }
  .wg-card-en .wg-card-word { font-size:.98rem; }
  .wg-card-rom { font-size:.82rem; }
}
@media (prefers-reduced-motion: reduce) {
  .wg-card { animation:none; }
  .wg-card.matched, .wg-card.wrong { animation:none; }
  .wg-card:not([disabled]):hover, .wg-card.sel { transform:none; }
}

/* ── Word Groups: Translate round (reading/meaning/recall multiple-choice —
   reuses the Vocabulary game's .voc-prompt / .pt-choices / .pt-explain
   classes as-is, own wrapper only for width/centering) ── */
.wg-translate-wrap { display:flex; flex-direction:column; align-items:center; gap:10px; width:100%; max-width:700px; margin:0 auto; }
.wg-translate-wrap .voc-prompt { width:100%; }
.wg-translate-wrap .pt-choices { width:100%; }
.wg-translate-wrap .pt-explain { width:100%; max-width:600px; margin-left:auto; margin-right:auto; }

/* ── Typed-recall round (#1) — production, not multiple choice ── */
.wg-type-wrap { position:relative; display:flex; flex-direction:column; align-items:center; gap:12px; width:100%; max-width:640px; margin:0 auto; }
.wg-type-wrap .voc-prompt { width:100%; text-align:center; }
.wg-type-instr { font-size:.92rem; color:var(--ink-3); text-align:center; margin:0; }
.wg-type-form { display:flex; gap:10px; width:100%; }
.wg-type-input {
  flex:1; min-width:0;
  padding:14px 16px;
  font-family:var(--serif); font-size:1.25rem;
  border-radius:var(--r-sm); border:1.5px solid var(--line-2);
  background:var(--paper); color:var(--ink);
}
.wg-type-input:focus { outline:none; border-color:var(--accent-text); }
.wg-type-input:disabled { opacity:.6; }
.wg-type-wrap .pt-explain { width:100%; max-width:600px; }

/* ── Real Japan Listening: ambient/announcement audio ── */
.real-listening-view { display:flex; flex-direction:column; align-items:center; gap:16px; padding:8px 0 40px; animation:viewIn .4s var(--ease); width:100%; }
.rl-title { font-family:var(--serif); font-size:1.7rem; font-weight:600; color:var(--ink); }
.rl-picker-wrap { display:flex; flex-direction:column; align-items:center; gap:14px; width:100%; }
.rl-cat-grid {
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(200px, 1fr));
  gap:14px;
  width:100%; max-width:900px;
}
.rl-cat-card {
  position:relative;
  display:flex; flex-direction:column; align-items:center; gap:6px;
  padding:22px 16px 18px;
  border-radius:var(--r-md);
  border:1px solid var(--line);
  background:var(--paper);
  text-align:center;
  cursor:pointer;
  transition:transform .15s var(--ease), border-color .15s;
}
.rl-cat-card:hover { transform:translateY(-3px); border-color:var(--line-2); }
.rl-cat-card.rl-cat-done { border-color:var(--moss); background:var(--moss-wash); }
/* "All settings mixed" is the default and the first card, so it spans the row
   rather than hiding among the twelve places at position thirteen. */
.rl-cat-card.rl-cat-all { grid-column:1 / -1; flex-direction:row; justify-content:center; gap:12px; padding:14px 16px; }
.rl-cat-card.rl-cat-all .rl-cat-emoji { font-size:1.4rem; }
.rl-cat-card.rl-cat-all .rl-cat-hint { display:none; }
/* Multi-select: mirror Word Groups' selected state (border+wash, not fill). */
.rl-cat-card.selected { border-color:var(--accent-text); background:var(--accent-wash); }
.rl-cat-card.selected::after {
  content:'✓'; position:absolute; top:8px; right:10px;
  color:var(--accent-ink); background:var(--accent);
  width:20px; height:20px; border-radius:50%;
  display:flex; align-items:center; justify-content:center; font-size:.72rem; font-weight:700;
}
.rl-start-bar {
  position:sticky; bottom:12px; margin-top:14px;
  display:flex; align-items:center; justify-content:space-between; gap:16px;
  width:100%; padding:14px 20px; border-radius:var(--r-md);
  background:var(--accent-wash); border:1px solid var(--accent);
  font-weight:600; color:var(--ink);
}
.rl-start-count { font-size:.9rem; }
.rl-cat-emoji { font-size:2.2rem; }
.rl-cat-label { font-weight:700; color:var(--ink); }
.rl-cat-hint { font-size:.82rem; color:var(--ink-3); }
.rl-cat-progress { font-size:.78rem; color:var(--accent-text); font-weight:600; }

.rl-play-wrap { display:flex; flex-direction:column; align-items:center; gap:16px; width:100%; max-width:640px; margin:0 auto; }
.rl-play-btn { margin:6px 0; }
.rl-transcript {
  display:flex; flex-direction:column; align-items:center; gap:6px;
  width:100%; padding:16px; border-radius:var(--r-md);
  border:1px solid var(--line); background:var(--accent-wash);
  text-align:center;
}
.rl-transcript-jp { font-family:var(--jp); font-size:1.25rem; font-weight:600; color:var(--ink); margin:0; }
.rl-transcript-romaji { font-size:1.05rem; color:var(--ink); margin:0; }
.rl-transcript-en { font-size:1.05rem; color:var(--ink); margin:0; }

/* Transcript language picker — R4a axis 2. Japanese, romaji and English are
   three different exercises, so they are three choices rather than three
   paragraphs printed at once. Free to switch: by the time the transcript is
   open there is nothing left to give away. */
.rl-done-wrap { display:flex; flex-direction:column; align-items:center; gap:16px; }

/* ── Long-form Listening (§J) — extended two-speaker dialogues ── */
/* The Long-form handoff. It was a fourteenth CARD in the picker grid, which
   made it look like a thirteenth setting to toggle while it actually navigated
   away — the third tap behaviour in a row of identical cards. It is a secondary
   action under the start bar now, the shape Reading Room uses for its own
   handoff to Reading Speed. */
.rl-longform-row { margin:12px 0 0; text-align:center; width:100%; }
.rl-longform-row .btn-ghost-sm { display:inline-flex; align-items:center; gap:6px; }
.rl-longform-row .kq-ico { display:block; }

/* The picker on a phone. `minmax(200px, 1fr)` resolves to ONE column at 390px,
   which is what made this entry screen 3,234px tall — twelve full-width cards
   at 2.2rem emoji plus a two-line hint each, and the Start bar four screens
   down. Two columns, and the hint (which repeats what the label already says
   for most settings) is kept for the desktop grid where it costs nothing. */
@media (max-width:720px) {
  .rl-cat-grid { grid-template-columns:1fr 1fr; gap:8px; }
  .rl-cat-card { padding:12px 10px 10px; gap:3px; }
  .rl-cat-emoji { font-size:1.5rem; }
  .rl-cat-label { font-size:.92rem; }
  .rl-cat-hint { display:none; }
  .rl-cat-progress { font-size:.72rem; }
  .rl-cat-card.rl-cat-all { padding:10px 12px; }
  /* The sticky Start bar and the floating Word Lookup share the bottom-right
     corner; without this the FAB sits on top of Start. */
  .rl-start-bar { padding-right:76px; }
}

/* Wider than the other views on purpose: the questions below sit two-up, and
   two columns only pays for itself if each one still has room for its options.
   Measured on the worst real clip (5 questions, an 82-character option): at
   1000px two columns saved little and squeezed options to 218px; at 1240px the
   list drops from 1011px to 733px — a 28% shorter page — with options back at a
   comfortable 278px. */
.lf-entry-wrap { display:flex; flex-direction:column; gap:6px; width:100%; }
.longform-view { display:flex; flex-direction:column; gap:14px; max-width:1240px; margin:0 auto; padding:8px 0 40px; width:100%; animation:viewIn .4s var(--ease); }

.lf-listen-body { display:flex; flex-direction:column; align-items:center; gap:14px; padding:8px 0; }
/* .lf-scene / .lf-lang-row retired: the scene blurb said nothing the screen
   was not already showing, and the question language is a deck setting now,
   chosen on the entry screen rather than flipped mid-clip (R9). */
/* --ink-2, not --accent-text: this line sits on the page ground, which is
   translucent over scene art, and accent-on-art measured 3.99:1 on mahou/light
   — the same same-hue pair R5 retired for the level pill. */
/* …and a PLATE, because even on --ink-2 it measured 4.49:1 — a hundredth under
   AA. It sits on the page ground, which under a scene is translucent over the
   illustration, and a plate is the prescribed fix for text on a busy backdrop
   (the `legibility` skill) rather than pushing the ink darker. Collapses to
   nothing while the note is empty, which is most of the time. */
.lf-replay-note {
  font-family:var(--mono); font-size:.78rem; color:var(--ink-2);
  margin:0; max-width:46ch; text-align:center;
  background:var(--paper-card); border-radius:999px; padding:4px 12px;
}
.lf-replay-note:empty { padding:0; background:none; }
.lf-targets { width:100%; max-width:560px; background:var(--accent-wash); border:1px solid var(--line); border-radius:var(--r-md); padding:14px 18px; }
.lf-targets-head { font-family:var(--mono); font-size:.72rem; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:var(--accent-text); margin:0 0 6px; }
.lf-targets ul { margin:0; padding-left:20px; display:flex; flex-direction:column; gap:5px; }
.lf-targets li { color:var(--ink); font-size:.95rem; line-height:1.4; }
.lf-answer-btn { margin-top:4px; }

/* Subtitles — the shared caption "hint" that follows the audio, off by default.
   Used by Long-form, Real Listening and Eavesdrop (systems/listening_subtitle.js). */
.sub-ctrl-wrap { display:flex; flex-direction:column; align-items:center; gap:5px; }
.sub-ctrl { display:flex; align-items:center; gap:6px; flex-wrap:wrap; justify-content:center; }
/* R4b — locked until one full listen. Reads as locked, not as broken: the
   options go quiet and the reason sits under them, saying why AND when. */
.sub-ctrl.is-locked .sub-opt { color:var(--ink-3); cursor:default; }
.sub-ctrl.is-locked .sub-opt.active { background:transparent; color:var(--ink-3); border-color:var(--line); }
/* R4 — and once it IS live, what the chosen mode costs. --ink-2, never --ink-3:
   this is a consequence the learner has to be able to read. */
.sub-note {
  margin:0; max-width:44ch; text-align:center; line-height:1.4;
  font-size:.74rem; color:var(--ink-2);
  background:var(--paper-card); border-radius:999px; padding:4px 12px;
}
.sub-note:empty { padding:0; background:none; }
.sub-ctrl-label { font-family:var(--mono); font-size:.7rem; font-weight:700; letter-spacing:.05em; text-transform:uppercase; color:var(--ink-2); margin-right:2px; }
.sub-opt { font-family:var(--mono); font-size:.8rem; padding:4px 12px; border-radius:999px; border:1px solid var(--line); background:var(--paper); color:var(--ink-2); cursor:pointer; transition:background .12s var(--ease), color .12s var(--ease); }
.sub-opt:hover { border-color:var(--line-2); }
.sub-opt.active { color:var(--on-ink); background:var(--ink); border-color:transparent; }
/* Caption bar: a dark film-subtitle strip. Fixed min-height so a changing line or
   the gaps between turns never jog the layout under it. */
.sub-caption {
  display:flex; align-items:center; justify-content:center; gap:12px;
  width:100%; max-width:600px; min-height:3.4em; margin:0 auto; padding:12px 20px;
  border-radius:var(--r-md); background:var(--ink); color:var(--on-ink); text-align:center;
}
.sub-who { font-family:var(--jp); font-size:.8rem; font-weight:700; opacity:.65; white-space:nowrap; }
.sub-text { font-family:var(--jp); font-size:1.4rem; font-weight:600; line-height:1.3; }
.sub-idle { opacity:.4; font-size:1.2rem; }
.sub-caption.sub-alt { flex-direction:row-reverse; }   /* second speaker mirrored, like alternating captions */

/* Long-form combined test screen: player on top, questions as a test below. */
.lf-player { display:flex; flex-direction:column; align-items:center; gap:12px; padding:4px 0 14px; border-bottom:1px solid var(--line); }
/* Every question is on screen at once (you answer as you listen), so a single
   column meant scrolling past the whole test to reach the end — 1011px of it on
   a 5-question clip. Two columns instead, in READING order:
       1 2
       3 4
   A grid, not CSS multi-column: multi-column packs tighter but fills the first
   column top-to-bottom before starting the second (1 2 3 / 4 5), which is the
   wrong order for numbered questions you answer as the audio reaches them.
   Row-major order is worth the extra height.
   Cards in a row stretch to a common height (the grid default) so the row reads
   as one band instead of two ragged boxes; .lf-q keeps its own content top-
   aligned inside that shared height. */
.lf-test { display:grid; grid-template-columns:repeat(2, 1fr); gap:14px; margin:12px 0; }
/* One column once two would leave each question too narrow for its options —
   below this the split costs height instead of saving it. */
@media (max-width:900px) { .lf-test { grid-template-columns:1fr; } }
.lf-q { display:flex; flex-direction:column; gap:10px; padding:14px 16px; border:1px solid var(--line); border-radius:var(--r-md); background:var(--paper-card); }
/* Equal-height cards in a row: the options block takes the slack so every card
   in the band ends level, instead of the shorter one floating with a gap under
   a taller neighbour. */
.lf-q > .rd-opts { margin-top:auto; }
.lf-q.lf-q-done { background:var(--paper); }
.lf-q .rd-qprompt { display:flex; align-items:baseline; gap:2px; }
.lf-q-num { display:inline-flex; align-items:center; justify-content:center; flex:none; min-width:1.7em; height:1.7em; margin-right:10px; border-radius:999px; background:var(--ink); color:var(--on-ink); font-family:var(--mono); font-size:.8rem; font-weight:700; }
.lf-finish-btn { align-self:center; margin-top:8px; }

/* ── docs/GAME_UI_LONGFORM.md §4 ──────────────────────────────────────────── */
/* ── The shapes docs/GAME_UI_LONGFORM.md specified for Long-form, named for
   every game that needs them. That spec's closing line is that the same shape
   then does Real Japan and the rest, so these are `game-*`; the `.lf-*` names
   stay as aliases, exactly as the ten head containers did. ──────────────── */
/* Player row — the pill plus the skip link, centred together. */
.game-playrow, .lf-playrow {
  display:flex; align-items:center; justify-content:center;
  gap:14px; flex-wrap:wrap;
}
.game-playrow .kqp, .lf-playrow .kqp { margin:0; }

/* SKIPPED is its own verdict, NOT a flavour of wrong (R8). The model files a
   skip under `assisted` — excluded as practice, never recorded as a miss — so
   red here would be the UI claiming something the data does not.
   This is the easiest of the three to get wrong: reusing .rd-expl.err is one
   character of CSS. */
.rd-expl.skipped { background:var(--paper-2); color:var(--ink-2); }
.lf-q-skipped .lf-q-num { background:var(--ink-3); }

/* Finish, disabled, with the reason beside it. */
.game-actionrow, .lf-finish-row {
  display:flex; align-items:center; justify-content:flex-end; gap:12px;
  padding:12px 15px; border-top:1px solid var(--line);
  /* The spec left this row transparent, and under a scene the page ground is
     translucent over the illustration — the count measured 1.46:1 on
     mahou/light, on the artwork rather than on a surface. It gets the same
     plate every other block on this screen has. */
  background:var(--paper-card); border-radius:var(--r-md);
}
.game-actionnote, .lf-finish-left { font-family:var(--mono); font-size:.74rem; color:var(--ink-2); }

/* Transcript language tabs — R4a. The language is chosen INSIDE the panel,
   Japanese first. Not three buttons in the head, and not all three stacked,
   which is what the reveal used to do: with the English already on screen
   there is no moment where you try to read the Japanese. */
.game-script-tabs, .lf-script-tabs {
  display:inline-flex; border:1px solid var(--line-2);
  border-radius:var(--r-md); overflow:hidden;
  background:var(--paper-card); margin-bottom:10px;
}
.game-script-tabs > button, .lf-script-tabs > button {
  font:inherit; font-size:.88rem; font-weight:600; color:var(--ink-2);
  background:transparent; border:0; padding:6px 13px; cursor:pointer;
}
.game-script-tabs > button + button, .lf-script-tabs > button + button { border-left:1px solid var(--line-2); }
.game-script-tabs > button[aria-selected="true"],
.lf-script-tabs > button[aria-selected="true"] { background:var(--paper-2); color:var(--ink); }
/* 13px floor: .88rem, not .72rem — the 日本語 tab is Japanese, and Japanese
   needs more size than latin at the same point value (`legibility` skill). */

/* The score line, after the last question. */
.game-score, .lf-score {
  display:flex; align-items:baseline; gap:12px; flex-wrap:wrap;
  padding:13px 15px; border-radius:var(--r-md);
  background:var(--paper-card); border:1px solid var(--line); margin-bottom:12px;
}
.game-score b, .lf-score b { font-family:var(--mono); font-size:1.5rem; font-weight:700; color:var(--ink); }
.game-score span, .lf-score span { font-size:.85rem; color:var(--ink-2); }

.lf-quiz-wrap { display:flex; flex-direction:column; gap:12px; }
.lf-gist-badge { align-self:flex-start; font-family:var(--mono); font-size:.72rem; font-weight:700; letter-spacing:.04em; color:var(--on-ink); background:var(--ink); border-radius:999px; padding:3px 12px; margin-bottom:8px; }
.lf-note-row { display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
.lf-note-input {
  flex:1; min-width:180px; font-family:var(--jp); font-size:1.2rem; color:var(--ink);
  padding:10px 14px; border-radius:var(--r-md); border:2px solid var(--line); background:var(--paper);
}
.lf-note-input:focus { outline:none; border-color:var(--accent-text); }

/* The note-taking line. The content authors these prompts WITH the gap in them
   ("Appointment time: ____", "かばんが保管されている駅：____駅"), so the input is
   the gap rather than a box under a row of dead underscores.

   It is a RULED LINE, not a field: no box, no fill, just the underline the
   paper form would have. That is also what lets it sit inside a sentence
   without breaking the line's rhythm — a bordered input mid-sentence reads as
   two separate objects. It sizes to its own content and grows with what is
   typed, capped so a long answer cannot push the suffix off the row. */
.lf-note-cloze { align-items:baseline; }
.lf-note-line {
  flex:1 1 16rem; min-width:0; margin:0;
  font-family:var(--jp); font-size:1.15rem; line-height:2; color:var(--ink);
}
.lf-note-blank {
  flex:none; width:7ch; min-width:0; max-width:14ch;
  margin:0 .25em; padding:0 .2em;
  font-family:var(--jp); font-size:1.15rem; font-weight:600;
  color:var(--ink); text-align:center;
  background:transparent; border:0; border-radius:0;
  border-bottom:2px solid var(--accent-text);
}
.lf-note-blank:focus {
  outline:none; border-bottom-color:var(--ink);
  background:var(--accent-wash);
}
/* The verdict paints the RULE, not a box — same reason as above. */
.lf-note-blank.correct { background:transparent; border-bottom-color:var(--ok); color:var(--ok); }
.lf-note-blank.wrong   { background:transparent; border-bottom-color:var(--err); color:var(--err); }
/* The cloze line IS the question, so its heading is an instruction, not a
   restatement — quieter than a prompt that carries the whole question. */
.lf-q-cloze-head { font-size:.72rem; font-weight:700; text-transform:uppercase; letter-spacing:.09em; color:var(--ink-2); }
.lf-q-cloze-head .lf-q-num { text-transform:none; letter-spacing:0; }
@media (max-width:520px) {
  .lf-note-line { font-size:1.05rem; }
  .lf-note-blank { font-size:1.05rem; width:6ch; }
}
.lf-note-input.correct { border-color:var(--ok); background:color-mix(in srgb, var(--ok) 10%, transparent); }
.lf-note-input.wrong { border-color:var(--err); background:color-mix(in srgb, var(--err) 10%, transparent); }
.lf-next { margin-top:12px; }

.lf-reveal-wrap { display:flex; flex-direction:column; align-items:center; gap:16px; }
.lf-transcript { display:flex; flex-direction:column; gap:8px; width:100%; }
.lf-transcript-head { font-family:var(--mono); font-size:.75rem; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:var(--ink-2); margin:0 0 2px; }
.lf-turn {
  display:grid; grid-template-columns:minmax(52px, auto) 1fr; gap:2px 12px;
  padding:10px 14px; border-radius:var(--r-md); border:1px solid var(--line); background:var(--paper-card);
}
.lf-turn-B { background:var(--accent-wash); }
.lf-turn-who { grid-row:1 / span 3; align-self:start; font-family:var(--jp); font-size:.85rem; font-weight:700; color:var(--accent-text); }
.lf-turn-jp { font-family:var(--jp); font-size:1.1rem; color:var(--ink); }
.lf-turn-romaji { font-size:.85rem; color:var(--accent-text); }
.lf-turn-en { font-size:.85rem; color:var(--ink-2); }

/* ── Reading Speed (docs/PRODUCT_TODO.md §N) ─────────────────────────────────
   A typing test whose text is Japanese. The line is the hero and everything
   else is instrumentation, so the Japanese gets the size and the chrome stays
   quiet. */
.read-speed-view { display:flex; flex-direction:column; gap:14px; max-width:1000px; margin:0 auto;
  padding:8px 0 40px; width:100%; animation:viewIn .4s var(--ease); }
.rs-stage { display:flex; flex-direction:column; align-items:center; gap:14px;
  background:var(--paper-card); border:1px solid var(--line); border-radius:var(--r-lg);
  padding:24px 22px; }
/* The line being read. Japanese needs more size than latin at the same point
   value (`legibility` skill), and this one is the whole exercise. */
.rs-jp { font-family:var(--jp); font-size:1.75rem; line-height:1.75; color:var(--ink);
  text-align:center; margin:0; max-width:34ch; }
/* The reading, revealed only on request — it is the assist, so it reads as a
   secondary line rather than as part of the prompt. */
.rs-reading { font-family:var(--jp); font-size:1.05rem; line-height:1.7; color:var(--ink-2);
  text-align:center; margin:0; max-width:40ch; }
/* The per-word track: done / on / left, which is as fine-grained as the
   comparator can honestly justify. */
.rs-track { display:flex; flex-wrap:wrap; justify-content:center; gap:4px 9px;
  font-family:var(--mono); font-size:.95rem; color:var(--ink-3); max-width:46ch; }
.rs-w-done { color:var(--moss); }
.rs-w-now  { color:var(--ink); background:var(--accent-wash); border-radius:var(--r-sm); padding:0 4px; }
.rs-w-bad  { color:var(--brick); background:var(--brick-wash); border-radius:var(--r-sm); padding:0 4px; }
.rs-input { width:100%; max-width:34rem; font-family:var(--mono); font-size:1.05rem;
  color:var(--ink); background:var(--paper); border:1.5px solid var(--line); border-radius:var(--r-md);
  padding:11px 14px; outline:none; transition:border-color .12s; }
.rs-input:focus { border-color:var(--accent); }
.rs-input-bad { border-color:var(--brick); }
.rs-foot { display:flex; align-items:center; gap:16px; }
.rs-clock { font-family:var(--mono); font-size:.9rem; color:var(--ink-2); font-variant-numeric:tabular-nums; }

/* The payoff — which kanji sat in the lines that slowed you down. */
.rs-kanji { background:var(--paper-card); border:1px solid var(--line); border-radius:var(--r-md);
  padding:16px 18px; display:flex; flex-direction:column; gap:10px; align-items:flex-start; }
.rs-kanji-title { font-size:.95rem; font-weight:700; color:var(--ink); margin:0; }
.rs-kanji-sub { font-size:.8rem; color:var(--ink-2); line-height:1.5; margin:0; max-width:60ch; }
.rs-kanji-grid { display:flex; flex-wrap:wrap; gap:8px; }
.rs-kanji-cell { display:flex; flex-direction:column; align-items:center; gap:2px;
  background:var(--paper-2); border:1px solid var(--line); border-radius:var(--r-sm); padding:7px 11px; }
.rs-kanji-cell b { font-family:var(--jp); font-size:1.5rem; font-weight:600; color:var(--ink); line-height:1.1; }
.rs-kanji-cell span { font-family:var(--mono); font-size:.72rem; color:var(--ink-2); }
/* The two handoffs out of an entry screen are the same object now — a ghost
   button, not an underlined link. An underline in a card of real controls
   reads as a stray anchor. Matches .rl-longform-row. */
.rd-speed-link { margin-top:12px; display:inline-flex; align-items:center; gap:6px; }
.rd-speed-link .kq-ico { display:block; }

/* ── Thread Reader (reading comprehension) ── */
.reading-view { display:flex; flex-direction:column; gap:14px; max-width:1000px; margin:0 auto; padding:8px 0 40px; width:100%; animation:viewIn .4s var(--ease); }
/* .rd-head / .rd-spacer / .rd-level are the shared .game-head family now. */
.rd-title { font-family:var(--serif); font-size:1.3rem; font-weight:600; color:var(--ink); }
.rd-passage {
  font-family:var(--jp); font-size:1.1rem; line-height:2; color:var(--ink);
  background:var(--paper-card); border:1px solid var(--line); border-radius:var(--r-md);
  padding:20px 24px; white-space:normal;
}
/* No max-height / overflow-y here on purpose. The box used to cap at 44vh and
   scroll INSIDE itself, which put a second scrollbar in the middle of the page:
   you scrolled the passage, hit its end, then scrolled the page again to reach
   the questions — and on a short window the visible slice was only a few lines.
   The passage is the thing being read, so it always shows in full and the PAGE
   is the only thing that scrolls. */
/* Furigana in reading passages — never falls below a comfortable minimum,
   even though the passage text itself shrinks on longer entries. */
.rd-passage rt {
  font-family:var(--jp);
  font-size:clamp(12px, .5em, 16px);
  color:var(--ink-2);
  font-weight:600;
}
.rd-genre { font-family:var(--mono); font-size:.72rem; letter-spacing:.06em; text-transform:uppercase;
  color:var(--ink-2); margin:0 0 .35em; }
.rd-ptitle { font-family:var(--serif); font-weight:600; margin-bottom:.5em; }
.rd-sep { display:block; text-align:center; color:var(--ink-3); opacity:.5; margin:.3em 0; }
.rd-empty { color:var(--ink-2); font-size:.9rem; }

/* ── Kanji flashcards ── */
.kanji-view { display:flex; flex-direction:column; gap:16px; max-width:1120px; margin:0 auto; padding:8px 0 40px; width:100%; animation:viewIn .4s var(--ease); }
/* .kj-head / .kj-level / .kj-progress are the shared .game-head family now. */
.kj-title { font-family:var(--serif); font-size:1.3rem; font-weight:600; color:var(--ink); }

.kj-body { display:flex; gap:18px; align-items:flex-start; }
.kj-main { flex:1 1 0; min-width:0; display:flex; flex-direction:column; gap:14px; }
.kj-side {
  flex:0 0 200px; background:var(--paper-card); border:1px solid var(--line); border-radius:var(--r-md); padding:12px 14px;
  position:sticky; top:12px;
}
.kj-side-title { font-size:.8rem; font-weight:700; color:var(--ink-2); margin:0 0 8px; }
.kj-rad-list { display:flex; flex-direction:column; gap:8px; }
.kj-rad-chip { display:flex; align-items:baseline; gap:8px; }
.kj-rad-c { font-family:var(--jp); font-size:1.4rem; font-weight:700; color:var(--accent-text); flex:0 0 auto; }
.kj-rad-m { font-size:.8rem; color:var(--ink-2); line-height:1.3; }
.kj-rad-note {
  margin:12px 0 0; padding-top:10px; border-top:1px dashed var(--line);
  font-family:var(--serif); font-size:.85rem; font-style:italic; color:var(--ink-2); line-height:1.4;
}

.kj-card {
  min-height:220px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px;
  padding:28px; background:var(--paper-card); border:2px solid var(--line); border-radius:var(--r-lg);
  max-width:640px; width:100%; margin:0 auto;
}
.kj-guess, .kj-reveal { max-width:640px; width:100%; margin:0 auto; }
/* …and so does the prompt, which lives INSIDE .kj-main (index.html) for exactly
   this reason: the card is centred in a column with the Parts panel beside it,
   so a prompt left at the page margin sat diagonally away from the glyph it was
   asking about. Same column, same left edge: head → question → glyph reads down
   one line. */
.kj-main > .game-prompt { max-width:640px; width:100%; margin:0 auto 12px; }
.kj-front { font-family:var(--jp); font-size:5.8rem; line-height:1; color:var(--ink); }
.kj-breakdown { display:flex; align-items:center; gap:8px; font-family:var(--jp); }
.kj-bd-part {
  font-size:1.8rem; font-weight:700; color:var(--accent-text); background:var(--accent-wash);
  border-radius:var(--r-sm); padding:2px 10px;
}
.kj-bd-plus { font-size:1.2rem; color:var(--ink-3); }
.kj-guess { display:flex; gap:8px; flex-wrap:wrap; }
.kj-input {
  flex:1 1 160px; min-width:0; font-family:var(--sans); font-size:1rem; padding:11px 14px;
  border:2px solid var(--line); border-radius:var(--r-md); background:var(--paper); color:var(--ink);
}
.kj-input:focus { border-color:var(--accent-text); outline:none; }
.kj-guess .btn-primary, .kj-guess .btn-ghost { flex:0 0 auto; }

.kj-reveal { display:flex; flex-direction:column; gap:12px; }
.kj-result { font-weight:700; font-size:1.05rem; }
.kj-result.st-ok   { color:var(--moss); }
.kj-result.st-err  { color:var(--brick); }
.kj-result.st-warn { color:var(--ink-2); }
.kj-answer { display:flex; flex-direction:column; align-items:center; gap:8px; padding:14px; background:var(--paper); border:1px solid var(--line); border-radius:var(--r-md); }
.kj-row { display:flex; align-items:center; gap:10px; }
.kj-tag { font-family:var(--jp); font-size:.85rem; font-weight:700; color:var(--accent-ink); background:var(--accent); border-radius:var(--r-sm); padding:1px 8px; }
.kj-read { font-family:var(--jp); font-size:1.35rem; color:var(--ink); }
.kj-mean { margin-top:4px; font-family:var(--serif); font-size:1.1rem; color:var(--ink-2); text-align:center; }
.kj-sentence { padding:12px 14px; border:1px solid var(--line); border-radius:var(--r-md); background:var(--paper); }
.kj-sjp { font-family:var(--jp); font-size:1.1rem; color:var(--ink); margin-bottom:4px; }
.kj-shl { color:var(--accent-text); font-weight:700; background:var(--accent-wash); border-radius:3px; padding:0 .15em; }
.kj-sread { font-size:.82rem; color:var(--ink-3); margin-bottom:4px; }
.kj-sen { font-family:var(--serif); font-style:italic; font-size:.9rem; color:var(--ink-2); }
.kj-next { align-self:flex-start; }
.kj-summary { font-size:.95rem; color:var(--ink-2); text-align:center; margin-bottom:4px; }
.kj-finish-btns { display:flex; gap:10px; justify-content:center; }
.kj-finish-btns .btn-primary, .kj-finish-btns .btn-ghost { flex:1; max-width:220px; }

@media (max-width:640px) {
  .kj-body { flex-direction:column; }
  .kj-side { flex:1 1 auto; width:100%; position:static; }
  .kj-front { font-size:4rem; }
}

/* ── Conjugation Rush (arcade) ── */
.rush-view { display:flex; flex-direction:column; gap:12px; max-width:1120px; margin:0 auto; padding:8px 0 40px; width:100%; animation:viewIn .4s var(--ease); }
/* .rush-head is the shared .game-head family now. */
.rush-title { font-family:var(--serif); font-size:1.3rem; font-weight:600; color:var(--ink); }
/* The arcade's own difficulty level — not a JLPT level, so not .game-level, but
   the same --ink-on-wash pairing for the same contrast reason (R5). */
.rush-chip { font-family:var(--mono); font-size:.75rem; font-weight:700; color:var(--ink);
  background:var(--accent-wash); border:1px solid var(--accent-wash-2); border-radius:999px; padding:2px 10px; }
.rush-combo { font-size:.8rem; font-weight:700; color:var(--accent-text); }
.rush-hearts { font-size:1.1rem; color:var(--brick); letter-spacing:2px; }
.rush-scorebox { font-family:var(--mono); font-size:.9rem; color:var(--ink-2); }
.rush-scorebox b { color:var(--ink); font-size:1.05rem; }
.rush-area {
  position:relative; height:min(48vh, 420px); overflow:hidden;
  background:var(--paper-2); border:2px solid var(--line); border-radius:var(--r-lg);
}
.rush-card {
  position:absolute; top:0; display:flex; flex-direction:column; align-items:center; gap:2px;
  min-width:132px; padding:10px 16px; background:var(--paper-card);
  border:2px solid var(--accent); border-radius:var(--r-md);
  /* The fall is a transform, not `top`. `top` is a layout property, so the
     card relayouted 60x a second, and `will-change:top` promoted nothing —
     will-change cannot composite a property the compositor can't animate. */
  will-change:transform;
}
.rush-card.danger { border-color:var(--brick); animation:rushPulse .5s infinite; }
/* `scale`, not `transform:scale()`. The fall animates `transform`, so a
   transform-based pulse would overwrite it and a dangerous card would snap back
   to the top of the area. These are independent properties and compose. */
@keyframes rushPulse { 50% { scale:1.05; } }
.rush-verb { font-family:var(--jp); font-size:1.55rem; font-weight:700; color:var(--ink); }
.rush-en { font-size:.75rem; color:var(--ink-2); }
.rush-form { margin-top:4px; font-size:.74rem; font-weight:700; color:var(--accent-text); background:var(--accent-wash); border-radius:999px; padding:2px 9px; white-space:nowrap; }
.rush-form i { font-style:normal; opacity:.75; font-weight:500; }
.rush-overlay {
  position:absolute; inset:0; z-index:5; display:flex; align-items:center; justify-content:center;
  background:color-mix(in srgb, var(--paper-2) 88%, transparent);
  /* The intro/summary panel can be taller than the fixed-height play area
     (.rush-area is overflow:hidden). Let the overlay scroll so the title and
     the bottom of the intro are never clipped/unreachable. */
  overflow-y:auto; padding:14px 0;
}
.rush-panel { display:flex; flex-direction:column; align-items:center; gap:10px; padding:18px 22px; text-align:center; max-width:440px; margin:auto; }
.rush-big { font-family:var(--serif); font-size:1.45rem; font-weight:600; color:var(--ink); }
.rush-how { font-size:.84rem; color:var(--ink-2); line-height:1.55; }
.rush-stat { font-size:.95rem; color:var(--ink-2); }
.rush-stat b { color:var(--ink); }
.rush-newbest { font-weight:700; color:var(--moss); }
.rush-keys { font-size:.75rem; color:var(--ink-3); }
.rush-toast {
  position:absolute; bottom:10px; left:50%; transform:translateX(-50%); z-index:4;
  background:var(--paper-card); border:1px solid var(--line); border-radius:var(--r-md); padding:7px 14px;
  font-size:.95rem; color:var(--ink-2); white-space:nowrap;
}
.rush-toast b { font-family:var(--jp); color:var(--ink); }
.rush-float {
  position:absolute; z-index:4; font-family:var(--mono); font-weight:700; font-size:1.05rem; color:var(--moss);
  animation:rushFloatUp .9s ease-out forwards; pointer-events:none;
}
@keyframes rushFloatUp { to { transform:translateY(-44px); opacity:0; } }
.rush-inputrow { display:flex; gap:8px; }
.rush-input {
  flex:1 1 160px; min-width:0; font-family:var(--sans); font-size:1rem; padding:11px 14px;
  border:2px solid var(--line); border-radius:var(--r-md); background:var(--paper); color:var(--ink);
}
.rush-input:focus { border-color:var(--accent-text); outline:none; }

.rd-vocab, .rd-kanji { background:var(--paper-card); border:1px solid var(--line); border-radius:var(--r-md); padding:10px 14px; }
.rd-vlist, .rd-klist { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:6px; font-size:.9rem; color:var(--ink-2); }
.rd-vw { font-family:var(--jp); color:var(--ink); font-weight:600; }
.rd-kw { font-family:var(--jp); color:var(--ink); font-weight:700; font-size:1.25rem; margin-right:.15em; }
.rd-vr { color:var(--accent-text); }
.rd-quiz { display:flex; flex-direction:column; gap:10px; }
.rd-qnum { font-size:.78rem; color:var(--ink-2); }
.rd-qprompt { font-size:1.05rem; font-weight:600; color:var(--ink); }
.rd-opts, .ev-opts, .mg-opts { display:grid; grid-template-columns:repeat(2, 1fr); gap:10px; }
/* Eavesdrop and Manga ask about ONE line of Japanese at a time and their
   options are whole utterances, so they stay one per row — the grid, the
   letter and the verdict are shared; the column count is not. */
.ev-opts, .mg-opts { grid-template-columns:1fr; }
/* Every option button in this grid, not just Reading's `.rd-opt`. Real
   Listening and Long-form render the same grid but name their buttons
   `.rl-opt` / `.lf-opt`, so they missed this rule and fell back to bare
   `.choice-btn` — which is built for short kana answers: 1.5rem type capped at
   max-width:220px. A sentence-length English answer then wrapped to five lines,
   giving 220x241px blocks and a 451px wall of options to scroll past. Matching
   on the child button keeps the three games in step and covers the next one. */
.rd-opts > button, .ev-opts > button, .mg-opts > button {
  max-width:none; min-width:0; width:100%;
  font-family:var(--sans); font-size:.92rem; font-weight:600; line-height:1.35;
  text-align:center; white-space:normal;
  padding:13px 14px; min-height:62px;
  display:flex; align-items:center; justify-content:center;
}
.rd-opts-3 > button:nth-child(3) { grid-column:1 / -1; }
@media (max-width:520px) { .rd-opts { grid-template-columns:1fr; } }
/* Japanese, at the size Japanese needs. */
.ev-opts > button, .mg-opts > button { font-family:var(--jp); font-size:1.1rem; }
.mg-opts > button { font-size:1.3rem; }
/* The explanation sat FLUSH against the options grid — measured 0px between the
   last option's border and the wash, so the two read as one bordered block and
   the answer you got wrong appeared to be part of the explanation. It is a
   separate statement about the question, so it gets air above it and a rule in
   its own verdict colour to tie the colour to the meaning rather than to the
   edge of the box above. */
.rd-expl {
  font-size:.9rem; line-height:1.55; padding:12px 14px;
  border-radius:var(--r-md); margin-top:16px;
  border-left:3px solid transparent;
}
.rd-expl.ok  { background:var(--moss-wash);  color:var(--moss); border-left-color:var(--moss); }
.rd-expl.err { background:var(--brick-wash); color:var(--ink);  border-left-color:var(--brick); }
/* Continue / See the transcript / Next clip. These were default .btn-primary,
   which is display:flex — so inside a block they stretched to the full column
   as a hard-cornered slab. They are one tap each and they hug their label. */
.rd-next, .rl-next-btn {
  display:inline-flex; width:auto; flex:0 0 auto; align-self:flex-start;
  padding:12px 24px; border-radius:999px; font-size:.95rem;
  box-shadow:0 4px 14px -8px rgba(0,0,0,.45);
  margin-top:16px;
}
.rd-next kbd, .rl-next-btn kbd {
  font-family:var(--mono); font-size:.68rem; font-weight:700;
  padding:2px 6px; border-radius:999px; margin-left:8px;
  background:rgba(0,0,0,.14); color:var(--grad-warm-ink);
}
.rd-done { font-weight:700; color:var(--moss); }
.rd-src a { color:var(--accent-text); font-size:.85rem; }

/* Listening Lab — big play button instead of text */
.kana-display.mode-listen { padding:10px 0; }
.listen-play {
  font-size:clamp(3rem, 12vw, 5rem);
  line-height:1;
  background:var(--accent-wash);
  border:2px solid var(--line);
  border-radius:50%;
  width:1.7em; height:1.7em;
  cursor:pointer;
  transition:transform .08s, border-color .12s, background .12s;
}
.listen-play:hover  { border-color:var(--accent-text); background:var(--accent-wash-2); }
.listen-play:active { transform:scale(.94); }

/* ── Shared listening transport (systems/audio_player.js) ────────────────────
   One player for every listening surface. Was: the pre-login demo had a real
   transport (button + seek + clock) and every listening GAME had a bare 🔊 with
   no progress and no scrub. Same shape as that demo player, but sized to sit
   inside a game view, themed off the derived ink and accent tokens so it
   follows light/dark and every scene. (No "--ink*" glob in this comment: the
   star-slash would close it early and swallow the rule below.) */
.kqp {
  width:100%; max-width:520px; margin:6px auto;
  display:flex; align-items:center; gap:12px;
  background:var(--paper); border:1.5px solid var(--line-2);
  border-radius:99px; padding:7px 16px 7px 7px;
}
.kqp-play {
  flex-shrink:0; width:44px; height:44px; border-radius:50%;
  border:2px solid var(--accent-text); color:var(--accent-text);
  background:var(--paper-card);
  font-size:1rem; line-height:1; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  transition:transform .15s var(--ease), background .12s, color .12s, border-color .12s;
}
.kqp-play svg { width:18px; height:18px; display:block; }
.kqp-play:hover { transform:scale(1.06); background:var(--accent-wash); }
.kqp-play:active { transform:scale(.96); }
.kqp-seek {
  flex:1; height:8px; border-radius:4px; background:var(--line);
  cursor:pointer; position:relative; overflow:hidden; touch-action:none;
}
/* How much has DOWNLOADED, behind the played fill. Declared first so the fill
   paints over it. */
.kqp-buf {
  position:absolute; top:0; left:0; bottom:0; width:0%;
  border-radius:4px; background:var(--line-2);
}
.kqp-fill {
  position:absolute; top:0; left:0; bottom:0; width:0%;
  border-radius:4px; background:var(--accent-text);
}
.kqp-seek:focus-visible { outline:2px solid var(--accent-text); outline-offset:3px; }
.kqp-time {
  /* --ink-2, not --ink-3: the clock is READ, and --ink-3 is the placeholder
     token (~4.4:1 at this size). */
  flex-shrink:0; font-size:.72rem; color:var(--ink-2);
  font-variant-numeric:tabular-nums; white-space:nowrap;
}
/* Nothing loaded yet, or speech synthesis is talking: there is no duration and
   nothing to scrub, so the bar reads as inert instead of pretending to seek. */
.kqp[data-state="idle"] .kqp-seek,
.kqp[data-state="tts"]  .kqp-seek { opacity:.4; cursor:default; }
.kqp[data-state="tts"] .kqp-time { font-style:italic; }

/* Buffering. A cold clip spends several seconds downloading, and with no state
   for it the transport looked broken — so people pressed play again and
   restarted the fetch. A scan across the track says "working"; the buffered
   bar behind it says how far it has got. */
.kqp[data-state="loading"] .kqp-seek::after {
  content:''; position:absolute; inset:0; border-radius:4px;
  background:linear-gradient(90deg, transparent, var(--accent-wash-2), transparent);
  background-size:42% 100%; background-repeat:no-repeat;
  animation:kqpScan 1.05s linear infinite;
}
@keyframes kqpScan {
  from { background-position:-42% 0; }
  to   { background-position:142% 0; }
}
/* Failure is visible now: a 404, an undecodable codec or a dead network used
   to leave the button sitting there looking fine. */
.kqp[data-state="error"] { border-color:var(--brick); }
.kqp[data-state="error"] .kqp-play { border-color:var(--brick); color:var(--brick); }
.kqp[data-state="error"] .kqp-play:hover { transform:none; background:var(--paper-card); }
.kqp[data-state="error"] .kqp-time { color:var(--brick); }
.kqp[data-state="error"] .kqp-seek { opacity:.35; cursor:default; }
@media (max-width:520px) {
  .kqp { gap:9px; padding:6px 13px 6px 6px; }
  /* Stays 44. It was 40 here — the only control in the section that passed the
     touch-target floor on desktop and failed it on a phone, which is the one
     place it is actually touched. */
  .kqp-play { width:44px; height:44px; }
}
@media (prefers-reduced-motion: reduce) {
  .kqp-play, .kqp-play:hover, .kqp-play:active { transition:none; transform:none; }
  .kqp[data-state="loading"] .kqp-seek::after {
    animation:none; background:var(--accent-wash); background-size:100% 100%;
  }
}

/* Progressive Reveal — masked kana hint */
.reveal-mask {
  font-family:var(--jp);
  font-size:1.5rem; font-weight:700;
  letter-spacing:.14em; color:var(--accent-text);
}

/* Grammar (cloze) mode — a full sentence with a blank to fill */
.kana-display.mode-grammar {
  font-family:var(--jp);
  font-size:clamp(1.6rem, 5.5vw, 2.6rem);
  font-weight:500;
  line-height:1.6;
  letter-spacing:.03em;
  flex-wrap:wrap;
  padding:14px 10px;
}
/* Furigana over the cloze sentence — floors so it stays legible even as
   the sentence itself shrinks for length/viewport. */
.kana-display.mode-grammar rt { font-size:clamp(12px, .42em, 22px); }
/* The empty slot the learner is filling */
.cloze-blank {
  display:inline-block;
  min-width:1.6em;
  margin:0 .12em;
  padding:0 .12em;
  text-align:center;
  color:var(--accent-text);
  border-bottom:3px dashed var(--accent);
  border-radius:2px;
  animation:clozePulse 1.6s ease-in-out infinite;
}
@keyframes clozePulse { 0%,100%{opacity:.5} 50%{opacity:1} }
/* The filled-in answer on the reveal face */
/* A3 transitivity — BASIC generic reveal animation (designer to refine per verb;
   see transitivityViz() note). Plays once on reveal; reduced-motion = end state. */
.tv-viz { display:flex; flex-direction:column; align-items:center; gap:4px; margin-top:12px; }
.tv-viz svg { overflow:visible; }
.tv-obj { fill:var(--accent-wash); stroke:var(--accent); stroke-width:2; transform-box:fill-box; transform-origin:center; }
.tv-arrow { fill:none; stroke:var(--ink-2); stroke-width:2.5; stroke-linecap:round; stroke-linejoin:round; }
.tv-cap { font-size:.72rem; font-weight:700; color:var(--ink-2); font-family:var(--jp); }
/* transitive: the agent arrow drives in, then the object reacts (nudge + tilt). */
.tv-transitive  .tv-arrow { animation:tv-drive .5s var(--ease,ease-out) both; }
.tv-transitive  .tv-obj   { animation:tv-hit .5s var(--ease,ease-out) .35s both; }
/* intransitive: no agent; the object changes on its own (self pulse). */
.tv-intransitive .tv-obj  { animation:tv-selfpulse .7s var(--ease,ease-out) .1s both; }
@keyframes tv-drive     { from { transform:translateX(-14px); opacity:0; } to { transform:translateX(0); opacity:1; } }
@keyframes tv-hit       { 0% { transform:rotate(0); } 55% { transform:rotate(-14deg); } 100% { transform:rotate(0); } }
@keyframes tv-selfpulse { 0% { transform:scale(1); } 45% { transform:scale(1.18) rotate(8deg); } 100% { transform:scale(1) rotate(0); } }
/* ── A3 · per-verb transitivity art (PRODUCT_TODO §A3 designer handoff) ──
   The drawing shows one object in two states; the polarity decides whether a
   HAND causes the change. On the transitive side the object's change is delayed
   until the hand has arrived, so cause visibly precedes effect — that ordering
   is the lesson, not decoration. Palette comes from tokens, so it retints with
   all 22 scenes in both themes. */
.tv-art svg { overflow:visible; }
.tv-art .tv-frame  { fill:none; stroke:var(--ink-3); stroke-width:2; stroke-linejoin:round; }
.tv-art .tv-dim    { fill:none; stroke:var(--ink-3); stroke-width:2; opacity:.55; stroke-linecap:round; }
.tv-art .tv-dim-s  { fill:none; stroke:var(--ink-3); stroke-width:2; opacity:.55; stroke-linecap:round; }
.tv-art .tv-fill   { fill:var(--accent-wash); stroke:var(--accent); stroke-width:2; stroke-linejoin:round; }
.tv-art .tv-fill-s { fill:none; stroke:var(--accent); stroke-width:3; stroke-linecap:round; }
.tv-art .tv-fill-d circle { fill:var(--accent-wash); stroke:var(--accent); stroke-width:2; }
/* the CHANGED state renders in the success colour — the eye should land on
   what moved, not on what stayed put */
.tv-art .tv-mark   { fill:color-mix(in srgb, var(--moss) 26%, transparent); stroke:var(--moss); stroke-width:2; stroke-linejoin:round; }
.tv-art .tv-mark-s { fill:none; stroke:var(--moss); stroke-width:3; stroke-linecap:round; }
.tv-art .tv-mark-d circle { fill:color-mix(in srgb, var(--moss) 26%, transparent); stroke:var(--moss); stroke-width:2; }
.tv-art .tv-ray    { fill:none; stroke:var(--moss); stroke-width:2; stroke-linecap:round; }
.tv-art .tv-agent  { fill:color-mix(in srgb, var(--a1) 30%, transparent); stroke:var(--a1); stroke-width:2; stroke-linejoin:round; }

/* Two states stacked; only one is visible at a time. Both are always in the
   DOM so the no-animation and reduced-motion paths can just pick one. */
.tv-art .tv-s2 { opacity:0; }
/* 自動詞 — no agent, the object changes immediately and on its own. */
.tv-intransitive.tv-art .tv-s1 { animation:tvFadeOut .45s var(--ease,ease-out) .12s both; }
.tv-intransitive.tv-art .tv-s2 { animation:tvFadeIn  .45s var(--ease,ease-out) .12s both; }
/* 他動詞 — the hand drives in first; the change waits for it. */
.tv-transitive.tv-art .tv-hand { animation:tvHandIn .38s var(--ease,ease-out) both; }
.tv-transitive.tv-art .tv-s1   { animation:tvFadeOut .45s var(--ease,ease-out) .42s both; }
.tv-transitive.tv-art .tv-s2   { animation:tvFadeIn  .45s var(--ease,ease-out) .42s both; }
@keyframes tvFadeIn  { from { opacity:0; } to { opacity:1; } }
@keyframes tvFadeOut { from { opacity:1; } to { opacity:0; } }
@keyframes tvHandIn  { from { transform:translateX(-26px); opacity:0; } to { transform:translateX(0); opacity:1; } }
/* per-asset motion, layered on top of the crossfade */
.tv-art .tv-spin { transform-box:fill-box; transform-origin:center; animation:tvSpin 1.1s linear .42s 1 both; }
@keyframes tvSpin { to { transform:rotate(300deg); } }
.tv-art .tv-rise { animation:tvRise 1.1s ease-out .42s 1 both; }
@keyframes tvRise { from { transform:translateY(6px); opacity:0; } 40% { opacity:.9; } to { transform:translateY(-6px); opacity:0; } }
.tv-art .tv-bub  { animation:tvBub .9s ease-out .42s 1 both; }
@keyframes tvBub { from { transform:translateY(6px) scale(.6); opacity:0; } 50% { opacity:1; } to { transform:translateY(-5px) scale(1); opacity:.85; } }
.tv-art .tv-drip { animation:tvDrip .8s ease-in .42s 1 both; }
@keyframes tvDrip { from { transform:translateY(-8px); opacity:0; } 45% { opacity:1; } to { transform:translateY(4px); opacity:.9; } }
@media (prefers-reduced-motion: reduce) {
  .tv-art .tv-s1, .tv-art .tv-s2, .tv-art .tv-hand,
  .tv-art .tv-spin, .tv-art .tv-rise, .tv-art .tv-bub, .tv-art .tv-drip { animation:none; }
  .tv-art .tv-s1 { opacity:0; }        /* land on the RESULT, which is the point */
  .tv-art .tv-s2 { opacity:1; }
}

@media (prefers-reduced-motion: reduce) {
  .tv-transitive .tv-arrow, .tv-transitive .tv-obj, .tv-intransitive .tv-obj { animation:none; }
  .tv-transitive .tv-arrow { opacity:1; }
}

.cloze-fill {
  display:inline-block;
  margin:0 .08em;
  padding:0 .14em;
  color:var(--accent-text);
  font-weight:700;
  background:var(--accent-wash);
  border-radius:3px;
}

/* Conjugation / Transform — base word + instruction chip */
.gr-base  { font-weight:600; }
.gr-arrow { margin:0 .35em; color:var(--ink-2); font-weight:400; }

/* Verb Group ID → conjugation preview shown on reveal */
.vg-conj-preview {
  display:block; margin-top:8px; padding-top:8px;
  border-top:1px dashed var(--line-2); font-size:.82rem; color:var(--ink-2);
}
.vg-conj-preview b { color:var(--ink); }
.vg-cf {
  display:inline-block; margin:2px 8px 2px 0; font-family:var(--mono);
  font-size:.95rem; color:var(--ink); white-space:nowrap;
}
.vg-cf i { color:var(--ink-3); font-style:normal; font-size:.72rem; margin-right:2px; }
.instr-chip {
  display:inline-block;
  font-family:var(--sans);
  font-size:.82rem;
  font-weight:700;
  letter-spacing:.01em;
  color:var(--accent-text);
  background:var(--accent-wash);
  border-radius:999px;
  padding:3px 12px;
}

/* Minimal-Pair Showdown — option buttons */
.choice-grid {
  display:flex;
  flex-wrap:wrap;
  gap:10px;
  justify-content:center;
  width:100%;
}
.choice-btn {
  flex:1 1 0;
  min-width:120px;
  max-width:220px;
  font-family:var(--jp);
  font-size:1.5rem;
  font-weight:600;
  color:var(--ink);
  background:var(--paper);
  border:2px solid var(--line);
  border-radius:var(--r-md);
  padding:16px 12px;
  cursor:pointer;
  transition:border-color .12s, background .12s, transform .08s;
}
.choice-btn:hover:not(:disabled)  { border-color:var(--accent-text); background:var(--accent-wash); }
.choice-btn:active:not(:disabled) { transform:scale(.97); }
.choice-btn:disabled { cursor:default; }
.choice-btn.correct {
  border-color:var(--moss);  color:var(--moss);
  background:var(--moss-wash);
}
.choice-btn.wrong {
  border-color:var(--brick); color:var(--brick);
  background:var(--brick-wash);
}

/* ── Sentence Builder (order) — sized up for comfortable reading, and tiles
   are draggable (wireOrderDrag()) as well as tappable: .order-tile-dragging
   marks the tile currently being dragged, .order-tile-dragover marks
   whichever build-line tile it's currently hovering over as a drop target. ── */
/* Design notes for the two zones:
   • The build line now reads as a WRITING LINE (solid underline, tight tile
     gaps) rather than a big dashed box. You're composing a sentence, so the
     tiles should sit together like words in a line — the old 16px gaps made
     「私」「は」「学生」「です」 read as four separate objects instead of one
     sentence you can proof-read.
   • Build and tray are visually different surfaces. Previously both were
     identical tiles and only the accent colour said which was which, so at a
     glance you couldn't tell composed from available.
   • A caret marks where the next tile lands — the single most common confusion
     in a tap-to-place builder is not knowing where the tap will put it. */
/* The English line IS the task in this game ("build THIS sentence in Japanese"),
   but .sent-tr is a shared caption style — .82rem italic secondary, correct
   where it's a footnote under a card, far too quiet as the prompt. Promote it
   here only, scoped to the sentence view so the other use is untouched. */
#wgSentenceWrap .sent-tr {
  font-size:1.22rem; font-style:normal; font-weight:600; color:var(--ink);
  text-align:center; margin:2px auto 18px; max-width:640px; line-height:1.45;
}
#wgSentenceWrap .sent-tr::before {
  content:'Say this in Japanese';
  display:block; font-size:.62rem; font-weight:700; letter-spacing:.09em;
  text-transform:uppercase; color:var(--ink-3); margin-bottom:6px;
}
.order-area { display:flex; flex-direction:column; gap:18px; width:100%; max-width:880px; margin:0 auto; }
.order-build {
  display:flex; flex-wrap:wrap; gap:6px; align-items:flex-end;
  min-height:96px;
  padding:18px 18px 14px;
  border:1px solid var(--line);
  border-bottom:2px solid var(--accent);
  border-radius:var(--r-md) var(--r-md) 0 0;
  background:color-mix(in srgb, var(--paper-card) 70%, transparent);
  position:relative;
}
/* the insertion caret — a soft blinking bar at the end of the built sentence */
.order-build:not(.is-empty)::after {
  content:''; width:2px; align-self:stretch; margin:6px 0;
  background:var(--accent); opacity:.55; border-radius:2px;
  animation:orderCaret 1.1s steps(1,end) infinite;
}
@keyframes orderCaret { 0%,60%{opacity:.55} 61%,100%{opacity:0} }
@media (prefers-reduced-motion:reduce) { .order-build::after { animation:none; } }
.order-placeholder { color:var(--ink-3); font-size:1rem; opacity:.75; font-style:italic; }
/* the tray is a distinct "parts bin" below the line */
.order-tray {
  display:flex; flex-wrap:wrap; gap:10px; justify-content:center;
  padding:16px 14px;
  background:color-mix(in srgb, var(--paper-2) 55%, transparent);
  border:1px solid var(--line);
  border-radius:var(--r-md);
  min-height:74px;
}
.order-tile {
  font-family:var(--jp);
  font-size:1.75rem;
  line-height:1.2;
  font-weight:600;
  color:var(--ink);
  background:var(--paper-card);
  border:1px solid var(--line-2);
  border-radius:var(--r-md);
  padding:11px 18px;
  cursor:grab;
  touch-action:none;
  box-shadow:0 1px 0 color-mix(in srgb, var(--ink) 12%, transparent);
  transition:border-color .12s, background .12s, transform .1s var(--ease), box-shadow .12s;
}
.order-tile:hover  { border-color:var(--accent-text); background:var(--accent-wash); transform:translateY(-2px); }
.order-tile:active { transform:translateY(0) scale(.97); cursor:grabbing; box-shadow:none; }
/* placed tiles sit ON the line: flatter, tighter, no card shadow — so the built
   sentence reads as continuous text rather than a row of buttons */
.order-tile.placed {
  border-color:transparent; background:transparent; color:var(--ink);
  box-shadow:none; padding:6px 4px; border-radius:var(--r-sm);
}
.order-tile.placed:hover { background:var(--brick-wash); border-color:transparent; color:var(--brick); transform:none; }
/* ── Dragging ──────────────────────────────────────────────────────────────
   touch-action MUST be declared here, not switched on when a drag starts: by
   the time pointerdown fires the browser has already decided whether it owns
   the gesture as a scroll, and it cannot be taken back (w3c/pointerevents#178).
   It is set on the TILES only, so the padding around them still scrolls the
   page on a phone. */
.order-tile { touch-action:none; -webkit-user-select:none; user-select:none; }

/* The tile left behind is the landing slot — it keeps its size so the line
   doesn't collapse, and reads as a space waiting to be filled. */
.order-tile.order-tile-source {
  opacity:.25;
  background:var(--accent-wash);
  border-color:var(--accent-text);
  border-style:dashed;
  box-shadow:none;
  transform:none;
}
/* The thing actually in your hand. Lifted, slightly larger, tilted — it should
   read as picked UP off the line, not sliding along it. */
.order-tile.order-ghost {
  /* NO transition. The ghost is a CLONE of .order-tile, which transitions
     transform over .1s — so the very first transform animated it from the
     origin (a visible jump to the top-left and back), and every move after that
     eased toward the pointer instead of tracking it, which reads as a second
     tile lagging behind your hand. A drag preview must be positioned, never
     animated. */
  transition:none;
  position:fixed; left:0; top:0; margin:0;
  padding:9px 16px; border-radius:var(--r-md); border:2px solid var(--accent-text);
  white-space:nowrap; width:auto; height:auto;
  z-index:80;
  pointer-events:none;
  opacity:.96;
  cursor:grabbing;
  box-shadow:0 12px 28px rgba(0,0,0,.28), 0 2px 6px rgba(0,0,0,.16);
  border-color:var(--accent-text);
  background:var(--paper-card);
  transform-origin:center;
  will-change:transform;
}
/* While a drag is live the build line shows it is a target. */
.order-build.order-dragging { border-bottom-color:var(--accent-text); }

/* FLIP moves tiles with inline transforms; hover lift would fight it. */
.order-build.order-dragging .order-tile:hover { transform:none; }

.order-tile[disabled] { cursor:default; opacity:1; }
/* solved: the finished sentence reads as one clean line */
.order-build.is-solved { border-bottom-color:var(--moss); }
.order-build.is-solved .order-tile.placed { color:var(--moss); }
@media (max-width:520px) {
  .order-area  { gap:14px; }
  .order-tile  { font-size:1.45rem; padding:9px 14px; }
  .order-tile.placed { padding:5px 3px; }
  .order-build { min-height:78px; padding:14px 14px 11px; }
  .order-tray  { gap:8px; padding:12px 10px; }
}

/* ── Draw the Letter ── */
.draw-area { display:flex; flex-direction:column; align-items:center; gap:10px; width:100%; }
/* In-view kana-type selector for the merged Draw category. */
.draw-type-picker { display:flex; gap:6px; flex-wrap:wrap; justify-content:center; }
.draw-type-btn {
  display:inline-flex; align-items:center; gap:6px;
  font-size:.8rem; font-weight:700;
  padding:6px 12px; border-radius:99px;
  color:var(--ink-2);
  background:var(--paper-2);
  border:1px solid var(--line);
  cursor:pointer;
  transition:color .15s, background .15s, border-color .15s;
}
.draw-type-btn:hover { border-color:var(--accent-text); color:var(--ink); }
.draw-type-btn.active { color:var(--on-ink); background:var(--ink); border-color:transparent; }
.draw-type-kana { font-family:var(--jp); font-size:1rem; }
/* Word Practice in-view script + level pickers. */
.wp-picker { display:flex; flex-direction:column; gap:8px; align-items:center; margin-bottom:14px; }
.wp-picker-row { display:flex; gap:4px; flex-wrap:wrap; justify-content:center; margin-bottom:0; }
.wp-picker-row .pt-level-btn.active { color:var(--on-ink); background:var(--ink); border-color:transparent; }
.wp-picker-lbl { align-self:center; font-size:11px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); margin-right:4px; }
.draw-pad {
  width:min(300px, 78vw);
  aspect-ratio:1 / 1;
  border:2px dashed var(--line);
  border-radius:var(--r-lg);
  background:var(--paper);
  touch-action:none;
  cursor:crosshair;
}
.draw-msg { min-height:1.2em; font-size:.85rem; color:var(--ink-2); text-align:center; }
.draw-controls { display:flex; gap:8px; flex-wrap:wrap; justify-content:center; }
.btn-ghost-sm.active { color:var(--accent-text); border-color:var(--accent-text); background:var(--accent-wash); }

/* Focus run — mnemonic hint chip + subtle "focus" marker */
.focus-hint { display:flex; gap:.5rem; align-items:center; justify-content:center; text-align:center;
  margin:.4rem auto; padding:.45rem .7rem; max-width:22rem;
  border:1px dashed var(--line); border-radius:var(--r-md); font-size:.85rem; color:var(--ink-2); }
.focus-hint .fh-emoji { font-size:1.2rem; }
.quiz-card.focus-run .attempt-label::after { content:" · focus"; opacity:.55; }

/* Draw reveal — correct order vs what you drew */
.draw-reveal { display:flex; gap:18px; justify-content:center; margin:4px 0 6px; }
.dr-col { display:flex; flex-direction:column; align-items:center; gap:5px; }
.dr-label { font-size:.74rem; color:var(--ink-2); letter-spacing:.02em; }
.dr-pad {
  width:130px; aspect-ratio:1 / 1;
  border:1px solid var(--line); border-radius:var(--r-md); background:var(--paper);
}
@media (max-width:720px) { .dr-pad { width:38vw; max-width:150px; } }

/* ── Grammar lesson (learn-then-play) ── */
.lesson-view { display:flex; justify-content:center; padding:8px 0 40px; animation:viewIn .4s var(--ease); }
.lesson-card {
  width:100%;
  max-width:860px;
  background:var(--paper-card);
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  padding:26px 28px 22px;
}
.lesson-top { display:flex; gap:16px; align-items:flex-start; margin-bottom:18px; }
.lesson-kana {
  flex:0 0 auto;
  font-family:var(--jp);
  font-size:2.6rem;
  line-height:1;
  color:var(--accent-text);
  background:var(--accent-wash);
  border-radius:var(--r-md);
  padding:12px 16px;
}
.lesson-title { font-family:var(--serif); font-size:1.4rem; font-weight:600; color:var(--ink); letter-spacing:-.01em; }
.lesson-intro { margin-top:6px; font-size:.92rem; line-height:1.55; color:var(--ink-2); }

.lesson-body { display:flex; flex-direction:column; gap:14px; }
.lesson-sec {
  border:1px solid var(--line);
  border-radius:var(--r-md);
  padding:13px 16px;
  background:var(--paper);
}
.lesson-h { font-family:var(--jp); font-size:1.02rem; font-weight:700; color:var(--ink); }
/* A section's numbered rules — one nuance each, with its own examples under it. */
.lesson-rule { margin-top:12px; padding:10px 12px 10px 14px; border-left:3px solid var(--accent-wash-2, var(--line)); background:var(--paper-card); border-radius:0 var(--r-md) var(--r-md) 0; }
.lesson-rule-h { display:flex; align-items:baseline; gap:8px; font-family:var(--jp); font-size:.95rem; font-weight:700; color:var(--ink); }
.lesson-rule-n { flex:none; width:20px; height:20px; border-radius:50%; display:inline-flex; align-items:center; justify-content:center; font-family:var(--mono); font-size:.68rem; font-weight:700; color:var(--accent-text); background:var(--accent-wash); }
.lesson-rule .lesson-b { margin-top:4px; }
.lesson-rule .lesson-ex { border-top-style:dotted; }
.lesson-b { margin-top:4px; font-size:.88rem; line-height:1.5; color:var(--ink-2); }
.lesson-ex {
  display:flex; flex-wrap:wrap; align-items:baseline; gap:6px 12px;
  margin-top:8px; padding-top:8px; border-top:1px dashed var(--line);
}
.lesson-ex-jp { font-family:var(--jp); font-size:1.05rem; color:var(--ink); }
.lesson-ex-en { font-family:var(--serif); font-style:italic; font-size:.85rem; color:var(--ink-2); }
.lesson-hl {
  color:var(--accent-text); font-weight:700;
  background:var(--accent-wash); border-radius:3px; padding:0 .2em;
}
/* third highlight — a JOINING word (が/けど = but, て = and): joins two clauses, marks no role */
.lesson-hl3 { color:var(--ink); font-weight:700; background:var(--gold-wash, color-mix(in srgb, var(--gold, #c9a227) 22%, transparent)); border-radius:3px; padding:0 .2em; }
.lesson-legend .lg-j { color:var(--gold, #a8861a); }
/* secondary highlight — the word a particle marks (vs. the particle itself) */
.lesson-hl2 {
  color:var(--clay); font-weight:700;
  background:var(--clay-wash); border-radius:3px; padding:0 .2em;
}
.lesson-legend { display:flex; gap:14px; flex-wrap:wrap; font-size:.8rem; color:var(--ink-2); margin-top:6px; }
.lesson-legend b { font-weight:700; }
.lesson-legend .lg-p { color:var(--accent-text); }
.lesson-legend .lg-w { color:var(--clay); }

.lesson-foot {
  display:flex; align-items:center; justify-content:space-between; gap:16px;
  margin-top:20px; padding-top:16px; border-top:1px solid var(--line);
}
.lesson-skip {
  display:flex; align-items:center; gap:8px;
  font-size:.85rem; color:var(--ink-2); cursor:pointer; user-select:none;
}
.lesson-skip input { width:16px; height:16px; accent-color:var(--accent-text); cursor:pointer; }
.lesson-foot .btn-primary { flex:0 0 auto; }

/* Already-tried answers (so you don't repeat a wrong guess) */
.tried-list { font-size:.72rem; color:var(--ink-3); min-height:0; display:flex; flex-wrap:wrap; gap:5px; align-items:center; padding-left:2px; }
.tried-list:empty { display:none; }
.tried-label { font-weight:700; text-transform:uppercase; letter-spacing:.06em; font-size:.6rem; }
.tried-chip { padding:1px 7px; border-radius:99px; background:var(--brick-wash); color:var(--brick); font-weight:600; text-decoration:line-through; }

/* Analytics — at-risk streak, tappable game rows, personal bests */
.an-streak.at-risk { color:var(--brick); background:var(--brick-wash); border-color:var(--brick); }
.an-cat.tap { cursor:pointer; transition:border-color .15s, transform .12s var(--ease), background .15s; }
.an-cat.tap:hover { border-color:var(--accent-text); background:var(--paper-2); transform:translateX(2px); }
.an-cat.tap:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.an-cat.tap .an-cat-name::after { content:'›'; margin-left:6px; color:var(--ink-3); font-weight:700; }
.an-bests { display:flex; gap:10px; margin:0 0 16px; }
.an-best { flex:1; background:var(--paper); border:1px solid var(--line); border-radius:var(--r-md); padding:9px 12px; text-align:center; }
.an-best-val { display:block; font-family:var(--mono); font-size:1.15rem; font-weight:700; color:var(--accent-text); font-variant-numeric:tabular-nums; }
.an-best-lbl { display:block; font-size:.62rem; text-transform:uppercase; letter-spacing:.06em; color:var(--ink-3); margin-top:2px; }

/* ═══════════════════════════════════════════════════
   Responsive — mobile
═══════════════════════════════════════════════════ */
@media (max-width:720px) {
  body { overflow:auto; }
  #appShell { height:auto; min-height:100dvh; overflow:auto; }
  .app-body { flex-direction:column; overflow:visible; }
  /* .mob-tabs stays display:none (base rule): the phone nav is the pull-out
     drawer (see the MOBILE section at EOF). The strip is still rendered by
     buildMobTabs() as the drawer's hidden state feed — never shown. The
     old `display:flex` here was removed outright so no cascade accident
     (or stale-cached copy of this file) can ever resurrect the strip. */
  .top-nav { display:none; }
  .main-content { padding:18px 14px 40px; overflow:visible; }
  .card-face { padding:22px 18px 20px; gap:14px; }
  .card-btns { flex-direction:column; }
  .kana-display.mode-letter  { font-size:clamp(4rem,18vw,6rem); }
  .kana-display.mode-word    { font-size:clamp(2rem,10vw,3.5rem); }
  .kana-display.mode-grammar { font-size:clamp(1.3rem,6vw,1.9rem); }
  .lesson-card { padding:20px 16px 18px; border-radius:var(--r-md); }
  .lesson-foot { flex-direction:column; align-items:stretch; }
  .lesson-foot .btn-primary { width:100%; }
  .kp-kana { font-size:3.4rem; }
  .combo-hud .ch-tag { display:none; }
}

@media (prefers-reduced-motion:reduce) {
  *,*::before,*::after { animation-duration:.001ms !important; transition-duration:.001ms !important; }
  /* The per-question speed bar is a FUNCTIONAL countdown, not decoration —
     killing its transition made it snap instantly to empty (looked broken and
     lost the timing cue). Keep this one animating. */
  .speed-fill { transition-duration:revert !important; }
}

/* ── Keyboard focus: a single, consistent, visible focus ring for every
   interactive control. Previously only a couple of inputs had focus styling
   and several actively removed the outline, leaving keyboard-only users unable
   to tell what was focused. :focus-visible shows the ring only for keyboard
   (not mouse) interaction, so it doesn't clutter pointer users. ── */
:focus-visible {
  outline:2px solid var(--accent);
  outline-offset:2px;
  border-radius:4px;
}
/* Elements that suppress the native outline still get the accessible ring. */
input:focus-visible,
.ans-input:focus-visible,
.lookup-input:focus-visible,
.wg-type-input:focus-visible,
.wg-type-input-en:focus-visible,
.kj-input:focus-visible,
.rush-input:focus-visible,
.pt-input:focus-visible,
.cl-input:focus-visible {
  outline:2px solid var(--accent);
  outline-offset:2px;
}

/* ═══════════════════════════════════════════════════
   Profile analytics — mistake profile + trend graph
═══════════════════════════════════════════════════ */
.analytics { margin-bottom:22px; }
.analytics-loading { font-family:var(--serif); font-style:italic; color:var(--ink-3); font-size:.86rem; padding:6px 2px; }
.analytics-empty {
  display:flex; gap:12px; align-items:center;
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-md);
  padding:14px 16px; color:var(--ink-2); font-size:.86rem;
}
.analytics-empty .ae-emoji { font-size:1.6rem; }
.analytics-empty p { margin:0; }

.an-stats { display:flex; gap:10px; margin:4px 0 16px; }
.an-stat {
  flex:1; background:var(--paper); border:1px solid var(--line);
  border-radius:var(--r-md); padding:12px 8px; text-align:center;
}
.an-val { display:block; font-family:var(--mono); font-size:1.5rem; font-weight:700; color:var(--accent-text); }
.an-lbl { display:block; font-size:.7rem; text-transform:uppercase; letter-spacing:.05em; color:var(--ink-3); margin-top:3px; }

.an-chart-wrap {
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-md);
  padding:12px 12px 8px; margin-bottom:18px;
}
.an-chart-legend { display:flex; gap:14px; font-size:.72rem; color:var(--ink-2); margin-bottom:6px; }
.an-chart-legend .lg-acc { color:var(--moss); }
.an-chart-legend .lg-spd { color:var(--clay); }
/* No preserveAspectRatio="none" any more — the chart has labelled axes now, so
   letting it stretch non-uniformly would distort the very slopes it exists to
   show. Uniform scale, dots and axis labels stay legible at any width. */
.an-chart { width:100%; height:auto; display:block; overflow:visible; }
.an-ylab { fill:var(--ink-3); font-size:7px; font-family:var(--mono); }
.an-dot  { fill:var(--moss); stroke:var(--paper); stroke-width:.6; }
/* A day with too few answers to mean anything is shown, but shown as thin —
   hollow rather than solid, so it can't read as a solid data point. */
.an-dot.thin { fill:none; stroke:var(--ink-3); stroke-width:.9; }
.an-chart-note { font-size:.68rem; color:var(--ink-3); line-height:1.4; margin:4px 0 0; }
.an-grid  { stroke:var(--line); stroke-width:1; stroke-dasharray:3 4; }
/* stroke colour comes from the <linearGradient> the SVG defines inline
   (trendChart() in app.js) — not set here, so it isn't overridden. */
.an-line-acc { fill:none; stroke:var(--moss); stroke-width:2; stroke-linejoin:round; stroke-linecap:round; }
.an-line-spd { fill:none; stroke:var(--clay); stroke-width:2; stroke-linejoin:round; stroke-linecap:round; }
/* Dots after the speed path inherit its hue — one rule, no per-dot classes. */
.an-line-spd ~ .an-dot { fill:var(--clay); }
.an-line-spd ~ .an-dot.thin { fill:none; stroke:var(--ink-3); }
.an-chart-x { display:flex; justify-content:space-between; font-size:.68rem; color:var(--ink-3); margin-top:2px; padding-left:28px; }
.an-note { color:var(--ink-3); font-size:.8rem; padding:2px; margin:0 0 14px; }

.an-sub { font-size:.72rem; text-transform:uppercase; letter-spacing:.05em; color:var(--ink-3); margin:14px 0 6px; }

/* "All areas" cross-family overview (#5) */
.an-all-intro { text-transform:none; letter-spacing:normal; font-size:.85rem; color:var(--ink-2); }
.an-areas { display:flex; flex-direction:column; gap:8px; margin:0 0 16px; }
.an-area-row {
  display:flex; align-items:center; gap:12px;
  width:100%; padding:12px 14px;
  border-radius:var(--r-sm); border:1px solid var(--line);
  background:var(--paper); text-align:left; cursor:pointer;
  transition:border-color .15s, transform .12s var(--ease), background .15s;
}
.an-area-row:hover { border-color:var(--accent-text); background:var(--paper-2); transform:translateX(2px); }
.an-area-row:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.an-area-emoji { font-size:1.3rem; line-height:1; }
.an-area-name { font-weight:700; color:var(--ink); flex:1; }
.an-area-name::after { content:'›'; margin-left:6px; color:var(--ink-3); font-weight:700; }
.an-area-stat { font-size:.82rem; color:var(--ink-3); white-space:nowrap; }
.an-area-stat b { color:var(--ink); font-family:var(--mono); }
.an-area-total { color:var(--ink-3); }
@media (max-width:480px) { .an-area-total { display:none; } }
.an-list { list-style:none; margin:0 0 4px; padding:0; display:flex; flex-direction:column; gap:4px; }
.an-row {
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-sm);
  padding:8px 12px; font-size:.88rem; color:var(--ink-2);
}
.an-row b { color:var(--ink); font-weight:600; }
.an-confuse { display:flex; align-items:center; gap:7px; flex-wrap:wrap; }
.an-arr { color:var(--ink-3); font-size:.78rem; }
.an-wrong b, .an-wrong { color:var(--brick); }
.an-confuse .an-wrong { color:var(--brick); }
.an-count { color:var(--ink-3); font-variant-numeric:tabular-nums; font-size:.8rem; }
.an-item { display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; min-width:0; }
.an-rom { color:var(--ink-2); font-size:.82rem; }
.an-mean { color:var(--ink-3); font-size:.8rem; }
.an-acc { font-variant-numeric:tabular-nums; font-weight:600; color:var(--moss); }
.an-acc.low { color:var(--brick); }

/* ── AI study coach ── */
.coach {
  background:color-mix(in srgb, var(--card) 78%, transparent);
  border:1px solid var(--edge2);
  border-radius:var(--rCard); padding:13px 15px; margin:4px 0 18px;
  box-shadow:0 0 calc(20px * var(--gk)) -14px var(--a1);
}
.coach-title { color:var(--gold) !important; }
.pt-coach { margin:10px 0 0; }
.coach-head { display:flex; align-items:center; justify-content:space-between; gap:10px; }
.coach-title { font-size:.86rem; font-weight:600; color:var(--ink); }
.coach-btn {
  border:1px solid var(--line-2); background:var(--paper-card); color:var(--accent-text);
  font:inherit; font-size:.8rem; font-weight:600; padding:5px 12px;
  border-radius:var(--r-sm); cursor:pointer; transition:background .15s, border-color .15s;
}
.coach-btn:hover:not(:disabled) { background:var(--accent-wash); border-color:var(--accent-text); }
.coach-btn:disabled { opacity:.55; cursor:default; }
.coach-body:empty { display:none; }
.coach-body { margin-top:10px; color:var(--ink-2); font-size:.88rem; line-height:1.5; }
.coach-body p { margin:0 0 8px; }
.coach-tips { margin:0 0 8px; padding-left:18px; display:flex; flex-direction:column; gap:6px; }
.coach-tips li { color:var(--ink); }
.coach-loading { font-family:var(--serif); font-style:italic; color:var(--ink-3); }
.coach-meta { color:var(--ink-3); font-size:.72rem; margin:4px 0 0 !important; }

.coach-err { color:var(--brick); font-size:.84rem; margin:0; }

/* ── AI coach: typewriter + history ── */
.coach-typing { white-space:pre-wrap; color:var(--ink); font-size:.88rem; line-height:1.5; }
.coach-typing::after {
  content:'▋'; color:var(--accent-text); margin-left:1px;
  animation:coachCaret 1s steps(1) infinite;
}
@keyframes coachCaret { 50% { opacity:0; } }
.coach-dots::after { content:''; animation:coachDots 1.2s steps(4,end) infinite; }
@keyframes coachDots { 0%{content:'';} 25%{content:'.';} 50%{content:'..';} 75%{content:'...';} }

.coach-history { margin-top:12px; }
.coach-hist-label { font-size:.72rem; text-transform:uppercase; letter-spacing:.05em; color:var(--ink-3); margin:0 0 6px; }
.coach-hist {
  border:1px solid var(--line); border-radius:var(--r-sm);
  background:var(--paper); margin-bottom:6px; padding:0 10px;
}
.coach-hist > summary {
  cursor:pointer; list-style:none; padding:8px 0; color:var(--ink-2);
  font-size:.8rem; display:flex; align-items:center; gap:6px;
}
.coach-hist > summary::before { content:'·'; font-size:1rem; color:var(--accent-text); }
.coach-hist > summary::-webkit-details-marker { display:none; }
.coach-hist[open] > summary { color:var(--ink); border-bottom:1px solid var(--line); }
.coach-hist .coach-tips { margin:8px 0; }

/* ── AI coach: clear-history control ── */
.coach-actions { display:inline-flex; align-items:center; gap:8px; }
.coach-clear {
  border:1px solid var(--line); background:transparent; color:var(--ink-3);
  font:inherit; font-size:.74rem; padding:4px 9px; border-radius:var(--r-sm);
  cursor:pointer; transition:color .15s, border-color .15s;
}
.coach-clear:hover:not(:disabled) { color:var(--brick); border-color:var(--brick); }
.coach-clear:disabled { opacity:.5; cursor:default; }

/* ── Analytics: per-family tabs ── */
.an-tabs {
  display:flex; gap:6px; margin:2px 0 14px; flex-wrap:wrap;
}
.an-tab {
  display:inline-flex; align-items:center; gap:6px;
  border:1px solid var(--line); background:var(--paper); color:var(--ink-2);
  font:inherit; font-size:.82rem; font-weight:600; padding:7px 13px;
  border-radius:999px; cursor:pointer; transition:all .15s;
}
.an-tab:hover { color:var(--ink); border-color:var(--line-2); }
.an-tab.active {
  color:var(--on-ink); border-color:transparent; background:var(--ink);
}
.an-tab-emoji { font-size:.95rem; }

/* ── Analytics: streak, deltas, mastery, per-game breakdown ── */
.an-head { display:flex; align-items:center; justify-content:space-between; gap:10px; }
.an-streak {
  font-size:.78rem; font-weight:700; color:var(--ochre);
  background:var(--ochre-wash); border:1px solid var(--ochre);
  padding:3px 10px; border-radius:999px; white-space:nowrap;
}

.an-deltas { display:flex; gap:10px; margin:0 0 16px; }
.an-delta {
  flex:1; background:var(--paper); border:1px solid var(--line);
  border-radius:var(--r-md); padding:10px 12px; text-align:center;
}
.an-delta-val { display:block; font-family:var(--mono); font-size:1.25rem; font-weight:700; color:var(--ink); }
.an-delta-note { display:block; font-size:.74rem; font-weight:600; margin-top:2px; color:var(--ink-3); }
.an-delta.up   .an-delta-note { color:var(--moss); }
.an-delta.down .an-delta-note { color:var(--brick); }
.an-delta-lbl { display:block; font-size:.66rem; text-transform:uppercase; letter-spacing:.05em; color:var(--ink-3); margin-top:3px; }

.an-mastery { margin:0 0 16px; }
.an-mastery-top { display:flex; align-items:baseline; justify-content:space-between; margin-bottom:6px; }
.an-mastery-num { font-size:.78rem; color:var(--ink-2); font-variant-numeric:tabular-nums; }
.an-bar { height:8px; border-radius:999px; background:var(--paper-2); overflow:hidden; }
.an-bar > span { display:block; height:100%; border-radius:999px; background:var(--grad-warm); transition:width .4s ease; }

.an-cats { list-style:none; margin:0 0 8px; padding:0; display:flex; flex-direction:column; gap:6px; }
.an-cat {
  display:grid; grid-template-columns:1fr 90px auto; align-items:center; gap:10px;
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-sm);
  padding:8px 12px; font-size:.86rem; color:var(--ink-2);
}
.an-cat-name { color:var(--ink); display:flex; align-items:center; gap:7px; min-width:0; }
.an-cat-kana { font-family:var(--jp); font-weight:600; color:var(--accent-text); }
.an-cat-bar { height:6px; border-radius:999px; background:var(--paper-2); overflow:hidden; }
.an-cat-bar > span { display:block; height:100%; background:var(--grad-warm); border-radius:999px; }
.an-cat-meta { font-size:.76rem; color:var(--ink-3); font-variant-numeric:tabular-nums; white-space:nowrap; }

/* ═══════════════════════════════════════════════════
   Topic mastery tab (panels/mastery.js, GET /api/topics) — per-topic × per-skill
   scores, the skill rollup, and the fading-review queue. Reuses .an-cat-bar /
   .an-sub / .an-stat from the analytics block above; only what's genuinely new
   is defined here.
═══════════════════════════════════════════════════ */
.tm-note { text-transform:none; letter-spacing:normal; font-size:.8rem; color:var(--ink-3); line-height:1.45; margin:0 0 8px; }
.tm-note b { color:var(--ink-2); }
.tm-head .an-val { font-size:1.35rem; }
.tm-bar { min-width:60px; }

/* By skill — the modality rollup */
.tm-skills { list-style:none; margin:0 0 16px; padding:0; display:flex; flex-direction:column; gap:6px; }
.tm-skill {
  display:grid; grid-template-columns:1fr 90px auto; align-items:center; gap:10px;
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-sm);
  padding:8px 12px; font-size:.86rem; color:var(--ink-2);
}
.tm-skill-name { color:var(--ink); font-weight:600; display:flex; flex-direction:column; min-width:0; }
.tm-skill-blurb { font-style:normal; font-size:.7rem; font-weight:400; color:var(--ink-3); }
.tm-skill-meta { font-size:.76rem; color:var(--ink-3); font-variant-numeric:tabular-nums; white-space:nowrap; }
.tm-skill-meta b { color:var(--ink); font-family:var(--mono); }

/* What you know — per-kind tiles, doubling as the list's kind filter */
/* Grid, not flex-wrap: with `flex:1 1 88px` a lone tile on the last row
   stretched to the full panel width, which read as a different kind of thing
   than its siblings. auto-fill keeps every tile the same size regardless of
   how the count divides. */
.tm-kinds { display:grid; grid-template-columns:repeat(auto-fill, minmax(92px, 1fr)); gap:8px; margin:0 0 16px; }
.tm-kind {
  display:flex; flex-direction:column; align-items:center; gap:1px;
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-md);
  padding:9px 8px; cursor:pointer; font:inherit; color:var(--ink-2);
  transition:border-color .15s, background .15s, transform .12s var(--ease);
}
.tm-kind:hover { border-color:var(--accent-text); background:var(--paper-2); transform:translateY(-1px); }
.tm-kind:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.tm-kind.on { border-color:var(--accent-text); background:var(--paper-2); }
.tm-kind-emoji { font-size:1.1rem; line-height:1.2; }
.tm-kind-val { font-family:var(--mono); font-size:1.2rem; font-weight:700; color:var(--accent-text); }
.tm-kind-lbl { font-size:.7rem; color:var(--ink); font-weight:600; text-align:center; }
.tm-kind-sub { font-size:.65rem; color:var(--ink-3); font-variant-numeric:tabular-nums; }

/* Fading — the review queue */
.tm-fades { list-style:none; margin:0 0 16px; padding:0; display:flex; flex-direction:column; gap:4px; }
.tm-fade {
  display:flex; align-items:baseline; justify-content:space-between; gap:10px;
  background:var(--paper); border:1px solid var(--line); border-left:3px solid var(--clay);
  border-radius:var(--r-sm); padding:7px 11px; font-size:.86rem;
}
.tm-fade-label { color:var(--ink); font-family:var(--jp); font-weight:600; min-width:0; overflow-wrap:anywhere; }
.tm-fade-meta { font-size:.74rem; color:var(--ink-3); font-variant-numeric:tabular-nums; white-space:nowrap; }

/* Filters */
.tm-filters { display:flex; flex-direction:column; gap:5px; margin:0 0 10px; }
.tm-chiprow { display:flex; flex-wrap:wrap; gap:5px; }
.tm-chip {
  background:transparent; border:1px solid var(--line); border-radius:999px;
  padding:3px 10px; font:inherit; font-size:.74rem; color:var(--ink-3);
  cursor:pointer; transition:color .15s, border-color .15s, background .15s;
}
.tm-chip:hover { color:var(--ink); border-color:var(--line-2); }
.tm-chip:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.tm-chip.on { color:var(--ink); border-color:var(--accent-text); background:var(--paper-2); font-weight:600; }

/* The inventory */
.tm-topics { list-style:none; margin:0 0 8px; padding:0; display:flex; flex-direction:column; gap:4px; }
.tm-topic {
  display:grid; grid-template-columns:auto 1fr 70px auto auto; align-items:center; gap:9px;
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-sm);
  padding:7px 11px; font-size:.86rem; color:var(--ink-2);
}
.tm-topic-tier { font-size:.9rem; line-height:1; }
.tm-topic-label { color:var(--ink); font-family:var(--jp); font-weight:600; min-width:0; overflow-wrap:anywhere; }
.tm-topic-dots { display:flex; gap:3px; }
.tm-dot { width:7px; height:7px; border-radius:50%; background:var(--paper-2); border:1px solid var(--line-2); }
.tm-dot.hi   { background:var(--moss);  border-color:var(--moss); }
.tm-dot.mid  { background:var(--clay);  border-color:var(--clay); }
.tm-dot.lo   { background:var(--brick); border-color:var(--brick); }
.tm-dot.zero { background:var(--paper-2); }
.tm-topic-meta { font-size:.74rem; color:var(--ink-3); font-variant-numeric:tabular-nums; white-space:nowrap; }
/* Tier accent on the left edge — a glanceable spine down the list */
.tm-t-learn { border-left:3px solid var(--line-2); }
.tm-t-fam   { border-left:3px solid var(--clay); }
.tm-t-conf  { border-left:3px solid var(--moss); }
.tm-t-mast  { border-left:3px solid var(--accent-text); }
.tm-t-burn  { border-left:3px solid var(--gold); }

.tm-tap { cursor:pointer; transition:border-color .15s, transform .12s var(--ease), background .15s; }
.tm-tap:hover { border-color:var(--accent-text); background:var(--paper-2); transform:translateX(2px); }
.tm-tap:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.tm-more { display:block; margin:0 auto 16px; }

/* Legend */
.tm-legend { margin:6px 0 4px; }
.tm-legend > summary {
  cursor:pointer; font-size:.74rem; text-transform:uppercase; letter-spacing:.05em;
  color:var(--ink-3); padding:4px 0;
}
.tm-legend > summary:hover { color:var(--ink-2); }
.tm-legs { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:3px; }
.tm-leg { display:flex; align-items:baseline; gap:8px; font-size:.8rem; color:var(--ink-2); }
.tm-leg-emoji { width:1.1em; text-align:center; }
.tm-leg-name { color:var(--ink); font-weight:600; min-width:5.5em; }
.tm-leg-rule { font-size:.74rem; color:var(--ink-3); font-variant-numeric:tabular-nums; }

@media (max-width:520px) {
  .tm-topic { grid-template-columns:auto 1fr auto; row-gap:5px; }
  .tm-topic .tm-bar { display:none; }
  .tm-skill { grid-template-columns:1fr auto; }
  .tm-skill .tm-bar { display:none; }
}
@media (prefers-reduced-motion:reduce) {
  .tm-kind, .tm-tap { transition:none; }
  .tm-kind:hover, .tm-tap:hover { transform:none; }
}

/* ═══════════════════════════════════════════════════
   Activity calendar — which days you practiced, click a day for the detail
═══════════════════════════════════════════════════ */
.activity-section { margin-bottom:22px; }
.act-head { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:10px; }
.act-nav { display:flex; align-items:center; gap:10px; }
.act-nav-btn {
  width:24px; height:24px; display:flex; align-items:center; justify-content:center;
  border-radius:var(--r-sm); border:1px solid var(--line); color:var(--ink-2);
  font-size:.85rem; transition:background .15s, color .15s, border-color .15s;
}
.act-nav-btn:hover { background:var(--paper-2); color:var(--ink); border-color:var(--line-2); }
.act-month { font-family:var(--serif); font-size:.9rem; font-weight:600; color:var(--ink); min-width:11ch; text-align:center; }

.act-weekdays {
  display:grid; grid-template-columns:repeat(7, 1fr); gap:4px; margin-bottom:4px;
  font-size:.62rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em;
  color:var(--ink-3); text-align:center;
}
.act-grid { display:grid; grid-template-columns:repeat(7, 1fr); gap:4px; }
.act-cell {
  aspect-ratio:1 / 1;
  display:flex; align-items:center; justify-content:center;
  font-family:var(--mono); font-size:.78rem; font-weight:600;
  border-radius:var(--r-sm); border:1px solid var(--line);
  /* --ink-2 not --ink-3: these are calendar day-numbers (real content, small
     text → needs AA 4.5:1). --ink-3 (tertiary) measured 3.14:1 on light/terrace
     where the bright scene art shows through --paper. --ink-2 clears it. */
  background:var(--paper); color:var(--ink-2);
  transition:transform .12s var(--ease), border-color .12s;
}
.act-cell-empty { border:none; background:transparent; }
.act-cell:disabled { cursor:default; }
.act-cell.act-active { color:var(--ink); cursor:pointer; border-color:var(--line-2); }
.act-cell.act-active:hover { transform:translateY(-1px); border-color:var(--accent-text); }
.act-cell.act-lvl-1 { background:var(--accent-wash); }
.act-cell.act-lvl-2 { background:var(--accent-wash-2); }
.act-cell.act-lvl-3 { background:color-mix(in srgb, var(--accent) 55%, var(--paper-card)); color:var(--accent-ink); }
.act-cell.act-lvl-4 { background:var(--accent); color:var(--accent-ink); }
.act-cell.act-today { box-shadow:inset 0 0 0 2px var(--clay); }
.act-cell.act-selected { background:var(--grad-warm) !important; color:var(--grad-warm-ink) !important; border-color:transparent; }

.act-detail {
  margin-top:14px; padding-top:14px; border-top:1px dashed var(--line);
}
.act-detail:empty { display:none; }
.act-detail-head {
  display:flex; align-items:baseline; justify-content:space-between; gap:10px; flex-wrap:wrap;
  margin-bottom:10px;
}
.act-detail-date { font-family:var(--serif); font-size:1rem; font-weight:600; color:var(--ink); }
.act-detail-sum { font-size:.78rem; color:var(--ink-2); }
.act-sessions { display:flex; flex-direction:column; gap:8px; }
.act-sess {
  display:grid; grid-template-columns:auto auto 1fr; align-items:center; gap:10px;
  padding:8px 12px; background:var(--paper); border:1px solid var(--line); border-radius:var(--r-sm);
}
.act-sess-time { font-family:var(--mono); font-size:.8rem; color:var(--ink); white-space:nowrap; }
.act-sess-dur {
  font-family:var(--mono); font-size:.72rem; font-weight:700; color:var(--clay);
  background:var(--clay-wash); border-radius:999px; padding:2px 9px; white-space:nowrap;
}
.act-sess-cats { display:flex; flex-wrap:wrap; gap:6px; justify-content:flex-end; }
.act-sess-cat {
  font-size:.72rem; color:var(--ink-2); background:var(--paper-2); border-radius:999px;
  padding:2px 9px; white-space:nowrap;
}
.act-sess-cat b { color:var(--ink); font-weight:700; }
.act-sess-cat i { font-style:normal; color:var(--moss); }

@media (max-width:480px) {
  .act-sess { grid-template-columns:1fr; }
  .act-sess-cats { justify-content:flex-start; }
}

/* ═══════════════════════════════════════════════════
   Particle Dojo
═══════════════════════════════════════════════════ */

.particles-view { max-width:1120px; margin:0 auto; padding:20px 16px; animation:viewIn .4s var(--ease); }

/* Header */
/* .pt-head is the shared .game-head family now. */
.pt-title {
  font-family:var(--serif);
  font-size:1.7rem; font-weight:600; letter-spacing:-.01em; margin:0;
  color:var(--ink);
}
.pt-toggles { display:flex; gap:8px; }
/* Inactive uses --ink (primary text), not the inherited accent-on-accent-wash of
   .btn-ghost-sm — that pair is same-hue/low-separation and under scenes
   (--accent=--a1, --accent-wash=a1-mix-on-card) drops below AA 4.5:1 for this
   small (.78rem) label. QA: npm run legibility. */
.pt-furigana-btn, .pt-hardcore-btn { font-size:.78rem; border-radius:99px; color:var(--ink); }
.pt-furigana-btn.active, .pt-hardcore-btn.active { background:var(--ink); color:var(--on-ink); border-color:transparent; font-weight:700; }

/* Game mode tabs */
.pt-modes {
  display:flex; gap:4px; margin-bottom:12px; flex-wrap:wrap;
  width:fit-content; padding:4px; background:var(--paper-2); border:1px solid var(--line); border-radius:var(--r-md);
}
.pt-mode-btn {
  padding:7px 15px; border-radius:var(--r-sm); background:transparent; color:var(--ink-2);
  border:1px solid transparent; font-size:.82rem; font-weight:600; cursor:pointer;
  transition: all .18s var(--ease);
}
.pt-mode-btn:hover { background:var(--paper-card); color:var(--ink); }
.pt-mode-btn.active { background:var(--grad-warm); color:var(--grad-warm-ink); }

/* Level select */
.pt-level-select {
  display:flex; gap:4px; margin-bottom:6px; flex-wrap:wrap;
  width:fit-content; padding:4px; background:var(--paper-2); border:1px solid var(--line); border-radius:var(--r-md);
}
.pt-level-btn {
  padding:6px 17px; border-radius:var(--r-sm); background:transparent; color:var(--ink-2);
  border:1px solid transparent; font-size:.82rem; font-weight:700; cursor:pointer;
  transition: all .18s var(--ease);
}
.pt-level-btn:hover { background:var(--paper-card); color:var(--ink); }
.pt-level-btn.active { background:var(--grad-warm); color:var(--grad-warm-ink); }
/* The cumulative "≤ & below" chip is visually set apart from the level toggles
   (it's a mode, not a level) with a leading divider. */
.pt-level-cumulative { margin-left:6px; border-left:1px solid var(--line); border-radius:var(--r-sm); }
.pt-level-count { margin:0 0 16px; font-size:.75rem; font-weight:700; color:var(--ink-2); }

/* HUD: streak, timer, score */
.pt-hud {
  display:flex; align-items:center; gap:18px; margin-bottom:16px; font-weight:700; font-size:.95rem;
  padding:10px 16px; background:var(--paper-2); border:1px solid var(--line); border-radius:99px;
}
.pt-streak { font-family:var(--mono); color:var(--ochre); }
.pt-timer  { font-family:var(--mono); color:var(--clay); font-variant-numeric:tabular-nums; }
.pt-timer-low { color:var(--brick); animation: pt-pulse .5s ease infinite alternate; }
/* Light theme: --ochre/--clay are decorative accents tuned for the dark ground
   and drop below WCAG AA (large-text 3:1) on the light --paper-2 pill
   (ochre ~1.9:1, clay ~2.6:1). Use darker on-brand text colours on light only;
   dark theme keeps the vivid tokens (already ~8:1). QA: npm run legibility. */
:root[data-theme="light"] .pt-streak { color:#8a5e00; }   /* ~4.6:1 on #e7ddc4 */
/* …but only on the DEFAULT light ground. Under a scene the pill is that
   scene's own --paper-2, so a hard-coded ochre drifts (4.35:1 on light
   shinjuku). Pull the accent toward the body ink instead: still warm, AA on
   whatever the scene makes the pill. */
:root[data-theme="light"][data-scene] .pt-streak {
  color:color-mix(in srgb, var(--ochre) 30%, var(--ink));
}
:root[data-theme="light"] .pt-timer  { color:#b8461c; }   /* ~3.4:1 on #e7ddc4 */
.pt-score  {
  font-family:var(--mono);
  color:var(--accent-text); margin-left:auto; font-weight:700;
}

@keyframes pt-pulse { from { opacity:1; } to { opacity:.5; } }
.pt-streak-pulse { animation: pt-streak-pop .5s ease; }
@keyframes pt-streak-pop { 0% { transform:scale(1); } 40% { transform:scale(1.4); } 100% { transform:scale(1); } }

/* Question card — now the primary focus, so it takes more of the column */
.pt-card {
  background:var(--paper-card);
  border:1px solid var(--line);
  border-radius:var(--r-lg); padding:44px 40px;
  position:relative; overflow:hidden;
  animation:viewIn .35s var(--ease);
}
.pt-card::before {
  content:''; position:absolute; top:0; left:0; right:0; height:3px;
  background:var(--accent);
}
.pt-sentence {
  font-family:var(--jp); font-size:2rem; line-height:1.85; color:var(--ink);
  text-align:center; margin-bottom:14px; word-break:keep-all; max-width:720px; margin-left:auto; margin-right:auto;
}
.pt-sentence ruby { font-size:2rem; }
/* Furigana floors at a comfortable minimum regardless of how long the
   sentence runs or how much the viewport shrinks it. */
.pt-sentence rt { font-family:var(--jp); font-size:clamp(15px, .65em, 22px); color:var(--ink-2); font-weight:600; }
.pt-blank {
  display:inline-block; min-width:2.2em; padding:2px 6px; margin:0 2px;
  border-bottom:3px solid var(--accent); color:var(--accent-text); font-weight:700;
  text-align:center; transition: all .25s var(--ease);
}
.pt-blank.pt-correct { border-color:var(--moss); color:var(--moss); background:var(--moss-wash); }
.pt-blank.pt-wrong-reveal { border-color:var(--brick); color:var(--moss); background:var(--brick-wash); }
.pt-translation { text-align:center; color:var(--ink-3); font-family:var(--serif); font-style:italic; font-size:.95rem; margin-bottom:24px; }

/* Multiple choice buttons */
.pt-choices { display:grid; grid-template-columns:1fr 1fr; gap:14px; max-width:600px; margin:0 auto; }
/* ── JLPT mock exam ────────────────────────────────────────────────────────
   The card carried a second copy of the head's clock at 28px in amber, which
   made the loudest thing on a timed exam the timer rather than the question.
   This row replaces it: where you are in the section, and where the section
   sits in the paper. */
.exam-progress {
  display:flex; align-items:center; gap:14px; margin-bottom:18px;
  font-size:.82rem; color:var(--ink-2);
}
.exam-prog-now { flex:0 0 auto; }
.exam-prog-now b { color:var(--ink); font-variant-numeric:tabular-nums; }
.exam-prog-track {
  flex:1 1 auto; height:4px; border-radius:99px;
  background:var(--paper-2); overflow:hidden; min-width:60px;
}
.exam-prog-track i { display:block; height:100%; background:var(--accent); transition:width .3s var(--ease); }
.exam-prog-secs { display:flex; gap:5px; flex:0 0 auto; }
.exam-secdot {
  width:7px; height:7px; border-radius:50%;
  background:var(--line-2); display:block;
}
.exam-secdot.done { background:var(--moss); }
.exam-secdot.now  { background:var(--accent); box-shadow:0 0 0 3px var(--accent-wash); }

/* The clock, now that it lives only in the head. */
#examView .game-count.exam-timer-low { color:var(--verm, #ff6f5e); font-weight:700; }

/* A/B/C/D, the same key the rest of the app puts on an option. */
.exam-key {
  font-family:var(--mono); font-size:.7rem; font-weight:700;
  color:var(--ink-3); border:1px solid var(--line-2); border-radius:4px;
  padding:1px 5px; margin-right:8px; flex:0 0 auto;
}
/* The answer you gave. It used to be merely disabled — identical to the three
   you did not pick, so the card forgot your answer the moment you gave it. */
.pt-choice-chosen {
  border-color:var(--accent) !important;
  background:var(--accent-wash) !important;
}
.pt-choice-chosen .exam-key { border-color:var(--accent); color:var(--accent-text); }

/* Recorded, not marked — the paper is graded at the end, which is what both
   the entry screen and the prompt promise. */
.exam-logged {
  display:flex; align-items:center; gap:8px;
  font-size:.86rem; font-weight:600; color:var(--ink-2); margin-bottom:12px;
}
/* .pt-explain is a brick-washed alert — correct for "here is why that was
   wrong", wrong for a receipt. During the paper nothing is being corrected,
   so the panel drops the wash and the vermilion rule and becomes a quiet row. */
#examExplain.exam-explain-quiet {
  background:transparent; border-left:0; padding:14px 0 0;
  border-top:1px solid var(--line); margin-top:20px;
  display:flex; align-items:center; gap:16px; flex-wrap:wrap;
}
#examExplain.exam-explain-quiet .exam-logged { margin-bottom:0; }
#examExplain.exam-explain-quiet .pt-next-btn { margin:0; }

/* Results: the headline figure at the size of the thing it is. */
.exam-verdict {
  display:flex; align-items:center; gap:18px; flex-wrap:wrap;
  padding:18px 20px; margin:4px 0 18px;
  background:var(--paper-card); border:1px solid var(--line);
  border-left:4px solid var(--clay); border-radius:var(--r-lg);
}
.exam-verdict.pass { border-left-color:var(--moss); }
.exam-verdict-pct {
  font-family:var(--mono); font-size:2.6rem; font-weight:700; line-height:1;
  color:var(--clay); font-variant-numeric:tabular-nums;
}
.exam-verdict.pass .exam-verdict-pct { color:var(--moss); }
.exam-verdict-pct i { font-size:1.2rem; font-style:normal; opacity:.7; margin-left:2px; }
.exam-verdict-txt { display:flex; flex-direction:column; gap:3px; }
.exam-verdict-txt b { font-size:1rem; color:var(--ink); }
.exam-verdict-txt span { font-size:.82rem; color:var(--ink-2); }

/* Each section against the mark it had to clear. */
.exam-seclist { list-style:none; padding:0; margin:0 0 20px; }
.exam-secrow {
  display:grid; grid-template-columns:minmax(120px,1.1fr) minmax(80px,2fr) auto;
  align-items:center; gap:14px;
  padding:11px 14px; border-radius:var(--r-md);
  background:var(--paper-card); border:1px solid var(--line); margin-bottom:6px;
}
.exam-secrow-name { display:flex; align-items:center; gap:8px; font-size:.92rem; color:var(--ink); }
.exam-secbar {
  position:relative; height:7px; border-radius:99px;
  background:var(--paper-2); overflow:visible;
}
.exam-secbar i {
  display:block; height:100%; border-radius:99px;
  background:var(--clay); transition:width .4s var(--ease);
}
.exam-secrow.ok .exam-secbar i { background:var(--moss); }
/* The pass line itself, drawn on the bar rather than described beside it. */
.exam-secline {
  position:absolute; top:-4px; bottom:-4px; width:2px;
  background:var(--ink-2); border-radius:1px; opacity:.75;
}
.exam-secrow-meta {
  font-family:var(--mono); font-size:.8rem; color:var(--ink-3);
  font-variant-numeric:tabular-nums; white-space:nowrap;
}
.exam-secrow-meta b { color:var(--ink); }
.exam-caveat { font-size:.8rem; color:var(--ink-3); line-height:1.55; margin:0 0 18px; max-width:70ch; }

@media (max-width:560px) {
  .exam-secrow { grid-template-columns:1fr auto; }
  .exam-secbar { grid-column:1 / -1; }
  .exam-progress { flex-wrap:wrap; gap:8px 12px; }
}

/* ── The exam's own type scale ───────────────────────────────────────────────
   The rubric ("＿＿＿の ことばは どう 読みますか。1・2・3・4から いちばん いい ものを
   一つ えらんで ください。") is the single most important line on an exam card
   after the question itself — it is what tells you what the mondai wants — and
   it was set at .82rem in --ink-2 in the UI font: smaller than the app's body
   text, in a face not designed for kana, on the one screen aimed at people who
   read Japanese slowly. It is Japanese CONTENT, so it gets the Japanese face,
   content size and a plate that separates it from the chrome above it. */

/* ── The three buttons that carry the whole flow ─────────────────────────────
   "Start the exam", "Start this section", "Next question", "Mark my paper".
   Each one is the only thing to press on its screen, and each was a default
   .875rem button — the same weight as a Guide link. A primary action that is
   the entire point of the screen is allowed to look like one. */
.exam-cta {
  display:inline-flex; width:auto; flex:0 0 auto;
  padding:15px 30px; font-size:1.06rem; letter-spacing:-.01em;
  border-radius:var(--r-md);
  box-shadow:0 6px 18px -10px rgba(0,0,0,.5);
}
.exam-cta kbd {
  font-family:var(--mono); font-size:.68rem; font-weight:700;
  padding:2px 6px; border-radius:4px; margin-left:2px;
  background:rgba(0,0,0,.14); color:var(--grad-warm-ink);
}
#examPicker .exam-cta, #examSectionIntro .exam-cta { margin:18px auto 0; }
/* .btn-primary.pt-start-big sets display:block (it is the big entry button on
   several games), which would stretch this one edge to edge. The exam's entry
   button hugs its label like the other two CTAs. */
#examStartBtn { display:inline-flex; width:auto; font-size:1.06rem; }
#examPicker { text-align:center; }
#examPicker .game-entry-row { align-items:center; }
#examSectionDone .exam-cta, .exam-results-actions .exam-cta { margin:4px auto 0; }
.exam-results-actions { display:flex; justify-content:center; margin-top:22px; }
/* The entry screen's blurb is the pitch for a 57-minute commitment. */
#examPicker .pt-start-how { font-size:1rem; max-width:62ch; margin-left:auto; margin-right:auto; }

/* ── Section intro: the facts, then the clock ───────────────────────────────*/
.exam-intro-facts { display:flex; gap:12px; flex-wrap:wrap; justify-content:center; margin:2px 0 14px; }
.exam-intro-fact {
  display:flex; flex-direction:column; gap:2px; align-items:center;
  min-width:9.5rem; padding:12px 16px;
  background:var(--paper-2); border:1px solid var(--line); border-radius:var(--r-md);
  font-size:.82rem; color:var(--ink-2);
}
.exam-intro-fact b { font-family:var(--mono); font-size:1.5rem; font-weight:700; color:var(--ink); line-height:1.1; }
.exam-intro-fact i { font-style:normal; font-size:.76rem; color:var(--ink-3); text-align:center; }
.exam-intro-note { display:block; font-size:.94rem; line-height:1.6; color:var(--ink-2); margin-top:8px; }
.exam-intro-note b { color:var(--ink); }

/* ── Between sections: what you got through, not what you scored ────────────*/
.exam-secdone-facts { display:flex; gap:10px; flex-wrap:wrap; justify-content:center; margin:6px 0 14px; }
.exam-fact {
  display:flex; flex-direction:column; align-items:center; gap:1px;
  min-width:8rem; padding:11px 14px;
  background:var(--paper-2); border:1px solid var(--line); border-radius:var(--r-md);
}
.exam-fact b { font-family:var(--mono); font-size:1.35rem; font-weight:700; color:var(--ink); }
.exam-fact i { font-style:normal; font-size:.75rem; color:var(--ink-2); }
.exam-fact.warn { border-color:var(--clay); }
.exam-fact.warn b { color:var(--clay); }

/* ── Listening: the shared transport, and the one-listen rule said out loud ─*/
.exam-listen {
  display:flex; flex-direction:column; align-items:center; gap:6px;
  margin:14px 0 18px;
}
.exam-listen .kqp { margin:0 auto; }
.kqp.kqp-exam { max-width:440px; }
.exam-listen-note { font-size:.8rem; color:var(--ink-2); text-align:center; margin:0; }
.exam-listen-where {
  display:flex; align-items:center; gap:8px; justify-content:center;
  font-family:var(--jp); font-size:1.02rem; color:var(--ink-2);
  background:var(--paper-2); border-radius:var(--r-sm);
  padding:8px 14px; margin:0 auto; width:max-content; max-width:100%;
}
/* The listen is spent: the control retires rather than sitting there inert. */
.kqp-play:disabled { opacity:.45; cursor:default; transform:none !important; }
.kqp-play:disabled:hover { background:var(--paper-card); }
.exam-listen-play { display:inline-flex; width:auto; }

/* ── The receipt: recorded, and visibly NOT a verdict ───────────────────────
   A green tick here was read — correctly — as "correct", on every question. */
.exam-rec-dot {
  width:8px; height:8px; border-radius:50%; flex:0 0 auto;
  background:var(--ink-3); box-shadow:0 0 0 3px var(--line);
}
.exam-next-btn { display:inline-flex; width:auto; padding:12px 22px; font-size:.95rem; }

/* ── Results ────────────────────────────────────────────────────────────────*/
.exam-results-sub { font-size:.86rem; color:var(--ink-2); margin:-4px 0 14px; }
.exam-verdict { align-items:stretch; }
.exam-verdict-pts {
  display:flex; flex-direction:column; gap:6px; margin-left:auto;
  padding-left:18px; border-left:1px solid var(--line);
}
.exam-verdict-pts i {
  font-style:normal; font-size:.78rem; color:var(--ink-2); white-space:nowrap;
}
.exam-verdict-pts b { font-family:var(--mono); font-size:1.05rem; color:var(--ink); }
.exam-verdict-pts em { font-style:normal; color:var(--ink-3); margin-left:4px; }
.exam-secrow-name { flex-direction:column; align-items:flex-start; gap:2px; }
.exam-secrow-sub { font-style:normal; font-size:.74rem; color:var(--ink-3); font-family:var(--mono); }
.exam-readout {
  font-size:.9rem; line-height:1.6; color:var(--ink-2);
  display:flex; gap:9px; align-items:flex-start;
  padding:12px 15px; margin:0 0 18px;
  background:var(--accent-wash); border-radius:var(--r-md);
}
.exam-readout b { color:var(--ink); }

/* Per-mondai, which is the level at which a JLPT result says what to study. */
.exam-mondai-grid {
  list-style:none; padding:0; margin:0 0 20px;
  display:grid; grid-template-columns:repeat(auto-fill, minmax(190px, 1fr)); gap:8px;
}
.exam-mondai-cell {
  display:grid; grid-template-columns:1fr auto; gap:2px 10px; align-items:baseline;
  padding:10px 13px; border-radius:var(--r-md);
  background:var(--paper-card); border:1px solid var(--line);
  border-left:3px solid var(--line-2);
}
.exam-mondai-cell.strong { border-left-color:var(--moss); }
.exam-mondai-cell.mid    { border-left-color:var(--ochre, var(--clay)); }
.exam-mondai-cell.weak   { border-left-color:var(--brick); }
.exam-mondai-cell-t { font-family:var(--jp); font-size:.9rem; color:var(--ink); }
.exam-mondai-cell-n { font-family:var(--mono); font-size:.95rem; font-weight:700; color:var(--ink); }
.exam-mondai-cell-s { grid-column:1 / -1; font-size:.7rem; color:var(--ink-3); text-transform:uppercase; letter-spacing:.05em; }

/* The paper, marked — every question, your answer, the right one, and why. */
.exam-review-head {
  display:flex; align-items:center; gap:14px; flex-wrap:wrap;
  margin:0 0 10px;
}
.exam-review-head .an-sub { margin:0; }
.exam-review-filter { display:flex; gap:6px; margin-left:auto; }
.exam-review-filter .active { border-color:var(--accent-text); color:var(--accent-text); background:var(--accent-wash); }
.exam-ansrows {
  list-style:none; padding:0; margin:0;
  display:flex; flex-direction:column; gap:8px;
  max-height:60vh; overflow-y:auto;
}
.exam-ansrows.exam-filter-wrong .exam-ansrow[data-ok="1"] { display:none; }
.exam-ansrow {
  padding:12px 15px; border-radius:var(--r-md);
  background:var(--paper-card); border:1px solid var(--line);
  border-left:3px solid var(--brick);
}
.exam-ansrow.ok { border-left-color:var(--moss); }
.exam-ansrow-tag {
  display:block; font-size:.7rem; font-weight:700; color:var(--ink-3);
  text-transform:uppercase; letter-spacing:.04em; margin-bottom:5px;
}
.exam-ansrow-tag .exam-sec-ja { text-transform:none; letter-spacing:0; }
.exam-ansrow-q { font-family:var(--jp); font-size:1.02rem; line-height:1.8; color:var(--ink); margin:0 0 7px; }
.exam-ansrow-q ruby rt { font-size:.8rem; color:var(--ink-2); }
.exam-ansrow-a { display:flex; gap:16px; flex-wrap:wrap; margin:0 0 6px; }
.exam-ansrow-mine, .exam-ansrow-right { display:flex; align-items:center; gap:6px; font-size:.85rem; color:var(--ink-2); }
.exam-ansrow-mine span, .exam-ansrow-right span { font-size:.72rem; text-transform:uppercase; letter-spacing:.05em; color:var(--ink-3); }
.exam-ansrow-mine b, .exam-ansrow-right b { font-family:var(--jp); font-size:1rem; color:var(--ink); }
.exam-ansrow.bad .exam-ansrow-mine b { color:var(--brick); }
.exam-ansrow-right b { color:var(--moss); }
.exam-ansrow-tr { font-size:.8rem; color:var(--ink-3); font-style:italic; margin:4px 0 0; }
@media (max-width:560px) {
  .exam-verdict-pts { margin-left:0; padding-left:0; border-left:0; border-top:1px solid var(--line); padding-top:10px; }
  .exam-cta { width:100%; }
  .exam-review-filter { margin-left:0; }
}

.pt-choice {
  padding:16px 12px; border-radius:var(--r-md); background:var(--paper);
  border:2px solid var(--line); color:var(--ink); font-family:var(--jp);
  font-size:1.3rem; font-weight:600; cursor:pointer; text-align:center;
  transition: all .15s var(--ease); display:flex; align-items:center; justify-content:center; gap:8px;
}
.pt-choice kbd {
  font-size:.7rem; padding:2px 6px; border-radius:3px; background:var(--paper-2);
  color:var(--ink-3); font-family:var(--mono);
}
.pt-choice:hover:not(:disabled) { border-color:var(--accent-text); background:var(--paper-card); transform:translateY(-2px); }
.pt-choice:active:not(:disabled) { transform:translateY(0); }
.pt-choice:disabled { cursor:default; opacity:.85; }
.pt-choice-correct { border-color:var(--moss) !important; background:var(--moss-wash) !important; color:var(--moss) !important; }
.pt-choice-wrong { border-color:var(--brick) !important; background:var(--brick-wash) !important; color:var(--brick) !important; }

/* Hardcore text input */
.pt-hardcore-input { display:flex; gap:8px; justify-content:center; }
.pt-input {
  font-family:var(--jp); font-size:1.2rem; padding:10px 16px; border-radius:var(--r-md);
  background:var(--paper); border:2px solid var(--line); color:var(--ink); width:140px; text-align:center;
  transition: border-color .15s;
}
.pt-input:focus { border-color:var(--accent-text); outline:none; }
.pt-input-correct { border-color:var(--moss) !important; }
.pt-input-wrong { border-color:var(--brick) !important; }

/* Explanation box */
.pt-explain {
  margin-top:18px; padding:14px 16px; border-radius:var(--r-md);
  background:var(--brick-wash); border-left:4px solid var(--brick);
  animation:fadein .25s ease;
}
.pt-explain-header { font-weight:700; color:var(--brick); margin-bottom:4px; font-size:.95rem; }
.pt-explain-text { color:var(--ink-2); font-size:.88rem; line-height:1.55; margin:0; }
/* Inline "explain this" affordance (PRODUCT_TODO A1 visual spec): a text link,
   not a button, so it reads as part of the explanation. */
.pt-explain-more {
  display:inline-flex; align-items:center; gap:6px; margin-top:10px;
  font-size:.82rem; font-weight:700; color:var(--accent-text);
  background:none; border:0; padding:0; cursor:pointer;
}
.pt-explain-more:hover { text-decoration:underline; }
.pt-next-btn { margin-top:10px; }
.pt-explain-ok { background:var(--moss-wash); border-left-color:var(--moss); }
.pt-explain-header-ok { color:var(--moss); }

/* Start overlay */
.pt-start-overlay {
  display:flex; align-items:center; justify-content:center;
  padding:8px 0 40px;
}
.pt-start-panel {
  text-align:center; max-width:480px;
  background:var(--paper-card);
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  padding:44px 38px;
  animation:viewIn .4s var(--ease);
}
.pt-start-big {
  font-family:var(--serif);
  font-size:2.3rem; font-weight:600; letter-spacing:-.01em; margin:0 0 10px;
  color:var(--ink);
}
/* When .pt-start-big is ALSO a .btn-primary (the JLPT #examStartBtn), it sits on
   the --reward gradient, so it must use the on-gradient ink, not --ink (body
   text). --ink over the gradient measured 1.93:1 under a light scene; --onr /
   --grad-warm-ink is the token designed for text on that fill. QA: legibility. */
.btn-primary.pt-start-big { color:var(--grad-warm-ink); font-size:1.05rem; display:block; margin:6px auto 0; }
.pt-start-how { color:var(--ink-2); font-size:.92rem; line-height:1.55; margin:0 0 16px; }
.pt-start-stat { color:var(--clay); font-weight:700; font-size:.95rem; margin-bottom:20px; min-height:1.2em; }
.pt-start-panel .btn-primary { width:100%; }
.pt-start-panel .btn-ghost { width:100%; margin-top:8px; }
.pt-start-keys { color:var(--ink-3); font-size:.78rem; margin-top:16px; }
.pt-start-keys kbd { padding:2px 6px; border-radius:3px; background:var(--paper-2); font-size:.72rem; }

/* ── The entry screen's setting rows (docs/GAME_UI.md R9) ────────────────────
   One shape for "choose this before you start", used by every game that has a
   deck setting. Names each choice by its consequence rather than its direction
   — a learner choosing between two flags cannot tell which one they are ready
   for; a learner reading *easier* and *harder* can. */
.game-entry-row {
  display:flex; flex-direction:column; align-items:center; gap:7px;
  margin:0 0 18px;
}
.game-entry-label {
  font-size:.68rem; font-weight:700; text-transform:uppercase; letter-spacing:.1em;
  color:var(--ink-2);
}
.game-entry-note { font-size:.78rem; color:var(--ink-2); line-height:1.5; max-width:34ch; }
.game-entry-row .pt-modes,
.game-entry-row .pt-level-select { margin:0 auto; justify-content:center; }
.game-entry-row .pt-level-count { margin:0; }
/* The two-way difficulty choice R3 asks for: named by consequence, with the
   easier one first and said to BE easier. */
.game-entry-opts { display:flex; flex-direction:column; gap:8px; width:100%; max-width:30rem; }
.game-entry-opt {
  display:flex; flex-direction:column; gap:2px; text-align:left;
  padding:11px 14px; border-radius:var(--r-md);
  border:1px solid var(--line); background:var(--paper-2); color:var(--ink);
  cursor:pointer; transition:border-color .15s, background .15s;
}
.game-entry-opt:hover { border-color:var(--accent); background:var(--paper-card); }
.game-entry-opt.on { border-color:var(--ink); background:var(--paper-card); }
.game-entry-opt-t { font-weight:700; font-size:.92rem; }
.game-entry-opt-t .game-entry-tag { font-weight:600; color:var(--ink-2); font-size:.8rem; margin-left:6px; }
.game-entry-opt-b { font-size:.79rem; color:var(--ink-2); line-height:1.45; }

/* The real paper's own Japanese section name, kept beside the English one as
   content — it is what the learner meets on the day (R10). */
.exam-sec-ja { font-family:var(--jp); font-size:.72em; font-weight:600; color:var(--ink-2); }

/* Mastery grid */
.pt-mastery { margin-top:24px; }
/* Sits directly over the scene artwork (Vocabulary / Particle Dojo mastery
   sections), where CONTRAST alone is fine (~16:1) but the text still reads as
   blending in — the backdrop is busy (measured pixel variance 2127). Give it a
   plate so it reads as a label on the UI rather than paint on the skyline.
   Inline-block + self-width so the plate hugs the text, not the row. */
.pt-mastery-title {
  font-size:.92rem; color:var(--ink-2); margin:0 0 10px; font-weight:600;
  display:inline-block;
  padding:3px 9px;
  border-radius:var(--r-sm);
  background:color-mix(in srgb, var(--paper-card) 86%, transparent);
}
.pt-mastery-grid { display:flex; flex-wrap:wrap; gap:8px; }
.pt-mastery-cell {
  display:flex; flex-direction:column; align-items:center; gap:4px;
  padding:6px 10px; border-radius:var(--r-sm); background:var(--paper);
  min-width:48px;
}
/* The kana glyph is CONTENT (esp. in Vocabulary) so it must stay legible — the
   tier's vivid colour (--ochre/--brick/--moss) measured 2.79–2.91:1 on the light
   --paper cell. The glyph is now --ink (always AA); the TIER is conveyed by the
   fill bar below, which keeps the vivid colour. Only 'unseen' stays muted
   (--ink-2, not --ink-3) since an untried item is meant to read as inactive.
   QA: npm run legibility. */
.pt-mastery-particle { font-family:var(--jp); font-size:1rem; font-weight:700; color:var(--ink); }
.pt-mastery-bar { width:100%; height:4px; border-radius:2px; background:var(--paper-2); overflow:hidden; }
.pt-mastery-fill { height:100%; border-radius:2px; transition:width .3s; }
.pt-tier-unseen .pt-mastery-particle { color:var(--ink-2); }
.pt-tier-unseen .pt-mastery-fill { background:var(--ink-3); }
.pt-tier-weak .pt-mastery-fill { background:linear-gradient(90deg, var(--brick), #e2685a); }
.pt-tier-learning .pt-mastery-fill { background:linear-gradient(90deg, var(--ochre), #f0b429); }
.pt-tier-mastered .pt-mastery-fill { background:linear-gradient(90deg, var(--moss), #6fae5a); }

/* Summary */
.pt-summary { padding:20px; }
.pt-summary-inner { background:var(--paper); border:1px solid var(--line); border-radius:var(--r-lg); padding:28px 24px; }
.pt-summary-title { font-family:var(--serif); text-align:center; font-size:1.4rem; font-weight:600; color:var(--ink); margin:0 0 20px; }
.pt-summary-stats { display:grid; grid-template-columns:repeat(4, 1fr); gap:12px; margin-bottom:20px; }
.pt-stat-item { text-align:center; }
.pt-stat-num { display:block; font-family:var(--mono); font-size:1.5rem; font-weight:700; color:var(--accent-text); }
.pt-stat-label { font-size:.75rem; color:var(--ink-3); text-transform:uppercase; letter-spacing:.04em; }
.pt-summary-note { text-align:center; color:var(--ink-2); font-size:.9rem; margin-bottom:16px; }
.pt-summary-missed-title { color:var(--ink-2); font-size:.9rem; margin:16px 0 8px; }
.pt-summary-missed { display:flex; flex-direction:column; gap:8px; }
.pt-missed-item { background:var(--paper-2); border-radius:var(--r-sm); padding:10px 12px; }
.pt-missed-particle { display:inline-block; font-family:var(--jp); font-weight:700; color:var(--accent-text); margin-right:8px; font-size:1.05rem; }
.pt-missed-sent { font-family:var(--jp); color:var(--ink); font-size:.88rem; }
.pt-missed-why { display:block; color:var(--ink-3); font-size:.8rem; margin-top:4px; line-height:1.4; }
.pt-summary-actions { display:flex; gap:10px; justify-content:center; margin-top:20px; }

/* ── Particle Dojo: Grammar Guide ── */
.pt-guide { display:flex; flex-direction:column; gap:16px; align-items:center; }
.pt-guide-intro { margin:0 auto; }
.pt-guide-body {
  display:flex; gap:16px; align-items:flex-start; width:100%; max-width:940px;
}
/* A1: search-first guide — the input sits above the scrollable chip list */
.pt-guide-listwrap { flex:1 1 260px; display:flex; flex-direction:column; gap:10px; min-width:0; }
.pt-guide-search {
  width:100%; font-size:.95rem; color:var(--ink); font-family:inherit;
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-md);
  padding:10px 14px; outline:none; transition:border-color .12s;
}
.pt-guide-search::placeholder { color:var(--ink-3); }
.pt-guide-search:focus { border-color:var(--accent-text); }
.pt-guide-list {
  flex:1 1 260px; max-height:60vh; overflow-y:auto; display:flex; flex-direction:column; gap:14px;
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-md); padding:14px;
}
/* Inside the search wrapper the list is not the flex child of the body, so it
   sizes to the wrapper (which carries the 260px basis) and shrinks a touch to
   leave room for the search box above. Rush guide has no wrapper → unaffected. */
.pt-guide-listwrap > .pt-guide-list { flex:1 1 auto; max-height:52vh; }
.pt-guide-gcount { font-size:.68rem; font-weight:700; opacity:.7; margin-left:2px; }
.pt-guide-group { display:flex; flex-direction:column; gap:6px; }
.pt-guide-glevel {
  font-size:.75rem; font-weight:700; color:var(--accent-text); background:var(--accent-wash);
  border-radius:999px; padding:2px 9px; width:fit-content; margin:0;
}
.pt-guide-chips { display:flex; flex-wrap:wrap; gap:6px; }
.pt-guide-chip {
  font-family:var(--jp); font-size:.95rem; font-weight:600; color:var(--ink);
  background:var(--paper-card); border:1px solid var(--line); border-radius:var(--r-sm); padding:5px 10px;
  cursor:pointer; transition:border-color .12s, background .12s;
}
.pt-guide-chip:hover { border-color:var(--accent-text); }
.pt-guide-chip.active { border-color:var(--accent-text); background:var(--accent-wash); color:var(--accent-text); }
.pt-guide-detail {
  flex:1 1 340px; min-height:200px; max-height:60vh; overflow-y:auto;
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-md); padding:18px;
}
.pt-guide-head { display:flex; gap:14px; align-items:flex-start; margin-bottom:14px; }
.pt-guide-char { font-family:var(--jp); font-size:2.4rem; color:var(--accent-text); line-height:1; padding-top:2px; }
.pt-guide-reading { font-size:.8rem; color:var(--ink-2); margin:0; }
.pt-guide-name { font-size:.92rem; font-weight:600; color:var(--ink); line-height:1.5; margin:2px 0 0; }
.pt-guide-jlpt { font-size:.72rem; font-weight:700; color:var(--ink-2); background:var(--paper-2); border-radius:999px; padding:1px 8px; margin-left:6px; white-space:nowrap; }
.pt-guide-ex { margin-bottom:12px; }
.pt-guide-explain { font-size:.85rem; color:var(--ink-2); line-height:1.5; margin:6px 0 0; }
.pt-guide-back { align-self:flex-start; }

/* ── Vocabulary: learning path (reuses pt-* chrome elsewhere) ── */
.voc-path { display:flex; align-items:center; flex-wrap:wrap; gap:0; margin-bottom:20px; }
.voc-node {
  display:flex; flex-direction:column; align-items:center; gap:6px;
  width:52px; height:52px; border-radius:50%; border:2px solid var(--line-2);
  background:var(--paper-2); color:var(--ink-2); font-family:var(--mono); font-weight:700; font-size:1.05rem;
  cursor:pointer; transition:transform .15s var(--ease), border-color .15s var(--ease);
  flex-shrink:0; position:relative;
}
.voc-node:hover:not([disabled]) { transform:scale(1.08); border-color:var(--accent-text); }
.voc-node[disabled] { cursor:not-allowed; opacity:.55; }
.voc-node-num { line-height:1; }
.voc-node-unlocked   { border-color:var(--accent-text); color:var(--accent-text); }
.voc-node-inprogress { border-color:var(--ochre); color:var(--ochre); }
.voc-node-mastered   { border-color:var(--moss); color:var(--moss); background:var(--moss-wash); }
.voc-node-bar {
  position:absolute; bottom:-8px; left:6px; right:6px; height:3px;
  background:var(--paper); border-radius:2px; overflow:hidden;
}
.voc-node-fill { display:block; height:100%; background:var(--grad-warm); }
.voc-node-link { width:22px; height:2px; background:var(--line); flex-shrink:0; }
.voc-node-link-on { background:var(--accent); }

/* Vocabulary practice card */
.voc-prompt {
  display:flex; flex-direction:column; align-items:center; gap:6px;
  padding:28px 10px; text-align:center;
}
.voc-prompt-jp { font-family:var(--jp); font-size:2.4rem; color:var(--ink); }
.voc-prompt-jp ruby rt { font-size:clamp(16px, 1.1rem, 24px); color:var(--ink-2); }
.voc-prompt-rom { font-family:var(--mono); font-size:.85rem; font-weight:700; color:var(--accent-text); }
.voc-prompt-en { font-family:var(--serif); font-size:1.4rem; font-weight:600; color:var(--ink); }

@media (max-width:560px) {
  .voc-node { width:44px; height:44px; font-size:.92rem; }
  .voc-node-link { width:14px; }
}

/* ── JLPT Mock Exam (reuses pt- / order- / an-cats chrome elsewhere) ── */
.exam-mondai-head  { margin-bottom:16px; }
.exam-mondai-num   { font-size:.82rem; font-weight:700; color:var(--accent-text); text-transform:uppercase; letter-spacing:.04em; }
.exam-mondai-num .exam-sec-ja { font-size:1.1em; }
/* The rubric — 「＿＿＿の ことばは どう 読みますか。1・2・3・4から いちばん いい
   ものを 一つ えらんで ください。」 — is the line that tells you what the mondai
   wants, and it was set at .82rem in --ink-2 in the UI face: smaller than body
   text, in a font not designed for kana, on the one screen aimed at people who
   read Japanese slowly. It is Japanese CONTENT: Japanese face, content size,
   and a plate so it reads as the paper's instruction rather than as chrome. */
.exam-mondai-instr {
  font-family:var(--jp);
  font-size:1.06rem; line-height:1.95; color:var(--ink-2);
  margin-top:8px; padding:10px 14px;
  background:var(--paper-2); border-left:3px solid var(--accent);
  border-radius:var(--r-sm);
  max-width:64ch;
}
.exam-prompt  { font-family:var(--jp); font-size:1.35rem; color:var(--ink); line-height:2; margin:16px 0; }
.exam-prompt ruby rt { font-size:.92rem; color:var(--ink-2); }
@media (max-width:560px) {
  .exam-mondai-instr { font-size:1rem; line-height:1.85; padding:9px 11px; }
  .exam-prompt { font-size:1.25rem; }
}
.exam-passage {
  font-family:var(--jp); font-size:1.05rem; color:var(--ink); line-height:1.9;
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-md);
  padding:16px 18px; margin:12px 0; max-height:40vh; overflow-y:auto;
}
.exam-passage ruby rt { font-size:.85rem; color:var(--ink-2); }
.exam-order-slots { font-family:var(--mono); color:var(--accent-text); font-weight:700; letter-spacing:.1em; }
.exam-blank-chip {
  display:inline-block; min-width:1.6em; padding:0 6px; margin:0 2px;
  border-bottom:2px solid var(--accent); color:var(--accent-text); font-weight:700; text-align:center;
}
.exam-cloze-blank { margin:14px 0; }
.exam-cloze-label { font-family:var(--jp); font-size:.95rem; font-weight:700; color:var(--ink); margin-bottom:6px; }
.exam-choice-picked { border-color:var(--accent-text); background:var(--accent-wash); color:var(--accent-text); }
.exam-timer-low { color:var(--err); animation:examPulse 1s ease-in-out infinite; }
@keyframes examPulse { 50% { opacity:.5; } }
.exam-review-list { display:flex; flex-direction:column; gap:10px; max-height:50vh; overflow-y:auto; }
.exam-review-row {
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-md); padding:10px 14px;
}
.exam-review-mondai { font-size:.72rem; font-weight:700; color:var(--ink-3); text-transform:uppercase; letter-spacing:.04em; margin-bottom:4px; }

/* Conjugation Guide (reuses pt-guide layout) */
.rg-h {
  font-size:.78rem; font-weight:700; color:var(--accent-text); text-transform:uppercase;
  letter-spacing:.04em; margin:14px 0 4px;
}
.rg-table { display:flex; flex-direction:column; gap:4px; margin:8px 0 4px; }
.rg-row {
  display:flex; gap:10px; align-items:baseline; flex-wrap:wrap;
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-sm); padding:6px 10px;
}
.rg-jp  { font-family:var(--jp); font-size:1.05rem; font-weight:600; color:var(--ink); }
.rg-en  { font-size:.78rem; color:var(--ink-2); }
.rg-arrow { color:var(--ink-2); }
.rg-out { font-family:var(--jp); font-size:1.05rem; font-weight:700; color:var(--accent-text); }
.rg-note { font-size:.72rem; color:var(--ink-2); margin-left:auto; }

/* Step-by-step practice order */
.rg-steps { display:flex; flex-direction:column; gap:10px; margin:6px 0 4px; padding-left:1.4em; }
.rg-steps li { font-size:.9rem; color:var(--ink-2); line-height:1.5; }
.rg-steps li b { color:var(--ink); }

/* Master reference table */
.rg-table-wrap { overflow-x:auto; margin:8px 0 4px; border:1px solid var(--line); border-radius:var(--r-md); }
.rg-master-table { border-collapse:collapse; width:100%; min-width:520px; font-family:var(--jp); }
.rg-master-table th, .rg-master-table td { padding:8px 12px; text-align:left; white-space:nowrap; }
.rg-master-table thead th {
  background:var(--paper-2); font-size:.85rem; font-weight:700; color:var(--ink);
  position:sticky; top:0; border-bottom:1px solid var(--line);
}
.rg-th-sub { display:block; font-size:.68rem; font-weight:500; color:var(--ink-2); text-transform:none; letter-spacing:0; }
.rg-master-table tbody tr:nth-child(odd) { background:var(--paper); }
.rg-master-table tbody tr:nth-child(even) { background:var(--paper-2); }
/* Lesson tables (grammar guides) carry prose in their cells — let them wrap. */
.lesson-table { min-width: 0; }
.lesson-table th, .lesson-table td { white-space: normal; vertical-align: top; line-height: 1.45; }
.lesson-table td:first-child, .lesson-table th.rg-row-label { white-space: nowrap; }
.rg-master-table tbody tr:first-child td { font-weight:700; color:var(--ink-2); }
/* --paper-card, NOT `background:inherit`. This column is position:sticky, so
   the scrolling cells pass underneath it — and `inherit` picked up the row's
   --paper, which under a scene is `color-mix(in srgb, … 82%, transparent)`.
   Measured: the frozen column computed to 82% opacity, so verb forms slid
   visibly through the one column that tells you which verb you are reading.
   --paper-card resolves to var(--card), which is opaque in the base look and in
   every scene. A border makes the freeze legible rather than implied. */
.rg-row-label { font-size:.78rem; font-weight:600; color:var(--ink-2);
  background:var(--paper-card); border-right:1px solid var(--line);
  position:sticky; left:0; }
.rg-master-table td { font-size:.98rem; color:var(--ink); font-weight:600; }

/* F2: a lesson table reuses rg-master-table but its cells hold prose (English
   glosses), not short verb forms — so let them WRAP instead of forcing a 520px
   nowrap table that always scrolls. */
.lesson-table { min-width:0; }
.lesson-table th, .lesson-table td { white-space:normal; }
.lesson-table td { font-weight:500; }

/* F3: on-demand "how to play" guide (Composition / Eavesdrop / Shadow) */
/* The inline "how to play" toggle these styled is retired (R11): one guide
   per game, in the head, under one name. Kept out rather than left behind as
   dead rules nobody can trace back to markup. */

@media (max-width:720px) {
  .pt-guide-body { flex-direction:column; }
  .pt-guide-list, .pt-guide-detail { max-height:40vh; width:100%; }
}

/* Responsive */
@media (max-width:560px) {
  .pt-sentence { font-size:1.3rem; }
  .pt-sentence ruby { font-size:1.3rem; }
  .pt-choices { grid-template-columns:1fr 1fr; gap:8px; }
  .pt-choice { font-size:1.1rem; padding:10px 8px; }
  .pt-summary-stats { grid-template-columns:repeat(2, 1fr); }
  .pt-hud { font-size:.85rem; gap:10px; }
}

/* ═══ Conjugation Lab ═══════════════════════════════════════════════
   Build the full stacked form from a plain-English prompt. Three modes:
   Starter (one rule, hint shown), Full build (stacked, no hint), and
   Reform (given a form, convert to a named target — kana input). */
.conj-lab-view { max-width:640px; margin:0 auto; padding:8px 4px 40px; }

/* ── C1 · Composition Dojo ─────────────────────────────────────────────── */
.composition-view { max-width:640px; margin:0 auto; padding:8px 4px 40px; }
/* .cmp-head / .cmp-level / .cmp-progress are the shared .game-head family now. */
.cmp-card { background:var(--paper-card); border:1px solid var(--line); border-radius:var(--r-lg); padding:16px 18px; margin-bottom:12px; }
.cmp-en { font-size:1.05rem; font-weight:600; color:var(--ink); line-height:1.5; margin:0 0 12px; }
.cmp-req { display:flex; flex-wrap:wrap; align-items:center; gap:6px; }
.cmp-req-label { font-size:.66rem; text-transform:uppercase; letter-spacing:.08em; color:var(--ink-3); margin-right:2px; }
.cmp-chip { font-family:var(--jp); font-size:.9rem; font-weight:600; color:var(--ink);
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-sm); padding:3px 9px; }
.cmp-chip-pat { color:var(--accent-text); border-color:var(--accent); background:var(--accent-wash); }
.cmp-input { width:100%; font-family:var(--jp); font-size:1.3rem; line-height:1.6; color:var(--ink);
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-md); padding:12px 14px;
  outline:none; resize:vertical; transition:border-color .12s; }
.cmp-input::placeholder { color:var(--ink-3); font-size:1rem; }
.cmp-input:focus { border-color:var(--accent); }
.cmp-actions { display:flex; justify-content:space-between; align-items:center; gap:10px; margin-top:10px; }

.cmp-reveal { margin-top:16px; background:var(--paper-card); border:1px solid var(--line);
  border-radius:var(--r-lg); padding:16px 18px; animation:viewIn .3s var(--ease); }
.cmp-reveal.cmp-ok  { border-color:var(--ok); }
.cmp-reveal.cmp-err { border-color:var(--err); }
.cmp-your { font-family:var(--jp); font-size:1.25rem; color:var(--ink); line-height:1.5;
  padding-bottom:12px; margin:0 0 12px; border-bottom:1px solid var(--line); }
.cmp-rows { display:flex; flex-direction:column; gap:8px; }
.cmp-row { display:flex; align-items:baseline; gap:10px; font-size:.9rem; }
.cmp-row-icon { flex:none; width:16px; text-align:center; font-weight:800; }
.cmp-row-label { flex:none; width:120px; color:var(--ink-2); font-weight:600; }
.cmp-row-val { color:var(--ink); font-family:var(--jp); }
.cmp-row-ok   .cmp-row-icon { color:var(--ok); }
.cmp-row-err  .cmp-row-icon { color:var(--err); }
.cmp-row-warn .cmp-row-icon { color:var(--ochre, var(--gold)); }
.cmp-row-pend .cmp-row-icon { color:var(--ink-3); }
.cmp-row-pend .cmp-row-val  { color:var(--ink-3); }
.cmp-w-ok   { color:var(--ok); }
.cmp-w-miss { color:var(--err); text-decoration:line-through; opacity:.7; }
.cmp-model { margin:14px 0 12px; font-family:var(--jp); font-size:1.05rem; color:var(--ink-2); line-height:1.5; }
.cmp-model-label { display:block; font-family:var(--sans, inherit); font-size:.66rem; text-transform:uppercase;
  letter-spacing:.08em; color:var(--ink-3); margin-bottom:3px; }
.cmp-next { width:100%; }

/* ── C4 · Eavesdrop ────────────────────────────────────────────────────── */
.eavesdrop-view { max-width:600px; margin:0 auto; padding:8px 4px 40px; }
/* .ev-head / .ev-level / .ev-progress are the shared .game-head family now. */
/* The line, then the player under it — a column since the shared transport is a
   full-width bar, not the small pill button that used to sit beside the text. */
.ev-a { display:flex; flex-direction:column; align-items:stretch; gap:10px; background:var(--paper-card);
  border:1px solid var(--line); border-radius:var(--r-lg); padding:16px 18px; margin-bottom:14px; }
.ev-a-who { font-weight:700; color:var(--accent-text); }
.ev-a-text { min-width:0; }
.ev-a-jp { font-family:var(--jp); font-size:1.2rem; color:var(--ink); line-height:1.5; }
.ev-instr { font-size:.82rem; color:var(--ink-2); margin:0 0 10px 2px; }  /* --ink-2 not --ink-3: 4.33:1 on light/sakura was under AA (docs/TODO.MD); it's the instruction the game depends on */
/* Eavesdrop's options ARE the shared grid now (see .rd-opts): same columns,
   same letter, same verdict. Only the type differs, because the content is
   Japanese and Japanese needs more size than latin at the same point value
   (qa/legibility.js). */
.ev-reveal { margin-top:16px; background:var(--paper-card); border:1px solid var(--line);
  border-radius:var(--r-lg); padding:16px 18px; animation:viewIn .3s var(--ease); }
.ev-reveal.ev-ok { border-color:var(--ok); }
.ev-reveal.ev-err { border-color:var(--err); }
.ev-result { font-weight:800; margin:0 0 8px; }
.ev-ok .ev-result { color:var(--ok); }
.ev-err .ev-result { color:var(--err); }
.ev-en { font-size:.92rem; color:var(--ink-2); margin:0 0 8px; }
.ev-why { font-size:.9rem; color:var(--ink); line-height:1.55; margin:0 0 12px; }
.ev-replay { width:100%; margin-bottom:8px; }
.ev-next { width:100%; }

/* ── C2 · Manga Theater ─────────────────────────────────────────────────────
   The strip is a 2x2 grid of square panels. Panel art is fixed illustrative
   colour (a face must not turn green under a scene); everything AROUND it —
   frame, ground, bubbles — is app tokens, so the strip belongs to the theme.
   .mg-line / .mg-brow are styled here rather than inline so one rule sets the
   ink weight for every face in the cast. */
/* 860, not 660: the panels are square and the bubbles hold Japanese at a legible
   ~17px, so a 320px panel left the text filling the frame. Wider panels are the
   fix — shrinking the type is not an option this script can afford. */
.manga-view { max-width:860px; margin:0 auto; padding:8px 4px 40px; }
/* .mg-head / .mg-level / .mg-progress are the shared .game-head family now. */
.mg-tag { font-size:.98rem; color:var(--ink-2); }

/* Level selector — pick which JLPT levels the strips are drawn from. */
.mg-levelsel { display:flex; align-items:center; gap:7px; margin:0 0 12px; flex-wrap:wrap; }
.mg-levelsel-label { font-size:.9rem; font-weight:700; color:var(--ink-2); margin-right:2px; }
/* The level chips are the app's own level row (.pt-level-btn), the one every
   other levelled game uses — Eavesdrop and Manga had a second idiom for the
   same job. Their own .on state is restyled rather than renamed, so the JS
   needs no rewrite for what is a purely visual change. */
.mg-lv, .mg-lv.on { font-family:var(--mono); font-size:.85rem; font-weight:700; border-radius:999px;
  padding:6px 14px; cursor:pointer; border:1px solid var(--accent-wash-2);
  background:var(--accent-wash); color:var(--ink);
  transition:border-color .15s, background .15s, color .15s; }
.mg-lv:hover { border-color:var(--accent-text); }
.mg-lv.on { background:var(--grad-warm); color:var(--grad-warm-ink); border-color:transparent; }

/* The setting — the establishing caption a manga puts before the scene starts.
   Every strip already carried a Japanese one on panel 1, but a learner who
   cannot yet read 「取引先の方が受付に。」 cannot know the situation, and the
   situation is what decides the register the whole drill is testing.

   It must not read as dialogue, so it differs from a bubble on four axes at
   once: square corners against their rounded ones, no tail, italic rather than
   upright, and an ink wash rather than paper. The rule down the left is the
   caption box's spine. Not a different typeface — manga would set narration in
   mincho against gothic dialogue, but this app deliberately ships one family
   (--jp, --sans and --serif all resolve to Noto Sans JP), and one caption is
   not worth either a second webfont or a system-serif stack that renders
   differently on every platform. */
.mg-setting {
  margin:0 0 11px; padding:8px 13px;
  font-size:1rem; line-height:1.45; font-style:italic; letter-spacing:.005em;
  color:var(--ink-2);
  background:color-mix(in srgb, var(--ink-3) 13%, var(--paper-card));
  border:1px solid color-mix(in srgb, var(--ink-3) 34%, transparent);
  border-left:3px solid var(--ink-3);
  border-radius:0 3px 3px 0; }

.mg-strip { display:grid; grid-template-columns:repeat(var(--mg-cols,3), 1fr); gap:10px; margin-bottom:16px; }
/* ── uniform panels ─────────────────────────────────────────────────────────
   Every frame is the same size. A lone last frame (the 4th of a 3+1 strip) used
   to get `grid-column:1 / -1` and, being square, ballooned into a huge full-width
   block. Now the strip is a grid of min(3, panelCount) columns (set as --mg-cols
   in mgRender), so 4 panels read as 3 + 1 at the SAME width, 5 as 3 + 2, and 2 or
   3 fill their row. The blank beat is drawn out by a highlight ring (below), not
   by extra size; only the payoff (mg-slot) still spans the full width — it is the
   reveal, and it is the one frame that earns the room. */
.mg-panel { position:relative; margin:0; aspect-ratio:4/3; min-width:0; overflow:hidden;
  background:var(--paper-card); border:2px solid var(--ink-3); border-radius:var(--r-sm); }
.mg-strip > .mg-panel .mg-bubble { font-size:1.1rem; padding:5px 8px; }
.mg-strip > .mg-slot { grid-column:1 / -1; }
/* the blank frame is "the one to fill" — a soft accent ring says so without size */
.mg-panel:has(.mg-blank) { box-shadow:0 0 0 2px color-mix(in srgb, var(--accent) 50%, transparent); }
.mg-panel-svg { position:absolute; inset:0; width:100%; height:100%; display:block; }
/* one rule for every drawn face. Brow weight is the exception: it is a per-
   character trait (--mg-bw, set on the feature group in mangaChar), because
   brow weight is the strongest thing telling two faces apart after eye size. */
.mg-panel .mg-line, .mg-panel .mg-brow path {
  fill:none; stroke:#7a5638; stroke-width:1.6; stroke-linecap:round; }
/* --mg-bw is the character's brow weight, --mg-bwx this expression's emphasis
   (anger drives them heavier). --mg-brow lets light hair take its own colour:
   the shared ink is darker than white hair, which reads as a wig. */
.mg-panel .mg-brow path {
  stroke:var(--mg-brow, #7a5638);
  stroke-width:calc(1.6px * var(--mg-bw, 1) * var(--mg-bwx, 1)); }
.mg-wall  { fill:color-mix(in srgb, var(--ink-3) 10%, var(--paper-card)); }
.mg-sky   { fill:color-mix(in srgb, var(--a2, var(--accent)) 14%, var(--paper-card)); }
.mg-floor { fill:color-mix(in srgb, var(--ink-3) 24%, var(--paper-card)); }
.mg-prop  { fill:color-mix(in srgb, var(--ink-3) 20%, var(--paper-card)); }
.mg-prop2 { fill:color-mix(in srgb, var(--ink-3) 34%, var(--paper-card)); }
.mg-propline { fill:none; stroke:var(--ink-3); stroke-width:.9; opacity:.45; stroke-linecap:round; }
/* screentone: `currentColor` inside the pattern resolves against this, so the
   dots follow the theme's ink instead of being a baked grey. */
.mg-tone  { color:var(--ink-3); }
.mg-focus { fill:none; stroke:var(--ink-3); stroke-width:.9; opacity:.28; }
.mg-flash { fill:none; stroke:var(--ink-3); stroke-width:1.4; opacity:.4; stroke-linecap:round; }

.mg-num { position:absolute; top:4px; left:6px; z-index:3; font-size:.62rem; font-weight:800;
  color:var(--ink-3); opacity:.65; font-variant-numeric:tabular-nums; }
.mg-narr { position:absolute; top:6px; left:22px; max-width:82%; z-index:2; font-family:var(--jp);
  font-size:1.32rem; line-height:1.3; color:var(--ink); background:var(--paper-card);
  border:1px solid var(--line); border-radius:3px; padding:3px 6px; }

/* Speech bubble. A tail on the side the speaker is on is what makes a panel
   read as dialogue rather than a caption over a portrait. Bubbles sit at the TOP:
   the cast is scaled about the panel's bottom edge, so the room opens up above
   their heads — which is where manga puts speech anyway.
   Text is deliberately LARGE (per request) — the dialogue is the thing a learner
   reads, so it takes the space; the art reads underneath it. */
.mg-bubble { position:absolute; z-index:3; top:6px; left:6px; right:6px;
  font-family:var(--jp); font-size:1.45rem; line-height:1.3; color:var(--ink);
  background:var(--paper-card); border:1.5px solid var(--ink-3); border-radius:10px;
  padding:5px 8px; text-align:center; }
.mg-bubble::after { content:''; position:absolute; bottom:-7px; width:0; height:0;
  border:6px solid transparent; border-top-color:var(--ink-3); }
.mg-bubble::before { content:''; position:absolute; bottom:-4.4px; z-index:1; width:0; height:0;
  border:5px solid transparent; border-top-color:var(--paper-card); }
/* the tail lands under whoever is speaking; `you` is the off-panel POV */
.mg-from-l::after, .mg-from-l::before { left:24%; }
.mg-from-r::after, .mg-from-r::before { right:24%; }
.mg-from-you::after, .mg-from-c::after { left:calc(50% - 9px); }
.mg-from-you::before, .mg-from-c::before { left:calc(50% - 7.5px); }

/* ── who is talking ──────────────────────────────────────────────────────────
   With one person in frame the bubble can span it: it belongs to the only one
   there. With two it could not — a full-width box with a 12px tail under it
   belongs to nobody, and that is exactly how it read. Three cues, because the
   panel is 320px wide and one cue at that size is a guess:
     · the bubble is anchored to the speaker's half, so position answers it
     · the tail sits near that same edge, pointing down at their head
     · the listener is dimmed, which is what manga does instead of name labels
   `data-cast` and `data-speaker` are set in mgPanelHTML. */
.mg-panel[data-cast="2"] .mg-bubble.mg-from-l,
.mg-panel[data-cast="3"] .mg-bubble.mg-from-l { right:auto; min-width:44%; max-width:70%; }
.mg-panel[data-cast="2"] .mg-bubble.mg-from-r,
.mg-panel[data-cast="3"] .mg-bubble.mg-from-r { left:auto;  min-width:44%; max-width:70%; }
/* anchored to a side, the tail belongs near that side's edge — a percentage
   would now be measured against the bubble, not the frame. */
.mg-panel[data-cast="2"] .mg-from-l::after,  .mg-panel[data-cast="3"] .mg-from-l::after,
.mg-panel[data-cast="2"] .mg-from-l::before, .mg-panel[data-cast="3"] .mg-from-l::before { left:26px; }
.mg-panel[data-cast="2"] .mg-from-r::after,  .mg-panel[data-cast="3"] .mg-from-r::after,
.mg-panel[data-cast="2"] .mg-from-r::before, .mg-panel[data-cast="3"] .mg-from-r::before { right:26px; }
/* a bigger tail: at this panel size the old 6px one was not readable */
.mg-bubble::after  { border-width:9px; }
.mg-bubble::before { border-width:7.5px; bottom:-5.6px; }
.mg-bubble.mg-b2::before { bottom:auto; top:-5.6px; }

/* `you` is the off-panel POV, and its tail pointed straight down from a bubble
   at the TOP of the frame — into the gap between two characters, which is
   precisely what made the question panel read as one of them talking. A voice
   from outside the frame belongs at the frame's edge: the bubble drops to the
   foot and the tail runs off the bottom, toward the reader. The caption goes
   back to the top, which is now the free end. */
.mg-panel[data-speaker="you"] .mg-bubble { top:auto; bottom:7px; }
.mg-panel[data-speaker="you"] .mg-narr   { top:6px; bottom:auto; }
/* the tail has to REACH the frame edge, or it just points at whoever is
   standing under it — which is the bug, one step lower down the panel. 7px of
   tail below a bubble 7px off the floor puts the tip exactly on the border. */
.mg-panel[data-speaker="you"] .mg-bubble::after  { bottom:-7px; border-width:7px; }
.mg-panel[data-speaker="you"] .mg-bubble::before { bottom:-5px; border-width:5px; }

.mg-cast { transition:opacity .2s var(--ease); }
.mg-panel[data-speaker="l"] .mg-at-r, .mg-panel[data-speaker="l"] .mg-at-c,
.mg-panel[data-speaker="r"] .mg-at-l, .mg-panel[data-speaker="r"] .mg-at-c,
.mg-panel[data-speaker="c"] .mg-at-l, .mg-panel[data-speaker="c"] .mg-at-r { opacity:.5; }
/* two speakers in one frame: the second bubble drops to the foot of the panel
   and flips its tail upward, so the pair reads top-then-bottom. */
.mg-bubble.mg-b2 { top:auto; bottom:6px; }
.mg-bubble.mg-b2::after { bottom:auto; top:-9px; border-top-color:transparent; border-bottom-color:var(--ink-3); }
.mg-bubble.mg-b2::before { bottom:auto; top:-5.6px; border-top-color:transparent; border-bottom-color:var(--paper-card); }
.mg-blank { color:var(--accent-text); border-style:dashed; border-color:var(--accent);
  font-weight:800; letter-spacing:.1em; }
.mg-said-ok  { border-color:var(--ok); }
.mg-said-bad { border-color:var(--err); }

/* Caption placement depends on whether the panel has a bubble. With one, the top
   is taken and the caption drops to the foot; without one the top is free and the
   foot is exactly where the characters are — captions were covering them. */
.mg-has-bubble .mg-narr { top:auto; bottom:6px; left:6px; }

.mg-slot:empty { display:none; }
.mg-payoff { animation:viewIn .34s var(--ease); }
.mg-payoff-ok  { border-color:var(--ok); }
.mg-payoff-bad { border-color:var(--err); }

.mg-instr { font-size:1.2rem; font-weight:600; color:var(--ink-2); margin:0 0 10px 2px; }
/* The criterion sits under the question: register items turn on FIT, and the
   prompt has to say so or a grammatical-but-blunt option is not excluded. */
.mg-instr-sub { display:block; font-size:.86rem; font-weight:400; color:var(--ink-3); margin-top:3px; }
/* Manga's options join the same grid — see the note on .ev-opts above. */

.mg-reveal { margin-top:16px; background:var(--paper-card); border:1px solid var(--line);
  border-radius:var(--r-lg); padding:16px 18px; animation:viewIn .3s var(--ease); }
.mg-reveal.mg-ok { border-color:var(--ok); }
.mg-reveal.mg-err { border-color:var(--err); }
.mg-result { font-size:1.2rem; font-weight:800; margin:0 0 8px; }
.mg-ok .mg-result { color:var(--ok); }
.mg-err .mg-result { color:var(--err); }
.mg-fix { font-size:1.28rem; color:var(--ink); margin:0 0 8px; }
.mg-fix b { font-family:var(--jp); }
.mg-why { font-size:1.23rem; color:var(--ink); line-height:1.5; margin:0 0 12px; }

/* The strip in English — collapsed by default so the reveal still leads with
   `why`, which is what the strip actually teaches. Opened, it gives the whole
   script line by line plus what each of the four options was. Never rendered
   next to the option buttons: see mgScriptHTML. */
.mg-script { margin:0 0 12px; border-top:1px solid var(--line); padding-top:10px; }
.mg-script > summary { font-size:1.06rem; color:var(--ink-2); cursor:pointer; padding:4px 0; }
.mg-script > summary:hover { color:var(--ink); }
.mg-line { display:grid; grid-template-columns:minmax(88px,auto) 1fr; gap:2px 12px; padding:7px 0; border-bottom:1px solid var(--line); }
.mg-line:last-of-type { border-bottom:0; }
.mg-line-who { font-family:var(--mono); font-size:.86rem; color:var(--ink-2); align-self:start; }
.mg-line-who i { font-style:normal; opacity:.72; }
.mg-line-jp { font-family:var(--jp); font-size:1.2rem; color:var(--ink); }
.mg-line-en { grid-column:2; font-size:1.02rem; color:var(--ink-2); line-height:1.45; }
.mg-optgloss { margin-top:10px; display:grid; gap:6px; }
.mg-og { padding:7px 10px; border-radius:8px; border:1px solid var(--line); background:var(--paper-2); }
.mg-og-ok { border-color:var(--ok); }
.mg-og-bad { border-color:var(--err); }
.mg-og-jp { display:block; font-family:var(--jp); font-size:1.14rem; color:var(--ink); }
.mg-og-en { display:block; font-size:.98rem; color:var(--ink-2); margin-top:2px; line-height:1.4; }
.mg-next { width:100%; }

/* 440, not 420. At 420 the two widest common phones miss the cutoff — 428
   (iPhone 12-14 Pro Max) and 430 (15/16 Pro Max) — and get THREE 4:3 panels
   instead of two. The arithmetic: 430 − 24 (.main-content padding at ≤720)
   − 8 (.manga-view padding) − 20 (two 10px gaps) = 378 / 3 = 126px per panel,
   against a floor of ~180px that the bubble type below was tuned for. At 440
   the two-up lands at (440−24−8−10)/2 = 199px.
   The whole block moves, not just .mg-strip: the type rules in here exist
   BECAUSE the panels got smaller, so splitting them desynchronises the two.
   Intended side effect on 421-440px devices: .mg-opt steps 1.45rem → 1.28rem,
   which is 23px — still far above the ≥13px Japanese floor. */
@media (max-width:440px) {
  /* Two panels per row on phones — four one-per-row is a 2,000px scroll and you
     lose the setup before reaching the options. The payoff still spans the row. */
  .mg-strip { grid-template-columns:repeat(2, 1fr); }
  .mg-strip > .mg-panel .mg-bubble { font-size:1.04rem; padding:4px 7px; }
  .mg-bubble { font-size:1.28rem; padding:6px 9px; }
  .mg-narr { font-size:1.15rem; }
  .mg-opt { font-size:1.28rem; }
}

/* ── C3 · Shadow Mode (bolt-on to Real Listening; feature-detected) ─────── */
.rl-shadow { margin:8px 0 4px; }
.rl-shadow-btn { font-size:.85rem; font-weight:700; color:var(--accent-text); background:var(--accent-wash);
  border:1px solid var(--accent); border-radius:999px; padding:7px 14px; cursor:pointer; }
.rl-shadow-btn.rl-shadow-listening { color:var(--on-ink); background:var(--err); border-color:var(--err); animation:kq-shimmer 1.2s ease-in-out infinite; }
.rl-shadow-result { margin-top:10px; padding:12px 14px; border:1px solid var(--line); border-radius:var(--r-md); background:var(--paper); }
.rl-shadow-good { border-color:var(--ok); }
.rl-shadow-ok   { border-color:var(--ochre, var(--gold)); }
.rl-shadow-low  { border-color:var(--line); }
.rl-shadow-row { display:flex; gap:10px; margin-bottom:6px; }
.rl-shadow-lbl { flex:none; width:64px; font-size:.68rem; text-transform:uppercase; letter-spacing:.06em; color:var(--ink-3); }
.rl-shadow-said, .rl-shadow-target { font-family:var(--jp); font-size:1rem; color:var(--ink); }
.rl-shadow-msg { font-size:.86rem; color:var(--ink-2); margin:6px 0 0; }
.rl-shadow-pct { color:var(--ink-3); font-size:.78rem; }

/* .cl-head is the shared .game-head family now. */
.cl-modes { display:flex; gap:6px; background:var(--paper-2); padding:4px; border-radius:12px; }
.cl-mode {
  border:none; background:transparent; color:var(--ink-2);
  font-weight:700; font-size:.9rem; padding:7px 12px; border-radius:9px;
  cursor:pointer; transition:background .12s, color .12s; white-space:nowrap;
}
.cl-mode.on { background:var(--paper-card); color:var(--accent-text); box-shadow:0 1px 2px rgba(0,0,0,.06); }
.cl-mode:hover:not(.on) { color:var(--ink); }

.cl-levelwrap { flex:1 1 auto; }
.cl-levellab { font-size:.78rem; color:var(--ink-2); font-weight:600; display:flex; align-items:center; gap:6px; }
.cl-level {
  font:inherit; font-size:.82rem; padding:5px 8px; border-radius:8px;
  border:1px solid var(--line-2); background:var(--paper-card); color:var(--ink); cursor:pointer;
}
.cl-stats { display:flex; gap:14px; margin-left:auto; font-size:.82rem; color:var(--ink-2); }
.cl-stats b { color:var(--clay); font-size:1rem; }

.cl-blurb { font-size:.82rem; color:var(--ink-3); margin:0 4px 14px; line-height:1.5; }

.cl-card {
  background:var(--paper-card); border:1px solid var(--line);
  border-radius:16px; padding:22px 20px; text-align:center; margin-bottom:14px;
}
.cl-group {
  display:inline-block; font-size:.72rem; font-weight:700; letter-spacing:.03em;
  text-transform:uppercase; color:var(--accent-text);
  background:var(--accent-wash); padding:3px 10px; border-radius:999px; margin-bottom:12px;
}
.cl-dict { font-size:1.5rem; font-weight:700; color:var(--ink); }
.cl-dict .cl-en { font-size:.95rem; font-weight:500; color:var(--ink-3); }
.cl-instr { margin-top:10px; font-size:1.05rem; color:var(--ink-2); }
.cl-instr-big { font-size:1.5rem; font-weight:700; color:var(--ink); margin-top:0; }
.cl-instr-big + .cl-dict { margin-top:12px; font-size:.95rem; font-weight:500; color:var(--ink-3); }
.cl-rule {
  margin-top:12px; font-size:.82rem; color:var(--ink-2); font-family:var(--mono);
  background:var(--paper-2); border-radius:9px; padding:8px 10px; line-height:1.6;
}

.cl-inputrow { display:flex; gap:8px; margin-bottom:12px; }
.cl-input {
  flex:1 1 auto; font:inherit; font-size:1.15rem; padding:12px 14px;
  border:1.5px solid var(--line-2); border-radius:12px;
  background:var(--paper-card); color:var(--ink);
}
.cl-input:focus { outline:none; border-color:var(--accent-text); box-shadow:0 0 0 3px var(--accent-wash); }

.cl-feedback { min-height:24px; font-size:.95rem; line-height:1.6; padding:0 4px; }
.cl-feedback.ok { color:var(--moss); }
.cl-feedback.bad { color:var(--brick); }
.cl-feedback.reveal { color:var(--ink-2); }
.cl-feedback.ok b, .cl-feedback.reveal b { font-size:1.25rem; }
.cl-you { font-family:var(--mono); background:var(--brick-wash); color:var(--brick); padding:1px 6px; border-radius:6px; }
.cl-romaji { color:var(--ink-3); font-family:var(--mono); font-size:.85rem; }
.cl-explain {
  margin-top:10px; font-size:.85rem; color:var(--ink-2); line-height:1.7;
  background:var(--paper-2); border-left:3px solid var(--accent); border-radius:0 9px 9px 0;
  padding:10px 12px;
}
.cl-explain b { color:var(--ink); }

@media (max-width:520px) {
  .cl-head { gap:8px; }
  .cl-stats { width:100%; margin-left:0; justify-content:flex-start; }
  .cl-dict { font-size:1.3rem; }
  .cl-instr-big { font-size:1.25rem; }
  .cl-input { font-size:1.05rem; }
}

/* ═══ Conjugation Combination Matrix (shared by Lab + Rush guides) ═══════
   Interactive helper/form × tense grid; tap a cell for meaning + example. */
.cm { margin-top:6px; }
.cm-intro { font-size:.84rem; color:var(--ink-2); line-height:1.6; margin:0 0 10px; }
.cm-samples { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:10px; }
.cm-sample {
  display:flex; flex-direction:column; align-items:center; gap:1px;
  border:1px solid var(--line-2); background:var(--paper-card); color:var(--ink);
  border-radius:9px; padding:5px 11px; cursor:pointer; font-family:var(--mono);
  font-size:1rem; font-weight:700; line-height:1.1; transition:background .12s, border-color .12s;
}
.cm-sample i { font-family:var(--sans); font-style:normal; font-weight:500; font-size:.62rem; color:var(--ink-3); }
.cm-sample.on { border-color:var(--accent-text); background:var(--accent-wash); color:var(--accent-text); }
.cm-sample.on i { color:var(--accent-text); }
.cm-sample:hover:not(.on) { border-color:var(--accent-text); }

.cm-table-wrap { overflow-x:auto; -webkit-overflow-scrolling:touch; border:1px solid var(--line); border-radius:12px; }
.cm-table { border-collapse:collapse; width:100%; font-size:.9rem; table-layout:fixed; }
.cm-table th, .cm-table td { border:1px solid var(--line); padding:0; text-align:center; white-space:nowrap; }
/* even column widths so the grid fills the (now wider) container evenly */
.cm-table thead th:not(.cm-corner) { width:auto; }
.cm-table thead th {
  background:var(--paper-2); color:var(--ink-2); font-size:.7rem; font-weight:700;
  padding:7px 6px; position:sticky; top:0;
}
.cm-corner { text-align:left !important; color:var(--ink-3) !important; font-weight:600 !important; }
.cm-rowlabel {
  background:var(--paper-2); color:var(--ink); font-weight:700; font-size:.72rem;
  text-align:left !important; padding:5px 8px !important; position:sticky; left:0; z-index:1;
}
.cm-cell {
  width:100%; border:none; background:transparent; color:var(--ink);
  font-family:var(--mono); font-size:1rem; padding:9px 8px; cursor:pointer;
  transition:background .1s; white-space:nowrap;
}
.cm-cell:hover { background:var(--accent-wash); }
.cm-cell.on { background:var(--accent); color:var(--accent-ink); font-weight:700; }

.cm-detail-slot { margin-top:12px; }
.cm-detail-hint { font-size:.8rem; color:var(--ink-3); text-align:center; padding:12px; }
.cm-detail {
  background:var(--paper-card); border:1px solid var(--line-2);
  border-left:3px solid var(--accent); border-radius:0 12px 12px 0; padding:12px 14px;
}
.cm-detail-form { font-family:var(--mono); font-size:1.5rem; font-weight:700; color:var(--ink); }
.cm-detail-rom { font-size:.85rem; font-weight:500; color:var(--ink-3); margin-left:6px; }
.cm-detail-combo { font-size:1rem; color:var(--accent-text); margin:8px 0 4px; }
.cm-detail-mean { font-size:.86rem; color:var(--ink-2); line-height:1.6; margin:4px 0; }
.cm-detail-note { font-size:.8rem; color:var(--ink-3); line-height:1.5; margin:4px 0; }
.cm-detail-eg { font-size:.86rem; color:var(--ink-2); margin:8px 0 0; padding-top:8px; border-top:1px dashed var(--line); }
.cm-jp { font-family:var(--mono); font-weight:700; color:var(--ink); }

/* Conjugation Lab guide shell */
/* The guide (with the big combination table) is wider than the practice view —
   it uses the horizontal space so the 9×8 grid isn't cramped. */
.cl-guide { max-width:min(1100px, 96vw); margin:0 auto; padding:8px 12px 40px; }
.cl-guide-head { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:14px; flex-wrap:wrap; }
.cl-guide-title { font-size:1.3rem; margin:0; color:var(--ink); }
.cl-guide-h { font-size:1rem; color:var(--ink); margin:22px 0 10px; }
.cl-guide-cards { display:grid; grid-template-columns:repeat(auto-fill, minmax(200px, 1fr)); gap:8px; }
.cl-guide-card { background:var(--paper-card); border:1px solid var(--line); border-radius:10px; padding:10px 12px; }
.cl-guide-card-t { font-weight:700; color:var(--accent-text); font-size:.9rem; }
.cl-guide-card-m { font-size:.82rem; color:var(--ink-2); margin-top:3px; line-height:1.5; }
.cl-guide-card-n { font-size:.74rem; color:var(--ink-3); margin-top:5px; font-family:var(--mono); line-height:1.5; }
.cl-guide-foot { font-size:.8rem; color:var(--ink-3); margin-top:12px; line-height:1.6; }

@media (max-width:520px) {
  .cm-table { font-size:.72rem; }
  .cm-cell { font-size:.8rem; padding:5px 5px; }
  .cm-detail-form { font-size:1.25rem; }
}

/* Godan base tags (-a/-i/-e/-o/-te) shown on the matrix and in cell detail */
.cm-base-tag {
  display:inline-block; font-family:var(--mono); font-size:.66rem; font-weight:700;
  color:var(--accent-text); background:var(--accent-wash); border:1px solid var(--accent-wash-2);
  border-radius:5px; padding:0 4px; line-height:1.5; vertical-align:middle;
}
.cm-col-base, .cm-row-base {
  display:block; font-family:var(--mono); font-size:.6rem; font-weight:700;
  color:var(--accent-text); margin-top:2px;
}
.cm-row-base { display:inline-block; margin:0 0 0 5px; padding:0 4px; background:var(--accent-wash); border-radius:4px; }
.cm-detail-base {
  font-size:.82rem; color:var(--ink-2); line-height:1.7; margin:8px 0;
  background:var(--paper-2); border-radius:8px; padding:8px 10px;
}

/* Standalone-forms strip below the matrix (te / ば / volitional / ましょう / imperative) */
.cm-extra-label { font-size:.76rem; color:var(--ink-3); font-weight:600; margin:14px 0 6px; }
.cm-extra-row { display:flex; flex-wrap:wrap; gap:8px; }
.cm-extra {
  display:flex; flex-direction:column; align-items:flex-start; gap:2px;
  border:1px solid var(--line-2); background:var(--paper-card); color:var(--ink);
  border-radius:10px; padding:7px 12px; cursor:pointer; transition:background .12s, border-color .12s;
}
.cm-extra:hover { border-color:var(--accent-text); background:var(--accent-wash); }
.cm-extra.on { border-color:var(--accent-text); background:var(--accent); }
.cm-extra.on .cm-extra-en, .cm-extra.on .cm-extra-jp { color:var(--accent-ink); }
.cm-extra.on .cm-base-tag { background:var(--accent-ink); color:var(--accent-text); border-color:transparent; }
.cm-extra-en { font-size:.72rem; font-weight:700; color:var(--ink-2); }
.cm-extra-jp { font-family:var(--mono); font-size:1.05rem; font-weight:700; color:var(--ink); }

/* Furigana (ruby) in conjugation displays + the on/off toggle */
ruby { ruby-align:center; }
ruby rt { font-size:.5em; color:var(--ink-3); font-weight:600; user-select:none; line-height:1; }
.cm-cell ruby rt, .cm-extra-jp ruby rt { color:inherit; opacity:.7; }
.cm-controls { display:flex; align-items:flex-start; justify-content:space-between; gap:10px; flex-wrap:wrap; margin-bottom:10px; }
.cm-controls .cm-samples { margin-bottom:0; flex:1 1 auto; }
.cm-furi-toggle {
  flex-shrink:0; font-family:var(--sans); font-size:.72rem; font-weight:700;
  border:1px solid var(--line-2); background:var(--paper-card); color:var(--ink-3);
  border-radius:9px; padding:6px 11px; cursor:pointer; white-space:nowrap; transition:all .12s;
}
.cm-furi-toggle.on { color:var(--accent-text); border-color:var(--accent-text); background:var(--accent-wash); }
.cm-furi-toggle:hover { border-color:var(--accent-text); }
/* keep matrix rows tall enough that ruby doesn't clip */
.cm-cell { line-height:1.35; }
.lk-cf-jp ruby rt { font-size:.55em; }

/* Word Groups typed-round: answer-language cues (romaji vs English) */
.wg-type-lang {
  display:inline-block; font-size:.62rem; font-weight:700; letter-spacing:.02em;
  text-transform:uppercase; padding:2px 8px; border-radius:999px; vertical-align:middle;
  margin-left:8px;
}
.wg-type-lang-romaji  { color:var(--accent-text); background:var(--accent-wash); border:1px solid var(--accent-wash-2); }
.wg-type-lang-english { color:var(--clay);   background:var(--clay-wash);   border:1px solid var(--clay); }
.wg-type-input-en { border-color:var(--clay); }
.wg-type-input-en:focus { border-color:var(--clay); box-shadow:0 0 0 3px var(--clay-wash); }

/* Blocking pause screen between typed-round modes (romaji → English) */
.wg-modeswitch {
  position:absolute; inset:0; z-index:40; display:flex; align-items:center; justify-content:center;
  background:color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px);
  opacity:0; pointer-events:none; transition:opacity .18s;
}
.wg-modeswitch.show { opacity:1; pointer-events:auto; }
.wg-ms-card {
  background:var(--paper-card); border:1px solid var(--line-2); border-radius:18px;
  box-shadow:0 12px 40px rgba(0,0,0,.2); padding:26px 28px; max-width:min(92vw, 400px);
  text-align:center; animation:wgMsPop .2s ease-out;
}
@keyframes wgMsPop { from { transform:scale(.94); opacity:0; } to { transform:scale(1); opacity:1; } }
.wg-ms-ic { font-size:2rem; }
.wg-ms-eyebrow { font-size:.8rem; font-weight:700; color:var(--ink-3); text-transform:uppercase; letter-spacing:.03em; margin:8px 0 12px; }
.wg-ms-flow { display:flex; align-items:center; justify-content:center; gap:12px; margin:6px 0 14px; }
.wg-ms-lang {
  font-size:1rem; font-weight:700; padding:6px 16px; border-radius:999px;
}
.wg-ms-lang-romaji  { color:var(--accent-text); background:var(--accent-wash); border:1px solid var(--accent-wash-2); }
.wg-ms-lang-english { color:var(--clay);   background:var(--clay-wash);   border:1px solid var(--clay); }
.wg-ms-arrow { color:var(--ink-3); font-size:1.3rem; font-weight:700; }
.wg-ms-desc { font-size:.95rem; color:var(--ink); font-weight:600; margin:0 0 6px; line-height:1.5; }
.wg-ms-hint { font-size:.85rem; color:var(--ink-2); margin:0 0 18px; line-height:1.6; }
.wg-ms-hint b { color:var(--ink); font-weight:700; }
.wg-ms-btn { width:100%; }

.wg-type-langwarn {
  font-size:.85rem; color:var(--clay); background:var(--clay-wash);
  border-radius:8px; padding:7px 10px; margin:6px 0; line-height:1.5;
}

/* Conjugation Lab — rich reveal card (correct / incorrect reinforcement) */
.cl-rc {
  text-align:left; border-radius:14px; padding:14px 16px; margin-top:4px;
  border:1px solid var(--line); background:var(--paper-card);
}
.cl-rc.ok  { border-left:4px solid var(--moss);  }
.cl-rc.bad { border-left:4px solid var(--brick); }
.cl-rc-head { font-size:1rem; font-weight:700; display:flex; align-items:center; gap:10px; }
.cl-rc.ok  .cl-rc-head { color:var(--moss);  }
.cl-rc.bad .cl-rc-head { color:var(--brick); }
.cl-rc-group {
  font-size:.66rem; font-weight:700; text-transform:uppercase; letter-spacing:.03em;
  color:var(--ink-3); background:var(--paper-2); border-radius:999px; padding:2px 9px;
}
.cl-rc-you { font-size:.85rem; color:var(--ink-2); margin-top:6px; }
.cl-rc-change {
  font-size:1.55rem; font-weight:700; color:var(--ink); margin:10px 0 4px;
  display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; line-height:1.3;
}
.cl-rc-arrow { color:var(--ink-3); font-weight:400; font-size:1.1rem; }
.cl-diff-base { color:var(--ink); }
.cl-diff-changed {
  color:var(--accent-ink); background:var(--accent); border-radius:6px;
  padding:0 4px; box-decoration-break:clone; -webkit-box-decoration-break:clone;
}
.cl-rc-change .cl-romaji { font-size:.8rem; font-weight:500; color:var(--ink-3); }
.cl-rc-mean { font-size:.92rem; color:var(--ink-2); margin:6px 0; }
.cl-rc-mean b { color:var(--ink); }
.cl-rc-eg {
  display:flex; align-items:baseline; gap:8px; flex-wrap:wrap;
  font-size:.95rem; margin:10px 0 0; padding-top:10px; border-top:1px dashed var(--line);
}
.cl-rc-eg-lbl {
  font-size:.7rem; font-weight:700; color:var(--accent-text);
  background:var(--accent-wash); border-radius:6px; padding:1px 7px; flex-shrink:0;
}
.cl-rc-eg .cl-jp { font-family:var(--mono); color:var(--ink); font-weight:600; }
.cl-rc-eg-en { font-size:.85rem; color:var(--ink-3); }
.cl-rc-rule {
  font-size:.82rem; color:var(--ink-2); line-height:1.7; margin-top:10px;
  background:var(--paper-2); border-radius:8px; padding:8px 10px;
}


/* ═══════════════════════════════════════════════════════════════════════════
   EVENT / CONTEXTUAL SCENE THEMES (theme_scenes.js)
   Ported from the theme demo. #sceneLayer holds the backdrop (#bg = art +
   particles, #fx = cameos). Palettes below retint app tokens per data-scene.
   body.scene-on retires the original #bgScenes/#sakuraLayer.
═══════════════════════════════════════════════════════════════════════════ */
#sceneLayer{position:fixed;inset:0;z-index:-1;overflow:hidden;pointer-events:none;}
body.scene-on #bgScenes, body.scene-on #sakuraLayer{display:none !important;}
/* when a scene is active the page ground goes transparent so #sceneLayer shows
   through behind the (translucent) cards/navbar; the default look is opaque. */
body.scene-on { background:transparent !important; }
/* ── Scene palette → base tokens (the retint that makes a scene THEME the app) ──
   The token aliases at the top of this file flow ONE WAY: base → scene
   (--a1: var(--accent), --card: var(--paper-card), …). That's right for the
   no-scene default, but it means an active scene only recolours the handful of
   rules written against --a1/--card/--txt. Everything styled with the BASE
   names — dropdowns, the leaderboard, dashboard stat cards, badge cards, the
   game grids — kept the default amber and looked untouched by the theme.
   That's the "still the old yellow" report.

   Fixing it per-rule would mean rewriting hundreds of declarations, so instead
   close the loop here: when a scene is active, point the base tokens AT the
   scene palette. One block, and every component follows the theme.

   Each scene block ([data-scene="…"] below) sets --stage/--bg2/--card/--card2/
   --edge/--edge2/--txt/--txt2/--txt3/--a1/--a2/--a3/--gold/--ok/--wrong, and
   those blocks come later in the file, so these references resolve to the
   ACTIVE scene's values, not the defaults. */
:root[data-scene] {
  /* NOT transparent. `body.scene-on { background:transparent }` already makes
     the PAGE ground show the art, so this token's only remaining job is the one
     it's actually used for in 56 rules: the fill of an inset panel (a stat tile,
     a mastery cell, a word-group card). Fully transparent, those panels vanish
     and their text lands straight on the illustration -- .wg-group-sub measured
     2.44:1 and .pt-mastery-particle 4.07:1 on light/terrace, varying card to
     card with whatever art was behind. A mostly-opaque tint gives them a real
     plate while still letting the scene tint through. */
  --paper:      color-mix(in srgb, var(--card) 82%, transparent);
  --paper-card: var(--card);
  --paper-2:    var(--card2);

  --ink:   var(--txt);
  --ink-2: var(--txt2);
  /* --ink-3 is the tertiary/label colour, and it carries far more real text than
     "tertiary" suggests: every .dash-stat-key, .sr-stat-key, .dash-card-title,
     .sr-card-title, .dq-count and .dash-focus-acc on the dashboard. Taken raw,
     13 of the 23 scene palettes put --txt3 at 3.59-4.49:1 against their own
     --card -- all just under AA, which is why the legibility sweep kept
     returning ~25 near-identical findings per scene.
     Fixed here rather than per-scene: pulling --txt3 a fixed distance toward
     --txt lifts every scene at once and keeps working for scenes added later,
     because --txt is by definition the high-contrast end in BOTH themes. 24%
     clears 4.8:1 on the worst palette (terrace) while leaving --ink-3 plainly
     lighter than --ink-2, so the type hierarchy survives.
     Measured by qa/legibility.js -- do not inline a flat colour here. */
  --ink-3: color-mix(in srgb, var(--txt) 24%, var(--txt3));
  /* NB: style text with --ink-3, never --txt3 directly. --txt3 is the raw
     scene value; --ink-3 is its AA-corrected derivation, so a rule reaching
     past it opts that text out of the fix — which is how .n5-pillar-left sat
     at 3.72:1 after every other label on the page had been corrected. */

  --line:   var(--edge);
  --line-2: var(--edge2);

  --accent:      var(--a1);
  --accent-ink:  var(--onr);
  --accent-wash:   color-mix(in srgb, var(--a1) 18%, var(--card));
  --accent-wash-2: color-mix(in srgb, var(--a1) 34%, var(--card));

  --clay:      var(--a2);
  --clay-wash: color-mix(in srgb, var(--a2) 20%, var(--card));
  --moss:      var(--ok, var(--a2));
  --moss-wash: color-mix(in srgb, var(--ok, var(--a2)) 20%, var(--card));
  --brick:      var(--wrong, var(--a2));
  --brick-wash: color-mix(in srgb, var(--wrong, var(--a2)) 20%, var(--card));
  --ochre:      var(--gold);
  --ochre-wash: color-mix(in srgb, var(--gold) 20%, var(--card));

  --grad-warm:      var(--reward);
  --grad-warm-ink:  var(--onr);
  --grad-warm-soft: color-mix(in srgb, var(--a1) 14%, transparent);

  --track: var(--card2);
}
/* The hero stat card inverts (ink ground, paper text) in the base look. Under a
   scene, --ink is the LIGHT text colour, so that inversion would render a
   near-white card with near-white text — unreadable. Re-point it at the scene's
   deep surface and give it the accent for its numbers. */
:root[data-scene] .dash-stat-card:first-child {
  background:var(--card2);
  border-color:var(--edge2);
}
/* Under a scene this hero card fills with --card2 (a deep surface), NOT --ink —
   so it needs text that contrasts with --card2, i.e. the scene's BODY text
   (--txt), like everything else on a card surface. The old --a1 (accent) here
   measured 1.66-1.87:1 on light scenes (theme-sweep) — the exact "--a1 is
   independent of the surface" trap. NOT --on-ink either: that's tuned for an
   --ink fill, and this card is --card2 under a scene. */
:root[data-scene] .dash-stat-card:first-child .dash-stat-val { color:var(--txt); }
:root[data-scene] .dash-stat-card:first-child .dash-stat-key { color:var(--txt2); }
:root[data-scene] .dash-stat-card:first-child:hover { border-color:var(--a1); }
/* a full-viewport themed sky wash sits behind the scene art so the backdrop
   reads as a complete background, not a bottom strip. Uses the scene stage
   tone (--stage set by each palette) fading to transparent at the top. */
/* NB the sky wash is a gradient, i.e. a background-IMAGE — and gradients do not
   interpolate between two different colour stops, so this surface cannot
   crossfade on a theme switch the way a flat background-color does. The colour
   is therefore driven by a solid `background-color` underneath (which DOES
   transition) with the gradient above it carrying only the shape, in
   transparent-to-black stops that work over either ground. */
#sceneLayer #bg{position:absolute;inset:0;z-index:0;overflow:hidden;pointer-events:none;
  background-color:var(--stage,#141018);
  background-image:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,.22) 55%, rgba(0,0,0,.42) 100%);}
/* The DAY wash is a second element stacked over the same box, faded in by
   OPACITY, rather than a background-image swap on #bg.
   Swapping the image is what produced the "snaps to white, then eases" flash:
   background-image can't interpolate, so the white overlay appeared on frame 1
   at full strength while the background-color underneath was still easing.
   Opacity does interpolate, so the light wash now arrives on the same ramp as
   everything else. */
#sceneLayer #bg::after{
  content:'';position:absolute;inset:0;pointer-events:none;opacity:0;
  background-image:linear-gradient(180deg, rgba(255,255,255,.55) 0%, rgba(255,255,255,.15) 55%, rgba(0,0,0,.06) 100%);}
:root[data-theme="light"][data-scene] #sceneLayer #bg::after{ opacity:1; }
  /* The scene SVG is only 480px tall and anchors to the BOTTOM
     (preserveAspectRatio="xMidYMax"), and every aurora blob was centred BELOW
     the viewport (108-120%). On a 900px window that left the top ~55% as flat
     --stage — measured rgb(8,6,16) on shinjuku, i.e. a black band above the
     skyline. Two blobs now sit high in the frame so the sky is lit all the way
     up; the original three still anchor the horizon glow. */
  #sceneLayer .aurora{position:absolute;inset:0;opacity:calc(.5*var(--gk));
    background:radial-gradient(900px 460px at 80% 108%,color-mix(in srgb,var(--a1) 55%,transparent),transparent 62%),
      radial-gradient(760px 440px at 12% 110%,color-mix(in srgb,var(--a2) 45%,transparent),transparent 60%),
      radial-gradient(700px 520px at 50% 120%,color-mix(in srgb,var(--a3) 40%,transparent),transparent 64%),
      radial-gradient(1100px 620px at 22% -12%,color-mix(in srgb,var(--a2) 26%,transparent),transparent 66%),
      radial-gradient(1000px 560px at 84% 4%,color-mix(in srgb,var(--a1) 20%,transparent),transparent 64%);
    animation:drift var(--drift) ease-in-out infinite alternate;transition:opacity .5s}
  @keyframes drift{from{transform:translateX(-3%) scale(1.06)}to{transform:translateX(3%) translateY(-2%) scale(1.13)}}
  #sceneLayer .scene{position:absolute;inset:0;display:none;align-items:flex-end}
  #sceneLayer .scene.on{display:flex}
  /* The svg uses preserveAspectRatio="…meet" (see mount() in theme_scenes.js),
     so it letterboxes rather than cropping: full width, height derived from the
     3:1 canvas, anchored to the bottom by .scene's align-items:flex-end. That
     keeps the edge landmarks (Tokyo Tower, the pagoda, the viaduct) on screen
     at every aspect ratio; #bg's sky-wash gradient fills above the band.
     max-height stops the band eating the whole screen on a tall/narrow window. */
  #sceneLayer .scene svg{width:100%;height:auto;max-height:100%;display:block;opacity:.72}
  /* Yamanote only: its loop is routed through the gutters BETWEEN panels and
     turns above the art band (the header sits at svg y≈-357, off-canvas), so
     the svg must not clip to its viewBox. Every other scene keeps the default
     clip — letting them all overflow would leak backdrop art over the header. */
  :root[data-scene="yamanote"] #sceneLayer .scene{overflow:visible}
  :root[data-scene="yamanote"] #sceneLayer .scene svg{overflow:visible}

  /* ── animated scene parts (cats, steam, lanterns) ── */
  .catTail{transform-origin:left center;animation:tailFlick 2.6s ease-in-out infinite}
  @keyframes tailFlick{0%,100%{transform:rotate(0)}45%{transform:rotate(-16deg)}70%{transform:rotate(8deg)}}

  /* ══ PIXEL SPRITE MOTION ══════════════════════════════════════════════════
     Pixel art animates by SWAPPING FRAMES, not by rotating limbs — a rotated
     limb resamples the pixel grid and turns crisp cells into smears. Each
     walker carries both frames stacked; these rules show one at a time with
     `steps()` so the change is instant, never a crossfade.

     Every scene names a DIFFERENT cadence class. That's deliberate: the old art
     drove nine separate figures off one shared hbob+hlegF+hlegB trio, so a
     monk, an office worker and a child all moved identically. Cadence is
     characterisation — a pilgrim plods, a commuter clips along, a child
     scampers. Same two frames, different rhythm and bob. */
  /* WALK CYCLE. Two fully-drawn poses (contact + passing) alternate, and the
     body rides a vertical bob that PEAKS on the passing pose — that coupling is
     what makes a walk read as weight transfer rather than a flicker.

     A previous version cut between the poses with steps(1) at up to 3Hz, which
     just strobed. Two changes fix it: the swap now happens over a very short
     ramp (so it reads as the limb arriving, not a jump cut), and the cadences
     are slower — a real walk is roughly 0.9-1.1s per stride, not 0.34s. */
  .wkB{opacity:0}
  @keyframes wkA{0%,46%{opacity:1}54%,100%{opacity:0}}
  @keyframes wkB{0%,46%{opacity:0}54%,100%{opacity:1}}
  /* bob peaks mid-stride (the passing pose), lowest at contact */
  @keyframes wkBobLow  {0%,100%{transform:translateY(.6px)}50%{transform:translateY(-1.6px)}}
  @keyframes wkBobMid  {0%,100%{transform:translateY(1px)} 50%{transform:translateY(-2.6px)}}
  @keyframes wkBobHigh {0%,100%{transform:translateY(1.4px)}50%{transform:translateY(-5px)}}
  /* a plod also rocks slightly side to side — weight shifting over each hip */
  @keyframes wkTrudge{0%,100%{transform:translateY(1px) rotate(-.8deg)}
                      50%{transform:translateY(-1.4px) rotate(.8deg)}}
  [class*="gait"]{transform-box:fill-box;transform-origin:bottom center}
  .gaitPlod   .wkA{animation:wkA 1.35s ease-in-out infinite}
  .gaitPlod   .wkB{animation:wkB 1.35s ease-in-out infinite}
  .gaitPlod        {animation:wkTrudge 1.35s ease-in-out infinite}
  .gaitStroll .wkA{animation:wkA 1.05s ease-in-out infinite}
  .gaitStroll .wkB{animation:wkB 1.05s ease-in-out infinite}
  .gaitStroll      {animation:wkBobLow 1.05s ease-in-out infinite}
  .gaitBrisk  .wkA{animation:wkA .78s ease-in-out infinite}
  .gaitBrisk  .wkB{animation:wkB .78s ease-in-out infinite}
  .gaitBrisk       {animation:wkBobMid .78s ease-in-out infinite}
  .gaitSkip   .wkA{animation:wkA .62s ease-in-out infinite}
  .gaitSkip   .wkB{animation:wkB .62s ease-in-out infinite}
  .gaitSkip        {animation:wkBobHigh .62s cubic-bezier(.3,1.5,.5,1) infinite}
  /* ── Per-creature motion. Each is bespoke: a loping bus-creature does not
     move like a swinging scout, which does not move like a bounding deer. The
     old art drove all of them off legL/legR/stride/swing. ── */
  .creatureLope{animation:creatureLope .62s cubic-bezier(.3,1.5,.5,1) infinite}
  @keyframes creatureLope{0%,100%{transform:translateY(0) scaleY(1)}
    30%{transform:translateY(-9px) scaleY(1.04)}55%{transform:translateY(0) scaleY(.96)}}
  /* body heat rolling off the giant — rises, widens and thins out */
  .giantSteam{animation:giantSteam 6.5s ease-out infinite;transform-origin:bottom center}
  @keyframes giantSteam{0%{transform:translateY(6px) scaleX(.8);opacity:0}
    18%{opacity:.5}60%{transform:translateY(-26px) scaleX(1.25);opacity:.28}
    100%{transform:translateY(-58px) scaleX(1.6);opacity:0}}
  .odmArc{animation:odmArc 1.5s ease-in-out infinite}
  @keyframes odmArc{0%,100%{transform:rotate(-7deg) translateY(0)}
    45%{transform:rotate(5deg) translateY(-14px)}}
  @media (prefers-reduced-motion:reduce){
    /* Naming the animations one at a time has lost this race twice: every new
       scene adds classes, and this list covered four of them while .aurora,
       .ypWheel, .ypSpin, .fwBurst, .blLamp, .blDolly, .catWalk, .catBob,
       .sway, .steam, .lantSwing, .crabLeg, .glicoRun and .wheel all kept
       running. The scene layer is decoration by definition (#sceneLayer is
       z-index:-1, pointer-events:none, aria-hidden), so scope the rule to the
       layer rather than trying to keep an inventory of it.
       buildParticles() and scheduleCameos() already bail on the preference in
       JS, so there is no .pcl or cameo to freeze mid-flight; this catches the
       in-SVG animations and .aurora, which mount() injects unconditionally. */
    #sceneLayer *, #sceneLayer *::before, #sceneLayer *::after{animation:none !important}
    /* .wkB is a POSE, not a motion — it hides the far leg when the swing
       stops, so it has to stay. */
    .wkB{opacity:0}
  }
  /* ── Yume Park ── slow wheel, carousel spin, firework bursts ── */
  .ypWheel{animation:ypWheel 26s linear infinite}
  @keyframes ypWheel{to{transform:rotate(360deg)}}
  .ypSpin{animation:ypSpin 9s linear infinite}
  @keyframes ypSpin{0%{transform:scaleX(1)}49%{transform:scaleX(.06)}50%{transform:scaleX(-.06)}99%{transform:scaleX(-1)}100%{transform:scaleX(-1)}}
  .fwBurst{animation:fwBurst 5.5s ease-out infinite;opacity:0}
  @keyframes fwBurst{0%{transform:scale(.15);opacity:0}8%{opacity:1}30%{transform:scale(1);opacity:.95}55%{transform:scale(1.25);opacity:0}100%{opacity:0}}
  /* ── Studio Backlot ── the 本番 lamp pulses, the dolly tracks slowly ── */
  .blLamp{animation:blLamp 3.4s ease-in-out infinite}
  @keyframes blLamp{0%,100%{opacity:.45}50%{opacity:1}}
  .blDolly{animation:blDolly 19s ease-in-out infinite}
  @keyframes blDolly{0%,100%{transform:translateX(0)}50%{transform:translateX(126px)}}
  /* ── Mahōtokoro ── candles bob independently on their own delays ── */
  .mhFloat{animation:mhFloat 6.5s ease-in-out infinite}
  @keyframes mhFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-13px)}}
  .catEar{transform-origin:bottom center;animation:earTwitch 4s ease-in-out infinite}
  @keyframes earTwitch{0%,88%,100%{transform:rotate(0)}92%{transform:rotate(-9deg)}96%{transform:rotate(5deg)}}
  .catBlink{animation:blink 4.5s ease-in-out infinite}
  @keyframes blink{0%,94%,100%{transform:scaleY(1)}97%{transform:scaleY(.1)}}
  .catWalk{animation:walk 22s linear infinite}
  @keyframes walk{0%{transform:translateX(-120px)}100%{transform:translateX(1500px)}}
  .catBob{animation:bob 1.1s ease-in-out infinite}
  @keyframes bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}
  .sway{transform-origin:top center;animation:sway 5s ease-in-out infinite}
  @keyframes sway{0%,100%{transform:rotate(-3deg)}50%{transform:rotate(3deg)}}
  .steam{transform-origin:bottom center;animation:steamRise 3.2s ease-out infinite}
  @keyframes steamRise{0%{opacity:0;transform:translateY(0) scaleX(1)}30%{opacity:.55}100%{opacity:0;transform:translateY(-46px) scaleX(1.5)}}
  .lantSwing{transform-origin:top center;animation:sway 4.2s ease-in-out infinite}
  /* Osaka crab: claws pinch, legs scuttle, Glico man pumps */
  .crabClawL{animation:pinchL 1.6s ease-in-out infinite}
  @keyframes pinchL{0%,100%{transform:rotate(0)}50%{transform:rotate(-14deg)}}
  .crabClawR{animation:pinchR 1.6s ease-in-out infinite}
  @keyframes pinchR{0%,100%{transform:rotate(0)}50%{transform:rotate(14deg)}}
  .crabLeg{animation:scuttle 1.2s ease-in-out infinite}
  @keyframes scuttle{0%,100%{transform:translateY(0)}50%{transform:translateY(-2px)}}
  .glicoRun{animation:glicoPump 2.4s ease-in-out infinite}
  @keyframes glicoPump{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}
  /* birds flap; walkers gently bob as they stride */
  .swing{transform-origin:70% 30%;animation:swing 1.4s ease-in-out infinite}
  @keyframes swing{0%,100%{transform:rotate(-8deg)}50%{transform:rotate(8deg)}}
  /* ── distinct cameo entrances (replacing the one repeated 'peek') ──
     #sceneLayer .cameo carries the entrance via these classes; the per-art inner animation
     (bob/blink/etc) still plays on top. */
  #sceneLayer .cameo.float{animation:cFloat var(--dur,7s) ease-in-out forwards;transform:translateY(120%)}
  @keyframes cFloat{0%{transform:translateY(120%) rotate(-6deg);opacity:0}15%{opacity:1}25%{transform:translateY(-6px) rotate(3deg)}50%{transform:translateY(4px) rotate(-3deg)}75%{transform:translateY(-4px) rotate(2deg);opacity:1}100%{transform:translateY(-40px) rotate(0);opacity:0}}
  #sceneLayer .cameo.pounce{animation:cPounce var(--dur,4.5s) cubic-bezier(.3,1.6,.5,1) forwards;transform:translateY(120%)}
  @keyframes cPounce{0%{transform:translateY(120%)}18%{transform:translateY(-24px)}30%{transform:translateY(0)}45%{transform:translateY(-12px)}58%{transform:translateY(0)}80%{transform:translateY(0)}100%{transform:translateY(120%)}}
  #sceneLayer .cameo.scatter{animation:cScatter var(--dur,5s) ease-out forwards;transform:translateY(120%)}
  @keyframes cScatter{0%{transform:translateY(120%);opacity:0}20%{transform:translateY(-4px);opacity:1}30%{transform:translate(6px,0)}40%{transform:translate(-6px,-3px)}50%{transform:translate(4px,2px)}60%{transform:translate(-3px,-2px)}75%{transform:translateY(0);opacity:1}100%{transform:translateY(-30px);opacity:0}}
  #sceneLayer .cameo.boing{animation:cBoing var(--dur,4.5s) cubic-bezier(.34,1.8,.5,1) forwards;transform:translateY(130%) scale(.4)}
  @keyframes cBoing{0%{transform:translateY(130%) scale(.4)}22%{transform:translateY(-14px) scale(1.15)}34%{transform:translateY(0) scale(.94)}44%{transform:translateY(-4px) scale(1.03)}54%{transform:translateY(0) scale(1)}80%{transform:translateY(0) scale(1)}100%{transform:translateY(130%) scale(.4)}}
  #sceneLayer .cameo.dropIn{animation:cDropIn var(--dur,7s) ease-in forwards;transform:translateY(-130%)}
  @keyframes cDropIn{0%{transform:translateY(-130%) rotate(-10deg)}20%{transform:translateY(0) rotate(4deg)}30%{transform:translateY(-8px) rotate(-3deg)}42%{transform:translateY(0) rotate(0)}78%{transform:translateY(0)}100%{transform:translateY(-130%) rotate(8deg)}}
  #sceneLayer .cameo.launch{animation:cLaunch var(--dur,4s) cubic-bezier(.2,.7,.3,1) forwards;transform:translateY(120%)}
  @keyframes cLaunch{0%{transform:translateY(120%);opacity:0}18%{opacity:1}45%{transform:translateY(-40vh);opacity:1}70%{transform:translateY(-58vh);opacity:.6}100%{transform:translateY(-64vh);opacity:0}}
  #sceneLayer .cameo.bowin{animation:cBowin var(--dur,6s) ease-in-out forwards;transform:translateY(120%)}
  @keyframes cBowin{0%{transform:translateY(120%)}16%{transform:translateY(0)}84%{transform:translateY(0)}100%{transform:translateY(120%)}}
  .officeBow{animation:officeBow var(--dur,6s) ease-in-out forwards}
  @keyframes officeBow{0%,30%{transform:rotate(0)}44%{transform:rotate(22deg)}60%{transform:rotate(22deg)}72%,100%{transform:rotate(0)}}
  .flap{transform-origin:center;animation:flap .5s ease-in-out infinite}
  @keyframes flap{0%,100%{transform:scaleY(1)}50%{transform:scaleY(.55)}}
  .stride{animation:stride .7s ease-in-out infinite}
  @keyframes stride{0%,100%{transform:translateY(0)}50%{transform:translateY(-1.5px)}}
  .legL{transform-origin:top center;animation:legswing .55s ease-in-out infinite}
  .legR{transform-origin:top center;animation:legswing .55s ease-in-out infinite reverse}
  @keyframes legswing{0%,100%{transform:rotate(12deg)}50%{transform:rotate(-12deg)}}
  /* deer diagonal walking gait — front-right+back-left in phase; the other
     diagonal offset by half a cycle. Small swing from the shoulder/hip. */
  .deerFR,.deerBL{transform-origin:top center;animation:deerStep .8s ease-in-out infinite}
  .deerFL,.deerBR{transform-origin:top center;animation:deerStep .8s ease-in-out infinite;animation-delay:.4s}
  @keyframes deerStep{0%,100%{transform:rotate(11deg)}50%{transform:rotate(-11deg)}}
  /* human walk — legs swing from the hip, arms counter-swing, opposite phases,
     plus a small up-down bob. Each limb pivots from its TOP (fill-box). */
  .hbob{animation:hbob .9s ease-in-out infinite}
  @keyframes hbob{0%,100%{transform:translateY(0)}25%,75%{transform:translateY(-1.2px)}}
  .hlegF,.harmB{transform-origin:top center;animation:hswing .9s ease-in-out infinite}
  .hlegB,.harmF{transform-origin:top center;animation:hswing .9s ease-in-out infinite;animation-delay:.45s}
  @keyframes hswing{0%,100%{transform:rotate(16deg)}50%{transform:rotate(-16deg)}}

  /* floating particles (petals / lanterns / bubbles / sparks) */
  #sceneLayer .pcl{position:absolute;top:-6%;opacity:0;animation:fall linear infinite}
  @keyframes fall{5%{opacity:var(--po,.55)}50%{transform:translate(24px,52vh) rotate(160deg)}92%{opacity:var(--po,.55)}100%{transform:translate(46px,110vh) rotate(320deg);opacity:0}}
  @keyframes rise{5%{opacity:var(--po,.5)}92%{opacity:var(--po,.5)}100%{transform:translate(-30px,-115vh);opacity:0}}

  /* ── dynamic cameo layer: carts/bikes/cats that roll in, pause, roll out ── */
  #sceneLayer #fx{position:absolute;inset:0;z-index:1;overflow:hidden;pointer-events:none}

  /* ── Phones: keep the scene, drop the moving parts ──────────────────────
     #sceneLayer is deliberately NOT display:none the way #bgScenes (:264) and
     .sakura-layer (:303) are, because it carries the page GROUND:
     `body.scene-on { background:transparent !important }` above means
     #sceneLayer #bg's `background-color:var(--stage)` is the only opaque
     surface left in the chain. Hiding the layer renders a dark scene on the
     UA default WHITE, with translucent dark cards over it.
     So the ground, the sky wash, the day overlay and the landmark band all
     stay — the scene still reads as the scene — and only the continuously
     animating decoration goes. Measured on a resting desktop dashboard: 28 CSS
     animations still running with nobody interacting, which is what the ~3%
     idle CPU in docs/PERF_REPORT.md was.
     Note this is CSS only, so it stops the compositor work, not the JS: the
     cameo scheduler and buildParticles still run (both already bail under
     prefers-reduced-motion). */
  @media (max-width:720px) {
    /* cameo sprites: absolutely-positioned SVGs on 3-13s keyframes, two of
       which translate 115vw off-screen, re-scheduled every 2.5-9.5s */
    #sceneLayer #fx{display:none}
    /* 16 ambient particles per mount, each on a 10-20s infinite fall/rise */
    #sceneLayer .pcl{display:none}
    /* five stacked radial gradients over the full viewport, translated AND
       scaled — the most expensive paint in the layer, because the scale forces
       a re-raster of the whole thing every frame */
    #sceneLayer .aurora{display:none}
  }
  /* STATIC elevated bridge, shown only for train destinations. Its rail (the top
     line) sits at 14vh — exactly where the train cameo's wheel-bottoms land, so
     the train always rides ON it. Lives in the same bottom-based space as .cameo. */
  #bridge{position:fixed;left:0;right:0;bottom:14vh;height:0;z-index:-1;display:none;pointer-events:none}
  body[data-dest="tokyo"] #bridge,body[data-dest="shinjuku"] #bridge{display:block}
  /* NOTE: bridge parts use b-prefixed classes to avoid colliding with the
     .rail destination-toolbar at the top of the page (that collision was
     inflating the "rail" into a padded flex block). */
  #bridge .brail{position:absolute;left:0;right:0;top:0;height:3px;background:#8a939e}
  #bridge .bleg{position:absolute;top:2px;width:11px;height:16vh;background:#252c38;opacity:.85}
  #sceneLayer .cameo{position:absolute;bottom:8vh;will-change:transform}
  /* Rail cameos must sit ON the viaduct, and the viaduct is drawn in the scene
     SVG's coordinate space — which scales with viewport WIDTH (the art band is
     `width:100%` on a 1440x480 canvas, preserveAspectRatio meet). A fixed `vh`
     offset therefore drifts off the deck as the window changes shape: the deck
     lands anywhere from ~10.9vh to ~14.1vh across common sizes.
     Deriving it from vw instead keeps the train welded to the rail:
       band height = 100vw * (480/1440) = 33.333vw
       deck offset = (480 - deckY)/480 * band height
     --rail-y is the resulting bottom offset; each scene sets its own deck Y. */
  #sceneLayer .cameo.on-rail{ bottom:var(--rail-y); }
  #sceneLayer .cameo svg{display:block;opacity:.82}
  /* Sprites are authored facing RIGHT; one travelling left is mirrored so it
     faces where it's going. Applied to the <svg> and not the .cameo wrapper,
     because the wrapper carries the travel transform — scaling that would
     reverse the direction of motion itself. */
  #sceneLayer .cameo.face-left svg{transform:scaleX(-1)}
  /* enters from a side, pauses in view, exits — direction set per event */
  #sceneLayer .cameo.fromR{right:-320px;animation:rollFromR var(--dur,13s) ease-in-out forwards}
  @keyframes rollFromR{0%{transform:translateX(0)}18%{transform:translateX(calc(-1 * var(--stop,420px)))}82%{transform:translateX(calc(-1 * var(--stop,420px)))}100%{transform:translateX(0)}}
  #sceneLayer .cameo.fromL{left:-320px;animation:rollFromL var(--dur,13s) ease-in-out forwards}
  @keyframes rollFromL{0%{transform:translateX(0)}18%{transform:translateX(var(--stop,420px))}82%{transform:translateX(var(--stop,420px))}100%{transform:translateX(0)}}
  /* fast pass-through (bike / shooting element) */
  #sceneLayer .cameo.zipL{right:-260px;animation:zipL var(--dur,3.2s) linear forwards}
  @keyframes zipL{from{transform:translateX(0)}to{transform:translateX(-115vw)}}
  #sceneLayer .cameo.zipR{left:-260px;animation:zipR var(--dur,3.2s) linear forwards}
  @keyframes zipR{from{transform:translateX(0)}to{transform:translateX(115vw)}}
  /* peek up from the bottom, then duck back */
  #sceneLayer .cameo.peek{animation:peek var(--dur,4.5s) var(--ease-bounce) forwards;transform:translateY(120%)}
  @keyframes peek{0%{transform:translateY(120%)}22%{transform:translateY(0)}78%{transform:translateY(0)}100%{transform:translateY(120%)}}
  /* drift ONLY across the canal water strip (25%–75% of width), fading at the
     ends so it never appears on the land banks. */
  #sceneLayer .cameo.driftWater{left:24%;animation:driftWater var(--dur,12s) linear forwards}
  @keyframes driftWater{0%{transform:translateX(0);opacity:0}10%{opacity:.9}90%{opacity:.9}100%{transform:translateX(48vw);opacity:0}}
  /* only spoked wheels spin, and they rotate about THEIR OWN centre (fill-box)
     so it reads as a wheel turning, not the whole shape orbiting. */
  #sceneLayer .wheel{transform-box:fill-box;transform-origin:center;animation:spin 1.6s linear infinite}
  @keyframes spin{to{transform:rotate(360deg)}}

  
/* Station-name plate that pops when the train stops.
   It sat at z-index:-1 — the same layer as #sceneLayer — so it rendered BEHIND
   the opaque page content and was never actually visible. It belongs above the
   backdrop but below the UI, and it's a decorative overlay, so it also needs
   pointer-events:none (already set) and to follow the scene palette rather than
   being hardcoded Yamanote green. */
/* Station-name plate, shown while the train dwells at a stop.
   Two bugs kept this invisible even after the first fix:
     1. z-index. It was -1 (behind everything), then 6 — but #appShell fills the
        viewport with opaque content, so anything below its paint order is
        covered regardless. The label is a decorative overlay pinned to the
        train, so it belongs with the other floating chrome: 55, under modals
        (58+) but above the page.
     2. `var(--bounce)` is not a token in this file — it's `--ease-bounce`. An
        invalid var() in a shorthand invalidates the WHOLE declaration, so the
        transition never applied and the plate had no reveal at all.
   pointer-events:none keeps it non-interactive; colours come from the scene
   palette so it works under all 22 themes in both light and dark. */
/* Station-name plate, shown while the train dwells at a stop.
   It lives INSIDE #sceneLayer (see mount()), so it shares the train's stacking
   context: absolute to the layer, above #bg (0) and #fx (1). Previously it was
   fixed to <body> at a high z-index, which put it in front of the page while
   the train sat behind it at z-index -1 — the plate floated over the UI,
   disconnected from what it was pointing at.
   Colours come from the scene palette, so it works under all 22 themes in both
   modes. pointer-events:none keeps it decorative. */
#ymLabel{position:absolute;z-index:3;transform:translate(-50%,-140%) scale(.6);opacity:0;
    pointer-events:none;transition:transform .25s var(--ease-bounce),opacity .25s;
    background:color-mix(in srgb, var(--card) 92%, #000);
    border:2px solid var(--a1);border-radius:var(--rBtn,8px);padding:5px 12px;white-space:nowrap;
    box-shadow:0 0 18px -4px var(--a1), 0 6px 18px -8px rgba(0,0,0,.55)}
  /* The gutter the rail runs down is only ~23px wide and the plate is ~130px,
     so it cannot fit beside the rail on either side — left puts it under the
     sidebar card, right under the main panel. It therefore sits ABOVE the rail
     (centred, pointer down), which is the one direction with real room, and
     the loop's top run is placed low enough to leave that room. */
  #ymLabel.show{opacity:1;transform:translate(-50%,-140%) scale(1)}
  /* Stacked, not inline. Side by side the plate was ~132px wide and the gutter
     it has to fit in is ~23px, so it always straddled a panel. Stacking the
     kanji over the romaji roughly halves the width and lets the name sit inside
     the channel instead of half-hidden under a card. */
  #ymLabel{display:flex;flex-direction:column;align-items:center;gap:1px;padding:4px 9px;line-height:1.15}
  #ymLabel .jp{font-size:.9rem;font-weight:700;color:var(--txt);font-family:'Noto Sans JP',sans-serif}
  #ymLabel .rom{font-size:.54rem;color:var(--a1);letter-spacing:.06em;white-space:nowrap}
  #ymLabel::after{content:'';position:absolute;left:50%;bottom:-7px;transform:translateX(-50%);
    border:6px solid transparent;border-top-color:var(--a1)}
  /* Hide the plate outside its own scene.
     THE BUG THAT MADE THIS INVISIBLE FOR THREE ATTEMPTS: this selector checked
     `body[data-scene]`, but mount() sets data-scene on the ROOT element
     (<html>), never on <body>. So :not(...) matched unconditionally and
     display:none applied ALWAYS — the plate could not render at any z-index, in
     any parent. Both earlier "fixes" (stacking, then reparenting) were real
     improvements to real problems, but neither could ever have made it appear.
     Keyed on :root now, matching where the attribute actually lives. */
  :root:not([data-scene="yamanote"]) #ymLabel{display:none}

/* ── per-scene palettes ── */
:root[data-scene="shinjuku"]{
    --stage:#0a0713;--bg2:#120b20;--card:#1a1030;--card2:#241542;--edge:#33215c;--edge2:#4a2f80;--track:#080510;
    --txt:#f6efff;--txt2:#c3b0e6;--txt3:#8674ad;
    --a1:#ff2d95;--a2:#22e0ff;--a3:#a855f7;--ok:#39ff14;--gold:#ffe14d;--wrong:#ff2e63;
    --reward:linear-gradient(120deg,#ff2d95,#a855f7 55%,#22e0ff);--onr:#0a0713;
    --gk:1.9;--drift:9s;--kanaGlow:0 0 26px rgba(34,224,255,.9);
  }
:root[data-scene="temple"]{
    --stage:#1a1712;--bg2:#221d17;--card:#2a231b;--card2:#332b21;--edge:#3d3427;--edge2:#524634;--track:#141009;
    --txt:#f2e9d8;--txt2:#c9baa0;--txt3:#93866c;
    --a1:#c14a34;--a2:#7a9b6e;--a3:#c99a3a;--ok:#7a9b6e;--gold:#c99a3a;--wrong:#b34a3a;
    --reward:linear-gradient(120deg,#a53d28,#c14a34 55%,#8f6d29);--onr:#faf4e6;
    --gk:.4;--drift:26s;--kanaGlow:0 0 10px rgba(201,154,58,.4);
  }
:root[data-scene="sakura"]{
    /* scene-art tokens (read by #sceneLayer + cameos) */
    --stage:#14121a;--bg2:#1b1824;--card:#231f30;--card2:#2c2740;--edge:#3a3350;--edge2:#4d4468;--track:#0f0d15;
    --txt:#fceef4;--txt2:#d6c3d2;--txt3:#93849a;
    --a1:#ff88b0;--a2:#7bb8e0;--a3:#8fd6a0;--ok:#8fd6a0;--gold:#f2c94c;--wrong:#ff7a95;
    --reward:linear-gradient(120deg,#ff88b0,#ffb0c8 50%,#f2c94c);--onr:#2a0f1a;
    --gk:.7;--drift:20s;--kanaGlow:0 0 16px rgba(255,136,176,.5);
    /* NO app-token remap here. Sakura predates the generic `:root[data-scene]`
       block above, and used to carry its own hand-written copy of it: --paper,
       --paper-card, --ink*, --line*, --accent*, --clay/--moss/--brick/--ochre,
       --grad-warm. Same specificity as the generic block but declared later, so
       it won every time — which quietly opted the one scene out of every
       token-level fix made there. It was still pinning --ink-3 to the raw --txt3
       (#93849a) after that token had been corrected everywhere else, and dark
       sakura was the only scene left with 18 contrast findings because of it.
       The generic block derives all of these from the scene-art tokens above.
       --sh-float is the only one it doesn't cover, so it stays. */
    --sh-float:0 16px 34px -16px rgba(0,0,0,.5),0 2px 6px -2px rgba(0,0,0,.4);
  }
:root[data-scene="catcafe"]{
    --stage:#20141c;--bg2:#291a24;--card:#33212d;--card2:#3e2937;--edge:#4b3243;--edge2:#634159;--track:#180f16;
    --txt:#fdeef5;--txt2:#e0c4d4;--txt3:#a88499;
    --a1:#ff9ec4;--a2:#7fd8c8;--a3:#c9a0e8;--ok:#8fe0b0;--gold:#ffcf6b;--wrong:#ff8fa8;
    --reward:linear-gradient(120deg,#ff9ec4,#ffbcd6 50%,#ffcf6b);--onr:#3a0f22;
    --gk:.85;--drift:17s;--kanaGlow:0 0 16px rgba(255,158,196,.55);
  }
:root[data-scene="osaka"]{
    --stage:#0f0d14;--bg2:#171320;--card:#1f1826;--card2:#2a2033;--edge:#3a2b3f;--edge2:#523a55;--track:#0b0910;
    --txt:#fdf3e8;--txt2:#d5c3b0;--txt3:#9a8676;
    --a1:#ff3b3b;--a2:#ffd23f;--a3:#2d9cdb;--ok:#5ec26a;--gold:#ffd23f;--wrong:#ff3b3b;
    --reward:linear-gradient(120deg,#ff3b3b,#ff8c1a 50%,#ffd23f);--onr:#1a0606;
    --gk:1.6;--drift:11s;--kanaGlow:0 0 20px rgba(255,210,63,.7);
  }
:root[data-scene="tokyo"]{
    --stage:#0d1018;--bg2:#131824;--card:#1a2030;--card2:#232b3f;--edge:#2f3a52;--edge2:#41506e;--track:#0a0d14;
    --txt:#eef2fa;--txt2:#b9c4d8;--txt3:#7d889c;
    --a1:#f2683c;--a2:#3fb8c9;--a3:#5b8cff;--ok:#5ec8a0;--gold:#f2b84c;--wrong:#f2683c;
    --reward:linear-gradient(120deg,#5b8cff,#3fb8c9 55%,#f2b84c);--onr:#0d1018;
    --gk:1.1;--drift:14s;--kanaGlow:0 0 18px rgba(63,184,201,.55);
  }
:root[data-scene="food"]{
    --stage:#170f0a;--bg2:#1f150d;--card:#281b11;--card2:#332318;--edge:#402c1c;--edge2:#573c26;--track:#120b06;
    --txt:#f8ece0;--txt2:#d6bda2;--txt3:#a08469;
    --a1:#d95d2a;--a2:#e8a838;--a3:#8a4a2a;--ok:#a3b048;--gold:#f0b429;--wrong:#c0432a;
    --reward:linear-gradient(120deg,#aa6e57,#d95d2a 45%,#f0b429);--onr:#faf0e0;
    --gk:1.0;--drift:15s;--kanaGlow:0 0 14px rgba(240,180,41,.5);
  }
:root[data-scene="meadow"]{
    --stage:#16221a;--bg2:#1c2c20;--card:#243529;--card2:#2d4032;--edge:#37493c;--edge2:#4a6150;--track:#111a14;
    --txt:#f0f6e9;--txt2:#c6d6bd;--txt3:#8ba081;
    --a1:#7bc86a;--a2:#7fbce0;--a3:#e0c06a;--ok:#8fd67a;--gold:#f0d264;--wrong:#e08a6a;
    --reward:linear-gradient(120deg,#7bc86a,#a8dc7a 50%,#f0d264);--onr:#132012;
    --gk:.6;--drift:20s;--kanaGlow:0 0 14px rgba(123,200,106,.5);
  }
:root[data-scene="bathhouse"]{
    --stage:#0e1226;--bg2:#141a33;--card:#1c2442;--card2:#262f52;--edge:#333d63;--edge2:#465285;--track:#0a0d1e;
    --txt:#f4eee0;--txt2:#cabfa8;--txt3:#8a8298;
    --a1:#e05a3c;--a2:#f0b429;--a3:#6a7ad0;--ok:#6ec2a0;--gold:#f5c542;--wrong:#e0483c;
    --reward:linear-gradient(120deg,#e05a3c,#f0873c 50%,#f5c542);--onr:#160a08;
    --gk:1.3;--drift:16s;--kanaGlow:0 0 20px rgba(240,180,41,.6);
  }
:root[data-scene="sky"]{
    --stage:#241a2a;--bg2:#2f2236;--card:#3a2b42;--card2:#463550;--edge:#553f60;--edge2:#6f537c;--track:#1c1420;
    --txt:#fceee6;--txt2:#e0c6c0;--txt3:#a88c9a;
    --a1:#f2955c;--a2:#7fc6d6;--a3:#d6a86a;--ok:#8fd6b0;--gold:#f2c96a;--wrong:#f0765c;
    --reward:linear-gradient(120deg,#f2955c,#f7b878 50%,#f2c96a);--onr:#241014;
    --gk:1.1;--drift:22s;--kanaGlow:0 0 16px rgba(242,149,92,.55);
  }
:root[data-scene="rooftop"]{
    --stage:#1a1826;--bg2:#221f33;--card:#2b2740;--card2:#352f4e;--edge:#40385a;--edge2:#544a72;--track:#141220;
    --txt:#fbeef2;--txt2:#d8c6d4;--txt3:#988ba0;
    --a1:#ff8a5c;--a2:#6aa8e0;--a3:#c98ad6;--ok:#7fd0a0;--gold:#ffc46a;--wrong:#ff6a7a;
    --reward:linear-gradient(120deg,#ff8a5c,#ffb07a 45%,#ffc46a);--onr:#241012;
    --gk:1.2;--drift:18s;--kanaGlow:0 0 18px rgba(255,138,92,.55);
  }
:root[data-scene="classroom"]{
    --stage:#1e2420;--bg2:#26302a;--card:#2f3a33;--card2:#39463d;--edge:#43524a;--edge2:#586b5f;--track:#171d19;
    /* --txt3 lightened #8b988a → #b0bba8: the old value was ~3.1:1 on --card
       (under AA), failing .sr-card-title/.sr-card-hint (docs/TODO.MD). --txt2 sits
       at 7.6:1 so there's headroom; this stays below it, keeping the hierarchy.
       Monotonic improvement — re-confirm exact ratio in the next theme-sweep. */
    --txt:#f1f4ea;--txt2:#c8d2c0;--txt3:#b0bba8;
    --a1:#e0a850;--a2:#6aa8c0;--a3:#8fb87a;--ok:#8fc87a;--gold:#f0c860;--wrong:#e08a6a;
    --reward:linear-gradient(120deg,#e0a850,#eac878 55%,#8fb87a);--onr:#1c1a12;
    --gk:.7;--drift:20s;--kanaGlow:0 0 12px rgba(224,168,80,.4);
  }
:root[data-scene="office"]{
    --stage:#1a1d24;--bg2:#22262f;--card:#2b303a;--card2:#353b47;--edge:#414855;--edge2:#556070;--track:#14171d;
    --txt:#eef1f6;--txt2:#c2c9d4;--txt3:#868f9c;
    --a1:#5b8cd0;--a2:#4bb0a0;--a3:#e0a860;--ok:#5ec26a;--gold:#e8b44c;--wrong:#e0705c;
    --reward:linear-gradient(120deg,#5b8cd0,#4bb0a0 55%,#e8b44c);--onr:#14171d;
    --gk:.85;--drift:18s;--kanaGlow:0 0 12px rgba(91,140,208,.4);
  }
:root[data-scene="yamanote"]{
    --stage:#131a14;--bg2:#19231a;--card:#212d22;--card2:#2b3a2c;--edge:#354736;--edge2:#476149;--track:#0e140f;
    --txt:#eef6ea;--txt2:#c4d6be;--txt3:#849a80;
    --a1:#80c342;--a2:#e8b44c;--a3:#5ec2a0;--ok:#80c342;--gold:#f0c860;--wrong:#e0705c;
    --reward:linear-gradient(120deg,#5a9e2e,#80c342 55%,#b8e070);--onr:#0e140f;
    --gk:1.3;--drift:12s;--kanaGlow:0 0 16px rgba(128,195,66,.5);
  }
:root[data-scene="sushi"]{
    --stage:#1c1712;--bg2:#241d16;--card:#2d251b;--card2:#382e22;--edge:#443829;--edge2:#5a4b37;--track:#150f0a;
    --txt:#f6eede;--txt2:#d4c4a8;--txt3:#a08a6c;
    --a1:#e0705c;--a2:#6ab0c0;--a3:#e8b44c;--ok:#9cc06a;--gold:#f0c860;--wrong:#d05545;
    --reward:linear-gradient(120deg,#e0705c,#f0946a 50%,#f0c860);--onr:#1a1008;
    --gk:1.0;--drift:14s;--kanaGlow:0 0 14px rgba(224,112,92,.45);
  }
:root[data-scene="gadget"]{
    --stage:#0f1c2a;--bg2:#152636;--card:#1d3244;--card2:#274056;--edge:#345067;--edge2:#456a87;--track:#0a1420;
    --txt:#eef6fb;--txt2:#bcd2e0;--txt3:#7e97a8;
    --a1:#f24a4a;--a2:#28c0e8;--a3:#f5d020;--ok:#5ec26a;--gold:#f5d020;--wrong:#f24a4a;
    --reward:linear-gradient(120deg,#28c0e8,#5ad0f0 50%,#f5d020);--onr:#0a1420;
    --gk:1.5;--drift:11s;--kanaGlow:0 0 20px rgba(40,192,232,.6);
  }
:root[data-scene="edo"]{
    --stage:#12130f;--bg2:#181a14;--card:#20231b;--card2:#2a2e24;--edge:#34392c;--edge2:#474e3b;--track:#0d0e0a;
    --txt:#eeece0;--txt2:#c4bfa8;--txt3:#8a8672;
    --a1:#c0392b;--a2:#5a7a9a;--a3:#c99a3a;--ok:#7a9b6e;--gold:#c99a3a;--wrong:#c0392b;
    --reward:linear-gradient(120deg,#8a2018,#c0392b 55%,#8f6d29);--onr:#f4efe2;
    --gk:.9;--drift:17s;--kanaGlow:0 0 14px rgba(201,154,58,.45);
  }
:root[data-scene="titans"]{
    --stage:#1c1a15;--bg2:#26231c;--card:#2f2b22;--card2:#3a342a;--edge:#453e30;--edge2:#5c5240;--track:#161410;
    --txt:#f0ece0;--txt2:#cabfa6;--txt3:#93876c;
    --a1:#b03a2a;--a2:#7a8a6a;--a3:#c99a5a;--ok:#8a9b5e;--gold:#d0a850;--wrong:#c0392b;
    --reward:linear-gradient(120deg,#7a2418,#b03a2a 55%,#896f35);--onr:#f0ece0;
    --gk:1.4;--drift:24s;--kanaGlow:0 0 16px rgba(176,58,42,.5);
  }
:root[data-scene="yumepark"]{
    --stage:#120a26;--bg2:#1b1038;--card:#241748;--card2:#31205e;--edge:#432c7a;--edge2:#5c3da4;--track:#0d0720;
    --txt:#f8eeff;--txt2:#cdb8e8;--txt3:#9280b4;
    --a1:#ff8ad0;--a2:#8ef0ff;--a3:#9affc0;--ok:#9affc0;--gold:#ffd76a;--wrong:#ff5a7a;
    --reward:linear-gradient(120deg,#ff8ad0,#ffd76a 55%,#8ef0ff);--onr:#231044;
    --gk:1.7;--drift:11s;--kanaGlow:0 0 24px rgba(255,138,208,.8);
  }
:root[data-scene="backlot"]{
    --stage:#141a28;--bg2:#1b2233;--card:#232b3e;--card2:#2e3750;--edge:#39435c;--edge2:#4d5a78;--track:#0f1420;
    --txt:#eef2f8;--txt2:#bcc6d8;--txt3:#8592a8;
    --a1:#e8b23c;--a2:#5a9ec8;--a3:#e0705c;--ok:#7fc08a;--gold:#f0c860;--wrong:#e0554a;
    --reward:linear-gradient(120deg,#c2871f,#e8b23c 55%,#ffe9a8);--onr:#141a28;
    --gk:1.1;--drift:16s;--kanaGlow:0 0 16px rgba(232,178,60,.55);
  }
:root[data-scene="mahou"]{
    --stage:#141826;--bg2:#1b2032;--card:#232941;--card2:#2e3552;--edge:#3b4260;--edge2:#525b80;--track:#0e1220;
    --txt:#f4eeda;--txt2:#cfc3a2;--txt3:#968a70;
    --a1:#e8c268;--a2:#7f9ad8;--a3:#b08ad8;--ok:#8fc08a;--gold:#ffd05a;--wrong:#c2564a;
    --reward:linear-gradient(120deg,#927439,#e8c268 55%,#fff0b8);--onr:#141826;
    --gk:1.5;--drift:20s;--kanaGlow:0 0 22px rgba(232,194,104,.7);
  }
:root[data-scene="terrace"]{
    /* the one DAYTIME scene — light surfaces even in dark mode, by design */
    --stage:#efe7d9;--bg2:#e6dccb;--card:#faf6ee;--card2:#efe6d6;--edge:#d6c9b2;--edge2:#bda98a;--track:#e0d4bf;
    --txt:#2c2418;--txt2:#5e5342;--txt3:#8d8069;
    --a1:#7d9c92;--a2:#e0956a;--a3:#c98f96;--ok:#6f9e63;--gold:#d9a441;--wrong:#c56a5c;
    --reward:linear-gradient(120deg,#8fa9a0,#c9a878 55%,#e8b48a);--onr:#ffffff;
    --gk:.25;--drift:26s;--kanaGlow:none;
  }

/* ── DAY VARIANT of every scene ──────────────────────────────────────────────
   The palettes above are unconditionally dark: [data-scene="shinjuku"] pins
   --stage/--card/--txt to night values whatever data-theme says. So under a
   scene, switching to light mode changed almost nothing — which read as "the
   slow fade doesn't work on Shinjuku Nights". It wasn't the fade; there was
   simply nothing to fade between.

   Rather than hand-authoring 18 more palettes (and needing a 19th for every new
   scene), derive the day version from each scene's OWN accents: keep --a1/--a2/
   --a3/--gold exactly as they are — that's the scene's identity, and it's what
   makes Shinjuku pink-and-cyan in daylight too — and rebuild only the surfaces
   and text by mixing those accents into white. Every scene therefore gets a
   coherent daytime look for free, tinted by its own colour family.

   Placed after all the scene blocks so it wins on order at equal specificity
   ((0,2,0) either way — this selector is [data-theme] + [data-scene]). */
/* :not([data-scene="terrace"]) — Terrace House is ALREADY a daytime interior
   (light surfaces, dark text) even in dark mode. Running the generic day
   derivation over it would mix its sage accent into white on top of an already
   light palette and wash the whole scene out. It keeps its own colours. */
:root[data-theme="light"][data-scene]:not([data-scene="terrace"]){
  --stage: color-mix(in srgb, var(--a1) 6%,  #fbfaf8);
  --bg2:   color-mix(in srgb, var(--a1) 9%,  #f4f1ec);
  --card:  color-mix(in srgb, var(--a1) 4%,  #ffffff);
  --card2: color-mix(in srgb, var(--a1) 10%, #f2eee8);
  --edge:  color-mix(in srgb, var(--a1) 20%, #ddd6cc);
  --edge2: color-mix(in srgb, var(--a1) 38%, #c6bcae);
  --track: color-mix(in srgb, var(--a1) 12%, #ebe6de);
  /* Text is the accent driven almost to black, so it stays on-theme while
     keeping the contrast a light ground needs. */
  --txt:   color-mix(in srgb, var(--a1) 16%, #17130f);
  --txt2:  color-mix(in srgb, var(--a1) 20%, #4a4238);
  --txt3:  color-mix(in srgb, var(--a1) 22%, #7d7365);
  /* --onr / --grad-warm-ink (text ON the reward gradient: Continue button,
     ghost CTAs) are NOT set here. Scene gradients range from pastel to
     near-black, so the right polarity differs per scene — white measured
     1.3-2.2:1 on the light ones, and a blanket dark ink then failed on the dark
     ones. theme_scenes.js mount() measures the resolved --a1 luminance and sets
     both as inline custom properties. This comment marks it as deliberate. */
  /* Neon glow is a night effect — at noon it just looks like a blur. */
  --gk: .35;
  --kanaGlow: none;

  /* CRITICAL: each scene block also remaps the APP's real tokens (--ink*,
     --paper*) to its night palette so the whole UI recolors. Overriding only
     the scene-art tokens above left --ink at the scene's near-white
     (e.g. sakura #fceef4) while these surfaces turned light — near-white text
     on a near-white card, i.e. invisible (right rail, stat values, leaderboard
     names, Jump-Back-In labels). Re-derive the app tokens from the corrected
     --txt* here so every scene is fixed, not just one. */
  --ink:   var(--txt);
  --ink-2: var(--txt2);
  /* Same tertiary-contrast lift as the dark scene block above, and for the same
     reason: --txt3 sits just under AA against --card. Mixing toward --txt works
     in this direction too, since --txt is the high-contrast end either way. */
  --ink-3: color-mix(in srgb, var(--txt) 24%, var(--txt3));
  --paper:      var(--stage);
  --paper-card: var(--card);
  --paper-2:    var(--card2);
  --line:   var(--edge);
  --line-2: var(--edge2);
  /* Accent washes are subtle TINTS behind dark text (the "(you)" leaderboard
     row, active tabs, badge tiles). The scenes set them to night fills, which
     under a light ground put dark text on a dark pill — keep them light. */
  --accent-wash:   color-mix(in srgb, var(--a1) 14%, #ffffff);
  --accent-wash-2: color-mix(in srgb, var(--a1) 28%, #ffffff);
  /* Scene accents are tuned to glow on a night ground (sakura #ff88b0 reads
     1.8:1 on a light card — well under AA). Darken the accent for TEXT use in
     daylight; --a1 itself stays bright for fills/glows/art. Generic on
     purpose: works for every scene, not one. Mix ratio picked so every scene
     clears AA on a light card (measured: sakura 5.3:1, shinjuku 6.3,
     bathhouse 5.1, edo 4.8, titans 7.6). */
  --accent:     color-mix(in srgb, var(--a1) 45%, #3a0a1e);
  --accent-ink: color-mix(in srgb, var(--a1) 40%, #33081a);
}
/* Nav labels are primary navigation, not secondary text: --ink-2 measured
   2.75:1 on the light header and 4.31:1 on the dark one (shinjuku, temple,
   bathhouse, sushi, gadget, mahou, backlot). This was scoped to light on the
   assumption that dark kept enough contrast for the hierarchy to be worth it;
   it doesn't, so promote them to full-strength ink under any scene. */
:root[data-scene] .nav-trigger:not(.active) { color:var(--ink); }
/* :not(.active) matters: the ACTIVE tab is filled WITH --ink, so letting this
   rule win there painted --ink text on an --ink background — a solid dark block
   with the label invisible (1:1). The active tab keeps .nav-trigger.active's
   --paper-card label. */
/* The scene ART is painted for night (dark buildings, lit windows). In daylight
   it reads as a heavy black band, so lift and lighten it rather than leaving a
   night-time silhouette under a bright UI. */
/* Night is the base state, so both ends of the filter are declared — going from
   `filter:none` to a filter list is a jump, whereas matching function lists
   interpolate smoothly. Same reason the opacity is stated on both sides. */
#sceneLayer .scene svg { filter:saturate(1) brightness(1); }
/* Terrace House is authored as a DAYTIME interior, so the generic day
   derivation above skips it (running it over an already-light palette washes it
   out). But the theme toggle must still do something everywhere — an inert
   scene reads as a bug — so it gets the opposite treatment: an explicit EVENING
   variant for dark mode. Same room, lamps on, sun down. */
:root[data-theme="dark"][data-scene="terrace"]{
  --stage:#241f1a;--bg2:#2b2620;--card:#332c25;--card2:#3d352c;--edge:#4a4036;--edge2:#635546;--track:#1d1915;
  --txt:#f2e9dc;--txt2:#cbbda9;--txt3:#958774;
  --a1:#8fb3a7;--a2:#e0a179;--a3:#c99aa0;--ok:#84b473;--gold:#e0b455;--wrong:#cd7a68;
  --reward:linear-gradient(120deg,#6d8880,#c9a878 55%,#e8b48a);--onr:#241f1a;
  --gk:.85;--kanaGlow:0 0 14px rgba(224,161,121,.45);
}
/* the room's own art: drop the daylight, warm the interior, dim the view out */
:root[data-theme="dark"][data-scene="terrace"] #sceneLayer .scene svg{
  filter:saturate(.85) brightness(.62) hue-rotate(-6deg);
}

:root[data-theme="light"][data-scene] #sceneLayer .scene svg { opacity:.42; filter:saturate(.9) brightness(1.35); }
:root[data-theme="light"][data-scene] #sceneLayer .aurora { opacity:calc(.22 * var(--gk)); }

/* ═══════════════════════════════════════════════════════════════════════════
   THEME UI FEEL — corner shape + interaction motion, driven per scene.
   theme_scenes.js sets data-shape / data-click / data-drop / data-modal on
   <html> when a scene is active. These layer over the base design: with no
   scene active none of it applies and the app looks exactly as before.
   (Framing: scenes are the redesign direction; the base washi UI is the
   default theme that will eventually be retired.)
═══════════════════════════════════════════════════════════════════════════ */

/* ── corner shape → --rBtn / --rCard, consumed by the real controls ── */
:root[data-shape="round"] { --rBtn:11px; --rCard:14px; }
:root[data-shape="pill"]  { --rBtn:99px; --rCard:20px; }
:root[data-shape="soft"]  { --rBtn:16px; --rCard:22px; }
:root[data-shape="sharp"] { --rBtn:3px;  --rCard:5px;  }
/* apply the shape vars to the app's real interactive surfaces (only when a
   shape is set, so the untethered base UI is untouched) */
:root[data-shape] .btn-primary,
:root[data-shape] .btn-ghost,
:root[data-shape] .btn-ghost-sm,
:root[data-shape] .btn-next,
:root[data-shape] .mode-btn,
:root[data-shape] .pt-mode-btn,
:root[data-shape] .pt-level-btn,
:root[data-shape] .choice-btn,
:root[data-shape] .nav-trigger,
:root[data-shape] .ans-input,
:root[data-shape] .kj-input,
:root[data-shape] .pt-input,
:root[data-shape] .rush-input { border-radius:var(--rBtn) !important; transition:border-radius .3s var(--ease); }
:root[data-shape] .quiz-card,
:root[data-shape] .nav-dropdown,
:root[data-shape] .modal,
:root[data-shape] .sl-card,
:root[data-shape] .sr-card,
:root[data-shape] .dash-card,
:root[data-shape] .pt-card,
:root[data-shape] .badge-pop { border-radius:var(--rCard) !important; transition:border-radius .3s var(--ease); }

/* ── click feedback → data-click, on the primary action buttons ── */
:root[data-click="press"] .btn-primary:active,
:root[data-click="press"] .btn-next:active,
:root[data-click="press"] .choice-btn:active:not(:disabled) { transform:translateY(1px) scale(.96); }
:root[data-click="squish"] .btn-primary:active,
:root[data-click="squish"] .btn-next:active,
:root[data-click="squish"] .choice-btn:active:not(:disabled) { transform:scale(.9,1.06); }
/* "pop" was the only click style that GREW the control (scale(1.08)): holding a
   choice button widened it ~18px and its edge crossed into the neighbouring
   button, covering it. Feedback now reads as a pop without ever exceeding the
   button's own footprint — a quick overshoot that settles slightly INSIDE it. */
:root[data-click="pop"] .btn-primary:active,
:root[data-click="pop"] .btn-next:active,
:root[data-click="pop"] .choice-btn:active:not(:disabled) {
  transform:scale(.97);
  animation:tsPop .18s var(--ease);
}
@keyframes tsPop { 0% { transform:scale(1); } 45% { transform:scale(1.02); } 100% { transform:scale(.97); } }
:root[data-click="jelly"] .btn-primary:active,
:root[data-click="jelly"] .btn-next:active { animation:tsJelly .45s ease; }
/* Damped: the old peaks (1.12 tall / 1.08 wide) briefly pushed a button over its
   neighbours. This is a 0.45s transient that returns to scale(1) — unlike the
   old "pop", which HELD its grown size for the whole press — so a smaller
   overshoot keeps the bounce readable without the overlap. */
@keyframes tsJelly { 0%{transform:scale(1);}25%{transform:scale(.92,1.05);}50%{transform:scale(1.03,.96);}75%{transform:scale(.98,1.01);}100%{transform:scale(1);} }

/* ── HOLD-TO-CHARGE: high-energy scenes vibrate under a sustained press ──
   The idea: on an energetic theme a held button should feel like it's building
   up charge. Two things make this work rather than just wobble:

   1. It is DRIVEN BY --gk, the glow/energy constant every scene already sets
      (Terrace House .25 · Temple .4 · Sushi 1.0 · Titans 1.4 · Shinjuku 1.9).
      So the intensity is authored data, not a new per-scene decision, and a
      calm theme is calm for free. --chg resolves to roughly 0 below gk 1 and
      climbs from there, so only genuinely high-energy scenes shake at all.

   2. It RAMPS. A constant jitter reads as a broken element; energy reads as
      building. The keyframe starts still, grows through the hold, and the
      whole thing is gated behind an animation-delay so a normal tap never
      triggers it — you have to actually hold.

   Composition note: the data-click rules above already set `transform` on
   :active, so a shake that also sets transform would replace their scale
   instead of adding to it. This animates `translate` (the individual
   property), which composes with `transform` rather than overwriting it. */
:root[data-scene]{
  /* 0 at gk<=1, ramping to ~1 by gk 1.9 — clamp keeps calm scenes at zero */
  --chg: clamp(0, calc((var(--gk) - 1) / .9), 1);
}
:root[data-scene] .btn-primary:active,
:root[data-scene] .btn-next:active,
:root[data-scene] .choice-btn:active:not(:disabled),
:root[data-scene] .wg-start-bar .btn-primary:active{
  animation:tsCharge .5s linear .12s infinite;   /* .12s delay = taps are exempt */
}
@keyframes tsCharge{
  0%   { translate:0 0; }
  12%  { translate:calc(var(--chg) * -.4px) calc(var(--chg) * .3px); }
  25%  { translate:calc(var(--chg) *  .7px) calc(var(--chg) * -.5px); }
  37%  { translate:calc(var(--chg) * -.9px) calc(var(--chg) * -.3px); }
  50%  { translate:calc(var(--chg) * 1.1px) calc(var(--chg) *  .6px); }
  62%  { translate:calc(var(--chg) * -1.3px) calc(var(--chg) * .4px); }
  75%  { translate:calc(var(--chg) * 1.5px) calc(var(--chg) * -.7px); }
  87%  { translate:calc(var(--chg) * -1.7px) calc(var(--chg) * -.4px); }
  100% { translate:calc(var(--chg) * 1.8px) calc(var(--chg) *  .8px); }
}
/* the glow swells with the shake, so the charge reads as energy rather than
   as a rattle — box-shadow already scales with --gk everywhere else */
:root[data-scene] .btn-primary:active{
  box-shadow:0 0 calc(26px * var(--gk) * var(--chg)) -6px var(--a1);
}
/* "pop" and "jelly" run their own :active animation; let those finish and the
   charge take over, rather than the two competing for the animation slot */
:root[data-scene][data-click="pop"] .btn-primary:active,
:root[data-scene][data-click="pop"] .btn-next:active,
:root[data-scene][data-click="jelly"] .btn-primary:active,
:root[data-scene][data-click="jelly"] .btn-next:active{
  animation:tsCharge .5s linear .34s infinite;
}
@media (prefers-reduced-motion:reduce){
  :root[data-scene] .btn-primary:active,
  :root[data-scene] .btn-next:active,
  :root[data-scene] .choice-btn:active:not(:disabled){ animation:none; }
}

/* ── dropdown open animation → data-drop, on the desktop nav dropdown ──
   base app already reveals .nav-dropdown on hover/focus-within via opacity +
   translateY; these swap the entrance style when a scene sets data-drop. */
:root[data-drop="scale"] .nav-dropdown { transform:scale(.85); transform-origin:top left; transition:opacity .2s var(--ease-bounce),transform .2s var(--ease-bounce),visibility .2s; }
:root[data-drop="scale"] .nav-menu:hover .nav-dropdown,
:root[data-drop="scale"] .nav-menu:focus-within .nav-dropdown,
:root[data-drop="scale"] .nav-menu.open .nav-dropdown { transform:scale(1); }   /* keep parity with the click/tap toggle */
:root[data-drop="stagger"] .nav-menu:hover .nav-item,
:root[data-drop="stagger"] .nav-menu:focus-within .nav-item,
:root[data-drop="stagger"] .nav-menu.open .nav-item { animation:tsDitem .34s var(--ease) backwards; }
:root[data-drop="stagger"] .nav-item:nth-child(1){ animation-delay:.03s; }
:root[data-drop="stagger"] .nav-item:nth-child(2){ animation-delay:.09s; }
:root[data-drop="stagger"] .nav-item:nth-child(3){ animation-delay:.15s; }
:root[data-drop="stagger"] .nav-item:nth-child(4){ animation-delay:.21s; }
@keyframes tsDitem { from{opacity:0;transform:translateX(-9px);} to{opacity:1;transform:translateX(0);} }
:root[data-drop="unroll"] .nav-dropdown { clip-path:inset(0 0 100% 0); transition:opacity .26s var(--ease),clip-path .3s var(--ease),visibility .26s; }
:root[data-drop="unroll"] .nav-menu:hover .nav-dropdown,
:root[data-drop="unroll"] .nav-menu:focus-within .nav-dropdown,
:root[data-drop="unroll"] .nav-menu.open .nav-dropdown { clip-path:inset(0 0 0 0); }   /* without this, a CLICK-opened dropdown stayed clipped to zero height (invisible) on Safari, which never focuses buttons on click */

/* ── modal open animation → data-modal, on the shared modal + badge pop ── */
:root[data-modal] .modal-overlay.open .modal,
:root[data-modal] .badge-pop-overlay.open .badge-pop { animation:var(--tsSheet,tsRise) .5s var(--ease-bounce) both; }
:root[data-modal="rise"] { --tsSheet:tsRise; }
:root[data-modal="zoom"] { --tsSheet:tsZoom; }
:root[data-modal="drop"] { --tsSheet:tsDrop; }
:root[data-modal="flip"] { --tsSheet:tsFlip; }
@keyframes tsRise { from{opacity:0;transform:translateY(70px) scale(.94);} to{opacity:1;transform:none;} }
@keyframes tsZoom { 0%{opacity:0;transform:scale(.35);}60%{opacity:1;}to{opacity:1;transform:scale(1);} }
@keyframes tsDrop { 0%{opacity:0;transform:translateY(-120px) scale(.9);}70%{opacity:1;transform:translateY(14px) scale(1.02);}85%{transform:translateY(-6px) scale(1);}to{transform:translateY(0);} }
@keyframes tsFlip { from{opacity:0;transform:perspective(1200px) rotateX(-55deg) translateY(30px);} to{opacity:1;transform:perspective(1200px) rotateX(0) translateY(0);} }

@media (prefers-reduced-motion:reduce) {
  :root[data-click] .btn-primary:active, :root[data-click] .btn-next:active, :root[data-click] .choice-btn:active { animation:none !important; transform:none !important; }
  :root[data-modal] .modal-overlay.open .modal, :root[data-modal] .badge-pop-overlay.open .badge-pop { animation:none !important; }
  :root[data-drop] .nav-item { animation:none !important; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THEMED APP LOOK — applied only when a scene is active (:root[data-scene]).
   Reshapes the REAL app chrome (navbar, HUD, cards, quiz) toward the theme-demo
   look: taller glowing navbar, a HUD pill row (🔥/⭐/⚡), glowing Kanji-of-the-
   Day + side cards, and glow on the quiz card / combo meter / reward FX.
   All gated on data-scene, so with no scene active the base app is unchanged.
   Colors read the scene palette (--a1/--a2/--a3/--gold/--reward/--gk).
═══════════════════════════════════════════════════════════════════════════ */

/* The 🔥/⭐/⚡ HUD pills are part of the default look now. */
#hudStreak .chip-val { color:var(--a1); }
#hudXP .chip-val     { color:var(--gold); }
#hudLevel .chip-val  { color:var(--a2); }
/* These used to be `display:none` in light mode (to avoid a cramped header),
   which read as a BUG: toggling day/night made your streak, XP and level
   vanish. Progress you've earned must not depend on the colour scheme — the
   header wins back the room by tightening chip padding instead (below), so all
   seven chips fit in either theme. */

/* ── taller, glowing navbar + themed pill HUD ── */
:root:not([data-theme="light"]) { --header:64px; }
/* Light keeps a slightly shorter bar, but the delta is now 4px rather than 8 —
   a smaller jump to animate across when the theme flips. */
:root[data-theme="light"] { --header:60px; }
/* .chip is still used by the in-game HUD pills; the header's own chips became
   the stats box. Tight padding so a row of them stays cheap horizontally. */
.chip { padding:4px 9px; min-width:38px; }
.app-header {
  background:color-mix(in srgb, var(--bg2) 82%, transparent);
  backdrop-filter:blur(10px);
  border-bottom:1px solid var(--edge);
  box-shadow:0 6px 24px -14px color-mix(in srgb, var(--a1) calc(60% * var(--gk)), transparent);
}
.chip-val { color:var(--txt); }
.chip:first-child .chip-val { color:var(--a1); }
.header-icon-btn, .statsbox {
  background:color-mix(in srgb, var(--card) 70%, transparent);
  border-color:var(--edge2);
}

/* ── glowing Kanji-of-the-Day + side-rail cards ── */
.sl-card, .sr-card, .dash-card {
  background:color-mix(in srgb, var(--card) 84%, transparent);
  /* 84% is fine at night — card and art are both dark, so what bleeds through
     is the same value and the text is unaffected. In LIGHT theme the art behind
     is still the night painting, so 16% of it under a near-white card drags the
     real backdrop down and the small label text on it lands at 4.06-4.41:1
     (measured on catcafe/sakura/osaka/tokyo). Nearly opaque there instead; the
     scene still reads through the page margins and the header. */
  border-color:var(--edge2);
  box-shadow:0 0 calc(26px * var(--gk)) -12px var(--a1), 0 12px 30px -20px #000;
  backdrop-filter:blur(6px);
}
#kotdCard { box-shadow:0 0 calc(30px * var(--gk)) -10px var(--a2), 0 12px 30px -20px #000; }
.sl-kanji-glyph {
  color:var(--txt);
  text-shadow:0 0 calc(22px * var(--gk)) color-mix(in srgb, var(--a2) 70%, transparent);
}
.sl-phrase-jp { color:var(--txt); }

/* ── quiz card + combo meter + reward FX glow ── */
.quiz-card {
  box-shadow:0 0 calc(30px * var(--gk)) -10px var(--a1), 0 20px 50px -26px #000;
}
.kana-display, #resultKana {
  text-shadow:0 0 calc(24px * var(--gk)) color-mix(in srgb, var(--a2) 55%, transparent);
}
.combo-hud {
  box-shadow:0 0 calc(18px * var(--gk)) -10px var(--a1);
}
.btn-primary { box-shadow:0 0 calc(22px * var(--gk)) -8px var(--a1); }
.score-pop { color:var(--gold); text-shadow:0 0 12px color-mix(in srgb,var(--gold) 60%,transparent); }

:root[data-theme="light"][data-scene] .sl-card,
:root[data-theme="light"][data-scene] .sr-card,
:root[data-theme="light"][data-scene] .dash-card {
  background:color-mix(in srgb, var(--card) 96%, transparent);
}

/* the light variant is the flat washi look — drop the glow there */
:root[data-theme="light"] .app-header,
:root[data-theme="light"] .statsbox,
:root[data-theme="light"] .sl-card, :root[data-theme="light"] .sr-card, :root[data-theme="light"] .dash-card,
:root[data-theme="light"] #kotdCard,
:root[data-theme="light"] .quiz-card, :root[data-theme="light"] .combo-hud,
:root[data-theme="light"] .btn-primary { box-shadow:none; }
:root[data-theme="light"] .sl-kanji-glyph, :root[data-theme="light"] .kana-display,
:root[data-theme="light"] #resultKana { text-shadow:none; }

@media (prefers-reduced-motion:reduce) {
  .app-header, .sl-card, .sr-card, .quiz-card, .combo-hud, .btn-primary { box-shadow:none; }
}

/* ── The pinned badge, worn beside a name ──────────────────────────────────
   The whole reason the crests had to survive 20px: this is where they are
   actually seen, in a row that is mostly text. Sized to the line, never
   allowed to shrink, and quiet enough that the name still leads. */
.lb-pin { display:inline-flex; align-items:center; flex-shrink:0; line-height:0; }
.sr-lb-row .lb-pin { margin-right:1px; }
.lb-player .lb-pin { margin-right:2px; }

/* Rank, now that 🥇🥈🥉 are gone. The number carries the position; the metal
   is a colour on the number, which is one less object in the row and renders
   the same on every OS. */
.sr-lb-rank.top { font-weight:700; }
.sr-lb-rank.t1 { color:#f0cf86; }
.sr-lb-rank.t2 { color:#cfd3d8; }
.sr-lb-rank.t3 { color:#d2874f; }
.lb-medal.m1 { color:#f0cf86; }
.lb-medal.m2 { color:#cfd3d8; }
.lb-medal.m3 { color:#d2874f; }
.lb-medal.m1, .lb-medal.m2, .lb-medal.m3 { font-weight:700; }

/* ── Choosing one, in the badge detail ─────────────────────────────────────
   The control shows the row it produces rather than describing it: the real
   handle, the real crest, at the size the leaderboard really draws. When
   nothing is worn the slot is the same pressed-in recess the locked badges
   use, so what you are filling is visible before you fill it. */
.badge-pin { margin-top:16px; }
.badge-pin-label {
  font-size:.68rem; font-weight:700; letter-spacing:.08em; text-transform:uppercase;
  color:var(--ink-3); margin:0 0 7px;
}
.badge-pin-preview {
  display:flex; align-items:center; gap:9px;
  padding:9px 12px; border-radius:var(--r-md);
  background:var(--paper-2); border:1px solid var(--line);
  transition:border-color .18s, background .18s;
}
.badge-pin-preview.on { border-color:var(--accent); background:var(--accent-wash); }
.bpp-rank { font-family:var(--mono); font-size:.8rem; color:var(--ink-3); min-width:12px; }
.bpp-crest { display:inline-flex; line-height:0; }
/* The empty slot: a dashed ring the crest is waiting to fill. */
.bpp-slot { display:inline-flex; line-height:0; opacity:.55; }
.bpp-name { font-size:.92rem; font-weight:600; color:var(--ink); }
.bpp-val { margin-left:auto; font-family:var(--mono); font-size:.85rem; color:var(--ink-2); }

.badge-pin-act { display:flex; align-items:center; gap:11px; flex-wrap:wrap; margin-top:9px; }
.badge-pin-btn {
  font-family:inherit; font-size:.8rem; font-weight:700; letter-spacing:.01em;
  padding:8px 15px; border-radius:var(--rBtn, var(--r-sm));
  background:var(--grad-warm); color:var(--grad-warm-ink);
  border:0; cursor:pointer;
  transition:filter .15s, transform .12s;
}
.badge-pin-btn:hover { filter:brightness(1.06); transform:translateY(-1px); }
.badge-pin-btn:active { transform:translateY(0) scale(.98); }
.badge-pin-btn:focus-visible { outline:2px solid var(--accent-text); outline-offset:2px; }
/* Worn is a STATE, not an action, so it stops looking like the primary button. */
.badge-pin-btn.on {
  background:var(--accent-wash); color:var(--accent-text);
  border:1px solid var(--accent);
}
.badge-pin-btn.on:hover { filter:none; background:var(--paper-2); }
.badge-pin-note { font-size:.76rem; color:var(--ink-3); }

/* ═══════════════════════════════════════════════════════════════════════════
   BADGE CRESTS (家紋) — panels/badge_crests.js.
   Rarity is not a word under the name, it is what the badge is MADE of:
   石 stone → 銅 copper → 金 gold → 漆 lacquer → 螺鈿 raden. Every one is
   built the same way — a field, the crest, a bevelled rim — so they read as
   one set while being unmistakably different metals. Collections use the same
   ladder, but driven by TIER rather than rarity, which is what lets the seal
   upgrade as you master it instead of growing a row of medals underneath.
═══════════════════════════════════════════════════════════════════════════ */
.kmn {
  position:relative; display:inline-grid; place-items:center;
  width:var(--km,44px); height:var(--km,44px); flex-shrink:0;
  isolation:isolate; vertical-align:middle;
}
.kmn-round, .kmn-round .kmn-field, .kmn-round .kmn-rim { border-radius:50%; }
/* 角印 — the square seal. A collection is a stamp of ownership, not a medal,
   and it stops a case of 87 badges reading as 87 identical circles. */
.kmn-seal, .kmn-seal .kmn-field, .kmn-seal .kmn-rim { border-radius:22%; }
.kmn-field { position:absolute; inset:0; z-index:0; }
.kmn-art {
  position:relative; z-index:1;
  width:78%; height:78%;      /* the crest FILLS it — the old icon sat at 60% */
  display:block; overflow:visible;
}
/* The bevel: a light top edge over a dark underside. This is the whole
   difference between "struck" and "printed". */
.kmn-rim {
  position:absolute; inset:0; z-index:2; pointer-events:none;
  box-shadow:
    inset 0 1.5px 0 rgba(255,255,255,.34),
    inset 0 -1.5px 0 rgba(0,0,0,.42),
    inset 0 0 0 1px rgba(0,0,0,.32);
}

/* 石 — deliberately the dullest thing in the set. If the common tier has any
   shine, nothing above it can feel earned. */
.kmn-stone .kmn-field { background:radial-gradient(120% 120% at 32% 24%, #7d746a, #59524a 62%, #443e38); }
.kmn-stone { color:#d8d0c4; }
.kmn-stone .kmn-art { filter:drop-shadow(0 1px 0 rgba(0,0,0,.5)); }

/* 銅 — the verdigris in the lower recess is what separates copper from orange. */
.kmn-copper .kmn-field {
  background:
    radial-gradient(80% 80% at 70% 78%, rgba(93,138,120,.34), transparent 60%),
    radial-gradient(120% 120% at 32% 22%, #c9793f, #8d4a27 58%, #5b2f1b);
}
.kmn-copper { color:#ffdcb8; }
.kmn-copper .kmn-art { filter:drop-shadow(0 1.5px 0 rgba(70,28,12,.75)); }

/* 金 */
.kmn-gold .kmn-field {
  background:
    linear-gradient(150deg, rgba(255,255,255,.42) 0 16%, transparent 34%),
    radial-gradient(120% 120% at 30% 20%, #f7e3a2, #c69a35 46%, #8a6a1f);
}
.kmn-gold { color:#4a3708; }
.kmn-gold .kmn-art { filter:drop-shadow(0 1px 0 rgba(255,240,190,.55)); }

/* 漆 — black lacquer, gold crest, one vermilion hairline. The most restrained
   of the five and, deliberately, the most expensive-looking. */
.kmn-lacquer .kmn-field {
  background:
    radial-gradient(90% 70% at 32% 18%, rgba(255,255,255,.16), transparent 55%),
    radial-gradient(120% 120% at 50% 50%, #241b15, #100b08);
}
.kmn-lacquer { color:#f0cf86; }
.kmn-lacquer .kmn-rim {
  box-shadow:inset 0 0 0 2px #b8412a, inset 0 2px 0 rgba(255,255,255,.2), inset 0 -2px 0 rgba(0,0,0,.6);
}
.kmn-lacquer .kmn-art { filter:drop-shadow(0 1px 1px rgba(0,0,0,.8)); }

/* 螺鈿 — mother-of-pearl. The only material that moves, which is exactly why
   it is reserved for the four badges that are genuinely hard. */
.kmn-raden .kmn-field {
  background:
    radial-gradient(70% 70% at 26% 22%, rgba(255,255,255,.5), transparent 58%),
    conic-gradient(from 200deg at 42% 40%,
      #7fd6c4, #b9a7e6 18%, #f0b7c8 34%, #f2dfa2 52%,
      #9fd8e6 68%, #c3a6e8 84%, #7fd6c4);
  background-size:100% 100%, 260% 260%;
}
.kmn-raden { color:#1b1720; }
.kmn-raden .kmn-art { filter:drop-shadow(0 1px 0 rgba(255,255,255,.5)); }
.kmn-raden .kmn-rim {
  box-shadow:inset 0 0 0 2px rgba(28,22,32,.85), inset 0 2px 0 rgba(255,255,255,.45), inset 0 -2px 0 rgba(0,0,0,.45);
}
@media (prefers-reduced-motion:no-preference) {
  .kmn-raden .kmn-field { animation:kmn-raden 9s ease-in-out infinite; }
}
@keyframes kmn-raden {
  0%,100% { background-position:0 0, 0% 50%; }
  50%     { background-position:0 0, 100% 50%; }
}

/* Unearned: the crest is pressed INTO the recess rather than struck onto
   metal — so a locked slot still says which badge is missing. */
.kmn-empty .kmn-field { background:radial-gradient(120% 120% at 50% 20%, #2f2822, #241e19); }
.kmn-empty { color:#6b6053; }
.kmn-empty .kmn-rim {
  box-shadow:inset 0 2px 5px rgba(0,0,0,.75), inset 0 -1px 0 rgba(255,255,255,.06);
}
/* The light theme drops every shadow by design, so the recess needs a border
   to stay a recess. */
:root[data-theme="light"] .kmn-empty .kmn-field { background:var(--paper-2); }
:root[data-theme="light"] .kmn-empty { color:var(--ink-3); }
:root[data-theme="light"] .kmn-rim { box-shadow:inset 0 0 0 1px rgba(0,0,0,.22); }

/* Tilt to catch the light, the way a real medal does. */
@media (prefers-reduced-motion:no-preference) {
  .kmn { transition:transform .2s cubic-bezier(.22,1.1,.34,1); }
  .badge-card:hover .kmn { transform:translateY(-3px) rotate(-2deg); }
}

/* ═══════════════════════════════════════════════════════════════════════════
   BADGE MEDALLIONS — themed icon for every badge (badgeIcon() in app.js).
   A reward-gradient ring + inner disc with a glyph (kanji/kana) or a hand-drawn
   inline SVG in the middle. Size is driven by --bi; recolors with the theme.
═══════════════════════════════════════════════════════════════════════════ */
.badge-medallion {
  position:relative; display:inline-flex; align-items:center; justify-content:center;
  width:var(--bi,44px); height:var(--bi,44px); flex-shrink:0; border-radius:50%;
  background:var(--grad-warm);
  box-shadow:0 0 calc(.28 * var(--bi,44px)) -4px var(--accent), inset 0 0 0 2px rgba(255,255,255,.14);
  isolation:isolate;
}
/* inner disc so the glyph sits on a dark face inside the bright ring */
.badge-medallion::before {
  content:''; position:absolute; inset:14%; border-radius:50%;
  background:var(--paper-card);
  box-shadow:inset 0 1px 3px rgba(0,0,0,.35);
}
.badge-medallion .bi-glyph {
  position:relative; z-index:1; font-family:var(--jp); font-weight:700; line-height:1;
  font-size:calc(var(--bi,44px) * .46); color:var(--ink);
  text-shadow:0 0 calc(var(--bi,44px) * .12) color-mix(in srgb, var(--accent) 55%, transparent);
}
.badge-medallion .bi-svg { position:relative; z-index:1; display:block; width:60%; height:60%; }
.badge-medallion .bi-svg svg { width:100%; height:100%; display:block; }
/* locked / not-yet-earned: drain the medallion */
.badge-medallion.dim { background:var(--line); box-shadow:none; opacity:.5; }
.badge-medallion.dim::before { background:var(--paper-2); }
.badge-medallion.dim .bi-glyph { color:var(--ink-3); text-shadow:none; }
/* the profile grid marks locked cards; grey their medallion too */
.badge-card.locked .badge-medallion { background:var(--line); box-shadow:none; filter:grayscale(1); opacity:.45; }

/* light variant: flatten the medallion glow (matches the flat washi look) */
:root[data-theme="light"] .badge-medallion { box-shadow:inset 0 0 0 2px rgba(255,255,255,.35); }
:root[data-theme="light"] .badge-medallion .bi-glyph { text-shadow:none; }

@media (prefers-reduced-motion:reduce) { .badge-medallion { box-shadow:inset 0 0 0 2px rgba(255,255,255,.14); } }

/* ── Profile ▸ Settings (default theme + default scene) ── */
.set-card {
  display:flex; flex-direction:column; gap:4px;
  background:color-mix(in srgb, var(--card) 80%, transparent);
  border:1px solid var(--edge2); border-radius:var(--rCard); padding:6px 4px; margin-bottom:14px;
}
.set-row {
  display:flex; align-items:center; justify-content:space-between; gap:14px;
  padding:12px 14px;
}
.set-row + .set-row { border-top:1px solid var(--edge); }
.set-label { display:flex; flex-direction:column; gap:2px; min-width:0; }
.set-label b { font-size:.9rem; font-weight:700; color:var(--txt); }
.set-label span { font-size:.74rem; color:var(--ink-3); line-height:1.4; }
.set-seg { display:flex; gap:4px; background:var(--track); padding:4px; border-radius:var(--rBtn); flex-shrink:0; }
.set-opt {
  cursor:pointer; font-family:var(--jp); font-size:.78rem; font-weight:700; white-space:nowrap;
  color:var(--txt2); background:transparent; border:0; padding:6px 12px; border-radius:calc(var(--rBtn) - 3px);
  transition:.15s;
}
.set-opt:hover { color:var(--txt); }
.set-opt.on { color:var(--grad-warm-ink); background:var(--grad-warm); box-shadow:0 0 calc(14px*var(--gk)) -6px var(--a1); }
.set-select {
  flex-shrink:0; max-width:200px; cursor:pointer; font-family:var(--jp); font-size:.8rem; font-weight:600;
  color:var(--txt); background:var(--track); border:1px solid var(--edge2);
  border-radius:var(--rBtn); padding:7px 10px;
}
.set-select:focus { outline:none; border-color:var(--accent-text); }
@media (max-width:520px) { .set-row { flex-direction:column; align-items:stretch; } .set-seg, .set-select { align-self:flex-start; } }

/* Legal & licenses — foot of Profile. Uses the app tokens so it tracks the
   live theme; native <details> for collapse. */
.legal-card { padding:2px 4px; }
.legal-item { border-radius:var(--rBtn); }
.legal-item + .legal-item { border-top:1px solid var(--edge); }
.legal-item > summary {
  cursor:pointer; list-style:none; padding:12px 14px;
  font-size:.86rem; font-weight:700; color:var(--txt);
  display:flex; align-items:center; justify-content:space-between; gap:10px;
}
.legal-item > summary::-webkit-details-marker { display:none; }
.legal-item > summary::after {
  content:'⌄'; color:var(--ink-3); font-size:1rem; transition:transform .2s;
}
.legal-item[open] > summary::after { transform:rotate(180deg); }
.legal-item > summary:hover { color:var(--accent-text); }
.legal-body { padding:0 14px 14px; }
.legal-body p { font-size:.78rem; color:var(--txt2); line-height:1.6; margin:0 0 8px; }
.legal-body p:last-child { margin-bottom:0; }
.legal-body b { color:var(--txt); font-weight:700; }
.legal-body a { color:var(--accent-text); }
.legal-upd { font-size:.68rem !important; color:var(--ink-3) !important; text-transform:uppercase; letter-spacing:.04em; }
.legal-mono {
  display:inline-block; font-family:var(--mono, ui-monospace, monospace); font-size:.72rem;
  color:var(--txt2); background:var(--track); border:1px solid var(--edge);
  border-radius:6px; padding:2px 7px; margin-top:2px;
}
.legal-foot { text-align:center; font-size:.72rem; color:var(--ink-3); margin:10px 0 4px; }
.legal-foot a { color:var(--accent-text); }

/* ═══════════════════════════════════════════════════════════════════════════
   MOBILE (phones) — every rule below is scoped to small viewports or
   touch-only media queries, so desktop/laptop rendering is untouched.
   Sits at the END of the file on purpose: several of the components it
   adjusts (e.g. .n5-bar-track, the [data-scene] header skin) define their
   base look later in the cascade than the old 720px block, which is exactly
   how the earlier mobile rules kept losing.
═══════════════════════════════════════════════════════════════════════════ */

@media (max-width:720px) {

  /* The stats box is the ONLY per-game readout on a phone — the rail card it
     replaced was hidden below 980px. So it gets its own full-width header row
     rather than competing with the logo and the action buttons, and the type
     steps down so four cells fit 360px without ellipsis. */
  /* 1 1, not 1 0: at 360px the four cells' own content is wider than the phone,
     and shrink:0 forbade the compression the cells are already built for
     (.sb-cell is min-width:0 / overflow:hidden). */
  .statsbox { order:10; flex:1 1 100%; width:100%; min-width:0; margin-top:2px; }
  .sb-cell { flex:1 1 0; }
  .sb-val { font-size:.92rem; }
  .sb-key { font-size:.62rem; letter-spacing:.02em; }
  .sb-cell { padding:5px 4px 4px; }
  /* At 360 the level's "· 90%" is the first thing to go: the rung matters, the
     fraction of the way through it does not, and wrapping would cost a row. */
  /* 380, matching the tier at the end of this file. These were 379 and 380 —
     one pixel apart, which no device sits between (SE/mini 375, Galaxy S8
     360, iPhone 12/13 mini 375), so this is two names for one breakpoint. */
  @media (max-width:380px) { .sb-sub { display:none; } }

  /* Manga: the speaker anchor has to survive a 180px panel. Keep the side, widen
     the cap — at 70% the bubble is three characters a line. */
  .mg-panel[data-cast="2"] .mg-bubble.mg-from-l,
  .mg-panel[data-cast="3"] .mg-bubble.mg-from-l,
  .mg-panel[data-cast="2"] .mg-bubble.mg-from-r,
  .mg-panel[data-cast="3"] .mg-bubble.mg-from-r { min-width:56%; max-width:88%; }
  .mg-panel[data-cast="2"] .mg-from-l::after,  .mg-panel[data-cast="3"] .mg-from-l::after,
  .mg-panel[data-cast="2"] .mg-from-l::before, .mg-panel[data-cast="3"] .mg-from-l::before { left:14px; }
  .mg-panel[data-cast="2"] .mg-from-r::after,  .mg-panel[data-cast="3"] .mg-from-r::after,
  .mg-panel[data-cast="2"] .mg-from-r::before, .mg-panel[data-cast="3"] .mg-from-r::before { right:14px; }

  /* ── Header: one compact row — logo + actions. Navigation lives in the
     left-docked pull-out drawer (styles at the end of this section;
     shell/mobile.js builds and drives it). Before this rework,
     .header-right (~370px, flex-shrink:0) squeezed .mob-tabs to 0px and
     pushed its own profile/leaderboard/user buttons past the right edge —
     a phone had NO reachable navigation at all. ── */
  .app-header {
    position:sticky; top:0;
    flex-wrap:wrap;
    gap:6px 8px;
    padding:6px 12px;
    /* Status-bar clearance when the page runs edge-to-edge (viewport-fit=
       cover): 0px in plain portrait Safari, real inset in standalone mode. */
    padding-top:max(6px, env(safe-area-inset-top));
  }
  /* The old 720px block gives #appShell overflow:auto, which makes it the
     header's sticky scrollport — but the shell never actually scrolls (it
     grows with content; the BODY scrolls), so sticky silently did nothing. */
  #appShell { overflow:visible; }
  .header-left { padding:2px 6px 2px 2px; }
  /* Row-1 width budget on a 390px screen: logo 130 + N5 pill 54 + four 38px
     buttons + 4px gaps + 24px page padding = 384. Anything looser wraps the
     action cluster onto a third header row. */
  /* WRAP. The island below asks for `flex:1 0 100%` — its own full-width row —
     but a flex-basis of 100% only becomes a row if the container wraps, and
     this one did not. So the island laid out 472px wide inside a 390px phone
     and pushed the header 271px off the right edge, taking the action buttons
     with it. (Pre-existing: it was 1009px before the island was even split
     into cells.) With wrapping on, `order:10` puts it under the logo and
     buttons exactly as intended. */
  /* `flex:1 1 auto` overrides the base `flex-shrink:0`. That zero is right on a
     desktop — the cluster must not be squeezed by the nav — but on a phone it
     meant the cluster stayed at its CONTENT width and simply hung off the right
     edge, taking the action buttons with it. It takes the row's leftover width
     and shrinks now. */
  .header-right { gap:4px; min-width:0; flex-wrap:wrap; flex:1 1 auto; }

  /* The track is already hidden by the 1300px rule, which now sits after the
     base .n5-bar-track and so actually applies — the re-hide that used to be
     needed here is gone. What the phone does need is the even-spacing autos
     off: this header WRAPS, and an auto margin on a wrapped row centres the
     pill in whatever space that row has left instead of sitting beside the
     logo. */
  .n5-bar-btn { margin-left:0; margin-right:0; }

  /* Reviewer-only door: icon-only pill on phones — "📝 Review" alone blew
     the row-1 width budget for reviewer accounts. font-size:0 hides the
     text node; the pseudo re-draws the glyph at full size. */
  .header-review-btn { font-size:0; gap:0; padding:0; min-width:38px; min-height:38px; justify-content:center; }
  .header-review-btn::before { content:'📝'; font-size:1rem; line-height:1; }
  /* Even icon-only, a fifth button doesn't fit NEXT TO the wordmark at 390px
     — for reviewer accounts the wordmark yields (the あ mark stays). */
  .app-header:has(.header-review-btn:not(.hidden)) .app-name { display:none; }

  /* Avatar-only user chip — the name span is what shoved the header off
     the right edge on a 390px screen. An account with no avatar image gets
     a generic head glyph instead of a 6px invisible button. */
  .user-trigger span { display:none; }
  .user-trigger { padding:3px; min-width:38px; min-height:38px; justify-content:center; }
  .user-trigger:not(:has(img))::before { content:'👤'; font-size:1.05rem; line-height:1; }
  /* 38px icon buttons: comfortable thumb targets (were 32px). */
  .header-icon-btn { width:38px; height:38px; }

  /* The horizontal tab strip is superseded by the nav drawer.
     buildMobTabs() still renders it and updateNavHighlight() still flips
     its .active classes — hidden, it doubles as the state feed the drawer
     mirrors (shell/mobile.js watches it), so app.js stays the single owner
     of nav state and needed no changes. */
  .mob-tabs { display:none; }

  /* The user dropdown opens on hover/focus-within, which touch can't do
     reliably — shell/mobile.js toggles .open on tap (touch devices only). */
  .user-menu.open .user-dropdown { opacity:1; visibility:visible; transform:translateY(0); }
  /* Bigger rows once open — it's a thumb menu on this screen. */
  .menu-link { padding:12px 14px; font-size:.88rem; }

  /* ── Dashboard hero: the 1.9rem serif greeting wrapped to three lines and
     shoved the Continue button below the fold. ── */
  .dash-hero { padding:20px 18px; }
  .dash-greeting { font-size:1.5rem; }

  /* ── Rows of pill-toggles that could clip off the right edge. ── */
  .pt-toggles { flex-wrap:wrap; }             /* Particle Dojo: Guide/AI Tip/Furigana/Hardcore */
  .cl-modes { flex-wrap:wrap; }               /* Conjugation Lab: Starter/Tenses/Reform/Full build */
  .mode-switch { flex-wrap:wrap; }            /* word-quiz Reading/Meaning/Recall/Listen/Reveal */
  .kj-guess { flex-wrap:wrap; }               /* kanji answer row on very narrow screens */

  /* ── Bottom-anchored UI clears the iPhone home indicator. ── */
  .wg-start-bar { bottom:calc(10px + env(safe-area-inset-bottom)); }
  .rl-start-bar { bottom:calc(10px + env(safe-area-inset-bottom)); }
  .lookup-toggle {
    right:14px;
    bottom:calc(14px + env(safe-area-inset-bottom));
    width:54px; height:54px; font-size:1.5rem;
  }
  .lookup-panel {
    left:12px; right:12px; width:auto;
    bottom:calc(76px + env(safe-area-inset-bottom));
    max-height:min(64dvh, 560px);
  }
  .main-content { padding:16px 12px calc(44px + env(safe-area-inset-bottom)); }

  /* ── Modals: near-full-sheet, dvh so the iOS URL bar can't eat the
     bottom edge, close button stays reachable. ── */
  .modal-overlay { padding:12px; padding-bottom:max(12px, env(safe-area-inset-bottom)); }
  .modal { max-height:calc(100dvh - 24px); padding:12px 16px 22px; }
  .kana-pop, .badge-pop { max-width:calc(100vw - 24px); }
  /* The ✕ was position:absolute INSIDE the scrolling modal, so on a long
     panel (N5 readiness, Mastery) it scrolled away with the top of the
     content — and with the near-full-sheet modal the tappable backdrop is a
     12px sliver, so "how do I get out of this?" was a real question. Sticky
     keeps it pinned to the modal's scrollport; the negative bottom margin
     returns its row's space so panel content doesn't shift down. */
  .modal-close {
    position:sticky; top:0; z-index:6;
    margin:0 0 -38px auto;
    width:38px; height:38px;
    display:flex;
    font-size:.95rem;
    box-shadow:0 2px 10px rgba(0,0,0,.18);
  }

  /* ── N5 readiness expanded item lists: .n5-remaining-chips is
     flex-shrink:0, so at phone width the two status chips crushed the
     word into a one-word-per-line sliver AND ran off the modal's right
     edge, clipped mid-word. Stack instead: word on top, chips wrapping
     beneath it. ── */
  .n5-remaining-row { flex-direction:column; align-items:stretch; gap:5px; }
  .n5-remaining-chips { justify-content:flex-start; }

  /* ── 31px-tall pill buttons (Shuffle, Kanji/Vocab help, Skip, New game,
     the listening subtitle toggles) → 34px+ touch height. ── */
  .btn-ghost-sm { padding:8px 12px; }
  .sub-opt { padding:8px 12px; }

  /* ── Landing (pre-login): the hero's demo card forces ~486px of
     min-content (non-wrapping tab row + pane content), so the whole page
     loaded wider than the screen and iPhones showed it clipped until the
     user pinch-zoomed out. Let the single grid column actually shrink, and
     let the demo tabs wrap. ── */
  .landing-hero { grid-template-columns:minmax(0,1fr); }
  .landing-pitch, .demo-card { min-width:0; max-width:100%; }
  .demo-card { padding:18px 14px; }
  .demo-tabs { flex-wrap:wrap; justify-content:center; }
  .demo-pane { min-width:0; }
  .demo-viewport { min-width:0; max-width:100%; }

  /* ── Word Groups picker: .wg-picker-wrap is the one wg-* wrap without
     width:100%, so the centered flex column sizes it to the grid's
     max-content — 956px on a 390px phone, cards clipped off BOTH edges with
     no way to scroll to them. Pin it to the view and pair the cards up. ── */
  .wg-picker-wrap { width:100%; max-width:100%; min-width:0; }
  .wg-group-grid { max-width:100%; grid-template-columns:repeat(2, minmax(0,1fr)); gap:10px; }
  .wg-group-card { padding:16px 10px 14px; }
  .wg-group-emoji { font-size:2.2rem; }
  .wg-group-name { font-size:.95rem; }

  /* ── Leaderboard: make the table fit the modal instead of hiding its XP
     column behind an unsignposted horizontal scroll — drop the decorative
     XP bar, tighten cells, ellipsize long player names. ── */
  .data-table th, .data-table td { padding:8px 7px; }
  .lb-total { min-width:0; }
  .lb-totbar { display:none; }
  .lb-player { gap:6px; }
  .lb-name { max-width:74px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
}

/* ── The phone navbar: a left-docked pull-out drawer. shell/mobile.js
   creates these elements the first time the phone media query matches and
   drives them with a pointer drag engine (pull the handle, swipe from the
   left edge, throw the drawer shut); it docks open/closed on release based
   on position + velocity. Hidden at every width by default so a window
   resized back up can never strand a floating handle over the desktop UI. ── */
.nav-drawer, .nav-drawer-scrim, .nav-drawer-handle { display:none; }

@media (max-width:720px) {
  .nav-drawer-scrim {
    display:block;
    position:fixed; inset:0; z-index:68;   /* over the sticky header (50), under modals (70) */
    background:rgba(21,17,12,.45);
    opacity:0; pointer-events:none;        /* mobile.js fades it with drag progress */
    transition:opacity .28s var(--ease);
  }
  .nav-drawer {
    display:flex; flex-direction:column;
    position:fixed; top:0; bottom:0; left:0; z-index:69;
    width:min(78vw, 300px);
    transform:translateX(-100%);
    background:var(--paper-card);
    border-right:1px solid var(--line-2);
    box-shadow:var(--sh-float);
    transition:transform .28s var(--ease);
    padding-top:max(10px, env(safe-area-inset-top));
    padding-bottom:max(10px, env(safe-area-inset-bottom));
    padding-left:env(safe-area-inset-left);
    /* Vertical list scrolling stays native; horizontal gestures keep firing
       pointermoves so the drag engine can throw the drawer closed. */
    touch-action:pan-y;
  }
  .nav-drawer-head {
    display:flex; align-items:center; gap:10px;
    padding:10px 14px 12px;
    border-bottom:1px solid var(--line);
  }
  .nav-drawer-head b { font-family:var(--serif); font-size:1.05rem; color:var(--ink); }
  .nd-close { position:static; margin-left:auto; }   /* .modal-close chrome, in-flow */
  .nav-drawer-body {
    flex:1; overflow-y:auto;
    overscroll-behavior:contain;           /* drawer scroll must not chain to the page */
    padding:4px 0 12px;
    scrollbar-width:none;
  }
  .nav-drawer-body::-webkit-scrollbar { display:none; }
  .nd-group {
    font-family:var(--jp);
    font-size:.62rem; font-weight:700; text-transform:uppercase; letter-spacing:.1em;
    color:var(--ink-3);
    margin:16px 16px 4px;
  }
  .nd-item {
    display:flex; align-items:center; gap:10px;
    width:100%; text-align:left;
    padding:11px 16px 11px 13px;
    border-left:3px solid transparent;
    font-size:.92rem; font-weight:600; color:var(--ink-2);
  }
  .nd-item-kana {
    font-family:var(--jp); font-size:1.05rem; color:var(--ink-3);
    width:24px; text-align:center; flex-shrink:0;
  }
  .nd-item.active { color:var(--accent-text); background:var(--accent-wash); border-left-color:var(--accent); }
  .nd-item.active .nd-item-kana { color:var(--accent-text); }

  /* The pull tab. Rides the drawer's right edge (mobile.js keeps their
     transforms in lockstep), so it reads as physically attached — pull it
     out, push it back, or tap it. Chevron flips to point back inward while
     docked open. */
  .nav-drawer-handle {
    display:flex; align-items:center; justify-content:center;
    position:fixed; left:0; top:50%; z-index:69;
    transform:translateY(-50%);
    width:27px; height:78px; padding:0;
    border:1px solid var(--line-2); border-left:none;
    border-radius:0 13px 13px 0;
    background:var(--paper-card);   /* fallback for pre-color-mix Safari */
    background:color-mix(in srgb, var(--paper-card) 94%, transparent);
    color:var(--ink-2); font-size:.72rem;
    box-shadow:var(--sh-float);
    transition:transform .28s var(--ease);
    touch-action:none;                     /* drag-first — never scrolls the page */
  }
  .nav-drawer-handle span { transition:transform .28s var(--ease); }
  .nd-open .nav-drawer-handle span { transform:scaleX(-1); }
  /* Mid-drag the finger IS the animation — transitions off so it tracks 1:1. */
  .nd-dragging .nav-drawer, .nd-dragging .nav-drawer-handle,
  .nd-dragging .nav-drawer-scrim, .nd-dragging .nav-drawer-handle span { transition:none; }
  html.nd-lock, html.nd-lock body { overflow:hidden; }
}

/* Two-up dashboard stats. The 900px rule's `grid-column:span 2` survived into
   the old 560px block (2 columns × span 2 = one giant card per row — a phone
   scrolled through five full-width tiles before reaching any content). Hero
   stat keeps the full-width top row; the rest pair up. */
@media (max-width:560px) {
  .dash-stats-grid { grid-template-columns:repeat(2, 1fr); }
  .dash-stat-card { grid-column:span 1; grid-row:auto; padding:12px 8px; }
  .dash-stat-card:first-child { grid-column:1 / -1; }
  .dash-stat-val { font-size:1.3rem; }
  .dash-stat-card:first-child .dash-stat-val { font-size:1.8rem; }
}

/* Very small phones (iPhone SE/mini): drop the wordmark, keep the あ mark. */
@media (max-width:380px) {
  .app-name { display:none; }
}

/* ── Touch ergonomics — applies on touch-primary devices at ANY width, and
   never on mouse/trackpad machines. ── */
@media (hover:none) and (pointer:coarse) {
  /* No double-tap-to-zoom on controls: rapid answer-tapping in the quiz
     games must never trigger a page zoom. (Pinch zoom is unaffected.) */
  button, [role="button"], a, input, select, textarea, .mob-tab, .nav-item,
  .choice-btn, .pt-choice, .order-tile, .mem-card, .wg-flashcard, .kj-card {
    touch-action:manipulation;
  }
  /* The draw pad + drag tiles keep their stricter touch-action:none
     (set at their definition) — restate so the list above can't win. */
  .draw-pad, .order-tile { touch-action:none; }
  button, .mob-tab, .mem-card, .order-tile { -webkit-tap-highlight-color:transparent; }
  /* Long-pressing a game tile must not pop the iOS text-selection loupe. */
  .choice-btn, .pt-choice, .order-tile, .mem-card, .wg-card, .wg-flashcard,
  .kana-display, .mob-tab, .kj-card { -webkit-user-select:none; user-select:none; }
  /* Hover lifts stick after a tap on touch screens ("sticky hover") and read
     as a broken button — neutralize just the transform, keep color feedback. */
  .btn-primary:hover, .dash-stat-card:hover, .dash-tile:hover,
  .wg-group-card:hover, .rl-cat-card:hover, .badge-card:hover,
  .mem-card:not(.flipped):not(.matched):hover, .wg-card:not([disabled]):hover,
  .lookup-toggle:hover, .logo-mark-sm:hover { transform:none; }
}

/* ── Touch target floors ─────────────────────────────────────────────────────
   Keyed on the same coarse-pointer query as the ergonomics block above rather
   than on a width, for three reasons: these controls are 24-37px tall on a
   PHONE and on an iPAD alike, and `max-width:720px` never reaches the tablet;
   a pointer query cannot regress a mouse machine, which matters because there
   is no visual test coverage below 1024px; and it is the same question
   qa/crawl.js now asks when it flags a small tap target.

   Sits AFTER that block so it wins every tie on equal specificity without
   needing !important anywhere — including over `.game-ref > button` (0,1,1),
   which is the whole point of one of these rules.

   `min-height` rather than padding, and rather than converting anything to
   flex: a <button> already centres its own content, so min-height alone leaves
   the label centred (measured: 12px above / 13px below in a 38px box) with no
   change to the display model, no per-selector padding arithmetic against
   whatever each rule already sets, and no risk to the icon+label spacing in
   KQHead.btn().

   38px is the house floor, matching the deliberate decision recorded on the
   header icon buttons ("comfortable thumb targets (were 32px)"). Heights below
   are as MEASURED in Chromium at 390 and 768 with hasTouch, not computed from
   the stylesheet.

   Deliberately NOT here:
     .act-cell        aspect-ratio:1/1 with no explicit height. A min-height
                      overrides the ratio's height and turns the calendar
                      squares into rectangles. Measured 52px at 390 anyway.
     .btn-primary     47px. .btn-ghost 45px. Both already clear the floor at
                      the 18px root — the earlier estimate of ~41px was wrong.
     .choice-btn      75px, and its min-width/max-width are load-bearing.
     .voc-node        44px at 390 by its own rule.
     .sb-cell         53px at 390. min-WIDTH on it is what once pushed the
                      header off a 360px screen, so nothing here touches it.
     .game-quietlink  23px, but it is styled as a text link on purpose ("Skip",
                      "Listen again"). Growing it would make it read as a
                      button. Flagged for design rather than fixed here.
   ── */
@media (hover:none) and (pointer:coarse) {
  /* A fixed 24x24 box — the smallest interactive element in the app (the
     dashboard calendar's month arrows). Needs width as well as height. */
  .act-nav-btn { min-width:38px; min-height:38px; }

  /* Measured 29px, and it is the "Dashboard" back control in every game head. */
  .game-back { min-height:38px; }

  /* 31px inside .game-ref, on phone AND tablet. The ≤720 rule
     `.btn-ghost-sm { padding:8px 12px }` is specificity (0,1,0) and cannot
     reach a button inside `.game-ref > button` (0,1,1) — a media query adds no
     specificity — so every Guide/reference button in every game head stayed
     ~31px on every touch device despite a rule that looks like it fixes them. */
  .game-ref > button,
  .btn-ghost-sm { min-height:38px; }

  .tm-chip       { min-height:38px; }   /* 27px — Topic Mastery filter chips */
  .n5-bar-btn    { min-height:38px; }   /* 33px */
  .demo-tab      { min-height:38px; }   /* 34px — landing demo tabs */
  .pt-level-btn  { min-height:38px; }   /* 35px */
  .pt-mode-btn   { min-height:38px; }   /* 37px */
  .mg-lv         { min-height:38px; }   /* 37px */
  .header-left   { min-height:38px; }   /* 36px — already flex, so this is enough */
  /* 37px on a phone but 29px on a tablet: the phone block that resizes it
     stops at 720. */
  .modal-close   { min-width:38px; min-height:38px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   PAYWALL (docs/MONETIZATION.md) — lock cards, meter chips, locked picker
   rows, and the Plus pricing panel. Text colors use the derived --ink* tokens
   (never raw --txt*) so scene contrast fixes apply here too.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Meter-exhausted / Plus-content card — shown IN PLACE of the next item,
   before anything starts. Friendly, never mid-play. */
.pw-lock-card {
  max-width: 440px; margin: 28px auto; padding: 26px 22px; text-align: center;
  background: var(--card, var(--paper-card)); border: 1px solid var(--edge, var(--line));
  border-radius: 16px;
}
.pw-lock-emoji { font-size: 2.2rem; margin-bottom: 8px; }
.pw-lock-title { margin: 0 0 6px; font-size: 1.05rem; font-weight: 800; color: var(--ink); }
.pw-lock-body  { margin: 0 0 16px; font-size: .86rem; line-height: 1.5; color: var(--ink-2); }
.pw-lock-actions { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }

/* "n of m free today" chip on consumable pickers. */
.pw-meter-row { margin: 0 0 10px; text-align: center; }
.pw-meter-chip {
  display: inline-block; font-size: .72rem; font-weight: 700; letter-spacing: .02em;
  color: var(--ink-2); background: var(--paper-2); border: 1px solid var(--edge, var(--line));
  border-radius: 999px; padding: 4px 12px;
}
.pw-lock-chip { font-size: .8em; opacity: .85; }

/* Locked level/paper rows (vocab, particles, exam, manga levels): dimmed but
   CLICKABLE — the click opens the honest Plus card, one of the four
   sanctioned upsell surfaces. */
.pt-level-btn.pw-locked, .mg-lv.pw-locked { opacity: .55; }
.pt-level-btn.pw-locked::after { content: ' 🔒'; font-size: .8em; }

/* Long-form free-preview banner — prominent so the 2-of-88 loop is never a
   mystery. Uses .btn-primary for the CTA so contrast is already handled. */
.pw-demo-banner {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap; justify-content: space-between;
  margin: 0 0 16px; padding: 12px 16px; border-radius: 12px;
  border: 1px solid var(--accent); background: color-mix(in srgb, var(--accent) 9%, var(--paper-card));
}
.pw-demo-banner-txt { flex: 1 1 220px; font-size: .92rem; line-height: 1.45; color: var(--ink); }
.pw-demo-cta { flex: none; }
/* Long-form free-demo note + inline link-style button (legacy inline surface). */
.pw-demo-note { margin: 0 0 10px; font-size: .78rem; color: var(--ink-3); text-align: center; }
.pw-link {
  background: none; border: 0; padding: 0; cursor: pointer; font: inherit;
  color: var(--accent-text); text-decoration: underline; text-underline-offset: 2px;
}

/* Plus bonus quest leg — subtly distinct, never louder than the core three. */
.dq-leg.dq-bonus { border-style: dashed; opacity: .92; }

/* ── Pricing panel (modal) ── */
.pw-pricing { padding: 4px 2px 12px; }
.pw-pricing .modal-sub { color: var(--ink-2); }
.pw-loading { color: var(--ink-3); text-align: center; padding: 30px 0; }
.pw-current {
  margin: 12px 0; padding: 12px 14px; font-size: .88rem; color: var(--ink);
  background: var(--paper-2); border: 1px solid var(--edge, var(--line)); border-radius: 12px;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
.pw-current .pw-manage { margin-left: auto; }

/* "Your subscription" panel — states the terms plainly: what you have, what it
   costs, what happens next and when, and how to cancel. */
.pw-sub-panel { display: block; }
.pw-sub-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pw-sub-title { margin: 0; font-size: 1rem; font-weight: 800; color: var(--ink); }
.pw-chip {
  font-size: .66rem; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;
  padding: 3px 9px; border-radius: 999px; border: 1px solid currentColor; white-space: nowrap;
}
.pw-chip-ok { color: var(--good, #7cc86a); }
.pw-chip-warn { color: var(--warn, #e6b450); }
.pw-sub-detail { margin: 8px 0 0; font-size: .85rem; line-height: 1.55; color: var(--ink-2); }
.pw-sub-detail b { color: var(--ink); }
.pw-sub-actions { margin-top: 12px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pw-sub-hint { font-size: .74rem; color: var(--ink-3); line-height: 1.4; flex: 1 1 220px; }
.pw-price-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 16px 0 6px; }
.pw-price-card {
  position: relative; display: flex; flex-direction: column; gap: 6px; align-items: center;
  padding: 18px 12px 14px; text-align: center;
  background: var(--card, var(--paper-card)); border: 1px solid var(--edge, var(--line));
  border-radius: 14px;
}
.pw-price-card .btn-primary { margin-top: auto; width: 100%; }
.pw-price-best { border-color: var(--accent-text); box-shadow: 0 0 18px -10px var(--a1, transparent); }
.pw-price-flag {
  position: absolute; top: -9px; left: 50%; transform: translateX(-50%);
  font-size: .62rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase;
  color: var(--on-ink); background: var(--ink); border-radius: 999px; padding: 2px 10px; white-space: nowrap;
}
.pw-price-name { font-size: .78rem; font-weight: 700; color: var(--ink-2); }
.pw-price-amount { font-size: 1.6rem; font-weight: 800; color: var(--ink); line-height: 1.1; }
.pw-price-per { font-size: .72rem; font-weight: 600; color: var(--ink-3); margin-left: 3px; }
.pw-price-note { font-size: .7rem; color: var(--ink-3); }
.pw-feature-list { list-style: none; margin: 16px 0 0; padding: 0; display: grid; gap: 7px; }
.pw-feature-list li { font-size: .84rem; line-height: 1.45; color: var(--ink-2); }
.pw-feature-list li b { color: var(--ink); }
.pw-fairness {
  margin-top: 16px; padding: 12px 14px; border: 1px dashed var(--edge, var(--line));
  border-radius: 12px;
}
.pw-fairness p { margin: 0 0 8px; font-size: .76rem; line-height: 1.5; color: var(--ink-3); }
.pw-fairness p:last-child { margin-bottom: 0; }
.pw-fairness b { color: var(--ink-2); }
.pw-note.pw-soon { margin: 10px 0 0; font-size: .78rem; color: var(--ink-3); text-align: center; }

/* Phone rules — scoped ≤720px, same convention as the MOBILE section above. */
@media (max-width:720px) {
  .pw-price-grid { grid-template-columns: 1fr; }
  .pw-price-card { flex-direction: row; flex-wrap: wrap; text-align: left; align-items: baseline; gap: 8px; }
  .pw-price-card .btn-primary { width: auto; margin-left: auto; }
  .pw-lock-card { margin: 18px auto; }
}

/* Billing panel: stated plainly when there is genuinely nothing to cancel
   (free grandfathered Plus / a one-time founder purchase), so the absence of a
   cancel button reads as intentional rather than missing. */
.pw-sub-none { margin: 10px 0 0; font-size: .8rem; font-weight: 600; color: var(--ink-2); }
.pw-cards-intro { margin: 14px 0 -2px; }

/* Visually hidden, still read by search engines and screen readers. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* ── The level picker (systems/level_picker.js) ──────────────────────────────
   The `.game-level` pill, where the level is an actual choice, opens this.
   position:fixed so a game view's own overflow cannot clip it, and measured
   against the pill rather than offset by a guess. */
.kq-level-panel {
  position:fixed; z-index:70; min-width:13rem; max-width:min(20rem, 92vw);
  padding:6px; border-radius:var(--r-lg);
  background:var(--paper-card); border:1px solid var(--line-2);
  box-shadow:var(--sh-float);
  animation:viewIn .16s var(--ease);
}
.kq-level-row {
  display:flex; align-items:center; gap:10px; width:100%; text-align:left;
  padding:9px 11px; border-radius:var(--r-md); color:var(--ink); cursor:pointer;
  background:none; border:0; font:inherit;
}
.kq-level-row:hover { background:var(--paper-2); }
.kq-level-row.on { background:var(--accent-wash); }
.kq-level-row b { font-family:var(--mono); font-weight:700; font-size:.85rem; flex:none; min-width:2.2em; }
.kq-level-row small { color:var(--ink-2); font-size:.78rem; line-height:1.35; }
.kq-level-tick, .kq-level-lock { margin-left:auto; display:flex; flex:none; }
.kq-level-tick { color:var(--accent-text); }
.kq-level-lock { color:var(--ink-3); }
/* A locked level is still readable — it says what Plus opens, it does not hide it. */
.kq-level-row.pw-locked { color:var(--ink-3); }
.kq-level-row.pw-locked b { color:var(--ink-2); }
@media (prefers-reduced-motion:reduce) { .kq-level-panel { animation:none; } }

/* ── A · B · C · D on every option ───────────────────────────────────────────
   One rule, three games: `.rd-opts` is the shared grid Reading Room, Real
   Listening and Long-form all render into, so the letter arrives everywhere at
   once and cannot drift between them.

   A CSS counter rather than markup: the letter is POSITIONAL, not content —
   the options are shuffled per render (optionOrder), so a letter baked into the
   HTML would have to be recomputed on every shuffle and would be one more thing
   to get out of step with `data-i`. The counter is always what the eye sees.

   It also gives the answer a name. "The second one" is not something a learner
   can hold while they read four sentences; "C" is. */
.rd-opts, .ev-opts, .mg-opts { counter-reset: kqopt; }
.rd-opts > button, .ev-opts > button, .mg-opts > button {
  counter-increment: kqopt;
  justify-content:flex-start; gap:12px; text-align:left;
}
.rd-opts > button::before, .ev-opts > button::before, .mg-opts > button::before {
  content: counter(kqopt, upper-alpha);
  flex:none; width:26px; height:26px; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  font-family:var(--mono); font-size:.72rem; font-weight:700;
  color:var(--ink-2); border:1.5px solid var(--line-2); background:transparent;
  transition:color .12s, border-color .12s, background .12s;
}
.rd-opts > button:hover:not(:disabled)::before, .ev-opts > button:hover:not(:disabled)::before, .mg-opts > button:hover:not(:disabled)::before { color:var(--ink); border-color:var(--accent-text); }
/* The verdict owns the whole control, letter included — a chip that stayed grey
   on a correct answer read as a second, contradicting state. */
.rd-opts > button.correct::before, .ev-opts > button.correct::before, .mg-opts > button.correct::before { color:var(--moss); border-color:var(--moss); background:color-mix(in srgb, var(--moss) 14%, transparent); }
.rd-opts > button.wrong::before, .ev-opts > button.wrong::before, .mg-opts > button.wrong::before   { color:var(--brick); border-color:var(--brick); background:color-mix(in srgb, var(--brick) 14%, transparent); }
@media (max-width:520px) {
  .rd-opts > button { gap:10px; }
  .rd-opts > button::before, .ev-opts > button::before, .mg-opts > button::before { width:24px; height:24px; }
}

/* ── One skip control, every game that has one ───────────────────────────────
   Reading Room had a ghost button at the foot of the passage; Long-form had an
   underlined `.game-quietlink` above the player. Same job, two idioms, and an
   underline in a column of real controls reads as a stray link. */
.game-skip-row { margin:10px 0 0; }
.game-skip { display:inline-flex; align-items:center; gap:6px; }
.game-skip .kq-ico { display:block; }
.game-skip kbd { margin-left:2px; }
/* Multi is a set, not a choice, so the panel says so before the first tap. */
.kq-level-multi::before {
  content:'Pick any — tap to add or remove';
  display:block; padding:6px 11px 8px;
  font-size:.68rem; font-weight:700; letter-spacing:.08em; text-transform:uppercase;
  color:var(--ink-3);
}

/* ═══════════════════════════════════════════════════════════════════════════
   The Journey — journey.js (docs/ADVENTURE_VISION.md). The map, the stage
   screen, the strip and the traveller card, on the same tokens as the rest of
   the desk: cards on --paper-card, text through the derived --ink* tokens,
   the accent for the current station only.  QA: legibility · theme-sweep.
   ═══════════════════════════════════════════════════════════════════════════ */
.journey-view { padding-bottom: 28px; }
.jn-level-line {
  font-family: var(--mono); font-size: .78rem; font-weight: 600; color: var(--ink-2);
  margin: 2px 0 14px; letter-spacing: .01em;
}
.jn-foot { font-size: .78rem; color: var(--ink-3); margin: 18px 0 0; }
.jn-map { display: flex; flex-direction: column; gap: 14px; }

/* ── a region ── */
.jn-region {
  position: relative; background: var(--paper-card); border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: 14px 16px 12px;
}
.jn-region-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
.jn-region-lv {
  font-family: var(--mono); font-size: .75rem; font-weight: 700; color: var(--ink);
  background: var(--accent-wash); border: 1px solid var(--accent-wash-2);
  border-radius: 999px; padding: 2px 10px;
}
.jn-region-title { font-size: 1rem; font-weight: 700; color: var(--ink); margin: 0; }
/* --ink-2 not --ink-3: this line sits straight on the region card over the scene
   art (legibility flagged it as busy-bg on shinjuku); one step up keeps it clear. */
.jn-region-meta { font-size: .76rem; color: var(--ink-2); margin-left: auto; }
.jn-region-note { font-size: .82rem; color: var(--ink-2); line-height: 1.5; margin: 0; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.jn-region-kana { padding-bottom: 12px; }

/* ── the road — a rail down the region, one row per station. The dot on the
   rail carries the state (done · current · open · locked), the card the title
   and the stage's steps as pips; a crossing is a gold diamond among them, a
   vocab station a side hop off the rail. ── */
.jn-road { list-style: none; margin: 0; padding: 4px 0 0 38px; position: relative; display: flex; flex-direction: column; gap: 8px; }
.jn-road::before { content: ''; position: absolute; left: 15px; top: 10px; bottom: 14px; width: 2px; background: var(--line-2, var(--line)); border-radius: 2px; }
.jn-station { position: relative; display: flex; }
.jn-rail-dot {
  position: absolute; left: -38px; top: 10px; width: 32px; height: 32px; border-radius: 999px; z-index: 1;
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--mono); font-size: .82rem; font-weight: 700;
  background: var(--paper-card); border: 2px solid var(--line-2, var(--line)); color: var(--ink-2);
  transition: transform .15s var(--ease, ease), box-shadow .15s var(--ease, ease);
}
/* the rail segment ABOVE a done station is lit — progress reads as a filled line */
.jn-station.is-done::before { content: ''; position: absolute; left: -23px; top: -10px; width: 2px; height: calc(100% + 10px); background: var(--accent); }
.jn-station.is-done .jn-rail-dot { background: var(--accent); border-color: var(--accent); color: var(--accent-ink, var(--on-ink, #22150a)); }
.jn-station.is-done .jn-rail-dot .kq-ico { color: var(--accent-ink, var(--on-ink, #22150a)); }
.jn-station.is-current .jn-rail-dot { border-color: var(--accent); color: var(--accent-text); box-shadow: 0 0 0 4px var(--accent-wash); }
.jn-station.is-locked .jn-rail-dot { color: var(--ink-3); border-style: dashed; }
.jn-node {
  flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 6px;
  padding: 9px 12px 10px; border-radius: var(--r-lg); background: var(--paper-2); border: 1px solid var(--line); color: var(--ink);
  transition: border-color .15s, background .15s, box-shadow .15s;
}
.jn-node:hover { border-color: var(--accent); background: var(--paper-card); }
.jn-node-main { display: flex; align-items: flex-start; gap: 10px; width: 100%; text-align: left; padding: 0; border: 0; background: none; font: inherit; color: inherit; cursor: pointer; }
.jn-node-txt { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.jn-title { font-size: .9rem; font-weight: 700; line-height: 1.3; }
.jn-sub { font-size: .72rem; color: var(--ink-3); line-height: 1.35; }
.jn-station.is-current .jn-node { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-wash); }
.jn-station.is-done .jn-node { border-color: var(--accent-wash-2, var(--line)); }
.jn-station.is-locked .jn-node { color: var(--ink-3); background: transparent; border-style: dashed; }
.jn-station.is-locked .jn-title { color: var(--ink-2); }
.jn-kind { font-size: .62rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3); }

/* the steps of a stage, as pips — the big picture of what is done inside it */
.jn-pips { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: center; }
.jn-pip { display: inline-flex; align-items: center; gap: 5px; font-size: 13px; color: var(--ink-2); line-height: 1.2; }   /* 13px: the Japanese legibility floor — pip labels carry kana */
.jn-pip-dot {
  width: 16px; height: 16px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center;
  border: 1.5px solid var(--line-2, var(--line)); background: var(--paper-card); color: var(--ink-3);
}
.jn-pip.is-open .jn-pip-dot { border-color: var(--ink-3); color: var(--ink-2); }
.jn-pip.is-done { color: var(--ink); }
.jn-pip.is-done .jn-pip-dot { background: var(--accent); border-color: var(--accent); color: var(--accent-ink, var(--on-ink, #22150a)); }
.jn-pip.is-done .jn-pip-dot .kq-ico, .jn-pip-cross.is-done .jn-diamond .kq-ico { color: var(--accent-ink, var(--on-ink, #22150a)); }
.jn-pip-lbl { white-space: nowrap; }
.jn-pip-btn { display: inline-flex; align-items: center; gap: 6px; padding: 0 2px; border: 0; background: none; font: inherit; font-size: 13px; color: var(--ink-2); cursor: pointer; border-radius: 4px; }
.jn-pip-btn:hover .jn-pip-lbl { text-decoration: underline; color: var(--ink); }
.jn-pip-cross .jn-pip-lbl { font-weight: 700; }
/* the diamond — a crossing's shape wherever it appears */
.jn-diamond {
  display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; margin: 0 3px;
  transform: rotate(45deg); border-radius: 3px;
  background: var(--paper-card); border: 1.5px solid var(--gold, var(--accent)); color: var(--gold, var(--accent-text));
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--gold, var(--accent)) 18%, transparent);
}
.jn-diamond .kq-ico { transform: rotate(-45deg); }
.jn-pip-cross.is-done .jn-diamond { background: var(--gold, var(--accent)); color: var(--accent-ink, var(--on-ink, #22150a)); }
.jn-pip-cross:not(.is-open):not(.is-done) .jn-diamond { border-style: dashed; box-shadow: none; opacity: .7; }
.jn-pip-cross:not(.is-open):not(.is-done) .jn-pip-btn { color: var(--ink-3); }

/* vocab stations hang off the rail as side hops */
.jn-station.jn-vocab { margin-left: 26px; }
.jn-station.jn-vocab::after { content: ''; position: absolute; left: -49px; top: -6px; width: 24px; height: 30px; border-left: 2px dashed var(--line-2, var(--line)); border-bottom: 2px dashed var(--line-2, var(--line)); border-bottom-left-radius: 14px; }
.jn-station.jn-vocab .jn-rail-dot { width: 26px; height: 26px; left: -34px; top: 9px; border-radius: var(--r-sm, 8px); border-style: dashed; font-size: .72rem; }
.jn-station.jn-vocab.is-done .jn-rail-dot { border-style: solid; }
.jn-station.jn-vocab::before { display: none; }
.jn-station.jn-vocab .jn-node { padding: 7px 12px 8px; background: transparent; border-style: dashed; }
.jn-station.jn-vocab .jn-title { font-size: .84rem; }

/* the boss closes the rail */
.jn-boss .jn-rail-dot { width: 36px; height: 36px; left: -40px; top: 8px; border-width: 2px; }
.jn-boss .jn-node { border-style: dashed; }
.jn-boss.is-open .jn-node { border-style: solid; border-color: var(--accent); }
.jn-boss.is-open .jn-rail-dot { border-color: var(--accent); color: var(--accent-text); }
.jn-boss-note { font-size: .8rem; color: var(--ink-2); line-height: 1.5; margin: 10px 0 0; }
/* A region with no stages yet: the locked-row surface the level picker uses. */
.jn-region-locked { padding: 6px; }
.jn-locked-row { width: 100%; }
.jn-locked-row small { flex: 1 1 auto; }

/* ── signposts: one line at the top of a stage, dismissible ── */
.jn-signpost { display: flex; gap: 10px; align-items: flex-start; margin: 0 0 12px; padding: 10px 12px; border-radius: var(--r-md); background: var(--paper-2); border: 1px solid var(--line); border-left: 3px solid var(--gold, var(--accent)); }
.jn-signpost .kq-ico { color: var(--gold, var(--accent-text)); flex: none; margin-top: 2px; }
.jn-signpost-txt { flex: 1 1 auto; font-size: .86rem; line-height: 1.5; color: var(--ink); }
.jn-signpost-txt b { font-size: .68rem; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-2); display: block; margin-bottom: 2px; }
.jn-signpost .coach-clear { flex: none; }
.jn-crosslinks { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 0 0 12px; font-size: .8rem; color: var(--ink-2); }
.jn-crosslink { display: inline-flex; align-items: center; gap: 5px; }

/* ── crossing screen ── */
.jn-cross-tagline { font-family: var(--serif); font-style: italic; font-size: .95rem; color: var(--ink-2); line-height: 1.55; margin: 0 0 14px; }
.jn-cross-steps { display: flex; flex-direction: column; gap: 14px; }
.jn-cross-step { background: var(--paper-card); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 14px 16px; }
.jn-cross-step.is-done { border-color: var(--accent-wash-2, var(--line)); }
.jn-cross-step-head { display: flex; align-items: center; gap: 10px; margin: 0 0 8px; }
.jn-cross-step-head h3 { margin: 0; font-size: 1rem; color: var(--ink); }
.jn-cross-step-head .jn-num { width: 24px; height: 24px; font-size: .74rem; }
.jn-cross-step.is-done .jn-cross-step-head .jn-num { background: var(--accent-wash); color: var(--ink); }
.jn-cross-note { font-size: .82rem; color: var(--ink-2); line-height: 1.5; margin: 0 0 8px; }
.jn-cross-explain .lesson-sec { margin-top: 10px; }
/* A crossing section's points: one idea, its sentence right under it. */
.jn-pt { margin-top: 12px; padding: 10px 12px 10px 14px; border-left: 3px solid var(--accent-wash-2, var(--line)); background: var(--paper-card); border-radius: 0 var(--r-md) var(--r-md) 0; }
.jn-pt > .lesson-b { margin: 0 0 4px; }
.jn-pt .lesson-ex { border-top-style: dotted; }
/* Word-by-word glosses on examples: one column per chunk — the Japanese, how to
   say it, what it does. Particles ('p') are lit; verbs ('v') carry the clay. */
.jn-gloss-legend { font-size: .82rem; color: var(--ink-2); line-height: 1.6; margin: 0 0 4px; }
.jn-gw-demo { display: inline-flex; flex-direction: row; align-items: baseline; gap: 5px; padding: 0 7px; margin: 0 2px; vertical-align: baseline; }
.jn-gw-demo .jn-gw-jp { font-size: 1rem; }
.jn-gw-demo .jn-gw-en { margin: 0; }
.lesson-ex.jn-glossed { display: block; }
.jn-gloss { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 6px 4px; margin: 2px 0 8px; }
.jn-gw { display: inline-flex; flex-direction: column; align-items: center; text-align: center; min-width: 2.2ch; padding: 4px 7px 5px; border-radius: 8px; background: var(--paper-2); border: 1px solid transparent; }
.jn-gw-jp { font-family: var(--jp); font-size: 1.3rem; line-height: 1.25; color: var(--ink); }
.jn-gw-ro { font-family: var(--mono); font-size: .66rem; color: var(--ink-3); margin-top: 2px; letter-spacing: .01em; }
.jn-gw-en { font-size: .74rem; line-height: 1.25; color: var(--ink-2); margin-top: 3px; max-width: 10.5rem; }
.jn-gw.is-p { background: var(--accent-wash); border-color: var(--accent-wash-2); }
.jn-gw.is-p .jn-gw-jp { color: var(--accent-text); font-weight: 700; }
.jn-gw.is-p .jn-gw-en { color: var(--accent-text); font-weight: 600; }
.jn-gw.is-v .jn-gw-jp { color: var(--clay); font-weight: 700; }
.jn-gloss-line { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px; }
.jn-gloss-line .lesson-ex-jp { font-size: 1rem; }
.jn-gloss-note { font-size: .84rem; line-height: 1.55; color: var(--ink-2); margin: 8px 0 0; padding-left: 10px; border-left: 3px solid var(--accent-wash-2); }
/* The quick check — three questions answered on the page */
.jn-check { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.jn-check-q { padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--r-md); background: var(--paper); }
.jn-check-q.is-right { border-color: var(--accent-wash-2); }
.jn-check-text { margin: 0 0 8px; font-size: .92rem; color: var(--ink); line-height: 1.5; display: flex; gap: 8px; align-items: baseline; }
.jn-check-n { flex: none; font-family: var(--mono); font-size: .72rem; font-weight: 700; color: var(--ink-3); }
.jn-check-opts { display: flex; flex-direction: column; gap: 6px; }
.jn-check-opt { text-align: left; font: inherit; font-size: .88rem; line-height: 1.4; color: var(--ink); background: var(--paper-card); border: 1px solid var(--line); border-radius: var(--r-md); padding: 8px 12px; cursor: pointer; }
.jn-check-opt:hover:not(:disabled) { border-color: var(--accent-wash-2); background: var(--accent-wash); }
.jn-check-opt:disabled { cursor: default; opacity: .6; }
.jn-check-opt.is-right { opacity: 1; border-color: var(--accent-text); background: var(--accent-wash); color: var(--accent-text); font-weight: 600; }
.jn-check-opt.is-wrong { border-color: var(--clay); background: var(--clay-wash); }
.jn-check-why { margin: 8px 0 0; font-size: .82rem; line-height: 1.5; color: var(--accent-text); }
.jn-check-why.is-miss { color: var(--clay); }
.jn-check-done { margin: 12px 0 0; font-size: .88rem; color: var(--accent-text); display: flex; gap: 6px; align-items: center; }
.jn-further { margin: 12px 0 0; font-size: .82rem; color: var(--ink-2); }
.jn-further a { color: var(--accent-text); }
.jn-write { display: flex; flex-direction: column; gap: 10px; }
.jn-write textarea { width: 100%; min-height: 96px; resize: vertical; font: inherit; font-size: .92rem; line-height: 1.5; color: var(--ink); background: var(--paper); border: 1px solid var(--line); border-radius: var(--r-md); padding: 10px 12px; }
.jn-write textarea:focus { outline: 2px solid var(--accent-wash-2, var(--line)); outline-offset: 1px; border-color: var(--accent); }
.jn-write-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.jn-reveal { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 6px; }
.jn-reveal-col { background: var(--paper-2); border: 1px solid var(--line); border-radius: var(--r-md); padding: 10px 12px; }
.jn-reveal-col h4 { margin: 0 0 6px; font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-2); }
.jn-reveal-col p { margin: 0; font-size: .9rem; line-height: 1.55; color: var(--ink); white-space: pre-wrap; }
.jn-reveal-col.is-model { border-color: var(--accent-wash-2, var(--line)); background: var(--accent-wash); }
.jn-selfcheck { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-top: 8px; font-size: .84rem; color: var(--ink-2); }
.jn-ai-note { margin-top: 8px; font-size: .84rem; line-height: 1.5; color: var(--ink); padding: 8px 12px; border-radius: var(--r-md); background: var(--paper-2); border: 1px solid var(--line); }
.jn-cross-done { font-size: .84rem; color: var(--ink); margin: 12px 0 0; padding: 10px 12px; border-radius: var(--r-md); background: var(--paper-2); border: 1px solid var(--line); }

/* ── vocab station screen ── */
/* ── Learn first — Vocabulary and Kanji ────────────────────────────────────────
   Both games opened straight onto a question for material the learner had
   never met. Each now has a Learn screen for a deck with unseen items: the set
   laid out in full, taught before it is tested, with a Words / Cards button in
   the head to bring it back mid-round. The vocabulary list reuses Word Groups'
   Learn shape (.rd-vlist); the kanji list is a card per character. */
.voc-learn, .kj-learn { display:flex; flex-direction:column; align-items:center; gap:14px; padding:6px 0 18px; }
/* Start stays in reach on a long list — the button sits on a strip of the page's own ground */
.voc-learn .card-btns, .kj-learn .card-btns { position:sticky; bottom:0; width:100%; display:flex; justify-content:center;
  padding:10px 0 12px; background:linear-gradient(180deg, transparent, var(--paper) 40%); }
/* the button keeps its own width inside the strip — .card-btns elsewhere stretches its children */
.voc-learn .card-btns .btn-primary, .kj-learn .card-btns .btn-primary { flex:0 0 auto; width:auto; min-width:260px; }
.voc-learn-list.rd-vlist { width:100%; max-width:720px; gap:12px; font-size:1.1rem; }
.voc-learn-list .rd-vw { font-size:1.5rem; }
.voc-learn-list li { padding:8px 12px; border-radius:var(--r-md); background:var(--paper-card); border:1px solid var(--line); }
.voc-learn-list li.is-new { border-color:var(--accent); }
.kj-learn-list { list-style:none; margin:0; padding:0; width:100%; max-width:860px;
  display:grid; grid-template-columns:repeat(auto-fit, minmax(340px, 1fr)); gap:12px; }
.kj-ln { display:flex; gap:14px; align-items:flex-start; padding:12px 14px;
  background:var(--paper-card); border:1px solid var(--line); border-radius:var(--r-md); }
.kj-ln.is-new { border-color:var(--accent); }
.kj-ln-glyph { font-family:var(--jp); font-size:2.6rem; line-height:1; color:var(--ink); flex:none; min-width:3rem; text-align:center; padding-top:2px; }
.kj-ln-body { min-width:0; flex:1; display:flex; flex-direction:column; gap:6px; }
.kj-ln-top { display:flex; flex-wrap:wrap; align-items:baseline; gap:4px 12px; }
.kj-ln-mean { font-size:1.05rem; color:var(--ink); }
.kj-ln-tag { font-size:.92rem; color:var(--ink-2); font-family:var(--jp); }
.kj-ln-tag i { font-style:normal; font-size:.68rem; color:var(--accent-text); margin-right:5px; vertical-align:1px; }
.kj-ln-parts { display:flex; flex-wrap:wrap; gap:6px; }
.kj-ln-part { font-size:.82rem; color:var(--ink-2); padding:2px 9px; border:1px solid var(--line); border-radius:99px; background:var(--paper-2); }
.kj-ln-part small { color:var(--ink-3); margin-left:5px; }
.kj-ln-sent { margin:2px 0 0; display:flex; flex-direction:column; gap:2px; }
.kj-ln-jp { font-family:var(--jp); font-size:1rem; color:var(--ink); }
.kj-ln-read { font-size:.78rem; color:var(--ink-3); }
.kj-ln-en { font-size:.82rem; color:var(--ink-2); font-style:italic; }
@media (max-width:560px) {
  .kj-learn-list { grid-template-columns:1fr; } .kj-ln-glyph { font-size:2.2rem; min-width:2.6rem; }
  .voc-learn .wg-learn-hint, .kj-learn .wg-learn-hint { padding:0 28px; }
}
/* The nav drawer's pull tab (.nav-drawer-handle, 27px, fixed at mid-height on
   phones) sits over the left edge of whatever scrolls under it. Cards carry
   their own padding, so only the flush-left instruction line loses its first
   letter — seen on the Write step's "Put the pieces in order…" at 390px. Every
   game's prompt keeps clear of it. */
@media (max-width:720px) {
  .game-prompt { padding-left:28px; }
  .game-head > .game-name { padding-left:12px; }   /* the title's mark, same clearance */
}

/* ── Vocab station ─────────────────────────────────────────────────────────
   One card per word group, each with its own Practise button, so a learner can
   take the sets one at a time instead of the whole station in one sitting.
   The words are SHOWN — surface, reading, meaning — because the station's job
   is to teach the set before Word Groups quizzes it. Chip fill is Word Groups'
   own progress: hollow = new, half = started, filled = mastered. */
.jn-vs-summary { background: var(--paper-card); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 12px 16px; margin: 0 0 14px; }
.jn-vs-summary-top { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; margin-bottom: 8px; }
.jn-vs-summary-top b { font-size: .95rem; color: var(--ink); }
.jn-vs-summary-top span { font-size: .78rem; color: var(--ink-3); font-family: var(--mono); }
.jn-vs-legend { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; font-size: .74rem; color: var(--ink-3); margin: 10px 0 0; }
.jn-vs-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; border: 1.5px solid var(--line-2); background: var(--paper-2); vertical-align: -1px; }
.jn-vs-dot.is-started { background: linear-gradient(90deg, var(--accent) 50%, var(--paper-2) 50%); border-color: var(--accent); }
.jn-vs-dot.is-mastered { background: var(--grad-warm); border-color: transparent; }

.jn-vs-groups { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 14px; margin: 0 0 16px; }
.jn-vs-group { display: flex; flex-direction: column; gap: 10px; }
.jn-vs-head { display: flex; align-items: center; gap: 12px; }
.jn-vs-art { flex: none; width: 44px; height: 44px; display: grid; place-items: center; border-radius: var(--r-md); background: var(--paper-2); overflow: hidden; }
.jn-vs-art svg { width: 44px; height: 44px; display: block; }
.jn-vs-art-fallback { color: var(--ink-3); display: grid; place-items: center; width: 100%; height: 100%; }
.jn-vs-title { min-width: 0; flex: 1; }
.jn-vs-title b { display: block; font-size: .98rem; color: var(--ink); line-height: 1.25; }
.jn-vs-title small { display: block; font-size: .76rem; color: var(--ink-3); margin-top: 2px; }
.jn-vs-pct { font-family: var(--mono); font-size: .8rem; font-weight: 700; color: var(--ink-2); font-variant-numeric: tabular-nums; }

.jn-vs-words { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
.jn-vs-word {
  display: flex; flex-direction: column; align-items: flex-start;
  padding: 6px 9px; min-width: 0;
  border-radius: var(--r-md); border: 1px solid var(--line);
  background: var(--paper-2);
  line-height: 1.2; cursor: help;
}
.jn-vs-word.is-started { border-color: var(--accent); background: linear-gradient(180deg, var(--accent-wash), var(--paper-2)); }
.jn-vs-word.is-mastered { border-color: transparent; background: var(--accent-wash); box-shadow: inset 0 0 0 1.5px var(--accent); }
.jn-vs-k { font-size: 1.02rem; font-weight: 600; color: var(--ink); }
.jn-vs-kana { font-size: .7rem; color: var(--ink-3); margin-top: 1px; }
.jn-vs-en { font-size: .72rem; color: var(--ink-2); margin-top: 3px; max-width: 14ch; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.jn-vs-play { align-self: flex-start; margin-top: 2px; }

.jn-vs-all { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; }
.jn-vs-all .jn-cross-note { margin: 0; max-width: 62ch; }
@media (max-width: 560px) { .jn-vs-groups { grid-template-columns: 1fr; } }
@media (max-width: 720px) { .jn-reveal { grid-template-columns: 1fr; } }

/* ── stage screen ── */
.jn-locked-note { display: flex; gap: 8px; align-items: center; font-size: .82rem; color: var(--ink-2); margin: 0 0 12px; }
.jn-stage { display: grid; grid-template-columns: minmax(0, 1fr) minmax(240px, 300px); gap: 14px; align-items: start; }
.jn-card { background: var(--paper-card); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 14px 16px; }
.jn-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.jn-step {
  display: flex; gap: 12px; align-items: flex-start; background: var(--paper-card);
  border: 1px solid var(--line); border-radius: var(--r-lg); padding: 12px 14px;
}
.jn-step.is-done { border-color: var(--accent-wash-2); }
.jn-step.is-locked { opacity: .72; }
.jn-step-num {
  flex: none; width: 26px; height: 26px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--mono); font-size: .78rem; font-weight: 700; background: var(--paper-2); border: 1px solid var(--line); color: var(--ink-2);
}
.jn-step.is-done .jn-step-num { background: var(--accent-wash); border-color: var(--accent-wash-2); color: var(--ink); }
.jn-step-cross { border-color: color-mix(in srgb, var(--gold, var(--accent)) 45%, var(--line)); background: linear-gradient(90deg, color-mix(in srgb, var(--gold, var(--accent)) 10%, var(--paper-card)) 0%, var(--paper-card) 55%); }
.jn-step-cross .jn-step-num.jn-diamond { width: 18px; height: 18px; margin: 4px 7px 0 4px; border-radius: 4px; font-size: .7rem; }
.jn-step-cross.is-done .jn-step-num.jn-diamond { background: var(--gold, var(--accent)); color: var(--accent-ink, var(--on-ink, #22150a)); }
.jn-step-cross.is-done .jn-step-num.jn-diamond .kq-ico { color: var(--accent-ink, var(--on-ink, #22150a)); transform: rotate(-45deg); }
.jn-step-btn-cross { border-color: color-mix(in srgb, var(--gold, var(--accent)) 50%, var(--line)); }
.jn-cross-continue { margin-left: 10px; vertical-align: middle; }
.jn-cross-done { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.jn-step-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
.jn-step-body b { font-size: .92rem; color: var(--ink); }
.jn-step-body small { font-size: .78rem; color: var(--ink-2); line-height: 1.45; }
.jn-step-btns { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.jn-step-btn .jn-count { margin-left: 6px; font-family: var(--mono); font-weight: 600; font-size: .7rem; color: var(--ink-3); }
/* Practice count on a step button — answers so far / needed for the stage */
/* ── The right rail, on the road ────────────────────────────────────────────
   Three cards about ground already covered: the region, this stage's steps, and
   the days you came back. The category cards stay in the DOM (their handles are
   cached once at load — see app.js D) and are hidden by this class instead. */
.sr-journey { display:flex; flex-direction:column; gap:14px; }
.side-right.is-journey > .sr-card { display:none; }
.side-right.is-journey > .sr-journey > .sr-card { display:block; }

.jn-rail-line { font-size:.84rem; font-weight:600; color:var(--ink); margin:0; line-height:1.3; }
.jn-rail-sub { font-size:.75rem; color:var(--ink-2); margin:7px 0 0; line-height:1.45; }
.jn-rail-top { display:flex; align-items:baseline; justify-content:space-between; gap:8px; margin-bottom:9px; }
.jn-rail-val { font-family:var(--mono); font-size:.82rem; font-weight:700; color:var(--ink); font-variant-numeric:tabular-nums; }
.jn-rail-val.is-dim { color:var(--ink-3); font-weight:600; }

/* The region as pips — one slot per stage. A slot that PAYS wears a diamond
   above its bar: hollow while the reward is still owed, filled once collected.
   The slot, not the 7px bar, is the hover target — the bar alone is unhittable. */
.jn-rail-pips { display:flex; gap:3px; margin-bottom:11px; align-items:flex-end; }
.jn-rail-slot {
  flex:1; position:relative; display:flex; align-items:flex-end;
  min-width:0; height:9px; padding-top:0;
}
.jn-rail-slot.pays { height:20px; cursor:help; }
.jn-rail-pips:has(.jn-rail-slot.pays) .jn-rail-slot { height:20px; }
.jn-rail-pip { flex:1; height:7px; border-radius:99px; background:var(--paper-2); }
.jn-rail-pip.is-done { background:var(--grad-warm); }
.jn-rail-pip.is-here { background:var(--accent); box-shadow:0 0 0 2px var(--accent-wash); }
.jn-rail-pip.is-open { background:var(--line-2); }
.jn-rail-mark {
  position:absolute; left:50%; top:0; transform:translateX(-50%) rotate(45deg);
  width:6px; height:6px; box-sizing:border-box;
  background:var(--paper-card); border:1.5px solid var(--line-2);
  transition:background .2s var(--ease), border-color .2s var(--ease);
}
.jn-rail-mark.is-won { background:var(--grad-warm); border-color:transparent; }
.jn-rail-slot.pays:hover .jn-rail-mark { border-color:var(--accent); }
.jn-rail-slot.pays:hover .jn-rail-pip { filter:brightness(1.25); }

.jn-rail-next {
  font-size:.73rem; color:var(--ink-2); margin:9px 0 0;
  padding-top:9px; border-top:1px solid var(--line);
  display:flex; align-items:baseline; gap:5px;
}
.jn-rail-next b { color:var(--ink); font-weight:700; }

/* this stage's steps */
.jn-rail-steps { list-style:none; display:flex; flex-wrap:wrap; gap:5px; margin:11px 0 0; padding:0; }
.jn-rail-step {
  display:inline-flex; align-items:center; gap:4px;
  font-size:.69rem; font-weight:600; color:var(--ink-3);
  padding:3px 8px; border-radius:99px;
  border:1px solid var(--line); background:var(--paper-2);
}
.jn-rail-step.is-done { color:var(--ink); border-color:var(--line-2); background:var(--accent-wash); }
.jn-rail-step.is-next { color:var(--ink); border-color:var(--accent); background:var(--paper-card); }
.jn-rail-tick { display:inline-flex; width:10px; justify-content:center; }
.jn-rail-extra { font-size:.71rem; color:var(--ink-3); margin:9px 0 0; display:flex; align-items:center; gap:5px; }
.jn-rail-go { width:100%; margin-top:11px; font-size:.78rem; padding:8px 10px; }

/* the effort chain — answers a day, so it can never report a decline */
.jn-rail-chain { display:flex; align-items:flex-end; gap:3px; height:40px; }
.jn-rail-chain > i { flex:1; display:block; min-height:3px; border-radius:2px 2px 0 0; background:var(--grad-warm); }
.jn-rail-chain > i.is-zero { background:var(--paper-2); }
.jn-rail-scale {
  display:flex; justify-content:space-between;
  font-family:var(--mono); font-size:.6rem; font-weight:700; letter-spacing:.04em;
  color:var(--ink-3); margin-top:3px;
}

/* A locked crossing anywhere it is drawn. Only the stage screen's button used
   to carry `disabled`; the map node, the step pips and the crosslinks did not,
   so all three opened a crossing whose guide had not been read. */
.jn-pip-cross.is-locked .jn-pip-btn,
.jn-crosslink[disabled],
.jn-crossing .jn-node[disabled] { opacity:.55; cursor:not-allowed; }
.jn-pip-cross.is-locked .jn-diamond { background:var(--paper-2); border-color:var(--line); }

/* ── Optional steps ─────────────────────────────────────────────────────────
   A pool a stage OFFERS rather than REQUIRES (encounter.opt — listening and
   manga through the early N5 stages). It must read as an invitation, never as
   an unmet demand: no "0/3", a quieter button, and its own block under the
   gate's bar so the bar stays a picture of what is actually being asked. */
.jn-prac.is-opt {
  background:transparent; border:1px dashed var(--line-2);
  color:var(--ink-3); font-weight:600;
}
.jn-prac.is-opt.is-free { font-size:.6rem; letter-spacing:.06em; text-transform:uppercase; padding:1px 6px; }
.jn-prac.is-opt.is-done { border-style:solid; border-color:var(--ok, var(--accent)); color:var(--ink-2); }
.jn-step-btn.is-optional { opacity:.86; border-style:dashed; }
.jn-step-btn.is-optional:hover { opacity:1; }
.jn-step-extra-note {
  font-size:.72rem; color:var(--ink-3); line-height:1.45;
  margin:10px 0 6px; max-width:62ch;
}
.jn-step-btns-extra { opacity:.94; }
.jn-gate-head-opt { margin-top:14px; }
.jn-gate-head-opt b { color:var(--ink-2); }
.jn-gate-steps.is-optional { opacity:.8; }
.jn-gate-steps.is-optional li small { font-style:italic; }

.jn-prac { display: inline-flex; align-items: center; gap: 3px; margin-left: 6px; padding: 1px 6px; border-radius: 999px; font-family: var(--mono); font-size: .68rem; font-weight: 700; color: var(--ink-2); background: var(--paper-2); border: 1px solid var(--line); }
.jn-prac.is-done { color: var(--accent-text); background: var(--accent-wash); border-color: var(--accent-wash-2); }
.jn-gate-sub { margin: 6px 0 0; font-size: .78rem; color: var(--ink-3); }
.jn-gate-block { margin-top: 12px; padding-top: 10px; border-top: 1px dashed var(--line); }
.jn-gate-block:first-of-type { margin-top: 4px; padding-top: 0; border-top: 0; }
.jn-gate-head { display: flex; justify-content: space-between; align-items: baseline; margin: 0 0 6px; font-size: .88rem; color: var(--ink); }
.jn-gate-head b { font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-2); }
.jn-gate-block .jn-gate-steps { margin-top: 10px; }
.jn-gate-steps { list-style: none; margin: 0 0 10px; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.jn-gate-steps li { display: flex; align-items: center; gap: 7px; font-size: .82rem; color: var(--ink-2); }
.jn-gate-steps li.is-done { color: var(--ink); }
.jn-gate-steps li small { margin-left: auto; font-family: var(--mono); font-size: .7rem; color: var(--ink-3); }
.jn-gate-steps li.is-done small { color: var(--accent-text); }
.jn-gate-steps .jn-dot { width: 10px; height: 10px; border-radius: 50%; border: 1.5px solid var(--line); flex: none; }
.jn-gate-left { white-space: pre-line; }
.jn-step-btn:disabled { opacity: .5; cursor: not-allowed; }
.jn-gate-line { font-family: var(--mono); font-size: .95rem; font-weight: 700; color: var(--ink); margin: 0 0 8px; display: flex; align-items: center; gap: 6px; }
.jn-gate-bar { height: 6px; border-radius: 999px; background: var(--paper-2); overflow: hidden; margin: 0 0 10px; }
.jn-gate-bar > span { display: block; height: 100%; background: var(--accent); border-radius: 999px; transition: width .3s var(--ease, ease); }
.jn-gate-left { font-size: .8rem; color: var(--ink); line-height: 1.5; margin: 0 0 8px; }
.jn-gate-note { font-size: .74rem; color: var(--ink-3); line-height: 1.5; margin: 0; }
.jn-next-btn { margin-top: 12px; width: 100%; }

/* ── the strip ── */
.journey-strip {
  display: flex; align-items: center; gap: 10px; margin: 0 0 10px; padding: 7px 12px;
  border-radius: var(--r-md); background: var(--accent-wash); border: 1px solid var(--accent-wash-2); color: var(--ink);
}
.jn-strip-ico { display: flex; flex: none; color: var(--accent-text); }
.jn-strip-txt { display: flex; flex-direction: column; gap: 0; min-width: 0; flex: 1; font-size: .8rem; line-height: 1.3; }
.jn-strip-txt b { font-weight: 700; }
.jn-strip-step { font-size: .72rem; color: var(--ink-2); }
.jn-strip-back { flex: none; }

/* ── the Journey's entry in the readiness panel (the N5 bar is the way in) ── */
.n5-journey {
  margin: 0 0 14px; padding: 12px 14px; border-radius: var(--r-lg);
  border: 1px solid var(--accent-wash-2); background: var(--accent-wash); color: var(--ink);
}
.n5-journey-eyebrow { font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-2); margin: 0 0 6px; display: flex; align-items: center; gap: 6px; }
.n5-journey-line { font-size: .95rem; font-weight: 700; margin: 0 0 4px; }
.n5-journey-sub { font-size: .8rem; color: var(--ink-2); line-height: 1.5; margin: 0 0 10px; }
.n5-journey .btn-primary { width: auto; }
/* cleared is not mastered — the second reading on a cleared stage */
.jn-gate-mastery { font-family: var(--mono); font-size: .8rem; font-weight: 600; color: var(--ink-2); margin: 0 0 8px; }
.jn-gate-encourage { font-size: .82rem; color: var(--ink); line-height: 1.55; margin: 8px 0 0; padding: 10px 12px; border-radius: var(--r-md); background: var(--paper-2); border: 1px solid var(--line); }

/* ── exam results: the road's verdict ── */
.jn-exam-result { margin: 14px 0 12px; padding: 12px 14px; border-radius: var(--r-md); background: var(--paper-2); border: 1px solid var(--line); text-align: left; }
.jn-exam-line { font-size: .88rem; color: var(--ink); margin: 0 0 10px; line-height: 1.5; }
.jn-route { display: flex; flex-wrap: wrap; gap: 6px; }

/* ── the guide, when a stage opened it: SPOTLIGHT the sections the stage is
   about and dim the rest. The lit section wears an accent frame and a tag; a
   dimmed one is still legible on hover/focus/tap (.is-lit), never hidden. ── */
/* Guide opened from a stage: the way back sits at the top and stays there. */
.lesson-nav { position: sticky; top: 0; z-index: 2; display: flex; justify-content: space-between; gap: 8px; flex-wrap: wrap; margin: -8px -8px 10px; padding: 8px; background: var(--paper-card); border-bottom: 1px solid var(--line); }
.lesson-more { margin: 14px 0 0; }
/* The "crossed" moment: an inline gold banner at the reveal, and a gold diamond in the popup. */
.jn-crossed { display: flex; align-items: center; gap: 10px; margin: 0 0 12px; padding: 10px 14px; border-radius: var(--r-md); color: var(--ink); font-size: .9rem;
  border: 1px solid color-mix(in srgb, var(--gold, var(--accent)) 55%, var(--line));
  background: linear-gradient(100deg, color-mix(in srgb, var(--gold, var(--accent)) 26%, var(--paper-card)) 0%, var(--paper-card) 45%, color-mix(in srgb, var(--gold, var(--accent)) 22%, var(--paper-card)) 100%);
  background-size: 220% 100%; animation: jnSweep 1.6s var(--ease) both; }
.jn-crossed-diamond, .jn-pop-diamond { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 6px; transform: rotate(45deg);
  background: var(--gold, var(--accent)); color: var(--accent-ink, var(--on-ink, #22150a)); flex: none; animation: jnDiamondIn .7s var(--ease-bounce, var(--ease)) both; }
.jn-crossed-diamond .kq-ico, .jn-pop-diamond .kq-ico { transform: rotate(-45deg); }
.jn-pop-diamond-wrap { display: flex; justify-content: center; }
.jn-pop-diamond { width: 58px; height: 58px; border-radius: 12px; box-shadow: 0 0 0 0 color-mix(in srgb, var(--gold, var(--accent)) 45%, transparent); animation: jnDiamondIn .7s var(--ease-bounce, var(--ease)) both, jnGlow 1.8s .5s ease-out 2; }
@keyframes jnDiamondIn { 0% { transform: rotate(45deg) scale(0); opacity: 0; } 60% { transform: rotate(45deg) scale(1.18); opacity: 1; } 100% { transform: rotate(45deg) scale(1); opacity: 1; } }
@keyframes jnGlow { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--gold, var(--accent)) 50%, transparent); } 100% { box-shadow: 0 0 0 22px color-mix(in srgb, var(--gold, var(--accent)) 0%, transparent); } }
@keyframes jnSweep { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }
@media (prefers-reduced-motion: reduce) { .jn-crossed, .jn-crossed-diamond, .jn-pop-diamond { animation: none; } }
.jn-cross-nav { margin: 0 0 12px; background: var(--paper); border-radius: var(--r-md); border: 1px solid var(--line); }
.jn-cross-nav.is-foot { position: static; margin: 14px 0 0; }
.lesson-rest { margin-top: 12px; display: flex; flex-direction: column; gap: 14px; }
.lesson-spot-note { font-size: .84rem; color: var(--ink-2); line-height: 1.5; margin: 0 0 12px; padding: 8px 12px; border-radius: var(--r-md); background: var(--accent-wash); border: 1px solid var(--accent-wash-2, var(--line)); }
.lesson-sec-focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-wash), 0 8px 24px -14px var(--accent);
  background: linear-gradient(180deg, var(--accent-wash) 0%, var(--paper) 46%);
}
.lesson-sec-tag { display: inline-flex; align-items: center; gap: 5px; font-size: .68rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--accent-text); margin: 0 0 6px; }
.lesson-sec-tag .kq-ico { color: var(--accent-text); }
.lesson-body.lesson-focused .lesson-sec-dim { opacity: .38; filter: saturate(.35); transition: opacity .18s ease, filter .18s ease; cursor: pointer; }
.lesson-body.lesson-focused .lesson-sec-dim:hover,
.lesson-body.lesson-focused .lesson-sec-dim:focus-within,
.lesson-body.lesson-focused .lesson-sec-dim:focus,
.lesson-body.lesson-focused .lesson-sec-dim.is-lit { opacity: 1; filter: none; cursor: default; }
.lesson-body.lesson-focused .lesson-sec-dim:focus { outline: 2px solid var(--accent-wash-2, var(--line)); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .lesson-body.lesson-focused .lesson-sec-dim { transition: none; } }

@media (max-width: 720px) {
  .jn-stage { grid-template-columns: 1fr; }
  .jn-station { flex: 1 1 100%; max-width: none; }
  .jn-station + .jn-station::before { display: none; }
  .jn-region-meta { margin-left: 0; width: 100%; }
  .journey-strip { flex-wrap: wrap; }
  .jn-strip-back { margin-left: auto; }
}

/* ── Clause Weaver — Sentence Builder weave items (ADVENTURE_TODO D9 / C11) ──
   The clause tile reads as a clause (bracketed, dashed); the nouns are the
   targets it attaches to. The reveal writes the phrase the way the lesson does:
   [clause] noun. Same .order-tile / .order-build as every other tile. */
.order-tile-clause { font-weight:700; border-style:dashed; }
.order-tile-clause::before { content:'['; opacity:.55; margin-right:3px; }
.order-tile-clause::after  { content:']'; opacity:.55; margin-left:3px; }
.order-tile-noun { border-color:var(--accent-wash-2); background:var(--accent-wash); }
.order-tile-noun.placed { border-color:var(--accent); }
.weave-clause { color:var(--accent-text); font-weight:700; }
.weave-noun   { font-weight:700; }

/* ── Readiness panel — the level row (ADVENTURE_TODO D8) ── */
.n5-level-row { margin:0 0 12px; }

/* ── Composition Dojo · BUILD mode (screens/composition.js) ──────────────────
   The Sentence Builder's tiles, with furigana: every kanji piece wears its
   reading, so a learner who has met the word by ear can still place it. */
.cmp-order { margin-top:4px; }
.cmp-order .order-tile { font-size:1.4rem; padding:8px 14px 7px; line-height:1.25; }
.cmp-order .order-tile.placed { padding:6px 4px; }
.cmp-order .order-tile rt, .cmp-chip rt, .cmp-model rt, .cmp-your rt {
  font-size:.5em; font-weight:500; color:var(--ink-2); letter-spacing:0;
}
.cmp-order .order-build { min-height:88px; }
.cmp-order .order-tray.hidden { display:none; }
.cmp-model rt { color:var(--ink-3); }
.cmp-your { line-height:1.9; }
.cmp-your ruby + ruby, .cmp-model ruby + ruby { margin-left:.2em; }
.cmp-order .order-build.is-done::after { display:none; }
.cmp-order .order-build.is-done .order-tile.placed { cursor:default; }
.cmp-order .order-build .order-tile.placed + .order-tile.placed ruby { margin-left:.1em; }
.cmp-chip rt { color:var(--ink-3); }
@media (max-width:640px) {
  .cmp-order .order-tile { font-size:1.2rem; padding:7px 11px 6px; }
  .cmp-order .order-tray { padding:12px 10px; gap:8px; }
}
