/* Times Tables Games: one stylesheet for every screen.

   The palette and fonts match the worksheet site so the two read as one
   family of tools. Fonts are served from fonts/, not a font service, so the
   page contacts no other site. Licenses: fonts/OFL-*.txt. */

@font-face { font-family: "Andika"; font-weight: 400; font-display: swap; src: url("fonts/andika-400.woff2") format("woff2"); }
@font-face { font-family: "Andika"; font-weight: 700; font-display: swap; src: url("fonts/andika-700.woff2") format("woff2"); }
@font-face { font-family: "Atkinson Hyperlegible"; font-weight: 400; font-display: swap; src: url("fonts/atkinson-hyperlegible-400.woff2") format("woff2"); }
@font-face { font-family: "Atkinson Hyperlegible"; font-weight: 700; font-display: swap; src: url("fonts/atkinson-hyperlegible-700.woff2") format("woff2"); }

:root {
  --desk: #e7ecf2;
  --surface: #ffffff;
  --surface-2: #f6f8fb;
  --text: #17202b;
  --muted: #586473;
  --line: #c9d2dd;
  --accent: #f5c13e;        /* pencil yellow, the brand color */
  --accent-ink: #2a2206;
  --blue: #2c5ba6;
  --blue-soft: #e3ecfa;
  --good: #16733d;        /* 5.9:1 on white and 4.97:1 on the light blue highlight, so green text passes AA everywhere it sits */
  --good-soft: #e1f4e8;
  --bad: #b42318;
  --bad-soft: #fde8e6;
  --yellow-soft: #fdf1c7;   /* Yellow's half in Two players: --accent-ink 13.97:1, --good 5.23:1, --bad 5.82:1, --muted 5.33:1, the --blue focus ring 5.89:1 */
  --level-new: #d7dee7;
  --level-known: #7cc3f0;
  --dog-evening: #f3c9a6;   /* the sky on an evening street */
  --dog-noon: #a9d4f5;      /* a clear midday blue, deeper than the morning's */
  --dog-dusk: #c7b5e3;      /* lavender over a pink horizon */
  --dog-dusk-low: #f4c6cf;
  --dog-road: #8f98a5;      /* the road, darker than the line color so what lies on it shows */
  --dog-wall: #ffffff;      /* house walls, lighter than the sky behind them */
  --font-num: "Andika", "Avenir Next", "Segoe UI", system-ui, sans-serif;
  --font-ui: "Atkinson Hyperlegible", system-ui, -apple-system, "Segoe UI", sans-serif;
  --radius: 16px;
  --shadow: 0 1px 2px rgba(23, 32, 43, 0.06), 0 8px 24px rgba(23, 32, 43, 0.08);
  /* Light only (decided 2026-10-23): one tested set of colors, whatever the system's theme. */
  color-scheme: light;
}

* { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body { margin: 0; height: 100%; }
/* Two quick taps on the same key (7 then 7 for 77) must not zoom the page on a phone.
   "manipulation" turns off only double-tap zoom: pinch zoom and scrolling still work. */
html { touch-action: manipulation; }
body {
  background: var(--desk);
  color: var(--text);
  font: 1.0625rem/1.45 var(--font-ui);   /* 17px at the default text size, and it follows the browser setting */
  -webkit-tap-highlight-color: transparent;
}
button { font: inherit; color: inherit; cursor: pointer; }
a.btn { text-decoration: none; color: inherit; }
.num { font-family: var(--font-num); font-variant-numeric: tabular-nums lining-nums; }

/* ---------- Frame ---------- */
.frame { min-height: 100%; display: flex; flex-direction: column; }
.topbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 12px;   /* wraps rather than push Chart off screen at 200% text */
  padding: 12px 16px;
  max-width: 760px; width: 100%; margin: 0 auto;
}
.topbar .brand {
  display: flex; align-items: center; gap: 10px;
  font-family: var(--font-num); font-weight: 700; font-size: 1.15rem;
  text-decoration: none; color: inherit;
}
.topbar .brand svg { width: 28px; height: 28px; flex: none; }
.topbar .spacer { flex: 1; }
main#screen {
  width: 100%; max-width: 760px; margin: 0 auto;
  padding: 8px 16px 40px;
  display: flex; flex-direction: column; gap: 20px;
}
footer.site {
  margin-top: auto; padding: 24px 16px; text-align: center;
  color: var(--muted); font-size: 0.85rem;
}
footer.site a { color: inherit; }

h1 { font-family: var(--font-num); font-size: 1.9rem; line-height: 1.15; margin: 8px 0 0; text-wrap: balance; }
h2 { font-size: 1.1rem; margin: 0; }
p { margin: 0; }
.muted { color: var(--muted); }
/* Read aloud but not shown: the status line, spoken questions, a chart cell's level. */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.lead { font-size: 1.05rem; color: var(--muted); }
/* A pilot deployment's one line above every screen (app.js drawPilotNote): the same width as the top bar, quiet. */
.pilot-note { max-width: 760px; width: 100%; margin: 0 auto; padding: 0 16px 8px; box-sizing: border-box; font-size: 0.95rem; color: var(--muted); }

.card {
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 20px;
  display: flex; flex-direction: column; gap: 14px;
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 48px; padding: 10px 20px;
  border: 1px solid var(--line); border-radius: 12px;
  background: var(--surface); font-weight: 700;
  transition: transform 0.08s, background-color 0.15s;
}
.btn:active { transform: scale(0.98); }
.btn.primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
/* The pilot's top-bar Sign in looks like the primary button but is not one: the checks and tests find a screen's main
   action as `a.btn.primary`, and this one is on every screen. */
.btn.signin { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
.btn.quiet { background: transparent; border-color: transparent; color: var(--muted); }
.btn.big { min-height: 60px; font-size: 1.15rem; }
/* The one step that cannot be undone (deleting the account): red, and only after a first tap. */
.btn.danger { background: var(--bad-soft); color: var(--bad); border-color: var(--bad); }
/* A link that leads nowhere yet (no tables picked) looks and acts inert. */
.btn[aria-disabled="true"] { opacity: 0.5; pointer-events: none; }
.key[aria-disabled="true"] { opacity: 0.5; }
.btn:focus-visible, .table-pick:focus-visible, .key:focus-visible, .switch:focus-visible, a:focus-visible {
  outline: 3px solid var(--blue); outline-offset: 2px;
}
.actions { display: flex; flex-wrap: wrap; gap: 10px; }
.actions .btn { flex: 1 1 160px; }
/* The home screen's choices: a name and one line each, two to a row (one under 400 px), equal heights; an odd one out spans the top row, below. */
.actions.tiles { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
.tile { flex-direction: column; align-items: flex-start; justify-content: flex-start; gap: 4px; text-align: left; padding: 14px 16px; min-height: 0; }
.tile .blurb { font-weight: 400; font-size: 0.9rem; line-height: 1.35; color: var(--muted); }
.tile.primary .blurb { color: var(--accent-ink); opacity: 0.85; }
/* An odd number of tiles: the first spans the top row, so none is left alone and the order stays as written. */
.actions.tiles > .tile:first-child:nth-last-child(odd) { grid-column: 1 / -1; }
@media (max-width: 400px) { .actions.tiles { grid-template-columns: 1fr; } }

/* ---------- Home: table picker ---------- */
.table-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(76px, 1fr)); gap: 10px;
}
.table-pick {
  position: relative;
  aspect-ratio: 1;
  border: 2px solid var(--line); border-radius: 14px;
  background: var(--surface-2);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  font-family: var(--font-num); font-size: 1.7rem; font-weight: 700;
  transition: transform 0.08s, border-color 0.15s, background-color 0.15s;
}
.table-pick:active { transform: scale(0.96); }
.table-pick[aria-pressed="true"] { border-color: var(--accent-ink); background: var(--accent); color: var(--accent-ink); }
.table-pick .bar {
  width: 70%; height: 6px; border-radius: 3px; background: var(--level-new); overflow: hidden;
  box-shadow: inset 0 0 0 1px var(--muted);   /* the empty track shows against the tile, not only the fill */
}
.table-pick[aria-pressed="true"] .bar { box-shadow: inset 0 0 0 1px var(--accent-ink); }
.table-pick .bar i { display: block; height: 100%; background: var(--good); }
.table-pick[aria-pressed="true"] .bar { background: rgba(42, 34, 6, 0.18); }
.table-pick[aria-pressed="true"] .bar i { background: var(--accent-ink); }
.presets { display: flex; flex-wrap: wrap; gap: 8px; }
.presets .btn { min-height: 38px; padding: 6px 14px; font-weight: 400; font-size: 0.95rem; }

/* ---------- Settings: switch rows ---------- */
.settings { display: flex; flex-direction: column; gap: 12px; padding-top: 14px; border-top: 1px solid var(--line); }
/* Label and hint on the left, the control on the right. The row wraps, so
   on a phone a wide control (three pills) drops under the words instead of
   squeezing them into a column or running off the edge. */
