/* Games — slice 4 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: Games launcher, first-run tour, how-to cards, game side rail. */

/* ── Games category (decision #66) ──────────────────────── */
/* Not-yet-built games in the launcher flower read as clearly greyed-out. */
.wu-hex-wrap.wu-game-soon { opacity: 0.5; filter: grayscale(1); }
.wu-hex-wrap.wu-game-soon .wu-hex-hit { cursor: default; }
/* Launcher ring (#140). OPEN room is greyed and blank, and inert — there is
   nothing behind it yet, so it must not paint a press or answer a tap. Fainter
   than a named "coming soon" game, which is information; open room is only
   the shape of what the launcher can hold. */
.wu-hex-wrap.wu-game-open { opacity: 0.3; filter: grayscale(1); pointer-events: none; }
/* The 🎮 centre is the category's mark, not a control. */
.wu-hex-wrap.wu-game-mark { pointer-events: none; }
/* …drawn WHITE on the amber, like home's ★ (#56). The brand tile's white comes
   from `color`, which only reaches a TEXT glyph; 🎮 is a colour emoji and
   ignores it, so it rendered near-black. brightness(0) flattens it to a solid
   silhouette and invert(1) turns that white, keeping its alpha, so the
   controller's outline survives on any platform's emoji art. */
.wu-hex-wrap.wu-game-mark .wu-hex-icon { filter: brightness(0) invert(1); }
/* Launcher names that cannot fit are dropped rather than clipped (#140) — the
   treatment home's compact flower uses (#87), switched on by MEASUREMENT in
   games.js rather than a size threshold. The icon grows into the room and the
   inner loses its side padding, which exists only to keep text off the hex's
   slanted edges. */
#wu-games-stage[data-labels="off"] .wu-hex-wrap:not(.wu-locked) .wu-hex-label { display: none; }
#wu-games-stage[data-labels="off"] .wu-hex-wrap:not(.wu-locked) .wu-hex-icon {
  font-size: calc(var(--hex-w) * 0.42);
}
/* An icon drawn as a FILE (Checkers' piece, v422) is sized by width, not
   font-size, so it needs its own copy of the growth or it stays small beside
   emoji that grew. Ratio matched to the emoji by measurement. */
#wu-games-stage[data-labels="off"] .wu-hex-wrap:not(.wu-locked) .wu-hex-icon-img {
  width: calc(var(--hex-w) * 0.46);
  height: calc(var(--hex-w) * 0.46);
}
#wu-games-stage[data-labels="off"] .wu-hex-wrap:not(.wu-locked) .wu-hex-inner { padding-inline: 0; }
/* Every launcher name reserves TWO lines (v414), so the icons in a row sit at
   one height. Three roadmap names wrap (Word search, Block fit, Colour in) and
   put their icons 6px above a one-line neighbour's. Whether a name wraps
   depends on the tile size, so the box is reserved rather than the names
   rewritten — the rail drawer grid's fix (v399) for the same misalignment.
   2.4em = two lines at the label's 1.2 line-height. */
#wu-games-stage .wu-hex-wrap:not(.wu-locked) .wu-hex-label { min-height: 2.4em; }
/* A game icon that is a FILE rather than an emoji (Checkers' piece, v422),
   set inline in a line of text: the how-to header and list, the dev panel.
   1.25em is an emoji's rendered box, so it sits like its neighbours. */
.wu-game-ic {
  display: inline-block;
  width: 1.25em;
  height: 1.25em;
  object-fit: contain;
  vertical-align: -0.28em;
}

/* ── Turn sideways (owner, 2026-09-13: games are landscape only) ──
   Where the screen cannot be locked sideways (a browser tab, iPhone/iPad, an APK
   before setOrientation), holding the device upright shows this card over the
   games instead of an upright layout. CSS on the orientation query, not JS: it
   follows the device the instant it turns and has nothing to leak. It sits
   UNDER the rail (60), so ★ still goes home without turning the device. */
.wu-game-turn { display: none; }
@media (orientation: portrait) {
  .wu-game-turn {
    display: flex;
    position: fixed;
    inset: 0;
    z-index: 50;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-md);
    padding: var(--space-lg);
    padding-left: calc(var(--safe-left) + var(--rail-w) + var(--space-lg));
    background: var(--colour-bg);
    text-align: center;
  }
  /* The rail stays above the card so the star can leave, but the games' own
     rail chips open panels that would land UNDER it: a control that does
     nothing visible is the bug #86 exists to catch, so they go too. */
  #wu-games-info, #wu-games-left { display: none; }
}
.wu-game-turn-ic {
  font-size: 72px;
  line-height: 1;
  animation: wu-turn-hint 2.4s ease-in-out infinite;
}
.wu-game-turn-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--font-size-lg);
  color: var(--colour-text-primary);
}
.wu-game-turn-sub {
  margin: 0;
  color: var(--colour-text-secondary);
}
@keyframes wu-turn-hint {
  0%, 25% { transform: rotate(0deg); }
  50%, 80% { transform: rotate(-90deg); }
  100% { transform: rotate(0deg); }
}
@media (prefers-reduced-motion: reduce) {
  .wu-game-turn-ic { animation: none; transform: rotate(-90deg); }
}

/* Pre-game difficulty picker (shared, difficulty.js) — one coloured card per
   level, showing its best. Keeps the in-game UI clean. */
.wu-diff-select {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-md);
  padding: var(--space-lg);
  text-align: center;
}
.wu-diff-title {
  font-family: var(--font-display);
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-bold);
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}
.wu-diff-ic { font-size: 1.3em; }
.wu-diff-sub { color: var(--colour-text-secondary); }
.wu-diff-cards {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  width: min(320px, 92%);
}
.wu-diff-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-md) var(--space-lg);
  border: 2px solid var(--dc);
  border-left-width: 8px;
  border-radius: var(--radius-card);
  background: color-mix(in srgb, var(--dc) 10%, var(--colour-surface));
  cursor: pointer;
  font-family: var(--font-display);
  transition: transform 120ms ease, background 120ms ease;
}
.wu-diff-card:hover {
  background: color-mix(in srgb, var(--dc) 18%, var(--colour-surface));
  transform: translateY(-2px);
}
.wu-diff-name { font-size: var(--font-size-lg); font-weight: var(--font-weight-bold); color: var(--dc); }
.wu-diff-best { font-family: var(--font-mono); color: var(--colour-text-secondary); }
/* SHORT LANDSCAPE — the two columns rule #142 set for the result and how-to
   screens, applied to the pre-game pick (owner, 2026-09-16: "the start pick is
   tight… without cropping elements"). Measured at 851×393: stacked, the title,
   subtitle and two cards came to 384px inside a 359px layer, so the screen
   scrolled and the lower card sat on the bottom edge. Side by side they fit with
   room to spare, and each card stacks its own name over its note so neither has
   to be squeezed. */
@media (orientation: landscape) and (max-height: 500px) {
  .wu-diff-select {
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-md);
  }
  .wu-diff-title { font-size: var(--font-size-lg); }
  .wu-diff-sub { font-size: var(--font-size-sm); }
  .wu-diff-cards {
    flex-direction: row;
    justify-content: center;
    width: min(560px, 96%);
  }
  .wu-diff-card {
    flex: 1 1 0;
    min-width: 0;
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    gap: 2px;
    padding: var(--space-sm) var(--space-md);
  }
  .wu-diff-name { font-size: var(--font-size-md); }
}

/* Hex Memory — a hex FLOWER (brand centre + card ring) in the drill-layer.
   Reuses the honeycomb; these rules restyle the card tiles for the flip states. */
.wu-mem-flower {
  flex: 1;
  position: relative;
  width: 100%;
  min-height: 0;
}
/* Flower size scales with difficulty: fewer rows → bigger hexes; the divisor
   tracks the row count so each tier fits under the header + difficulty bar.
   --hex-h + --hex-gap MUST be re-declared with --hex-w — a var() inside a custom
   property resolves where it's DECLARED (:root), so overriding --hex-w alone
   leaves --hex-h at the root height and the flower geometry breaks. */
.wu-mem-flower[data-size="s1"] {
  --hex-w: clamp(62px, calc((var(--vh) - 230px) / 4.6), 132px);
  --hex-h: calc(var(--hex-w) * 1.1547);
  --hex-gap: 5px;
}
.wu-mem-flower[data-size="s2"] {
  --hex-w: clamp(44px, calc((var(--vh) - 230px) / 6.6), 112px);
  --hex-h: calc(var(--hex-w) * 1.1547);
  --hex-gap: 5px;
}
.wu-mem-flower[data-size="s3"] {
  --hex-w: clamp(30px, calc((var(--vh) - 230px) / 8.6), 82px);
  --hex-h: calc(var(--hex-w) * 1.1547);
  --hex-gap: 4px;
}
/* Cards use the SAME faint look as the main flower (the default honeycomb
   pending style — soft outline + ~8% fill over the ghost grid) so the games read
   as one family; only the special states override. Face-down vs face-up differ
   only by the glyph (❔ / emoji); a matched pair goes white. */
.wu-mem-flower .wu-hex-wrap:not(.wu-brand-tile) .wu-hex-label { display: none; }
.wu-mem-flower .wu-hex-icon { color: var(--colour-text-primary); }
.wu-mem-flower .wu-mem-matched .wu-hex::before { background: color-mix(in srgb, var(--colour-border) 70%, #fff); }
.wu-mem-flower .wu-mem-matched .wu-hex::after  { background: #fff; }
.wu-mem-flower .wu-mem-matched .wu-hex-icon    { opacity: 0.4; }
/* Cells beyond this level's pair count — inert "empty comb" the flower will
   grow into. Dimmed and untappable so they read as "not in play yet". */
.wu-mem-flower .wu-mem-inert { opacity: 0.25; pointer-events: none; }
/* A mismatch the child SHOULD have known (both cards already seen) — costs a
   heart, so it gets a red shake the free exploratory misses never get. */
.wu-mem-flower .wu-mem-miss .wu-hex::before { background: var(--colour-danger); }
.wu-mem-flower .wu-mem-miss { animation: wu-srv-shake 340ms ease; }
/* Waiting star between levels — spins like the boot splash until pushed. */
.wu-mem-flower .wu-srv-wait .wu-hex-icon {
  animation: wu-boot-spin 2.4s ease-in-out infinite;
}

/* ⬡ THE GAME ZONE — one rule for all six boards (owner, 2026-08-30: "define
   also the gaming hex extent … thin outline would be nice then and open
   pattern", clarified as "the overall outline of the game zone. not the
   individual hex").

   Every game drew its board into an invisible box, so the play area's EXTENT
   was implied by wherever the tiles happened to reach — which moves as a board
   grows a ring (Memory, Survivor, and now Copy all step up mid-run) and as the
   fitter re-solves on rotation. A thin outline states the zone instead: the
   board grows INSIDE a stable frame rather than the frame being whatever the
   board currently is.

   OPEN, not filled: the ghost lattice already shows through at
   `--colour-grid-line`, and the point is to read the space the board may still
   grow into. A fill would make the empty part look like something rather than
   room. Hairline and low-contrast on purpose — this is furniture, and the
   saturated paint colours sit inside it. */
.wu-game-zone {
  border: 1px solid color-mix(in srgb, var(--colour-border) 70%, transparent);
  border-radius: var(--radius-card);
  /* The lattice is clipped to the frame so it stops AT the outline instead of
     running under it — otherwise the border reads as a line drawn on top of a
     bigger pattern rather than as the edge of the zone. */
  overflow: hidden;
}

/* ⚠️ A FIXED-SIZE BOARD FRAMES ITSELF, so it gets no zone outline (owner,
   2026-09-23, marking the hairline on a chess screenshot: "there is a double
   frame that is consuming space with padding… remove or reduce the padding and
   hide the border").

   The rule above is not decoration — it states the room a board may still GROW
   into, and Memory, Survivor and Copy all step up a ring mid-run, so on those
   the outline is information. A chessboard is 8×8 and a sudoku 9×9 for the whole
   run: there is no growth to state, the board's own 3px ring is already its
   edge, and a second rectangle around the empty gutter is a frame around
   nothing. THE CLASS STAYS — it is what charges the game credit on the first
   board touch (#66/v411) — only the paint goes.

   The layer's 24px inset goes with it. These boards are HEIGHT-bound on a phone
   and nothing else sits in that band (chess has no rail at all, #147), so it was
   pure loss. `--safe-top` stays: the camera is physical, where a frame is not
   (§6). Measured at 851×393: chess 36 → 41px a square, One of Me's ladder now
   reaches 7×7 where it had to drop to 6×6, and sudoku's 9×9 32 → 37. */
.wu-game-zone.wu-duel-zone,
.wu-game-zone.wu-sdk-zone,
.wu-game-zone.wu-ome-zone {
  border: 0;
}
.wu-game-layer:has(.wu-duel-zone),
.wu-game-layer:has(.wu-sdk-zone),
.wu-game-layer:has(.wu-ome-zone) {
  padding: var(--space-sm);
  padding-top: calc(var(--space-sm) + var(--safe-top));
  /* What the layer gave up, for the side panel to take back below. */
  --rail-regain-y: calc(var(--layer-pad-y) - var(--space-sm));
  --rail-regain-x: calc(var(--layer-pad-x) - var(--space-sm));
}

/* Shared game status line (Copy / Survivor shells) + the pop-blast keyframe
   every game's hit/merge feedback reuses. (Named after the retired Pop-up game,
   kept for the shared feedback language.) */
.wu-pop-status {
  text-align: center;
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: var(--font-size-md);
  min-height: 1.5em;
  color: var(--colour-text-secondary);
  padding-bottom: var(--space-xs);
}
@keyframes wu-pop-blast { 0% { transform: scale(1); } 45% { transform: scale(1.15); } 100% { transform: scale(1.06); } }

/* Copy the Flower — a pattern flashes, the child repaints it from memory.
   (Replaced Pop-up — Survivor covered the same reflex loop better.) */
.wu-cpy-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  min-height: 48px;
  padding-bottom: var(--space-xs);
}
.wu-cpy-chip {
  width: 38px;
  height: 44px;
  border: none;
  padding: 0;
  cursor: pointer;
  clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
  opacity: 0.55;
  transition: opacity 160ms ease, transform 160ms ease;
}
.wu-cpy-chip-sel {
  opacity: 1;
  transform: scale(1.18);
}
.wu-cpy-check { margin-left: var(--space-md); }
.wu-cpy-flower {
  flex: 1;
  position: relative;
  width: 100%;
  min-height: 0;
}
/* Size scales with difficulty (re-declare --hex-h/--hex-gap alongside --hex-w).
   easy [3,4,3] 3 rows / medium [3,4,5,4,3] 5 rows / hard [4,5,6,7,6,5,4] 7 rows.
   Offset 300 leaves room for the header + status + palette bar. */
.wu-cpy-flower[data-size="easy"] {
  --hex-w: clamp(56px, calc((var(--vh) - 300px) / 4.6), 120px);
  --hex-h: calc(var(--hex-w) * 1.1547);
  --hex-gap: 5px;
}
.wu-cpy-flower[data-size="medium"] {
  --hex-w: clamp(44px, calc((var(--vh) - 300px) / 6.6), 108px);
  --hex-h: calc(var(--hex-w) * 1.1547);
  --hex-gap: 5px;
}
.wu-cpy-flower[data-size="hard"] {
  --hex-w: clamp(30px, calc((var(--vh) - 300px) / 8.6), 82px);
  --hex-h: calc(var(--hex-w) * 1.1547);
  --hex-gap: 4px;
}
.wu-cpy-flower .wu-hex-label { display: none; }
.wu-cpy-flower .wu-hex-icon { font-size: calc(var(--hex-w) * 0.44); }
/* Painted / shown-pattern cell — full colour, like a confirmed home tile. */
.wu-cpy-flower .wu-cpy-on .wu-hex::before { background: var(--tile-color); }
.wu-cpy-flower .wu-cpy-on .wu-hex::after  { background: color-mix(in srgb, var(--tile-color) 55%, var(--colour-surface)); }
/* Locked-in correct cell — solid, with a gentle settle pop. */
.wu-cpy-flower .wu-cpy-lock .wu-hex::after { background: var(--tile-color); }
.wu-cpy-flower .wu-cpy-lock .wu-hex { animation: wu-pop-blast 260ms ease; }
/* Wrong paint — red flash + shake before it clears. */
.wu-cpy-flower .wu-cpy-wrong .wu-hex::before { background: var(--colour-danger); }
.wu-cpy-flower .wu-cpy-wrong .wu-hex { animation: wu-srv-shake 450ms ease; }
/* Level-start button — the boot-splash mark: amber hex + spinning white ★. */
.wu-cpy-flower .wu-cpy-go .wu-hex::before { background: var(--colour-brand-deep); }
.wu-cpy-flower .wu-cpy-go .wu-hex::after  { background: var(--colour-brand); }
.wu-cpy-flower .wu-cpy-go .wu-hex-icon {
  color: #fff;
  animation: wu-boot-spin 2.4s ease-in-out infinite;
}

/* Honey Harvest — SameGame on a fixed 37-cell board (replaced Flow). Honey
   cells fill their colour; tap a group of 2+ to harvest, singles just shake. */
.wu-hvt-flower {
  flex: 1;
  position: relative;
  width: 100%;
  min-height: 0;
  --hex-w: clamp(30px, calc((var(--vh) - 260px) / 8.6), 82px);
  --hex-h: calc(var(--hex-w) * 1.1547);
  --hex-gap: 4px;
}
.wu-hvt-flower .wu-hex-label { display: none; }
.wu-hvt-flower .wu-hvt-on .wu-hex::before { background: var(--tile-color); }
.wu-hvt-flower .wu-hvt-on .wu-hex::after  { background: color-mix(in srgb, var(--tile-color) 55%, var(--colour-surface)); }
.wu-hvt-flower .wu-hvt-pop .wu-hex { animation: wu-pop-blast 260ms ease; z-index: 3; }
.wu-hvt-flower .wu-hvt-no .wu-hex { animation: wu-srv-shake 400ms ease; }
/* 💥 Bee bomb — forged by a 5+ harvest; solid brand amber, pulsing glyph. */
.wu-hvt-flower .wu-hvt-bomb .wu-hex::before { background: var(--colour-brand-deep); }
.wu-hvt-flower .wu-hvt-bomb .wu-hex::after  { background: var(--colour-brand); }
.wu-hvt-flower .wu-hvt-bomb .wu-hex-icon {
  font-size: calc(var(--hex-w) * 0.34);
  animation: wu-stk-pulse 1100ms ease infinite;
}

/* 👑 Queen bee — 7+ harvest. She wears the colour she clears (solid fill), so
   the child can read at a glance which honey she'll call home. */
.wu-hvt-flower .wu-hvt-queen .wu-hex::before { background: var(--colour-text-primary); }
.wu-hvt-flower .wu-hvt-queen .wu-hex::after  { background: var(--tile-color); }
.wu-hvt-flower .wu-hvt-queen .wu-hex-icon {
  font-size: calc(var(--hex-w) * 0.38);
  animation: wu-stk-pulse 900ms ease infinite;
}
/* Two+ queens of one colour = a points multiplier. Gold rim + faster pulse so
   the child can spot the pairing before deciding to cash one in. */
.wu-hvt-flower .wu-hvt-royal .wu-hex::before { background: var(--colour-brand-deep); }
.wu-hvt-flower .wu-hvt-royal .wu-hex-icon {
  animation-duration: 520ms;
}

/* ⚠️ Needs support — drills that scored under half. Flagged, never blocking. */
.wu-support-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  padding: var(--space-xs) 0;
  border-bottom: 1px solid var(--colour-border);
  font-size: var(--font-size-sm);
}
.wu-support-what { flex: 1; color: var(--colour-text-primary); }
.wu-support-score { font-family: var(--font-mono); color: var(--colour-danger); }
.wu-support-date { color: var(--colour-text-secondary); font-size: var(--font-size-xs); }
/* ✓ dealt-with — clears one row (v248). Without a way to clear it, a standing
   list stops being a signal and becomes wallpaper. */
.wu-support-done {
  flex: none;
  width: 32px;
  height: 32px;
  border: 1px solid var(--colour-border);
  border-radius: 50%;
  background: var(--colour-surface);
  color: var(--colour-success);
  font-size: var(--font-size-sm);
  line-height: 1;
  cursor: pointer;
}
.wu-support-done:active { background: var(--colour-bg); }
.wu-support-clear {
  margin-top: var(--space-sm);
  padding: var(--space-xs) var(--space-md);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-pill);
  background: var(--colour-surface);
  color: var(--colour-text-secondary);
  font-family: var(--font-display);
  font-size: var(--font-size-xs);
  cursor: pointer;
}
/* Encouragement line on a low-scoring results screen. */
.wu-results-nudge {
  max-width: 22em;
  margin: 0 auto;
  color: var(--colour-text-secondary);
  line-height: 1.4;
}

