/* Crystal Spell Caverns — kid-friendly, touch-first cavern theme.
   Design rules from UX.md: BIG targets (primary >= ~110px, nothing tappable
   < 64px), generous spacing, large rounded text, high contrast, and the
   praise.js tier colors reused everywhere for consistent meaning. */

/* Self-hosted Atkinson Hyperlegible — the UI/spelling typeface (latin subset, SIL OFL;
   see fonts/README.md). Purpose-built for letter DISTINCTION (I/l/1, 0/O, b/d/p/q), the
   property a spelling task needs most. font-display:swap so first paint never blocks; the
   system fallback below covers the (rare) offline-first-load gap. */
@font-face {
  font-family: 'Atkinson Hyperlegible';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/atkinson-hyperlegible-400.woff2') format('woff2');
}
@font-face {
  font-family: 'Atkinson Hyperlegible';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/fonts/atkinson-hyperlegible-700.woff2') format('woff2');
}

:root {
  --gold: #ffd23f; /* perfect  */
  --cyan: #36f1cd; /* amazing  */
  --emerald: #7ae582; /* great    */
  --amethyst: #9d8df1; /* good     */
  --slate: #6c7a89; /* gentle "try again" */

  --bg-0: #070a1c;
  --bg-1: #0e1430;
  --bg-2: #161d44;
  --panel: rgba(255, 255, 255, 0.06);
  --panel-border: rgba(150, 180, 255, 0.18);
  --ink: #eaf0ff;
  --ink-dim: #9fb0d8;
  --accent: #7aa2ff;

  --radius: 22px;
  --tap-min: 64px;

  font-size: 18px;
}

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

html,
body {
  margin: 0;
  height: 100%;
  overscroll-behavior: none;
  /* HARD GUARD against phantom horizontal scroll. Real Android/Samsung devices report
     occasional "I can pan the page a few px right / it looks oversize" even when no screen
     legitimately overflows — a transient fixed overlay (drag-ghost), a sub-pixel %/vw round,
     or the browser's own viewport handling can nudge the LAYOUT viewport wider than the
     VISUAL one. Nothing in this app should ever paint outside the viewport horizontally, so
     clip that axis at the root. `clip` (not `hidden`) avoids creating a scroll container and
     leaves the vertical axis `visible` untouched (the per-screen containers own vertical
     scroll). Supported in Chromium 90+ / Samsung Internet 15+. */
  overflow-x: clip;
  max-width: 100%;
}

