/* AI Quest — base styles (vanilla, no framework) — "Sunrise Dojo" theme (P2-C) */
:root{
  /* Sunrise Dojo — light-first, one color one job */
  /* WY-V3 W-V2-4 (Ram, app-wide ruling A2): secondary body-text token darkened a few shades —
     the old #5B5A72 passed 4.5:1 on white but still read "a little too light" at small sizes.
     #45445C is ~7.4:1 on --panel white. Night mode gets the mirror move (LIGHTER, same intent:
     more contrast against the dark panel). */
  --bg:#F7F8FC; --panel:#FFFFFF; --panel2:#EDEBFF; --ink:#2A2440; --muted:#45445C;
  /* Opus gate (sv150 dark-sweep wave): --ink-soft and --panel2-ink were CONSUMED globally by that
     wave (.kh-tile-sub here, the minis.js code-output boxes) but only ever DEFINED inside readv2.js's
     own .readv2 scope -- outside it they fell through to their literal fallbacks in BOTH themes, so
     night mode lost its themed value. Defined here as real global tokens, reusing readv2's already
     audited light/night pair (#6B668F / #B3BBDA); readv2's own scoped copies still win inside .readv2. */
  --ink-soft:#6B668F; --panel2-ink:#2A2440;
  /* B24 (Ram-ruled): white #btn text on --brand computed 4.35:1 (just under 4.5). #7C5CFF -> #7458F0
     clears 4.5:1 (4.77:1 measured) while staying visually the same purple; brand-shade nudged to match. */
  --brand:#7458F0; --brand-shade:#5230C9; --brand2:#00D4B8; --line:#E3E6F2;
  --good:#2ECC71; --warn:#FFC800; --error:#FF5D5D; --streak:#FF9F1C;
  /* WP-2 bug-sweep (2026-08-12, dark-mode class-sweep): --good/--error are tuned as
     fill/border/accent colors (3:1 non-text bar) -- #2ECC71 on white is only 2.10:1 as small BODY
     TEXT, which is why .opt.right/.msg.ok/.pill.ok already reach past --good for a separate,
     darker literal (#1C8A4B) whenever the color labels actual text. These two tokens name that
     "text-safe" pairing so future call sites reuse it instead of reaching for --good/--error (or a
     fresh hardcoded hex) directly on a panel background. Values are the exact literals already
     proven throughout .msg.err/.msg.ok/.opt.right/.opt.wrong -- not new colors. */
  --error-text:#B23A3A; --good-text:#1C8A4B;
  --radius:16px; --shadow:0 4px 14px rgba(42,36,64,.08);
  --font-display:"Baloo 2",system-ui,sans-serif; --font-body:"Nunito",system-ui,sans-serif;
  --font-mono:"JetBrains Mono",monospace;
  --belt: var(--brand); --belt-shade: var(--brand-shade); --belt-tint: var(--panel2);
}
/* P2-F: "Neon Dojo" night mode — grades 7-12 opt-in toggle (kid-home button),
   same token names so every component that already uses var(--x) just works.
   Applied via [data-theme="night"] on <html> (app.js setTheme(), localStorage-persisted). */
