/* GENERATED by tools/bundle_css.py -- do not edit.
   Edit the files in assets/css/ and re-run. Order is the cascade and is fixed by
   the marker in the page that links this. */

/* ==== foundations.css ==== */
/* ════════════════════════════════════════════════════════════
   HOW A FOUNDATION LOOKS — the one place that decides

   assets/js/foundations.js owns the twelve names. This owns their colour on the surfaces that REPORT
   what happened: the "how it helps" panel, the between-game receipt and the session roll-up. Both
   files exist for the same reason — anything decided per surface drifts.

   NOT ON THE PRICING CARD, by decision. There the twelve are the offer rather than a record of a
   session, they sit in their own two boxed groups with a heading each, and they are not competing with
   brown prose for attention — so they keep the ink they had. Which is why this file is loaded on
   game.html and not on pricing.html.

   ── WHY GREEN ──────────────────────────────────────────────────────────────────
   On a receipt the labels were `--brown`, which is also the colour of the lead line above them, the
   group headings, the "Starting the next game" line and most of the card's other text — so the one
   thing a parent is meant to take away was the same weight as everything around it. Green separates
   them from the brown they sit in without shouting.

   NOT `--sage` (#7BBF8E), which is the obvious candidate and wrong twice over. It is the colour of a
   correct answer in four games, so it would read as "your child got these right" rather than "your
   child practised these" — and against the card's #FEFAF4 it comes to about 1.9:1, which is not
   legible as text at any size.

   #2f7d4a is about 4.85:1 on the card, which passes AA for body text, and it is already in the
   product: it was the colour of the tick this list used to carry before the family icons replaced it.

   ── WHAT STAYS BROWN ───────────────────────────────────────────────────────────
   Every heading: "Learning foundations", "Thinking foundations", "Your little one practised", "What
   you worked on together today". They are labels on a shelf, and if they went green too there would
   be nothing for the green to distinguish.
   ════════════════════════════════════════════════════════════ */
:root {
  /* The foundations themselves, wherever they are named. */
  --nz-found: #2f7d4a;
  /* Their headings — an alias rather than a second value, so the pair is visibly one decision. */
  --nz-found-head: var(--brown, #9B6B4A);
}

/* ── THE LINK AND THE PANEL ─────────────────────────────────────────────────────
   Moved here from player.css, and renamed off its td- prefix. Two surfaces list foundations and
   both use these: the player's "how it helps" for the game being played, and My Home's "what
   today builds" for the union across the day's three. A shared thing named after one of its
   callers is the next file somebody duplicates instead of reusing.

   ── THE LINK ───────────────────────────────────────────────────────────────────
   Deliberately small: it is available, not advertised. */
.nz-helps {
  background: none; border: 0; padding: 0; margin: 0;
  font-family: 'Nunito', sans-serif; font-weight: 700;
  font-size: clamp(.56rem, 2.3vw, .64rem);
  color: inherit; opacity: .82;
  text-decoration: underline; text-underline-offset: 2px;
  cursor: pointer; white-space: nowrap;
}
.nz-helps:hover { opacity: 1; }
.nz-helps:focus { outline: none; }
.nz-helps:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; border-radius: 3px; }

/* ── THE PANEL ──────────────────────────────────────────────────────────────────
   Under the queue, not inside a capsule: it is about the session's current game and it needs the
   player's full width to stay two short lines rather than six wrapped ones. */
