/* ============================================================
   qn-theme.css — shared QuizNote design system (CSS)

   The CSS sibling to the shared JS files (qn-profile.js, qn-audio.js,
   qn-staff.js, qn-nav.js, qn-music.js). This is the SINGLE SOURCE OF TRUTH
   for the design tokens and the cross-module question-area styling, so a
   look change happens HERE, once — not in 9 (eventually 24) inline copies.

   Load in each module's <head>, AFTER the font <link> and BEFORE the
   module's own inline <style> block:

     <link rel="stylesheet" href="qn-theme.css">

   A module that links this file MUST NOT also define these rules inline —
   an inline copy with equal specificity but later source order would
   silently override the shared file, defeating the point. When migrating a
   module: add the link, then delete the duplicated :root / .staff-label /
   .choice-btn rules from its inline CSS.

   SCOPE (v1.0.0): design tokens, the canonical question prompt, and the
   full answer-tile system. More of the shared design system migrates here
   over time (the same incremental-adoption model qn-music.js uses).

   ── Question prompt (.staff-label) ──────────────────────────────────────
   The prompt that sits above the staff in every module. Canonical look
   (decided May 2026, "quiet A" + fixed-height zone): quiet sentence-case,
   centered, in a FIXED-HEIGHT band so the staff never shifts vertically
   between a one-line and a two-line prompt. This is the genre standard for
   notation-drill apps (content-forward: the staff is the hero, the prompt
   is stable furniture, not a shouting headline). Folds in the old
   per-module Time-Signatures wrap override (long prompts wrap, don't clip).

   ── Answer tiles (.choice-btn) ──────────────────────────────────────────
   The four multiple-choice buttons. Base + state variants (correct / wrong
   / reveal-correct / active / disabled) + responsive sizing. One canonical
   tile everywhere; long labels ("harmonic minor") wrap as a designed state
   rather than forcing a per-module font-size override.

   Authored: May 2026. No dependencies. Tokens were byte-identical across
   all 9 modules at extraction (verified) — lifted verbatim, no reconcile.
   ============================================================ */

/* ── Design tokens ───────────────────────────────────────────────────── */
:root {
  --cream: #FFFAF0;
  --cream-2: #FFF3DC;
  --ink: #2A2A3E;
  --ink-soft: #5a5a6e;
  --ink-faint: #8a8472;
  --teal: #1FB8A8;
  --teal-dk: #0e8475;
  --teal-lt: #d3f1ec;
  --sun: #FFB627;
  --sun-dk: #c98800;
  --grape: #5B3FE4;
  --grape-dk: #3d28a8;
  --coral: #FF6B6B;
  --coral-dk: #c2453f;
  --white: #ffffff;
  --shadow-cool: #d8d6cc;
  /* ── Gamification semantic layer (Studio) ──────────────────────────────
     Principled reward color: XP/progress = teal; rank+mastery = scarce gold;
     celebrate = the teal→grape sweep used ONLY for level-up moments. */
  --xp: var(--teal);
  --rank-gold: #E6B422;
}

/* ── Question prompt (canonical: quiet A + fixed-height zone) ─────────── */
.staff-label {
  font-family: 'Fredoka', sans-serif;
  font-weight: 600;
  font-size: 22px;
  line-height: 1.25;
  color: var(--ink-soft);
  text-transform: none;
  letter-spacing: 0;
  text-align: center;
  white-space: normal;
  min-height: 2.5em;            /* fixed zone: ~2 lines reserved so the staff never jumps */
  display: flex;
  align-items: center;
  justify-content: center;
  max-width: 92%;
  margin: 0 auto;
}

/* ── Answer tiles ────────────────────────────────────────────────────── */
.choice-btn {
  font-family: 'Fredoka', sans-serif; font-weight: 700;
  font-size: 22px;
  background: white;
  border: 2.5px solid var(--ink);
  border-radius: 16px;
  box-shadow: 0 5px 0 var(--ink);
  padding: 14px 10px;
  text-align: center;
  cursor: pointer;
  transition: transform 80ms ease-out, box-shadow 80ms ease-out, background 120ms;
  color: var(--ink);
  line-height: 1.1;
  display: flex; align-items: center; justify-content: center; gap: 6px;
}
.choice-btn:active:not(:disabled) {
  transform: translateY(4px);
  box-shadow: 0 1px 0 var(--ink);
}
.choice-btn.correct {
  background: var(--teal); color: white;
  border-color: var(--teal-dk);
  box-shadow: 0 5px 0 var(--teal-dk);
}
.choice-btn.wrong {
  background: var(--coral); color: white;
  border-color: var(--coral-dk);
  box-shadow: 0 5px 0 var(--coral-dk);
}
.choice-btn.reveal-correct {
  background: var(--teal-lt); color: var(--teal-dk);
  border-color: var(--teal-dk);
  box-shadow: 0 5px 0 var(--teal-dk);
}
.choice-btn:disabled { cursor: default; }
.choice-acc {
  font-size: 16px;
  color: var(--ink-soft);
  font-weight: 600;
}
.choice-btn.correct .choice-acc,
.choice-btn.wrong .choice-acc { color: rgba(255,255,255,0.8); }
.choice-btn.reveal-correct .choice-acc { color: var(--teal-dk); }

@media (max-width: 760px) {
  .choice-btn { font-size: 18px; padding: 12px 8px; }
  /* Summary card density (shared, May 2026). Two height drivers tamed: (1) the
     base .stars (36px/4px, no nowrap) wrapped to several lines in a 1/3-width
     card — nowrap + smaller metrics keep them one line; (2) the "Best streak"
     label wrapped to two lines, and the grid stretches every card to match the
     tallest — a smaller label + tighter letter-spacing keeps it one line. Net:
     compact, wide-and-short cards on every module. */
  .summary-grid { gap: 8px; margin-top: 14px; }
  .summary-grid .stat-card { padding: 7px 6px; }
  .summary-grid .stat-label { font-size: 10px; letter-spacing: 0.04em; }
  .summary-grid .stat-value { font-size: 25px; }
  .summary-grid .stat-value .of { font-size: 14px; }
  .summary-grid .stars { font-size: 18px; letter-spacing: 1px; white-space: nowrap; }
  .hero-stars .s { font-size: 40px; }
  .hero-score { font-size: 16.5px; }
}
@media (min-width: 1100px) {
  .choice-btn { font-size: 26px; padding: 18px 14px; }
}

/* ── Summary screen ──────────────────────────────────────────────────────
   The post-round screen, shared across all modules. Extracted May 2026
   (summary cluster). Canonical/fuller versions lifted — scales gains the
   tier-perfect/great title animation it was missing (it was the subset).
   NOTE: @keyframes summaryPop stays defined inline in each module (scales
   uses it elsewhere too), and .summary-speed styling is inert in modules
   without that element (e.g. scales) — harmless, future-proofs them. */
