/* Learning Curve — the Chunky Play design system: tokens, primitives, the shell, and the
   Home / Stats / Settings screens. Loaded AFTER styles.css and BEFORE ui-learn.css.

   ==== TOKENS (read this before adding a rule anywhere) =====================================
   Every colour lives HERE, in exactly one block:
     :root                                   the --c-* values + shape/type tokens + EVERY re-point
   The app is light only (there is no dark mode). Never add a colour token anywhere else:
   everything else re-points at a --c-* token in :root.
   This block is Chunky Play, the default PALETTE. themes.css (loaded right after this
   file) holds the two café palettes, <html data-palette="teahouse|latte">: each redefines
   every --c-* token below in one block of its own. A new token goes in ALL THREE blocks
   (tools/verify_themes.js fails otherwise).

   Colour roles (strict):
     neutrals   --c-ground (page)  --c-card  --c-raised (keys, chips, inputs, tab bar)
                --c-key (answer-key face, number badge)  --c-panel (card that holds answer keys)
                --c-recess (trays, tracks, holes)  --c-ink  --c-ink-2  --c-muted
                --c-line (2-2.5px outlines)  --c-line-soft (hairlines inside a card)
     depth      --c-slab (hard shadow: the block's side)  --c-inset (shadow inside a recess)
     solid fill --c-fill / --c-on-fill  (the "ink" pill: active tab, filled stairs, ink tags)
     on colour  --c-on-color  text, outline AND shadow on a phase or green fill (dark in every
                palette). --c-sh is computed at :root, so a component sitting on a colour fill
                writes its shadow with --c-on-color explicitly.
     action     --c-go / --c-on-go / --c-go-ink / --c-go-wash   the ONE main-action colour
                --c-go-ring / --c-go-ring-sh  the outline and shadow of a go key on a raised
                card (the Learn Continue card's play key)
     done       --c-ok / --c-ok-ink / --c-ok-wash                done or correct, nothing else
     wrong      --c-bad / --c-bad-wash    wrong, error, destructive; always with a word or a ✗
     disabled   --c-dis-bg / --c-dis-ink / --c-dis-line   flat, solid edge, never dashed
     phases     --c-p1..--c-p12 via .ph-math .ph-code .ph-ds .ph-dl .ph-llm .ph-sec .ph-build .ph-quantum .ph-cloud .ph-ops .ph-robotics
                and .ph-cert (Learn's Cert Exams tab, not a phase) (.ph-x = unknown), which set --pc.
                app.js phaseClass(ph) picks the class. Each is a pastel of its own: not another phase's hue,
                not the palette's go or done colour, and --c-on-color text on it at 4.5:1 or more.
     code       --c-code-bg --c-code-ink --c-code-kw --c-code-cm --c-code-st
     charts     --c-series-1..3 --c-chart-surface/-grid/-axis   HEX ONLY (widgets parse them)
                --c-series-1..3-text: the same hues for canvas TEXT (>= 4.5:1 on the surface)
     diagrams   --c-dg-1..3 (+ -text): the lesson diagrams' accents a / b / c (ui-learn.css), hues
                that are never a palette's go or ok colour, so a solid box is not read as a key
   Dashed borders mean EMPTY SLOT (match drop holes, the NOT STARTED chip) and nothing else.
   Borders and shadows use --c-line / --c-slab, never --c-ink.

   Shape: --c-bw 2.5px / --c-bw-sm 2px; radii --c-r 18 (cards) --c-r-md 14 --c-r-key 12
   --c-r-sm 8; shadows --c-sh (cards 4px 4px) --c-sh-key (keys 0 3px) --c-sh-sm (stickers)
   --c-sh-lift (a lifted idea) --c-sticker (drop-shadow filter); --match-tilt (0 when reduced).
   Type: --sans (Bricolage) --mono (Space Mono: labels, chips, numbers) --code (system mono:
   code only) --serif (maths only, `var`/.m) live in styles.css; --fs-display/-title/-h3/
   -prose/-small here. Nothing under 12px, anywhere.

   Primitives (this file): .ui-card(.tap)  keys .ui-btn/.btn (+ .primary/.btn-primary, .ghost/
   .btn-ghost, .danger; :disabled is flat)  .ui-chip/.chip (button.ui-chip presses; [aria-pressed]
   = fill)  .st (.done .no .bad) status chips  .tag-stk(.ink) stickers  .ui-label/.ui-eyebrow
   .ui-icon(.ok .ph)  .pdot  .sticker + .stk-out/.stk-cut/.stk-body/.stk-txt/.stk-ln + .seal
   .mascot (mascot.js pictures, a .mascot-anim stack of <img>s; one per screen at most)  .ui-input  checkbox rows (.proj-item)  .segbar  .lrn-secbar>.lrn-seg step blocks  .ui-bar(.sm)
   .seg segmented control  .vh visually hidden.  app.js helpers: phaseClass phaseShort pdot
   stChip segbar sealSvg levelStairs (see its "shared helpers" block).
   ========================================================================================== */

:root {
  color-scheme: light;

  /* ---- LIGHT colour values: Chunky Play exactly (the mock's tokens) ---- */
  --c-ground: #FFF5E1;
  --c-card: #FFFDF7;
  --c-raised: #FFFFFF;
  --c-key: #FFF5E1;
  --c-panel: #FFFFFF;
  --c-recess: #F1E4C9;
  --c-ink: #17140F;
  --c-ink-2: #2E2921;
  --c-muted: #5A5246;
  --c-line: #17140F;
  --c-line-soft: #E4D9C4;
  --c-slab: #17140F;
  --c-inset: rgba(23, 20, 15, .2);
  --c-fill: #17140F;
  --c-on-fill: #FFF5E1;
  --c-on-color: #17140F;
  --c-on-color-bg: rgba(255, 255, 255, .8);
  --c-go: #3A5BFF;
  --c-on-go: #FFFFFF;
  --c-go-ink: #3A5BFF;
  --c-go-wash: #E3E8FF;
  --c-go-ring: #17140F;
  --c-go-ring-sh: #17140F;
  --c-ok: #3BD497;
  --c-ok-ink: #0B7A4B;
  --c-ok-wash: #D6F6E7;
  --c-bad: #C42B25;
  --c-bad-wash: #FFE1DA;
  --c-dis-bg: #EFE5D2;
  --c-dis-ink: #6B6254;
  --c-dis-line: #A59A86;
  --c-focus: #3A5BFF;
  --c-scrim: rgba(23, 20, 15, .5);
  --c-p1: #B9A0FF; --c-p2: #FF8FC8; --c-p3: #FFC92E; --c-p4: #FF8A5B;
  --c-p5: #7FD3FF; --c-p6: #AFC0D3; --c-p7: #E2BF8C;
  --c-p8: #8EE8CA; --c-p9: #EFBDFC;   /* p8 Quantum (aqua), p9 the Cert Exams tab (orchid) */
  --c-p10: #B4E05C;   /* p10 Cloud & AI on AWS (lime, A1.09) */
  --c-p11: #64B2FA;   /* p11 DevSecOps (azure, D1.09) */
  --c-p12: #AAB446;   /* p12 Robotics (brass, RB1.09) */
  --c-code-bg: #221D16;
  --c-code-ink: #F6EBD6;
  --c-code-kw: #A9B8FF;
  --c-code-cm: #A89B86;
  --c-code-st: #7FE0B5;
  --c-heat-1: #C8F1DE; --c-heat-2: #8FE3BD; --c-heat-3: #3BD497; --c-heat-4: #1C9E6B;
  --c-series-1: #3A5BFF;
  --c-series-2: #E8622C;
  --c-series-3: #17A673;
  /* the same three hues as TEXT on a canvas (labels, legends): >= 4.5:1 on the chart surface
     (6.15 / 5.39 / 5.26 here). Lines and fills keep --c-series-N. */
  --c-series-1-text: #2F4CEB;
  --c-series-2-text: #B5451B;
  --c-series-3-text: #0D7A52;
  --c-chart-surface: #FFFDF7;
  --c-chart-grid: #ECE2CF;
  --c-chart-axis: #8C8069;
  /* lesson diagrams (variants a / b / c): purple, orange and a cool third hue, chosen so
     none is the main action (go) or done (ok) colour. -text is the solid fill and text
     shade: >= 4.5:1 on the card, and under --c-on-fill. */
  --c-dg-1: #7B4DFF;  --c-dg-2: #E8622C;  --c-dg-3: #0F9AA8;
  --c-dg-1-text: #5F33D6;  --c-dg-2-text: #B5451B;  --c-dg-3-text: #0A7380;

  /* ---- shape and motion (theme-independent) ---- */
  --c-bw: 2.5px;  --c-bw-sm: 2px;
  --c-r: 18px;    --c-r-md: 14px;  --c-r-key: 12px;  --c-r-sm: 8px;
  --c-sh: 4px 4px 0 var(--c-slab);
  --c-sh-key: 0 3px 0 var(--c-slab);
  --c-sh-sm: 2px 2px 0 var(--c-slab);
  --c-sh-lift: 4px 5px 0 var(--c-slab);
  --c-sticker: drop-shadow(2.5px 2.5px 0 var(--c-slab));
  --match-tilt: -1.5deg;

  /* ---- type scale (phone; desktop values below) ---- */
  --fs-display: 28px; --fs-title: 22px; --fs-h3: 17px; --fs-prose: 17px; --fs-small: 14px;

  /* ---- ui.css tokens, re-pointed ---- */
  --ui-bg: var(--c-ground);
  --ui-dot: transparent;
  --ui-surface: var(--c-card);
  --ui-surface-2: var(--c-recess);
  --ui-line: var(--c-line);
  --ui-line-strong: var(--c-line);
  --ui-ink: var(--c-ink);
  --ui-ink-2: var(--c-ink-2);
  --ui-muted: var(--c-muted);
  --ui-accent: var(--c-go);
  --ui-accent-ink: var(--c-go-ink);
  --ui-accent-wash: var(--c-go-wash);
  --ui-on-accent: var(--c-on-go);
  --ui-overlay: var(--c-ground);
  --h-blue: var(--c-go-ink);   --w-blue: var(--c-go-wash);
  --h-green: var(--c-ok-ink);  --w-green: var(--c-ok-wash);
  --h-red: var(--c-bad);       --w-red: var(--c-bad-wash);
  --h-orange: var(--c-ink);    --w-orange: var(--c-recess);
  --h-purple: var(--c-ink);    --w-purple: var(--c-recess);
  --h-teal: var(--c-ink);      --w-teal: var(--c-recess);
  --ui-radius: 18px;
  --ui-radius-sm: 12px;
  --ui-pad: 16px;
  --ui-gap: 12px;
  /* --ui-tabbar-h is the PUBLIC contract other screens position against (the lesson footer
     sits on top of it): the tab bar's full footprint from the viewport bottom, excluding the
     safe area, which callers add themselves. The bar is docked now, so the gap is 0. */
  --ui-tabbar-bar-h: 64px;
  --ui-tabbar-gap: 0px;
  --ui-tabbar-h: calc(var(--ui-tabbar-bar-h) + var(--ui-tabbar-gap));
  --ui-safe-b: env(safe-area-inset-bottom, 0px);

  /* ---- styles.css tokens, re-pointed (styles.css keeps only the font stacks) ---- */
  --bg: var(--c-ground);
  --surface: var(--c-card);
  --surface-2: var(--c-recess);
  --ink: var(--c-ink);
  --ink-2: var(--c-ink-2);
  --muted: var(--c-muted);
  --line: var(--c-line);
  --line-soft: var(--c-line-soft);
  --accent: var(--c-go);
  --accent-deep: var(--c-go-ink);
  --accent-wash: var(--c-go-wash);
  --on-accent: var(--c-on-go);
  --series-1: var(--c-series-1);
  --series-2: var(--c-series-2);
  --series-3: var(--c-series-3);
  --series-1-text: var(--c-series-1-text);
  --series-2-text: var(--c-series-2-text);
  --series-3-text: var(--c-series-3-text);
  --good: var(--c-ok-ink);
  --good-wash: var(--c-ok-wash);
  --bad: var(--c-bad);
  --bad-wash: var(--c-bad-wash);
  --chart-surface: var(--c-chart-surface);
  --chart-grid: var(--c-chart-grid);
  --chart-axis: var(--c-chart-axis);
  --code-bg: var(--c-code-bg);
  --code-ink: var(--c-code-ink);
  --code-accent: var(--c-code-kw);
  --shadow: var(--c-sh);
}
/* No tab bar at desktop widths (>=900px), so nothing reserves space for it there. */
@media (min-width: 900px) {
  :root {
    --ui-tabbar-h: 0px;
    --ui-pad: 20px; --ui-gap: 14px;
    --fs-display: 36px; --fs-title: 26px; --fs-h3: 18px; --fs-prose: 17px; --fs-small: 14.5px;
  }
  /* keyboard focus never scrolls under the sticky 64px top bar (WCAG 2.4.11) */
  html { scroll-padding-top: 76px; }
}
/* phones and tablets: a focused control the browser scrolls into view stops above the docked tab bar
   (WCAG 2.4.11 Focus Not Obscured). The lesson player adds its docked step footer (ui-learn.css), and the
   cert screens their own rules there. tools/smoke.js tabs through Home, Settings and the player at 390 px. */
@media (max-width: 899px) {
  html { scroll-padding-bottom: calc(var(--ui-tabbar-h) + var(--ui-safe-b) + 12px); }
}

/* ---- phase colours: the Learn tabs only, numbered in the order they were added (p8 Quantum, p9
   the Cert Exams tab, which is not a phase, p10 Cloud, p11 DevSecOps and p12 Robotics came after Build). Every id in app.js PHASE_SHORT needs a
   rule here and a token in all three blocks (tools/verify_themes.js checks both, and the contrast). ---- */