/* ── First-run tour (v255). Text cards, not screenshots: this UI changes too
   fast for screenshots to stay true, and a stale one teaches the wrong thing. */
.wu-onboard { max-width: 380px; }
/* A landscape phone is 360px tall and the sheet's desktop spacing made the
   arrival step 376px: tighter padding and gaps keep every step on screen. */
@media (orientation: landscape) and (max-height: 500px) {
  .wu-onboard {
    padding-block: var(--space-lg);
    gap: var(--space-sm);
  }
}
.wu-onboard-points {
  margin: 0;
  padding-left: 1.25em;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  text-align: left;
  color: var(--colour-text-primary);
  font-size: var(--font-size-sm);
  line-height: 1.45;
}
.wu-onboard-input {
  width: 100%;
  box-sizing: border-box;
  padding: var(--space-sm);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-button, 8px);
  font-family: var(--font-display);
  font-size: var(--font-size-md);
  text-align: center;
}
/* Language step (v408). Sized for a CHILD, not for the parent panel's 36px
   .wu-set-toggle — this sheet is the one the seven-year-old is looking at.
   Glyph ABOVE word, the shape the Study Timer's mood chips settled on (#123e):
   an inline "flag Português" beside a stacked one reads as two different kinds
   of control, and stacking lets all four sit at equal width without the longest
   label deciding it. */
.wu-onboard-base {
  margin: 0;
  color: var(--colour-text-secondary);
  font-size: var(--font-size-sm);
}
.wu-onboard-langs {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-sm);
  width: 100%;
}
.wu-onboard-lang {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  /* A min-height is only a floor if nothing shrinks it (#90, #129) — a grid item
     can be squeezed below its own content, so the box is pinned here and the
     44px tap floor holds with room to spare. */
  min-height: 64px;
  padding: var(--space-xs);
  background: var(--colour-surface);
  border: 2px solid var(--colour-border);
  border-radius: var(--radius-button);
  color: var(--colour-text-secondary);
  font-family: var(--font-body);
  font-size: var(--font-size-sm);
  line-height: 1.2;
  cursor: pointer;
}
.wu-onboard-lang .wu-onboard-flag {
  font-size: 24px;
  line-height: 1;
}
.wu-onboard-lang.wu-on {
  border-color: var(--colour-brand-deep);
  background: color-mix(in srgb, var(--colour-brand) 18%, var(--colour-surface));
  color: var(--colour-text-primary);
  font-weight: var(--font-weight-bold);
}
/* The refusal cue is WEIGHT, never a colour: every warm accent here fails the
   4.5:1 body-text floor on a white sheet (#121). */
.wu-onboard-warn {
  color: var(--colour-text-primary);
  font-weight: var(--font-weight-bold);
}
/* The quiet escape from the setup steps. Underlined rather than boxed so it
   cannot compete with the primary Next beside it, but a full 44px tall — it is
   the control a child who does not want to be named has to be able to hit. */
.wu-onboard-skip {
  min-height: 44px;
  padding: var(--space-sm);
  background: none;
  border: 0;
  color: var(--colour-text-secondary);
  font-family: var(--font-body);
  font-size: var(--font-size-sm);
  text-decoration: underline;
  cursor: pointer;
}
/* The arrival step's Skip, in the corner where a sheet's ✕ sits: the least
   important choice on the sheet, so the smallest (owner). */
.wu-onboard-skip.wu-onboard-corner {
  position: absolute;
  top: 8px;
  right: 8px;
  min-width: 44px;
  padding: 0 var(--space-sm);
  text-decoration: none;
}
.wu-onboard-dots {
  display: flex;
  justify-content: center;
  gap: 6px;
  padding-top: var(--space-xs);
}
.wu-onboard-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  /* --colour-border on a white sheet is effectively invisible; a progress dot
     nobody can see is not progress. */
  background: color-mix(in srgb, var(--colour-text-secondary) 45%, transparent);
}
.wu-onboard-dot.wu-on { background: var(--colour-brand); }
.wu-help-tour { width: 100%; margin-top: var(--space-md); }
/* The welcome card's install nudge (#183). A quiet panel, so the card still
   reads as "set up" rather than "install"; only its one-tap button is filled,
   because installing is the step that has to come first. Its own display rule
   means [hidden] needs one too (the house trap: an element with a display of
   its own beats the UA's hidden rule). */
.wu-onboard-install {
  width: 100%;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding: var(--space-sm) var(--space-md);
  background: var(--colour-bg);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-button);
  text-align: left;
}
.wu-onboard-install[hidden] { display: none; }
/* The added-child card's "this device is linked" line. Body colour and weight,
   not a green: success-as-text fails the body-text floor on a white sheet (#121). */
.wu-onboard-linked {
  margin: 0;
  color: var(--colour-text-primary);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-bold);
  line-height: 1.4;
}
.wu-onboard-install p {
  margin: 0;
  color: var(--colour-text-primary);
  font-size: var(--font-size-sm);
  line-height: 1.4;
}
.wu-onboard-install .wu-onboard-install-title { font-weight: var(--font-weight-bold); }
.wu-onboard-install .wu-sheet-btn { width: 100%; margin-top: var(--space-xs); }

/* ── How-to-play cards (shown on a game's first run, and from the launcher's
   ℹ️ button). Short by design: goal, steps, one tip. ───────────────────── */
/* View-specific HUD chip (Games' ℹ️). Lives in the LEFT HUD zone next to the
   section badge — NEVER floated top-right: the Android shell's system status
   overlay (clock / network / battery) sits there and would hide it. See
   CLAUDE.md §6 "Protected UI zones". */
.wu-hud-chip {
  /* Mirrors .wu-hud-section (layout.css) so it reads as the same family of
     pill as the 🎮 Games badge it sits under — same height, radius, type
     scale and shadow. Only the neutral fill differs (no --sec-colour tint). */
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  padding: 0 var(--space-sm);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-pill);
  background: var(--colour-surface);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
  font-family: var(--font-display);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--colour-text-primary);
  cursor: pointer;
}
/* Emoji glyph sized to match the badge's 18px icon slot — emoji render large
   for their font-size, so it's set below the pill's own type scale. */
.wu-hud-chip-ic {
  font-size: 14px;
  line-height: 18px;
}
/* Same treatment as the trail ✕: the pill keeps its 32px height so it matches
   the badge family beside it, and the hit area is grown to 44px underneath. */
.wu-hud-chip::after {
  content: "";
  position: absolute;
  inset: -6px;
  min-height: 44px;
}
.wu-hud-chip:active { background: var(--colour-bg); }
/* ⚠️ A CENTRED PANEL THAT CAN OUTGROW ITS LAYER MUST BE ABLE TO SCROLL
   (owner, 2026-09-23: "the game help does not allow to scroll on mobile").
   The ℹ️ index lists every game that has a card — SIX when #142 laid these out
   in two columns, FOURTEEN now — so on a 851×393 phone the last rows are simply
   off the bottom with no way to reach them. The list grows with the ring (#140),
   so this is a rule, not a one-off: the panel scrolls.
   `min-height: 0` because it is a flex CHILD of the layer, where the default
   `auto` minimum refuses to shrink below the content and there is nothing to
   scroll; `safe center` so a panel taller than its box starts at the TOP instead
   of being centred off both edges — the same guard #142's grids already use. */
.wu-howto {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  justify-content: safe center;
  gap: var(--space-md);
  max-width: 460px;
  margin: 0 auto;
  padding: var(--space-md);
  text-align: left;
}
.wu-howto-goal {
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: var(--font-size-lg);
  color: var(--colour-text-primary);
  text-align: center;
}
.wu-howto-steps {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  margin: 0;
  padding-left: 1.4em;
  color: var(--colour-text-primary);
  line-height: 1.45;
}
.wu-howto-tip {
  padding: var(--space-sm);
  border-radius: var(--radius-card);
  background: var(--colour-bg);
  color: var(--colour-text-secondary);
  line-height: 1.45;
}
.wu-howto-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

/* 🕯️ Wax — the escalating obstacle. Deliberately dead-looking: no tint, no
   pulse, so it reads as "nothing happens here" against the live honey. */
.wu-hvt-flower .wu-hvt-wax .wu-hex::before { background: #9E9689; }
.wu-hvt-flower .wu-hvt-wax .wu-hex::after  { background: #CFC7B8; }
.wu-hvt-flower .wu-hvt-wax .wu-hex-icon {
  font-size: calc(var(--hex-w) * 0.3);
  opacity: 0.75;
}

/* 2048 Hex — merge game, swipe in 6 directions. No difficulty levels — one
   fixed 19-cell board; the challenge is the endless top-score climb. */
.wu-g2048-status {
  text-align: center;
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: var(--font-size-md);
  min-height: 1.5em;
  color: var(--colour-text-secondary);
  padding-bottom: var(--space-xs);
}
.wu-g2048-flower {
  flex: 1;
  position: relative;
  width: 100%;
  min-height: 0;
  touch-action: none; /* the board is a swipe surface — never scroll */
  /* Fixed 19-cell board (5 rows) — re-declare --hex-h/--hex-gap with --hex-w. */
  --hex-w: clamp(44px, calc((var(--vh) - 260px) / 6.6), 108px);
  --hex-h: calc(var(--hex-w) * 1.1547);
  --hex-gap: 5px;
}
.wu-g2048-flower .wu-hex-label { display: none; }
/* Numbers in mono, dark on the light fill; big values (≥128) go solid + white. */
.wu-g2048-flower .wu-hex-icon {
  font-family: var(--font-mono);
  font-weight: 700;
  color: var(--colour-text-primary);
}
.wu-g2048-flower .wu-g2048-on .wu-hex::before { background: var(--tile-color); }
.wu-g2048-flower .wu-g2048-on .wu-hex::after  { background: color-mix(in srgb, var(--tile-color) 40%, var(--colour-surface)); }
.wu-g2048-flower .wu-g2048-hi .wu-hex::after  { background: var(--tile-color); }
.wu-g2048-flower .wu-g2048-hi .wu-hex-icon    { color: #fff; }
.wu-g2048-flower .wu-g2048-pop .wu-hex { animation: wu-pop-blast 250ms ease; z-index: 3; }
.wu-g2048-flower .wu-g2048-new .wu-hex { animation: wu-g2048-in 220ms ease; }
@keyframes wu-g2048-in { from { transform: scale(0.55); opacity: 0.3; } to { transform: scale(1); opacity: 1; } }

/* Games layer — sits on the PANE tone, not the white drill surface. The home
   flower lives on --colour-grid-bg, so the ghost lattice blends into it; on
   white the same lattice has more contrast and the whole board reads darker
   than the main flower. Matching the backdrop restores the home look.
   touch-action none: no pinch or pan may START on a game surface — zooming
   mid-game compromised tap accuracy (owner report 2026-07-12). */
.wu-game-layer {
  background: var(--colour-grid-bg);
  /* A GAME'S INSET IS SQUARE (owner, 2026-09-23: "standardised and equalised").
     The shared layer narrows its SIDE gutter under 720px so a full-width
     settings list sits flush — right for a list, wrong for a game, whose panel
     then reads 16px from the top and 8px from the edge. Games are landscape
     only (#142), so width is the axis they have spare and squaring it costs the
     board nothing. */
  --layer-pad-x: var(--layer-pad-y);
}
.wu-game-layer .wu-mem-flower,
.wu-game-layer .wu-cpy-flower,
.wu-game-layer .wu-hvt-flower,
.wu-game-layer .wu-g2048-flower,
.wu-game-layer .wu-stk-flower,
.wu-game-layer .wu-srv-flower { touch-action: none; }

/* Stack / Hexa-sort — place colour piles, matching neighbours hop together,
   10 bursts. Fixed 19-cell board; hard mode adds TWO-LAYER pieces rendered as a
   split hex (top half = top layer, bottom half = the hidden layer beneath). */
.wu-stk-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  min-height: 56px;
  padding-bottom: var(--space-xs);
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  color: var(--colour-text-secondary);
}
.wu-stk-lbl-next { opacity: 0.7; margin-left: var(--space-md); }
.wu-stk-mini {
  width: 42px;
  height: 48px;
  clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-evenly;
  color: #fff;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 15px;
  line-height: 1;
}
.wu-stk-piece-sm .wu-stk-mini {
  width: 30px;
  height: 35px;
  font-size: 11px;
  opacity: 0.75;
}
/* 🤝 Hold slot — park an awkward piece for later. */
.wu-stk-hold {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  width: 100%;
  /* 44px floor — measured at 38px on a 375×812 phone (2026-08-29 sweep). It is
     a real control, not a board cell: 🤝 Hold parks the current piece, and a
     child who misses it loses the piece instead. `flex: none` because a width
     or height is only a PREFERENCE to flexbox and the rail was shrinking it —
     the same trap that left .wu-drill-quit at 40.5px against a declared 44
     (#90). */
  min-height: 44px;
  flex: none;
  padding: var(--space-xs);
  border: 1px dashed var(--colour-border);
  border-radius: var(--radius-card);
  background: none;
  cursor: pointer;
  font: inherit;
}
.wu-stk-hold:active { background: var(--colour-bg); }
.wu-stk-hold .wu-stk-mini { opacity: 1; }
.wu-stk-empty {
  font-size: 20px;
  color: var(--colour-text-secondary);
  opacity: 0.5;
}
/* Colour → times-table legend (owner design: each colour is a different table). */
.wu-stk-legend {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  padding-bottom: var(--space-xs);
}
.wu-stk-key {
  width: 42px;
  height: 48px;
  clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 12px;
  line-height: 1.15;
}
.wu-stk-key-t { font-size: 10px; opacity: 0.85; }
/* Tiny table tag under a pile's count. */
/* FLOORS, not just ratios (v288 audit). These scaled with --hex-w and had no
   minimum, so a squeezed board took the pile count to 5px and the ×table tag —
   the label carrying this game's entire subject — to 2.5px. A ratio with no
   floor is the same class of bug as a floor with no ratio. */
.wu-stk-tb {
  font-family: var(--font-mono);
  font-weight: 700;
  line-height: 1;
  color: var(--colour-text-secondary);
  font-size: max(10px, calc(var(--hex-w) * 0.14));
}
.wu-stk-flower {
  flex: 1;
  position: relative;
  width: 100%;
  min-height: 0;
  /* Fixed 19-cell board (5 rows) — re-declare --hex-h/--hex-gap with --hex-w.
     Offset 340 leaves room for header + piece bar + legend + status line. */
  --hex-w: clamp(44px, calc((var(--vh) - 340px) / 6.6), 108px);
  --hex-h: calc(var(--hex-w) * 1.1547);
  --hex-gap: 5px;
}
.wu-stk-flower .wu-hex-label { display: none; }
.wu-stk-flower .wu-hex-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.wu-stk-flower .wu-stk-dual .wu-hex-inner { justify-content: space-around; }
.wu-stk-n {
  font-family: var(--font-mono);
  font-weight: 700;
  line-height: 1;
  color: var(--colour-text-primary);
  font-size: max(11px, calc(var(--hex-w) * 0.28));
}
.wu-stk-half { font-size: max(10px, calc(var(--hex-w) * 0.22)); }
/* Single-colour pile — the shared soft fill; outline carries the pile colour. */
.wu-stk-flower .wu-stk-on .wu-hex::before { background: var(--tile-color); }
.wu-stk-flower .wu-stk-on .wu-hex::after  { background: color-mix(in srgb, var(--tile-color) 45%, var(--colour-surface)); }
/* Two-layer pile — split surface: top half = top layer, bottom half = hidden layer. */
.wu-stk-flower .wu-stk-dual .wu-hex::after {
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--tile-color) 45%, var(--colour-surface)) 0 50%,
    color-mix(in srgb, var(--tile-color-2) 45%, var(--colour-surface)) 50% 100%
  );
}
/* Frozen pile — overshot its target; dead ice until a neighbouring burst
   shatters it. */
