/* ==========================================================================
   Starloom Studio — Global Styles
   A moodier, more sophisticated cosmic theme built for a tween, not a
   toddler: near-black indigo space, electric violet/magenta/teal accents,
   sleeker rectangular-ish UI instead of bubbly pill shapes, and subtle
   (not bouncy) motion. Dyslexia-friendly typography is preserved throughout.
   ========================================================================== */

/* Fonts are bundled locally in fonts/ (no Google Fonts / jsDelivr CDN calls —
   keeps the "nothing leaves the device" privacy promise literally true and
   works offline/file://). Lexend is the default body font; OpenDyslexic is
   an opt-in toggle (My Artist Profile). Both faces declare their own bold
   weight so text isn't browser-synthesized ("faux bold"). */
@font-face {
  font-family: 'Lexend';
  src: url('../fonts/Lexend-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Lexend';
  src: url('../fonts/Lexend-SemiBold.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Lexend';
  src: url('../fonts/Lexend-Bold.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Lexend';
  src: url('../fonts/Lexend-ExtraBold.woff2') format('woff2');
  font-weight: 800;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'OpenDyslexic';
  src: url('../fonts/OpenDyslexic-Regular.woff') format('woff');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  /* OpenDyslexic only ships regular + bold — map the whole 600-800 range
     used across the UI to the real bold face instead of letting the
     browser fake-bold the regular face. */
  font-family: 'OpenDyslexic';
  src: url('../fonts/OpenDyslexic-Bold.woff') format('woff');
  font-weight: 600 800;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Deep, moody space background — closer to black than purple. */
  --space-black: #07060f;
  --space-deep: #0e0a23;
  --space-mid: #1a1338;
  --space-card: #181230;

  /* Electric, jewel-toned cosmic accents (no pastel/bubblegum tones). */
  --accent-violet: #8b5cf6;
  --accent-magenta: #d6336c;
  --accent-teal: #2dd4bf;
  --accent-cyan: #22d3ee;
  --accent-gold: #f2b94d;

  --star-white: #f5f3ff;
  --success: #2dd4a8;
  --retry: #f59e6c;
  --muted: #6b6490;
  --text-main: #ece9ff;
  --text-dim: #a59fc9;

  --glow: 0 0 16px rgba(139, 92, 246, 0.45);
  --glow-tight: 0 0 10px rgba(139, 92, 246, 0.35);
  --radius-lg: 16px;
  --radius-md: 10px;
  --radius-sm: 8px;
  /* Lexend is the default (modern, well-supported, already the fallback);
     OpenDyslexic is a parent-toggleable option — see .font-opendyslexic below. */
  --font-main: 'Lexend', -apple-system, 'Segoe UI', Roboto, sans-serif;
  --header-height: 66px;
  --header-height-mobile: 108px; /* taller on narrow screens where the nav row wraps */

  /* Size of hero-mode Nova. Drives BOTH her box (.companion-hero) and the
     space the hero screens reserve for her below the header (see the
     #app-root push rule near the companion layer) — keeping them in one
     variable is what stops her body from ever landing on the heading. */
  --nova-hero-size: clamp(120px, 22vw, 180px);

  /* Nova companion "star freckle" glow slots. --nova-glow-1..4 are left
     UNDEFINED here on purpose — a later task sets them inline (per-student,
     from progress.js's unlocked colors). --nova-unlit is the fallback dim
     color every glow slot's var() resolves to until it's lit. */
  --nova-unlit: rgba(165, 159, 201, 0.35); /* dim version of --text-dim */
}

/* Applied to <html> by app.js on boot when the student has opted into
   OpenDyslexic in My Artist Profile. Overriding the custom property means
   every rule using var(--font-main) picks it up with no per-rule changes. */
html.font-opendyslexic {
  --font-main: 'OpenDyslexic', 'Lexend', -apple-system, 'Segoe UI', Roboto, sans-serif;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  min-height: 100%;
  background:
    radial-gradient(ellipse 900px 500px at 15% 0%, rgba(139, 92, 246, 0.16), transparent 60%),
    radial-gradient(ellipse 700px 500px at 90% 20%, rgba(45, 212, 191, 0.10), transparent 55%),
    linear-gradient(180deg, var(--space-deep) 0%, var(--space-black) 70%);
  color: var(--text-main);
  font-family: var(--font-main);
  font-size: 18px;
  line-height: 1.65;
  letter-spacing: 0.015em;
  overflow-x: hidden;
}

/* The header is position:fixed (see #app-header below) so it's removed from
   normal document flow — this padding reserves its space at the top of
   every screen so content never renders underneath it. */
body { padding-top: var(--header-height); }

/* Starfield background — finer, dimmer points than a "kid app" sparkle field */
#stars-bg {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(1.5px 1.5px at 20% 30%, #fff, transparent),
    radial-gradient(1.5px 1.5px at 65% 15%, #fff, transparent),
    radial-gradient(1px 1px at 80% 60%, #fff, transparent),
    radial-gradient(1px 1px at 35% 75%, #fff, transparent),
    radial-gradient(1.5px 1.5px at 90% 85%, #fff, transparent),
    radial-gradient(1px 1px at 10% 90%, #fff, transparent),
    radial-gradient(1px 1px at 50% 50%, #fff, transparent);
  background-repeat: repeat;
  background-size: 600px 600px;
  opacity: 0.4;
  animation: drift 140s linear infinite;
}

@keyframes drift {
  from { background-position: 0 0; }
  to { background-position: -1200px 800px; }
}

/* ---------------- Header ---------------- */

#app-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  min-height: var(--header-height);
  display: flex;
  align-items: center;
  padding: 12px 22px;
  background: rgba(7, 6, 15, 0.78);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid rgba(139, 92, 246, 0.22);
}

.header-inner {
  width: 100%;
  max-width: 1000px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
}

.brand {
  display: flex;
  align-items: center;
  gap: 9px;
  font-weight: 700;
  font-size: 1.2rem;
  color: var(--star-white);
  letter-spacing: 0.03em;
}

.brand-icon { font-size: 1.4rem; filter: drop-shadow(0 0 6px rgba(139, 92, 246, 0.6)); }
/* The header brand glyph is now the paint-star app mark (SVG) instead of an
   emoji — size it to match the old 1.4rem cap height; the glow above still applies. */
.brand-icon svg { display: block; width: 1.4rem; height: 1.4rem; }

.header-nav {
  display: flex;
  gap: 8px;
  flex: 1;
}

.nav-btn {
  font-family: var(--font-main);
  display: flex;
  align-items: center;
  gap: 6px;
  background: rgba(139, 92, 246, 0.12);
  border: 1px solid rgba(139, 92, 246, 0.35);
  color: var(--text-main);
  border-radius: var(--radius-sm);
  padding: 8px 14px;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}

.nav-btn:hover { background: rgba(139, 92, 246, 0.22); border-color: var(--accent-violet); transform: translateY(-1px); }
.nav-btn.hidden { display: none; }
.nav-btn-icon { font-size: 1rem; }

/* ---- UI icon set (P2-IDENTITY; sprite lives in index.html) ----
   Base for every <svg class="ui-icon"><use href="#icon-…">. Sized in em so it
   scales with the surrounding text, inherits its color via currentColor, and
   keeps one consistent rounded-outline weight across the whole set. */
.ui-icon {
  width: 1em;
  height: 1em;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  vertical-align: -0.14em;
  flex: none;
}
.nav-btn-icon .ui-icon { width: 1.15em; height: 1.15em; }
.stat { display: inline-flex; align-items: center; gap: 5px; }
.stat-icon { width: 1.05em; height: 1.05em; }
.profile-menu-item { display: flex; align-items: center; gap: 10px; }
.menu-icon { width: 1.2em; height: 1.2em; }

/* Screen-reader-only text — labels the otherwise icon-only stat pills so a
   reader announces "Day streak: 3" instead of a bare "3". */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

.header-stats { display: flex; gap: 10px; }
.header-stats.hidden { display: none; }

.profile-circle {
  flex-shrink: 0;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: 2px solid var(--accent-violet);
  background: linear-gradient(135deg, var(--accent-violet), var(--accent-magenta));
  color: var(--star-white);
  font-family: var(--font-main);
  font-weight: 700;
  font-size: 1rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.profile-circle:hover { transform: translateY(-1px); box-shadow: 0 0 12px rgba(139, 92, 246, 0.5); }
.profile-circle.hidden { display: none; }
.profile-circle[aria-expanded="true"] { box-shadow: 0 0 12px rgba(139, 92, 246, 0.6); }

/* Tap-to-toggle profile menu — a small popover anchored to the top-right,
   just under the fixed header. Tap-toggle (not hover) so it works on touch
   when wrapped for iOS, where it can later grow into a full-width sheet. */
.profile-menu {
  position: fixed;
  top: calc(var(--header-height) - 6px);
  right: 16px;
  z-index: 60;
  min-width: 210px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px;
  background: var(--space-card);
  border: 1px solid rgba(139, 92, 246, 0.4);
  border-radius: var(--radius-md);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5), var(--glow-tight);
}

.profile-menu.hidden { display: none; }

.profile-menu-item {
  font-family: var(--font-main);
  text-align: left;
  background: transparent;
  border: none;
  color: var(--text-main);
  border-radius: var(--radius-sm);
  padding: 11px 12px;
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s ease;
}

.profile-menu-item:hover { background: rgba(139, 92, 246, 0.18); }

@media (max-width: 520px) {
  /* On narrow screens the header is taller (the nav row wraps), so anchor the
     menu below that taller header and let it span most of the width. */
  .profile-menu {
    top: calc(var(--header-height-mobile) - 6px);
    right: 8px;
    left: 8px;
    min-width: 0;
  }

  /* AUDIT #1 fix: the profile circle is the ONLY route to My Artist Profile /
     Switch Artist / Parent Zone. On a phone-width header, brand + nav +
     header-stats (Level/streak/stars) used to fill the whole row and push the
     profile circle past the right edge — with body overflow-x hidden, it was
     unreachable. Two changes fix that without touching desktop:
     1) Hide the header-stats pills here. They're secondary (also shown on the
        dashboard/lesson screens), so dropping them from the cramped header
        frees up the room the profile circle needs.
     2) Let the header row wrap as a safety net for anything still tight, so
        content degrades gracefully instead of clipping off-screen. */
  .header-stats {
    display: none;
  }

  .header-inner {
    flex-wrap: wrap;
  }
}

.stat {
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(139, 92, 246, 0.25);
  padding: 6px 11px;
  border-radius: var(--radius-sm);
  font-size: 0.88rem;
  font-weight: 600;
  color: var(--text-dim);
}

/* ---------------- Layout ---------------- */

#app-root {
  position: relative;
  z-index: 1;
  max-width: 1000px;
  margin: 0 auto;
  padding: 30px 20px 60px;
  min-height: calc(100vh - var(--header-height));
  display: flex;
  align-items: center;
  justify-content: center;
}

.screen { width: 100%; animation: fade-in 0.4s ease; }

/* Accessibility: a heading focused programmatically after a screen change
   (see moveFocusToScreen in app.js) shouldn't show a focus outline — it's a
   non-interactive title receiving focus only so screen readers announce it.
   Interactive controls keep their normal focus indicator. */
.title-glow[tabindex="-1"]:focus { outline: none; }

/* Accessibility (AUDIT #11): moveFocusToScreen() in app.js focuses the new
   screen's first h1/h2 so keyboard/screen-reader users land at the top of
   each screen. The browser then scrolls that heading into view — but with no
   scroll-margin-top it can land UNDER the fixed header (title half-hidden;
   seen live on the consent screen). scroll-margin-top tells the browser to
   leave a gap above the heading equal to the header's height (plus a small
   buffer) when scrolling it into view. Not every screen's heading uses
   .title-glow (e.g. the warm-up/new-skill/sentence-quest titles), so this
   targets h1/h2 generally to match moveFocusToScreen's own selector. */
h1, h2 { scroll-margin-top: calc(var(--header-height) + 12px); }

/* AUDIT #11 (cascade fix): on narrow screens the header is taller
   (--header-height-mobile, the nav row wraps), so the scroll gap above a
   focused heading needs to be bigger here too, or it still tucks in behind
   the header. This override is placed IMMEDIATELY AFTER the base h1/h2 rule
   above (same selector = equal specificity) so source order guarantees it
   wins at <=520px; the base rule still wins above that width. Do not move
   this away from the base rule or a later same-specificity rule could win
   instead depending on where it lands in the file. */
@media (max-width: 520px) {
  h1, h2 { scroll-margin-top: calc(var(--header-height-mobile) + 12px); }
}

/* Accessibility: the "Skip to main content" link. It's pulled off-screen by
   default (not display:none, so screen readers and the keyboard can still
   reach it) and slides into view only when it receives keyboard focus, so a
   keyboard user can jump past the fixed header straight to the content. */
/* "Skip to main content" — a KEYBOARD-only accessibility aid: hidden until a
   keyboard user Tabs to it (the first focus on the page), then it slides in to
   let them jump past the header straight to the content. It is intentionally
   NOT for mouse users, so it must never catch the pointer. We park it well
   off-screen and disable pointer events until it's focused. */
.skip-link {
  position: absolute;
  left: 8px;
  top: -96px; /* parked well above the top edge, fully out of sight */
  z-index: 1000;
  padding: 10px 16px;
  background: var(--accent-purple, #8b5cf6);
  color: #fff;
  border-radius: var(--radius-md, 10px);
  text-decoration: none;
  font-weight: 600;
  pointer-events: none; /* the mouse can never hover/catch it while hidden */
  transition: top 0.15s ease;
}
.skip-link:focus {
  top: 8px; /* drops into view only on keyboard focus */
  pointer-events: auto;
  outline: 2px solid var(--accent-teal, #2dd4bf);
  outline-offset: 2px;
}

/* Accessibility: respect the OS "reduce motion" setting. This matters here
   beyond the usual reasons — our reader has a seizure history, so honoring a
   reduced-motion preference is a safety-minded default. We near-disable the
   infinite/looping animations (the drifting starfield, the spinning ring, the
   mic pulse) and shorten transitions to effectively-instant, while keeping
   layout and functionality identical. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* Test mode (open the app with ?test=1): the exact same animation kill-switch
   as prefers-reduced-motion above, but forced by a class app.js puts on <html>.
   Exists for automated verification — looping animations keep the renderer
   busy, which makes headless screenshot capture time out. Never active during
   normal use (requires the explicit query flag). */
html.test-mode *,
html.test-mode *::before,
html.test-mode *::after {
  animation-duration: 0.001ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.001ms !important;
  scroll-behavior: auto !important;
}

@keyframes fade-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}

/* ---------------- Cards ---------------- */

.welcome-card, .story-card, .assessment-card, .lesson-card {
  max-width: 620px;
  margin: 0 auto;
  background: linear-gradient(165deg, rgba(26, 19, 56, 0.85), rgba(14, 10, 35, 0.92));
  border: 1px solid rgba(139, 92, 246, 0.25);
  border-radius: var(--radius-lg);
  padding: 38px 32px;
  text-align: center;
  box-shadow: 0 16px 50px rgba(0, 0, 0, 0.5);
}

.title-glow {
  color: var(--star-white);
  background: linear-gradient(120deg, var(--star-white) 30%, var(--accent-violet) 70%, var(--accent-teal));
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  margin-top: 0;
  font-size: 1.6rem;
  font-weight: 700;
  letter-spacing: 0.01em;
}

.subtitle, .story-text, .lesson-instruction {
  color: var(--text-dim);
  font-size: 1.02rem;
}

.welcome-art, .story-art {
  font-size: 2.4rem;
  margin: 16px 0;
  opacity: 0.92;
}

/* Stacked brand lockup (the chosen "stacked" wordmark) — the paint-star mark
   over the STARLOOM / studio wordmark. Used on the welcome screen in place of
   the old emoji cluster, set in Lexend (the app's default, already bundled).
   The negative right margin cancels the trailing letter-spacing so each
   letter-spaced line stays optically centered. */
.brand-lockup {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  margin: 18px 0;
}
.brand-lockup svg { display: block; width: 46px; height: 46px; filter: drop-shadow(0 0 8px rgba(139, 92, 246, 0.5)); }
.brand-lockup .lockup-name {
  font-weight: 800;
  font-size: 1.35rem;
  letter-spacing: 0.28em;
  margin-right: -0.28em;
  color: var(--star-white);
}
.brand-lockup .lockup-studio {
  font-weight: 400;
  font-size: 0.8rem;
  letter-spacing: 0.42em;
  margin-right: -0.42em;
  color: var(--text-dim);
}

.unlock-burst { animation: pop 0.5s ease; }
@keyframes pop {
  0% { transform: scale(0.5); opacity: 0; }
  70% { transform: scale(1.08); opacity: 1; }
  100% { transform: scale(1); }
}

/* ---------------- Buttons ---------------- */

.btn {
  font-family: var(--font-main);
  font-size: 0.98rem;
  font-weight: 600;
  border: none;
  border-radius: var(--radius-md);
  padding: 12px 24px;
  cursor: pointer;
  transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease, border-color 0.15s ease;
  margin: 6px;
}

.btn:hover { transform: translateY(-2px); }
.btn:active { transform: translateY(0); }
.btn:focus-visible { outline: 2px solid var(--accent-teal); outline-offset: 2px; }
.btn:disabled { opacity: 0.45; cursor: default; pointer-events: none; transform: none; }

.btn-primary {
  background: linear-gradient(120deg, var(--accent-violet), var(--accent-magenta));
  color: var(--star-white);
  box-shadow: 0 6px 20px rgba(139, 92, 246, 0.3);
}

.btn-lg { font-size: 1.05rem; padding: 15px 30px; }

.btn-secondary {
  background: rgba(255, 255, 255, 0.06);
  color: var(--text-main);
  border: 1px solid rgba(255, 255, 255, 0.18);
}

.btn-success {
  background: rgba(45, 212, 168, 0.18);
  color: var(--success);
  border: 1px solid rgba(45, 212, 168, 0.5);
}

.btn-muted {
  background: rgba(107, 100, 144, 0.18);
  color: var(--text-main);
  border: 1px solid var(--muted);
}

.btn-icon {
  background: rgba(255, 255, 255, 0.07);
  color: var(--accent-teal);
  font-size: 1.2rem;
  padding: 10px 13px;
  border-radius: var(--radius-sm);
}

.btn-mic {
  background: rgba(34, 211, 238, 0.12);
  color: var(--accent-cyan);
  border: 1px solid rgba(34, 211, 238, 0.4);
}

.btn-mic:hover { background: rgba(34, 211, 238, 0.22); }

/* ---------------- Inputs ---------------- */

.text-input {
  font-family: var(--font-main);
  font-size: 1.15rem;
  padding: 12px 18px;
  border-radius: var(--radius-md);
  border: 2px solid var(--accent-violet);
  background: rgba(255, 255, 255, 0.05);
  color: var(--text-main);
  width: 80%;
  max-width: 320px;
  margin: 16px 0;
  text-align: center;
}

.text-input::placeholder { color: var(--text-dim); }

/* ---------------- Assessment / Portal ---------------- */

.portal-ring {
  width: 210px;
  height: 210px;
  margin: 8px auto 18px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(139, 92, 246, 0.18), rgba(14, 10, 35, 0.5));
  border: 2px solid var(--accent-violet);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--glow-tight);
  position: relative;
}

.portal-ring::before {
  content: '';
  position: absolute;
  inset: -8px;
  border-radius: 50%;
  border: 1px solid rgba(45, 212, 191, 0.3);
  animation: ring-spin 14s linear infinite;
}

@keyframes ring-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.portal-word {
  font-size: 2rem;
  font-weight: 700;
  color: var(--star-white);
  word-break: break-word;
  padding: 0 10px;
}

.assessment-prompt { margin: 18px 0 6px; color: var(--text-dim); }
.assessment-actions { display: flex; justify-content: center; flex-wrap: wrap; }

.mic-status {
  min-height: 1.3em;
  font-size: 0.92rem;
  color: var(--text-dim);
  margin: 8px 0;
}

.mic-listening {
  color: var(--accent-cyan);
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.mic-listening::after {
  content: '';
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent-cyan);
  animation: mic-pulse 1s ease-in-out infinite;
}

@keyframes mic-pulse {
  0%, 100% { opacity: 0.3; transform: scale(0.8); }
  50% { opacity: 1; transform: scale(1.4); }
}

.btn-mic.listening {
  border-color: var(--accent-cyan);
  box-shadow: 0 0 18px rgba(34, 211, 238, 0.55);
  animation: mic-btn-pulse 1.2s ease-in-out infinite;
}

@keyframes mic-btn-pulse {
  0%, 100% { box-shadow: 0 0 10px rgba(34, 211, 238, 0.35); }
  50% { box-shadow: 0 0 22px rgba(34, 211, 238, 0.75); }
}

.mic-success { color: var(--success); font-weight: 600; }
.mic-retry { color: var(--retry); }

/* "Ready" state for the manual mic fallback button — shown when the browser's
   gesture policy blocks the automatic mic start after the audio clip finishes.
   The gentle pulsing glow makes it very clear this button is the next thing
   to tap. Respects prefers-reduced-motion (the reduced-motion block below
   near-disables all looping animations — seizure safety). */
.btn-mic.btn-mic-ready {
  border-color: var(--accent-cyan);
  box-shadow: 0 0 14px rgba(34, 211, 238, 0.5);
  animation: mic-ready-pulse 1.6s ease-in-out infinite;
}

@keyframes mic-ready-pulse {
  0%, 100% { box-shadow: 0 0 8px rgba(34, 211, 238, 0.3); }
  50% { box-shadow: 0 0 20px rgba(34, 211, 238, 0.7); }
}

.mic-status .btn-mic-retry {
  margin-left: 8px;
  padding: 4px 12px;
  font-size: 0.82rem;
  border-radius: var(--radius-sm, 8px);
}

.progress-track {
  height: 6px;
  width: 100%;
  background: rgba(255, 255, 255, 0.08);
  border-radius: 999px;
  margin-top: 22px;
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--accent-violet), var(--accent-teal));
  width: 0%;
  transition: width 0.4s ease;
}

/* ---------------- Home screen ---------------- */

.screen-home { width: 100%; }
.screen-home h2 { text-align: center; }

.home-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 18px;
  margin-top: 26px;
}

.big-tile {
  font-family: var(--font-main);
  background: linear-gradient(165deg, rgba(26, 19, 56, 0.85), rgba(14, 10, 35, 0.92));
  border: 1px solid rgba(139, 92, 246, 0.22);
  border-radius: var(--radius-lg);
  padding: 28px 16px;
  color: var(--text-main);
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}

.big-tile:hover { transform: translateY(-3px); box-shadow: 0 12px 32px rgba(139, 92, 246, 0.22); border-color: rgba(139, 92, 246, 0.5); }

.big-tile:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  filter: grayscale(40%);
}

.big-tile:disabled:hover { transform: none; box-shadow: none; border-color: rgba(139, 92, 246, 0.22); }
.tile-icon { font-size: 2.1rem; opacity: 0.95; line-height: 1; }
/* Home-tile icons (P2-IDENTITY batch 2): the sprite SVG fills the .tile-icon
   font-size box; display:block stops the span adding phantom baseline height.
   Each activity keeps its own accent colour so the grid stays as colourful as
   the emoji it replaced (the Canvas palette and Star Log star bring their own
   colours from the sprite itself). */
.tile-icon .ui-icon { display: block; width: 1em; height: 1em; }
.tile-lesson .tile-icon { color: var(--accent-violet); }
.tile-sentences .tile-icon { color: var(--accent-cyan); }
.tile-story .tile-icon { color: #ff9a5a; }   /* warm orange — same family as the streak flame */
.tile-gallery .tile-icon { color: #ff5ea8; } /* pink — matches the palette's paint dot */
.tile-arcade .tile-icon { color: var(--accent-teal); } /* echoes this tile's teal border */
.tile-wordbuilder .tile-icon { color: var(--accent-violet); }
.tile-label { font-weight: 700; font-size: 1.05rem; }
.tile-sub { color: var(--text-dim); font-size: 0.88rem; }

/* ---------------- Lesson screens ---------------- */

.lesson-banner {
  text-align: center;
  color: var(--accent-teal);
  font-weight: 700;
  font-size: 0.85rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  margin-bottom: 14px;
}

.big-letter-display {
  font-size: 2.7rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  margin: 18px 0;
  color: var(--star-white);
}

.word-display {
  font-size: 2.4rem;
  font-weight: 700;
  margin: 22px 0;
  color: var(--star-white);
}

.word-sound-hint {
  font-size: 1.6rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--accent-teal);
  margin: -10px 0 18px;
}

/* Sentence Quest (sentence-level reading). The text wraps and uses generous
   line spacing so a whole phrase/sentence/passage stays dyslexia-friendly. */
.sentence-tier-label {
  text-align: center;
  color: var(--accent-teal);
  font-weight: 700;
  font-size: 0.8rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  margin: 4px 0 10px;
}

.sentence-display {
  font-size: 1.9rem;
  font-weight: 700;
  line-height: 1.7;
  letter-spacing: 0.02em;
  margin: 10px 0 20px;
  color: var(--star-white);
  max-width: 36ch;
  margin-left: auto;
  margin-right: auto;
}

/* ---------------- Story Time (reading comprehension) ---------------- */
.story-text {
  font-size: 1.65rem;
  font-weight: 700;
  line-height: 1.8;
  letter-spacing: 0.02em;
  margin: 10px auto 18px;
  color: var(--star-white);
  max-width: 34ch;
  text-align: left;
  background: rgba(139, 92, 246, 0.12);
  border: 1px solid rgba(139, 92, 246, 0.3);
  border-radius: 16px;
  padding: 18px 22px;
}
.story-question {
  margin-top: 22px;
}
.story-question-counter {
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--accent-gold);
  letter-spacing: 0.04em;
  margin-bottom: 4px;
}
.story-question-prompt {
  font-size: 1.5rem;
  font-weight: 800;
  color: var(--star-white);
  margin: 4px 0 16px;
}
.story-choices {
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-width: 24rem;
  margin: 0 auto 8px;
}
/* Big, full-width, finger-friendly answer buttons. */
.story-choice {
  width: 100%;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.15s ease, opacity 0.25s ease, background 0.2s ease;
}
.story-choice.chosen-correct {
  background: rgba(45, 212, 191, 0.85);
  border-color: var(--accent-teal);
  color: #06231f;
  transform: scale(1.03);
}
.story-choice.faded { opacity: 0.4; }
.story-choice.wobble { animation: minigame-wobble 0.5s ease; }

.lesson-progress-dots {
  display: flex;
  justify-content: center;
  gap: 7px;
  margin-top: 18px;
}

.dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.18);
}

