/* Results — slice 6 of 9 of the former components.css.

   ⚠️ LOAD ORDER IS PART OF THESE FILES. They are CONTIGUOUS slices of one
   6841-line sheet, cut at section banners and never re-sorted, so concatenating
   them in the order index.html lists them reproduces the original byte for byte
   — which is the proof the cascade did not shift. Moving a rule to a different
   slice throws that guarantee away. index.html and sw.js's SHELL_ASSETS must
   list all 9 in the same order as each other.

   Contents: Path puzzle, drill results, quest composer, times table. */

/* ── Path puzzle ─────────────────────────────────────────── */

/* Centred with BOTH insets and auto margins, not left:50% + translateX(-50%).
   An absolutely positioned box at left:50% shrink-to-fits into the space right
   of its left edge, i.e. HALF the pane, so on a phone the sentence was squeezed
   to its longest word and cut off: measured at 375×812 (v418), Path's prompt
   wrapped one word per line and ended "Portu…". With both insets the bar can
   use the whole pane and still centres. */
.wu-path-bar {
  position: absolute;
  top: var(--space-md);
  left: 0;
  right: 0;
  margin-inline: auto;
  width: fit-content;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-pill);
  padding: var(--space-xs) var(--space-md);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
  max-width: min(92%, 600px);
}

.wu-path-quit {
  background: none;
  border: none;
  cursor: pointer;
  font-size: var(--font-size-md);
  color: var(--colour-text-secondary);
  /* 32 → 44px (v289 audit). It sits inside a pill, so the glyph stays the same
     size; only the target grows. */
  min-width: 44px;
  min-height: 44px;
}

.wu-path-prompt {
  /* The FLEXIBLE item in the bar: it must yield to the fixed controls rather
     than push them off-screen. Growing the ▸ to a 44px target (v291) took the
     bar 14px past the right edge on a phone — the caught-in-the-sweep example
     of a control minimum propagating into its container. Without min-width:0 a
     flex item refuses to shrink below its text. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: var(--font-size-sm);
  color: var(--colour-text-primary);
  /* Wrap long instructions onto multiple lines instead of truncating —
     the pill grows taller and the quit/errors stay vertically centred. */
  white-space: normal;
  line-height: var(--line-height-tight);
  text-align: center;
}

.wu-path-prompt::before {
  content: "";
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--path-color, var(--colour-accent));
  margin-right: 6px;
}

.wu-path-errors {
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  color: var(--colour-danger);
  flex: none;
}

/* One slip in — the warning takes the error tally's place so the bar keeps
   its three-part layout on a phone (v274). Amber, not red: it's a caution,
   and red is already what a wrong tap flashes. */
.wu-path-warn {
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  color: var(--colour-warning);
  white-space: nowrap;
}

.wu-path-wrong .wu-hex {
  animation: wu-path-shake 320ms;
}

@keyframes wu-path-shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-5px); }
  50% { transform: translateX(5px); }
  75% { transform: translateX(-3px); }
}

.wu-path-start .wu-hex {
  animation: wu-path-pulse 1.4s ease-in-out infinite;
}

@keyframes wu-path-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.07); }
}

/* ── Drill results ───────────────────────────────────────── */

.wu-results {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-md);
  width: 100%;
}

/* Compact: a footer under the exercise, not a takeover of the layer. `flex: 1`
   would have it stretch to eat whatever room the grid left. */
.wu-results-compact {
  flex: none;
  gap: var(--space-sm);
}

.wu-results-emoji {
  font-size: 64px;
  line-height: 1;
}

.wu-results-score {
  font-family: var(--font-mono);
  font-size: var(--font-size-3xl);
  margin: 0;
  color: var(--colour-text-primary);
}

.wu-results-time {
  font-family: var(--font-mono);
  font-size: var(--font-size-lg);
  color: var(--colour-text-secondary);
  margin: 0;
}

/* 🗺️ Quest progression modal. A LIST of the whole set, not a progress bar: the
   question the child is answering is "what have I done and what is next", and a
   bar answers neither. Three states, with the next one called out rather than
   merely being the first undone. */
.wu-quest-list {
  list-style: none;
  margin: var(--space-md) 0;
  padding: 0;
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}
.wu-quest-item {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  min-height: 32px;
  font-size: var(--font-size-sm);
  color: var(--colour-text-secondary);
}
.wu-quest-mark {
  flex: 0 0 20px;
  text-align: center;
  font-weight: var(--font-weight-bold);
}
.wu-quest-item[data-state="done"] { color: var(--colour-success); }
.wu-quest-item[data-state="next"] {
  color: var(--colour-text-primary);
  font-weight: var(--font-weight-bold);
}
.wu-quest-item[data-state="next"] .wu-quest-mark { color: var(--colour-brand-deep); }
.wu-quest-item[data-state="todo"] { opacity: 0.6; }

/* The answer language, sitting on the letter strip directly above the keys —
   "what to type" over "where to type". A full-width caption rather than a chip:
   the strip owns the whole width in both crossword layouts, so it costs one
   short line and can never squeeze a control off screen (which is exactly what
   it did on the clue bar at 375px). */
.wu-xw-lang {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.3em;
  margin-bottom: var(--space-xs);
  font-family: var(--font-body);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--colour-accent);
  white-space: nowrap;
}

/* The child asked, rather than a threshold inferring it — the stronger signal
   of the two, so it is marked rather than left to look like a stray good score
   in a panel about struggling. */