.wu-stk-flower .wu-stk-frozen .wu-hex::before { background: #90A4AE; }
.wu-stk-flower .wu-stk-frozen .wu-hex::after  { background: color-mix(in srgb, #90A4AE 25%, var(--colour-surface)); }
.wu-stk-ice { font-size: calc(var(--hex-w) * 0.34); line-height: 1; opacity: 0.9; }
/* Cracked ice — one more burst and it shatters. Reads as thinner/fainter. */
.wu-stk-flower .wu-stk-cracked .wu-hex::after { opacity: 0.75; }
.wu-stk-flower .wu-stk-cracked .wu-stk-ice { animation: wu-stk-pulse 1400ms ease infinite; }
/* 💡 Stall hint — a soft glow on one cell that would burst. Never instant. */
.wu-stk-flower .wu-stk-hint .wu-hex::before {
  background: var(--colour-brand-deep);
  animation: wu-stk-pulse 900ms ease infinite;
}
/* Nearly full (3 from bursting) — the count pulses as a "so close!" cue. */
.wu-stk-flower .wu-stk-hot .wu-stk-n:first-child { animation: wu-stk-pulse 900ms ease infinite; }
@keyframes wu-stk-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.25); } }
.wu-stk-flower .wu-stk-pop .wu-hex { animation: wu-pop-blast 260ms ease; z-index: 3; }

/* Survivor — defend the centre ★; bugs march inward, tap to squash. */
.wu-srv-flower {
  flex: 1;
  position: relative;
  width: 100%;
  min-height: 0;
}
/* The flower GROWS with wave progression: s1 19 cells (waves 1–5) / s2 37
   (6–10) / s3 61 (11+) — re-declare --hex-h/--hex-gap alongside --hex-w. */
.wu-srv-flower[data-size="s1"] {
  --hex-w: clamp(44px, calc((var(--vh) - 260px) / 6.6), 108px);
  --hex-h: calc(var(--hex-w) * 1.1547);
  --hex-gap: 5px;
}
.wu-srv-flower[data-size="s2"] {
  --hex-w: clamp(30px, calc((var(--vh) - 260px) / 8.6), 82px);
  --hex-h: calc(var(--hex-w) * 1.1547);
  --hex-gap: 4px;
}
.wu-srv-flower[data-size="s3"] {
  --hex-w: clamp(24px, calc((var(--vh) - 260px) / 10.6), 64px);
  --hex-h: calc(var(--hex-w) * 1.1547);
  --hex-gap: 3px;
}
.wu-srv-flower .wu-hex-label { display: none; }
.wu-srv-flower .wu-hex-icon { font-size: calc(var(--hex-w) * 0.36); }
/* An occupied cell — full-colour fill so the bug is unmissable. */
.wu-srv-flower .wu-srv-on .wu-hex::before { background: var(--tile-color); }
.wu-srv-flower .wu-srv-on .wu-hex::after  { background: color-mix(in srgb, var(--tile-color) 45%, var(--colour-surface)); }
/* Survivor header: bugs + coins tally on its own line under the wave count. */
.wu-srv-tally {
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  color: var(--colour-text-secondary);
}
/* Vitals card (hearts/shields/⚔️/🐌) — slightly larger than the message line. */
.wu-srv-vitals { font-size: var(--font-size-md); }

/* The BOSS — bigger glyph, menacing glow. Every multi-tap enemy wears the
   .wu-srv-hp remaining-taps counter chip (the fill-strength fade didn't read). */
.wu-srv-flower .wu-srv-boss .wu-hex-icon { font-size: calc(var(--hex-w) * 0.48); }
.wu-srv-flower .wu-srv-boss .wu-hex {
  filter: drop-shadow(0 0 10px color-mix(in srgb, var(--tile-color) 60%, transparent));
  z-index: 2;
}
.wu-srv-hp {
  position: absolute;
  top: 16%;
  right: 14%;
  z-index: 3;
  min-width: 1.5em;
  padding: 1px 4px;
  border-radius: 999px;
  background: #fff;
  color: var(--colour-danger);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: clamp(9px, calc(var(--hex-w) * 0.16), 14px);
  text-align: center;
  pointer-events: none;
}
.wu-srv-flower .wu-srv-hit .wu-hex { animation: wu-pop-blast 260ms ease; z-index: 3; }
/* Waiting star — spins like the boot splash until the child pushes it. */
.wu-srv-flower .wu-srv-wait .wu-hex-icon {
  animation: wu-boot-spin 2.4s ease-in-out infinite;
}
/* After-boss upgrade pick — health vs attack, centred over the board. */
.wu-srv-pick {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-md);
  background: color-mix(in srgb, var(--colour-grid-bg) 82%, transparent);
  backdrop-filter: blur(3px);
}
.wu-srv-pick-title {
  margin: 0;
  font-family: var(--font-display);
  color: var(--colour-text-primary);
}
.wu-srv-pick-cards {
  display: flex;
  gap: var(--space-md);
}
.wu-srv-pick-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xs);
  min-width: 130px;
  padding: var(--space-md);
  border: 2px solid var(--colour-border);
  border-radius: var(--radius-card, 12px);
  background: var(--colour-surface);
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: var(--font-size-md);
  color: var(--colour-text-primary);
  cursor: pointer;
}
.wu-srv-pick-card:disabled { opacity: 0.45; cursor: default; }
.wu-srv-pick-ic { font-size: 34px; line-height: 1; }
.wu-srv-price {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--colour-text-secondary);
}
/* Current-level line on shop cards ("now ⚔️2", "3 slow waves left"). */
.wu-srv-have {
  font-size: 11px;
  font-weight: var(--font-weight-normal, 400);
  color: var(--colour-text-secondary);
}
/* Star hit — red flash + shake on the centre tile. */
.wu-srv-flower .wu-srv-ouch .wu-hex::before { background: var(--colour-danger); }
.wu-srv-flower .wu-srv-ouch .wu-hex { animation: wu-srv-shake 450ms ease; }
@keyframes wu-srv-shake { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-6px); } 50% { transform: translateX(6px); } 75% { transform: translateX(-3px); } }

/* (Game boards get NO hover styles at all — their stages carry .wu-static and
   honeycomb.css scopes every :hover rule to :not(.wu-static) stages. This
   replaced the earlier transform-only suppression: on touch the stuck :hover
   also recoloured ::before/::after, leaving the last-tapped tile "active".) */

/* ── Two-player square board: Checkers (#145) and Chess (#147) ──────
   `wu-duel-*` is shared by both games (sized by duel-board.js); each game's
   piece art is its own (`wu-chk-*`, `wu-chs-*`). The first SQUARE board. Sized
   in JS from the zone (`--duel-cell`, whole pixels per square so the pattern
   stays crisp); everything below scales from it.
   The pieces are the only thing that moves, and only `left`/`top` animate, so a
   move reads as a piece travelling rather than the board redrawing. */
.wu-duel-zone {
  flex: 1;
  position: relative;
  width: 100%;
  min-height: 0;
  touch-action: none;
  --duel-cell: 36px;
  --duel-dark: color-mix(in srgb, var(--colour-text-secondary) 50%, var(--colour-surface));
  --duel-light: color-mix(in srgb, var(--colour-brand) 12%, var(--colour-surface));
}
/* A 2×2 conic tile is exactly one light/dark/dark/light block, so the board is
   one background with no 64 elements behind it. Dark lands on (row + col) odd,
   which is the rule the engine plays on. */
.wu-duel-board {
  position: absolute;
  inset: 0;
  margin: auto;
  width: calc(var(--duel-cell) * 8);
  height: calc(var(--duel-cell) * 8);
  border-radius: 4px;
  background: repeating-conic-gradient(var(--duel-dark) 0 25%, var(--duel-light) 0 50%)
    0 0 / calc(var(--duel-cell) * 2) calc(var(--duel-cell) * 2);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--colour-text-secondary) 35%, var(--colour-surface));
}
.wu-duel-marks,
.wu-duel-pieces {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.wu-duel-mark {
  position: absolute;
  width: 12.5%;
  height: 12.5%;
}
.wu-duel-last { background: color-mix(in srgb, var(--colour-brand) 40%, transparent); }
.wu-duel-sel { box-shadow: inset 0 0 0 3px var(--colour-accent); }
.wu-duel-dest::after {
  content: "";
  position: absolute;
  inset: 34%;
  border-radius: 50%;
  background: color-mix(in srgb, var(--colour-accent) 80%, transparent);
}

/* Pieces never take a tap: the zone finds the nearest dark square instead, so
   a piece's target is a diamond far bigger than the piece (see checkers.js). */
.wu-duel-piece {
  position: absolute;
  width: 12.5%;
  height: 12.5%;
  transition: left 220ms ease, top 220ms ease, opacity 240ms ease, transform 240ms ease;
}
.wu-duel-piece svg {
  display: block;
  width: 88%;
  height: 88%;
  margin: 6%;
  overflow: visible;
}
.wu-chk-shadow { fill: color-mix(in srgb, var(--colour-text-primary) 35%, transparent); }
.wu-chk-groove { fill: none; stroke-width: 2.5; }
.wu-chk-crown { fill: none; stroke-width: 3.5; }

/* AMBER. An ordinary piece has the ★ CUT IN: the dark edge copy sits up-left and
   the light one down-right, which is how light falls into a groove. */
.wu-duel-a .wu-chk-body { fill: var(--colour-brand); }
.wu-duel-a .wu-chk-groove { stroke: color-mix(in srgb, var(--colour-brand-deep) 70%, var(--colour-brand)); }
.wu-duel-a .wu-chk-star-lo { fill: color-mix(in srgb, var(--colour-brand-deep) 70%, var(--colour-text-primary)); }
.wu-duel-a .wu-chk-star-hi { fill: color-mix(in srgb, var(--colour-brand) 35%, var(--colour-surface)); }
.wu-duel-a .wu-chk-star { fill: color-mix(in srgb, var(--colour-brand) 60%, var(--colour-brand-deep)); }

/* CHARCOAL. The body is a shade off the darkest token so the cut edge can go
   darker than it. */
.wu-duel-b .wu-chk-body { fill: color-mix(in srgb, var(--colour-text-primary) 82%, var(--colour-text-secondary)); }
.wu-duel-b .wu-chk-groove { stroke: color-mix(in srgb, var(--colour-text-primary) 55%, var(--colour-text-secondary)); }
.wu-duel-b .wu-chk-star-lo { fill: var(--colour-text-primary); }
.wu-duel-b .wu-chk-star-hi { fill: color-mix(in srgb, var(--colour-text-secondary) 55%, var(--colour-surface)); }
.wu-duel-b .wu-chk-star { fill: color-mix(in srgb, var(--colour-text-primary) 62%, var(--colour-text-secondary)); }

/* KINGS. The ★ is RAISED and GOLD: shadow down-right, highlight up-left, and a
   dark outline so a gold star still reads on the amber body, where gold alone
   is nearly the same colour. The crown ring is white on amber for the same
   reason, gold on charcoal. These come after the side rules: same specificity,
   so order decides. */
.wu-chk-king .wu-chk-star-lo { fill: color-mix(in srgb, var(--colour-text-primary) 55%, transparent); }
.wu-chk-king .wu-chk-star-hi { fill: color-mix(in srgb, var(--colour-surface) 85%, transparent); }
.wu-chk-king .wu-chk-star {
  fill: var(--colour-gold);
  stroke: var(--colour-text-primary);
  stroke-width: 1.6;
  stroke-linejoin: round;
}
.wu-duel-a .wu-chk-crown { stroke: var(--colour-surface); }
.wu-duel-b .wu-chk-crown { stroke: var(--colour-gold); }

/* Two players on one flat device: the far player's pieces face them. Only the
   pieces ON the board: the count and the promotion choice turn as a whole. */
.wu-duel-duo .wu-duel-pieces .wu-duel-b svg { transform: rotate(180deg); }

.wu-duel-lift { transform: scale(1.1); z-index: 2; }
.wu-duel-gone { opacity: 0; transform: scale(0.5); }
.wu-duel-nudge { animation: wu-srv-shake 420ms ease; }
/* Compulsory jumps are SHOWN: every piece that can jump glows. Both keyframes
   carry one drop-shadow each, so the filter interpolates instead of snapping. */
.wu-chk-must svg { animation: wu-chk-glow 1.1s ease-in-out infinite alternate; }
@keyframes wu-chk-glow {
  from { filter: drop-shadow(0 0 1px var(--colour-accent)); }
  to   { filter: drop-shadow(0 0 7px var(--colour-accent)); }
}

/* Each player's CORNER: a turn pill and, next to it, that side's pieces (#147).
   There is no info panel beside these boards, so everything a player needs sits
   at their own edge. BESIDE the board when the gutters are wide enough (top-left
   and bottom-right), in a band above and below it otherwise. `data-cues` is
   written by fitDuelBoard(). */
.wu-duel-side {
  position: absolute;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 8px;
}
/* THE PLAYER'S COLOUR IS THE PILL (owner, 2026-09-15: "the turn pill should
   match the piece count and undo button in size, make the background colour the
   differentiator with the text inside"). The same 32px height as ↩️ and the
   count, so a corner reads as one row of equal parts, with no separate colour
   dot. SOLID means your turn, FADED means wait for the other player (owner:
   "the fading should suffice for the turn indicator"). Faded, both pills take
   dark text: white on a faded charcoal would drop under 4.5:1, as white on
   amber does at any strength (#121). */
.wu-duel-cue {
  --duel-fill: var(--colour-brand);
  --duel-ink: var(--colour-text-primary);
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-height: 32px;
  max-width: 100%;
  padding: 4px 12px;
  border-radius: 16px;
  background: color-mix(in srgb, var(--duel-fill) 45%, var(--colour-surface));
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: var(--font-size-sm);
  line-height: 1.15;
  color: var(--colour-text-primary);
  text-align: center;
  white-space: nowrap;
}
.wu-duel-cue-b {
  --duel-fill: var(--colour-text-primary);
  --duel-ink: var(--colour-surface);
}
.wu-duel-cue.wu-duel-on {
  background: var(--duel-fill);
  color: var(--duel-ink);
}
/* The player's ↩️ undo and their count. In a band (tablet) they follow the pill
   in one row; beside the board (phone) they share the row ABOVE it. */
.wu-duel-tools {
  display: flex;
  align-items: center;
  gap: 8px;
}
.wu-duel-undo {
  position: relative;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 3px;
  box-sizing: border-box;
  min-width: 44px;
  height: 32px;
  padding: 0 10px;
  border: 1px solid var(--colour-border);
  border-radius: 999px;
  background: var(--colour-surface);
  color: var(--colour-text-primary);
  font-family: var(--font-display);
  font-size: var(--font-size-sm);
  line-height: 1;
  cursor: pointer;
}
/* 44px tall to the finger, 32px to the eye, like the pill beside it. */
.wu-duel-undo::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 44px;
  transform: translateY(-50%);
}
.wu-duel-undo b {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
}
.wu-duel-undo:focus-visible {
  outline: 2px solid var(--colour-accent);
  outline-offset: 2px;
}
/* Nothing to undo: dimmed, but still a button, so a tap makes the game say why
   ("No undos left.") instead of doing nothing (#86). */