.dot.active { background: var(--accent-violet); box-shadow: 0 0 8px var(--accent-violet); }
.dot.done { background: var(--success); }

/* Small, muted "Word 3 of 6"-style counter — concrete progress info for
   kids who benefit from a number, kept quiet so it never competes with the
   word itself. Empty when there's nothing to show (the :empty rule hides
   its margin so a blank counter leaves no gap). */
.progress-counter {
  font-size: 0.8rem;
  color: var(--text-dim);
  text-align: center;
  margin: 10px 0 0;
}
.progress-counter:empty { margin: 0; }

/* ---------------- Canvas ---------------- */

.screen-canvas { width: 100%; max-width: 940px; }

.canvas-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(139, 92, 246, 0.18);
  border-radius: var(--radius-md);
  padding: 10px 14px;
}

.tool-group { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }

.color-swatch, .stamp-btn {
  width: 32px;
  height: 32px;
  border-radius: var(--radius-sm);
  border: 2px solid rgba(255, 255, 255, 0.25);
  cursor: pointer;
}

.color-swatch.selected, .stamp-btn.selected {
  border-color: var(--accent-teal);
  box-shadow: 0 0 10px var(--accent-teal);
}

.color-swatch.locked, .stamp-btn.locked {
  opacity: 0.28;
  cursor: not-allowed;
  filter: grayscale(60%);
}