.wu-support-asked {
  display: inline-block;
  margin-left: var(--space-xs);
  padding: 1px 7px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--colour-accent) 14%, var(--colour-surface));
  color: var(--colour-accent);
  font-size: var(--font-size-xs, 12px);
  font-weight: var(--font-weight-bold);
  white-space: nowrap;
}

/* 🙋 "I found this tricky" (v355). Quiet and BELOW the two main actions: it is
   always available, at any score, but it must not compete with "carry on" or
   look like the expected response to a good run. Full-width so it is easy to
   hit without being loud. */
.wu-results-help {
  display: block;
  width: 100%;
  margin-top: var(--space-md);
  min-height: 44px; /* tap floor, #88–#90 */
  padding: var(--space-sm);
  background: transparent;
  border: 1px dashed var(--colour-border);
  border-radius: var(--radius-button);
  font-family: var(--font-body);
  font-size: var(--font-size-sm);
  color: var(--colour-text-secondary);
  cursor: pointer;
}
.wu-results-help:hover { border-color: var(--colour-accent); color: var(--colour-accent); }
/* Stays pressed rather than vanishing — a child who taps it should SEE that it
   worked, and a disappearing button reads as "did that register?". */
.wu-results-help.wu-set-on {
  border-style: solid;
  border-color: var(--colour-success);
  color: var(--colour-success);
  cursor: default;
}

/* A rail stat that just went UP (Phase 6) — in practice the ⭐, at the moment a
   weekly flower pays out. Fires only on an increase, so the number a child has
   held all week never re-celebrates itself on every app launch. */
.wu-rail-stat.wu-stat-up {
  animation: wu-stat-bump 620ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes wu-stat-bump {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.35); }
  100% { transform: scale(1); }
}

/* The results emoji arrives rather than appearing. Deliberately NOT applied to
   the low band (🔁) or a rushed run (⏳): a bounce is a celebration, and
   celebrating a run that went badly is the tonal mistake #117 set out to fix. */
.wu-results-emoji {
  animation: wu-result-in 520ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.wu-results-emoji[data-quiet="1"] { animation: none; }
@keyframes wu-result-in {
  0%   { transform: scale(0.4) rotate(-8deg); opacity: 0; }
  60%  { transform: scale(1.15) rotate(3deg); opacity: 1; }
  100% { transform: scale(1) rotate(0deg); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .wu-rail-stat.wu-stat-up,
  .wu-results-emoji { animation: none; }
}

/* ✋ Abandon guard (v354). The SAFE action is the primary one and sits on the
   right, where this app's sheets already put the go-ahead (the quest sheet's
   "▶ Keep going", the study choice's confirm) — so the muscle memory the child
   has says "carry on", and stopping takes a deliberate reach to the quieter
   button. */
/* .wu-abandon-backdrop .wu-sheet-primary lived here and declared exactly what
   the shared .wu-sheet-primary in sheets.css now declares — a generic class
   name scoped to one sheet, which meant the next sheet to use it got an
   unstyled button. Moved, not duplicated (#68). */
.wu-abandon-backdrop .wu-abandon-leave {
  color: var(--colour-text-secondary);
}

/* ── 🗺️ Quest composer (parent dashboard, v353) ──────────────────────────────
   "The setup should be as simple as to add the 1,2,3 badge to the activities."
   A row IS the exercise; tapping it appends a step and the badge appears on the
   row itself, so what-is-in-the-quest is readable from the tree with nothing
   else open. Repeats show as ①③ on one row — no counter to explain. */
.wu-q-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  width: 100%;
  min-height: 44px; /* tap floor, #88–#90 */
  padding: var(--space-xs) var(--space-sm);
  background: var(--colour-bg);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-button);
  font-family: var(--font-body);
  font-size: var(--font-size-sm);
  color: var(--colour-text-primary);
  cursor: pointer;
  margin-bottom: var(--space-xs);
}
.wu-q-row:hover { border-color: var(--colour-accent); }
.wu-q-row.wu-set-on {
  border-color: var(--colour-brand-deep);
  background: color-mix(in srgb, var(--colour-brand) 12%, var(--colour-surface));
}
.wu-q-row:disabled {
  opacity: 0.45;
  cursor: default;
}
.wu-q-row:disabled:hover { border-color: var(--colour-border); }
.wu-q-row-label { text-align: left; }
.wu-q-row-badges {
  display: inline-flex;
  gap: 4px;
  align-items: center;
}

/* The step-number badge — the same disc in the tree, the steps list and (via
   the shared sheet) nowhere else, so "①" always means "step 1 of the quest". */
.wu-q-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 999px;
  background: var(--colour-brand-deep);
  color: #fff;
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 800;
  line-height: 1;
}

/* The ＋ affordance on an unbadged row: quiet, so the badges stay the loudest
   thing in the column. */
.wu-q-add {
  color: var(--colour-text-secondary);
  font-size: var(--font-size-md);
  opacity: 0.7;
}

/* Times-table rows pack as a grid — twelve list rows would be a wall.
   ⚠️ IT REFLOWS, it does not shrink. `repeat(4, 1fr)` is `minmax(auto, 1fr)`
   four times, so no track could go below a row's min-content 56px: measured at
   375px inside the doubly-nested ×-tables panel, a 62px button overflowing its
   column by 42px. `auto-fit` + a 56px floor drops to three columns, or two, and
   keeps every row whole — the same answer the rail drawer's button grid reached
   in v399, and the same reason: at these widths the choice is reflow or clip,
   and shrinking a 44px-tall tap target is not on the table (#88–#90). */