.switch-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px 14px; cursor: pointer; }
.switch-row .text { flex: 1 1 11em; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.switch-row .hint { font-size: 0.85rem; color: var(--muted); }
/* The switch: a pill with a knob that slides right when on. Color plus
   position, so it reads in grayscale too. */
/* Off: an outlined track with a dark knob on the left. On: green, a light knob on the right.
   The outline and the knob are dark enough to see (3:1) on any background. */
.switch {
  position: relative; flex: none;
  width: 52px; height: 30px; padding: 0;
  border: 1px solid var(--muted); border-radius: 15px;
  background: var(--level-new);
  transition: background-color 0.15s;
}
.switch::after {
  content: ""; position: absolute; top: 3px; left: 3px;
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--muted); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
  transition: transform 0.15s;
}
.switch[aria-checked="true"] { background: var(--good); border-color: transparent; }
.switch[aria-checked="true"]::after { transform: translateX(22px); background: var(--surface); }
/* A few joined choices, one on: the same row shape as a switch. They wrap onto a second row when the text is large
   (200% on a phone), rather than clip the last one ("40 questions", Quick, Grey); the 1 px gaps over the group's line
   color draw the lines between them, across a row and between rows. */
.choice-group { display: inline-flex; flex-wrap: wrap; gap: 1px; flex: 0 1 auto; margin-left: auto; max-width: 100%; border: 1px solid var(--line); border-radius: 12px; overflow: hidden; background: var(--line); }
.choice-pill {
  flex: 1 1 auto; min-height: 38px; padding: 4px 12px; border: 0;
  background: var(--surface-2); font-weight: 700; font-size: 0.95rem;
}
.choice-pill[aria-checked="true"] { background: var(--accent); color: var(--accent-ink); box-shadow: inset 0 0 0 2px var(--accent-ink); }
/* The pills' focus ring is with the fallbacks at the end of this file, on :focus (B33). */
/* Windows High Contrast drops the fill and the inset that mark the chosen pill, so a system-color border marks it there. */
@media (forced-colors: active) {
  .choice-group { background: CanvasText; }   /* the 1 px gaps are the lines between pills: the line color would become Canvas, the pills' own */
  .choice-pill[aria-checked="true"] { outline: 3px solid Highlight; outline-offset: -3px; }
}
/* The typed range, shown under "Multiply by" when Other is chosen. */
.range-inputs { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding-left: 4px; }
.range-inputs label { display: inline-flex; align-items: center; gap: 6px; font-size: 0.95rem; color: var(--muted); }
.range-inputs input {
  width: 4.2em; min-height: 38px; padding: 4px 8px;
  font: inherit; font-family: var(--font-num); font-weight: 700; color: var(--text);
  background: var(--surface-2); border: 1px solid var(--muted); border-radius: 10px;
}
.range-inputs input[aria-invalid="true"] { border-color: var(--bad); border-width: 2px; }
.range-inputs input:focus-visible { outline: 3px solid var(--blue); outline-offset: 1px; }
.range-inputs .hint { flex-basis: 100%; color: var(--bad); font-size: 0.85rem; min-height: 1.2em; }

/* ---------- Quiz ---------- */
.quiz { gap: 18px; }
/* Wraps at large text on a phone, so the count moves to a second line rather than past the edge; on one line, the
   count's auto margin is 0, and when it wraps it sits at the right, under the clock. */