/* "NEW" highlight: a just-unlocked color/stamp glows and wears a little star
   badge so she can actually find what she just earned (it used to look the
   same as everything else). Cleared after her first canvas visit. */
.color-swatch.newly-unlocked, .stamp-btn.newly-unlocked {
  position: relative;
  border-color: var(--accent-gold, #ffc857);
  box-shadow: 0 0 12px var(--accent-gold, #ffc857);
  animation: newly-unlocked-pulse 1.4s ease-in-out infinite;
}
.color-swatch.newly-unlocked::after, .stamp-btn.newly-unlocked::after {
  content: "⭐";
  position: absolute;
  top: -10px;
  right: -10px;
  font-size: 0.85rem;
  line-height: 1;
  filter: drop-shadow(0 0 3px rgba(0, 0, 0, 0.6));
  pointer-events: none;
}
@keyframes newly-unlocked-pulse {
  0%, 100% { box-shadow: 0 0 8px var(--accent-gold, #ffc857); }
  50% { box-shadow: 0 0 16px var(--accent-gold, #ffc857); }
}

/* ---- Switch Artist (multi-student picker) ---- */
.student-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-width: 22rem;
  margin: 18px auto;
}
.student-row {
  width: 100%;
  text-align: center;
}
.student-row.active {
  border-color: var(--accent-teal, #2dd4bf);
  box-shadow: 0 0 12px var(--accent-teal, #2dd4bf);
}

/* The big color disc shown on the unlock screen (the actual earned color). */
.unlock-color-swatch {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  margin: 0 auto 8px;
  border: 3px solid rgba(255, 255, 255, 0.5);
  box-shadow: 0 0 26px rgba(255, 200, 87, 0.7);
  animation: pop 0.5s ease;
}

.stamp-btn {
  background: rgba(255, 255, 255, 0.06);
  font-size: 1.05rem;
  display: flex;
  align-items: center;
  justify-content: center;
}

.canvas-wrap { position: relative; }

#art-canvas {
  width: 100%;
  aspect-ratio: 900 / 560;
  background: var(--space-black);
  border-radius: var(--radius-md);
  border: 1px solid rgba(139, 92, 246, 0.25);
  touch-action: none;
  cursor: crosshair;
}

.canvas-tip {
  position: absolute;
  top: 16px;
  left: 50%;
  transform: translateX(-50%);
  max-width: 90%;
  display: flex;
  align-items: center;
  gap: 14px;
  background: rgba(14, 10, 35, 0.95);
  border: 1px solid var(--accent-teal);
  border-radius: var(--radius-md);
  padding: 12px 16px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
  animation: fade-in 0.3s ease;
}

/* `.canvas-tip` and the browser's built-in `[hidden] { display: none }` rule
   have equal CSS specificity, and `.canvas-tip` is declared later in this
   file, so it was winning the cascade — `tip.hidden = true` in JS was firing
   correctly, but the tooltip never actually disappeared visually. This rule
   gives [hidden] the extra specificity it needs to win instead. */
.canvas-tip[hidden] {
  display: none;
}

/* ---- Brush size slider ---- */

/* The group that holds the label, slider, and preview dot sits in the
   toolbar flex row with the same alignment as the color/stamp groups. */
.canvas-brush-group {
  align-items: center;
  gap: 10px;
  flex-wrap: nowrap;
}

/* Small label so the child can see what the slider does at a glance. */
.canvas-brush-label {
  font-family: var(--font-main);
  font-size: 0.78rem;
  color: var(--text-muted, rgba(255, 255, 255, 0.55));
  white-space: nowrap;
  user-select: none;
}

/* The range input itself — wide enough to be comfortable on a tablet,
   with a tall touch target and cosmic purple theming. */
.canvas-brush-slider {
  -webkit-appearance: none;
  appearance: none;
  width: 120px;
  height: 6px;
  border-radius: 3px;
  background: linear-gradient(
    to right,
    var(--accent-violet, #7c3aed),
    var(--accent-teal, #14b8a6)
  );
  outline: none;
  cursor: pointer;
  /* Make the whole bar taller so fingers can land on it easily. */
  padding: 10px 0;
  box-sizing: content-box;
}

/* The draggable thumb — large enough for a child's finger (44 px touch
   target matches WCAG 2.5.5 AAA). */
.canvas-brush-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--accent-gold, #ffc857);
  border: 2px solid rgba(255, 255, 255, 0.7);
  box-shadow: 0 0 8px rgba(255, 200, 87, 0.55);
  cursor: pointer;
  transition: box-shadow 0.15s ease, transform 0.1s ease;
}
.canvas-brush-slider::-moz-range-thumb {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--accent-gold, #ffc857);
  border: 2px solid rgba(255, 255, 255, 0.7);
  box-shadow: 0 0 8px rgba(255, 200, 87, 0.55);
  cursor: pointer;
  transition: box-shadow 0.15s ease, transform 0.1s ease;
}
.canvas-brush-slider:active::-webkit-slider-thumb {
  box-shadow: 0 0 16px rgba(255, 200, 87, 0.8);
  transform: scale(1.15);
}
.canvas-brush-slider:active::-moz-range-thumb {
  box-shadow: 0 0 16px rgba(255, 200, 87, 0.8);
  transform: scale(1.15);
}

/* Live preview circle: a filled circle that grows/shrinks in real time
   to show the current brush dot size. The JS sets its width and height
   directly so the visual matches exactly what will land on the canvas. */
.canvas-brush-preview {
  display: inline-block;
  background: var(--text-main, #fdfcff);
  border-radius: 50%;
  /* Start at the default brush radius (10 px / 2 = 5 px visible radius →
     10 px diameter). JS updates width + height live. */
  width: 10px;
  height: 10px;
  flex-shrink: 0;
  /* Smooth resize so the preview doesn't jump when dragging fast. */
  transition: width 0.08s ease, height 0.08s ease;
}

@media (max-width: 540px) {
  /* On small phones give the slider more room by letting it expand; the
     toolbar already wraps so it will sit on its own line when needed. */
  .canvas-brush-slider {
    width: 90px;
  }
}

.canvas-tip p { margin: 0; font-size: 0.92rem; color: var(--text-main); }
.canvas-tip .btn { margin: 0; flex-shrink: 0; padding: 8px 16px; font-size: 0.85rem; }

@media (max-width: 540px) {
  .canvas-tip { flex-direction: column; text-align: center; }
}

/* ---------------- Profile panel / Voice picker ---------------- */

.voice-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 18px 0 10px;
  max-height: 360px;
  overflow-y: auto;
  text-align: left;
}

.voice-option {
  font-family: var(--font-main);
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(139, 92, 246, 0.2);
  border-radius: var(--radius-sm);
  padding: 12px 16px;
  color: var(--text-main);
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}

.voice-option:hover { background: rgba(139, 92, 246, 0.12); }
.voice-option.selected { border-color: var(--accent-teal); box-shadow: 0 0 0 1px var(--accent-teal) inset; }

.voice-name { font-weight: 600; }
.voice-lang { color: var(--text-dim); font-size: 0.85rem; }

.voice-unsupported-msg { color: var(--text-dim); font-size: 0.95rem; }

.settings-block {
  text-align: left;
  margin: 22px 0;
  padding-bottom: 18px;
  border-bottom: 1px solid rgba(139, 92, 246, 0.15);
}

.settings-block:last-of-type { border-bottom: none; }

.settings-subhead {
  color: var(--star-white);
  font-size: 1rem;
  margin: 0 0 4px;
}

/* Used when a second labeled setting shares one .settings-block with the
   first (e.g. Calm Mode living under Nova's Presence) — adds breathing
   room above the second subheading without a whole extra block/divider. */
.settings-subhead-spaced {
  margin-top: 16px;
}

.field-row {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
}

.field-row .text-input { width: auto; flex: 1; min-width: 220px; margin: 8px 0; text-align: left; }

/* A simple labeled checkbox row (e.g. the OpenDyslexic font toggle) — the
   whole row is the clickable/focusable label, and the native checkbox is
   sized up so it's easy to tap on a tablet. */
.toggle-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 8px;
  cursor: pointer;
  color: var(--text-main);
}

.toggle-row input[type="checkbox"],
.toggle-row input[type="radio"] {
  width: 22px;
  height: 22px;
  accent-color: var(--accent-violet);
  cursor: pointer;
}

.field-status {
  font-size: 0.88rem;
  color: var(--text-dim);
  min-height: 1.2em;
}

/* ---------------- Privacy promise & first-run consent ---------------- */

/* The short bullet summary on the consent screen. */
.consent-summary {
  text-align: left;
  margin: 16px 0;
  padding-left: 22px;
  line-height: 1.6;
}
.consent-summary li { margin: 8px 0; }

/* The collapsible full policy on the consent screen. */
.privacy-details {
  text-align: left;
  margin: 14px 0 20px;
  border: 1px solid rgba(139, 92, 246, 0.25);
  border-radius: var(--radius-md, 10px);
  padding: 10px 14px;
}
.privacy-details > summary {
  cursor: pointer;
  font-weight: 600;
  color: var(--accent-teal, #2dd4bf);
}

/* Full policy body — shared by the consent details and the Privacy screen. */
.privacy-body { text-align: left; line-height: 1.6; }
.privacy-heading {
  color: var(--star-white);
  font-size: 1rem;
  margin: 18px 0 4px;
}
.privacy-text { margin: 0 0 8px; }
.privacy-updated {
  margin-top: 18px;
  font-size: 0.85rem;
  color: var(--text-dim);
  font-style: italic;
}
.screen-privacy { max-width: 680px; margin: 0 auto; }

.voice-option.loading { opacity: 0.55; cursor: progress; }

.avatar-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 10px;
}

.avatar-option {
  width: 46px;
  height: 46px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.2);
  background: rgba(255, 255, 255, 0.05);
  font-size: 1.4rem;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: border-color 0.15s ease, transform 0.15s ease;
}

.avatar-option:hover { transform: translateY(-2px); }
.avatar-option.selected { border-color: var(--accent-teal); box-shadow: 0 0 10px var(--accent-teal); }

/* Character-creation-screen sizing for onboarding — bigger tap targets than
   the compact profile-panel picker. */
.avatar-picker-lg {
  justify-content: center;
  gap: 16px;
  margin: 18px 0;
}

.avatar-picker-lg .avatar-option {
  width: 64px;
  height: 64px;
  font-size: 2rem;
}

.onboarding-confirm-avatar {
  font-size: 3.5rem;
  margin: 14px 0;
}

.parent-settings-divider {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 26px 0 6px;
  color: var(--text-dim);
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.parent-settings-divider::before,
.parent-settings-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: rgba(139, 92, 246, 0.25);
}

.parent-settings-placeholder { text-align: left; font-size: 0.85rem; }

/* ---- Parent PIN + Parent Zone form helpers ---- */

/* A small caption above an input (used on the PIN / recovery / change forms). */
.field-label {
  display: block;
  text-align: left;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text-dim);
  margin: 10px 0 2px;
}

/* A more compact secondary button for low-emphasis actions like "Forgot PIN?" */
.btn-sm {
  padding: 6px 12px;
  font-size: 0.85rem;
  margin-top: 8px;
}

/* The PIN screen's three modes are plain settings-blocks; the inputs should
   stack full-width rather than sit inline. */
.screen-parent-pin .text-input,
.screen-parent-zone details .text-input {
  width: 100%;
  margin: 4px 0 6px;
  text-align: left;
}

/* Collapsible parent-zone sections (Change PIN, Clear data) use <details>. */
.settings-block > summary.settings-subhead {
  cursor: pointer;
  list-style: none;
}
.settings-block > summary.settings-subhead::-webkit-details-marker { display: none; }
.settings-block > summary.settings-subhead::before {
  content: '▸ ';
  color: var(--accent-violet);
}
.settings-block[open] > summary.settings-subhead::before { content: '▾ '; }

/* The "clear all data" block is destructive — tint it so it reads as danger. */
.danger-block[open] { border-bottom-color: rgba(214, 51, 108, 0.4); }
.danger-block > summary.settings-subhead { color: var(--accent-magenta); }
.danger-block > summary.settings-subhead::before { color: var(--accent-magenta); }

.danger-btn {
  border-color: rgba(214, 51, 108, 0.6);
  color: #ffd0e0;
}
.danger-btn:hover {
  background: rgba(214, 51, 108, 0.18);
  border-color: var(--accent-magenta);
}

/* ---- Parent Dashboard (PR B) ---- */

/* Top-line stat tiles: level, streak, stars, sessions. */
.dash-stat-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px;
  margin: 18px 0 8px;
}

.dash-stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 14px 8px;
  background: rgba(139, 92, 246, 0.1);
  border: 1px solid rgba(139, 92, 246, 0.3);
  border-radius: var(--radius-md);
}

.dash-stat-value {
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--star-white);
}

.dash-stat-label {
  font-size: 0.78rem;
  color: var(--text-dim);
  text-align: center;
}

/* On narrow screens drop to two tiles per row. */
@media (max-width: 520px) {
  .dash-stat-grid { grid-template-columns: repeat(2, 1fr); }
}

/* A skill group in "words to watch" / uncertain lists. */
.dash-skill-group { margin: 12px 0; }

.dash-skill-name {
  font-weight: 700;
  color: var(--accent-cyan);
  margin: 6px 0 2px;
  text-align: left;
}

.dash-word-list {
  color: var(--text-main);
  text-align: left;
  margin: 0;
  font-size: 0.95rem;
}

/* "Time for a calibration check?" nudge — a gentle gold-accented call-out so
   it reads as a friendly invitation, not an alert. Hidden until due. */
.dash-reassess {
  border: 1px solid rgba(242, 185, 77, 0.5); /* soft gold edge */
  border-radius: 14px;
  background: rgba(242, 185, 77, 0.08);
  padding: 14px 16px;
  margin: 14px 0;
}
.dash-reassess .settings-subhead { color: var(--accent-gold); }
.dash-reassess .btn { margin-top: 8px; }

/* Discreet parent-only debug panel — collapsed by default via <details>. */
.debug-panel {
  margin: 18px 0 8px;
  text-align: left;
  font-size: 0.78rem;
  color: var(--text-dim);
}

.debug-panel summary {
  cursor: pointer;
  user-select: none;
  font-size: 0.78rem;
  color: var(--text-dim);
  opacity: 0.7;
}

.debug-panel summary:hover { opacity: 1; }

.debug-panel-body {
  margin-top: 8px;
  padding: 10px 12px;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid rgba(139, 92, 246, 0.15);
  border-radius: var(--radius-sm);
  font-family: 'Courier New', monospace;
  font-size: 0.74rem;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 220px;
  overflow-y: auto;
}

.debug-panel-body.debug-ok { color: var(--success); }
.debug-panel-body.debug-error { color: var(--retry); }

/* ---------------- Progress screen ---------------- */

.progress-summary {
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(139, 92, 246, 0.18);
  border-radius: var(--radius-md);
  padding: 18px;
  margin: 16px 0 26px;
}

.progress-summary p { margin: 6px 0; }

.unlock-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
  gap: 12px;
  margin-bottom: 26px;
}

