/*
 * Phone-first, and still phone-first: everything below 700px is what it always
 * was. Above that the layout widens on one principle —
 *
 *     a wider screen shows MORE THINGS, not BIGGER THINGS.
 *
 * A talk button, a flashcard and a drill question do not improve at 900px; they
 * get worse. Lists of books, words, decks and rules improve enormously. So the
 * old `body { max-width: 560px }` is not removed, it is split in three and
 * attached per screen type. See --w-focus / --w-read / --w-list below.
 */
:root {
  color-scheme: dark;
  --bg: #0b0d10;
  --panel: #15181d;
  --panel-2: #1c2027;
  --line: #262b33;
  --text: #e8eaed;
  --muted: #8b929c;
  --accent: #3b82f6;
  --me: #1e293b;
  --teacher: #16251b;
  --live: #22c55e;
  --danger: #dc2626;
  /* Not knowing a word is not a failure, so it is not red. Amber says "come
     back to this", which is what it actually means. */
  --warn: #eab308;
  --pad-bottom: max(14px, env(safe-area-inset-bottom));

  /* Three widths, one per kind of screen.
   *
   * --w-focus never grows, at any viewport, on purpose. Those screens show one
   * thing at a time and a wider one is a worse one.
   * --w-read is a line length, so it stops where prose stops being readable.
   * --w-list is the only one that really opens up, because scanning is the only
   * thing that gets better with room. */
  --w-focus: 560px;
  --w-read: 560px;
  --w-list: 560px;
}

/* Breakpoints come from the content, not from device names. 700px is where a
   second list column stops being cramped; 1040px is where a 240px rail plus
   940px of content fits without either being squeezed. */
@media (min-width: 700px) { :root { --w-read: 660px; --w-list: 720px; } }
@media (min-width: 1040px) { :root { --w-list: 940px; } }

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html, body { height: 100%; }

/* The `hidden` attribute is hidden by a UA rule of the lowest possible
 * specificity, so any author rule setting `display` on the same element beats
 * it silently. This bit twice: `#screen-speak { display: flex }` left the talk
 * button showing under the home screen, and `.card { display: flex }` showed a
 * "Last session" card to learners who had never had one.
 *
 * Guarding the attribute itself is the only version of this that stays fixed —
 * anything else has to be re-fought every time a hideable element gains a
 * layout. Elements that must show while carrying `hidden` do not exist here. */
[hidden] { display: none !important; }

body {
  margin: 0;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  /* Installed, this is an app: rubber-band scrolling of the whole page and
     accidental text selection while holding the talk button both read as bugs. */
  overscroll-behavior: none;
  -webkit-user-select: none;
  user-select: none;
}

/* ── Header ───────────────────────────────────────────────────────────── */

header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: max(10px, env(safe-area-inset-top)) 16px 10px;
  border-bottom: 1px solid var(--line);
  background: var(--bg);
}

h1 { flex: 1; font-size: 16px; font-weight: 600; margin: 0; }

.dot {
  width: 9px; height: 9px; border-radius: 50%;
  background: #4b5563;
  flex: none;
  transition: background .2s;
}
.dot.connecting { background: #f59e0b; animation: pulse 1s ease-in-out infinite; }
.dot.open { background: var(--live); }
.dot.error { background: var(--danger); }

@keyframes pulse { 50% { opacity: .3; } }

.icon-btn {
  display: grid; place-items: center;
  width: 36px; height: 36px; padding: 0;
  background: none; border: 0; border-radius: 10px;
  color: var(--muted); cursor: pointer;
}
.icon-btn:active { background: var(--panel-2); }

/* ── Transcript ───────────────────────────────────────────────────────── */

/* Scoped to the transcript by id, not written as a bare `main`.
 *
 * There are three <main> elements now — transcript, home and review — and a
 * bare element selector styled all of them, giving the home screen a nested
 * scroll container it never asked for. Written back when this app had one
 * screen; it stops being true the moment a second one exists. */
#log {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  /* Text in the transcript is worth being able to copy, unlike the chrome. */
  -webkit-user-select: text;
  user-select: text;
}

/* Centred in the transcript's empty column; left alone anywhere else, where
   `margin: auto 0` would push it away from the content above it. */
#log .empty { margin: auto 0; }
.empty { text-align: center; color: var(--text); }
.empty p { margin: 0 0 10px; }
.empty .muted, .muted { color: var(--muted); font-size: 13px; }

.turn {
  padding: 10px 13px;
  border-radius: 14px;
  max-width: 88%;
  white-space: pre-wrap;
  word-break: break-word;
  animation: rise .18s ease-out;
}
@keyframes rise { from { opacity: 0; transform: translateY(4px); } }

