/* ============================================================
   Nihongo Quest — visual system
   Palette drawn from the heroine's artwork:
   indigo cloak, gold filigree, white lace, crystal-blue gem.
   ============================================================ */

:root {
  --ink:      #111830;  /* deep indigo night */
  --panel:    #1c2747;  /* navy panel */
  --panel-2:  #253158;  /* raised navy */
  --gold:     #c9a25a;  /* brass filigree */
  --gold-dim: #8a7040;
  --lace:     #f2ede1;  /* lace white */
  --mist:     #a9b4d0;  /* muted label blue */
  --crystal:  #6ea3e8;  /* sword-gem blue */
  --magma:    #f0854b;  /* World 3 ember orange */
  --storm:    #b18cff;  /* World 4 thunder violet */
  --bolt:     #f5d76e;  /* World 4 lightning yellow */
  --nebula:   #5ce1ff;  /* World 5 nebula cyan */
  --pulsar:   #ff7ad9;  /* World 5 pulsar magenta */
  --life:     #6fbf73;
  --hurt:     #d96a5f;

  --font-display: "Palatino Linotype", Palatino, Georgia, serif;
  --font-body: "Segoe UI", system-ui, sans-serif;
  /* Windows, Apple, then Android — the Noto names are what an Android phone
     actually ships, and without one of them the stack fell through to generic
     sans-serif, where the glyph shapes depend on the device's locale rather
     than on the language of the text. Nothing is downloaded: a name that is
     not installed is simply skipped. */
  --font-jp: "Yu Gothic UI", "Meiryo", "Hiragino Sans", "Noto Sans JP", "Noto Sans CJK JP", sans-serif;
  --font-mono: Consolas, "Courier New", monospace;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html, body { height: 100%; }
/* reserve the scrollbar lane even when the page fits — otherwise the centred
   #app shifts ~8px left on the worlds whose page happens to be taller */
html { scrollbar-gutter: stable; }

body {
  font-family: var(--font-body);
  color: var(--lace);
  background:
    radial-gradient(1px 1px at 12% 22%, rgba(242,237,225,.7) 50%, transparent 51%),
    radial-gradient(1px 1px at 78% 12%, rgba(242,237,225,.55) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 55% 38%, rgba(201,162,90,.6) 50%, transparent 51%),
    radial-gradient(1px 1px at 30% 72%, rgba(242,237,225,.4) 50%, transparent 51%),
    radial-gradient(1px 1px at 90% 58%, rgba(242,237,225,.5) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 66% 85%, rgba(110,163,232,.5) 50%, transparent 51%),
    radial-gradient(1px 1px at 8% 90%, rgba(242,237,225,.4) 50%, transparent 51%),
    radial-gradient(ellipse at 50% -20%, #2a3763 0%, var(--ink) 60%);
  /* body carries the fit zoom (ui.js applyFitZoom); dividing 100vh by it keeps
     the page filling the real viewport instead of zoom×viewport.
     dvh follows the mobile browser's shrinking address bar (100vh there is the
     TALLEST state, which leaves a dead strip at the bottom) — vh stays first as
     the fallback for browsers without dvh */
  min-height: calc(100vh / var(--fit-zoom, 1));
  min-height: calc(100dvh / var(--fit-zoom, 1));
}

#app {
  max-width: 900px;
  min-height: calc(100vh / var(--fit-zoom, 1));
  min-height: calc(100dvh / var(--fit-zoom, 1));
  margin: 0 auto;
  padding: 16px;
  position: relative;
  display: flex;
  flex-direction: column;
}

.hidden { display: none !important; }

/* Read aloud, never drawn. display:none or visibility:hidden would take it out
   of the accessibility tree too, so it has to stay rendered — a 1px clipped box
   with no width to reflow. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- screens ---------- */
.screen { display: none; flex: 1; }
/* show() focuses the screen it opens so Tab carries on from there; the focus
   ring belongs on the controls inside it, not on the whole page. */
.screen:focus { outline: none; }
.screen.active { display: flex; flex-direction: column; }

/* ---------- ornate gold frame (signature element) ---------- */
.ornate {
  position: relative;
  background: linear-gradient(160deg, var(--panel-2), var(--panel));
  border: 1px solid var(--gold-dim);
  box-shadow:
    inset 0 0 0 3px var(--panel),
    inset 0 0 0 4px var(--gold-dim),
    0 12px 32px rgba(0, 0, 0, .45);
  border-radius: 6px;
  padding: 24px;
}
.ornate::before, .ornate::after {
  content: "◆";
  position: absolute;
  color: var(--gold);
  font-size: 13px;
  line-height: 1;
  text-shadow: 0 0 6px rgba(201,162,90,.6);
}
.ornate::before { top: -7px; left: 50%; transform: translateX(-50%); }
.ornate::after  { bottom: -7px; left: 50%; transform: translateX(-50%); }

/* ---------- type ---------- */
.eyebrow {
  font-family: var(--font-body);
  font-size: 11px;
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--gold);
}

.game-title {
  font-family: var(--font-display);
  font-size: clamp(44px, 9vw, 72px);
  font-weight: 700;
  letter-spacing: .02em;
  line-height: 1.05;
  margin: 10px 0 6px;
  color: var(--lace);
}
.title-accent {
  display: block;
  color: var(--gold);
  text-shadow: 0 0 18px rgba(201,162,90,.35);
}
.hero-badge { border-radius: 8px; padding: 2px 6px; }

/* ---------- buttons ---------- */
.btn {
  font-family: var(--font-body);
  font-size: 16px;
  padding: 12px 26px;
  border-radius: 4px;
  border: 1px solid var(--gold-dim);
  background: var(--panel-2);
  color: var(--lace);
  cursor: pointer;
  transition: transform .12s ease, box-shadow .12s ease, background .12s ease;
}
.btn:hover { background: #2d3a66; box-shadow: 0 0 12px rgba(201,162,90,.25); }
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--crystal); outline-offset: 2px; }

