/* Drills — slice 2 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: Maths drill shell, vocab flashcards / matching / verbs. Lodger: device notices, which rides here to keep the slice contiguous. */

/* ── Maths drill ─────────────────────────────────────────── */

.wu-drill-layer {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* ONE place declares this padding; everything else moves --layer-pad-*
     (layout.css), so a narrower breakpoint cannot clobber the --safe-top line
     below by re-declaring the shorthand. */
  padding: var(--layer-pad-y) var(--layer-pad-x);
  /* Drill/game layers cover the pane, so their own top row (✕, score, timer)
     has to clear the camera too — the page no longer does it for them (v265). */
  padding-top: calc(var(--layer-pad-y) + var(--safe-top));
  background: var(--colour-surface);
}

/* ⚠️ THE PARENT AREA IS A FULL-SCREEN MODAL ON EVERY SCREEN (owner,
   2026-09-26: "should the parents page take fullscreen on all screens with the
   X?? — make it as a full screen modal approach").

   It used to render into the home split's right-hand PANE, so on a 375px phone
   the dashboard was living in 269px: 56 to the rail, then the pane's padding,
   then the layer's. Measured, the settings column was **505px of content inside
   that 269px box** with `overflow: hidden` clipping the rest and no page scroll
   to reach it — the school-websites panel simply ran off the side (the grid
   track was the other half of that bug; see games.css). A modal reclaims the
   rail's 56px and both nested paddings, and it puts the parent area in the same
   language as the two other things that cover the screen to be read and then
   dismissed: the calendar (#162) and the journey day panel (#163).

   z 71 is the calendar modal's layer, above the rail's 60. `position: fixed`
   also means `body`'s rail padding never reaches it, which is exactly what
   gives back the width. Every screen in here keeps its OWN ✕ — the rail is
   covered, so `candidateQuit` refuses to adopt from inside this class (rail.js)
   — and the safe-area insets come with it, since a fixed box is no longer
   inside the padded body. */
.wu-drill-layer.wu-layer-modal {
  position: fixed;
  inset: 0;
  z-index: 71;
  padding:
    calc(var(--layer-pad-y) + var(--safe-top))
    calc(var(--layer-pad-x) + var(--safe-right))
    calc(var(--layer-pad-y) + var(--safe-bottom))
    /* THE CHROME COLUMN IS RESERVED, not floated over (#164). It runs ~150px
       down the left edge, so on a document screen it would sit across the top
       of the first panel — and content scrolling UNDER a fixed control is the
       clash the owner named. The modal already took the rail's 56px by covering
       it, so giving 52 back leaves the content 4px WIDER than before the modal
       existed, with chrome that can no longer be hidden by the thing it closes.
       A full-bleed screen (the journey) zeroes this padding and floats instead. */
    calc(var(--layer-pad-x) + var(--safe-left) + var(--chrome-w));
  overflow-x: hidden;
  overflow-y: auto;
}

/* THE TITLE IS ALWAYS DEAD CENTRE (v393). It used to be a `space-between` flex
   row — ✕ · title · timer — which centred the title only while all three were
   present. Since #108 the rail ADOPTS the ✕ and hides the original, so on most
   screens the title collapsed into the vacated left slot: measured at 1161px
   wide it sat at x=329 against a content centre of 608 — aligned to neither the
   rail nor the centre, and moving from screen to screen depending on whether
   that screen happened to carry a timer.

   It is ABSOLUTELY centred over the header rather than being a flex or grid
   child, and the reason is truncation. As a centred grid item it is sized to its
   content and can outgrow its own track (measured: an over-long title crossed
   the timer by 48px and then overflowed the header itself). Taken out of flow,
   `max-width` finally has a real percentage to resolve against — the header's
   own width — so the title truncates BEFORE it reaches either side control
   instead of running underneath it.

   --head-side is the room kept for the widest side control. 56px is not a guess:
   it is `.wu-drill-timer`'s own declared min-width, the widest control that
   appears on a normal header. A screen carrying something wider has to SAY so —
   the journey page's 104px "📋 Activity" is the one that does, below. Keeping it
   a token rather than a literal is what stops this becoming another reserve that
   silently goes stale (#89, #90, #98, #114); an over-generous default is not
   free either, and 64px clipped "Score 0" on the narrow games header. */