.unlock-item {
  background: rgba(255, 255, 255, 0.05);
  border-radius: var(--radius-md);
  padding: 14px 8px;
  text-align: center;
  font-size: 1.5rem;
  border: 1px solid rgba(139, 92, 246, 0.2);
}

.unlock-item .unlock-name {
  display: block;
  font-size: 0.68rem;
  color: var(--text-dim);
  margin-top: 4px;
}

/* ---------------- Misc ---------------- */

@media (max-width: 540px) {
  .welcome-card, .story-card, .assessment-card, .lesson-card { padding: 26px 16px; }
  body { font-size: 17px; }
  .header-nav { flex-wrap: wrap; }
  .nav-btn-label { display: none; }
}

@media (max-width: 380px) {
  /* Very narrow screens can wrap the header onto two rows (brand + nav +
     profile circle); give content extra breathing room so nothing tucks
     in behind the taller fixed header. */
  body { padding-top: var(--header-height-mobile); }
  #app-root { min-height: calc(100vh - var(--header-height-mobile)); }
}

::selection { background: var(--accent-violet); color: var(--star-white); }

/* ---------------- Mini-games (js/screens/minigame.js) ----------------
   The short, no-failure-state fun beats. All the float/drift motion below is
   gentle and is fully disabled by the prefers-reduced-motion block above
   (important given Kayden's seizure history) — the games still lay out and
   play fine as a calm, static grid when motion is reduced. */

.minigame-instruction {
  font-size: 1.4rem;
  font-weight: 700;
  color: var(--star-white);
  margin: 6px 0 16px;
}

.minigame-arena {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 18px;
  min-height: 200px;
  margin: 10px auto 18px;
  max-width: 520px;
}

/* Touch-friendliness for ALL game tap targets: `manipulation` removes the
   ~300ms tap delay and disables double-tap-zoom on touch screens (phones,
   Windows touch), and tap-highlight is suppressed so taps feel native. The
   targets themselves are intentionally large (90px+). */
.minigame-star,
.minigame-bubble,
.minigame-pathstar {
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}

/* --- Star Catch --- */
.minigame-star {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Widened from 130px → 150px so 9-letter words (wonderful, adventure,
     telescope) have comfortable room inside the star glyph. */
  width: 150px;
  height: 120px;
  background: none;
  border: none;
  cursor: pointer;
  animation: minigame-float 3.2s ease-in-out infinite;
  animation-delay: var(--drift-delay, 0s);
  transition: transform 0.15s ease;
}
.minigame-star:hover { transform: scale(1.06); }
.minigame-star-glyph {
  position: absolute;
  font-size: 6.2rem;
  line-height: 1;
  filter: drop-shadow(0 0 10px rgba(242, 185, 77, 0.55));
}
.minigame-star-word {
  position: relative;
  z-index: 1;
  /* clamp() shrinks the font for long words: min 0.85rem (still readable),
     preferred 3.8vw (scales with viewport), max 1.4rem (normal words). This
     prevents 9-letter words from overflowing the star on any screen size. */
  font-size: clamp(0.85rem, 3.8vw, 1.4rem);
  font-weight: 800;
  /* White text is immediately readable against the dark star emoji body.
     The dark navy (#1b1340) had poor contrast on the deep-blue star. */
  color: #ffffff;
  letter-spacing: 0.02em;
  /* Multi-directional text-shadow creates a crisp dark halo behind the white
     letters. This ensures the word is legible no matter what's behind it
     (dark star, bright glow) — a dyslexia-friendly high-contrast technique. */
  text-shadow:
    1px  1px 0 #1b1340,
   -1px  1px 0 #1b1340,
    1px -1px 0 #1b1340,
   -1px -1px 0 #1b1340,
    0    2px 4px rgba(0, 0, 0, 0.6);
  /* Safety net: long words cannot overflow their container. */
  max-width: 90%;
  word-break: break-word;
}
.minigame-star.caught {
  animation: none;
  transform: scale(1.18);
}
.minigame-star.caught .minigame-star-glyph {
  filter: drop-shadow(0 0 18px rgba(242, 185, 77, 0.95));
}
.minigame-star.faded { opacity: 0.3; animation: none; }
.minigame-star.wobble { animation: minigame-wobble 0.5s ease; }

/* --- Paint Pop --- */
.minigame-bubble {
  width: 92px;
  height: 92px;
  border-radius: 50%;
  border: 3px solid rgba(255, 255, 255, 0.55);
  cursor: pointer;
  box-shadow: 0 0 16px rgba(255, 255, 255, 0.25), inset 0 0 18px rgba(255, 255, 255, 0.3);
  animation: minigame-float 3s ease-in-out infinite;
  animation-delay: var(--drift-delay, 0s);
  transition: transform 0.18s ease, opacity 0.3s ease;
}
.minigame-bubble:hover { transform: scale(1.1); }
.minigame-bubble.popped {
  transform: scale(1.5);
  opacity: 0;
  pointer-events: none;
  animation: none;
}

/* --- Constellation Canvas (the redesigned paint-pop reward game) -----------
   Layout: the arena is a vertical flex column. Inside it:
     - .cc-palette: a horizontal scrollable row of color swatches at the top.
     - .cc-canvas: a relative-position box where the shape cells live.
       Cells (.cc-cell) are absolutely positioned using the brain's x/y
       percentages so they form the chosen cosmic shape at any screen size.

   ACCESSIBILITY / SEIZURE SAFETY — no strobe, no fast loops, no flashing:
     1. Empty cell "twinkle" is a gentle 3-second opacity pulse (one slow wave).
        The global prefers-reduced-motion block (css/style.css ~line 294) sets
        animation-duration:0.001ms and iteration-count:1 — effectively static.
        The game is fully playable without any motion.
     2. The reveal (.cc-canvas-revealed) is a one-shot CSS transition (0.6s on
        box-shadow and filter). Reduced-motion collapses it to near-instant.
     3. The swatch "selected" ring is a pure CSS border change (no animation).
   All three are safe for Kayden's seizure history.
   ----------------------------------------------------------------- */

/* The arena is a flex column: palette on top, then the canvas below. */
.cc-arena {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  width: 100%;
  max-width: 520px;
  padding: 0 4px;
}