#summary-screen {
  align-items: center; justify-content: safe center;
  padding: 20px;
}
/* Bounce-back on the summary. The modules set `overscroll-behavior: none` on
   html,body to lock the fixed game screens (no rubber-band, no pull-to-refresh
   mid-quiz). But the summary page-scrolls, and `none` made it feel stiff at the
   top/bottom. Scope the relax to the active summary via :has() — game screens
   stay locked, the summary gets its native iOS rubber-band back. */
html:has(#summary-screen.active),
body:has(#summary-screen.active) { overscroll-behavior: auto; }
.summary-wrap { width: 100%; max-width: 640px; text-align: center; }
.summary-title {
  font-family: 'Fredoka', sans-serif; font-weight: 700;
  font-size: 52px; line-height: 1;
  color: var(--ink);
}
.summary-title.tier-perfect { animation: summaryPop 600ms cubic-bezier(.4,1.6,.5,1) both; color: var(--teal); }
.summary-title.tier-great   { animation: summaryPop 500ms cubic-bezier(.4,1.6,.5,1) both; color: var(--grape); }
.summary-title.tier-good    { animation: summaryPop 450ms cubic-bezier(.4,1.4,.5,1) both; color: var(--ink); }
.summary-title.tier-practice{ animation: summaryPop 400ms cubic-bezier(.4,1.2,.5,1) both; color: var(--ink); }
.summary-sub {
  font-family: 'Fredoka', sans-serif; font-weight: 500;
  font-size: 17px; color: var(--ink-soft);
  margin-top: 6px;
}
.summary-sub.tier-perfect { color: var(--teal-dk); font-size: 19px; font-weight: 600; }
.summary-sub.tier-great   { color: var(--grape-dk); }
.summary-sub.tier-good    { color: var(--ink-soft); }
.summary-sub.tier-practice{ color: var(--ink-soft); }
.summary-grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px;
  margin-top: 22px;
}
.summary-grid .stat-value { font-size: 44px; }
.summary-grid .stars { color: var(--sun-dk); font-size: 36px; letter-spacing: 4px; }
.summary-actions {
  display: flex; gap: 12px; justify-content: center; margin-top: 24px;
  flex-wrap: wrap;
}
.summary-speed {
  margin-top: 14px;
  display: inline-flex; align-items: center; gap: 8px;
  background: white;
  border: 2px solid var(--ink); border-radius: 999px;
  padding: 7px 18px;
  box-shadow: 0 3px 0 var(--ink);
  font-family: 'Fredoka', sans-serif; font-weight: 600;
  font-size: 15px; color: var(--ink);
}
.summary-speed.timed-out { background: var(--coral); color: white; box-shadow: 0 3px 0 var(--coral-dk); }
.summary-speed[hidden] { display: none; }

/* ── Start screen + timer controls + tiles ──────────────────────────────
   Difficulty-tier tiles, timer toggle/pills, the start CTA, back link, and
   question-block labels. Shared across all 9 modules (extracted May 2026,
   start-screen cluster). All 34 rules were byte-identical across modules;
   the 3 clef-tile rules (.tile-clef, .tile.on svg .clef/.staff-line) are
   inert in modules without a tile clef (e.g. scales) — harmless. */
.start-wrap { width: 100%; max-width: 720px; }
.q-block { margin-top: 22px; }
.q-label {
  font-family: 'Fredoka', sans-serif; font-weight: 600;
  font-size: 18px; color: var(--ink);
  margin-bottom: 10px;
  display: flex; align-items: center; gap: 10px;
}
.q-label .num {
  width: 28px; height: 28px;
  background: var(--ink); color: white;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 15px; font-weight: 700;
}
.tile-row {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;
}
.tile {
  background: white;
  border: 2.5px solid var(--ink);
  border-radius: 18px;
  box-shadow: 0 5px 0 var(--ink);
  padding: 14px 10px 12px;
  text-align: center;
  cursor: pointer;
  transition: transform 80ms, box-shadow 80ms;
  font-family: 'Fredoka', sans-serif; font-weight: 500;
}
.tile:active { transform: translateY(3px); box-shadow: 0 2px 0 var(--ink); }
.tile.on { background: var(--teal); color: white; box-shadow: 0 5px 0 var(--teal-dk); }
.tile.on svg .clef { fill: white; }
.tile.on svg .staff-line { stroke: white; }
.tile-title { font-size: 20px; font-weight: 700; line-height: 1.1; }
.tile-emoji { font-size: 38px; line-height: 1; margin-bottom: 4px; }
.tile-sub {
  font-family: 'Nunito', sans-serif; font-weight: 600;
  font-size: 12px; color: var(--ink-soft);
  margin-top: 4px; line-height: 1.3;
}
.tile.on .tile-sub { color: rgba(255,255,255,0.9); }
.tile-clef { height: 54px; margin-bottom: 4px; }
.start-cta { text-align: center; margin-top: 28px; }

/* ===== Start screen: sticky CTA bar — twin of the summary-screen pattern =====
   The #start-screen base rule (flex column / 100dvh / padding:0) stays INLINE in
   each module; these are the new shared pieces. .start-scroll is an inner
   overflow container; .start-bar is its non-scrolling flex sibling, pinned to
   the bottom, carrying the honest fade-scrim + animated "More ▾" cue. JS
   (qn-roundend.js initStartScroll/updateStartScrim) toggles #start-screen.scroll-end. */
.start-scroll {
  /* Plain content wrapper — the PAGE (window) scrolls, not this element, so the
     sticky .start-bar behaves exactly like the summary bar (proven on iOS
     Safari). An inner overflow container fought Safari's min-height flex chain
     and either occluded or un-pinned the bar. */
  display: flex; flex-direction: column; align-items: center;
  padding: 24px 18px 16px;
}
.start-bar {
  position: sticky; bottom: 0; z-index: 5;  /* stays at the viewport bottom (twin of .summary-bar) */
  margin-top: auto;               /* pin to the bottom when the config is short */
  display: flex; flex-direction: column; gap: 9px;
  padding: 12px 16px calc(14px + env(safe-area-inset-bottom));
  background: var(--cream);
  border-top: 1px solid rgba(42,42,62,0.08);
  box-shadow: 0 -6px 16px -10px rgba(42,42,62,0.16);
}
.start-bar::before {
  content: "";
  position: absolute;
  left: 0; right: 0; top: -26px;
  height: 26px;
  pointer-events: none;
  background: linear-gradient(to top, var(--cream) 18%, rgba(255,250,240,0));
  opacity: 1;
  transition: opacity .22s ease;
}
#start-screen.scroll-end .start-bar::before { opacity: 0; }
.start-bar .scroll-cue {
  position: absolute;
  left: 50%; top: 0;
  transform: translate(-50%, calc(-100% - 4px));
  display: inline-flex; align-items: center; gap: 4px;
  padding: 5px 11px 5px 13px;
  font-family: inherit; font-size: 13px; font-weight: 700;
  line-height: 1; color: var(--grape);
  background: var(--cream);
  border: 1.5px solid rgba(42,42,62,0.12);
  border-radius: 999px;
  box-shadow: 0 3px 10px -3px rgba(42,42,62,0.28);
  cursor: pointer; z-index: 2;
  opacity: 1; transition: opacity .22s ease;
}
.start-bar .scroll-cue:active { transform: translate(-50%, calc(-100% - 2px)); }
.start-bar .scroll-cue .cue-arrow {
  font-size: 15px; display: inline-block;
  animation: cueBob 1.4s ease-in-out infinite;   /* keyframes shared w/ summary */
}
#start-screen.scroll-end .start-bar .scroll-cue { opacity: 0; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
  .start-bar .scroll-cue .cue-arrow { animation: none; }
}
.start-bar .start-cta { margin-top: 0; }      /* override the shared 28px */
.start-bar .start-cta .btn { width: 100%; }   /* full-width thumb target */
/* Mobile density so each step fits ~one screen */
@media (max-width: 560px) {
  .start-scroll { padding: 16px 16px 12px; }
  .q-block { margin-top: 16px; }
  .tile { padding: 12px 8px; }
  .tile-emoji { font-size: 26px; margin-bottom: 2px; }
  .tile-title { font-size: 16px; }
  .tile-sub { font-size: 11px; margin-top: 2px; line-height: 1.25; }
  .tile-clef { height: 44px; }
  .tile-row { gap: 8px; }
  .settings-card { margin-top: 16px; }
}