.wu-duel-undo[aria-disabled="true"] { opacity: 0.55; }
/* The count: small pieces, each with a number when there is more than one. */
.wu-duel-tally {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 2px 4px;
  box-sizing: border-box;
  min-height: 32px;
  padding: 4px 10px;
  border: 1px solid var(--colour-border);
  border-radius: 16px;
  background: var(--colour-surface);
}
.wu-duel-group {
  display: inline-flex;
  align-items: flex-end;
}
.wu-duel-mini {
  display: block;
  width: 20px;
  height: 20px;
}
.wu-duel-mini svg {
  display: block;
  width: 100%;
  height: 100%;
}
.wu-duel-group b {
  margin-left: 1px;
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--colour-text-primary);
}

.wu-duel-zone[data-cues="band"] .wu-duel-side {
  left: 0;
  right: 0;
  margin-inline: auto;
  width: fit-content;
  max-width: calc(100% - 16px);
}
.wu-duel-zone[data-cues="band"] .wu-duel-side-top { top: 6px; }
.wu-duel-zone[data-cues="band"] .wu-duel-side-bottom { bottom: 6px; }
/* Beside the board a corner is a FIXED-WIDTH block (owner, 2026-09-15: "instruction
   pill should sit at the base, and have the count and undo on top of it. so its
   a fixed width contained information"): the count and ↩️ share the top row and
   the pill runs the full width underneath. The pill no longer changes width
   with every message, and the longest messages get the most room. Never wider
   than the gutter, so a message wraps rather than reaching the board. */
.wu-duel-zone[data-cues="side"] .wu-duel-side {
  flex-direction: column-reverse;
  align-items: stretch;
  gap: 6px;
  width: min(124px, calc((100% - var(--duel-cell) * 8) / 2 - 16px));
}
.wu-duel-zone[data-cues="side"] .wu-duel-side-top { top: 8px; left: 8px; }
.wu-duel-zone[data-cues="side"] .wu-duel-side-bottom { bottom: 8px; right: 8px; }
/* The count takes what ↩️ leaves, so the row is as wide as the pill. Where the
   gutter is too narrow for both (a 4:3 tablet leaves ~95px), the count drops
   under ↩️ and the block stays a single column. */
.wu-duel-zone[data-cues="side"] .wu-duel-tools {
  flex-direction: row-reverse;
  flex-wrap: wrap;
  gap: 6px;
}
.wu-duel-zone[data-cues="side"] .wu-duel-tally {
  flex: 1 0 auto;
  justify-content: center;
}
/* A gutter beside the board is ~116px on a landscape phone, so the pill's words
   step down to the count's size, which keeps "You must jump!" on one line. */
.wu-duel-zone[data-cues="side"] .wu-duel-cue {
  padding-inline: 8px;
  font-size: var(--font-size-xs);
  white-space: normal;
}
/* Two players: the far corner is laid out like the near one and then turned
   round, so it reads the same way from the far side. */
.wu-duel-duo .wu-duel-side-top { transform: rotate(180deg); }

/* Who's playing — over the board, so the board is already on screen while the
   child chooses. */
.wu-duel-pick {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  background: color-mix(in srgb, var(--colour-grid-bg) 82%, transparent);
  backdrop-filter: blur(3px);
}
.wu-duel-pick-title {
  margin: 0 0 var(--space-xs);
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: var(--font-size-lg);
  color: var(--colour-text-primary);
}
.wu-duel-pick .wu-sheet-btn {
  flex-direction: column;
  gap: 2px;
  min-width: 220px;
  padding: var(--space-sm) var(--space-lg);
}
.wu-duel-pick .wu-sheet-btn small {
  font-family: var(--font-body);
  font-weight: var(--font-weight-normal, 400);
  font-size: var(--font-size-xs);
  opacity: 0.85;
}

/* Both carry their own `display`, which beats the UA's `[hidden]` rule, so the
   attribute alone left the overlay up and swallowing every board tap. */
.wu-duel-pick[hidden],
.wu-duel-side[hidden],
.wu-duel-undo[hidden],
.wu-duel-tally[hidden],
.wu-duel-tools[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .wu-duel-piece { transition: none; }
  .wu-duel-nudge { animation: none; }
  .wu-chk-must svg { animation: none; filter: drop-shadow(0 0 5px var(--colour-accent)); }
}

/* ── ♟️ Chess (#147) ─────────────────────────────────────────────────────
   On the shared square board above. Only the pieces, the capture ring, the
   check glow, the rail's taken pieces and the promotion choice are Chess's own. */
.wu-chs-piece svg {
  width: 94%;
  height: 94%;
  margin: 3%;
}
.wu-chs-shadow { fill: color-mix(in srgb, var(--colour-text-primary) 30%, transparent); }
.wu-chs-body { stroke-width: 3.2; stroke-linejoin: round; }
.wu-duel-a .wu-chs-body {
  fill: var(--colour-brand);
  stroke: color-mix(in srgb, var(--colour-brand-deep) 60%, var(--colour-text-primary));
}
.wu-duel-b .wu-chs-body {
  fill: color-mix(in srgb, var(--colour-text-primary) 82%, var(--colour-text-secondary));
  stroke: var(--colour-text-primary);
}
/* The knight's eye and the bishop's cut: dark on amber, light on charcoal. */
.wu-chs-cut { fill: none; stroke-width: 4; stroke-linecap: round; }
.wu-chs-body .wu-chs-dot { stroke: none; }
.wu-duel-a .wu-chs-dot { fill: color-mix(in srgb, var(--colour-brand-deep) 35%, var(--colour-text-primary)); }
.wu-duel-a .wu-chs-cut { stroke: color-mix(in srgb, var(--colour-brand-deep) 35%, var(--colour-text-primary)); }
.wu-duel-b .wu-chs-dot { fill: color-mix(in srgb, var(--colour-text-secondary) 45%, var(--colour-surface)); }
.wu-duel-b .wu-chs-cut { stroke: color-mix(in srgb, var(--colour-text-secondary) 45%, var(--colour-surface)); }
/* The king's ★ is gold on both sides, like a Checkers king's. */
.wu-chs-body .wu-chs-star {
  fill: var(--colour-gold);
  stroke: var(--colour-text-primary);
  stroke-width: 2.5;
}

/* A square the chosen piece can TAKE on gets a ring rather than a dot: the dot
   would hide under the piece standing there. */
.wu-chs-hit::after {
  content: "";
  position: absolute;
  inset: 5%;
  border-radius: 50%;
  border: 4px solid color-mix(in srgb, var(--colour-accent) 75%, transparent);
}
.wu-chs-check {
  background: radial-gradient(circle, color-mix(in srgb, var(--colour-danger) 70%, transparent) 0 38%, transparent 72%);
}

/* Promotion: four pieces in the mover's colour. `.wu-duel-pick .wu-sheet-btn`
   sets a 220px minimum for the Who's-playing buttons, so these undo it. */
/* Always 2×2: a wrapping row fell to three-and-one on a small phone, which
   reads as if one choice were different from the others. */
.wu-chs-promo {
  display: grid;
  grid-template-columns: repeat(2, auto);
  gap: var(--space-sm);
}
.wu-duel-pick .wu-chs-promo-btn {
  min-width: 0;
  width: 80px;
  padding: 6px 4px;
  gap: 0;
}
.wu-chs-promo-btn svg {
  width: 48px;
  height: 48px;
}
.wu-duel-pick .wu-chs-promo-back {
  min-width: 0;
  padding: 6px var(--space-md);
}
/* Two players: the choice faces whoever is promoting. */
.wu-chs-flip { transform: rotate(180deg); }

/* ── 🎼 Songs (#140 i) ────────────────────────────────────────────────────
   Eight lanes, and the lane IS the note. Notes are positioned in BEATS
   (`--s` start, `--b` length) and scaled by `--sng-ppb`, which songs.js sets
   from the lanes' height, so a rotation re-scales the song without rebuilding
   it. Only the track's transform changes while a song falls. Lane colours are
   the `--colour-note-*` tokens: the child's practice-book keyboard. */
.wu-sng-zone {
  flex: 1;
  position: relative;
  width: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
  touch-action: none;
  /* The shared zone frame (.wu-game-zone), drawn here because the class itself
     sits on the PLAY area: that is where a game credit is charged (songs.js). */
  border: 1px solid color-mix(in srgb, var(--colour-border) 70%, transparent);
  border-radius: var(--radius-card);
  overflow: hidden;
}
.wu-sng-zone [data-l="0"], .wu-sng-sheet [data-l="0"] { --lane: var(--colour-note-1); }
.wu-sng-zone [data-l="1"], .wu-sng-sheet [data-l="1"] { --lane: var(--colour-note-2); }
.wu-sng-zone [data-l="2"], .wu-sng-sheet [data-l="2"] { --lane: var(--colour-note-3); }
.wu-sng-zone [data-l="3"], .wu-sng-sheet [data-l="3"] { --lane: var(--colour-note-4); }
.wu-sng-zone [data-l="4"], .wu-sng-sheet [data-l="4"] { --lane: var(--colour-note-5); }
.wu-sng-zone [data-l="5"], .wu-sng-sheet [data-l="5"] { --lane: var(--colour-note-6); }
.wu-sng-zone [data-l="6"], .wu-sng-sheet [data-l="6"] { --lane: var(--colour-note-7); }
.wu-sng-zone [data-l="7"], .wu-sng-sheet [data-l="7"] { --lane: var(--colour-note-8); }

/* The song picker: six songs around the instrument, which is the ⇄ swap. */
.wu-sng-pick {
  flex: 1;
  position: relative;
  min-height: 0;
}
.wu-hex-wrap.wu-sng-open { opacity: 0.3; filter: grayscale(1); pointer-events: none; }
/* The picker is height-bound in landscape (the 19-flower fills its box), so the
   wrap's shared 16px padding is the only room left to give the tiles. */
.wu-sng-pick .wu-honeycomb-wrap { padding: var(--space-sm); }
/* A song tile is its name: bigger and darker than a sub-flower label, which
   shares its tile with an icon, and allowed nearly the tile's full width (the
   shared 16px side padding left a 57px tile 25px, and names ran to 3 lines).
   The name owns the hex's wide middle band; the 🎵 badge hangs in the BOTTOM
   point and a played song's ✓ in the TOP point, so neither can land on a word.
   The corner ✓ other flowers use sat on the second line of a two-line name.
   The instrument's ⇄ uses the same bottom point: in the corner it sat on the
   🎻 (owner), and one place for "what this tile does" reads as one rule. */
.wu-hex-wrap.wu-sng-tile .wu-hex-inner { padding: 0 2px; }
.wu-hex-wrap.wu-sng-tile .wu-hex-label {
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: clamp(10px, calc(var(--hex-w) * 0.14), var(--font-size-sm));
  letter-spacing: 0;
  text-transform: none;
  text-align: center;
  line-height: 1.05;
  color: var(--colour-text-primary);
}
.wu-hex-wrap.wu-sng-tile .wu-hex-badge,
.wu-hex-wrap.wu-sng-inst .wu-hex-badge {
  top: auto;
  right: auto;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  box-sizing: border-box;
  min-width: 0;
  width: max(18px, calc(var(--hex-w) * 0.2));
  height: max(18px, calc(var(--hex-w) * 0.2));
  padding: 0;
  border-width: 1px;
  font-size: max(10px, calc(var(--hex-w) * 0.12));
}
.wu-hex-wrap.wu-sng-tile .wu-hex-check {
  top: 0;
  right: auto;
  left: 50%;
  transform: translateX(-50%);
}

.wu-sng-play {
  flex: 1;
  min-height: 0;
  /* Inside the frame above, so it must not draw a second one. */
  border: 0;
  border-radius: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-sm) var(--space-sm);
}
/* Both carry their own `display`, which beats the UA's `[hidden]` rule. */
.wu-sng-pick[hidden],
.wu-sng-play[hidden] { display: none; }

/* The lanes, the falling track over them and the keys under them share one
   8-column grid with the same gap, so a note always lands on its own key. The
   bottom border is the line a note stops on. */
.wu-sng-lanes,
.wu-sng-track,
.wu-sng-grades,
.wu-sng-keys {
  display: grid;
  grid-template-columns: repeat(8, minmax(0, 1fr));
  gap: 5px;
}
.wu-sng-lanes {
  flex: 1;
  min-height: 0;
  position: relative;
  overflow: hidden;
  border-bottom: 3px solid color-mix(in srgb, var(--colour-text-secondary) 45%, transparent);
}
.wu-sng-lane {
  border-radius: 8px 8px 0 0;
  background: color-mix(in srgb, var(--lane) 8%, transparent);
}
.wu-sng-track {
  position: absolute;
  inset: 0;
  pointer-events: none;
  will-change: transform;
}
.wu-sng-col { position: relative; }
.wu-sng-note {
  position: absolute;
  left: 8%;
  right: 8%;
  box-sizing: border-box;
  bottom: calc(var(--sng-ppb, 60px) * var(--s));
  height: calc(var(--sng-ppb, 60px) * var(--b) - 6px);
  min-height: 16px;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding-bottom: 1px;
  border: 2px solid var(--lane);
  border-radius: 8px;
  background: color-mix(in srgb, var(--lane) 26%, var(--colour-surface));
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: var(--font-size-sm);
  line-height: 1.1;
  color: var(--colour-text-primary);
  transition: opacity 280ms ease, transform 280ms ease;
}
/* The note the song is waiting for. 45%, not stronger: the navy and purple
   lanes would take the dark letter under 4.5:1. */
.wu-sng-note.wu-sng-next {
  background: color-mix(in srgb, var(--lane) 45%, var(--colour-surface));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--lane) 35%, transparent);
}
.wu-sng-note.wu-sng-hit {
  opacity: 0;
  transform: scale(1.15);
}
/* The note being HELD stays and slides down through the line at the song's
   tempo (the lanes clip it there), so its top edge reaching the line is the
   moment to let go. Once let go, whatever is left of it fades, which is how an
   early release shows. */
.wu-sng-note.wu-sng-held {
  background: color-mix(in srgb, var(--lane) 45%, var(--colour-surface));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--lane) 55%, transparent);
}
.wu-sng-note.wu-sng-let {
  opacity: 0.3;
  box-shadow: none;
}

/* How well a hold matched its note, sing-along style, rising from the line in
   the note's own lane. A pill on the surface with the grade as its border, so
   the word keeps the text colour's contrast whatever the grade. The outer lanes
   pin it to their outside edge so a wide word runs inward, not off the board. */
.wu-sng-grades {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}
.wu-sng-gcol {
  display: flex;
  justify-content: center;
  align-items: flex-end;
  padding-bottom: 14px;
}
.wu-sng-gcol[data-l="0"] { justify-content: flex-start; }
.wu-sng-gcol[data-l="7"] { justify-content: flex-end; }
.wu-sng-grade {
  flex: none;
  padding: 3px 8px;
  border: 2px solid var(--grade, var(--colour-border));
  border-radius: 999px;
  background: var(--colour-surface);
  color: var(--colour-text-primary);
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: var(--font-size-xs);
  line-height: 1.2;
  white-space: nowrap;
  animation: wu-sng-grade 950ms ease-out forwards;
}
.wu-sng-g-perfect { --grade: var(--colour-success); background: color-mix(in srgb, var(--colour-success) 14%, var(--colour-surface)); }
.wu-sng-g-great { --grade: var(--colour-accent); }
.wu-sng-g-good { --grade: var(--colour-text-secondary); }
.wu-sng-g-short,
.wu-sng-g-long { --grade: var(--colour-warning); }
@keyframes wu-sng-grade {
  0%   { opacity: 0; transform: translateY(6px) scale(0.9); }
  15%  { opacity: 1; transform: none; }
  70%  { opacity: 1; transform: translateY(-14px); }
  100% { opacity: 0; transform: translateY(-22px); }
}
@keyframes wu-sng-grade-fade {
  0%, 70% { opacity: 1; }
  100%    { opacity: 0; }
}

.wu-sng-keys {
  flex: none;
  height: clamp(56px, calc(var(--vh) * 0.17), 88px);
}
.wu-sng-key {
  box-sizing: border-box;
  min-width: 0;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  border: 2px solid var(--lane);
  border-top-width: 6px;
  border-radius: 0 0 12px 12px;
  background: var(--colour-surface);
  color: var(--colour-text-primary);
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: var(--font-size-lg);
  line-height: 1;
  cursor: pointer;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
  transition: background 120ms ease;
}
.wu-sng-finger {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--colour-text-secondary);
}
/* The key the next note lands on. */
.wu-sng-key.wu-sng-glow { background: color-mix(in srgb, var(--lane) 32%, var(--colour-surface)); }
.wu-sng-key.wu-sng-down { animation: wu-sng-press 160ms ease; }
/* Held down: stays pressed in its lane's colour until the finger lifts. */
.wu-sng-key.wu-sng-held {
  background: color-mix(in srgb, var(--lane) 45%, var(--colour-surface));
  transform: translateY(2px);
}
/* A lane the song never uses: quiet, still playable. */
.wu-sng-lane.wu-sng-unused { background: transparent; }
.wu-sng-key.wu-sng-unused {
  opacity: 0.55;
  border-color: color-mix(in srgb, var(--lane) 45%, var(--colour-border));
}
/* After .wu-sng-down, so a wrong key shakes rather than just dipping. */
.wu-sng-key.wu-sng-shake { animation: wu-srv-shake 420ms ease; }
@keyframes wu-sng-press {
  from { transform: translateY(2px) scale(0.96); }
  to   { transform: none; }
}