/* Palette row: a horizontally-scrollable strip of color swatches.
   Overflow-x: auto lets it scroll on very small screens without truncating. */
.cc-palette {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
  width: 100%;
  padding: 6px 0;
}

/* Each swatch: a round color button with a generous touch target.
   The border is the selection indicator — no animation needed. */
.cc-swatch {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 3px solid transparent;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
  transition: transform 0.12s ease, border-color 0.12s ease;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  flex-shrink: 0;
}
.cc-swatch:hover { transform: scale(1.12); }
.cc-swatch:focus-visible {
  outline: 2px solid var(--accent-teal);
  outline-offset: 3px;
}

/* Selected swatch: a bright white ring makes the active color unmistakably obvious. */
.cc-swatch-selected {
  border-color: rgba(255, 255, 255, 0.95);
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.6), 0 2px 8px rgba(0, 0, 0, 0.45);
  transform: scale(1.18);
}

/* The shape canvas: a relative box that holds all the cells.
   Proportioned as a tall rectangle so the shape has vertical room. */
.cc-canvas {
  position: relative;
  width: 100%;
  max-width: 360px;
  height: 340px;
  border-radius: var(--radius-lg);
  border: 1.5px solid rgba(139, 92, 246, 0.22);
  background: rgba(7, 6, 15, 0.55);
  overflow: hidden;
  transition: box-shadow 0.6s ease, filter 0.6s ease;
}

/* When all cells are filled, the canvas glows with a celebration pulse.
   One-shot transition (not a loop) — safe for seizure history.
   Reduced-motion collapses this to near-instant. */
.cc-canvas.cc-canvas-revealed {
  box-shadow: 0 0 36px 10px rgba(139, 92, 246, 0.5),
              inset 0 0 28px rgba(139, 92, 246, 0.2);
  filter: brightness(1.12);
}

/* Each cell: a circular dot absolutely positioned inside the canvas.
   - Empty cells are faint outlines with a slow twinkle (opacity pulse).
   - Filled cells are solid circles in the selected color. */
.cc-cell {
  position: absolute;
  transform: translate(-50%, -50%); /* centre the cell on its x/y position */
  width: 42px;
  height: 42px;
  border-radius: 50%;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: background 0.18s ease, transform 0.15s ease;
}
.cc-cell:hover { transform: translate(-50%, -50%) scale(1.14); }
.cc-cell:focus-visible {
  outline: 2px solid var(--accent-teal);
  outline-offset: 2px;
}

/* Empty cell: a translucent ring that gently twinkles so the child can see
   where to tap. The slow animation is covered by prefers-reduced-motion. */
.cc-cell-empty {
  background: rgba(255, 255, 255, 0.08);
  border: 2px solid rgba(255, 255, 255, 0.35);
  animation: cc-twinkle 3s ease-in-out infinite;
}
@keyframes cc-twinkle {
  0%,  100% { border-color: rgba(255, 255, 255, 0.35); background: rgba(255, 255, 255, 0.08); }
  50%        { border-color: rgba(255, 255, 255, 0.65); background: rgba(255, 255, 255, 0.18); }
}

/* Filled cell: solid color circle with a soft inner glow. No animation. */
.cc-cell-filled {
  border: 2px solid rgba(255, 255, 255, 0.5);
  box-shadow: inset 0 0 8px rgba(255, 255, 255, 0.25), 0 2px 6px rgba(0, 0, 0, 0.3);
  animation: none; /* remove the twinkle once a cell is painted */
}

/* --- Coloring Page (Phase 3.5 — tap whole regions of a scene to color it) ---
   Reuses .cc-arena + .cc-palette + .cc-swatch for the palette row. The scene is
   ONE SVG (viewBox 0 0 100 100) of tappable <path> regions.

   ACCESSIBILITY / SEIZURE SAFETY — no strobe, no fast loops:
     1. Empty regions show a faint outline + a slow one-wave stroke twinkle; the
        global prefers-reduced-motion block collapses it to static.
     2. The finish glow (.coloring-reveal box-shadow/filter) is a one-shot CSS
        transition; the extra scale "pop" is DOUBLE-gated by prefers-reduced-
        motion AND Calm Mode (body:not(.calm-mode)) — the glow still applies
        without motion so completion always feels rewarding.
   --------------------------------------------------------------------------- */

/* The SVG canvas: a square frame that sits under the palette row. */
.coloring-svg {
  width: 100%;
  max-width: 340px;
  aspect-ratio: 1 / 1;
  height: auto;
  border-radius: var(--radius-lg);
  border: 1.5px solid rgba(139, 92, 246, 0.22);
  background: rgba(7, 6, 15, 0.55);
  transition: box-shadow 0.6s ease, filter 0.6s ease;
  touch-action: manipulation;
}

/* Each region: a tappable path. Faint outline while empty; solid fill once colored. */
.coloring-region {
  stroke: rgba(255, 255, 255, 0.55);
  stroke-width: 1;
  stroke-linejoin: round;
  cursor: pointer;
  transition: fill 0.18s ease;
  -webkit-tap-highlight-color: transparent;
}
.coloring-region:focus-visible {
  outline: 2px solid var(--accent-teal);
  outline-offset: 2px;
}

/* Empty region: near-transparent fill + a slow one-wave stroke twinkle so the
   child can see where to tap. The slow animation is covered by reduced-motion. */
.coloring-region-empty {
  fill: rgba(255, 255, 255, 0.06);
  animation: coloring-twinkle 3s ease-in-out infinite;
}
@keyframes coloring-twinkle {
  0%, 100% { stroke: rgba(255, 255, 255, 0.4); }
  50%      { stroke: rgba(255, 255, 255, 0.75); }
}

/* Filled region: keeps the child's chosen color (set inline via fill=), no twinkle. */
.coloring-region-filled {
  animation: none;
  stroke: rgba(255, 255, 255, 0.5);
}

/* Finish reveal: a one-shot celebratory glow on the whole scene, plus a gentle
   scale pop (motion-gated). The glow is a plain transition so it still fires
   under reduced-motion / Calm Mode. */
.coloring-svg.coloring-reveal {
  box-shadow: 0 0 36px 10px rgba(139, 92, 246, 0.5),
              inset 0 0 28px rgba(139, 92, 246, 0.2);
  filter: brightness(1.12);
}
@media (prefers-reduced-motion: no-preference) {
  body:not(.calm-mode) .coloring-svg.coloring-reveal {
    animation: coloring-reveal-pop 900ms ease-out 1;
  }
}
@keyframes coloring-reveal-pop {
  0%   { transform: scale(1); }
  30%  { transform: scale(1.04); }
  100% { transform: scale(1); }
}

/* --- Star Path (tap stars in order to trace a named constellation) --- */

/* The constellation name label that appears above the star field. It tells the
   child which real constellation she is about to trace, making the activity feel
   meaningful rather than just "tap random stars". Styled to look like a soft
   title card in the cosmic theme — gold text, generous size, easy to read.
   No animation here (static label) — fully safe for prefers-reduced-motion. */
.star-path-name-label {
  position: absolute;
  top: 4px;
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--font-main);
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--accent-gold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  pointer-events: none; /* taps go through to the stars below */
  z-index: 2;
  text-shadow: 0 0 12px rgba(242, 185, 77, 0.55);
}

.star-path-arena {
  position: relative;
  width: 100%;
  max-width: 480px;
  height: 300px;
  display: block;
}
.star-path-lines {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none; /* taps go to the stars, not the line overlay */
}
.star-path-line {
  fill: none;
  stroke: var(--accent-gold);
  stroke-width: 0.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 3px rgba(242, 185, 77, 0.8));
  vector-effect: non-scaling-stroke;
}
.minigame-pathstar {
  position: absolute;
  transform: translate(-50%, -50%); /* x/y position the star's CENTRE */
  /* Small white star DOTS (claude-design spec 2026-07-09): the constellation
     reads as a shape, not a cluster of big stars. The button is a larger
     TRANSPARENT hit area (comfortable 46px touch target) around a small
     visible dot (::before). */
  width: 46px;
  height: 46px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  cursor: pointer;
  opacity: 0.65;                /* untapped stars sit dim until they're traced */
  transition: opacity 0.2s ease, transform 0.15s ease;
}
/* The visible star = a small white dot with a soft glow. The NEXT star grows to
   24px to hold its numeral (see below); the size change is a one-shot property
   transition, so it lands fine under reduced motion / Calm Mode. */
.minigame-pathstar::before {
  content: '';
  position: absolute;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #ffffff;
  box-shadow: 0 0 7px rgba(255, 255, 255, 0.75);
  transition: width 0.2s ease, height 0.2s ease;
}
/* The order numeral shows ONLY on the next star to tap: the guiding pulse says
   WHERE, the numeral on that same star says WHY that one — no field of numbers
   to scan (a real cost with dyslexia). Tapped stars drop their numeral; the
   traced line is the record of the order. */
.minigame-pathstar-num {
  display: none;
  position: relative;
  z-index: 1;                   /* the number sits centred ON the white dot */
  font-size: 0.78rem;
  font-weight: 800;
  color: #1b1340;
}
.minigame-pathstar.pathstar-next .minigame-pathstar-num { display: block; }
.minigame-pathstar:hover { transform: translate(-50%, -50%) scale(1.08); }
.minigame-pathstar.lit { opacity: 1; }             /* traced star: full brightness */
.minigame-pathstar.lit::before {
  box-shadow: 0 0 12px rgba(255, 255, 255, 0.95);
}
.minigame-pathstar.wobble { animation: minigame-pathstar-wobble 0.5s ease; }

/* Gentle twinkle on the small dots — slow, one-wave, seizure-safe. Only the
   untapped, non-next dots twinkle; double-gated by reduced-motion + Calm Mode. */
@media (prefers-reduced-motion: no-preference) {
  body:not(.calm-mode) .minigame-pathstar:not(.lit):not(.pathstar-next)::before {
    animation: pathstar-twinkle 3s ease-in-out infinite;
  }
}
@keyframes pathstar-twinkle {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.5; }
}

/* The next star to tap gets a gentle guiding pulse so she always knows where to
   go. The pulse is DOUBLE-gated (prefers-reduced-motion + Calm Mode); when motion
   is off it falls back to a static full-opacity + soft glow so the guidance is
   still visible without any animation. */
.minigame-pathstar.pathstar-next { opacity: 1; }
.minigame-pathstar.pathstar-next::before {
  width: 24px;                  /* grows to hold the numeral (0.2s transition) */
  height: 24px;
  box-shadow: 0 0 9px rgba(255, 255, 255, 0.9), 0 0 0 3px rgba(242, 185, 77, 0.6);
}
@media (prefers-reduced-motion: no-preference) {
  body:not(.calm-mode) .minigame-pathstar.pathstar-next {
    animation: pathstar-next-pulse 1.4s ease-in-out infinite;
  }
}
@keyframes pathstar-next-pulse {
  0%, 100% { transform: translate(-50%, -50%) scale(1); }
  50%      { transform: translate(-50%, -50%) scale(1.16); }
}

/* Completed trace: a one-shot brighter glow on the whole line. It's a filter
   change (transition), so it still lands under reduced-motion / Calm Mode. */
.star-path-line.star-path-complete {
  stroke-width: 1.1;
  filter: drop-shadow(0 0 7px rgba(242, 185, 77, 1)) drop-shadow(0 0 14px rgba(242, 185, 77, 0.6));
  transition: filter 0.6s ease, stroke-width 0.6s ease;
}

/* The translucent line-figure revealed behind a completed constellation.
   Polished art (claude-design 2026-07-09) carries opacity 0.22 — an "oh, I drew
   a swan!" whisper that never competes with the traced line or the dots. Gold
   by default; themed worlds recolor it via the SAME .game-theme-* overrides as
   the traced line (see the .star-path-line theme group below), so figure and
   line can never mismatch. Motion-gated one-shot fade-in. */
.star-figure {
  fill: none;
  stroke: var(--accent-gold);
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.22;
}
@media (prefers-reduced-motion: no-preference) {
  body:not(.calm-mode) .star-figure {
    animation: star-figure-fade 900ms ease-out 1;
  }
}
@keyframes star-figure-fade {
  0%   { opacity: 0; }
  100% { opacity: 0.22; }
}