/* Start-screen entrance: each config block springs in, cascaded top→bottom.
   qn-roundend.js adds `.pop-in` to `.start-wrap` on #start-screen (re)activation
   (initial load + returning from a round). `both` holds the 0% state pre-start
   so there's no flash of un-animated content. */
@keyframes startPopIn {
  0%   { transform: scale(.92) translateY(8px); opacity: 0; }
  55%  { transform: scale(1.03) translateY(0);  opacity: 1; }
  78%  { transform: scale(.99); }
  100% { transform: scale(1); }
}
.start-wrap.pop-in > * { animation: startPopIn .5s cubic-bezier(.34,1.56,.64,1) both; }
.start-wrap.pop-in > *:nth-child(2) { animation-delay: .05s; }
.start-wrap.pop-in > *:nth-child(3) { animation-delay: .10s; }
.start-wrap.pop-in > *:nth-child(4) { animation-delay: .15s; }
.start-wrap.pop-in > *:nth-child(5) { animation-delay: .20s; }
.start-wrap.pop-in > *:nth-child(6) { animation-delay: .25s; }
.start-wrap.pop-in > *:nth-child(n+7) { animation-delay: .30s; }
@media (prefers-reduced-motion: reduce) {
  .start-wrap.pop-in > * { animation: none; }
}

/* Tap bounce: a quick scale squish→overshoot on press. qn-roundend.js adds
   `.tap-bounce` on pointerdown to #start-screen tiles + the Start button (fired
   on press so it's visible BEFORE the Start tap navigates away). On the tiles
   the full overshoot is seen; the Start button shows the squish during press. */
@keyframes tapBounce {
  0%  { transform: scale(1); }
  30% { transform: scale(.91); }
  62% { transform: scale(1.045); }
  100%{ transform: scale(1); }
}
.tap-bounce { animation: tapBounce .4s cubic-bezier(.34, 1.56, .64, 1); }
@media (prefers-reduced-motion: reduce) {
  .tap-bounce { animation: none; }
}

.timer-toggle {
  display: inline-flex; align-items: center; gap: 10px;
  background: white;
  border: 2.5px solid var(--ink);
  border-radius: 999px;
  box-shadow: 0 4px 0 var(--ink);
  padding: 10px 16px;
  cursor: pointer;
  font-family: 'Fredoka', sans-serif; font-weight: 600;
  font-size: 17px; color: var(--ink);
  transition: transform 80ms, box-shadow 80ms;
}
.timer-toggle:active { transform: translateY(2px); box-shadow: 0 2px 0 var(--ink); }
.timer-toggle.on { background: var(--teal); color: white; box-shadow: 0 4px 0 var(--teal-dk); }
.timer-toggle .t-icon { font-size: 20px; line-height: 1; }
.timer-toggle .t-state {
  font-family: 'Fredoka', sans-serif; font-size: 13px; font-weight: 600;
  background: rgba(0,0,0,0.08);
  padding: 3px 9px; border-radius: 999px;
  color: var(--ink-soft);
}
.timer-toggle.on .t-state { background: rgba(255,255,255,0.25); color: white; }
.timer-pills {
  display: flex; gap: 10px; justify-content: center;
  margin-top: 14px; flex-wrap: wrap;
}
.timer-pills[hidden] { display: none; }
.tpill {
  font-family: 'Fredoka', sans-serif; font-weight: 600;
  font-size: 18px;
  background: white;
  border: 2.5px solid var(--ink);
  border-radius: 14px;
  box-shadow: 0 4px 0 var(--ink);
  padding: 8px 18px;
  cursor: pointer;
  color: var(--ink);
  transition: transform 80ms, box-shadow 80ms;
}
.tpill:active { transform: translateY(2px); box-shadow: 0 2px 0 var(--ink); }
.tpill.on { background: var(--teal); color: white; box-shadow: 0 4px 0 var(--teal-dk); }
.timer-badge[hidden] { display: none; }
.timer-badge .t-icon { font-size: 16px; margin-right: 2px; }
.timer-badge.urgent {
  background: var(--coral); color: white;
  border-color: var(--coral-dk);
  box-shadow: 0 4px 0 var(--coral-dk);
  animation: timerPulse 700ms ease-in-out infinite;
}
/* ── Settings card (grouped preferences) ── */
.settings-card {
  background: #fff;
  border: 1.5px solid var(--shadow-cool);
  border-radius: 16px;
  overflow: hidden;
  margin-top: 22px;
}
.setting-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 18px;
  cursor: pointer;
  transition: background 100ms;
  user-select: none;
}
.setting-row:hover { background: rgba(31, 184, 168, 0.03); }
.setting-row + .setting-row { border-top: 1px solid var(--shadow-cool); }
.settings-card .timer-pills { border-top: 1px solid var(--shadow-cool); }
.setting-left {
  display: flex; align-items: center; gap: 10px;
  font-family: 'Fredoka', sans-serif;
  font-weight: 600; font-size: 16px; color: var(--ink);
}
.setting-icon { font-size: 18px; width: 24px; text-align: center; }
.pill-state {
  font-family: 'Fredoka', sans-serif;
  font-size: 13px; font-weight: 600;
  letter-spacing: 0.02em;
  border-radius: 999px;
  padding: 4px 12px;
  background: rgba(0,0,0,0.06);
  color: var(--ink-soft);
  transition: background 200ms, color 200ms;
}
.pill-state.on {
  background: rgba(31, 184, 168, 0.15);
  color: var(--teal-dk, #0F8A7E);
}
.settings-card .timer-pills {
  padding: 10px 18px 14px;
  margin-top: 0;
}

.start-back-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: 'Fredoka', sans-serif;
  font-weight: 600;
  font-size: 14px;
  color: var(--ink);
  text-decoration: none;
  background: white;
  border: 2px solid var(--ink);
  border-radius: 12px;
  padding: 7px 14px 7px 12px;
  box-shadow: 0 3px 0 var(--ink);
  transition: transform 120ms ease-out, box-shadow 120ms ease-out;
  margin-bottom: 18px;
  align-self: flex-start;
}
.start-back-link:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 0 var(--ink);
}
.start-back-link:active {
  transform: translateY(2px);
  box-shadow: 0 1px 0 var(--ink);
}
.timer-badge { font-size: 16px; padding: 4px 8px; min-width: 64px; }