[data-theme="night"]{
  --bg:#0B0E1A; --panel:#151A2E; --panel2:#1E2540; --ink:#F0F2FF; --muted:#B3BBDA; /* WY-V3 W-V2-4: contrast bump (see :root note) */
  --ink-soft:#B3BBDA; --panel2-ink:#BFE6CF;   /* night halves of the two tokens added in :root above */
  /* B24 (Ram-ruled): same white-on-brand-button issue as light theme -- #B98CFF measured ~2.5:1 with
     white text, well under 4.5:1. Darkened to #7A4DFF (4.85:1) -- still reads as the "neon violet" but
     passes; brand-shade left as the old --brand value so the button's 3D shadow still darkens further. */
  --brand:#7A4DFF; --brand-shade:#5A35D9; --brand2:#00F5D4; --line:#2A3158;
  --good:#39D98A; --warn:#FFE347; --error:#FF5D7A; --streak:#FF9F1C;
  --error-text:#FF9DB2; --good-text:#5CE8A6;
  --shadow:0 4px 18px rgba(0,0,0,.45);
}
[data-theme="night"] .msg.err{background:#3A1622;color:#FF9DB2;border-color:#5A2233}
/* WP-2 dark-sweep completion (sv136, Opus-caught sibling): C2 drag-drop mini injects its own
   light styles from minis.js (.c2-zone #fbfaff / .c2-chip #fff, no text colour) -> unreadable in
   night. These attribute-scoped overrides (higher specificity) repaint it in the night palette. */
[data-theme="night"] .c2-zone{background:var(--panel2);border-color:var(--line)}
[data-theme="night"] .c2-zone.over{background:var(--panel);border-color:var(--brand)}
[data-theme="night"] .c2-zone-sub,[data-theme="night"] .c2-bank-h{color:var(--muted)}
[data-theme="night"] .c2-bank{background:var(--panel)}
[data-theme="night"] .c2-chip{background:var(--panel2);color:var(--ink);border-color:var(--line)}
[data-theme="night"] .c2-chip.placed.ok{background:#173a2c;border-color:var(--good)}
/* SBF sweep (payments-wave fix round, 2026-08-14, same defect class as the WP-2 sweep just above):
   the "wrong category" sibling state was missed when .placed.ok got its night override — minis.js's
   own .c2-chip.placed.off{background:var(--coral-tint,#fdeee7);...} falls back to that light-pastel
   literal (--coral-tint is only ever defined inside readv2.js's OWN .readv2-scoped variables, not
   globally, so it's always undefined here), while the base .c2-chip{color:var(--ink)} override two
   lines up already resolves near-white in night — near-white text on a light pastel background.
   #3A1622 is this app's own established "coral, dark mode" literal (already used identically at
   readv2.js's --coral-tint:#3A1622 and at this same file's [data-theme="night"] .msg.err
   background, line 39 above) — reused here rather than inventing a new dark red; border-color
   mirrors .placed.ok's own var(--good)-for-"correct" pairing with var(--error)-for-"wrong". */
[data-theme="night"] .c2-chip.placed.off{background:#3A1622;border-color:var(--error)}
[data-theme="night"] .msg.ok{background:#0F3324;color:#5CE8A6;border-color:#1C5A3E}
[data-theme="night"] .msg.info{background:var(--panel2);color:var(--brand2);border-color:var(--line)}
[data-theme="night"] .msg.warn{background:#3A2E0A;color:#FFE347;border-color:#5A4712}
/* R11 bug fix 0e5110f8: email field visibility in dark mode */
[data-theme="night"] #pf-email{border-color:var(--line);color:var(--ink);background:var(--panel)}
[data-theme="night"] .opt.right{background:#0F3324;color:#5CE8A6;border-color:var(--good)}
[data-theme="night"] .opt.wrong{background:#3A1622;color:#FF9DB2;border-color:var(--error)}
[data-theme="night"] .chatmsg.mine{background:#241D42}
[data-theme="night"] .beltchip{color:#0B0E1A}
[data-theme="night"] .celebrate-overlay{background:rgba(0,0,0,.65)}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
html{background:var(--bg)}
body{
  font-family:var(--font-body);
  background:var(--bg);
  color:var(--ink); min-height:100vh; display:flex; flex-direction:column;
}
h1,h2,h3,h4,.brand,.btn{font-family:var(--font-display)}
a{color:var(--brand-shade);text-decoration:none}

.topbar{display:flex;align-items:center;justify-content:space-between;flex-wrap:nowrap;gap:12px;
  padding:8px 20px;border-bottom:1px solid var(--line);position:sticky;top:0;
  background:rgba(255,255,255,.92);backdrop-filter:blur(8px);z-index:41;
  /* UXR2-3: auto-hide on scroll down, reappear on scroll up (all screen sizes). transform only —
     the sticky element keeps its flow box, so hiding causes NO layout jump. The left rail's own
     sticky offset (--hdr-h) is unchanged. */
  transition:transform .26s ease;will-change:transform}
body.topbar-hidden .topbar{transform:translateY(-100%)}
@media (prefers-reduced-motion:reduce){.topbar{transition:none}}
.brand{display:inline-flex;align-items:center;gap:9px;font-weight:800;font-size:20px;letter-spacing:.3px;
  color:var(--brand);cursor:pointer;border:0;padding:0;background:none;line-height:1;flex:none}
.brand-logo{flex:none;display:block}
.brand-word{font-family:var(--font-display);white-space:nowrap}
.brand-4{color:#E06A00}
@media(max-width:520px){.brand-word{font-size:16px}}
.nav{display:flex;gap:10px;align-items:center}
.nav .who{color:var(--muted);font-size:13px;margin-right:6px}

/* ===== Gold Coins (Fable Brief v1, WP2): top-bar coin chip, kid role only ===== */
.coinchip{display:inline-flex;align-items:center;gap:5px;padding:6px 12px;border-radius:20px;
  font-size:13px;font-weight:800;color:#7A5A00;background:#FFF3CD;border:1px solid #FFE79E;
  white-space:nowrap;flex:none}
.coinchip-ico{font-size:15px;line-height:1}
[data-theme="night"] .coinchip{color:#FFE9A8;background:#4A3A08;border-color:#5A4712}
@media(max-width:640px){.coinchip{padding:5px 9px;font-size:12px}}
@media(max-width:380px){.coinchip .coinchip-ico{font-size:13px}}

/* Tier-1 non-blocking "+N 🪙" toast — fades in near the coin chip, never blocks the lesson flow
   (celebrate-overlay's modal is reserved for the bigger quiz-band awards). Fixed-position so it
   floats free of whatever tab/route is on screen when a Tier-1 hook fires. */
.coin-toast{position:fixed;z-index:9996;background:#151A2E;color:#FFE9A8;font-weight:800;font-size:13px;
  padding:6px 12px;border-radius:12px;box-shadow:0 4px 14px rgba(0,0,0,.3);
  opacity:0;transform:translateY(-4px);transition:opacity .25s ease,transform .25s ease;pointer-events:none}
.coin-toast.show{opacity:1;transform:translateY(0)}
@media(max-width:640px){.coin-toast{right:12px !important;left:auto !important;top:64px !important}}

/* Quiz-band coin line inside the existing celebrate() modal (never a new component) */
.celebrate-coins{font-size:16px;font-weight:800;color:#7A5A00;background:#FFF3CD;
  display:inline-block;padding:4px 14px;border-radius:999px;margin-top:8px}
[data-theme="night"] .celebrate-coins{color:#FFE9A8;background:#4A3A08}

/* ===== R1.3 C2 (WP2): the greeting strip INSIDE the header (Ram's mockup) =====
   Avatar -> "Hi <name>!" -> belt chip -> topics-mastered bar -> Invite a friend.
   It sits between the brand and the nav and takes the slack, so the nav stays hard right.
   Belt chip + bar are kid-only (app.js only fills them on a belt page). */
.hdr-hi{flex:1;min-width:0;margin:0 18px}
/* inline-flex so the card hugs its contents: a parent/admin (greeting only) gets a small pill,
   a kid (chip + bar + invite) gets a wide one. Without this the empty card spanned the header. */
/* Ram design (shell hotfix): the greeting is a SLIM inline strip (no bordered card) so the top bar
   reads as one tidy row: avatar + "Hi <name>" + belt chip + a slim inline progress meter. */
.hdr-hi-inner{display:inline-flex;align-items:center;gap:10px;flex-wrap:nowrap;max-width:100%;
  background:none;border:0;border-radius:0;padding:0}
/* QA hotfix 2026-08-13: the button had no width/height, so a kid's CUSTOM avatar photo
   (an <img> at width:100%/height:100% of this button) rendered at the photo's natural size —
   a huge picture over the whole header. Fixed circle + overflow clip bounds ANY content
   (emoji or photo) to icon size; flex centering keeps the emoji exactly as before. */
.hdr-avatar{width:34px;height:34px;font-size:22px;line-height:1;border:0;background:none;cursor:pointer;padding:0;flex:none;display:flex;align-items:center;justify-content:center;border-radius:50%;overflow:hidden}
.hdr-hi-txt{display:flex;align-items:center;gap:8px;flex-wrap:nowrap;min-width:0}
.hdr-greet{font-family:"Baloo 2",inherit;font-weight:800;font-size:17px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.hdr-beltchip{white-space:nowrap}
.hdr-progress{display:flex;align-items:center;gap:8px;flex:1;min-width:220px}
/* a `display` rule beats the plain [hidden] attribute, so say it explicitly: a parent/teacher/admin
   (and a kid off a belt page) must NOT see an empty belt chip, an empty bar, or Invite a friend */
.hdr-beltchip[hidden],.hdr-progress[hidden],.hdr-invite[hidden]{display:none !important}
.hdr-progress-bar{flex:1;height:9px;background:#e6e2f2;border-radius:6px;overflow:hidden}
.hdr-progress-fill{height:100%;background:var(--belt,var(--brand,#6c4cf1));border-radius:6px;transition:width .4s}
.hdr-progress-txt{font-size:12px;color:var(--muted);white-space:nowrap}
[data-theme="night"] .hdr-progress-bar{background:#2c2740}
@media(max-width:900px){
  /* narrow: keep the greeting + chip, drop the bar and the invite button — the nav must not wrap away */
  .hdr-hi{margin:0 10px}
  .hdr-progress,.hdr-invite{display:none !important}
}
@media(max-width:640px){
  .topbar{flex-wrap:wrap}
  .hdr-hi{order:3;flex-basis:100%;margin:8px 0 0}
  /* B6 (08-12 rulings, settings-menu clipped at 375px): at these widths #nav (coin chip + name +
     the ⚙️ settings-menu-wrap) can outgrow the room left after brand, so flex-wrap above drops it
     to its own topbar row. justify-content:space-between only spreads a line's items apart when
     there are 2+ of them — a LONE wrapped item collapses to flex-start (the line's left edge),
     dragging .settings-menu-wrap left with it. The ⚙️ dropdown (app.js, WP-A-owned — injected
     style, not this file) anchors itself with right:0 relative to that wrap; once the wrap is no
     longer flush against the topbar's true right edge, right:0 still "resolves" but against a
     point near the LEFT of the screen, so the 220px-wide panel runs off the viewport's left edge —
     the reported clipping. Root-cause fix: pin #nav to its own line's right edge with an auto
     margin — a no-op when it still shares line 1 with brand (space-between already put it there)
     and the real fix when it wraps alone. */
  #nav{margin-left:auto}
}

/* ===== Persistent shell + left belt-menu (kid routes only; hidden elsewhere) ===== */
/* body stays flex-column: header / shell / footer. flex:1 moves OFF .app ONTO .shell. */
.app{max-width:1000px;width:100%;margin:0 auto;padding:26px 20px 130px}   /* was: flex:1;max-width:1000px;... — drop flex:1. bottom padding bumped to 130px (Wave 3 fix): the fixed #bug-fab (bottom:16px, ~85px tall) was landing directly on top of end-of-page action buttons (e.g. FakeOut step-1 "Next ->") on pages short enough that the last button naturally sits near the viewport bottom -- a real kid could not tap through it. Extra bottom clearance keeps the last on-page button above the fab on every route. */
.shell{flex:1;display:flex;align-items:flex-start;gap:16px;width:100%;max-width:1180px;margin:0 auto;position:relative}
.shell:not(.with-menu) .app{max-width:1000px;margin:0 auto}         /* menu hidden -> byte-for-byte old centered layout */
.shell.with-menu .app{max-width:none;flex:1;min-width:0;margin:0}   /* menu shown -> #app fills the remaining width */

.belt-menu{flex:0 0 200px;align-self:flex-start;position:sticky;top:88px;z-index:1;max-height:calc(100vh - 100px);overflow-y:auto;
  background:var(--panel2);border:1px solid var(--line);border-radius:14px;padding:12px 8px;margin:12px 0}
.belt-menu[hidden]{display:none}
.bm-title{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);padding:2px 10px 8px}
.bm-belt{display:flex;align-items:center;gap:8px;width:100%;padding:9px 10px;border:0;border-left:4px solid transparent;
  border-radius:9px;background:transparent;font:inherit;font-size:14px;color:var(--ink);text-align:left;cursor:pointer}
.bm-belt:hover{background:rgba(124,92,255,.10)}
.bm-belt.on{background:var(--panel);font-weight:700;box-shadow:0 1px 4px rgba(0,0,0,.08)}
.bm-belt.locked{opacity:.5}
.bm-belt.locked:hover{background:transparent}
.bm-dot{width:12px;height:12px;border-radius:50%;flex:0 0 auto;border:1px solid rgba(0,0,0,.15)}
.bm-name{flex:1;min-width:0}
.bm-badge{font-weight:800;margin-left:auto}
.bm-sub{list-style:none;margin:2px 0 8px;padding:0 0 0 26px}
.bm-item{display:block;width:100%;text-align:left;font:inherit;font-size:13px;color:var(--ink);
  background:transparent;border:0;padding:6px 10px;border-radius:8px;cursor:pointer}
.bm-item:hover{background:rgba(124,92,255,.10)}
.bm-item.on{background:var(--panel);font-weight:700}

/* hamburger: desktop hidden; shown <=720px only when a kid menu is present */
.nav-burger{display:none;border:0;background:none;font-size:22px;line-height:1;color:var(--ink);cursor:pointer;padding:0;flex:none}
@media(max-width:720px){
  body.has-belt-nav .nav-burger{display:inline-flex}
  .shell{flex-direction:column}
  .belt-menu{position:fixed;top:0;left:0;bottom:0;z-index:40;flex:none;width:78vw;max-width:300px;margin:0;
    border-radius:0;transform:translateX(-100%);transition:transform .22s ease;padding-top:88px;box-shadow:0 0 40px rgba(0,0,0,.25)}
  .belt-menu[hidden]{display:block}          /* keep in DOM for the slide; off-canvas transform hides it */
  body.bn-open .belt-menu{transform:translateX(0)}
  body.bn-open::after{content:"";position:fixed;inset:0;z-index:39;background:rgba(0,0,0,.35)}
  .shell.with-menu .app{max-width:1000px;margin:0 auto}   /* on mobile the drawer overlays; #app re-centers */
}
[data-theme="night"] .belt-menu{box-shadow:0 0 40px rgba(0,0,0,.55)}

/* ===== #18: desktop belt rail aligned to Ram's UI Shell Template — compact static top bar + a
   FLUSH-LEFT, AUTO-HIDE left rail + a target area. Keeps the aiquest palette; master template file
   is not touched. Only active >720px (the <=720px off-canvas drawer above stays as-is).
   ROBUSTNESS: the rail is an IN-FLOW STICKY column (never position:fixed), so it always begins BELOW
   the top bar — no overlap regardless of top-bar height (greeting card + TESTER banner). Its sticky
   offset uses --hdr-h, the LIVE top-bar height set by a ResizeObserver in app.js, so a pinned rail sits
   exactly under the sticky top bar when scrolling. The content column is the flex sibling (auto width);
   the ☰ button hides the rail (content reflows full-width); a far-left hover strip brings it back. ===== */
.rail-hotzone{display:none}
@media(min-width:721px){
  body.has-belt-nav .nav-burger{display:inline-flex;color:var(--brand)}
  .shell.with-menu{max-width:none;margin:0;padding:0;gap:0;align-items:stretch}
  .shell.with-menu .app{max-width:1000px;margin:0 auto;min-width:0;flex:1 1 auto}
  .shell.with-menu .belt-menu{position:sticky;top:var(--hdr-h,72px);align-self:flex-start;
    flex:0 0 220px;width:220px;margin:0;padding:12px 8px 24px;border:0;border-right:1px solid var(--line);
    border-radius:0;max-height:calc(100vh - var(--hdr-h,72px));overflow-y:auto;background:var(--panel2);z-index:5}
  body.rail-hidden .shell.with-menu .belt-menu{display:none}
  .rail-hotzone{position:fixed;left:0;top:var(--hdr-h,72px);bottom:0;width:16px;z-index:31}
  body.rail-hidden.has-belt-nav .rail-hotzone{display:block}
  /* keep the top bar to ONE compact row: a SLIM inline progress meter (not the full-width bar), and a
     belt chip that truncates rather than wrapping. Case File / Bug tracker / Invite now live in the rail. */
  .hdr-progress{min-width:0;flex:0 0 auto;gap:6px}
  .hdr-progress-bar{width:64px;flex:none}
  .hdr-progress-txt{white-space:nowrap;font-size:12px}
  .hdr-beltchip{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:0 1 auto}
}
/* rail "Tools" utility group (Case File / Bug tracker / Invite), separated from the belt list */
.bm-util{margin-top:14px;padding-top:10px;border-top:1px solid var(--line)}
.bm-util-item{display:block;width:100%;text-align:left}

.loading{color:var(--muted);text-align:center;padding:60px}

.foot{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;
  padding:12px 22px;border-top:1px solid var(--line);color:var(--muted);font-size:12px}
.foot-tag{opacity:.8;font-style:italic}
/* S4 fix (Opus Wave sv138 inspection): groups .foot-tag + .foot-legal as ONE .foot flex child so
   the tag stays right-aligned against #foot-status exactly like before .foot-legal existed --
   scoped to this wrapper only, .foot's own display:flex;justify-content:space-between is untouched. */
.foot-right{display:flex;align-items:center;gap:12px;flex-wrap:wrap}

/* buttons — Duolingo-style 3D press: solid fill + darker bottom border that collapses on press */
.btn{display:inline-flex;align-items:center;justify-content:center;min-height:44px;
  border:0;cursor:pointer;font-weight:700;font-size:15px;
  padding:12px 20px;border-radius:14px;color:#fff;background:var(--brand);
  box-shadow:0 4px 0 var(--brand-shade);transform:translateY(0);
  transition:transform .08s,box-shadow .08s}
.btn:hover{filter:brightness(1.04)}
.btn:active{transform:translateY(4px);box-shadow:none}
.btn.alt{background:var(--panel2);color:var(--ink);box-shadow:0 4px 0 var(--line)}
.btn.ghost{background:transparent;border:1px solid var(--line);color:var(--ink);box-shadow:none}
.btn.ghost:active{transform:none}
.btn.sm{padding:8px 14px;font-size:13px;border-radius:11px;min-height:44px}
/* R2 fix wave #7 (Bug_Review_SidV_R2 finding #7, "not able to choose true or false" / "reveal and
   finish button not responsive"): several mini-project pickers (minis.js C3 fact-check-trail
   True/False/Can't-tell, the privacy-photo Safe/Unsafe choice, and any other .btn.ghost toggle)
   add/remove an "on" class on tap to mark the selected choice, exactly like .chip.on / .ltab.on /
   .bm-item.on elsewhere in this file -- but NO rule ever styled bare .btn.on, so the click WAS
   registering in JS state (confirmed live: classList picks up "on") with ZERO visible change. A
   kid tapping True/False saw nothing happen and reasonably reported the button as unresponsive.
   This one rule covers every existing and future .btn.ghost "on"-toggle picker app-wide. */
.btn.ghost.on{background:var(--brand);color:#fff;border-color:transparent;box-shadow:0 2px 8px rgba(124,92,255,.35)}
.btn:disabled{opacity:.5;cursor:not-allowed;transform:none}

/* cards & layout */
.grid{display:grid;gap:16px}
.grid.cols{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}
.card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);
  padding:20px;box-shadow:var(--shadow)}
button.card{font:inherit;color:inherit;display:block;width:100%;cursor:pointer}
button.card:disabled{cursor:not-allowed}
.card h3{margin:.1em 0 .4em}
:focus-visible{outline:3px solid var(--brand2);outline-offset:2px}
/* f14: some browsers (older WebKit especially) paint their OWN default blue focus ring on a plain
   :focus even when a custom :focus-visible rule exists above — the standard fallback is to
   explicitly suppress the native ring whenever :focus-visible wouldn't have shown one anyway
   (i.e. a mouse click, not keyboard nav). Covers the quiz-feedback stars/chips and the read/lesson
   tab buttons, which is where testers saw the stray native outline. */
:focus:not(:focus-visible){outline:none}
/* A11Y AUDIT FIX #3 (4/10 score): this rule used to be `.fb-stars .star:focus{outline:none}`,
   an UNCONDITIONAL :focus (not :focus-visible) suppression that stripped the keyboard focus ring
   off the quiz-feedback stars even for keyboard/AT users, overriding the correct :focus-visible
   rule above it. The f14 comment above already gives .fb-stars .star the right behavior generically
   (native ring hidden only on a non-keyboard :focus, :focus-visible ring shown otherwise) — this
   rule was pure regression and is removed rather than re-scoped. */
.muted{color:var(--muted)}
/* A11Y AUDIT FIX #4: standard visually-hidden utility for aria-live regions that carry an
   announcement but have no sighted-UI reason to take up layout space (chat-log new-message
   announcer, etc. — the app had no such class before this pass). Clip-rect technique, not
   display:none/visibility:hidden, so assistive tech still reads it. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.hero{text-align:center;padding:40px 10px 10px}
.hero h1{font-size:clamp(24px,6vw,34px);margin:.2em 0}
.hero p{color:var(--muted);max-width:620px;margin:8px auto;font-size:clamp(14px,3.5vw,16px)}

/* role picker */
.roles{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px;margin-top:22px}
.role{cursor:pointer;text-align:center;transition:transform .1s,border-color .1s}
.role:hover{transform:translateY(-3px);border-color:var(--brand)}
.role .emoji{font-size:34px}

/* forms */
.form{max-width:420px;margin:0 auto}
.form label{display:block;font-size:13px;color:var(--muted);margin:12px 0 4px}
.form input,.form select,.form textarea{width:100%;min-height:44px;padding:11px 12px;box-sizing:border-box;border-radius:9px;
  border:1px solid var(--line);background:var(--panel);color:var(--ink);font-size:16px}
.form textarea{min-height:auto}
.form .row{display:flex;gap:10px}.form .row>*{flex:1;min-width:0}
.msg{margin:12px 0;padding:10px 12px;border-radius:9px;font-size:13px}
.msg.err{background:#FFEAEA;color:#B23A3A;border:1px solid #FFC9C9}
.msg.ok{background:#E7F9EF;color:#1C8A4B;border:1px solid #BEEFD3}
.msg.info{background:var(--panel2);color:var(--brand-shade);border:1px solid #D8D3FF}

/* belt chips */
.belts{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
.beltchip{padding:6px 10px;border-radius:20px;font-size:12px;font-weight:700;color:#222}
.beltchip.big{font-size:18px;padding:12px 22px}
/* MAJOR-7 fix: Black Belt (belts.color = #4A4A4A, _content_sql/belts.0.sql) is the one belt dark
   enough that the shared #222 chip text fails AA -- measured 1.80:1 in day mode, and the night
   override below (#0B0E1A, meant for the other 6 light pastel belts) made it worse at 2.17:1. The 6
   other belts (white/yellow/orange/green/blue/brown) are all light-to-mid pastels and clear >=6.7:1
   against #222/#0B0E1A already (see Testing/_monday_nightfix_checks.cjs), so this is scoped to just
   the one belt color rather than reworked as a per-instance JS contrast calc (chips are rendered
   inline by many files outside this fix's scope: admin.js/teacher.js/parent.js/sis.js/lesson.js/
   placement.js). Matches the inline `style="background:#4A4A4A"` belt.color renders unconditionally
   emit (case-insensitive, in case of future casing drift) and wins in BOTH themes: measured 8.13:1. */
.beltchip[style*="4A4A4A" i]{color:#F5F5F5}

.msg.warn{background:#FFF6DA;color:#8A6B00;border:1px solid #FFE79E}

/* placement quiz */
.qtext{white-space:pre-wrap;font-family:inherit;line-height:1.45}
.qbar{height:8px;background:var(--panel2);border-radius:999px;margin:8px 0 14px;overflow:hidden}
.qbar-fill{height:100%;background:var(--belt,var(--brand));transition:width .3s;border-radius:999px}
.opts{display:flex;flex-direction:column;gap:10px;margin-top:14px}
.opt{text-align:left;min-height:44px;padding:13px 16px;box-sizing:border-box;border-radius:14px;font-size:14px;cursor:pointer;
  background:var(--panel);border:2px solid var(--line);color:var(--ink);white-space:pre-wrap;
  box-shadow:0 3px 0 var(--line);transition:transform .06s,box-shadow .06s}
.opt:hover{border-color:var(--brand)}
.opt:active{transform:translateY(3px);box-shadow:none}
.opt.selected{border-color:var(--brand);box-shadow:0 0 0 2px var(--brand) inset;font-weight:700}
.opt:disabled{cursor:default;opacity:.95}
.opt.reveal{border-color:var(--good);box-shadow:0 0 0 1px var(--good) inset}
.opt.right{background:#E7F9EF;border-color:var(--good);color:#1C8A4B;font-weight:700}
.opt.wrong{background:#FFEAEA;border-color:var(--error);color:#B23A3A}

/* kid banner + knowledge map */
.kidbanner{display:flex;gap:16px;align-items:center;background:var(--panel);
  border:1px solid var(--line);border-radius:var(--radius);padding:18px 22px;box-shadow:var(--shadow)}
.kb-emoji{font-size:44px}
.kmrow{display:flex;gap:12px;align-items:center;margin-top:10px}
.kmbar{flex:1;height:12px;background:var(--panel2);border-radius:6px;overflow:hidden}
.kmbar-fill{height:100%;background:var(--belt,var(--brand));transition:width .4s;border-radius:6px}

/* character picker chips */
.charlist{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px;min-height:10px}
.charchip{min-height:44px;display:inline-flex;align-items:center;padding:7px 14px;box-sizing:border-box;border-radius:20px;font-size:13px;cursor:pointer;
  background:var(--panel2);border:1px solid var(--line)}
.charchip:hover{border-color:var(--brand)}
.charchip.on{border-color:transparent;background:var(--brand);color:#fff}
.chips{display:flex;gap:8px;flex-wrap:wrap;margin-top:6px}
/* SBF sweep (payments-wave fix round, 2026-08-14): same missing-color defect shape as
   .mbtn/.mopt/.c2-chip.placed.off — .chip is rendered as a real <button class="chip"> in several
   kid-facing spots (engines.js: chat suggestion chips, taboo-category picker, hero-persona preset
   chips), and unlike a <div>/<span> a <button> does NOT inherit body's color:var(--ink) via the
   normal CSS cascade (browsers' UA stylesheet gives <button> its own default text color) — so it
   fell back to browser-default black, unreadable against var(--panel2)'s dark night background.
   --ink is a GLOBAL var already correctly redefined for both themes (styles.css :root / [data-
   theme="night"] at the top of this file), so reusing it here (no separate night-only override
   needed, same as the .mbtn/.mopt fix) resolves correctly in both. Sibling .charchip (same visual
   shape) was checked and left untouched — it is only ever rendered as <div>/<span>, never
   <button>, so it already inherits body's color correctly and was never actually broken. */
.chip{min-height:44px;display:inline-flex;align-items:center;padding:6px 14px;box-sizing:border-box;border-radius:18px;font-size:13px;cursor:pointer;color:var(--ink);
  background:var(--panel2);border:1px solid var(--line)}
.chip.on{border-color:var(--brand2);background:var(--panel2)}

/* bdd281ac: per-box favorite-character type-ahead (profile page) — dropdown sits in normal
   document flow directly under its own input (not position:absolute) so it never overlaps the
   next box and needs no z-index/viewport math on small phone screens. */
.fav-typeahead-list{margin-top:-4px;margin-bottom:2px;border:1px solid var(--line);border-radius:10px;
  background:var(--panel);box-shadow:var(--shadow);max-height:220px;overflow-y:auto}
.fav-typeahead-item{min-height:44px;display:flex;align-items:center;gap:6px;padding:8px 12px;font-size:14px;
  cursor:pointer;border-bottom:1px solid var(--line)}
.fav-typeahead-item:last-child{border-bottom:none}
.fav-typeahead-item:hover{background:var(--panel2)}
.fav-typeahead-item b{color:var(--brand-shade)}

/* tables */
.tbl{width:100%;border-collapse:collapse;font-size:13px;margin-top:6px}
.tbl th{text-align:left;color:var(--muted);font-weight:600;padding:8px 10px;border-bottom:1px solid var(--line)}
.tbl td{padding:8px 10px;border-bottom:1px solid var(--line)}
.ok-dot{color:var(--good);font-size:13px}
.lockedcard{opacity:.55}
.qrow{padding:7px 0;border-bottom:1px solid var(--line);font-size:13px}
.qtext-sm{white-space:pre-wrap}
.rosterbox{overflow-x:auto}

/* group chat */
.chatlog{max-height:380px;overflow-y:auto;display:flex;flex-direction:column;gap:8px;
  padding:10px;background:var(--bg);border:1px solid var(--line);border-radius:10px}
.chatmsg{background:var(--panel2);border:1px solid var(--line);border-radius:10px;
  padding:8px 12px;font-size:14px;max-width:85%;align-self:flex-start}
.chatmsg.mine{align-self:flex-end;background:#EDEBFF;border-color:var(--brand)}
.chatmsg b{color:var(--brand-shade);margin-right:6px;font-size:12px}

/* ---- lesson engine ---- */
.stepper{display:flex;gap:8px;margin:6px 0 2px;flex-wrap:wrap}
.stepdot{font-size:13px;padding:4px 10px;border-radius:999px;background:var(--panel2);
  color:var(--muted);border:1px solid var(--line)}
.stepdot.on{background:var(--belt,var(--brand));color:#fff;border-color:transparent}
.stepdot.did{color:var(--good);border-color:var(--good)}
.sbstage{margin-top:6px}
.sbbeat{text-align:center;padding:18px;background:var(--panel2);border-radius:var(--radius);border:1px solid var(--line)}
.sbemoji{font-size:64px;line-height:1}
.sbbeat h3{margin:10px 0 4px}
.sbsub{color:var(--brand-shade);margin:0 0 10px;font-style:italic}
.sbsay{color:var(--ink);max-width:620px;margin:0 auto;line-height:1.5}
.sbnav{display:flex;align-items:center;justify-content:center;gap:16px;margin-top:16px}
.card.read p{line-height:1.65;font-size:16px}
/* f16: the Detail-tab title (a bare <h4>) fell back to the browser default heading size, which
   computed no larger than the 16px body paragraph it introduces — a title must read as a title. */
.card.read h4{font-size:19px;font-weight:800;margin:.2rem 0 .6rem}
.keyterm{background:var(--panel2);border:1px solid var(--line);border-radius:10px;
  padding:8px 12px;margin-bottom:12px;font-weight:700;color:var(--brand-shade)}
.card.donecard{border-color:var(--good)}
.card.fb{background:var(--panel2);border:1px dashed var(--line)}
.fb-stars{font-size:26px;letter-spacing:4px;cursor:pointer;color:var(--warn)}
.fb-stars .star{cursor:pointer}
.fb-comp{display:flex;gap:6px;flex-wrap:wrap;margin:8px 0}
.chip.on{background:var(--brand);color:#fff;border-color:transparent}

/* SidV feedback TAB (Fable 2026-07-20). One labelled 1-5 star row per dimension. The rows are a
   grid so every star strip lines up under the same left edge regardless of label length; below
   520px the label sits on its own line so the stars never wrap mid-strip on a phone. */
.fb-rows{display:flex;flex-direction:column;gap:6px;margin:4px 0 14px}
.fb-row{display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:10px;
  padding:8px 10px;background:var(--panel);border:1px solid var(--line);border-radius:10px}
.fb-label{font-weight:700;font-size:15px}
.fb-row .fb-stars{font-size:24px;letter-spacing:2px;line-height:1;white-space:nowrap}
.fb-row .star{appearance:none;background:none;border:none;padding:0 1px;font:inherit;
  color:var(--muted);cursor:pointer;min-width:26px;min-height:34px}
.fb-row .star.on{color:var(--warn)}
.fb-row .star:hover{transform:scale(1.12)}
.fb-clear{appearance:none;background:none;border:none;color:var(--muted);cursor:pointer;
  font-size:13px;padding:4px 6px;min-height:32px;border-radius:8px}
.fb-clear:hover{background:var(--panel2);color:var(--ink)}
.fb-notelabel{display:block;font-weight:700;font-size:14px;margin:2px 0 4px}
@media (max-width:520px){
  .fb-row{grid-template-columns:1fr auto;row-gap:2px}
  .fb-label{grid-column:1 / -1}
}
.fb-sug{width:100%;background:var(--panel);color:var(--ink);border:1px solid var(--line);
  border-radius:10px;padding:8px;margin:6px 0;font-family:inherit}

/* ---- P2-C quick wins: icon sprite, mobile-safe tap targets, mascot loader ---- */
.icon{width:1.2em;height:1.2em;fill:currentColor;vertical-align:-.15em}
.byte-icon{vertical-align:middle}
.loading .byte-icon{animation:byte-bounce 1.1s ease-in-out infinite}
@keyframes byte-bounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
.kidbanner h2 .byte-icon{vertical-align:-8px;margin-left:4px}

/* ---- P2-C: persistent stat strip (streak / XP / belt) ---- */
.statstrip{display:flex;gap:14px;align-items:center;font-weight:800;font-size:14px}
.statchip{display:flex;gap:5px;align-items:center;padding:5px 10px;border-radius:999px;background:var(--panel2)}
.statchip.streak{color:var(--streak)}
.statchip.xp{color:var(--warn)}
.statchip.belt{color:#fff;background:var(--belt,var(--brand))}

/* ---- P2-C: belt quest-map path ---- */
.questmap{display:flex;flex-direction:column;align-items:center;gap:0;padding:10px 0;position:relative}
.qnode-wrap{display:flex;align-items:center;gap:14px;position:relative;padding:10px 0}
.qnode-wrap:nth-child(even){flex-direction:row-reverse}
.qnode{width:64px;height:64px;border-radius:50%;display:flex;align-items:center;justify-content:center;
  font-size:26px;background:var(--panel2);border:3px solid var(--line);cursor:pointer;flex:none;
  box-shadow:0 3px 0 var(--line);transition:transform .1s}
.qnode:hover{transform:translateY(-2px)}
.qnode.mastered{background:var(--belt,var(--brand));border-color:var(--belt-shade,var(--brand-shade));
  box-shadow:0 3px 0 var(--belt-shade,var(--brand-shade));color:#fff}
.qnode.current{border-color:var(--belt,var(--brand));box-shadow:0 0 0 4px var(--belt-tint,var(--panel2))}
.qnode.locked{opacity:.45;cursor:not-allowed}
.qnode-label{max-width:220px}
.qnode-label h4{margin:0;font-size:14px}
.qnode-label .meta{font-size:12px;color:var(--muted)}
.qtrack{position:absolute;left:50%;top:0;bottom:0;width:4px;background:var(--line);
  transform:translateX(-32px);z-index:-1}
.byte-here{position:absolute;font-size:30px;transform:translate(38px,-6px)}

/* ---- P2-C: celebration overlay ---- */
.celebrate-overlay{position:fixed;inset:0;background:rgba(42,36,64,.55);display:flex;
  align-items:center;justify-content:center;z-index:50}
.celebrate-card{background:var(--panel);border-radius:24px;padding:36px 44px;text-align:center;
  box-shadow:0 20px 60px rgba(42,36,64,.35);max-width:360px}
.celebrate-emoji{font-size:64px}
/* B24: plain color:var(--warn) (#FFC800) text on the white celebrate-card measured ~1.55:1 --
   turned into an actual pill (tint bg + darkened amber text) instead of just darkening the yellow,
   which stayed muddy at 4.5:1. #B45309 on #FFF3CD measures ~5.0:1. Night mode gets its own pairing. */
.celebrate-xp{font-size:28px;font-weight:800;color:#B45309;background:#FFF3CD;
  display:inline-block;padding:6px 18px;border-radius:999px;margin-top:6px}
[data-theme="night"] .celebrate-xp{color:#FFE9A8;background:#4A3A08}
#confetti-canvas{position:fixed;inset:0;pointer-events:none;z-index:51}

/* ==================================================================
   P2-E: mobile pass — 640px (tablet/small-laptop) and 380px (phone)
   breakpoints. Type sizing already uses clamp() above; this section
   is layout: single-column grids, no zigzag quest-map, tighter chrome.
   ================================================================== */
@media (max-width:640px){
  .topbar{padding:10px 14px}
  .brand{font-size:clamp(18px,5vw,22px)}
  .app{padding:18px 14px}
  .hero{padding:26px 6px 6px}
  .roles{grid-template-columns:1fr}
  .grid.cols{grid-template-columns:1fr}
  .form .row{flex-direction:column;gap:0}
  .card{padding:16px}
  .kidbanner{flex-wrap:wrap}
  /* quest-map: drop the alternating left/right zigzag, one straight column reads better narrow */
  .qnode-wrap,.qnode-wrap:nth-child(even){flex-direction:row}
  .qtrack{transform:translateX(calc(-1 * (32px + 0px)))}
  .celebrate-card{padding:26px 22px;max-width:90vw}
  .stepper{gap:6px}
}
@media (max-width:380px){
  .celebrate-card{padding:20px 16px}
  .celebrate-emoji{font-size:52px}
  .qnode{width:52px;height:52px;font-size:20px}
  .qnode-label{max-width:150px}
  .hero p{margin:8px auto;max-width:94vw}
  .statstrip{gap:8px;font-size:12px}
  .statchip{padding:4px 8px}
}
/* B6 (08-12 rulings) belt-and-suspenders: #nav{margin-left:auto} above (640px block) is the actual
   clipping fix (it keeps the ⚙️ settings-menu-wrap flush against the topbar's true right edge, so
   the dropdown's own right:0 anchor is always valid) — this is only a hard width ceiling for the
   narrowest real phones, so the dropdown (app.js-injected, min-width:220px) can never itself exceed
   the viewport. Two-class selector (specificity 0,2,0) beats the injected single-class .settings-menu
   rule (0,1,0) regardless of which <style> tag loads later, so it applies whether or not app.js has
   run yet. Only sets max-width — never touches display, so it can't interact with the documented
   .settings-menu[hidden] override history (app.js) at all. */
.settings-menu-wrap .settings-menu{max-width:calc(100vw - 28px)}

/* ===== Kid home — tiled dashboard (test099_V1 redesign) ===== */
.kh-layout{display:flex;gap:16px;margin-top:16px;align-items:flex-start}
.kh-rail{flex:0 0 188px;background:#efeafc;border-radius:14px;padding:12px 8px;position:sticky;top:16px}
.kh-rail-title{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted,#6b7280);padding:2px 10px 8px}
.kh-belt{display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:9px;border-left:4px solid transparent;font-size:14px;color:var(--ink,#1f2333)}
.kh-belt.on{background:#fff;font-weight:700;box-shadow:0 1px 4px rgba(0,0,0,.08)}
.kh-belt.future{opacity:.45}
.kh-belt-dot{width:12px;height:12px;border-radius:50%;flex:0 0 auto;border:1px solid rgba(0,0,0,.15)}
.kh-belt-name{flex:1}
.kh-belt-tick{color:var(--good,#2e7d32);font-weight:800}
.kh-belt-here{color:var(--brand,#6c4cf1);font-size:20px;line-height:1}
.kh-main{flex:1;min-width:0}
.kh-top{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;background:var(--panel,#fff);border-radius:14px;padding:16px 18px;box-shadow:0 1px 4px rgba(0,0,0,.06)}
.kh-hi{display:flex;align-items:center;gap:12px}
.kh-emoji{font-size:38px}
.kh-hi h2{margin:0;font-size:22px}
.kh-beltline{margin-top:4px}
.kh-actions{display:flex;gap:8px;flex-wrap:wrap}
.kh-progress{display:flex;align-items:center;gap:10px;margin:12px 2px}
.kh-progress-bar{flex:1;height:10px;background:#e6e2f2;border-radius:6px;overflow:hidden}
.kh-progress-fill{height:100%;background:var(--brand,#6c4cf1);border-radius:6px;transition:width .4s}
.kh-progress-txt{font-size:12px;color:var(--muted,#6b7280);white-space:nowrap}
.kh-tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:12px;margin-top:8px}
.kh-tile{position:relative;display:flex;align-items:center;gap:11px;text-align:left;background:var(--panel,#fff);border:1px solid var(--line,#e7e4f0);border-radius:12px;padding:13px 14px;cursor:pointer;font:inherit;color:inherit;transition:transform .08s,box-shadow .12s}
/* WP5: review-sheet corner square on Mastered tiles */
.kh-review-sq{position:absolute;top:6px;right:6px;width:26px;height:26px;border-radius:8px;display:inline-flex;
  align-items:center;justify-content:center;font-size:14px;background:#fff;border:1px solid var(--line,#e7e4f0);
  box-shadow:0 1px 3px rgba(0,0,0,.12);cursor:pointer;z-index:1}
.kh-review-sq:hover{border-color:var(--brand,#7C5CFF);transform:scale(1.06)}
.kh-tile:hover{transform:translateY(-2px);box-shadow:0 4px 14px rgba(0,0,0,.1)}
.kh-tile-ico{font-size:22px;width:26px;text-align:center;flex:0 0 auto}
.kh-tile-body{display:flex;flex-direction:column;min-width:0}
.kh-tile-title{font-weight:700;font-size:14px;line-height:1.25}
.kh-tile-sub{font-size:13px;color:var(--ink-soft,#6b668f);margin-top:2px}
.kh-tile.done{border-color:#bfe3c6;background:#f3faf4}
.kh-tile.done .kh-tile-ico{color:var(--good,#2e7d32)}
.kh-tile.partial{border-color:#f0d9a8;background:#fffaf0}
.kh-tile.current{border-color:var(--brand,#7C5CFF);box-shadow:0 0 0 2px var(--belt-tint,#EDEBFF)}
.kh-tile.big{grid-column:1/-1}
.kh-tile.ready{border-color:var(--brand,#6c4cf1);background:#f4f1fe}
.kh-tile.locked{opacity:.5;cursor:not-allowed;background:#f6f6f8}
.kh-tile.locked:hover{transform:none;box-shadow:none}
.kh-tile.sel{border-color:var(--brand,#7C5CFF);box-shadow:0 0 0 2px var(--belt-tint,#EDEBFF)}

/* EXTERNAL_GAMES (Round-1 rulings, Ram 2026-08-07) — gamesHub's "More games — free to play"
   section (minis.js). These tiles reuse the SAME .kh-tile visual language as the coin-priced
   grid above so the hub reads as one page, but they are real <a target="_blank"> elements when
   unlocked (never a button + gameView/iframe) and a plain <div aria-disabled="true"> when locked
   by belt — .kh-tile.locked above already covers the dimmed/not-allowed look for the locked case.
   .rg-free-pill replaces the 🪙 price line so kids never read these as costing coins. */
.rg-ext-game-btn{text-decoration:none}
.rg-ext-game-btn.locked{text-decoration:none;cursor:not-allowed}
.rg-free-pill{display:inline-block;margin-top:4px;font-size:11px;font-weight:800;letter-spacing:.02em;
  color:var(--good,#2ECC71);background:rgba(46,204,113,.12);border-radius:999px;padding:2px 9px;width:fit-content}

/* ===== Lesson Reorg (Ram-approved mockup, Lesson_Reorg_Mockup.html) — White/Yellow/Orange ONLY.
   The split lesson tile: a non-clickable top title band. Sits as a normal item inside the EXISTING
   .kh-tiles grid (placement.js), so it inherits the same grid-column sizing as every other tile.
   REDESIGN (sv77): the status-chip row (.rg-tile-status/.rg-circle — "Not started"/"In progress"/
   "Done" + the radio-look dot) is GONE — tester feedback read it as noise. Tabbed Lesson Redesign
   (Ram-approved port, 08-05): every reorg belt now renders the ONE-CLICK tile (.rg-tile-oneclick,
   below) — the original 3-day-button split tile (Week-N · Lesson-N title + three clickable Day
   zones, .rg-tile-days/.rg-day) that used to live here for Yellow/Orange is retired; placement.js's
   dayBtn()/.rg-tile-title-l2 markup for it is deleted. .rg-tile-days/.rg-day themselves STAY defined
   below — the Games tile (placement.js gamesTile) reuses that same family for its own row of
   buttons. ===== */
.rg-tile{background:var(--belt-tint,var(--panel,#fff));border:1px solid var(--line,#e7e4f0);border-radius:12px;overflow:hidden;
  box-shadow:0 1px 3px rgba(0,0,0,.05);transition:box-shadow .12s;
  /* F7(b) (sv77 inspection nit): a grid neighbour whose title wraps to 3 lines grows .rg-tile-top
     taller than the rest of the row's tiles (grid rows don't equalize each tile's INTERNAL split on
     their own) -- flex column + pushing .rg-tile-days down with margin-top:auto (below) bottom-aligns
     the Games tile's day buttons across the row regardless of its own title's line count. Mobile
     stack (.rg-tile-days{flex-direction:column} at 640px) is untouched -- that's the row's OWN
     children, not this outer column axis. */
  display:flex;flex-direction:column}
.rg-tile.done{border-color:#bfe3c6}
.rg-tile.locked{opacity:.6}
.rg-tile-top{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px 14px;
  background:var(--belt-tint,#EDEBFF);border-bottom:1px solid var(--line,#e7e4f0)}
.rg-tile-title{display:flex;flex-direction:column;gap:2px;font-weight:700;font-size:13.5px;line-height:1.3;color:var(--brand,#7C5CFF)}
.rg-tile-title-l1{font-size:11.5px;font-weight:700;letter-spacing:.02em;color:var(--muted,#6b7280);text-transform:uppercase}
/* 53ac1a28: one-click tile (now every reorg belt) — name first, lesson number smaller below */
.rg-tile-name{font-size:14.5px;font-weight:800;color:var(--brand,#7C5CFF)}
.rg-tile-lessonno{font-size:11px;font-weight:700;letter-spacing:.02em;color:var(--muted,#6b7280);margin-top:2px}
/* the Games tile reuses the same rg-tile/rg-tile-top/rg-tile-days family — a single title line only.
   This is the ONLY surviving caller of .rg-tile-title-l1 now that the lesson tile's old two-line
   Week-N/title header (which paired it with the retired .rg-tile-title-l2) is gone. */
.rg-games-tile .rg-tile-title-l1{font-size:14.5px;text-transform:none;color:var(--brand,#7C5CFF)}
/* Games tile only, below (placement.js gamesTile): one row of equal-width buttons — .rg-day's flex:1
   already distributes evenly across however many buttons are present, so no column-count rule is
   needed here beyond keeping the mobile stack below working for any child count (it already does). */
.rg-tile-days{display:flex;min-height:96px;margin-top:auto}
.rg-day{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;
  padding:12px 8px;position:relative;background:var(--panel,#fff);border:0;font:inherit;color:inherit;text-align:center}
.rg-day+.rg-day{border-left:1px solid var(--line,#e7e4f0)}
.rg-day-label{font-weight:800;font-size:15px;color:var(--brand,#7C5CFF)}
.rg-day-sub{font-size:11px;color:var(--muted,#6b7280);font-weight:700}
.rg-day.clickable{cursor:pointer;transition:background .12s,transform .08s}
.rg-day.clickable:hover{background:var(--belt-tint,#EDEBFF)}
.rg-day.clickable:active{transform:translateY(1px)}
.rg-day.inert{opacity:.55;cursor:not-allowed}
@media (max-width:640px){.rg-tile-days{flex-direction:column}.rg-day+.rg-day{border-left:0;border-top:1px solid var(--line,#e7e4f0)}}

/* ===== Tabbed Lesson Redesign (Ram-approved port, 08-05) — the one-click tile, now shared by every
   reorg belt (W/Y/O). Replaces the retired 3-day-button split tile above; the WHOLE .rg-tile is the
   click target (placement.js wires .rg-tile-oneclick[data-code]). ===== */
.rg-tile-oneclick{cursor:pointer;transition:background .12s,transform .08s}
.rg-tile-oneclick:hover{filter:brightness(.96)} /* tint==resting bg, so brightness gives real hover feedback on any belt */
.rg-tile-oneclick:active{transform:translateY(1px)}
.rg-tile-oneclick:focus-visible{outline:2px solid var(--brand,#7C5CFF);outline-offset:-2px}
/* Ram sv81 dry-run: tile is ONE clean pane — no ticks row, no internal divider lines;
   name + Lesson-N centered both axes. */
.rg-tile-oneclick .rg-tile-top{border:0;flex:1;justify-content:center;min-height:96px}
.rg-tile-title-center{text-align:center}

/* ===== Day-3 Review tab: Big-Idea recap rows (lesson.js showDay3Review) — the same numbered-row
   visual language as readv2.js's buildKeyPoints() .bigrow/.bn (that file's styles are scoped
   .readv2-only + light-mode-locked, so this is a theme-aware equivalent living here instead of
   duplicating/leaking into that scope). ===== */
.rg-bigidea-list{margin-top:6px}
.rg-bigidea-row{display:flex;gap:13px;align-items:flex-start;padding:11px 0}
.rg-bigidea-row+.rg-bigidea-row{border-top:1.5px dashed var(--line)}
.rg-bigidea-n{width:27px;height:27px;border-radius:50%;background:var(--belt-tint,var(--panel2));
  color:var(--brand-shade,var(--brand));display:flex;align-items:center;justify-content:center;
  font-weight:800;font-size:13px;flex:none}
.rg-bigidea-row p{margin:0;font-size:16px;font-weight:600;line-height:1.55}
[data-theme="night"] .rg-bigidea-n{color:var(--brand2)}
/* Tabbed Lesson Redesign (Ram-approved port, 08-05) — Day-3 Review, crisper: 1 line per Big Idea
   (lesson.js showDay3Review crispBigIdea()). Same row family, tighter spacing + smaller type since
   each row is now a single (complete) sentence instead of the full paragraph. Applied to every reorg
   belt now — showDay3Review() always adds this class (the old White-only branch that left Yellow/
   Orange on the plain, untouched full-text .rg-bigidea-list is retired).
   F4 fix (bug 4c937061, Ram ruling 2026-08-06): white-space:nowrap + text-overflow:ellipsis here was
   RE-TRUNCATING crispBigIdea()'s output at the container's width regardless of what the JS-level cap
   was — the actual visible cause of testers' "incomplete sentences" screenshots, on top of (and
   independent from) the JS cap lesson.js also had. crispBigIdea() now returns the complete first
   sentence with no length cap, so this row must wrap and show all of it, never clip it. */
.rg-bigidea-crisp .rg-bigidea-row{padding:7px 0}
.rg-bigidea-crisp .rg-bigidea-row p{font-size:14.5px;font-weight:600;line-height:1.4}
.rg-bigidea-crisp .rg-bigidea-n{width:22px;height:22px;font-size:11.5px}

/* ===== gameView (redesign, sv77): full-page mini games framed IN the SPA shell (minis.js). The
   iframe fills the content area below the "← Back" link; height is derived from the live top-bar
   variable (--hdr-h, set by app.js's ResizeObserver) so it never overlaps the sticky header. ===== */
.gv-wrap{width:100%;margin-top:10px}
.gv-frame{display:block;width:100%;height:calc(100vh - var(--hdr-h,72px) - 170px);min-height:480px;
  border:0;border-radius:var(--radius);box-shadow:var(--shadow);background:var(--panel)}

/* Sid dry-run fix (bug 5626d20b, 2026-08-09): "the arcade should become the whole screen not this
   small window." Byte's Arcade is already its own full-bleed galaxy-themed document (arcade.html's
   .ovl overlay etc) when visited standalone at /games/arcade -- framed in-shell it was getting the
   SAME boxed sizing as the other 3 games (gv-frame's -170px header/footer allowance + 480px floor),
   which read as a small window next to that. minis.js's gameView route adds these classes ONLY for
   slug==="arcade" (gtp/ttt/cipher/pet keep the untouched .gv-frame rule above) so the arcade
   presents as large as the SPA shell allows: full remaining height below the sticky top bar, minus
   just enough room for the "← Back" link above it. */
.gv-wrap.gv-arcade{margin-top:4px}
.gv-frame.gv-frame--arcade{height:calc(100vh - var(--hdr-h,72px) - 26px);min-height:420px}

/* Lesson Reorg lesson-page "Day 1"/"Day 2" badge (lesson.js frame()) — RETIRED (Tabbed Lesson
   Redesign, Ram-approved port, 08-05): every reorg belt now shows the .dtabs row below instead,
   which already indicates the active day, so the badge markup was removed as dead (a badge would
   just repeat the .dtabs row). .day-badge itself is deleted here too — grep confirms lesson.js no
   longer emits the markup that referenced this class. */

/* Tabbed Lesson Redesign (Ram-approved port, 08-05) — TOP row of the lesson page: Day 1/2/3 tabs,
   sitting above the SECOND row of per-day content tabs (.ltabs/.ltab, reused unchanged below), now
   shared by every reorg belt (was White-only pre-port). A distinct class family (.dtabs/.dtab) keeps
   this row visually and structurally separate from .ltabs/.ltab so every existing
   ".ltab[data-tab=...]" selector elsewhere keeps meaning "the current day's content tab", never
   "which day". */
/* Sid dry-run fix (bug 51c0e820): "attach the Day-1 tab to Story/The Big Idea/See it happen/Feedback
   ... like a browser tab attaches to its page" — no gap between the Day row and the content-tab row
   below it. Was margin:10px 0 6px (6px gap to .ltabs); now 0 so the two tab rows read as one unit. The
   active day pill also overlaps 2px into whatever sits directly below it (see .dtab.on) so there's no
   hairline seam either. Applies everywhere (every reorg-belt lesson) — this is the shared class, not a
   per-lesson tweak. */
.dtabs{display:flex;gap:8px;margin:10px 0 0;flex-wrap:wrap}
.dtab{appearance:none;border:1.5px solid var(--line);background:var(--panel2);color:var(--ink);
  border-radius:999px;padding:6px 16px;font:inherit;font-size:13px;font-weight:800;cursor:pointer}
.dtab:hover{background:var(--panel)}
.dtab.on{background:var(--brand);color:#fff;border-color:transparent;box-shadow:0 2px 10px rgba(124,92,255,.30);
  position:relative;margin-bottom:-2px}
/* C8 (08-12 rulings): "days already done" cue — sits inside .dtabs (lesson.js frame()) next to the
   Day pills, reassuring a kid who lands on (or revisits) Day 2/3 that earlier days carry a real ✓,
   not a silent skip. Never shown on Day 1 (lesson.js only emits it for day>1). Baked into .dtabs'
   own template string on every frame() repaint — never appended after the fact — so it is present
   at first paint and causes no layout shift. Pill-shaped like the other small status chips
   (.coinchip/.statchip) but its own class: those are either interactive or theme-specific (coin
   gold), and this is a passive status readout, so no cursor:pointer and no click handler, ever. */
.day-done-cue{display:inline-flex;align-items:center;padding:5px 12px;border-radius:999px;
  background:var(--panel2);border:1px solid var(--line);color:var(--good);font-size:12px;
  font-weight:800;white-space:nowrap;flex:none}

/* WP1/WP2 discovery dot-badge on the Mini Projects destination + its hub tiles */
.kh-dot,.mh-dot{display:inline-block;color:#fff;background:var(--brand,#7C5CFF);border-radius:999px;
  min-width:16px;height:16px;line-height:16px;text-align:center;font-size:12px;font-weight:800;
  padding:0 4px;vertical-align:middle;margin-left:2px}
@media (max-width:720px){.kh-layout{flex-direction:column}.kh-rail{flex:1 0 auto;width:100%;position:static;display:flex;flex-wrap:wrap;gap:4px}.kh-rail-title{width:100%}.kh-belt{flex:1 1 120px}}

/* ===== V9: clickable belt rail (button reset) ===== */
.kh-belt{width:100%;background:transparent;border:none;border-left:4px solid transparent;cursor:pointer;font:inherit;text-align:left}
.kh-belt:hover{background:rgba(124,92,255,.10)}
.kh-belt.future:hover{opacity:.7}

/* ===== V9: lesson tabs ===== */
/* Sid dry-run fix (bug 51c0e820): the tab row used to end in a border-bottom line, THEN a 12px gap
   before the content started — reporters wanted no gap, so the active tab reads as "attached" to its
   panel below, browser-tab style. Dropped the row's own border-bottom (that line was the visible
   seam) and closed .ltab-body's margin-top to 0; the active tab additionally overlaps 2px into
   whatever renders first below it (margin-bottom:-2px) so there's no hairline gap even at the pixel
   level. This is the shared .ltabs/.ltab-body pair reused by every lesson tab bar in the file (main
   Story/Big-Idea/See-it/Feedback row, RPS/FOP sub-tabs, mini-project week nav) — one CSS fix covers
   all of them, no markup changes needed. Colors stay on the existing themed vars (--line/--panel/
   --brand/--accent), so light and night mode both pick this up automatically.
   Bug 4a6652e3 follow-up ("tabs and subtabs are overlapping... there should be little gap"):
   0 margin plus .dtab.on's own -2px margin-bottom (below) put the active Day pill visibly
   overlapping/touching the Story/Big-Idea/etc row under narrower viewports -- not the intended
   "attached like a browser tab" seam-less look, just overlap. Restores a small gap (NOT the
   "folder look" redesign, which is a separate, still-unruled ask) so the two rows sit close but
   never touch; still comfortably less than the default 10px .ltabs uses when NOT preceded by a
   Day row. */
.dtabs + .ltabs{margin-top:0}
.ltabs{display:flex;gap:6px;margin:10px 0 0;flex-wrap:wrap}
.ltab{appearance:none;border:1px solid var(--line);border-bottom:none;background:var(--panel2);color:var(--ink);
  padding:8px 14px;border-radius:10px 10px 0 0;font:inherit;font-size:13px;font-weight:700;cursor:pointer;min-height:40px}
.ltab:hover{background:var(--panel)}
/* R1-061: active tab is ALWAYS a bright, obvious highlight. Previously it used the belt
   colour, which on pale belts (e.g. White) rendered as a washed-out grey. Use the vivid
   brand purple + a glow + an accent underline so the current tab is unmistakable. */
.ltab.on{position:relative;background:var(--brand);color:#fff;border-color:transparent;box-shadow:0 2px 10px rgba(124,92,255,.35);margin-bottom:-2px}
/* WP-2 bug-sweep: --accent was never a declared token (:root/night have no --accent), so this
   underline silently ran on its literal fallback #00D4B8 in BOTH themes -- not itself a night
   regression, but a pre-existing non-text-contrast shortfall against the tab's own --brand fill in
   BOTH themes (measured 2.53:1 day / 2.56:1 night, need >=3:1). var(--brand2) is the app's real
   "accent" token and clears 3:1 in night (3.46:1) while staying the SAME literal value in day
   (--brand2 day == the old #00D4B8 fallback exactly, so light mode is byte-identical). */
.ltab.on::after{content:"";position:absolute;left:10px;right:10px;bottom:-2px;height:3px;border-radius:3px;background:var(--brand2)}
.ltab-body{margin-top:0}

/* ===== V9: read 3-section layout ===== */
.rsec{margin-top:12px}
.rsec-h{margin:.1rem 0 .5rem;font-size:15px;color:var(--brand-shade)}
[data-theme="night"] .rsec-h{color:var(--brand2)}
/* ===== Read tab: inner pill tabs (distinct from the outer subtopic .ltabs) + audio player ===== */
.rtabs{display:flex;gap:6px;flex-wrap:wrap;margin:2px 0 0}
.rtab{appearance:none;border:1px solid var(--line);background:var(--panel2);color:var(--ink);border-radius:999px;
  padding:5px 12px;font-size:13px;font-weight:600;cursor:pointer}
.rtab:hover{background:var(--panel)}
.rtab.on{background:var(--brand);color:#fff;border-color:transparent}
/* R1.3 WP7: our own narration transport. The old rule squeezed a NATIVE <audio controls> into
   220x34, a width at which Chrome collapses its built-in timeline into something that renders but
   cannot be dragged — that was the "scrubber doesn't seek" bug. The scrub target is now a real
   <input type=range> with a comfortable hit area. */
.rsec-audio-wrap{vertical-align:middle}
.aq-play{width:32px;height:32px;flex:0 0 32px;border-radius:50%;border:none;cursor:pointer;
  background:var(--brand);color:#fff;font-size:13px;line-height:1;display:inline-flex;align-items:center;justify-content:center}
.aq-play:hover{filter:brightness(1.08)}
.aq-seek{width:190px;height:22px;cursor:pointer;accent-color:var(--brand);vertical-align:middle}
.aq-time{font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}
@media (max-width:640px){.aq-seek{width:130px}}
.keypoints{margin:.2rem 0 0;padding-left:1.3rem}
.keypoints li{margin:.35rem 0;line-height:1.55;font-size:15px}

/* ===== V9: Other Videos ===== */
.ovgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px}
.ovcard{display:flex;gap:10px;align-items:flex-start;text-decoration:none;color:inherit;background:var(--panel);
  border:1px solid var(--line);border-radius:12px;padding:12px;transition:transform .08s,box-shadow .12s}
.ovcard:hover{transform:translateY(-2px);box-shadow:0 4px 14px rgba(0,0,0,.12)}
.ovthumb{font-size:24px;flex:0 0 auto;width:40px;height:40px;display:flex;align-items:center;justify-content:center;
  background:var(--panel2);border-radius:9px}
.ovtitle{font-weight:700;font-size:14px;line-height:1.25}
.ovsub{font-size:12px;color:var(--muted);margin:2px 0}
.ovwhy{font-size:12.5px;color:var(--ink);opacity:.85;line-height:1.4;margin-top:2px}

/* ===== V9: project wizard ===== */
.projland>.card,.projstep>.card{margin-top:12px}
.projhead-top{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.weekmap{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px;margin-top:8px}
.weekchip{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:8px 10px;display:flex;flex-direction:column;gap:2px;font-size:13px}
.weekchip b{color:var(--brand-shade)}
[data-theme="night"] .weekchip b{color:var(--brand2)}
.projtask{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:10px 12px}

/* ===== V9: belt-view panel + pills ===== */
.bv-list{display:flex;flex-direction:column;gap:8px;margin-top:6px}
.bv-item{display:flex;align-items:center;justify-content:space-between;gap:10px;background:var(--panel2);
  border:1px solid var(--line);border-radius:10px;padding:10px 12px}
.bv-item.tapable{cursor:pointer}
.bv-item.tapable:hover{border-color:var(--belt,var(--brand))}
.bv-title{font-weight:700;font-size:14px}
.bv-sub{font-size:12px;margin-top:2px}
.pill{display:inline-flex;align-items:center;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:700;
  background:var(--panel2);border:1px solid var(--line);color:var(--muted)}
.pill.ok{background:#E7F9EF;color:#1C8A4B;border-color:#BEEFD3}
[data-theme="night"] .pill.ok{background:#0F3324;color:#5CE8A6;border-color:#1C5A3E}

/* ===== project step: instructions + activity engines (Chunk B) ===== */
.instr-list,.tool-list{margin:.4rem 0 0;padding-left:1.2rem}
.instr-list li{margin:5px 0;line-height:1.4}
.tool-list{list-style:none;padding-left:0}
.tool-list li{padding:5px 0;border-bottom:1px solid var(--line)}
.engine{margin-top:2px}
.engine-lbl{display:block;font-weight:700;font-size:13px;margin:10px 0 4px;color:var(--muted)}
.engine .row{display:flex;align-items:center}
.e-form input,.e-form textarea,.engine input,.engine textarea{width:100%}
/* E3 image picker */
.engine-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px;margin-top:12px}
.e3pick{position:relative;border:3px solid transparent;border-radius:12px;overflow:hidden;cursor:pointer;background:var(--panel2)}
/* A11Y AUDIT FIX #1: .e3pick's picker tiles moved from <div onclick> to real <button> elements
   (engines.js E3_ai_image_picker) so they're keyboard-reachable — this strips the UA button
   chrome (padding/border/font/appearance) so a button.e3pick still renders identically to the old
   div; the border/background above still apply since they're plain class selectors. */
button.e3pick{display:block;width:100%;padding:0;margin:0;font:inherit;text-align:inherit;appearance:none;-webkit-appearance:none}
.e3pick img{display:block;width:100%;aspect-ratio:1;object-fit:cover}
.e3pick.chosen{border-color:var(--brand2)}
.e3tag{position:absolute;left:0;right:0;bottom:0;background:rgba(0,0,0,.55);color:#fff;font-size:11px;text-align:center;padding:3px}
/* E2 chat */
.chatlog{background:var(--panel2);border:1px solid var(--line);border-radius:12px;padding:10px;min-height:90px;max-height:300px;overflow-y:auto}
.cmsg{margin:6px 0;padding:8px 11px;border-radius:12px;max-width:88%;line-height:1.35;font-size:14px}
.cmsg.hero{background:var(--panel);border:1px solid var(--line)}
.cmsg.me{background:var(--brand);color:#fff;margin-left:auto}
/* E1 buttons/colors/review */
.btn-row .b-del{min-width:40px}
.theme-swatches .swatch{width:40px;height:40px;border-radius:10px;cursor:pointer;position:relative;padding:0}
.theme-swatches .swatch span{position:absolute;left:6px;bottom:6px;width:14px;height:14px;border-radius:50%}
.hero-card{border-radius:16px;padding:16px;text-align:center;margin-bottom:10px}
.hero-av{width:96px;height:96px;border-radius:50%;object-fit:cover;background:#222;border:3px solid rgba(255,255,255,.4)}
.hero-av.ph{display:inline-block}
/* E4 teachable machine */
.tm-steps{margin:.2rem 0 12px;padding-left:1.2rem}.tm-steps li{margin:6px 0;line-height:1.4}
.tm-cam{margin-top:14px;text-align:center}
.tm-cam video{width:100%;max-width:320px;border-radius:14px;background:#000}
.tm-cam .bar,.e-tm .bar{height:14px;background:var(--panel2);border:1px solid var(--line);border-radius:8px;overflow:hidden;margin-top:8px}
.tm-cam .bar>i{display:block;height:100%;width:0;background:var(--brand);transition:width .15s}
.tm-seen{font-weight:700;margin:8px 0 0}

/* R1.3 C5 (WP12): lesson-video controls — captions toggle + playback speed. Kept OUTSIDE the native
   control bar (which we don't control) so they work on every browser. */
.lvideo-cc.on{background:var(--brand);color:#fff;border-color:transparent}
.lvideo-speed{font:inherit;font-size:13px;padding:3px 6px;border-radius:8px;border:1px solid var(--line);background:var(--panel);color:var(--ink)}
.lvideo video::cue{font-size:15px;line-height:1.35;background:rgba(0,0,0,.72);color:#fff}
/* Bug #14: captions are BURNED into the frame near the bottom; the native control bar appears on
   hover at the very bottom and was covering that caption line. Letterbox the video into a slightly
   taller box (object-fit:contain, anchored to the top) so the picture sits above a thin black bottom
   band — the on-hover control bar rides that band and stays clear of the burned caption line. */
.lvideo-el{aspect-ratio:16/10;object-fit:contain;object-position:top center;background:#000}

/* ===== WY-V3 WP-X (rulings A2 / W-V2-3): shared gate-feedback component =====
   One app-wide vocabulary for "you can't move on yet": the locked Next button plays a
   head-shake, and every incomplete gating section shows a bright red exclamation on its
   LEFT edge until satisfied. Wired via APP.gateFeedback (public/js/app.js) — never
   hand-rolled per screen, so the pattern can't drift (SOP #81 fix-the-generator posture). */
@keyframes gate-headshake{
  0%,100%{transform:translateX(0)}
  15%{transform:translateX(-7px)}
  30%{transform:translateX(6px)}
  45%{transform:translateX(-5px)}
  60%{transform:translateX(4px)}
  75%{transform:translateX(-2px)}
}
.gate-shake{animation:gate-headshake .45s ease both}
.gate-incomplete{position:relative}
.gate-incomplete::before{
  content:"\2757"; /* bright red exclamation */
  position:absolute; left:-6px; top:8px; z-index:5;
  font-size:18px; line-height:1;
  filter:drop-shadow(0 1px 2px rgba(0,0,0,.35));
  animation:gate-bang-pop .3s ease both;
}
@keyframes gate-bang-pop{from{transform:scale(.3);opacity:0}to{transform:scale(1);opacity:1}}
/* OPUS WY-V3 INSPECTION FIX: the new gate feedback was the app's most motion-heavy affordance
   and had no reduced-motion guard. A kid on a device set to "reduce motion" (a real
   accessibility need — motion sensitivity/vestibular) still got the full shake + pop. The
   SIGNAL is preserved (the red ❗ still appears, the button still highlights); only the
   movement is dropped. */
@media (prefers-reduced-motion: reduce){
  .gate-shake{animation:none;outline:3px solid #E5484D;outline-offset:2px}
  .gate-incomplete::before{animation:none}
}

/* =====================================================================
   SIGNUP_V2 (2026-08-07, "CSU & Usertypes") — signup-choice / plan tiles, the parent console's
   kid tiles, PIN inputs, the read-only lesson-view banner, and the mail-notification icon.
   Reuses existing families (.role/.card tile, .form, .msg, .chip, .grid.cols, .pill) rather than
   inventing a parallel design language — see public/js/signup_v2.js for the markup these pair with.
   ===================================================================== */
.sv2-tile{cursor:pointer;text-align:center;transition:transform .1s,border-color .1s}
.sv2-tile:hover{transform:translateY(-3px);border-color:var(--brand)}
.sv2-tile.add{border-style:dashed}
.sv2-price{font-size:26px;font-weight:800;margin-top:6px}
.sv2-price .muted{font-size:14px;font-weight:600}
.sv2-readonly-banner{font-weight:700}
/* kid-friendly 4-digit PIN fields (signup_v2.js: add-a-kid form + the Kid sign-in toggle in
   app.js routes.signin) — masked, numeric, spaced-out digits so it visually reads as a PIN pad
   rather than a password field. */
#kid-pin,#ak-pin,#ak-pin2{letter-spacing:8px;font-weight:800;text-align:center;font-size:20px}

/* ---- mail notification icon (Ram: "shaking mail icon" near the avatar) ---- */
.hdr-mail-btn{position:relative;font-size:19px;line-height:1;border:0;background:none;cursor:pointer;
  padding:2px;flex:none;color:inherit}
.hdr-mail-btn[hidden]{display:none !important}
.hdr-mail-dot{position:absolute;top:-5px;right:-7px;min-width:15px;height:15px;padding:0 3px;
  border-radius:999px;background:var(--error,#FF5D5D);color:#fff;font-size:10px;font-weight:800;
  line-height:15px;text-align:center;box-shadow:0 0 0 2px var(--panel,#fff)}
.hdr-mail-dot[hidden]{display:none !important}
@keyframes mailWiggle{
  0%,100%{transform:rotate(0)}
  15%{transform:rotate(-14deg)} 30%{transform:rotate(11deg)} 45%{transform:rotate(-9deg)}
  60%{transform:rotate(7deg)} 75%{transform:rotate(-4deg)} 90%{transform:rotate(2deg)}
}
.hdr-mail-btn.unread{animation:mailWiggle 1.8s ease-in-out infinite}
/* motion-sensitive kids/parents: keep the signal (the red dot), drop the movement — same posture
   as .gate-shake's reduced-motion rule above. */
@media (prefers-reduced-motion: reduce){
  .hdr-mail-btn.unread{animation:none}
}

/* =====================================================================
   COMMON SENSE WAVE 1 (Fable_Brief_CommonSense_Wave1_2026-08-09) — bookend components.
   Class names carried over verbatim from Mockups\Bookend_Wave\01/02/03_*_new.html (real-app CSS,
   --brand #7458F0) so the mockups and the live app match exactly. ADDITIVE ONLY — nothing existing
   is edited above this line.
   ===================================================================== */

/* ---- item 1: Essential Question opener (lesson.js showStory/showDay3Review) ---- */
.eq-card{background:var(--panel2);border:1px solid var(--line);border-left:4px solid var(--brand2,#00D4B8);
  border-radius:12px;padding:12px 16px;margin-bottom:14px;display:flex;gap:10px;align-items:flex-start}
.eq-card b{color:var(--brand-shade,var(--brand))}
/* tile subtitle (placement.js patch — see build report; CSS shipped here so the patch is a 1-line
   markup add with no CSS follow-up needed) */
.rg-tile-q{font-size:11.5px;min-height:0;font-style:italic;color:var(--muted,#45445C);
  padding:2px 12px 12px;text-align:center;line-height:1.35}
/* Day-3 echo strip — deliberately smaller/quieter than .eq-card so the two never look like the
   same component reused; this one closes the loop ("you can answer this now"), it doesn't open it. */
.eq-echo{background:var(--panel);border:1.5px dashed var(--line);border-radius:12px;padding:10px 14px;
  margin-bottom:12px;display:flex;gap:9px;align-items:flex-start}
.eq-echo-q{font-size:13.5px;font-style:italic;color:var(--muted,#45445C)}
.eq-echo-cta{font-size:12.5px;font-weight:800;color:var(--brand-shade,var(--brand));margin-top:3px}

/* ---- item 2: Closing principle "takeaway" + Dojo Code card (lesson.js quiz-passed screen,
   dojocode.js mini-strip + full collectible page) ---- */
.takeaway{background:var(--panel2);border:1px solid var(--line);border-radius:14px;padding:16px 18px;
  margin-top:16px;text-align:left}
.takeaway .tag{font-weight:800;color:var(--brand-shade,var(--brand));font-size:14px}
.takeaway .line{font-size:16px;font-weight:700;margin-top:6px;line-height:1.4}
.dojocode{margin-top:14px;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:12px 14px}
.dc-head{display:flex;justify-content:space-between;align-items:center;font-size:12px;font-weight:800;
  color:var(--muted,#45445C);text-transform:uppercase;letter-spacing:.04em;gap:8px;flex-wrap:wrap}
.dc-strip{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}
.dc-chip{flex:1;min-width:150px;background:var(--panel2);border:1px solid var(--line);border-radius:10px;
  padding:8px 10px;font-size:12px}
.dc-chip.new{border-color:var(--brand)}
.dc-chip .n{font-weight:800;color:var(--brand-shade,var(--brand))}
.justadded{display:inline-block;margin-left:4px;font-size:10px;font-weight:800;color:#04564B;
  background:var(--brand2,#00D4B8);border-radius:999px;padding:1px 7px;vertical-align:middle}
.dc-dots{display:flex;gap:6px;margin-top:8px;flex-wrap:wrap}
.dc-dot{width:16px;height:16px;border-radius:50%;background:var(--line)}
.dc-dot.lit{background:var(--brand)}
.dc-foot{margin-top:8px;font-size:11.5px;color:var(--muted,#45445C)}
/* full collectible page (dojocode.js render()) */
.dojo-rows{margin-top:10px}
.dojo-row{display:flex;gap:10px;align-items:baseline;padding:10px 0;border-top:1px dashed var(--line);
  flex-wrap:wrap}
.dojo-row:first-child{border-top:none}
.dojo-row .n{font-weight:800;font-size:12px;color:var(--brand-shade,var(--brand));flex:none}
.dojo-row .t{font-weight:700;font-size:13px;flex:none}
.dojo-row .p{font-size:14px;flex:1 1 220px}
.dojo-row.locked{opacity:.55}
.dojo-row.locked .p{font-style:italic}

/* ---- item 3: inverted Day-3 review — tap-to-reveal retrieval cards (lesson.js showDay3Review) ---- */
.qcards{display:flex;flex-direction:column;gap:10px;margin-top:8px}
.qcard{border:1px solid var(--line);border-radius:12px;padding:12px 14px;background:var(--panel2);
  cursor:pointer;transition:background .15s,border-color .15s}
.qcard .q{font-weight:700;font-size:14.5px}
.qcard .tap{font-size:12px;color:var(--brand-shade,var(--brand));font-weight:700;margin-top:4px}
.qcard.revealed{background:#E7F9EF;border-color:var(--good,#2ECC71);cursor:default}
[data-theme="night"] .qcard.revealed{background:rgba(46,204,113,.14)}
.qcard.revealed .a{margin-top:8px;font-size:13.5px;color:#1C8A4B;display:flex;gap:6px;align-items:flex-start;
  animation:qcardReveal .18s ease both}
[data-theme="night"] .qcard.revealed .a{color:#7CE7A6}
.qcard:focus-visible{outline:2px solid var(--brand);outline-offset:2px}
@keyframes qcardReveal{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:translateY(0)}}
@media (prefers-reduced-motion:reduce){
  .qcard.revealed .a{animation:none}
  .qcard{transition:none}
}

/* ---- item 5 note: sidebar 🐞 dead link — diagnosed only (app.js/index.html off-limits this wave);
   fix patch is in the build report, no CSS change needed here. ---- */

/* ===== Night-mode overrides (Monday wave, M6): topbar + placed-kid dashboard.
   Same-class fixes for hardcoded light-mode colors that never re-themed. ===== */
[data-theme="night"] .topbar{
  background:rgba(21,26,46,.92);
  border-bottom:1px solid var(--line);
}
[data-theme="night"] .kh-rail{background:var(--panel2)}
[data-theme="night"] .kh-belt.on{background:var(--panel);box-shadow:0 1px 4px rgba(0,0,0,.35)}
[data-theme="night"] .kh-progress-bar{background:var(--panel2)}
[data-theme="night"] .kh-review-sq{background:var(--panel);border-color:var(--line)}
[data-theme="night"] .kh-tile.done{border-color:#1C5A3E;background:#0F3324}
[data-theme="night"] .kh-tile.partial{border-color:#5A4712;background:#3A2E0A}
[data-theme="night"] .kh-tile.ready{border-color:var(--brand);background:var(--panel2)}
[data-theme="night"] .kh-tile.locked{background:var(--panel2)}
/* darkmode2 audit (Sid bug 7bb36116/be4feafc): .rg-tile.done (the lesson-tile family, distinct
   from .kh-tile.done above) had a border-color literal (#bfe3c6, a light mint) with no night
   pairing -- .kh-tile.done got one in the M6 wave but this sibling class was missed. Same mint
   family as .kh-tile.done's night border so the "done" signal reads the same across both tile
   families in night mode. Routed through var(--good) (not a fixed hex) per the "prefer existing
   themed vars" guidance -- night --good (#39D98A) measures 8.23:1 against night --panel2, well
   past the >=3:1 non-text-UI bar (border, not body text). */
[data-theme="night"] .rg-tile.done{border-color:var(--good)}

/* =====================================================================
   Darkmode2 (Sid be4feafc, 2026-08-09): theme toggle -> an accessible SWITCH.
   Replaces the old text button ("🌙 Night mode" / "☀️ Day mode") with a real
   role="switch" control (iOS-style pill + sliding knob + sun/moon icons at the
   two ends). Same setTheme()/getTheme() underneath (see app.js themeSwitchHTML/
   wireThemeSwitch/syncThemeSwitches, and placement.js's #kb-theme which now
   shares the same markup+wiring). Every color here routes through the existing
   theme vars (--panel2/--line/--brand/--brand-shade) so it needs NO separate
   [data-theme="night"] block -- it re-themes for free like the rest of the app.
   ===================================================================== */
.theme-switch{display:inline-flex;align-items:center;justify-content:center;
  width:44px;height:44px;padding:0;border:0;background:none;cursor:pointer;flex:none;
  -webkit-tap-highlight-color:transparent}
.theme-switch .ts-track{position:relative;width:52px;height:28px;border-radius:999px;
  background:var(--panel2);border:1px solid var(--line);display:flex;align-items:center;
  justify-content:space-between;padding:0 5px;box-sizing:border-box;
  transition:background .18s ease,border-color .18s ease}
.theme-switch:hover .ts-track{border-color:var(--brand)}
.theme-switch .ts-ico{font-size:12px;line-height:1;z-index:1;opacity:.5;transition:opacity .18s ease}
/* knob uses var(--ink) in BOTH states (not --brand/a fixed hex) -- --ink is already the "always
   readable against this theme's surfaces" token (dark in day, near-white in night), so the knob
   stays high-contrast against the track in both the resting (day, light track) and checked (night,
   --brand-shade track) cases without a separate per-state color. State is read from the TRACK color
   + the knob's slid position + which icon is dimmed, same as any iOS-style switch. */
.theme-switch .ts-knob{position:absolute;top:2px;left:2px;width:22px;height:22px;border-radius:50%;
  background:var(--ink);box-shadow:0 2px 5px rgba(0,0,0,.3);
  transition:transform .18s ease}
.theme-switch[aria-checked="true"] .ts-track{background:var(--brand-shade);border-color:var(--brand-shade)}
.theme-switch[aria-checked="true"] .ts-knob{transform:translateX(24px)}
.theme-switch[aria-checked="true"] .ts-sun{opacity:.45}
.theme-switch[aria-checked="false"] .ts-moon{opacity:.45}
.theme-switch:focus-visible{outline:3px solid var(--brand2);outline-offset:2px;border-radius:999px}
@media (prefers-reduced-motion:reduce){
  .theme-switch .ts-knob,.theme-switch .ts-track,.theme-switch .ts-ico{transition:none}
}
/* kidbanner's copy sits inline with text buttons ("👯 Invite a friend" etc) so it needs a touch
   more breathing room than the topbar's tight nav row -- purely spacing, same control. */
.kidbanner .theme-switch{margin-left:-4px}

/* =====================================================================
   WP-2 Bug-Sweep (Fable_Brief_BugSweep_2026-08-12.md) -- DARK-MODE class-sweep continued.
   aa3497f0 STILL-HAPPENING: sv129's dark fix only covered quiz .opt; this section is every OTHER
   unguarded surface a static audit + recomputed WCAG contrast (relative-luminance, alpha-
   compositing-aware) turned up across Day-2/lesson-review/project panels. Full numbers self-
   verified in the build report. Light mode is untouched everywhere below -- additive [data-theme=
   "night"] overrides only, same posture as every prior darkmode audit in this file.
   ===================================================================== */

/* --brand-shade is tuned for LIGHT surfaces only (the :root comment already says so for the
   button-shadow use); at night (#5A35D9) it measures 2.11-2.42:1 as small accent TEXT against
   --panel/--panel2/#241D42 -- a hard AA fail, not just "a little low". Every one of these had the
   IDENTICAL bug: an accent label/tag/link riding --brand-shade with no night pairing. Reroutes to
   var(--brand2) (the app's real dark-safe accent, already the fix for .rsec-h/.weekchip b/
   .rg-bigidea-n above) -- measures 9.05-13.75:1 on every affected background at night. */
[data-theme="night"] .eq-card b,
[data-theme="night"] .eq-echo-cta,
[data-theme="night"] .takeaway .tag,
[data-theme="night"] .dc-chip .n,
[data-theme="night"] .dojo-row .n,
[data-theme="night"] .qcard .tap,
[data-theme="night"] .chatmsg b{color:var(--brand2)}
/* the base a{color:var(--brand-shade)} rule has the same night gap, but a blanket night override
   at equal-or-higher specificity would also repaint classed anchors that deliberately opt OUT of
   the link color (.ovcard{color:inherit} etc, several call sites app-wide) -- scoped to bare,
   unclassed <a> so every already-classed anchor component is untouched either way. */
[data-theme="night"] a:not([class]){color:var(--brand2)}

/* lesson.js "Review what you missed" (quiz results, missed-question review): the wrong-answer
   span hardcoded a random literal (#ff8a8a, 2.27:1 on white -- fails in DAY mode); its sibling
   correct-answer span used var(--good) directly (2.10:1 on white -- --good is a fill/border token,
   not a text-safe one, same class of mistake as the --brand-shade family above). Both now route
   through the new --error-text/--good-text tokens (:root block) -- the SAME literals already
   proven via .msg.err/.msg.ok/.opt.right/.opt.wrong, so this reuses rather than invents. */
.ans-wrong{color:var(--error-text)}
.ans-right{color:var(--good-text)}

/* lesson.js's two amber "notice" cards (Yellow-project grown-up disclosure + the FakeOut/RPS
   project-finished celebration) hardcoded border+background inline (#f0c86b/#e3a83b border,
   #fffaf0 cream fill) with no night pairing at all -- their default --ink/--muted text (h3/.muted
   inside) measured 1.07:1 / 1.83:1 on that cream at night, i.e. near-invisible white-on-cream.
   Night reroutes to the SAME amber-night pairing already shipped for .msg.warn/.kh-tile.partial
   (#3A2E0A bg / #5A4712 border) rather than a new hex -- --ink/--muted clear 11.97:1 / 7.01:1 on
   it. Both cards now share one border color (was a barely-visible #f0c86b-vs-#e3a83b difference,
   an imperceptible spec -- unified to the #E3A83B already used by the FakeOut share-link card
   elsewhere in this file, so inline color styling can be dropped and the night rule can win). */
.notice-amber{border:1px solid #E3A83B;background:#fffaf0}
[data-theme="night"] .notice-amber{border-color:#5A4712;background:#3A2E0A}

/* b58ee4d1: the bottom-left #bug-fab (app.js, WP-1-owned this wave -- inline-positioned
   left:16px/bottom:16px/z-index:9998, not edited here) was never adjusted for the sticky belt-nav
   rail's presence. .shell caps at max-width:1180px and centers, so left:16px only clears the rail
   on very wide viewports -- on anything <=~1300px wide (most laptops) the rail's own column starts
   inside the FAB's ~16-116px footprint and the FAB (z-index 9998) draws on top of the rail's Tools
   items (Case File / Bug tracker / Invite), blocking taps -- exactly the reported overlap. Fix:
   whenever the sticky desktop rail is actually showing, push the FAB clear of the rail's real
   rendered right edge -- shell's own centering offset (max(0, (100vw-1180px)/2)) + the rail's fixed
   220px + a 20px gap -- via calc()/max(), so it tracks correctly at EVERY width rather than a single
   magic-number breakpoint. On the <=720px off-canvas drawer, reposition isn't meaningful (the
   drawer + its scrim already cover the left edge while open) -- instead the FAB drops behind the
   scrim's stacking order so it can never sit on top of / intercept taps meant for the open drawer.
   !important is required in both cases: #bug-fab's left/z-index are set via an inline style
   attribute in app.js, which nothing but !important can out-rank from an external stylesheet.
   NOTE on scoping the mobile rule: beltmenu.js's ☰ handler toggles bn-open AND rail-hidden
   TOGETHER on every click regardless of viewport ("both classes are media-scoped in CSS so only
   the relevant one takes effect" -- its own comment) -- .belt-menu/::after's own bn-open rules
   are scoped inside this SAME max-width:720px block for exactly that reason, so the z-index drop
   must be scoped identically. Unscoped, it would also fire at desktop widths any time a desktop
   user has toggled the rail hidden (rail-hidden and bn-open flip together), quietly burying the
   FAB behind page content there for no reason -- the drawer/scrim it's meant to defer to doesn't
   even render outside this breakpoint. */
@media (min-width:721px){
  body.has-belt-nav:not(.rail-hidden) #bug-fab{left:calc(max(0px, (100vw - 1180px) / 2) + 240px) !important}
}
@media (max-width:720px){
  body.has-belt-nav.bn-open #bug-fab{z-index:1 !important}
}

/* 266a8b43: Guru launcher (#aiqcw-launcher, chat_widget.js) already carried aria-label (a11y only,
   not visually rendered) but no `title`, so hovering it showed no visible tooltip at all before
   this fix (chat_widget.js now sets title="Guru — chat helper"). No CSS change is needed for the
   native browser tooltip itself; chat_widget.css is otherwise fully var()-driven (color-mix()
   against --panel/--brand/--error throughout) and re-verified theme-clean in this sweep -- no
   hardcoded literal found there needing a night pairing. */

/* =====================================================================
   WP-B (Byte's AI Arcade hub redesign) — hand-cut game-hub tiles with
   per-position rotation and asymmetric border-radius, matching arcade.html's
   .gcard playful sticker aesthetic. Grid items use .gh-card in addition to
   .kh-tile; the new class overrides the parent's flat rectangular style.
   ===================================================================== */
.gh-card {
  position: relative; display: flex; flex-direction: column; gap: 10px;
  background: var(--surface); border: 2px solid var(--line);
  padding: 20px 18px 18px; box-shadow: var(--shadow-sm);
  text-align: left; transition: transform .18s cubic-bezier(.2,.7,.3,1), box-shadow .18s;
}
/* every sticker peels a little differently — on purpose (per :nth-child) */
.gh-card:nth-child(5n+1) { transform: rotate(-1.4deg); border-radius: 22px 17px 24px 15px; }
.gh-card:nth-child(5n+2) { transform: rotate(1.1deg);  border-radius: 16px 24px 14px 22px; }
.gh-card:nth-child(5n+3) { transform: rotate(-.5deg);  border-radius: 24px 15px 20px 18px; }
.gh-card:nth-child(5n+4) { transform: rotate(1.8deg);  border-radius: 15px 21px 17px 25px; }
.gh-card:nth-child(5n)   { transform: rotate(-2deg);   border-radius: 20px 14px 23px 16px; }
.gh-card:hover  { transform: rotate(0deg) translateY(-4px) scale(1.02); box-shadow: var(--shadow-md); z-index: 2; }
.gh-card:active { transform: scale(.97); }

@media (prefers-reduced-motion: reduce) {
  .gh-card {
    transition: none;
  }
  .gh-card:nth-child(5n+1),
  .gh-card:nth-child(5n+2),
  .gh-card:nth-child(5n+3),
  .gh-card:nth-child(5n+4),
  .gh-card:nth-child(5n) {
    transform: none !important;
  }
  .gh-card:hover {
    transform: none !important;
  }
  .gh-card:active {
    transform: none !important;
  }
}
}

/* Pacing v1: Today panel styles (placed home + kid's own current belt only) */
.pace-hero-panel {
  margin-top: 16px;
  padding: 16px;
  background: var(--panel2);
  border-radius: var(--radius);
  text-align: center;
}
.pace-hero-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}
.pace-hero-title {
  font-size: 18px;
  font-weight: 600;
  color: var(--ink);
}
.pace-hero-sub {
  font-size: 14px;
  color: var(--muted);
}

.pace-done-panel {
  margin-top: 16px;
  padding: 16px;
  background: var(--panel2);
  border-radius: var(--radius);
}
.pace-done-banner {
  text-align: center;
}
.pace-done-header {
  font-size: 16px;
  font-weight: 600;
  color: var(--ink);
  margin-bottom: 8px;
}
.pace-done-line {
  font-size: 14px;
  color: var(--muted);
  margin-bottom: 8px;
}
.pace-done-early {
  margin-top: 12px;
}
.pace-early-link {
  font-size: 14px;
  color: var(--brand);
  text-decoration: none;
  cursor: pointer;
}
.pace-early-link:hover {
  text-decoration: underline;
}

/* Week strip: 7 day pips (Mon-Sun), marking which days learned this week */
.pace-week-strip {
  display: flex;
  gap: 6px;
  margin: 14px 0 8px;
}
.pace-pip {
  flex: 1;
  height: 34px;
  border-radius: 9px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  background: var(--panel2);
  color: var(--ink-soft);
  border: 1px solid var(--line);
  font-weight: 600;
}
.pace-pip-done {
  background: linear-gradient(135deg, var(--good), var(--brand2));
  color: white;
  border-color: transparent;
  font-weight: 800;
}
.pace-pip-today {
  border: 2px dashed var(--brand);
  color: var(--brand);
  font-weight: 700;
}
.pace-week-note {
  font-size: 12.5px;
  color: var(--muted);
  margin-top: 4px;
}