.wu-drill-head {
  --head-side: 56px;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  width: 100%;
  max-width: 560px;
  /* The hidden ✕ leaves nothing on the left, so without this the lone right-hand
     control would sit hard left on a railed screen. */
  min-height: 44px;
}
.wu-drill-head > .wu-drill-progress {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  max-width: calc(100% - 2 * (var(--head-side) + var(--space-sm)));
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* A SCREEN WHOSE PILL ALREADY NAMES IT KEEPS NO TITLE (owner, 2026-09-26:
   "with the pill saying the title of the page having the title at the top
   becomes redundant"). What is left is the ✕, which the rail adopts and hides
   — so the header collapses to nothing rather than reserving 44px for it.

   min-height: 0, NOT display: none. Adoption reads the ✕ through `inHidden`,
   which tests for the .wu-hidden CLASS rather than computed visibility, so
   hiding the header outright happens to work today and would break silently the
   day that check becomes a real one. Collapsing also FAILS SAFE: if adoption
   ever misses this screen the ✕ is un-railed, still visible, and the header
   grows back around it. */
.wu-drill-head-bare {
  min-height: 0;
}

/* A railed screen has only ONE flow child left, and space-between would put it
   at the left edge. Push it right, where the timer/action belongs. */
.wu-drill-head > .wu-drill-quit[data-railed] ~ *:last-child {
  margin-left: auto;
}
/* The one screen whose side control is wider than the default reserve: the
   journey's "📋 Activity" measures 104px. Declared by the control's own
   presence rather than by knowing which route this is, so a screen that gains
   a wide control cannot forget to say so. */
.wu-drill-head:has(.wu-jrnpage-list-btn) {
  --head-side: 108px;
}
/* ⚠️ A NET, not the fix — and it currently matches nothing on purpose.
   THE BUG: Survivor and Copy the Flower put a GROWING readout (`.wu-srv-tally`,
   "0 🐞 · 0 🪙") inside the header. The title is out of flow, so its siblings
   are laid out as though it were not there, which makes --head-side the ONLY
   thing keeping them apart — and 56px was sized for `.wu-drill-timer`, while
   the tally is 86px before a single bug is squashed. Reported in landscape
   2026-08-30 and reproduced exactly: in the games rail the header is 224px AND
   a COLUMN, so the centred title floated over the column's first row while the
   tally sat in it — overlapping BY 27px AT ZERO SCORE, 61px by 128 kills.
   Portrait hid it (a 343px row header only collided past 128 kills), which is
   why it read as landscape-only.

   THE FIX IS STRUCTURAL AND LIVES IN THE MARKUP: the header holds ✕ + title +
   best, and anything that grows with play sits BELOW it. Four of the six games
   already did that; the two that overlapped were the two that did not. So the
   tally moved out rather than the reserve being re-guessed.

   This rule stays as a self-activating guard, in the spirit of the tap guard
   (#86) — a per-instance fix has not held in this codebase, and a reserve is a
   guess that goes stale silently (#89, #90, #98, #114). If anyone ever puts a
   growing control back in a header, `:has()` turns this on by itself: below the
   width where centring can possibly work, the title returns to FLOW, where
   flexbox makes an overlap structurally impossible rather than merely unlikely.
   A CONTAINER query, because the header's own width is the thing in question
   and it depends on the rail as much as on the viewport (#88 moves the rail in
   short landscape). */
.wu-drill-head:has(.wu-srv-tally) {
  container-type: inline-size;
  --head-side: 128px;
}
.wu-drill-head > .wu-srv-tally {
  min-width: 0;
}
@container (max-width: 420px) {
  .wu-drill-head:has(.wu-srv-tally) > .wu-drill-progress {
    position: static;
    transform: none;
    max-width: none;
  }
}
/* On a phone that reserve is most of the header — measured at 375px it left the
   title 37px and clipped "👣 My Journey Log" to nothing. The button sheds its
   WORD rather than the title shedding its meaning: the glyph still reads, the
   44px target is untouched, and aria-label carries the name for a screen
   reader. The reserve returns to the default in the same breath, so the two can
   never disagree about how wide that control is. */
@media (max-width: 560px) {
  .wu-btn-label {
    display: none;
  }
  .wu-drill-head:has(.wu-jrnpage-list-btn) {
    --head-side: 56px;
  }
}

.wu-native-cluster .wu-drill-head {
  margin-top: var(--system-band);
}
.wu-native-cluster .wu-timer-overlay {
  padding-top: var(--system-band);
}

/* ── Device notices ────────────────────────────────────────────────────────
   Low battery / no connection. Replaces the shell's permanent status pill and
   the band the layout reserved to dodge it (#101, v392).

   ⚠️ It RESERVES NOTHING. `position: fixed`, so no screen pays for a condition
   that is almost never true — that was the whole fault of the band. It clears
   the rail by offsetting --rail-w itself, because the rail is fixed too and the
   body's padding therefore does not apply here. Bottom, never the top band:
   that region is the one the system can take back, and the reason this exists. */
.wu-notices {
  position: fixed;
  z-index: 60;
  left: calc(var(--safe-left) + var(--rail-bar-w) + var(--space-md));
  right: calc(var(--safe-right) + var(--space-md));
  bottom: var(--support-bottom);
  display: flex;
  flex-direction: column-reverse; /* newest nearest the thumb */
  gap: var(--space-sm);
  /* The container spans the content width so cards align, but it must never eat
     taps meant for what is behind it. Each card re-enables its own. */
  pointer-events: none;
}
.wu-notice {
  pointer-events: auto;
  box-sizing: border-box;
  width: fit-content;
  max-width: min(420px, 100%);
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-sm) var(--space-sm) var(--space-md);
  border-radius: var(--radius-card);
  border: 1px solid var(--colour-border);
  background: var(--colour-surface);
  box-shadow: 0 6px 20px rgb(43 41 38 / 0.16);
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: var(--font-size-sm);
  color: var(--colour-text-primary);
  opacity: 0;
  transform: translateY(8px);
  transition:
    opacity 180ms ease,
    transform 180ms ease;
}
.wu-notice-in {
  opacity: 1;
  transform: translateY(0);
}
/* Tone rides a left edge, not the card's background: white keeps the text at
   full contrast, where a tinted card drags every reading toward the accent
   (#121 measured that trade repeatedly). */
.wu-notice[data-tone="warn"] {
  border-left: 4px solid var(--colour-warning);
}
.wu-notice[data-tone="alert"] {
  border-left: 4px solid var(--colour-danger);
}
.wu-notice[data-tone="info"] {
  border-left: 4px solid var(--colour-accent);
}
.wu-notice-ic {
  flex: none;
  font-size: var(--font-size-lg);
  line-height: 1;
}
.wu-notice-text {
  flex: 1;
  min-width: 0;
  line-height: 1.35;
}
.wu-notice-x {
  /* flex: none — a width is a preference to flexbox, and this is a tap target
     (#90). 44px square, the floor, even though the glyph is small. */
  flex: none;
  width: 44px;
  height: 44px;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--colour-text-secondary);
  font-size: var(--font-size-md);
  cursor: pointer;
}
.wu-notice-x:hover {
  background: var(--colour-grid-bg);
  color: var(--colour-text-primary);
}
@media (prefers-reduced-motion: reduce) {
  .wu-notice {
    transition: none;
  }
}