/* Rail: the song being played. */
.wu-sng-song {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
}
.wu-sng-song-ic {
  flex: none;
  font-size: 28px;
  line-height: 1;
}
.wu-sng-song-txt {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.wu-sng-song-txt b {
  font-family: var(--font-display);
  font-size: var(--font-size-md);
  color: var(--colour-text-primary);
}
.wu-sng-song-txt small {
  font-size: var(--font-size-xs);
  color: var(--colour-text-secondary);
}
.wu-sng-song-txt .wu-sng-hint { margin-top: 4px; }

/* On a landscape PHONE the details panel folds away while a song plays (owner:
   "on mobile the side screen should collapse… that information is secondary").
   With it, eight keys came out 30px wide at 640×360, under the 44px floor; the
   song itself already says everything the panel did — the next note is lit and
   its key glows, and a wrong key shakes. Keyed on the play area being SHOWN, so
   it is pure CSS with no state to leak, and the picker keeps its panel: that
   flower is height-bound (hiding the panel would not grow a tile) and the panel
   carries the "game sounds are off" notice. The panel's ✕ stays in the DOM, so
   the rail's ✕ still reaches the game's own finish (games.js confirmQuit).
   Tablets and laptops, taller than 500px, keep it. */
@media (orientation: landscape) and (max-height: 500px) {
  .wu-game-layer:has(.wu-sng-play:not([hidden])) > .wu-game-rail { display: none; }
}

/* The song sheet (details panel, tablet and laptop): the song in lines like the
   child's book, one dot per note in its key's colour, the syllable underneath.
   A dot sits 1.5px higher per note up the keyboard, as the book draws them; the
   margins trade off so every note's column is the same height and the words
   stay on one baseline. The panel widens while a song plays so a line of up to
   nine notes fits across (224px wrapped Jingle Bells' long lines). */
@media (min-width: 900px) {
  .wu-game-layer:has(.wu-sng-play:not([hidden])) > .wu-game-rail { width: 300px; }
}
.wu-game-rail > .wu-sng-sheet {
  flex: 1 1 0;
  min-height: 96px;
  overflow-y: auto;
  position: relative;
  overscroll-behavior: contain;
}
.wu-sng-line {
  display: flex;
  flex-wrap: wrap;
  column-gap: 3px;
  row-gap: 6px;
  align-items: flex-start;
  padding-block: 6px;
}
.wu-sng-line + .wu-sng-line {
  border-top: 1px solid color-mix(in srgb, var(--colour-border) 70%, transparent);
}
.wu-sng-sn {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  min-width: 18px;
  gap: 3px;
  transition: opacity 200ms ease;
}
.wu-sng-sn i {
  display: block;
  width: 14px;
  height: 14px;
  margin-top: calc((7 - var(--p)) * 1.5px);
  margin-bottom: calc(var(--p) * 1.5px);
  border-radius: 50%;
  background: var(--lane);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--colour-text-primary) 15%, transparent);
  transition: transform 200ms ease, box-shadow 200ms ease;
}
.wu-sng-sn em {
  font-style: normal;
  font-family: var(--font-display);
  font-size: var(--font-size-xs);
  line-height: 1.1;
  color: var(--colour-text-secondary);
  white-space: nowrap;
}
.wu-sng-sn.wu-sng-done { opacity: 0.4; }
.wu-sng-sn.wu-sng-now i {
  transform: scale(1.3);
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--colour-text-primary) 15%, transparent),
    0 0 0 3px color-mix(in srgb, var(--lane) 40%, transparent);
}
.wu-sng-sn.wu-sng-now em {
  color: var(--colour-text-primary);
  font-weight: var(--font-weight-bold);
}

@media (prefers-reduced-motion: reduce) {
  .wu-sng-note { transition: none; }
  .wu-sng-sn,
  .wu-sng-sn i { transition: none; }
  .wu-sng-key.wu-sng-down,
  .wu-sng-key.wu-sng-shake { animation: none; }
  .wu-sng-grade { animation-name: wu-sng-grade-fade; }
}

/* ── Game side rail (owner request 2026-07-12) ─────────────────────────────
   The info rows above the board (✕/score, status, piece bar, legend) were
   crunching the play area's height. On wide screens the whole info column
   moves to the LEFT as a rail so the board gets the pane's full height;
   narrow/portrait screens keep the stacked layout unchanged. */
/* ⚠️ `--hud-stack-h` is GONE (v395), not merely unused. It measured the height
   of the old TOP-LEFT HUD PILL STACK, which v296 replaced with a 56px left
   SPINE — `body` is padded by `--rail-w`, so every layer already clears it
   horizontally and nothing sits above a game at all. It was corrected twice
   (78px measured → derived from `--support-top`) before anyone asked whether
   the reserve should exist; it should not, and a token left declared with no
   consumers is an invitation to re-apply it. Anything that genuinely has to
   clear the Android status cluster uses `--system-band`, which is gated on the
   shell actually drawing one (#101). */

/* ⚠️ NO EXTRA TOP OFFSET (v395, owner: "the score is not aligned to the top
   padding… standardize top alignments"). These carried `--hud-stack-h` on TOP of
   the layer's own padding, which put every games screen 24px below every other
   screen in the app — measured at 1161×940, a game header at y=65 against y=41
   for the journey, library, help and drill screens.

   The reserve budgeted for the old TOP-LEFT HUD pill stack. #98 replaced that
   with a 56px left SPINE, which content already clears horizontally via the
   body's padding-left — so there has been nothing above a game to clear since.
   #98 named this exact debt ("every derived reserve is still keyed to the old
   top-left geometry") and this is the last of it.

   Android band clearance is NOT lost with it: that comes from the shared
   `.wu-native-cluster .wu-drill-head` rule, which matches these headers too. The
   result screen has no header, so it keeps a band offset of its own — but only
   the BAND, not a whole HUD stack. */
.wu-game-layer > .wu-game-result {
  padding-top: var(--system-band);
}

.wu-game-rail {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
}
/* There is no global box-sizing reset in this project (see the note further
   down this file), and .wu-drill-head sets width:100% — so with the rail's
   16px card padding the headline card computed 258px inside a 224px rail and
   spilled over the board. Border-box for every rail card fixes it at source
   and keeps future cards safe. */
.wu-game-rail > * {
  box-sizing: border-box;
}
/* The rail's own ✕ is HIDDEN but kept in the DOM (v243): the game-name pill's
   ✕ delegates to it by .click(), so each game keeps its own finish() — score
   recorded, result shown — with no per-game change. Scoped to the rail so the
   how-to card, lock panel, difficulty picker and result screen keep theirs. */
.wu-game-rail .wu-drill-quit {
  display: none;
}
/* The side rail is gated on WIDTH-OR-LANDSCAPE, not width alone (v288 audit).
   At 851×393 a phone in landscape missed the 900px gate by 49px and got the
   STACKED layout, which reserves --hud-stack-h plus the whole rail out of a
   393px screen — Stack's board collapsed to 18px hexes, its pile counts to 5px
   and its ×table tag to 2.5px, and the rail then overlapped the board anyway.
   In landscape the scarce axis is height and the plentiful one is width, which
   is exactly what a side rail trades. Tablet PORTRAIT keeps the stack: there
   the height is there and the width is not. */
@media (min-width: 900px), (orientation: landscape) and (min-width: 640px) {
  .wu-game-layer:has(.wu-game-rail) {
    /* Rail sits on the LEFT (owner call 2026-07-21, reversing the v215 move to
       the right). The Android status overlay lives in the TOP-RIGHT corner, so
       a right-hand rail put the score card — and the ✕ pinned in it — straight
       under the system pill, where the ✕ was invisible. The left is also where
       the HUD lives, which the owner set as the app's action anchor. */
    flex-direction: row;
    align-items: stretch;
    gap: var(--space-md);
  }
  .wu-game-rail {
    width: 224px;
    flex: none;
    align-items: stretch;
    gap: var(--space-sm);
    /* Clears the HUD stack, PLUS a 40px band for the ✕ that sits above the
       first card (v241 — pinning it inside the card forced 48px of padding
       that squashed the headline). The BOARD keeps full height: the HUD and
       rail only occupy this left column, so no hex clamp needs adjusting. */
    position: relative;
    /* No top offset here either — see the note above. The rail sits beside the
       spine, not beneath it. */
  }
  /* Rail redesign (v219, owner: "all on top of each other"): every info group
     is its own CARD, with a hierarchy — headline score card (✕ pinned in its
     corner, best beneath), then the status message, then game controls. */
  .wu-game-rail > * {
    background: var(--colour-surface);
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-card, 12px);
    padding: var(--space-sm) var(--space-md);
  }
  /* An empty group (e.g. Copy's paint chips outside paint mode) must not
     render as a blank card. */
  .wu-game-rail > :empty { display: none; }
  /* ⚠️ A LIST that becomes a card must re-take the card's inset. Three of them
     were written as BARE lists and later moved into the rail, and a list's own
     `padding` — `1.1em` on tangram's notes and One of Me's rules, a flat `0` on
     the builder's colour key — sits after the card padding above at the SAME
     specificity, so it silently replaced it: measured, tangram's notes started
     their text 2px inside every other card with the discs all but touching the
     border, and the key card had no inset at all (owner, 2026-09-20: "recheck
     text framing on the side notes"). An element selector here is deliberate —
     (0,1,1) outranks a bare component class, so a list added later is framed
     correctly without knowing this rule exists, and a component that genuinely
     wants a different inset says so with a two-class selector. */
  .wu-game-rail > ul,
  .wu-game-rail > ol { padding: var(--space-sm) var(--space-md); }
  .wu-game-rail .wu-drill-head {
    max-width: none;
    /* Deliberately NOT position:relative — the ✕ must anchor to the rail so it
       lands above this card, not inside it. */
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
  }
  /* ⚠️ THE RAIL'S HEADER IS A COLUMN, so the title must be IN FLOW here.
     drills.css centres it absolutely (v393), which is right for a wide header
     row and wrong for a 209px column: out of flow, it floats over the line
     below — measured on Sudoku at 851×393, "Level 7 · 9×9" drawn straight over
     "Best 36" (owner, 2026-09-16). In a column, flow is the only thing keeping
     two lines apart. Same shape as the #127 net a few lines up in drills.css,
     which fires on a growing control; this one is about the DIRECTION the
     header lays out in, so it belongs with the rules that set that direction. */
  .wu-game-rail .wu-drill-head > .wu-drill-progress {
    position: static;
    transform: none;
    max-width: none;
  }
  /* Headline: the live score, big; best-so-far small beneath it. */
  .wu-game-rail .wu-drill-progress {
    font-family: var(--font-display);
    font-weight: var(--font-weight-bold);
    font-size: var(--font-size-lg);
    color: var(--colour-text-primary);
  }
  .wu-game-rail .wu-drill-timer {
    font-size: var(--font-size-xs);
    color: var(--colour-text-secondary);
    /* The base rule right-aligns it for the horizontal header row; in the
       rail's stacked card it must read left like everything else. */
    text-align: left;
    min-width: 0;
  }
  .wu-game-rail .wu-pop-status,
  .wu-game-rail .wu-g2048-status {
    text-align: left;
    font-size: var(--font-size-sm);
    min-height: 0;
    padding-bottom: var(--space-sm);
  }
  /* A rail card must never be SHRUNK below its content (v288 audit). The rail
     is a flex column and its cards carry `min-height: 0`, which removes the
     automatic minimum — so on a short screen flexbox compressed Stack's piece
     card to 48px while its 131px of content spilled 42px ABOVE it, drawing the
     current piece straight over the score. Invisible until now because this
     layout had only ever run on tall screens; widening the gate to landscape
     phones is what exposed it. Cards keep their height and the rail scrolls if
     the column genuinely cannot fit. */
  .wu-game-rail { overflow-y: auto; }
  .wu-game-rail > * { flex: none; }

  /* ⚠️ THE BOARD GIVES UP THE LAYER'S INSET; THE SIDE PANEL DOES NOT (owner,
     2026-09-23: "the side information on the games does not have consistent
     padding from the top and right. can this be standardised and equalised?").
     #160 tightened the layer for the four fixed-size boards so they could grow.
     Chess and Checkers have no rail (#147) so it cost them nothing, but Sudoku
     and One of Me do — and their panel moved to 8px from the screen edge while
     every other game's sat at the standard inset. The rail re-adds exactly what
     the layer gave up, on its three OUTER sides only, so the board keeps the
     size #160 bought it (sudoku still 37px a square) and the panel lines up with
     the rest of the ring. The rail is on the LEFT here; words.css mirrors this
     for the short-landscape layout, where it is on the right. */
  .wu-game-layer:has(.wu-sdk-zone) .wu-game-rail,
  .wu-game-layer:has(.wu-ome-zone) .wu-game-rail {
    margin: var(--rail-regain-y) 0 var(--rail-regain-y) var(--rail-regain-x);
  }

  /* Stack: piece + legend groups with small-caps section labels. */
  .wu-game-rail .wu-stk-bar {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-xs);
    min-height: 0;
  }
  .wu-game-rail .wu-stk-lbl {
    font-size: 10px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
  }
  .wu-game-rail .wu-stk-lbl-next { margin-left: 0; margin-top: var(--space-sm); }
  .wu-game-rail .wu-stk-legend,
  .wu-game-rail .wu-cpy-bar {
    flex-wrap: wrap;
    justify-content: flex-start;
    gap: var(--space-xs);
    min-height: 0;
  }
  .wu-game-rail .wu-cpy-check { margin-left: 0; width: 100%; }
  /* Re-run each board's sizing with the small side-rail offset (~140px of
     layer padding) instead of the stacked-header offsets — bigger tiles.
     --hex-h/--hex-gap stay valid: they're declared on the same element and
     recompute from the overridden --hex-w. */
  .wu-game-layer .wu-mem-flower[data-size="s1"]   { --hex-w: clamp(62px, calc((var(--vh) - 140px) / 4.6), 150px); }
  .wu-game-layer .wu-mem-flower[data-size="s2"] { --hex-w: clamp(44px, calc((var(--vh) - 140px) / 6.6), 124px); }
  .wu-game-layer .wu-mem-flower[data-size="s3"]   { --hex-w: clamp(30px, calc((var(--vh) - 140px) / 8.6), 94px); }
  .wu-game-layer .wu-cpy-flower[data-size="easy"]   { --hex-w: clamp(56px, calc((var(--vh) - 140px) / 4.6), 150px); }
  .wu-game-layer .wu-cpy-flower[data-size="medium"] { --hex-w: clamp(44px, calc((var(--vh) - 140px) / 6.6), 124px); }
  .wu-game-layer .wu-cpy-flower[data-size="hard"]   { --hex-w: clamp(30px, calc((var(--vh) - 140px) / 8.6), 94px); }
  .wu-game-layer .wu-hvt-flower { --hex-w: clamp(30px, calc((var(--vh) - 140px) / 8.6), 94px); }
  .wu-game-layer .wu-g2048-flower { --hex-w: clamp(44px, calc((var(--vh) - 140px) / 6.6), 124px); }
  .wu-game-layer .wu-stk-flower   { --hex-w: clamp(44px, calc((var(--vh) - 140px) / 6.6), 124px); }
  .wu-game-layer .wu-srv-flower[data-size="s1"] { --hex-w: clamp(44px, calc((var(--vh) - 140px) / 6.6), 124px); }
  .wu-game-layer .wu-srv-flower[data-size="s2"] { --hex-w: clamp(30px, calc((var(--vh) - 140px) / 8.6), 94px); }
  .wu-game-layer .wu-srv-flower[data-size="s3"] { --hex-w: clamp(24px, calc((var(--vh) - 140px) / 10.6), 74px); }
}

/* Result + lock panels — both fill the drill-layer, centred. They scroll for
   the same reason the how-to card above does: a result with a long message or a
   lock panel on a 360px-tall phone can outgrow the layer, and a centred flex
   column that overflows is unreachable in BOTH directions without `safe`. */
.wu-game-result,
.wu-game-lock {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: safe center;
  gap: var(--space-sm);
  text-align: center;
  padding: var(--space-lg);
}
.wu-game-result-emoji,
.wu-game-lock-emoji { font-size: 48px; }

/* Lock face (v258) — drawn, not an emoji, so the frown can lift with progress.
   Sized off the pane so it stays generous on a phone and doesn't dominate a
   desktop pane. */
.wu-game-lock-face {
  width: clamp(104px, 26vmin, 168px);
  height: auto;
  display: block;
  filter: drop-shadow(0 6px 16px color-mix(in srgb, var(--colour-brand-deep) 30%, transparent));
}
/* A slow blink is what sells it as a face rather than a graphic. Transform on
   the ellipse itself, so it scales with whatever size the SVG resolves to. */