/* The "Star Fact" banner shown at completion (claude-design Deliverable C): a
   visible home for the spoken fact, under the praise line, so she can re-read
   it after the voice finishes. Reveal = the same one-shot 900ms fade the figure
   uses, double-gated by reduced-motion + Calm Mode; no looping motion. */
.star-fact-banner {
  max-width: 480px;             /* matches the arena width */
  margin: 10px auto 0;
  padding: 14px 18px;
  border: 1px solid rgba(242, 185, 77, 0.4);
  border-radius: 12px;
  background: rgba(242, 185, 77, 0.08);
  text-align: left;
}
.star-fact-kicker {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent-gold);
}
.star-fact-text {
  margin: 6px 0 0;
  font-size: 0.92rem;
  color: var(--text-main);
}
@media (prefers-reduced-motion: no-preference) {
  body:not(.calm-mode) .star-fact-banner {
    animation: star-fact-fade 900ms ease-out 1;
  }
}
@keyframes star-fact-fade {
  0%   { opacity: 0; }
  100% { opacity: 1; }
}

/* Centre-preserving wobble for the absolutely-positioned path stars (the plain
   .wobble would drop their translate(-50%,-50%) centring mid-animation). */
@keyframes minigame-pathstar-wobble {
  0%, 100% { transform: translate(-50%, -50%) rotate(0); }
  25% { transform: translate(-50%, -50%) rotate(-7deg); }
  75% { transform: translate(-50%, -50%) rotate(7deg); }
}

@keyframes minigame-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-12px); }
}
@keyframes minigame-wobble {
  0%, 100% { transform: translateX(0) rotate(0); }
  25% { transform: translateX(-7px) rotate(-5deg); }
  75% { transform: translateX(7px) rotate(5deg); }
}

/* --- Sound Studio (tap-to-place blending game) ---
   Layout: a progress star row at the top, a slot row below it (one slot per
   sound chunk), then the shuffled tile tray at the bottom. All elements are
   large enough for comfortable touch use. Float/drift animations from other
   games are intentionally absent here (no looping motion) — a brief snap/glow
   on tile placement is fine because it's one-shot and short. The existing
   prefers-reduced-motion block near-disables all animations globally, so we
   get the calm static layout automatically on reduced-motion devices. */

.sound-studio-arena {
  flex-direction: column;  /* stack the three rows vertically */
  align-items: center;
  gap: 20px;
  padding: 12px 4px;
}

/* Progress stars — 3 small stars showing which rounds are done. */
.ss-progress-row {
  display: flex;
  gap: 14px;
  justify-content: center;
  align-items: center;
}
.ss-progress-star {
  font-size: 1.6rem;
  color: var(--muted);
  transition: color 0.2s ease, transform 0.2s ease;
}
.ss-star-done {
  color: var(--accent-gold);
  /* A one-shot scale-up when a star fills — not looping, seizure-safe. */
  animation: ss-star-pop 0.35s ease;
}
@keyframes ss-star-pop {
  0%   { transform: scale(1); }
  50%  { transform: scale(1.35); }
  100% { transform: scale(1); }
}

/* Slot row — empty landing pads that fill as tiles are placed. */
.ss-slot-row {
  display: flex;
  gap: 10px;
  justify-content: center;
  flex-wrap: wrap;
}
.ss-slot {
  min-width: 72px;
  min-height: 72px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-md);
  font-family: var(--font-main);
  font-size: 1.55rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  transition: background 0.18s ease, box-shadow 0.18s ease;
}
/* Empty slot: a faint glowing outline so the child knows where tiles go. */
.ss-slot-empty {
  border: 2.5px dashed var(--muted);
  background: rgba(255, 255, 255, 0.04);
  color: transparent; /* no text yet */
}
/* Filled slot: glows and shows the grapheme. The colour matches the tile type. */
.ss-slot-filled {
  border: 2.5px solid transparent;
}
/* Vowel slot: gold glow (matches vowel tile colour) */
.ss-slot-filled.ss-slot-vowel {
  background: rgba(242, 185, 77, 0.18);
  box-shadow: 0 0 14px rgba(242, 185, 77, 0.55);
  color: var(--accent-gold);
}
/* Consonant slot: cyan/blue glow (matches consonant tile colour) */
.ss-slot-filled.ss-slot-consonant {
  background: rgba(34, 211, 238, 0.13);
  box-shadow: 0 0 14px rgba(34, 211, 238, 0.45);
  color: var(--accent-cyan);
}

/* Tile tray — the shuffled buttons the child taps. */
.ss-tray {
  display: flex;
  gap: 12px;
  justify-content: center;
  flex-wrap: wrap;
  padding: 8px 4px;
}
/* Base tile styles — large touch targets, dyslexia-friendly font. */
.ss-tile {
  min-width: 80px;
  min-height: 80px;
  padding: 10px 16px;
  border-radius: var(--radius-md);
  font-family: var(--font-main);
  font-size: 1.7rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  cursor: pointer;
  border: 3px solid transparent;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.12s ease, opacity 0.2s ease, box-shadow 0.2s ease;
}
.ss-tile:hover:not(:disabled) { transform: scale(1.07); }
/* Vowel tiles: gold — matches OG vowel colour convention. */
.ss-tile-vowel {
  background: rgba(242, 185, 77, 0.22);
  border-color: var(--accent-gold);
  color: var(--accent-gold);
  box-shadow: 0 0 10px rgba(242, 185, 77, 0.35);
}
/* Consonant tiles: cyan/blue — OG consonant colour convention. */
.ss-tile-consonant {
  background: rgba(34, 211, 238, 0.14);
  border-color: var(--accent-cyan);
  color: var(--accent-cyan);
  box-shadow: 0 0 10px rgba(34, 211, 238, 0.3);
}
/* Placed tile: fades out so the child can see which ones are still available. */
.ss-tile-placed {
  opacity: 0.28;
  transform: scale(0.92);
  cursor: default;
}
/* Wobble: reuses the existing keyframe, applied on wrong-tile tap. */
.ss-tile.wobble { animation: minigame-wobble 0.5s ease; }

/* ---------------- Phase 3.5: multi-round runs + pop-the-sounds -------------
   The run progress trail, the between-round / finish "juice", and the floating
   sound bubbles. ALL motion here is one-shot (or a gentle gated loop) and
   DOUBLE-GATED: the global prefers-reduced-motion / test-mode blocks near the
   top of this file collapse every animation to ~instant, and the rules below
   only apply when motion is allowed AND Calm Mode is off. NOTE: Calm Mode is a
   class on <body> (body.calm-mode), so the gate is body:not(.calm-mode).
   Seizure-safety stays intact — no looping flashes, no fast strobes. */

/* Run progress trail: one dot per round, sits above the arena. */
.run-trail {
  display: flex;
  gap: 8px;
  justify-content: center;
  align-items: center;
  margin: 8px 0 4px;
}
.run-trail-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.22);
  transition: background 0.2s ease, box-shadow 0.2s ease;
}
.run-trail-dot.run-dot-done { background: var(--accent-gold); }
.run-trail-dot.run-dot-current {
  background: #fff;
  box-shadow: 0 0 0 3px rgba(242, 185, 77, 0.35);
}

/* Between-round + finish juice — one-shot, motion- and Calm-Mode-gated. */
@media (prefers-reduced-motion: no-preference) {
  body:not(.calm-mode) #minigame-surface.run-beat {
    animation: run-beat-pulse 500ms ease-out 1;
  }
  body:not(.calm-mode) #minigame-surface.run-finish {
    animation: run-finish-pop 900ms ease-out 1;
  }
}
@keyframes run-beat-pulse {
  0%   { transform: scale(1); }
  50%  { transform: scale(1.02); }
  100% { transform: scale(1); }
}
@keyframes run-finish-pop {
  0%   { transform: scale(1); }
  30%  { transform: scale(1.04); }
  100% { transform: scale(1); }
}

/* Pop-the-sounds: floating sound bubbles tapped in word order. Colour comes
   from the shared .ss-tile-vowel / .ss-tile-consonant classes (gold / cyan) —
   this base rule deliberately does NOT set background / border-color / color. */
.sound-pop-arena {
  flex-direction: column;
  align-items: center;
  gap: 18px;
  padding: 12px 4px;
}
.sound-pop-field {
  display: flex;
  gap: 16px;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
  padding: 8px 4px;
}
.sound-pop-bubble {
  min-width: 84px;
  min-height: 84px;
  padding: 10px 16px;
  border-radius: 50%;
  border-width: 3px;
  border-style: solid;
  font-family: var(--font-main);
  font-size: 1.7rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.12s ease, opacity 0.2s ease, box-shadow 0.2s ease;
}
.sound-pop-bubble:hover:not(:disabled) { transform: scale(1.07); }
.sound-pop-bubble.wobble { animation: minigame-wobble 0.5s ease; }
/* Popped bubble: fades + shrinks so the remaining sounds stand out. */
.sound-pop-popped {
  opacity: 0.25;
  transform: scale(0.9);
  cursor: default;
}
/* Gentle idle bob so the bubbles feel alive. Gated by motion + Calm Mode, and
   skipped for popped bubbles so their shrink shows cleanly. --bob-delay staggers
   each bubble so they don't bob in sync. */
@media (prefers-reduced-motion: no-preference) {
  body:not(.calm-mode) .sound-pop-bubble:not(.sound-pop-popped) {
    animation: sound-pop-bob 2.8s ease-in-out infinite;
    animation-delay: var(--bob-delay, 0s);
  }
}
@keyframes sound-pop-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-5px); }
}

/* ---------------- Per-planet mini-game theming (Phase 3, Wave 4a) ----------
   A LIGHT reskin so each planet's games feel like that planet: the four games
   pick up the CURRENT WORLD's accent colors. js/screens/minigame.js sets two
   custom properties + a `game-theme-<worldId>` class on the stable
   `.lesson-card` wrapper of the mini-game screen; the rules below read those
   variables to tint the backdrop and glow the game targets.

   SCOPE = colors/backdrop only (owner decision — deeper mechanics are Phase
   3.5). NO new animation is added here (seizure-safety stays exactly as-is);
   we only recolor existing static/one-shot styles.

   SAFE FALLBACKS: every var() has a fallback equal to the game's ORIGINAL
   color, so if minigame.js can't resolve a world (StarloomWorld absent on the
   file:// edge, tests, a partial load) the games look completely unchanged.

   CONTRAST (dyslexia-friendly rule): backdrop tints are kept very light (a
   low-opacity wash via a ::before overlay, never a saturated fill) so word/
   number text and tap targets stay high-contrast. Touch targets are unchanged.
   ------------------------------------------------------------------------- */

/* Backdrop wash: a soft per-world tint behind the play area. Applied as a
   low-opacity overlay UNDER the game content (::before, z-index below the
   arena's children) so it never washes out the high-contrast targets/text.
   The arena is positioned so the absolutely-placed overlay is contained. */
/* NOTE: matched by attribute ([class*="game-theme-"]) instead of naming each
   world id — the old per-world lists silently skipped Zephyra and Lunara when
   those worlds shipped (untheme games at L13+). minigame.js is the only
   emitter of game-theme-<worldId> classes, so the blanket match is safe and
   every future world is themed for free. */
[class*="game-theme-"] .minigame-arena {
  position: relative;
  border-radius: var(--radius-lg);
}
[class*="game-theme-"] .minigame-arena::before {
  content: '';
  position: absolute;
  inset: -6px;
  /* Negative z-index so the wash sits BEHIND every game target/label without
     us having to touch each child's positioning. (An earlier draft lifted the
     children with `position: relative`, which clobbered Star Path's absolutely-
     positioned stars/line — this keeps their layout untouched.) */
  z-index: -1;
  border-radius: var(--radius-lg);
  pointer-events: none;             /* never intercept taps meant for targets */
  /* A gentle radial wash in the world's soft accent. Low opacity keeps it a
     TINT, not a fill — text/targets above stay high-contrast. */
  background: radial-gradient(
    ellipse at 50% 40%,
    var(--game-accent-soft, transparent),
    transparent 72%
  );
  opacity: 0.12;
}

/* Star Catch / Rhyme Time / Which Word? — the tappable word-stars glow in the
   world's accent instead of the fixed gold. The white word text + dark halo
   are untouched, so readability is identical. */
[class*="game-theme-"] .minigame-star-glyph {
  filter: drop-shadow(0 0 10px var(--game-accent, rgba(242, 185, 77, 0.55)));
}
[class*="game-theme-"] .minigame-star.caught .minigame-star-glyph {
  filter: drop-shadow(0 0 18px var(--game-accent, rgba(242, 185, 77, 0.95)));
}