.quiz-head { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
.quiz-head .count { margin-left: auto; }
.quiz-head .progress { flex: 1; height: 8px; border-radius: 4px; background: var(--level-new); overflow: hidden; }
.quiz-head .progress i { display: block; height: 100%; background: var(--blue); transition: width 0.2s; }
.quiz-head .count, .quiz-head .clock { font-family: var(--font-num); font-weight: 700; min-width: 3.5em; text-align: right; }
.quiz-head .clock.low { color: var(--bad); }

/* At 200% text on a phone the line can be wider than the card: it wraps, and "safe" keeps any overflow to the right,
   where it can be scrolled to, never off the left edge (the plain center first, for browsers before "safe"). */
.question {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center; justify-content: safe center; gap: 0.3em;
  font-family: var(--font-num); font-weight: 700;
  font-size: clamp(2.6rem, 11vw, 4.5rem); line-height: 1.1;
  min-height: 1.3em;
}
.question .sign { color: var(--muted); font-weight: 400; }
/* At the default size a six-digit line ("9 × 12 = 108", "108 ÷ 9 = 12", from the 1 to 12 preset) is 2 to 10 px wider
   than a 320 px phone's card: a little less space between its pieces keeps it on one line, so the keypad never jumps
   between questions as the line wraps and unwraps. */
@media (max-width: 340px) { .question { gap: 0.2em; } }
/* The answer box (the typed field, or the Choose style's span) is at least as wide as the widest answer the session
   can ask (--answer-digits, set by the screen), never under 1.6em, and never shrinks: a full line used to squeeze it
   to 67 px at 320 px and clip "100" for 10 × 10. 1ch is a digit's width in the numbers' font. The span can still grow
   for a wide near miss a child picks (100 on the 5s); the field cannot grow from its text, so it gets the width. */
.question .entry {
  min-width: max(1.6em, calc(var(--answer-digits, 2) * 1ch + 0.2em)); flex-shrink: 0; padding: 0 0.1em; box-sizing: border-box;
  border-bottom: 0.08em solid var(--muted);
  text-align: center; color: var(--blue);
  transition: border-color 0.15s, color 0.15s;
}
.question .entry.empty::after { content: "?"; color: var(--muted); }
/* The typed answer is a real text field, drawn like the rest of the line. Its
   focus shows as a thicker blue underline rather than a box. */
input.entry { font: inherit; width: max(1.6em, calc(var(--answer-digits, 2) * 1ch + 0.2em)); margin: 0; background: transparent; border: 0; border-bottom: 0.08em solid var(--muted); border-radius: 0; }
input.entry::placeholder { color: var(--muted); opacity: 1; }
/* On :focus, not :focus-visible, which Safari before 15.4 does not know: a text field always matches both where both
   are known, so newer browsers look the same (Issue #56, B34). */
input.entry:focus { outline: none; border-bottom-color: var(--blue); box-shadow: 0 0.06em 0 var(--blue); }
.question.right .entry { color: var(--good); border-color: var(--good); }
.question.wrong .entry { color: var(--bad); border-color: var(--bad); }
.feedback { min-height: 1.6em; text-align: center; font-weight: 700; }
.feedback.good { color: var(--good); }
.feedback.bad { color: var(--bad); }
.feedback .num { font-size: 1.2em; }

/* On-screen number pad: for phones and tablets, and anyone who likes it.
   Physical keyboards work the same way; the pad mirrors what they type. */
.keypad {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
  max-width: 320px; width: 100%; margin: 0 auto;
}
.key {
  min-height: 56px; border-radius: 12px;
  border: 1px solid var(--line); background: var(--surface-2);
  font-family: var(--font-num); font-size: 1.5rem; font-weight: 700;
  transition: transform 0.06s, background-color 0.1s;
}
.key:active { transform: scale(0.95); background: var(--blue-soft); }
.key.go { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
.key.back { font-family: var(--font-ui); font-size: 1rem; font-weight: 400; }

/* ---------- Speed test ---------- */
.settings.first { border-top: 0; padding-top: 0; }
/* The ghost of the best run: fill is this run, the end marker is the best. */
.ghost { display: flex; align-items: center; gap: 10px; font-size: 0.85rem; color: var(--muted); }
.ghost-bar { flex: 1; position: relative; height: 6px; border-radius: 3px; background: var(--level-new); }
.ghost-bar i { display: block; height: 100%; max-width: 100%; border-radius: 3px; background: var(--blue); transition: width 0.2s; }
.ghost-bar .mark { position: absolute; right: 0; top: -4px; width: 3px; height: 14px; border-radius: 2px; background: var(--muted); }
.ghost.beaten { color: var(--good); font-weight: 700; }
.ghost.beaten .ghost-bar i { background: var(--good); }
/* Ten seconds a question: a thin bar under the question that drains. */
.cap { width: 100%; max-width: 320px; height: 4px; margin: -6px auto 0; border-radius: 2px; background: var(--level-new); overflow: hidden; }
.cap i { display: block; height: 100%; background: var(--blue); transition: width 0.1s linear; }
.best-note { text-align: center; font-weight: 700; color: var(--good); }

/* Four answers to pick from, in place of the keypad. The small corner
   number is the key that picks it, shown only where a keyboard is likely. */
.choices { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; max-width: 420px; width: 100%; margin: 0 auto; }
.choice {
  position: relative; min-height: 64px; border-radius: 14px;
  border: 2px solid var(--line); background: var(--surface-2);
  font-family: var(--font-num); font-size: 1.6rem; font-weight: 700;
  transition: transform 0.06s, border-color 0.15s, background-color 0.15s;
}
.choice:active { transform: scale(0.97); }
.choice.picked { border-color: var(--blue); background: var(--blue-soft); }
.choice.right { border-color: var(--good); background: var(--good-soft); color: var(--good); }
.choice.wrong { border-color: var(--bad); background: var(--bad-soft); color: var(--bad); }
.choice:focus-visible { outline: 3px solid var(--blue); outline-offset: 2px; }
/* The key that picks each answer (a real span, hidden from readers: keyHint in app.js), where a keyboard is likely. */
.key-hint { display: none; }
@media (hover: hover) and (min-width: 600px) {
  .choice .key-hint { display: block; position: absolute; top: 4px; left: 8px; font-size: 0.7rem; font-weight: 400; color: var(--muted); }
}

/* ---------- Balloon pop ---------- */
/* The sky the balloons rise through. Four still columns fill it, one per answer: the whole column is
   the button, and only the balloon drawn inside it rises (from app.js, with element.animate), so a
   target never moves and a focus ring never leaves the sky. */
.balloon-sky {
  position: relative; width: 100%; max-width: 460px; margin: 0 auto;
  height: clamp(220px, 42vh, 340px);
  overflow: hidden; border: 1px solid var(--line); border-radius: 12px;
  background: linear-gradient(var(--blue-soft), var(--surface) 85%);
}
.balloon-field { position: absolute; top: 0; right: 0; bottom: 0; left: 0; display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; padding: 0 4px; }
/* Each lane clips its own balloon, so a drawing can never sit over another lane's target (after a resize into two by two, say). */
.balloon-lane {
  position: relative; display: block; width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden;
  border: 0; border-radius: 10px; background: transparent; color: var(--text); font: inherit; cursor: pointer;
}
/* Inside the column, so the sky's edge never clips it. */
.balloon-lane:focus-visible { outline: 3px solid var(--blue); outline-offset: -3px; }
/* The width cap grows with the text size, so a three- or four-digit answer still fits at 200%. */
.balloon {
  position: absolute; top: 10px; left: 0; right: 0; margin: 0 auto;
  width: min(calc(100% - 6px), max(88px, 3.3rem)); aspect-ratio: 5 / 6;
  display: flex; align-items: center; justify-content: center;
  border: 2px solid var(--text); border-radius: 50% 50% 46% 46% / 56% 56% 44% 44%;
  font-size: 1.5rem; font-weight: 700; line-height: 1; white-space: nowrap;
}
/* Browsers older than aspect-ratio (Chrome 88) get a fixed height instead. */
@supports not (aspect-ratio: 1) { .balloon { height: 4.5rem; } }
.balloon-field.long .balloon { font-size: 1.15rem; }
/* Colors by lane mean nothing: right and wrong are said in words, and marked with a tick. */
.balloon-lane:nth-child(1) .balloon { background: var(--accent); }
.balloon-lane:nth-child(2) .balloon { background: var(--level-known); }
.balloon-lane:nth-child(3) .balloon { background: var(--dog-dusk); }
.balloon-lane:nth-child(4) .balloon { background: var(--dog-dusk-low); }
/* The string: decoration. */
.balloon::after { content: ""; position: absolute; top: 100%; left: calc(50% - 1px); width: 2px; height: 26px; background: var(--muted); }
.balloon-lane.right .balloon { background: var(--good-soft); border: 4px solid var(--good); color: var(--good); }
/* The tick sits on the balloon's shoulder, clear of the digits. The plain content line is for browsers without
   alt text in content (Safari before 17.4, Firefox before 128), which then name the lane "✓ 56". */
.balloon-lane.right .balloon::before {
  content: "✓"; content: "✓" / "";
  position: absolute; top: -0.35em; right: 0; width: 1.3em; height: 1.3em;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%; background: var(--good); color: var(--surface); font-size: 0.7em;
}
.balloon-lane.wrong .balloon { background: var(--bad-soft); border-color: var(--bad); color: var(--bad); }
/* Done with and not the answer: pale by color, not opacity, so a focus ring on it keeps its contrast. */
.balloon-lane.done .balloon { background: var(--surface-2); border-color: var(--muted); color: var(--muted); }
/* Popped: only the green number and its tick are left. */
.balloon-lane.popped .balloon { background: transparent; border-color: transparent; }
.balloon-lane.popped .balloon::after { display: none; }
/* Floated away: unseen, but its number still names the lane for a screen reader. */
.balloon-lane.away .balloon { opacity: 0; }
.balloon-bit { position: absolute; width: 8px; height: 8px; border-radius: 50%; background: var(--good); pointer-events: none; }
/* The key that picks each lane, where a keyboard is likely, at the foot of the column. */
@media (hover: hover) and (min-width: 600px) {
  .balloon-lane .key-hint { display: block; position: absolute; bottom: 6px; left: 0; right: 0; text-align: center; font-size: 0.75rem; color: var(--text); }
}
/* Paused: the balloons and the question are covered, so a pause cannot be used to read them. */
.balloon-cover { position: absolute; top: 0; right: 0; bottom: 0; left: 0; z-index: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; background: var(--blue-soft); font-weight: 700; }
.question.balloon-covered { visibility: hidden; }
/* Large text (200%): the four lanes become two by two, with a taller sky. */
@media (max-width: 19.5em) {
  .balloon-field { grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(2, 1fr); }
  .balloon-sky { height: clamp(300px, 60vh, 480px); }
}

/* ---------- Learn a table ---------- */
/* The step strip sits where the progress bar would: five names, the done
   ones ticked with a solid green bar, the one to come a dotted bar, the
   current one blue and bold. Names wrap rather than being cut off. */
.steps { flex: 1; display: flex; gap: 4px; min-width: 0; list-style: none; margin: 0; padding: 0; }
.step {
  flex: 1; min-width: 0; padding: 4px 2px 6px; text-align: center;
  font-size: 0.78rem; line-height: 1.2; color: var(--muted);
  border-bottom: 4px dotted var(--muted);
  overflow-wrap: anywhere;
}
.step.done { border-bottom-style: solid; border-color: var(--good); color: var(--text); }
/* The plain form first: Safari before 17.4 and Firefox before 128 drop the whole declaration with alt text (B30). */
.step.done::before { content: "✓ "; content: "✓ " / "done: "; font-size: 0.78rem; }
.step[aria-current="step"] { border-bottom-style: solid; border-color: var(--blue); color: var(--text); font-weight: 700; }
.learn-body { display: flex; flex-direction: column; gap: 16px; }
.learn-done { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.learn-done .btn { min-width: 220px; }
/* Stage 1: the table laid out to read. */
/* Columns sized in the list's own em, so larger text gives fewer columns instead of rows that run into the next one
   ("12 × 10 = 120" is about 6.6em wide); 6.75em keeps four columns on a laptop, min() one full column on a phone, and
   the 12 px gap two columns on a 390 px phone. */
.table-list {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 6.75em), 1fr)); gap: 6px 12px;
  font-family: var(--font-num); font-size: 1.45rem; font-weight: 700;
  list-style: none; margin: 0; padding: 0;
}
/* One line per fact, read as one phrase (its words are one sr-only span). Its drawn pieces wrap between them when a
   fact is wider than the whole card (the 12s at 200% text on a 320 px phone), never inside a number. */
.table-list .row { display: flex; flex-wrap: wrap; justify-content: center; align-items: baseline; font-variant-numeric: tabular-nums; }
.table-list .sign { color: var(--muted); font-weight: 400; margin: 0 0.3em; }
.table-list .ans { color: var(--blue); }
/* Stage 3: questions down the left, answers down the right. */
.match { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; max-width: 480px; width: 100%; margin: 0 auto; }
.match-col { display: flex; flex-direction: column; gap: 8px; }
.match-item {
  min-height: 48px; padding: 6px 10px; border-radius: 12px;
  border: 2px solid var(--line); background: var(--surface-2);
  font-family: var(--font-num); font-size: 1.25rem; font-weight: 700;
  transition: border-color 0.15s, background-color 0.15s;
}
.match-item[aria-pressed="true"] { border-color: var(--blue); background: var(--blue-soft); }
.match-item.wrong { border-color: var(--bad); background: var(--bad-soft); }
.match-item.locked { border-color: var(--good); background: var(--good-soft); color: var(--good); cursor: default; }
.match-item.locked::after { content: " ✓"; }
.match-item:focus-visible { outline: 3px solid var(--blue); outline-offset: 2px; }

/* ---------- Memory match ----------
   Two decks of cards, questions above answers, with the feedback line
   between them. Each card is a button holding two faces stacked in one
   grid cell; turning it up rotates them. The reduce-motion rules near the
   end (the system's and the "Less motion" switch's) turn off every
   transition and animation here, so the faces simply swap. States differ
   by content and border style as well as color: a solid blue border for
   the card up this turn, dashed for a pair that did not match, a double
   green border and a tick once matched. */