.btn-primary {
  background: linear-gradient(160deg, #d8b26a, var(--gold));
  border-color: var(--gold);
  color: #2a1f0c;
  font-weight: 600;
}
.btn-primary:hover { background: linear-gradient(160deg, #e6c17a, #d0aa61); box-shadow: 0 0 16px rgba(201,162,90,.45); }

.btn-ghost { background: transparent; }

.btn-sound {
  font-size: 14px;
  padding: 8px 16px;
  margin-top: 10px;
  border-color: var(--crystal);
  color: var(--crystal);
  background: transparent;
}

/* ---------- title screen ---------- */
#screen-title.active {
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 32px;
  flex-wrap: wrap;
  padding: 24px 0;
}
.title-hero img {
  width: min(320px, 70vw);
  border-radius: 8px;
  border: 1px solid var(--gold-dim);
  box-shadow: 0 0 0 4px var(--panel), 0 0 0 5px var(--gold-dim), 0 18px 44px rgba(0,0,0,.55);
}
.title-panel { max-width: 400px; text-align: center; }
.title-tag { color: var(--mist); font-size: 15px; line-height: 1.55; margin-bottom: 22px; }
.title-buttons { display: flex; flex-direction: column; gap: 10px; align-items: center; }
.title-buttons .btn { min-width: 230px; }
/* audio can't start before the first gesture — this says so, then fades for good */
.audio-hint {
  margin-top: 18px;
  font-size: 12px;
  letter-spacing: .04em;
  color: var(--mist);
  opacity: .55;
  transition: opacity 1.2s ease;
}
.audio-hint.gone { opacity: 0; }
/* the missing-voice line is a real warning, not an aside — it keeps its
   contrast (and unlike the music hint, nothing ever fades it out) */
.voice-hint { margin-top: 6px; opacity: .9; color: var(--hurt); }
/* the voice libraries require this line; it is a licence notice, so it is quiet
   but never hidden and never faded (unlike .audio-hint) */
.voice-credit {
  margin-top: 10px;
  font-size: 11px;
  letter-spacing: .04em;
  color: var(--mist);
  opacity: .5;
}
.set-credit {
  margin: 10px 0 0;
  font-size: 11px;
  line-height: 1.5;
  color: var(--mist);
  opacity: .65;
}
.title-foot {
  margin-top: 6px;
  font-family: var(--font-display);
  font-style: italic;
  font-size: 13px;
  color: var(--gold-dim);
}

/* ---------- bgm toggle ---------- */
/* fixed, not absolute. The map and the journal are both taller than a phone
   screen, and an absolute toggle scrolls off the top with them — the player
   loses the mute button exactly when a long list makes them want it.
   The inset grows on windows wider than the 900px column (50% of the viewport,
   less half the column, less the 14px gap) so the pair stays over the panel
   instead of drifting out to the far corner of a desktop monitor. */
.bgm-toggle {
  position: fixed;
  top: 14px;
  right: max(14px, calc(50% - 436px));   /* 436 = 900/2 - 14 */
  z-index: 10;
  width: 44px; height: 44px;             /* 44 = the smallest reliable touch target */
  border-radius: 50%;
  border: 1px solid var(--gold-dim);
  background: var(--panel);
  color: var(--gold);
  font-size: 18px;
  cursor: pointer;
}
.bgm-toggle.off { color: var(--mist); opacity: .55; }
.bgm-toggle:focus-visible { outline: 2px solid var(--crystal); outline-offset: 2px; }

/* settings (⚙) button + popover — 50px further in: 44 wide plus a 6px gap */
.settings-toggle { right: max(64px, calc(50% - 386px)); }
.settings-pop {
  position: fixed;
  top: 66px;
  right: max(14px, calc(50% - 436px));   /* hangs under ♪, same lane */
  z-index: 11;
  width: 232px;
  padding: 16px 18px;
}
.set-label { display: block; font-size: 12px; color: var(--mist); margin: 10px 0 6px; }
.set-label:first-child { margin-top: 0; }
/* the rule above beats the browser's own [hidden] { display: none }, so a label
   hidden by script stayed on screen. The voice picker is hidden whenever the
   bundled clips are in use (ui.js syncVoiceSelect) — that is every device — and
   its heading was left standing over nothing. */
.set-label[hidden] { display: none; }
.set-range { width: 100%; accent-color: var(--gold); cursor: pointer; }
/* voice names are long ("Microsoft Nanami Online (Natural) - Japanese (Japan)")
   and the panel is narrow: the box may never grow past it */
.set-select {
  width: 100%;
  max-width: 100%;
  font-size: 12px;
  padding: 6px 8px;
  border-radius: 6px;
  border: 1px solid var(--gold-dim);
  background: var(--ink);
  color: var(--mist);
  cursor: pointer;
}
.set-select:disabled { opacity: .6; cursor: default; }
.rate-btns { display: flex; gap: 6px; }
.rate-btn {
  flex: 1;
  font-size: 12px;
  padding: 6px 4px;
  border-radius: 6px;
  border: 1px solid var(--gold-dim);
  background: var(--ink);
  color: var(--mist);
  cursor: pointer;
}
.rate-btn.on { color: var(--gold); border-color: var(--gold); box-shadow: 0 0 6px rgba(201,162,90,.35); }
/* a full-width toggle whose label states what it currently does, so the panel
   never shows a lone word the reader has to interpret */
.set-toggle { width: 100%; text-align: left; padding: 8px 10px; }

/* ---------- map ---------- */
/* two deliberate rows: row 1 = badge / ★ chip / ⌂ Title, row 2 = world
   title. A single wrapping row put the long W2/W3 titles past the row budget,
   dropping the button to the left of a surprise second row and shifting the
   whole map ~55px between worlds. */
.map-header { margin: 8px 0 16px; }
.map-header-top { display: flex; align-items: center; gap: 18px; }
.hero-badge { display: flex; align-items: center; gap: 10px; }
.hero-badge-img {
  width: 52px; height: 52px;
  object-fit: cover; object-position: 50% 12%;
  border-radius: 50%;
  border: 2px solid var(--gold);
}
.hero-level { font-family: var(--font-display); font-size: 18px; color: var(--gold); }
.xp-bar { width: 130px; height: 8px; background: var(--ink); border: 1px solid var(--gold-dim); border-radius: 4px; overflow: hidden; }
.xp-fill { height: 100%; width: 0; background: linear-gradient(90deg, var(--crystal), #9cc2f0); transition: width .5s ease; }
.xp-text { font-size: 11px; color: var(--mist); }
.map-title { font-family: var(--font-display); font-size: clamp(22px, 4vw, 30px); color: var(--lace); margin-top: 8px; }
.map-mastery { margin-top: 2px; font-size: 13px; color: var(--mist); }
.btn-map-title { font-size: 13px; padding: 8px 14px; color: var(--mist); margin-left: auto; margin-right: 106px; /* clear both fixed toggles: ⚙ (right:64px) + ♪ (right:14px) */ }
.btn-map-title:hover { color: var(--lace); }
.star-total {
  font-family: var(--font-display);
  font-size: 14px;
  color: var(--gold);
  border: 1px solid var(--gold-dim);
  border-radius: 14px;
  padding: 4px 12px;
  white-space: nowrap;
}
.star-total.max { color: #ffd977; border-color: var(--gold); box-shadow: 0 0 10px rgba(201,162,90,.5); }
/* Days come back, not a streak — so it is written quietly, in the label colour
   rather than the gold of a score. Nothing here can ever go down. */
.day-count { font-family: var(--font-body); font-size: 12.5px; color: var(--mist); white-space: nowrap; }

.map-path {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 14px;
  align-content: start;
}
.stage-node {
  position: relative;
  padding: 14px 12px;
  border-radius: 6px;
  border: 1px solid var(--gold-dim);
  background: linear-gradient(160deg, var(--panel-2), var(--panel));
  color: var(--lace);
  text-align: left;
  cursor: pointer;
  font-family: var(--font-body);
  transition: transform .12s ease, box-shadow .12s ease;
}
.stage-node:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 0 14px rgba(201,162,90,.3); }
.stage-node:focus-visible { outline: 2px solid var(--crystal); outline-offset: 2px; }
.stage-node:disabled { opacity: .38; cursor: default; filter: grayscale(.7); }
.stage-node .stage-no { font-size: 10px; letter-spacing: .2em; color: var(--gold); text-transform: uppercase; }
.stage-node .stage-name { display: block; font-size: 15px; margin: 4px 0 6px; font-weight: 600; }
.stage-node .stage-enemy { font-size: 26px; }
.stage-node .stage-enemy-img { width: 34px; height: 34px; border-radius: 50%; object-fit: cover; border: 1px solid var(--gold-dim); vertical-align: middle; }
.stage-node .stage-skill { display: block; font-size: 11px; color: var(--mist); margin-top: 4px; }
.stage-node .stage-stars { position: absolute; top: 8px; right: 10px; font-size: 13px; color: var(--gold); letter-spacing: .05em; }
.stage-node .stage-stars .star-off { color: var(--gold-dim); opacity: .5; }
.stage-node.available { border-color: var(--gold); box-shadow: 0 0 12px rgba(201,162,90,.35); }
.stage-node.boss { border-width: 2px; background: linear-gradient(160deg, #3a2b4d, #241d38); }
/* review-recommended: cleared but under 2★ */
.stage-node.review { border-color: #d9913e; animation: review-pulse 2.4s ease-in-out infinite; }
.stage-node.review .stage-no::after { content: " · review"; color: #e8b36a; }
@keyframes review-pulse {
  0%, 100% { box-shadow: 0 0 4px rgba(217,145,62,.25); }
  50% { box-shadow: 0 0 14px rgba(217,145,62,.6); }
}
/* the stage a win has just opened, once, on the render where the frontier moved
   (ui.js renderMap). Decoration only — the same event is announced in text. */
.stage-node.just-unlocked { animation: unlock-pop 1.5s ease-out 1; }
.stage-node.just-unlocked .stage-no::after { content: " · new"; color: #ffd977; }
@keyframes unlock-pop {
  0%   { transform: scale(.92); box-shadow: 0 0 0 rgba(201,162,90,0); }
  25%  { transform: scale(1.04); box-shadow: 0 0 26px rgba(255,217,119,.85); }
  60%  { transform: scale(1); box-shadow: 0 0 18px rgba(201,162,90,.6); }
  100% { transform: scale(1); box-shadow: 0 0 12px rgba(201,162,90,.35); }
}
.map-hint { margin-top: 18px; text-align: center; font-size: 13px; color: var(--mist); }

/* ---- world tabs (N5 kingdom / N4 northern realm) ---- */
.world-tabs { display: flex; gap: 10px; margin: 0 0 16px; }
.world-tab {
  flex: 1; max-width: 300px; padding: 10px 14px; cursor: pointer;
  font-family: var(--font-display); font-size: 15px; letter-spacing: .04em;
  color: var(--mist); text-align: left;
  background: var(--panel); border: 1px solid var(--gold-dim); border-radius: 10px;
  transition: transform .15s, box-shadow .15s;
}
.world-tab small { display: block; font-family: var(--font-body); font-size: 11px; opacity: .75; margin-top: 2px; }
.world-tab:hover:not(.locked) { transform: translateY(-1px); }
.world-tab.active { color: var(--lace); border-color: var(--gold); box-shadow: 0 0 12px rgba(201,162,90,.35); background: var(--panel-2); }
.world-tab.locked { opacity: .55; cursor: not-allowed; filter: grayscale(.5); }
.world-tab.shake { animation: tab-shake .3s; }
@keyframes tab-shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-4px); }
  75% { transform: translateX(4px); }
}

/* ---- World 2 (N4): icy blue-violet theme so the border is unmistakable ---- */
#screen-map.world-2 .map-title { color: #cfe4ff; text-shadow: 0 0 14px rgba(110,163,232,.45); }
#screen-map.world-2 .world-tab.active { border-color: var(--crystal); box-shadow: 0 0 12px rgba(110,163,232,.45); }
#screen-map.world-2 .stage-node { background: linear-gradient(160deg, #22315e, #17224a); border-color: #46608f; }
#screen-map.world-2 .stage-node.available { border-color: var(--crystal); box-shadow: 0 0 12px rgba(110,163,232,.45); }
#screen-map.world-2 .stage-node .stage-no { color: var(--crystal); }
#screen-map.world-2 .stage-node.boss { background: linear-gradient(160deg, #2b3a6b, #1b2450); }
#screen-map.world-2 .adv-ring { stroke: #46608f; }
#screen-map.world-2 .adv-node.available .adv-ring { stroke: var(--crystal); filter: drop-shadow(0 0 8px rgba(110,163,232,.65)); }
#screen-map.world-2 .adv-node.cleared .adv-ring { stroke: var(--crystal); }
#screen-map.world-2 .adv-seg.cleared { stroke: var(--crystal); }

/* ---- World 3 (N3): blazing magma theme — hot sand, ember glow ---- */
#screen-map.world-3 .map-title { color: #ffe0c4; text-shadow: 0 0 14px rgba(240,133,75,.5); }
#screen-map.world-3 .world-tab.active { border-color: var(--magma); box-shadow: 0 0 12px rgba(240,133,75,.5); }
#screen-map.world-3 .stage-node { background: linear-gradient(160deg, #5a2b22, #3c1c18); border-color: #96543a; }
#screen-map.world-3 .stage-node.available { border-color: var(--magma); box-shadow: 0 0 12px rgba(240,133,75,.5); }
#screen-map.world-3 .stage-node .stage-no { color: var(--magma); }
#screen-map.world-3 .stage-node.boss { background: linear-gradient(160deg, #6b3220, #4a1f14); }
#screen-map.world-3 .adv-ring { stroke: #96543a; }
#screen-map.world-3 .adv-node.available .adv-ring { stroke: var(--magma); filter: drop-shadow(0 0 8px rgba(240,133,75,.7)); }
#screen-map.world-3 .adv-node.cleared .adv-ring { stroke: var(--magma); }
#screen-map.world-3 .adv-seg.cleared { stroke: var(--magma); }

/* ---- World 4 (N2): dark stormveil theme — thunder violet, lightning glow ---- */
#screen-map.world-4 .map-title { color: #e6dcff; text-shadow: 0 0 16px rgba(177,140,255,.55); }
#screen-map.world-4 .world-tab.active { border-color: var(--storm); box-shadow: 0 0 12px rgba(177,140,255,.5); }
#screen-map.world-4 .stage-node { background: linear-gradient(160deg, #312a5e, #1e1a40); border-color: #5f4d9e; }
#screen-map.world-4 .stage-node.available { border-color: var(--storm); box-shadow: 0 0 14px rgba(245,215,110,.4); }
#screen-map.world-4 .stage-node .stage-no { color: var(--storm); }
#screen-map.world-4 .stage-node.boss { background: linear-gradient(160deg, #3a2c6b, #241a4d); }
#screen-map.world-4 .adv-ring { stroke: #5f4d9e; }
#screen-map.world-4 .adv-node.available .adv-ring { stroke: var(--bolt); filter: drop-shadow(0 0 8px rgba(245,215,110,.75)); }
#screen-map.world-4 .adv-node.cleared .adv-ring { stroke: var(--storm); }
#screen-map.world-4 .adv-seg.cleared { stroke: var(--storm); }

/* ---- World 5 (N1): starfall abyss theme — deep-space black, nebula cyan, pulsar magenta ---- */
#screen-map.world-5 .map-title { color: #d9f6ff; text-shadow: 0 0 16px rgba(92,225,255,.55); }
#screen-map.world-5 .world-tab.active { border-color: var(--nebula); box-shadow: 0 0 12px rgba(92,225,255,.5); }
#screen-map.world-5 .stage-node { background: linear-gradient(160deg, #1b2447, #0c1029); border-color: #3a6f8f; }
#screen-map.world-5 .stage-node.available { border-color: var(--nebula); box-shadow: 0 0 14px rgba(255,122,217,.4); }
#screen-map.world-5 .stage-node .stage-no { color: var(--nebula); }
#screen-map.world-5 .stage-node.boss { background: linear-gradient(160deg, #2c1b47, #140c29); }
#screen-map.world-5 .adv-ring { stroke: #3a6f8f; }
#screen-map.world-5 .adv-node.available .adv-ring { stroke: var(--pulsar); filter: drop-shadow(0 0 8px rgba(255,122,217,.75)); }
#screen-map.world-5 .adv-node.cleared .adv-ring { stroke: var(--nebula); }
#screen-map.world-5 .adv-seg.cleared { stroke: var(--nebula); }

/* ---------- adventure road (SVG world map) ---------- */
.adventure-map { margin-top: 22px; }
.adventure-map svg { width: 100%; height: auto; display: block; }
.adv-seg {
  fill: none;
  stroke: rgba(169,180,208,.28);
  stroke-width: 4;
  stroke-dasharray: 2 9;
  stroke-linecap: round;
}
.adv-seg.cleared {
  stroke: var(--gold);
  stroke-dasharray: none;
  filter: drop-shadow(0 0 4px rgba(201,162,90,.5));
}
.adv-ring { fill: var(--panel); stroke: var(--gold-dim); stroke-width: 2; }
.adv-node.available .adv-ring { stroke: var(--gold); filter: drop-shadow(0 0 8px rgba(201,162,90,.65)); }
.adv-node.cleared .adv-ring { stroke: var(--gold); fill: var(--panel-2); }
.adv-node.review .adv-ring { stroke: #d9913e; }
.adv-node.boss .adv-ring { stroke-width: 3; fill: #2c2344; }
.adv-node.locked { opacity: .38; }
.adv-node:not(.locked) { cursor: pointer; }
.adv-node:not(.locked):hover .adv-ring { stroke-width: 3.5; }
.adv-node:focus-visible { outline: 2px solid var(--crystal); outline-offset: 2px; }
.adv-emoji { font-size: 26px; text-anchor: middle; pointer-events: none; }
.adv-node.boss .adv-emoji { font-size: 32px; }
.adv-node image { pointer-events: none; }  /* enemy art node: clicks land on the <g>, same as .adv-emoji */
.adv-no { font-size: 12px; fill: var(--mist); text-anchor: middle; pointer-events: none; font-family: var(--font-body); }
.adv-stars { font-size: 12px; fill: var(--gold); text-anchor: middle; pointer-events: none; letter-spacing: .08em; }
.adv-hero { pointer-events: none; }
.adv-hero-ring { fill: var(--ink); stroke: var(--gold); stroke-width: 2.5; filter: drop-shadow(0 0 8px rgba(201,162,90,.6)); }

/* ---------- battle ---------- */
#screen-battle.active { justify-content: flex-start; gap: 14px; padding-top: 30px; }

.battle-stage {
  --art-w: min(170px, 34vw);   /* shared width of the hero card and the enemy card */
  display: flex;
  justify-content: space-between;
  align-items: stretch;
  gap: 12px;
  min-height: 240px;
  position: relative;
  padding: 0 8px;
}
/* Stretch both columns to the same height and pin the plate to the top, the art
   to the bottom. The hero portrait and the enemy (emoji / art / boss art) are
   different heights, so aligning only at the bottom would drop the shorter
   side's HP plate by the difference. */
.fighter { display: flex; flex-direction: column; align-items: center; justify-content: space-between; gap: 10px; }
.fighter-hero img {
  width: var(--art-w);
  border-radius: 6px;
  border: 1px solid var(--gold-dim);
  box-shadow: 0 10px 24px rgba(0,0,0,.5);
  transition: transform .18s ease;
}
.fighter-hero.attack img { transform: translateX(26px) rotate(-2deg); }
.fighter-hero.hurt img { animation: shake .4s ease; }

/* The enemy gets the same card as the hero. Its aspect ratio matches the hero
   art (1122×1402), so the two columns end up the same height with no gap between
   plate and picture. Shadows go through box-shadow only — a drop-shadow on a box
   that has a background paints a second, rectangular shadow. */
.enemy-sprite {
  width: var(--art-w);
  aspect-ratio: 1122 / 1402;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: clamp(64px, 14vw, 110px);   /* for stages with no art, which show an emoji */
  line-height: 1;
  border-radius: 6px;
  border: 1px solid var(--gold-dim);
  background: rgba(8, 11, 24, .55);
  backdrop-filter: blur(1.5px);
  box-shadow: 0 10px 24px rgba(0,0,0,.5);
  transition: transform .18s ease, opacity .4s ease;
}
.fighter-enemy.hurt .enemy-sprite { animation: shake .4s ease; }
.fighter-enemy.attack .enemy-sprite { transform: translateX(-26px); }
.fighter-enemy.dead .enemy-sprite { opacity: 0; transform: scale(.3) rotate(20deg); }
/* a boss keeps the card size and only grows the art inside it — menace without
   breaking the two columns' matched height */
.fighter-enemy.boss .enemy-sprite {
  font-size: clamp(90px, 19vw, 150px);
  box-shadow: 0 10px 20px rgba(0,0,0,.65), 0 0 24px rgba(217,106,95,.3);
}
/* optional enemy art (pics/enemy-<stageId>) — kept inside the card */
.enemy-sprite .enemy-img {
  display: block;
  width: auto;
  height: auto;
  max-width: 86%;
  max-height: 86%;
  object-fit: contain;
}
.fighter-enemy.boss .enemy-sprite .enemy-img {
  max-width: 100%;
  max-height: 100%;
}

.enemy-taunt {
  position: absolute;
  /* anchored to the bottom, over the enemy art: measured from the top it sat on
     the HP plates — the first thing a player checks when a boss appears — and a
     long enemy name makes that plate two lines tall, so no fixed top clears it */
  bottom: 6%;
  right: 6%;
  /* wide enough that the Japanese line never has to break: the longest one is
     13 glyphs at 14px, and a phone leaves ~208px of text here */
  max-width: min(62%, 300px);
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(14px, 2.6vw, 18px);
  color: #ffd0ca;
  background: rgba(8, 11, 24, .76);
  border: 1px solid rgba(201, 162, 90, .24);
  border-radius: 6px;
  padding: 8px 12px;
  box-shadow: 0 10px 24px rgba(0, 0, 0, .46);
  text-shadow:
    0 2px 4px rgba(0,0,0,.95),
    0 0 10px rgba(0,0,0,.85);
  text-align: right;
  pointer-events: none;
  z-index: 6;
  animation: taunt-fade 3.8s ease forwards;
}
.enemy-taunt .taunt-en, .enemy-taunt .taunt-ja { display: block; }
.enemy-taunt .taunt-ja {
  margin-top: 4px;
  font-family: var(--font-jp);
  font-style: normal;
  font-size: .92em;
  color: #fff1e8;
  /* Japanese breaks between any two characters by default, which lands mid-word
     on a narrow banner. keep-all leaves the only break opportunities where the
     level's own orthography put them — the spaces of N5/N4 分かち書き — and the
     unspaced N3+ lines are short enough to stay on one line (logic.mjs checks). */
  word-break: keep-all;
  overflow-wrap: normal;
}
@keyframes taunt-fade {
  0% { opacity: 0; transform: translateY(6px); }
  12%, 78% { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(-8px); }
}

@keyframes shake {
  0%,100% { transform: translateX(0); }
  25% { transform: translateX(-8px); }
  50% { transform: translateX(6px); }
  75% { transform: translateX(-4px); }
}

.hp-plate {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  min-width: 174px;
  padding: 8px 12px 7px;
  background: rgba(8, 11, 24, .72);
  border: 1px solid rgba(201, 162, 90, .22);
  border-radius: 8px;
  box-shadow:
    0 10px 24px rgba(0, 0, 0, .42),
    inset 0 0 16px rgba(0, 0, 0, .28);
  backdrop-filter: blur(1.5px);
}
.fighter-name {
  font-family: var(--font-display);
  font-size: 14px;
  color: var(--gold);
  letter-spacing: .06em;
  text-shadow:
    0 1px 2px rgba(0,0,0,1),
    0 0 8px rgba(0,0,0,.95);
}
.hp-bar { width: 150px; height: 12px; background: var(--ink); border: 1px solid var(--gold-dim); border-radius: 6px; overflow: hidden; }
.hp-fill { height: 100%; width: 100%; background: linear-gradient(90deg, #58a85e, var(--life)); transition: width .45s ease, background .45s ease; }
.hp-fill.low { background: linear-gradient(90deg, #b3554b, var(--hurt)); }
.hp-fill.enemy { background: linear-gradient(90deg, #a35286, #c76aa5); }
.hp-text {
  font-size: 11px;
  color: #dbe4ff;
  font-family: var(--font-mono);
  text-shadow:
    0 1px 2px rgba(0,0,0,1),
    0 0 8px rgba(0,0,0,.95);
}

/* Banner across the bottom of the stage. It used to sit at top:6px, which on a
   phone is the HP plate band — the one strip on this screen carrying a name and
   numbers you have to be able to read, and the plates are semi-opaque, so they
   painted over it. Down here it crosses the darkest part of the backdrop
   instead, well clear of the ♪ / ⚙ lane at the top.
   The anchored left/right pair (rather than left:50% + translateX) is what
   keeps it honest: with left:50% the available width is only half the stage,
   so a long string wrapped into the plates. */
.combo-badge {
  position: absolute;
  bottom: 8px;
  left: 10px; right: 10px;
  width: max-content;
  max-width: 100%;
  margin: 0 auto;
  white-space: nowrap;      /* "MAX COMBO ×12" is one line or it is nothing */
  text-align: center;
  transform: rotate(-3deg);
  z-index: 9;               /* over .particle (8); .hp-fill is z-index 1 */
  font-family: var(--font-display);
  font-size: clamp(16px, 4.6vw, 20px);
  color: var(--gold);
  text-shadow:
    0 2px 3px rgba(0,0,0,1),
    0 0 10px rgba(0,0,0,.95),
    0 0 14px rgba(201,162,90,.55);
}
/* on the class ui.js toggles, not on the element: the counter climbs while the
   badge stays on screen, and an animation on .combo-badge itself only ever
   played once, when display first changed */
.combo-badge.pop { animation: pop .3s ease; }
@keyframes pop { from { transform: rotate(-3deg) scale(.5); opacity: 0; } to { transform: rotate(-3deg) scale(1); opacity: 1; } }

.dmg-float {
  position: absolute;
  font-family: var(--font-display);
  font-size: 26px;
  font-weight: 700;
  pointer-events: none;
  animation: floatup .9s ease forwards;
  z-index: 5;
  text-shadow:
    0 2px 3px rgba(0,0,0,1),
    0 0 10px rgba(0,0,0,.95);
}
.dmg-float.hurt-hero { color: var(--hurt); }
.dmg-float.hurt-enemy { color: var(--gold); }
.dmg-float.heal-hero { color: var(--life); }
@keyframes floatup { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-46px); } }

/* battle window */
.battle-window { min-height: 260px; }
#question-area, #feedback-area { display: flex; flex-direction: column; align-items: center; text-align: center; }
/* the real JLPT rubric, in Japanese — it runs to 2-3 lines, so it needs the
   Japanese stack and a looser leading than a one-line English caption did */
.q-inst {
  font-family: var(--font-jp);
  font-size: 14px;
  line-height: 1.7;
  color: var(--mist);
  margin-top: 10px;
}
/* N5/N4 rubrics are 分かち書き, and CJK text breaks between any two kana by
   default — which split 「ただしい」 across two lines. keep-all leaves the spaces
   as the only break opportunities, so the line wraps at 文節 like the paper does.
   ui.js adds this class only when the string actually has spaces: on the
   unspaced N3-N1 wording it would make the whole rubric one unbreakable run. */
.q-inst.wakachi { word-break: keep-all; }
/* spaced listening choices: break only at the 分かち書き spaces */
.choice.wakachi { word-break: keep-all; }
.question-text {
  font-family: var(--font-jp);
  font-size: clamp(20px, 4.5vw, 28px);
  margin: 12px 0 18px;
  line-height: 1.5;
}
.q-target {
  text-decoration: underline;
  text-decoration-color: var(--gold);
  text-decoration-thickness: 2px;
  text-underline-offset: 5px;
}
/* inside a choice the line sits closer: the button's text is smaller and the
   rule would otherwise collide with the row below it */
.choice .q-target { text-decoration-thickness: 1.5px; text-underline-offset: 3px; }
.passage-box {
  font-family: var(--font-jp);
  font-size: 16px;
  line-height: 1.9;
  text-align: left;
  white-space: pre-line;   /* keep \n breaks (N2 A/B comparison: 相談者 / A / B blocks) */
  background: var(--ink);
  border: 1px solid var(--gold-dim);
  border-left: 3px solid var(--gold);
  border-radius: 4px;
  padding: 14px 18px;
  margin: 12px 0 4px;
  max-width: 620px;
  /* tall enough that an N2/N1 A/B comparison doesn't force scroll round-trips */
  max-height: min(340px, 40vh);
  overflow-y: auto;
}
/* the devices the paper carries under a passage: word notes in small type,
   and the source line set to the right */
.passage-note {
  font-size: 13px;
  line-height: 1.7;
  color: var(--mist);
  margin-top: 8px;
}
.passage-src {
  font-size: 12px;
  color: var(--mist);
  text-align: right;
  margin-top: 8px;
  opacity: .85;
}
/* figure area: SVG clock / notice board / hatsuwa scene */
.figure-box { display: flex; justify-content: center; margin: 12px 0 4px; width: 100%; }
.scene-emoji { font-size: 54px; line-height: 1.15; text-align: center; }
.scene-img { max-height: 110px; max-width: min(240px, 70vw); object-fit: contain; }
.clock-face { width: min(150px, 38vw); }
.clock-rim  { fill: var(--ink); stroke: var(--gold); stroke-width: 2.5; }
.clock-tick { stroke: var(--gold-dim); stroke-width: 2; }
.clock-tick.big { stroke: var(--gold); stroke-width: 3; }
.clock-hand { stroke: var(--lace); stroke-width: 4.5; stroke-linecap: round; }
.clock-hand.minute { stroke: var(--crystal); stroke-width: 3; }
.clock-pin  { fill: var(--gold); }

.notice-board {
  background: var(--ink);
  border: 1px solid var(--gold-dim);
  border-top: 3px solid var(--gold);
  border-radius: 4px;
  padding: 12px 18px 14px;
  max-width: 420px;
  width: 100%;
  text-align: left;
}
.notice-title {
  font-family: var(--font-jp);
  font-size: 16px;
  font-weight: 700;
  color: var(--gold);
  letter-spacing: .08em;
  text-align: center;
  margin-bottom: 8px;
}
.notice-rows { width: 100%; border-collapse: collapse; font-family: var(--font-jp); font-size: 14px; }
.notice-rows td { padding: 5px 6px; border-bottom: 1px dotted rgba(201,162,90,.3); }
.notice-rows td:last-child { text-align: right; color: var(--lace); }
.notice-rows td:first-child { color: var(--mist); }
/* a real 情報検索 table is read ACROSS: every column carries data, so the
   label/value colouring and the right-aligned last column do not apply */
.notice-grid { table-layout: fixed; font-size: 13px; }
.notice-grid th {
  padding: 5px 6px;
  color: var(--gold);
  font-weight: 700;
  text-align: left;
  border-bottom: 1px solid rgba(201,162,90,.5);
}
.notice-grid td, .notice-grid td:first-child, .notice-grid td:last-child {
  text-align: left;
  color: var(--lace);
  word-break: break-word;
}
/* 情報検索 decision ladder. The paper draws the branches sideways; at 375px
   there is no room for that, so every part of a step stacks and nothing is
   given a fixed width — the board's own max-width is the only limit. */
.notice-flow { display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px; }
.flow-step {
  border: 1px solid var(--gold-dim);
  border-left: 3px solid var(--gold);
  border-radius: 3px;
  padding: 7px 9px;
}
.flow-q {
  font-family: var(--font-jp);
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--lace);
  margin-bottom: 5px;
}
.flow-branch {
  font-family: var(--font-jp);
  font-size: 13px;
  line-height: 1.6;
  display: flex;
  gap: 7px;
  align-items: baseline;
  margin-top: 3px;
  word-break: break-word;
}
/* はい / いいえ read as labels on the branch, not as part of the outcome */
.flow-tag {
  flex: 0 0 auto;
  color: var(--gold);
  font-weight: 700;
}
.flow-out  { color: var(--lace); }
.flow-next { color: var(--mist); }
/* the paperwork column of the paper, which is where the trap usually sits */
.flow-small {
  font-family: var(--font-jp);
  font-size: 11.5px;
  line-height: 1.55;
  color: var(--mist);
  margin: 2px 0 0 1.6em;
}

/* ※ footnote — the line that cancels half the table */
.notice-note {
  font-family: var(--font-jp);
  font-size: 12px;
  color: var(--mist);
  margin-top: 6px;
  line-height: 1.6;
}

.choices {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  width: 100%;
  max-width: 560px;
}
.choice {
  position: relative;  /* anchors the ::before hotkey badge */
  font-family: var(--font-jp);
  font-size: 18px;
  padding: 14px 10px;
  border-radius: 4px;
  border: 1px solid var(--gold-dim);
  background: var(--ink);
  color: var(--lace);
  cursor: pointer;
  transition: background .12s ease, border-color .12s ease;
}
.choice[data-key]::before {  /* 1–4 choice number (and the hotkey on a keyboard) — pseudo-element so textContent stays = the choice string */
  content: attr(data-key);
  position: absolute;
  top: 3px;
  left: 6px;
  font-family: var(--font-body);
  font-size: 11px;
  line-height: 1;
  color: var(--mist);
}
.choice:hover:not(:disabled) { background: #232f56; border-color: var(--gold); }
.choice:focus-visible { outline: 2px solid var(--crystal); outline-offset: 2px; }
.choice:disabled { cursor: default; }
.choice.right { background: #24422a; border-color: var(--life); }
.choice.wrong { background: #45231f; border-color: var(--hurt); }
.choice[data-romaji]::after {  /* St1–2 romaji hint — pseudo-element so textContent stays = the choice string */
  content: attr(data-romaji);
  display: block;
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--mist);
  margin-top: 2px;
}
.choices.stack { grid-template-columns: 1fr; }
.choices.stack .choice { font-size: 16px; text-align: left; padding: 12px 16px; }

/* feedback */
.feedback-verdict { font-family: var(--font-display); font-size: 26px; margin-bottom: 12px; }
.feedback-verdict.good { color: var(--life); }
.feedback-verdict.bad { color: var(--hurt); }
.word-card {
  border: 1px solid var(--gold-dim);
  border-radius: 6px;
  background: var(--ink);
  padding: 14px 34px;
  margin-bottom: 10px;
}
.word-jp { font-family: var(--font-jp); font-size: 30px; }
/* a conversation keeps one turn per line, the way the tapescript is written */
.word-jp.long { font-size: 19px; line-height: 1.7; white-space: pre-line; }
.word-romaji { font-family: var(--font-mono); font-size: 14px; color: var(--crystal); margin: 2px 0; }
.word-en { font-size: 17px; color: var(--lace); }
.word-card { max-width: 620px; }
.word-card .btn-sound { margin-top: 6px; }
/* the reading of a sentence written with kanji — quieter than the sentence,
   and it keeps one line per speaker like the sentence above it */
.word-kana {
  font-family: var(--font-jp);
  font-size: 14px;
  color: var(--mist);
  margin: 4px 0 2px;
  line-height: 1.7;
  white-space: pre-line;
}
/* the translation of the text a question belongs to, under the translation of
   the answer itself */
.passage-en { font-size: 14px; color: var(--mist); margin-top: 6px; line-height: 1.55; }

/* what was said, on the feedback card: shorter than a reading passage, because
   the answer and Next are below it and neither may be pushed off the screen */
.fb-transcript { margin: 0 0 10px; font-size: 15px; max-height: min(240px, 30vh); }
.fb-transcript-jp { margin: 0; }
.fb-transcript-kana { font-size: 13px; color: var(--mist); margin: 8px 0 0; line-height: 1.7; }
.fb-transcript-kana:empty { display: none; }

/* sokuji/hatsuwa feedback — all three replies, answer/your-pick marked */
.feedback-replies { max-width: 620px; margin-bottom: 10px; text-align: left; }
.reply-line { font-family: var(--font-jp); font-size: 15px; color: var(--mist); margin: 3px 0; line-height: 1.5; }
.reply-line.right { color: var(--life); }
.reply-line.wrong { color: var(--hurt); }

.feedback-explain { font-size: 14px; color: var(--crystal); margin: 2px 0 6px; max-width: 500px; line-height: 1.6; }
.feedback-note { font-size: 13px; color: var(--mist); margin-bottom: 14px; max-width: 430px; line-height: 1.5; }

.btn-flee {  /* fixed bottom-right (same recipe as .journal-back-float) — away from the choices, so a stray click can't flee mid-question */
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 30;
  font-size: 13px;
  padding: 8px 18px;
  background: var(--panel-2);
  color: var(--mist);
  border-color: var(--gold-dim);
  box-shadow: 0 4px 14px rgba(0,0,0,.45);
}
.btn-flee:hover { color: var(--lace); border-color: var(--gold); }

/* ---------- result ---------- */
#screen-result.active { align-items: center; justify-content: center; }
.result-panel { max-width: 520px; width: 100%; text-align: center; }
.result-title { font-family: var(--font-display); font-size: clamp(34px, 7vw, 48px); margin: 8px 0 6px; }
.result-title.win { color: var(--gold); text-shadow: 0 0 18px rgba(201,162,90,.4); }
.result-title.lose {
  color: var(--hurt);
  letter-spacing: .14em;
  text-shadow: 0 0 22px rgba(217,106,95,.55), 0 2px 0 #000;
  animation: gameover-slam .5s cubic-bezier(.2,1.4,.4,1);
}
@keyframes gameover-slam {
  from { transform: scale(2.2); opacity: 0; }
  to   { transform: scale(1); opacity: 1; }
}
.result-sub { color: var(--mist); font-size: 15px; margin-bottom: 14px; }
/* Shown from the second loss on the same stage. Amber like the REVIEW stages —
   it points at the way through, it is not a warning and not a penalty. */
.result-coach {
  margin: -4px 0 14px;
  padding: 9px 14px;
  font-size: 13.5px;
  line-height: 1.7;
  text-align: left;
  color: #e8b36a;
  border: 1px solid rgba(217,145,62,.35);
  border-radius: 8px;
  background: rgba(217,145,62,.07);
}
.result-stars { font-size: 34px; margin: 2px 0 8px; letter-spacing: .12em; }
.result-stars .rstar { display: inline-block; }
.result-stars .rstar.on { color: var(--gold); text-shadow: 0 0 14px rgba(201,162,90,.6); animation: pop2 .45s ease backwards; }
.result-stars .rstar.off { color: var(--gold-dim); opacity: .35; }
.newbest-chip {
  display: inline-block;
  vertical-align: middle;
  font-size: 12px;
  font-family: var(--font-body);
  letter-spacing: .1em;
  color: #2a1f0c;
  background: linear-gradient(160deg, #d8b26a, var(--gold));
  border-radius: 10px;
  padding: 3px 10px;
  margin-left: 12px;
  animation: pop2 .45s ease backwards;
  animation-delay: .7s;
}
/* score by exam section — a row of small chips under the stars */
.result-cats {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 6px;
  margin: 10px 0 2px;
}
.cat-chip {
  font-size: 12px;
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid var(--gold-dim);
  color: var(--mist);
  white-space: nowrap;
}
.cat-chip.clean { color: var(--gold); border-color: var(--gold); }
.levelup {
  font-family: var(--font-display);
  color: var(--crystal);
  font-size: 18px;
  margin: 8px 0 4px;
  animation: pop2 .5s ease;
}
/* save-failure toast */
.save-warn {
  position: fixed;
  left: 50%; bottom: 18px;
  transform: translateX(-50%);
  max-width: min(92vw, 480px);
  background: #45231f;
  border: 1px solid var(--hurt);
  border-radius: 6px;
  color: var(--lace);
  font-size: 13px;
  padding: 10px 14px;
  z-index: 50;
  cursor: pointer;
  transition: opacity .7s ease;
}
.save-warn.fade { opacity: 0; }
@keyframes pop2 { from { transform: scale(.6); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.review-block { margin: 16px 0 6px; text-align: left; }
.review-heading { font-family: var(--font-display); font-size: 16px; color: var(--gold); margin-bottom: 8px; text-align: center; }
.review-list { list-style: none; max-height: 180px; overflow-y: auto; }
.review-list li {
  font-family: var(--font-jp);
  font-size: 15px;
  padding: 7px 10px;
  border-bottom: 1px solid rgba(201,162,90,.18);
}
.review-list .r-romaji { font-family: var(--font-mono); color: var(--crystal); font-size: 12px; margin: 0 6px; }
.review-list .r-en { color: var(--mist); }
/* grammar note under a review row — carries kanji readings */
.review-list .r-note { font-size: 12.5px; color: var(--mist); margin-top: 3px; line-height: 1.55; }
/* replay a missed item's audio from the review list */
.btn-review-speak {
  margin-left: 8px;
  padding: 1px 7px;
  font-size: 12px;
  background: transparent;
  border: 1px solid var(--gold-dim);
  border-radius: 5px;
  color: var(--crystal);
  cursor: pointer;
}
.btn-review-speak:hover { border-color: var(--crystal); }
.btn-review-speak:focus-visible { outline: 2px solid var(--crystal); outline-offset: 2px; }
.result-buttons { display: flex; gap: 10px; justify-content: center; margin-top: 18px; flex-wrap: wrap; }

/* ---------- save export / import ---------- */
.save-buttons { display: flex; gap: 10px; justify-content: center; margin-top: 14px; flex-wrap: wrap; }
.btn-save { font-size: 13px; padding: 8px 16px; color: var(--mist); }
.btn-save:hover { color: var(--lace); }

/* ---------- story cutscene ---------- */
.cutscene-overlay {
  position: fixed;
  inset: 0;
  background: rgba(8, 11, 24, .92);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  justify-content: safe center;   /* the letters are long — overflow must not clip the top away */
  overflow-y: auto;
  gap: 18px;
  z-index: 40;
  cursor: pointer;
  padding: 20px;
}
.cutscene-skip {
  position: absolute;
  top: 14px; right: 14px;
  font-size: 13px;
  padding: 8px 16px;
  color: var(--mist);
}
.cutscene-skip:hover { color: var(--lace); }
.cutscene-emoji {
  font-size: clamp(64px, 14vw, 110px);
  line-height: 1;
  filter: drop-shadow(0 0 24px rgba(201,162,90,.45));
  animation: pop2 .5s ease;
}
/* key art (pics/cutscene-<id>) — max-height keeps the panel on screen on short viewports */
.cutscene-art {
  width: 100%;
  max-width: 640px;
  aspect-ratio: 16 / 9;
  max-height: min(40vh, 340px);
  object-fit: cover;
  border-radius: 10px;
  border: 1px solid var(--gold-dim);
  box-shadow: 0 0 28px rgba(201,162,90,.28);
  animation: pop2 .5s ease;
}
.cutscene-panel {
  max-width: 640px;
  width: 100%;
  display: flex;
  align-items: center;
  gap: 18px;
  text-align: left;
}
.cutscene-portrait {
  width: 84px;
  height: 84px;
  object-fit: cover;
  object-position: top;
  border-radius: 8px;
  border: 1px solid var(--gold-dim);
  box-shadow: 0 0 0 3px var(--panel), 0 0 0 4px var(--gold-dim);
  flex-shrink: 0;
}
.cutscene-textbox { flex: 1; min-width: 0; }
/* the letter pages carry no portrait, a hairline in the realm accent instead
   (border-color: inherit picks up whatever .cutscene-panel's world theme set) */
.cutscene-panel.letter .cutscene-textbox {
  border-left: 2px solid;
  border-left-color: inherit;
  padding-left: 16px;
}
/* …unless the sender's own portrait is on the page. It already divides the
   panel, and a hairline beside it reads as a double rule. */
.cutscene-panel.letter.writer .cutscene-textbox { border-left: 0; padding-left: 0; }
/* achievement line — kept out of the dialogue itself */
.cutscene-badge {
  display: inline-block;
  font-family: var(--font-body);
  font-size: 10px;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: var(--gold);
  border: 1px solid var(--gold-dim);
  border-radius: 999px;
  padding: 3px 10px 3px 12px;
  margin-bottom: 8px;
}
.cutscene-speaker { margin-bottom: 6px; }
.cutscene-text {
  font-size: 16px;
  line-height: 1.65;
  color: var(--lace);
  min-height: 4.5em;
  white-space: pre-line;
}
.cutscene-more {
  display: inline-block;
  color: var(--gold);
  font-size: 14px;
  animation: cs-blink 1s steps(2) infinite;
}
@keyframes cs-blink { 50% { opacity: 0; } }

/* world flavours: panel frame + scene glow pick up each realm's accent */
.cutscene-overlay.world-2 .cutscene-panel { border-color: var(--crystal); }
.cutscene-overlay.world-2 .cutscene-emoji { filter: drop-shadow(0 0 24px rgba(110,163,232,.6)); }
.cutscene-overlay.world-2 .cutscene-speaker { color: var(--crystal); }
.cutscene-overlay.world-2 .cutscene-art { border-color: var(--crystal); box-shadow: 0 0 28px rgba(110,163,232,.35); }
.cutscene-overlay.world-2 .cutscene-badge { color: var(--crystal); border-color: var(--crystal); }
.cutscene-overlay.world-3 .cutscene-panel { border-color: var(--magma); }
.cutscene-overlay.world-3 .cutscene-emoji { filter: drop-shadow(0 0 24px rgba(240,133,75,.65)); }
.cutscene-overlay.world-3 .cutscene-speaker { color: var(--magma); }
.cutscene-overlay.world-3 .cutscene-art { border-color: var(--magma); box-shadow: 0 0 28px rgba(240,133,75,.35); }
.cutscene-overlay.world-3 .cutscene-badge { color: var(--magma); border-color: var(--magma); }
.cutscene-overlay.world-4 .cutscene-panel { border-color: var(--storm); }
.cutscene-overlay.world-4 .cutscene-emoji { filter: drop-shadow(0 0 24px rgba(177,140,255,.65)); }
.cutscene-overlay.world-4 .cutscene-speaker { color: var(--bolt); }
.cutscene-overlay.world-4 .cutscene-art { border-color: var(--storm); box-shadow: 0 0 28px rgba(177,140,255,.35); }
.cutscene-overlay.world-4 .cutscene-badge { color: var(--bolt); border-color: var(--storm); }
.cutscene-overlay.world-5 .cutscene-panel { border-color: var(--nebula); }
.cutscene-overlay.world-5 .cutscene-emoji { filter: drop-shadow(0 0 24px rgba(92,225,255,.65)); }
.cutscene-overlay.world-5 .cutscene-speaker { color: var(--pulsar); }
.cutscene-overlay.world-5 .cutscene-art { border-color: var(--nebula); box-shadow: 0 0 28px rgba(92,225,255,.35); }
.cutscene-overlay.world-5 .cutscene-badge { color: var(--pulsar); border-color: var(--nebula); }

/* ---------- opening / ending cinematics ----------
   A full-bleed layer inside the cutscene overlay. Every motion lives on a
   class that JS adds (an animation written on the element itself only ever
   plays once, on first paint), so the blanket
   prefers-reduced-motion rule at the end of this file switches the whole
   sequence off and the JS drops its waits to zero. */
.cutscene-overlay.cine {
  background: #000;
  padding: 0;
  gap: 0;
  overflow: hidden;
}
.cutscene-overlay.cine .cutscene-art,
.cutscene-overlay.cine .cutscene-panel { display: none; }
/* Skip lands inside the upper letterbox bar, so it has to sit above the bar as
   well as the layer — measured, not assumed: a synthetic .click() in a test
   ignores hit-testing, and at z-index 2 a real finger hit the bar instead. */
.cutscene-skip { z-index: 5; }

.cine-layer {
  position: absolute;
  inset: 0;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* key art, slowly pushing in */
.cine-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transform: scale(1.02);
}
.cine-bg.in { animation: cine-fade-in 2.2s ease forwards, cine-kenburns 30s linear forwards; }
/* held back for the roll. Dimming has to be a filter, not opacity: the fade-in
   above is `forwards`, and a filled animation beats a plain declaration on the
   property it animates — an opacity rule here would simply never show up. */
.cine-bg.dim { filter: brightness(.3) saturate(.75); transition: filter 1.6s ease; }
@keyframes cine-fade-in { to { opacity: 1; } }
@keyframes cine-kenburns { to { transform: scale(1.16) translate(-1.5%, -1.2%); } }

/* letterbox — the bars grow in, and the frame stops being a web page */
.cine-bar {
  position: absolute;
  left: 0; right: 0;
  height: 0;
  background: #000;
  transition: height 1.2s ease;
  z-index: 3;               /* above the roll — a letterbox that text runs over is just a stripe */
}
.cine-bar-top { top: 0; }
.cine-bar-bot { bottom: 0; }
.cine-layer.box .cine-bar { height: clamp(30px, 8vh, 84px); }

/* the three title cards share the centre of the frame, so only the one being
   shown is in the layout at all */
.cine-logo, .cine-line, .cine-end, .cine-card {
  position: relative;
  z-index: 1;
  display: none;
  opacity: 0;
  text-align: center;
  padding: 0 20px;
}
.cine-logo.on, .cine-line.on, .cine-end.on, .cine-card.on { display: block; }
.cine-logo img { width: min(340px, 70vw); height: auto; filter: drop-shadow(0 6px 26px rgba(0,0,0,.7)); }
.cine-logo span {
  font-family: var(--font-display);
  font-size: clamp(30px, 8vw, 60px);
  color: var(--gold);
  text-shadow: 0 0 26px rgba(201,162,90,.5);
}
.cine-logo.in { animation: cine-rise 1.8s ease forwards; }
@keyframes cine-rise { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: none; } }

/* a title card sits on top of a bright painting, so each one carries its own
   pool of shadow — without it the starfield swallows the text */
.cine-logo::before, .cine-line::before, .cine-end::before, .cine-card::before {
  content: "";
  position: absolute;
  inset: -180% -20%;
  z-index: -1;
  background: radial-gradient(ellipse at center, rgba(4,6,16,.82) 0%, rgba(4,6,16,.6) 38%, rgba(4,6,16,0) 72%);
  pointer-events: none;
}
/* the last line of the letter, alone: the payoff the whole game builds to */
.cine-line {
  font-family: var(--font-jp);
  font-size: clamp(26px, 6.2vw, 46px);
  line-height: 1.5;
  letter-spacing: .06em;
  color: var(--lace);
  text-shadow: 0 2px 16px rgba(0,0,0,.9), 0 0 30px rgba(92,225,255,.6), 0 0 70px rgba(92,225,255,.3);
}
.cine-line.in { animation: cine-soft-in 2.6s ease forwards; }
.cine-end {
  font-family: var(--font-display);
  font-size: clamp(26px, 7vw, 52px);
  letter-spacing: .42em;
  text-indent: .42em;               /* the tracking would push it off-centre */
  color: var(--gold);
  text-shadow: 0 2px 16px rgba(0,0,0,.9), 0 0 34px rgba(201,162,90,.5);
}
.cine-end.in { animation: cine-soft-in 2s ease forwards; }
.cine-logo.out, .cine-line.out, .cine-end.out, .cine-card.out { animation: cine-soft-out 1s ease forwards; }

/* the world cards: "N5 — Complete / The Kingdom of Kotoba" when a world falls,
   "World 2 · N4 / The Northern Realm of Bunpou / Grammar arrives." when the
   next one opens. Each kingdom's accent colours its own name. */
.cine-card { max-width: 92vw; }
.cine-card p { margin: 0; }
.cine-card-eyebrow {
  font-family: var(--font-display);
  font-size: clamp(12px, 2.6vw, 16px);
  letter-spacing: .32em;
  text-indent: .32em;
  text-transform: uppercase;
  color: var(--lace);
  opacity: .85;
  text-shadow: 0 2px 10px rgba(0,0,0,.9);
}
.cine-card-title {
  margin-top: 10px;
  font-family: var(--font-display);
  font-size: clamp(24px, 5.6vw, 44px);
  line-height: 1.15;
  color: var(--gold);
  text-shadow: 0 2px 16px rgba(0,0,0,.9), 0 0 34px rgba(201,162,90,.5);
}
.cine-card-sub {
  margin-top: 14px;
  font-style: italic;
  font-size: clamp(14px, 3vw, 19px);
  color: var(--lace);
  text-shadow: 0 2px 10px rgba(0,0,0,.9);
}
.cine-card.w2 .cine-card-title { color: var(--crystal); text-shadow: 0 2px 16px rgba(0,0,0,.9), 0 0 34px rgba(140,220,255,.5); }
.cine-card.w3 .cine-card-title { color: var(--magma);   text-shadow: 0 2px 16px rgba(0,0,0,.9), 0 0 34px rgba(255,140,80,.5); }
.cine-card.w4 .cine-card-title { color: var(--bolt);    text-shadow: 0 2px 16px rgba(0,0,0,.9), 0 0 34px rgba(200,190,255,.5); }
.cine-card.w5 .cine-card-title { color: var(--pulsar);  text-shadow: 0 2px 16px rgba(0,0,0,.9), 0 0 34px rgba(160,200,255,.5); }
.cine-card.in { animation: cine-rise 1.8s ease forwards; }
@keyframes cine-soft-in { from { opacity: 0; letter-spacing: .5em; } to { opacity: 1; } }
@keyframes cine-soft-out { from { opacity: 1; } to { opacity: 0; } }

/* the journey, rolling by */
.cine-roll {
  position: absolute;
  inset: 0;
  overflow: hidden;
  z-index: 1;
  display: none;
}
.cine-roll.on { display: block; }
.cine-roll-inner {
  position: absolute;
  left: 0; right: 0;
  top: 100%;
  padding: 0 24px;
  text-align: center;
  color: var(--lace);
}
/* how far to travel is measured in JS from the real heights, so the closing
   block comes to rest in the middle of the frame; `forwards` holds it there */
.cine-roll-inner.go { animation: cine-roll var(--roll-s, 26s) linear forwards; }
@keyframes cine-roll { to { transform: translateY(var(--roll-end, -120vh)); } }
.cine-row { margin: 12px 0; font-size: 17px; }
.cine-row .cine-k {
  display: block;
  font-family: var(--font-body);
  font-size: 10px;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: var(--mist);
  margin-bottom: 2px;
}
/* pre-line so a credit with several names can put one on each line */
.cine-row .cine-v {
  font-family: var(--font-display);
  font-size: 22px;
  line-height: 1.45;
  color: var(--lace);
  white-space: pre-line;
}
/* the prose lines in the credits — quieter than a credit, and narrow enough
   to read while it is moving */
.cine-note {
  max-width: 30em;
  margin: 14px auto 0;
  font-size: 14px;
  line-height: 1.7;
  color: var(--mist);
}
/* the closing block travels as one, so the roll parks it whole */
.cine-tail { padding-top: 34px; }
.cine-tail .cine-head { margin-top: 0; }
.cine-head {
  font-family: var(--font-display);
  font-size: clamp(22px, 5vw, 30px);
  color: var(--gold);
  letter-spacing: .12em;
  margin: 34px 0 14px;
}

/* ---------- battle juice ---------- */
.hit-flash {
  position: absolute;
  inset: 0;
  background: #fff;
  pointer-events: none;
  z-index: 7;
  border-radius: 14px;
  animation: hit-flash-fade .3s ease forwards;
}
.hit-flash.red { background: var(--hurt); }
@keyframes hit-flash-fade { from { opacity: .26; } to { opacity: 0; } }

.battle-stage.stage-shake { animation: stage-shake .32s ease; }
.battle-stage.stage-shake-hard { animation: stage-shake-hard .42s ease; }
@keyframes stage-shake {
  25% { transform: translate(-4px, 2px); }
  50% { transform: translate(3px, -2px); }
  75% { transform: translate(-2px, 1px); }
}
@keyframes stage-shake-hard {
  20% { transform: translate(-8px, 4px); }
  40% { transform: translate(7px, -3px); }
  60% { transform: translate(-5px, 3px); }
  80% { transform: translate(3px, -2px); }
}

.particle {
  position: absolute;
  pointer-events: none;
  z-index: 8;
  line-height: 1;
  animation: particle-fly .9s ease-out forwards;
}
@keyframes particle-fly {
  from { opacity: 1; transform: translate(0, 0) rotate(0deg); }
  to { opacity: 0; transform: translate(var(--dx), var(--dy)) rotate(var(--rot)); }
}

@keyframes screen-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
/* animate the screen's CONTENT, not the screen itself. An element with
   opacity < 1 or a transform becomes the containing block for its
   position:fixed descendants — so while this ran on .screen, #btn-flee and
   .journal-back-float were laid out against the content column for the
   animation's 350ms and visibly snapped to the viewport corner afterwards.
   Excluding the two floats keeps the entry fade and pins them from frame 1. */
.screen.active > *:not(.btn-flee):not(.journal-back-float) { animation: screen-in .35s ease; }

/* world-flavored battle backdrops (a pics/bg-worldN can layer on top via JS) */
.battle-stage.bstage {
  --battle-bg-image: linear-gradient(180deg, rgba(74,104,168,.30), rgba(20,28,24,.35) 52%, rgba(46,80,44,.45));
  border-radius: 14px;
  border: 1px solid rgba(201, 162, 90, .16);
  padding: 12px 14px;
  box-shadow:
    inset 0 0 0 1px rgba(0, 0, 0, .28),
    inset 0 -60px 80px rgba(0, 0, 0, .38);
  background-image:
    linear-gradient(180deg, rgba(0,0,0,.64) 0%, rgba(0,0,0,.32) 24%, rgba(0,0,0,.18) 46%, rgba(0,0,0,.44) 76%, rgba(0,0,0,.72) 100%),
    radial-gradient(ellipse at 0% 100%, rgba(0,0,0,.58) 0%, rgba(0,0,0,.30) 33%, transparent 62%),
    radial-gradient(ellipse at 100% 100%, rgba(0,0,0,.62) 0%, rgba(0,0,0,.32) 33%, transparent 62%),
    linear-gradient(90deg, rgba(0,0,0,.40), transparent 24%, transparent 76%, rgba(0,0,0,.40)),
    var(--battle-bg-image);
  background-size: cover, cover, cover, cover, cover;
  background-position: center;
}
.battle-stage.bstage-world-1 { --battle-bg-image: linear-gradient(180deg, rgba(74,104,168,.30), rgba(20,28,24,.35) 52%, rgba(46,80,44,.45)); }
.battle-stage.bstage-world-2 { --battle-bg-image: linear-gradient(180deg, rgba(120,170,220,.22), rgba(16,26,44,.40) 52%, rgba(70,110,160,.35)); }
.battle-stage.bstage-world-3 { --battle-bg-image: linear-gradient(180deg, rgba(60,18,14,.45), rgba(30,10,8,.40) 50%, rgba(214,80,38,.32)); }
.battle-stage.bstage-world-4 { --battle-bg-image: linear-gradient(180deg, rgba(52,32,84,.50), rgba(18,12,34,.45) 55%, rgba(94,66,160,.35)); }
.battle-stage.bstage-world-5 { --battle-bg-image: linear-gradient(180deg, rgba(10,14,34,.60), rgba(30,20,56,.45) 45%, rgba(64,190,210,.16)); }

/* hp ghost trail — lingering pale band shows the chunk just lost */
.hp-bar { position: relative; }
.hp-fill { position: relative; z-index: 1; }
.hp-ghost {
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  width: 100%;
  background: rgba(255, 235, 205, .55);
  transition: width .5s ease .3s;
  z-index: 0;
}

/* optional extra art (pics/logo, pics/Heroin-victory) */
.title-logo { display: block; width: min(340px, 76%); margin: 6px auto 2px; }
.result-hero {
  display: block;
  width: 96px;
  margin: 2px auto 0;
  border-radius: 8px;
  border: 1px solid var(--gold-dim);
  box-shadow: 0 6px 16px rgba(0, 0, 0, .4);
}
/* On defeat it is her ordinary portrait, dimmed a little: still standing,
   not celebrating. The panel used to be empty under "GAME OVER". */
.result-hero.lose { filter: saturate(.72) brightness(.82); border-color: rgba(138, 112, 64, .55); }

/* heroine bobs gently on the adventure map (image only — the g keeps its
   SVG transform attribute, which a CSS transform would override) */
.adv-hero image { animation: hero-bob 2.2s ease-in-out infinite; }
@keyframes hero-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }

/* ---------- touch input ---------- */
/* no 300ms tap delay, no long-press callout, no blue tap flash, and no text
   selection when a tap lands slightly off a button */
.btn, .choice, .stage-node, .world-tab, .bgm-toggle, .rate-btn,
.btn-review-speak, .btn-journal-del, .adv-node, .cutscene-overlay {
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
}

/* keep the fixed corners clear of the notch and the home bar (viewport-fit=cover) */
#app {
  padding-left: max(16px, env(safe-area-inset-left));
  padding-right: max(16px, env(safe-area-inset-right));
  padding-bottom: max(16px, env(safe-area-inset-bottom));
}
.btn-flee, .journal-back-float {
  bottom: calc(18px + env(safe-area-inset-bottom));
  right: calc(18px + env(safe-area-inset-right));
}
.save-warn { bottom: calc(18px + env(safe-area-inset-bottom)); }
/* the ♪ / ⚙ pair repeats its whole right: expression here — the inset is a
   max() of "clear of the notch" and "stay over the 900px column", and adding
   the notch to only one half of that would let a wide screen ignore it */
.bgm-toggle {
  top: calc(14px + env(safe-area-inset-top));
  right: max(calc(14px + env(safe-area-inset-right)), calc(50% - 436px));
}
.settings-toggle { right: max(calc(64px + env(safe-area-inset-right)), calc(50% - 386px)); }
.settings-pop {
  top: calc(66px + env(safe-area-inset-top));
  right: max(calc(14px + env(safe-area-inset-right)), calc(50% - 436px));
}
.cutscene-skip { top: calc(14px + env(safe-area-inset-top)); }

@media (pointer: coarse) {
  /* the number badge stays on touch (it was hidden through v85 as a
     "keyboard-only hint"). Without a keyboard it is not a shortcut, but it is
     still the choice's name — the listening formats number the replies out
     loud, so the printed formats have to be countable the same way. One step
     larger than the desktop hint so it reads at arm's length — but no further
     in: a stacked choice's text starts at padding-left 16px, and the digit has
     to stay clear of it. */
  .choice[data-key]::before { font-size: 12px; }
  /* :hover sticks after a tap on touch — keep the lift out of it */
  .stage-node:hover:not(:disabled) { transform: none; }
  .world-tab:hover:not(.locked) { transform: none; }
  /* thumb-sized targets */
  .btn, .world-tab { min-height: 44px; }
  .choice { min-height: 52px; }
  .stage-node { min-height: 92px; }
  .rate-btn { min-height: 44px; }
  .set-select { min-height: 44px; }
  /* 🔊 (replay a missed item) is the smallest control in the game — 34x30 is
     under half the area a thumb needs. Give the button a real 44x44 box and
     draw the chip the player sees with ::before inside it, so the rows keep
     their proportions while the target that actually gets hit is full size. */
  .btn-review-speak {
    position: relative;
    min-width: 44px; min-height: 44px;
    padding: 0;
    border: 0;
    display: inline-flex; align-items: center; justify-content: center;
    vertical-align: middle;
  }
  .btn-review-speak::before {
    content: "";
    position: absolute;
    inset: 7px 5px;                       /* the 34x30 chip, centred in the 44x44 target */
    border: 1px solid var(--gold-dim);
    border-radius: 5px;
    pointer-events: none;
  }
  .btn-review-speak:hover::before { border-color: var(--crystal); }
}

/* ---------- responsive ---------- */

/* ---------- sideways phone ---------- */
/* A phone on its side is 812x375: it matches neither the <=480px sheet nor the
   desktop fit-zoom (which needs width >= 900), and .battle-stage and
   .battle-window carry 240px + 260px of min-height between them — that does
   not go into 375px at any font size. Ask for the phone back instead of
   shipping a layout that cannot fit. The battle is written to the save on
   every answer, so turning the phone loses nothing.
   (pointer: coarse) is what keeps a short desktop window out of this, and the
   height cap keeps a tablet in landscape (768px) playing normally. */
.rotate-hint { display: none; }

@media (orientation: landscape) and (max-height: 500px) and (pointer: coarse) {
  .rotate-hint {
    position: fixed;
    inset: 0;
    z-index: 60;            /* above .save-warn (50), the highest thing until now */
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 12px;
    padding: 20px;
    text-align: center;
    background: var(--ink);  /* opaque: the layout behind it must not show through */
  }
  /* upright, not turned: the picture is the position being asked for */
  .rotate-hint-icon { font-size: 44px; line-height: 1; }
  .rotate-hint-text { font-family: var(--font-display); font-size: 22px; color: var(--gold); }
  .rotate-hint-sub { font-size: 14px; color: var(--mist); max-width: 34ch; }
}

/* Reserve the corner float's lane. "Back to map" is position:fixed at the
   bottom right, and once the page scrolls the last choice ends up underneath
   it — a thumb aiming at an answer would leave the battle instead. Any touch
   device needs this (a phone in landscape is 812px wide but only 375px tall,
   so a width rule alone misses it), and so does a mouse window narrow enough
   for the button to reach the centred choices. */
@media (max-width: 900px), (pointer: coarse) {
  #screen-battle.active { padding-bottom: calc(66px + env(safe-area-inset-bottom)); }
  /* Ride the bottom of the screen while the card is taller than it. A reading
     passage keeps the question on screen after the answer (deliberately), so
     "Next" starts up to 500px below the fold and nothing scrolls to it. Sticky
     leaves it in the normal flow whenever it already fits.
     72px clears the lane above: "Back to map" is fixed at bottom:18px with a
     44px box, so its top edge is 62px up. */
  #btn-next {
    position: sticky;
    bottom: calc(72px + env(safe-area-inset-bottom));
    z-index: 20;
    box-shadow: 0 6px 20px rgba(0, 0, 0, .55);
  }
  /* room for the button to float over. Without it the last line of the note
     stays under the button at full scroll, with nowhere left to scroll to. */
  #feedback-area { padding-bottom: 62px; }
}

@media (max-width: 640px) {
  .cutscene-panel { flex-direction: column; text-align: center; }
  .cutscene-panel.letter .cutscene-textbox { border-left: 0; padding-left: 0; }   /* centred: the hairline would sit wrong */
  .cutscene-art { max-height: 30vh; }
  .battle-stage { min-height: 190px; }
  .hp-bar { width: 110px; }
  .choices { grid-template-columns: 1fr; }
  /* the top row may wrap on narrow screens — its content is identical in every
     world, so wrapping here can't cause the per-world shift of C13 */
  .map-header-top { flex-wrap: wrap; }
}

/* phones. The layout above assumes a mouse and ~640px; below 480px the
   battle plates, the five world tabs and the SVG road all stop fitting. */
@media (max-width: 480px) {
  #app { padding: 10px max(10px, env(safe-area-inset-left)) max(10px, env(safe-area-inset-bottom)) max(10px, env(safe-area-inset-right)); }

  /* title */
  #screen-title.active { gap: 14px; padding: 8px 0; }
  .title-hero img { width: min(240px, 58vw); }
  .title-panel { max-width: 100%; }
  .title-tag { font-size: 14px; margin-bottom: 16px; }
  .title-buttons { width: 100%; }
  .title-buttons .btn, .save-buttons .btn { width: 100%; min-width: 0; }
  .save-buttons { flex-direction: column; }

  /* map header: ♪ and ⚙ live in the top-right corner, so give the row its own
     line — 14px inset + a 44px button = 58px, clear of the 10px page padding */
  .map-header { margin-top: 50px; }
  .map-header-top { gap: 10px; }
  .hero-badge-img { width: 42px; height: 42px; }
  .xp-bar { width: 96px; }
  /* thumb-sized even where the browser reports a fine pointer (a phone in
     desktop-site mode, a narrow desktop window) */
  .btn-map-title, .btn-journal { margin-right: 0; font-size: 12px; padding: 8px 10px; min-height: 44px; }
  .btn-flee, .journal-back-float { min-height: 44px; padding: 10px 16px; }
  .star-total { font-size: 13px; padding: 3px 9px; }

  /* five tabs in one row leave ~60px each — wrap them into two columns */
  .world-tabs { flex-wrap: wrap; gap: 8px; }
  .world-tab { flex: 1 1 calc(50% - 4px); max-width: none; font-size: 13px; padding: 8px 10px; }
  .world-tab small { font-size: 10px; }

  .map-path { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .stage-node { padding: 12px 10px; }
  .stage-node .stage-name { font-size: 14px; }
  .stage-node .stage-skill { font-size: 11px; }
  /* the SVG road is 1000×330 by design — its nodes shrink to ~9px here.
     The card grid above lists the same ten stages, so drop the road. */
  .adventure-map { display: none; }

  /* battle: the 174px plate is what pushes the stage past the viewport.
     The top padding is the ♪ / ⚙ lane (14px inset + a 44px button): the enemy's
     name plate is the top-right element on this screen and the two opaque
     circles were sitting across the end of its name. */
  #screen-battle.active { padding-top: 48px; gap: 10px; }
  .battle-stage { --art-w: min(140px, 38vw); min-height: 0; padding: 0 4px; gap: 8px; }
  /* a fight always carries .bstage, and that rule is one class more specific —
     without this line the padding above never applies during a battle */
  .battle-stage.bstage { padding: 10px 8px; }
  .fighter { gap: 6px; width: var(--art-w); }
  .hp-plate { min-width: 0; width: 100%; padding: 6px 8px 5px; }
  .hp-bar { width: 100%; max-width: 130px; }
  .fighter-name { font-size: 13px; }
  .hp-text { font-size: 11px; }
  .ornate { padding: 16px 14px; }
  .battle-window { min-height: 0; }
  .question-text { font-size: clamp(18px, 5.5vw, 22px); margin: 10px 0 14px; }
  .q-inst { font-size: 13px; }
  .passage-box { font-size: 15px; padding: 12px 14px; max-height: min(300px, 38vh); }
  .choice { font-size: 17px; padding: 12px 10px; }
  .choices.stack .choice { font-size: 15px; }
  .word-card { padding: 12px 16px; }
  .word-jp { font-size: 26px; }
  .word-romaji, .feedback-note, .reply-line { font-size: 13px; }
  .feedback-explain { font-size: 13.5px; }
  .choices, .result-buttons { margin-bottom: 8px; }

  /* cutscene: the letter is the point — give it the width */
  .cutscene-overlay { padding: 14px 10px; gap: 12px; }
  .ornate.cutscene-panel { padding: 16px 14px; }
  .cutscene-text { font-size: 15px; line-height: 1.6; }
  .cutscene-art { max-height: 26vh; }
  .cutscene-emoji { font-size: clamp(52px, 16vw, 80px); }
  .cutscene-portrait { width: 64px; height: 64px; }
  .cutscene-overlay.cine { padding: 0; gap: 0; }
  .cine-row { font-size: 15px; margin: 10px 0; }
  .cine-row .cine-v { font-size: 19px; }

  .settings-pop { width: min(232px, calc(100vw - 28px)); }
  .journal-panel .review-list li { padding-right: 40px; }
  .r-chip { font-size: 10px; }
}

/* ---------- reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
  /* the cinematics are built out of those animations, so without this the
     opening and the ending would be an invisible sequence of nothing. Show
     every beat of them at rest instead, and let the roll be scrolled by hand. */
  .cine-bg, .cine-logo, .cine-line, .cine-end, .cine-card { opacity: 1; transform: none; }
  .cine-bg.dim { filter: brightness(.3) saturate(.75); }
  .cine-layer.box .cine-bar { height: clamp(30px, 8vh, 84px); }
  .cine-roll { overflow-y: auto; }
  .cine-roll-inner, .cine-roll-inner.go, .cine-roll-inner.parked {
    position: static;
    transform: none;
    padding: 24px;
  }
}

/* ---------- Word Journal ---------- */
.btn-journal { font-size: 13px; padding: 8px 14px; color: var(--mist); margin-left: auto; }
.btn-journal:hover { color: var(--lace); }
/* the journal button takes the flex gap now — stop ⌂ Title from opening a second one */
.map-header-top .btn-journal ~ .btn-map-title { margin-left: 0; }
/* the bottom padding is the floating "Back to map" button's lane — the list
   scrolls, so without it the last row sits underneath the button */
#screen-journal.active { align-items: center; padding: 26px 14px calc(66px + env(safe-area-inset-bottom)); overflow-y: auto; }
.journal-panel { max-width: 640px; width: 100%; }

/* Journal review drill: the battle screen without the fight. The stage (HP
   plates, sprites, combo badge) is hidden; the question window is the room. */
#screen-battle.practice .battle-stage { display: none; }
#screen-battle.practice #combo-badge { display: none; }
/* The drill has no enemy to hit, so a right answer had no impact anywhere on
   screen. The ✓ she just earned is stamped on the card instead. It ends at
   opacity 1 (`both`) and is removed by JS, so under prefers-reduced-motion —
   where the animation is switched off entirely — the mark is still there; only
   the stamping movement is lost, and the movement is the decorative half. */
#feedback-area { position: relative; }
.journal-stamp {
  position: absolute;
  top: 2px;
  right: 6px;
  font-size: 46px;
  line-height: 1;
  color: var(--life);
  text-shadow: 0 0 16px rgba(111, 191, 115, .55);
  pointer-events: none;
  animation: stamp-in .5s cubic-bezier(.2, 1.4, .4, 1) both;
}
@keyframes stamp-in {
  from { transform: scale(2.3) rotate(-16deg); opacity: 0; }
  60%  { transform: scale(.92) rotate(-8deg); opacity: 1; }
  to   { transform: scale(1) rotate(-8deg); opacity: 1; }
}

/* two-document 情報検索: stacked, each its own board */
.notice-pair { display: flex; flex-direction: column; gap: 10px; width: 100%; }

/* ---- gallery (bestiary + story replay) — same shell as the journal ---- */
#screen-gallery.active { align-items: center; padding: 26px 14px calc(66px + env(safe-area-inset-bottom)); overflow-y: auto; }
.map-header-top .btn-journal ~ .btn-journal { margin-left: 0; } /* only the first one takes the flex gap */
.bestiary-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; margin: 6px 0 14px; }
.bestiary-card {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 8px 4px; text-align: center;
  background: rgba(0, 0, 0, .18); border: 1px solid var(--gold-dim); border-radius: 10px;
}
.bestiary-card.locked { opacity: .55; }
.bestiary-card.boss { border-color: var(--gold); }
.bestiary-face { font-size: 30px; line-height: 40px; }
.bestiary-face img { width: 40px; height: 40px; object-fit: contain; display: block; }
.bestiary-name { font-size: 11px; color: var(--lace); }
.bestiary-card.locked .bestiary-name { color: var(--mist); font-style: italic; }
.bestiary-stage { font-size: 10px; color: var(--mist); }
.bestiary-taunt { font-size: 10px; color: var(--gold); font-style: italic; }
.bestiary-taunt .taunt-en, .bestiary-taunt .taunt-ja { display: block; }
.bestiary-taunt .taunt-ja { font-family: var(--font-jp); font-style: normal; color: var(--gold); }
.gallery-story-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 8px 2px; border-bottom: 1px dashed var(--gold-dim);
}
.gallery-story-row.locked .gallery-story-name { color: var(--mist); font-style: italic; }
.gallery-play { min-height: 44px; padding: 8px 14px; }
/* placed AFTER the base rules on purpose: the ≤480px block further up would
   lose the same-specificity tie against them */
@media (max-width: 480px) {
  .bestiary-grid { grid-template-columns: repeat(3, 1fr); }
}
.journal-panel .eyebrow, .journal-title, #journal-sub, #journal-empty { text-align: center; }
.journal-title { font-size: clamp(26px, 5vw, 36px); color: var(--gold); }
.journal-heading { margin: 18px 0 4px; text-align: left; }
/* the result list is capped at 180px to fit its panel — the journal is a full screen */
.journal-panel .review-list { max-height: none; overflow-y: visible; }
.journal-panel .review-list li { text-align: left; }
.r-chip {
  float: right;
  font-family: var(--font-body);
  font-size: 11px;
  color: var(--crystal);
  border: 1px solid rgba(201,162,90,.3);
  border-radius: 10px;
  padding: 1px 8px;
  margin-left: 8px;
  white-space: nowrap;
}
/* A page answered right since the last miss (v88). The page stays in the book,
   so keep the mark quiet — a green edge and a green chip, not a greyed-out or
   struck-through row: she opened the journal to read these, ✓ or not. The
   left border eats 2px of the row's 10px padding so nothing shifts. */
.journal-panel .review-list li.done { border-left: 2px solid var(--life); padding-left: 8px; }
.journal-panel .review-list li.done .r-chip { color: var(--life); border-color: rgba(111,191,115,.45); }
#journal-empty { margin: 18px 0; }
/* manual delete — ✕ pinned to the row's top-right (li padding keeps the floated chip clear of it) */
.journal-panel .review-list li { position: relative; padding-right: 36px; }
.btn-journal-del {
  position: absolute;
  top: 6px;
  right: 4px;
  padding: 1px 7px;
  font-size: 12px;
  background: transparent;
  border: 1px solid var(--gold-dim);
  border-radius: 5px;
  color: var(--mist);
  cursor: pointer;
}
.btn-journal-del:hover { border-color: var(--crystal); color: var(--lace); }
.btn-journal-del:focus-visible { outline: 2px solid var(--crystal); outline-offset: 2px; }
/* On a finger the ✕ gets a real 44x44 box — same trick as .btn-review-speak:
   the chip the player sees is drawn by ::before inside the target, so a dense
   list of rows doesn't turn into a column of big boxes. This lives here rather
   than in the main coarse block because the row padding it needs is set by the
   journal rules below it, and the last rule of equal weight wins. */
@media (pointer: coarse) {
  .btn-journal-del {
    top: 0; right: 0;
    width: 44px; height: 44px;
    padding: 0;
    border: 0;
    display: flex; align-items: center; justify-content: center;
  }
  .btn-journal-del::before {
    content: "";
    position: absolute;
    inset: 7px 5px;
    border: 1px solid var(--gold-dim);
    border-radius: 5px;
    pointer-events: none;
  }
  .btn-journal-del:hover::before { border-color: var(--crystal); }
  .journal-panel .review-list li { padding-right: 48px; }
}

/* undo bar — the ✕ deletes at once (the row has to leave the list to show it
   worked), so the way back is a one-tap undo rather than a confirm dialog on
   every removal. It replaces itself on each delete and clears on leaving. */
.journal-undo {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  flex-wrap: wrap;
  margin: 12px 0 0;
  padding: 10px 14px;
  border: 1px solid var(--gold-dim);
  border-radius: 8px;
  background: var(--panel-2);
  font-size: 13px;
  color: var(--mist);
  text-align: center;
}
.journal-undo .btn { font-size: 13px; padding: 7px 16px; }

/* long journals scroll — keep an always-visible way back (Esc works too) */
.journal-back-float {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 30;
  font-size: 14px;
  padding: 9px 18px;
  background: var(--panel-2);   /* solid over scrolling rows, unlike the transparent ghost default */
  box-shadow: 0 4px 14px rgba(0,0,0,.45);
}