/* Star Path — the traced constellation line, the revealed figure + lit stars
   take the world accent (falling back to the original gold). The figure rides
   the SAME override as the line so the two can never mismatch. */
[class*="game-theme-"] .star-path-line,
[class*="game-theme-"] .star-figure {
  stroke: var(--game-accent, var(--accent-gold));
}
[class*="game-theme-"] .minigame-pathstar.lit::before {
  box-shadow: 0 0 12px var(--game-accent, rgba(255, 255, 255, 0.95));
}

/* Constellation Canvas (paint-pop) — the canvas frame + completion glow take
   the world accent. The rich fixed 12-color painting palette is UNCHANGED (it
   is the point of that game); only the frame/glow are reskinned. */
[class*="game-theme-"] .cc-canvas {
  border-color: var(--game-accent-soft, rgba(139, 92, 246, 0.22));
}
[class*="game-theme-"] .cc-canvas.cc-canvas-revealed {
  box-shadow: 0 0 36px 10px var(--game-accent, rgba(139, 92, 246, 0.5)),
              inset 0 0 28px var(--game-accent-soft, rgba(139, 92, 246, 0.2));
}

/* ---------------- Game Tester (Parent Zone tool) ----------------
   A list of every mini-game with a Play button for QA-ing each game
   individually. Only reachable behind the Parent PIN. Dyslexia-friendly
   spacing, high contrast — same design language as other parent tools. */

/* The container for the whole game list. Stacks rows vertically with
   generous spacing so the Play buttons are easy to tap on any device. */
.game-tester-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin: 18px 0;
}

/* One row per game: name on the left, type badge in the middle, Play
   button on the right. Wraps gracefully on very narrow screens. */
.game-tester-row {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(139, 92, 246, 0.18);
  border-radius: var(--radius-md);
  padding: 14px 16px;
}

/* Game name — large, white, dyslexia-friendly. Takes up available space
   so the type badge and button stay right-aligned. */
.game-tester-name {
  flex: 1;
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--star-white);
  min-width: 120px;
}

/* Small pill badge showing the game type (reading / reward). Helps the
   parent know whether the game involves words or is a pure brain-break. */
.game-tester-type {
  font-size: 0.78rem;
  font-weight: 700;
  padding: 3px 10px;
  border-radius: 999px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  white-space: nowrap;
}

/* "reading" games: violet tint — they use the student's current level words. */
.game-tester-type-reading {
  background: rgba(139, 92, 246, 0.22);
  color: var(--accent-violet);
  border: 1px solid rgba(139, 92, 246, 0.45);
}

/* "reward" games: gold tint — they are pure brain-break games, no words. */
.game-tester-type-reward {
  background: rgba(255, 200, 87, 0.18);
  color: var(--accent-gold);
  border: 1px solid rgba(255, 200, 87, 0.45);
}

/* ==========================================================================
   My Gallery screen — saved Constellation Canvas artwork
   ========================================================================== */

/* Responsive card grid. auto-fill means as many columns as fit at 150 px
   each; on a phone this typically gives 2 columns, on a tablet 3–4. */
.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 18px;
  padding: 12px 0 24px;
  width: 100%;
}

/* Each card is a quiet dark panel with a subtle violet border — cosmic, calm. */
.gallery-card {
  background: var(--space-card);
  border: 1px solid rgba(139, 92, 246, 0.3);
  border-radius: var(--radius-md);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 10px 10px 12px;
  gap: 8px;
  /* No hover transform here — this is a static display, not a button. */
}

/* The SVG thumbnail fills the card width and has a fixed square aspect ratio
   so every piece of art sits in the same-sized frame regardless of how many
   dots it contains. 120 px is large enough to see the shape at a glance. */
.gallery-thumb-svg {
  width: 100%;
  aspect-ratio: 1;
  border-radius: var(--radius-sm);
  display: block;
  max-width: 120px;
}

/* Shape name caption below the thumbnail — small, soft, readable. */
.gallery-card-caption {
  font-size: 0.85rem;
  color: var(--star-white);
  text-align: center;
  margin: 0;
  font-family: var(--font-main);
  letter-spacing: 0.03em;
}

/* Empty-state message — centered, calm, not a sad "nothing here" wall of text. */
.gallery-empty-msg {
  grid-column: 1 / -1;   /* span all columns so it centres in the grid */
  text-align: center;
  color: rgba(255, 255, 255, 0.6);
  font-size: 1.05rem;
  font-family: var(--font-main);
  padding: 32px 16px;
  line-height: 1.6;
}

/* Honour the user's reduced-motion preference — no new animations were added
   for the gallery, so nothing to disable here. The grid itself is static. */

/* ============================================================
   Cosmic Arcade hub (js/screens/arcade.js)

   The child-facing free-play front door: a grid of game cards
   she can tap to replay any mini-game and earn stardust.

   ACCESSIBILITY / SEIZURE SAFETY:
     - NO new looping or flashing animations are added here.
     - The only motion is the same gentle hover lift used on every
       .big-tile (transform: translateY, covered by the existing
       prefers-reduced-motion block above which sets animation-
       duration:0.001ms and collapses transitions to near-instant).
     - All tap targets are large (min 160px wide, generous padding).
     - High contrast: white text on dark cosmic card backgrounds.
   ============================================================ */

/* The "Cosmic Arcade" home tile gets the electric teal accent to
   set it apart from the lesson/reading tiles (which use violet).
   Reuses .big-tile base styles — only the accent colour differs. */
.tile-arcade {
  border-color: rgba(45, 212, 191, 0.35);  /* teal border hint */
}

.tile-arcade:hover {
  border-color: rgba(45, 212, 191, 0.7);
  box-shadow: 0 12px 32px rgba(45, 212, 191, 0.2);
}

/* Responsive grid for the arcade hub cards. auto-fill lets it
   flow from 1 column on small phones to 2–3 on tablet, naturally,
   without any JS breakpoint logic. */
.arcade-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 16px;
  margin-top: 24px;
  width: 100%;
  padding-bottom: 32px;  /* breathing room above the bottom of the viewport */
}

/* Each game card is a large tap target styled as a compact dark panel.
   It mirrors the .big-tile aesthetic (same dark gradient, same rounded
   corners, same gentle hover lift) but at a tighter size so 6 cards fit
   comfortably on screen without scrolling on most tablets. */
.arcade-card {
  font-family: var(--font-main);
  background: linear-gradient(165deg, rgba(26, 19, 56, 0.88), rgba(14, 10, 35, 0.95));
  border: 1px solid rgba(45, 212, 191, 0.22);  /* teal border matches the tile */
  border-radius: var(--radius-lg);
  padding: 22px 14px 18px;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  /* Touch-friendly min size — at least 48×48 px tap area per a11y guidelines,
     but in practice the card is much larger. */
  min-height: 140px;
  /* Same gentle lift transition as .big-tile — covered by reduced-motion. */
  transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}

.arcade-card:hover {
  transform: translateY(-3px);
  box-shadow: 0 10px 28px rgba(45, 212, 191, 0.18);
  border-color: rgba(45, 212, 191, 0.55);
}

.arcade-card:focus-visible {
  outline: 2px solid var(--accent-teal);
  outline-offset: 3px;
}

/* Large sprite icon — decorative, aria-hidden in the DOM. */
.arcade-card-icon {
  font-size: 2rem;
  line-height: 1;
}

/* Batch-4 icon accents: each arcade card keeps its own colour, matching the
   home grid's treatment (Star Catch's gold star and Paint Pop's palette dots
   come from the sprite itself). */
.arcade-card-icon .ui-icon { display: block; }
.arcade-icon-rhyme-time { color: #ff5ea8; }                /* pink — music */
.arcade-icon-word-listen { color: var(--accent-cyan); }    /* cyan — ear */
.arcade-icon-sound-studio { color: var(--accent-violet); } /* violet — tiles */
.arcade-icon-star-path { color: var(--accent-teal); }      /* teal — constellation */

/* Game name — medium weight, dyslexia-friendly font, high contrast white. */
.arcade-card-name {
  font-weight: 700;
  font-size: 0.98rem;
  color: var(--star-white);
  text-align: center;
  letter-spacing: 0.01em;
  line-height: 1.3;
}

/* Sub-label — smaller, dimmed. "Earns stardust ✨" vs "Just for fun".
   Uses text-dim (soft lavender) so it reads as secondary info, not a score. */
.arcade-card-sub {
  font-size: 0.80rem;
  color: var(--text-dim);
  text-align: center;
  line-height: 1.4;
}

/* --- read-along highlighting ---
   When the child taps "Hear it" in Sentence Quest or Story Time, each word
   of the text is wrapped in a <span class="read-along-word"> and the one
   currently being spoken gets .read-along-active added to it so she can
   follow along karaoke-style.

   Design decisions:
   - Warm gold background + slightly darker text — high contrast without red.
   - Gentle transition so the highlight slides from word to word (reduced-
     motion collapses it to nearly instant via the block at line ~294 above).
   - Inline display so it wraps naturally inside the existing paragraph.
   - No border-radius, no animation loop — avoids any strobe risk.
   - Never red, never a score, never a failure state.
   ---------------------------------------------------------------------- */
.read-along-word {
  display: inline;
  /* Small breathing room around each word so the highlight doesn't crowd. */
  padding: 0 2px;
  border-radius: 3px;
  /* Smooth the highlight appearing/disappearing (no flash). */
  transition: background-color 0.12s ease, color 0.12s ease;
}

/* The currently-spoken word: a soft, TRANSLUCENT teal glow against the dark
   cosmic theme — visible enough to follow along, but gentler than the old solid
   gold (owner felt gold was too contrasty). The bright text is KEPT (no dark-on-
   light flip), so the word stays easy to read; a teal underline doubles the cue
   for anyone who finds colour alone hard to track. Never red / failure-coloured. */
.read-along-word.read-along-active {
  background-color: rgba(45, 212, 191, 0.32); /* --accent-teal at low opacity */
  color: var(--star-white, #f5f3ff);
  border-radius: 4px;
  text-decoration: underline;
  text-decoration-color: rgba(45, 212, 191, 0.85);
  text-underline-offset: 3px;
}
/* --- end read-along highlighting --- */

/* ============================================================
   Nova companion layer (#companion-layer / #companion-nova)
   ============================================================
   Nova is a silent, decorative cosmic-critter guide — the tutor/Star
   Log carry her spoken lines, she never gets her own TTS. This is v1
   placeholder art (a simple flat-fill critter); the SVG's structural
   contract (data-pose groups, 4 nova-glow-N slots, no external/script
   content) is designed so later art is a drop-in replacement.

   A later screen-glue task (Task 5) will, on #companion-layer:
     - remove the `hidden` attribute to show it
     - toggle `.companion-hero` / `.companion-small` for size+placement
     - add `.companion-animate` to fire the reveal once, and remove it
       again on `animationend`
   ============================================================ */

/* Base: fixed so Nova floats independent of page scroll/layout, and
   inert to touch/mouse (purely decorative, aria-hidden). Position/size
   come from the .companion-hero / .companion-small modifiers below so
   an unpositioned layer never blocks anything.

   IMPORTANT: `display` is only ever set inside a `:not([hidden])`
   rule below — never directly on `#companion-layer` — so the native
   `hidden` attribute's UA-stylesheet `display:none` always wins when
   present. An ID-selector `display` rule on `#companion-layer` itself
   would out-specificity that and defeat `hidden`. */
#companion-layer {
  position: fixed;
  z-index: 45; /* above page content, below the profile menu (60) */
  pointer-events: none;
}

#companion-layer:not([hidden]) {
  display: flex;
  align-items: center;
  justify-content: center;
}

#companion-nova {
  width: 100%;
  height: 100%;
  display: block;
}

/* .companion-hero — a large top-center "hero" perch for the celebration beats
   (welcome, unlock, Star Log). Pinned just under the fixed header (NOT a % of
   viewport height) so her band is a fixed size regardless of screen height,
   and the hero screen's content is pushed clear of that band by the rule
   below. Earlier this used top:16% + up to 240px and her opaque body landed
   right on top of the vertically-centered heading on wider screens. */
#companion-layer.companion-hero {
  top: calc(var(--header-height) + 10px);
  left: 50%;
  transform: translateX(-50%);
  width: var(--nova-hero-size);
  height: var(--nova-hero-size);
}