.memory-board { display: flex; flex-direction: column; gap: 10px; container-type: inline-size; }
.deck { display: flex; flex-direction: column; gap: 6px; }
.deck h2 { font-size: 0.95rem; color: var(--muted); display: flex; align-items: center; gap: 8px; }
/* The cue keeps its room when hidden, so the heading's height never changes (at large text it may wrap). */
.deck .deck-next { visibility: hidden; font-size: 0.8rem; padding: 1px 8px; border-radius: 999px; background: var(--blue-soft); color: var(--blue); }
.deck.next h2 { color: var(--text); }
.deck.next .deck-next { visibility: visible; }
/* Sizes in em, so larger text moves the board to fewer, wider cards instead of spilling a number out of its card:
   a deck in one row while each card can be about 5 em wide, then three across (two for four pairs), then two. */
.mem-grid { display: grid; grid-template-columns: repeat(var(--wide, 6), minmax(0, 7em)); justify-content: center; gap: 10px; }
@container (max-width: 32em) { .mem-grid { grid-template-columns: repeat(var(--narrow, 3), minmax(0, 7em)); gap: 8px; } }
@container (max-width: 14.5em) { .mem-grid { grid-template-columns: repeat(2, minmax(0, 7em)); } }
/* The line between the decks keeps room for its longest sentence (about 60 characters, such as "20 × 99 is not 1980.
   Have a good look, then turn a new card."), so the answer cards never move under a finger. The board's width in em
   says how many lines that takes, at any text size: two, then three, four, or five as the board narrows. */
.memory-line { min-height: 2.9em; display: flex; align-items: center; justify-content: center; }
@container (max-width: 17em) { .memory-line { min-height: 4.35em; } }
@container (max-width: 11em) { .memory-line { min-height: 5.8em; } }
@container (max-width: 8.5em) { .memory-line { min-height: 7.25em; } }
/* Browsers without container queries (Chrome before 105, Safari before 16; districts may run Chrome 86): the same
   steps by the window's width. The board is the window less 72 px of page and card padding (64 px under 480 px
   wide), and its em is 17 px where the window's is 16 px, so 32 em of board is about 38.5 em of window, 14.5 em
   about 19.4, and the line's 17, 11, and 8.5 em about 22.1, 15.7, and 13. Window em follows the browser's text
   size too; there the padding counts for less, so these step a little early, which only makes room sooner. */