.wu-q-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(56px, 1fr));
  gap: var(--space-xs);
}
.wu-q-grid .wu-q-row { margin-bottom: 0; justify-content: center; gap: 4px; }
.wu-q-grid .wu-q-row .wu-q-row-label { text-align: center; }

/* Nested accordion inside the quest panel — one visual step in from its parent
   so the tree reads as a tree. */
.wu-q-sub {
  margin-left: var(--space-sm);
}
.wu-q-sub > summary { font-size: var(--font-size-sm); }

/* A quest row inside the weekly mix — indented under its subject's slider so it
   reads as a property of those tiles, not a competing activity. */
.wu-set-row.wu-set-sub {
  margin-left: var(--space-lg);
  padding-top: 0;
}

.wu-results-actions {
  display: flex;
  gap: var(--space-md);
  margin-top: var(--space-md);
}

/* ⚠️ THE BUTTON SHAPE IS THE CLASS'S OWN, not the container's. Everything here
   used to be scoped to `.wu-results-actions button`, so `.wu-results-done` on
   its own — which is exactly how the stacked grid renders "✓ Check answers" —
   matched NO rule and came out as a bare UA button: measured 23px tall, half
   the Phase 6 tap floor, on the control that submits a whole round. It had been
   that way since the times table gained two rounds (#124/v379) and went unseen
   because the layout walker only ever reached the times-table PICKER; 🪜 Ladder
   sits on the approach flower, so the walker finally entered a running grid and
   reported it on the first run.

   Third instance of the same shape: `.wu-sheet-go` (#167a) and `.wu-sheet-
   primary` (#68) were both class names that looked like components and matched
   nothing outside one ancestor. A class that names a control has to carry that
   control's own shape. */
.wu-results-actions button,
.wu-results-done {
  padding: var(--space-md) var(--space-xl);
  font-family: var(--font-display);
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-bold);
  background: var(--colour-bg);
  color: var(--colour-text-primary);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-button);
  cursor: pointer;
  min-height: var(--tap-target-min);
}

.wu-results-actions button:hover,
.wu-results-done:hover {
  border-color: var(--colour-accent);
}

/* The success tint, which the results footer and the grid's ✓ Check share. */
.wu-results-actions .wu-results-done,
.wu-results-done {
  background: color-mix(in srgb, var(--colour-success) 22%, var(--colour-bg));
  border-color: var(--colour-success);
  color: var(--colour-success);
}

/* ── Times Table summary (replaces the mixed Path tile) ───────── */
.wu-tt-pick {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-lg);
  margin-top: var(--space-xl);
}
.wu-tt-prompt {
  font-family: var(--font-display);
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-bold);
  color: var(--colour-text-primary);
}
.wu-tt-pickgrid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-sm);
  width: min(360px, 100%);
}
.wu-tt-pickbtn {
  aspect-ratio: 1;
  font-family: var(--font-mono);
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-bold);
  background: var(--colour-surface);
  border: 2px solid var(--colour-border);
  border-radius: var(--radius-card);
  color: var(--colour-text-primary);
  cursor: pointer;
  transition: border-color var(--transition-fast), background var(--transition-fast);
}
.wu-tt-pickbtn:hover {
  border-color: var(--colour-accent);
  background: color-mix(in srgb, var(--colour-accent) 8%, var(--colour-surface));
}
.wu-tt-pickbtn:disabled {
  opacity: 0.3;
  cursor: not-allowed;
}
.wu-tt-pickbtn:disabled:hover {
  border-color: var(--colour-border);
  background: var(--colour-surface);
}
/* Already credited today (v274) — same green "done" reading as a filled hex.
   Still tappable: a spent table remains open as practice, it just won't fill a
   slot, and the child can see that before choosing rather than after. */
.wu-tt-done {
  position: relative;
  border-color: color-mix(in srgb, var(--colour-success) 55%, var(--colour-border));
  background: color-mix(in srgb, var(--colour-success) 14%, var(--colour-surface));
}
.wu-tt-done::after {
  content: "✓";
  position: absolute;
  top: 2px;
  right: 5px;
  font-family: var(--font-display);
  font-size: var(--font-size-xs);
  font-weight: 800;
  color: var(--colour-success);
}

/* The full table — one column of 12 rows on phones, two columns when wide. */
/* Order toggle, under the number grid (owner, 2026-08-29). Two named buttons
   rather than one switch: "In order" and "Mixed up" are two different
   exercises, and a labelled pair says which one you are in without the child
   having to work out what a switch's position means. */
/* Round banner. There is no toggle: the exercise IS both rounds, in order then
   shuffled (#124, owner: "saves on UI clutter"), so this only has to say where
   the child currently is. */
.wu-tt-round {
  margin: var(--space-sm) 0 0;
  text-align: center;
  color: var(--colour-text-secondary);
  font-size: var(--font-size-sm);
}
/* The split. It is the most useful line on the footer for a grown-up: 12/12
   then 8/12 says the ladder held and the recall did not, which neither number
   says alone. */
.wu-results-split {
  margin: var(--space-xs) 0 0;
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  color: var(--colour-text-primary);
}
/* A 20-row ladder is taller than a short screen, and .wu-drill-layer is a flex
   column with no overflow of its own — so without this the last rows and the
   ✓ Check button are simply unreachable (the #161(c) shape: `min-height: 0` so
   the flex child can shrink, and `safe` centring so an overflow is not pushed
   off the unreachable start edge). */