body {
  /* Atkinson Hyperlegible leads (letter-distinct, self-hosted); the rounded display fonts
     stay as fallbacks. The big decorative .home-title re-asserts Baloo 2 for character. */
  font-family: "Atkinson Hyperlegible", "Quicksand", "Segoe UI Rounded", "Nunito", system-ui,
    -apple-system, sans-serif;
  color: var(--ink);
  background:
    radial-gradient(1200px 800px at 50% -10%, #21306e 0%, transparent 60%),
    radial-gradient(900px 700px at 90% 110%, #2a1b54 0%, transparent 55%),
    linear-gradient(160deg, var(--bg-1), var(--bg-0));
  background-attachment: fixed;
  -webkit-user-select: none;
  user-select: none;
  touch-action: manipulation;
}

#app {
  height: 100dvh;
  display: flex;
  flex-direction: column;
  padding: max(14px, env(safe-area-inset-top)) max(14px, env(safe-area-inset-right))
    max(14px, env(safe-area-inset-bottom)) max(14px, env(safe-area-inset-left));
  /* Second line of defence (with html,body above): the app shell never scrolls sideways. */
  overflow-x: clip;
}

/* ------- screen container + entry animation ------- */
.screen {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  animation: screen-in 0.32s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
@keyframes screen-in {
  from {
    opacity: 0;
    transform: translateY(14px) scale(0.99);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* ------- header ------- */
.app-header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 6px 4px 14px;
}
.header-title {
  font-size: 1.3rem;
  font-weight: 700;
  flex: 1;
  /* let the title shrink (flexbox defaults to min-width:auto = won't shrink below its
     text) so the gem/depth chips can never be pushed off the right edge on a phone. */
  min-width: 0;
  /* clip — NOT hidden. A long truncated title (e.g. on the catalog) with `overflow:hidden`
     + nowrap becomes its own horizontal SCROLL container: on a narrow phone its clipped text
     is ~55-95px wider than the box, so the title element itself can be PANNED right by a finger
     (root overflow-x:clip can't reach an inner overflow:hidden box). `overflow:clip` keeps the
     ellipsis but creates no scroll container, so it can't pan. (Caught by qa_galaxy.mjs on real
     Galaxy S8/S9+/S24 descriptors — the user's "I can scroll a bit right / oversize" symptom.) */
  overflow: clip;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.header-stats {
  margin-left: auto;
  display: flex;
  gap: 10px;
}
.stat {
  display: flex;
  align-items: center;
  gap: 7px;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: 999px;
  padding: 8px 16px;
  font-weight: 700;
  font-size: 1.1rem;
  white-space: nowrap;
}
.stat .icon {
  font-size: 1.2rem;
}
/* the cavern-depth chip carries the miner's chosen crystal colour (personalization) */
.stat.depth {
  border-color: color-mix(in srgb, var(--accent) 65%, var(--panel-border));
  box-shadow: 0 0 14px -8px var(--accent);
}
.gem-count.bump {
  animation: pop 0.4s ease;
}
@keyframes pop {
  30% {
    transform: scale(1.5);
    color: var(--gold);
  }
}

/* ------- buttons ------- */
button {
  font-family: inherit;
  color: var(--ink);
  cursor: pointer;
  border: none;
  background: none;
}
.btn-icon.back {
  width: var(--tap-min);
  height: var(--tap-min);
  border-radius: 50%;
  /* center the chevron glyph exactly, not just by line-height (which left it
     sitting a hair high/left in the circle) — shared centering token (§17.B) */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 2.4rem;
  line-height: 1;
  background: var(--panel);
  border: 1px solid var(--panel-border);
}
.btn-icon.back:active {
  transform: scale(0.92);
}

/* SHARED button token (§17.B app-store-consistency pass): every pill/box button
   centers its content the SAME way — flex centering instead of relying on the
   browser's default button baseline (which leaves emoji+text and wrapped labels
   misaligned, and differs across fonts/platforms — the iPad uses Apple emoji + a
   fallback font). `gap` evenly spaces an icon from its text where they're separate
   nodes; line-height keeps a single label vertically centered in the fixed height. */
.btn {
  min-height: var(--tap-min);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45em;
  border-radius: var(--radius);
  padding: 16px 26px;
  font-size: 1.2rem;
  font-weight: 700;
  line-height: 1.15;
  text-align: center;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  transition: transform 0.1s ease;
}
.btn:active {
  transform: scale(0.96);
}
.btn.primary {
  background: linear-gradient(150deg, #5f7bff, #8a5cff);
  border-color: transparent;
  box-shadow: 0 10px 30px -8px #5f7bff88;
}
.btn[disabled] {
  opacity: 0.4;
  pointer-events: none;
}

/* ------- home ------- */
.home {
  /* scroll if the menu (up to 7 cards + streak strip) is taller than the viewport,
     instead of clipping the bottom cards (regression when the Repair card appears) */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
.home-hero {
  text-align: center;
  padding: 12px 0 4px;
}
.home-title {
  /* the one big decorative title keeps the rounded display font for character */
  font-family: "Baloo 2", "Quicksand", system-ui, sans-serif;
  font-size: clamp(2rem, 7vw, 3.2rem);
  font-weight: 800;
  margin: 0;
  background: linear-gradient(120deg, var(--cyan), var(--amethyst), var(--gold));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  letter-spacing: 0.5px;
}
.home-sub {
  color: var(--ink-dim);
  font-size: 1.15rem;
  margin-top: 8px;
}

/* daily streak ("glowing vein") + tiny daily gem goal */
.home-streak {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  margin-top: 16px;
}
.streak-row {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
}
.streak-chip {
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: 999px;
  padding: 6px 16px;
  font-weight: 800;
  font-size: 1.05rem;
  color: var(--ink-dim);
}
.streak-chip.lit {
  color: var(--gold);
  border-color: rgba(255, 210, 63, 0.45);
  box-shadow: 0 0 18px -4px var(--gold);
}
.streak-chip.lantern {
  color: var(--cyan);
}
.goal {
  width: min(420px, 92%);
}
.goal-bar {
  height: 10px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
}
.goal-fill {
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--emerald), var(--cyan), var(--gold));
  box-shadow: 0 0 12px -2px var(--cyan), inset 0 1px 0 rgba(255, 255, 255, 0.55);
  transition: width 0.4s ease;
}
.goal-bar {
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5);
}
.goal-label {
  text-align: center;
  margin-top: 5px;
  font-size: 0.95rem;
  color: var(--ink-dim);
  font-weight: 700;
}
.quest-chip {
  font-family: inherit;
  cursor: pointer;
}
.quest-chip.lit {
  color: var(--gold);
  border-color: rgba(255, 210, 63, 0.5);
  box-shadow: 0 0 18px -4px var(--gold);
  animation: pop 0.6s ease;
}

/* daily quest rows (Progress screen) */
.quest {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 0;
}
.quest + .quest {
  border-top: 1px solid var(--panel-border);
}
/* a quest row is a button that launches its activity (tap to "work on it") */
button.quest {
  width: 100%;
  text-align: left;
  font: inherit;
  color: inherit;
}
.quest-link {
  cursor: pointer;
  transition: transform 0.1s ease;
}
.quest-link:active {
  transform: scale(0.98);
}
.quest-link:not(.done) .quest-count {
  color: var(--accent);
  font-size: 1.4rem;
}
.quest-ic {
  font-size: 1.6rem;
  width: 1.8rem;
  text-align: center;
}
.quest-body {
  flex: 1;
  min-width: 0;
}
.quest-text {
  font-weight: 700;
  font-size: 1.05rem;
  margin-bottom: 6px;
}
.quest.done .quest-text {
  color: var(--emerald);
}
.quest-bar {
  height: 8px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
}
.quest-fill {
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--emerald), var(--cyan));
  transition: width 0.4s ease;
}
.quest-count {
  font-weight: 800;
  color: var(--ink-dim);
  font-size: 0.95rem;
  white-space: nowrap;
}
.quest-note {
  color: var(--ink-dim);
  text-align: center;
  margin: 10px 0 0;
  font-weight: 700;
}

/* cavern map — a visual depth path ("you are here" + next-level goal) */
.cavern-strip {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.depth-node {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}
.depth-dot {
  width: 46px;
  height: 46px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.35rem;
  background: var(--panel);
  border: 2px solid var(--panel-border);
}
.depth-node.done .depth-dot {
  background: linear-gradient(160deg, #1f6f4a, #2bb673);
  border-color: var(--emerald);
}
.depth-node.current .depth-dot {
  background: linear-gradient(150deg, #5f7bff, #8a5cff);
  border-color: var(--gold);
  box-shadow: 0 0 18px -2px var(--gold);
  transform: scale(1.12);
}
.depth-node.locked {
  opacity: 0.5;
}
.depth-cap {
  font-size: 0.8rem;
  font-weight: 800;
  color: var(--ink-dim);
}
.depth-node.current .depth-cap {
  color: var(--gold);
}
.depth-link {
  flex: 1;
  height: 4px;
  margin: 0 4px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.12);
  align-self: flex-start;
  margin-top: 21px;
}
.depth-link.lit {
  background: var(--emerald);
}
.home-grid {
  display: grid;
  /* minmax(0,1fr) — NOT 1fr: a bare 1fr column has min-width:auto, so a long unbreakable label
     (e.g. a level card's "information") sets a min-content floor that pushes the GRID wider than
     its box, making the scroll parent x-pannable. minmax(0,1fr) lets columns shrink so the text
     clips (cards are overflow:clip) instead of expanding the grid. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
  padding-bottom: 12px;
}
.menu-card {
  position: relative;
  min-height: 118px;
  border-radius: 28px;
  padding: 18px 20px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 6px;
  text-align: left;
  /* The utility cards aren't flat slate — a soft top-lit gradient + inner highlight +
     drop shadow give them depth so the whole hub reads as one game world, not a settings
     list (§A polish). The hero/craft/lab/etc. variants layer their own gradient on top. */
  background: linear-gradient(160deg, #222c54, #1a2240);
  border: 1px solid var(--panel-border);
  box-shadow: 0 10px 26px -16px rgba(0, 0, 0, 0.9), inset 0 1px 0 rgba(255, 255, 255, 0.07);
  transition: transform 0.12s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.menu-card .ic {
  filter: drop-shadow(0 3px 8px rgba(0, 0, 0, 0.35));
}
.menu-card:active {
  transform: scale(0.96);
}
.menu-card .ic {
  font-size: 2.6rem;
}
.menu-card .lbl {
  font-size: 1.5rem;
  font-weight: 800;
}
.menu-card .desc {
  color: var(--ink-dim);
  font-size: 1rem;
}
/* On the saturated gradient cards (Play/Craft/Lab) the dim slate desc is
   near-invisible — use a bright near-white instead (QA I3). */
.menu-card.play .desc,
.menu-card.craft .desc,
.menu-card.lab .desc,
.menu-card.repair .desc {
  color: rgba(255, 255, 255, 0.86);
}
/* Craft hero subtitle: solid white + a soft dark shadow scrim so it stays crisp over the
   (now AA-passing) magenta gradient regardless of where the gradient lands behind it. */
.menu-card.craft.hero .desc {
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.38);
}
/* CRAFT is the hero (§B): the premium, full-width headline card with the richest
   gradient + glow. A small badge calls out that it pays the best gems. */
.menu-card.craft.hero {
  position: relative;
  grid-column: 1 / -1;
  min-height: 148px;
  background:
    radial-gradient(120% 140% at 85% 0%, #36f1cd2e, transparent 60%),
    /* pink/magenta end deepened ~12% so white .desc clears WCAG AA (the 55%-stop
       magenta the subtitle sits over now measures ≈5:1 vs the old ~3.95:1) */
    linear-gradient(150deg, #7350e8, #9a3fe0 55%, #d44fbe);
  border-color: transparent;
  box-shadow: 0 18px 46px -12px #9a5cffbb, inset 0 1px 0 rgba(255, 255, 255, 0.22);
}
.menu-card.craft.hero .ic {
  font-size: 3rem;
  filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.3));
}
.menu-card.craft.hero .lbl {
  font-size: 1.85rem;
}
.menu-card .badge {
  position: absolute;
  top: 12px;
  right: 14px;
  font-size: 0.82rem;
  font-weight: 800;
  letter-spacing: 0.3px;
  color: #1a1330;
  background: linear-gradient(120deg, #ffe27a, #ffd23f);
  border-radius: 999px;
  padding: 4px 11px;
  box-shadow: 0 4px 14px -4px rgba(255, 210, 63, 0.7);
}
/* Mining is reframed as PRACTICE (§B): a calmer, shorter secondary banner — still
   inviting, clearly subordinate to the Craft hero. */
.menu-card.play.practice {
  grid-column: 1 / -1;
  min-height: 96px;
  flex-direction: row;
  align-items: center;
  gap: 16px;
  background: linear-gradient(150deg, #2a3570, #20285a);
  border-color: #5f7bff55;
}
.menu-card.play.practice .ic {
  font-size: 2.3rem;
}
.menu-card.play.practice .lbl {
  font-size: 1.4rem;
}
.menu-card.lab {
  background: linear-gradient(150deg, #6a3ca855, #20285a);
  border-color: #9d8df166;
}
/* §30 Mastery (draw) — the headline test; a warm gold accent to read as an achievement. */
.menu-card.mastery {
  background: linear-gradient(150deg, #8a6a1f55, #2a2350);
  border-color: #ffd23f66;
}
/* §31.C: when the recommender says "go master your known words", pulse the Mastery card (and
   any reward-screen "Master them!" CTA) with a gentle gold glow to pull the eye there. */
.menu-card.mastery.nudge {
  border-color: var(--gold, #ffd23f);
  animation: master-nudge 1.8s ease-in-out infinite;
}
.btn.primary.nudge {
  animation: master-nudge 1.8s ease-in-out infinite;
}
@keyframes master-nudge {
  0%, 100% { box-shadow: 0 8px 22px -10px #000a, 0 0 0 0 rgba(255, 210, 63, 0); }
  50% { box-shadow: 0 8px 22px -10px #000a, 0 0 22px 2px rgba(255, 210, 63, 0.6); }
}
/* §30 a not-yet-unlocked mode (e.g. Practice/mining before [set size] mastered): dimmed so
   it reads as "coming soon", but still tappable so the kid can see how to unlock it. */
.menu-card.locked {
  opacity: 0.6;
  filter: saturate(0.6);
}
/* Repair is a full-width amber call-to-action banner when there are cracked words
   (keeps the home grid balanced + gives "fix your misses" real prominence). */
.menu-card.repair {
  grid-column: 1 / -1;
  min-height: 92px;
  flex-direction: row;
  align-items: center;
  gap: 16px;
  background: linear-gradient(150deg, #b5791f55, #20285a);
  border-color: #ffd23f88;
}
.menu-card.repair .ic {
  font-size: 2.2rem;
}
/* Feedback is a normal half-width utility card paired with Settings. */
.menu-card.feedback .ic {
  font-size: 2.4rem;
}
.menu-card.soon {
  opacity: 0.7;
}

/* ------- rhythm mode ------- */
.rhythm {
  gap: 10px;
}

/* Shared play layout (rhythm + puzzle): split the area below the header into an
   upper PROMPT zone (the dictated word / sentence + verdict, vertically centered)
   and a lower ANSWER zone (tiles / slots+tray, vertically centered). Each half
   centers its own content, so nothing is jammed against an edge and the
   whitespace reads as deliberate framing — not a bottom-heavy void (QA I1). */
.play-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* §33 fit-to-viewport: a single multiplier the mode JS shrinks (fitPlayArea in ui.js) so the
     word being filled, the interaction surface, AND the action buttons stay co-visible for ANY
     word length on ANY phone. Defaults to 1 → iPad/tablet render is unchanged (it only bites when
     the content would otherwise overflow + push the tray/controls below the fold). */
  --play-scale: 1;
  gap: clamp(26px, 6vh, 56px);
  /* `safe center` centers the prompt+answer pair on tall screens but, when space is
     tight, aligns to the START edge so content packs from the top and scrolls — instead
     of the margin-top:auto flexbox bug that CLIPPED the top of the prompt (the "Hear it
     again" button) unreachably on short phones (QA §A regression fix). */
  justify-content: safe center;
  overflow-y: auto;
  padding: 12px 0 max(10px, env(safe-area-inset-bottom));
}
.answer-zone {
  flex: 0 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 14px;
  min-height: 0;
}
.dots {
  display: flex;
  gap: 7px;
  justify-content: center;
  margin: 2px 0 6px;
}
.dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.18);
}
.dot.done {
  background: var(--cyan);
}
.dot.current {
  background: var(--gold);
  box-shadow: 0 0 12px var(--gold);
}

.combo-wrap {
  height: 14px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
  margin: 2px 6px 6px;
}
.combo-fill {
  height: 100%;
  width: 0%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--emerald), var(--cyan), var(--gold));
  transition: width 0.3s ease;
}
.combo-label {
  text-align: center;
  font-weight: 800;
  color: var(--cyan);
  min-height: 1.4rem;
  font-size: 1.1rem;
}

.prompt {
  flex: 0 1 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(10px, 2vh, 18px);
  min-height: 0;
}
.hear-again {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-size: 1.25rem;
  font-weight: 700;
  padding: 14px 26px;
  border-radius: 999px;
  background: var(--panel);
  border: 1px solid var(--panel-border);
}
.hear-again .spk {
  font-size: 1.6rem;
}
/* the dictation controls row (Hear it again + Sound it out) */
.hear-row {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
}
/* "Sound it out" is a secondary aid — a touch smaller/quieter than Hear it again */
.hear-again.sound-out {
  font-size: 1.05rem;
  padding: 12px 20px;
  color: var(--ink-dim);
}
.hear-again.sound-out .spk {
  font-size: 1.35rem;
}
.sentence {
  font-size: clamp(1.3rem, 4.4vw, 2rem);
  line-height: 1.5;
  text-align: center;
  max-width: 22ch;
  color: var(--ink);
}
.sentence .blank {
  color: var(--gold);
  font-weight: 800;
  letter-spacing: 2px;
}

.verdict {
  min-height: 3rem;
  font-size: clamp(2rem, 8vw, 3.4rem);
  font-weight: 900;
  text-align: center;
  opacity: 0;
  letter-spacing: 1px;
}
.verdict.flash {
  animation: verdict-pop 1.1s ease both;
}
@keyframes verdict-pop {
  0% {
    opacity: 0;
    transform: scale(0.6);
  }
  18% {
    opacity: 1;
    transform: scale(1.15);
  }
  80% {
    opacity: 1;
    transform: scale(1);
  }
  100% {
    opacity: 0.85;
    transform: scale(1);
  }
}
.verdict-chip {
  text-align: center;
  font-weight: 800;
  font-size: 1.15rem;
  min-height: 1.3rem;
  letter-spacing: 0.5px;
}

/* live "gems if you answer now" meter — depletes to push speed */
.speedmeter {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 0 6px 10px;
}
.speed-pot {
  font-size: 1.6rem;
  font-weight: 900;
  min-width: 5.5ch;
  text-align: right;
  color: var(--gold);
}
.speed-bar {
  flex: 1;
  height: 18px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
}
.speed-fill {
  height: 100%;
  width: 100%;
  border-radius: 999px;
  background: var(--gold);
  transition: width 0.08s linear;
}
/* armed = word being spoken / read-grace: full bar, gently pulsing, not counting */
.speedmeter.armed .speed-fill {
  animation: armed-pulse 1.1s ease-in-out infinite;
}
.speedmeter.armed .speed-pot {
  opacity: 0.85;
}
@keyframes armed-pulse {
  50% {
    opacity: 0.45;
  }
}

.tiles {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: calc(16px * var(--play-scale, 1));
  padding-top: 6px;
}
.tile {
  /* scale with viewport height so two rows of tiles still fit (and stay off the
     bottom edge) on short screens — landscape / pre-install Safari chrome (QA I1).
     The --play-scale multiplier lets fitPlayArea shrink them further when needed. */
  min-height: calc(clamp(74px, 11vh, 116px) * var(--play-scale, 1));
  /* center the spelling option in the tile (a wrapped long word stays centered as a
     block, not top-aligned) — shared centering token (§17.B) */
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  border-radius: var(--radius);
  /* smaller max + wrap-anywhere so a long word (e.g. "communications", 14ch) shrinks
     and breaks instead of clipping at the tile edge — there's no space to wrap on */
  font-size: calc(clamp(1.15rem, 5vw, 2.05rem) * var(--play-scale, 1));
  line-height: 1.1;
  padding: calc(8px * var(--play-scale, 1)) 12px;
  overflow-wrap: anywhere;
  font-weight: 800;
  letter-spacing: 0.5px;
  background: linear-gradient(160deg, #243164, #1b2350);
  border: 2px solid var(--panel-border);
  box-shadow: 0 8px 22px -10px #000a;
  transition: transform 0.1s ease, border-color 0.15s ease, background 0.15s ease;
}
.tile:active {
  transform: scale(0.95);
}
.tile.correct {
  background: linear-gradient(160deg, #1f6f4a, #2bb673);
  border-color: var(--emerald);
}
.tile.wrong {
  background: linear-gradient(160deg, #3a3550, #2c2940);
  border-color: var(--slate);
  opacity: 0.85;
}
.tile.reveal {
  border-color: var(--gold);
  box-shadow: 0 0 0 3px var(--gold) inset, 0 0 24px var(--gold);
}
/* After an answer, fade the OTHER (wrong-spelling) tiles so the last thing on
   screen is the CORRECT spelling — seeing misspellings linger imprints them
   (Roediger & Marsh 2005), and our learner is a weak speller. */
.tile.dim-out {
  opacity: 0.1;
  filter: blur(1.5px);
  transition: opacity 0.3s ease 0.15s, filter 0.3s ease 0.15s;
}
.tiles.locked .tile {
  pointer-events: none;
}

/* ------- puzzle (build-the-word) mode ------- */
.puzzle {
  gap: 10px;
}
.slots {
  display: flex;
  flex-wrap: wrap;
  gap: calc(10px * var(--play-scale, 1));
  justify-content: center;
  padding: 4px 6px;
}
.slot {
  width: calc(clamp(44px, 13vw, 72px) * var(--play-scale, 1));
  height: calc(clamp(50px, min(15vw, 12vh), 84px) * var(--play-scale, 1));
  border-radius: 16px;
  font-size: calc(clamp(1.5rem, 6vw, 2.4rem) * var(--play-scale, 1));
  font-weight: 800;
  text-transform: lowercase;
  /* Empty slots read as glowing CRYSTAL SOCKETS waiting to be filled — an inset well
     with a faint cyan inner glow + a soft dashed rim — not a flat web-form input. */
  background: radial-gradient(circle at 50% 32%, rgba(54, 241, 205, 0.12), rgba(0, 0, 0, 0.32) 72%);
  border: 2px dashed rgba(124, 196, 255, 0.4);
  box-shadow: inset 0 3px 12px rgba(0, 0, 0, 0.5), inset 0 0 14px rgba(54, 241, 205, 0.07);
  color: var(--ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  animation: socket-glow 2.6s ease-in-out infinite;
  transition: transform 0.1s ease, border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
}
/* a slow, gentle breathing rim so the empty sockets feel alive (not a dead form field) */
@keyframes socket-glow {
  0%, 100% { border-color: rgba(124, 196, 255, 0.32); box-shadow: inset 0 3px 12px rgba(0, 0, 0, 0.5), inset 0 0 12px rgba(54, 241, 205, 0.05); }
  50% { border-color: rgba(54, 241, 205, 0.55); box-shadow: inset 0 3px 12px rgba(0, 0, 0, 0.5), inset 0 0 20px rgba(54, 241, 205, 0.16); }
}
.slot.filled {
  background: linear-gradient(160deg, #2a3a78, #1b2350);
  border-style: solid;
  border-color: var(--accent);
  box-shadow: 0 6px 18px -8px var(--accent), inset 0 1px 0 rgba(255, 255, 255, 0.18);
  animation: none;
}
.slot.filled:active {
  transform: scale(0.94);
}
.slot.locked {
  background: linear-gradient(160deg, #1f6f4a, #2bb673);
  border-color: var(--emerald);
  border-style: solid;
  box-shadow: 0 6px 18px -8px var(--emerald), inset 0 1px 0 rgba(255, 255, 255, 0.2);
  animation: none;
}
.slots.shake {
  animation: shake 0.4s ease;
}
@keyframes shake {
  10%, 90% { transform: translateX(-3px); }
  30%, 70% { transform: translateX(6px); }
  50% { transform: translateX(-8px); }
}

.tray {
  display: flex;
  flex-wrap: wrap;
  gap: calc(12px * var(--play-scale, 1));
  justify-content: center;
  padding: 10px 6px 4px;
}
.tray-tile {
  width: calc(clamp(48px, min(14vw, 11vh), 76px) * var(--play-scale, 1));
  height: calc(clamp(48px, min(14vw, 11vh), 76px) * var(--play-scale, 1));
  /* center the letter in the square (matches .slot, which already centers) */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 16px;
  font-size: calc(clamp(1.5rem, 6vw, 2.4rem) * var(--play-scale, 1));
  font-weight: 800;
  line-height: 1;
  text-transform: lowercase;
  background: linear-gradient(160deg, #2c3a72, #20285a);
  border: 2px solid var(--panel-border);
  box-shadow: 0 8px 22px -10px #000a;
  touch-action: none; /* let pointer-drag own the gesture */
  transition: transform 0.1s ease, opacity 0.15s ease;
}
.tray-tile:active {
  transform: scale(0.92);
}
.tray-tile.used {
  visibility: hidden;
  pointer-events: none;
}
.tray-tile.drag-ghost {
  position: fixed;
  z-index: 60;
  transform: translate(-50%, -50%) scale(1.1);
  pointer-events: none;
  box-shadow: 0 12px 30px -8px #000c;
  border-color: var(--cyan);
}
.puzzle-controls {
  display: flex;
  gap: 12px;
  justify-content: center;
  padding: 2px 6px;
}
.btn.ghost {
  min-height: 52px;
  padding: 10px 22px;
  font-size: 1.05rem;
  opacity: 0.92;
}
/* §30: the hint button glows once the learner has been stuck a few seconds with no
   correct letter (highlight @4s; auto-fires @8s). Cleared the moment a correct letter
   lands. A steady, calm glow — not an alarm. */
.btn.ghost.hint-ready {
  opacity: 1;
  color: var(--gold, #ffd23f);
  border-color: var(--gold, #ffd23f);
  box-shadow: 0 0 0 2px var(--gold, #ffd23f) inset, 0 0 16px rgba(255, 210, 63, 0.55);
  animation: hint-glow 1.4s ease-in-out infinite;
}
@keyframes hint-glow {
  50% {
    box-shadow: 0 0 0 2px var(--gold, #ffd23f) inset, 0 0 26px rgba(255, 210, 63, 0.85);
  }
}

/* ------- §30 mastery (draw the letters) ------- */
.mastery .draw-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  width: 100%;
}
.draw-canvas {
  width: min(100%, 360px);
  /* height tracks the viewport but is capped both ways: generous on iPad, yet short enough
     on a small phone that "Read my letter" stays above the fold (no scroll to submit). The
     --play-scale multiplier lets fitPlayArea shrink it further for a long word on a short phone. */
  height: calc(clamp(132px, 24vh, 230px) * var(--play-scale, 1));
  border-radius: 18px;
  background: radial-gradient(circle at 50% 35%, rgba(54, 241, 205, 0.08), rgba(0, 0, 0, 0.34) 72%);
  border: 2px dashed rgba(124, 196, 255, 0.45);
  box-shadow: inset 0 3px 14px rgba(0, 0, 0, 0.5), inset 0 0 18px rgba(54, 241, 205, 0.08);
  touch-action: none; /* drawing must not pan/scroll the page */
  cursor: crosshair;
}
.draw-slots .slot.current {
  border-color: var(--gold, #ffd23f);
  border-style: solid;
  box-shadow: inset 0 3px 12px rgba(0, 0, 0, 0.5), 0 0 14px rgba(255, 210, 63, 0.45);
  animation: none;
}
.draw-candidates {
  min-height: 8px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}
.cand-label {
  color: var(--ink-dim);
  font-size: 0.95rem;
}
.cand-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
}
.cand-letter {
  width: calc(clamp(48px, 14vw, 72px) * var(--play-scale, 1));
  height: calc(clamp(48px, 14vw, 72px) * var(--play-scale, 1));
  border-radius: 14px;
  font-size: calc(clamp(1.6rem, 7vw, 2.4rem) * var(--play-scale, 1));
  font-weight: 800;
  text-transform: lowercase;
  color: var(--ink);
  background: linear-gradient(160deg, #2a3a78, #1b2350);
  border: 2px solid var(--accent, #7cc4ff);
  box-shadow: 0 6px 18px -8px var(--accent, #7cc4ff);
}
.cand-letter:active {
  transform: scale(0.94);
}
.draw-hint {
  color: var(--ink-dim);
  font-size: 0.92rem;
  text-align: center;
}
.draw-controls {
  display: flex;
  gap: 12px;
  justify-content: center;
  flex-wrap: wrap;
}
/* §11 (2026-06-20): the type fallback is an APP-DRAWN A–Z keypad — NOT a native <input>, whose OS
   keyboard would show a word-SUGGESTION strip that gives away the spelling (and can't be reliably
   disabled). No OS keyboard here → no suggestions, on any device, offline. Keys reuse the crystal-gem
   look of the draw candidates. All three rows span the container width (10 / 9 / 8 keys) so it reads
   as a tidy block; dimensions scale with --play-scale so a long word on a short phone still fits. */
.type-keyboard {
  width: 100%;
  max-width: 540px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: calc(7px * var(--play-scale, 1));
}
.key-row {
  display: flex;
  justify-content: center;
  gap: calc(6px * var(--play-scale, 1));
}
.key {
  flex: 1 1 0;
  min-width: 0;
  max-width: calc(52px * var(--play-scale, 1));
  height: calc(clamp(40px, 8vw, 56px) * var(--play-scale, 1));
  border-radius: 12px;
  font-size: calc(clamp(1.1rem, 4.6vw, 1.7rem) * var(--play-scale, 1));
  font-weight: 800;
  text-transform: lowercase;
  color: var(--ink);
  background: linear-gradient(160deg, #2a3a78, #1b2350);
  border: 2px solid var(--accent, #7cc4ff);
  box-shadow: 0 5px 14px -8px var(--accent, #7cc4ff);
}
.key:active {
  transform: scale(0.92);
  background: linear-gradient(160deg, #34469a, #232c66);
}
.key-back {
  max-width: calc(74px * var(--play-scale, 1));
  border-color: var(--gold, #ffd23f);
  color: var(--gold, #ffd23f);
}

/* ------- §31.A whole-word writing on WIDE screens: a row of per-letter boxes + ONE ink overlay -------
   The boxes are GUIDES; a single .boxes-ink canvas sits over the whole row so a kid can write
   slightly outside a box and still be captured, and each stroke is routed to the nearest box. */
.draw-boxes {
  position: relative;
  width: 100%;
  /* generous padding gives drawing room ABOVE/BELOW/beside the boxes — the overlay covers it,
     so "mostly in the box, a bit outside" is still captured (Ian 2026-06-19g). Scales with
     --play-scale so a short landscape phone reclaims it rather than scrolling the controls off. */
  padding: calc(18px * var(--play-scale, 1)) 14px;
}
.box-guides {
  display: flex;
  flex-wrap: nowrap; /* always one row → simple left-to-right box columns, never wraps/overflows */
  gap: 10px;
  justify-content: center;
}
.lbox {
  position: relative;
  flex: 1 1 0; /* equal columns that shrink to fit a long word on one line */
  min-width: 0;
  max-width: 96px;
  height: calc(clamp(60px, 11vw, 120px) * var(--play-scale, 1));
  border-radius: 16px;
  /* same glowing crystal-socket look as the empty .slot, so a box reads as "write here" */
  background: radial-gradient(circle at 50% 32%, rgba(54, 241, 205, 0.12), rgba(0, 0, 0, 0.32) 72%);
  border: 2px dashed rgba(124, 196, 255, 0.4);
  box-shadow: inset 0 3px 12px rgba(0, 0, 0, 0.5), inset 0 0 14px rgba(54, 241, 205, 0.07);
  transition: transform 0.1s ease, border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
}
.boxes-ink {
  position: absolute;
  inset: 0; /* covers the guides AND the padding around them */
  width: 100%;
  height: 100%;
  touch-action: none; /* drawing must not pan/scroll the page */
  cursor: crosshair;
}
.draw-boxes.display-only .boxes-ink {
  pointer-events: none; /* while TYPING the boxes are a display, not a draw surface */
}
.lbox-letter {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: clamp(1.7rem, 5vw, 2.6rem);
  font-weight: 800;
  text-transform: lowercase;
  color: var(--ink);
  pointer-events: none;
}
.lbox.current {
  border-color: var(--gold, #ffd23f);
  border-style: solid;
  box-shadow: inset 0 3px 12px rgba(0, 0, 0, 0.5), 0 0 14px rgba(255, 210, 63, 0.4);
}
.lbox.filled {
  background: linear-gradient(160deg, #2a3a78, #1b2350);
  border-style: solid;
  border-color: var(--accent, #7cc4ff);
  box-shadow: 0 6px 18px -8px var(--accent, #7cc4ff), inset 0 1px 0 rgba(255, 255, 255, 0.18);
}
.lbox.locked {
  background: linear-gradient(160deg, #1f6f4a, #2bb673);
  border-color: var(--emerald, #2bb673);
  border-style: solid;
  box-shadow: 0 6px 18px -8px var(--emerald, #2bb673), inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.draw-boxes.shake {
  animation: shake 0.4s ease;
}
/* §31 explicit submit (wide multi-box): grade only when the learner taps Check, so a misread
   auto-filled letter can be corrected first. Dim it until every box is filled. */
.draw-submit {
  display: flex;
  justify-content: center;
  padding: 2px 0 4px;
}
.check-btn[disabled] {
  opacity: 0.45;
  pointer-events: none;
}
.check-btn.ready {
  box-shadow: 0 8px 22px -10px #000a, 0 0 16px rgba(54, 241, 205, 0.5);
}

/* ------- §31.B dictation: peek button + active-toggle styling ------- */
.peek-row {
  display: flex;
  justify-content: center;
  padding: 2px 0 4px;
}
.peek-btn {
  min-height: 44px;
  padding: 8px 18px;
  font-size: 1rem;
}
.btn.ghost.on {
  color: var(--gold, #ffd23f);
  border-color: var(--gold, #ffd23f);
  box-shadow: 0 0 0 2px var(--gold, #ffd23f) inset, 0 0 14px rgba(255, 210, 63, 0.45);
}

/* ------- §32 voice spelling: the "listening" indicator ------- */
.mic-indicator {
  align-self: center;
  padding: 10px 18px;
  border-radius: 999px;
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--ink);
  background: linear-gradient(160deg, #2a3a78, #1b2350);
  border: 2px solid var(--accent, #7cc4ff);
}
.mic-indicator.listening {
  border-color: var(--emerald, #2bb673);
  animation: mic-pulse 1.3s ease-in-out infinite;
}
@keyframes mic-pulse {
  50% { box-shadow: 0 0 0 3px rgba(43, 182, 115, 0.35), 0 0 18px rgba(43, 182, 115, 0.55); }
}
/* live "heard:" readout — shows the recogniser is working + what it understood (also aids tuning). */
.voice-heard {
  align-self: center;
  min-height: 20px;
  margin-top: 4px;
  font-size: 0.95rem;
  color: var(--ink-dim);
  text-align: center;
  max-width: 90%;
}

/* ------- §32 parental gate (grown-up math challenge before the mic) ------- */
.gate-overlay {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(6, 10, 28, 0.72);
  backdrop-filter: blur(3px);
}
.gate-box {
  width: min(440px, 100%);
  max-height: 90vh;
  overflow: auto;
  border-radius: 20px;
  padding: 22px 22px 18px;
  background: linear-gradient(160deg, #1a2350, #11183c);
  border: 1px solid var(--panel-border, #ffffff22);
  box-shadow: 0 24px 60px -20px #000d;
}
.gate-box h2 { margin: 0 0 10px; font-size: 1.3rem; }
.gate-body { color: var(--ink-dim); font-size: 0.95rem; line-height: 1.5; }
.gate-body p { margin: 0 0 8px; }
.gate-consent {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  margin: 6px 0 12px;
  font-size: 0.92rem;
  color: var(--ink);
}
.gate-consent input { margin-top: 4px; width: 20px; height: 20px; flex: 0 0 auto; }
.gate-q {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  font-weight: 700;
  margin: 4px 0;
}
.gate-answer {
  width: 90px;
  height: 48px;
  border-radius: 12px;
  text-align: center;
  font-size: 1.3rem;
  font-weight: 800;
  color: var(--ink);
  background: rgba(0, 0, 0, 0.3);
  border: 2px solid var(--accent, #7cc4ff);
}
.gate-err { color: #ff9aa2; min-height: 18px; font-size: 0.9rem; margin: 6px 0; }
.gate-actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 6px; }

/* §30 Progress: the kid-visible "Words I'm learning" set (2-step pips toward known). */
.learn-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.learn-word {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 12px;
  border-radius: 12px;
  background: linear-gradient(160deg, #2a3a78, #1b2350);
  border: 1px solid #ffffff14;
}
.learn-text {
  font-weight: 700;
  text-transform: lowercase;
}
.learn-pips {
  display: inline-flex;
  gap: 4px;
}
.learn-pips .pip {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: #ffffff22;
  box-shadow: inset 0 0 0 1px #ffffff1f;
}
.learn-pips .pip.on {
  background: var(--emerald, #2bb673);
  box-shadow: 0 0 8px rgba(43, 182, 115, 0.7);
}

/* ------- crystal lab ------- */
.lab-body {
  display: flex;
  flex-direction: column;
}
.lab-stage {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  text-align: center;
  padding: 8px 4px 16px;
}
.lab-emoji {
  font-size: 3.4rem;
}
.lab-title {
  font-size: clamp(1.5rem, 5.5vw, 2.2rem);
  margin: 0;
}
.lab-lead {
  color: var(--ink-dim);
  font-size: 1.1rem;
  max-width: 24ch;
  margin: 0;
}
.hear-again.big {
  font-size: 1.5rem;
  padding: 18px 32px;
}
.lab-go {
  min-width: 240px;
}
.lab-canvas {
  border-radius: var(--radius);
  border: 2px solid var(--panel-border);
  background: #0b1233;
  touch-action: none;
  box-shadow: 0 12px 32px -12px #000a;
  cursor: crosshair;
}
.palette {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
  align-items: center;
}
.swatch {
  width: 46px;
  height: 46px;
  border-radius: 50%;
  border: 3px solid rgba(255, 255, 255, 0.25);
  transition: transform 0.1s ease, border-color 0.12s ease;
}
.swatch.on {
  border-color: #fff;
  transform: scale(1.12);
}
.swatch:active {
  transform: scale(0.92);
}
.lab-name {
  text-align: center;
  font-weight: 800;
  max-width: 320px;
}
.lab-word {
  font-size: 1.3rem;
  font-weight: 800;
  color: var(--cyan);
}
.lab-preview {
  width: 180px;
  height: 180px;
  border-radius: 18px;
  border: 2px solid var(--panel-border);
  object-fit: cover;
}

/* specimen collection (progress screen) */
.specimen-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 12px;
}
.specimen {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}
.specimen-img {
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: 14px;
  border: 1px solid var(--panel-border);
  object-fit: cover;
  background: #0b1233;
}
.specimen-img.placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2rem;
}
.specimen-name {
  font-size: 0.95rem;
  font-weight: 700;
  text-align: center;
  word-break: break-word;
}

/* ------- wave-complete reward ------- */
.reward {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  text-align: center;
}
.reward .big {
  font-size: 4rem;
}
.reward h2 {
  font-size: 2rem;
  margin: 0;
}
.reward .earned {
  font-size: 1.4rem;
  color: var(--gold);
  font-weight: 800;
}
.reward .row {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  justify-content: center;
}
.unlock-banner {
  font-size: 1.3rem;
  font-weight: 800;
  color: var(--gold);
  background: rgba(255, 210, 63, 0.12);
  border: 1px solid rgba(255, 210, 63, 0.4);
  border-radius: 999px;
  padding: 10px 22px;
  animation: pop 0.6s ease;
}
.unlock-hint {
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--ink-dim);
}

/* ------- generic panels / settings / progress ------- */
.scroll {
  flex: 1;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  min-height: 0;
}
.panel {
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  padding: 18px;
  margin-bottom: 16px;
}
.panel h3 {
  margin: 0 0 12px;
  font-size: 1.25rem;
}

/* Grown-up settings disclosure (§26-A rec #8): collapse the parent/advanced panels behind a
   single tap so the child-facing Settings stays short and simple. Native <details>/<summary>. */
.gp-disclosure {
  margin-bottom: 16px;
}
.gp-summary {
  list-style: none; /* hide the default disclosure triangle; we draw our own */
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: var(--panel);
  border: 1px dashed var(--panel-border);
  border-radius: var(--radius);
  padding: 16px 18px;
  -webkit-user-select: none;
  user-select: none;
}
.gp-summary::-webkit-details-marker {
  display: none;
}
.gp-summary-label {
  font-weight: 800;
  font-size: 1.15rem;
}
.gp-summary-label::after {
  content: ' ▸';
  color: var(--ink-dim);
}
.gp-disclosure[open] .gp-summary-label::after {
  content: ' ▾';
}
.gp-summary-hint {
  color: var(--ink-dim);
  font-size: 0.95rem;
}
.gp-body {
  margin-top: 14px;
}
/* the inner panels keep their own spacing; trim the last margin so the disclosure ends clean */
.gp-body > .panel:last-child {
  margin-bottom: 0;
}
.field {
  margin-bottom: 16px;
}
.field > label {
  display: block;
  font-weight: 700;
  margin-bottom: 8px;
  font-size: 1.1rem;
}
.seg {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}
.seg button {
  flex: 1;
  min-width: 90px;
  min-height: var(--tap-min);
  /* center label + any lock/emoji as a unit, vertically and horizontally (§17.B) */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4em;
  border-radius: 16px;
  font-weight: 800;
  font-size: 1.1rem;
  line-height: 1.15;
  text-align: center;
  background: var(--panel);
  border: 2px solid var(--panel-border);
}
.seg button.on {
  background: linear-gradient(150deg, #5f7bff, #8a5cff);
  border-color: transparent;
}
.seg button.locked {
  opacity: 0.45;
}
input[type="range"] {
  width: 100%;
  height: 38px;
}
input[type="text"] {
  width: 100%;
  min-height: var(--tap-min);
  border-radius: 14px;
  border: 2px solid var(--panel-border);
  background: rgba(0, 0, 0, 0.25);
  color: var(--ink);
  font-size: 1.2rem;
  padding: 0 16px;
}
select {
  width: 100%;
  min-height: var(--tap-min);
  border-radius: 14px;
  border: 2px solid var(--panel-border);
  background: rgba(0, 0, 0, 0.25);
  color: var(--ink);
  font-size: 1.1rem;
  padding: 0 12px;
}

/* Parents & privacy: backup/restore/delete actions + notes */
.data-actions {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.data-actions .btn {
  width: 100%;
}
.backup-status {
  margin: 0 0 2px;
  text-align: center;
  font-weight: 700;
  color: var(--ink-dim);
}
.backup-status.due {
  color: var(--gold);
}
.privacy-note {
  margin: 8px 2px 0;
  color: var(--ink-dim);
  font-size: 0.95rem;
  line-height: 1.5;
}
.field-hint {
  margin: 6px 2px 0;
  color: var(--ink-dim);
  font-size: 0.9rem;
}
/* build version (Settings → Parents & privacy footer) — dim + centered; turns amber when
   the cached SW version is behind the running code (an update is pending). */
.version-line {
  margin: 14px 2px 0;
  text-align: center;
  color: var(--ink-dim);
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
}
.version-line.stale {
  color: var(--gold);
  font-weight: 700;
}
.cloud-sync {
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--panel-border);
}
.cloud-title {
  margin: 0 0 10px;
  font-size: 1.05rem;
  font-weight: 800;
}
.consent-row {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  font-size: 0.95rem;
  color: var(--ink-dim);
  line-height: 1.45;
  margin-bottom: 12px;
}
.consent-row input {
  width: 22px;
  height: 22px;
  margin-top: 2px;
  flex: 0 0 auto;
}
.sync-setup {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.sync-code {
  font-size: 1.8rem;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-align: center;
  color: var(--gold);
  background: rgba(255, 210, 63, 0.1);
  border: 1px solid rgba(255, 210, 63, 0.4);
  border-radius: 14px;
  padding: 12px;
  user-select: all;
}

/* picture password — kid-friendly sync code (tap pictures, no typing) */
.pic-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 10px;
  max-width: 460px;
  margin: 0 auto;
}
@media (max-width: 560px) {
  .pic-grid {
    grid-template-columns: repeat(5, 1fr);
  }
}
.pic-btn {
  aspect-ratio: 1 / 1;
  font-size: clamp(1.8rem, 8vw, 2.6rem);
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 16px;
  background: var(--panel);
  border: 2px solid var(--panel-border);
  transition: transform 0.1s ease, border-color 0.12s ease;
}
.pic-btn:active {
  transform: scale(0.9);
  border-color: var(--accent);
}
.pic-chosen {
  display: flex;
  gap: 12px;
  justify-content: center;
}
.pic-slot {
  width: clamp(46px, 12vw, 64px);
  height: clamp(46px, 12vw, 64px);
  border-radius: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: clamp(1.6rem, 7vw, 2.3rem);
  background: rgba(0, 0, 0, 0.22);
  border: 2px dashed var(--panel-border);
  color: var(--ink-dim);
}
.pic-slot.filled {
  border-style: solid;
  border-color: var(--gold);
  background: rgba(255, 210, 63, 0.12);
}

/* mastery spectrum bar (progress screen) */
.spectrum {
  display: flex;
  height: 28px;
  border-radius: 999px;
  overflow: hidden;
  border: 1px solid var(--panel-border);
}
.spectrum > span {
  display: block;
}
.spectrum .known {
  background: var(--emerald);
}
.spectrum .learning {
  background: var(--amethyst);
}
.spectrum .shaky {
  background: var(--slate);
}
.legend {
  display: flex;
  gap: 18px;
  flex-wrap: wrap;
  margin-top: 12px;
  color: var(--ink-dim);
}
.legend i {
  display: inline-block;
  width: 14px;
  height: 14px;
  border-radius: 4px;
  margin-right: 6px;
  vertical-align: -1px;
}
/* "tricky words" chips — the actual cracked-crystal words, for the kid + parent */
.tricky {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 12px;
}
.tricky-word {
  background: rgba(108, 122, 137, 0.18);
  border: 1px solid var(--panel-border);
  border-radius: 999px;
  padding: 5px 12px;
  font-weight: 700;
  font-size: 0.95rem;
}
.tricky-word.more {
  color: var(--ink-dim);
}
.big-num {
  font-size: 2.4rem;
  font-weight: 800;
}

/* "Your haul" treasure tiles (§A): the gems/depth/streak stats read like loot — each a
   glowing gradient tile, not a flat pill — so Progress feels rewarding, not analytics. */
.seg.haul {
  gap: 12px;
}
.seg.haul .haul-tile {
  flex: 1;
  min-width: 92px;
  flex-direction: column;
  gap: 4px;
  border-radius: 20px;
  padding: 14px 10px;
  text-align: center;
  border: 1px solid var(--panel-border);
  background: linear-gradient(160deg, #232d57, #1a2240);
  box-shadow: 0 10px 26px -16px rgba(0, 0, 0, 0.9), inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.seg.haul .haul-tile.gem {
  background: linear-gradient(160deg, #1d3a6b, #16244a);
  border-color: #36f1cd55;
}
.seg.haul .haul-tile.depth {
  background: linear-gradient(160deg, #2b2766, #1a1c40);
  border-color: #9d8df155;
}
.seg.haul .haul-tile.streak {
  background: linear-gradient(160deg, #4a2c1f, #2a1c30);
  border-color: #ffae4255;
}
.seg.haul .big-num {
  font-size: 2rem;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.45);
}
.seg.haul .haul-label {
  font-size: 0.82rem;
  color: var(--ink-dim);
  font-weight: 700;
}

/* ------- feedback ------- */
.rating-row {
  display: flex;
  gap: 8px;
  justify-content: space-between;
}
.rating {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  min-height: var(--tap-min);
  padding: 10px 4px;
  border-radius: 16px;
  background: var(--panel);
  border: 2px solid var(--panel-border);
  transition: transform 0.1s ease, border-color 0.12s ease, background 0.12s ease;
}
.rating:active {
  transform: scale(0.94);
}
.rating.on {
  background: linear-gradient(150deg, #5f7bff, #8a5cff);
  border-color: transparent;
}
.rating-emoji {
  font-size: 2rem;
  line-height: 1;
}
.rating-label {
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--ink-dim);
}
.rating.on .rating-label {
  color: var(--ink);
}
.feedback-note {
  width: 100%;
  border-radius: 14px;
  border: 2px solid var(--panel-border);
  background: rgba(0, 0, 0, 0.25);
  color: var(--ink);
  font-family: inherit;
  font-size: 1.1rem;
  padding: 12px 14px;
  resize: vertical;
}
.feedback-send {
  width: 100%;
  margin-bottom: 16px;
}

/* ------- engagement: idle pause overlay + nudge pulse ------- */
.pause-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(5, 8, 22, 0.86);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  animation: screen-in 0.25s ease both;
}
.pause-box {
  text-align: center;
  background: var(--panel);
  border: 2px solid var(--panel-border);
  border-radius: 28px;
  padding: 30px 38px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  max-width: 80vw;
}
.pause-emoji {
  font-size: 3.4rem;
  animation: armed-pulse 1.4s ease-in-out infinite;
}
.pause-box h2 {
  margin: 0;
  font-size: 2rem;
}
.pause-box p {
  margin: 0;
  color: var(--ink-dim);
  font-size: 1.1rem;
}

/* gentle "do something" pulse for nudged buttons / tiles */
.pulse,
.tiles.nudge .tile,
.tray.nudge .tray-tile {
  animation: nudge-pulse 0.7s ease-in-out 2;
}
@keyframes nudge-pulse {
  50% {
    transform: scale(1.06);
    box-shadow: 0 0 0 3px var(--cyan) inset, 0 0 18px var(--cyan);
  }
}

/* ------- toast + particles ------- */
.toast {
  position: fixed;
  left: 50%;
  bottom: 8%;
  transform: translate(-50%, 20px);
  background: #11183c;
  border: 1px solid var(--panel-border);
  color: var(--ink);
  padding: 14px 22px;
  border-radius: 999px;
  font-weight: 700;
  font-size: 1.1rem;
  opacity: 0;
  transition: opacity 0.25s ease, transform 0.25s ease;
  z-index: 50;
  pointer-events: none;
}
.toast.show {
  opacity: 1;
  transform: translate(-50%, 0);
}
.particle {
  position: fixed;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  pointer-events: none;
  z-index: 40;
  animation: fly 0.72s ease-out forwards;
}
@keyframes fly {
  to {
    transform: translate(var(--dx), var(--dy)) scale(0.2);
    opacity: 0;
  }
}

/* ------- Crystal Catalog (mineral collection / gem spend sink) ------- */
.menu-card.catalog {
  background: linear-gradient(150deg, #2a4a8e55, #20285a);
  border-color: #7aa2ff66;
}
.menu-card.catalog .desc {
  color: rgba(255, 255, 255, 0.82);
}
/* a collectable is affordable right now — pull the eye to it */
.menu-card.catalog.has-unlock {
  border-color: var(--cyan);
  box-shadow: 0 0 22px -6px var(--cyan);
}
.catalog-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(108px, 1fr));
  gap: 14px;
}
.crystal-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 14px 8px 12px;
  border-radius: 18px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--panel-border);
  transition: transform 0.12s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.crystal-cell:active {
  transform: scale(0.95);
}
.crystal-art {
  width: 96px;
  height: 96px;
  display: flex;
  align-items: center;
  justify-content: center;
  filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.45));
}
/* Locked minerals are a DIM TINTED SILHOUETTE, not an identical black hexagon: keep ~half
   the hue + enough brightness that each mineral's colour and facet-cut (5-8 sides) still read,
   so the collection looks like 24 distinct specimens to find — not 24 grey placeholders
   (DESIGN_ANALYSIS rec #3). Still clearly "not yet collected": dimmer than affordable (0.7)
   and far dimmer than an owned, full-colour gem. */
.crystal-cell.locked .crystal-art {
  filter: grayscale(0.5) brightness(0.58) drop-shadow(0 2px 6px rgba(0, 0, 0, 0.5));
  opacity: 0.85;
}
.crystal-cell.owned .crystal-art {
  /* pop once on collect, then a slow, subtle GLINT so an owned gem reads as alive/premium
     (§26-B "feel" polish, dependency-free — no GSAP). Idle ~90% of the cycle; staggered by
     nth-child below so the 24 specimens twinkle out of sync, not in a single coordinated flash. */
  animation: pop 0.5s ease, crystal-glint 5.5s ease-in-out 0.6s infinite;
}
@keyframes crystal-glint {
  0%, 86%, 100% { filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.45)) brightness(1) saturate(1); }
  93% { filter: drop-shadow(0 5px 14px rgba(255, 255, 255, 0.22)) brightness(1.2) saturate(1.12); }
}
.crystal-cell.owned:nth-child(3n+1) .crystal-art { animation-delay: 0.6s, 1.4s; }
.crystal-cell.owned:nth-child(3n+2) .crystal-art { animation-delay: 0.6s, 3.1s; }
.crystal-cell.owned:nth-child(4n) .crystal-art { animation-delay: 0.6s, 4.3s; }
.crystal-name {
  font-size: 0.98rem;
  font-weight: 700;
  text-align: center;
  line-height: 1.1;
}
.crystal-cell.locked .crystal-name {
  color: var(--ink-dim);
}
.crystal-rarity {
  font-size: 0.78rem;
  color: var(--ink-dim);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.crystal-price {
  font-size: 0.92rem;
  font-weight: 700;
  color: var(--ink-dim);
}
.crystal-price.can {
  color: var(--cyan);
}
/* an affordable, un-owned crystal glows + gently pulses to invite the unlock */
.crystal-cell.affordable {
  border-color: var(--cyan);
  box-shadow: 0 0 18px -6px var(--cyan);
}
.crystal-cell.affordable .crystal-art {
  filter: grayscale(0.55) brightness(0.7) drop-shadow(0 2px 8px rgba(54, 241, 205, 0.4));
  opacity: 0.92;
  animation: breathe 2.4s ease-in-out infinite;
}
@keyframes breathe {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.06); }
}
.rarity-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 12px;
}
.rarity-chip {
  font-size: 0.85rem;
  font-weight: 700;
  padding: 5px 12px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--glow, var(--panel-border));
  color: var(--glow, var(--ink));
}
/* milestone "new mineral discovered" banner on the wave-reward screen */
.crystal-grant {
  display: block;
  width: 100%;
  font-size: 1.1rem;
  font-weight: 800;
  color: var(--cyan);
  background: rgba(54, 241, 205, 0.12);
  border: 1px solid rgba(54, 241, 205, 0.45);
  border-radius: 16px;
  padding: 12px 18px;
  margin: 4px 0;
  animation: pop 0.6s ease;
}
.crystal-grant:active {
  transform: scale(0.97);
}
/* tap-a-crystal detail card (preview + fact + deliberate unlock) */
.crystal-detail-overlay {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(7, 10, 28, 0.72);
  backdrop-filter: blur(3px);
  animation: fade-in 0.2s ease;
}
@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
.crystal-detail {
  width: min(360px, 90vw);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 24px;
  border-radius: 24px;
  text-align: center;
  background: linear-gradient(160deg, var(--bg-2), var(--bg-1));
  border: 1px solid var(--panel-border);
  box-shadow: 0 24px 60px -16px rgba(0, 0, 0, 0.8);
  animation: pop 0.5s ease;
}
.crystal-detail .crystal-art.big {
  width: 140px;
  height: 140px;
}
.crystal-detail-name {
  font-size: 1.7rem;
  font-weight: 800;
}
.crystal-detail-fact {
  color: var(--ink-dim);
  margin: 0;
  font-size: 1.05rem;
}
.detail-cta {
  font-weight: 800;
  font-size: 1.1rem;
  padding: 10px 0;
}
.detail-cta.owned {
  color: var(--emerald);
}
.detail-cta.need {
  color: var(--ink-dim);
}

/* ------- first-run onboarding + Geo the mascot ------- */
.onboarding {
  justify-content: center;
  position: relative;
  /* clip — NOT hidden. The decorative ::before glow uses inset:-10% (≈33px beyond the content
     box), and `overflow:hidden` clips it VISUALLY but leaves the whole onboarding/"Who's playing?"
     screen touch-PANNABLE by ~33-39px on every phone width — a big contributor to the reported
     right-side pan (it's the first screen every launch). `overflow:clip` clips the glow AND
     creates no scroll container, so it can't pan. The inner .onboard-body keeps its own y-scroll. */
  overflow: clip;
}
/* The mascot block centres, but on a tall screen the space below read as an empty void
   (DESIGN_ANALYSIS rec #7). Give it PURPOSE: a few faint, slowly-drifting gem glows in the
   cavern colours so the lower half feels like deliberate atmosphere, not unfinished framing.
   Pure CSS, behind the content; reduced-motion zeroes the drift globally. */
.onboarding::before {
  content: '';
  position: absolute;
  inset: -10%;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(130px 130px at 18% 80%, color-mix(in srgb, var(--accent) 22%, transparent), transparent 70%),
    radial-gradient(100px 100px at 84% 88%, color-mix(in srgb, var(--cyan) 15%, transparent), transparent 70%),
    radial-gradient(80px 80px at 72% 20%, color-mix(in srgb, var(--amethyst) 14%, transparent), transparent 70%);
  animation: onb-drift 14s ease-in-out infinite alternate;
}
@keyframes onb-drift {
  from { transform: translateY(0) scale(1); opacity: 0.65; }
  to { transform: translateY(-16px) scale(1.05); opacity: 1; }
}
.onboard-body {
  position: relative;
  z-index: 1;
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: safe center;
  /* y scrolls; x must not. CSS won't let a y-scroller also clip-x (overflow-x:clip computes to
     hidden when overflow-y:auto), and a few-px sub-pixel content artifact then leaves it touch-
     pannable. `touch-action: pan-y` settles it at the INPUT layer: the browser allows only vertical
     finger-panning here, so the screen can't be dragged sideways regardless of scrollWidth. (This
     screen is taps + vertical scroll only — no horizontal drag — so pan-y is safe.) */
  overflow-x: hidden;
  overflow-y: auto;
  touch-action: pan-y;
  gap: 26px;
  padding: 24px;
  text-align: center;
}
.mascot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  max-width: 460px;
}
.mascot-char {
  width: 104px;
  height: 104px;
  background: var(--accent); /* solid fallback if color-mix() (below) is unsupported */
  background: radial-gradient(circle at 36% 30%, #ffffff, var(--accent) 62%, color-mix(in srgb, var(--accent) 60%, #000) 100%);
  clip-path: polygon(50% 0%, 95% 26%, 95% 74%, 50% 100%, 5% 74%, 5% 26%);
  position: relative;
  filter: drop-shadow(0 8px 22px color-mix(in srgb, var(--accent) 55%, transparent));
  animation: bob 2.6s ease-in-out infinite;
}
@keyframes bob {
  0%, 100% { transform: translateY(0) rotate(-2deg); }
  50% { transform: translateY(-9px) rotate(2deg); }
}
.mascot-face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 7px;
}
.mascot-eyes {
  display: flex;
  gap: 16px;
}
.mascot-eyes .eye {
  width: 15px;
  height: 17px;
  background: #1a1330;
  border-radius: 50%;
  position: relative;
}
.mascot-eyes .eye::after {
  content: '';
  position: absolute;
  top: 3px;
  left: 3px;
  width: 5px;
  height: 5px;
  background: #fff;
  border-radius: 50%;
}
.mascot-smile {
  width: 26px;
  height: 13px;
  border: 3px solid #1a1330;
  border-top: none;
  border-radius: 0 0 26px 26px;
}
/* Geo reacts (DESIGN_ANALYSIS rec #6): a couple of cheap CSS expression states layered on
   the existing hexagon. 'cheer' = a bigger, faster happy bounce + wider grin (celebratory
   moments — a cracked geode, a milestone); 'wink' = one eye closed (a friendly greeting). */
.mascot.cheer .mascot-char {
  animation: geo-cheer 0.72s ease-in-out infinite;
}
@keyframes geo-cheer {
  0%, 100% { transform: translateY(0) rotate(-3deg) scale(1); }
  30% { transform: translateY(-14px) rotate(4deg) scale(1.07); }
  62% { transform: translateY(-3px) rotate(-2deg) scale(1.02); }
}
.mascot.cheer .mascot-smile {
  width: 30px;
  height: 16px;
}
.mascot.wink .mascot-eyes .eye:last-child {
  transform: scaleY(0.18); /* squish one eye into a friendly wink */
}
.speech-bubble {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: 18px;
  padding: 16px 20px;
  max-width: 420px;
}
.speech-bubble::before {
  content: '';
  position: absolute;
  top: -9px;
  left: 50%;
  transform: translateX(-50%) rotate(45deg);
  width: 16px;
  height: 16px;
  background: var(--panel);
  border-left: 1px solid var(--panel-border);
  border-top: 1px solid var(--panel-border);
}
.speech-name {
  font-weight: 800;
  color: var(--accent);
  font-size: 0.95rem;
  margin-bottom: 4px;
}
.speech-text {
  font-size: 1.3rem;
  line-height: 1.35;
}
.onboard-go {
  font-size: 1.35rem;
  padding: 16px 34px;
  min-width: 220px;
}
.onboard-go.big {
  font-size: 1.5rem;
  padding: 20px 40px;
}
.onboard-name {
  font-size: 1.5rem;
  text-align: center;
  padding: 14px 18px;
  width: min(360px, 80vw);
  border-radius: 16px;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  color: var(--ink);
}
.onboard-name:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 40%, transparent);
}
.colour-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  justify-content: center;
}
.colour-swatch {
  width: 84px;
  height: 84px;
  border-radius: 50%;
  border: 3px solid transparent;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding-bottom: 8px;
  box-shadow: 0 6px 16px -6px rgba(0, 0, 0, 0.6);
  transition: transform 0.12s ease, border-color 0.15s ease;
}
.colour-swatch:active {
  transform: scale(0.93);
}
.colour-swatch.on {
  border-color: #fff;
  transform: scale(1.1);
  box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.28), 0 0 22px -2px var(--accent), 0 8px 22px -6px rgba(0, 0, 0, 0.7);
}
.colour-swatch .colour-name {
  font-size: 0.82rem;
  font-weight: 800;
  color: rgba(0, 0, 0, 0.72);
  text-shadow: 0 1px 2px rgba(255, 255, 255, 0.45);
}
.colour-swatch.small {
  width: 52px;
  height: 52px;
  padding: 0;
}

/* level select (onboarding "where to start" + Settings "starting level") — 2 columns so
   the 9 levels (one per tier) stay compact and don't push the screen too tall (§21-D). */
.level-grid {
  display: grid;
  /* minmax(0,1fr) — NOT 1fr: a bare 1fr column has min-width:auto, so a long unbreakable label
     (e.g. a level card's "information") sets a min-content floor that pushes the GRID wider than
     its box, making the scroll parent x-pannable. minmax(0,1fr) lets columns shrink so the text
     clips (cards are overflow:clip) instead of expanding the grid. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  /* fill the container (so it respects Settings panel padding instead of overflowing on a
     phone) but cap + center on wide screens like the onboarding full-width step. */
  width: 100%;
  max-width: 520px;
  margin: 0 auto;
}
.level-card {
  position: relative;
  /* clip not hidden — clips the absolute depth-stripe while staying un-pannable even when the
     card's age/word text overflows slightly at large OS text scale (else the card itself pans). */
  overflow: clip;
  text-align: left;
  padding: 12px 16px 12px 20px;
  border-radius: 16px;
  background: linear-gradient(155deg, #1d2649, #161d3a);
  border: 2px solid var(--panel-border);
  display: flex;
  flex-direction: column;
  gap: 2px;
  transition: transform 0.1s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
/* A per-tier DEPTH STRIPE turns the flat 9-card grid into a visual ladder: a cool→warm
   progression (shallow/easy cyan → deep/hard magenta) so picking a level reads as
   choosing how deep to dig, not ticking a form row (§A polish). */
.level-card::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 5px;
  background: var(--tier, var(--cyan));
  opacity: 0.9;
}
.level-grid .level-card:nth-child(1) { --tier: #36f1cd; }
.level-grid .level-card:nth-child(2) { --tier: #36e0e0; }
.level-grid .level-card:nth-child(3) { --tier: #7ae582; }
.level-grid .level-card:nth-child(4) { --tier: #b6e36a; }
.level-grid .level-card:nth-child(5) { --tier: #ffd23f; }
.level-grid .level-card:nth-child(6) { --tier: #ffae42; }
.level-grid .level-card:nth-child(7) { --tier: #ff8a5c; }
.level-grid .level-card:nth-child(8) { --tier: #c08cff; }
.level-grid .level-card:nth-child(9) { --tier: #ff6ec7; }
.level-card:active {
  transform: scale(0.98);
}
.level-card.on {
  border-color: var(--tier, var(--accent));
  background: linear-gradient(155deg, color-mix(in srgb, var(--tier, var(--accent)) 22%, #1d2649), #161d3a);
  box-shadow: 0 0 22px -6px var(--tier, var(--accent));
}
.level-card.on::before {
  width: 7px;
  box-shadow: 0 0 14px var(--tier);
}
.level-label {
  font-weight: 800;
  font-size: 1.15rem;
}
.level-age {
  font-size: 0.85rem;
  color: var(--ink-dim);
  font-weight: 700;
}
.level-examples {
  font-size: 1rem;
  color: var(--cyan);
  font-weight: 700;
  margin-top: 2px;
}

/* "Who's playing?" profile picker */
.profile-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  justify-content: center;
  max-width: 520px;
}
.profile-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  width: 130px;
  padding: 18px 10px;
  border-radius: 22px;
  background: var(--panel);
  border: 2px solid var(--panel-border);
  transition: transform 0.12s ease, border-color 0.15s ease;
}
.profile-card:active {
  transform: scale(0.95);
}
.profile-card.add {
  opacity: 0.85;
}
.profile-dot {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.8rem;
  background: var(--accent);
  box-shadow: 0 6px 16px -6px rgba(0, 0, 0, 0.6);
}
.profile-card.add .profile-dot {
  background: var(--panel);
  border: 2px dashed var(--panel-border);
  color: var(--ink-dim);
}
.profile-name {
  font-weight: 800;
  font-size: 1.2rem;
}

/* ------- Geode Boss (milestone celebration) ------- */
.boss-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* center when it fits; otherwise top-align + scroll so the action buttons are
     never pushed off-screen on short/landscape viewports */
  justify-content: safe center;
  overflow-y: auto;
  gap: 18px;
  padding: 24px;
  text-align: center;
}
.geode {
  width: 200px;
  height: 200px;
  border-radius: 50%;
  position: relative;
  border: none;
  background: radial-gradient(circle at 38% 32%, #7c7d90, #3a3a4c 58%, #23232f 100%);
  box-shadow: inset -10px -14px 30px rgba(0, 0, 0, 0.6), inset 8px 8px 20px rgba(255, 255, 255, 0.12),
    0 18px 40px -12px rgba(0, 0, 0, 0.7);
  overflow: hidden;
}
.geode:active {
  transform: scale(0.97);
}
.geode-shell {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.5);
}
.geode-glow {
  position: absolute;
  inset: 16%;
  border-radius: 50%;
  background: radial-gradient(circle, #fff, var(--gold) 50%, transparent 72%);
  opacity: calc(var(--crack, 0) * 0.95);
  transform: scale(calc(0.55 + var(--crack, 0) * 0.7));
  transition: opacity 0.15s ease, transform 0.15s ease;
  pointer-events: none;
}
.geode.hit {
  animation: geode-shake 0.22s ease;
}
@keyframes geode-shake {
  0%, 100% { transform: translate(0, 0) rotate(0); }
  25% { transform: translate(-5px, 2px) rotate(-2deg); }
  75% { transform: translate(5px, -2px) rotate(2deg); }
}
.geode.cracked {
  animation: geode-pop 0.5s ease forwards;
}
@keyframes geode-pop {
  0% { transform: scale(1); opacity: 1; }
  40% { transform: scale(1.18); }
  100% { transform: scale(0.2); opacity: 0; }
}
.crack-meter {
  width: min(280px, 70vw);
  height: 14px;
  border-radius: 999px;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  overflow: hidden;
}
.crack-fill {
  height: 100%;
  width: 0;
  background: linear-gradient(90deg, var(--gold), var(--cyan));
  transition: width 0.12s ease;
}
.boss-hint {
  color: var(--ink-dim);
  font-size: 1.15rem;
}
.boss-burst {
  animation: pop 0.6s ease;
}
.crystal-art.big {
  width: 168px;
  height: 168px;
  filter: drop-shadow(0 8px 22px rgba(255, 210, 63, 0.4));
}
.boss-emoji {
  font-size: 6rem;
}
.boss-crystal-name {
  font-size: 2rem;
  margin: 4px 0 0;
}
.boss-fact {
  color: var(--ink-dim);
  max-width: 420px;
  margin: 0;
}
.depth-banner {
  font-size: 1.25rem;
  font-weight: 800;
  color: var(--gold);
}
.boss .row {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  justify-content: center;
}
.boss .earned {
  font-size: 1.5rem;
  color: var(--gold);
  font-weight: 800;
}

/* Daily geode (§C): compact the reveal so the gem haul, the next goals AND the primary
   "Keep crafting" CTA all fit a phone without a hunt-scroll. */
.geode-day .boss-body {
  gap: 12px;
  padding: 16px 18px max(16px, env(safe-area-inset-bottom));
}
.geode-day .boss-emoji {
  font-size: 4.2rem;
}
.geode-day .boss-crystal-name {
  font-size: 1.7rem;
}
.geode-day .mascot {
  margin: 0;
}
/* The "next goals" preview shown after cracking — a tidy list of the ratcheted-up
   quests, with the craft goal highlighted as the headline. */
.geode-goals {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: min(360px, 88vw);
}
.geode-goal {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  border-radius: 16px;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  text-align: left;
}
.geode-goal.craft {
  background: linear-gradient(120deg, #7b5cff33, #b14bff22);
  border-color: #b14bff66;
}
.geode-goal-ic {
  font-size: 1.5rem;
  flex: 0 0 auto;
}
.geode-goal-text {
  font-weight: 700;
  font-size: 1.02rem;
}

/* ------- accessibility: easy-read text ------- */
/* Easy-read / dyslexia-friendly mode (opt-in via Settings → html.readable). Built on the
   British Dyslexia Association Style Guide + the readability research (Rello & Baeza-Yates
   2013; the gains come from SPACING + letter clarity, not a "special" font — the app already
   ships Atkinson Hyperlegible). Three levers: (a) global generous line-height + a touch more
   tracking on running text, (b) extra tracking on the spelling-critical elements so similar
   spellings separate (free / freu / frree), (c) LEFT-align the dictated sentence (the BDA
   advises against centring body text — left-align reduces "rivers" and is easier to track).
   Scoped to text so it never reflows the chrome; modest values so tile clamp/wrap guards hold. */
html.readable .sentence,
html.readable .menu-card .desc,
html.readable .field-hint,
html.readable .boss-fact,
html.readable .home-sub,
html.readable p {
  line-height: 1.6;
  letter-spacing: 0.018em;
}
html.readable .sentence {
  letter-spacing: 0.03em;
  line-height: 1.65;
  text-align: left; /* BDA: left-align the reading line, not centred */
}
html.readable .tile,
html.readable .tray-tile,
html.readable .slot,
html.readable .lbox-letter,
html.readable .lab-word,
html.readable .crystal-name {
  letter-spacing: 0.06em;
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.001s !important;
    transition-duration: 0.001s !important;
  }
}

/* ------- picture pad (kid-lock) ------- */
.pic-pad {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding: 8px 0;
}
/* .pic-grid, .pic-btn, .pic-chosen, .pic-slot already defined above near sync-code */

/* snapshot rows (Time machine / rollback list) */
.snapshot-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 0;
  border-top: 1px solid var(--panel-border);
  flex-wrap: wrap;
}
.snapshot-row:first-child {
  border-top: none;
}
.snapshot-info {
  flex: 1;
  min-width: 0;
  font-size: 0.95rem;
  line-height: 1.4;
}
.snapshot-when {
  font-weight: 700;
  color: var(--ink);
}
.snapshot-label {
  color: var(--ink-dim);
}

/* lock-pad overlay inside profiles "who's playing?" */
.lock-pad-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
  padding: 8px 0;
}
.lock-pad-wrap.shake {
  animation: shake 0.4s ease;
}

/* sensitive-block: parent-gated area in Parents & privacy panel */
.sensitive-block {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* ============================================================================
   PHONE OVERRIDES (≤480px). Placed LAST so they win over the iPad-tuned base
   rules above (a media query adds no specificity, so source order decides). The
   app was laid out at iPad proportions; these compact the header, home hero, and
   level cards so a phone isn't top-heavy with content shoved below the fold.
   ============================================================================ */
@media (max-width: 480px) {
  /* In-game header: shrink the gem/depth pills + drop the "Depth" word so the title
     never clips and the depth chip can't run off the right edge. */
  .app-header {
    gap: 8px;
    padding: 4px 2px 12px;
  }
  .header-title {
    font-size: 1.1rem;
  }
  .header-stats {
    gap: 6px;
  }
  .stat {
    padding: 6px 10px;
    font-size: 0.9rem;
    gap: 5px;
  }
  .stat .icon {
    font-size: 1rem;
  }
  .depth-word {
    display: none;
  }

  /* Home: the hero (title + welcome + streak/goal) ate ~40% of a phone screen, pushing 4
     of 7 menu cards below the fold. Compact the hero + cards so the whole menu fits. */
  .home-hero {
    padding: 2px 0 2px;
  }
  /* §34: step the brand title + welcome down a notch on phones (the iPad-sized hero made the
     title feel oversized relative to the play content) and tighten the hero→streak→grid spacing
     so the menu cards start higher and the title no longer dominates the first screen. */
  .home-title {
    font-size: clamp(1.4rem, 6vw, 1.85rem);
    letter-spacing: 0;
  }
  .home-sub {
    font-size: 0.9rem;
    margin-top: 3px;
  }
  .home-streak {
    margin-top: 7px;
    gap: 6px;
  }
  .streak-chip {
    padding: 5px 12px;
    font-size: 0.9rem;
  }
  .home-grid {
    gap: 9px;
  }
  .menu-card {
    min-height: 78px;
    border-radius: 22px;
    padding: 11px 16px;
    gap: 3px;
  }
  .menu-card.craft.hero {
    min-height: 112px;
  }
  .menu-card.craft.hero .ic {
    font-size: 2.4rem;
  }
  .menu-card.craft.hero .lbl {
    font-size: 1.5rem;
  }
  .menu-card.play.practice {
    min-height: 64px;
  }
  .menu-card.play.practice .ic {
    font-size: 2rem;
  }
  .menu-card.play.practice .lbl {
    font-size: 1.2rem;
  }
  .menu-card .badge {
    font-size: 0.72rem;
    padding: 3px 9px;
    top: 10px;
    right: 11px;
  }
  .menu-card.repair {
    min-height: 70px;
  }
  .menu-card .ic {
    font-size: 1.9rem;
  }
  .menu-card .lbl {
    font-size: 1.2rem;
  }
  .menu-card .desc {
    font-size: 0.88rem;
  }

  /* Level select (onboarding + Settings): 9 cards (one per tier) are tall on a phone —
     compact each so far less scrolling is needed to reach them + the CTA. */
  .level-card {
    padding: 9px 12px;
  }
  .level-label {
    font-size: 1rem;
  }
  .level-examples {
    font-size: 0.88rem;
  }
  .level-age {
    font-size: 0.78rem;
  }

  /* Onboarding "Let's dig!" — pin it to the bottom of the scroll area so it's always
     reachable while the 9 level cards scroll behind it (it otherwise fell below the fold).
     A FULL-BLEED gradient footer backdrop (::before) makes the cards fade out cleanly
     BEHIND a footer — not peek around the centered pill (§24 QA). */
  .onboarding .level-cta {
    position: sticky;
    bottom: 8px;
    z-index: 3;
    margin-top: 10px;
    box-shadow: 0 10px 26px -6px rgba(0, 0, 0, 0.85);
  }
  .onboarding .level-cta::before {
    content: '';
    position: absolute;
    /* Bleed past the pill to cover the onboarding/app padding, but NOT a full 100vw — a
       -50vw/-50vw box is (containerWidth + 100vw) wide and only `.onboarding{overflow:hidden}`
       stops it forcing a giant horizontal scroll area. Fixed insets are clipped cleanly and
       can never widen the layout viewport. */
    left: -60px;
    right: -60px;
    top: -30px;
    bottom: -22px;
    background: linear-gradient(to top, var(--bg-0) 62%, transparent);
    z-index: -1;
    pointer-events: none;
  }

  /* Picture pad: fewer columns so the buttons stay tappable on narrow phones. */
  .pic-grid {
    grid-template-columns: repeat(3, 1fr);
    max-width: 100%;
  }

  /* Snapshot rows: stack info + restore button vertically on narrow phones. */
  .snapshot-row {
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
  }

  /* §33 PLAY SCREENS (Craft / Mastery / Mining) on phones: the iPad-tuned vertical gaps + full-
     size prompt ate the height a long word needs, so the tray (Craft) and the Clear/Type controls
     + candidates (Mastery) dropped below the fold. Tighten the framing so the word being filled,
     the tiles/canvas, AND the action buttons are co-visible; fitPlayArea() (ui.js) then shrinks the
     tiles via --play-scale for the very longest words so nothing ever has to scroll off-screen. */
  .play-body {
    gap: 10px;
    padding: 6px 0 max(8px, env(safe-area-inset-bottom));
  }
  /* §34 phone proportions: the strip BETWEEN the header and the play area — session dots +
     combo bar + combo label — is iPad-tuned, and with the label EMPTY on a fresh word it reserved
     ~100px of dead vertical space. That pushed the play area down and forced fitPlayArea to shrink
     the tiles (the "titles too big, play area too small" feel). Compact the strip and collapse the
     reserves that are empty on a fresh word so the play area keeps FULL-SIZE tiles (scale ~1). The
     small expand when a combo / verdict lands is at a positive moment, so the shift reads as reward. */
  .dots {
    margin: 1px 0 3px;
  }
  .dot {
    width: 10px;
    height: 10px;
  }
  .combo-wrap {
    height: 10px;
    margin: 1px 6px 3px;
  }
  .combo-label {
    font-size: 0.95rem;
    min-height: 1.1rem;
  }
  .combo-label:empty {
    min-height: 0;
    margin: 0;
  }
  .verdict:empty {
    min-height: 0;
  }
  .verdict-chip:empty {
    min-height: 0;
  }
  .prompt {
    gap: 6px;
  }
  .hear-again {
    font-size: 1.05rem;
    padding: 10px 18px;
  }
  .hear-again .spk {
    font-size: 1.3rem;
  }
  .sentence {
    font-size: clamp(1.05rem, 4.4vw, 1.45rem);
    line-height: 1.35;
  }
  .verdict {
    min-height: 2rem;
    font-size: clamp(1.6rem, 7vw, 2.6rem);
  }
  .answer-zone {
    gap: 9px;
  }
  .puzzle-controls,
  .draw-controls {
    gap: 10px;
    padding: 0 6px;
  }
  .puzzle-controls .btn.ghost,
  .draw-controls .btn.ghost {
    min-height: 44px;
    padding: 8px 15px;
    font-size: 0.98rem;
  }
  .draw-candidates {
    gap: 4px;
  }
  .draw-hint {
    font-size: 0.82rem;
  }
  /* §11/§34-fix: Mastery word-display tiles are SMALLER on phones so a long word (e.g.
     "international", 13 letters) wraps to ~2 rows instead of 3 big ones — that 3-row block
     dominated the screen and crowded the keypad ("cramped, letters overlap their borders").
     Phone DRAW mode draws on the separate canvas, so smaller display tiles don't hurt it. */
  .mastery .draw-slots .slot {
    width: calc(clamp(30px, 7.6vw, 44px) * var(--play-scale, 1));
    height: calc(clamp(34px, 8vw, 48px) * var(--play-scale, 1));
    font-size: calc(clamp(1.1rem, 4.4vw, 1.6rem) * var(--play-scale, 1));
  }
  .draw-slots {
    gap: calc(7px * var(--play-scale, 1));
  }
}

/* ============================================================================
   SHORT-VIEWPORT OVERRIDES (≤520px tall): phone LANDSCAPE and very short phones.
   The iPad-tuned home hero (title + welcome + streak/goal) is vertically tall and,
   on a ~390px-tall landscape phone, pushes EVERY menu card — including Craft itself —
   below the fold (DESIGN_ANALYSIS §6: the report's #1 fix). These collapse the hero
   hard so Craft + Practice are reachable without scrolling, and pin the reward/boss/
   geode action row to the bottom so the primary CTA is never below the fold (the
   measured +213px landscape overflow). Placed LAST so source order wins.
   ============================================================================ */
@media (max-height: 520px) {
  /* --- header + home hero: collapse hard to a single compact band. min-height alone
     won't shrink these — the cards are as tall as their CONTENT — so we also cut the
     padding + icon/label/desc sizes and drop the (non-essential, space-eating) welcome
     greeting + daily-goal bar so the Craft hero AND the Practice banner clear the fold. */
  .app-header {
    padding: 2px 2px 6px;
  }
  .home-hero {
    padding: 2px 0 0;
  }
  .home-title {
    font-size: clamp(1.1rem, 4.2vw, 1.5rem);
    letter-spacing: 0;
  }
  .home-sub {
    display: none;
  }
  .home-streak {
    margin-top: 6px;
    gap: 5px;
  }
  .streak-row {
    gap: 6px;
  }
  .streak-chip {
    padding: 3px 11px;
    font-size: 0.8rem;
  }
  /* the daily-goal bar is the least essential hero element — drop it to buy fold space */
  .home-hero .goal {
    display: none;
  }
  .home-grid {
    gap: 8px;
  }
  .menu-card {
    min-height: 0;
    padding: 9px 16px;
  }
  .menu-card .ic {
    font-size: 1.7rem;
  }
  .menu-card .lbl {
    font-size: 1.15rem;
  }
  .menu-card .desc {
    font-size: 0.8rem;
  }
  .menu-card.craft.hero {
    min-height: 0;
    padding: 10px 18px;
  }
  .menu-card.craft.hero .ic {
    font-size: 1.8rem;
  }
  .menu-card.craft.hero .lbl {
    font-size: 1.25rem;
  }
  .menu-card.play.practice {
    min-height: 0;
  }

  /* §33 PLAY SCREENS in LANDSCAPE / very short viewports: collapse the dictation prompt hard so
     the slots + tiles/canvas + buttons fit a ~390px-tall band (fitPlayArea then shrinks tiles). */
  .play-body {
    gap: 6px;
    padding: 4px 0 max(6px, env(safe-area-inset-bottom));
  }
  /* §12-fix: the prompt is the DOMINANT fixed cost in the play-body (it doesn't scale with
     --play-scale), so stacking the hear button ABOVE a 3-line sentence starved the play area and
     forced the keypad/tiles to the --play-scale floor (tiny keys). Landscape has plenty of WIDTH:
     lay the hear button + sentence side-by-side on ONE row, drop the sentence wrap cap, and collapse
     the empty verdict reserve — freeing the vertical room for a usable-size keypad. */
  .prompt {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    column-gap: 14px;
    row-gap: 2px;
  }
  .hear-row {
    flex: 0 0 auto;
  }
  .hear-again {
    font-size: 0.9rem;
    padding: 5px 12px;
  }
  .sentence {
    font-size: clamp(0.8rem, 2.2vw, 1.02rem);
    line-height: 1.12;
    max-width: none;
    flex: 1 1 auto;
    min-width: 0;
    text-align: left;
  }
  .verdict {
    min-height: 1.3rem;
    font-size: clamp(1.3rem, 5.5vw, 2rem);
    flex-basis: 100%;
  }
  .verdict-chip {
    flex-basis: 100%;
  }
  .verdict:empty,
  .verdict-chip:empty {
    min-height: 0;
    margin: 0;
  }
  .answer-zone {
    gap: 6px;
  }
  /* landscape has horizontal room but almost none vertical — shrink the Hint/Clear + Clear/Type
     control row hard so the tray's last row (Craft) / the candidates (Mastery) clear the fold. */
  .puzzle-controls,
  .draw-controls {
    gap: 8px;
    padding: 0;
  }
  .puzzle-controls .btn.ghost,
  .draw-controls .btn.ghost {
    min-height: 38px;
    padding: 5px 14px;
    font-size: 0.9rem;
  }
  .combo-wrap {
    margin: 1px 6px 3px;
  }
  .combo-label {
    min-height: 0;
    font-size: 0.95rem;
  }
  .dots {
    margin: 1px 0 3px;
  }
  /* compact the big primary Check button + speedmeter so Mastery's controls and Mining's tiles
     clear a 390px-tall landscape fold (the ~70px button + 49px meter were the last few px over). */
  .play-body .btn.primary {
    min-height: 40px;
    padding: 7px 18px;
    font-size: 1rem;
  }
  .draw-submit {
    padding: 0;
  }
  .speedmeter {
    gap: 10px;
    padding: 0 6px 4px;
  }
  .speed-pot {
    font-size: 1.25rem;
  }
  .speed-bar {
    height: 14px;
  }
  /* In a 390px-tall landscape band, drop the (non-essential) progress dots + the empty combo
     bar so the word + interaction surface + action buttons are fully co-visible. The combo
     LABEL (streak text) stays. The Mastery draw-hint text is redundant with the ✓ Check button
     (and wraps to 2 lines on the dense 11-box wide layout), so it's dropped here too. */
  .dots,
  .combo-wrap {
    display: none;
  }
  .draw-hint {
    display: none;
  }
  .tiles {
    padding-top: 2px;
  }
  /* §11/§12-fix LANDSCAPE: the title bar + prompt were full-size while the play area floored
     at the --play-scale minimum, so the keypad rendered tiny ("top bar very large vs the little
     letters"). Make the HEADER a thin band and the KEYPAD compact so fitPlayArea keeps the keys
     usable (less to shrink), and trim the word boxes a touch (in type mode they're just display). */
  .header-title {
    font-size: 1.02rem;
  }
  .app-header .stat {
    padding: 3px 9px;
    font-size: 0.82rem;
  }
  .type-keyboard {
    gap: calc(4px * var(--play-scale, 1));
    max-width: 470px;
  }
  .key-row {
    gap: calc(4px * var(--play-scale, 1));
  }
  .key {
    height: calc(clamp(24px, 7vh, 40px) * var(--play-scale, 1));
    max-width: calc(46px * var(--play-scale, 1));
    font-size: calc(clamp(0.9rem, 3.4vh, 1.3rem) * var(--play-scale, 1));
    border-radius: 9px;
  }
  .lbox {
    height: calc(clamp(44px, 12vh, 72px) * var(--play-scale, 1));
  }
  /* a sub-700px landscape phone keeps the single-canvas/.slots flow — shrink that display too */
  .mastery .draw-slots .slot {
    width: calc(clamp(32px, 7vh, 50px) * var(--play-scale, 1));
    height: calc(clamp(36px, 9vh, 56px) * var(--play-scale, 1));
    font-size: calc(clamp(1.05rem, 5vh, 1.7rem) * var(--play-scale, 1));
  }
}

/* Reward: pin the CTA row to the bottom and let the gem haul scroll behind it, on any
   PHONE-height viewport (≤800px catches landscape phones + small/short portraits like the
   360×740 Android, where the reward content runs ~+60px past the fold — but NOT iPad
   portrait/landscape, where it fits centred). Fixes the measured +213px landscape overflow. */
@media (max-height: 800px) {
  .reward {
    justify-content: flex-start;
    overflow-y: auto;
    gap: 8px;
  }
  .reward .big {
    font-size: 2.6rem;
  }
  .reward h2 {
    font-size: 1.5rem;
  }
  .reward .row {
    position: sticky;
    bottom: 0;
    margin-top: auto;
    width: 100%;
    gap: 8px;
    padding: 8px 0 4px;
    background: linear-gradient(to top, var(--bg-0) 72%, transparent);
    z-index: 2;
  }
}

/* §34-fix (reward overlay) — NARROW short viewports (portrait phones). With up to 5 buttons the
   pinned row grew to ~60% of the viewport and COVERED the celebration text scrolling behind it.
   Keep the 1-2 primary CTAs full-width + prominent, but pack the secondary nav (Mine / Progress /
   Home) into ONE short row of compact buttons so the pinned block stays small and the haul stays
   readable above it. Scoped to ≤480px wide so WIDE short viewports (iPad landscape) keep their
   natural-width buttons instead of stretching a primary across the whole screen. */
@media (max-height: 800px) and (max-width: 480px) {
  .reward .row .btn.primary {
    flex: 1 1 100%;
    min-height: 54px;
    font-size: 1.1rem;
  }
  .reward .row .btn:not(.primary) {
    flex: 1 1 0;
    min-width: 0;
    min-height: 46px;
    padding: 8px 10px;
    font-size: 0.95rem;
    white-space: nowrap;
  }
}

/* LANDSCAPE / very-short reward (≤520px tall): even compact stacked buttons + the celebration
   text overflow a 360px-tall band, so the haul scrolls behind the pinned row. Drop the purely
   decorative bits (big emoji, the Total/next-step hint paragraphs) and — since landscape is WIDE —
   let every button sit inline (auto width) so all 5 fit in ~2 short rows. Placed AFTER the ≤800px
   reward block so it wins where both match. */
@media (max-height: 520px) {
  .reward {
    gap: 5px;
  }
  .reward .big {
    display: none;
  }
  .reward h2 {
    font-size: 1.25rem;
  }
  .reward .earned {
    font-size: 1.1rem;
  }
  .reward > p {
    display: none;
  }
  .reward .row .btn.primary,
  .reward .row .btn:not(.primary) {
    flex: 0 1 auto;
    min-height: 46px;
    font-size: 0.95rem;
  }
}

/* The boss/geode body already scrolls (justify-content: safe center; overflow-y:auto),
   but in short viewports pin its action row to the bottom too so "Keep crafting / Home"
   is always visible without a hunt-scroll. Same ≤800px phone band as the reward. */
@media (max-height: 800px) {
  .boss .row {
    position: sticky;
    bottom: 0;
    margin-top: auto;
    padding: 8px 0 4px;
    background: linear-gradient(to top, var(--bg-0) 68%, transparent);
    z-index: 2;
  }
}

/* ── Printables (§28.C): offline practice sheets ───────────────────────────── */
.print-controls {
  margin-bottom: 18px;
}
.print-sub {
  margin-top: 10px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.print-sub-label {
  font-weight: 700;
  font-size: 0.95rem;
  opacity: 0.85;
}
.print-select {
  width: 100%;
  min-height: var(--tap-min);
  border-radius: 14px;
  border: 2px solid var(--panel-border);
  background: var(--panel);
  color: inherit;
  font-size: 1.05rem;
  font-weight: 700;
  padding: 8px 12px;
}
/* The sheet preview on screen: a white "paper" so a grown-up sees what prints. */
.printable-sheet {
  background: #ffffff;
  color: #111111;
  border-radius: 12px;
  padding: 22px 24px;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
}
.printable-sheet .sheet-head {
  border-bottom: 2px solid #111;
  padding-bottom: 10px;
  margin-bottom: 16px;
}
.printable-sheet .sheet-title {
  margin: 0 0 6px;
  font-size: 1.5rem;
  color: #111;
}
.printable-sheet .sheet-meta {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  font-size: 1rem;
  font-weight: 600;
  color: #333;
}
.printable-sheet .word-list {
  margin: 0;
  padding-left: 1.4em;
  columns: 2;
  column-gap: 32px;
  font-size: 1.35rem;
  line-height: 2;
}
.printable-sheet .word-list li {
  break-inside: avoid;
}
.printable-sheet .sheet-empty {
  font-size: 1.1rem;
  color: #444;
}
.printable-sheet table.lcwc {
  width: 100%;
  border-collapse: collapse;
  font-size: 1.15rem;
}
.printable-sheet table.lcwc th,
.printable-sheet table.lcwc td {
  border: 1.5px solid #222;
  padding: 10px 8px;
  text-align: left;
}
.printable-sheet table.lcwc th {
  background: #eee;
  font-size: 0.95rem;
}
.printable-sheet .lcwc-n {
  width: 2.2em;
  text-align: center;
  color: #555;
}
.printable-sheet .lcwc-word {
  font-weight: 800;
  white-space: nowrap;
}
.printable-sheet .lcwc-blank {
  min-width: 5.5em;
}

/* ============================================================================
   REDUCED-MOTION (accessibility + professional polish). The app uses a lot of
   decorative/looping motion (screen-in, breathe, glint, bob, geo-cheer, drift,
   socket-glow, geode-shake, nudge-pulse…). Some kids — and the vestibular-
   sensitive — set the OS "reduce motion" preference; honour it. We KEEP brief,
   meaning-carrying feedback (the press/active scale, correct/wrong tile states)
   but cut the looping ambience and the entrance animation to a near-instant
   fade, so nothing pulses or drifts continuously. Placed LAST so it wins.
   ============================================================================ */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    /* kill INFINITE/decorative loops; leave short one-shot transitions intact */
    animation-iteration-count: 1 !important;
    animation-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  /* the screen entrance + appear pops should simply not move the layout */
  .screen {
    animation: none !important;
  }
  /* still allow the quick tactile press feedback (transform on :active) */
  .crystal-cell.affordable,
  .crystal-cell.owned .crystal-art {
    animation: none !important;
  }
}

/* Print: hide ALL app chrome, show only the sheet, full black-on-white. */
@media print {
  body {
    background: #fff;
  }
  body * {
    visibility: hidden;
  }
  .printable-sheet,
  .printable-sheet * {
    visibility: visible;
  }
  .printable-sheet {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    box-shadow: none;
    border-radius: 0;
    padding: 0;
  }
  .no-print {
    display: none !important;
  }
}

/* ── Feedback archive (§28.A, developer-only) ──────────────────────────────── */
.feedback-archive { display: flex; flex-direction: column; gap: 12px; }
.archive-count { font-weight: 700; opacity: 0.8; margin: 2px 2px 0; }
.feedback-entry { padding: 14px 16px; }
.feedback-entry .fb-row {
  display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
}
.feedback-entry .fb-stars { font-size: 1.1rem; letter-spacing: 1px; }
.feedback-entry .fb-when { font-size: 0.85rem; opacity: 0.65; white-space: nowrap; }
.feedback-entry .fb-meta {
  display: flex; flex-wrap: wrap; gap: 8px 14px; margin: 6px 0 4px; font-size: 0.95rem; font-weight: 600;
}
.feedback-entry .fb-nick { opacity: 0.9; }
.feedback-entry .fb-diff { opacity: 0.75; }
.feedback-entry .fb-note { margin: 4px 0 0; line-height: 1.4; white-space: pre-wrap; }
.feedback-entry .fb-note.muted { opacity: 0.5; font-style: italic; }