@supports not (container-type: inline-size) {
  @media (max-width: 38.5em) { .mem-grid { grid-template-columns: repeat(var(--narrow, 3), minmax(0, 7em)); gap: 8px; } }
  @media (max-width: 19.4em) { .mem-grid { grid-template-columns: repeat(2, minmax(0, 7em)); } }
  @media (max-width: 22.1em) { .memory-line { min-height: 4.35em; } }
  @media (max-width: 15.7em) { .memory-line { min-height: 5.8em; } }
  @media (max-width: 13em) { .memory-line { min-height: 7.25em; } }
}
.memory-left { display: flex; flex-direction: column; gap: 4px; }
.memory-left-text { text-align: center; }
/* No transform or transform animation on the card itself: Chrome then draws the wrong face while it runs. */
/* A height in em, not a fixed shape, so a card grows with its text (larger text, or a whole fact once matched). */
.mcard {
  display: grid; min-height: 4.7em; padding: 0; border: 0; border-radius: 12px; background: none;
  perspective: 600px; font-family: var(--font-num); font-weight: 700;
}
.mcard:active .face { filter: brightness(0.94); }
.mcard:focus-visible { outline: 3px solid var(--blue); outline-offset: 3px; }
.mcard[aria-disabled="true"] { cursor: default; }
.mcard .inner { display: grid; transform-style: preserve-3d; transition: transform 0.28s ease; }
.mcard.up .inner { transform: rotateY(180deg); }
.mcard .face {
  grid-area: 1 / 1; display: flex; flex-direction: column; gap: 2px;
  padding: 4px 8px 8px; border-radius: 12px; border: 2px solid transparent;
  -webkit-backface-visibility: hidden; backface-visibility: hidden;
}
/* The small top row: the letter or sign on the left, the tick on the right. In the flow, so a card grows rather than letting them overlap the face. */
.mcard .mrow { display: flex; justify-content: space-between; font-family: var(--font-ui); font-size: 0.8rem; font-weight: 400; line-height: 1.2; min-height: 1.2em; }
.mcard .letter { flex: 1; display: flex; align-items: center; justify-content: center; font-family: var(--font-ui); font-size: 1.6rem; font-weight: 700; }
.mcard.q .back { background: var(--blue); color: #fff; }
.mcard.a .back { background: var(--accent); color: var(--accent-ink); border-color: var(--accent-ink); }   /* the ink border: yellow alone barely shows on white */
.mcard .front { transform: rotateY(180deg); background: var(--surface-2); border-color: var(--muted); color: var(--text); }
.mcard .corner { color: var(--muted); }
.mcard .tick { color: var(--good); font-weight: 700; visibility: hidden; }
.mcard .text { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; line-height: 1.1; font-size: 1.15rem; }
.mcard.a .text .value { font-size: 1.5rem; }
.mcard.a .text .value.long { font-size: 1.15rem; }   /* four digits or more (12 × 94 = 1128): the question's size, so it fits two across at 320 px with 200% text */
.mcard .sign { color: var(--muted); font-weight: 400; }
.mcard .sub { font-size: 0.8rem; font-weight: 400; color: var(--text); }
.mcard.first .front { border: 3px solid var(--blue); }
.mcard.waiting .front { border-style: dashed; }
.mcard.matched .front { background: var(--good-soft); border: 4px double var(--good); transition: background-color 0.2s 0.28s, border-color 0.2s 0.28s; }
.mcard.matched .tick { visibility: visible; }
/* A found pair glows once, after it has turned; at the end the glow runs across the board, card by card. The end has
   its own animation name: reusing the pair's would only change the timing of a finished animation, not replay it. */
.mcard.just-found .front { animation: mem-glow 0.6s ease-out 0.28s; }
.memory-board.done .mcard .front { animation: mem-wave 0.6s ease-out calc(0.28s + var(--i) * 50ms); }
@keyframes mem-glow { from { box-shadow: 0 0 0 0 rgba(22, 115, 61, 0.5); } to { box-shadow: 0 0 0 10px rgba(22, 115, 61, 0); } }
@keyframes mem-wave { from { box-shadow: 0 0 0 0 rgba(22, 115, 61, 0.5); } to { box-shadow: 0 0 0 10px rgba(22, 115, 61, 0); } }
.memory-end, .duo-end { display: flex; flex-direction: column; gap: 12px; }
.memory-end h2 { font-family: var(--font-num); font-size: 1.4rem; color: var(--good); }
.memory-end h3, .duo-end h3 { font-size: 1rem; margin: 0; color: var(--muted); }
@media (forced-colors: active) {
  .mcard .face { border: 2px solid CanvasText; }
  .mcard.first .front { border-width: 3px; }
  .mcard.waiting .front { border-style: dashed; }
  .mcard.matched .front { border: 4px double CanvasText; }
}
/* A tick in the corner of a tile whose check is done. */
.table-pick.learned::after {
  content: "✓"; position: absolute; top: 4px; right: 7px;
  font-size: 0.85rem; line-height: 1; color: var(--good);
}
.table-pick[aria-pressed="true"].learned::after { color: var(--accent-ink); }

/* ---------- Diplomas and the certificate ---------- */
.diploma-card h3 { font-family: var(--font-num); font-size: 1.3rem; margin: 0; }   /* the card's title, under the group's h2 */
.diploma-card .earned { color: var(--good); font-weight: 700; }
.name-field { display: flex; flex-direction: column; gap: 6px; font-weight: 700; }
.name-field input {
  font: inherit; font-weight: 400; min-height: 48px; padding: 8px 12px;
  border: 1px solid var(--muted); border-radius: 12px;
  background: var(--surface-2); color: var(--text);
}
.name-field input:focus-visible { outline: 3px solid var(--blue); outline-offset: 1px; }
.name-field input::placeholder { color: var(--muted); opacity: 1; }
/* The certificate is paper: white with dark ink whatever the color scheme,
   so the screen shows what the printer will. Sized like the printed sheet
   (7.5 in by 9.5 in, inside letter margins) and scaled to the screen. */
.certificate-wrap { display: flex; justify-content: center; }
.certificate {
  --paper: #ffffff; --ink: #17202b; --ink-soft: #586473;
  box-sizing: border-box; width: 100%; max-width: 7.5in; aspect-ratio: 7.5 / 9.5;
  background: var(--paper); color: var(--ink);
  border: 3px double var(--ink); outline: 8px solid var(--paper);
  box-shadow: var(--shadow);
  padding: clamp(14px, 6%, 56px);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.55em;
  text-align: center; font-family: var(--font-ui);
  font-size: 1.1rem; line-height: 1.4;   /* rem, not the window width, so it follows the browser's text size */
}
.certificate p, .certificate h2 { margin: 0; }
/* Auto margins on the brand and the footer line hold them at the top and
   bottom of the sheet, with everything else centered between. */
.cert-brand { display: flex; align-items: center; gap: 8px; margin-bottom: auto; font-family: var(--font-num); font-weight: 700; }
.cert-brand svg { width: 1.8em; height: 1.8em; }
.cert-kicker { text-transform: uppercase; letter-spacing: 0.22em; font-size: 0.8em; color: var(--ink-soft); }
.cert-title { font-family: var(--font-num); font-size: 2.4em; line-height: 1.1; }
/* A border, not a background, so it prints even with "background graphics" off. */
.cert-rule { width: 28%; height: 0; border-top: 0.3em solid var(--accent); border-radius: 0.15em; }
.cert-name {
  min-width: 60%; max-width: 100%; min-height: 1.5em; padding: 0.1em 0.5em;
  overflow-wrap: anywhere;
  font-size: 2em; font-weight: 700; line-height: 1.3;
  border-bottom: 0.08em solid var(--ink);
}
.cert-date { color: var(--ink-soft); }
.cert-sign { margin-top: 1.6em; width: 55%; padding-top: 0.3em; border-top: 1px solid var(--ink); font-size: 0.8em; color: var(--ink-soft); }
.certificate .cert-foot { margin-top: auto; font-size: 0.7em; color: var(--ink-soft); }

/* ---------- Dog to the park ---------- */
/* The scene strip sits between the quiz head and the question. The world
   inside is drawn in fixed units (240 tall) and scaled to the strip, so the
   same numbers work on a phone and a wide screen. Only transforms animate. */
.dog-scene { --k: 0.75; position: relative; height: 180px; overflow: hidden; border-radius: 12px; border: 1px solid var(--line); background: linear-gradient(var(--blue-soft), var(--surface) 75%); cursor: pointer; }
.dog-scene[data-light="evening"] { background: linear-gradient(var(--dog-evening), var(--surface) 75%); }
.dog-scene[data-light="noon"] { background: linear-gradient(var(--dog-noon), var(--surface) 75%); }
.dog-scene[data-light="dusk"] { background: linear-gradient(var(--dog-dusk), var(--dog-dusk-low) 45%, var(--surface) 80%); }
/* --cam: the walk's camera, set from app.js when a resize mid-walk narrows the world (in world units, before the scale). */
.dog-world { position: absolute; left: 0; bottom: 0; width: calc(100% / var(--k)); height: 240px; transform: scale(var(--k)) translateX(var(--cam, 0px)); transform-origin: 0 100%; }
.dog-world .layer { position: absolute; left: 0; top: 0; }
.dog-world .dog-leo { position: absolute; left: var(--dog-x, 28%); bottom: 12px; width: 100px; height: 70px; overflow: visible; transform-origin: 50% 100%; }
.dog-world .pal { position: absolute; left: calc(var(--dog-x, 28%) - 92px); bottom: 12px; width: 78px; height: 70px; display: none; }
.dog-world .pal.shown { display: block; }
.dog-leo * { transform-box: fill-box; }
.dog-leo .body { transform-origin: 50% 55%; }
.dog-leo .leg { transform-origin: 50% 5%; }
.dog-leo .tail { transform-origin: 100% 100%; }
.dog-leo .ear { transform-origin: 80% 0; }
.dog-leo .head { transform-origin: 20% 90%; }
.dog-leo .eye, .dog-leo .halo, .dog-leo .spark { transform-origin: 50% 50%; }
.dog-leo .halo, .dog-leo .spark, .dog-leo .smile, .dog-leo .board { opacity: 0; }
.dog-leo.riding .board { opacity: 1; }
.dog-leo.happy .smile { opacity: 1; }
.dog-leo.happy .eye { opacity: 0; }
/* Coats: three looks through four classes on the dog's shapes. */
[data-coat="blonde"] .coat { fill: #e8c87a; } [data-coat="blonde"] .coat-d { fill: #c9a45a; } [data-coat="blonde"] .coat-l { fill: #f9ecc9; } [data-coat="blonde"] .coat-s { stroke: #e8c87a; }
[data-coat="brown"] .coat { fill: #c58b55; } [data-coat="brown"] .coat-d { fill: #a46f3f; } [data-coat="brown"] .coat-l { fill: #f1dcc0; } [data-coat="brown"] .coat-s { stroke: #c58b55; }
[data-coat="grey"] .coat { fill: #9aa3ad; } [data-coat="grey"] .coat-d { fill: #717b86; } [data-coat="grey"] .coat-l { fill: #dfe4ea; } [data-coat="grey"] .coat-s { stroke: #9aa3ad; }
/* Theme classes the art uses, so every drawing takes its colors from the palette above. */
.f-sur { fill: var(--surface); } .f-sur2 { fill: var(--surface-2); } .f-line { fill: var(--line); } .f-muted { fill: var(--muted); }
.f-text { fill: var(--text); } .f-acc { fill: var(--accent); } .f-blue { fill: var(--blue); } .f-bsoft { fill: var(--blue-soft); }
.f-good { fill: var(--good); } .f-gsoft { fill: var(--good-soft); } .f-bad { fill: var(--bad); }
.s-line { stroke: var(--line); } .s-blue { stroke: var(--blue); } .s-sur { stroke: var(--surface); } .s-text { stroke: var(--text); } .s-muted { stroke: var(--muted); }
.f-ink { fill: var(--accent-ink); } .f-road { fill: var(--dog-road); } .f-wall { fill: var(--dog-wall); }
.dog-world text { font-family: var(--font-num); }
/* Two lines are reserved so the question below does not jump between states. */
.dog-status { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 2.9em; font-weight: 700; }
.dog-status .words { flex: 1 1 auto; min-width: 0; }
.paws { display: inline-flex; gap: 5px; flex: none; }
.paw { width: 12px; height: 12px; border-radius: 50%; background: var(--surface); border: 2px solid var(--muted); }
.paw.on { background: var(--good); border-color: var(--good); }
.dog-choice { display: flex; gap: 10px; flex-wrap: wrap; }
.dog-choice .btn { flex: 1 1 140px; }
/* A fork's smalls around: hidden until the child picks that way (the walk fades them in). */
.dog-world .dg-around { opacity: 0; }
.dog-preview { width: 120px; height: 84px; margin: 0 auto; }
.dog-preview svg { width: 100%; height: 100%; overflow: visible; }
.dog-name { font: inherit; min-height: 40px; padding: 6px 10px; border: 1px solid var(--muted); border-radius: 10px; background: var(--surface-2); color: var(--text); }
/* The drawn neighborhood: the SVG scales to the card; each street is a button. */
.dog-map-wrap { width: 100%; }
.dog-map { display: block; width: 100%; height: auto; border-radius: 12px; }
.dog-map text { font-family: var(--font-num); }
.dog-map .label { font-size: 30px; }   /* in map units: about 13px on a phone, 26px on a laptop */
.dog-map .map-street { cursor: pointer; }
.dog-map .map-street:hover .label, .dog-map .map-street:focus-visible .label { fill: var(--blue); }
.dog-map .map-street.next .label { fill: var(--blue); }
.dog-map .map-street:focus-visible { outline: 3px solid var(--blue); outline-offset: 4px; border-radius: 8px; }
/* A green-space spot (the empty lot, Splash Pond) is a button like a street; a closed one is paler but still says what opens it. */
.dog-map .map-spot { cursor: pointer; }
.dog-map .map-spot:focus-visible { outline: 3px solid var(--blue); outline-offset: 4px; border-radius: 8px; }
.dog-map .map-spot:hover .area, .dog-map .map-spot:focus-visible .area { stroke: var(--blue); }
.dog-map .map-spot.closed .area { opacity: 0.6; }
.map-hint { margin-top: 6px; }

/* ---------- The doghouse ---------- */
/* The picked colors reach the sprite's painted parts (f-paint, f-roof in art/dog/building.svg) as custom
   properties set on the house, which <use> passes through. Parts not picked yet stay in primer. */
:root { --house-yellow: #f6d365; --house-blue: #8fbbe8; --house-red: #c8483c; --house-green: #4a9a5a; --house-primer: #e6e0d4; }
.doghouse { --paint: var(--house-primer); --roof: var(--house-primer); }
.doghouse[data-paint="yellow"] { --paint: var(--house-yellow); }
.doghouse[data-paint="blue"] { --paint: var(--house-blue); }
.doghouse[data-roof="red"] { --roof: var(--house-red); }
.doghouse[data-roof="green"] { --roof: var(--house-green); }
.f-paint { fill: var(--paint, var(--house-yellow)); }
.f-roof { fill: var(--roof, var(--house-red)); }
/* Both tops are drawn; the house's data-top shows one, the flag until the weathervane is picked. */
.doghouse:not([data-top="vane"]) .top-vane, .doghouse[data-top="vane"] .top-flag { display: none; }
/* The pieces still to come show faintly, an outline of the house; the name goes up with its plate. */
.doghouse .ghost { opacity: 0.2; }
.doghouse .plate { fill: #3a2718; font-family: var(--font-num); }
.doghouse .plate.ghost { opacity: 0; }
.build-scene { cursor: default; }
/* The build's status: the words take the full width and always two lines' room, the ten dots sit on a row under
   them, so the question and keypad below never move between pieces, down to 320 px wide. */
.dog-status.build-status { flex-direction: column; align-items: flex-start; gap: 6px; min-height: 0; }
.build-status .words { align-self: stretch; min-height: 2.9em; }
.build-scene .lot { fill: #efe3c8; }
/* ---------- Splash Pond ---------- */
/* The water, its line in front of each row of swimmers, and the far bank's sand. The ripples show as each dog lands,
   the splash plays at each landing, and the drops fly off at the end (app.js); each grows from its own bottom middle,
   and each dog's wiggle turns about its feet. */
.pond-scene { cursor: default; }
.pond-scene .pond-water { fill: #8fc0ee; }
.pond-scene .pond-line { stroke: #dcebf9; }
.pond-scene .pond-shore { fill: #efe3c8; }
.pond-scene .ripple, .pond-scene .splash, .pond-scene .shake-drops { opacity: 0; }
.pond-scene .splash, .pond-scene .shake-drops { transform-box: fill-box; transform-origin: 50% 100%; }
.pond-scene .wiggle { transform-box: fill-box; transform-origin: 50% 90%; }
.swatch { display: inline-block; width: 1em; height: 1em; margin-right: 0.5em; vertical-align: -0.15em; border: 2px solid #5b3a1e; border-radius: 4px; }
.swatch-paint-yellow { background: var(--house-yellow); } .swatch-paint-blue { background: var(--house-blue); }
.swatch-roof-red { background: var(--house-red); } .swatch-roof-green { background: var(--house-green); }
.dog-street { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 10px 0; border-top: 1px solid var(--line); }
.dog-street:first-of-type { border-top: 0; }
.dog-street .text { display: flex; flex-direction: column; gap: 2px; flex: 1 1 14em; }
.dog-street .name { font-weight: 700; }
.dog-street .hint { font-size: 0.85rem; color: var(--muted); }
/* The streets as a list under the map, folded: its summary reads as a quiet link. */
.dog-streets summary { cursor: pointer; padding: 10px 0; font-weight: 700; color: var(--blue); }
.dog-streets[open] summary { margin-bottom: 2px; }
@media (max-width: 480px) { .dog-scene { --k: 0.62; height: 150px; } }
@media (min-width: 720px) { .dog-scene { --k: 0.85; height: 204px; } }
/* A short screen (most phones held upright) keeps the keypad in reach: a lower strip and tighter gaps. */
@media (max-height: 720px) {
  .dog-scene { --k: 0.5; height: 120px; }
  .dog-run main#screen, .balloon-run main#screen, .duo-run main#screen { gap: 10px; padding-top: 0; }
  .dog-run main#screen > h1, .balloon-run main#screen > h1, .duo-run main#screen > h1 { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }   /* still read aloud */
  .dog-run .quiz, .balloon-run .quiz { gap: 10px; }
  .dog-run .key { min-height: 44px; }
  .dog-run .question, .balloon-run .question { font-size: clamp(2.2rem, 9vw, 3.4rem); }
}
/* A session with long lines ("12 × 10 = 120", "12 × 99 = 1188": seven digits or more across the numbers and the
   answer box) is drawn smaller on a phone, so it stays inside the card at 320 px, and the same as ever on a laptop
   (7.5vw passes 4.5rem from 960 px up). After the short-screen rule above, so it wins there too. */
.question.long { font-size: clamp(1.8rem, 7.5vw, 4.5rem); }
/* Every Balloon pop round narrows its quiet head buttons, so a Steady or Quick round, with Pause too, still keeps the bar in view. */
.balloon-run .quiz-head .btn.quiet { padding-left: 10px; padding-right: 10px; }
/* A phone held sideways (under 500 px tall), or a laptop zoomed in: the round takes the whole page, as Two players
   does, and the sky is short enough to end on screen, so a balloon that floats away rises in view from its first
   moment (Issue #56, B9: at 667 by 320 the first number came into view 9 seconds into a 12-second question). In px,
   not vh: iOS Safari's 100vh is the height with its bars hidden (see Two players). */
@media (max-height: 500px) {
  body.balloon-run .topbar, body.balloon-run footer.site { display: none; }
  body.balloon-run .balloon-sky { height: 150px; }   /* four across: ends on screen at 568 by 320 and up, at the default text size */
}
/* Two by two (large text) on a short screen keeps the large-text sky: two whole balloons a column, with room to rise,
   need its 300 px, even though it then runs below the screen; 150 px would cut each balloon through its number. */
@media (max-height: 500px) and (max-width: 19.5em) {
  body.balloon-run .balloon-sky { height: 300px; }
}

/* ---------- Two players ---------- */
/* Two halves, Blue on the left (on top when they stack) and Yellow on the right, each with its band, its question,
   and its pad: the four answers sit like the keys that pick them, one above three (W A S D, the arrows), with Pass
   in the spare corner. Sized with fixed units and flex only: no vh, svh, or dvh (the district review promises
   Chrome 86, and iOS Safari's 100vh is the height with its bars hidden). */
.duo { gap: 12px; }
.duo-setup { gap: 8px; }
.duo-save-line { font-weight: 700; }
.duo-board { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; border-radius: 14px; }
/* Inset, so the screen's edge and the safe-area padding never clip it. */
.duo-board:focus { outline: 3px solid var(--blue); outline-offset: -3px; }
.duo-board:focus:not(:focus-visible) { outline: none; }   /* after a tap; Safari before 15.4 drops this and keeps the ring (B32) */
@media (min-width: 36em) { .duo-board { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.duo-half {
  display: flex; flex-direction: column; gap: 8px;
  padding: 10px; border-radius: 12px; border: 2px solid transparent;
  /* A waiting thumb never starts a text selection or the loupe. */
  -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
}
.duo-half.blue { background: var(--blue-soft); }
/* Yellow on white alone is 1.67:1, so the half carries an ink border (Memory match's answer cards do the same). */
.duo-half.yellow { background: var(--yellow-soft); border-color: var(--accent-ink); }
.duo-band { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 2px 10px; font-weight: 700; }
/* Each side's ink, on its band and its Ready button. */
.duo-half.blue .duo-band, .duo-half.blue .duo-ready { color: var(--blue); }
.duo-half.yellow .duo-band, .duo-half.yellow .duo-ready { color: var(--accent-ink); }
.duo-score { display: inline-block; min-width: 1.2em; font-size: 1.15em; text-align: center; }
.duo-score.bump { animation: duo-bump 0.3s ease-out; }
@keyframes duo-bump { 50% { transform: scale(1.4); } }
.duo-saving { font-size: 0.85rem; font-weight: 400; color: var(--muted); }
.duo-half.saving .duo-saving { font-weight: 700; color: inherit; }
/* Andika's line box is about 1.32 em: any less and the question overflows its own box (unclipped, but it crowds the pad). */
.duo-q { text-align: center; font-weight: 700; font-size: 2.6rem; line-height: 1.32; min-height: 1.32em; }
.duo-pad {
  display: grid; gap: 8px; grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-areas: "pass up ." "left down right";
  /* Stacked, the page may scroll: a swipe scrolls, but a pinch or a double tap does not zoom. */
  touch-action: pan-y;
}
.duo-half.yellow .duo-pad { grid-template-areas: ". up pass" "left down right"; }
.duo-ready { touch-action: pan-y; }
/* Side by side: two hands on the two pads at once must never start a pinch. */
@media (min-width: 36em) { .duo-pad, .duo-ready { touch-action: none; } }
.duo-ans {
  position: relative; min-height: 64px; padding: 0 4px;
  border-radius: 14px; border: 2px solid var(--line); background: var(--surface);
  font-size: 1.7rem; font-weight: 700; overflow-wrap: anywhere;
}
.duo-pad.long .duo-ans { font-size: 1.25rem; }
.duo-ans:active { background: var(--blue-soft); }
/* In: the numbers are hidden until both are in. Hidden, not removed, so the buttons keep their size and nothing below them moves. */
.duo-half.in .duo-ans { background: var(--surface-2); }
.duo-half.in .duo-ans .value { visibility: hidden; }
/* The reveal: green for the right answer, a thick border and a ✓ when it was the pick, dashed red for a pick that was not quite. */
.duo-ans.right { border-color: var(--good); background: var(--good-soft); color: var(--good); }
.duo-ans.pick.right { border-width: 4px; }
/* The plain form first: Safari before 17.4 and Firefox before 128 drop the whole declaration with alt text. */
.duo-ans.pick.right::before { content: "✓"; content: "✓" / ""; position: absolute; top: 3px; right: 7px; font-family: var(--font-ui); font-size: 0.9rem; }
.duo-ans.miss { border: 3px dashed var(--bad); background: var(--bad-soft); color: var(--bad); }
/* Pass: smaller, outlined, in words, so it never reads as a fifth answer. */
.duo-pass {
  align-self: center; justify-self: center; min-height: 44px; padding: 4px 14px;
  border: 2px dashed var(--muted); border-radius: 999px; background: transparent;
  color: var(--muted); font-weight: 700; font-size: 0.95rem;
}
.duo-pass.pick { border-style: solid; border-color: var(--text); color: var(--text); }
/* The key that picks each answer, where a keyboard is likely. */
.duo-key { display: none; }
@media (hover: hover) and (min-width: 600px) {
  /* The number sits below the key's corner, so a long answer at large text never runs into the letter. */
  .duo-ans { padding-top: 0.9rem; }
  .duo-ans .duo-key { display: block; position: absolute; top: 4px; left: 8px; font-family: var(--font-ui); font-size: 0.75rem; font-weight: 400; color: var(--muted); }
  .duo-pass .duo-key { display: inline; margin-left: 6px; font-weight: 400; }
}
.duo-ready {
  min-height: 120px; border-radius: 14px; border: 2px solid currentColor; background: var(--surface);
  font-family: var(--font-num); font-size: 1.8rem; font-weight: 700;
}
/* Pressed: green throughout, words and all ("Ready ✓", --good on --good-soft 5.15:1). As specific as the side's ink
   above and later, so it wins. */
.duo-half .duo-ready[aria-pressed="true"] { border: 4px double var(--good); background: var(--good-soft); color: var(--good); }
.duo-line { min-height: 1.4em; text-align: center; font-weight: 700; }
.duo-line.good { color: var(--good); }
.duo-line.bad { color: var(--bad); }
.duo-keys-hint { text-align: center; font-weight: 700; color: var(--blue); }
.duo-tip { display: none; }
@media (max-width: 36em) and (pointer: coarse) { .duo-tip { display: block; } }
.duo-end h2 { font-family: var(--font-num); font-size: 1.6rem; }
.duo-rights { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; font-weight: 700; }
.duo-rights .blue { color: var(--blue); }
.duo-together { font-weight: 700; }
/* During a round the board gets the width. */
body.duo-run main#screen { max-width: 1100px; }
/* A laptop or Chromebook shows about 580 px of page: the heading is said, not shown (the short-screen rule the dog
   game and Balloon pop share, above), and the gaps tighten. */
@media (max-height: 720px) {
  .duo-run .duo { gap: 8px; }
  .duo-run .duo-q { font-size: 2.2rem; }
}
@media (min-width: 60em) and (min-height: 721px) {
  .duo-ans { min-height: 80px; }
  .duo-q { font-size: 3rem; }
}
/* A phone sideways (under 500 px tall), or the halves stacked: the round takes the whole page. No top bar or footer,
   no card padding, and the safe-area insets on every side, bottom included, so a notch or the home indicator never
   covers an answer. About 280 px in all side by side (277 px measured at 844x390): right at a 360 px Android
   phone's 280 px of page, so anything added here must come out of something else. */
@media (max-height: 500px), (max-width: 36em) {
  body.duo-run .topbar, body.duo-run footer.site { display: none; }
  body.duo-run main#screen {
    padding: env(safe-area-inset-top, 0px) max(8px, env(safe-area-inset-right, 0px)) env(safe-area-inset-bottom, 0px) max(8px, env(safe-area-inset-left, 0px));
  }
  body.duo-run main#screen > h1 { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }   /* still read aloud */
  body.duo-run .card.duo { padding: 6px 0 0; gap: 6px; background: transparent; box-shadow: none; }
  body.duo-run .duo .quiz-head .btn { min-height: 40px; padding: 6px 12px; }
  body.duo-run .duo-board { gap: 6px; }
  body.duo-run .duo-half { padding: 4px 8px 2px; gap: 4px; }
  body.duo-run .duo-band { font-size: 0.95rem; }
  body.duo-run .duo-q { font-size: 1.9rem; min-height: 0; }
  body.duo-run .duo-pad { gap: 6px; }
  body.duo-run .duo-ans { min-height: 56px; font-size: 1.5rem; }
  body.duo-run .duo-ready { min-height: 96px; }
  body.duo-run .duo-line { min-height: 1.25em; font-size: 0.95rem; line-height: 1.25; }
}
/* Large text (200%) or a very narrow half: two by two, with Pass on its own row, and room for a correction on two
   lines, so the "In." that replaces it never pulls the other half's answers up under a finger. */
@media (max-width: 19.5em) {
  .duo-pad, .duo-half.yellow .duo-pad { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-areas: "up left" "down right" "pass pass"; }
  body.duo-run .duo-line { min-height: 2.5em; }   /* 19.5em is inside the compact rule's 36em, so this is the line that applies */
}
@media (forced-colors: active) {
  .duo-half { border: 2px solid CanvasText; }
  .duo-ans, .duo-ready { border-color: ButtonText; }
  .duo-ans.right { border: 4px double CanvasText; }
  .duo-ans.miss { border: 3px dashed CanvasText; }
  .duo-ready[aria-pressed="true"] { border: 4px double CanvasText; }
}

/* ---------- Results ---------- */
.stars { font-size: 2.4rem; letter-spacing: 0.1em; line-height: 1; }
.stars .off { color: var(--muted); }
.stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; }
.stat {
  background: var(--surface-2); border-radius: 12px; padding: 12px 14px;
  display: flex; flex-direction: column; gap: 2px;
}
.stat .label { font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; }
.stat .value { font-family: var(--font-num); font-size: 1.5rem; font-weight: 700; }
.fact-list { display: flex; flex-wrap: wrap; gap: 8px; list-style: none; margin: 0; padding: 0; }
.fact-chip {
  font-family: var(--font-num); font-weight: 700;
  padding: 6px 12px; border-radius: 999px;
  background: var(--bad-soft); color: var(--bad);
}
.fact-chip.slow { background: var(--blue-soft); color: var(--blue); }
.fact-chip.found { background: var(--good-soft); color: var(--good); }   /* Memory match's finished facts: found, not missed */

/* ---------- Chart ---------- */
.chart { overflow-x: auto; position: relative; }   /* keeps the cells' hidden level words inside the scroll box, so the page does not scroll sideways */
.chart table { border-collapse: collapse; font-family: var(--font-num); font-variant-numeric: tabular-nums; min-width: 100%; }
.chart th, .chart td { text-align: center; padding: 6px 4px; min-width: 2.4em; border: 1px solid var(--line); }
.chart th { background: var(--surface-2); font-weight: 700; }
.chart td { cursor: default; }
/* Each level is told apart by its line as well as its color: none for not
   tried, dotted for still learning, solid for known, double and bold for
   fast. The key under the chart draws the same samples. */
.chart [data-level="new"], .legend [data-level="new"] { color: var(--muted); }
.chart [data-level="learning"], .legend [data-level="learning"] { color: var(--text); text-decoration-line: underline; text-decoration-style: dotted; text-decoration-thickness: 2px; text-underline-offset: 3px; }
.chart [data-level="known"], .legend [data-level="known"] { color: var(--blue); text-decoration-line: underline; text-decoration-style: solid; text-decoration-thickness: 2px; text-underline-offset: 3px; }
.chart [data-level="fast"], .legend [data-level="fast"] { color: var(--good); font-weight: 700; text-decoration-line: underline; text-decoration-style: double; text-decoration-thickness: 2px; text-underline-offset: 3px; }
/* The tables picked on the home screen: dark text on yellow, whatever the level, which keeps its line. */
.chart td.pick, .legend .pick { background: var(--accent); font-weight: 700; }
.chart td.pick[data-level] { color: var(--accent-ink); }
/* The row and column under the pointer or the keyboard: tinted, and outlined so it shows on yellow too. */
.chart td.hl, .chart th.hl { background: var(--blue-soft); box-shadow: inset 0 0 0 2px var(--blue); }
.chart td.pick.hl { background: var(--accent); }
.chart td:focus-visible { outline: 3px solid var(--blue); outline-offset: -3px; }
.chart td.pick:focus-visible { outline-color: var(--accent-ink); }
/* The account screen: a stack of cards, the save status in plain text. */
.account { display: flex; flex-direction: column; gap: 16px; }
.account h2 { margin: 0; font-size: 1.1rem; }
.account p { margin: 0; }
.account-status { color: var(--muted); }
.account-points { margin: 0; padding-left: 1.2em; display: flex; flex-direction: column; gap: 6px; }

.legend { display: flex; flex-wrap: wrap; gap: 8px 16px; font-size: 0.9rem; color: var(--muted); list-style: none; margin: 0; padding: 0; }
.legend .sample { display: inline-block; min-width: 2.2em; padding: 1px 4px; margin-right: 4px; text-align: center; border: 1px solid var(--muted); border-radius: 4px; }
.legend .pick { color: var(--accent-ink); }
/* The playtest's numbers on the results after a walk with pt=1. */
.playtest-list { display: grid; grid-template-columns: auto 1fr; gap: 6px 14px; margin: 0; }
.playtest-list dt { color: var(--muted); }
.playtest-list dd { margin: 0; font-weight: 700; }
.clear-progress { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; }
.clear-progress p { margin: 0; }

/* ---------- Prose pages (privacy) ---------- */
/* One readable column instead of the app's cards. */
.doc { width: 100%; max-width: 44rem; margin: 0 auto; padding: 16px 16px 48px; line-height: 1.6; overflow-wrap: break-word; }   /* a long address or heading wraps at 200% text */
.doc h1 { margin-bottom: 6px; }
.doc .lede { color: var(--muted); margin-bottom: 24px; }
.doc h2 { font-family: var(--font-num); font-size: 1.3rem; margin: 0; }
.doc h2.sec { font-family: var(--font-ui); font-size: 1rem; margin: 28px 0 6px; padding-top: 16px; border-top: 1px solid var(--line); }
.doc p, .doc ul { margin: 0 0 12px; }
.doc ul { padding-left: 1.3em; }
.doc li { margin-bottom: 6px; }
.doc a { color: var(--blue); }
.doc code {
  font-size: 0.92em; background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 4px; padding: 1px 5px; overflow-wrap: anywhere;
}
/* The short version, set apart so a parent or teacher can stop there. */
.doc .short { background: var(--surface); border-radius: var(--radius); box-shadow: var(--shadow); padding: 18px 22px 8px; margin-bottom: 20px; }
.doc .meta { color: var(--muted); font-size: 0.88rem; }
/* The conformance table: scrolls inside its own box on a narrow screen. */
.doc-table { overflow-x: auto; margin: 0 0 16px; border-radius: 12px; background: var(--surface); box-shadow: var(--shadow); }
.doc-table:focus-visible { outline: 3px solid var(--blue); outline-offset: 2px; }
.doc-table table { border-collapse: collapse; width: 100%; min-width: 34rem; font-size: 0.92rem; }
.doc-table th, .doc-table td { text-align: left; vertical-align: top; padding: 8px 10px; border-bottom: 1px solid var(--line); }
.doc-table thead th { background: var(--surface-2); }
.doc-table tbody th { font-weight: 700; width: 34%; }
/* ---------- Print: the certificate alone, one letter page ----------
   The same path as the worksheet site: letter paper with 0.5 in margins,
   the sheet 7.5 in wide and well inside the 10 in of height, so phones and
   iPhone Safari's footer still keep it to one page. Fonts are this site's
   own files, so printing needs nothing from anywhere else. */
@media print {
  @page { size: letter; margin: 0.5in; }
  html, body { background: #fff; color: #000; }
  .topbar, footer.site, .no-print { display: none !important; }
  main#screen { display: block; max-width: none; padding: 0; }
  .doc { max-width: none; padding: 0; }
  .doc a { color: inherit; }
  .certificate-wrap { display: block; }
  .certificate {
    width: 7.5in; max-width: none; height: 9.25in; aspect-ratio: auto;
    margin: 0 auto; padding: 0.5in; font-size: 13pt;
    box-shadow: none; outline: none;
    break-inside: avoid;
  }
}

/* Pseudo-elements too: * alone does not reach them, so the switch knob still slid (Issue #56, B28). */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition: none !important; animation: none !important; }
}
/* The "Less motion" switch (on the dog's map, the Balloon pop setup screen, and the two-player Ready screen): the same as the system setting. */
body.calm *, body.calm *::before, body.calm *::after { transition: none !important; animation: none !important; }
@media (max-width: 480px) {
  .card { padding: 16px; }
  .certificate { font-size: 0.85rem; }
  /* Four columns (the 20%), fewer once large text would push two-digit numbers past their tiles (the 2.4rem). */
  .table-grid { grid-template-columns: repeat(auto-fill, minmax(max(20%, 2.4rem), 1fr)); }
  /* Narrow screens: only the current step keeps its name, and most of the
     room; the rest are bars. */
  .step:not([aria-current]) { font-size: 0; }
  .step[aria-current="step"] { flex: 4 1 auto; }
  .match { gap: 10px; }
  /* Two quiet links in the top bar: tighter padding so the brand stays on one line. */
  .topbar { gap: 6px; }
  .topbar .brand { white-space: nowrap; }
  .topbar .btn.quiet { padding: 10px 10px; }
  .topbar #top-account { padding: 10px 12px; }
  /* Three buttons and the brand's words do not fit one line on a phone: with the pilot's Sign in beside Diploma and
     Chart, the brand keeps its icon and its aria-label (as at 360 px), so Sign in stays next to Chart. */
  .topbar.has-account .brand span { display: none; }
  .topbar.has-account .spacer { display: none; }   /* its two gaps are room the signed-in "Account" needs at 320 px; the brand pushes the buttons right instead */
  .topbar.has-account .brand { margin-right: auto; }
}
@media (max-width: 360px) {
  /* The narrowest phones: the brand keeps its icon and its aria-label, not its words. */
  .topbar .brand span { display: none; }
  /* With Sign in as well, 320 px has no room to spare: a little less padding keeps all three buttons on one line. */
  .topbar.has-account .btn { padding-left: 8px; padding-right: 8px; }
  /* Pills take a full line of their own and share it equally. */
  .choice-group { flex: 1 1 100%; }
  .choice-pill { flex: 1 1 0; padding-left: 6px; padding-right: 6px; }
}

/* ---------- Times tables or division (Issue #21) ---------- */
/* The home toggle: two full-width segments, the sign for the eye and the word as the name. Each segment can wrap
   below 7.5em, so at 200% text on a narrow phone the two stack instead of running to three lines. */
.op-toggle { display: flex; flex-wrap: wrap; gap: 0; width: 100%; margin-left: 0; flex: none; }
.op-toggle .choice-pill { flex: 1 1 7.5em; min-height: 48px; font-size: 1.05rem; }
.op-pill { display: inline-flex; align-items: center; justify-content: center; gap: 0.4em; }
.op-sign { font-size: 1.5em; line-height: 1; }
.home-note { margin: 0; color: var(--muted); font-size: 0.95rem; }
/* ÷ is drawn at the numbers' weight: only two dots tell it from a minus. */
.question .sign.div, .table-list .sign.div, .mcard .sign.div, .chart .div { font-weight: 700; color: var(--text); }
/* The times fact under a division miss, and under each line of a division table to learn: the way in, in muted type.
   Under, not beside: beside it, a line could break inside the times fact ("28 ÷ 7 = 4  7 ×" over "4 = 28"). */
.feedback .teach { display: block; font-weight: 400; color: var(--muted); font-size: 0.95em; }
.table-list .teach { flex-basis: 100%; text-align: center; color: var(--muted); font-weight: 400; font-size: 0.8em; }
/* The chart's two switch links, the one on show marked. */
.chart-switch { display: flex; flex-direction: row; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 4px; }
.chart-switch .btn { flex: 0 0 auto; width: auto; }
.chart-switch .btn[aria-current="page"] { background: var(--accent); color: var(--accent-ink); box-shadow: inset 0 0 0 2px var(--accent-ink); }
.diploma-group { margin: 8px 0 0; }

/* ---------- Fallbacks: older Safari and forced colors (Issue #56) ---------- */
/* The joined pills' focus ring, on :focus so Safari before 15.4 (no :focus-visible) shows it too; inset so the
   group's clipped edge and the next pill cannot hide it. The :not() rule, which those browsers drop, hides it after
   a tap where :focus-visible is known (B33). */
.choice-pill:focus { outline: 3px solid var(--blue); outline-offset: -3px; }
.choice-pill[aria-checked="true"]:focus { outline-color: var(--accent-ink); outline-offset: -6px; }
.choice-pill:focus:not(:focus-visible) { outline: none; }
/* Windows contrast themes replace the colors that mark a picked table, an on switch, a picked chart cell (and its
   sample in the key), and the chart switch's current view, so system colors mark them there (B17). A picked table
   and the current view are marked by a double border, so the outline stays focus's ring; a chart cell (the table's
   borders collapse) is marked by an outline, and a focused one gets a thicker one. The knob moves too, so on and off
   differ by place as well as color. */
@media (forced-colors: active) {
  .switch::after { background: ButtonText; }
  .switch[aria-checked="true"] { background: Highlight; border-color: Highlight; }
  .switch[aria-checked="true"]::after { background: HighlightText; }
  .table-pick[aria-pressed="true"] { border: 5px double Highlight; }
  .chart-switch .btn[aria-current="page"] { border: 4px double Highlight; }
  .chart td.pick, .legend .pick { outline: 2px solid Highlight; outline-offset: -2px; }
  .chart td.pick:focus-visible { outline-width: 4px; outline-offset: -4px; }
  .choice-pill[aria-checked="true"]:focus { outline-color: Highlight; }
  /* A chosen pill keeps its mark after a click, when the fallback above would take the ring away. */
  .choice-pill[aria-checked="true"]:focus:not(:focus-visible) { outline: 3px solid Highlight; outline-offset: -3px; }
}