.wu-drill-layer:has(.wu-tt-grid) {
  justify-content: safe center;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* ⌨️ THE BOXES ARE TAPPED, NOT TYPED INTO — the house keypad, exactly the
   one the 🧩 Grid ends with (#179). It began as a device-keyboard report
   (owner, on a tablet, 2026-09-27: "the input panel comes up and hides part of
   the exercise") and the pad is the fix rather than a compensation for it.
   Android does not shrink the LAYOUT viewport for the keyboard, so a layer at
   `inset: 0` keeps its full height and centres the rows inside a box whose
   lower half nobody can see; re-centring against `visualViewport` would have
   worked, and would have been a listener, a focus latch and a zoom guard spent
   compensating for a keyboard this screen never wanted. Two more things went
   with it: `inputmode="numeric"` is only a HINT, so some devices raised a full
   QWERTY with an autocorrect bar in front of a seven-year-old, and a native box
   measured ~51×32px — under the 44px floor (#88–#90) on every row. */
.wu-tt-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  min-height: 0;
}
.wu-tt-side { flex: none; }
/* An element with its own `display` beats the UA's `[hidden]` rule; this one
   has none, but the guard is cheap and the trap has bitten three times. */
.wu-tt-side[hidden] { display: none; }
.wu-tt-pad {
  /* The standalone drill's pad lifts itself off the screen bottom; here it sits
     with its own grid, so it keeps neither the lift nor the padding. */
  margin: var(--space-md) 0 0;
  padding-bottom: 0;
}
/* ▸ is a move and ✓ is a commit, so ▸ wears the SAME green at a whisper and
   ✓ wears it in full — the key says which of the two it is before it is
   pressed. v470 drew ▸ as a plain digit, which kept the distinction and lost
   the family: the owner asked for it back ("can the next keep a green tone").
   The pair now reads as one control at two strengths rather than two
   unrelated keys that happen to share a slot. */
.wu-numpad.wu-tt-pad .wu-numpad-enter:not(.wu-tt-go-ready) {
  background: color-mix(in srgb, var(--colour-success) 9%, var(--colour-bg));
  color: var(--colour-success-ink);
  border-color: color-mix(in srgb, var(--colour-success) 50%, var(--colour-border));
}
.wu-tt-anyway {
  background: none;
  border: none;
  color: var(--colour-text-secondary);
  font-size: var(--font-size-sm);
  text-decoration: underline;
  cursor: pointer;
  min-height: 44px;
  padding: 0 var(--space-md);
}

.wu-tt-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-xs);
  width: min(440px, 100%);
  margin: var(--space-md) auto 0;
  /* wu-portrait-stack ── UPRIGHT, THE ROWS SIT ABOVE AND THE PAD BELOW, BOTH
     ON SCREEN (owner, on a tablet held upright, 2026-09-28: "on vertical it
     should keep the times table above and pad below"). The TABLE scrolls
     inside its own box, not the whole layer: with the layer scrolling, a
     12- or 20-row table put the pad below the fold, so tapping a digit meant
     scrolling the row being filled out of sight. `min-height: 0` lets this
     flex item shrink below its rows; the wrap already can; the pad is
     `flex: none` and never gives up height. The selected row is scrolled into
     view inside this box on every move (`scrollIntoView`, block: nearest). */
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}
@media (min-width: 760px) {
  .wu-tt-grid {
    /* ⚠️ FLOW DOWN, NOT ACROSS — this was the bug behind "it said 2 × 3 = 6
       was wrong" (owner, 2026-08-29). With the default row-major flow, two
       columns lay 1× and 2× SIDE BY SIDE, so reading down the left column gave
       the odd multiples only (1, 3, 5, 7, 9, 11) while Enter advanced to the
       cell on the RIGHT. A child filling the table in sequence — which is the
       one thing a times table is FOR — put every answer in the wrong box.
       Column flow makes DOM order, keyboard order and reading order the same
       single sequence: 1-6 down the left, 7-12 down the right. It cannot
       reproduce on a phone (single column), which is why the same child got it
       right on one run and wrong on the next. */
    grid-auto-flow: column;
    /* ⚠️ ALWAYS EXACTLY TWO COLUMNS, whatever the row count. This was
       `repeat(6, auto)`, which is 12 rows in two columns and nothing else —
       correct while the times table was the only caller, and silently wrong the
       moment 🪜 Ladder arrived with the parent's 10/15/20 questions: 15 rows
       would have flowed into THREE columns and 20 into four, inside a 440px
       box. Worse than cramped — it re-opens the #124 bug, where the sequence
       the child reads is not the sequence Enter follows.
       The caller sets --tt-rows to ceil(n / 2), so column flow keeps DOM order,
       keyboard order and reading order one single sequence at any length. */
    grid-template-rows: repeat(var(--tt-rows, 6), auto);
    grid-template-columns: 1fr 1fr;
    column-gap: var(--space-lg);
    width: min(640px, 100%);
  }
}
.wu-tt-row {
  display: flex;
  align-items: center;
  justify-content: center;
  justify-self: center;
  /* Fixed slots, not `fit-content`: every row is the same width, so the ×, the
     = and the boxes line up down the column the way a printed table does, and
     the reserved → slot means nothing MOVES when the answers are revealed. */
  width: min(100%, 17em);
  gap: var(--space-sm);
  padding: var(--space-xs) var(--space-md);
  background: var(--colour-bg);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-button);
}
.wu-tt-q {
  font-family: var(--font-mono);
  font-size: var(--font-size-md);
  color: var(--colour-text-primary);
  min-width: 5ch;
  text-align: right;
}
.wu-tt-eq {
  color: var(--colour-text-secondary);
}
.wu-tt-q {
  min-width: 4.4em;
  text-align: right;
}
/* The correct answer, shown BESIDE the child's own — never over it. Always in
   the DOM so the row does not jump when it is filled in. */