.wu-drill-quit {
  /* flex: none or a flex parent shrinks it BELOW the stated width — measured
     40.5px against a declared 44px in the verbs Match header (v291 audit). A
     width is a preference to flexbox, not a minimum; a tap target has to be
     the latter. */
  flex: none;
  width: 44px;  /* Phase 6 minimum — was 40px (v288 audit) */
  height: 44px;
  border-radius: 50%;
  background: transparent;
  border: 1px solid var(--colour-border);
  color: var(--colour-text-secondary);
  cursor: pointer;
  font-size: var(--font-size-md);
}
.wu-drill-quit:hover {
  color: var(--colour-text-primary);
  border-color: var(--colour-danger);
}

/* DISPLAY, not mono (v395, owner). This element started life as a drill counter
   ("Q 1 / 10") and mono plus 0.08em tracking suited that. It is now the SCREEN
   TITLE on every screen — "👣 My Journey Log", "📚 My library" — and a name is
   not tabular data (§6 reserves mono for scores, timers and counts; the timer
   beside it keeps it). It is also what made the title too wide to fit: mono at
   14px with 1.12px of tracking needed 163px on a 375px phone against 141px
   available, so the app's longest title ellipsised on its narrowest screen.
   Nunito at the same size needs ~120px. Tracking drops to 0.02em — display type
   does not need letter-spacing to read, and the tracking was there to make a
   monospaced counter breathe. */
.wu-drill-progress {
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: var(--font-size-sm);
  color: var(--colour-text-secondary);
  letter-spacing: 0.02em;
}

.wu-drill-timer {
  font-family: var(--font-mono);
  font-size: var(--font-size-md);
  color: var(--colour-accent);
  min-width: 56px;
  text-align: right;
}

.wu-drill-timer.wu-timer-low {
  color: var(--colour-danger);
  font-weight: var(--font-weight-bold);
}

.wu-drill-q {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-md);
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: clamp(2rem, 6vw, 3.25rem);
  color: var(--colour-text-primary);
  text-align: center;
  min-height: 0;
}

.wu-q-written {
  font-size: clamp(1.1rem, 3vw, 1.6rem);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-loose);
  max-width: 28ch;
  margin: 0;
}

.wu-q-small {
  font-size: var(--font-size-lg);
  color: var(--colour-text-secondary);
  margin: 0;
}

.wu-drill-feedback {
  height: 36px;
  font-family: var(--font-display);
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-bold);
}
.wu-fb-right { color: var(--colour-success); }
.wu-fb-wrong { color: var(--colour-danger); }