/* ── Play screen chassis ─────────────────────────────────────────────────
   In-play furniture shared across all 9 modules: stat cards (question/score/
   last-answers), progress bar, streak badge, topbar, icon + mute buttons,
   last-answer dots, and the correct-toast cycle/show states. Extracted
   May 2026 (play-chassis cluster). Only byte-identical-across-all-9 rules
   moved; module-specific staff-card/staff-actions sizing, the base
   .toast.correct (scales runs 40px), and retry-mechanic toast/dot rules
   stay inline. JS still toggles these classes at runtime — only styling moved. */
.icon-btn {
  width: 46px; height: 46px;
  border: 2px solid var(--ink); background: white;
  color: var(--ink); font-size: 20px;
  border-radius: 13px;
  box-shadow: 0 4px 0 var(--ink);
  display: inline-flex; align-items: center; justify-content: center;
  transition: transform 80ms ease-out, box-shadow 80ms ease-out;
}
.icon-btn:active { transform: translateY(3px); box-shadow: 0 1px 0 var(--ink); }
.topbar {
  display: flex; align-items: center; gap: 10px;
  flex-shrink: 0;
}
.progress-track {
  flex: 1; height: 22px;
  background: white;
  border: 2.5px solid var(--ink);
  border-radius: 12px;
  overflow: hidden;
  position: relative;
  box-shadow: inset 0 2px 0 rgba(0,0,0,0.04);
}
.progress-fill {
  height: 100%; width: 0%;
  background: linear-gradient(180deg, #34d4c2, var(--teal));
  border-right: 2.5px solid var(--teal-dk);
  border-radius: 8px 0 0 8px;
  transition: width 280ms cubic-bezier(.5,1.6,.6,1);
  position: relative;
}
.progress-fill::after {
  content: ""; position: absolute;
  top: 3px; left: 8px; right: 8px; height: 5px;
  background: rgba(255,255,255,0.55);
  border-radius: 4px;
}
.streak-badge {
  display: flex; align-items: center; gap: 4px;
  font-family: 'Fredoka', sans-serif; font-weight: 700;
  font-size: 20px; color: var(--sun-dk);
  background: white; border: 2.5px solid var(--ink);
  border-radius: 14px; padding: 6px 12px;
  box-shadow: 0 4px 0 var(--ink);
  min-width: 62px; justify-content: center;
}
.streak-badge .num { color: var(--ink); }
.play-body {
  display: grid;
  grid-template-columns: 1.6fr 1fr;
  gap: 12px;
  align-items: stretch;
  flex-shrink: 0;
}
.stats-col {
  display: flex; flex-direction: column; gap: 8px;
}
.stat-card { padding: 10px 12px; border-radius: 16px; }
.stat-label {
  font-family: 'Nunito', sans-serif; font-weight: 800;
  font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-faint);
}
.stat-card.sun-card .stat-label { color: #7a4f00; }
.stat-card.teal-card .stat-label { color: rgba(255,255,255,0.85); }
.stat-value {
  font-family: 'Fredoka', sans-serif; font-weight: 700;
  font-size: 34px; line-height: 1.05;
  color: var(--ink);
  margin-top: 2px;
}
.stat-card.teal-card .stat-value { color: white; }
.stat-value .of { color: var(--ink-faint); font-size: 22px; font-weight: 600; }
.stat-card.sun-card .stat-value .of { color: #7a4f00; }
.last-dots { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
.last-dot {
  width: 20px; height: 20px; border-radius: 50%;
  border: 2px solid var(--ink); background: white;
}
.last-dot.correct { background: var(--teal); }
.last-dot.wrong { background: var(--coral); }
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0) scale(1) rotate(-3deg); }
.toast.correct.cycle-1 { color: var(--grape); text-shadow: 3px 3px 0 rgba(91,63,228,0.18); }
.toast.correct.cycle-2 { color: var(--sun-dk); text-shadow: 3px 3px 0 rgba(255,182,39,0.25); }
.mute-btn:active { transform: translateY(3px); box-shadow: 0 1px 0 var(--ink); }
.mute-btn.muted { background: var(--cream-2); color: var(--ink-faint); }
.stats-col .stat-card { flex: 1; }
.stats-col .last-dots-card { display: none; }

/* ── Quit/confirm modal + miss-list + personal-best rows ─────────────────
   The modal backdrop + card + actions, the post-round "missed" list children,
   and personal-best row bits. Shared across all 9 (extracted May 2026, modal
   cluster). LEFT INLINE: .modal-overlay.show (only the 2 component-migrated
   modules have it, overrides [hidden]); .miss-list base container (only scales
   styles it); @keyframes modalFade (referenced by .modal-overlay, kept local
   in each module — resolves against the shared rule fine). */
.modal-overlay {
position: fixed; inset: 0;
background: rgba(42, 42, 62, 0.5);
display: flex; align-items: center; justify-content: center;
z-index: 100;
padding: 20px;
animation: modalFade 200ms ease-out;
}
.modal-overlay[hidden] { display: none; }
.modal-card {
  background: white;
  border: 2.5px solid var(--ink);
  border-radius: 22px;
  box-shadow: 0 8px 0 var(--ink);
  padding: 28px 28px 24px;
  max-width: 420px;
  width: 100%;
  text-align: center;
  animation: modalPop 320ms cubic-bezier(.4,1.6,.6,1);
}
.modal-card h2 {
  font-family: 'Fredoka', sans-serif; font-weight: 700;
  font-size: 28px; color: var(--ink);
  line-height: 1.1;
}
.modal-card p {
  font-family: 'Nunito', sans-serif; font-weight: 500;
  font-size: 15px; color: var(--ink-soft);
  margin: 8px 0 22px;
  line-height: 1.4;
}
.modal-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
.miss-list h3 {
  font-family: 'Fredoka', sans-serif; font-weight: 600;
  font-size: 17px; color: var(--ink);
  margin-bottom: 8px;
}
.miss-list ul {
  list-style: none; padding: 0; margin: 0;
  font-family: 'Nunito', sans-serif; font-weight: 600;
  font-size: 14px; color: var(--ink-soft);
  line-height: 1.6;
}
.miss-list li::before { content: "• "; color: var(--coral); font-weight: 800; }
.miss-empty { font-family: 'Nunito',sans-serif; font-weight: 600; font-size:14px; color: var(--teal-dk); }
.pb-label { color: var(--ink-faint); font-weight: 600; }
.pb-new { color: var(--teal-dk); font-weight: 800; }
.pb-sep { color: var(--ink-faint); }
.pb-row[hidden] { display: none; }

/* ── HERO verdict (round-end redesign, May 2026) ──────────────────────────
   Stars are the round's headline; the score sits beneath as a detail; the PB
   flag appears only when a record is beaten. The old Score/Best-streak/Stars
   3-card grid is retired in favour of this. Populated by qn-roundend.js
   render() (#sum-stars / #sum-score / #sum-score-of / #sum-pct), so every
   module renders an identical verdict. */
/* Unified summary section rhythm (May 2026): every top-level summary block uses
   margin-top:18 and NO margin-bottom, so inter-section gaps are an even 18px
   instead of the old 6/16/0/18/22/14 mishmash (margin-collapse made them
   unpredictable). xp-earn/mastery keep `auto` L/R to stay centered. */
.summary-hero { display: flex; flex-direction: column; align-items: center; margin-top: 18px; }
.hero-stars { display: flex; gap: 8px; margin: 4px 0 2px; line-height: 1; }
.hero-stars .s { font-size: 46px; line-height: 1; filter: drop-shadow(0 2px 0 rgba(0,0,0,0.05)); }
.hero-stars .s.on { color: var(--sun); }
.hero-stars .s.off { color: rgba(42,42,62,0.16); }
.hero-score { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 18px;
  color: var(--ink-soft); margin: 10px 0 0; }
.hero-score .of { color: var(--ink-faint); }
.hero-pct { color: var(--ink-faint); margin-left: 2px; }
.pb-flag { margin-top: 16px; display: inline-flex; align-items: center; gap: 6px;
  font-family: 'Fredoka', sans-serif; font-weight: 700; font-size: 14px; color: #7a4f00;
  background: linear-gradient(135deg, #FFF1CF, #FFE3A0); border: 2px solid var(--sun);
  padding: 6px 14px; border-radius: 999px; }
.pb-flag[hidden] { display: none; }
.miss-list {
  margin-top: 18px; text-align: left;
  background: white; border: 2.5px solid var(--ink); border-radius: 18px;
  box-shadow: 0 5px 0 var(--ink);
  padding: 14px 18px;
}

/* ── Feedback toasts (correct / retry / reveal) ──────────────────────────
   The in-play feedback text. Shared base + per-kind size/color. Option-2
   positioning (May 2026): the big "correct" praise pops high over the staff;
   the smaller retry/reveal sit lower so corrective text reads clean and
   doesn't fight the notation. PROVING ON scale-degrees FIRST — the other 8
   modules still carry their own inline .toast (which overrides this via
   source order) until this positioning is QA'd and rolled out per-module.
   scales is a separate feedback model (40px correct, .toast.wrong, no
   reveal/retry) and stays inline regardless. */
.toast {
  position: absolute; left: 50%; top: 38px;
  transform: translateX(-50%) translateY(-10px) scale(0.6);
  opacity: 0;
  pointer-events: none;
  font-family: 'Fredoka', sans-serif; font-weight: 700;
  text-align: center;
  z-index: 5;
  transition: transform 240ms cubic-bezier(.4,1.8,.5,1), opacity 180ms;
  white-space: nowrap;
  max-width: 92%;
}
/* Option-2 per-kind vertical placement: praise over the staff, guidance lower. */
.toast.correct { color: var(--teal); font-size: 44px; text-shadow: 3px 3px 0 rgba(31,184,168,0.18); top: 46%; }
.toast.retry   { color: var(--sun-dk); font-size: 30px; top: 64%; }
.toast.reveal  { color: var(--grape); font-size: 26px; top: 64%; }


/* ============================================================
   CHUNKY BUTTON SYSTEM
   Lifted from all 9 modules (byte-identical across the set; the
   .btn:disabled rule was present only in scales — lifted as the
   superset so the cluster is complete and the disabled state is
   available app-wide). The responsive @media .btn shrink stays
   inline in each module per the extraction convention.
   ============================================================ */
.btn {
  font-family: 'Fredoka', sans-serif; font-weight: 600;
  border: none; border-radius: 16px;
  padding: 12px 24px; font-size: 18px;
  color: white; background: var(--teal);
  box-shadow: 0 5px 0 var(--teal-dk);
  transition: transform 80ms ease-out, box-shadow 80ms ease-out;
  position: relative;
  line-height: 1.1;
  white-space: nowrap;
}
.btn:active, .btn.is-pressed {
  transform: translateY(4px);
  box-shadow: 0 1px 0 var(--teal-dk);
}
.btn.sun { background: var(--sun); color: var(--ink); box-shadow: 0 5px 0 var(--sun-dk); }
.btn.sun:active { box-shadow: 0 1px 0 var(--sun-dk); }
.btn.grape { background: var(--grape); box-shadow: 0 5px 0 var(--grape-dk); }
.btn.grape:active { box-shadow: 0 1px 0 var(--grape-dk); }
.btn.ghost {
  background: white; color: var(--ink);
  border: 2px solid var(--ink); box-shadow: 0 5px 0 var(--ink);
  padding: 10px 22px;
}
.btn.ghost:active { box-shadow: 0 1px 0 var(--ink); }
.btn.big { font-size: 22px; padding: 14px 32px; border-radius: 18px; }
.btn.huge { font-size: 26px; padding: 16px 40px; border-radius: 20px; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }


/* ============================================================
   PAGE CHROME — cards, screen system, brand block, site
   header + footer. Lifted from all 9 modules (byte-identical
   across the set, modulo cosmetic whitespace in piano-quiz's
   .brand body and a missing comment header in scales' footer
   — both fully equivalent rule-for-rule).

   Module-specific holdouts left inline (NOT lifted):
   - piano-quiz: .brand .brand-logo (only piano-quiz has the
     brand-logo element on its start screen).
   - scales: body.playing .site-footer { display: none; }
     (only scales hides the footer in play view).
   ============================================================ */

/* Cards */
.card {
  background: white;
  border: 2.5px solid var(--ink);
  border-radius: 20px;
  box-shadow: 0 6px 0 var(--ink);
}
.card.sun-card { background: var(--sun); }
.card.teal-card { background: var(--teal); color: white; }

/* Screen system */
.screen { display: none !important; flex: 1; pointer-events: none; }
.screen.active { display: flex !important; flex-direction: column; pointer-events: auto; }

/* Start-screen brand block */
.brand { text-align: center; margin-bottom: 22px; }
.brand .logo-mark {
  display: inline-flex; align-items: center; gap: 12px;
  justify-content: center;
}
.brand h1 {
  font-family: 'Fredoka', sans-serif; font-weight: 700;
  font-size: 56px; line-height: 1; color: var(--ink);
  letter-spacing: -0.01em;
}
.brand .tagline {
  font-family: 'Fredoka', sans-serif; font-weight: 500;
  font-size: 17px; color: var(--ink-soft);
  margin-top: 8px;
}

/* Site header */
.site-header {
  background: white;
  border-bottom: 2.5px solid var(--ink);
  padding: 10px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 6px;
}
.site-header .brand-name {
  font-family: 'Fredoka', sans-serif;
  font-weight: 700;
  font-size: 26px;
  color: var(--ink);
  text-decoration: none;
  letter-spacing: -0.01em;
}
.site-header .brand-name span { color: var(--teal); }

/* Site footer */
.site-footer {
  background: white;
  border-top: 2px solid var(--shadow-cool);
  padding: 14px 20px;
  text-align: center;
  font-family: 'Nunito', sans-serif;
  font-size: 13px;
  color: var(--ink-faint);
}
.site-footer a {
  color: var(--ink-soft);
  text-decoration: none;
  font-weight: 600;
}
.site-footer a:hover { text-decoration: underline; }
.site-footer .sep { margin: 0 8px; }
/* On mobile, hide the game-screen footer (.site-footer is used ONLY by the 35
   game pages; catalog pages — index/play/path/dashboard — use a plain <footer>
   and are unaffected). The legal links stay reachable on desktop here, on every
   catalog page, and on the landing page, which satisfies app-store policy.
   When a "More/Settings" menu destination exists, Privacy/Terms move there and
   this game footer can retire entirely. May 2026. */
@media (max-width: 760px) {
  .site-footer { display: none; }
}

/* ============ TEACHING HINTS ============ */
/* Mobile: modal overlay with centered card. Desktop: inline banner between question and choices. */

.hint-overlay {
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.35);
  z-index: 50;
  display: flex; align-items: center; justify-content: center;
  animation: hintFadeIn 180ms ease-out;
}
.hint-overlay[hidden] { display: none; }

.hint-card {
  background: #fffdf5;
  border-radius: 18px;
  padding: 24px 22px 20px;
  max-width: 340px;
  width: calc(100% - 40px);
  display: flex;
  flex-direction: column;
  gap: 14px;
  box-shadow: 0 8px 32px rgba(0,0,0,0.12);
  animation: hintSlideUp 200ms ease-out;
}

.hint-header {
  display: flex; align-items: center; gap: 8px;
  font-family: 'Fredoka', sans-serif;
  font-weight: 600; font-size: 15px;
  color: var(--ink-soft);
}

.hint-header svg {
  width: 22px; height: 22px; flex-shrink: 0;
}

.hint-body {
  font-size: 17px; line-height: 1.55;
  color: var(--ink);
}

.hint-actions {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
}

.hint-dismiss-link {
  background: none; border: none;
  font-family: 'Nunito', sans-serif;
  font-size: 13px; color: var(--ink-faint);
  cursor: pointer; text-decoration: underline;
  padding: 0;
}
.hint-dismiss-link:hover { color: var(--ink-soft); }

@keyframes hintFadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}
@keyframes hintSlideUp {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: translateY(0); }
}