.wu-tt-fix {
  min-width: 3.4em;
  flex: none;
  text-align: left;
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-bold);
  color: var(--colour-success);
}
.wu-tt-input {
  width: 3.4em;
  /* The 44px floor. A native input was ~32px tall, which nobody had measured
     because a text box does not read as a tap target until it becomes one. */
  min-height: 44px;
  cursor: pointer;
  flex: none;
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--font-size-md);
  padding: var(--space-xs) var(--space-xs);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-button);
  color: var(--colour-text-primary);
}
.wu-tt-input:focus-visible {
  outline: 2px solid var(--colour-accent);
  outline-offset: 2px;
}
/* WHERE THE NEXT DIGIT LANDS. Nothing else on this screen is selected, so the
   ring is the only thing saying which of twenty boxes the pad is typing into
   — it is information, not decoration. */
.wu-tt-input.wu-tt-sel {
  border-color: var(--colour-accent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--colour-accent) 38%, transparent);
}
.wu-tt-input.wu-tt-right {
  border-color: var(--colour-success);
  background: color-mix(in srgb, var(--colour-success) 14%, var(--colour-surface));
  color: var(--colour-success);
}
.wu-tt-input.wu-tt-wrong {
  border-color: var(--colour-danger);
  background: color-mix(in srgb, var(--colour-danger) 12%, var(--colour-surface));
  color: var(--colour-danger);
}
/* SIDE BY SIDE IN LANDSCAPE, AND ONLY IN LANDSCAPE (owner, on a tablet:
   "maths grid has lots of space on tablet. keep grid on left and input on the
   right"). Stacked, four 66px key rows plus a ladder do not fit under 400px of
   height at any padding; side by side, width is the axis a sideways screen has
   spare.

   ⚠️ This was `(min-width: 700px), (orientation: landscape)` until v472, and
   the WIDTH arm was the bug: an upright tablet is 768px or more across, so it
   took the side-by-side layout on the axis it has LEAST of, and at 760px the
   table also switches to two columns of rows — which then had ~360px beside a
   268px pad (owner: "vertical times table … gets hidden below the input pad").
   Upright, height is the spare axis, so the page stacks, whatever the width. */
@media (orientation: landscape) {
  .wu-tt-wrap {
    flex-direction: row;
    align-items: center;
    justify-content: center;
    gap: var(--space-lg);
    flex: 1 1 auto;
  }
  .wu-tt-grid {
    margin: 0;
    flex: 0 1 auto;
    min-width: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    max-height: 100%;
  }
  .wu-tt-pad { margin: 0; }
}
@media (orientation: landscape) and (max-height: 500px) {
  .wu-tt-pad { grid-template-columns: repeat(3, 60px); }
  .wu-tt-pad button { height: 48px; }
  .wu-tt-grid { max-width: 520px; }
}

.wu-tt-actions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-md);
}
/* The compact results headline (v362) — was .wu-tt-score, renamed when the
   times table adopted the shared results screen. Same one-line treatment, now
   available to any footer-shaped result. */
.wu-results-line {
  font-family: var(--font-display);
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-bold);
  color: var(--colour-text-primary);
  margin: 0;
}

/* In-app APK update banner (native shell only) — fixed bottom pill */
/* A CARD, not a pill: it has to hold a progress bar and a line saying where the
   file went, because the shell hides the status bar the download notification
   would otherwise appear in (see app-update.js). A pill can hold neither. */
.wu-update-banner {
  position: fixed;
  /* Centred on the CONTENT, not the viewport. The rail is `position: fixed`, so
     the body's padding-left never reaches another fixed element — the same trap
     `notice.js` documents. Centring on 50vw put this card 16px under the spine
     on a phone. */
  left: calc(var(--safe-left) + var(--rail-bar-w) + var(--space-md));
  right: calc(var(--safe-right) + var(--space-md));
  bottom: var(--support-bottom);
  z-index: 220;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-sm);
  width: max-content;
  /* BORDER-BOX, or the clamp below bounds only the content and the padding and
     border hang outside it: on a phone the card ran 26px past the right gutter
     (device test 10, v425). There is no global box-sizing here (#90), so every
     clamp written against the viewport has to say so itself (#99). The 420px
     cap keeps its old content width, so a tablet sees the same card. */
  box-sizing: border-box;
  max-width: min(
    calc(420px + var(--space-sm) + var(--space-md) + 2px),
    calc(100vw - var(--safe-left) - var(--safe-right) - var(--rail-bar-w) - 2 * var(--space-md))
  );
  margin-inline: auto;
  padding: var(--space-sm) var(--space-sm) var(--space-sm) var(--space-md);
  background: var(--colour-surface);
  border: 1px solid var(--colour-accent);
  border-radius: var(--radius-card);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.18);
  font-size: var(--font-size-sm);
  color: var(--colour-text-primary);
}
.wu-update-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
}
/* min-width:0 or the text refuses to shrink below its content size and shoves
   the ✕ off the card — a flex item's automatic minimum, the same trap that
   pushed Cross Words' ▸ off screen (#90). It WRAPS rather than ellipsising:
   truncating a version number loses the one fact the line carries. */