.wu-drill-answer {
  font-family: var(--font-mono);
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-bold);
  color: var(--colour-accent);
  min-height: 44px;
  letter-spacing: 0.1em;
}

.wu-numpad {
  display: grid;
  grid-template-columns: repeat(3, 84px);
  gap: var(--space-sm);
  justify-content: center;
  /* Lift off the screen bottom so the drill reads more centred / higher */
  margin-bottom: 12vh;
  padding-bottom: var(--space-md);
}

.wu-numpad button {
  height: 66px;
  font-family: var(--font-mono);
  font-size: var(--font-size-xl);
  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;
  transition: border-color var(--transition-fast), background var(--transition-fast);
}

.wu-numpad button:hover {
  border-color: var(--colour-accent);
}

.wu-numpad button:active {
  background: color-mix(in srgb, var(--colour-accent) 18%, var(--colour-bg));
}

.wu-numpad .wu-numpad-enter {
  background: color-mix(in srgb, var(--colour-success) 22%, var(--colour-bg));
  border-color: var(--colour-success);
  color: var(--colour-success-ink);
}

/* wu-step-keys ── THE TWO STEP KEYS HAVE THEIR OWN TONES (owner, 2026-09-28:
   "can the next keep a green tone and the back a grey"). Four kinds of key on
   one pad, and now four looks: a digit is neutral, ⌫ is GREY (a step back),
   ▸ is a QUIET green (a step forward — see results.css) and ✓ is the full
   green (the commit). ⌫ was drawn as a digit, so the one key that undoes
   work sat in the pad looking like one more number to type.

   On every numpad, the PIN pad included: a rule for one screen's ⌫ is how
   two screens end up disagreeing about what that key looks like. Text is the
   secondary token, which measures 4.31:1 on this ground — the glyph would
   clear the 3:1 large-text floor anyway at this size and weight. */
.wu-numpad [data-key="back"] {
  background: color-mix(in srgb, var(--colour-border) 45%, var(--colour-bg));
  color: var(--colour-text-secondary);
}

/* ── THE TOP BAND GIVES ITS SLACK BACK ON A SHORT SCREEN ─── wu-head-compress
   Owner, 2026-09-27: "compress that upper header too much white space wasted
   on the maths exercises."

   Measured on a 🪜 Ladder at 851×393 before: 112px between the top of the
   pane and the first row — 28% of the screen — made of four separate reserves
   that are each individually reasonable and only add up on a screen this short:
     24px  the layer's own inset
     44px  the header's min-height — a tap floor for a ✕ that is not there,
           because the rail ADOPTS it (#108) and the original is hidden
     28px  the round line's own margin plus its text
     16px  the grid's top margin
   The grid then still had to scroll, so the reserve was being paid for out of
   the exercise.

   THE HEADER'S 44px IS THE INTERESTING ONE. It is a tap-target floor, and it is
   correct whenever the ✕ is in the header — but on every railed screen the
   header holds nothing but a title and a timer, two 20px text nodes. So the
   rule asks whether the ✕ was actually adopted (`[data-railed]`) rather than
   assuming: a screen whose adoption missed keeps its 44px and its visible ✕,
   which is the same fail-safe #165 built the bare-header collapse on.

   The GAME layer is excluded, as it is from the rail-hiding state: #161(d)
   measured its insets to a flush 24/24 and 16/16, and this would undo that. */
@media (orientation: landscape) and (max-height: 500px) {
  .wu-drill-layer:not(.wu-game-layer):not(.wu-layer-modal) {
    /* NEVER the `padding` shorthand — it would drop the --safe-top term and
       put the first row under the camera on a notched phone (§6's trap, and
       the bug #161(d) found in the <=720px rule). */
    padding-top: calc(var(--space-sm) + var(--safe-top));
  }
  .wu-drill-layer:not(.wu-game-layer):not(.wu-layer-modal)
  .wu-drill-head:has(> .wu-drill-quit[data-railed]) {
    /* 24px, NOT 0 - and `flex: none` beside it, which is the whole reason this
       is not simply "remove the floor". The layer is a flex COLUMN and a long
       grid makes it overflow, so every item's default `flex-shrink: 1` is live:
       with the floor gone the header shrank to literally 0, and its absolutely
       centred title spilled upward out of its own box and landed on the round
       line (measured: title 15-35, round line 33-53). The 44px was doing two
       jobs - a tap floor for a x that the rail has taken, and the header's
       shrink floor. Only the first one is spare. #90's rule on the block axis:
       a height is a preference to flexbox until something says otherwise. */
    min-height: 24px;
    flex: none;
  }
  .wu-tt-round { margin-top: 2px; }
  .wu-tt-grid { margin-top: var(--space-sm); }
}