/* Reserve Nova's hero band so nothing centers up underneath her. #companion-layer
   sits immediately before #app-root in the DOM, so this sibling selector can
   top-align the hero screen's content and pad it down past her. Scoped to
   .companion-hero, so every non-hero screen keeps its normal centered layout. */
#companion-layer.companion-hero ~ #app-root {
  align-items: flex-start;
  padding-top: calc(var(--header-height) + var(--nova-hero-size) + 24px);
}

/* At ≤380px the header wraps to two rows (--header-height-mobile), so pin Nova
   and reserve her band against that taller header instead. This block MUST stay
   after the two rules above: a media query adds no specificity, so between equal
   selectors the later source position is what makes these win when it matches. */
@media (max-width: 380px) {
  #companion-layer.companion-hero { top: calc(var(--header-height-mobile) + 10px); }
  #companion-layer.companion-hero ~ #app-root {
    padding-top: calc(var(--header-height-mobile) + var(--nova-hero-size) + 24px);
  }
}

/* .companion-small — a small corner "studio perch" presence used on
   band screens (home, canvas, arcade, etc.) and reserved-level reads. */
#companion-layer.companion-small {
  bottom: 16px;
  right: 16px;
  width: 72px;
  height: 72px;
}

/* ---- Pose visibility ----
   Hide every pose group by default, then show only the one whose
   data-pose matches the root <svg>'s current data-pose attribute. The
   descendant combinator (space) means `[data-pose="cheer"]` here only
   ever matches a <g> INSIDE #companion-nova, never the svg root itself
   (whose own data-pose attribute lives on a different element). */
#companion-nova [data-pose] { display: none; }
#companion-nova[data-pose="calm"] [data-pose="calm"],
#companion-nova[data-pose="cheer"] [data-pose="cheer"],
#companion-nova[data-pose="encourage"] [data-pose="encourage"] {
  display: inline;
}

/* ---- Shared body shapes (flat fills, placeholder v1 art) ---- */
.nova-body { fill: var(--accent-violet); }
.nova-ear { fill: var(--accent-magenta); }
.nova-eye { fill: var(--space-black); }
.nova-mouth, .nova-arm, .nova-tail {
  fill: none;
  stroke: var(--space-black);
  stroke-width: 2;
  stroke-linecap: round;
}
.nova-arm, .nova-tail { stroke: var(--accent-violet); stroke-width: 6; }
.nova-sparkle { fill: var(--accent-gold); }

/* ---- Reveal animation ----
   Fires ONLY when a later task adds `.companion-animate` to
   #companion-layer (never merely from being visible) — scoped to the
   inner <svg> so it doesn't fight the outer layer's own positioning
   `transform` (translateX for .companion-hero centering). Animates
   only transform/opacity per the motion contract (no color/fill/
   layout properties — those don't composite well and can look janky
   on low-end devices). The later task removes the class again on
   `animationend`. */
@keyframes nova-reveal {
  from { opacity: 0; transform: scale(0.8) translateY(10px); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}

#companion-layer.companion-animate #companion-nova {
  animation: nova-reveal 0.5s ease-out;
}

/* Reduced motion: no extra rule needed here — the global
   `@media (prefers-reduced-motion: reduce)` block above (css/style.css
   ~line 395) already targets `*` / `*::before` / `*::after`, so it
   catches #companion-nova's `nova-reveal` animation too, forcing its
   duration to a near-zero 0.001ms (NOT `animation: none`) so
   `animationend` still fires — the screen-glue task relies on that
   event to clean up `.companion-animate`. */

/* Calm Mode (a parent-facing settings toggle a later task wires up):
   the SAME near-zero-duration treatment as reduced-motion above,
   scoped to the companion layer only. Deliberately NOT `animation:
   none` — that would strand `.companion-animate` forever because
   `animationend` would never fire. */
body.calm-mode #companion-layer,
body.calm-mode #companion-layer * {
  animation-duration: 0.001ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.001ms !important;
}
/* --- end Nova companion layer --- */

/* ==========================================================================
   The Loom — Phase 3 world map (js/screens/world-map.js).
   A scrollable vertical "thread" of planet worlds (OG level clusters). Each
   planet is a layered SVG (dim base + colored overlay clipped by paint
   fraction) — see world-map.js for how the paint layering works. Mobile-first:
   the vertical thread lays out cleanly at 375px; a widen-up rule below gives it
   more breathing room at 1024px. Motion is one-shot only and killed by BOTH
   prefers-reduced-motion (the global block near the top of this file) AND Calm
   Mode (the scoped rule at the end of this section).
   ========================================================================== */

.screen-world-map { text-align: center; }
.screen-world-map .subtitle { margin: 0 auto 8px; max-width: 34ch; }

/* The scroll container for the thread. A comfortable readable width, centered;
   it grows with the planets and scrolls within the page as normal. */
.loom {
  margin: 8px auto 0;
  max-width: 460px;
  padding: 8px 12px 32px;
}

/* The thread itself: a single vertical column of rows (segment + planet). */
.loom-thread {
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* Each row holds the connecting segment (above) + one planet, centered. */
.loom-row {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
}

/* The connecting thread between planets. Solid behind her (travelled), dashed
   ahead (the road still to come). A thin vertical line, centered. */
.loom-segment {
  width: 0;
  height: 34px;
  border-left: 3px solid var(--accent-violet);
}
.loom-segment-solid { border-left-style: solid; opacity: 0.85; }
.loom-segment-dashed { border-left-style: dashed; opacity: 0.4; }

/* A planet is a tappable button: the layered SVG + a short name label under it.
   Generous tap target (dyslexia/motor-friendly). No failure iconography. */
.loom-planet {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  background: none;
  border: none;
  padding: 6px;
  cursor: pointer;
  border-radius: var(--radius-md);
  transition: transform 0.15s ease;
}
.loom-planet:hover { transform: translateY(-2px); }
.loom-planet:focus-visible { outline: 2px solid var(--accent-teal); outline-offset: 3px; }

.loom-planet-svg {
  width: clamp(88px, 24vw, 116px);
  height: clamp(88px, 24vw, 116px);
  display: block;
  overflow: visible; /* let the "you are here" star + rings sit outside the disk */
}

/* The dim "unpainted" base disk — always visible under the colored paint layer.
   A muted grey-violet so an unreached world reads as "waiting," not broken. */
.loom-planet-base { fill: rgba(120, 112, 160, 0.32); }

/* The crescent world's "bite" disk is filled with the page background so it
   carves a crescent out of the painted circle above it. */
.loom-crescent-bite { fill: var(--space-black); stroke: none; }

/* The "you are here" star sitting on the current planet. */
.loom-here-star {
  fill: var(--accent-gold);
  stroke: var(--space-black);
  stroke-width: 1.5;
}

/* Planet name label. High-contrast on the current/painted worlds, dimmer on a
   world she hasn't reached yet (still legible — never hidden). */
.loom-planet-name {
  font-size: 0.92rem;
  font-weight: 700;
  color: var(--text-main);
  letter-spacing: 0.01em;
}
.loom-planet-locked .loom-planet-name { color: var(--text-dim); }
.loom-planet-current .loom-planet-name { color: var(--star-white); }

/* A soft glow ring around the current world's planet to draw the eye there. */
.loom-planet-here .loom-planet-svg {
  filter: drop-shadow(0 0 10px rgba(242, 185, 77, 0.55));
}

/* One-shot arrival fade of the whole thread when the screen opens. Removed on
   animationend by the screen module, so it can never get stuck on. */
.loom-arrive { animation: loom-arrive-fade 0.45s ease-out; }
@keyframes loom-arrive-fade {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ---- Graduation beat (v1-light) ----
   When she travels into a NEW world, that one planet gets a gentle ONE-SHOT
   pulse-in + soft glow (NO loop). The screen module removes the class on
   animationend so it can never stick on. Reduced-motion (the global `*` block)
   AND Calm Mode (the scoped block at the bottom of this section) both near-zero
   its duration so animationend still fires — same seizure-safe discipline as
   .loom-arrive and Nova's reveal. */
.loom-planet-arrive { animation: loom-planet-arrive-pulse 1.1s ease-out; }
@keyframes loom-planet-arrive-pulse {
  0%   { transform: scale(0.72); opacity: 0.35; filter: drop-shadow(0 0 0 rgba(242, 185, 77, 0)); }
  55%  { transform: scale(1.08); opacity: 1;    filter: drop-shadow(0 0 16px rgba(242, 185, 77, 0.85)); }
  100% { transform: scale(1);    opacity: 1;    filter: drop-shadow(0 0 8px rgba(242, 185, 77, 0.45)); }
}

/* The "Welcome to <World>!" line under the newly-entered planet's name. A warm
   celebratory accent, high-contrast, no failure-state styling. */
.loom-planet-arrive-line {
  display: block;
  margin-top: 4px;
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--accent-gold);
  text-shadow: 0 0 8px rgba(242, 185, 77, 0.4);
}

/* ---- Planet info card overlay ----
   A centered modal shown when a planet is tapped. Always dismissible (backdrop
   tap, Back button, X, or Escape) — never a trap. */
.loom-info {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.loom-info[hidden] { display: none; }

.loom-info-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(7, 6, 15, 0.72);
}

.loom-info-body {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 420px;
  background: linear-gradient(165deg, rgba(26, 19, 56, 0.98), rgba(14, 10, 35, 0.99));
  border: 1px solid rgba(139, 92, 246, 0.35);
  border-radius: var(--radius-lg);
  padding: 28px 24px 24px;
  text-align: center;
  box-shadow: 0 16px 50px rgba(0, 0, 0, 0.6);
  animation: fade-in 0.25s ease;
}

.loom-info-close {
  position: absolute;
  top: 8px;
  right: 10px;
  background: none;
  border: none;
  color: var(--text-dim);
  font-size: 1.6rem;
  line-height: 1;
  cursor: pointer;
  padding: 6px 10px;
  border-radius: var(--radius-sm);
}
.loom-info-close:hover { color: var(--star-white); }
.loom-info-close:focus-visible { outline: 2px solid var(--accent-teal); outline-offset: 2px; }

.loom-info-name {
  margin: 0 0 4px;
  font-size: 1.3rem;
  color: var(--star-white);
}
.loom-info-teaser {
  margin: 0 0 12px;
  color: var(--accent-teal);
  font-weight: 600;
  font-size: 0.98rem;
}
.loom-info-blurb {
  margin: 0 0 16px;
  color: var(--text-dim);
  font-size: 1rem;
}

/* The painted-percent meter — a simple filled bar echoing the % text. */
.loom-info-meter {
  height: 10px;
  background: rgba(255, 255, 255, 0.08);
  border-radius: 999px;
  overflow: hidden;
  margin: 0 0 18px; /* was 6px above the (removed) percent text — now owns the gap */
}
.loom-info-meter-fill {
  height: 100%;
  width: 0;
  background: linear-gradient(90deg, var(--accent-violet), var(--accent-teal));
  border-radius: 999px;
  transition: width 0.3s ease;
}
/* (No numeric percent readout under the meter — a number reads like a score
   to the child; the bar alone carries the progress. See the no-scores rule.) */

/* The action + back buttons stack on their own lines with a little breathing room. */
.loom-info-body .btn { display: block; width: 100%; margin: 8px 0 0; }

/* Widen-up for tablets/desktop (1024px): give the thread more room and larger
   planets. Mobile-first base above already works at 375px. */
@media (min-width: 720px) {
  .loom { max-width: 560px; }
  .loom-planet-svg { width: 128px; height: 128px; }
  .loom-segment { height: 42px; }
}

/* Calm Mode: near-zero the map's own animations (the arrival fade, the
   graduation-beat pulse, + the meter transition), scoped to .loom / the info
   card — the SAME treatment as Nova's layer. NOT `animation: none` (that would
   strand a mid-flight class waiting on animationend). This block MUST come AFTER
   the base .loom-arrive / .loom-planet-arrive / meter rules above so the
   equal-specificity override actually wins in source order (the P0-4 cascade
   gotcha). prefers-reduced-motion needs no rule here — the global `*` block near
   the top of this file already covers it. */
body.calm-mode .loom-arrive,
body.calm-mode .loom-planet-arrive,
body.calm-mode .loom-info-body,
body.calm-mode .loom-info-meter-fill {
  animation-duration: 0.001ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.001ms !important;
}
/* --- end The Loom world map --- */