.wu-update-text {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
}
/* Full width, below the text — the phone had all three on one row and the
   button ran off the edge. A stacked action also gives it a target the width
   of the card instead of whatever was left over. */
.wu-update-go {
  width: 100%;
  min-height: 44px;
  padding: var(--space-xs) var(--space-md);
  border: none;
  background: var(--colour-accent);
  color: #fff;
  border-radius: var(--radius-pill);
  font-family: inherit;
  font-size: inherit;
  font-weight: var(--font-weight-bold);
  text-decoration: none;
  cursor: pointer;
}
.wu-update-x {
  position: relative;
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  border: none;
  background: transparent;
  color: var(--colour-text-secondary);
  font-size: var(--font-size-md);
  cursor: pointer;
}
/* 28px disc, 44px target — the disc stays small so it does not outweigh the
   Update button beside it (the games ✕ treatment, decisions #88–#90). */
.wu-update-x::after {
  content: "";
  position: absolute;
  inset: 50% auto auto 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}
.wu-update-prog {
  height: 6px;
  overflow: hidden;
  background: color-mix(in srgb, var(--colour-accent) 18%, transparent);
  border-radius: var(--radius-pill);
}
.wu-update-prog > i {
  display: block;
  width: 0;
  height: 100%;
  background: var(--colour-accent);
  border-radius: var(--radius-pill);
  transition: width 0.25s ease;
}
/* Indeterminate: the shell hands the file to DownloadManager and tells us
   nothing further, so the bar must show ACTIVITY without claiming a figure. */
.wu-update-banner[data-state="starting"] .wu-update-prog > i,
.wu-update-banner[data-state="handoff"] .wu-update-prog > i {
  width: 40%;
  animation: wu-update-slide 1.4s ease-in-out infinite;
}
@keyframes wu-update-slide {
  from {
    transform: translateX(-110%);
  }
  to {
    transform: translateX(260%);
  }
}
.wu-update-note {
  margin: 0;
  max-width: 34ch;
  color: var(--colour-text-secondary);
  font-size: var(--font-size-xs);
  line-height: 1.4;
}
.wu-update-note b {
  color: var(--colour-text-primary);
  overflow-wrap: anywhere;
}
@media (prefers-reduced-motion: reduce) {
  .wu-update-banner[data-state="starting"] .wu-update-prog > i,
  .wu-update-banner[data-state="handoff"] .wu-update-prog > i {
    width: 100%;
    opacity: 0.45;
    animation: none;
  }
  .wu-update-prog > i {
    transition: none;
  }
}

/* Parent dashboard → 📊 Insights (last-30-day usage) */
.wu-insight-overview {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-sm);
  margin-bottom: var(--space-sm);
}
.wu-insight-acc {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-bottom: var(--space-sm);
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  color: var(--colour-text-secondary);
}

/* Help panels (child Settings flower → ❓ Help) */
.wu-help-text {
  margin: 0;
  padding: 0 var(--space-xs) var(--space-sm);
  color: var(--colour-text-secondary);
  font-size: var(--font-size-sm);
  line-height: 1.5;
}
/* The games topic is a LIST built from the roster (v472), one game a line:
   icon, name, the goal line from that game's own how-to card. */
.wu-help-games {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}
.wu-help-games li {
  display: flex;
  gap: var(--space-sm);
  align-items: baseline;
}
.wu-help-game-ic {
  flex: none;
  width: 1.4em;
  text-align: center;
}
/* Checkers' icon is an SVG file, not an emoji (#145). */
.wu-help-game-ic img {
  width: 1.2em;
  height: 1.2em;
  vertical-align: -0.2em;
}


/* ── 🧩 The Operation Grid (owner's page 44) ─────────────────────────── wu-og-
   Its own sub-prefix per working rule 8: it shares the stacked-grid FAMILY with
   the times table but not its layout, so it does not share its classes. */
.wu-og-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-md);
  min-height: 0;
  width: 100%;
}
/* wu-og-centre ── CENTRED VERTICALLY, LIKE THE TIMES TABLE (owner, 2026-09-28:
   "times table is good centred vertically on both landscape and portrait.
   grid is aligned to the top"). The drill layer's default is `flex-start`,
   and only the stacked drill's layer overrode it, so an UPRIGHT grid packed
   everything at the top: measured 178px of empty layer under it on a 768×1024
   tablet and 206px on a 375×812 phone. This is the same rule the times table
   uses, for the same reasons: `safe` so a column that ever outgrows the layer
   starts at the top instead of being pushed off an unreachable edge, and the
   scroll is the net that makes that edge reachable.

   Sideways it changes nothing, and that was measured too: side by side the
   wrap GROWS to fill the height and centres the grid inside itself (#180), and
   on a sideways phone the layer is a grid whose fr tracks leave no free space
   for `justify-content` to act on. */