/* ── A NUMPAD DRILL IS TWO COLUMNS ON A LANDSCAPE PHONE ──── wu-drill-two-col
   Owner, 2026-09-27: "on tablet and web it can stack, but mobile it creates two
   columns. one with the questions on the left and the input on the right. this
   way the input is closer to the right side for most are right handed."

   ⚠️ THIS IS A BREAKAGE FIX, NOT A PREFERENCE — measured at 851×393 before:
     · `.wu-drill-q` is `flex: 1` in a column needing 417px of a 311px box, so
       it collapsed to HEIGHT 0 and its 51px glyph painted from y=49 to y=119 —
       straight through the header and over the answer field. The child was
       looking at a screen with NO QUESTION on it.
     · The bottom numpad row (⌫ 0 ✓) ran to y=417 against a 393px viewport, so
       the ✓ that submits the answer was off screen entirely.
   Four numpad rows plus a question do not fit under 400px of height in one
   column, at any padding. Two columns spend the axis this screen HAS (817px of
   width, of which the pad wants 268) instead of the one it has not.

   NO JS AND NO NEW MARKUP: the flat drill DOM (head · question · feedback ·
   answer · pad) becomes a grid through named areas, so every approach that
   renders into it — quickfire, written, variables, balance — is
   covered without a call site, including ones not written yet.

   THE GATE IS HEIGHT, not width, and that is the owner's own split: a tablet or
   a laptop in landscape is 768+ tall and keeps the stack ("on tablet and web it
   can stack"), while a portrait phone has 812px of height and never needed
   this. Only the short landscape screen is short of the axis stacking spends.

   `:not(.wu-pin-pad)` because the PIN gate builds a `.wu-numpad` too, and it
   lives in a sheet — it must never re-lay-out a drill it is sitting over. */
@media (orientation: landscape) and (max-height: 500px) {
  .wu-drill-layer:has(> .wu-numpad:not(.wu-pin-pad)) {
    display: grid;
    /* The pad takes its own width on the RIGHT (the owner's handedness point);
       the question takes everything left over. `minmax(0, …)` because a grid
       track's default floor is its content, and a long written problem would
       otherwise push the pad off the screen rather than wrapping (#170). */
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: auto minmax(0, 1fr) auto auto;
    grid-template-areas:
      "head head"
      "ask  pad"
      "fb   pad"
      "ans  pad";
    align-items: center;
    justify-items: center;
    column-gap: var(--space-lg);
  }
  .wu-drill-layer:has(> .wu-numpad:not(.wu-pin-pad)) > .wu-drill-head { grid-area: head; }
  .wu-drill-layer:has(> .wu-numpad:not(.wu-pin-pad)) > .wu-drill-q { grid-area: ask; }
  .wu-drill-layer:has(> .wu-numpad:not(.wu-pin-pad)) > .wu-drill-feedback { grid-area: fb; }
  .wu-drill-layer:has(> .wu-numpad:not(.wu-pin-pad)) > .wu-drill-answer { grid-area: ans; }
  .wu-drill-layer:has(> .wu-numpad:not(.wu-pin-pad)) > .wu-numpad {
    grid-area: pad;
    align-self: center;
    /* The 12vh lift is for a STACKED drill, where the pad is the bottom of the
       page and wants to sit off the screen edge. In a column of its own it is
       vertically centred, so the lift is just height taken from the keys. */
    margin-bottom: 0;
    padding-bottom: 0;
    gap: var(--space-xs);
  }
  /* 66 → 56px: four rows plus gaps is 236px against the 267px the row has once
     the header is out. Still 12px clear of the 44px tap floor (#88–#90) — the
     key gives up slack, never the target, and the 84px width is untouched. */
  .wu-drill-layer:has(> .wu-numpad:not(.wu-pin-pad)) > .wu-numpad button { height: 56px; }

  /* ⚠️ 🔤 VARIABLES BROKE HERE, and it is the SAME failure this block
     was written to fix, turned through ninety degrees. The letter pad is ONE
     ROW of up to six square chips (words.css, owner 2026-08-17), so its
     max-content width is ~590px — and the pad sits in an `auto` track, which
     sizes to exactly that. On an 851px phone the question was left ~200px and
     on a 640px one the `minmax(0, 1fr)` collapsed to nothing, so the drill
     rendered as a pad with no question beside it. One row is right where the
     pad is the full width of the screen and wrong the moment it is a COLUMN of
     it: two across, three down, is the same six chips in 1/3 of the width.
     `aspect-ratio` keeps them square, so the height comes back on its own —
     hence `height: auto`, undoing the 56px the rule above sets for digits. */
  .wu-drill-layer:has(> .wu-varpad) > .wu-varpad {
    grid-auto-flow: row;
    grid-template-columns: repeat(2, minmax(56px, 80px));
    grid-auto-columns: auto;
  }
  .wu-drill-layer:has(> .wu-varpad) > .wu-varpad button { height: auto; }
}