.ph-math { --pc: var(--c-p1); }  .ph-code { --pc: var(--c-p2); }  .ph-ds { --pc: var(--c-p3); }
.ph-dl { --pc: var(--c-p4); }    .ph-llm { --pc: var(--c-p5); }   .ph-sec { --pc: var(--c-p6); }
.ph-build { --pc: var(--c-p7); } .ph-quantum { --pc: var(--c-p8); } .ph-cert { --pc: var(--c-p9); }
.ph-cloud { --pc: var(--c-p10); }  .ph-ops { --pc: var(--c-p11); }  .ph-robotics { --pc: var(--c-p12); }
/* the LeetCode style tab (not a phase): it borrows the Code phase's token, so its contrast is the Code phase's */
.ph-lc { --pc: var(--c-p2); }
/* .ph-x (a phase with no rule yet) borrows a phase token, so its --c-on-color text stays at 4.5:1 in every
   palette; the raised-card colour it used to take fell far below that. verify_themes fails on any such phase. */
.ph-x { --pc: var(--c-p7); }

/* ---- ground: flat cream (the old dot grid is gone) ---- */
html { background-color: var(--c-ground); }
body { background: var(--c-ground); color: var(--c-ink); }

/* ---- legacy hue modifiers: still defined, now neutral (new markup must not use them) ---- */
.h-blue   { --hue: var(--h-blue);   --wash: var(--w-blue); }
.h-green  { --hue: var(--h-green);  --wash: var(--w-green); }
.h-orange { --hue: var(--h-orange); --wash: var(--w-orange); }
.h-red    { --hue: var(--h-red);    --wash: var(--w-red); }
.h-purple { --hue: var(--h-purple); --wash: var(--w-purple); }
.h-teal   { --hue: var(--h-teal);   --wash: var(--w-teal); }

/* ============================================================
   Type
   ============================================================ */
.ui-label, .ui-eyebrow {
  font: 700 12px/1.25 var(--mono); letter-spacing: .06em; text-transform: uppercase;
  color: var(--c-ink); margin: 0;
}
.ui-eyebrow.center { text-align: center; }
.ui-title {
  font: 800 var(--fs-display)/1.05 var(--sans); letter-spacing: -.02em; color: var(--c-ink); margin: 0;
}
.ui-title.sm { font-size: var(--fs-title); line-height: 1.15; letter-spacing: -.015em; }
.ui-sub { font: 400 16px/1.5 var(--sans); color: var(--c-ink-2); margin: 8px 0 0; max-width: 62ch; }
.ui-mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.ui-muted { color: var(--c-muted); }
/* maths only: a system serif italic (course text has no maths markup today) */
var, .m { font: italic 500 1.05em var(--serif); }
/* long lesson prose stays comfortable: build2 refines the rest of .prose */
.prose, .lesson-body, .topic-lesson { line-height: 1.62; }
.vh {
  position: absolute !important; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
/* app.js escTitle(): "ROUGE-2", "GPT-4" never break after the hyphen (display only) */
.nobr { white-space: nowrap; }

/* ============================================================
   Card
   ============================================================ */
.ui-card {
  background: var(--c-card); border: var(--c-bw) solid var(--c-line); border-radius: var(--c-r);
  box-shadow: var(--c-sh); padding: var(--ui-pad); min-width: 0;
}
.ui-card.tap {
  display: block; position: relative; color: inherit; text-decoration: none; cursor: pointer;
  transition: transform .08s ease, box-shadow .08s ease; -webkit-tap-highlight-color: transparent;
}
@media (hover: hover) {
  .ui-card.tap:hover { transform: translate(-1px, -1px); box-shadow: 5px 5px 0 var(--c-slab); text-decoration: none; }
}
.ui-card.tap:active { transform: translate(2px, 2px); box-shadow: 2px 2px 0 var(--c-slab); }
.ui-card .ui-go { color: var(--c-ink); display: inline-flex; }

/* ============================================================
   Keys (buttons). Keys lift on hover and press down on :active; they never change colour.
   ============================================================ */
.ui-btn, .btn, .mark-done {
  min-height: 44px; padding: 0 16px; border-radius: var(--c-r-key);
  border: var(--c-bw) solid var(--c-line); background: var(--c-raised); color: var(--c-ink);
  font: 800 15.5px/1.1 var(--sans); box-shadow: var(--c-sh-key); cursor: pointer; text-decoration: none;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px; opacity: 1; filter: none;
  text-align: center; transition: transform .08s ease, box-shadow .08s ease;
  -webkit-tap-highlight-color: transparent;
}
.ui-btn:hover, .btn:hover { text-decoration: none; }
@media (hover: hover) {
  .ui-btn:hover:not(:disabled), .btn:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 4px 0 var(--c-slab); }
}
.ui-btn:active:not(:disabled), .btn:active:not(:disabled), .mark-done:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--c-slab); }
/* primary: go-blue, the ONE main action */
.ui-btn.primary, .btn-primary, .mark-done { background: var(--c-go); color: var(--c-on-go); }
/* secondary = the base key; ghost = a tertiary text action */
.ui-btn.ghost, .btn-ghost {
  background: transparent; border-color: transparent; box-shadow: none; color: var(--c-ink);
  text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 3px;
}
@media (hover: hover) {
  .ui-btn.ghost:hover:not(:disabled), .btn-ghost:hover:not(:disabled) { transform: none; box-shadow: none; text-decoration-thickness: 3px; }
}
.ui-btn.ghost:active:not(:disabled), .btn-ghost:active:not(:disabled) { transform: none; box-shadow: none; }
.ui-btn.danger, .btn.danger { color: var(--c-bad); }
/* DISABLED: flat and greyed, a solid edge, no shadow, no motion, NEVER dashed (dashed = drop slot) */
.ui-btn:disabled, .btn:disabled, .btn[disabled], .mark-done:disabled, [aria-disabled="true"].ui-btn {
  background: var(--c-dis-bg); border: var(--c-bw) solid var(--c-dis-line); color: var(--c-dis-ink);
  box-shadow: none; transform: none; cursor: not-allowed; opacity: 1;
}
.ui-btn.ghost:disabled, .btn-ghost:disabled { background: transparent; border-color: transparent; text-decoration: none; }
.ui-btn.block { width: 100%; }
.mark-done.is-done { background: var(--c-ok); color: var(--c-on-color); }
.link-btn {
  display: inline-flex; align-items: center; min-height: 44px; padding: 0; background: none; border: 0;
  color: var(--c-ink); font: 700 14.5px/1.2 var(--sans); cursor: pointer;
  text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 3px;
}

/* ============================================================
   Chips, status chips, tag stickers
   ============================================================ */
.ui-chip, .chip {
  display: inline-flex; align-items: center; gap: 5px; font: 700 12px/1.4 var(--mono);
  background: var(--c-raised); color: var(--c-ink); border: var(--c-bw-sm) solid var(--c-line);
  border-radius: var(--c-r-sm); padding: 1px 7px 2px; white-space: nowrap;
}
button.ui-chip {
  position: relative; min-height: 36px; min-width: 44px; justify-content: center; padding: 0 10px;
  box-shadow: 0 2px 0 var(--c-slab); cursor: pointer; transition: transform .08s ease, box-shadow .08s ease;
}
button.ui-chip::after { content: ""; position: absolute; inset: -5px 0; }   /* 46px hit area */
button.ui-chip:active { transform: translateY(2px); box-shadow: none; }
button.ui-chip[aria-pressed="true"] { background: var(--c-fill); color: var(--c-on-fill); border-color: var(--c-line); }
button.ui-chip[class*="ph-"]::before {   /* phase chips carry a phase dot */
  content: ""; width: 10px; height: 10px; border-radius: 3px; border: var(--c-bw-sm) solid currentColor; background: var(--pc);
}
.ui-chips { display: flex; flex-wrap: wrap; gap: 8px; }
/* legacy pill: only .ui-pill.done (Practice "Solved", exam "Passed") is still used; it takes the .st.done look */
.ui-pill {
  display: inline-flex; align-items: center; gap: 5px; font: 700 12px/1.4 var(--mono);
  letter-spacing: .06em; text-transform: uppercase; white-space: nowrap;
  color: var(--c-ink); background: var(--c-raised); border: var(--c-bw-sm) solid var(--c-line);
  border-radius: var(--c-r-sm); padding: 1px 8px 2px;
}
.st, .ui-pill.done {
  display: inline-flex; align-items: center; gap: 3px; font: 700 12px/1.45 var(--mono);
  letter-spacing: .04em; text-transform: uppercase; border: var(--c-bw-sm) solid var(--c-line);
  border-radius: 7px; padding: 0 7px 1px; color: var(--c-ink); background: var(--c-raised); white-space: nowrap;
}
.st svg, .ui-pill svg { flex: none; }
.st.done, .ui-pill.done { background: var(--c-ok); color: var(--c-on-color); }
/* an empty marker: dashed (= empty), in muted so the edge still reads */
.st.no { border-style: dashed; border-color: var(--c-muted); color: var(--c-muted); background: var(--c-card); }
.st.bad { background: var(--c-bad-wash); color: var(--c-ink); border-color: var(--c-bad); }                   /* "✗ Not quite" */
.st.new { background: var(--c-go-wash); color: var(--c-ink); border-color: var(--c-go-ink); }                 /* "✦ New lessons": a finished module an appended group reopened */
.tag-stk {
  display: inline-block; font: 700 12px/1.3 var(--mono); letter-spacing: .06em; text-transform: uppercase;
  border: var(--c-bw-sm) solid var(--c-line); border-radius: var(--c-r-sm); padding: 2px 8px 3px;
  background: var(--c-raised); color: var(--c-ink); box-shadow: var(--c-sh-sm); transform: rotate(2deg); white-space: nowrap;
}
.tag-stk.ink { background: var(--c-fill); color: var(--c-on-fill); box-shadow: none; transform: rotate(-2deg); }

/* ============================================================
   Icon tiles, dots, stickers, the seal
   ============================================================ */
.ui-icon {
  width: 40px; height: 40px; border-radius: 11px; flex: none; display: grid; place-items: center;
  border: var(--c-bw) solid var(--c-line); background: var(--c-key); color: var(--c-ink);
}
.ui-icon svg { width: 22px; height: 22px; display: block; }
.ui-icon.lg { width: 52px; height: 52px; border-radius: 14px; }
.ui-icon.lg svg { width: 28px; height: 28px; }
.ui-icon.ok { background: var(--c-ok); color: var(--c-on-color); }
.ui-icon.ph { background: var(--pc); color: var(--c-on-color); }            /* phase marker only */
.pdot {
  display: inline-block; width: 12px; height: 12px; border-radius: 4px; flex: none;
  border: var(--c-bw-sm) solid var(--c-line); background: var(--pc, var(--c-raised)); vertical-align: -1px;
}
.sticker { filter: var(--c-sticker); }
.stk-out { fill: var(--c-line); stroke: var(--c-line); }
.stk-cut { fill: var(--c-raised); stroke: var(--c-raised); }
.stk-body { fill: var(--c-fill); stroke: var(--c-fill); }
.stk-txt { fill: var(--c-on-fill); }
.stk-ln { stroke: var(--c-on-fill); fill: none; }
/* seal text is in viewBox units (68 across); app.js sealSvg picks the class by length */
.seal-top { font: 700 12px var(--mono); letter-spacing: .5px; }
.seal-top.sm { font-size: 9px; letter-spacing: 0; }     /* 6+ characters: "NOT YET" */
.seal-big { font: 800 22px var(--sans); }
.seal-big.sm { font-size: 17px; }                        /* 3 characters: "4/5", "80%" */
.seal-big.xs { font-size: 15px; }                        /* 4: "100%", "5/10" */
.seal-big.xxs { font-size: 13px; }                       /* 5+: "10/10" */
/* the mascot (mascot.js anim(): a .mascot-anim span of stacked <img>s, each an SVG sticker that
   draws its own cream die-cut edge and ink shadow, so no .sticker filter here and no color token
   to follow: the z's, thought dots and sparkles sit on that same die-cut, so they read on every
   palette and mode; img() is the plain single picture). Always decorative beside words that say
   the same: the stack is aria-hidden and never a Tab stop. It takes a tap (app.js adds .is-tap for
   a 1.1 s hop), but its <img>s never take the pointer: no drag ghost, no long-press menu. Each
   placement sets its own size, on the wrapper (120 px at most). Motion is CSS only (opacity,
   transform, scale and translate, each at most once per element) and follows Settings > Motion:
   Full (no attribute, the default) loops for as long as Yuzu is on screen; Calm
   (data-motion="calm") plays the same loops cut at a still moment within 5 s, except the Python
   loader, which keeps thinking; Off (data-motion="reduced") and the device's reduce-motion
   setting switch every animation off (below and in styles.css). app.js pauses a Yuzu that is off
   screen or in a hidden tab. tools/verify_mascot.js checks all of this; tools/verify_motion.js plays it. */