.wu-drill-layer:has(.wu-og-wrap) {
  justify-content: safe center;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.wu-og-grid {
  display: grid;
  /* One track per column INCLUDING the header column, all equal, so the header
     line up with the cells they label. 1fr rather than a fixed px: the grid is
     capped by its own width below and the cell size falls out of that, which is
     what keeps a 5-column and a 7-column grid the same overall size. */
  grid-template-columns: repeat(var(--og-cols, 5), minmax(0, 1fr));
  gap: 2px;
  /* WIDTH IS SET INLINE by startOpGrid, which measures the room the grid will
     have and solves the cell size from it. This is only the fallback for a
     grid rendered before that runs; a `cols * Npx` cap here silently overrode
     the solved size and spilled the grid out of its own wrap. */
  width: min(420px, 100%);
  flex: none;
}
.wu-og-head,
.wu-og-cell {
  /* Square cells whatever the column width, so the grid reads as a table and
     not as a set of letterboxes. */
  aspect-ratio: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  border-radius: var(--radius-cell);
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  line-height: 1.05;
  min-width: 0;
}
.wu-og-head {
  background: color-mix(in srgb, var(--colour-accent) 14%, var(--colour-bg));
  color: var(--colour-text-primary);
  font-weight: var(--font-weight-bold);
}
.wu-og-corner {
  background: color-mix(in srgb, var(--colour-accent) 30%, var(--colour-bg));
  font-size: var(--font-size-md);
}
.wu-og-cell {
  padding: 0;
  border: 1px solid var(--colour-border);
  background: var(--colour-surface);
  color: var(--colour-text-primary);
  cursor: pointer;
  font-weight: var(--font-weight-bold);
}
/* A GIVEN cell is the worked example, so it reads as printed rather than as
   something the child left blank. */
.wu-og-given {
  background: var(--colour-bg);
  color: var(--colour-text-secondary);
  border: 1px solid var(--colour-border);
  cursor: default;
}
.wu-og-cell.wu-og-sel {
  border-color: var(--colour-accent);
  box-shadow: inset 0 0 0 2px var(--colour-accent);
}
.wu-og-cell.wu-og-right {
  background: color-mix(in srgb, var(--colour-success) 18%, var(--colour-bg));
  border-color: var(--colour-success);
}
.wu-og-cell.wu-og-wrong {
  background: color-mix(in srgb, var(--colour-danger) 12%, var(--colour-bg));
  border-color: var(--colour-danger);
}
/* The child's own answer stays, struck through; the right one goes UNDER it,
   never over it (#124). */
.wu-og-cell.wu-og-wrong .wu-og-in {
  text-decoration: line-through;
  color: var(--colour-danger);
  font-size: var(--font-size-xs);
}
.wu-og-fix {
  color: var(--colour-success);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
}
/* The pad is the sudoku-style input the owner asked for. Ten digits plus ⌫ in
   a fixed three-across block, so the digits sit where they do on every other
   numpad in the app. */
.wu-og-side {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  flex: none;
}
/* An element with its own `display` needs its own [hidden] rule, or the UA's
   `display: none` loses to it — the trap #145 hit with the two-player overlay. */
.wu-og-side[hidden] { display: none; }

.wu-og-pad {
  margin-bottom: 0;
  padding-bottom: 0;
  grid-template-columns: repeat(3, 64px);
  gap: var(--space-xs);
  flex: none;
}
.wu-og-pad button { height: var(--tap-target-min); }
/* Inherits .wu-numpad-enter's green from drills.css — declared there once for
   every numpad in the app, so the ✓ cannot end up a different green here. */

/* ⚠️ THE GRID RESERVES THE CHROME COLUMN, and it is the only drill that has
   to. During an activity the rail sheds its spine (#171) but the trail circle
   and the ✕ stay, floating over the content at the top left. Every other
   drill CENTRES its work in the layer, so it clears them by accident; the grid
   fills its half from the top-left corner and ran under them — 5px of
   clearance at 851×393 and none at all on a phone (owner: "grid to be clear
   of the left padding for the pills").

   Same reserve a full-screen modal makes (#164) and the SAME TOKEN, so a pill
   that changes size takes the gutter with it rather than this going stale —
   the hardcoded-reserve failure this file has recorded five times (#89, #90,
   #98, #114, #143). startOpGrid reads the layer's real padding when it solves
   the cell size, so the grid simply comes out a little smaller and nothing
   else has to know.

   ⚠️ SCOPED TO THE STATE WHERE THE SPINE IS ACTUALLY HIDDEN — the same
   query #171 uses, and it has to be the same one. Where the rail still draws
   its spine the pills sit ON it, the layer already starts past them (measured:
   layer at x=73 against pills ending at 50 on a 1280×800 desktop), and this
   reserve would push the grid 52px right of centre to clear something that was
   never in the way. A gutter for chrome that is not overlapping is just a
   margin nobody asked for. */
@media (max-width: 720px), (orientation: landscape) and (max-height: 500px) {
  .wu-drill-layer:has(.wu-og-wrap) {
    padding-left: calc(var(--layer-pad-x) + var(--safe-left) + var(--chrome-w));
  }
}

/* ── HALF THE SCREEN IS THE GRID, HALF IS EVERYTHING ELSE ───────────────
   Owner, on this screen specifically: "half for the grid and half for the
   title, timer, input". The same trade the two-column drill (#172) and the
   games rail (#88) make — in short landscape height is the scarce axis and
   width is the plentiful one, so chrome that sat ABOVE the work moves BESIDE
   it and the work keeps the full height.

   `display: contents` on the wrap is what lets the head, the hint, the grid,
   the pad and the results be placed as five items of ONE grid while the DOM
   keeps its honest reading order (head → hint → work → results) for portrait,
   for a screen reader and for tab order. Same technique as the calendar's
   two-column card (#163), and for the same reason: no DOM move, no second
   copy of anything.

   ⚠️ `--og-split` is a CONTRACT with startOpGrid, which solves the cell size
   from the room the grid will have. The two layouts leave it very different
   room (here the grid spans every row and loses half the WIDTH; stacked it
   keeps the width and loses the head, hint and pad out of its HEIGHT), and a
   flag set where the layout is decided is the only honest way for the
   measurement to know which it is. Inferring it from a flex-direction broke
   the moment the wrap stopped being a flex container. */
/* ▸ in a whisper of the ✓'s green — the stacked drill's rule, above. */
.wu-numpad.wu-og-pad .wu-numpad-enter:not(.wu-og-go-ready) {
  background: color-mix(in srgb, var(--colour-success) 9%, var(--colour-bg));
  color: var(--colour-success-ink);
  border-color: color-mix(in srgb, var(--colour-success) 50%, var(--colour-border));
}

/* THE TIMES TABLE'S KEYS ON ANY ROOMY SCREEN (owner, on a tablet: "maths
   grid input is too small… the size of the times table is better"). The base
   64×44 is a PHONE size, kept only where a phone needs it: upright, where the
   pad sits under the grid and every pixel of key comes out of the cells.

   On a roomy screen the house numpad costs the grid nothing it needs, in
   either orientation. Sideways the pad is beside a height-bound grid; upright
   (a tablet, v472) the pad is below and the height is spare — worked through at
   768×1024, the solver still deals one 6×6 grid at 64px cells with a 288px
   pad under it. So the Grid, the Ladder and the Times table share one pad.

   ⚠️ These blocks come BEFORE the short-landscape one, which a sideways
   phone also matches and which must win there (60×48 keys). */
@media (min-width: 700px) {
  .wu-og-pad {
    grid-template-columns: repeat(3, 84px);
    gap: var(--space-sm);
  }
  .wu-og-pad button { height: 66px; }
}
/* SIDE BY SIDE ON A SIDEWAYS ROOMY SCREEN, and only sideways (owner: "keep
   grid on left and input on the right", then, of an upright tablet: "on
   vertical it should keep the times table above and pad below. same with the
   grid"). The head and hint keep the full width across the top and only the
   grid and the pad share the row, which also leaves the header centred over
   the whole screen rather than half of it (#101).

   It also sets the owner's "1/3 proportion" without a number of its own: the
   side is `flex: none`, so its width IS the pad's (~268px), and startOpGrid
   subtracts the side's MEASURED width before it solves. The grid is
   height-bound here, so 1024×768, 1024×600 and 1280×800 all deal one 6×6.

   ⚠️ Until v472 this was `(min-width: 700px)` alone, so an UPRIGHT tablet
   (768+ across) was side by side too — on the axis it has least of. Upright it
   now stacks (`--og-split` 0): the solver subtracts the pad's measured height
   instead, which is the contract working as designed. */
@media (min-width: 700px) and (orientation: landscape) {
  .wu-drill-layer:has(.wu-og-wrap) { --og-split: 2; }
  .wu-og-wrap {
    flex-direction: row;
    justify-content: center;
    gap: var(--space-lg);
    flex: 1 1 auto;
  }
}

@media (orientation: landscape) and (max-height: 500px) {
  .wu-drill-layer:has(.wu-og-wrap) {
    --og-split: 1;
    display: grid;
    /* TWO THIRDS FOR THE GRID, ONE FOR EVERYTHING ELSE (owner, 2026-09-28:
       "the 1/3 proportion works better", replacing #179's half and half).
       ⚠️ The third has a FLOOR: at 640×360 a strict third is ~172px and
       the pad is 196px, so `minmax(0, 1fr)` would have spilled the ✓ key
       over the screen edge. `min-content` lets the side take what the pad
       needs and hands the grid the rest — which at 640 is MORE than the old
       half gave it, and takes that phone from a 3×3 grid to a 4×4. */
    grid-template-columns: minmax(0, 2fr) minmax(min-content, 1fr);
    grid-template-rows: auto auto minmax(0, 1fr) auto;
    grid-template-areas:
      "grid head"
      "grid hint"
      "grid pad"
      "grid res";
    column-gap: var(--space-lg);
    align-items: center;
    justify-items: center;
  }
  .wu-drill-layer:has(.wu-og-wrap) > .wu-drill-head {
    grid-area: head;
    width: 100%;
    align-self: start;
  }
  .wu-drill-layer:has(.wu-og-wrap) > .wu-og-hint { grid-area: hint; margin: 0; }
  .wu-og-wrap { display: contents; }
  .wu-og-grid { grid-area: grid; }
  /* Full width of its own half, so the measurement can read one rect and know
     what is left for the grid without re-deriving the track sizes. */
  .wu-og-side { grid-area: pad; width: 100%; }
  .wu-drill-layer:has(.wu-og-wrap) > .wu-og-results { grid-area: res; }
  /* The Times table's sideways keys, exactly (.wu-tt-pad under 500px tall,
     at the house gap), so the two screens share a pad here too. 4 × 48 +
     gaps = 216px against a ~250px pad row at 640×360, the tightest case.
     Never under the 44px floor — the 40px this block once set was caught by
     tools/layout-audit.js (#88–#90). */
  .wu-og-pad {
    grid-template-columns: repeat(3, 60px);
    gap: var(--space-sm);
  }
  .wu-og-pad button { height: 48px; }
}