.wu-lock-eye {
  transform-box: fill-box;
  transform-origin: center;
  animation: wu-lock-blink 5.2s infinite;
}
@keyframes wu-lock-blink {
  0%, 92%, 100% { transform: scaleY(1); }
  95%           { transform: scaleY(0.12); }
}
@media (prefers-reduced-motion: reduce) {
  .wu-lock-eye { animation: none; }
}
.wu-game-result-title,
.wu-game-lock-title { font-family: var(--font-display); font-size: var(--font-size-xl); }
.wu-game-result-stats { color: var(--colour-text-secondary); font-family: var(--font-mono); }
.wu-game-result-score { font-weight: var(--font-weight-bold); }
.wu-game-result-actions {
  display: flex;
  gap: var(--space-sm);
  margin-top: var(--space-md);
  flex-wrap: wrap;
  justify-content: center;
}
.wu-game-lock-sub { color: var(--colour-text-secondary); max-width: 34ch; }
.wu-game-lock-count { font-family: var(--font-mono); font-weight: var(--font-weight-bold); }

/* 🎟️ Out of games (#66). The two routes to more stacked at ONE width, so
   neither reads as the lesser option. Width comes from the flex column's
   stretch, never width:100% — there is no global box-sizing here, and a 100%
   button plus its padding overflows its column (#90). */
.wu-game-more {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-sm);
  width: min(300px, 100%);
  margin-top: var(--space-sm);
}
.wu-game-lock-note {
  color: var(--colour-text-secondary);
  font-size: var(--font-size-sm);
  max-width: 34ch;
}
.wu-game-result-credits {
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
}

/* ── Short landscape: end of game + Help in TWO COLUMNS (owner, 2026-09-13) ──
   Games are landscape only, and on a landscape phone (851×393) one column does
   not fit: every how-to card's ▶ Play sat 60-130px BELOW the screen, the Help
   index's last game 150px below, and the result's buttons on the bottom edge.
   Sideways, width is the axis a phone has spare, which a second column spends.
   `safe center` so anything still too tall starts at the top instead of being
   centred off both edges. Tablets and laptops (taller than 500px) keep one. */
@media (orientation: landscape) and (max-height: 500px) {
  /* How-to card: the goal across the top, then steps | tip + Play, so the
     reading order still ends on the button, bottom right. */
  .wu-howto {
    display: grid;
    /* Steps get the wider column: they are the longest text, and at 1fr 1fr
       Stack's wrapped 57px past the panel on a 640×360 phone. */
    grid-template-columns: 3fr 2fr;
    grid-template-areas: "goal goal" "steps tip" "steps go";
    align-content: safe center;
    column-gap: var(--space-lg);
    row-gap: var(--space-sm);
    max-width: 760px;
  }
  .wu-howto-goal { grid-area: goal; margin: 0; }
  .wu-howto-steps { grid-area: steps; align-self: center; }
  .wu-howto-tip { grid-area: tip; margin: 0; align-self: end; }
  #wu-howto-go { grid-area: go; align-self: start; }
  /* Help index: the games two to a row. */
  .wu-howto:has(.wu-howto-list) { grid-template-areas: "goal goal" "list list"; }
  .wu-howto-list {
    grid-area: list;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-sm);
  }
  /* Result: what happened on the left, what to do next on the right. */
  .wu-game-result {
    display: grid;
    grid-template-columns: auto auto;
    justify-content: center;
    align-content: safe center;
    column-gap: var(--space-xl);
    row-gap: var(--space-xs);
    padding: var(--space-md);
  }
  .wu-game-result > :not(.wu-game-result-actions) { grid-column: 1; margin: 0; }
  .wu-game-result-actions {
    grid-column: 2;
    grid-row: 1 / span 5;
    align-self: center;
    flex-direction: column;
    align-items: stretch;
    margin-top: 0;
  }
  /* Lock panels (warm-up not done, out of games): the picture on the left,
     the words and buttons on the right. Its ⬡ button sat 116px below a
     393px-tall screen. The picture is always the first child. */
  .wu-game-lock {
    display: grid;
    grid-template-columns: auto minmax(0, 360px);
    justify-content: center;
    align-content: safe center;
    column-gap: var(--space-xl);
    row-gap: var(--space-xs);
    padding: var(--space-md);
  }
  .wu-game-lock > :first-child { grid-column: 1; grid-row: 1 / span 6; align-self: center; }
  .wu-game-lock > :not(:first-child) { grid-column: 2; margin: 0; }
  .wu-game-lock .wu-game-more { justify-self: center; }
}
/* The smallest landscape phones (640×360) need the card's spacing tightened
   as well; above 400px tall the default spacing fits. */
@media (orientation: landscape) and (max-height: 400px) {
  .wu-howto { padding-block: var(--space-xs); row-gap: var(--space-xs); }
  .wu-howto-steps { gap: 2px; line-height: 1.3; }
  .wu-howto-tip { line-height: 1.3; }
}

.wu-set-heading {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: var(--space-sm);
  font-family: var(--font-display);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-bold);
  color: var(--colour-text-primary);
}

.wu-set-heading small {
  font-family: var(--font-body);
  font-weight: normal;
  font-size: var(--font-size-xs);
  color: var(--colour-text-secondary);
}

/* Reading diary + activity log read views (child Settings flower) */
.wu-log-week {
  margin-bottom: var(--space-sm);
}

/* Calendar page (Settings → 📅) — calendar left (static), activity list right
   (scrolls). Click a day filters the list. Stacks below 900px. */
/* Activity filter above the list — the fold that retired the standalone reading
   diary (2026-08-16). Reuses .wu-cal-tab because filtering this list is the same
   affordance as the calendar panel's own tabs.

   TWO BUTTONS, ONE ROW (owner, 2026-09-19). A chip per activity wrapped to three
   rows on a phone, where this panel is a bottom sheet at 62% of the screen — the
   filter was eating the list it filters. `All` keeps its natural width and the
   picker takes the rest, TRUNCATING rather than wrapping, so the row count no
   longer depends on how many activities a family has recorded.
   The ::after is the 44px hit area (#88–#90): the button itself is 36px. */
.wu-calpage-filters {
  display: flex;
  flex-wrap: nowrap;
  gap: 10px;
  margin-bottom: var(--space-sm);
}
.wu-calpage-filters .wu-cal-tab {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 36px;
}
.wu-calpage-filters .wu-cal-tab::after {
  content: "";
  position: absolute;
  inset: 50% auto auto 50%;
  width: 100%;
  min-width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}
.wu-calpage-filters .wu-cal-pick {
  flex: 1;
  min-width: 0;
  justify-content: center;
}
/* The label is its own box so it can ellipsise while the ▾ stays put — a
   truncating flex item needs min-width:0, since its default minimum is its own
   content and it would otherwise push the arrow out of the button. */
.wu-calpage-filters .wu-cal-pick-lab {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.wu-calpage-filters .wu-set-icon {
  width: 16px;
  height: 16px;
  font-size: 14px;
}

.wu-calpage-head {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-sm);
  min-height: 36px;
}
.wu-calpage-day {
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
}

.wu-log-row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--colour-border);
  font-size: var(--font-size-sm);
}

.wu-log-icon {
  flex: 0 0 auto;
  display: inline-flex;
}

.wu-log-when {
  flex: 0 0 5.5em;
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--colour-text-secondary);
}

.wu-log-what {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--colour-text-primary);
}

.wu-log-what small {
  color: var(--colour-text-secondary);
}

.wu-log-val {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--colour-text-secondary);
}

.wu-log-diaryday {
  background: color-mix(in srgb, var(--colour-accent) 8%, transparent);
  border-radius: var(--radius-cell);
}

.wu-log-practice {
  font-size: var(--font-size-xs);
  color: var(--colour-warning);
  border: 1px solid var(--colour-warning);
  border-radius: var(--radius-pill);
  padding: 0 6px;
}

/* Parent dashboard (decision #55, Slice B) — at-a-glance stats + section list */
.wu-dash-overview {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}

.wu-dash-stat {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-sm) var(--space-md);
  background: var(--colour-bg);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-card);
}

.wu-dash-num {
  font-family: var(--font-mono);
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-bold);
  color: var(--colour-text-primary);
}

.wu-dash-name {
  font-family: var(--font-display);
  font-size: var(--font-size-md);
}

.wu-dash-cap {
  font-size: var(--font-size-xs);
  color: var(--colour-text-secondary);
}

.wu-dash-row {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  width: 100%;
  padding: var(--space-md);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-card);
  margin-bottom: var(--space-sm);
  cursor: pointer;
  text-align: left;
  transition: border-color var(--transition-fast), background var(--transition-fast);
}

.wu-dash-row:hover {
  border-color: var(--colour-brand);
  background: color-mix(in srgb, var(--colour-brand) 8%, var(--colour-surface));
}

.wu-dash-ic {
  flex: 0 0 auto;
  display: inline-flex;
  font-size: 20px;
}

.wu-dash-text {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.wu-dash-title {
  font-family: var(--font-display);
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-bold);
  color: var(--colour-text-primary);
}

.wu-dash-sub {
  font-size: var(--font-size-sm);
  color: var(--colour-text-secondary);
}

.wu-dash-chev {
  flex: 0 0 auto;
  font-size: var(--font-size-lg);
  color: var(--colour-text-secondary);
}

.wu-dash-badge {
  flex: 0 0 auto;
  min-width: 24px;
  height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 8px;
  background: var(--colour-danger);
  color: #fff;
  border-radius: var(--radius-pill);
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-bold);
}

/* Dashboard is wider than a normal settings list — it carries two columns */
.wu-set-list.wu-dash {
  width: min(940px, 100%);
}

/* Widen the drill header to match the dashboard so the ✕ lines up */
.wu-drill-layer:has(.wu-dash) .wu-drill-head {
  max-width: 940px;
}

.wu-dash-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
  gap: var(--space-lg);
  align-items: start;
}

/* ⚠️ AND THE ITEMS NEED IT TOO — `minmax(0, …)` frees the TRACK, `min-width: 0`
   frees the BOX. A grid item's `min-width: auto` is its own automatic minimum
   size, so a column whose widest unbreakable content is 505px still renders
   505px wide inside a 269px track and spills out of a parent that clips. Both
   halves or neither. */
.wu-dash-grid > * {
  min-width: 0;
}

/* Stack the columns when the pane is too narrow for two.
   ⚠️ `minmax(0, 1fr)`, NOT a bare `1fr`. A grid item's automatic minimum size is
   its MIN-CONTENT size, and `1fr` means `minmax(auto, 1fr)` — so the single
   stacked track could not shrink below whatever the settings column's widest
   unbreakable content demanded. Measured at 375px: a **505px track in a 269px
   grid**, with `.wu-set-list`'s `overflow: hidden` clipping the excess and no
   page scroll to reach it, which is what made the school-websites panel
   unusable on a phone (owner, 2026-09-26). The TWO-column rule above was
   already written `minmax(0, 1fr)`; only the phone case was unprotected. */
@media (max-width: 900px) {
  .wu-dash-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* Phones: the dashboard/settings live inside .wu-drill-layer (centred, padded).
   Tighten the side padding and clip any sub-pixel overflow so the settings list
   sits flush with no stray horizontal scroll. */
@media (max-width: 720px) {
  .wu-drill-layer {
    /* The inset itself is --layer-pad-* on :root now (layout.css) — setting the
       `padding` shorthand here silently dropped --safe-top. */
    overflow-x: hidden;
  }
  .wu-set-list,
  .wu-set-list.wu-dash {
    width: 100%;
  }
}

.wu-dash-main {
  display: flex;
  flex-direction: column;
}

.wu-dash-settings {
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-card);
  background: var(--colour-surface);
  padding: 0 var(--space-md) var(--space-sm);
}

.wu-dash-set-head {
  margin-top: var(--space-sm);
}

.wu-set-slider {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex: 1;
  max-width: 280px;
}

.wu-set-slider input[type="range"] {
  flex: 1;
  accent-color: var(--colour-accent);
  min-height: 36px;
}

.wu-set-stepper {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex: none;
}

.wu-set-stepper button {
  width: 36px;
  font-size: var(--font-size-md);
}

.wu-set-value {
  font-family: var(--font-mono);
  font-size: var(--font-size-md);
  color: var(--colour-text-primary);
  min-width: 1.5ch;
  text-align: center;
}

/* ── #️⃣ Sudoku (#148) ───────────────────────────────────────────────────
   The app's second SQUARE board, after the duel board. Sized in JS from the zone
   it sits in (`--sdk-cell`, whole pixels per square); everything below scales
   from that one number, including the type.

   THE ZOOM IS ONE TRANSFORM on the grid, not a second board: the cells keep
   their identity and their hit areas grow with them, so a 9×9 square that is
   ~32px flat on a phone is ~100px once its box fills the board — comfortably
   over the 44px tap floor (#88–#90). The zone already clips (.wu-game-zone), so
   the rest of the grid simply runs off the edge while a box is open. */
.wu-sdk-zone {
  flex: 1;
  position: relative;
  width: 100%;
  min-height: 0;
  --sdk-cell: 36px;
  --sdk-n: 9;
  /* Times-table squares run to three digits (12 × 9), so they are set smaller.
     ONE variable, written where the grid is built, so the two modes cannot
     disagree about it. */
  --sdk-fs: 0.5;
}
.wu-sdk-grid {
  position: absolute;
  inset: 0;
  margin: auto;
  width: calc(var(--sdk-cell) * var(--sdk-n));
  height: calc(var(--sdk-cell) * var(--sdk-n));
  display: grid;
  grid-template-columns: repeat(var(--sdk-n), var(--sdk-cell));
  grid-auto-rows: var(--sdk-cell);
  background: var(--colour-surface);
  /* The board's OWN edge, and now the only frame around it (see .wu-game-zone). */
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--colour-text-secondary) 35%, var(--colour-surface));
}
/* Border-box, so the thick box edges below take their width out of the square
   rather than adding to it — there is no global box-sizing reset here (#90). */
.wu-sdk-cell {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid color-mix(in srgb, var(--colour-border) 80%, var(--colour-text-secondary));
  background: transparent;
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: calc(var(--sdk-cell) * var(--sdk-fs));
  line-height: 1;
  /* The child's own numbers are a deep blue against the near-black givens: two
     colours that both clear AA on the surface (#121), so "what I put there" is
     visible without either being hard to read. */
  color: color-mix(in srgb, var(--colour-accent) 55%, var(--colour-text-primary));
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.wu-sdk-given { color: var(--colour-text-primary); }
.wu-sdk-br { border-right: 3px solid color-mix(in srgb, var(--colour-text-secondary) 45%, var(--colour-surface)); }
.wu-sdk-bb { border-bottom: 3px solid color-mix(in srgb, var(--colour-text-secondary) 45%, var(--colour-surface)); }
.wu-sdk-fill { background: color-mix(in srgb, var(--colour-accent) 9%, var(--colour-surface)); }
/* The selected square and its row, column and box — the three rules shown
   rather than explained. After .wu-sdk-fill, same specificity, so a selected
   square that the child filled still reads as selected. */
.wu-sdk-peer { background: color-mix(in srgb, var(--colour-accent) 7%, var(--colour-surface)); }
.wu-sdk-sel {
  background: color-mix(in srgb, var(--colour-accent) 22%, var(--colour-surface));
  box-shadow: inset 0 0 0 2px var(--colour-accent);
}
.wu-sdk-cell:focus-visible {
  outline: 2px solid var(--colour-accent);
  outline-offset: -2px;
}
.wu-sdk-bad {
  animation: wu-srv-shake 420ms ease;
  background: color-mix(in srgb, var(--colour-danger) 22%, var(--colour-surface));
}

/* The number pad. Three columns whatever the grid size, so 1–9 reads as a
   keypad; each key is at least the tap floor tall. */
.wu-sdk-pad {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-xs);
  width: 100%;
  max-width: 260px;
  margin-inline: auto;
}
.wu-sdk-key {
  box-sizing: border-box;
  min-height: 44px;
  padding: 4px;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-button);
  background: var(--colour-surface);
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: var(--font-size-md);
  color: var(--colour-text-primary);
  cursor: pointer;
}
.wu-sdk-key:focus-visible {
  outline: 2px solid var(--colour-accent);
  outline-offset: 2px;
}
/* Every one of that number is placed: dimmed, but still a button, so a tap says
   why instead of doing nothing (#86). */
.wu-sdk-key[aria-disabled="true"] { opacity: 0.4; }
/* Score and hearts share ONE rail card (owner, 2026-09-16: "move hearts to rail
   to save space"). They were two, and on a landscape phone the rail's own
   content came to 335px inside 311px — it scrolled. */
/* WHERE TO START (batch item 6). A modifier under the mode cards, sized and
   coloured to read as secondary to them — the card is the action, this only
   changes what it does. Wraps rather than shrinking, so the 44px targets hold
   on a 360px phone. */