/* ── "Best in portrait" landscape hint ───────────────────────────────────
   Markup is injected by qn-nav.js, but ONLY on practice-module pages (those
   with a #play-screen). Visibility is gated purely by CSS: shown only on
   short, touch landscape (phones) — tablets in landscape (taller) and desktop
   are unaffected. Portrait-first practice UI doesn't fit a phone's landscape
   height, so we ask the user to rotate rather than show a clipped layout. */
#qn-rotate { display: none; }
@media (orientation: landscape) and (max-height: 500px) and (pointer: coarse) {
  #qn-rotate {
    display: flex; position: fixed; inset: 0; z-index: 99999;
    align-items: center; justify-content: center;
    padding: 24px; text-align: center;
    background: var(--cream, #FFFAF0);
  }
}
.qn-rotate-card { max-width: 340px; }
.qn-rotate-card svg {
  width: 72px; height: 72px; color: var(--teal-dk);
  display: block; margin: 0 auto 14px;
}
.qn-rotate-title {
  font-family: 'Fredoka', sans-serif; font-weight: 700;
  font-size: 22px; line-height: 1.15; color: var(--ink);
}
.qn-rotate-sub {
  font-family: 'Nunito', sans-serif; font-weight: 600;
  font-size: 14px; color: var(--ink-soft); margin-top: 6px;
}