/* ── Vocab — flashcards ──────────────────────────────────── */

.wu-fc-stage {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-lg);
  width: 100%;
  min-height: 0;
}

.wu-flashcard {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  background: var(--colour-bg);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-card);
  padding: var(--space-xl) var(--space-2xl);
  width: min(480px, 100%);
}

.wu-fc-lang {
  font-size: var(--font-size-xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--colour-text-secondary);
  margin: 0;
}

.wu-fc-word,
.wu-fc-translation {
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: clamp(1.75rem, 5vw, 2.75rem);
  line-height: var(--line-height-tight);
  text-align: center;
}

.wu-fc-translation {
  color: var(--colour-accent);
}

.wu-fc-translation.wu-fc-veiled {
  visibility: hidden;
}

/* Word + 🔊 speak button on one row */
.wu-fc-wordrow {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
}

.wu-fc-say {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1.5rem;
  line-height: 1;
  padding: 4px;
  opacity: 0.65;
  transition: opacity var(--transition-fast), transform var(--transition-fast);
}

.wu-fc-say:hover {
  opacity: 1;
  transform: scale(1.1);
}

.wu-fc-divider {
  width: 60%;
  border: none;
  border-top: 1px solid var(--colour-border);
  margin: var(--space-sm) 0;
}

.wu-fc-controls {
  display: flex;
  gap: var(--space-md);
  min-height: 56px;
}

.wu-fc-btn {
  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-fc-btn:hover { border-color: var(--colour-accent); }

.wu-fc-yes {
  background: color-mix(in srgb, var(--colour-success) 22%, var(--colour-bg));
  border-color: var(--colour-success);
  color: var(--colour-success);
}

.wu-fc-no {
  background: color-mix(in srgb, var(--colour-danger) 16%, var(--colour-bg));
  border-color: var(--colour-danger);
  color: var(--colour-danger);
}

/* ── Vocab — matching ────────────────────────────────────── */

/* ⚠️ A CENTRED BOARD THAT OUTGROWS ITS BOX IS CLIPPED AT BOTH ENDS, and this
   one had no media query at all — the desktop layout ran on every screen
   (owner, 2026-09-26: "review the vocab matching on mobile. it appears
   cutoff"). Measured before: 322px of grid in a 267px stage at 851x393 and a
   250px stage at 640x360, so it spilled 27px and 36px off EACH end — the last
   pair was cut in half by the card edge while the column heads were pushed up
   into the drill header, overlapping the round title AND the timer.
   `safe center` plus a real scroller is the #161(c) net: the tightened metrics
   below make the normal case FIT, and this catches whatever is left (a word
   that wraps to two lines, a taller shell). Both are needed — `min-height: 0`
   alone gives a flex child nothing to scroll, and plain `center` puts the
   overflow out of reach in both directions. */
.wu-match-stage {
  flex: 1;
  display: flex;
  align-items: safe center;
  /* BOTH axes take `safe`, because setting `overflow-y` makes `overflow-x`
     compute to `auto` as well — so the stage is a scroller in both directions
     and plain `center` puts any horizontal overflow off the START edge, where
     no scroll can reach it. Measured on the way in: the grid landed at
     left: -9 with the answer column clipped behind the rail. */
  justify-content: safe center;
  width: 100%;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* ⚠️ `1fr` IS `minmax(auto, 1fr)`, so a track GROWS past its container to fit
   its widest word. Measured at 375x812 with a real draw: the columns came out
   163.5px and 118.4px — unequal, and the whole ANSWER column ran 27.9px off the
   right edge, clipped rather than scrollable. 119 of the dictionary's 690 words
   (17.2%) are wider than the space a portrait phone gives, so this is the
   common case on a phone, not an edge one. `minmax(0, 1fr)` lets the track
   shrink below its content, which is what keeps the two columns equal and on
   screen; the button then wraps instead (see `overflow-wrap` below). */
.wu-match-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-md) var(--space-xl);
  /* `flex: none` is REQUIRED beside the minmax, not tidiness. The grid is a
     flex ITEM, so `width` is only a preference (#90) and its automatic minimum
     was the only thing holding it open: with `minmax(0, 1fr)` the tracks lose
     their content floor, the item shrinks to min-content, and `break-word`
     then takes that to ONE CHARACTER. Measured on the way in — tracks
     36.5px, buttons 322px tall, a column of single letters. */
  flex: none;
  width: min(560px, 100%);
}