.wu-sdk-sizes {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  margin-top: var(--space-md);
}
.wu-sdk-sizes-lab {
  font-family: var(--font-display);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-bold);
  color: var(--colour-text-secondary);
}
.wu-sdk-size {
  box-sizing: border-box;
  min-height: 44px;
  padding: 0 var(--space-md);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-pill);
  background: var(--colour-surface);
  color: var(--colour-text-primary);
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-bold);
  cursor: pointer;
}
.wu-sdk-size:hover { border-color: var(--colour-accent); }
.wu-sdk-size-on {
  border-color: var(--colour-accent);
  background: color-mix(in srgb, var(--colour-accent) 14%, var(--colour-surface));
}
/* Locked reads as NOT YET, never as broken: it keeps its shape and its tap, and
   answers in the status line when pressed (#134). */
.wu-sdk-size[aria-disabled="true"] {
  opacity: 0.55;
  color: var(--colour-text-secondary);
}

.wu-sdk-line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-sm);
}

/* SHORT LANDSCAPE with the biggest pad there is — nine keys, three rows of 44px.
   Measured at 851×393 the rail's content came to 326px inside 311px, so the last
   row of keys was cut off (owner, 2026-09-16). The cards give up their vertical
   padding rather than the keys giving up their tap size. */
@media (orientation: landscape) and (max-height: 500px) {
  /* Scoped to this game's rail: the other games' columns are shorter and have
     no reason to tighten. */
  .wu-game-layer:has(.wu-sdk-zone) .wu-game-rail {
    gap: var(--space-xs);
    /* The 16px band this used to shave is gone for every game (2026-09-23): it
       was --support-top's gutter counted a second time on top of the layer's
       own inset, so the rail now starts level with its right-hand gap and this
       game needs no reserve of its own. */
  }
  .wu-game-layer:has(.wu-sdk-zone) .wu-game-rail > .wu-drill-head {
    padding-top: var(--space-xs);
    padding-bottom: var(--space-xs);
  }
  .wu-game-rail > .wu-sdk-line,
  .wu-game-rail > .wu-sdk-pad,
  .wu-game-rail > #wu-sdk-status {
    padding-top: var(--space-xs);
    padding-bottom: var(--space-xs);
  }
}

/* ── 🙂 One of Me (#149) ──────────────────────────────────────────────────────
   Coloured regions on a square board; the child's own avatar is the piece.

   The eight region colours are deliberately spread across the wheel, but the
   board is NOT read by colour alone: every boundary between two regions is
   drawn as a thick edge, so the shapes are legible to a child who cannot tell
   two of the hues apart. Eight, because the board stops at 8×8. */
.wu-ome-zone {
  flex: 1;
  position: relative;
  width: 100%;
  min-height: 0;
  --ome-cell: 44px;
  --ome-n: 5;
  --ome-c0: #E57373;
  --ome-c1: #FFB74D;
  --ome-c2: #A1887F;
  --ome-c3: #AED581;
  --ome-c4: #4DB6AC;
  --ome-c5: #64B5F6;
  --ome-c6: #BA68C8;
  --ome-c7: #F06292;
}
.wu-ome-grid {
  position: absolute;
  inset: 0;
  margin: auto;
  width: calc(var(--ome-cell) * var(--ome-n));
  height: calc(var(--ome-cell) * var(--ome-n));
  display: grid;
  grid-template-columns: repeat(var(--ome-n), var(--ome-cell));
  grid-auto-rows: var(--ome-cell);
  background: var(--colour-surface);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--colour-text-secondary) 45%, var(--colour-surface));
}
/* Border-box, so the thick region edges take their width out of the square
   rather than adding to it — there is no global box-sizing reset here (#90). */
.wu-ome-cell {
  box-sizing: border-box;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid color-mix(in srgb, var(--colour-border) 80%, var(--colour-text-secondary));
  background: color-mix(in srgb, var(--rc) 38%, var(--colour-surface));
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
/* Only the right and bottom edges are drawn, so each internal boundary belongs
   to exactly one cell and is never doubled. */
.wu-ome-br { border-right: 3px solid var(--colour-text-primary); }
.wu-ome-bb { border-bottom: 3px solid var(--colour-text-primary); }
.wu-ome-cell:focus-visible {
  outline: 2px solid var(--colour-accent);
  outline-offset: -2px;
}
/* A white pad under the avatar, so the disc reads on ANY region colour —
   including one the child is also wearing. Drawn by the cell rather than as a
   ring on the disc, because `avatarStyle` already owns the disc's box-shadow
   and outline for the ⭐ frames (#126) and a second ring there would be
   overwritten by them. */
.wu-ome-mark::before {
  content: "";
  position: absolute;
  inset: 8%;
  border-radius: 50%;
  background: var(--colour-surface);
}
.wu-ome-disc {
  position: relative;
  flex: none;
  width: 68%;
  aspect-ratio: 1;
  height: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: calc(var(--ome-cell) * 0.4);
  line-height: 1;
  animation: wu-ome-land 260ms ease;
}
.wu-ome-bad {
  animation: wu-srv-shake 420ms ease;
  background: color-mix(in srgb, var(--colour-danger) 26%, var(--colour-surface));
}
/* The mark that made a tap illegal, pointed at rather than only described. */
.wu-ome-culprit::after {
  content: "";
  position: absolute;
  inset: 2px;
  border-radius: 50%;
  box-shadow: 0 0 0 3px var(--colour-danger);
  animation: wu-ome-point 900ms ease;
}
@keyframes wu-ome-land {
  from { transform: scale(0.4); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}
@keyframes wu-ome-point {
  0%, 100% { opacity: 0; }
  20%, 70% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .wu-ome-disc { animation: none; }
  .wu-ome-culprit::after { animation: none; opacity: 1; }
}

/* Score and hearts share one line, so the rail stays short (#148). */
.wu-ome-line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xs);
  width: 100%;
}
.wu-ome-rules {
  margin: 0;
  padding-left: 0;
  list-style: none;
  font-size: var(--font-size-sm);
  line-height: 1.45;
  color: var(--colour-text-secondary);
}
/* Same hanging indent as tangram's notes, for the same reason. Two of these
   three notes WRAP at rail width, so without a gap between items the second
   line of one sat as close to the next note as to its own first line and the
   three read as five. The gap is given up in short landscape below, where the
   rail fits its box exactly (#149) and a tap target would have to pay for it —
   there the tightened line-height does the separating instead. */
.wu-ome-rules li { padding-left: 0.9em; text-indent: -0.9em; }
.wu-ome-rules li::before { content: "· "; }
.wu-ome-rules li + li { margin-top: var(--space-xs); }

/* Short landscape: the rail gives up padding before anything gives up a tap
   target or a word (#148). */
@media (orientation: landscape) and (max-height: 500px) {
  .wu-game-layer:has(.wu-ome-zone) .wu-game-rail { gap: var(--space-xs); }
  .wu-ome-rules { font-size: var(--font-size-xs); line-height: 1.35; }
  /* ⚠️ An item-drawn bullet is INSIDE the content box, so unlike a `list-style`
     marker it costs every line ~9px of an already narrow column — measured at
     640×360 that wrapped the longest note and put 17px of scroll on a rail that
     fits its box exactly (#149). Here the notes drop the bullet and take the
     width: at this size each is one line, so nothing is ambiguous without it,
     and the rail gives up decoration before a word or a tap target (#148). */
  .wu-ome-rules li { padding-left: 0; text-indent: 0; }
  .wu-ome-rules li::before { content: none; }
  .wu-ome-rules li + li { margin-top: 0; }
}

/* ── One of Me: what a tap does (#149) ───────────────────────────────────────
   A MODE rather than a tap-cycle, so a board tap only ever means one thing
   (owner). Two buttons rather than one that swaps its own label: a single
   toggle has to say either what it IS or what it DOES, and a child reads it
   the other way half the time. */
.wu-ome-modes {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-xs);
  width: 100%;
}
.wu-ome-mode {
  box-sizing: border-box;
  min-height: 44px; /* the tap floor — a rail control is tapped mid-puzzle */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 4px 6px;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-button);
  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-secondary);
  cursor: pointer;
}
.wu-ome-mode-ic {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 1.5em;
  height: 1.5em;
  border-radius: 50%;
  font-size: 0.85em;
  line-height: 1;
}
.wu-ome-mode[aria-pressed="true"] {
  border-color: var(--colour-accent);
  background: color-mix(in srgb, var(--colour-accent) 14%, var(--colour-surface));
  color: var(--colour-text-primary);
}
.wu-ome-mode:focus-visible { outline: 2px solid var(--colour-accent); outline-offset: 2px; }
/* The switch ANNOUNCES itself — the same expand-and-settle language the rail's
   trail circle uses, on the button that just took over rather than on the
   circle, which names the screen and must not be made to say something else. */
.wu-ome-just { animation: wu-ome-took 620ms ease; }
@keyframes wu-ome-took {
  0% { transform: scale(1); box-shadow: 0 0 0 0 var(--colour-accent); }
  35% { transform: scale(1.06); box-shadow: 0 0 0 4px color-mix(in srgb, var(--colour-accent) 35%, transparent); }
  100% { transform: scale(1); box-shadow: 0 0 0 0 transparent; }
}
/* And the BOARD says which mode it is in for as long as it is in it — a mode
   the child has to remember is one they will forget by the third square. */
.wu-ome-grid.wu-ome-marking {
  box-shadow: 0 0 0 3px var(--colour-accent);
}
/* A square the child has RUBBED OUT — their own working: free, never checked,
   and never applied for them, because working out which squares are impossible
   IS the puzzle.

   IT IS A GREY HATCH LAID OVER THE SQUARE, NOT A SYMBOL AND NOT A FLAT GREY
   (owner, 2026-09-19). Two things were rejected on the way here, both for real
   reasons. A ✕ is the obvious mark and the wrong one: the rail carries a ✕ a
   few centimetres above and that one QUITS THE GAME, so the same shape would
   mean two things in one column — the clash #75 exists to catch, with the worse
   of the two outcomes attached. Greying the square outright was the next idea
   and it steals the COLOUR CHANNEL, which on this board is the puzzle's own
   data: the child still has to see which region a rubbed-out square belongs to
   in order to reason about it. A hatch takes neither — the region colour shows
   between the stripes, and "crossed off" is carried by texture, which no other
   part of the board uses. It also costs no glyph at any cell size. */
.wu-ome-note::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    -45deg,
    color-mix(in srgb, var(--colour-text-secondary) 55%, transparent) 0 2px,
    transparent 2px 6px
  );
}

/* The button shows WHAT THE SQUARE WILL LOOK LIKE rather than naming it with a
   symbol — the same hatch, so the control and the board cannot drift apart. */
.wu-ome-swatch {
  position: relative;
  width: 1.15em;
  height: 1.15em;
  flex: none;
  border-radius: 3px;
  border: 1px solid var(--colour-border);
  background:
    repeating-linear-gradient(
      -45deg,
      color-mix(in srgb, var(--colour-text-secondary) 55%, transparent) 0 2px,
      transparent 2px 6px
    ),
    color-mix(in srgb, var(--colour-accent) 22%, var(--colour-surface));
}

@media (prefers-reduced-motion: reduce) {
  .wu-ome-just { animation: none; }
}
@media (orientation: landscape) and (max-height: 500px) {
  .wu-ome-mode { min-height: 44px; font-size: var(--font-size-xs); }
}
/* ── 🔺 Tangram (#140 a) ─────────────────────────────────────────────────────
   Pick a shape on the flower, then make it with the seven classic pieces. The
   play surface is ONE <svg> with a viewBox, so the browser does all the
   scaling: no JS fitter to keep in step with the container, and board and tray
   share one coordinate system, which is what makes picking a piece up over
   there and putting it down here a single gesture. */

.wu-tan-zone {
  flex: 1;
  position: relative;
  width: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
  touch-action: none;
  /* The shared zone frame (.wu-game-zone), drawn here because the class itself
     sits on the PLAY area: that is where a game credit is charged (#66). */
  border: 1px solid color-mix(in srgb, var(--colour-border) 70%, transparent);
  border-radius: var(--radius-card);
  overflow: hidden;
}

/* The picker flower. */
.wu-tan-pick {
  flex: 1;
  position: relative;
  min-height: 0;
}
.wu-hex-wrap.wu-tan-open { opacity: 0.3; filter: grayscale(1); pointer-events: none; }
/* 🎨 An empty slot on the child's own flower (#156). Faint like `.wu-tan-open`
   above and unlike it STILL TAPPABLE — every free slot starts a new shape, so
   the six tiles state the room left without a word of copy. Same treatment as
   the star menu's empty website slots (`.wu-centre-add`), which is the same
   idea: a pointer at something you can add rather than a disabled thing. */
.wu-hex-wrap.wu-tan-add { opacity: 0.5; }
.wu-hex-wrap.wu-tan-add .wu-hex-inner { gap: 1px; }
/* Height-bound in landscape like the song picker, so the wrap's shared 16px
   padding is the only room left to give the tiles. */
.wu-tan-pick .wu-honeycomb-wrap { padding: var(--space-sm); }
/* A tile carries a PICTURE over its name. The picture is the puzzle itself,
   drawn from the same placements the child will match, so it can never be a
   stand-in that drifts from the target. */
.wu-hex-wrap.wu-tan-tile .wu-hex-inner { padding: 0 2px; gap: 1px; }
.wu-tan-mini {
  display: block;
  width: 62%;
  height: max(18px, calc(var(--hex-w) * 0.40));
}
.wu-tan-mini polygon { fill: var(--colour-text-primary); }
.wu-hex-wrap.wu-tan-tile.wu-completed .wu-tan-mini polygon {
  fill: color-mix(in srgb, var(--colour-success) 70%, var(--colour-text-primary));
}
.wu-hex-wrap.wu-tan-tile .wu-hex-label {
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: clamp(10px, calc(var(--hex-w) * 0.13), var(--font-size-sm));
  letter-spacing: 0;
  text-transform: none;
  text-align: center;
  line-height: 1.05;
  color: var(--colour-text-primary);
}
.wu-hex-wrap.wu-tan-tile .wu-hex-check {
  top: 0;
  right: auto;
  left: 50%;
  transform: translateX(-50%);
}

/* The board. */
.wu-tan-play {
  flex: 1;
  min-height: 0;
  display: flex;
}
/* An element that sets its own `display` beats the UA's `[hidden]` rule, so it
   needs its own — without this the board and its tray stay drawn UNDER the
   picker flower, which is the house trap this file keeps re-learning. */
.wu-tan-play[hidden] { display: none; }
.wu-tan-svg {
  width: 100%;
  height: 100%;
  /* Without this the browser claims the gesture as a scroll or a zoom and the
     drag is cancelled mid-move (#86). */
  touch-action: none;
  user-select: none;
}

/* The shape to make: the seven target placements filled in ONE colour with no
   strokes, so their union comes free and there is no outline stored anywhere
   that could disagree with the pieces it describes. */
.wu-tan-sil polygon {
  fill: color-mix(in srgb, var(--colour-text-primary) 70%, var(--colour-surface));
  /* ROUND 2 MUST READ AS ONE SOLID SILHOUETTE (owner, 2026-09-20: "the outline
     is still visible on the second round"). Emptying the seams group is not
     enough: seven abutting FILLS leave anti-aliased hairlines along every shared
     edge, so the dissection stayed faintly legible and round 2 still showed the
     child where the pieces went. Stroking each polygon in its OWN fill colour
     closes them. It costs the outer boundary half a stroke, which at 1px is
     invisible, and round 1 is untouched because its white seams are a separate
     layer drawn on top. */
  stroke: color-mix(in srgb, var(--colour-text-primary) 70%, var(--colour-surface));
  stroke-width: 1px;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
/* ROUND 1 draws the SEAMS between the seven pieces, so the target says where
   each shape goes; round 2 leaves this group empty and the fills close back up
   into one plain silhouette. It is a SEPARATE layer above the fills rather than
   a stroke on them: stroking the fills would let a later polygon's fill paint
   over an earlier one's edge, so some seams would show and some would not.
   The stroke is the page's own surface colour, so a seam reads as a CUT in the
   figure rather than as a line drawn on top of it. */
.wu-tan-seams polygon {
  fill: none;
  stroke: var(--colour-surface);
  stroke-width: 2px;
  stroke-linejoin: round;
  /* The viewBox is 22 units across a board that ranges from ~470px to ~1200px,
     so a width in user units would be four times heavier on a laptop than on a
     phone. Same reason `.wu-tan-area` below carries it. */
  vector-effect: non-scaling-stroke;
}
.wu-tan-round {
  font: 600 var(--font-size-xs) / 1 var(--font-body);
  color: var(--colour-text-secondary);
  background: var(--colour-grid-bg);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-pill);
  padding: 4px 9px;
  white-space: nowrap;
}
.wu-tan-round[hidden] { display: none; }
/* THE TRAY IS A PLACE, NOT A LINE (owner, 2026-09-20: "define the border to the
   tray and background colour. it is difficult to understand when it starts/
   ends"). One dashed rule said where the boundary was and nothing about which
   side was which, so the two halves read as a single field with a mark down it.
   They are two panels now, and they meet at exactly the x the code tests —
   `onBoard` compares a piece's centroid against TRAY_X — so the rule a figure
   is judged by is the rule you can see. Only the TRAY is filled: the board
   keeps the layer's own ground, because that is where the work happens and a
   second tint would be competing with the silhouette drawn on it. */