.mascot {
  display: block; flex: none; width: 88px; height: 88px; max-width: none;
  pointer-events: none; -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
}
@keyframes mascot-pop {
  0% { transform: scale(.8); opacity: 0; }
  60% { transform: scale(1.06); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
/* ---- Yuzu, alive (mascot.js anim()). Written by tools/mascot/timeline.js from its choreography table: change the
   timing there and regenerate, never these lines by hand. One master cycle per pose: every picture layer switches on and
   off with steps(1, end) over the same cycle, so one picture shows at a time (or the still with a blink or glance on it);
   only the correct pose's glints fade. Each element animates each property at most once (body: transform, entrance:
   translate), so everything stays on the compositor. No continuous scale: between beats he is at rest and drawn
   pixel-sharp. Loops run through var(--mascot-loop): infinite under Full, cut by the Calm rules below. ---- */
/* the wrapper takes the tap (the layers never do: no drag ghost, no long-press menu); (0,2,0) beats .mascot { pointer-events: none } */
.mascot.mascot-anim { position: relative; pointer-events: auto; -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
.mascot-anim > img { position: absolute; inset: 0; display: block; width: 100%; height: 100%; max-width: none; margin: 0; border: 0; pointer-events: none; -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
.mascot-anim > .mf { opacity: 0; }
/* the correct pose's entrance: it rises into place (translate and opacity, not transform, which its body loop owns) */
@keyframes mascot-rise { 0% { translate: 0 10%; opacity: 0; } 60% { translate: 0 -3%; opacity: 1; } 100% { translate: 0 0; opacity: 1; } }
/* Hello (Home greeting): a 52.8s cycle; Calm stops at 4.6s */
@keyframes mascot-hello-still { 0% { opacity: 1; } 0.568% { opacity: 0; } 2.083% { opacity: 1; } 9.47% { opacity: 0; } 50% { opacity: 1; } 50.568% { opacity: 0; } 52.083% { opacity: 1; } 59.47% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-hello-wave-out { 0% { opacity: 0; } 0.568% { opacity: 1; } 0.871% { opacity: 0; } 1.174% { opacity: 1; } 1.477% { opacity: 0; } 1.78% { opacity: 1; } 2.083% { opacity: 0; } 50.568% { opacity: 1; } 50.871% { opacity: 0; } 51.174% { opacity: 1; } 51.477% { opacity: 0; } 51.78% { opacity: 1; } 52.083% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-hello-wave-in { 0% { opacity: 0; } 0.871% { opacity: 1; } 1.174% { opacity: 0; } 1.477% { opacity: 1; } 1.78% { opacity: 0; } 50.871% { opacity: 1; } 51.174% { opacity: 0; } 51.477% { opacity: 1; } 51.78% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-hello-lower { 0% { opacity: 0; } 9.47% { opacity: 1; } 9.697% { opacity: 0; } 49.773% { opacity: 1; } 50% { opacity: 0; } 59.47% { opacity: 1; } 59.697% { opacity: 0; } 99.773% { opacity: 1; } 100% { opacity: 1; } }
@keyframes mascot-hello-rest { 0% { opacity: 0; } 9.697% { opacity: 1; } 12.879% { opacity: 0; } 15.985% { opacity: 1; } 21.212% { opacity: 0; } 21.439% { opacity: 1; } 21.78% { opacity: 0; } 22.008% { opacity: 1; } 25.379% { opacity: 0; } 26.515% { opacity: 1; } 26.705% { opacity: 0; } 26.97% { opacity: 1; } 33.333% { opacity: 0; } 33.598% { opacity: 1; } 37.879% { opacity: 0; } 40.417% { opacity: 1; } 46.212% { opacity: 0; } 46.477% { opacity: 1; } 49.773% { opacity: 0; } 59.697% { opacity: 1; } 62.879% { opacity: 0; } 65.985% { opacity: 1; } 71.212% { opacity: 0; } 71.439% { opacity: 1; } 71.78% { opacity: 0; } 72.008% { opacity: 1; } 75.379% { opacity: 0; } 76.515% { opacity: 1; } 76.705% { opacity: 0; } 76.97% { opacity: 1; } 83.333% { opacity: 0; } 83.598% { opacity: 1; } 87.879% { opacity: 0; } 88.258% { opacity: 1; } 90.152% { opacity: 0; } 90.417% { opacity: 1; } 96.212% { opacity: 0; } 96.477% { opacity: 1; } 99.773% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-hello-rest-look-r { 0% { opacity: 0; } 12.879% { opacity: 1; } 15.72% { opacity: 0; } 62.879% { opacity: 1; } 65.72% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-hello-rest-look-l { 0% { opacity: 0; } 37.879% { opacity: 1; } 40.152% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-hello-rest-ear-back { 0% { opacity: 0; } 87.879% { opacity: 1; } 88.068% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-hello-rest-ear { 0% { opacity: 0; } 88.068% { opacity: 1; } 88.258% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-hello-rest-blink { 0% { opacity: 0; } 15.72% { opacity: 1; } 15.985% { opacity: 0; } 21.212% { opacity: 1; } 21.439% { opacity: 0; } 21.78% { opacity: 1; } 22.008% { opacity: 0; } 26.705% { opacity: 1; } 26.97% { opacity: 0; } 33.333% { opacity: 1; } 33.598% { opacity: 0; } 40.152% { opacity: 1; } 40.417% { opacity: 0; } 46.212% { opacity: 1; } 46.477% { opacity: 0; } 65.72% { opacity: 1; } 65.985% { opacity: 0; } 71.212% { opacity: 1; } 71.439% { opacity: 0; } 71.78% { opacity: 1; } 72.008% { opacity: 0; } 76.705% { opacity: 1; } 76.97% { opacity: 0; } 83.333% { opacity: 1; } 83.598% { opacity: 0; } 90.152% { opacity: 1; } 90.417% { opacity: 0; } 96.212% { opacity: 1; } 96.477% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-hello-glasses-reach { 0% { opacity: 0; } 25.379% { opacity: 1; } 25.72% { opacity: 0; } 75.379% { opacity: 1; } 75.72% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-hello-glasses { 0% { opacity: 0; } 25.72% { opacity: 1; } 26.515% { opacity: 0; } 75.72% { opacity: 1; } 76.515% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-hello-blink { 0% { opacity: 0; } 5.871% { opacity: 1; } 6.136% { opacity: 0; } 55.871% { opacity: 1; } 56.136% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-hello-body { 0% { transform: none; } 0.568% { transform: none; } 0.947% { transform: translateY(-2.5%); } 1.705% { transform: translateY(-2.5%); } 2.083% { transform: none; } 12.879% { transform: none; } 13.447% { transform: rotate(1.5deg); } 15.152% { transform: rotate(1.5deg); } 15.72% { transform: none; } 25.72% { transform: none; } 26.098% { transform: translateY(-1.5%); } 26.515% { transform: translateY(-1.5%); } 26.894% { transform: none; } 37.879% { transform: none; } 38.447% { transform: rotate(-1.2deg); } 39.583% { transform: rotate(-1.2deg); } 40.152% { transform: none; } 50.568% { transform: none; } 50.947% { transform: translateY(-2.5%); } 51.705% { transform: translateY(-2.5%); } 52.083% { transform: none; } 62.879% { transform: none; } 63.447% { transform: rotate(1.5deg); } 65.152% { transform: rotate(1.5deg); } 65.72% { transform: none; } 75.72% { transform: none; } 76.098% { transform: translateY(-1.5%); } 76.515% { transform: translateY(-1.5%); } 76.894% { transform: none; } 100% { transform: none; } }
.mascot-anim.mascot-hello { transform-origin: 50% 92%; animation: mascot-hello-body 52.8s ease-in-out var(--mascot-loop, infinite); }
.mascot-hello > .mascot-still { animation: mascot-hello-still 52.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-hello > .mf-wave-out { animation: mascot-hello-wave-out 52.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-hello > .mf-wave-in { animation: mascot-hello-wave-in 52.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-hello > .mf-lower { animation: mascot-hello-lower 52.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-hello > .mf-rest { animation: mascot-hello-rest 52.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-hello > .mf-rest-look-r { animation: mascot-hello-rest-look-r 52.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-hello > .mf-rest-look-l { animation: mascot-hello-rest-look-l 52.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-hello > .mf-rest-ear-back { animation: mascot-hello-rest-ear-back 52.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-hello > .mf-rest-ear { animation: mascot-hello-rest-ear 52.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-hello > .mf-rest-blink { animation: mascot-hello-rest-blink 52.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-hello > .mf-glasses-reach { animation: mascot-hello-glasses-reach 52.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-hello > .mf-glasses { animation: mascot-hello-glasses 52.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-hello > .mf-blink { animation: mascot-hello-blink 52.8s steps(1, end) var(--mascot-loop, infinite); }
/* Thinking (Python loader): a 14.4s cycle; Calm stops at 4.6s (but not in the loader) */
@keyframes mascot-thinking-still { 0% { opacity: 0; } 12.5% { opacity: 1; } 16.667% { opacity: 0; } 29.167% { opacity: 1; } 33.333% { opacity: 0; } 45.833% { opacity: 1; } 66.667% { opacity: 0; } 79.167% { opacity: 1; } 83.333% { opacity: 0; } 84.722% { opacity: 1; } 87.5% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-thinking-dot-1 { 0% { opacity: 1; } 4.167% { opacity: 0; } 16.667% { opacity: 1; } 20.833% { opacity: 0; } 33.333% { opacity: 1; } 37.5% { opacity: 0; } 66.667% { opacity: 1; } 70.833% { opacity: 0; } 87.5% { opacity: 1; } 91.667% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-thinking-dot-2 { 0% { opacity: 0; } 4.167% { opacity: 1; } 8.333% { opacity: 0; } 20.833% { opacity: 1; } 25% { opacity: 0; } 37.5% { opacity: 1; } 41.667% { opacity: 0; } 70.833% { opacity: 1; } 75% { opacity: 0; } 91.667% { opacity: 1; } 95.833% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-thinking-hop { 0% { opacity: 0; } 8.333% { opacity: 1; } 12.5% { opacity: 0; } 25% { opacity: 1; } 29.167% { opacity: 0; } 41.667% { opacity: 1; } 45.833% { opacity: 0; } 75% { opacity: 1; } 79.167% { opacity: 0; } 95.833% { opacity: 1; } 100% { opacity: 1; } }
@keyframes mascot-thinking-look-l { 0% { opacity: 0; } 48.611% { opacity: 1; } 58.333% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-thinking-ear-back { 0% { opacity: 0; } 83.333% { opacity: 1; } 84.028% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-thinking-ear { 0% { opacity: 0; } 84.028% { opacity: 1; } 84.722% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-thinking-blink { 0% { opacity: 0; } 29.861% { opacity: 1; } 30.833% { opacity: 0; } 58.333% { opacity: 1; } 59.306% { opacity: 0; } 80.556% { opacity: 1; } 81.528% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-thinking-body { 0% { transform: none; } 8.333% { transform: rotate(-2deg); } 23.611% { transform: rotate(1.5deg); } 30.556% { transform: none; } 33.333% { transform: none; } 41.667% { transform: rotate(-2deg); } 48.611% { transform: rotate(-1deg); } 58.333% { transform: rotate(1.5deg); } 66.667% { transform: none; } 75% { transform: rotate(-2deg); } 91.667% { transform: rotate(1.5deg); } 100% { transform: none; } }
.mascot-anim.mascot-thinking { transform-origin: 50% 95%; }
.mascot-anim.mascot-thinking:not(.yz-wait) { animation: mascot-thinking-body 14.4s steps(6, end) var(--mascot-loop, infinite); }
.mascot-thinking:not(.yz-wait) > .mascot-still { animation: mascot-thinking-still 14.4s steps(1, end) var(--mascot-loop, infinite); }
.mascot-thinking:not(.yz-wait) > .mf-dot-1 { animation: mascot-thinking-dot-1 14.4s steps(1, end) var(--mascot-loop, infinite); }
.mascot-thinking:not(.yz-wait) > .mf-dot-2 { animation: mascot-thinking-dot-2 14.4s steps(1, end) var(--mascot-loop, infinite); }
.mascot-thinking:not(.yz-wait) > .mf-hop { animation: mascot-thinking-hop 14.4s steps(1, end) var(--mascot-loop, infinite); }
.mascot-thinking:not(.yz-wait) > .mf-look-l { animation: mascot-thinking-look-l 14.4s steps(1, end) var(--mascot-loop, infinite); }
.mascot-thinking:not(.yz-wait) > .mf-ear-back { animation: mascot-thinking-ear-back 14.4s steps(1, end) var(--mascot-loop, infinite); }
.mascot-thinking:not(.yz-wait) > .mf-ear { animation: mascot-thinking-ear 14.4s steps(1, end) var(--mascot-loop, infinite); }
.mascot-thinking:not(.yz-wait) > .mf-blink { animation: mascot-thinking-blink 14.4s steps(1, end) var(--mascot-loop, infinite); }
/* Correct (good results): a 23.6s cycle; Calm stops at 4.9s */
@keyframes mascot-correct-still { 0% { opacity: 0; } 2.331% { opacity: 1; } 35.593% { opacity: 0; } 36.271% { opacity: 1; } 36.949% { opacity: 0; } 37.627% { opacity: 1; } 72.881% { opacity: 0; } 73.559% { opacity: 1; } 74.237% { opacity: 0; } 74.915% { opacity: 1; } 100% { opacity: 1; } }
@keyframes mascot-correct-spark-0 { 0% { opacity: 1; } 0.847% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-correct-spark-s { 0% { opacity: 0; } 0.847% { opacity: 1; } 1.483% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-correct-spark-l { 0% { opacity: 0; } 1.483% { opacity: 1; } 2.331% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-correct-yuzu { 0% { opacity: 0; } 35.593% { opacity: 1; } 36.271% { opacity: 0; } 36.949% { opacity: 1; } 37.627% { opacity: 0; } 72.881% { opacity: 1; } 73.559% { opacity: 0; } 74.237% { opacity: 1; } 74.915% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-correct-glint-a { 0% { opacity: 0; transform: scale(0.3); } 17.797% { opacity: 0; transform: scale(0.3); } 19.068% { opacity: 1; transform: scale(1); } 20.339% { opacity: 0; transform: scale(0.3); } 88.136% { opacity: 0; transform: scale(0.3); } 89.407% { opacity: 1; transform: scale(1); } 90.678% { opacity: 0; transform: scale(0.3); } 100% { opacity: 0; transform: scale(0.3); } }
@keyframes mascot-correct-glint-b { 0% { opacity: 0; transform: scale(0.3); } 55.085% { opacity: 0; transform: scale(0.3); } 56.356% { opacity: 1; transform: scale(1); } 57.627% { opacity: 0; transform: scale(0.3); } 89.195% { opacity: 0; transform: scale(0.3); } 90.466% { opacity: 1; transform: scale(1); } 91.737% { opacity: 0; transform: scale(0.3); } 100% { opacity: 0; transform: scale(0.3); } }
@keyframes mascot-correct-body { 0% { transform: none; } 35.593% { transform: none; } 36.017% { transform: translateY(-2.5%); } 36.61% { transform: none; } 37.373% { transform: translateY(-2.5%); } 38.051% { transform: none; } 72.881% { transform: none; } 73.305% { transform: translateY(-2.5%); } 73.898% { transform: none; } 74.661% { transform: translateY(-2.5%); } 75.339% { transform: none; } 100% { transform: none; } }
.mascot-anim.mascot-correct { transform-origin: 50% 88%; animation: mascot-rise .45s ease-out both; }
.mascot-anim.mascot-correct:not(.yz-wait) { animation: mascot-rise .45s ease-out both, mascot-correct-body 23.6s ease-in-out var(--mascot-loop, infinite); }
.mascot-correct:not(.yz-wait) > .mascot-still { animation: mascot-correct-still 23.6s steps(1, end) var(--mascot-loop, infinite); }
.mascot-correct:not(.yz-wait) > .mf-spark-0 { animation: mascot-correct-spark-0 23.6s steps(1, end) var(--mascot-loop, infinite); }
.mascot-correct:not(.yz-wait) > .mf-spark-s { animation: mascot-correct-spark-s 23.6s steps(1, end) var(--mascot-loop, infinite); }
.mascot-correct:not(.yz-wait) > .mf-spark-l { animation: mascot-correct-spark-l 23.6s steps(1, end) var(--mascot-loop, infinite); }
.mascot-correct:not(.yz-wait) > .mf-yuzu { animation: mascot-correct-yuzu 23.6s steps(1, end) var(--mascot-loop, infinite); }
.mascot-correct:not(.yz-wait) > .mf-glint-a { transform-origin: 13% 23%; animation: mascot-correct-glint-a 23.6s ease-in-out var(--mascot-loop, infinite); }
.mascot-correct:not(.yz-wait) > .mf-glint-b { transform-origin: 90% 61%; animation: mascot-correct-glint-b 23.6s ease-in-out var(--mascot-loop, infinite); }
/* Oops (a missed answer): a 18.6s cycle; Calm stops at 4.4s */
@keyframes mascot-oops-still { 0% { opacity: 1; } 1.613% { opacity: 0; } 2.258% { opacity: 1; } 2.903% { opacity: 0; } 3.548% { opacity: 1; } 4.194% { opacity: 0; } 4.839% { opacity: 1; } 67.742% { opacity: 0; } 68.387% { opacity: 1; } 69.032% { opacity: 0; } 69.677% { opacity: 1; } 100% { opacity: 1; } }
@keyframes mascot-oops-scratch { 0% { opacity: 0; } 1.613% { opacity: 1; } 2.258% { opacity: 0; } 2.903% { opacity: 1; } 3.548% { opacity: 0; } 4.194% { opacity: 1; } 4.839% { opacity: 0; } 67.742% { opacity: 1; } 68.387% { opacity: 0; } 69.032% { opacity: 1; } 69.677% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-oops-look-r { 0% { opacity: 0; } 33.333% { opacity: 1; } 40.323% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-oops-blink { 0% { opacity: 0; } 12.903% { opacity: 1; } 13.656% { opacity: 0; } 40.323% { opacity: 1; } 41.075% { opacity: 0; } 54.839% { opacity: 1; } 55.484% { opacity: 0; } 56.452% { opacity: 1; } 57.097% { opacity: 0; } 84.946% { opacity: 1; } 85.699% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-oops-body { 0% { transform: none; } 1.613% { transform: none; } 2.957% { transform: rotate(2deg); } 4.301% { transform: rotate(-1deg); } 5.484% { transform: none; } 33.333% { transform: none; } 34.946% { transform: rotate(-1deg); } 38.71% { transform: rotate(-1deg); } 40.323% { transform: none; } 67.742% { transform: none; } 68.817% { transform: rotate(1.5deg); } 70.43% { transform: none; } 100% { transform: none; } }
.mascot-anim.mascot-oops { transform-origin: 50% 95%; }
.mascot-anim.mascot-oops:not(.yz-wait) { animation: mascot-oops-body 18.6s ease-in-out var(--mascot-loop, infinite); }
.mascot-oops:not(.yz-wait) > .mascot-still { animation: mascot-oops-still 18.6s steps(1, end) var(--mascot-loop, infinite); }
.mascot-oops:not(.yz-wait) > .mf-scratch { animation: mascot-oops-scratch 18.6s steps(1, end) var(--mascot-loop, infinite); }
.mascot-oops:not(.yz-wait) > .mf-look-r { animation: mascot-oops-look-r 18.6s steps(1, end) var(--mascot-loop, infinite); }
.mascot-oops:not(.yz-wait) > .mf-blink { animation: mascot-oops-blink 18.6s steps(1, end) var(--mascot-loop, infinite); }
/* Studying (an unstarted phase): a 21s cycle; Calm stops at 4.4s */
@keyframes mascot-studying-still { 0% { opacity: 1; } 12.381% { opacity: 0; } 14.952% { opacity: 1; } 74.286% { opacity: 0; } 76.857% { opacity: 1; } 100% { opacity: 1; } }
@keyframes mascot-studying-page-1 { 0% { opacity: 0; } 12.381% { opacity: 1; } 13.333% { opacity: 0; } 74.286% { opacity: 1; } 75.238% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-studying-page-mid { 0% { opacity: 0; } 13.333% { opacity: 1; } 14% { opacity: 0; } 75.238% { opacity: 1; } 75.905% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-studying-page-2 { 0% { opacity: 0; } 14% { opacity: 1; } 14.952% { opacity: 0; } 75.905% { opacity: 1; } 76.857% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-studying-look-up { 0% { opacity: 0; } 42.857% { opacity: 1; } 51.429% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-studying-blink { 0% { opacity: 0; } 6.667% { opacity: 1; } 7.333% { opacity: 0; } 28.095% { opacity: 1; } 28.762% { opacity: 0; } 51.429% { opacity: 1; } 52.095% { opacity: 0; } 62.857% { opacity: 1; } 63.429% { opacity: 0; } 64.286% { opacity: 1; } 64.857% { opacity: 0; } 89.524% { opacity: 1; } 90.19% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-studying-body { 0% { transform: none; } 12.143% { transform: none; } 13.333% { transform: rotate(-0.8deg); } 14.286% { transform: rotate(-0.8deg); } 15.714% { transform: none; } 42.857% { transform: none; } 44.286% { transform: translateY(-1.2%) rotate(-1deg); } 50% { transform: translateY(-1.2%) rotate(-1deg); } 51.429% { transform: none; } 74.048% { transform: none; } 75.238% { transform: rotate(-0.8deg); } 76.19% { transform: rotate(-0.8deg); } 77.619% { transform: none; } 100% { transform: none; } }
.mascot-anim.mascot-studying { transform-origin: 50% 95%; }
.mascot-anim.mascot-studying:not(.yz-wait) { animation: mascot-studying-body 21s ease-in-out var(--mascot-loop, infinite); }
.mascot-studying:not(.yz-wait) > .mascot-still { animation: mascot-studying-still 21s steps(1, end) var(--mascot-loop, infinite); }
.mascot-studying:not(.yz-wait) > .mf-page-1 { animation: mascot-studying-page-1 21s steps(1, end) var(--mascot-loop, infinite); }
.mascot-studying:not(.yz-wait) > .mf-page-mid { animation: mascot-studying-page-mid 21s steps(1, end) var(--mascot-loop, infinite); }
.mascot-studying:not(.yz-wait) > .mf-page-2 { animation: mascot-studying-page-2 21s steps(1, end) var(--mascot-loop, infinite); }
.mascot-studying:not(.yz-wait) > .mf-look-up { animation: mascot-studying-look-up 21s steps(1, end) var(--mascot-loop, infinite); }
.mascot-studying:not(.yz-wait) > .mf-blink { animation: mascot-studying-blink 21s steps(1, end) var(--mascot-loop, infinite); }
/* Resting (empty states): a 13.8s cycle; Calm stops at 3s */
@keyframes mascot-resting-still { 0% { opacity: 0; } 16.667% { opacity: 1; } 23.913% { opacity: 0; } 50% { opacity: 1; } 51.087% { opacity: 0; } 53.261% { opacity: 1; } 55.072% { opacity: 0; } 83.333% { opacity: 1; } 90.58% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-resting-z0 { 0% { opacity: 1; } 1.087% { opacity: 0; } 2.536% { opacity: 1; } 3.623% { opacity: 0; } 33.333% { opacity: 1; } 36.957% { opacity: 0; } 66.667% { opacity: 1; } 70.29% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-resting-z1 { 0% { opacity: 0; } 3.623% { opacity: 1; } 10.145% { opacity: 0; } 36.957% { opacity: 1; } 43.478% { opacity: 0; } 70.29% { opacity: 1; } 76.812% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-resting-z2 { 0% { opacity: 0; } 10.145% { opacity: 1; } 16.667% { opacity: 0; } 43.478% { opacity: 1; } 50% { opacity: 0; } 76.812% { opacity: 1; } 83.333% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-resting-z-top2 { 0% { opacity: 0; } 23.913% { opacity: 1; } 28.986% { opacity: 0; } 57.246% { opacity: 1; } 62.319% { opacity: 0; } 90.58% { opacity: 1; } 95.652% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-resting-z-top1 { 0% { opacity: 0; } 28.986% { opacity: 1; } 33.333% { opacity: 0; } 62.319% { opacity: 1; } 66.667% { opacity: 0; } 95.652% { opacity: 1; } 100% { opacity: 1; } }
@keyframes mascot-resting-bob { 0% { opacity: 0; } 51.087% { opacity: 1; } 53.261% { opacity: 0; } 55.072% { opacity: 1; } 57.246% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-resting-ear-back { 0% { opacity: 0; } 1.087% { opacity: 1; } 1.812% { opacity: 0; } 100% { opacity: 0; } }
@keyframes mascot-resting-ear { 0% { opacity: 0; } 1.812% { opacity: 1; } 2.536% { opacity: 0; } 100% { opacity: 0; } }
.mascot-anim.mascot-resting { transform-origin: 50% 96%; }
.mascot-resting:not(.yz-wait) > .mascot-still { animation: mascot-resting-still 13.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-resting:not(.yz-wait) > .mf-z0 { animation: mascot-resting-z0 13.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-resting:not(.yz-wait) > .mf-z1 { animation: mascot-resting-z1 13.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-resting:not(.yz-wait) > .mf-z2 { animation: mascot-resting-z2 13.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-resting:not(.yz-wait) > .mf-z-top2 { animation: mascot-resting-z-top2 13.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-resting:not(.yz-wait) > .mf-z-top1 { animation: mascot-resting-z-top1 13.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-resting:not(.yz-wait) > .mf-bob { animation: mascot-resting-bob 13.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-resting:not(.yz-wait) > .mf-ear-back { animation: mascot-resting-ear-back 13.8s steps(1, end) var(--mascot-loop, infinite); }
.mascot-resting:not(.yz-wait) > .mf-ear { animation: mascot-resting-ear 13.8s steps(1, end) var(--mascot-loop, infinite); }
/* Settings > Motion > Calm: the same loops, cut by a fractional iteration count at a moment when only the still shows,
   no glint is lit and the body is at rest (so it settles without a jump). All within 5 s. */
:root[data-motion="calm"] .mascot-hello { --mascot-loop: 0.0871; }
:root[data-motion="calm"] .mascot-thinking { --mascot-loop: 0.3194; }
:root[data-motion="calm"] .mascot-correct { --mascot-loop: 0.2076; }
:root[data-motion="calm"] .mascot-oops { --mascot-loop: 0.2366; }
:root[data-motion="calm"] .mascot-studying { --mascot-loop: 0.2095; }
:root[data-motion="calm"] .mascot-resting { --mascot-loop: 0.2174; }
/* ...except in the Python loader, which keeps thinking for as long as the download takes: a loading indicator whose
   movement shows the app has not hung (WCAG 2.2.2 exempts it). Off and the device setting still keep it still. */
:root[data-motion="calm"] .ex-loading .mascot-thinking { --mascot-loop: infinite; }
/* the tap reaction (app.js holds .is-tap for 1.1 s, adds .is-tap2 on every other tap and .is-tap-end at 1 s): every idle
   layer is hidden but keeps running, so the loop carries on underneath and returns exactly where it would have been */
@keyframes mascot-tap-hop { 0% { transform: none; } 12% { transform: scale(1.05, .93); } 34% { transform: translateY(-8%) scale(.97, 1.04); } 56% { transform: scale(1.04, .95); } 70% { transform: translateY(-2%); } 84%, 100% { transform: none; } }
@keyframes mascot-tap-hop-w { 0% { transform: none; } 12% { transform: scale(1.05, .93); } 30% { transform: translateY(-8%) rotate(-4deg) scale(.97, 1.04); } 46% { transform: translateY(-4.8%) rotate(4deg); } 62% { transform: scale(1.04, .95); } 76% { transform: translateY(-2%); } 88%, 100% { transform: none; } }
@keyframes mascot-tap-hop-low { 0% { transform: none; } 12% { transform: scale(1.03, .96); } 34% { transform: translateY(-4%) scale(.98, 1.02); } 56% { transform: scale(1.02, .97); } 70% { transform: translateY(-1%); } 84%, 100% { transform: none; } }
@keyframes mascot-tap-hop-low-w { 0% { transform: none; } 12% { transform: scale(1.03, .96); } 30% { transform: translateY(-4%) rotate(-2deg) scale(.98, 1.02); } 46% { transform: translateY(-2.4%) rotate(2deg); } 62% { transform: scale(1.02, .97); } 76% { transform: translateY(-1%); } 88%, 100% { transform: none; } }
@keyframes mascot-tap-wave-a { 0% { opacity: 1; } 16% { opacity: 0; } 32% { opacity: 1; } 48% { opacity: 0; } 64%, 100% { opacity: 1; } }
@keyframes mascot-tap-wave-b { 0% { opacity: 0; } 16% { opacity: 1; } 32% { opacity: 0; } 48% { opacity: 1; } 64%, 100% { opacity: 0; } }
.mascot-anim.is-tap > img { visibility: hidden; }
.mascot-anim.is-tap > .mf-tap, .mascot-anim.is-tap > .mf-tap-a, .mascot-anim.is-tap > .mf-tap-b { visibility: visible; transform-origin: 50% 95%; }
.mascot-anim.is-tap > .mf-tap, .mascot-anim.is-tap > .mf-tap-a { opacity: 1; }
.mascot-anim.is-tap > .mf-tap { animation: mascot-tap-hop 1s ease-in-out; }
.mascot-anim.is-tap > .mf-tap-a { animation: mascot-tap-hop 1s ease-in-out, mascot-tap-wave-a 1s steps(1, end); }
.mascot-anim.is-tap > .mf-tap-b { animation: mascot-tap-hop 1s ease-in-out, mascot-tap-wave-b 1s steps(1, end); }
/* every other tap adds a wiggle, so it does not feel canned */
.mascot-anim.is-tap.is-tap2 > .mf-tap { animation: mascot-tap-hop-w 1s ease-in-out; }
.mascot-anim.is-tap.is-tap2 > .mf-tap-a { animation: mascot-tap-hop-w 1s ease-in-out, mascot-tap-wave-a 1s steps(1, end); }
.mascot-anim.is-tap.is-tap2 > .mf-tap-b { animation: mascot-tap-hop-w 1s ease-in-out, mascot-tap-wave-b 1s steps(1, end); }
/* a lying Yuzu (resting) and the loader's (inside .cx-ex, which clips) hop 4 %, not 8 % */
.mascot-anim.mascot-resting.is-tap > .mf-tap, .ex-loading .mascot-anim.is-tap > .mf-tap { animation: mascot-tap-hop-low 1s ease-in-out; }
.mascot-anim.mascot-resting.is-tap.is-tap2 > .mf-tap, .ex-loading .mascot-anim.is-tap.is-tap2 > .mf-tap { animation: mascot-tap-hop-low-w 1s ease-in-out; }
/* the last 0.1 s: his blink hides the face going back from happy to idle. The blink layer's own loop is untouched
   (opacity !important only overrides its value), so nothing restarts; a pose with no blink keeps its tap face */
.mascot-anim.is-tap-end > .mf-blink { visibility: visible; opacity: 1 !important; }
.mascot-anim.is-tap-end > .mf-blink ~ .mf-tap, .mascot-anim.is-tap-end > .mf-blink ~ .mf-tap-a, .mascot-anim.is-tap-end > .mf-blink ~ .mf-tap-b { visibility: hidden; }
/* off screen (.is-away) or in a hidden tab (.is-hidden), both set by app.js: hold every loop where it is. !important on
   purpose: each pose's `animation` shorthand resets animation-play-state, so neither order nor specificity is enough */
.mascot-anim.is-away, .mascot-anim.is-away > img, .mascot-anim.is-hidden, .mascot-anim.is-hidden > img { animation-play-state: paused !important; }
/* ---- end of the Yuzu block (tools/mascot/timeline.js writes everything from the opening line to this one) ---- */
.mascot-correct { transform-origin: 50% 85%; animation: mascot-pop .45s cubic-bezier(.3, 1.4, .6, 1) both; }
/* round arrow and play keys: decorative inside a card that is itself the link */
.go-arrow {
  margin-left: auto; width: 32px; height: 32px; border-radius: 50%; flex: none; display: grid; place-items: center;
  background: var(--c-raised); color: var(--c-ink); border: var(--c-bw) solid var(--c-line); box-shadow: 0 2px 0 var(--c-slab);
}
.go-arrow svg { width: 16px; height: 16px; stroke-width: 2.8; }
.playkey {
  width: 48px; height: 48px; border-radius: 50%; flex: none; display: grid; place-items: center;
  background: var(--c-raised); color: var(--c-ink); border: var(--c-bw) solid var(--c-line); box-shadow: var(--c-sh-key);
}
.playkey svg { width: 20px; height: 20px; }
.playkey.play svg { fill: currentColor; width: 18px; height: 18px; margin-left: 2px; }
/* a neutral row card (actionCard in app.js): icon tile, label, title, line, chips, arrow */
.ui-card.act { display: flex; align-items: center; gap: 12px; padding: 12px 14px; }
.act-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.act .ui-label { color: var(--c-muted); }
.act-title { font: 800 16.5px/1.2 var(--sans); letter-spacing: -.01em; color: var(--c-ink); }
.act-line { font: 400 14px/1.35 var(--sans); color: var(--c-muted); }
.act-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }

/* ============================================================
   Inputs and the custom checkbox row
   ============================================================ */
.ui-input, .pf-search, .cost-field input, .cost-field select, .control select, .pf-field select {
  min-height: 48px; padding: 0 14px; border-radius: var(--c-r-key); border: var(--c-bw) solid var(--c-line);
  background: var(--c-raised); color: var(--c-ink); font: 500 16px/1.2 var(--sans);
  box-shadow: inset 0 3px 0 var(--c-recess);
}
.control select { min-height: 44px; }
.ui-input { display: block; width: 100%; margin: 4px 0; }
.ui-input::placeholder, .pf-search::placeholder { color: var(--c-muted); opacity: 1; }
.ex-editor {
  background: var(--c-code-bg); color: var(--c-code-ink); caret-color: var(--c-code-kw);
  border: var(--c-bw) solid var(--c-line); border-radius: var(--c-r-md); box-shadow: var(--c-sh);
  padding: 14px 16px; font: 400 16px/1.6 var(--code); tab-size: 4;
}
.ex-editor::selection { background: rgba(169, 184, 255, .35); }
/* the editor's way out for keyboard users, visible to everyone (app.js renderExercise) */
.ex-kbd-help { margin: 6px 0 0; font: 400 var(--fs-small, 14px)/1.4 var(--sans); color: var(--c-muted); }
@media (min-width: 900px) { .ex-editor { font-size: 14px; } .ui-input, .pf-search { font-size: 15px; } }
/* checkbox rows: the recovery-code confirmation (Settings), project and milestone lists */
.proj-item, .milestone label {
  display: flex; gap: 12px; align-items: center; min-height: 44px; padding: 10px 14px; cursor: pointer;
  background: var(--c-raised); color: var(--c-ink); border: var(--c-bw-sm) solid var(--c-line); border-radius: 12px;
  font: 500 15px/1.4 var(--sans); opacity: 1;
}
.proj-item input[type="checkbox"], .milestone input[type="checkbox"] {
  -webkit-appearance: none; appearance: none; margin: 0; flex: none; cursor: pointer;
  width: 22px; height: 22px; border: var(--c-bw-sm) solid var(--c-line); border-radius: 6px; background: var(--c-raised);
}
.proj-item input[type="checkbox"]:checked, .milestone input[type="checkbox"]:checked {
  background: var(--c-ok) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2317140F' stroke-width='3.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7.5'/%3E%3C/svg%3E") center / 16px no-repeat;
}
.proj-item.checked, .milestone.checked label { opacity: 1; color: var(--c-muted); }

/* ============================================================
   Progress
   ============================================================ */
/* segmented bar: modules in a phase (segbar() in app.js). Separators are drawn in the
   background; the fill snaps to whole segments because the width is done/total. */
.segbar {
  --seg-line: var(--c-line); --seg-bg: var(--c-raised);
  position: relative; display: block; flex: 1 1 auto; min-width: 60px; height: 16px;
  border: var(--c-bw) solid var(--c-line); border-radius: 6px; overflow: hidden;
  background: linear-gradient(90deg, transparent calc(100% - 2px), var(--seg-line) 0) 0 0 / calc(100% / var(--n, 10)) 100% repeat-x, var(--seg-bg);
}
.segbar.dense { background: var(--seg-bg); }
.segbar i { position: absolute; left: 0; top: 0; bottom: 0; background: var(--c-fill); }
/* step blocks: one per step in the current section (player, sessions) */
.lrn-secbar { display: flex; align-items: center; gap: 4px; height: 16px; }
.lrn-secbar > .lrn-seg {
  flex: 1 1 0; min-width: 0; height: 12px; border: 2px solid var(--c-line); border-radius: 4px;
  background: var(--c-raised); opacity: 1;
}
.lrn-secbar > .lrn-seg.past { background: var(--c-fill); opacity: 1; }
.lrn-secbar > .lrn-seg.current {
  height: 16px; border-width: var(--c-bw);
  background: linear-gradient(90deg, var(--c-fill) 0 50%, var(--c-raised) 50%);
}
/* continuous bar */
.ui-bar {
  height: 14px; border: var(--c-bw) solid var(--c-line); border-radius: 8px;
  background: var(--c-recess); overflow: hidden;
}
.ui-bar > span { display: block; height: 100%; background: var(--c-fill); }
.ui-bar.sm { height: 12px; border-width: 2px; }

/* ============================================================
   Layout helpers
   ============================================================ */
.ui-grid { display: grid; gap: var(--ui-gap); grid-template-columns: repeat(2, minmax(0, 1fr)); }
.ui-grid.one { grid-template-columns: minmax(0, 1fr); }
.ui-stack { display: grid; gap: var(--ui-gap); }
.ui-row { display: flex; align-items: center; gap: 14px; min-width: 0; }
.ui-row.between { justify-content: space-between; }
.ui-spacer { flex: 1 1 auto; }
@media (max-width: 359px) { .ui-grid { grid-template-columns: minmax(0, 1fr); } }
@media (min-width: 900px) { .ui-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1240px) { .ui-grid.wide { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* ============================================================
   Focus, press motion, reduced motion
   ============================================================ */
/* No border-radius here on purpose: the outline follows each element's own radius, and
   setting one on :focus-visible would reshape the focused element itself. */
:focus-visible { outline: 3px solid var(--c-focus); outline-offset: 3px; }
/* "Skip to content" (app.js): the first Tab stop on every screen, visually hidden (the .vh
   technique, so it stays focusable) until it has focus */
.skip-link {
  position: fixed; left: 12px; top: 10px; z-index: 1000; min-height: 44px; padding: 0 16px;
  background: var(--c-raised); color: var(--c-ink); border: var(--c-bw) solid var(--c-line); border-radius: 13px;
  box-shadow: var(--c-sh-key); font: 700 15px/1 var(--sans); cursor: pointer;
}
.skip-link:not(:focus) {
  width: 1px; height: 1px; min-height: 0; padding: 0; border: 0; box-shadow: none;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
/* where script puts focus after a route change or a skip (tabindex=-1, not Tab stops and not
   controls): the next Tab starts from there, so no ring is drawn around a title or the page */
#page:focus, .step-title:focus, .lrn-stage:focus, .step-content [tabindex="-1"]:focus { outline: none; }
.ex-editor:focus-visible, .ui-input:focus-visible, .pf-search:focus-visible { outline-offset: 2px; }
/* Reduced motion: the tilt token goes to 0 here; every press / lift / tilt transform is
   switched off in ONE block at the very end of ui-learn.css (the last stylesheet), with
   !important, because the motion rules carry :not() and later positions that a plain
   override here lost to (keys still moved 2px under reduce). */
@media (prefers-reduced-motion: reduce) {
  :root { --match-tilt: 0deg; }
  .mascot { animation: none !important; }
  .mascot-anim > img { animation: none !important; }
}
/* the same, from Settings > Motion > Off (html data-motion="reduced"): under Full, Yuzu moves for as
   long as he is on screen beside the words (WCAG 2.2.2), so the app itself offers the off switch.
   (Settings > Motion > Calm needs no rule here: it only sets --mascot-loop in the Yuzu block
   above.) */
:root[data-motion="reduced"] { --match-tilt: 0deg; }
:root[data-motion="reduced"] .mascot, :root[data-motion="reduced"] .mascot-anim > img { animation: none !important; }

/* ============================================================
   Scrollbars: slim and themed inside the app (the rail, the step list, chip rows, tables,
   code). A transparent track with a muted thumb on light surfaces; on the code slate (dark in
   every palette) the thumb is the slate's own muted ink. Never hidden where content scrolls.
   scrollbar-width/-color serve Firefox and Chromium 121+; ::-webkit-scrollbar serves Safari
   (Chromium ignores it wherever the standard properties are set).
   ============================================================ */
.rail, .stepper, .pf-chips, .ftabs, .prose table, .table-scroll {
  scrollbar-width: thin; scrollbar-color: var(--c-muted) transparent;
}
.prose pre, pre.starter, .codecard, .ex-editor, .ex-out {
  scrollbar-width: thin; scrollbar-color: var(--c-code-cm) transparent;
}
:is(.rail, .stepper, .pf-chips, .ftabs, .prose table, .table-scroll, .prose pre, pre.starter, .codecard, .ex-editor, .ex-out)::-webkit-scrollbar {
  width: 10px; height: 10px; background: transparent;
}
:is(.rail, .stepper, .pf-chips, .ftabs, .prose table, .table-scroll, .prose pre, pre.starter, .codecard, .ex-editor, .ex-out)::-webkit-scrollbar-track,
:is(.rail, .stepper, .pf-chips, .ftabs, .prose table, .table-scroll, .prose pre, pre.starter, .codecard, .ex-editor, .ex-out)::-webkit-scrollbar-corner {
  background: transparent;
}
:is(.rail, .stepper, .pf-chips, .ftabs, .prose table, .table-scroll)::-webkit-scrollbar-thumb {
  background: var(--c-muted); border: 2px solid transparent; border-radius: 10px; background-clip: padding-box;
}
:is(.prose pre, pre.starter, .codecard, .ex-editor, .ex-out)::-webkit-scrollbar-thumb {
  background: var(--c-code-cm); border: 2px solid transparent; border-radius: 10px; background-clip: padding-box;
}
/* the page's own scrollbar under a café palette (themes.css): the palette's muted thumb on its
   own page colour, instead of the browser's grey-and-white strip beside the
   matcha-foam or oat page. Chunky Play (no data-palette) keeps the browser default, unchanged.
   Colour only: the bar keeps its width, so nothing shifts. scrollbar-color is inherited, so
   body resets it: every inner scroller keeps exactly the styling it had (the viewport's bar
   takes the root element's value). */
:root[data-palette] { scrollbar-color: var(--c-muted) var(--c-ground); }
:root[data-palette] body { scrollbar-color: auto; }

/* ============================================================
   Shell: phone tab bar (<900) / desktop topbar + sidebar rail (>=900)
   One list of five destinations (app.js NAV_TABS), rendered twice.
   ============================================================ */
.shell { min-height: 100vh; display: flex; flex-direction: column; }
.frame { display: flex; flex: 1; min-height: 0; }
.page { flex: 1; min-width: 0; }
.page-inner { max-width: 880px; margin: 0 auto; }
.tabbar { display: none; }
/* ---- desktop topbar ---- */
.topbar {
  display: flex; align-items: center; gap: 20px; flex-shrink: 0;
  height: 64px; padding: 0 24px; background: var(--c-raised);
  border-bottom: var(--c-bw) solid var(--c-line); position: sticky; top: 0; z-index: 40;
}
.wordmark {
  display: inline-flex; align-items: center; gap: 9px; min-height: 44px; font: 800 19px/1 var(--sans);
  letter-spacing: -.02em; color: var(--c-ink); text-decoration: none; white-space: nowrap;
}
.wordmark:hover { text-decoration: none; }
.wm-logo {
  width: 30px; height: 30px; border-radius: 9px; flex: none; display: grid; place-items: center;
  background: var(--c-fill); color: var(--c-on-fill); transform: rotate(-4deg);
}
.wm-logo svg { width: 18px; height: 18px; display: block; }
.topbar-progress { margin-left: auto; display: flex; align-items: center; gap: 12px; min-width: 0; }
.topbar-progress-label {
  font: 400 12px/1.3 var(--mono); color: var(--c-muted); white-space: nowrap;
  min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
.topbar-progress-track {
  flex-shrink: 0; width: 160px; height: 14px; border: 2px solid var(--c-line); border-radius: 8px;
  background: var(--c-recess); overflow: hidden;
}
.topbar-progress-fill { height: 100%; width: 0%; background: var(--c-fill); transition: width .4s ease; }

/* ---- desktop rail ---- */
.rail {
  width: 292px; flex-shrink: 0; background: var(--c-ground);
  border-right: var(--c-bw) solid var(--c-line); padding: 18px 0 40px;
  overflow-y: auto; position: sticky; top: 64px; height: calc(100vh - 64px);
  scroll-padding-block: 8px;
}
@media (min-width: 900px) {
  .page { padding: 32px 40px 80px; }
  /* every page starts at the same left edge, so the header never jumps between tabs (the
     focused sessions, the player and a single problem keep their own centred column) */
  .page-inner { margin-left: 0; margin-right: auto; }
}
.nav-item {
  display: flex; align-items: center; gap: 12px; min-height: 44px; margin: 0 14px 6px; padding: 0 12px;
  border-radius: 12px; border: var(--c-bw-sm) solid transparent;
  font: 700 15px/1.2 var(--sans); color: var(--c-ink); text-decoration: none;
  transition: transform .08s ease, box-shadow .08s ease;
}
.nav-item:hover { background: var(--c-raised); border-color: var(--c-line); text-decoration: none; }
.nav-item[aria-current="page"] {
  background: var(--c-fill); color: var(--c-on-fill); border-color: var(--c-line); box-shadow: var(--c-sh-key);
}
.nav-ic { display: flex; flex: none; color: inherit; }
.nav-ic svg { width: 20px; height: 20px; display: block; }
.nav-item-label { flex: 1; min-width: 0; }
.nav-group { margin-bottom: 6px; }
/* the five destinations come first; the phase tree is its own block under them, so Practice,
   Stats and Settings are never buried below an open phase's modules */
.rail-tree { margin-top: 10px; padding-top: 12px; border-top: var(--c-bw-sm) solid var(--c-line-soft); }
.rail-tree-label { margin: 0 22px 4px; color: var(--c-muted); }
/* full-width rows: an inset ring, or the rail's scroll box clips its sides */
.rail-phase-head:focus-visible, .rail-mod:focus-visible { outline-offset: -3px; }
.rail a { scroll-margin-block: 10px; }   /* app.js scrolls a keyboard-focused link fully in */
.rail-count {
  margin-left: auto; font: 700 12px/1.4 var(--mono); border: 2px solid currentColor; border-radius: 7px;
  padding: 0 6px; font-variant-numeric: tabular-nums;
}
.rail-phase { margin-top: 4px; }
.rail-phase-head {
  display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 4px 22px;
  text-decoration: none; color: var(--c-ink);
}
.rail-phase-head:hover { text-decoration: none; }
.rail-phase-num {
  flex: none; min-width: 28px; height: 22px; padding: 0 3px; display: grid; place-items: center;
  background: var(--pc, var(--c-raised)); color: var(--c-on-color); border: 2px solid var(--c-line); border-radius: 7px;
  font: 700 12px/1 var(--mono);
}
.rail-phase-name {
  flex: 1; min-width: 0; font: 700 12px/1.3 var(--mono); letter-spacing: .05em; text-transform: uppercase;
  color: var(--c-ink); text-decoration-thickness: 2px; text-underline-offset: 3px;
}
.rail-phase-head:hover .rail-phase-name, .rail-phase-head.active .rail-phase-name { text-decoration: underline; }
.rail-phase-count, .rail-pct { flex: none; font: 400 12px/1.3 var(--mono); color: var(--c-muted); }
/* an elective module (optional: true): a small outlined tag, never counted in the phase totals */
.rail-opt { flex: none; font: 700 12px/1.2 var(--mono); color: var(--c-muted); border: 1.5px solid var(--c-line-soft); border-radius: 999px; padding: 1px 7px; margin-left: 6px; }
.rail-phase:not(.open) .rail-mod { display: none; }   /* only the current phase unfolds */
.rail-mod {
  display: flex; align-items: center; gap: 10px; min-height: 44px; margin: 0 14px 2px 22px; padding: 6px 10px;
  border-radius: 10px; border: var(--c-bw-sm) solid transparent;
  font: 500 14px/1.3 var(--sans); color: var(--c-ink-2); text-decoration: none;
}
.rail-mod:hover { background: var(--c-raised); color: var(--c-ink); text-decoration: none; }
.rail-mod.active { background: var(--c-raised); border-color: var(--c-line); box-shadow: var(--c-sh-sm); font-weight: 700; color: var(--c-ink); }
.rail-mod-title { flex: 1; min-width: 0; }
/* done boxes: a square with a CSS check, never a glyph */
.rail-dot {
  position: relative; flex: none; width: 16px; height: 16px; border-radius: 5px;
  border: 2px solid var(--c-line); background: var(--c-raised); font-size: 0;
}
.rail-mod.done .rail-dot { background: var(--c-ok); }
.rail-mod.done .rail-dot::after {
  content: ""; position: absolute; left: 4px; top: 1px; width: 4px; height: 8px;
  border: solid var(--c-on-color); border-width: 0 2px 2px 0; transform: rotate(45deg);
}

/* ---- phone (<900): the docked tab bar replaces the topbar and rail. Declared AFTER the
   topbar/rail base rules so its display:none wins. ---- */
@media (max-width: 899px) {
  /* The tab bar is the only nav on a phone; each screen prints its own header. */
  .topbar { display: none; }
  .frame > .rail { display: none; }
  .tabbar {
    display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 60; transform: none; max-width: none;
    height: calc(var(--ui-tabbar-bar-h) + var(--ui-safe-b)); padding: 6px 8px var(--ui-safe-b);
    background: var(--c-raised); border: 0; border-top: var(--c-bw) solid var(--c-line); border-radius: 0;
    box-shadow: none; -webkit-backdrop-filter: none; backdrop-filter: none;
  }
  .tab-item {
    flex: 1 1 0; min-width: 0; min-height: 50px; display: flex; flex-direction: column; align-items: center;
    justify-content: flex-start; gap: 3px; padding: 0; background: none; border-radius: 12px;
    color: var(--c-muted); text-decoration: none; -webkit-tap-highlight-color: transparent;
  }
  .tab-item:hover { text-decoration: none; }
  .tab-item .nav-ic {
    width: 48px; height: 30px; border-radius: 11px; display: grid; place-items: center; color: var(--c-ink);
    transition: transform .08s ease;
  }
  .tab-item .nav-ic svg { width: 22px; height: 22px; display: block; }
  .tab-label { font: 600 12px/1.1 var(--sans); }
  .tab-item[aria-current="page"] { color: var(--c-ink); background: none; }
  .tab-item[aria-current="page"] .tab-label { font-weight: 800; }
  .tab-item[aria-current="page"] .nav-ic { background: var(--c-fill); color: var(--c-on-fill); box-shadow: 0 3px 0 var(--c-slab); }
  .tab-item:active .nav-ic { transform: translateY(2px); }
  .page { padding: calc(12px + env(safe-area-inset-top, 0px)) 16px calc(var(--ui-tabbar-h) + var(--ui-safe-b) + 28px); }
  /* a thumb-sized dock on every phone (the home-screen icon opens the site in the browser on purpose, see the manifest,
     so there is no separate installed-app mode to size for): taller targets, bigger icons and labels.
     --ui-tabbar-bar-h moves every screen that sits on the bar. */
  :root { --ui-tabbar-bar-h: 78px; }
  .tabbar { padding-top: 8px; }
  .tab-item { min-height: 62px; gap: 5px; }
  .tab-item .nav-ic { width: 58px; height: 38px; border-radius: 13px; }
  .tab-item .nav-ic svg { width: 27px; height: 27px; }
  .tab-label { font-size: 13px; }
}

/* ============================================================
   Home — top bar, level card, today's question, tiles, next-lesson CTA, stat tiles
   ============================================================ */
.home-page { display: grid; gap: 12px; }
.home-top { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; min-height: 44px; }
/* the greeting row (app.js greetRow): the mascot, a time-of-day hello and one line. On a phone
   it wraps onto its own line under the wordmark and the streak; from 900px (no wordmark here,
   the topbar has it) it takes the row beside the streak sticker. */
.home-greet { order: 3; flex: 1 0 100%; display: flex; align-items: center; gap: 12px; min-width: 0; }
.home-greet .mascot, .home-greet .yz-gap { width: 76px; height: 76px; margin: -2px 0 -4px -6px; }   /* .yz-gap: his slot while the question's Yuzu talks */
.home-greet-text { min-width: 0; }
/* Settings > Motion, said once (app.js yuzuTip): a quiet note on its own line at the end of the
   greeting row, inside .home-top, so the wide Home grid is untouched */
.home-tip { order: 9; flex: 1 0 100%; margin: 2px 0 0; padding: 10px 12px; border: var(--c-bw) solid var(--c-line); border-radius: var(--c-r-md); background: var(--c-card); font: 400 14px/1.4 var(--sans); color: var(--c-ink-2); }
.home-tip p { margin: 0 0 8px; }
.home-tip-acts { display: flex; flex-wrap: wrap; gap: 8px; }
.home-tip-acts .ui-chip { min-height: 44px; }
.home-greet-title { margin: 0; font: 800 var(--fs-title)/1.1 var(--sans); letter-spacing: -.015em; color: var(--c-ink); }
.home-greet-line { margin: 3px 0 0; font: 400 15px/1.35 var(--sans); color: var(--c-ink-2); text-wrap: pretty; }   /* no lone "start." on a phone */
.streak-stk {
  margin-left: auto; margin-right: 4px; display: inline-flex; align-items: center; gap: 6px; height: 34px;
  padding: 0 14px 0 10px; border-radius: 17px; background: var(--c-fill); color: var(--c-on-fill);
  border: 3px solid var(--c-raised); box-shadow: 0 0 0 2.5px var(--c-line), 3px 3px 0 2.5px var(--c-slab);
  transform: rotate(-3deg); white-space: nowrap; flex: none;
}
.streak-stk svg { width: 18px; height: 18px; fill: currentColor; stroke: currentColor; }
.streak-stk .n { font: 800 17px/1 var(--sans); }
.streak-stk .u { font: 700 13px/1 var(--mono); letter-spacing: .03em; }

/* level card */
.ui-card.lvl-card { padding: 0; overflow: hidden; }
.lvl-head { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: flex-end; gap: 12px; padding: 12px 14px 0; }
/* the level label and name take the room the XP line leaves (a basis of 0, so the line wraps only
   when even the name's longest word cannot sit beside the XP). A title is one unbreakable word at its
   longest: beside the XP line, "Distinguished" cut the XP off at 320-360px (R1.05 review) and
   "Practitioner" at 320px. Now the XP line moves under the name instead, still on the right. */
.lvl-head > div { flex: 1 1 0; }
.lvl-name { font: 800 var(--fs-display)/1 var(--sans); letter-spacing: -.02em; margin: 3px 0 0; color: var(--c-ink); }
.lvl-xp { text-align: right; font: 400 13.5px/1.3 var(--sans); margin: 0 0 0 auto; flex: none; }
.lvl-xp b { font: 700 13px var(--mono); }
.lvl-zone { position: relative; height: 92px; margin: 8px 14px 12px; }
.lvl-stairs {
  position: absolute; left: 0; right: 0; bottom: 0; display: flex; align-items: flex-end; gap: 6px;
  margin: 0; padding: 0; list-style: none;
}
.lvl-step { position: relative; flex: 1 1 0; min-width: 0; height: var(--h); }
.lvl-step.cur { flex-grow: 1.9; }
/* ten stairs (levels 9 and 10 since R1.05; app.js levelStairs sets .ten): with the 6px gap and a
   1.9x current stair, 320px left about 18px a stair and "10" (12px mono, about 15px wide) was
   clipped by the block's borders and overflow. A gap that narrows with the viewport and a
   slimmer current stair keep every number whole at 320px, also beside a classic 15px scrollbar
   (tools/verify_levels.js --render checks it). The seal is absolutely placed; on the end stairs it
   is kept inside the card by the .seal rules below. */
.lvl-stairs.ten { gap: clamp(2px, .9vw, 6px); }
.lvl-stairs.ten .lvl-step.cur { flex-grow: 1.4; }
.lvl-step .blk {
  position: absolute; inset: 0; border: var(--c-bw) solid var(--c-line); border-radius: 8px 8px 5px 5px;
  background: var(--c-raised); overflow: hidden; box-shadow: inset 0 -4px 0 rgba(0, 0, 0, .1);
}
.lvl-step b {
  position: absolute; left: 0; right: 0; top: 3px; z-index: 1; text-align: center;
  font: 700 12px/1 var(--mono); color: var(--c-ink);
}
.lvl-step .blk i { position: absolute; left: 0; right: 0; bottom: 0; background: var(--c-fill); }
.lvl-step.done .blk, .lvl-step.cur .blk { background: var(--c-fill); }
.lvl-step.done b { color: var(--c-on-fill); }
/* the next step fills with your progress, so its number may straddle the fill line: the fill
   carries a second, on-fill copy of the number at the same spot and clips it, so the digit
   reads ink above the line and cream below it (content box = --h minus both borders; the
   number sits 3px from the top and is 12px tall, so its bottom is --h - 20px from the floor) */
.lvl-step.next b { z-index: auto; }
.lvl-step.next .blk i { overflow: hidden; }
.lvl-step.next .blk i b { top: auto; bottom: calc(var(--h) - 20px); color: var(--c-on-fill); }
.lvl-step .seal {   /* 68px = the seal's own viewBox, so its 12-unit "LVL" is 12px, not 11.3 */
  position: absolute; left: 50%; bottom: min(calc(var(--h) - 4px), 22px); width: 68px; height: 68px;
  transform: translateX(-50%) rotate(-8deg); z-index: 2;
}
/* The seal is wider than a stair. Centred on the first or the last stair (level 1, which every new
   learner sees, and level 10) it crossed the card's edge, and the Home card's overflow cut its points
   flat (R1.05 review). There its centre stays at least 26px inside the stair's outer edge, so even the
   corners of its rotated 68px box (38.4px from the centre at -8deg) end within 12.4px of the stair:
   inside the Home zone's 14px margin, so the card never clips or scrolls it, and inside the Stats
   card's padding. */
.lvl-step.cur:first-child .seal { left: max(50%, 26px); }
.lvl-step.cur:last-child .seal { left: min(50%, calc(100% - 26px)); }
/* Ten narrow stairs: the 68px seal is wider than the current stair and its two neighbours' gaps, so
   it covered the numbers on the stairs either side (level 10 at 320-360px hid the "9" entirely; R1.05
   review). The seal's lowest point sits 22px above the floor (20px at level 2), so on narrow stairs
   those two numbers move to the foot of their stair (4px up, 6.5-18.5px above the floor), below the
   seal; the next stair's on-fill copy moves with its number. The switch is on the stairs' OWN width
   (a container query on .lvl-zone), not the viewport's: the Home and Stats cards differ, and the
   desktop grid narrows the card again (about 400px of stairs at a 1280px window). Measured: the seal
   touched a neighbour's number at up to 336px of stairs and at none from 346px; 400px leaves a
   margin. tools/verify_levels.js --render checks every number against the seal's own outline at
   levels 1-10. */
.lvl-zone { container: lvlzone / inline-size; }
@container lvlzone (max-width: 400px) {
  .lvl-stairs.ten .lvl-step.cur + .lvl-step b,
  .lvl-stairs.ten .lvl-step:has(+ .cur) b { top: auto; bottom: 4px; }
}
.goal-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 14px 10px;
  background: var(--c-raised); border-top: var(--c-bw) solid var(--c-line);
}
.goal-val { font: 600 15px/1.3 var(--sans); margin: 2px 0 0; }
.goal-val b { font: 700 15px var(--mono); }
.goal-blocks { display: flex; gap: 4px; flex: none; }
.goal-blocks i {
  display: block; width: 22px; height: 18px; border: 2.2px solid var(--c-line); border-radius: 6px; background: var(--c-ground);
}
.goal-blocks i.full { background: var(--c-fill); }
.goal-blocks i.part { background: linear-gradient(90deg, var(--c-fill) 0 var(--p), var(--c-ground) var(--p)); }

/* today's question */
.ui-card.today-q { background: var(--c-panel); padding: 10px 12px 12px; }
.qhead { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 30px; }
.q-sig { width: 60px; height: 40px; flex: none; transform: rotate(5deg); margin-right: -2px; }
.today-q .qq { border: 0; padding: 0; margin-top: 8px; }
.today-q .qq-num { display: none; }
.today-done { display: flex; align-items: center; gap: 12px; margin-top: 10px; }
.today-done-main { flex: 1; min-width: 0; }
.today-done-title { font: 800 var(--fs-h3)/1.2 var(--sans); letter-spacing: -.01em; margin: 0; color: var(--c-ink); }
.today-done .st { margin-bottom: 4px; }
.today-done .mascot { width: 72px; height: 72px; margin: -4px 0 -4px -4px; }   /* a missed answer: oops */
.today-done a {   /* a secondary action: ink, like .link-btn (blue belongs to the Next lesson card) */
  display: inline-flex; align-items: center; min-height: 44px; color: var(--c-ink); font: 700 14.5px/1.2 var(--sans);
  text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 3px;
}
/* the answered card's two actions: re-open the explanation here, or go to the lesson */
.today-done-links { display: flex; flex-wrap: wrap; align-items: center; gap: 0 18px; margin: 2px 0 0; }
.today-why-btn { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 3px; }
.today-why { margin-top: 10px; padding-top: 10px; border-top: var(--c-bw-sm) solid var(--c-line-soft); }
.today-why .qq-text { margin: 0 0 8px; font-weight: 700; }
.today-why-ans { margin: 0 0 8px; display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 8px; }
/* after answering, the explanation stays until the learner closes it (secondary key: blue is the Next lesson card's) */
.today-got { margin-top: 12px; }

/* action tiles */
.home-duo { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
@media (max-width: 359px) { .home-duo { grid-template-columns: minmax(0, 1fr); } }
.ui-card.tile { display: flex; flex-direction: column; gap: 2px; padding: 9px 10px 11px; }
.tile-row { display: flex; align-items: center; gap: 5px; margin-bottom: 6px; min-width: 0; }
/* the chip gives way (ellipsis) before the round arrow can spill past the tile's edge */
.tile-row .chip { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* two tiles a row on a narrow phone leave no room for icon + chip + arrow: the arrow is
   decorative (the whole tile is the link), so it gives way and the time chip stays whole */
@media (min-width: 360px) and (max-width: 379px) { .tile .go-arrow { display: none; } }
.tile .ui-icon { width: 36px; height: 36px; }
.tile .ui-icon svg { width: 20px; height: 20px; }
.tile .ui-label { color: var(--c-muted); }
.tile-title {
  font: 800 16.5px/1.15 var(--sans); letter-spacing: -.01em; margin-top: 2px; color: var(--c-ink);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* next lesson: the go-blue card */
.ui-card.cta-go { display: flex; align-items: center; gap: 12px; padding: 9px 12px 10px; background: var(--c-go); color: var(--c-on-go); }
.cta-go .ui-label { color: var(--c-on-go); }
.cta-main { flex: 1; min-width: 0; }
.cta-title { display: block; font: 800 18px/1.1 var(--sans); letter-spacing: -.01em; margin-top: 2px; }
.cta-sub { display: flex; align-items: center; gap: 6px; font: 400 14px/1.3 var(--sans); margin-top: 2px; min-width: 0; }
.cta-sub span:last-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cta-chips { margin-left: auto; display: flex; flex-direction: column; align-items: flex-end; gap: 5px; flex: none; }
.cta-go .playkey { background: #FFFFFF; color: #17140F; border-color: #17140F; }   /* a white key on blue, every palette */

/* stat tiles (Home, Stats, Exam) */
.stat-tile { text-align: left; }
.stat-tile .ui-label { color: var(--c-muted); }
.stat-big { font: 800 28px/1.05 var(--sans); letter-spacing: -.02em; margin: 6px 0 2px; color: var(--c-ink); font-variant-numeric: tabular-nums; }
.stat-sub { font: 400 13px/1.35 var(--sans); color: var(--c-muted); margin: 0; }

/* very narrow phones: the CTA's chips would squeeze the module name to a few letters */
@media (max-width: 359px) {
  .cta-chips { display: none; }
  .goal-blocks { gap: 3px; }
  .goal-blocks i { width: 18px; }
}
/* 900-1199: the rail takes 292px, so two columns would be too narrow for the cards (the
   tiles' arrows spilled out and the answer keys ran one word per line): one column, in the
   phone's order, capped so it never stretches across a wide window. */
@media (min-width: 900px) {
  .home-page { max-width: 760px; gap: 16px; }
  .home-top .wordmark { display: none; }   /* the topbar carries it */
  .home-greet { order: 0; flex: 1 1 auto; }
  .home-greet .mascot, .home-greet .yz-gap { width: 88px; height: 88px; }
  .home-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1200px) {
  /* the question spans two rows; the third track is flexible, so any extra height it needs
     goes under the CTA instead of opening a gap between the level card and the CTA */
  .home-page {
    max-width: 1080px; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-rows: auto auto 1fr auto auto;
    grid-template-areas: "top top" "lvl q" "cta q" "a b" "st st"; gap: 20px; align-items: start;
  }
  .home-top { grid-area: top; }
  .lvl-card { grid-area: lvl; }
  .today-q { grid-area: q; }
  /* answered, the card is short: it still fills both rows it spans (no hole under it beside the
     CTA), with its result centred in the space under the heading */
  .today-q.answered { align-self: stretch; display: flex; flex-direction: column; }
  .today-q.answered .today-done { margin-block: auto; }
  .cta-go { grid-area: cta; }
  .home-duo.a { grid-area: a; }
  .home-duo.b { grid-area: b; }
  .home-stats { grid-area: st; }
}

/* ============================================================
   Stats — tiles, level progress, 12-week heatmap, per-phase rows
   ============================================================ */
.stats-page { display: grid; gap: 14px; }
.stat-grid { display: grid; gap: 12px; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.stats-duo { display: grid; gap: 14px; grid-template-columns: minmax(0, 1fr); }
.stats-lvl .lvl-name { font-size: var(--fs-title); margin: 4px 0 0; }
.stats-lvl .lvl-zone { margin: 12px 0 10px; }
.stats-lvl-line { font: 400 12.5px/1.3 var(--mono); color: var(--c-muted); margin: 0; }
.card-title { margin: 0 0 12px; }
/* 7 EXPLICIT rows (days) with auto-flow:column: each new cell fills the current week's
   column top-to-bottom, then starts a new column (week) — the contribution-graph layout. */
.heatmap {
  display: grid; grid-template-rows: repeat(7, 16px); grid-auto-flow: column;
  grid-auto-columns: minmax(10px, 1fr); gap: 4px; height: auto;
}
.heat-cell { display: block; border: 1.5px solid var(--c-line-soft); border-radius: 4px; background: var(--c-recess); opacity: 1; }
.heat-cell.l1 { background: var(--c-heat-1); border-color: var(--c-line); }
.heat-cell.l2 { background: var(--c-heat-2); border-color: var(--c-line); }
.heat-cell.l3 { background: var(--c-heat-3); border-color: var(--c-line); }
.heat-cell.l4 { background: var(--c-heat-4); border-color: var(--c-line); }
.heat-note { font: 400 14px/1.4 var(--sans); color: var(--c-ink-2); margin: 12px 0 0; }
.heat-note.has-mascot { display: flex; align-items: center; gap: 12px; }   /* no activity yet: resting */
.heat-note .mascot { width: 80px; height: 80px; }
.heat-note.has-mascot > span { padding-top: 22px; }   /* level with the sleeping body, not the z's above it */
.phase-list { display: grid; gap: 4px; }
.phase-row {
  display: flex; align-items: center; gap: 12px; min-height: 44px; padding: 8px 6px; margin: 0 -6px;
  border-radius: 12px; color: inherit; text-decoration: none;
}
a.phase-row:hover { background: var(--c-raised); text-decoration: none; }
a.phase-row:hover .phase-row-name { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 3px; }
.phase-row-main { flex: 1; min-width: 0; }
.phase-row-top { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; }
.phase-row-name { font: 700 15px/1.3 var(--sans); color: var(--c-ink); }
.phase-row-pct { font: 700 12.5px/1.3 var(--mono); color: var(--c-ink); flex: none; }
.phase-row .ui-bar { margin-top: 6px; }
.phase-row-sub { margin: 4px 0 0; font: 400 12.5px/1.3 var(--mono); color: var(--c-muted); }
@media (min-width: 900px) {
  .stats-page { max-width: 1000px; gap: 20px; }
  .stat-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
}
@media (min-width: 1100px) {
  .stats-duo { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: stretch; gap: 20px; }
  .phase-list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px 28px; }
}

/* ============================================================
   Settings — segmented controls, sync/passkey section, reset
   ============================================================ */
.settings-page { display: grid; gap: 14px; }
.set-card .ui-title.sm, .ui-card.prose > .ui-title { font-family: var(--sans); font-weight: 800; margin: 0 0 10px; }
.set-help { font: 400 14px/1.4 var(--sans); color: var(--c-ink-2); margin: 10px 0 0; max-width: 62ch; }
/* Settings > Motion: the device note shows only while the device asks for reduced motion (a media
   query, so it is right even if that setting changes while Settings is open) */
.set-os-note { display: none; }
@media (prefers-reduced-motion: reduce) { .set-os-note { display: block; } }
/* the sync card's own paragraphs: UI body copy, not lesson prose */
.sync-body > p:not([class]), .sync-body > p.ui-muted {
  font: 400 15px/1.5 var(--sans); color: var(--c-ink-2); max-width: 62ch; margin: 0 0 12px;
}
.set-card .controls, .sync-card .controls { gap: 10px 12px; margin-top: 12px; }
.danger-title { color: var(--c-bad); }
.seg {
  display: flex; padding: 4px; gap: 4px; background: var(--c-recess); border: var(--c-bw) solid var(--c-line);
  border-radius: 14px; box-shadow: inset 2px 2px 0 var(--c-inset); max-width: 480px;
}
/* sized by content (then sharing the rest), so a longer label stays on one line on a phone */
.seg .ui-chip {
  flex: 1 1 auto; min-height: 44px; min-width: 0; padding: 0 8px; border: 0; background: transparent; box-shadow: none;
  font: 700 14px/1.1 var(--sans); border-radius: 10px; color: var(--c-ink); white-space: nowrap;
}
.seg .ui-chip::after { display: none; }
.seg .ui-chip[aria-pressed="true"] { background: var(--c-fill); color: var(--c-on-fill); box-shadow: 0 2px 0 var(--c-slab); }
.sync-body p { margin: 0 0 12px; }
/* the backup readout ("Currently saved: ...") sits in a recess */
.sync-body .backup-state {
  background: var(--c-recess); border: 2px solid var(--c-line); border-radius: 12px;
  padding: 10px 12px; line-height: 1.6; color: var(--c-ink); max-width: none;
}
.set-card .backup-state { font: 400 14px/1.45 var(--sans); color: var(--c-muted); margin-top: 14px; }
.sync-body > .btn, .sync-body > .proj-item { margin-top: 10px; }
.sync-body > .proj-item { margin-bottom: 4px; }
.sync-alert {
  color: var(--c-ink); background: var(--c-bad-wash); border: var(--c-bw-sm) solid var(--c-bad); border-radius: 10px;
  padding: 10px 12px; font-size: 14px; margin: 8px 0 !important;
}
.recovery-code {
  display: block; padding: 14px; border-radius: 12px; background: var(--c-recess);
  border: 2px solid var(--c-line); font: 400 16px/1.4 var(--code); word-break: break-all; margin: 10px 0;
}
.passkey-list { list-style: none; margin: 12px 0; padding: 0; display: grid; gap: 8px; }
.passkey-item {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; min-height: 44px;
  padding: 8px 12px; background: var(--c-raised); border: 2px solid var(--c-line); border-radius: 12px;
}
.passkey-item .ui-mono:first-child { flex: 1; min-width: 0; }
@media (min-width: 900px) {
  .settings-page { max-width: 760px; gap: 20px; }
}

/* ============================================================
   Shared question block (questionBlock in app.js): answer keys A-D
   ============================================================ */
/* The question block is a size container: whether its answer keys sit two-up depends on the
   width the block actually has, not on the window. On a 1200-1300px desktop Home's two
   columns left each key 184-208px and options ran to 4-5 lines; there they now stack. */
.qq { padding: 14px 0 4px; border-top: var(--c-bw-sm) solid var(--c-line-soft); container-type: inline-size; }
.qq-text { font: 800 17px/1.3 var(--sans); letter-spacing: -.01em; color: var(--c-ink); margin: 0 0 10px; }
.qq-num {
  display: inline-block; font: 700 12px/1 var(--mono); background: var(--c-fill); color: var(--c-on-fill);
  border-radius: 6px; padding: 3px 6px; margin-right: 8px; vertical-align: 2px; letter-spacing: 0;
}
.qq-num:empty { display: none; }
.qq-opts { display: grid; gap: 9px; grid-template-columns: minmax(0, 1fr); }
/* fit-s (every option <= 48 characters): two-up from a 296px block (a 360px phone);
   fit-m (<= 140): two-up only from 600px, so each key keeps ~290px */
@container (min-width: 296px) { .qq-opts.fit-s { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@container (min-width: 600px) { .qq-opts.fit-m { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@supports not (container-type: inline-size) {
  @media (min-width: 360px) { .qq-opts.fit-s { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
  @media (min-width: 700px) and (max-width: 1199px) { .qq-opts.fit-m { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
}
/* min-width 0 + overflow-wrap anywhere: a long unbroken run (code) wraps inside its key instead
   of running past the border or widening the page (app.js optsFit() also stacks such options) */
.qq-opt {
  position: relative; display: block; width: 100%; min-width: 0; overflow-wrap: anywhere; min-height: 48px; text-align: left; padding: 9px 12px 10px; cursor: pointer;
  background: var(--c-key); color: var(--c-ink); border: var(--c-bw-sm) solid var(--c-line); border-radius: 12px;
  box-shadow: 0 3px 0 var(--c-slab); font: 400 15px/1.35 var(--sans); opacity: 1;
  transition: transform .08s ease, box-shadow .08s ease; -webkit-tap-highlight-color: transparent;
}
.qq-opt::before { content: none; }
/* the key is 22px, taller than one 20.25px line: the -4px bottom margin keeps its float box
   inside the first line, so only line 1 sits beside it and line 2 returns to the left edge
   (without it the first TWO lines were indented, a stair-step) */
.qq-key {
  float: left; display: grid; place-items: center; width: 24px; height: 22px; margin: 0 8px -4px 0;
  border: var(--c-bw-sm) solid var(--c-line); border-radius: 6px; background: var(--c-panel); color: var(--c-ink);
  font: 700 12px/1 var(--mono);
}
@media (hover: hover) {
  .qq-opt:hover:not([disabled]) { transform: translateY(-1px); box-shadow: 0 4px 0 var(--c-slab); }
}
.qq-opt:active:not([disabled]) { transform: translateY(2px); box-shadow: 0 1px 0 var(--c-slab); }
.qq-opt[disabled] { cursor: default; opacity: 1; box-shadow: none; background: var(--c-card); color: var(--c-muted); }
/* answered: the fill changes, and questionBlock puts ✓ / ✗ in the key in place of the letter.
   Padding, weight and size stay exactly as they were, so the option text does not move. */
.qq-opt.correct { background: var(--c-ok); color: var(--c-on-color); border-color: var(--c-line); }
.qq-opt.correct .qq-key { background: var(--c-on-color); color: var(--c-ok); border-color: var(--c-on-color); }
.qq-opt.wrong { background: var(--c-bad-wash); color: var(--c-ink); border-color: var(--c-bad); }
.qq-opt.wrong .qq-key { color: var(--c-bad); border-color: var(--c-bad); }
.qq-key.mark { font: 800 15px/1 var(--sans); }
.qq-why {
  margin-top: 10px; padding: 10px 12px; border-radius: 12px; background: var(--c-recess);
  border: var(--c-bw-sm) solid var(--c-line); font: 400 14px/1.45 var(--sans); color: var(--c-ink);
}
.qq-why.ok { background: var(--c-ok-wash); }
.qq-why.no { background: var(--c-bad-wash); }   /* the red label on recess was 4.48:1; on bad-wash it is 4.58 / 5.93 */
.qq-why b { font: 700 12px var(--mono); letter-spacing: .06em; }
.qq-why.ok b { color: var(--c-ok-ink); }
.qq-why.no b { color: var(--c-bad); }
/* Yuzu explains answers (app.js yzSay): the .qq-why box becomes his thought bubble. The box keeps its
   classes, colors and words; the lead-in (.yz-lead, the verdict in words) keeps the old label's color
   pairs (ok-ink and bad on their washes, 4.5:1 in every palette and mode) at the body size. Two layouts,
   by the width of the question block (.qq is a size container):
   - narrow, under 560 px (a phone; Home's question column on a desktop): the box keeps the question's
     full width, so its words wrap exactly as the plain box's do. Yuzu (56 px) stands under its lower-left
     corner and two dots rise from his head to the box. He floats, so the Next / Got it key that follows
     sits beside him, in the row the key needs anyway;
   - wide, 560 px and up (a session or a check-in on a tablet or desktop): he sits beside the box's top
     left, facing the words, the dots running from the box to his face.
   An earlier bubble whose Yuzu moved on (.yz-past: a check-in) keeps his seat as an empty slot (.yz-gap)
   and drops the dots, so nothing below it moves; .yz-gap is also the slot Home's greeting or the Drill's
   resting Yuzu leaves while the bubble's Yuzu talks. The bubble's 0.2 s arrival is switched off with every
   other animation under reduced motion (styles.css). */
.qq { display: flow-root; }   /* holds a bubble's floated Yuzu inside its own question */
.yz-say { margin-top: 10px; }
.yz-say > .qq-why { position: relative; min-width: 0; margin: 0; border-radius: 18px; }
.yz-say > .mascot, .yz-say > .yz-gap { float: left; width: 56px; height: 56px; margin: 20px 14px 0 2px; }
.yz-bubble::before, .yz-bubble::after {
  content: ""; position: absolute; box-sizing: border-box; border: var(--c-bw-sm) solid var(--c-line); border-radius: 50%; background: inherit; pointer-events: none;
}
.yz-bubble::before { width: 14px; height: 14px; left: 53px; bottom: -19px; }
.yz-bubble::after { width: 8px; height: 8px; left: 42px; bottom: -27px; }
@container (min-width: 560px) {
  .yz-say { display: grid; grid-template-columns: 56px minmax(0, 1fr); column-gap: 18px; align-items: start; }
  .yz-say > .qq-why { grid-area: 1 / 2; min-height: 56px; }
  .yz-say > .mascot, .yz-say > .yz-gap { grid-area: 1 / 1; float: none; margin: 0; }
  .yz-bubble::before { width: 12px; height: 12px; left: -15px; top: 12px; bottom: auto; }
  .yz-bubble::after { width: 7px; height: 7px; left: -20px; top: 26px; bottom: auto; }
}
.yz-say.yz-past > .qq-why::before, .yz-say.yz-past > .qq-why::after { content: none; }
.qq-why .yz-lead { font: 800 14px/1.45 var(--sans); letter-spacing: 0; }
.yz-gap { display: block; flex: none; }
@keyframes yz-bubble-in { from { opacity: 0; transform: scale(.96); } to { opacity: 1; transform: none; } }
.yz-bubble { transform-origin: 52px 100%; animation: yz-bubble-in .2s ease-out; }
@container (min-width: 560px) { .yz-bubble { transform-origin: -14px 20px; } }
/* Even under Full motion the bubble's Yuzu plays his reaction once and then settles on his still
   picture, drawn sharp (no breath): the learner is reading here, on every answer, hundreds of times, so
   nothing loops beside the words. These are the loop's own Calm cuts (4.9 s right, 4.4 s wrong),
   scoped to the bubble; the greeting and the Drill keep their full loop. Before the loop ships the
   old poses ignore --mascot-loop and play their one-offs, which also end. */
.yz-say > .mascot-correct { --mascot-loop: 0.2076; }
.yz-say > .mascot-oops { --mascot-loop: 0.2366; }
.drill-tag {
  display: block; margin: 0 0 8px; padding: 0; background: none; border: 0;
  font: 700 12px/1.3 var(--mono); letter-spacing: .06em; text-transform: uppercase; color: var(--c-muted);
}
@media (min-width: 900px) { .qq-text { font-size: 18px; } }

/* ============================================================
   Go deeper (the optional last step, app.js goDeeperBlock): the source links keep the .res-*
   look from styles.css; below them the Credits block lists what the module’s topic list
   draws on, as plain text, in the same raised card as a source row.
   ============================================================ */
.res-item .res-name { font-weight: 700; color: var(--c-ink); }
/* "FREE PREVIEW" or "PAID" (app.js resourceAccess): the same chip and place as styles.css's FREE */
.res-item .res-access { margin-left: auto; }
.res-credits {
  margin: 22px 0 0; padding: 12px 14px 14px; max-width: 68ch; background: var(--c-raised); color: var(--c-ink);
  border: var(--c-bw-sm) solid var(--c-line); border-radius: 12px;
}
.res-credits-sub { font: 400 14px/1.45 var(--sans); color: var(--c-ink-2); margin: 4px 0 8px; }
.res-credits-list { display: grid; gap: 6px; margin: 0; padding: 0 0 0 18px; font: 400 14px/1.45 var(--sans); color: var(--c-ink-2); overflow-wrap: anywhere; }