.wu-match-col {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

/* Which language is which column (owner, 2026-08-28 — the child could not tell
   what was being asked for). Sticky-free, one row, and deliberately quiet: it is
   a label on the columns, not a heading competing with the words themselves. */
.wu-match-head {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.35em;
  padding-bottom: var(--space-xs);
  border-bottom: 2px solid var(--colour-border);
  font-family: var(--font-display);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-bold);
  color: var(--colour-text-secondary);
}

/* The ANSWER side carries the accent, so "the one I have to produce" is visible
   without reading either label — the same trick the verb-table pills use (#82). */
.wu-match-head-tgt {
  color: var(--colour-accent);
  border-bottom-color: var(--colour-accent);
}

.wu-match-btn {
  /* With the track capped above, a single long word ("asciugamano",
     "quatre-vingt-dix") would otherwise overflow its own button. It breaks
     instead — an ugly word is readable, a clipped one is not. */
  overflow-wrap: break-word;
  padding: var(--space-md);
  font-family: var(--font-body);
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-semibold);
  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);
  transition: border-color var(--transition-fast), background var(--transition-fast);
}

.wu-match-btn:hover { border-color: var(--colour-text-secondary); }

.wu-match-btn.wu-selected {
  border-color: var(--colour-accent);
  background: color-mix(in srgb, var(--colour-accent) 18%, var(--colour-bg));
}

.wu-match-btn.wu-matched {
  border-color: var(--colour-success);
  background: color-mix(in srgb, var(--colour-success) 14%, var(--colour-bg));
  color: var(--colour-success);
  pointer-events: none;
  opacity: 0.75;
}

.wu-match-btn.wu-mismatch {
  border-color: var(--colour-danger);
  animation: wu-shake 300ms ease;
}

/* A NARROW SCREEN BUYS WIDTH BACK FROM THE GUTTERS, never from the tap target.
   At 375px the 40px column gap is 15% of the whole board and the button spends
   another 32px on side padding, leaving 80.5px of text. Trading both takes it
   to ~108px — a third more — which is the difference between "elephant" fitting
   and wrapping. The gap stays at 16px because it is what says these are two
   columns to match BETWEEN, and the 44px floor is untouched. */
@media (max-width: 480px) {
  .wu-match-grid { gap: var(--space-sm) var(--space-md); }
  .wu-match-btn { padding: var(--space-md) var(--space-sm); }
}

/* A SHORT SCREEN HAS TO FIT FOUR ROWS, and it is height it is short of, so the
   vertical padding is what goes. The button keeps `min-height:
   var(--tap-target-min)`, so it lands on exactly 44px rather than 58px and the
   tap target is unchanged — the #125b rule (trade a row or a gap, never the
   target). Budget at 851x393: head 30 + gap 8 + 4x44 + 3x8 = 238 in a 267px
   stage, against 322 before. */
@media (orientation: landscape) and (max-height: 500px) {
  .wu-match-grid { gap: var(--space-sm) var(--space-md); }
  .wu-match-col { gap: var(--space-sm); }
  .wu-match-btn { padding: var(--space-sm) var(--space-md); }
}

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

/* ── Verbs — tap-the-form conjugation drill ──────────────── */
.wu-verb-stage {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-xl);
  width: 100%;
  min-height: 0;
}

.wu-verb-prompt {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xs);
  text-align: center;
}

.wu-verb-emoji {
  font-size: 2.5rem;
  line-height: 1;
}

.wu-verb-inf {
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: clamp(1.5rem, 4vw, 2.25rem);
  color: var(--colour-text-primary);
}

.wu-verb-inf-tgt {
  color: var(--colour-accent);
}

.wu-verb-person {
  font-family: var(--font-body);
  font-size: var(--font-size-lg);
  color: var(--colour-text-secondary);
  text-transform: capitalize;
}

/* ⏳ Tenses (v275) — the time marker is the QUESTION, so it has to outrank the
   pronoun visually. A pill, not another grey line: the three options differ
   only by tense, and the child needs to know which one is being asked for. */
.wu-verb-tense {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: var(--space-xs);
  padding: 4px 14px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--colour-danger) 14%, var(--colour-surface));
  border: 2px solid color-mix(in srgb, var(--colour-danger) 45%, transparent);
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: var(--font-size-lg);
  color: var(--colour-text-primary);
}

.wu-verb-options {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-md);
  width: min(440px, 90%);
}

/* Verb language sheet (decision #64) — two rows of flag pills: the learning
   language and the child-picked hint (gloss) language. */
.wu-lang-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}

.wu-lang-opt {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px; /* v290 audit — was 40px */
  padding: var(--space-sm) var(--space-md);
  border: 2px solid var(--colour-border);
  border-radius: var(--radius-pill);
  background: var(--colour-surface);
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: var(--font-size-sm);
  color: var(--colour-text-primary);
  cursor: pointer;
}