.wu-tan-area {
  fill: none;
  stroke: var(--colour-border);
  stroke-width: 1.5px;
  vector-effect: non-scaling-stroke;
}
.wu-tan-area-tray { fill: color-mix(in srgb, var(--colour-border) 24%, transparent); }
.wu-tan-area-tag {
  fill: var(--colour-text-secondary);
  font-family: var(--font-body);
  font-size: 0.42px;          /* SVG user units — the viewBox is 22 across */
  font-weight: 700;
  letter-spacing: 0.08px;
  text-anchor: middle;
}

/* The pieces. EACH OF THE SEVEN HAS ITS OWN COLOUR, and the seven are the
   family's cheat-sheet hues taken at the app's lighter tone (owner: "align
   colours with the reference images", "keep the pastel types but align the
   colours"). A child copying a figure off the card can then match piece for
   piece instead of shape for shape. The fills are set INLINE from
   PIECE_COLOURS in src/data/tangram.js, so the game, the picker's previews and
   the dev builder all read one list and cannot drift; there are deliberately no
   per-type fill rules here any more. */
.wu-tan-piece { cursor: grab; }
.wu-tan-piece polygon {
  stroke: var(--colour-surface);
  stroke-width: 1.5px;
  stroke-linejoin: round;
  /* A stroke measured in screen pixels rather than puzzle units, so it stays
     hairline whatever size the board resolves to. */
  vector-effect: non-scaling-stroke;
}
.wu-tan-piece.wu-tan-sel { cursor: grabbing; }
.wu-tan-piece.wu-tan-sel polygon {
  stroke: var(--colour-text-primary);
  stroke-width: 2.5px;
}

/* Rail: how many are placed, and the two tools. */
.wu-tan-line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
  margin-top: var(--space-xs);
}
.wu-tan-tools {
  display: flex;
  gap: var(--space-xs);
  margin-top: var(--space-xs);
}
/* Same again: on the picker there is nothing to turn, so the tools must go. */
.wu-tan-tools[hidden] { display: none; }
.wu-tan-tool {
  flex: 1;
  min-height: 44px;           /* the floor three audits have defended (#88–#90) */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 0 var(--space-xs);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-button);
  background: var(--colour-surface);
  font: inherit;
  font-size: var(--font-size-sm);
  color: var(--colour-text-primary);
  cursor: pointer;
}
.wu-tan-tool-ic { font-size: 1.15em; line-height: 1; }
/* aria-disabled, never the `disabled` attribute: a disabled button fires no
   click, so nothing can explain why it did nothing (#134). It stays tappable
   and says why. */
.wu-tan-tool[aria-disabled="true"] { opacity: 0.45; }
.wu-tan-rules {
  margin: var(--space-sm) 0 0;
  /* No inset of its own: in the rail the CARD supplies it (see the note on
     `.wu-game-rail > ul`), and outside the rail there is no card to sit in. */
  padding-left: 0;
  list-style: none;
  font-size: var(--font-size-xs);
  color: var(--colour-text-secondary);
  line-height: 1.5;
}
/* A HANGING INDENT the layout can be held to: the bullet is drawn by the item
   itself, so it lands exactly on the item's own left edge — which is the
   column's text line — and wrapped lines indent past it. A UA `list-style`
   marker cannot be pinned that way: it is laid out OUTSIDE the content box at a
   width the browser chooses, which is how the discs came to hang ~7px into the
   card's gutter, all but touching the border (owner, 2026-09-20: "recheck text
   framing on the side notes"). */
.wu-tan-rules li { padding-left: 0.95em; text-indent: -0.95em; }
.wu-tan-rules li::before { content: "· "; }
/* A note that WRAPS must not run into the next one: the third note is two lines
   at this width, and at 1.5 line-height the gap between items read the same as
   the gap inside one, so three notes looked like five lines of prose. */
.wu-tan-rules li + li { margin-top: var(--space-xs); }

/* Edit mode on the picker (dev only): the tiles mean "open this in the builder"
   rather than "make this", so the board says so rather than relying on the one
   line of status text — a mode you cannot see is a mode you forget you are in. */
.wu-tan-editing .wu-honeycomb-stage { outline: 2px dashed var(--colour-accent); outline-offset: 6px; }
.wu-tan-dev { margin-top: var(--space-sm); }

/* ── 🛠 Shape builder (dev only, #155) ─────────────────────────────────────
   Its own sub-prefix, per working rule 8: a new surface never borrows another
   component's. It reuses the game's board, tools and rail wholesale — only the
   controls that have no equivalent in the game are styled here. */
.wu-tbu-play { flex: 1; min-height: 0; }
.wu-tbu-row {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  margin-top: var(--space-xs);
}
.wu-tbu-lab {
  flex: none;
  font-size: var(--font-size-xs);
  color: var(--colour-text-secondary);
}
.wu-tbu-sel {
  flex: 1;
  min-width: 0;               /* or a long shape name stretches the whole rail */
  min-height: 34px;
  padding: 0 var(--space-xs);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-button);
  background: var(--colour-surface);
  font: inherit;
  font-size: var(--font-size-xs);
  color: var(--colour-text-primary);
}
.wu-tbu-key {
  margin: var(--space-sm) 0 0;
  padding: 0;
  list-style: none;
  font-size: var(--font-size-xs);
  color: var(--colour-text-secondary);
}
.wu-tbu-key li {
  display: flex;
  align-items: center;
  gap: 6px;
  line-height: 1.7;
}
.wu-tbu-dot {
  width: 12px;
  height: 12px;
  flex: none;                 /* a width is only a preference to flexbox (#90) */
  border-radius: 3px;
}
/* The numbers, over the board rather than squeezed into the rail: they are meant
   to be selected and copied, and a 200px column cannot show a line of them. */
.wu-tbu-out {
  position: fixed;            /* fixed, not absolute: the drill layer is not a
                                 positioned ancestor, so `inset: 0` would fall
                                 through to the viewport anyway — saying so is
                                 what keeps it clear of the rail on purpose. */
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-md);
  padding-left: calc(var(--rail-w) + var(--space-md));
  background: color-mix(in srgb, var(--colour-text-primary) 45%, transparent);
  z-index: 5;
}
.wu-tbu-out[hidden] { display: none; }
.wu-tbu-card {
  width: min(680px, 100%);
  box-sizing: border-box;     /* no global box-sizing here (#90) */
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding: var(--space-md);
  border-radius: var(--radius-card);
  background: var(--colour-surface);
}
.wu-tbu-outhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  font-size: var(--font-size-sm);
}
.wu-tbu-outbtns { display: flex; gap: var(--space-xs); }
.wu-tbu-outhead .wu-tan-tool { flex: none; padding: 0 var(--space-sm); white-space: nowrap; }
.wu-tbu-text {
  width: 100%;
  box-sizing: border-box;
  padding: var(--space-xs);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-button);
  background: var(--colour-bg);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--colour-text-primary);
  resize: vertical;
}

/* Three to a row needs the smaller face: at the rail's 224px a 3-up row leaves
   each button ~59px, and "Back" at the body size leaves 2px of slack — which
   is not slack. The 44px tap floor is untouched: it is a min-HEIGHT, and 59px
   wide clears it too. */
.wu-tbu-tools3 .wu-tan-tool { font-size: var(--font-size-xs); }
.wu-tan-tool[aria-pressed="true"] {
  border-color: var(--colour-accent);
  background: color-mix(in srgb, var(--colour-accent) 14%, var(--colour-surface));
}
/* ✓ Save it is the one thing the child came here to do, so it is the only solid
   button on the rail. SOLID GREEN rather than a green tint: every accent used
   as text over its own 14% wash on this palette fails the 4.5:1 body floor
   (#121 measured it), while white on #2E7D32 is 5.3:1 — the same value and the
   same reasoning as the Study Timer's Start (#123 e). A disabled-looking save
   is never wrong here: it only dims while the figure is not yet a real one, and
   it still answers when tapped (#134). */
.wu-tan-tool.wu-tbu-keep {
  border-color: #2E7D32;
  background: #2E7D32;
  color: #FFFFFF;
}
.wu-tan-tool.wu-tbu-keep[aria-disabled="true"] { opacity: 0.4; }
.wu-tan-tool.wu-tbu-bin { flex: 0 0 auto; padding: 0 var(--space-sm); }

/* 👁 Solid: the seven pieces drawn as the ONE silhouette the child meets, so
   "does this read as a lion?" is answered before the figure is saved rather
   than after it ships. Both selectors are spelled out because
   `.wu-tan-piece.wu-tan-sel polygon` is the same specificity as the first of
   them and would otherwise draw the dissection back in on whichever piece
   happens to be selected — source order is not a thing to rest a correctness
   question on (#155 g). */
.wu-tbu-sil .wu-tan-piece polygon,
.wu-tbu-sil .wu-tan-piece.wu-tan-sel polygon {
  fill: color-mix(in srgb, var(--colour-text-primary) 70%, var(--colour-surface));
  stroke: color-mix(in srgb, var(--colour-text-primary) 70%, var(--colour-surface));
  stroke-width: 1.5px;
}

@media (orientation: landscape) and (max-height: 500px) {
  .wu-tan-tool { font-size: var(--font-size-xs); }
  .wu-tan-rules { display: none; }
  .wu-tbu-key { display: none; }
}


/* ── 🔍 Word search (#158) and 💠 Rings (#159) — shared board frame ─────────
   Both play on a honeycomb inside the game zone, like Harvest. The CSS size is
   only the starting point: the fitter (honeycomb.js) grows or shrinks the board
   to its box, and both pass tapFloor so a short screen fills the box rather
   than keeping a ghost ring (v417). */
.wu-ws-flower,
.wu-rng-flower {
  flex: 1;
  position: relative;
  width: 100%;
  min-height: 0;
  --hex-w: clamp(30px, calc((var(--vh) - 260px) / 8.6), 82px);
  --hex-h: calc(var(--hex-w) * 1.1547);
  --hex-gap: 4px;
}
.wu-rng-flower { --hex-w: clamp(44px, calc((var(--vh) - 260px) / 6.6), 108px); }
.wu-game-layer .wu-ws-flower,
.wu-game-layer .wu-rng-flower { touch-action: none; }
.wu-ws-flower .wu-hex-label,
.wu-rng-flower .wu-hex-label { display: none; }
@media (min-width: 900px), (orientation: landscape) and (min-width: 640px) {
  .wu-game-layer .wu-ws-flower  { --hex-w: clamp(30px, calc((var(--vh) - 140px) / 8.6), 94px); }
  .wu-game-layer .wu-rng-flower { --hex-w: clamp(44px, calc((var(--vh) - 140px) / 6.6), 124px); }
}

/* ── 🔍 Word search ──────────────────────────────────────────────────────── */
/* The letters ARE the board, so they take the display face at a size that
   reads from arm's length on a tablet lying flat. */
.wu-ws-flower .wu-hex-icon {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: calc(var(--hex-w) * 0.44);
  color: var(--colour-text-primary);
}
/* While a first letter is held, every cell a straight line reaches is shaded:
   "in a straight line" is the rule a hex board hides, so the board says it.
   Declared BEFORE found/selected so those win on a cell that is both. */
.wu-ws-flower .wu-ws-ray .wu-hex::after {
  background: color-mix(in srgb, var(--colour-accent) 16%, var(--colour-surface));
}
.wu-ws-flower .wu-ws-found .wu-hex::before { background: var(--tile-color); }
.wu-ws-flower .wu-ws-found .wu-hex::after {
  background: color-mix(in srgb, var(--tile-color) 62%, var(--colour-surface));
}
.wu-ws-flower .wu-ws-sel .wu-hex::before { background: var(--colour-accent); }
.wu-ws-flower .wu-ws-sel .wu-hex::after {
  background: color-mix(in srgb, var(--colour-accent) 34%, var(--colour-surface));
}
/* The stall hint: the brand ring and Stack's pulse — the same "look here" the
   app already uses, so it needs no explaining. */
.wu-ws-flower .wu-ws-hint .wu-hex::before { background: var(--colour-brand-deep); }
.wu-ws-flower .wu-ws-hint .wu-hex::after {
  background: color-mix(in srgb, var(--colour-brand) 38%, var(--colour-surface));
}
.wu-ws-flower .wu-ws-hint .wu-hex { animation: wu-stk-pulse 1100ms ease infinite; }
.wu-ws-flower .wu-ws-pop .wu-hex { animation: wu-pop-blast 260ms ease; z-index: 3; }
.wu-ws-flower .wu-ws-miss .wu-hex { animation: wu-srv-shake 400ms ease; }

.wu-ws-lang {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: var(--font-size-sm);
  color: var(--colour-text-secondary);
}
/* The language line is a BUTTON (batch item 4). A pill outline and the ⇄,
   because #82/v285 measured that a bare tappable label reads as a label; and
   44px, the tap floor, since it is the only way to change the pair from inside
   the game. It keeps the block's own left edge — a centred control in a rail
   of left-aligned text reads as something else again. */
.wu-ws-find {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  align-self: flex-start;
  max-width: 100%;
  min-height: 44px;
  margin: 2px 0;
  padding: 0 var(--space-sm);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-pill);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.wu-ws-find:hover { border-color: var(--colour-accent); }
.wu-ws-swap {
  flex: none;
  color: var(--colour-accent);
  font-weight: var(--font-weight-bold);
}
.wu-ws-theme {
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  color: var(--colour-text-primary);
}
.wu-ws-back { font-size: var(--font-size-xs); }

/* The list is in the ASKING language; a found word gains its answer in the
   colour it was found in, so the list and the board point at each other. */
.wu-ws-list {
  margin: 0;
  padding-left: 0;
  list-style: none;
  font-size: var(--font-size-sm);
  line-height: 1.35;
  color: var(--colour-text-primary);
}
.wu-ws-list li {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0 var(--space-xs);
}
.wu-ws-list li + li { margin-top: 3px; }
.wu-ws-dot {
  flex: none;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 1.5px solid var(--colour-border);
  box-sizing: border-box;
  transform: translateY(1px);
}
.wu-ws-done .wu-ws-dot { border-color: transparent; }
.wu-ws-done .wu-ws-clue { color: var(--colour-text-secondary); }
.wu-ws-answer { font-weight: 800; }

@media (orientation: landscape) and (max-height: 500px) {
  .wu-game-layer:has(.wu-ws-flower) .wu-game-rail { gap: var(--space-xs); }
  .wu-ws-lang,
  .wu-ws-list { font-size: var(--font-size-xs); }
  .wu-ws-list li + li { margin-top: 1px; }
}
@media (prefers-reduced-motion: reduce) {
  .wu-ws-flower .wu-ws-hint .wu-hex { animation: none; }
}

/* ── 💠 Rings ────────────────────────────────────────────────────────────── */
/* The rings are drawn as nested hex outlines in an SVG sized from the tile. */
.wu-rng-flower .wu-hex-icon {
  display: flex;
  align-items: center;
  justify-content: center;
}
.wu-rng-flower .wu-rng-svg {
  width: calc(var(--hex-w) * 0.98);
  height: calc(var(--hex-w) * 0.98);
  display: block;
}
.wu-rng-ring {
  fill: none;
  stroke-width: 8;
  stroke-linejoin: round;
}
/* An empty slot still shows, faintly, so the child can SEE which sizes are
   free — the one rule that decides where a piece can go. */
.wu-rng-empty {
  fill: none;
  /* Not --colour-border: on the pale tile that measured next to invisible at
     a 48px phone hex, which defeats the whole point of drawing it. */
  stroke: color-mix(in srgb, var(--colour-text-secondary) 45%, transparent);
  stroke-width: 3;
  stroke-dasharray: 4 5;
}
/* A full spot has no room for anything: say so with the surface. */
.wu-rng-flower .wu-rng-full .wu-hex::after {
  background: color-mix(in srgb, var(--colour-border) 55%, var(--colour-surface));
}
.wu-rng-flower .wu-rng-pop .wu-hex::before { background: var(--colour-brand-deep); }
.wu-rng-flower .wu-rng-pop .wu-hex { animation: wu-pop-blast 300ms ease; z-index: 3; }
.wu-rng-flower .wu-rng-no .wu-hex { animation: wu-srv-shake 400ms ease; }

.wu-rng-queue {
  display: flex;
  align-items: flex-end;
  gap: var(--space-md);
}
.wu-rng-slot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}
.wu-rng-lbl {
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--colour-text-secondary);
}
.wu-rng-piece { width: 64px; height: 64px; }
.wu-rng-piece-sm { width: 42px; height: 42px; opacity: 0.8; }
.wu-rng-piece .wu-rng-svg { width: 100%; height: 100%; display: block; }
.wu-rng-rules {
  margin: 0;
  padding-left: 0;
  list-style: none;
  font-size: var(--font-size-sm);
  line-height: 1.4;
  color: var(--colour-text-secondary);
}
.wu-rng-rules li + li { margin-top: var(--space-xs); }

/* Short landscape: the rules live on the how-to card, and the rail keeps what
   the child needs mid-move — the score, the piece and what just happened. */
@media (orientation: landscape) and (max-height: 500px) {
  .wu-game-layer:has(.wu-rng-flower) .wu-game-rail { gap: var(--space-xs); }
  .wu-rng-rules { display: none; }
  .wu-rng-piece { width: 54px; height: 54px; }
  .wu-rng-piece-sm { width: 38px; height: 38px; }
}