.nz-helps-panel {
  margin: .16rem .2rem .05rem; padding: .34rem .6rem .38rem;
  background: rgba(255, 255, 255, .62); border-radius: 12px;
}
.nz-helps-head {
  margin: 0 0 .15rem;
  font-family: 'Nunito', sans-serif; font-weight: 900;
  font-size: clamp(.58rem, 2.4vw, .66rem);
  letter-spacing: .04em; text-transform: uppercase;
  color: var(--brown, #9B6B4A); opacity: .85;
}
.nz-helps-line {
  margin: 0; font-family: 'Nunito', sans-serif; font-weight: 700;
  font-size: clamp(.64rem, 2.6vw, .72rem); line-height: 1.45;
  color: var(--ink, #3D2C1E);
}
/* ── THE PANEL'S TWO GROUPS ─────────────────────────────────────────────────────
   The same two foundations, the same glyphs and the same headings as the receipt and the pricing
   card. Side by side, because the pair being level is the clearest statement that neither is the
   junior one — and because this panel can carry five entries a side for Tot and ABC, which stacked
   would push the game a long way down for a parent who only wanted a glance.

   .nz-helps-lbl has gone with "Main learning / Also building". */
.nz-helps-lede {
  margin: 0 0 .3rem;
  font-family: 'Nunito', sans-serif; font-weight: 700;
  font-size: clamp(.62rem, 2.5vw, .7rem); line-height: 1.45;
  color: var(--ink, #3D2C1E); opacity: .92;
}
.nz-helps-founds {
  display: flex; flex-wrap: wrap; gap: .1rem 1.6rem;
}
.nz-helps-found { flex: 1 1 9rem; min-width: 0; }
.nz-helps-fh {
  margin: .18rem 0 .1rem;
  font-family: 'Nunito', sans-serif; font-weight: 900;
  font-size: clamp(.54rem, 2.2vw, .6rem);
  letter-spacing: .05em; text-transform: uppercase;
  color: var(--nz-found-head, #9B6B4A); opacity: .72;    /* the heading stays brown */
}
.nz-helps-row {
  display: flex; align-items: baseline; gap: .32rem;
  margin: 0; padding: .04rem 0;
  font-family: 'Nunito', sans-serif; font-weight: 700;
  font-size: clamp(.6rem, 2.5vw, .68rem); line-height: 1.35;
  /* The foundation colour, the same one the receipt uses — see assets/css/foundations.css. This panel
     and the receipt name the same things, so a parent should not have to notice they are the same. */
  color: var(--nz-found, #2f7d4a);
}
.nz-helps-ico { flex: 0 0 auto; width: 1.05em; text-align: center; font-size: .95em; }

/* ==== mobile-nav.css ==== */
/* Reflow at ANY width: pills wrap to a new row instead of overflowing (no horizontal scroll) */
html, body { overflow-x: hidden; }
nav { flex-wrap: wrap; }
nav > div, .nav-links { flex-wrap: wrap; }

/* Shared mobile fixes for legacy pages: nav overflow + horizontal scroll guard */
@media (max-width: 680px) {
  html, body { overflow-x: hidden; }
  nav { height: auto !important; padding: .6rem .8rem !important; flex-wrap: wrap; gap: .4rem; }
  nav .logo img { height: 58px !important; }
  nav > div { flex-wrap: wrap !important; justify-content: center !important; gap: .4rem !important; }
  nav a[href] { font-size: .8rem !important; padding: .3rem .7rem !important; }
  /* .game-tabs / .game-tab used to be squeezed here. /game has a one-line switcher now,
     which needs no shrinking at any width — see assets/css/game-bar.css. */
}

/* ==== site-nav.css ==== */
/* A SENTINEL, SO THE BUNDLE COUNTS AS "ALREADY LOADED".
   site-nav.js injects this file when it cannot find a <link> to it — a page that forgets the
   link still gets a correct nav. That check looked for the link by href, so the moment /game
   folded this file into bundle-game.css the link was gone, the check failed, and the browser
   fetched the whole stylesheet a second time: an extra request and the 48px nav jump the link
   exists to prevent. Asking the DOCUMENT whether the rules are in effect is the question that
   was always meant, and it is true either way. */
:root { --nz-site-nav: 1; }

/* Numzi shared nav - appearance.

   This lives in a real stylesheet, linked from every page's <head>, rather than being
   injected by site-nav.js at runtime. Injecting it meant the browser painted the nav at
   whatever size the page's own leftover CSS said (126px on game.html), then snapped it to
   78px the moment the script ran, shoving the whole page up 48px. Loading it in the head
   means the first paint is already correct.

   It owns the bar completely, with !important, because pages still carry their own older
   nav rules at wildly different sizes. site-nav.js only fills in the links. */

nav {
  display: flex !important;
  align-items: center !important;
  justify-content: space-between !important;
  height: auto !important;
  min-height: 0 !important;
  max-height: none !important;
  overflow: visible !important;
  flex-wrap: wrap !important;
  gap: .4rem 1rem !important;
  padding: .4rem 1.5rem !important;
  background: var(--cream) !important;
  border-bottom: 3px solid var(--warm) !important;
  position: sticky !important;
  top: 0 !important;
  z-index: 100 !important;
}
nav .logo { line-height: 0 !important; }
nav .logo img { height: 62px !important; width: auto !important; display: block !important; }

/* Reserve the row height before the links exist, so filling them in shifts nothing. */
.nav-links {
  display: flex !important;
  gap: .45rem !important;
  align-items: center !important;
  flex-wrap: wrap !important;
  justify-content: flex-end !important;
  min-height: 62px !important;
}
.nav-links a {
  font-family: "Fredoka One", cursive !important;
  font-size: .9rem !important;
  text-decoration: none !important;
  padding: .34rem .8rem !important;
  border-radius: 10px !important;
  white-space: nowrap !important;
  line-height: 1.4 !important;
}
.nav-links a.plain { color: var(--brown) !important; background: none !important; box-shadow: none !important; }
.nav-links a.active { outline: 2.5px solid var(--ink); outline-offset: 2px; }

.nav-more-wrap { position: relative; display: none; }
.nav-more-btn {
  font-family: "Fredoka One", cursive; font-size: .9rem; color: var(--ink);
  background: var(--kraft); border: 2.5px solid var(--ink); border-radius: 10px;
  padding: .28rem .8rem; cursor: pointer; box-shadow: 0 3px 0 var(--warm);
}
.nav-more-btn:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--warm); }
.lbl-burger { display: none; }

.nav-drop {
  position: absolute; right: 0; top: calc(100% + .45rem);
  background: var(--cream); border: 2.5px solid var(--ink); border-radius: 14px;
  box-shadow: 4px 4px 0 var(--warm); padding: .5rem;
  display: none; flex-direction: column; gap: .4rem; min-width: 200px; z-index: 300;
}
.nav-drop.open { display: flex; }
.nav-drop a { display: block; text-align: left; }

/* .is-on is set by site-nav.js once it has counted what is actually hidden. Both breakpoints
   require it, so the button can never appear over an empty menu — which is exactly what a
   logged-out visitor between 640px and 1060px would otherwise get now that Videos, Story Books
   and Activities have left the bar and nothing is left marked `nav-sec`. */
@media (max-width: 1060px) {
  .nav-links a.nav-sec { display: none !important; }
  .nav-more-wrap.is-on { display: block; }
}
@media (max-width: 640px) {
  nav { padding: .25rem 1rem !important; }
  nav .logo img { height: 42px !important; }
  .nav-links { min-height: 42px !important; }
  .nav-links > a { display: none !important; }
  .nav-more-wrap.is-on { display: block; }
  .lbl-more { display: none; }
  .lbl-burger { display: inline; }
}

/* ==== mic-ui.css ==== */
/* ════════════════════════════════════════════════════════════════════════════
   mic-ui.css — the microphone's face, wherever it appears

   MOVED OUT OF game.html, NOT REWRITTEN. Every rule below is byte-for-byte what that file's
   inline <style> already carried; the only change is that it now lives somewhere a second page
   can load it.

   WHY IT HAD TO MOVE. nzMicPanel() in _mic.js builds .drw-mic-wrap for every game that listens,
   and that markup is shared — but its styling was not, so ABC Time v2 on its lab page rendered
   the microphone as a bare emoji: 76x26px, no circle, no border, permanently visible, because
   `.drw-mic-wrap { display:none }` was not there to hide it either. The JavaScript was working
   perfectly and the mic was invisible as a control. lock-ui.css had already noticed the split
   and worked around it in a comment — "comes from game.html's own .drw-mic-wrap block".

   The `drw-` prefix is Tot Reads' and is kept deliberately: these are the class names _mic.js
   writes, and renaming them here would mean renaming them there, in a shared engine, for
   cosmetic tidiness. The prefix is where this came from, not who is allowed to use it.
   ════════════════════════════════════════════════════════════════════════════ */

/* One-time, parent-facing card asking for the microphone before it is needed. */
.drw-mic-ask { max-width:520px; margin:0 auto 1rem; background:var(--kraft,#f6ecd9); border:2.5px solid var(--ink,#3D2C1E);
               border-radius:16px; padding:1rem 1.15rem; text-align:left; box-shadow:0 4px 0 var(--warm); }
.drw-mic-ask-title { font-family:'Fredoka One',cursive; font-size:1.02rem; color:var(--ink,#3D2C1E); margin-bottom:.35rem; }
.drw-mic-ask p { font-size:.88rem; font-weight:700; line-height:1.55; color:var(--brown); margin:0 0 .8rem; }
.drw-mic-ask a { color:#B85E28; font-weight:800; }
.drw-mic-ask-btns { display:flex; gap:.6rem; flex-wrap:wrap; }
.drw-mic-ask-yes { font-family:'Fredoka One',cursive; font-size:.92rem; background:var(--orange,#E8844A); color:#fff;
                   border:2.5px solid var(--ink,#3D2C1E); border-radius:11px; padding:.5rem 1.1rem; cursor:pointer; box-shadow:0 3px 0 #b85e28; }
.drw-mic-ask-yes:disabled { opacity:.55; cursor:default; }
.drw-mic-ask-no { font-family:'Fredoka One',cursive; font-size:.92rem; background:none; color:var(--brown);
                  border:2px solid var(--warm); border-radius:11px; padding:.5rem 1.1rem; cursor:pointer; }

/* Mic panel for reading aloud — only takes up space once the mic is actually open. */
.drw-mic-wrap { display:none; flex-direction:column; align-items:center; gap:.35rem; margin-top:.6rem; }
.drw-mic-wrap.active { display:flex; }
.drw-mic { width:64px; height:64px; border-radius:50%; display:grid; place-items:center;
           font-size:1.7rem; background:#fff3f1; border:3px solid #E8524A; box-shadow:0 4px 0 #a82820; }
.drw-mic { transition:transform .15s, background .2s, border-color .2s; }
/* Quiet but present: the mic stays on screen after the listening window closes and can
   be tapped to listen again. */
.drw-mic.is-idle { cursor:pointer; animation:none; background:#fff; border-color:var(--warm); opacity:.9; }
.drw-mic.is-idle:hover { transform:scale(1.07); border-color:#E8524A; opacity:1; }
.drw-mic.is-listening { animation:drwMicBreathe 1.6s ease-in-out infinite; }
.drw-mic.is-hearing   { animation:drwMicHearing .38s ease-in-out infinite alternate; }
.drw-mic.is-heard     { animation:none; background:#eefaf0; border-color:#5BBF5E; box-shadow:0 4px 0 #2e8030; }
/* A line for the parent, and only when we can genuinely hear something too quiet to
   use. Empty the rest of the time, so it is never clutter. */
.drw-soft { font-size:.78rem; font-weight:800; color:#B8701A; min-height:1.1em; }
.drw-mic-wrap.is-softly .drw-mic { border-color:#E8A33A; }
@keyframes drwMicBreathe { 50% { transform:scale(1.07); box-shadow:0 4px 0 #a82820, 0 0 0 12px rgba(232,82,74,.16); } }
@keyframes drwMicHearing { to  { transform:scale(1.2);  box-shadow:0 4px 0 #a82820, 0 0 0 18px rgba(232,82,74,.22); } }
.drw-listen-status { font-family:'Fredoka One',cursive; font-size:.95rem; color:var(--brown); text-align:center; }
/* THE MICROPHONE IS OFF, AND THE PAGE SAYS WHERE THE SWITCH IS. _mic.js adds .is-off when a
   microphone was refused or could not start. The line carries real instructions — which tap, in
   which browser — so it reads as a notice rather than the caption "Listening…" is, and the muted
   🔇 above it is the button that asks again. */
.drw-mic-wrap.is-off .drw-mic { cursor:pointer; animation:none; background:#fff; opacity:1;
  filter:grayscale(1); border-color:var(--warm,#e3cfa8); box-shadow:0 4px 0 var(--warm,#e3cfa8); }
.drw-mic-wrap.is-off .drw-mic:hover { filter:none; border-color:#E8524A; }
.drw-mic-code { display:block; margin-top:.35rem; font-size:.72rem; font-weight:600; opacity:.55; font-family:ui-monospace,Menlo,monospace; }
.drw-mic-wrap.is-off .drw-listen-status {
  max-width:30rem; font-family:'Nunito',system-ui,sans-serif; font-weight:700; font-size:.9rem;
  line-height:1.5; color:var(--ink,#3D2C1E); background:var(--kraft,#f6ecd9);
  border:2px solid var(--warm,#e3cfa8); border-radius:12px; padding:.6rem .85rem;
}
.drw-heard { font-size:.8rem; font-weight:700; color:var(--brown); opacity:.75; min-height:1.1em; text-align:center; }

/* ==== trace-ui.css ==== */
/* ════════════════════════════════════════════════════════════════════════════
   trace-ui.css — everything around a tracing canvas

   MOVED, NOT REWRITTEN. Every rule below is byte-for-byte what game.html's inline <style> and
   abc-game.css already carried. The only change is that they now live somewhere a third game
   can load them.

   WHY IT HAD TO MOVE. nzTraceLetter() and nzInkPicker() in _trace.js are shared — any game can
   call them — but the look around them was not: the box and its buttons were inline in game.html,
   the crayon meter was in ABC v1's own stylesheet, and the ink chips were in a third place. So
   ABC Time v2, on its own page, could reach the engine and not the face of it. It reinvented the
   box, got no ink picker at all, and absolutely-positioned its buttons over the canvas.

   The prefixes are left alone on purpose. `.drw-` is Tot Reads', `.abc-ink` is ABC Time v1's,
   `.nz-ink-` is the picker's — they are the class names _trace.js and the two shipped games
   already write, and renaming them here would mean renaming them there for no gain. Same
   reasoning as mic-ui.css. Where the prefix came from is not who is allowed to use it.
   ════════════════════════════════════════════════════════════════════════════ */

/* 100% OF ITS CONTAINER, NOT 92vw. The viewport measure was right when Tot's content sat
   directly on the page; it has a card now, and 92vw is wider than the card's inside — so the
   dashed box and the crayon under it both hung past the right border. This is the rule
   docs/engineering/05_adding-a-game.md already states: give anything you add width:100%.

   AND THE HEIGHT COMES FROM THE WIDTH. `min(80vw,25rem)` was 288px on a 360px phone against a
   280px-wide box — taller than it was wide, for one letter — which is most of why the buttons
   fell off the fold. An aspect ratio keeps the letter comfortable at any width. */
.drw-trace-wrap { position:relative; width:100%; max-width:30rem;
                  aspect-ratio:4/3; max-height:23rem;
                  margin:.3rem auto .1rem; background:#FEFAF4;
                  border:4px dashed var(--warm); border-radius:24px; }
.drw-trace-word { font-family:'Fredoka One',cursive; font-size:clamp(5rem,16vw,8rem); color:transparent; -webkit-text-stroke:4px #d4c0e8; text-stroke:4px #d4c0e8; letter-spacing:.08em; text-align:center; user-select:none; pointer-events:none; padding:1rem .5rem; line-height:1.15; display:block; width:100%; }
.drw-trace-canvas { position:absolute; top:0; left:0; width:100%; height:100%; touch-action:none; cursor:crosshair; border-radius:16px; }
.drw-trace-hint { font-size:.88rem; color:var(--brown); text-align:center; margin-top:.3rem; }
/* NOT display:none BY DEFAULT ANY MORE. It was, because the old code revealed the button
   with style.display='block'. These buttons are now shown and hidden with the `hidden`
   attribute, and a class rule beats the browser's own [hidden] rule — so leaving it here
   meant neither "I traced it!" nor "Start over" could ever appear, however correct the
   scoring underneath was. */
/* ONE ROW, ALWAYS. "↺ Start over" and "✅ I traced it!" at 1.1rem with 2rem of side padding
   came to about 360px on a 280px-wide card, so flex-wrap put the second one underneath — and
   "I traced it" only appears once the tracing is done, which is the exact moment a child should
   not have to scroll to answer. Both scale with the viewport and the row never wraps. */
.drw-trace-cta { display:flex; gap:.5rem; justify-content:center; align-items:stretch;
                 flex-wrap:nowrap; width:100%; margin-top:.1rem; }
.drw-trace-btn { font-family:'Fredoka One',cursive; font-size:clamp(.86rem,3.6vw,1.1rem);
                 background:#B59FD6; color:#fff; border:none; border-radius:14px;
                 padding:.6rem clamp(.7rem,3.2vw,2rem); cursor:pointer;
                 box-shadow:0 4px 0 #7a5fa8; transition:all .15s;
                 flex:0 1 auto; min-width:0; white-space:nowrap; }
.drw-trace-btn[hidden] { display:none; }
.drw-trace-btn:hover { transform:translateY(-2px); box-shadow:0 6px 0 #7a5fa8; }

/* ── the crayon meter ────────────────────────────────────────────────────────
   TAKEN FROM game.html, NOT FROM abc-game.css. Both carried a copy and they had drifted:
   the stylesheet still said `width:min(92vw,30rem)` — the viewport, not the card — which is
   the same fault the trace box itself was fixed for. game.html's is the corrected one, and
   shipping the other would have overridden it, because a linked sheet beats an inline block
   that appears before it. */
/* The crayon. No number and no percentage on it — a child who cannot read still has to
   be able to see that it is running out, so it is length and colour only. */
.abc-ink { width:100%; max-width:30rem; margin:.35rem auto .1rem; display:flex; align-items:center;
           gap:.4rem; opacity:0; transition:opacity .3s ease; }
.abc-ink.is-in { opacity:1; }
.abc-ink-cap { font-size:1.05rem; line-height:1; }
.abc-ink-track { flex:1; height:.6rem; border-radius:999px; background:#EFE4D4;
                 border:2px solid var(--warm); overflow:hidden; }
.abc-ink-fill { display:block; height:100%; width:100%; border-radius:999px;
                background:var(--sage); transition:width .12s linear, background .2s; }
.abc-ink.is-empty .abc-ink-fill { background:var(--warm); }
.abc-ink[hidden] { display:none; }
/* WRITE'S TRACE ROWS SPACED AS MEDLEY'S: box, crayon, inks, hint, each 1rem apart - Medley gets
   that from its game area's flex gap; ABC's card has none, so the rows carry it. */
#abcTrace + .abc-ink { margin-top:1rem; }
#abcInkPick { margin-top:1rem; }
#abcInkPick + .abc-trace-hint { margin-top:1rem; }

/* ── THE INK PICKER ─────────────────────────────────────────────────────
   Shared by both games, so the CSS lives with the shared markup rather than being copied
   into two prefixes. Sits under the crayon meter and above the box: outside the canvas,
   because anything tappable inside the drawing area gets drawn on rather than tapped. */
.nz-ink-row { width:100%; max-width:30rem; margin:.2rem auto .3rem; display:flex; gap:.35rem;
              justify-content:center; flex-wrap:nowrap; }
.nz-ink-chip { flex:1 1 0; min-width:0; display:flex; flex-direction:column; align-items:center;
               gap:.1rem; padding:.32rem .2rem; border:2px solid var(--warm); border-radius:12px;
               background:#FFFDF8; color:var(--brown); cursor:pointer; line-height:1.05;
               font-family:'Fredoka One',cursive; transition:transform .12s ease, background .15s ease; }
.nz-ink-chip:active { transform:translateY(1px); }
.nz-ink-chip.is-on { background:var(--sage); border-color:var(--ink); color:#fff; }
.nz-ink-emoji { font-size:1.15rem; }
.nz-ink-name { font-size:.66rem; letter-spacing:.01em; }
@media (max-width:400px) { .nz-ink-name { font-size:.6rem; } .nz-ink-emoji { font-size:1rem; } }

/* ── ABC TIME v1's LEVEL 3, WHICH IS THE ONE THAT WORKS ─────────────────────
   Two tracing screens shipped before this file existed and they were not the same screen.
   Tot Reads puts the crayon and the ink picker UNDER the box; ABC Time v1 puts them above
   it, and its comment says why: below, the meter lands at y=897 on a 390x844 phone — off the
   bottom, while the box it describes is still on screen. A meter nobody can see during the
   one activity it measures may as well not exist, and that is how it was first reported.

   So v1's is the arrangement, and these are its rules, moved here so a third screen cannot
   invent a fourth. */
.abc-trace { width:100%; max-width:30rem; aspect-ratio:4/3; max-height:22rem; margin:.4rem auto .2rem;
             background:#FEFAF4; border:4px dashed var(--warm); border-radius:24px;
             opacity:0; transition:opacity .3s ease; position:relative; }
.abc-trace.is-in { opacity:1; }
/* touch-action:none or the first stroke scrolls the page instead of drawing. */
.abc-trace-canvas { position:absolute; inset:0; width:100%; height:100%;
                    touch-action:none; cursor:crosshair; border-radius:20px; }

.abc-trace-hint { font-weight:800; font-size:.9rem; color:var(--brown);
                  text-align:center; margin:.2rem 0 .1rem; }
/* The spoken ask, on screen too. Fades in with the trace box rather than the letter,
   so it cannot be read before "This is A." has been said. */
.abc-trace-ask { opacity:0; transition:opacity .35s ease; font-family:'Fredoka One',cursive;
                 font-size:clamp(1.05rem,4.2vw,1.35rem); color:var(--ink);
                 text-align:center; margin:.1rem auto .35rem; }
.abc-trace-ask.is-in { opacity:1; }

/* .abc-btn.abc-traced and .abc-btn.abc-retry are NOT here. They were, briefly, and that was a
   half-move: they only mean anything alongside .abc-btn, which is ABC v1's own button base and
   stays in game.html — so on a page without it they resolved to a browser-default button.
   A shared sheet should hold what is actually shared, and nothing else. */

/* ── THE SHARED PANEL — nzTracePanel() in _trace.js ──────────────────────────
   The note above was written when every game brought its own buttons, and it is still the right
   rule; what changed is what is actually shared. nzTracePanel builds the whole screen now — box,
   meter, inks, hint, two buttons — so these ARE the shared classes, with no game's base class
   underneath them and nothing to resolve to a browser default. The box, the meter, the hint and
   the ink chips keep the class names they already had in this file; only the wrapper, the ink
   mount and the button row are new. */
.nz-trace-panel { width:100%; display:flex; flex-direction:column; align-items:center; }
.nz-trace-inks  { width:100%; }
/* ONE ROW, ALWAYS, and it never wraps: "I traced it!" appears the moment the tracing is done,
   which is the exact moment a child should not have to scroll to answer. */
.nz-trace-cta { display:flex; gap:.5rem; justify-content:center; align-items:stretch;
                flex-wrap:nowrap; width:100%; margin-top:.5rem; }
.nz-trace-btn { font-family:'Fredoka One',cursive; font-size:clamp(.86rem,3.6vw,1.05rem);
                border:3px solid var(--ink); border-radius:14px; cursor:pointer;
                padding:.55rem clamp(.7rem,3.2vw,1.8rem); transition:all .15s;
                flex:0 1 auto; min-width:0; white-space:nowrap; }
/* `display:flex` on a button base would beat the browser's own [hidden] rule, so the row and the
   done button both say this themselves — the trap already recorded above about .drw-trace-btn. */
.nz-trace-btn[hidden] { display:none; }
.nz-trace-btn.is-quiet { background:var(--card); color:var(--ink); box-shadow:0 4px 0 var(--warm); }
.nz-trace-btn.is-go    { background:var(--sage); color:#fff; box-shadow:0 4px 0 #4f7a55; }
.nz-trace-btn:active   { transform:translateY(2px); box-shadow:0 1px 0 var(--warm); }

/* ==== say-ui.css ==== */
/* ════════════════════════════════════════════════════════════════════════════
   say-ui.css — the "can you say it?" screen

   MOVED OUT OF game.html, NOT REWRITTEN. Byte for byte what its inline <style> carried; the only
   change is that a second game can now load it.

   WHY. Picture Parade's naming step is the shape every other game copies when it asks a child to
   say a word: one picture, big, on the card's own background, then the microphone, then the way
   out. ABC Time v2 was told to look like it and could not — the ENGINE was shared (nzAskAndListen,
   nzMicPanel) and the LOOK was not, so it rebuilt the screen and got it wrong in three ways: a
   heavy bordered box around the picture, the word printed underneath it, and the question written
   out above the mic. None of those are on the screen it was copying.

   That is the fourth time the same split has bitten — mic-ui.css and trace-ui.css are the other
   two halves of it. The rule this file exists to enforce: if two games are meant to look the same,
   the CSS has to be somewhere both can reach.

   `.par-` is Picture Parade's prefix and stays. Those are the class names its markup already
   writes; renaming them here would mean renaming them there, for tidiness and nothing else.
   ════════════════════════════════════════════════════════════════════════════ */

.par-pic-wrap { height:250px; display:flex; align-items:center; justify-content:center; }
.par-pic { width:238px; height:238px; object-fit:contain; cursor:pointer;
           opacity:0; transform:scale(.9);
           transition:opacity .35s ease, transform .35s cubic-bezier(.34,1.4,.64,1); }
.par-pic.is-in { opacity:1; transform:scale(1); }
.par-pic[hidden] { display:none; }     /* the <img> and the moving-picture <video> share the box */
/* The written word is environmental print, not a reading task. In Talk mode it stays
   hidden until the word has been said, so it is not sitting there as the answer. */
.par-word { font-family:'Fredoka One',cursive; font-size:2rem; color:var(--brown);
            letter-spacing:.04em; min-height:2.5rem; opacity:0; transition:opacity .3s; }
.par-word.is-shown { opacity:1; }
.par-mic { min-height:44px; display:flex; flex-direction:column; align-items:center; gap:.5rem; }

.par-btn { font-family:'Fredoka One',cursive; font-size:.95rem; border:none;
           border-radius:999px; padding:.55rem 1.3rem; cursor:pointer;
           background:var(--warm); color:var(--brown); }
.par-btn:hover { filter:brightness(.96); }
.par-next { background:var(--orange); color:#fff; }
.par-said { background:var(--sage); color:#fff; }

/* On a phone the picture comes down with the card. */
@media (max-width:560px) {
  .par-pic-wrap { height:196px; }
  .par-pic { width:186px; height:186px; }
  .par-word { font-size:1.6rem; }
}

/* ── SPEAK'S GAME AREA (docs/games/speak/01_game-area-design.md; founder review 2026-10-01) ──────
   Row 1: the level tabs (the selected one named) and the mode menu. Row 2, Level 1 only: the set
   emoji. The picture owns the card; under it one row - mic, ✅ We said it, Next - aligned on the
   mic's circle, its "Listening…" hung beneath so it cannot push the row out of line. */
#paradeHud, #sec-parade .game-card { max-width:760px; }
#paradeHud { position:relative; }
.par-bar { flex-wrap:nowrap; }
/* Read's sizes exactly (read-game.css .rd-where / .rd-mode / .rd-w): small pills, small text. */
.par-levels { display:flex; align-items:center; gap:.3rem; min-width:0; flex-wrap:wrap; }
.par-lv { font:800 .85rem/1 'Nunito',sans-serif; color:var(--brown,#7a5a3a); background:#fff;
          border:2px solid var(--warm,#E0CCA8); border-radius:999px; padding:.45rem .65rem; cursor:pointer; white-space:nowrap; }
.par-lv:hover { border-color:var(--orange,#E8844A); }
/* A set's name under its emoji, tiny. width:0 + min-width over the button's padding means the label
   spans the tile (padding included) and adds no width: the emoji alone sizes the button, so the strip keeps its shape. Labels are
   one short word chosen to fit (tools/import_noto.py SHORT); overflow is clipped, never spread. */
.par-set { display:inline-flex; flex-direction:column; align-items:center; gap:.15rem; }
.par-set-lbl { width:0; min-width:calc(100% + 1rem); margin:0 -.5rem; overflow:hidden; text-align:center;
               font:800 .55rem/1.1 'Nunito',sans-serif; letter-spacing:0; white-space:nowrap; color:var(--brown,#7a5a3a); }
.par-set.is-on .par-set-lbl { color:#fff; }
.par-lv.is-on { background:var(--orange,#E8844A); border-color:var(--orange,#E8844A); color:#fff; }
.par-lv:focus-visible { outline:3px solid var(--ink,#3D2C1E); outline-offset:2px; }
.par-menu-mode .rd-opt { white-space:normal; align-items:flex-start; }
.par-opt-txt { display:flex; flex-direction:column; gap:.15rem; }
.par-opt-txt small { font-weight:600; font-size:.78rem; color:var(--brown,#7a5a3a); }
.par-menu-mode { min-width:16rem; max-width:20rem; top:2.9rem; }

.par-run { width:100%; display:flex; flex-direction:column; align-items:center; gap:.9rem; padding-top:1.1rem; }
.par-pic-wrap { width:100%; height:auto; min-height:min(48vh, 28rem); display:flex; align-items:center; justify-content:center; }
.par-pic { width:min(100%, 42rem); height:min(48vh, 28rem); border-radius:18px; }
.par-word { font-size:3rem; min-height:3.6rem; }
.par-act { display:flex; flex-wrap:wrap; align-items:center; justify-content:center; gap:1rem 1.1rem; min-height:4.5rem;
           padding-bottom:1.6rem; }
.par-act .par-mic { flex-direction:row; flex-wrap:nowrap; justify-content:center; align-items:center; gap:1.1rem; min-height:0; }
.par-act .drw-mic-wrap { margin-top:0; position:relative; }
.par-act .drw-listen-status { position:absolute; top:calc(100% + .75rem); left:50%; transform:translateX(-50%); white-space:nowrap; }
.par-act .drw-soft, .par-act .drw-heard { position:absolute; top:calc(100% + 1.9rem); left:50%; transform:translateX(-50%);
                                          white-space:nowrap; }
.par-act .par-btn { font-size:1.15rem; height:3.5rem; padding:0 1.6rem; display:inline-flex; align-items:center; }
.par-more-quiet { cursor:default; opacity:.55; }

/* The chooser: what game this is, then the five levels in one row - level, icon, name, an example. */
.par-choose { display:flex; flex-direction:column; align-items:center; gap:1.6rem; width:100%; padding:.8rem 0 1.4rem; }
.par-choose-head { display:flex; flex-direction:column; align-items:center; gap:.2rem; }
.par-choose-ico { font-size:2.6rem; line-height:1; }
.par-choose-title { font:400 2.2rem/1.1 'Fredoka One',cursive; color:var(--ink,#3D2C1E); margin:.2rem 0 0; }
.par-choose-sub { font:700 1rem/1.3 'Nunito',sans-serif; color:var(--brown,#7a5a3a); margin:0; }
.par-choose-grid { width:100%; display:grid; grid-template-columns:repeat(5, minmax(0, 1fr)); gap:.8rem; }
.par-tile { display:flex; flex-direction:column; align-items:center; gap:.35rem; padding:1rem .5rem 1.1rem;
            background:#fff; border:3px solid var(--warm,#E0CCA8); border-radius:20px; box-shadow:0 5px 0 var(--warm,#E0CCA8);
            cursor:pointer; transition:transform .12s, box-shadow .12s; text-align:center; }
.par-tile:active { transform:translateY(3px); box-shadow:0 2px 0 var(--warm,#E0CCA8); }
.par-tile.is-on { background:#F5C842; border-color:var(--ink,#3D2C1E); box-shadow:0 5px 0 var(--ink,#3D2C1E); }
.par-tile:focus-visible { outline:3px solid var(--ink,#3D2C1E); outline-offset:3px; }
.par-tile-lv { font:800 .72rem/1 'Nunito',sans-serif; letter-spacing:.08em; text-transform:uppercase; color:var(--brown,#7a5a3a); }
.par-tile-ico { font-size:2.6rem; line-height:1.1; }
.par-tile-name { font:400 1.05rem/1.15 'Fredoka One',cursive; color:var(--ink,#3D2C1E); }
.par-tile-ex { font:italic 600 .8rem/1.2 'Nunito',sans-serif; color:var(--brown,#7a5a3a); }
.par-go { font-size:1.4rem; padding:.85rem 2.6rem; }

/* The end of a deal: the pictures just played, as a sheet. */
.par-sheet { display:flex; flex-wrap:wrap; justify-content:center; gap:.5rem; margin:.8rem 0 1.2rem; max-width:40rem; }
.par-thumb { width:5.2rem; height:5.2rem; object-fit:contain; background:#fff; border:2px solid var(--warm,#E0CCA8); border-radius:14px; padding:.25rem; }

@media (max-width:640px) {
  .par-bar { flex-wrap:wrap; }
  .par-choose-grid { grid-template-columns:repeat(2, minmax(0, 1fr)); }
  .par-pic-wrap { min-height:min(40vh, 20rem); }
  .par-pic { height:min(40vh, 20rem); width:100%; }
  .par-word { font-size:2rem; min-height:2.5rem; }
}

/* ==== lock-ui.css ==== */
/* Numzi lock UI. Pairs with assets/js/lock-ui.js.

   Tokens (--ink, --cream, --orange, --warm, --brown, --sage) come from the page: numzi.css on the
   site pages, charm-game.css inside the games. This file declares none of its own, so it can load
   anywhere without fighting whatever is already in :root. */

/* A locked control is NOT faded. It looks exactly like an open one and the badge does all the
   telling.

   Fading was wrong twice over. On Charm's pills and Tot's cards it made a paid level look like a
   disabled control rather than an available one, which is the "you have not earned this" reading
   we keep trying to kill. And on Shape Explorer and Colour Hunt the nav buttons already carry
   their OWN opacity (.42 unmet, .9 met, 1 current — see .shp-nav-btn in game.html), so an extra
   .6 multiplied down to .25 and the locked shapes were barely visible. A parent has to be able to
   see WHICH shape they cannot have; that is the entire pitch.

   position:relative stays, because the badge is positioned against it. */
.nz-locked { position: relative; }

/* ── A FENCE ROUND A RUN OF LOCKED CONTROLS ───────────────────────────────────
   The thin outlined box. It has to behave like the controls it swallowed, because the nav rows are
   flex with flex-shrink:0 on every child — they wrap rather than compress.

   `flex: 0 1 auto` with min-width:0 is load-bearing: never grow, but DO shrink. At 0 0 auto the
   box sized itself to its content and refused to give way, so ABC's twenty-one locked letters made
   a 700px row inside a 500px column and, being centred, spilled off BOTH edges — F to I vanished
   on the left and the label on the right. Shrinking is what lets its own flex-wrap engage. */
/* THE OUTLINE COSTS ZERO LAYOUT. This is the whole requirement: Shape Explorer's shapes must sit
   in exactly the positions they sat in before anything was locked. So the wrapper has no padding
   and no border of its own — it inherits the row's gap, which keeps the spacing between the
   shapes it swallowed identical to the spacing between the ones it did not — and the dashed box is
   drawn by an absolutely positioned ::before that is outset from it and therefore takes no space
   at all. Nothing reflows. */
.nz-lk-fence { display: flex; align-items: center; justify-content: center; gap: inherit;
               flex: 0 1 auto; min-width: 0; flex-wrap: wrap;
               position: relative; padding: 0; border: 0;
               /* ROOM FOR THE OUTLINE TO BE SEEN AS ITS OWN EDGE. The row's own gap is .4rem
                  (6.4px) and the dashes are outset 8px, so with no margin the outline landed ON
                  the last free shape and the two read as one merged group. This buys the dashes
                  about 10px of clear space. It is the one thing that does move the locked items,
                  and it moves them together: their spacing relative to each other is untouched,
                  which is what kept the row looking like itself. */
               margin-left: .7rem; }
.nz-lk-fence::before { content: ''; position: absolute; inset: -7px -8px;
                       border: 1.5px dashed var(--warm, #E0CCA8); border-radius: 13px;
                       pointer-events: none; }

/* "Premium" at the box's TOP RIGHT, straddling the outline — the same badge treatment as a single
   locked control, so one visual idea covers a run and an item alike. Absolutely positioned, so it
   costs no layout either. A button, not a span: a parent will tap the words, and that tap is the
   same offer as tapping a locked item. */
.nz-lk-fence-lbl { position: absolute; top: -15px; right: 2px; z-index: 2;
                   border: 1.5px solid var(--ink, #3D2C1E); border-radius: 999px;
                   background: var(--kraft, #EFE3C8); color: var(--ink, #3D2C1E);
                   font-family: 'Fredoka One', cursive; font-size: .58rem; letter-spacing: .01em;
                   line-height: 1.1; padding: .13rem .4rem; cursor: pointer;
                   white-space: nowrap; }

/* The same badge, inline in a sentence — for places that are prose rather than a control, like
   Picture Parade's "N more with Premium" line. Same pill, same border, same font as the badge on a
   locked capsule, so a parent recognises one idea rather than learning a second one. */
.nz-lk-inline { display: inline-block; vertical-align: middle; margin-right: .4rem;
                font-family: 'Fredoka One', cursive; font-size: .62rem; letter-spacing: .01em;
                line-height: 1.1; white-space: nowrap;
                background: var(--kraft, #EFE3C8); color: var(--ink, #3D2C1E);
                border: 1.5px solid var(--ink, #3D2C1E); border-radius: 999px;
                padding: .14rem .42rem; }

/* ── ONE LOCKED ITEM, LABELLED IN PLACE ───────────────────────────────────────
   Two variants, because two shapes of control need it.

   DEFAULT — Tot's 2x2 level cards, where a fence cannot enclose levels 2, 3 and 4 without tearing
   the grid. The words go on their own line inside the card, which is easily large enough, and the
   card's border goes dashed to match the fence's language. */
.nz-lk-marked:not(.is-chip) { border-style: dashed !important; }
/* A real badge, not a caption. At .62rem in plain brown it read as a footnote on a card big enough
   to carry something you can actually see, and the lock was the size of a full stop. */
.nz-lk-marked:not(.is-chip) .nz-lk-mark-lbl {
  display: inline-block; margin-top: .55rem;
  font-family: 'Fredoka One', cursive; font-size: .82rem; letter-spacing: .01em;
  line-height: 1.1; background: var(--kraft, #EFE3C8); color: var(--ink, #3D2C1E);
  border: 2px solid var(--ink, #3D2C1E); border-radius: 999px;
  padding: .22rem .7rem; }

/* .is-chip — Charm Counting's level pills. ON the capsule's edge, the way the padlock was: it
   straddles the top-right border, half over the pill and half outside it, so it reads as a badge
   belonging to that control rather than as a separate item sitting next to it. The pill keeps its
   exact size and its solid border; the chip costs it nothing.

   The pair wrapper stays (it is what guarantees a chip cannot be separated from its pill by a line
   break) but it no longer adds a gap — the chip is positioned against the pill, not laid out
   beside it. */
.nz-lk-pair { display: inline-flex; position: relative; flex: 0 0 auto; }
.nz-lk-marked.is-chip { border-style: solid; }
.nz-lk-pair .nz-lk-mark-lbl {
  position: absolute; top: -.5rem; right: -.45rem; z-index: 2;
  margin: 0; white-space: nowrap; line-height: 1.1;
  font-family: 'Fredoka One', cursive; font-size: .56rem; letter-spacing: .01em;
  background: var(--kraft, #EFE3C8); color: var(--ink, #3D2C1E);
  border: 1.5px solid var(--ink, #3D2C1E); border-radius: 999px;
  padding: .13rem .38rem;
  /* ── IT TAKES TAPS NOW, AND THAT IS THE FIX ─────────────────────────────────
     It was pointer-events:none so that a tap near the corner fell through to the control beneath.
     Which is exactly what went wrong: a parent tapping the word "Premium" — the most obvious thing on
     the row to tap when you want to know what Premium costs — had their tap delivered to the game,
     which jumped to a locked letter, which ended the run, which the session then turned into "Game
     completed!". The one screen whose job is to answer "what would I be buying" was unreachable
     through the one word that names it.

     The cost is a few pixels of the control's corner, which is the right trade: the chip is a target
     worth having, and the control has all of its own middle. */
  pointer-events: auto; cursor: pointer; }
.nz-lk-pair .nz-lk-mark-lbl:hover { background: var(--warm, #E0CCA8); }
.nz-lk-pair .nz-lk-mark-lbl:focus { outline: none; }
.nz-lk-pair .nz-lk-mark-lbl:focus-visible {
  outline: 2px solid var(--ink, #3D2C1E); outline-offset: 2px; }

/* Breathing room ONLY when a chip is present, so wrapped lines cannot push a badge into the pill
   above it and make it look like that one is locked. Untouched for a subscriber, who has no
   chips. */
.level-row:has(.nz-lk-pair),
.lv-row:has(.nz-lk-pair) { row-gap: .85rem; }

/* ── THE POPUP: /pricing's PLAN CARD, CENTRED ─────────────────────────────────
   It used to sit at the bottom of the screen like a mobile action sheet, which made the one screen
   that has to feel like an offer read like a system tray. Centred, and allowed to scroll inside
   itself on a short screen, because the pricing card is tall and a popup that cannot be scrolled
   to its own button is a popup with no button. */
.nz-lk-wrap { position: fixed; inset: 0; z-index: 9000; display: flex;
              align-items: center; justify-content: center; padding: 1rem .8rem;
              overflow-y: auto; }
.nz-lk-back { position: fixed; inset: 0; background: rgba(61, 44, 30, .55); }

/* Matching pricing.html's .pr-plan: same border, same 7px offset shadow, same radius, same
   padding, so this is the card a parent has already seen rather than a lookalike. */
.nz-lk-sheet { position: relative; width: 100%; max-width: 430px; margin: auto;
               background: var(--card, #FDF6E9); color: var(--ink, #3D2C1E);
               border: 3px solid var(--ink, #3D2C1E); border-radius: 24px;
               box-shadow: 7px 7px 0 var(--ink, #3D2C1E);
               padding: 1.8rem 1.7rem 1.6rem; text-align: center;
               animation: nz-lk-up .22s ease-out; }
/* 430px on a phone, 620px where there is room — which is what lets the container queries put
   the unlock rows and the foundation names in two columns and bring the whole offer above the
   fold on a laptop. The VIEWPORT query is right here and only here: it asks the screen how wide
   to make the card, and everything inside the card then asks the card.
   AFTER the rule it overrides, not before it with !important. The first attempt used
   !important — which then beat the inline width a resize test was setting, so every width in
   the sweep silently measured a 620px card and reported no change at any of them. Source order
   is the fix; !important is what hides a fix. */
@media (min-width: 760px) {
  .nz-lk-sheet { max-width: 620px; }
}

@keyframes nz-lk-up { from { transform: translateY(12px); opacity: 0; } to { transform: none; opacity: 1; } }
/* Someone who has set "reduce motion" is telling us they get motion sick, not that they want a
   slower animation. */
@media (prefers-reduced-motion: reduce) { .nz-lk-sheet { animation: none; } }

/* The green badge straddling the top border, exactly as on /pricing. */
.nz-lk-plan-badge { position: absolute; top: -15px; left: 50%; transform: translateX(-50%);
                    background: var(--sage, #7BBF8E); color: #fff;
                    font-family: 'Fredoka One', cursive; font-size: .76rem;
                    border: 2.5px solid var(--ink, #3D2C1E); border-radius: 999px;
                    padding: .22rem .9rem; white-space: nowrap; }

/* The old price block — a 3rem figure, a cadence line and a tax note — is gone with the
   markup that used it. .nz-lk-priceline below is the one line that replaced all three. */

.nz-lk-x { position: absolute; top: .5rem; right: .65rem; width: 2rem; height: 2rem;
           border: 0; background: transparent; color: var(--brown, #9B6B4A);
           font-size: 1.5rem; line-height: 1; cursor: pointer; }

.nz-lk-badge { display: inline-block; font-family: 'Fredoka One', cursive; font-size: .68rem;
               letter-spacing: .08em; text-transform: uppercase;
               background: var(--warm, #E0CCA8); color: var(--ink, #3D2C1E);
               border-radius: 999px; padding: .22rem .6rem; margin-bottom: .45rem; }

/* ── THE AMBITION, ABOVE THE PRICE ───────────────────────────────────────────
   The card opens on what a parent wants rather than on what we charge. The price still has to
   be found without hunting, which is why the lede is two lines and the pills are one. */
.nz-lk-head { font-family: 'Fredoka One', cursive; font-size: 1.24rem; line-height: 1.3;
              margin: .45rem 0 .35rem; color: var(--ink, #3D2C1E);
              /* balance evens the two lines instead of leaving "learn." alone on the second. */
              text-wrap: balance; }
.nz-lk-pills { display: flex; flex-wrap: wrap; gap: .26rem; justify-content: center;
               margin: 0 0 .5rem; }
/* Sized so all three sit on ONE line inside the card, at any card width — the clamp shrinks
   them on a narrow phone rather than letting the row break 2 + 1, which reads as a list with an
   afterthought instead of three marks of the same standing. */
.nz-lk-pills span { font-weight: 800; line-height: 1.3;
                    font-size: .66rem; font-size: clamp(.56rem, 2.66cqw, .66rem);
                    background: var(--kraft, #EFE3C8); color: var(--ink, #3D2C1E);
                    border-radius: 999px; white-space: nowrap;
                    /* The padding shrinks with the text, or on a narrow phone the three pills
                       are mostly padding and break the row anyway. */
                    padding: .22rem .42rem; padding: .22rem clamp(.3rem, 1.7cqw, .42rem); }
.nz-lk-pe { font-style: normal; margin-right: .22rem; }
/* THE EMOJI GOES BEFORE THE ROW DOES. Below this the three pills cannot hold one line at a
   readable size, and three marks broken 2 + 1 read as a list with an afterthought. The words
   are the claim; the leaf and the globe are decoration, so decoration is what is cut. */
@container (max-width: 358px) {
  .nz-lk-pe { display: none; }
}


/* ── THE TWO HALVES, AND WHY THEY ARE STYLED IDENTICALLY ─────────────────────
   One rule for both section headings, one rule for both capsule lists. The halves were once
   "Main Learning" and "Also Building", and any difference in size, weight or colour here would
   re-introduce exactly the demotion that renaming them was meant to remove. If one of these
   ever needs its own style, that is the moment to check the naming again. */
/* ── THE TIER ABOVE THE TWO HALVES ───────────────────────────────────────────
   "With your membership" and "What Numzi helps build" are the card's two real sections; the
   foundations headings are subsections of the second. So these are INK and carry no rule, while
   .nz-lk-h2 below is brown and does — the difference is what tells a parent that Learning and
   Thinking belong under one idea rather than being two more pitches in a row. */
.nz-lk-sec { font-family: 'Fredoka One', cursive; letter-spacing: .09em; text-transform: uppercase;
             color: var(--ink, #3D2C1E); text-align: left; margin: 1.35rem 0 .2rem;
             font-size: .82rem; font-size: clamp(.76rem, 3.6cqw, .86rem); }
/* The one sentence that connects the games to the reasons. Italic so it reads as a note on the
   heading rather than as the first item under it. */
.nz-lk-secsub { font-style: italic; font-weight: 700; text-align: left; line-height: 1.45;
                color: var(--brown, #9B6B4A); margin: 0 0 .1rem; text-wrap: pretty;
                font-size: .82rem; font-size: clamp(.76rem, 3.5cqw, .86rem); }
/* The first section label sits right under the pills and needs no gap of its own. */
.nz-lk-pills + .nz-lk-sec { margin-top: .9rem; }

.nz-lk-h2 { font-family: 'Fredoka One', cursive; font-size: .74rem; letter-spacing: .1em;
            text-transform: uppercase; color: var(--brown, #9B6B4A); text-align: left;
            margin: 1.25rem 0 .1rem; padding-bottom: .3rem;
            border-bottom: 2px solid var(--warm, #E0CCA8); }

/* ── COLLAPSED AND EXPANDED ──────────────────────────────────────────────────
   Both faces are in the DOM and this shows one. Toggling a class rather than re-rendering
   matters because the card can be inside a scrolled sheet: replacing its middle would move
   everything under the parent's thumb mid-read.

   :not() rather than a plain descendant rule, and that is not style: the two-column media query
   further down carries `.nz-lk-sheet .nz-lk-caps { display: block }`, which is the same
   specificity and sits later in the file, so a plain `.nz-lk-offer .nz-lk-caps` lost and the
   collapsed card rendered BOTH faces — measured taller closed (1773px) than open (1618px).
   `.nz-lk-offer:not(.is-open)` is three classes and wins wherever it sits. */
/* The unlock rows are EXCLUDED here rather than un-hidden by a later rule. They are the one
   capsule list the toggle must not touch — they are the answer this modal exists to give — and
   a separate `.nz-lk-offer .nz-lk-unlock { display: block }` lost to this selector on
   specificity and silently hid all four. Carving them out of the hide is the version that
   cannot lose. */
.nz-lk-offer:not(.is-open) .nz-lk-caps:not(.nz-lk-unlock) { display: none; }
.nz-lk-offer.is-open .nz-lk-names { display: none; }

/* Past the clip: in the DOM, hidden until the detail opens. And "+ 5 more" is not a button —
   the one control below it opens everything, so a second tappable thing saying the same would
   be two ways to do one job. It is a count, and the count is itself an argument. */
.nz-lk-offer:not(.is-open) .nz-lk-over { display: none; }
.nz-lk-offer.is-open .nz-lk-moren { display: none; }
.nz-lk-moren { font-weight: 800; color: var(--brown, #9B6B4A); opacity: .85;
               padding: .26rem 0 .26rem 1.62rem; line-height: 1.4;
               font-size: .84rem; font-size: clamp(.78rem, 3.7cqw, .88rem); }

/* ── THE PRICE, ONE LINE ─────────────────────────────────────────────────────
   Under the "Unlock all of Numzi" heading, because a parent who tapped a lock is asking what
   ₹499 buys and the figure belongs in the same breath as the promise. One line rather than the
   old 3rem figure over two more lines of cadence and tax notes: the button repeats the price on
   the way out, and /pricing carries the terms in its own fact cards. */
.nz-lk-priceline { display: flex; align-items: baseline; justify-content: center;
                   flex-wrap: wrap; gap: .1rem .5rem; margin: .1rem 0 .9rem; }
.nz-lk-priceline b { font-family: 'Fredoka One', cursive; color: var(--orange, #E8844A);
                     font-size: 1.6rem; font-size: clamp(1.35rem, 7cqw, 1.7rem); line-height: 1.1; }
.nz-lk-per { font-size: .62em; color: var(--brown, #9B6B4A); }
.nz-lk-cancel { font-weight: 700; color: var(--brown, #9B6B4A);
                font-size: .8rem; font-size: clamp(.74rem, 3.4cqw, .84rem); }

/* ── THE CARD SIZES ITSELF TO ITSELF ─────────────────────────────────────────
   container-type, so every clamp() below can use cqw — a share of THIS CARD's width, not the
   viewport's. vw would repeat the bug the two-column grid had: the card is 430px wide whether
   the screen is 1512px or 375px, so 4vw is 60px on a desktop popup and 15px on a phone, which
   is backwards. cqw asks the only box that matters.
   Each rule states a plain size first: where cqw is unsupported the clamp is dropped and the
   plain value stands, rather than the text losing its size altogether. */
.nz-lk-offer { container-type: inline-size; }

/* The same names, one per line, no descriptions. */
.nz-lk-names { list-style: none; padding: 0; margin: .55rem 0 0; text-align: left; }
.nz-lk-names li { display: flex; gap: .5rem; align-items: baseline; padding: .26rem 0;
                  font-weight: 800; color: var(--ink, #3D2C1E); line-height: 1.4;
                  font-size: .88rem; font-size: clamp(.8rem, 3.9cqw, .92rem); }

/* Quiet. It opens an explanation; it is not competing with Subscribe. */
.nz-lk-detail { display: block; margin: .9rem auto 0; font-family: 'Fredoka One', cursive;
                font-size: .8rem; background: none; border: 0; cursor: pointer;
                color: var(--brown, #9B6B4A); text-decoration: underline;
                text-underline-offset: 3px; padding: .2rem .4rem; }
.nz-lk-detail:hover { color: var(--orange, #E8844A); }

.nz-lk-caps { list-style: none; padding: 0; margin: .6rem 0 0; text-align: left; }
.nz-lk-caps li { display: flex; gap: .55rem; align-items: flex-start; padding: .34rem 0; }
.nz-lk-cap-i { font-size: 1.05rem; line-height: 1.35; flex: 0 0 auto; }
.nz-lk-cap-b { display: block; }
.nz-lk-cap-b b { display: block; font-weight: 800; line-height: 1.35;
                 color: var(--ink, #3D2C1E);
                 font-size: .9rem; font-size: clamp(.82rem, 3.9cqw, .92rem); }
.nz-lk-cap-b span { display: block; line-height: 1.4;
                    color: var(--brown, #9B6B4A); font-weight: 600; text-wrap: pretty;
                    font-size: .8rem; font-size: clamp(.74rem, 3.4cqw, .82rem); }

/* ── TWO COLUMNS, WHEN THE CARD IS ACTUALLY WIDE ─────────────────────────────
   A @media two-column rule lived here once and was deleted, because it asked the VIEWPORT: on a
   1512px desktop it fired inside a 430px card and made two 176px columns where "Language,
   reading & writing" wrapped onto three lines. It is back as a CONTAINER query, which asks the
   card — so it can only fire once the card is genuinely wide enough, which it now is on a
   desktop (see .nz-lk-sheet / .pr-plan below). This is the change that gets the whole offer
   above the fold on a laptop: four unlock rows and seven foundation names in half the height. */
@container (min-width: 520px) {
  /* The four unlock rows in two columns. */
  .nz-lk-caps.nz-lk-unlock { display: grid; grid-template-columns: 1fr 1fr;
                             column-gap: 1.3rem; align-items: start; }
  /* And the two foundations halves side by side rather than one after the other. Not each list
     split into two columns — that was the first attempt, and three names across two columns is
     an awkward shape that saves almost nothing. Standing the SECTIONS level is both the bigger
     saving and the better statement: neither half is above the other on the page. */
  .nz-lk-fwrap { display: grid; grid-template-columns: 1fr 1fr;
                 column-gap: 1.6rem; align-items: start; }
  /* The first heading in each column must not add its own top margin, or the two headings sit
     at different heights and the whole point of standing them level is lost. */
  .nz-lk-fgroup .nz-lk-h2 { margin-top: .4rem; }
  /* Wide is also where the fold matters, so the vertical rhythm tightens with it. Every one of
     these is a gap that reads as generous on a phone and as wasted on a laptop. */
  .nz-lk-sec    { margin-top: .85rem; }
  .nz-lk-foot   { margin-top: .8rem; padding-top: .65rem; }
  .nz-lk-detail { margin-top: .5rem; }
  .nz-lk-alt    { margin-top: .45rem; }
  .nz-lk-caps li, .nz-lk-names li { padding-top: .26rem; padding-bottom: .26rem; }
}

/* ── THE PRICE LINE IS DESKTOP-ONLY ──────────────────────────────────────────
   On a phone it repeats the Subscribe button, which is now pinned to the bottom of the screen
   and reads "₹499/mo" whatever a parent has scrolled to — so the line at the top is the same
   number twice with the height of a row between them. On a wide card the button is not always
   in view, so the figure earns its place. Container query, not media: the test is whether THIS
   card has the room. */
/* Hidden by default; the container query below turns it on for a narrow card. DECLARED
   BEFORE that query, not after — it was written after it first, and being the same
   specificity and later in the file it won, so the button never appeared on the phone it
   exists for while the sentence it reveals was already hidden. */
/* Hidden by default and revealed only on a narrow card, so a desktop reader never sees a control
   for a sentence that is already in front of them. */
.nz-lk-i { display: none; align-items: center; justify-content: center;
           width: 1.05rem; height: 1.05rem; margin-left: .4rem; vertical-align: middle;
           border: 1.5px solid var(--brown, #9B6B4A); border-radius: 50%;
           background: none; color: var(--brown, #9B6B4A); cursor: pointer;
           font-family: 'Fredoka One', cursive; font-size: .62rem; line-height: 1;
           padding: 0; text-transform: none; }
.nz-lk-i:hover { color: var(--orange, #E8844A); border-color: var(--orange, #E8844A); }

/* ── EVERYTHING A NARROW CARD DOES DIFFERENTLY ───────────────────────────────
   Container, not media: the test is whether THIS card is narrow, which is true on a phone and
   false in a 620px desktop popup on the same page. Desktop is untouched by all of it. */
@container (max-width: 470px) {
  .nz-lk-priceline { display: none; }

  /* The bridge sentence goes behind the (i). It is a line of prose sitting between a parent and
     the two lists they came to read, and on a phone the lists are what have to clear the fold. */
  .nz-lk-i { display: inline-flex; }
  .nz-lk-secsub { display: none; }
  .nz-lk-offer.is-note .nz-lk-secsub { display: block; margin-top: .3rem; }

  /* AND THE UNLOCK ROWS TIGHTEN. Four rows of title-plus-sentence was the single biggest block
     on the card and it was pushing LEARNING FOUNDATIONS off the bottom of a phone. Nothing is
     cut — the padding between rows, the leading inside a description and the gap to the icon are
     each a couple of pixels that read as generous on a desktop card and as distance here. */
  .nz-lk-caps li     { padding: .15rem 0; gap: .45rem; }
  .nz-lk-cap-b b     { line-height: 1.25; }
  .nz-lk-cap-b span  { line-height: 1.3; }
  .nz-lk-cap-i       { font-size: .95rem; }
  .nz-lk-sec         { margin-top: .7rem; }
  .nz-lk-h2          { margin-top: .7rem; padding-bottom: .2rem; }
  .nz-lk-names       { margin-top: .3rem; }
  .nz-lk-names li    { padding: .16rem 0; }
  .nz-lk-moren       { padding: .16rem 0 .16rem 1.62rem; }
  .nz-lk-pills       { margin-bottom: .3rem; }
  .nz-lk-head        { margin: .25rem 0 .4rem; }
  .nz-lk-caps        { margin-top: .35rem; }
}


/* ── SUBSCRIBE, ALWAYS ON SCREEN ─────────────────────────────────────────────
   The card carries three sections now and a parent reading the thinking foundations is a long
   way from a button. Sticking the button to the bottom of the sheet is what lets the content be
   this long with nothing hidden behind an accordion or a "+7 more" — they scroll the whole
   proposition and the decision is always one tap away.
   The background is opaque and the negative margins bleed it to the card's edges, so text
   scrolling underneath does not show through. */
.nz-lk-foot { margin: 1.2rem 0 0; padding: .9rem 0 0;
              border-top: 2px solid var(--warm, #E0CCA8); }
/* STICKY ONLY INSIDE THE POPUP. The in-place card and /pricing sit in normal page flow, where
   sticking to the viewport bottom would clip the guarantee under the fold instead of keeping
   the button in reach. The negative margins bleed the opaque background to the sheet's own
   edges — they match .nz-lk-sheet's 1.7rem/1.6rem padding exactly, so text scrolling underneath
   cannot show through at the sides. */
.nz-lk-sheet .nz-lk-foot { position: sticky; bottom: -1.6rem; z-index: 2;
              background: var(--card, #FDF6E9);
              margin: 1.3rem -1.7rem -1.6rem; padding: .8rem 1.7rem 1rem; }

/* ── AND ON /pricing, ON A PHONE ─────────────────────────────────────────────
   The page is the scroll container there, so bottom:0 pins Subscribe to the bottom of the
   screen exactly as it is pinned inside the popup — a parent scrolling the foundations always
   has the button. A viewport query, correctly: this one is about the SCREEN doing the
   scrolling, not about how wide the card is. Above this width the whole card fits and a pinned
   button would be a bar floating over nothing. */
@media (max-width: 760px) {
  .pr-plan .nz-lk-foot { position: sticky; bottom: 0; z-index: 2;
                         background: var(--card, #FDF6E9);
                         margin: 1.3rem -1.7rem -1.6rem; padding: .8rem 1.7rem 1rem; }
}

/* No bottom margin any more: the sticky footer supplies its own top spacing, and 1.4rem here
   left a gap the footer then sat below. */
/* Each tick's own sentence, riding under it rather than costing a tick of its own. Lighter, so
   the ticks stay a list of three things and these read as notes on them. */
/* .nz-lk-pillnote was an attribution line under the three marks and is gone with the Harvard
   pill that needed it. Kept out rather than left unused: a disclaimer on a card whose job is to
   convert reads as a warning, whatever it actually says. */

/* ── "See Premium" ────────────────────────────────────────────────────────────
   The only thing a card or the microphone asks for. Deliberately quieter than the orange
   Subscribe button, because it is not the transaction — it opens the explanation. */
.nz-lk-see { font-family: 'Fredoka One', cursive; font-size: .85rem;
             background: var(--kraft, #EFE3C8); color: var(--ink, #3D2C1E);
             border: 2px solid var(--ink, #3D2C1E); border-radius: 11px;
             padding: .4rem .9rem; cursor: pointer;
             box-shadow: 0 3px 0 var(--warm, #E0CCA8); }
.nz-lk-see:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--warm, #E0CCA8); }
.nz-lk-see.is-mic { font-size: .8rem; padding: .34rem .8rem; }

/* pricing.html's .pr-cta */
.nz-lk-cta { display: block; text-decoration: none; font-family: 'Fredoka One', cursive;
             font-size: 1.08rem; background: var(--orange, #E8844A); color: #fff;
             border: 2.5px solid var(--ink, #3D2C1E); border-radius: 14px;
             padding: .85rem 1rem; box-shadow: 0 5px 0 #b85e28; white-space: nowrap; }
.nz-lk-cta:active { transform: translateY(3px); box-shadow: 0 2px 0 #b85e28; }

/* GREEN, and pricing.html's .pr-guar sizing. It was brown, which made the one reassuring line on
   the screen read as fine print. */
.nz-lk-fine { font-size: .8rem; font-weight: 800; color: #2f7d4a;
              margin: .5rem 0 0; line-height: 1.45; }

/* ── an in-place card ──────────────────────────────────────────────────────────
   .is-offer carries the full plan card in the game stage, so it takes the plan card's own solid
   border and offset shadow rather than the dashed outline used for a fenced run. The dashes say
   "there is more behind here"; this is not behind anything, it is the offer. */
.nz-lk-card { background: var(--cream, #FDF6E9); border: 2.5px dashed var(--warm, #E0CCA8);
              border-radius: 16px; padding: .9rem .9rem .8rem; text-align: center;
              max-width: 380px; margin: .4rem auto; }
.nz-lk-card.is-offer { position: relative; max-width: 430px; margin: 1.4rem auto 0;
                       background: var(--card, #FDF6E9);
                       border: 3px solid var(--ink, #3D2C1E); border-radius: 24px;
                       box-shadow: 7px 7px 0 var(--ink, #3D2C1E);
                       padding: 1.8rem 1.7rem 1.6rem; }
.nz-lk-card-t { font-family: 'Fredoka One', cursive; font-size: 1rem;
                color: var(--ink, #3D2C1E); margin: 0 0 .55rem; }

/* ── the locked microphone ────────────────────────────────────────────────────
   Scoped to .drw-mic.nz-mic-locked and never to a bare .drw-mic, so it cannot restyle the real
   panel a paid visitor sees. Everything else about the panel — its size, its position, its status
   line — comes from game.html's own .drw-mic-wrap block, untouched. */
.drw-mic.nz-mic-locked { animation: none !important;
                         background: var(--cream, #FDF6E9);
                         border-color: var(--warm, #E0CCA8);
                         box-shadow: 0 4px 0 var(--warm, #E0CCA8);
                         transform: none; }
/* Tight to the status line above it, so the padlock, the sentence and the button are one offer
   rather than three things that happen to be stacked. */
/* text-align:center because the button is inline-level and the panel is a column flex whose
   items stretch — without it "See Premium" sat hard left while the padlock and the sentence
   above it were centred. */
.nz-lk-micask { margin-top: .4rem; width: 100%; max-width: 300px; text-align: center; }
.nz-lk-cta.is-mic { font-size: .88rem; padding: .48rem .7rem; }

/* The per-game block that lived here — a tinted box carrying "What your little one just
   practised" over two outcome lines — is gone, and its seven rules with it. The popup is
   the offer and only the offer now; see lock-ui.js. The fork it was half of survives as
   .nz-lk-alt below. */

/* The second door. Deliberately not styled as a button: subscribing stays the primary action,
   this is the way out that is not "close". */
.nz-lk-alt {
  display: flex; align-items: center; justify-content: center; gap: .45rem;
  margin-top: .6rem;
}
.nz-lk-or { font-size: .74rem; opacity: .45; }
.nz-lk-more {
  font-family: 'Fredoka One', cursive; font-size: .88rem;
  color: var(--ink, #3D2C1E); text-decoration: underline; text-underline-offset: 2px;
}
.nz-lk-more:hover { color: var(--orange, #E8844A); }
.nz-lk-more:focus { outline: none; }
.nz-lk-more:focus-visible { outline: 3px solid var(--orange, #E8844A); outline-offset: 3px; }

/* ==== level-strip.css ==== */
/* A game's levels, as a row above the play area. See assets/js/level-strip.js for why this is a
   strip rather than a screen, and why locked levels are chipped rather than fenced. */

/* WIDTH 100%, AND THIS IS THE WHOLE CLIPPING BUG. Every .game-section is `flex-direction: column;
   align-items: center`, so a host with no width of its own is a flex item that shrinks to
   FIT-CONTENT — and the fit-content width of a row of six sets is 620px. Inside a 500px section
   that host sits at x = -60 with 60px outside the viewport, and no amount of scrolling the row
   brings it back, because the loss is the host's position and not the row's scroll offset. It also
   gave the page 60px of horizontal overflow.
   The row underneath is the scroller (overflow-x: auto); it can only do its job if its box is the
   width of the screen. max-width lines the strip up with the game card on a desktop. */
.lv-wrap { width: 100%; max-width: 580px; margin-inline: auto; }
.lv-wrap[hidden] { display: none; }

/* ONE SCROLLING LINE. Four levels fit a phone; ABC has three but Charm has five and a future game
   may have more, and a row that wraps is the fault this codebase has now paid for three times —
   ten game tabs, eight subject capsules, seven game capsules. Scrolling costs one row whatever the
   count, and level-strip.js scrolls the current level into view so it is never the one off the
   right edge.

   ROW-GAP EXISTS DESPITE nowrap, and it is not dead. lock-ui's chip hangs a "🔒 Premium" label
   beside its button inside a .nz-lk-pair; on a row with no vertical slack that label is what
   collides with whatever sits above. Same reason lock-ui.css carries a :has(.nz-lk-pair) rule. */
.lv-row {
  display: flex; gap: .45rem;
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity;
  padding: .5rem .1rem .3rem;
  scrollbar-width: none;
  justify-content: center;
}
.lv-row::-webkit-scrollbar { display: none; }
/* justify-content: center looks right for four capsules and breaks scrolling once the row
   overflows — the first item ends up unreachable. flex-start the moment it does. */
.lv-row > * { scroll-snap-align: start; }
/* Set by level-strip.js when the row is genuinely wider than its box. Centring an overflowing
   scroll row clips it at BOTH ends, and the left-hand loss is unreachable — see the note there. */
.lv-row.is-scrolling { justify-content: flex-start; }

/* Two lines per capsule: the level, and what it actually is. "Level 2" alone means nothing to a
   parent deciding whether it is worth paying for; "Three-Letter Words" is the whole argument. */
.lv-cap {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .34rem .7rem;
  border: 2px solid var(--warm, #E0CCA8); border-radius: 14px;
  background: var(--cream, #FDF6E9); color: var(--ink, #3D2C1E);
  cursor: pointer; text-align: left;
  transition: background .15s, border-color .15s, color .15s;
}
.lv-cap:hover { border-color: var(--orange, #E8844A); }
.lv-cap:active { transform: translateY(2px); }

/* The current level takes the ink, not the orange. Orange is spoken for twice already on this page
   — the subject capsule and the See all games button — and a third orange thing above the game
   would be a row of shouting. Ink reads as "you are here". */
.lv-cap.is-on {
  background: var(--ink, #3D2C1E); border-color: var(--ink, #3D2C1E);
  color: var(--cream, #FDF6E9);
}
.lv-cap:focus { outline: none; }
.lv-cap:focus-visible { outline: 3px solid var(--ink, #3D2C1E); outline-offset: 2px; }

.lv-cap-ico { font-size: .95rem; line-height: 1; }
.lv-cap-txt { display: flex; flex-direction: column; line-height: 1.1; }
.lv-cap-name {
  font-family: 'Fredoka One', cursive; font-weight: normal;
  font-size: clamp(.68rem, 3vw, .78rem);
}
.lv-cap-sub {
  font-family: 'Nunito', sans-serif; font-weight: 800;
  font-size: clamp(.56rem, 2.5vw, .64rem);
  opacity: .62;
}
.lv-cap.is-on .lv-cap-sub { opacity: .78; }

/* A locked capsule is not faded. lock-ui.css says why in full: these rows carry their own opacity
   ladder for met/unmet items, and an extra multiplier took the locked ones down to a quarter. The
   chip beside it is the signal. */
.lv-cap.nz-locked { cursor: pointer; }

/* ── THE COMPACT PREMIUM RUN ───────────────────────────────────────────────────
   ABC Time's tray used to draw all twenty-six letters and fence off the twenty-one a free visitor
   cannot have — three rows of a phone screen spent on letters that only said no, and the fence grew
   to hold them, so withholding more cost more room. It collapses to one capsule reading "F G … Z".

   Styled here rather than beside .abc-cell because the next game with a long locked run wants the
   same thing — Picture Parade is that game, and its "6 … 10" shares these rules unchanged. Sized to sit in a row of letter cells without being one: wider, same height, and
   letter-spaced so the glyphs read as an alphabet rather than as a word. */
.abc-more, .par-more {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 2rem; padding: .2rem .8rem;
  font-family: 'Fredoka One', cursive; font-size: .82rem; letter-spacing: .12em;
  border: 2px dashed var(--warm, #E0CCA8); border-radius: 10px;
  background: var(--cream, #FDF6E9); color: var(--brown, #9B6B4A);
  cursor: pointer; white-space: nowrap;
}
.abc-more:hover, .par-more:hover { border-color: var(--orange, #E8844A); color: var(--ink, #3D2C1E); }
.abc-more:active, .par-more:active { transform: translateY(2px); }
.abc-more:focus, .par-more:focus { outline: none; }
.abc-more:focus-visible, .par-more:focus-visible { outline: 3px solid var(--ink, #3D2C1E); outline-offset: 2px; }

/* The chip hangs beside it in a pair, so the tray needs vertical slack for the same reason the
   level row does — see lock-ui.css. */
.abc-tray:has(.nz-lk-pair) { row-gap: .9rem; }

/* ── GAME MODE CONTROLS ────────────────────────────────────────────────────────
   Deliberately here rather than in a game's own stylesheet. /game carries inline copies of the
   per-game CSS and does NOT link assets/css/abc-game.css, so anything added there would silently
   do nothing on the only page that matters — the "change both" trap flagged above charm's level
   row. This file is linked, and these classes are generic, so the next game to move its modes
   into the card gets the same treatment for free.

   THEY MUST NOT LOOK LIKE THE STRIPS ABOVE. Subject, game and level are all pill capsules and all
   navigation; who-speaks is a setting for the game you are already in. So: square corners rather
   than pills, a hairline rather than a 2px border, small type, and grouped tight against the top
   right of the card so it reads as belonging to the play area. */
/* WIDTH-MATCHED TO THE CARD, so its right edge lines up with the card's rather than with the
   section's. .game-card is `max-width: 580px; width: 100%`, so this is the same, centred the same
   way — otherwise on a wide screen the controls float off to the right of the box they belong to.

   The negative margin pulls it against the card. .game-section is a flex column with a 1rem gap
   between every child, which is right for content and too much for a control that should read as
   attached to the thing below it. */
.nz-modes {
  position: relative;              /* the (i) callout measures itself against this */
  width: 100%; max-width: 580px;
  margin: 0 auto -.75rem;          /* tight against the card it belongs to */
  padding: 0 .2rem .3rem;
}

/* THE ROW THAT ACTUALLY LAYS THE BUTTONS OUT IS THE INNER ONE, and missing that is why they came
   out left-aligned on a phone. abc.js renders the buttons into <span class="abc-modes">, so
   .nz-modes only ever contained that span and the callout — putting justify-content on .nz-modes
   right-aligned the span, while the buttons inside it were still laid out flex-start by
   .abc-modes' own rule. It has to be set on the container that holds them.

   ONE LINE UNLESS THE SCREEN REALLY CANNOT TAKE IT. Spacing is spent first — a 4px gap and tight
   padding — and the type shrinks with the viewport only after that, which is the order that keeps
   the labels readable for as long as possible. flex-wrap stays as the last resort rather than the
   first, and when it does wrap the lines stay right-aligned. */
.nz-modes > .abc-modes {
  display: flex; flex-wrap: wrap; justify-content: flex-end;
  gap: .25rem;
  width: 100%;
}
.nz-modes[hidden] { display: none; }

.nz-mode {
  display: inline-flex; align-items: center; gap: .22rem;
  font-family: 'Nunito', sans-serif; font-weight: 800;
  /* Wound down from 2.7vw so three capsules hold one line on a 360px phone. Spacing above was
     spent first; this is the second lever, not the first. */
  font-size: clamp(.52rem, 2.3vw, .7rem); line-height: 1.2;
  padding: .24rem .42rem;
  border: 1.5px solid var(--warm, #E0CCA8); border-radius: 8px;
  background: none; color: var(--brown, #9B6B4A);
  cursor: pointer; white-space: nowrap;
  transition: background .15s, border-color .15s, color .15s;
}
.nz-mode:hover { background: var(--cream, #FDF6E9); color: var(--ink, #3D2C1E); }
.nz-mode.is-on {
  background: var(--kraft, #EFE3C8); border-color: var(--brown, #9B6B4A);
  color: var(--ink, #3D2C1E);
}
/* Not built yet for this level. Greyed rather than hidden, for the reason abc.js gives: a control
   that vanishes between levels confuses more than one that says it is not ready. */
.nz-mode.is-locked { opacity: .45; }
.nz-mode:focus { outline: none; }
.nz-mode:focus-visible { outline: 3px solid var(--ink, #3D2C1E); outline-offset: 2px; }

/* The (i) is a target in its own right — abc.js stops the tap from also switching mode — so it
   needs to be big enough to hit without being big enough to shout.

   POSITION IS RESET, and that is the whole reason this rule is long. .abc-i was written as
   `position:absolute; right:.5rem; top:50%` for a chip that was position:relative with padding
   held open on its right. Dropped into a flex capsule with neither, it left the flow entirely and
   printed itself over the last letter of every label — "Numzi leadsi". It is a flex item here, so
   it is laid out rather than placed. */
.nz-mode .abc-i {
  position: static; right: auto; top: auto; transform: none;
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto;
  width: .95em; height: .95em; border-radius: 50%;
  font: 900 .8em/1 'Nunito', sans-serif; font-style: normal;
  background: rgba(61, 44, 30, .12); color: inherit;
}
.nz-mode.is-on .abc-i { background: rgba(61, 44, 30, .2); }

/* ── WHAT TODAY HOLDS, AND WHICH OF IT IS YOURS ────────────────────────────────
   "🍎 Today's Fruits – 10", then 1 2 3 4 5, then "6 … 10" behind one Premium badge.

   Two earlier attempts said this in prose. "1 of 5" beside "+5 more" asked a parent to add two
   numbers that measured different things; "5 of 10 free" fixed the arithmetic but still asked them
   to picture the deal from a sentence, and on a wide screen the badge — absolutely positioned at
   the right edge of its pair — ended up stranded a hand's width from the words it belonged to.

   Numbers carry it without a sentence, and the badge goes back to hanging off a pill, which is what
   lock-ui's chip is drawn for. See the note on .abc-more: same pattern, one file up. */
.par-chrome { display: flex; flex-direction: column; align-items: center; }

/* Title and numbers on ONE line, wrapping to two only when the screen cannot hold them — a title
   plus ten cells is about 400px, so a 360px phone wraps and nothing else does. Centred either way. */
.par-nav {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: .3rem .6rem; max-width: 100%;
}
.par-cells {
  display: inline-flex; align-items: center; justify-content: center;
  flex-wrap: wrap; gap: .25rem;
}
/* .par-where has gone: the set's name and size are the HUD's label now (play-hud.css .hud-where). */
/* Small, because there can be ten and they are navigation rather than the game. Square enough that
   a two-digit "10" keeps the same shape as "1". */
.par-cell {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.65rem; min-height: 1.65rem; padding: 0 .2rem;
  font-family: 'Fredoka One', cursive; font-size: .76rem;
  border: 2px solid var(--warm, #E0CCA8); border-radius: 9px;
  background: var(--cream, #FDF6E9); color: var(--brown, #9B6B4A);
  cursor: pointer; transition: background .15s, border-color .15s, color .15s;
}
.par-cell:hover { border-color: var(--orange, #E8844A); color: var(--ink, #3D2C1E); }
.par-cell.is-on {
  background: var(--ink, #3D2C1E); border-color: var(--ink, #3D2C1E);
  color: var(--cream, #FDF6E9);
}
.par-cell:focus { outline: none; }
.par-cell:focus-visible { outline: 3px solid var(--ink, #3D2C1E); outline-offset: 2px; }
/* The run's chip hangs beside it in a pair, so this row needs the vertical slack the level row and
   ABC's tray need — see lock-ui.css. */
.par-cells:has(.nz-lk-pair) { row-gap: .9rem; }

/* Parade's mode chips and their (i) are .hud-switch / .hud-switch-i now — see play-hud.css. The
   rescues that used to live here (.nz-mode .par-i, .nz-modes > .par-toggles) went with the row. */

/* ==== play-hud.css ==== */
/* ════════════════════════════════════════════════════════════
   THE PLAY AREA'S HUD  —  see assets/js/play-hud.js for what it is and why

   ── THE SHAPE LANGUAGE, AND WHY IT DIFFERS FROM THE PLAYER ──────────────────────
   The player above uses capsules. This uses rounded SQUARES. That is deliberate: the two zones do
   different jobs — one is the session, one is the run — and a parent should be able to tell them
   apart without reading either. Squares also carry the level-select grammar every game has used since
   arcades, where a numbered cell means "a stage you can go to".

   ── WELDED, NOT STACKED ─────────────────────────────────────────────────────────
   It shares an edge with the card so the two read as one object. No outer box around the pair: the
   card already has a border, and a box around a bordered card is two concentric rounded rectangles —
   the dashboard look this product is avoiding. No rule under the play bar either; that would be a
   third horizontal edge saying what the player's tint already says.
   ════════════════════════════════════════════════════════════ */

/* ── THE FRAME ──────────────────────────────────────────────────────────────────
   Same fill and border as the card, no bottom border, top corners rounded and bottom square. The
   negative margin cancels the section's 1.5rem gap plus the card's 3px border so the two overlap into
   a single edge; `.game-card` gives up its top rounding to meet it. */
.nz-hud {
  box-sizing: border-box;
  /* The anchor for a switch's (i) callout: it is positioned against the strip, so a chip near either
     end can be clamped inside it rather than pushed off screen. */
  position: relative;
  display: block;
  width: 100%; max-width: 580px;
  /* -22px = the section's 1.5rem gap, plus the card's 3px top border, plus this strip's own 3px
     bottom border — so the card's border tucks exactly underneath the strip's and one line shows. */
  margin: 0 auto -22px;
  padding: 0;
  /* ── A BAND, NOT MORE CARD ─────────────────────────────────────────────────
     THE DIVIDER WAS MISSING, and the cause is worth recording because it will happen again. The
     strip used to rely on the CARD's own 3px top border showing through as the line between them.
     Then `position: relative` was added here so ABC's mode callout could anchor to the strip — and a
     positioned element paints above its static siblings whatever the DOM order says, so the strip's
     background covered the card's border and the line vanished. In all five games at once.

     So the line belongs to the strip: its own bottom border, which no paint order can hide. And with
     a line there it may as well be a band — cream against the card's white, the same treatment the
     site nav already uses (cream fill, 3px warm rule under it). That is what an arcade HUD does
     right: it is visibly a different surface from the play field, so the eye never has to work out
     where the chrome ends and the game starts. */
  background: var(--cream, #FDF6E9);
  border: 3px solid var(--warm, #E0CCA8);
  border-radius: 20px 20px 0 0;
}
.nz-hud[hidden] { display: none; }
/* Belt and braces: mount() hides an empty HUD, but if play-hud.js never ran at all the host would
   otherwise draw as a 6px frame with nothing in it. */
.nz-hud:empty { display: none; }
.nz-hud + .game-card,
.nz-hud + .drw-game-wrap,
.nz-hud + .clay-wrap,
.nz-hud + .weather-wrap,
.nz-hud + .baby-wrap,
.nz-hud + .animal-wrap { border-top-left-radius: 0; border-top-right-radius: 0; }

/* Below 900px the section's own top padding was already cut to .5rem, so the gap this cancels is the
   same either way — the margin is the section gap, not the padding. */
/* The extra top padding is for a long run's badge, which hangs above its fence — 14px up, so the row
   has to own that space or the badge is clipped by the frame's rounded corner. */
.hud-row:has(.hud-cells.is-long .nz-lk-fence-lbl) { padding-top: 1.25rem; }
.hud-row {
  display: flex; align-items: center; flex-wrap: wrap;
  /* The BOTTOM padding is larger than the top on purpose. The first build had them equal and the
     level pill sat on the seam with the card, which reads as a collision rather than a join — the
     eye needs more clearance where two objects meet than where one ends. */
  padding: .42rem .55rem .5rem;
  gap: .3rem .5rem;
}

/* ── THE LEAD: LABEL AND CELLS, ON ONE UNBREAKABLE LINE ────────────────────────
   See play-hud.js — this exists so the label shrinks instead of the cells wrapping, which is what a
   plain wrapping row does (flexbox breaks a line before it shrinks anything on it). */
.hud-lead {
  display: flex; flex-wrap: nowrap; align-items: center;
  gap: .5rem; min-width: 0; flex: 1 1 auto;
}

/* ── WHERE: THE ONLY WORDS ──────────────────────────────────────────────────────
   What is on, and what it means, once. This is the label the numbered cells need in order not to be
   ambiguous — which is why there is no separate "Levels:" in front of them: "Level 1 · 1–5" names the
   run and states the current step in the same breath, and printing "Level" twice in one row is the
   same duplication we just took out of the player.

   It shrinks before anything else does. On a narrow phone the meaning can ellipse away; the cells
   cannot, because they are the depth this strip exists to show. */
.hud-where {
  display: inline-flex; align-items: baseline; gap: .28rem;
  /* Shrinks first and never disappears entirely: a couple of characters plus an ellipsis still says
     which set this is, where an empty space would say nothing. */
  flex: 0 1 auto; min-width: 2.4rem; overflow: hidden;
  font-family: 'Fredoka One', cursive;
  font-size: clamp(.6rem, 2.7vw, .76rem);
  line-height: 1.3;
  color: var(--brown, #9B6B4A);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.hud-where { margin-right: .15rem; }   /* the label needs a beat before the first cell */
.hud-where-main, .hud-where-sub { white-space: nowrap; }
.hud-where-sub::before { content: '·'; opacity: .5; margin-right: .28rem; }

/* ── THE CELLS ──────────────────────────────────────────────────────────────────
   One per step, current lit. Small enough that five plus a Premium badge fit a 360px phone without
   scrolling, big enough to stay a comfortable tap: 26px is under the 44px guideline for a primary
   control, which is right — the primary control in this product is the game, and these are a parent's
   occasional adjustment, made with a fingertip on a phone they are holding for the child. */
.hud-cells { display: inline-flex; align-items: center; gap: .26rem; flex: 0 0 auto; }
.hud-cell {
  min-width: 1.62rem; height: 1.62rem; padding: 0 .22rem;
  box-sizing: border-box;
  display: inline-flex; align-items: center; justify-content: center;
  font-family: 'Fredoka One', cursive;
  font-size: clamp(.66rem, 2.9vw, .78rem);
  color: var(--brown, #9B6B4A);
  background: var(--cream, #FFF8EE);
  border: 2px solid var(--warm, #E0CCA8);
  border-radius: 8px;
  cursor: pointer;
  transition: background .16s, color .16s, transform .1s;
}
.hud-cell:hover { background: #FFE8C8; }
.hud-cell.is-on {
  background: var(--orange, #E8844A);
  border-color: #B85E28;
  color: #fff;
}
.hud-cell:focus { outline: none; }
.hud-cell:focus-visible { outline: 3px solid var(--ink, #3D2C1E); outline-offset: 2px; }

/* A GLYPH CELL. Shape Explorer's cells hold the shape itself rather than a number; the SVG is drawn
   in currentColor, so it takes the cell's state for free — brown when upcoming, white on orange when
   it is the one being taught. */
.hud-cell > svg { display: block; width: 100%; height: 100%; }
.hud-cell:has(> svg) { padding: .18rem; line-height: 0; }

/* ── A SWATCH CELL ──────────────────────────────────────────────────────────────
   Colour Hunt's cells are the colours. A circle, filled, because that is how this game has always
   shown a colour and because a colour needs area to be read as itself. The current one is marked by
   a ring rather than by a fill — filling it would paint over the one thing the cell is for. */
.hud-cell.is-swatch {
  border-radius: 50%; padding: 0;
  background: var(--swatch);
  opacity: .55;
}
.hud-cell.is-swatch:hover { opacity: 1; background: var(--swatch); }
.hud-cell.is-swatch.is-done { opacity: .92; border-color: #B59FD6; }
.hud-cell.is-swatch.is-on {
  opacity: 1; background: var(--swatch);
  border-color: var(--ink, #3D2C1E); color: inherit;
  box-shadow: 0 0 0 2px rgba(61, 44, 30, .18);
}
/* Locked swatches keep their colour — dimming them to grey would hide what is being offered. */
.nz-hud .hud-cell.is-swatch.nz-locked { opacity: .45; }
.nz-hud .hud-cell.is-swatch.nz-locked:hover { opacity: .7; background: var(--swatch); }

/* AN EMOJI CELL. Picture Parade's sets are named by their own icon — an apple says "fruits" without
   being read — and an emoji needs the whole cell rather than a text baseline inside it. */
.hud-cell > .par-set-ico {
  display: block; width: 100%; line-height: 1;
  font-size: .96rem; text-align: center;
}
.hud-cell:has(> .par-set-ico) { padding: .1rem; }
.hud-cell.is-on > .par-set-ico { filter: saturate(1.15); }

/* ALREADY MET, in the product's "done" purple — the same colour Shape Explorer's own row used. Quieter
   than the current cell and clearly not the same as an upcoming one. */
.hud-cell.is-done {
  color: #8f79c4; border-color: #B59FD6;
}
.hud-cell.is-done.is-on { color: #fff; border-color: #B85E28; }

/* ── A LONG RUN ─────────────────────────────────────────────────────────────────
   Six or more cells: a little smaller, a little tighter, and the Premium badge goes back above the
   fence where it costs no width. See LONG_RUN in play-hud.js for why. */
.hud-cells.is-long { gap: .2rem; }
.hud-cells.is-long .hud-cell { min-width: 1.46rem; height: 1.46rem; }
.hud-cells.is-long .nz-lk-fence { margin-left: .34rem; }
.hud-cells.is-long .nz-lk-fence-lbl {
  /* -11px, not lock-ui's -15: the badge has to straddle the fence's outline AND stay clear of the
     frame's own top border, which the run does not have to worry about anywhere else in the product.
     At -15 the badge crossed the HUD's edge and read as a rendering slip. */
  position: absolute; top: -11px; right: 0; margin-left: 0;
}

/* ── THE LOCKED TAIL ────────────────────────────────────────────────────────────
   lock-ui's fence, restyled for a strip. Everywhere else it hangs its badge above the run, straddling
   the outline — which needs ~17px of clear space overhead, and in a row this tight that space is the
   difference between a HUD and a second header. The badge is the LAST child of the fence, so making it
   static puts it at the end of the run and it reads as part of the sequence: 3 4 5 Premium.

   The outline stays: it is what says these three belong together, and it is the same idea a parent
   has already met on Shape Explorer's locked shapes. */
.nz-hud .nz-lk-fence {
  display: inline-flex; align-items: center; gap: .26rem;
  margin-left: .5rem;
  position: relative;
}
.nz-hud .nz-lk-fence::before { inset: -3px -5px; border-radius: 10px; }
.nz-hud .nz-lk-fence-lbl {
  position: static; top: auto; right: auto;
  margin-left: .1rem;
  font-size: clamp(.5rem, 2.2vw, .58rem);
  padding: .1rem .34rem;
  line-height: 1.5;
}
/* Locked cells stay legible rather than being dimmed into a wall: the point of showing them is that a
   parent can see what is there, and the badge is what says they are not open yet. */
.nz-hud .hud-cell.nz-locked { opacity: .72; }
.nz-hud .hud-cell.nz-locked:hover { opacity: .9; background: var(--cream, #FFF8EE); }

/* ── SWITCHES ───────────────────────────────────────────────────────────────────
   Who speaks, and what gets traced. Right-aligned, because progress reads left and preference reads
   right — and they keep their words: an icon alone does not say "Take turns", and a parent choosing
   who speaks is deciding about their own behaviour, which is not a decision to make from a pictogram.

   `is-off` is not `nz-locked`. Locked means Premium; off means this level has no such choice (ABC's
   Take turns on Level 2). Nothing is being sold, so nothing is chipped — and it stays in place rather
   than disappearing, so the row does not reshuffle when a parent changes level. */
.hud-switch-group {
  display: inline-flex; align-items: center; gap: .26rem;
  margin-left: auto; flex: 0 1 auto;
  /* WRAPS RATHER THAN CLIPS. Three chips with their words come to about 300px, and a 360px phone has
     roughly 264px inside the card — so on the narrowest screens they would have been cut off at the
     edge. They tighten below 380px (see the media query) and, if that is still not enough, they wrap.
     A third row on a very small phone is worse than two and better than a chip with its label sliced
     off; nothing here is worth losing to save a row. */
  flex-wrap: wrap; justify-content: flex-end;
}
.hud-switch-lbl {
  font-family: 'Nunito', sans-serif; font-weight: 800;
  font-size: clamp(.52rem, 2.2vw, .6rem);
  letter-spacing: .05em; text-transform: uppercase;
  color: var(--brown, #9B6B4A); opacity: .75;
  margin-right: .1rem;
}
.hud-switch {
  display: inline-flex; align-items: center; gap: .2rem;
  height: 1.62rem; padding: 0 .46rem;
  box-sizing: border-box;
  font-family: 'Nunito', sans-serif; font-weight: 800;
  font-size: clamp(.6rem, 2.6vw, .7rem);
  color: var(--brown, #9B6B4A);
  background: var(--cream, #FFF8EE);
  border: 2px solid var(--warm, #E0CCA8);
  border-radius: 999px;              /* a capsule, not a cell: it is a choice, not a step */
  cursor: pointer;
  white-space: nowrap;
}
.hud-switch-ico { font-size: .82em; line-height: 1; }
/* The (i). Small, round, and its own tap target inside the chip — the chip changes the mode, this
   explains it. */
.hud-switch-i {
  display: inline-flex; align-items: center; justify-content: center;
  width: .92em; height: .92em; margin-left: .1rem;
  font-family: 'Nunito', sans-serif; font-style: normal; font-weight: 900;
  font-size: .78em; line-height: 1;
  border: 1.5px solid currentColor; border-radius: 50%;
  opacity: .62; flex: 0 0 auto;
}
.hud-switch:hover .hud-switch-i { opacity: 1; }
.hud-switch:hover:not(.is-off) { background: #FFE8C8; }
.hud-switch.is-on {
  background: var(--orange, #E8844A); border-color: #B85E28; color: #fff;
}
.hud-switch.is-off { opacity: .42; cursor: default; }
.hud-switch:focus { outline: none; }
.hud-switch:focus-visible { outline: 3px solid var(--ink, #3D2C1E); outline-offset: 2px; }

/* ── THE NARROWEST PHONES ───────────────────────────────────────────────────────
   Under ~380px the switches drop to their own line rather than squeezing the cells, and they stay
   right-aligned so the shape of the strip is recognisably the same one. Never a third row. */
@media (max-width: 379px) {
  .hud-switch-group { flex-basis: 100%; justify-content: flex-end; gap: .2rem; }
  /* Tighter, so three chips and their words fit a 360px card without wrapping or clipping. The (i)
     stays: a phone is exactly where a parent needs the sentence, and hover titles do not exist. */
  .hud-switch { padding: 0 .34rem; }
  /* The tail of the label goes rather than the label wrapping to a line of its own: "Level 1" plus
     five cells plus the badge fits 306px, and one row is the whole point of this strip. */
  .hud-where-sub { display: none; }
}

/* ==== pattern-game.css ==== */
/* ════════════════════════════════════════════════════════════
   WHAT COMES NEXT? — the play area

   IN A LINKED FILE, NOT IN game.html's INLINE BLOCK. Every game before this one carries its CSS
   inline, which is why a rule added to assets/css/<game>.css can be dead on the only page that
   matters. New games go the other way: the file is linked from game.html's head, so there is one
   copy and /play can pick it up too.

   Tokens only — --cream, --kraft, --warm, --ink, --sage, --brown come from game.html's :root.
   The pattern row and the answer cards deliberately reuse Colour Hunt's card geometry (136px,
   3.5px warm border, 22px radius, the 4px shadow that presses in on :active), because a child
   who has played Colour Hunt should recognise a tappable card here without being taught it again.
════════════════════════════════════════════════════════════ */

.pat-wrap { min-height: 420px; padding-top: 1.2rem; }
.pat-round { display: flex; flex-direction: column; align-items: center; gap: .5rem; width: 100%; }

/* THE PATTERN ITSELF. One row, never wrapping: an AB pattern that breaks onto two lines stops
   looking like a sequence, which is the whole thing being read. Four tiles at 96px plus gaps fits
   inside a 280px card at the small end because the tiles shrink, not the row. */
.pat-row { display: flex; align-items: center; justify-content: center;
           gap: .7rem; flex-wrap: nowrap; width: 100%; margin: .4rem 0 .2rem; }

/* SIX SLOTS FIT BY SHRINKING, NEVER BY WRAPPING OR SCROLLING. The teach row shows four tiles and
   asks for two more, and an AB pattern that breaks onto a second line stops being a sequence — it
   is the one thing on this screen that has to be read left to right in one go. Measured: six at
   72px plus five 11px gaps is 431px inside the 446px card at 500, and six at 44px plus 5px gaps is
   289px inside the ~290px card a 360px phone produces. */
.pat-row.is-long { gap: .5rem; }
.pat-row.is-long .pat-tile { width: 72px; height: 72px; flex: 0 1 72px; border-radius: 16px; padding: .4rem; }

.pat-tile { width: 96px; height: 96px; flex: 0 1 96px; border-radius: 20px;
            display: flex; align-items: center; justify-content: center;
            background: var(--cream); border: 3px solid var(--warm); padding: .55rem;
            opacity: 0; transform: translateY(10px) scale(.94);
            transition: opacity .45s, transform .45s cubic-bezier(.34,1.56,.64,1),
                        box-shadow .25s, background-color .3s; }
.pat-tile.is-in { opacity: 1; transform: none; }

/* EACH TILE LANDS, it does not fade up. The walk names one thing at a time and a three-year-old
   has to notice that a NEW thing arrived — a 450ms opacity ramp reads as the row having been
   there all along. It overshoots and settles, which is the same spring the answer cards use, so
   arriving is one movement across the whole game. */
.pat-tile.is-landing { animation: patDrop .42s cubic-bezier(.34,1.56,.64,1); }
@keyframes patDrop {
  from { transform: translateY(-14px) scale(.6); opacity: 0; }
  60%  { transform: translateY(0) scale(1.12); opacity: 1; }
  to   { transform: none; opacity: 1; }
}

/* The empty tile breathes once it is the only thing left in the row — an invitation, not an
   alarm. It stops the moment the answer lands in it. */
.pat-gap.is-in { animation: patWaiting 1.8s ease-in-out infinite; }
@keyframes patWaiting { 50% { transform: scale(1.06); border-color: var(--orange, #E8843A); } }

/* ── THE RECAP BEAT: THE TILE BEING SAID IS THE TILE THAT GROWS ────────────────
   Each tile zooms while its own word is spoken and settles as the next takes over, so a child
   who is listening and a child who is looking are being pointed at the same thing.

   It LIFTS AND GROWS rather than flashing: a flash reads as an alert, and this is a re-teach.
   1.22 because at 1.06 it was a nudge nobody noticed on a 66px tile — the scale has to survive
   being small. The ring is the warm border thickening rather than a new colour arriving, so the
   tile that is speaking is louder than its neighbours without becoming a different object. */
.pat-tile.is-beat {
  transform: translateY(-10px) scale(1.22);
  border-color: var(--orange, #E8843A);
  box-shadow: 0 10px 0 var(--warm), 0 0 0 4px rgba(232,132,58,.18);
  z-index: 2;
}

/* THE GAP. Dashed, hollow and quiet — it is a question, not an answer waiting to be pressed. */
.pat-gap { border-style: dashed; background: transparent;
           font-family: 'Fredoka One', cursive; font-size: 2.2rem; color: var(--warm); }
.pat-tile.is-filled { animation: patLand .55s cubic-bezier(.34,1.56,.64,1); }
@keyframes patLand { from { transform: scale(.5); opacity: .2; } to { transform: scale(1); opacity: 1; } }

.pat-swatch { width: 100%; height: 100%; border-radius: 12px; display: block; }

/* PICTURE PARADE'S EMOJI, AT TILE SIZE. `contain` because these are drawn to different aspect
   ratios inside their own boxes — a banana is wide, a strawberry tall — and cropping one to fill
   the tile would cut the very silhouette the child is matching on. No filter and no tint: the
   whole reason this art is here is that it already looks like the thing. */
.pat-pic { width: 100%; height: 100%; object-fit: contain; display: block;
           -webkit-user-drag: none; user-select: none; }
.pat-svg    { width: 100%; height: 100%; display: block;
              filter: drop-shadow(2px 3px 4px rgba(0,0,0,.12)); }
.pat-obj    { width: 100%; height: 100%; display: block; }

.pat-ask { font-family: 'Fredoka One', cursive; font-size: 1.4rem; color: var(--ink);
           text-align: center; min-height: 2.1rem; }
.pat-feedback { font-family: 'Fredoka One', cursive; font-size: 1.3rem; height: 2.1rem;
                line-height: 2.1rem; text-align: center; }
.pat-good { color: var(--sage); }
.pat-try  { color: var(--brown); }

/* THE TWO CHOICES. Bigger than the pattern tiles, because these are the things to press. */
.pat-choices { display: flex; gap: 1.4rem; justify-content: center; align-items: center;
               flex-wrap: wrap; margin-top: .2rem; }
.pat-choices.is-locked { pointer-events: none; }
.pat-card { width: 136px; height: 136px; border: 3.5px solid var(--warm); border-radius: 22px;
            background: var(--cream); cursor: pointer; display: flex; align-items: center;
            justify-content: center; padding: .9rem; box-shadow: 0 4px 0 var(--warm);
            opacity: 0; transform: translateY(16px) scale(.94);
            transition: opacity .6s, transform .55s cubic-bezier(.34,1.56,.64,1),
                        border-color .3s, background-color .3s, box-shadow .15s; }
.pat-choices.is-in .pat-card { opacity: 1; transform: none; }
.pat-choices.is-in .pat-card:hover  { transform: translateY(-4px); box-shadow: 0 8px 0 var(--warm); }
.pat-choices.is-in .pat-card:active { transform: translateY(2px);  box-shadow: 0 2px 0 var(--warm); }
.pat-card.is-right { border-color: var(--sage); background: #E8F7EE;
                     animation: correctPop .5s cubic-bezier(.34,1.56,.64,1); }
.pat-card.is-wrong { animation: shake .4s ease; }

/* .pat-card.is-hint and its float went with the "Tap the banana." line. Pointing at the answer is
   one step from giving it — and a card that bobs is very hard for a two-year-old NOT to press, so
   what looked like help was really the game answering itself. After a second miss the row simply
   repeats and the child tries again. */

.pat-empty { text-align: center; color: var(--brown); padding: 2rem 1rem;
             font-family: 'Nunito', sans-serif; }

/* The card gives up its top rounding to the HUD band welded above it. An id beats two classes,
   so this is written against the section — the trap ENG-010 records from ABC and Tot. */
#sec-pattern .nz-hud + .pat-wrap { border-radius: 0 0 24px 24px; }

@media (prefers-reduced-motion: reduce) {
  .pat-tile, .pat-card { transition-duration: .01ms; animation: none; }
}

/* Sub-380 is measured by constraining a container, never by rendering at 360 — Chrome clamps the
   headless viewport at 500. Four 66px tiles and three .4rem gaps is 283px, which fits the
   narrowest card the play area produces. */
@media (max-width: 520px) {
  .pat-wrap { min-height: 372px; padding-top: 1rem; }
  .pat-row { gap: .4rem; }
  .pat-tile { width: 66px; height: 66px; flex: 0 1 66px; border-radius: 14px; padding: .35rem; }
  .pat-row.is-long { gap: .3rem; }
  .pat-row.is-long .pat-tile { width: 44px; height: 44px; flex: 0 1 44px; border-radius: 10px; padding: .22rem; }
  .pat-gap { font-size: 1.6rem; }
  .pat-ask { font-size: 1.2rem; }
  .pat-choices { gap: .9rem; }
  .pat-card { width: 104px; height: 104px; padding: .6rem; }
}

/* ── THE (i) CALLOUT FOR THE SHUFFLE CHIP ─────────────────────────────────────
   An anchored popover under the chip that was tapped, with a caret pointing at it — not a line of
   text under the row, which leaves a parent working out which chip it belongs to and pushes the
   game down as it appears. Lifted from .abc-hint so the two behave identically; a parent should
   meet one kind of explanation in this product, not one per game.

   `visibility` is deliberately not transitioned — it is not interpolatable, and all it has to do
   is flip so the callout cannot be read out or tabbed to while invisible. */
.pat-hint {
  position: absolute; top: calc(100% + 9px); left: 50%;
  transform: translateX(-50%) translateY(-4px);
  max-width: min(22rem, 86vw); width: max-content; margin: 0; padding: .5rem .8rem;
  background: var(--ink); color: var(--cream); border-radius: 12px;
  box-shadow: 0 4px 0 rgba(61,44,30,.35);
  font-family: 'Nunito', sans-serif;
  font-size: .8rem; line-height: 1.4; text-align: center; z-index: 30;
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .16s ease, transform .16s ease;
}
.pat-hint.is-shown { opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0); }
.pat-hint::after {
  content: ''; position: absolute; bottom: 100%;
  left: calc(50% + var(--caret, 0px)); transform: translateX(-50%);
  border: 7px solid transparent; border-bottom-color: var(--ink);
}

/* ==== abc2-game.css ==== */
/* ABC Time v2. Started from abc-game.css and kept deliberately close to it: a child moving from
   one to the other should not notice the engine changed underneath, and a parent who has seen v1
   should recognise the game. What is new here is the pick row and the step-revealed layout.

   THE WHOLE CARD IS PRESENT FROM THE FIRST FRAME and each piece fades in as its step runs — see
   abc2.js. So everything a step reveals starts at opacity 0 and gets `.is-in`, and nothing is
   display:none, because a piece that appears by changing `display` reflows the card and shoves
   the letter sideways while a child is looking at it. That was learned in v1 and is the reason
   the picture and the word already work this way there. */

.abc2-wrap { display:flex; flex-direction:column; align-items:center; }
/* ── THE STAGE HAS TO BE TOLD IT IS FULL WIDTH ────────────────────────────────
   .abc2-wrap is a centring flex column, so #abc2Stage is a flex item that shrink-to-fits — and
   every screen inside it inherits that. This rule lived in lab-abc2.html's inline CSS with a
   comment claiming "on /game the section is a block and already does", which was simply wrong:
   the card is the same flex column on both pages. So the lab looked right and /game did not, and
   the way it showed was the trace box: 205px of panel inside a 580px card, with the crayon meter,
   the ink chips and the canvas all squeezed to match, because they are all `width:100%` of a
   parent that had collapsed. */
#abc2Stage { width:100%; }

/* ── the letter, and the word it starts ─────────────────────────────────── */
/* The letter, and beside it whatever the screen is showing. Column by default so a question and
   its options get the whole width; a row on the screens whose content is a thing to look at. */
/* WIDTH:100%, AND IT IS load-BEARING. Everything inside sizes itself against this — the trace
   box, the crayon meter and the ink picker are all `width:100%; max-width:30rem`. Without a width
   here the frame shrink-to-fits inside a centring flex parent, those three resolve their 100%
   against their own content, and the tracing box came out 205px wide on a 410px card. */
.abc2-top { display:flex; flex-direction:column; align-items:center; width:100%; }
.abc2-top.is-side { flex-direction:row; align-items:flex-start; justify-content:center;
                    gap:clamp(.8rem,3vw,2rem); flex-wrap:wrap; }
.abc2-screen { width:100%; }
.abc2-top.is-side .abc2-screen { width:auto; }
.abc2-card { display:flex; align-items:flex-start; justify-content:center;
             padding:.5rem 0 .2rem; }

/* ── THE LETTER, AND THE TWO SIZES IT HAS ─────────────────────────────────
   It opens huge and alone, because for the first sentence the letter IS the screen — that is what
   v1's Level 1 looked like and it is what a parent recognises. Once it has something to stand
   beside it shrinks to a reference and stays that size for the rest of the lesson.

   The two sizes are a class, not an animation: the change is made while the transition is off and
   the movement is played back as a transform instead. Animating width and height re-lays-out the
   card on every frame, which is where a phone stutters; a transform is composited. See
   travelToWord() in abc2.js — same trick, and for the same reason, as shapes.js:499. */
.abc2-letter { font-family:'Fredoka One',cursive; font-size:clamp(4.5rem,15vw,8rem); line-height:1;
               color:var(--orange); background:#FEFAF4; border:4px solid var(--ink);
               border-radius:24px; box-shadow:6px 6px 0 var(--ink);
               width:clamp(6.5rem,20vw,10rem); height:clamp(6.5rem,20vw,10rem);
               display:flex; align-items:center; justify-content:center; cursor:pointer; padding:0;
               transition:transform .12s, box-shadow .12s; }
.abc2-letter:hover  { transform:translate(-2px,-2px); box-shadow:8px 8px 0 var(--ink); }
.abc2-letter:active { transform:translate(3px,3px);   box-shadow:2px 2px 0 var(--ink); }

/* Screen one, first beat: nothing else on the card, so the letter takes the room. */
.abc2-letter.is-hero { font-size:clamp(6rem,21vw,9.5rem); border-radius:34px;
                       width:clamp(9rem,33vw,14rem); height:clamp(9rem,33vw,14rem); }

/* Screens three and four: the letter is a REFERENCE. Shape Explorer's 80px slot against its
   140px choice cards — small enough to give the play area the room, still there to look up at.
   It is not removed, because a child choosing the /a/ picture, or copying the letter into a
   trace box, has to be able to see the letter she is doing it for. */
.abc2-letter.is-ref { font-size:2.6rem; width:80px; height:80px;
                      border-radius:18px; box-shadow:4px 4px 0 var(--ink); }
@media (max-width:560px) {
  .abc2-letter.is-ref { font-size:2.1rem; width:64px; height:64px; border-radius:15px; }
}

/* Screen two hides it altogether — see the note on the step in abc2.js. */
.abc2-top.is-bare .abc2-card { display:none; }

/* ── THE WORD HAS NO ROOM UNTIL THE LETTER MAKES ROOM FOR IT ────────────────
   Hidden with `display` rather than opacity, which the rest of this file avoids on purpose — an
   invisible picture that still holds its box leaves the opening letter floating above a column of
   nothing, which is what the first build looked like. It is safe here and nowhere else because
   the reflow it causes happens inside the travel: abc2.js adds `is-side`, measures where the
   letter landed, and plays the whole move back as one transform. The jump is never on screen.

   MARKED BY ITS OWN CLASS, not by "any picture on a screen with no row". The rule used to read
   `.abc2-top:not(.is-side) .abc2-for` and it reached forward in time: the moment screen two
   became a single centred picture, it matched that too and the apple vanished from the step
   whose entire subject it is. A hiding rule has to name the thing it hides. */
.abc2-top:not(.is-side) .abc2-arrives { display:none; }

.abc2-for { display:flex; flex-direction:column; align-items:center; gap:.6rem; }
.abc2-pic { width:clamp(6.5rem,20vw,10rem); height:clamp(6.5rem,20vw,10rem);
            background:#FEFAF4; border:4px solid var(--ink); border-radius:28px;
            box-shadow:6px 6px 0 var(--ink); display:grid; place-items:center; padding:.7rem;
            opacity:0; transition:opacity .28s ease; }
.abc2-pic.is-in { opacity:1; }
.abc2-pic img { max-width:100%; max-height:100%; object-fit:contain; display:block; }

.abc2-word { font-family:'Fredoka One',cursive; font-size:clamp(1.1rem,4vw,1.7rem);
             letter-spacing:.09em; color:var(--ink); background:var(--yellow);
             border:2.5px solid var(--ink); border-radius:12px; padding:.2rem .8rem;
             box-shadow:0 4px 0 var(--ink); opacity:0; transition:opacity .25s ease; }
.abc2-word.is-in { opacity:1; }

/* ── REVEALING GROWS DOWNWARD, IT DOES NOT RESERVE ──────────────────────────
   Every step's block is in the DOM from the first frame so that revealing it never re-renders the
   card — but it must not hold its full height while hidden either. The first version did, and
   step one was a letter floating above six hundred pixels of nothing.

   So hidden blocks are max-height:0 and grow into place. The letter never moves: everything here
   is BELOW it, and a page growing downward is what a child already expects when something new
   appears. What the rule in v1 actually forbids is the letter being shoved SIDEWAYS mid-look,
   which is why the picture beside it still fades rather than expands. */
.abc2-step { max-height:0; opacity:0; overflow:hidden;
             transition:max-height .34s ease, opacity .28s ease, margin .34s ease;
             margin:0; width:100%; }
.abc2-step.is-in { opacity:1; }

/* ── the pick: its question, then its options ─────────────────────────────── */
.abc2-ask { font-family:'Fredoka One',cursive; font-size:clamp(1rem,3.6vw,1.25rem);
            color:var(--brown); text-align:center; }
.abc2-ask.is-in { max-height:4rem; margin:.5rem 0 .1rem; }
.abc2-pick-wrap.is-in { max-height:34rem; margin:.5rem 0 .2rem; }

/* ── THE PLAY AREA IS THE BIGGEST THING ON THE SCREEN ───────────────────────
   A GRID, not a row. Three choices across a phone leaves each one thumbnail-sized, and a child
   picking with a finger needs the opposite. Two columns, and an odd last card spans both so it
   sits centred underneath rather than stranded in the left column. */
.nz-pick { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
           gap:clamp(.6rem,3vw,1.1rem); justify-items:center; max-width:30rem; margin:0 auto; }
.nz-pick-opt:last-child:nth-child(odd) { grid-column:1 / -1; }
@media (min-width:620px) { .nz-pick { grid-template-columns:repeat(3,minmax(0,1fr)); }
                           .nz-pick-opt:last-child:nth-child(odd) { grid-column:auto; } }
/* ── THEY ARRIVE, THEY DO NOT APPEAR ────────────────────────────────────────
   Three cards materialising in one frame is a jolt at a child looking at an empty space. They
   rise and settle instead, one after another — Shape Explorer's card entry exactly (its .shp-card
   uses the same cubic-bezier overshoot), with Charm Counting's stagger over the top so the eye is
   led across the row rather than hit by all three at once. Slowed once already on the founder's
   ear: three cards in three-quarters of a second still arrived faster than a two-year-old looks.

   The stagger is on nth-child rather than in script: three cards is the whole of it, and a delay
   the CSS owns cannot get out of step with a reveal the JS owns. */
.nz-pick-opt { background:#FEFAF4; border:4px solid var(--warm); border-radius:22px;
               box-shadow:0 5px 0 var(--warm); padding:.7rem .6rem .6rem; cursor:pointer;
               width:100%; max-width:9.5rem; display:flex; flex-direction:column;
               align-items:center; gap:.4rem;
               opacity:0; transform:translateY(20px) scale(.92);
               transition:opacity .68s ease, transform .78s cubic-bezier(.34,1.4,.64,1),
                          box-shadow .14s, border-color .14s; }
.nz-pick.is-in .nz-pick-opt { opacity:1; transform:none; }
.nz-pick.is-in .nz-pick-opt:nth-child(2) { transition-delay:.26s; }
.nz-pick.is-in .nz-pick-opt:nth-child(3) { transition-delay:.52s; }
@media (prefers-reduced-motion:reduce) {
  .nz-pick-opt { transition:opacity .01ms; transform:none; }
  .nz-pick.is-in .nz-pick-opt { transition-delay:0s; }
}
.nz-pick-opt img { width:100%; height:clamp(5rem,22vw,7rem); object-fit:contain; display:block; }
.abc2-opt-word { font-family:'Fredoka One',cursive; font-size:.95rem; letter-spacing:.06em;
                 color:var(--brown); }
.nz-pick.is-in .nz-pick-opt:hover:not(:disabled) { border-color:var(--ink);
                                    transform:translateY(-3px); box-shadow:0 8px 0 var(--warm); }

/* RIGHT LIFTS. WRONG SHAKES AND COMES BACK. Nothing turns red and nothing is crossed out — no
   game in this product corrects a child, and a wrong tap here is a guess, not a mistake. */
.nz-pick-opt.is-right { border-color:var(--ink); background:var(--yellow);
                        box-shadow:0 6px 0 var(--ink); transform:translateY(-4px); }
.nz-pick-opt.is-wrong { animation:nzPickShake .34s ease; opacity:.55; }
.nz-pick-opt.is-out   { opacity:.25; }
/* SHOWN, NOT ANSWERED. Two misses and the right card is pointed out — sage, and it pulses twice
   so the eye goes to it — but it stays live and so does every other card, because the tap that
   follows should still be the child's. Shape Explorer's .shp-card.is-shown, same treatment. */
.nz-pick-opt.is-shown { border-color:var(--sage); background:#E8F7EE;
                        animation:nzPickPulse .9s ease 2; }
@keyframes nzPickPulse {
  0%,100% { box-shadow:0 5px 0 var(--sage); }
  50%     { box-shadow:0 5px 0 var(--sage), 0 0 0 10px rgba(123,191,142,.22); }
}
@media (prefers-reduced-motion:reduce) { .nz-pick-opt.is-shown { animation:none; } }
@keyframes nzPickShake {
  0%,100% { transform:translateX(0); }
  25%     { transform:translateX(-6px); }
  75%     { transform:translateX(6px); }
}
@media (prefers-reduced-motion:reduce) {
  .nz-pick-opt.is-wrong { animation:none; }
}

/* ── screen 2: the microphone ──────────────────────────────────────────────
   NOT HERE ANY MORE EITHER. That screen is Picture Parade's naming step and now uses its markup
   and its stylesheet — .par-pic-wrap, .par-pic, .par-mic, .par-btn.par-said in say-ui.css. What
   was here was a second version of the same screen, and it looked like one. */
.abc2-mic-slot { width:100%; display:flex; justify-content:center; }

/* ── the trace screen ───────────────────────────────────────────────────────
   NOT HERE. It is ABC Time v1's Level 3 — .abc-trace-ask, .abc-ink, .abc-trace, .abc-trace-hint
   out of assets/css/trace-ui.css, which v1 and Tot Reads load too. This game had its own copy
   twice over: first a hand-rolled one with the buttons positioned on top of the canvas, then Tot
   Reads' arrangement with the crayon under the box. v1's is the one that had already been through
   the phone-sized version of this problem. */
.abc2-trace-row { margin-top:.4rem; }

/* ── buttons and the tray ───────────────────────────────────────────────── */
.abc2-btn { font-family:'Fredoka One',cursive; font-size:.95rem; color:#fff; background:var(--orange);
            border:2.5px solid var(--ink); border-radius:12px; padding:.5rem 1.2rem; cursor:pointer;
            box-shadow:0 4px 0 #b85e28; transition:transform .1s, box-shadow .1s; }
.abc2-btn:active:not(:disabled) { transform:translateY(2px); box-shadow:0 1px 0 #b85e28; }
.abc2-done { background:var(--sage); box-shadow:0 4px 0 #4a9e68; }
.abc2-said { background:var(--sage); box-shadow:0 4px 0 #4a9e68; }

.abc2-tray { display:flex; flex-wrap:wrap; gap:.25rem; justify-content:center;
             max-width:34rem; margin:.5rem auto .2rem; }
.abc2-cell { font-family:'Fredoka One',cursive; font-size:.82rem; line-height:1;
             width:1.75rem; height:1.75rem; padding:0; flex:0 0 auto;
             display:flex; align-items:center; justify-content:center;
             background:var(--card); color:var(--brown); border:2px solid var(--warm);
             border-radius:8px; cursor:pointer;
             transition:background .15s, color .15s, border-color .15s; }
.abc2-cell:hover { border-color:var(--ink); }
.abc2-cell.is-on { background:var(--orange); color:#fff; border-color:var(--ink); }
.abc2-more { font-family:'Fredoka One',cursive; font-size:.82rem; color:var(--brown);
             background:var(--card); border:2px dashed var(--warm); border-radius:8px;
             padding:0 .5rem; height:1.75rem; cursor:pointer; }

/* The mode chips' callout, anchored under the chip that was tapped. */
.abc2-hint { position:absolute; z-index:5; max-width:16rem; background:var(--ink); color:var(--cream);
             font-size:.8rem; line-height:1.35; padding:.45rem .65rem; border-radius:10px;
             opacity:0; pointer-events:none; transition:opacity .18s ease; }
.abc2-hint.is-in { opacity:1; }

/* ── THE FEEDBACK LINE AND THE CONTROLS ────────────────────────────────────
   Both borrowed from what the product already does, so a parent moving between games meets the
   same two things in the same places: Tot Reads' .drw-feedback for how it went, and ABC Time's
   own ‹ Back / Next › for moving on. */
.abc2-feedback { font-family:'Fredoka One',cursive; font-size:1.3rem; text-align:center;
                 min-height:1.7rem; margin:.3rem 0 .1rem; color:var(--sage); }
.abc2-feedback.is-good { color:var(--sage); }
.abc2-feedback.is-enc  { color:var(--orange); }

.abc2-nav { display:flex; gap:.7rem; justify-content:center; margin-top:1rem; }
/* `display:flex` beats the browser's own `[hidden] { display:none }`, so the attribute alone
   cannot hide this row — the same trap trace-ui.css records about the trace buttons. Said here
   explicitly so the trace step can put its own pair of buttons up and take the frame's away. */
.abc2-nav[hidden] { display:none; }
.abc2-prev { background:var(--card); color:var(--brown); border-color:var(--warm);
             box-shadow:0 4px 0 var(--warm); }
.abc2-prev:disabled { opacity:.45; cursor:default; }

/* ==== read-game.css ==== */
/* Read — the game's own styles, and nothing the page around it owns.

   THE HOST OWNS THE CHROME: the palette, the .game-card frame, the HUD (play-hud.css), the
   microphone panel (mic-ui.css) and the "We said it" button (say-ui.css). The word-as-slider is
   _blend-step.js's and it brings its own styles. What is left here is the navigation (units,
   lessons, dots), the read-aloud screen, the picture reveals and the mode hint.

   EVERY SELECTOR IS PREFIXED .rd-, because /game holds fourteen games in one document. */

#readStage { width:100%; }
/* Wider than the house 580px: two dropdowns, the stepper and both modes in words fit on one line
   ("we have plenty of space"). The HUD and the card widen together so they still meet as one edge. */
#readHud, #sec-read .game-card { max-width:760px; }
.rd { width:100%; display:flex; flex-direction:column; align-items:center; gap:1.1rem; position:relative; }

/* ── THE HUD: two thin bands in the shared .nz-hud frame (read.js, NAVIGATION) ─────────────
   Top: where (level · unit ▾), the lesson stepper, the mode. Under it: the word rail, which is the
   navigation a child actually uses, so it gets the weight. */
.rd-bar { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:.4rem .5rem;
          padding:.4rem .55rem; }
.rd-pickers { display:flex; align-items:center; gap:.3rem; min-width:0; }
.rd-where { font:800 .85rem/1 'Nunito',sans-serif; color:var(--brown,#7a5a3a); background:#fff;
            border:2px solid var(--warm,#E0CCA8); border-radius:999px; padding:.45rem .65rem;
            cursor:pointer; white-space:nowrap; }
.rd-where:hover, .rd-where[aria-expanded="true"] { border-color:var(--orange,#E8844A); }
.rd-caret { font-size:.7rem; opacity:.7; }
.rd-step { display:flex; align-items:center; gap:.15rem; }
.rd-arrow { width:2.2rem; height:2.2rem; border-radius:50%; border:2px solid var(--warm,#E0CCA8); background:#fff;
            font:400 1.5rem/1 'Fredoka One',cursive; color:var(--ink,#3D2C1E); cursor:pointer; padding:0 0 .15rem; }
.rd-arrow:disabled { opacity:.3; cursor:default; }
.rd-arrow:not(:disabled):active { transform:scale(.92); }
.rd-count { font:400 1rem/1 'Fredoka One',cursive; color:var(--ink,#3D2C1E); min-width:3rem; text-align:center; }
/* Both modes on show, icon and words: the one in use is filled. */
.rd-modes { display:flex; gap:.3rem; }
.rd-mode { font:800 .8rem/1 'Nunito',sans-serif; color:var(--brown,#7a5a3a); background:#fff;
           border:2px solid var(--warm,#E0CCA8); border-radius:999px; padding:.45rem .65rem; cursor:pointer;
           white-space:nowrap; }
.rd-mode.is-on { background:var(--orange,#E8844A); border-color:var(--orange,#E8844A); color:#fff; }

.rd-rail { display:flex; flex-wrap:wrap; justify-content:center; gap:.3rem .5rem; padding:.45rem .55rem .55rem;
           border-top:2px solid var(--warm,#E0CCA8); }
.rd-w { font:400 1.15rem/1 'Fredoka One',cursive; letter-spacing:.04em; color:var(--brown,#7a5a3a);
        background:transparent; border:2px solid transparent; border-radius:12px; padding:.4rem .6rem;
        min-width:2.8rem; cursor:pointer; }
.rd-w:hover { background:#fff; }
.rd-w.is-on { background:var(--orange,#E8844A); border-color:var(--orange,#E8844A); color:#fff; }
.rd-w.is-star { color:var(--orange,#E8844A); font-size:1.4rem; line-height:1; padding:.3rem .5rem; }   /* a payoff's emoji */
.rd-w.is-star.is-on { color:#fff; }
.rd-where:focus-visible, .rd-arrow:focus-visible, .rd-w:focus-visible { outline:3px solid var(--ink,#3D2C1E); outline-offset:2px; }

/* The dropdowns: the level, and that level's units. Rare, so they are menus and not rows. */
.rd-menu { position:absolute; z-index:6; top:2.9rem; left:.5rem; min-width:12rem; max-width:calc(100% - 1rem);
           background:#fff; border:2px solid var(--warm,#E0CCA8); border-radius:14px; padding:.4rem;
           box-shadow:0 8px 24px rgba(61,44,30,.14); }
.rd-menu[hidden] { display:none; }
.rd-opt { display:flex; align-items:center; gap:.35rem; width:100%; text-align:left; background:transparent;
          border:none; border-radius:10px; padding:.55rem .55rem; font:700 .95rem/1.1 'Nunito',sans-serif;
          color:var(--ink,#3D2C1E); cursor:pointer; white-space:nowrap; }
.rd-opt:hover { background:var(--cream,#FDF6E9); }
.rd-opt.is-on { color:var(--orange,#E8844A); }
.rd-tick { width:1rem; display:inline-block; }
.rd-opt:focus-visible, .rd-mode:focus-visible { outline:3px solid var(--ink,#3D2C1E); outline-offset:2px; }

.rd-area { width:100%; min-height:18rem; display:flex; flex-direction:column; align-items:center;
           justify-content:center; }

/* ── THE ONE SCREEN. The word and its bar stay; under them one place of fixed height takes the
   microphone, then the picture, so nothing arriving there moves the word. ── */
.rd-blend { width:100%; display:flex; flex-direction:column; align-items:center; }
.rd-below { width:100%; min-height:14rem; display:flex; flex-direction:column; align-items:center;
            justify-content:flex-start; }
.rd-mic-row[hidden] { display:none !important; }
.rd-blend.is-saying .nzb-bar, .rd-blend.is-saying .nzb-next { display:none; }
/* PAST THE FREE LINE (read.js locked()): the slider frozen inside lock-ui's dashed outline, and
   lock-ui's "Premium" chip under it, larger - here it is the only thing on the screen to tap. */
.rd-blend.is-locked .nzb-bar { outline:1.5px dashed var(--warm, #E0CCA8); outline-offset:7px;
                               border-radius:13px; opacity:.55; cursor:pointer; }
.rd-blend.is-locked .nzb-next { display:none; }
.rd-next.is-wait { visibility:hidden; animation:none; }
.rd-below.is-side { flex-direction:row; align-items:center; justify-content:center; gap:1rem; }
.rd-below.is-side .rd-next { margin-top:0; }
/* Next, under a story scene: the child decides when the play is over (read.js waitNext). */
.rd-next { margin-top:.9rem; font-size:1.15rem; padding:.6rem 1.8rem; background:var(--orange, #E8844A); color:#fff;
           box-shadow:0 4px 0 #b85e28; touch-action:manipulation; animation:rdPop .45s cubic-bezier(.3,1.4,.5,1) both; }
.rd-lock { margin-top:1.4rem; border:1.5px solid var(--ink, #3D2C1E); border-radius:999px;
           background:var(--kraft, #EFE3C8); color:var(--ink, #3D2C1E);
           font-family:'Fredoka One', cursive; font-size:1.1rem; padding:.45rem 1.1rem; cursor:pointer;
           touch-action:manipulation; }
.rd-pic { width:min(40vw, 13rem); height:min(40vw, 13rem); object-fit:contain;
          animation:rdPop .55s cubic-bezier(.3,1.4,.5,1) both; }
.rd-pic.is-scene { width:min(80vw, 24rem); height:auto; max-height:16rem; border-radius:18px;
                   animation:rdBoom .8s cubic-bezier(.2,1.25,.4,1) both; }

/* An item with no sound pack: its print, where the word goes. */
.rd-word { font-family:'Fredoka One',cursive; font-size:clamp(4rem, 16vw, 7rem); line-height:1;
           letter-spacing:.06em; color:var(--ink,#3D2C1E); text-align:center; }
.rd-word.is-phrase { font-size:clamp(2.4rem, 9vw, 4.6rem); letter-spacing:.03em; }
.rd-mic-slot:empty { display:none; }

@keyframes rdPop  { from { opacity:0; transform:scale(.4); } to { opacity:1; transform:none; } }

@keyframes rdBoom { 0% { opacity:0; transform:scale(.2) rotate(-6deg); } 70% { opacity:1; transform:scale(1.06) rotate(1deg); }
                    100% { opacity:1; transform:none; } }
/* A scene is dragged, not scrolled: a finger on it never scrolls the page (founder, 2026-10-10:
   "dragging chars in animation and scroll thing leads to odd behaviour"). The browser started a
   scroll and cancelled the drag half way. The rest of the page still scrolls, so nothing on a
   short screen is out of reach. */
.rd-story, .rd-story svg, .rd-fx { touch-action:none; -webkit-user-select:none; user-select:none; }
/* The letter effect (read.js letterFx, doc 11): a word with no scene and no picture. */
.rd-pic.rd-fx { width:auto; height:auto; position:relative; display:flex; justify-content:center; gap:.06em;
                padding:.5em .3em .2em; font-family:'Fredoka One',cursive; font-size:clamp(3.6rem, 13vw, 6rem);
                line-height:1; color:#E8833A; animation:rdFxSettle .3s ease-in-out var(--settle, 600ms) both; }
.rd-fx-l { display:inline-block; cursor:pointer; transform-origin:50% 100%; touch-action:manipulation;
           animation:rdFxHop .38s ease-out both; }
.rd-fx-l.is-again { animation:rdFxHop .38s ease-out both; animation-name:rdFxHop2; }
.rd-fx-s { position:absolute; left:50%; top:55%; font-style:normal; font-size:1.4rem; color:#F5B83D;
           pointer-events:none; opacity:0; animation:rdFxSpark .7s ease-out both; }
@keyframes rdFxHop  { 0% { transform:none; } 45% { transform:translateY(-14px); } 75% { transform:scale(1.12,.86); } 100% { transform:none; } }
@keyframes rdFxHop2 { 0% { transform:none; } 45% { transform:translateY(-14px); } 75% { transform:scale(1.12,.86); } 100% { transform:none; } }
@keyframes rdFxSettle { 0% { transform:none; } 50% { transform:scale(1.12, .9); } 100% { transform:none; } }
@keyframes rdFxSpark { 0% { opacity:0; transform:translate(-50%,-50%) scale(.3); } 25% { opacity:1; }
                       100% { opacity:0; transform:translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) scale(1); } }
@media (prefers-reduced-motion:reduce) { .rd-pic, .rd-fx-l, .rd-fx-s { animation:none; } }

/* The (i) on a mode chip — the same box ABC Lessons shows. */
#readHud { position:relative; }
.rd-hint { position:absolute; z-index:5; top:2.9rem; right:.5rem; max-width:16rem; background:var(--ink); color:var(--cream);
           font-size:.8rem; line-height:1.35; padding:.45rem .65rem; border-radius:10px;
           opacity:0; pointer-events:none; transition:opacity .18s ease; }
.rd-hint.is-in { opacity:1; }

/* Replay a story scene: a round button at the scene's top right, shown once it has played (read.js). */
.rd-story { position:relative; }
.rd-replay { position:absolute; top:.6rem; right:.6rem; z-index:2; width:3rem; height:3rem; border-radius:50%;
  border:none; background:rgba(255,255,255,.92); color:var(--brown, #5a3e2b); font:700 1.6rem/1 system-ui, sans-serif;
  box-shadow:0 2px 8px rgba(0,0,0,.18); cursor:pointer; touch-action:manipulation; display:flex; align-items:center; justify-content:center; }
.rd-replay[hidden] { display:none; }
.rd-replay:focus-visible { outline:3px solid var(--ink, #333); outline-offset:2px; }

/* ==== player.css ==== */
/* ════════════════════════════════════════════════════════════
   THE PLAYER

   One component, and the only thing between the browse strip and the game. Everything about it is
   answering one question — how does a session of three games announce itself without taking the
   screen away from the game?

   ── WHAT IT BORROWS, AND WHAT IT CANNOT ─────────────────────────────────────────
   Spotify and Netflix hand us the grammar: a collection, a queue, and something showing which item
   is running. What they do NOT have to solve is that their content has no interaction surface —
   a song is audio, so its player can occupy the bottom of the screen forever. Our content is the
   game, and it is the thing a toddler touches. So this is a thin header, never a bottom bar
   (10_numzi_edutainment.md addendum §10).

   ── IT IS ONE ROW ───────────────────────────────────────────────────────────────
   Everything is inside the queue. There is no line above it: the collection is named by the lit
   capsule in the browse strip, the position is the playing capsule's own second line, and the age
   asked for a row and earned none — a number of months is a poor proxy for what a child knows, and a
   control implying otherwise does not belong above a game. It still picks the three games; the tabs
   on /games set it.

   ── THE STATE IS SHOWN, NOT LABELLED ────────────────────────────────────────────
   No "NOW PLAYING" text anywhere. Every real player uses a mark — an equalizer, a triangle, a pulse
   — because the words are redundant beside the item they describe. Ours is a ball, bouncing.

   That is not decoration: the words were what broke the last version. "NOW PLAYING · See how it
   helps your toddler" beside a game name made the playing capsule 600px wide, three times its
   neighbours, sized by a sentence rather than by the game. THE RULE THIS FILE KEEPS: nothing inside
   a capsule may be wider than the game's name.

   ── THE EDUTAINMENT PART ────────────────────────────────────────────────────────
   The one thing streaming has no equivalent of is the learning, and it sits inside the playing
   capsule as a quiet link. Before play it must stay quiet — the parent's question here is "can we
   just play?" and the answer to "what did that do?" belongs after the game, on the receipt, where it
   is automatic and prominent (§15). So: a link, short enough not to set the capsule's width, and a
   panel that opens under the queue rather than pushing the game down permanently.
   ════════════════════════════════════════════════════════════ */

/* ── THE SHELL ──────────────────────────────────────────────────────────────────
   Grouped by a tint rather than a border: one surface, not a card. It hugs its content and centres,
   so on a desktop it is as wide as the queue and no wider — a full-width strip would read as a
   toolbar, which is the dashboard feeling this product is trying not to have. */
.nz-player {
  width: fit-content; max-width: 100%;
  margin: 0 auto .45rem; padding: .26rem .5rem .3rem;
  box-sizing: border-box;
  background: rgba(224, 204, 168, .2);
  border-radius: 18px;
  display: flex; flex-direction: column; gap: .1rem;
}
/* Outside Today only the queue shows, and a tint around a lone row of pills reads as a container
   with nothing in it. */
/* WAS .is-today. Every subject is a playlist now, so the class marks a SESSION rather than one
   particular list — and a name that says "today" on the Language tab is a trap for whoever reads it
   next. The :not() branch is kept: the strip renders before a session is entered, and for that one
   frame it is a bare row of capsules. */
.nz-player:not(.is-session) { background: none; padding: 0; }
.nz-player .gp-wrap { margin: 0; }

.pl-helps[hidden] { display: none; }

/* ── THE POSITION ───────────────────────────────────────────────────────────────
   "2 of 3", on the playing capsule's second line. It had a row of its own with a collection name in
   front of it and an age select beside it; both were redundant (see the header). Kept faint and
   tabular-ish so a changing digit does not shift the line beside it. */
.gp-now-at  { font-variant-numeric: tabular-nums; opacity: .78; }
.gp-now-dot { margin: 0 .3rem; opacity: .45; }

/* ── THE PLAYING CAPSULE ────────────────────────────────────────────────────────
   Two lines: the game, and the one question worth asking about it. THE GAME'S NAME SETS THE WIDTH —
   "How it helps" is shorter than every game title we have, which is the whole reason it is phrased
   that way rather than "See how it helps your toddler" (that sentence lives on the panel it opens,
   where there is room for it). */
.gp-cap.is-now {
  flex-direction: column; align-items: flex-start;
  gap: 0; padding: .26rem .7rem .28rem;
  cursor: default;
}
.gp-cap-head { display: inline-flex; align-items: center; gap: .3rem; }
.gp-cap-now  { display: inline-flex; align-items: center; margin-left: 1.15rem; }

/* ── THE MARK: A BALL, BOUNCING ─────────────────────────────────────────────────
   THE TRIANGLE WAS WRONG, and wrong in a way worth recording. ▶ is the control you press to START
   something — on every remote, every video, every player, it is the button shown while a thing is
   STOPPED. Pause is what a running player shows. So a triangle beside the game that is already open
   read as "waiting to be played", which is the opposite of what this mark exists to say. Breathing it
   did not fix that; a pulsing Play button reads as "press me", if anything more strongly.

   A ball, then, and it bounces. Motion is the only honest way to say "this is happening" without
   words, and a ball is the one object every toddler in the world already owns — so the mark belongs
   to this category rather than being borrowed from a music app. It squashes when it lands and
   stretches as it leaves, which is what separates it from a loading dot: loading dots never deform.

   Considered and rejected: PAUSE (⏸), correct in player grammar but it reads as "paused" to everyone
   who is not thinking about player grammar, and it is a control, so a parent would tap it; an
   EQUALIZER, which is a sound meter and belongs to music — our content is a child using their hands;
   a LIVE PING, which implies a broadcast we are not running; a SPROUT, which already means "session
   finished" on the last receipt; and TWO DOTS TAKING TURNS, the most Numzi-native idea of the lot —
   a grown-up and a child alternating — but two pulsing dots read as LOADING as readily as playing.

   It collides with nothing else in the product's language: ⭐ is a reward, ✓ is played, ○ is upcoming,
   🔒 is Premium, 🌱 is a finished session. Drawn in CSS, in currentColor, so it stays monochrome and
   scales with the text — U+25B6 and ⏸ both render as colour emoji on iOS, which is the other reason
   neither could have stayed. */
/* THE BOX IS TALLER THAN THE BALL BECAUSE THE HOP LIVES IN THE DIFFERENCE. At .42rem the ball
   travelled .42rem — 7px of arc, which photographs as a bullet point and reads as one. The box is
   now 1rem tall, the ball .44rem, and the arc is 130% of the ball, so it clears its own height and
   the motion is legible at a glance without the mark taking any more width. */
.gp-play {
  position: relative; display: inline-block;
  width: .62rem; height: 1rem; flex: 0 0 auto;
}
.gp-play::before {
  content: ''; position: absolute; left: 50%; bottom: 0;
  width: .44rem; height: .44rem; margin-left: -.22rem;
  border-radius: 50%; background: currentColor;
  transform-origin: 50% 100%;
  animation: gp-bounce 1.05s infinite;
}
/* The easing is the animation, not the keyframes: ease-out leaving the ground, ease-in coming back,
   so it hangs at the top and accelerates down the way a ball does. A single ease-in-out across the
   whole cycle floats instead, and a floating ball reads as a balloon. */
@keyframes gp-bounce {
  0%   { animation-timing-function: cubic-bezier(.16, .84, .38, 1);
         transform: translateY(0) scale(1.24, .76); }
  16%  { animation-timing-function: cubic-bezier(.3, .62, .42, 1);
         transform: translateY(-58%) scale(.93, 1.09); }
  50%  { animation-timing-function: cubic-bezier(.62, .1, .88, .46);
         transform: translateY(-130%) scale(1, 1); }
  88%  { animation-timing-function: cubic-bezier(.7, 0, .95, .5);
         transform: translateY(-18%) scale(.93, 1.09); }
  100% { transform: translateY(0) scale(1.24, .76); }
}
/* A still ball is still a ball, and it is still not a button — which is more than the triangle
   managed. */
@media (prefers-reduced-motion: reduce) {
  .gp-play::before { animation: none; transform: none; }
}

/* The "how it helps" link and its panel moved to assets/css/foundations.css and lost the td-
   prefix on the way — they are .nz-helps* now. They were named for today.js because it was the
   only caller; My Home lists the same foundations for the same reason, and a shared thing named
   after one of its callers is the next file somebody duplicates rather than reuses. */

/* ── DESKTOP ────────────────────────────────────────────────────────────────────
   Nothing to rearrange any more — the label and the age that used to take a row of their own on a
   phone are gone, so the phone layout IS the one-band layout. All this does is let the panel sit
   beside nothing and keep the shell from stretching. */
@media (min-width: 900px) {
  .nz-player.is-session { padding: .3rem .7rem; }
}

/* ==== today.css ==== */
/* Today's block, in the slot every game already keeps above its play area. See
   assets/js/today.js for why it replaces that block rather than adding a row, and
   docs/product/today/ for what Today is.

   NOT in game.html's inline styles, deliberately. That file carries inline copies of the per-game
   CSS and does not link abc-game.css or charm-game.css, so a rule added there is dead on the only
   page that matters. This file is linked. */

/* The player — the collection line, the queue, what is playing and the one educational link — is a
   component of its own now: assets/css/player.css. What stays here is the receipt, which is Today's
   other surface and shares nothing with it but a colour. */

/* ── THE TICKED LIST, USED BY THE RECEIPT ───────────────────────────────────────
   A tick and the words, with no pill around them: the flow block draws the same kind of list as
   green capsules, which works there because nothing else in that block is a capsule. Here they
   would sit an inch from the orange Play button and read as more things to press. */
.td-builds {
  display: flex; flex-wrap: wrap; gap: .1rem .8rem;
  margin: .25rem 0 0;
}
/* A FOUNDATION, so it takes the foundation colour — see assets/css/foundations.css. It was --brown,
   which is also the lead line above it, the group headings and most of the card's other text, so the
   one thing a parent is meant to take away carried the same weight as everything around it. */
.td-build {
  display: inline-flex; align-items: baseline; gap: .25rem;
  font-family: 'Nunito', sans-serif; font-weight: 700;
  font-size: clamp(.72rem, 3vw, .82rem);
  color: var(--nz-found, #2f7d4a);
}
.td-build-tick { color: #2f7d4a; font-weight: 900; }
/* ── the buttons, which now live only on the receipt ────────────────────────── */
.td-row {
  display: flex; align-items: center; justify-content: flex-end;
  flex-wrap: wrap; gap: .4rem; margin: .45rem 0 0;
}
.td-btn {
  display: inline-flex; align-items: center; justify-content: center;
  padding: .34rem .9rem; min-height: 2rem;
  font-family: 'Fredoka One', cursive; font-size: clamp(.8rem, 3.3vw, .92rem);
  border: 2px solid var(--warm, #E0CCA8); border-radius: 999px;
  background: #fff; color: var(--ink, #3D2C1E);
  cursor: pointer; text-decoration: none;
}
.td-btn-go {
  background: var(--orange, #E8844A); border-color: #b85e28; color: #fff;
  box-shadow: 0 3px 0 #b85e28;
}
.td-btn-go:hover { filter: brightness(1.04); }
.td-btn-go:active { transform: translateY(2px); box-shadow: none; }
.td-btn:focus { outline: none; }
.td-btn:focus-visible { outline: 3px solid var(--ink, #3D2C1E); outline-offset: 3px; }

/* ── THE PLAY BOX SET ASIDE, WHERE THAT IS NEEDED ──────────────────────────────
   Almost never: every game hands flow.end a host that already sits in its play box, so the summary
   renders there and everything above it stays exactly where it was. This is for the one ending no
   game announces — "Finish today's session", pressed while a game is still mid-play — where the box
   has to be stood down and the card put in its place.

   !important, and not from laziness: several games write display INLINE (nzLoopDone sets the play
   area to display:none and its resume sets it back to ''), and an inline style beats any class. */
.game-section .nz-played-out { display: none !important; }
.td-inplace { width: 100%; }

/* ── THE CARD THAT PLAYS ITSELF ────────────────────────────────────────────────
   "Game completed!" lands large and settles smaller, then the ticks arrive one at a time, then the
   two buttons. A list that assembles itself gets read; the same list appearing at once gets skimmed.

   ENTIRELY DELAYS, NO JS TIMELINE. Every part is rendered in one pass and holds its own start time,
   so there is nothing to cancel and nothing left half-run if a parent taps straight through. And
   because each part is hidden by its own keyframes rather than by display, it holds its place in the
   layout from the first frame — the card cannot grow as things appear, which is what would put the
   jerk back.

       0ms   ⭐ glides in over 1.1s — floats up to size and settles
     300ms   "Game completed!" arrives at 1.4× and settles over 1s
    1300ms   "Your little one practised"
    1400ms   the two buttons and the bar, so nobody is made to wait to act
    1600ms   the first line — and then one line per the time the LAST one takes to
             read, which is 252ms a word plus a fixation. today.js sets each delay
             from the words in that line; see readMs and Brysbaert (2019) there.
*/
/* Inside the game's own box, so it fills the width it was handed and brings its own padding
   rather than borrowing the box's. */
/* THE RECEIPT CARD'S RULES START HERE, and this property is how assets/js/receipt-card.js asks
   whether they arrived. The card is drawn on pages that do not all link this file by name --
   game.html folds it into bundle-game.css -- so looking for a <link> would answer "missing" for a
   page that has the rules and make it fetch them twice. Same trick, same reason, as
   --nz-site-nav in site-nav.css. */
:root { --nz-receipt-card: 1; }

.td-end { text-align: center; width: 100%; box-sizing: border-box; margin: 0 auto; }
/* AND NO SECOND FRAME WHERE THERE IS ALREADY ONE. Five games end inside .game-card, which is itself
   a bordered cream box — the card's own border made two nested rounded rectangles a few pixels
   apart. The games whose ending host is a bare div (Clay, Weather, Who is my Baby, Animal Freeze,
   Tot) keep it, because there it IS the box. */
.game-card .td-end { border: 0; background: none; padding: .2rem .1rem; }

/* IT GLIDES. This was a half-second pop with an overshoot of 1.5 on the curve — a credit-roll
   flourish, over before a parent had looked up. It is the moment the whole session has been building
   to, so it takes its time: 1.1s, floating up to a touch over size and settling, on a curve with no
   bounce in it at all. */
.td-star {
  font-size: clamp(2.4rem, 12vw, 3.4rem); line-height: 1;
  animation: td-glide 1.1s cubic-bezier(.19, .68, .29, 1) both;
}
@keyframes td-glide {
  from { opacity: 0; transform: scale(.62) translateY(10px); }
  65%  { opacity: 1; transform: scale(1.06) translateY(0); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}

/* Scale, not font-size: a transform costs no layout, so the words can start big without pushing
   anything else about while they settle. */
/* And the words settle after it rather than with it, over a full second — 1.4× rather than 1.6×,
   because a smaller distance travelled slowly reads as calm where a large one reads as a stunt. */
.td-end .td-end-head { animation: td-settle 1s cubic-bezier(.19, .68, .29, 1) .30s both; }
@keyframes td-settle {
  from { opacity: 0; transform: scale(1.4); }
  to   { opacity: 1; transform: scale(1); }
}

.td-end .td-end-lbl,
.td-end .td-found-h,
.td-end .td-done-founds .td-build,
.td-end .td-build,
.td-end .td-done-sub,
.td-end .td-end-row,
.td-end .td-done-why { animation: td-line .75s cubic-bezier(.19, .68, .29, 1) both; }
.td-end .td-end-lbl  { animation-delay: 1.30s; }
.td-end .td-done-sub { animation-delay: 1.30s; }
/* The ticks carry their own delay, set per item in today.js: there can be six of them and a stack
   of nth-child rules would have to guess how many. */
.td-end .td-end-row  { animation-delay: 1.40s; }
.td-end .td-done-why { animation-delay: 1.70s; }
/* pointer-events in the keyframes, so a button cannot be pressed before it can be seen. The game
   that just ended had its own controls in this space — a stray tap where "Skip" used to be should
   not spend the session. It is a discrete property, so it flips halfway through the fade; engines
   that do not animate it simply ignore it and nothing else changes. */
/* A LINE ARRIVES, IT DOES NOT SNAP ON. Three quarters of a second and a little further to travel,
   so the eye is drawn to it rather than startled by it — at 340ms these read as a flicker.

   pointer-events in the keyframes, so a button cannot be pressed before it can be seen: the game
   that just ended had its own controls in this space, and a stray tap where "Skip" used to be must
   not spend the session. It is a discrete property, so it flips halfway; engines that do not animate
   it simply ignore it. */
@keyframes td-line {
  from { opacity: 0; transform: translateY(7px); pointer-events: none; }
  to   { opacity: 1; transform: none; pointer-events: auto; }
}

.td-end-lbl {
  font-family: 'Nunito', sans-serif; font-weight: 800;
  font-size: clamp(.68rem, 2.8vw, .76rem);
  letter-spacing: .04em; text-transform: uppercase;
  color: var(--brown, #9B6B4A); opacity: .8;
  margin: .35rem 0 .15rem;
}
/* ── THE LIST ──────────────────────────────────────────────────────────────────
   A list, not a line of chips: each item on its own row so each one lands as its own beat, and left
   aligned because a ragged centre is hard to read down.

   TWO COLUMNS FILL THE FIRST BEFORE THE SECOND, which is what CSS columns do and what was asked
   for: read down the left, then down the right. `break-inside: avoid` keeps an item from being split
   across the fold between them. A phone gets one column whatever the count — two columns of
   "Counts a small group with one number for each object" would be four words a line. */
.td-builds-list {
  display: block; text-align: left;
  margin: .1rem auto 0; max-width: 34rem;
}

/* ── THE TWO FOUNDATIONS, ON THE RECEIPT ───────────────────────────────────────
   Side by side when the card is wide enough, stacked on a phone — the same arrangement the pricing
   card uses for the same two groups, and deliberately so: a parent who has seen the offer should
   recognise the shape of what they are being shown, not have to work out that these are the same two
   things. Left aligned, because a ragged centre is hard to read down a list of short labels. */
.td-founds {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: .2rem 2rem; text-align: left;
  margin: .1rem auto 0; max-width: 34rem;
}
.td-found { flex: 0 1 auto; min-width: 0; }
/* The group's name, quieter than the lead line above it: that one carries "practised", these only
   say which half of the pair this is. Same case and weight as the lead so the three read as one
   block rather than a heading and two sub-headings competing with it. */
.td-found-h {
  /* Brown, deliberately: a heading in the foundation colour would leave the green nothing to
     distinguish. --nz-found-head is the same brown, named so the pair reads as one decision. */
  font-family: 'Nunito', sans-serif; font-weight: 900;
  font-size: clamp(.6rem, 2.5vw, .68rem);
  letter-spacing: .05em; text-transform: uppercase;
  color: var(--nz-found-head, #9B6B4A); opacity: .72;
  margin: .3rem 0 .1rem;
}
.td-found .td-build {
  display: flex; align-items: baseline; gap: .4rem;
  padding: .08rem 0;
}
/* The icon is the family's, and it is the same glyph the pricing card shows for that family. Fixed
   width so a row of labels lines up down the left whatever emoji precede them. */
.td-build-ico {
  flex: 0 0 auto; width: 1.05em; text-align: center;
  font-size: .95em; line-height: 1.3;
}
/* Below 400px the two groups always stack, whatever the flex would otherwise do: two columns of
   four-word labels at that width is two words a line. */
@media (max-width: 399px) {
  .td-founds { flex-direction: column; gap: 0; }
}
.td-builds-list .td-build {
  display: flex; align-items: baseline; gap: .35rem;
  padding: .1rem 0; break-inside: avoid;
}
@media (min-width: 560px) {
  .td-builds-list.is-two-col { columns: 2; column-gap: 1.6rem; }
}

/* ── THE DAY'S ROLL-UP ─────────────────────────────────────────────────────────
   The same two columns as a game's receipt, so the last card of the session is recognisably the same
   object as the two before it — only the grain changes, from what was practised to which foundations
   it belonged to. Its label and rows animate on the same beats. */
.td-done-founds { margin: .15rem 0 .1rem; }
.td-end .td-done-founds .td-end-lbl { animation-delay: 1.30s; }

/* Asked for less movement: everything is simply there. */
@media (prefers-reduced-motion: reduce) {
  .td-star, .td-end .td-end-head, .td-end .td-end-lbl, .td-end .td-found-h, .td-end .td-build,
  .td-end .td-done-sub, .td-end .td-end-row, .td-end .td-done-why { animation: none; }
}

/* ── THE END OF A GAME'S FREE RUN, IN A TODAY SESSION ───────────────────────────
   Rendered where the game's own end card would have been — in its stage, replacing the round, the
   way every ending on this page already works. Headline and two buttons: on to the next of today's
   three, or the membership. Nothing else, because nothing else is a question a parent has here. */
.td-end-head {
  font-family: 'Fredoka One', cursive; font-size: clamp(1rem, 4.2vw, 1.22rem);
  color: var(--ink, #3D2C1E); margin: 0 0 .1rem;
}
/* EQUAL WEIGHT, as asked: same height, same share of the row, side by side where there is room and
   stacked where there is not. Play next leads because it is what a parent has come for. */
.td-end .td-row { justify-content: center; }
.td-end .td-btn { flex: 1 1 11rem; justify-content: center; text-align: center; }

/* ── THE WAIT, MADE VISIBLE ──────────────────────────────────────────────────────
   A bar rather than a number: a countdown in figures reads as pressure, and this is only saying that
   the session will carry on if it is left to. Its duration is set inline by today.js — ten seconds,
   or however long the list needs, whichever is longer — so the bar and the timer are the same thing
   rather than two things that agree by luck. */
.td-tick { margin: .55rem 0 0; }
.td-tick i {
  display: block; height: 4px; width: 0;
  background: var(--orange, #E8844A); border-radius: 4px;
  animation: td-fill 10s linear both;
}
@keyframes td-fill { from { width: 0 } to { width: 100% } }
.td-tick-lbl {
  display: block; margin-top: .2rem;
  font-family: 'Nunito', sans-serif; font-weight: 700;
  font-size: clamp(.66rem, 2.7vw, .74rem);
  color: var(--brown, #9B6B4A);
}
/* Held, because they pressed Subscribe: the bar stops claiming anything. */
.td-block.is-held .td-tick { display: none; }
@media (prefers-reduced-motion: reduce) {
  .td-tick i { animation: none; width: 100%; }
}

/* ── the session card ────────────────────────────────────────────────────────────
   Rendered in this same slot, with the game left playable underneath: a parent reads this while a
   child carries on, which is the register user_exp_v1.md argues for — the account is offered after
   value, as "save this and get tomorrow's", never as a gate in front of playing. */
.td-done { text-align: center; }
.td-done-head {
  font-family: 'Fredoka One', cursive; font-size: clamp(1rem, 4.2vw, 1.2rem);
  color: var(--ink, #3D2C1E); margin: 0;
}
.td-done-sub {
  font-family: 'Nunito', sans-serif; font-weight: 700;
  font-size: clamp(.76rem, 3.1vw, .88rem);
  color: var(--brown, #9B6B4A); margin: .25rem 0 0;
}
.td-done-row { justify-content: center; }
.td-done-why {
  font-family: 'Nunito', sans-serif; font-weight: 700;
  font-size: clamp(.68rem, 2.8vw, .76rem);
  color: var(--brown, #9B6B4A); margin: .3rem 0 0;
}

/* ── the tick on a finished capsule ──────────────────────────────────────────────
   Lives here rather than in game-picker.css because it means something only while Today is
   running: the three capsules are the session's progress strip, which is why the session adds no
   row of its own. */
/* ── THE PLAYLIST'S THREE STATES ────────────────────────────────────────────────
   ● current is the strongest thing above the game; ✓ played is deliberately QUIETER than current,
   because it means "played in today's session" and must never read as mastery; ○ upcoming is
   neutral and present because three pills are the proposition. Only Today's row wears them. */
.gp-cap-dot { margin-right: .28rem; font-weight: 900; font-size: .82em; line-height: 1; }
.gp-cap.is-on .gp-cap-dot { color: #fff; opacity: .9; }
.gp-cap.is-done { opacity: .72; border-style: dashed; }
.gp-cap.is-done .gp-cap-dot { color: #2f7d4a; }
.gp-cap.is-next .gp-cap-dot { color: var(--warm, #E0CCA8); }

/* ==== game-picker.css ==== */
/* The strip of games to choose between. See assets/js/game-picker.js for why it exists and why
   it is one scrolling line rather than a wrapping block. */

.gp-wrap[hidden], .ss-wrap[hidden] { display: none; }

/* ── BOTH ROWS HUG THEIR CONTENT AND CENTRE ───────────────────────────────────
   body is a flex column with no content column of its own, so a stretched child runs the full
   width of the window. On a desktop that gave the subject control a 1500px pill with its nine
   capsules crowded into the left third, and left the three game capsules alone against the left
   edge while every other thing on the page — the block above the game, the card, the level strip —
   sits centred. It read as two stray toolbars.

   fit-content rather than a max-width, so there is no number to be wrong: on a wide screen each box
   is exactly as wide as its capsules and centred under the page; on a phone `max-width: 100%` caps
   it at the viewport and it scrolls as before. The .lv-wrap bug is not repeatable here BECAUSE of
   that cap — a fit-content box with nothing stopping it is what put that strip 60px off-screen. */
.ss-wrap, .gp-wrap { width: fit-content; max-width: 100%; margin-inline: auto; }

/* ONE ROW, SCROLLED, NEVER WRAPPED. Seven Language capsules wrap to three rows on a phone, which
   is the exact fault that cost /game its first screen when it had ten tabs, and /games its first
   card when it had eight subject capsules. A row that scrolls sideways costs one row of height
   however long the list is, and game-picker.js scrolls the selected capsule into view so the
   current game is never the one off the right edge. */
.gp-row {
  display: flex; gap: .4rem;
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity;
  /* ── THE SNAP HAS TO KNOW ABOUT THE PEEK ──────────────────────────────────
     game-picker.js positions the playing item one peek in from the left edge, so a sliver of the
     item before it shows and the capsule cannot read as truncated. Without this, `scroll-snap-align:
     start` on the capsules then snapped that item flush to the edge again and ate the peek —
     measured: a computed target of 181px landing at 207px. scroll-padding moves where "start" IS,
     which is the mechanism meant for exactly this, and it keeps a thumb-flick landing in the same
     place as a tap. The 26px matches PEEK in game-picker.js; change both together. */
  scroll-padding-inline-start: 26px;
  padding: 0 .1rem .45rem;
  scrollbar-width: none;
}
.gp-row::-webkit-scrollbar { display: none; }

/* ── SUBJECTS ARE ONE CONTROL; GAMES ARE SEPARATE THINGS ──────────────────────
   Two scrolling rows of identical capsules read as one block, and a parent cannot tell which
   axis they are moving along — every unselected capsule in both rows was the same cream pill, so
   only the two lit ones differed at all.

   So the subject row is now a single segmented control: one border, one radius, capsules butted
   together with hairline dividers and no gaps. "Pick one of these." The game row is unchanged —
   separate floating pills. "Here are the things." The difference is structural rather than
   decorative, so it survives at any size and in any colour.

   THE BORDER AND RADIUS LIVE ON THE SCROLLING BOX, not on the row inside it. Nine subjects do
   not fit 360px, so this has to scroll like everything else — and a joined control whose border
   scrolls with its content has its rounded ends sliced off the moment a parent moves it. On the
   box, the ends stay welded to the control and the capsules travel inside. */
.ss-wrap {
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  border: 2px solid var(--warm, #E0CCA8); border-radius: 50px;
  background: var(--cream, #FDF6E9);
  margin-bottom: .4rem;
}
.ss-wrap::-webkit-scrollbar { display: none; }
/* Centred inside the control, which only matters when the capsules are narrower than the box — on
   a phone `width: max-content` exceeds it and there is no free space to distribute, so this cannot
   clip an overflowing row the way centring a scroll container does. */
.ss-row { display: flex; gap: 0; width: max-content; min-width: 100%; justify-content: center; }

/* Deliberately the same capsule as the subject filters on /games: a parent who picked "Language"
   there and lands here should recognise the control, not learn a second one. */
.gp-cap,
.ss-cap {
  flex: 0 0 auto; scroll-snap-align: start;
  display: inline-flex; align-items: center; gap: .3rem;
  font-family: 'Fredoka One', cursive; font-weight: normal;
  font-size: clamp(.72rem, 3.2vw, .82rem); line-height: 1.2;
  padding: .32rem .7rem;
  color: var(--ink, #3D2C1E);
  cursor: pointer; white-space: nowrap;
  transition: background .15s, border-color .15s, color .15s;
}
/* The game capsule keeps its own outline — it is a thing, not a segment. */
.gp-cap {
  border: 2px solid var(--warm, #E0CCA8); border-radius: 50px;
  background: var(--cream, #FDF6E9);
}
/* The subject capsule has none: the control it sits in owns the edge, and a hairline separates
   one from the next. Slightly taller so the joined row still reads as a control rather than as
   text, and the last one drops its divider so the run ends cleanly. */
.ss-cap {
  border: 0; background: none; border-radius: 0;
  padding: .42rem .75rem;
  border-right: 1px solid var(--warm, #E0CCA8);
}
.ss-cap:last-child { border-right: 0; }
.gp-cap:hover { border-color: var(--orange, #E8844A); }
.ss-cap:not(.is-dead):not(.is-on):hover { background: var(--kraft, #EFE3C8); }
/* ── TWO ROWS, TWO COLOURS, ON PURPOSE ────────────────────────────────────────
   Stacked and identical, the subject row and the game row read as one two-line block and a
   parent cannot tell which axis they are moving along. The SUBJECT takes the orange, because it
   is the same orange /games uses for its subject filters and the axis a parent thinks in; the
   GAME takes the ink, so the pair reads as "this subject, this game" rather than as eight things
   and ten things. */
.ss-cap.is-on {
  background: var(--orange, #E8844A); color: #fff;
  border-right-color: var(--orange, #E8844A);
}
.gp-cap.is-on {
  background: var(--ink, #3D2C1E); border-color: var(--ink, #3D2C1E); color: var(--cream, #FDF6E9);
  box-shadow: 0 3px 0 #241a11;
}
.gp-cap:active { transform: translateY(2px); box-shadow: none; }
.gp-cap:focus, .ss-cap:focus { outline: none; }
.gp-cap:focus-visible { outline: 3px solid var(--ink, #3D2C1E); outline-offset: 2px; }
/* Inset, because an outset ring on a segment is clipped by the control that contains it. */
.ss-cap:focus-visible { outline: 3px solid var(--ink, #3D2C1E); outline-offset: -3px; }
.gp-cap-ico, .ss-cap-ico { font-size: .95em; line-height: 1; }

/* The count is the point of a subject capsule: a parent should know Language holds seven games
   before tapping it, not after. Same badge /games puts on its filters. */
.ss-cap-n {
  font-family: 'Nunito', sans-serif; font-size: .68rem; font-weight: 900;
  padding: 0 .3rem; border-radius: 50px;
  background: rgba(61, 44, 30, .1);
}
.ss-cap.is-on .ss-cap-n { background: rgba(255, 255, 255, .28); }

/* Social & Emotional: present, so the curriculum reads as the eight things it is, but plainly
   not a door. See the note in assets/js/subject-strip.js. */
.ss-cap.is-dead { opacity: .42; cursor: default; }

/* ==== game-bar.css ==== */
/* The /game play bar. See assets/js/game-bar.js for why it looks like this.

   The whole bar is 60px and cannot wrap at any width — the ten tabs it replaced took 249px
   at 500px wide and eight rows on a real 360px phone. Nothing here is tuned to a breakpoint;
   it is built so wrapping is impossible instead. */

/* ── PER-GAME ACCENT ───────────────────────────────────────────────────────────
   These were six scattered .game-tab[data-game=X].active rules inside game.html. The bar
   carries data-game, so each game keeps the colour its tab had. Games with no entry
   (shapes, colours, parade, abc) fall through to orange, exactly as before. */
.game-switch                      { --gs-accent: var(--orange); --gs-shadow: #b85e28; }
.game-switch[data-game="clay"]    { --gs-accent: #7BBF5E; --gs-shadow: #4e9e35; }
.game-switch[data-game="animal"]  { --gs-accent: #E87B7B; --gs-shadow: #a84040; }
.game-switch[data-game="weather"] { --gs-accent: #7BBFDB; --gs-shadow: #4fa3c7; }
.game-switch[data-game="baby"]    { --gs-accent: #F06292; --gs-shadow: #ad3368; }
.game-switch[data-game="tot"]     { --gs-accent: #B59FD6; --gs-shadow: #7a5fa8; }

.game-switch {
  display: flex; justify-content: center;
  padding: .8rem 1rem .1rem;
}
.gs-bar {
  display: flex; align-items: center; gap: .5rem;
  width: 100%; max-width: 430px;
  font-family: 'Fredoka One', cursive; font-size: 1.05rem;
  padding: .42rem .45rem .42rem 1rem;
  border-radius: 50px;
  border: 2.5px solid var(--gs-accent); background: var(--gs-accent); color: #fff;
  box-shadow: 0 4px 0 var(--gs-shadow);
}

/* The game's name is identity, not a control — there is nothing to tap here, which is why
   the only tappable thing on the bar is unambiguous. */
.gs-now { display: flex; align-items: center; gap: .45rem; flex: 1 1 auto; min-width: 0; }
.gs-icon { flex: 0 0 auto; }
/* "Animal Freeze Detective" is the longest name and fits at 360px. Ellipsis is the guard
   against a longer one, so the bar can never wrap and bring the old problem back. */
.gs-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── THE WAY OUT ───────────────────────────────────────────────────────────────
   Solid cream on a saturated bar, so it reads as the one thing to press. This used to be a
   translucent white chip and it disappeared into the bar; contrast is the entire fix. */
.gs-all {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: .3rem;
  font-size: .84rem; padding: .38rem .7rem .38rem .75rem;
  background: var(--cream, #FDF6E9); color: var(--ink, #3D2C1E);
  border-radius: 50px; text-decoration: none;
  box-shadow: 0 2px 0 rgba(61, 44, 30, .22);
  white-space: nowrap;
  transition: transform .12s ease;
}
.gs-all:hover { background: #fff; }
.gs-all:active { transform: translateY(1px); box-shadow: none; }
.gs-all:focus { outline: none; }
.gs-all:focus-visible { outline: 3px solid var(--ink, #3D2C1E); outline-offset: 2px; }
.gs-chev { font-size: .95rem; line-height: 1; opacity: .55; }

@media (max-width: 380px) {
  .gs-bar { font-size: .96rem; padding-left: .8rem; }
  .gs-all { font-size: .78rem; padding: .34rem .6rem; }
}

/* ==== game-flow.css ==== */
/* The top block and the end-of-run block on /game. See assets/js/game-flow.js for why each
   exists and why the CTAs are weighted the way they are. */

/* ── ONE BLOCK, TWO MOMENTS ───────────────────────────────────────────────────
   The same component before the game and when a free run ends.

   EVERY MEASUREMENT HERE IS HEIGHT THE GAME DOES NOT GET. On a 360x780 phone this block, the
   title, the score bar and the level row together pushed the charms off the screen, so a parent
   arriving from an ad met a pitch and no game. It is now a strip: a small label top-left, both
   chips on one line, a rule, and the routes. */
.nz-flow {
  background: var(--cream); border: 2px solid var(--warm); border-radius: 14px;
  padding: .4rem .55rem .45rem;
  max-width: 30rem; margin: 0 auto;
}
.nz-flow.is-end {
  background: none; border: 0; padding: .8rem .4rem 1rem;
  max-width: none; text-align: center;
}

.nz-flow-stars { font-size: 1.3rem; letter-spacing: .1rem; line-height: 1; }
.nz-flow-head {
  font-family: 'Fredoka One', cursive; font-size: 1.2rem;
  color: var(--ink); margin-top: .3rem;
}
.nz-flow-lbl {
  font-family: 'Nunito', sans-serif; font-size: .6rem; font-weight: 900;
  letter-spacing: .05em; text-transform: uppercase; opacity: .5;
}
.nz-flow.is-end .nz-flow-lbl { text-align: center; }

/* Both chips on one line. They are the reason a parent would press anything below, so they stay
   the loudest thing here — but stacked they cost two rows the game needs. */
.nz-flow-said {
  display: flex; flex-wrap: wrap; gap: .3rem;
  margin-top: .25rem;
}
.nz-flow.is-end .nz-flow-said { justify-content: center; }
.nz-flow-chip {
  display: inline-flex; align-items: center; gap: .22rem;
  font-family: 'Fredoka One', cursive;
  font-size: clamp(.6rem, 2.9vw, .8rem); line-height: 1.2;
  padding: .16rem .45rem; border-radius: 50px;
  background: #e4f2e6; color: #226b3c; border: 1.5px solid #bfe0c8;
  white-space: nowrap;
}
/* ── THE TWO FOUNDATION LINES ───────────────────────────────────────────────────
   One line per group rather than a column each: this card is the compact one and has its own buttons
   to leave room for. The heading sits inline with its chips for the same reason — a heading on its own
   row would cost two more rows for four words.

   THE CHIPS KEEP THEIR GREEN PILL, which they already had before the foundations arrived, and which
   happens to be the same argument the receipt's green makes: these are the thing to take away from
   the card, and everything else on it is chrome. */
.nz-flow-founds {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: .28rem; margin-top: .25rem;
}
.nz-flow.is-end .nz-flow-founds { justify-content: center; }
.nz-flow-found-h {
  font-family: 'Nunito', sans-serif; font-weight: 900;
  font-size: clamp(.52rem, 2.3vw, .6rem);
  letter-spacing: .05em; text-transform: uppercase;
  color: var(--brown, #9B6B4A); opacity: .75;
  margin-right: .1rem;
}
.nz-flow-found-ico { font-size: .88em; line-height: 1; }

/* .nz-flow-tick went with the ✓ list it belonged to — see game-flow.js. */

.nz-flow-rule {
  border: 0; border-top: 1.5px solid var(--warm);
  margin: .4rem 0;
}

/* See all games leads: breadth is what the subscription is bought on, and a parent who has just
   watched one game work is asking what else there is. Play next game is the lighter of the two. */
.nz-flow-row {
  display: grid; grid-template-columns: 1fr 1fr; gap: .4rem;
  max-width: 26rem; margin: 0 auto;
}
.nz-flow.is-end .nz-flow-row { margin-top: .2rem; }
.nz-flow-row.is-single { grid-template-columns: 1fr; max-width: 15rem; }
.nz-flow-btn, .nz-flow-all {
  display: flex; align-items: center; justify-content: center; text-align: center;
  font-family: 'Fredoka One', cursive;
  font-size: clamp(.72rem, 3.4vw, .88rem); line-height: 1.15;
  padding: .42rem .5rem; border-radius: 12px;
  border: 2.5px solid var(--ink); cursor: pointer; text-decoration: none;
}
.nz-flow-all { background: var(--orange); color: #fff; box-shadow: 0 3px 0 #b85e28; }
.nz-flow-all:hover { background: #d4713a; }
.nz-flow-next, .nz-flow-keep {
  background: #fff; color: var(--ink); box-shadow: 0 3px 0 var(--warm);
}
.nz-flow-next:hover, .nz-flow-keep:hover { background: var(--cream); }
.nz-flow-btn:active, .nz-flow-all:active { transform: translateY(2px); box-shadow: none; }

/* The offer is an underlined line of text, not a third button. It is the quietest route on
   purpose — it opens the membership sheet, where the price and the promises live. */
.nz-flow-rest { text-align: center; margin-top: .35rem; }
.nz-flow-unlock {
  background: none; border: 0; padding: 0;
  font-family: 'Nunito', sans-serif; font-size: .72rem; font-weight: 900;
  color: var(--ink); opacity: .65; cursor: pointer;
  text-decoration: underline; text-underline-offset: 2px;
}
.nz-flow-unlock:hover { opacity: 1; }
.nz-flow.is-end .nz-flow-rest { display: flex; flex-direction: column; align-items: center; gap: .45rem; }
.nz-flow.is-end .nz-flow-all { max-width: 15rem; width: 100%; }

.nz-flow-btn:focus, .nz-flow-unlock:focus, .nz-flow-all:focus { outline: none; }
.nz-flow-btn:focus-visible, .nz-flow-unlock:focus-visible, .nz-flow-all:focus-visible {
  outline: 3px solid var(--ink); outline-offset: 3px;
}

/* The end block replaces the round rather than joining it. Hidden, not cleared: buildRound()
   writes into #itemsStage, #choices and #feedback by id, so they have to survive for play to
   resume when a parent taps Keep playing. */
.game-section.is-ended .question-label,
.game-section.is-ended .feedback,
.game-section.is-ended .choices,
.game-section.is-ended .stars,
.game-section.is-ended .btn-row { display: none; }


/* The Next button is two deliberate lines — a small label and the game — because
   "Next: 📖 Tot Reads & Writes →" wrapped mid-name and read as broken text rather than a choice. */
.nz-flow-next { flex-direction: column; gap: .1rem; padding: .5rem .5rem .55rem; }
.nz-flow-next-lbl {
  font-family: 'Nunito', sans-serif; font-size: .6rem; font-weight: 900;
  letter-spacing: .06em; text-transform: uppercase; opacity: .75;
}
.nz-flow-next-name { font-size: .86rem; line-height: 1.15; }

/* .game-section is a flex column with a 24px gap between every child, tuned when this section
   had four. It now has five, so the gap is paid one extra time — tightened here, and only here,
   so the other nine games keep the spacing they were designed with. */
#sec-quiz { gap: 1rem; }

/* ── THE BLOCK SITS BELOW THE GAME NOW ────────────────────────────────────────
   It used to be the first thing in every section, which was right when it was the only thing
   naming the game. With a subject row and a game row above it, it was saying a third time what
   two lit capsules already say — and it is 163px, the single biggest item above any play area.
   Moved below, the game itself comes back above the fold on every screen.

   ORDER, NOT A MARKUP MOVE. Every .game-section is a flex column, so one rule relocates the
   block in all ten without touching a line of any game's JS — including Tot, which rebuilds its
   whole section on every screen and would otherwise need the host repositioned in three
   templates. Everything else defaults to order 0 and keeps its written sequence. */
#quizTop, #totTop, #shapesTop, #coloursTop, #paradeTop,
#clayTop, #abcTop, #babyTop, #animalTop, #weatherTop { order: 9; }

/* ── EXCEPT WHEN TODAY IS USING THE SLOT, WHICH GOES BACK ABOVE ────────────────
   The block above went below the game because it was a parent-facing card explaining a game two
   lit capsules had already named. Today's version is not that: it is where you are in a session,
   which age the three games were picked for, and the way to the next one. Below the game, a parent
   would have to scroll past the whole thing to find Next.

   The ids are repeated because they have to be: `.td-host { order: -1 }` loses to an id selector
   however far down the file it sits. assets/js/today.js adds the class when it takes the slot and
   game-flow.js removes it when the generic block takes it back. */
#quizTop.td-host, #totTop.td-host, #shapesTop.td-host, #coloursTop.td-host, #paradeTop.td-host,
#clayTop.td-host, #abcTop.td-host, #abc2Top.td-host, #babyTop.td-host, #animalTop.td-host,
#weatherTop.td-host {
  order: -1;
  /* AND A WIDTH, for the same reason .lv-wrap needs one: the section is `align-items: center`, so a
     host with none shrinks to fit-content and the block inside it can never reach the 580px the game
     card and the level strip use. Without this the block is as narrow as its longest line, and the
     age control ends up pressed against the position instead of sitting at the far end of the row. */
  width: 100%;
}

/* ── EVERY OTHER GAME CARRYING THE BLOCK ──────────────────────────────────────
   Same reason as #sec-quiz above: the flow block is a new first child of a flex column that was
   spaced for the children it had. Tot pays it on both of its screens, so it is set on the section
   rather than on either one. Add the next game's section here when it is wired. */
#sec-tot,
#sec-shapes,
#sec-colours,
#sec-parade,
#sec-abc,
#sec-clay,
#sec-baby,
#sec-animal,
#sec-weather,
/* What Comes Next?, added when it was wired — and it was caught by MEASURING, not by reading: its
   HUD sat 8px off the card while every other game's welded, because 24px of section gap against
   play-hud's -22px margin leaves a 2px hairline where the other nine overlap by 6. The seam is the
   tell for this whole rule, and it is the first thing to measure on a new game. */
#sec-pattern,
/* ABC Lessons, added with the same measurement: card.top - hud.bottom must read -6, not +2. */
#sec-abc2 { gap: 1rem; }

/* The block is the whole screen when a free run ends, so it gets the width the game had rather
   than the narrow measure the in-stage version wants. Centred because .game-section centres its
   children and .nz-flow.is-end is written expecting that. */
#sec-tot .drw-pause { width: 100%; max-width: 34rem; margin: 0 auto; }

/* Tot's own name is gone from the level-select screen — the flow block above it carries the name,
   the age range and the route to /games, and three headers in a row was the state before this.
   Kept as a rule rather than deleting the class, because .drw-title is Fredoka One at a size the
   subtitle below it is measured against. */
#sec-tot .drw-title { display: none; }

/* ── THE FIVE GAMES THAT HAD NO PLAY AREA ─────────────────────────────────────
   Tot was the first, and Clay, Weather, Who is my Baby and Animal Freeze were the rest: their
   content sat directly on the page background while the other five games had a bordered card. Next
   to a game with a HUD band and a card, a game with neither read as unfinished — which is what the
   founder saw. They all have one now, at the same 580px the rest use, so the play area is the same
   object in every game whether or not it has anything to put above it.

   THESE FOUR GET NO HUD, and that is the rule working rather than an omission: none of them has a
   level, a mode or a locked item, so there is nothing for a strip to carry and an empty band would be
   chrome added to four games for no reason (play-hud.js draws nothing when it has nothing). Every other game's content sits in a bordered card — ABC, Charm,
   Shape Explorer, Colour Hunt all use `.game-card` — and Tot's floated directly on the page
   background, so its five activities had no edge and nothing for the HUD to join. Now it does, and
   the HUD welds to it exactly as it does on Charm.

   The tiles inside keep their own borders and that is not nesting frames: a card holding controls is
   a container with content, not two concentric outlines round the same thing.

   The topbar it replaces held "← Levels" and the star count. The back button went with the
   level-select screen it pointed at; the star count went because two numbers starting at zero, above
   the word a child is learning, is not what a parent came for. */
#sec-tot .drw-game-wrap,
#sec-clay .clay-wrap,
#sec-weather .weather-wrap,
#sec-baby .baby-wrap,
#sec-animal .animal-wrap {
  max-width: 580px;
  box-sizing: border-box;
  background: #FEFAF4;
  border: 3px solid var(--warm);
  border-radius: 24px;
  box-shadow: 0 8px 32px rgba(61, 44, 30, .10);
  padding: 1rem .8rem 1.3rem;
}
/* SQUARE WHERE THE HUD MEETS IT. play-hud.css says this with `.nz-hud + .game-card`, and that rule
   lost: the id above it outranks two classes, so the card kept rounding its top corners and drew a
   second outline 3px inside the HUD's — two boxes where there should be one. Scoped to the sibling
   rather than set flat, so a Tot without a HUD would still be a proper card. */
#sec-tot .nz-hud + .drw-game-wrap { border-radius: 0 0 24px 24px; }
@media (min-width: 900px) {
  #sec-tot .drw-game-wrap,
  #sec-clay .clay-wrap,
  #sec-weather .weather-wrap,
  #sec-baby .baby-wrap,
  #sec-animal .animal-wrap { padding: 1.5rem 1.2rem 1.8rem; }
}

/* ── AND THE COUNTERS GO WITH THE CHROME ──────────────────────────────────────
   "⭐ Score: 0 · 🎯 Round: 1" was the first thing a parent read in both of these, above the animal
   their child is naming. Same argument as Charm's, which went earlier: a counter that starts at
   nothing, resets every round and is never saved is decoration. Hidden rather than deleted, because
   both games write into #babyScore / #aScoreVal unconditionally and would throw on nulls.

   Animal Freeze's round badge STAYS. "🔊 Sound round" names what kind of question this is — the
   game's content rather than a tally — and it is inside the card, where content belongs. */
#sec-baby .score-bar,
#sec-animal .score-bar { display: none; }

/* Clay prints its own name inside its board wrapper, the same duplication Tot had: with the block
   above it and the play bar standing down, "🎨 Clay Counting!" was the second header saying the
   same thing. */
#sec-clay .clay-title { display: none; }

/* Who Is My Baby? and Animal Freeze Detective print their own names too, so they go the same way.
   WEATHER IS THE EXCEPTION AND STAYS: "What's the weather outside?" is not the game's name, it is
   the question the child is being asked, and hiding it would take the instruction with it. */
#sec-baby .baby-title,
#sec-animal .animal-title { display: none; }

/* ── CHARM'S OWN CHROME, TRIMMED SO THE GAME FITS ─────────────────────────────
   These override the inline block in game.html, which is where charm's styles actually live
   (assets/css/charm-game.css is not linked by this page — see the note at game.html:148).

   The title wrapped to two lines on a phone: 4vw of a 360px screen is 14px, but the clamp floor
   was 1.5rem, so the floor won and it never got smaller. Scaling with the viewport for real
   keeps it on one line at every width. */
/* THE INSTRUCTION WAS PRINTED TWICE. "How many charms do you see? 🔢" above the card and "Count
   the charms!" inside it say the same thing to the same person, about 90px apart — and the outer one
   is the larger of the two, so the duplicate was the one shouting. It goes, and the card's own line
   inherits the type it had: the instruction belongs to the play area, which is the whole point of
   separating the play bar from it. Nothing is lost; it moved inside. */
#sec-quiz .game-title { display: none; }
#sec-quiz .question-label {
  font-size: clamp(1.05rem, 5.2vw, 1.7rem);
  line-height: 1.2; text-align: center;
}

/* Score and Round are gone. A toddler cannot read them and a parent does not care what round it
   is — it was a whole row of the screen for nothing. Hidden rather than deleted, because
   charm.js writes into #scoreVal and #roundVal unconditionally and would throw on nulls. */
#sec-quiz .score-bar { display: none; }

/* Five levels plus three Premium chips wrapped to four rows. Smaller type and tighter padding
   brings that down without hiding anything — the chips stay exactly as visible. */
/* Charm's levels are the HUD now — assets/css/play-hud.css and assets/js/play-hud.js. The rules that
   used to live here (a bordered scrolling row welded to the card) went with them, generalised, because
   per-game overrides in this file are how ten games ended up with ten different chromes. */

/* The game's name, now that the orange bar stands down for this game. */
.nz-flow-name {
  display: flex; align-items: center; gap: .35rem;
  font-family: 'Fredoka One', cursive;
  font-size: clamp(.95rem, 4.4vw, 1.15rem); line-height: 1.2;
  color: var(--ink); margin-bottom: .3rem;
}
.nz-flow-name-ico { font-size: 1em; line-height: 1; }
.game-switch[hidden] { display: none; }

/* The age range, beside the game's name. Small and quiet — it is reassurance a parent glances
   at, not a claim competing with what the game builds. */
.nz-flow-ages {
  margin-left: auto;
  font-family: 'Nunito', sans-serif; font-size: .62rem; font-weight: 900;
  letter-spacing: .02em; white-space: nowrap;
  padding: .14rem .45rem; border-radius: 50px;
  background: var(--kraft); color: var(--brown);
}

/* Picture Parade's one move that is not in the block: back to the set list. It has to survive,
   because parade paints no chrome during a set — without it a parent who has spent their five
   pictures has no way to reach another set. Quiet, under the block, in the game's own button
   style rather than the block's, so it reads as belonging to Picture Parade and not as a fifth
   route competing with the four above it. */
.par-other-after {
  display: block; margin: .5rem auto 0;
  font-family: 'Nunito', sans-serif; font-size: .74rem; font-weight: 900;
  background: none; border: 0; padding: .2rem .4rem;
  color: var(--ink, #3D2C1E); opacity: .6; cursor: pointer;
}
.par-other-after:hover { opacity: 1; }


/* ── THE SECTION'S OWN PADDING, ON A PHONE ────────────────────────────────────
   1.5rem of top padding was 24px of nothing between the play bar and the play area, on a screen
   where the game was starting at y = 473. The bar's tint already ends the chrome and the HUD's border
   begins the game, so the gap was doing no work. Left alone above 900px, where there is room for it
   and the two objects benefit from the air. */
@media (max-width: 899px) {
  .game-section { padding-top: .5rem; }
}

/* ── A CARD SHOWING AN ENDING IS NOT HOLDING A GAME ──────────────────────────────
   Four play areas reserve a height so the card does not resize under a child mid-round:
   .shp-wrap 440px, .clr-wrap and .par-wrap 452px, .abc-wrap 340px. That is right while a game is
   in there and wrong the moment one is not: an ending is shorter than any round, so the reserved
   height became a band of empty cream under the receipt — about 200px of it on a laptop.

   Not visible on a phone, which is why it survived: there the card's padding shrinks and the
   foundation chips wrap onto twice as many lines, so the receipt happens to fill the same box.

   :has() rather than a class toggled from JS, because the removal is the hard half. A game
   resumes down several paths — Keep playing, a level tap, a shape jump, Play next, a fresh deal —
   and a class added on the way in has to be taken off on all of them or one route leaves a
   collapsed card behind. This asks the question instead of remembering the answer.

   Descendant rather than a child combinator, because the depth differs per game: the card holds a
   stage element (#shpStage, #clrStage, #abcStage) and Parade's host sits deeper again. Safe as a
   descendant because neither card exists while a game is running -- both replace the round.

   Degrades to exactly today's appearance where :has() is unsupported: the whole rule is dropped,
   the reservation stays, and nothing breaks. */
.game-card:has(.nz-flow),
.game-card:has(.td-block) { min-height: 0; }

/* ==== no-select.css ==== */
/* ── A PAGE A CHILD TOUCHES IS NOT A DOCUMENT ──────────────────────────────
   Hold a finger on a word and iOS offers Look Up / Copy / Share, or the word turns half
   blue. Both land on top of the game, and neither can be dismissed by somebody who cannot
   read. Put `class="nz-noselect"` on the body of any page a toddler plays on.

   INHERITED ON PURPOSE, and that is the whole point. The games build nearly all of their
   DOM in JavaScript, so marking elements one by one can only ever cover the ones somebody
   remembered — which is how Read Together ended up with four scattered user-select:none
   declarations while every other word, letter and number stayed selectable until a
   two-year-old found one.

   Three properties, three different accidents:
     user-select          the selection itself, which is what the dictionary needs
     -webkit-touch-callout  the long-press menu on iPad. THIS is the dictionary popup
     -webkit-user-drag      dragging a picture off the page — same accident, other finger

   touch-action: manipulation turns off DOUBLE-TAP zoom only. A child tapping fast zoomed the game
   for no reason (founder, 2026-10-09: "it zooms and all for no reason"). Pinch stays, so a grown-up
   can still zoom in on purpose and pinch back out. */
.nz-noselect{
  -webkit-user-select:none; user-select:none;
  -webkit-touch-callout:none;
  touch-action:manipulation;
}
.nz-noselect img, .nz-noselect svg{ -webkit-user-drag:none; }

/* Typing has to keep working. Who is my Baby holds the only input in the games, and
   without this a parent cannot place a cursor to fix a typo. */
.nz-noselect input, .nz-noselect textarea{
  -webkit-user-select:text; user-select:text;
  -webkit-touch-callout:default;
}

/* No rubber-band bounce or pull-to-refresh past the page's ends - it tugged the game mid-play.
   On <html> ONLY, never on body: mobile-nav.css gives body overflow-x:hidden, which makes body a
   scroll box of its own, and overscroll-behavior:none there stopped every wheel and finger scroll
   from reaching the page - the game page could not scroll at all (founder, 2026-10-24). */
html:has(> body.nz-noselect){ overscroll-behavior:none; }