/* ==========================================================================
   Gamified round-end (shared) — paired with qn-roundend.js. Extracted
   verbatim from the note-names P0 proof (May 2026) so all modules render
   an identical round-end. Beats 1-3 + sticky action bar + scroll scrim +
   'More v' cue. Variables resolve from the :root block above.
   ========================================================================== */
  /* ---- Gamification P0: the three-beat round-end (spec §3) ---- */
  /* Beat 1 — THE EARN: XP gain + level progress. Always something, even at 2/10. */
  .xp-earn {
    background: linear-gradient(135deg, var(--grape) 0%, #6f4ff0 100%);
    border: 3px solid var(--ink);
    border-radius: 20px;
    box-shadow: 0 5px 0 var(--ink);
    padding: 16px 20px;
    margin: 18px auto 0;
    max-width: 420px;
    color: white;
  }
  .xp-earn-top {
    display: flex; align-items: baseline; justify-content: center;
    gap: 10px; margin-bottom: 12px;
  }
  .xp-plus {
    font-family: 'Fredoka', sans-serif; font-weight: 700;
    font-size: 34px; line-height: 1; color: #fff;
    text-shadow: 0 2px 0 rgba(0,0,0,0.18);
  }
  .xp-plus-label {
    font-family: 'Nunito', sans-serif; font-weight: 800;
    font-size: 15px; letter-spacing: 0.04em; opacity: 0.92;
  }
  .xp-level-row {
    display: flex; align-items: center; gap: 10px;
    font-family: 'Nunito', sans-serif; font-weight: 700; font-size: 13px;
  }
  .xp-level-badge {
    flex: 0 0 auto;
    background: rgba(255,255,255,0.18);
    border-radius: 999px; padding: 4px 12px; white-space: nowrap;
  }
  .xp-bar {
    flex: 1 1 auto; height: 14px;
    background: rgba(0,0,0,0.22);
    border-radius: 999px; overflow: hidden;
    border: 1.5px solid rgba(0,0,0,0.25);
  }
  .xp-bar-fill {
    height: 100%; width: 0%;
    background: linear-gradient(90deg, var(--sun) 0%, #ffd75e 100%);
    border-radius: 999px;
    transition: width 900ms cubic-bezier(.25,.9,.3,1);
  }
  .xp-to-next { flex: 0 0 auto; opacity: 0.9; white-space: nowrap; }
  .xp-streak-kept {
    text-align: center; margin-top: 10px;
    font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 15px;
  }

  /* Beat 2 — THE PROGRESS: this round as one step toward the medal. */
  .mastery-meter {
    max-width: 420px; margin: 18px auto 0;
    background: var(--teal-lt, #e6faf7);
    border: 2.5px solid var(--teal);
    border-radius: 16px; padding: 12px 16px;
    font-family: 'Nunito', sans-serif; font-weight: 700;
    color: var(--ink); font-size: 14px;
  }
  .mastery-meter .mm-top {
    display: flex; align-items: center; justify-content: space-between;
    gap: 8px; margin-bottom: 8px;
  }
  /* Progress toward the next medal as a thin fill BAR — not dots. Discrete dots
     read as a swipeable carousel pager (people try to swipe them); a fill bar
     reads unambiguously as progress. CSS-drawn so it can't tofu like the old
     ▓/░ shade-block glyphs did on iOS. */
  .mastery-meter .mm-pips {
    display: inline-block; width: 54px; height: 8px; border-radius: 999px;
    background: rgba(14,132,117,0.18); overflow: hidden; vertical-align: middle;
  }
  .mastery-meter .mm-pips .mm-bar-fill {
    display: block; height: 100%; border-radius: 999px;
    background: var(--teal-dk); transition: width .45s ease;
  }
  .mastery-meter .mm-note { color: var(--ink-soft); font-weight: 600; }

  /* Beat 3 — THE NEXT STEP: one dominant action, recommender-driven. */
  .next-step-cta {
    width: 100%;
    display: flex; flex-direction: column; align-items: center; gap: 2px;
    text-decoration: none; text-align: center;
  }
  .next-step-cta .nsc-reason {
    font-family: 'Nunito', sans-serif; font-weight: 700; font-size: 12px;
    color: var(--ink-soft); margin-bottom: 6px;
  }
  .summary-actions .btn.secondary-link {
    background: none; box-shadow: none; border: none;
    color: var(--ink-soft); font-size: 15px; text-decoration: underline;
    padding: 6px;
  }
  .summary-actions .btn.secondary-link:active { transform: none; box-shadow: none; }

  /* ---- Sticky action bar (mobile-reach proof, spec §3 / §5) ----
     Opt-in per module via the `.has-roundbar` marker class on #summary-screen,
     added in the same commit that adds the .summary-scroll / .summary-bar
     markup. Un-migrated modules keep the baseline centered #summary-screen
     (padding:20px) above — the marker's id+class specificity wins only where
     present, so this restructure never touches a module that lacks the markup.
     Marker class (not :has) keeps it working on older school-iPad Safari. */
  #summary-screen.has-roundbar {
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    padding: 0;                 /* padding moves onto .summary-scroll */
    /* NB: no fixed height. The page has a flow <header> + <footer>, so forcing
       100dvh here made the document taller than the viewport — the window then
       scrolled and the "pinned" bar sat below the fold (and the "More" cue could
       over-scroll the page). Instead the summary flows naturally and the bar is
       position:sticky (below), which stays reachable and can't over-scroll. */
  }
  .summary-scroll {
    /* Plain content wrapper now — the page scrolls, not this element. */
    display: flex; flex-direction: column; align-items: center;
    padding: 20px 18px 16px;
  }
  .summary-bar {
    position: sticky; bottom: 0; z-index: 5;   /* stays at the viewport bottom */
    display: flex; flex-direction: column; gap: 9px;
    padding: 12px 16px calc(14px + env(safe-area-inset-bottom));
    /* Cap the bar to the same 640px content width as .summary-wrap and center
       it, so on wide desktop the buttons line up with the cards above instead of
       stretching the full viewport (which looked goofy). The bar's background is
       var(--cream) — identical to the page bg — so capping the whole bar shows
       no background edge; only the buttons narrow and the top hairline/scrim
       center with them. On phones (<672px) this is a no-op (width:100% wins). */
    max-width: calc(640px + 32px);   /* 640 content + the 16px L/R padding */
    margin-left: auto; margin-right: auto;
    width: 100%;
    background: var(--cream);
    /* Hairline, not a hard ink line: a crisp border reads as "page ends here"
       and kills the scroll affordance. The fade scrim below does the real
       separation work. */
    border-top: 1px solid rgba(42,42,62,0.08);
    box-shadow: 0 -6px 16px -10px rgba(42,42,62,0.16);
  }
  /* ---- Scroll-fade scrim (industry-standard scrollability cue) ----
     A soft gradient sitting just ABOVE the bar so content fades into it
     rather than stopping at a hard line — the Material "elevated on scroll"
     / Lea Verou scrolling-shadow pattern. It is HONEST: JS hides it once the
     scroll area is at the bottom (or doesn't overflow), so a visible fade
     always means "there's more below." */
  .summary-bar::before {
    content: "";
    position: absolute;
    left: 0; right: 0; top: -26px;
    height: 26px;
    pointer-events: none;
    background: linear-gradient(to top, var(--cream) 18%, rgba(255,250,240,0));
    opacity: 1;
    transition: opacity .22s ease;
  }
  /* At the end of the scroll (or when nothing overflows) the fade is dishonest
     — kill it so its presence reliably signals "keep scrolling". */
  #summary-screen.scroll-end .summary-bar::before { opacity: 0; }
  /* ---- Scroll cue pill (animated "More ▾") ----
     Floats just above the action bar in the fade zone. Bobs to draw the eye,
     taps to scroll, and shares the fade's honesty: hidden the moment the
     scroll area is at the bottom (or doesn't overflow). */
  .summary-bar .scroll-cue {
    position: absolute;
    left: 50%; top: 0;
    transform: translate(-50%, calc(-100% - 4px));
    display: inline-flex; align-items: center; gap: 4px;
    padding: 5px 11px 5px 13px;
    font-family: inherit; font-size: 13px; font-weight: 700;
    line-height: 1; color: var(--grape);
    background: var(--cream);
    border: 1.5px solid rgba(42,42,62,0.12);
    border-radius: 999px;
    box-shadow: 0 3px 10px -3px rgba(42,42,62,0.28);
    cursor: pointer; z-index: 2;
    opacity: 1; transition: opacity .22s ease;
  }
  .summary-bar .scroll-cue:active { transform: translate(-50%, calc(-100% - 2px)); }
  .summary-bar .scroll-cue .cue-arrow {
    font-size: 15px; display: inline-block;
    animation: cueBob 1.4s ease-in-out infinite;
  }
  @keyframes cueBob {
    0%, 100% { transform: translateY(-1px); }
    50%      { transform: translateY(2px); }
  }
  #summary-screen.scroll-end .summary-bar .scroll-cue {
    opacity: 0; pointer-events: none;
  }
  @media (prefers-reduced-motion: reduce) {
    .summary-bar .scroll-cue .cue-arrow { animation: none; }
  }
  .summary-bar .bar-row { display: flex; gap: 9px; }
  /* Both buttons share the row evenly. min-width:0 lets them shrink below their
     text's intrinsic width (flex items default to min-width:auto), and the
     label may wrap to two lines, so a long "Drill missed …" label can never
     push the button off-screen — the failure mode was label-length dependent. */
  .summary-bar .bar-row > .btn {
    flex: 1 1 0; min-width: 0;
    white-space: normal; line-height: 1.15;
    /* Compact — overrides .btn.big so the row stays low-profile. */
    font-size: 16px;
    padding: 9px 10px;
    border-radius: 13px;
  }
  /* In the bar, the Next CTA fills width and drops its huge padding so the
     whole bar stays compact. */
  .summary-bar .next-step-cta { width: 100%; }
  .summary-bar .next-step-cta .btn { width: 100%; font-size: 20px; padding: 12px 20px; }
  /* Demoted low-frequency actions live in the scroll, as quiet links. */
  .summary-demote {
    display: flex; flex-direction: column; gap: 6px;
    align-items: center; margin-top: 18px;
  }
  .summary-demote .btn.secondary-link { font-size: 15px; }

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

/* ════════════════════════════════════════════════════════════════════════
   STUDIO P0 — the level-up moment.
   The persistent status masthead (.qn-status*) injects its OWN styles from
   qn-home.js, because the hub pages (path/dashboard) do NOT link qn-theme.css
   and have colliding class names (.btn/.card/.stat-card) — a component that
   carries its own CSS reaches any page with zero collision risk. The level-up
   interstitial below stays here: it only ever fires INSIDE a module's
   round-end, and modules all link qn-theme.css.
   ════════════════════════════════════════════════════════════════════════ */

/* ── The level-up moment (interstitial fired from qn-roundend.js) ───────── */
.levelup-overlay {
  position: fixed; inset: 0; z-index: 1000;
  display: flex; align-items: center; justify-content: center;
  background: rgba(42,42,62,0.55);
  opacity: 0; transition: opacity 240ms ease;
  padding: 24px;
}
.levelup-overlay.show { opacity: 1; }
.levelup-card {
  position: relative; overflow: hidden;
  background: var(--white);
  border: 2.5px solid var(--ink);
  border-radius: 24px;
  box-shadow: 0 8px 0 var(--shadow-cool);
  padding: 30px 28px 24px;
  max-width: 360px; width: 100%;
  text-align: center;
  transform: scale(.86);
}
.levelup-overlay.show .levelup-card { animation: levelUpFlourish .6s cubic-bezier(.34,1.56,.64,1) forwards; }
.levelup-card::before {       /* celebrate sweep backdrop, gated below */
  content: ''; position: absolute; inset: 0; z-index: 0;
  background: linear-gradient(120deg, transparent 30%, rgba(31,184,168,0.12) 50%, rgba(91,63,228,0.12) 60%, transparent 80%);
  transform: translateX(-100%);
}
.levelup-overlay.show .levelup-card::before { animation: levelUpSweep 1.1s ease 0.2s forwards; }
.levelup-card > * { position: relative; z-index: 1; }
.levelup-eyebrow { font-family: 'Nunito', sans-serif; font-weight: 800; font-size: 13px; letter-spacing: .08em; text-transform: uppercase; color: var(--teal-dk); }
.levelup-ring { margin: 10px auto 6px; width: 96px; height: 96px; position: relative; }
.levelup-ring svg { width: 96px; height: 96px; transform: rotate(-90deg); }
.levelup-ring .qn-ring-track { stroke: var(--teal-lt); stroke-width: 7; fill: none; }
.levelup-ring .qn-ring-fill  { stroke: var(--rank-gold); stroke-width: 7; fill: none; stroke-linecap: round; }
.levelup-ring .levelup-num {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-family: 'Fredoka', sans-serif; font-weight: 700; font-size: 38px; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.levelup-rank { font-family: 'Fredoka', sans-serif; font-weight: 700; font-size: 26px; color: var(--ink); margin: 2px 0; }
.levelup-sub  { font-family: 'Nunito', sans-serif; font-weight: 700; font-size: 15px; color: var(--ink-soft); margin-bottom: 18px; }
.levelup-btn {
  font-family: 'Fredoka', sans-serif; font-weight: 700; font-size: 17px;
  background: var(--teal); color: var(--white);
  border: 2.5px solid var(--ink); border-radius: 14px;
  box-shadow: 0 4px 0 var(--ink); padding: 11px 28px; cursor: pointer;
  transition: transform 80ms ease, box-shadow 80ms ease;
}
.levelup-btn:active { transform: translateY(4px); box-shadow: 0 1px 0 var(--ink); }

/* confetti particles (built + positioned inline by qn-roundend.js) */
.levelup-particles { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.levelup-particle {
  position: absolute; top: 46%; left: 50%;
  font-size: 16px; opacity: 0;
  animation: levelUpBurst 1s ease-out forwards;
}

@keyframes levelUpFlourish {
  0%   { transform: scale(.86); }
  60%  { transform: scale(1.04); }
  100% { transform: scale(1); }
}
@keyframes levelUpSweep   { to { transform: translateX(100%); } }
@keyframes levelUpBurst {
  0%   { opacity: 0; transform: translate(0,0) scale(.5); }
  15%  { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--dx,0), var(--dy,-90px)) scale(1.1); }
}
@media (prefers-reduced-motion: reduce) {
  .levelup-overlay.show .levelup-card,
  .levelup-overlay.show .levelup-card::before { animation: none; }
  .levelup-card { transform: scale(1); }
  .levelup-particle { display: none; }
  .levelup-ring .qn-ring-fill { transition: none; }
}