.wu-lang-opt.wu-lang-on {
  border-color: var(--colour-accent);
  background: color-mix(in srgb, var(--colour-accent) 12%, var(--colour-surface));
}

.wu-verb-opt {
  padding: var(--space-md);
  font-family: var(--font-body);
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-semibold);
  background: var(--colour-surface);
  color: var(--colour-text-primary);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-button);
  cursor: pointer;
  min-height: var(--tap-target-min);
  transition: border-color var(--transition-fast), background var(--transition-fast);
}

.wu-verb-opt:hover { border-color: var(--colour-accent); }

.wu-verb-opt.wu-verb-right {
  background: color-mix(in srgb, var(--colour-success) 18%, var(--colour-surface));
  border-color: var(--colour-success);
  color: var(--colour-success);
}

.wu-verb-opt.wu-verb-wrong {
  border-color: var(--colour-danger);
  color: var(--colour-danger);
  animation: wu-shake 300ms ease;
  opacity: 0.6;
}

/* Verb conjugation table (study/overview) */
/* box-sizing is NOT global in this project (see the note further down), so the
   16px padding below was added ON TOP of width:100% and pushed the paradigm
   table 7px past the right edge on a phone (v291 audit). */
.wu-verb-table {
  box-sizing: border-box;
  flex: 1;
  width: min(720px, 100%);
  max-width: 720px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-sm);
  padding: var(--space-md);
  min-height: 0;
}
.wu-vt-head {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}
.wu-vt-inf {
  font-family: var(--font-display);
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-bold);
  color: var(--colour-text-primary);
}
/* Three-tense grid: pronoun column + Now / Before / Soon */
.wu-vt-grid {
  display: grid;
  grid-template-columns: auto repeat(3, 1fr);
  gap: var(--space-sm);
  align-items: stretch;
}
.wu-vt-col {
  font-family: var(--font-display);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-bold);
  color: var(--colour-text-secondary);
  text-align: center;
  padding: 2px 0;
}
.wu-vt-pron {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding-right: var(--space-sm);
  color: var(--colour-text-secondary);
  font-size: var(--font-size-md);
  white-space: nowrap;
}
.wu-vt-cell {
  padding: var(--space-md) var(--space-sm);
  background: var(--colour-bg);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-button);
  color: var(--colour-text-primary);
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-medium);
  cursor: pointer;
  text-align: center;
  min-width: 0;
  min-height: var(--tap-target-min);
  overflow-wrap: anywhere;
}
.wu-vt-cell:hover { border-color: var(--colour-accent); }
.wu-vt-nav {
  display: flex;
  justify-content: center;
  gap: var(--space-md);
  margin-top: var(--space-md);
}
.wu-vt-btn {
  min-height: 44px; /* v291 audit — was 37px */
  padding: var(--space-sm) var(--space-lg);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-button);
  background: var(--colour-surface);
  color: var(--colour-text-primary);
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  cursor: pointer;
}
.wu-vt-btn:hover { border-color: var(--colour-accent); }

/* Fill-the-gap sentence */
.wu-fill-sentence {
  font-family: var(--font-display);
  font-size: var(--font-size-xl);
  text-align: center;
  color: var(--colour-text-primary);
  line-height: var(--line-height-relaxed, 1.5);
  padding: 0 var(--space-md);
}
.wu-fill-blank {
  color: var(--colour-accent);
  letter-spacing: 0.05em;
}


/* ── Maths-terms multiple choice (owner's page 22) ─────────────────────
   Four word chips under the clue. `auto-fit` rather than a fixed two columns:
   "denominator" and "sum" differ by a factor of four in width, so a rigid grid
   either wraps the long one or strands the short one in a half-empty cell. */
.wu-termpad {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--space-sm);
  width: min(460px, 100%);
  margin: var(--space-md) auto 0;
}
.wu-termpad button {
  /* The Phase 6 floor, and `flex/grid` cannot shrink it below that: a grid
     item's default minimum is its content, and one-word chips are small. */
  min-height: var(--tap-target-min);
  padding: var(--space-sm) var(--space-md);
  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;
  overflow-wrap: break-word;
}
.wu-termpad button:hover { border-color: var(--colour-accent); }
.wu-termpad button:active { background: color-mix(in srgb, var(--colour-accent) 18%, var(--colour-bg)); }

/* The pad has nothing to offer a word question, and leaving it there would be
   a 12-key grid the child must learn to ignore. In the two-column landscape
   layout its track is `auto`, so it collapses and the clue takes the width. */
.wu-drill-layer.wu-q-terms .wu-numpad,
.wu-drill-layer.wu-q-terms .wu-drill-answer { display: none; }