.turn.me { background: var(--me); align-self: flex-end; border-bottom-right-radius: 4px; }
.turn.teacher { background: var(--teacher); align-self: flex-start; border-bottom-left-radius: 4px; }
.turn.partial { opacity: .55; font-style: italic; }
.turn.note {
  align-self: center; max-width: 100%; background: none; color: var(--muted);
  font-size: 12.5px; text-align: center; padding: 2px 0;
}
.turn.note.bad { color: #f87171; }

.ms {
  display: block;
  font-size: 11px; font-weight: 600; letter-spacing: .04em;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  margin-bottom: 2px;
}
.ms.slow { color: #f59e0b; }
.ms.bad { color: #f87171; }

/* ── Role-play goals ──────────────────────────────────────────────────── */

/* Above the transcript, not inside it: the three belong to the whole session
   and have to be glanceable mid-sentence, which is not true of anything that
   scrolls away. */
.goal-bar {
  display: flex; flex-wrap: wrap; gap: 6px 14px;
  padding: 9px 16px;
  border-bottom: 1px solid var(--line);
  background: var(--panel);
}

.goal-bar { justify-content: center; gap: 6px 12px; }

/* Three dim stars and nothing else. Writing the goals out turned the scene
   into a checklist and told the learner what to say before they had tried to
   work it out; a star says only that there is something there. */
.goal {
  display: flex; align-items: center; gap: 7px;
  padding: 2px 4px;
  font: inherit; font-size: 12.5px; color: var(--muted);
  background: none; border: 0; cursor: pointer;
}
.goal-star { font-size: 15px; color: #333b47; transition: color .25s; }

/* Not rendered until revealed.
 *
 * This animated open by width first, and the label stayed at zero however wide
 * the max-width: as a flex item inside the button it was shrunk to nothing —
 * content 176px, used width 0 — because overflow: hidden takes its automatic
 * minimum size down to zero and nothing pushed back. Fighting that is a CSS
 * puzzle with no reader; not rendering the label is the same feature without
 * one. */
.goal-label { display: none; }
.goal.is-open .goal-label { display: inline; }

/* The one thing on this screen that changes on its own, so it earns the colour
   and the only animation in the conversation. */
.goal.is-met { color: var(--text); cursor: default; }
.goal.is-met .goal-star { color: var(--warn); }

.scene-pick { margin-bottom: 8px; }

/* The same three, afterwards. Filled and hollow stars rather than a score:
   three of three is a good evening, one of three is a thing to try again, and
   neither is improved by being made a percentage. */
.goal-list { display: flex; flex-direction: column; gap: 7px; }
.goal-done {
  display: flex; align-items: baseline; gap: 10px;
  font-size: 14.5px; color: var(--muted);
}
.goal-done span:first-child { color: #3a4250; font-size: 15px; }
.goal-done.is-met { color: var(--text); }
.goal-done.is-met span:first-child { color: var(--warn); }

/* ── Status line ──────────────────────────────────────────────────────── */

.status {
  padding: 7px 16px;
  font-size: 12.5px;
  color: var(--muted);
  text-align: center;
  border-top: 1px solid var(--line);
  background: var(--panel);
}
.status.live { color: #86efac; }
.status.error { color: #f87171; }

/* ── Talk button ──────────────────────────────────────────────────────── */

footer {
  padding: 14px 16px var(--pad-bottom);
  background: var(--panel);
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.ptt {
  position: relative;
  width: 100%;
  min-height: 84px;
  border: 2px solid var(--line);
  border-radius: 18px;
  background: var(--panel-2);
  color: var(--text);
  font: inherit; font-size: 17px; font-weight: 600;
  cursor: pointer;
  /* touch-action: none stops the browser treating a held thumb as a scroll or a
     long-press context menu, either of which cancels the pointer stream and
     leaves the microphone open. */
  touch-action: none;
  transition: background .12s, border-color .12s, transform .08s;
}
.ptt:disabled { opacity: .35; }
.ptt.live {
  background: #14532d;
  border-color: var(--live);
  transform: scale(.99);
}
.ptt.live .ptt-label::after {
  content: "";
  position: absolute; inset: -2px;
  border-radius: 18px;
  border: 2px solid var(--live);
  animation: ring 1.1s ease-out infinite;
}
@keyframes ring { to { opacity: 0; transform: scale(1.03); } }


/* Sits above the controls rather than inside them: the blurb is the point —
   it says what is about to happen, so choosing is not a guess from a name. */
.game-pick { margin-bottom: 10px; }
.game-pick p { margin: 7px 2px 0; font-size: 13px; min-height: 1.15em; }

/* Bleeds past the footer padding so a half-visible card at the right edge
   says "scrollable" without a scrollbar, which phones do not draw anyway. */
.game-strip {
  display: flex; gap: 8px;
  overflow-x: auto; scroll-snap-type: x proximity;
  padding: 2px 14px 2px 0; margin-right: -14px;
  scrollbar-width: none;
}
.game-strip::-webkit-scrollbar { display: none; }

.game-card {
  position: relative;
  flex: 0 0 auto; width: 78px; min-height: 70px;
  display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 5px;
  padding: 9px 6px 8px;
  font: inherit; color: var(--muted); text-align: center;
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 12px; cursor: pointer;
  scroll-snap-align: start;
  transition: border-color .12s, color .12s, background .12s;
}
.game-icon { font-size: 21px; line-height: 1; }
.game-name {
  font-size: 11.5px; font-weight: 600; line-height: 1.25;
  /* Two lines maximum: "Two truths and a lie" must not make one card taller
     than the rest and shift the whole row. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}

.game-card.is-on {
  color: var(--text);
  border-color: var(--accent);
  /* Solid first: a browser without color-mix still shows a selected card,
     rather than one that reads as unselected while the session runs a game. */
  background: #1a2436;
  background: color-mix(in srgb, var(--accent) 14%, var(--panel-2));
}

/* Locked games stay on the strip on purpose. A level the learner cannot see
   the point of becomes one when it is the thing standing between them and
   Devil's advocate. */
.game-card.is-locked { opacity: .55; cursor: default; }
.game-lock {
  position: absolute; top: 4px; right: 4px;
  font-size: 9px; font-weight: 700; letter-spacing: .04em;
  color: var(--muted); background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 5px; padding: 0 4px; line-height: 1.6;
}

.game-strip:disabled, .game-strip[aria-disabled="true"] { opacity: .45; }
.game-strip[aria-disabled="true"] .game-card { cursor: default; }

.controls { display: flex; align-items: center; gap: 12px; }

.switch { display: flex; align-items: center; gap: 8px; color: var(--muted);
          font-size: 13.5px; cursor: pointer; flex: 1; }
.switch input { width: 18px; height: 18px; accent-color: var(--accent); }

button.start, button[type="submit"], .ghost {
  font: inherit; font-weight: 600;
  padding: 11px 20px; border: 0; border-radius: 11px;
  background: var(--accent); color: #fff; cursor: pointer;
}
button.start.stop { background: var(--danger); }
.ghost { background: var(--panel-2); color: var(--text); }

/* ── Settings sheet ───────────────────────────────────────────────────── */

.sheet { position: fixed; inset: 0; z-index: 10; display: flex;
         flex-direction: column; justify-content: flex-end; }
.sheet-backdrop { position: absolute; inset: 0; background: #000a; }

.sheet-body {
  position: relative;
  background: var(--panel);
  border-radius: 18px 18px 0 0;
  border-top: 1px solid var(--line);
  padding: 18px 18px var(--pad-bottom);
  max-width: 560px; margin-inline: auto; width: 100%;
  animation: up .2s ease-out;
  max-height: 92vh; overflow-y: auto;
}
@keyframes up { from { transform: translateY(100%); } }

.sheet-body h2 { font-size: 16px; margin: 0 0 14px; }
.sheet-body label:not(.switch) {
  display: block; font-size: 12px; text-transform: uppercase;
  letter-spacing: .06em; color: var(--muted); margin: 14px 0 6px;
}
.sheet-body input[type="url"], .sheet-body input[type="password"] {
  width: 100%; font: inherit; padding: 11px 13px;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--line); border-radius: 11px;
  -webkit-user-select: text; user-select: text;
}
.hint { margin: 6px 0 0; font-size: 12px; color: var(--muted); }
.hint code { background: var(--panel-2); padding: 1px 5px; border-radius: 4px; }
.sheet-body .switch { margin-top: 16px; }
.sheet-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; }

/* ── Screens ──────────────────────────────────────────────────────────────
   Only one is visible at a time. The speaking screen keeps its original
   full-height column layout; the others are ordinary scrolling pages. */

/* Full-height column so the log scrolls between a fixed header and footer.
 * Only the speaking screen wants this; the others scroll normally. */
#screen-speak {
  display: flex;
  flex-direction: column;
  height: 100dvh;
}

/* The other screens manage their own height, so the body must not stretch them
 * into a single flex column — a leftover from when this app had one screen. */
body { display: block; }

/* Which of the three widths a screen gets, decided by the container it already
 * has. There are only eight of those classes across nineteen screens and they
 * already encode what kind of screen it is, so nothing needs a new class in the
 * HTML — and a screen added later inherits the list width by default, which is
 * the forgiving direction to be wrong in.
 *
 * :has() is the same vintage as color-mix() and dvh, both already load-bearing
 * here. Where it fails every screen falls back to --w-list: wide, not broken. */
.screen { --w: var(--w-list); }

.screen:has(.cards-main),
.screen:has(.listen-main),
.screen:has(.teach-main),
.screen:has(.journey-main),
.screen:has(.trail-main),
.screen:has(.bonus-main),
.screen:has(.run-main),
.screen:has(.board-main),
.screen:has(.stop-main),
.screen:has(.drill-main),
.screen:has(.duel-main),
#screen-speak { --w: var(--w-focus); }

.screen:has(.read-main),
.screen:has(.review-main) { --w: var(--w-read); }

/* The header, the status line and the footer are siblings of main, not inside
   it, so every one has to be capped or the title floats off the edge of its own
   content. */
.screen > header,
.screen > main,
.screen > footer,
.screen > .status,
.screen > .learn-progress {
  width: 100%;
  max-width: var(--w);
  margin-inline: auto;
}

/* ── Account screens ───────────────────────────────────────────────────── */

.auth-screen {
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: 24px max(16px, env(safe-area-inset-left)) calc(24px + env(safe-area-inset-bottom));
}

.auth-card { width: 100%; max-width: 380px; }

.brand { font-size: 24px; font-weight: 700; margin: 0 0 6px; }
.tagline { color: var(--muted, #8b929c); margin: 0 0 28px; line-height: 1.5; }

.auth-card form { display: flex; flex-direction: column; gap: 6px; }

.auth-card label {
  font-size: 13px; font-weight: 600; margin-top: 14px;
  color: var(--muted, #8b929c);
}

.auth-card input,
.auth-card select {
  font: inherit;
  padding: 13px 14px;
  border-radius: 12px;
  border: 1px solid #2a3038;
  background: #14171c;
  color: inherit;
  /* 16px or larger, or iOS Safari zooms the whole page on focus. */
  font-size: 16px;
}

.auth-card input:focus,
.auth-card select:focus {
  outline: none;
  border-color: #3b82f6;
}

.auth-card .hint {
  font-size: 12px; color: var(--muted, #8b929c);
  margin: 4px 0 0; line-height: 1.45;
}

button.primary {
  margin-top: 22px; padding: 15px;
  font: inherit; font-weight: 600; font-size: 16px;
  border: 0; border-radius: 12px;
  background: #3b82f6; color: #fff; cursor: pointer;
}
button.primary:disabled { opacity: .5; }

.form-error {
  margin: 14px 0 0; padding: 11px 13px;
  border-radius: 10px;
  background: #2a1416; border: 1px solid #7f1d1d; color: #fca5a5;
  font-size: 13px;
}

.switch-auth {
  margin-top: 22px; text-align: center;
  font-size: 14px; color: var(--muted, #8b929c);
}
.switch-auth a { color: #60a5fa; text-decoration: none; }

/* ── Home ──────────────────────────────────────────────────────────────── */

.home-main {
  padding: 8px max(16px, env(safe-area-inset-left))
           calc(24px + env(safe-area-inset-bottom));
  display: flex; flex-direction: column; gap: 12px;
}

.card {
  display: flex; align-items: center; gap: 14px;
  padding: 18px 16px;
  border-radius: 16px;
  background: #14171c; border: 1px solid #232830;
  color: inherit; text-decoration: none;
}

.card-primary { background: #10231a; border-color: #1c4a32; }

.card-soon { opacity: .45; }

.card-icon { font-size: 24px; line-height: 1; }
.card-body { display: flex; flex-direction: column; gap: 3px; }
.card-body b { font-size: 16px; }
.card-body span { font-size: 13px; color: var(--muted, #8b929c); line-height: 1.4; }

button.ghost.wide {
  margin-top: 18px; padding: 13px;
  width: 100%;
  font: inherit; border-radius: 12px;
  background: transparent; border: 1px solid #2a3038;
  color: var(--muted, #8b929c); cursor: pointer;
}

/* ── Native-language push-to-talk ──────────────────────────────────────── */

/* Deliberately quieter than the English button: an escape hatch should be easy
   to find when you need it and easy to ignore when you do not. */
.ptt-native {
  display: block; width: 100%; margin-top: 8px;
  padding: 13px;
  font: inherit; font-size: 14px; font-weight: 500;
  border-radius: 12px;
  background: transparent; border: 1px dashed #3a4250;
  color: var(--muted, #8b929c);
  user-select: none; -webkit-user-select: none; touch-action: none;
}
.ptt-native:disabled { opacity: .35; }
.ptt-native.live { background: #2a1c06; border-color: #b45309; color: #fbbf24; }

/* Unmistakable while armed: the learner is about to throw away what they said,
   and discovering that afterwards is worse than a loud colour. */
.ptt.cancelling, .ptt-native.cancelling {
  background: #2a1416; border-color: #dc2626; color: #fca5a5;
}

/* ── Progress on home ──────────────────────────────────────────────────── */

.progress {
  padding: 16px; border-radius: 16px;
  background: #14171c; border: 1px solid #232830;
}

.stat-row { display: flex; gap: 24px; }
.stat { display: flex; flex-direction: column; }
.stat b { font-size: 24px; font-variant-numeric: tabular-nums; }
.stat span { font-size: 12px; color: var(--muted, #8b929c); }

/* The second row is the rest of the app, and it is deliberately quieter than
   the first. Speaking is what this is for; the games are how you get there. */
.stat-row-2 {
  margin-top: 14px; padding-top: 14px;
  border-top: 1px solid #232830;
  flex-wrap: wrap; gap: 20px;
}
.stat-row-2 .stat b { font-size: 19px; }

/* What moved this week. The only green on the panel besides the improving
   line, because a total that never changes is not evidence of anything. */
.stat-delta {
  font-size: 11.5px; font-style: normal; font-weight: 600;
  color: #4ade80;
  min-height: 1.2em;
}

.improving, .focus { list-style: none; margin: 14px 0 0; padding: 0; }
.improving li, .focus li {
  font-size: 13px; padding: 7px 0;
  border-top: 1px solid #232830;
}
/* The one place a colour is earned: this line is the reason to come back. */
.improving .win { color: #4ade80; }
.focus li { color: var(--muted, #8b929c); }

/* ── Review ────────────────────────────────────────────────────────────── */

.review-main {
  padding: 8px max(16px, env(safe-area-inset-left))
           calc(24px + env(safe-area-inset-bottom));
  overflow-y: auto;
}

.review-summary {
  margin: 4px 0 22px; line-height: 1.55;
  color: #cbd5e1; font-size: 15px;
}

.review-block { margin-bottom: 26px; }
.review-block h2 {
  font-size: 12px; text-transform: uppercase; letter-spacing: .07em;
  color: var(--muted, #8b929c); margin: 0 0 10px;
}
.review-block ul { margin: 0; padding-left: 18px; }
.review-block li { margin-bottom: 8px; line-height: 1.5; font-size: 14px; }

.error-card {
  padding: 14px; margin-bottom: 10px;
  border-radius: 14px;
  background: #14171c; border: 1px solid #232830;
  border-left: 3px solid #475569;
}
/* Severity as a left edge rather than a badge: visible while scanning, not
   another label to read on every card. */
.error-card.sev-3 { border-left-color: #ef4444; }
.error-card.sev-2 { border-left-color: #f59e0b; }
.error-card.sev-1 { border-left-color: #64748b; }

.error-kind {
  font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted, #8b929c); margin-bottom: 8px;
}
.error-card .said {
  margin: 0 0 4px; font-size: 15px; line-height: 1.45;
  color: #94a3b8; text-decoration: line-through;
  text-decoration-color: #64748b;
}
.error-card .fixed {
  margin: 0; font-size: 15px; line-height: 1.45; color: #86efac;
}
.error-card .why {
  margin: 9px 0 0; font-size: 13px; line-height: 1.5;
  color: var(--muted, #8b929c);
}

/* ── Weekly call-time chart ────────────────────────────────────────────── */

.chart { margin: 18px 0 0; padding: 0; }

.chart figcaption {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: 12px; color: var(--muted, #8b929c);
  margin-bottom: 10px;
}
.chart figcaption b {
  color: #e6e8eb; font-size: 13px; font-variant-numeric: tabular-nums;
}

.bars {
  display: flex; align-items: flex-end; gap: 6px;
  height: 96px;
}

.bar-col {
  flex: 1;
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  height: 100%;
}

/* The track is full height and the fill grows inside it, so every day gets the
   same footprint whether or not anything happened on it — a missing bar reads
   as a broken chart, an empty one reads as a day off. */
.bar-track {
  flex: 1; width: 100%;
  display: flex; align-items: flex-end;
  border-radius: 6px;
  background: #14171c;
}

.bar-fill {
  width: 100%;
  border-radius: 6px;
  background: #22c55e;
  min-height: 2px;
  transition: height .35s ease;
}
.bar-fill.empty { background: #232830; }

.bar-col.today .bar-fill { background: #4ade80; }
.bar-col.today .bar-label { color: #e6e8eb; font-weight: 700; }

.bar-label {
  font-size: 11px; color: var(--muted, #8b929c);
  text-transform: uppercase;
}

@media (prefers-reduced-motion: reduce) {
  .bar-fill { transition: none; }
}

/* ── Level ─────────────────────────────────────────────────────────────── */

.level-now {
  margin: 14px 0 0; padding-top: 12px;
  border-top: 1px solid #232830;
  font-size: 13px; color: var(--muted, #8b929c);
}
.level-now b { color: #e6e8eb; font-weight: 600; }

/* The one moment on this screen worth a colour: proof the effort worked. */
.level-up {
  margin: 0 0 14px; padding: 11px 13px;
  border-radius: 10px;
  background: #10231a; border: 1px solid #1c4a32;
  color: #86efac; font-size: 13.5px; line-height: 1.45;
}

/* ── Starter section: decks and picture cards ──────────────────────────── */

.kids-main, .cards-main { padding: 16px; overflow-y: auto; }

.deck-list { display: flex; flex-direction: column; gap: 10px; }

.deck {
  position: relative;
  display: flex; align-items: center; gap: 14px;
  width: 100%; padding: 14px 16px;
  font: inherit; color: var(--text); text-align: left;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 14px; cursor: pointer;
}
.deck-icon { font-size: 30px; line-height: 1; }
.deck-body { display: flex; flex-direction: column; gap: 2px; flex: 1; }
.deck-body b { font-size: 16px; font-weight: 600; }
.deck-body span { font-size: 13px; color: var(--muted); }

/* Sits along the bottom edge of the row: progress you can see at a glance
   without a number competing with the deck name. */
.deck-bar {
  position: absolute; left: 16px; right: 16px; bottom: 8px;
  height: 3px; background: var(--line); border-radius: 2px; overflow: hidden;
}
.deck-bar span { display: block; height: 100%; background: var(--live); }

.cards-count { margin-left: auto; font-size: 13px; color: var(--muted); }

.cards-main { display: flex; align-items: center; justify-content: center; }

.flashcard {
  position: relative;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 10px;
  width: 100%; max-width: 340px; min-height: 300px;
  padding: 28px 20px;
  font: inherit; color: var(--text); text-align: center;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 20px; cursor: pointer;
}
.card-image { font-size: 92px; line-height: 1; }
.card-word { font-size: 30px; font-weight: 600; letter-spacing: -0.01em; }

.card-back { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; }
/* The learner's own script first, and biggest. IPA is the correct answer and
   the useless one at A0 — someone still learning this alphabet cannot read a
   second one invented to describe it. */
.card-say { font-size: 24px; font-weight: 600; color: var(--live); }
.card-ipa { font-size: 14px; color: var(--muted); font-family: ui-monospace, monospace; }
.card-translation { font-size: 19px; color: var(--text); }

.card-hint {
  position: absolute; left: 0; right: 0; bottom: 14px;
  font-size: 13px; color: var(--muted);
}

.card-answers { display: flex; gap: 10px; }
.card-answers button { flex: 1; }

.cards-done { text-align: center; color: var(--muted); font-size: 14px; }

/* ── Listen and point ──────────────────────────────────────────────────── */

.mode-pick { display: flex; gap: 8px; margin-bottom: 14px; }
.mode {
  flex: 1; padding: 10px 8px;
  font: inherit; font-size: 13.5px; font-weight: 600; color: var(--muted);
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 11px; cursor: pointer;
}
.mode.is-on {
  color: var(--text);
  border-color: var(--accent);
  background: #1a2436;
  background: color-mix(in srgb, var(--accent) 14%, var(--panel-2));
}

.listen-main {
  padding: 16px;
  display: flex; flex-direction: column; align-items: center; gap: 20px;
  overflow-y: auto;
}

.play-word {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 20px 28px;
  font: inherit; color: var(--text);
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 18px; cursor: pointer;
}
.play-icon { font-size: 44px; line-height: 1; }
.play-label { font-size: 12.5px; color: var(--muted); }
.play-word.is-playing { border-color: var(--accent); }

.choices {
  display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
  width: 100%; max-width: 340px;
}
.choice {
  aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
  font-size: 60px; line-height: 1;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 18px; cursor: pointer;
  transition: border-color .12s, background .12s;
}
/* Both answers are marked, not just the wrong one: being shown which picture
   it actually was is the only part of getting it wrong that teaches anything. */
.choice.is-right { border-color: var(--live); background: rgba(34,197,94,.14); }
.choice.is-wrong { border-color: var(--danger); background: rgba(220,38,38,.14); }
.choices.is-locked .choice { cursor: default; }

.listen-feedback { text-align: center; font-size: 15px; font-weight: 600; min-height: 1.4em; }
.listen-feedback.good { color: var(--live); }
.listen-feedback.bad { color: var(--text); }

/* ── Level tabs on the home screen ─────────────────────────────────────── */

.level-tabs {
  display: flex; gap: 6px; margin-bottom: 14px;
  overflow-x: auto; scrollbar-width: none;
}
.level-tabs::-webkit-scrollbar { display: none; }

.level-tab {
  position: relative; flex: 1 0 auto; min-width: 44px; padding: 8px 4px;
  font: inherit; font-size: 13px; font-weight: 600; color: var(--muted);
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 10px; cursor: pointer;
}
.level-tab.is-on {
  color: var(--text);
  border-color: var(--accent);
  background: #1a2436;
  background: color-mix(in srgb, var(--accent) 14%, var(--panel-2));
}
/* A dot on the learner's own level, so browsing never loses where they are.
   Not a colour change — that would fight the selected state for attention. */
.level-tab.is-you::after {
  content: ""; position: absolute; left: 50%; bottom: 3px;
  width: 4px; height: 4px; margin-left: -2px;
  background: var(--live); border-radius: 50%;
}


/* ── Grammar practice ──────────────────────────────────────────────────── */

.practice-intro { color: var(--muted); font-size: 13.5px; margin-bottom: 14px; }

.drill-main {
  padding: 20px 16px;
  display: flex; flex-direction: column; gap: 22px;
  overflow-y: auto;
}

.drill-prompt {
  font-size: 21px; line-height: 1.45; text-align: center;
  padding: 8px 4px;
}

.drill-options { display: flex; flex-direction: column; gap: 10px; }

.drill-option {
  width: 100%; padding: 15px 16px;
  font: inherit; font-size: 16px; color: var(--text); text-align: center;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 13px; cursor: pointer;
  transition: border-color .12s, background .12s;
}
/* Both are marked. Seeing which one was right is the part of getting it wrong
   that teaches something. */
.drill-option.is-right { border-color: var(--live); background: rgba(34,197,94,.14); }
.drill-option.is-wrong { border-color: var(--danger); background: rgba(220,38,38,.14); }
.drill-options.is-locked .drill-option { cursor: default; }

.drill-note {
  text-align: center; font-size: 14.5px; line-height: 1.5;
  color: var(--muted); min-height: 1.4em; padding: 0 8px;
}
.drill-note.good { color: var(--live); }

/* ── Grammar reference ─────────────────────────────────────────────────── */

.grammar-main { padding: 16px; overflow-y: auto; }

.rule-list { display: flex; flex-direction: column; gap: 8px; }

.rule {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 13px; overflow: hidden;
}
.rule.is-open { border-color: var(--accent); }

.rule-head {
  display: grid; grid-template-columns: 1fr auto; align-items: center;
  gap: 2px 10px; width: 100%; padding: 13px 15px;
  font: inherit; color: var(--text); text-align: left;
  background: none; border: 0; cursor: pointer;
}
.rule-head b { font-size: 15.5px; font-weight: 600; }
.rule-head > span:not(.rule-chev) {
  grid-column: 1; font-size: 13px; color: var(--muted); line-height: 1.4;
}
.rule-chev {
  /* The column matters as much as the row. Without it the chevron auto-places
     into column 1 — the 1fr one — which pushes the title into the narrow
     `auto` column and right-aligns it against the edge. It read as a styling
     choice on a phone, one rule to a row; two rules side by side made it
     obvious it was never one. */
  grid-column: 2;
  grid-row: 1 / span 2; font-size: 20px; color: var(--muted);
  transition: transform .15s;
}
.rule.is-open .rule-chev { transform: rotate(90deg); }

.rule-body { padding: 2px 15px 15px; }
.rule-body h3 {
  margin: 14px 0 7px; font-size: 11.5px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
}

/* Two columns on a phone, because a real table would scroll sideways and
   these are all short pairs. */
.rule-rows { display: grid; grid-template-columns: auto 1fr; gap: 5px 14px; margin: 0; }
.rule-rows dt { color: var(--muted); font-size: 14px; }
.rule-rows dd { margin: 0; font-size: 14.5px; }

.rule-list-items { margin: 0; padding-left: 18px; }
.rule-list-items li { font-size: 14.5px; line-height: 1.55; margin-bottom: 4px; }

/* The mistake the rule actually produces — the most useful line on the page,
   so it must not read as one more block of the same. */
.rule-watch {
  margin: 15px 0 0; padding: 10px 12px;
  font-size: 14px; line-height: 1.5;
  border-left: 2px solid var(--accent);
  background: rgba(255,255,255,.03);
  border-radius: 0 8px 8px 0;
}

.rule-practise {
  margin-top: 14px; padding: 9px 14px;
  font: inherit; font-size: 13.5px; font-weight: 600; color: var(--text);
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 9px; cursor: pointer;
}

.rule-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.rule-actions button { margin-top: 0; }

.rule-translate {
  padding: 9px 14px;
  font: inherit; font-size: 13.5px; font-weight: 600; color: var(--muted);
  background: none; border: 1px solid var(--line);
  border-radius: 9px; cursor: pointer;
}
.rule-translate.is-on { color: var(--text); border-color: var(--accent); }
.rule-translate:disabled { opacity: .6; cursor: default; }

/* ── The Rules section ─────────────────────────────────────────────────── */

.verb-head {
  padding: 13px 15px 4px;
  font-size: 15px; font-weight: 600;
}
.verb-hint { margin: 0; padding: 0 15px 10px; font-size: 13px; color: var(--muted); }
.verb-block { padding: 0 15px 10px; }

.verb-table { padding: 0 15px 14px; }
/* Three columns, fixed: base, past, participle. The whole value of the table
   is that the third column lines up down the page. */
.verb-row {
  display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px;
  padding: 5px 0; font-size: 14.5px;
  border-top: 1px solid var(--line);
}
.verb-row span:first-child { font-weight: 600; }
.verb-row span:not(:first-child) { color: var(--muted); }

/* ── Word lists ────────────────────────────────────────────────────────── */

.deck-group {
  margin: 18px 0 8px; font-size: 11.5px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
}
.deck-group:first-child { margin-top: 0; }

.word-list { display: flex; flex-direction: column; gap: 6px; }

.word-row {
  display: flex; align-items: center; gap: 12px;
  width: 100%; padding: 11px 14px;
  font: inherit; color: var(--text); text-align: left;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 11px; cursor: pointer;
}
.word-row.is-known { border-color: var(--live); }
.word-pic { font-size: 24px; line-height: 1; min-width: 26px; }
.word-body { display: flex; flex-direction: column; gap: 1px; flex: 1; }
.word-body b { font-size: 15.5px; font-weight: 600; }
.word-body span { font-size: 13px; color: var(--muted); }
.word-mark { font-size: 17px; color: var(--live); min-width: 16px; text-align: right; }

/* A word card with nothing to show: the word takes the space the picture
   would have had, rather than leaving a hole above it. */
.flashcard.no-picture { justify-content: center; }
.flashcard.no-picture .card-word { font-size: 36px; }

/* ── The frequency corpus ──────────────────────────────────────────────── */

/* The level code stands in for the picture the themed decks have. Tabular so
   A0 and C1 occupy the same width and the words below them line up. */
.band-code {
  font-size: 14px; font-weight: 700; letter-spacing: .02em;
  font-variant-numeric: tabular-nums;
  color: var(--muted); min-width: 26px;
}
.word-row.is-known .band-code { color: var(--live); }

/* A quiet pill under the word: it is a fact about the word, and it should
   read as one rather than competing with the word itself. */
.round-pos:not(:empty) {
  padding: 3px 10px; margin-top: -2px;
  font-size: 11px; font-weight: 600; color: var(--muted);
  text-transform: uppercase; letter-spacing: .08em;
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 999px;
}

/* ── Swipe to answer ───────────────────────────────────────────────────── */

/* pan-y keeps vertical scrolling with the browser while the horizontal axis
   is ours, so a swipe never fights the page. */
.flashcard, #wordCard { touch-action: pan-y; user-select: none; }

/* The tint rides on --swipe, which the gesture sets from 0 to 1 as the card
   travels, so the answer is visible long before the finger lifts. */
.flashcard.swipe-yes, #wordCard.swipe-yes {
  border-color: var(--live);
  background: color-mix(in srgb, var(--live) calc(var(--swipe, 0) * 22%), var(--panel));
}
.flashcard.swipe-no, #wordCard.swipe-no {
  border-color: var(--warn);
  background: color-mix(in srgb, var(--warn) calc(var(--swipe, 0) * 22%), var(--panel));
}

/* A word that says which way it is going, in case the colour alone does not
   land — and it does not, for anyone who cannot tell green from amber. */
.flashcard::after, #wordCard::after {
  content: attr(data-swipe-label);
  position: absolute; top: 12px; left: 0; right: 0;
  text-align: center; font-size: 12px; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase;
  opacity: var(--swipe, 0); pointer-events: none;
}
.flashcard.swipe-yes::after, #wordCard.swipe-yes::after {
  content: "I know it"; color: var(--live);
}
.flashcard.swipe-no::after, #wordCard.swipe-no::after {
  content: "Show me again"; color: var(--warn);
}

.flashcard, #wordCard { position: relative; }

/* The answer buttons carry the same two colours, so the gesture and the
   buttons are visibly the same two choices rather than two separate systems. */
#roundAgain, #againBtn { border-color: color-mix(in srgb, var(--warn) 45%, var(--line)); }

/* ── The sound toggle ──────────────────────────────────────────────────── */

/* The only audio control on a card screen. It sits in the header with the
   other controls rather than on the card, which has room for one thing. */
.icon-btn.mute { margin-left: auto; font-size: 16px; line-height: 1; }

/* The end-of-round button sits under the line that says how it went, so the
   result is read before the way onward is offered. */
#roundMore { margin-top: 12px; }

/* ── Report a problem ──────────────────────────────────────────────────── */

/* Top right, above every screen, small enough to ignore until it is wanted.
   It sits opposite the back arrow so it never lands under a thumb reaching
   for that. */
.report-btn {
  position: fixed; z-index: 40;
  top: calc(env(safe-area-inset-top) + 10px); right: 12px;
  width: 26px; height: 26px;
  font: inherit; font-size: 14px; font-weight: 700; line-height: 1;
  color: var(--muted);
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 50%; cursor: pointer; opacity: .5;
}
.report-btn:hover, .report-btn[aria-expanded="true"] { opacity: 1; color: var(--text); }

.report-sheet {
  position: fixed; inset: 0; z-index: 50;
  display: flex; align-items: flex-end; justify-content: center;
  background: rgba(0,0,0,.55);
}
.report-card {
  width: 100%; max-width: 420px;
  padding: 20px 18px calc(18px + env(safe-area-inset-bottom));
  background: var(--panel); border-top: 1px solid var(--line);
  border-radius: 18px 18px 0 0;
}
.report-card h2 { margin: 0 0 6px; font-size: 17px; }
.report-card p { margin: 0 0 14px; font-size: 13.5px; color: var(--muted); line-height: 1.5; }

.report-kinds { display: flex; flex-direction: column; gap: 8px; margin-bottom: 14px; }
.report-kind {
  width: 100%; padding: 13px 15px;
  font: inherit; font-size: 15px; color: var(--text); text-align: left;
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 12px; cursor: pointer;
}

.toast {
  position: fixed; z-index: 60;
  left: 50%; bottom: calc(24px + env(safe-area-inset-bottom));
  transform: translateX(-50%);
  margin: 0; padding: 10px 16px;
  font-size: 14px; color: var(--text);
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 999px;
}

/* Three seconds-settings, cycling. Tabular so 5s and 15s do not shift the
   header as it changes. */
.icon-btn.pace {
  margin-left: auto; width: auto; padding: 0 8px;
  font-size: 12.5px; font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* The rule is tappable once it is showing: it skips the rest of the wait. */
.drill-note:not([hidden]) { cursor: pointer; }

.deck-group-note {
  margin: -4px 0 8px; padding: 0 2px;
  font-size: 12.5px; color: var(--muted); line-height: 1.45;
}

/* The chosen teacher. Same row as a deck, so the app has one list shape
   rather than a new one for every kind of choice. */
.voice-row.is-on { border-color: var(--accent); }

/* ── Books ─────────────────────────────────────────────────────────────── */

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

.book {
  display: flex; align-items: stretch; gap: 14px;
  width: 100%; padding: 0; overflow: hidden;
  font: inherit; color: var(--text); text-align: left;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 12px; cursor: pointer;
}

/* A drawn spine rather than a stored image: a hue and the title are enough to
   find a book again, and it costs no files, no licensing and no bandwidth. */
.book-cover {
  position: relative; flex: 0 0 58px;
  display: flex; align-items: flex-end;
  padding: 8px 7px;
  border-left: 4px solid;
}
.book-cover-title {
  font-size: 9.5px; line-height: 1.25; font-weight: 600;
  color: rgba(255,255,255,.82);
  display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical;
  overflow: hidden;
}

.book-body { display: flex; flex-direction: column; gap: 2px; padding: 12px 14px 12px 0; flex: 1; }
.book-body b { font-size: 15.5px; font-weight: 600; line-height: 1.3; }
.book-author { font-size: 13px; color: var(--muted); }
.book-meta { font-size: 12.5px; color: var(--muted); margin-top: 2px; }

.read-main { padding: 18px 18px 8px; overflow-y: auto; }
.read-body { font-size: 17.5px; line-height: 1.85; }
.read-body p { margin: 0 0 1.1em; }

/* Underlined, not coloured: a page where every third word is highlighted is a
   page nobody reads. The mark is there when you look for it. */
.read-body .w { cursor: pointer; border-bottom: 1px solid var(--line); }
.read-body .w.is-on { background: color-mix(in srgb, var(--accent) 22%, transparent); }

.read-source { margin: 6px 0 0; font-size: 12px; color: var(--muted); }

.read-word {
  padding: 12px 14px; margin-bottom: 10px;
  background: var(--panel); border: 1px solid var(--accent); border-radius: 12px;
}
.rw-head { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; margin-bottom: 4px; }
.rw-head b { font-size: 18px; font-weight: 600; }
.rw-level {
  font-size: 11px; font-weight: 700; letter-spacing: .04em;
  padding: 2px 7px; border-radius: 999px;
  background: var(--panel-2); color: var(--muted);
}
.rw-say { font-size: 14px; color: var(--live); }
.rw-t { font-size: 17px; margin-bottom: 10px; }

.read-nav { display: flex; gap: 10px; }
.read-nav button { flex: 1; }

/* The translation sits over the text, anchored to the word, so the sentence
   stays in view while it is read. */
.read-main { position: relative; }
.read-word {
  position: absolute; z-index: 5; max-width: min(320px, calc(100% - 16px));
  padding: 11px 13px;
  background: var(--panel-2); border: 1px solid var(--accent);
  border-radius: 12px;
  box-shadow: 0 6px 20px rgba(0,0,0,.45);
}


/* The pattern on a card: an example, not a second translation. Quieter than
   the meaning, because it is read after it. */
.card-note {
  margin-top: 8px; padding-top: 8px;
  border-top: 1px solid var(--line);
  font-size: 13.5px; line-height: 1.45; color: var(--muted);
}
.card-note[hidden] { display: none !important; }

/* What a whole deck has in common — the reason these are three decks and not
   one long list. */
.deck-note { font-size: 12px; color: var(--muted); line-height: 1.4; margin-top: 3px; }

/* Search sits above the tabs and replaces them while it has something in it. */
#ruleSearch { width: 100%; margin-bottom: 12px; }

/* On a search result the level is a fact about the rule, not a place to go. */
.rule-level {
  grid-row: 1; margin-left: auto;
  font-size: 11px; font-weight: 700; letter-spacing: .04em;
  padding: 2px 7px; border-radius: 999px;
  background: var(--panel-2); color: var(--muted);
}


/* ── The word duel ─────────────────────────────────────────────────────── */

/* The cheapest screen here: no model, no synthesis, just words already in the
   database. It is the one that can carry advertising, which is what lets the
   conversation stay expensive. */

.duel-main { padding: 16px; overflow-y: auto; }

.duel-record { font-size: 13.5px; color: var(--muted); margin: 0 0 16px; }
.duel-missed { font-size: 14px; line-height: 1.7; color: var(--warn); margin: 0; }

.duel-spinner {
  width: 30px; height: 30px; margin: 40px auto 16px;
  border: 2px solid var(--line); border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 900ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.duel-head {
  display: flex; align-items: baseline; justify-content: space-between;
  margin-bottom: 8px;
}
.duel-count { font-size: 13px; color: var(--muted); font-variant-numeric: tabular-nums; }
.duel-scores { font-size: 17px; font-variant-numeric: tabular-nums; }
.duel-scores span { color: var(--muted); }

/* A bar, not a number. At this speed a counting digit is one more thing to
   read, and reading it costs the seconds it is counting. */
.duel-bar {
  height: 3px; background: var(--line); border-radius: 2px;
  overflow: hidden; margin-bottom: 26px;
}
.duel-bar span { display: block; height: 100%; width: 100%; background: var(--accent); }

.duel-word {
  margin: 0 0 22px; text-align: center;
  font-size: 38px; font-weight: 600; letter-spacing: -0.02em;
}

.duel-options { display: flex; flex-direction: column; gap: 9px; }
.duel-option {
  width: 100%; padding: 15px 16px;
  font: inherit; font-size: 16px; color: var(--text); text-align: left;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 12px; cursor: pointer;
  transition: border-color .12s, background .12s;
}
.duel-options.is-locked .duel-option { cursor: default; }

/* Yours before the result arrives, so the tap registers immediately rather
   than after a round trip nobody can see the length of. */
.duel-option.is-mine { border-color: var(--accent); }
.duel-option.is-right { border-color: var(--live); background: rgba(34,197,94,.14); }
.duel-option.is-wrong { border-color: var(--danger); background: rgba(220,38,38,.14); }

/* Their pick, marked on the button rather than written out somewhere else —
   and only after the round closes, or the second player is just copying. */
.duel-option.is-theirs::after {
  content: "them";
  float: right;
  font-size: 10.5px; font-weight: 700; letter-spacing: .06em;
  color: var(--muted); text-transform: uppercase;
  padding-top: 3px;
}

.duel-them {
  display: flex; align-items: center; gap: 8px;
  margin: 22px 0 0; font-size: 13.5px;
}
.duel-them b { font-weight: 600; }

/* Never dressed as a person. Somebody who works this out for themselves stops
   trusting the numbers on every other screen too, and saying it costs nothing:
   people play against a computer happily when they are told that is what it is. */
.duel-bot-tag {
  font-size: 10px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  color: var(--muted); background: var(--panel-2);
  border: 1px solid var(--line); border-radius: 999px;
  padding: 2px 7px;
}

.duel-result { margin: 30px 0 4px; text-align: center; font-size: 22px; font-weight: 600; }
.duel-final {
  margin: 0 0 6px; text-align: center;
  font-size: 34px; font-weight: 600; font-variant-numeric: tabular-nums;
}
#duel-over .muted, #duel-over .practice-intro { text-align: center; }
#duel-over .ghost.wide { display: block; text-align: center; text-decoration: none; }

/* ── The word list after a duel, and after a learning set ──────────────── */

.duel-words { display: flex; flex-direction: column; gap: 4px; margin: 20px 0 18px; }

.duel-word-row {
  display: grid; grid-template-columns: 20px 1fr 1fr;
  align-items: baseline; gap: 10px;
  padding: 9px 12px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 10px;
}
.duel-word-row b { font-size: 15px; font-weight: 600; }
.duel-word-row span:last-child { font-size: 14px; color: var(--muted); }
.duel-mark { color: var(--warn); font-size: 14px; }
.duel-word-row.is-got { border-color: color-mix(in srgb, var(--live) 35%, var(--line)); }
.duel-word-row.is-got .duel-mark { color: var(--live); }

/* ── Learning a set ────────────────────────────────────────────────────── */

/* The one screen that takes the wide tier without being a list.
 *
 * It is four different exercises behind one header, and they do not agree
 * about width. Matching wants room — twelve chips in two columns is a grid,
 * and squeezing it into 560px on a laptop wasted two thirds of the display for
 * no reason. A question with three buttons wants exactly what the drill wants,
 * which is not to be a metre wide.
 *
 * So the screen is wide and the panels that should not be are capped inside
 * it. That is the honest version: the width belongs to the passes that use it.
 */
.learn-main { padding: 16px; overflow-y: auto; }

#learn-idle, #learn-quiz, #learn-type {
  width: 100%; max-width: 560px; margin-inline: auto;
}
/* The summary reads as a table and gets the room; the buttons under it do not. */
#learn-over { width: 100%; max-width: 720px; margin-inline: auto; }

@media (min-width: 700px) {
  /* The grid is capped and centred rather than spread across the screen. Given
     the full 940px the two columns drift apart, and the drag between them gets
     longer while nothing gets easier to read — the width is better spent on
     showing every pair at once, which is what perPage() does. */
  .match-grid { max-width: 660px; margin-inline: auto; gap: 14px; }
}

/* The bar sits above the content and outside it, because it belongs to the
   whole run rather than to whichever of the four passes is on screen. */
.learn-progress { height: 3px; background: var(--line); }
.learn-progress span {
  display: block; height: 100%; width: 0%;
  background: var(--live);
  transition: width .35s ease-out;
}

.learn-stage {
  margin: 0 0 18px;
  font-size: 12px; font-weight: 600; letter-spacing: .05em;
  text-transform: uppercase; color: var(--muted);
}

.match-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.match-col { display: flex; flex-direction: column; gap: 8px; }

.match-chip {
  padding: 14px 12px;
  font: inherit; font-size: 14.5px; color: var(--text); text-align: center;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 11px; cursor: pointer;
  transition: border-color .12s, background .12s, opacity .2s;
  /* Ours, both axes. Without this a drag downwards is read as a page scroll
     and the pointer stream is cancelled halfway across. */
  touch-action: none;
  -webkit-user-select: none; user-select: none;
}
.match-chip.is-picked { border-color: var(--accent); background: #1a2436; }

/* Dragging: a copy follows the pointer and the original stays where it was, so
   the column does not reflow and move the chip being aimed at. */
.match-ghost {
  position: fixed; z-index: 70;
  transform: translate(-50%, -50%);
  pointer-events: none;
  border-color: var(--accent);
  box-shadow: 0 8px 24px rgba(0,0,0,.5);
  opacity: .95;
}
.match-chip.is-dragging { opacity: .3; }
.match-chip.is-over { border-color: var(--accent); background: #1a2436; }
.match-chip.is-wrong {
  border-color: var(--danger);
  animation: shake .35s;
}
/* Matched pairs stay on screen rather than vanishing: the column they were in
   is the memory aid, and a shrinking list makes the last pair free. */
.match-chip.is-done {
  opacity: .35; cursor: default;
  border-color: color-mix(in srgb, var(--live) 40%, var(--line));
}
@keyframes shake {
  25% { transform: translateX(-5px); }
  75% { transform: translateX(5px); }
}

.learn-prompt {
  margin: 8px 0 6px; text-align: center;
  font-size: 34px; font-weight: 600; letter-spacing: -0.02em;
}
/* The learner's own language, smaller: it is the question, not the thing being
   learned, and at this size it stops competing with the English. */
.learn-prompt.is-native { font-size: 26px; font-weight: 500; }

#learnSay { text-align: center; margin: 0 0 20px; font-size: 15px; }

.learn-options { display: flex; flex-direction: column; gap: 9px; margin-top: 18px; }
.learn-option {
  width: 100%; padding: 15px 16px;
  font: inherit; font-size: 16px; color: var(--text); text-align: left;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 12px; cursor: pointer;
}
.learn-options.is-locked .learn-option { cursor: default; }
.learn-option.is-right { border-color: var(--live); background: rgba(34,197,94,.14); }
.learn-option.is-wrong { border-color: var(--danger); background: rgba(220,38,38,.14); }

/* The box and the hint button on one line, so asking for a letter does not
   mean looking away from what has been typed so far. */
.learn-type-row { display: flex; align-items: stretch; gap: 8px; margin: 18px 0 8px; }
.learn-type-row .learn-typed { margin: 0; flex: 1; }

.learn-hint-btn {
  flex: 0 0 48px;
  font: inherit; font-size: 17px; font-weight: 700; font-style: italic;
  color: var(--muted);
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 12px; cursor: pointer;
}
.learn-hint-btn:disabled { opacity: .35; cursor: default; }

.learn-typed {
  width: 100%; margin: 18px 0 8px; padding: 14px 16px;
  font: inherit; font-size: 20px; text-align: center;
  color: var(--text); background: var(--bg);
  border: 1px solid var(--line); border-radius: 12px;
  -webkit-user-select: text; user-select: text;
}
.learn-typed.is-right { border-color: var(--live); }
.learn-typed.is-wrong { border-color: var(--danger); animation: shake .35s; }

/* The answer, spelled out. By this pass the word has been met three times, and
   withholding the spelling turns the one place it is taught into a guess. */
.learn-hint {
  margin: 0 0 14px; min-height: 1.4em; text-align: center;
  font-size: 19px; font-weight: 600; color: var(--live);
}

/* One letter, asked for. Bigger than the whole word is shown at, because it is
   a single character being read at a glance and then typed. */
.learn-hint.is-letter {
  font-size: 32px; letter-spacing: .08em; color: var(--accent);
  font-family: ui-monospace, monospace;
}

/* A word set aside after three attempts. Quiet: it is a decision the game made
   for the learner, not a verdict on them, and it must not read as a scolding. */
.learn-skip {
  margin: 12px 0 0; min-height: 1.3em; text-align: center;
  font-size: 13.5px; color: var(--muted);
}

/* Given rather than found, in the matching grid. The pair resolves itself so
   the page can complete, and the colour says it was not earned. */
.match-chip.is-given {
  border-color: color-mix(in srgb, var(--warn) 45%, var(--line));
  opacity: .45;
}

/* Running out of words is the goal, and on screen it looks like a dead end.
   The panel exists so it reads as an achievement with somewhere to go. */
.learn-empty {
  margin: 22px 0 14px; padding: 18px 16px 8px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 14px;
}
.learn-empty-title {
  margin: 0 0 8px; text-align: center;
  font-size: 17px; font-weight: 600; color: var(--live);
}
.learn-empty .ghost.wide {
  display: block; text-align: center; text-decoration: none; margin-bottom: 8px;
}

/* ── The map ───────────────────────────────────────────────────────────── */

/* Ninety-seven rules on one page. The grouping is by kind rather than by
   level, so a row is a thread running A0 to C1 — the tenses from am/is/are to
   the subjunctive — and the ten rows are the same ten kinds the review
   pipeline labels spoken mistakes with. */

.map-group { margin-bottom: 22px; }

.map-head {
  display: flex; align-items: center; gap: 9px;
  margin: 0 0 9px;
  font-size: 14px; font-weight: 600;
}
.map-icon { font-size: 16px; line-height: 1; }
.map-count {
  margin-left: auto;
  font-size: 12px; font-weight: 600; color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* Wraps rather than scrolls. A row that runs off the edge hides exactly the
   part of the thread the learner has not reached yet, which is the part worth
   seeing. */
.map-row { display: flex; flex-wrap: wrap; gap: 7px; }

.map-cell {
  position: relative; overflow: hidden;
  flex: 1 1 132px; max-width: 200px;
  display: flex; flex-direction: column; gap: 3px;
  padding: 9px 10px 11px;
  font: inherit; color: var(--text); text-align: left;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 10px; cursor: pointer;
}
.map-cell b {
  font-size: 12.5px; font-weight: 600; line-height: 1.3;
  /* Two lines, so one long title cannot make its row taller than the others
     and break the horizontal reading the whole screen is for. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}

.map-level {
  font-size: 10px; font-weight: 700; letter-spacing: .06em;
  color: var(--muted); font-variant-numeric: tabular-nums;
}

/* Three states and no fourth: not started, started, finished. A percentage on
   ninety-seven cells is ninety-seven numbers nobody reads. */
.map-cell.is-started { border-color: color-mix(in srgb, var(--warn) 40%, var(--line)); }
.map-cell.is-done {
  border-color: color-mix(in srgb, var(--live) 45%, var(--line));
  background: color-mix(in srgb, var(--live) 7%, var(--panel));
}
.map-cell.is-done .map-level { color: var(--live); }

/* Where the learner is now, so the map never loses them. A left edge rather
   than a colour, which would fight the three progress states for attention. */
.map-cell.is-yours::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0;
  width: 3px; background: var(--accent);
}
.map-cell.is-yours { padding-left: 13px; }

.map-bar {
  display: block; height: 2px; margin-top: 3px;
  background: var(--line); border-radius: 2px; overflow: hidden;
}
.map-bar span { display: block; height: 100%; background: var(--live); }

/* ══ Wider than a phone ═══════════════════════════════════════════════════
 *
 * Everything above this line is the phone layout and is reached by every
 * viewport. Everything below is additive: if a change here is visible at
 * 375px wide, it is a bug.
 * ════════════════════════════════════════════════════════════════════════ */

/* ── Lists spread out ──────────────────────────────────────────────────── */

@media (min-width: 700px) {
  /* The grid goes on the list itself rather than on `main`, so the intro
     paragraph and the tab strip above it are left alone. */
  .deck-list, .word-list, .rule-list, .home-main {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 10px;
    /* Without this a short card stretches to match a tall one beside it and
       the whole column reads as one block. */
    align-items: start;
  }

  /* Home's level tabs and its progress panel are siblings of the cards, not
     cards. They span. */
  .home-main > nav,
  .home-main > .progress { grid-column: 1 / -1; }

  /* cards.js appends the group headings straight into the list, so in a grid
     they land in a cell and take a card's place. A heading is not a card. */
  .deck-list > .deck-group, .deck-list > .deck-group-note,
  .word-list > .deck-group, .word-list > .deck-group-note { grid-column: 1 / -1; }

  /* Collapsed rules scan nicely two-up; an open one carries verb tables and
     example rows that need the whole width, so it takes the row. */
  .rule.is-open { grid-column: 1 / -1; }

  /* Six tabs sharing 940px would be 150px each. They should be the size of
     their own labels. */
  .level-tabs, .mode-pick { justify-content: flex-start; }
  .level-tab { flex: 0 0 auto; min-width: 62px; padding: 8px 15px; }
  .mode { flex: 0 0 auto; padding: 10px 20px; }
}

@media (min-width: 1040px) {
  /* Word rows are a single line — icon, word, gloss — so they take a third
     column where the chunkier deck rows do not. */
  .word-list { grid-template-columns: repeat(3, 1fr); }
}

/* ── Books become a shelf ──────────────────────────────────────────────── */

/* On a phone a book is a row with a spine at the left edge. With room it turns
   into what it actually is: a cover you pick out by its colour. Same DOM —
   books.js already paints the hue inline. */
@media (min-width: 700px) {
  .shelf {
    display: grid;
    gap: 20px 14px;
    /* auto-fill, so one rule carries three across a tablet and six across a
       monitor without a second breakpoint guessing at counts. */
    grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
  }

  .book {
    flex-direction: column; align-items: stretch; gap: 0;
    background: none; border: 0; overflow: visible;
  }

  .book-cover {
    flex: none; width: 100%;
    aspect-ratio: 2 / 3;
    padding: 13px 12px;
    border-radius: 9px;
    border-left-width: 6px;
  }
  .book-cover-title { font-size: 13.5px; -webkit-line-clamp: 5; }

  .book-body { padding: 9px 2px 0; }
  .book-body b { font-size: 14px; }
  .book-author, .book-meta { font-size: 12px; }
}

/* ── The rail ──────────────────────────────────────────────────────────── */

/* Hidden below 1040px, where the back arrow and the home screen are the whole
   navigation and a rail would only steal width. */
#rail { display: none; }

@media (min-width: 1040px) {
  body {
    display: grid;
    grid-template-columns: 240px minmax(0, 1fr);
    /* `html, body { height: 100% }` (:50) is left over from the one-screen
     * layout, and it silently breaks the rail: it gives the grid container a
     * definite 860px while the row inside it is 1600px of rules, so the sticky
     * containing block ends up exactly as tall as the rail and there is no room
     * left to travel. The rail then scrolls away with the page while still
     * computing as `position: sticky`, which is the kind of bug you only find
     * by measuring where it actually is.
     *
     * Nothing else depends on body having a percentage height — the two other
     * `height: 100%` rules here resolve against their own flex parents. */
    height: auto;
    min-height: 100dvh;
  }

  #rail {
    display: flex; flex-direction: column; gap: 2px;
    position: sticky; top: 0; height: 100dvh;
    padding: 22px 12px calc(12px + env(safe-area-inset-bottom));
    background: var(--panel);
    border-right: 1px solid var(--line);
  }

  .rail-brand {
    padding: 0 12px 16px;
    font-size: 15px; font-weight: 700; letter-spacing: -0.01em;
  }

  #rail a {
    display: flex; align-items: center; gap: 10px;
    padding: 9px 12px;
    font-size: 14.5px; font-weight: 500;
    color: var(--muted); text-decoration: none;
    border-radius: 9px;
  }
  .rail-icon { font-size: 15px; line-height: 1; width: 20px; text-align: center; }

  /* Signing in is not a place in the app, so it gets no rail and no column. */
  body[data-screen="login"], body[data-screen="signup"] { display: block; }
  body[data-screen="login"] #rail, body[data-screen="signup"] #rail { display: none; }
}

/* Which link is lit, straight off the data-screen the router already writes
   (router.js:59) — so this costs no JavaScript at all. Sub-screens light their
   own section: reading a book is still Books, a word round is still Words. */
body[data-screen="home"] #rail [href="#/home"],
body[data-screen="speak"] #rail [href="#/speak"],
body[data-screen="rules"] #rail [href="#/rules"],
body[data-screen="books"] #rail [href="#/books"],
body[data-screen="read"] #rail [href="#/books"],
body[data-screen="kids"] #rail [href="#/kids"],
body[data-screen="cards"] #rail [href="#/kids"],
body[data-screen="listen"] #rail [href="#/kids"],
body[data-screen="bands"] #rail [href="#/kids"],
body[data-screen="wordround"] #rail [href="#/kids"],
body[data-screen="wordlist"] #rail [href="#/kids"],
body[data-screen="mywords"] #rail [href="#/kids"],
body[data-screen="browse"] #rail [href="#/kids"],
body[data-screen="practice"] #rail [href="#/practice"],
body[data-screen="drill"] #rail [href="#/practice"] {
  color: var(--text);
  background: var(--panel-2);
}

/* ── Mouse and keyboard ────────────────────────────────────────────────── */

/* Guarded, always. An unguarded :hover sticks after a tap on a touchscreen and
   leaves the last thing you touched looking selected. */
@media (hover: hover) {
  .card:hover, .deck:hover, .word-row:hover, .book:hover,
  .rule-head:hover, .game-card:not(.is-locked):hover,
  .mode:hover, .level-tab:hover { border-color: #39414d; }

  .book:hover { border-color: transparent; }
  .book:hover .book-cover { outline: 2px solid var(--accent); outline-offset: 2px; }

  .icon-btn:hover { background: var(--panel-2); color: var(--text); }
  #rail a:hover { color: var(--text); background: var(--panel-2); }
}

/* There was not one focus style in the whole sheet. Tabbing through a book
   list on a laptop showed nothing at all happening. */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The body turns off text selection, which is right for a button held under a
   thumb and wrong for a browser, where the first thing anyone does with a rule
   or a translation is copy it. */
.read-body, .rule-body, .word-body, .card-back, .review-main, .rw-t {
  -webkit-user-select: text; user-select: text;
}

/* ── Short viewports ───────────────────────────────────────────────────── */

/* A phone on its side is 375px tall. This is the one place the old layout does
 * not merely look narrow, it overflows: the speaking footer alone measured
 * 312px of a 375px screen, so the talk button sat below the fold.
 *
 * Shrinking the button was not enough — it is the whole stack above and below
 * it. Nothing is removed, because every one of these is needed mid-conversation:
 * the escape hatch into your own language, the start button, the game picker.
 * They are compressed instead, and the game cards drop to icons. */
@media (max-height: 520px) {
  header { padding-top: max(6px, env(safe-area-inset-top)); padding-bottom: 6px; }
  .status { padding-block: 4px; }

  footer { gap: 8px; padding-top: 10px; }
  .ptt { min-height: 56px; font-size: 15px; }
  .ptt-native { margin-top: 4px; padding: 9px; }
  .controls { gap: 10px; }
  button.start, .ghost { padding: 9px 16px; }

  /* The blurb explains the game about to start; with no room it is the first
     thing to go, because the games are named on their own cards. */
  .game-pick { margin-bottom: 0; }
  .game-pick p { display: none; }
  /* Icon-only cards: still a picker, a third of the height. */
  .game-card { width: 46px; min-height: 44px; padding: 6px 4px; }
  .game-name { display: none; }
  .game-icon { font-size: 19px; }

  .flashcard { min-height: 180px; padding: 16px 20px; gap: 6px; }
  .card-image { font-size: 52px; }
  .card-word { font-size: 26px; }
  .card-hint { bottom: 8px; }
  .play-word { padding: 12px 22px; }
  .play-icon { font-size: 32px; }
  .choice { font-size: 40px; }
}

/* ── Sheets stop being bottom sheets ───────────────────────────────────── */

/* A sheet rises from the bottom edge because that is where the thumb is. With
   a mouse there is no bottom edge to speak of, and a dialog belongs in the
   middle. */
@media (min-width: 700px) {
  .sheet { justify-content: center; }
  .sheet-body {
    max-width: 480px;
    border-radius: 18px;
    border: 1px solid var(--line);
    animation: none;
  }

  .report-sheet { align-items: center; }
  .report-card {
    border-radius: 18px;
    border: 1px solid var(--line);
    padding-bottom: 18px;
  }
}

/* ── Teach the chameleon ───────────────────────────────────────────────── */

.teach-main {
  display: flex; flex-direction: column; align-items: center; gap: 18px;
}

.teach-intro {
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  max-width: 340px; text-align: center; padding-top: 12px;
}
.teach-hero { width: 160px; height: 128px; }
.teach-hero svg { width: 100%; height: 100%; }
.teach-hero .cham-body, .teach-hero .cham-crest { fill: #56b98a; }
.teach-hero .cham-tail, .teach-hero .cham-leg { fill: none; stroke: #56b98a; }
.teach-pitch { margin: 0; color: var(--muted); font-size: 15px; line-height: 1.5; }

/* The chameleon is positioned against this box, so it has to be the thing
   the tiles sit in. The padding is his parking space above them. */
.teach-stage {
  position: relative; width: 100%; max-width: 420px; padding-top: 104px;
}

.teach-choices {
  grid-template-columns: repeat(3, 1fr); max-width: 420px; gap: 10px;
}
.teach-choices .choice { font-size: 44px; border-radius: 16px; }
/* A picture already ruled out stops being a target rather than going dark:
   the child is still looking at it while they decide. */
.teach-choices .choice:disabled { opacity: .45; cursor: default; }

.cham {
  position: absolute; left: 0; top: 0; width: 98px; height: 80px;
  /* Never in the way of a tap. The tiles underneath are what a child is
     aiming at, and a chameleon sitting on one must not swallow the press. */
  pointer-events: none;
  transition: transform .55s cubic-bezier(.34, 1.25, .5, 1);
  --cham: #8296a2;
}
.cham svg { width: 100%; height: 100%; overflow: visible; }

/* One colour drives the whole animal, so his mood is a single custom property
   and every state below is one line. */
.cham-body, .cham-crest { fill: var(--cham); transition: fill .4s ease; }
.cham-tail, .cham-leg {
  fill: none; stroke: var(--cham); stroke-width: 9;
  stroke-linecap: round; stroke-linejoin: round;
  transition: stroke .4s ease;
}
.cham-eye { fill: #f6f8f9; }
.cham-pupil { fill: #10151a; }
.cham-branch { stroke: rgba(150, 110, 78, .55); stroke-width: 4; stroke-linecap: round; }

.cham.is-thinking { --cham: #8296a2; }
.cham.is-right    { --cham: #39c06b; }

/* A question mark rather than a sentence. Nobody here can read one. */
.cham-says {
  position: absolute; right: -6px; top: -4px;
  min-width: 26px; height: 26px; padding: 0 6px;
  display: flex; align-items: center; justify-content: center;
  font-size: 15px; font-weight: 700; color: var(--bg);
  background: #e7dcf5; border-radius: 13px;
}

@keyframes cham-shake {
  0%, 100% { transform: translateX(0); }
  25%      { transform: translateX(-5px); }
  75%      { transform: translateX(5px); }
}
/* On the svg, not the wrapper: the wrapper owns the transform that moves him
   from tile to tile, and animating the same property would drop him. */
.cham.is-shaking svg { animation: cham-shake .4s ease; }

@media (prefers-reduced-motion: reduce) {
  .cham { transition: none; }
  .cham.is-shaking svg { animation: none; }
}

/* A face, so the state reads as a mood and not only as a colour. Two mouths,
   swapped by display: the smile only exists once he has it right. */
.cham-mouth, .cham-smile {
  fill: none; stroke: rgba(10, 16, 22, .38);
  stroke-width: 2.4; stroke-linecap: round;
}
.cham-smile { display: none; }
.cham.is-right .cham-mouth { display: none; }
.cham.is-right .cham-smile { display: inline; }

/* The twig grounds the portrait on the start card. In the game he stands on
   a picture instead, and a stick floating across the tile looked like a
   scratch on the screen. */
.cham-branch { display: none; }
.teach-hero .cham-branch { display: inline; }

/* ── The path ──────────────────────────────────────────────────────────── */

.journey-main { display: flex; flex-direction: column; align-items: center; gap: 14px; }
.journey-note {
  margin: 0; min-height: 1.3em; text-align: center;
  font-size: 15px; font-weight: 600; color: var(--muted);
}

/* The drawn track and the stops are two layers over one box, so a stop's
   percentage position and the curve underneath it scale together. */
.journey-map {
  position: relative; width: 100%; max-width: 330px; aspect-ratio: 300 / 440;
}
.journey-line { position: absolute; inset: 0; width: 100%; height: 100%; }
.journey-track {
  fill: none; stroke: var(--line); stroke-width: 9;
  stroke-linecap: round; stroke-dasharray: 2 15;
}
/* The same path drawn shorter. Two coordinates would drift apart the first
   time anybody nudged the curve. */
.journey-walked {
  /* The same width as the dashed track, not narrower: a thinner line lets the
     dashes show past it on both sides and the road grows a halo. */
  fill: none; stroke: var(--live); stroke-width: 9; stroke-linecap: round;
  opacity: .62; transition: stroke-dashoffset .6s ease-out;
}

.journey-stops { position: absolute; inset: 0; }
.journey-stop {
  position: absolute; transform: translate(-50%, -50%);
  width: 52px; height: 52px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 22px; line-height: 1;
  background: var(--panel); border: 2px solid var(--line);
  cursor: pointer; transition: transform .15s, border-color .2s, background .2s;
}
.journey-stop:disabled { opacity: .4; cursor: default; }
.journey-stop.is-done {
  border-color: var(--live); background: rgba(34, 197, 94, .16);
  color: var(--live); font-size: 24px; font-weight: 700;
}
/* The one a child is meant to press. It is the only thing on the screen that
   moves, which is the whole instruction. */
.journey-stop.is-open {
  border-color: var(--live); background: var(--panel-2);
  animation: journey-pulse 1.8s ease-in-out infinite;
}
@keyframes journey-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, .38); }
  50%      { box-shadow: 0 0 0 10px rgba(34, 197, 94, 0); }
}

/* He walks the track rather than standing beside it, so he sits on the stop
   the learner is about to open, just above it. */
.journey-cham {
  position: absolute; width: 74px; height: 60px;
  margin-left: -37px; margin-top: -68px;
  transition: left .6s ease-in-out, top .6s ease-in-out;
  pointer-events: none;
}
.journey-cham svg { width: 100%; height: 100%; }

.journey-reset {
  font: inherit; font-size: 14px; color: var(--muted);
  background: none; border: none; text-decoration: underline; cursor: pointer;
}

@media (prefers-reduced-motion: reduce) {
  .journey-cham { transition: none; }
  .journey-stop.is-open { animation: none; }
  .journey-walked { transition: none; }
}

/* ── One learning stop ─────────────────────────────────────────────────── */

.stop-main { display: flex; flex-direction: column; align-items: center; gap: 16px; }

.stop-card {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  width: 100%; padding: 30px 20px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 20px;
}
.stop-image { font-size: 84px; line-height: 1; }
.stop-word { font-size: 30px; font-weight: 700; letter-spacing: -.01em; }
.stop-ipa { font-size: 14px; color: var(--muted); }
/* Their own language, once, here. Nothing after this stop translates. */
.stop-tr { margin-top: 6px; font-size: 19px; color: var(--text); }

/* ── The whole map ─────────────────────────────────────────────────────── */

.trail-main { display: flex; flex-direction: column; align-items: center; gap: 14px; }

/* The height comes from JS: it depends on how many lessons the word pool
   holds, and the curve inside is generated to match. */
.trail-map { position: relative; width: 100%; max-width: 330px; }

/* Numbers rather than icons, and smaller: a map of twelve wants to read as a
   route, and twelve big emoji read as a toolbar. */
.trail-map .journey-stop { width: 46px; height: 46px; font-size: 17px; font-weight: 700; }
.trail-map .journey-stop.is-done { font-size: 21px; }

/* ── Balloons ──────────────────────────────────────────────────────────── */

.party {
  position: fixed; inset: 0; z-index: 60;
  pointer-events: none; overflow: hidden;
}

.balloon {
  position: absolute; bottom: -100px;
  width: 34px; height: 44px;
  border-radius: 50% 50% 46% 46% / 46% 46% 54% 54%;
  background: var(--c);
  animation-name: balloon-rise;
  animation-timing-function: ease-in;
  animation-fill-mode: forwards;
}
/* A highlight and a string. Without them a balloon is a coloured egg. */
.balloon::before {
  content: ""; position: absolute; left: 21%; top: 17%;
  width: 8px; height: 11px; border-radius: 50%;
  background: rgba(255, 255, 255, .45);
}
.balloon::after {
  content: ""; position: absolute; left: 50%; top: 100%;
  width: 1px; height: 26px; background: rgba(255, 255, 255, .3);
}

@keyframes balloon-rise {
  0%   { transform: translate(0, 0) rotate(0deg); opacity: 0; }
  10%  { opacity: .92; }
  85%  { opacity: .92; }
  100% { transform: translate(var(--sway), -120vh) rotate(7deg); opacity: 0; }
}

/* No balloons at all rather than still ones: the whole thing is the motion,
   and party() returns before building any of them. */
@media (prefers-reduced-motion: reduce) {
  .party { display: none; }
}

/* ── The bonus stop ────────────────────────────────────────────────────── */

.bonus-main { display: flex; flex-direction: column; align-items: center; gap: 14px; }

/* A fixed patch of sky. Balloons are positioned against it, so it cannot be
   allowed to grow with its contents. */
.bonus-sky {
  position: relative; width: 100%; max-width: 420px;
  height: min(58vh, 460px); overflow: hidden;
  background: linear-gradient(180deg, transparent, rgba(59, 130, 246, .05));
  border-radius: 20px;
}

.balloon-pop {
  position: absolute; bottom: -110px;
  width: 74px; height: 92px; padding: 0;
  background: none; border: none; cursor: pointer;
  animation-name: balloon-fly;
  animation-timing-function: linear;
  animation-fill-mode: forwards;
}
/* The skin carries the colour and the shape, so it can burst while the button
   around it stays frozen wherever it had climbed to. */
.balloon-skin {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50% 50% 46% 46% / 46% 46% 54% 54%;
  background: var(--c);
}
.balloon-skin::before {
  content: ""; position: absolute; left: 19%; top: 15%;
  width: 15px; height: 20px; border-radius: 50%;
  background: rgba(255, 255, 255, .42);
}
.balloon-skin::after {
  content: ""; position: absolute; left: 50%; top: 100%;
  width: 1px; height: 30px; background: rgba(255, 255, 255, .32);
}
.balloon-face { font-size: 34px; line-height: 1; pointer-events: none; }

/* Frozen where it was hit, and the skin pops there. */
.balloon-pop.is-popped { animation-play-state: paused; }
.balloon-pop.is-popped .balloon-skin { animation: balloon-burst .34s ease-out forwards; }
.balloon-pop.is-wrong { --c: #64748b; }
/* The rest drift off once the round is won, rather than vanishing. */
.balloon-pop.is-drifting { animation-play-state: paused; opacity: .25; transition: opacity .4s; }

@keyframes balloon-fly {
  0%   { transform: translate(0, 0); }
  100% { transform: translate(var(--sway), calc(-58vh - 210px)); }
}
@keyframes balloon-burst {
  0%   { transform: scale(1); opacity: 1; }
  45%  { transform: scale(1.35); opacity: .85; }
  100% { transform: scale(.25); opacity: 0; }
}

/* A bonus is a different kind of stop and says so before it is tapped. */
.journey-stop.is-bonus { border-style: dashed; }
.journey-stop.is-bonus.is-open { border-style: solid; }

@media (prefers-reduced-motion: reduce) {
  .balloon-pop { animation-duration: 22s !important; }
}

/* Once he has committed, the others ask to be pressed. There is no sentence
   that would work here: the audience cannot read one. */
.teach-choices.is-asking .choice:not(:disabled) {
  animation: choice-invite 1.7s ease-in-out infinite;
}
@keyframes choice-invite {
  0%, 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
  50%      { box-shadow: 0 0 0 5px rgba(34, 197, 94, .13); }
}

/* The "?" pops rather than appearing, so the hesitation is something you can
   see happen instead of something that was suddenly already there. */
.cham-says { animation: says-pop .26s ease-out; transform-origin: 50% 100%; }
@keyframes says-pop {
  from { transform: scale(.2); opacity: 0; }
  to   { transform: scale(1); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .teach-choices.is-asking .choice { animation: none; }
  .cham-says { animation: none; }
}

/* ── The runner ────────────────────────────────────────────────────────── */

.run-main { display: flex; flex-direction: column; align-items: center; gap: 12px; }

/* What to collect, in the header where it stays put while everything else
   moves. A picture, never the word: the audience cannot read one. */
.run-target {
  font-size: 30px; line-height: 1;
  min-width: 40px; text-align: right;
}

.run-canvas {
  width: 100%; height: min(52vh, 380px);
  background: var(--panel); border: 1px solid var(--line); border-radius: 18px;
  /* The canvas is the button. Without this a tap scrolls the page instead. */
  touch-action: none; cursor: pointer;
}

/* A second way to jump, for anyone who does not work out that the sky is
   tappable. Wide, because it is pressed in a hurry. */
.run-jump { width: 100%; }

.run-after { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.run-board-link { margin-top: 2px; }

/* ── The tournament ────────────────────────────────────────────────────── */

.board-main { display: flex; flex-direction: column; gap: 12px; }

.board-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.board-row {
  display: grid; grid-template-columns: 32px 1fr auto auto; gap: 10px;
  align-items: center; padding: 11px 14px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
}
.board-row.is-you { border-color: var(--live); background: rgba(34, 197, 94, .1); }
.board-place { font-size: 17px; text-align: center; }
.board-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.board-runs { font-size: 13px; color: var(--muted); }
.board-score { font-size: 17px; font-variant-numeric: tabular-nums; }

/* Two ways out of the map that are not the next stop. */
.trail-extras { display: flex; gap: 10px; align-items: center; justify-content: center; }
.trail-extra {
  font: inherit; font-size: 14px; font-weight: 600; color: var(--muted);
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 999px; padding: 8px 15px; cursor: pointer; text-decoration: none;
}

/* ── Three lanes ───────────────────────────────────────────────────────── */

/* Two halves of the road are the real control; these are for anyone who does
   not discover that. Wide and far apart, because they are pressed in a hurry
   and a thumb landing between them costs a life. */
.lane-controls {
  display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
  width: 100%; max-width: 420px;
}
.lane-btn {
  font: inherit; font-size: 26px; line-height: 1; padding: 14px 0;
  color: var(--text); background: var(--panel-2);
  border: 1px solid var(--line); border-radius: 14px;
  cursor: pointer; touch-action: none; user-select: none;
}
.lane-btn:active { background: var(--panel); border-color: var(--live); }


/* ── Kiko, pleased with you ────────────────────────────────────────────── */

/* On the svg, never on the wrapper: the wrapper owns the transform that puts
   him where he is, and animating the same property would drop him on the
   floor. The same rule as his shake, learned the same way. */
.cham.is-happy svg { animation: kiko-hop 1.05s ease-in-out infinite; transform-origin: 50% 85%; }

/* The baseline sits low in the stage on purpose: he grows by a fifth and
   hops on top of that, and from a baseline of zero the top of him crossed
   into the header. */
@keyframes kiko-hop {
  0%, 100% { transform: translateY(20px) scale(1.22) rotate(0deg); }
  20%      { transform: translateY(4px) scale(1.28) rotate(-5deg); }
  45%      { transform: translateY(20px) scale(1.20) rotate(0deg); }
  65%      { transform: translateY(11px) scale(1.25) rotate(5deg); }
}

/* A chameleon celebrates by changing colour. It is the one thing this animal
   does that nothing else on the screen can, so it is what the ending is. */
@keyframes kiko-glad-fill {
  0%, 100% { fill: #39c06b; }
  25%      { fill: #4fd1c5; }
  50%      { fill: #f6c445; }
  75%      { fill: #7bd44f; }
}
@keyframes kiko-glad-stroke {
  0%, 100% { stroke: #39c06b; }
  25%      { stroke: #4fd1c5; }
  50%      { stroke: #f6c445; }
  75%      { stroke: #7bd44f; }
}
.cham.is-happy .cham-body,
.cham.is-happy .cham-crest { animation: kiko-glad-fill 1.9s ease-in-out infinite; }
.cham.is-happy .cham-tail,
.cham.is-happy .cham-leg { animation: kiko-glad-stroke 1.9s ease-in-out infinite; }

/* And he is smiling, which is the same mouth a right answer gets. */
.cham.is-happy .cham-mouth { display: none; }
.cham.is-happy .cham-smile { display: inline; }

.cham-sparks { position: absolute; inset: 0; pointer-events: none; }
.spark {
  position: absolute; left: 50%; top: 46%;
  font-size: 17px; line-height: 1; opacity: 0;
  animation: spark-out 1.5s ease-out infinite;
}
@keyframes spark-out {
  0%   { transform: translate(-50%, -50%) scale(.2); opacity: 0; }
  35%  { transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) scale(1); opacity: 1; }
  100% { transform: translate(calc(-50% + var(--dx) * 1.35), calc(-50% + var(--dy) * 1.35)) scale(.55); opacity: 0; }
}

/* Still and green rather than nothing: the sentence beside him says he is
   pleased, and an empty space where an animal was reads as a bug. */
@media (prefers-reduced-motion: reduce) {
  .cham.is-happy svg,
  .cham.is-happy .cham-body,
  .cham.is-happy .cham-crest,
  .cham.is-happy .cham-tail,
  .cham.is-happy .cham-leg { animation: none; }
  .spark { animation: none; opacity: .8; transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))); }
}
