/* Words — slice 8 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: Letter games, Balance, Spell it, study peek, times-table cover, verb table, Cross Words, reading library, reading sheet, audit fixes. */

/* ── Letter games (v267): 🔤 Guess the Letter · 🔡 Letter Sequence ────────
   Both answer from one A–Z strip — the input for the chain, and the
   "alphabet at the top" reference the sequence game asked for. */

.wu-alpha-stage {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-lg);
  width: 100%;
  /* Wide enough for 13 keys per row (13 × 56px), so the 26 letters land as TWO
     EVEN ROWS on a tablet/desktop instead of a ragged 11/11/4. */
  max-width: 760px;
}

/* Reference row — deliberately plain and unclickable; the strip below answers. */
.wu-alpha-ref {
  font-family: var(--font-mono);
  font-size: clamp(11px, 2.6vw, 15px);
  letter-spacing: 0.18em;
  color: var(--colour-text-secondary);
  text-align: center;
  word-spacing: 0.1em;
}

/* The question sits on a card, like the flashcard — it's the thing being read,
   so it should look like a thing rather than floating text. */
.wu-alpha-q {
  text-align: center;
  width: 100%;
  padding: var(--space-lg) var(--space-md);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-card);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.05);
}

.wu-alpha-hint {
  margin: 0 0 var(--space-lg);
  color: var(--colour-text-secondary);
  font-size: var(--font-size-sm);
}

/* The word chain: two words, one shared blank. Each word gets its own chip so
   the pair reads as TWO words with a junction between them, rather than one
   run-on string — and each stays whole when the row wraps on a phone. */
.wu-chain {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-md);
  font-family: var(--font-display);
  font-size: clamp(28px, 9vw, 52px);
  font-weight: var(--font-weight-bold);
  line-height: 1.15;
  color: var(--colour-text-primary);
}

.wu-chain-word {
  display: inline-flex;
  align-items: baseline;
  white-space: nowrap;
  padding: 0.12em 0.3em;
  border-radius: var(--radius-card);
  background: color-mix(in srgb, var(--colour-accent) 7%, var(--colour-surface));
}

.wu-chain-link {
  color: var(--colour-text-secondary);
  opacity: 0.45;
  font-size: 0.6em;
}

/* The blank is an empty SQUARE, not a "?" (owner 2026-08-01). A question mark
   asks something; an empty slot says "put a letter here", which is the actual
   instruction — and it stops the two chain gaps reading as punctuation inside
   the words. Same treatment in both games. */
.wu-chain-gap,
.wu-seq-gap {
  display: inline-block;
  width: 0.82em;
  height: 0.82em;
  vertical-align: -0.06em;
  text-align: center;
  line-height: 0.82em;
  color: var(--colour-accent);
  border: 0.07em dashed color-mix(in srgb, var(--colour-accent) 70%, transparent);
  border-radius: 0.14em;
  background: color-mix(in srgb, var(--colour-accent) 8%, transparent);
}

/* Filled in — solid and green, so the answer visibly lands in the slot. */
.wu-chain-filled {
  color: var(--colour-success);
  border-style: solid;
  border-color: var(--colour-success);
  background: color-mix(in srgb, var(--colour-success) 14%, transparent);
}

/* The sequence row. */
.wu-seq {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  font-family: var(--font-mono);
  font-size: clamp(24px, 8vw, 44px);
  font-weight: var(--font-weight-bold);
  color: var(--colour-text-primary);
}

.wu-seq-cell {
  min-width: 1.1em;
  text-align: center;
}

/* A–Z answer keys. Wraps to as many rows as the width needs; ≥40px tall so it
   stays a real tap target on a phone (accessibility floor is 44px, and the
   26-key strip is the one place we sit just under it by necessity). */
/* Width is capped so the 26 keys land on EVEN rows instead of wrapping ragged:
   ~13 per row on desktop (exactly 2 rows), ~7 on a phone. Flex rather than a
   grid so the short final row centres itself. */
.wu-alpha-strip {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
  width: 100%;
  max-width: 740px;
  margin: 0 auto;
}

@media (max-width: 720px) {
  .wu-alpha-strip { max-width: 400px; }
}

.wu-alpha-key {
  min-width: 48px;
  min-height: 48px;
  padding: 0 6px;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-button);
  background: var(--colour-surface);
  font-family: var(--font-display);
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-bold);
  color: var(--colour-text-primary);
  cursor: pointer;
  transition: background var(--transition-fast), border-color var(--transition-fast),
              transform var(--transition-fast);
}

.wu-alpha-key:active:not(:disabled) {
  transform: translateY(1px);
}

.wu-alpha-key:disabled {
  opacity: 0.4;
  cursor: default;
}

.wu-alpha-key.wu-alpha-on {
  background: var(--colour-success);
  border-color: var(--colour-success);
  color: #fff;
}

/* Wrong key shakes rather than scolding — same treatment as a wrong Path tap. */
.wu-alpha-key.wu-alpha-wrong {
  animation: wu-alpha-shake 340ms ease;
  border-color: var(--colour-danger);
}

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

@media (prefers-reduced-motion: reduce) {
  .wu-alpha-key.wu-alpha-wrong { animation: none; }
}

/* ── ⚖️ Balance the equation (v270) ──────────────────────────────────────
   Two expressions either side of an equals, one operand blanked. The blank
   reuses the letter games' empty-square idea so "fill this in" looks the same
   everywhere in the app. */
.wu-balance {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.18em;
  font-family: var(--font-mono);
  font-size: clamp(26px, 8vw, 52px);
  font-weight: var(--font-weight-bold);
  color: var(--colour-text-primary);
  line-height: 1.2;
}

.wu-bal-n {
  min-width: 1.1em;
  text-align: center;
}

.wu-bal-op {
  color: var(--colour-text-secondary);
  padding: 0 0.1em;
}

/* The equals is the pivot the whole exercise turns on — give it weight. */
.wu-bal-eq {
  color: var(--colour-accent);
  padding: 0 0.28em;
}

.wu-bal-gap {
  display: inline-block;
  width: 1.05em;
  height: 0.9em;
  vertical-align: -0.06em;
  border: 0.07em dashed color-mix(in srgb, var(--colour-accent) 70%, transparent);
  border-radius: 0.12em;
  background: color-mix(in srgb, var(--colour-accent) 8%, transparent);
}

/* ── ✍️ Spell it (v276) ─────────────────────────────────────────────────
   The production drill. Reuses .wu-alpha-strip/.wu-alpha-key wholesale, so
   only the prompt card and the answer slots are new. Slots follow the letter
   games' convention the owner set: an empty SQUARE, never a "?" — a question
   mark asks something, an empty slot says "put a letter here". */
.wu-spell-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-lg);
  flex: 1;
  width: 100%;
  padding: var(--space-md);
}

/* ⚠️ TWO COLUMNS ON A SHORT SCREEN (owner, on a device: "vocab spell it hides
   the input on landscape. it should show two columns"). Stacked, the card and a
   26-key strip need ~470px of height and a sideways phone has ~340 once the
   header is out — so the strip, which is the only way to answer, went off the
   bottom. Height is the scarce axis there and width the plentiful one, which is
   exactly the trade a side column makes: the same call the crossword keys (#85),
   the games rail (#88) and the numpad drill (#172) each arrived at.
   6 across rather than the strip's own wrap: 26 keys land on 5 even rows of
   ~244px, where 5 across is 6 rows and does not fit. Keys go 48 → 44px, which
   is AT the tap floor rather than under it — the strip gives up its slack, never
   the target. */
@media (orientation: landscape) and (max-height: 500px) {
  .wu-spell-stage {
    flex-direction: row;
    align-items: center;
    gap: var(--space-lg);
    padding: var(--space-sm) var(--space-md);
  }
  .wu-spell-card {
    flex: 1 1 auto;
    /* A grid/flex item's floor is its content, and a long word's slot row would
       otherwise push the keys off the screen rather than wrapping (#170). */
    min-width: 0;
    width: auto;
    max-width: 460px;
    padding: var(--space-md);
  }
  .wu-spell-keys { flex: none; }
  /* Fixed tracks, not 1fr: 1fr divides the container and the key's own
     min-width then overflows the track it was given (#85's note). */
  .wu-spell-keys .wu-alpha-strip {
    display: grid;
    grid-template-columns: repeat(6, 44px);
    justify-content: center;
    gap: 6px;
    max-width: none;
  }
  .wu-spell-keys .wu-alpha-key {
    min-width: 44px;
    min-height: 44px;
    font-size: var(--font-size-md);
  }
}

.wu-spell-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xs);
  width: min(560px, 100%);
  padding: var(--space-lg) var(--space-md);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-card);
  text-align: center;
}

.wu-spell-lang,
.wu-spell-ask {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--font-size-sm);
  color: var(--colour-text-secondary);
}

.wu-spell-ask { margin-top: var(--space-sm); }

/* 🔊 Hear the word being spelled (owner, 2026-08-29). Sits under the
   "Spell it in <language>" line, because that line is what names the language
   the sound will be in — the button and its label say the same thing.
   44px min per the tap-target floor (#88–#90); the disc stays visually small
   so it does not compete with the slots, which are the thing to look at. */
.wu-spell-say {
  margin-top: var(--space-xs);
  min-width: 44px;
  min-height: 44px;
  border: 2px solid var(--colour-border);
  border-radius: 999px;
  background: var(--colour-surface);
  font-size: var(--font-size-md);
  line-height: 1;
  cursor: pointer;
}

.wu-spell-say:active { opacity: 0.6; }

/* The word to translate — the biggest thing on screen, since it's the whole
   question. */
.wu-spell-prompt {
  font-family: var(--font-display);
  font-size: clamp(30px, 6vw, 46px);
  font-weight: var(--font-weight-bold);
  color: var(--colour-text-primary);
  line-height: 1.1;
}

.wu-spell-slots {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 6px;
  margin-top: var(--space-xs);
  font-family: var(--font-display);
  font-size: clamp(24px, 5vw, 36px);
  font-weight: var(--font-weight-bold);
}

.wu-spell-slot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 0.78em;
  height: 1em;
  padding: 0 0.06em;
  border: 0.07em dashed color-mix(in srgb, var(--colour-accent) 55%, transparent);
  border-radius: 0.14em;
  background: color-mix(in srgb, var(--colour-accent) 6%, transparent);
  color: var(--colour-text-primary);
}

/* Where the next letter lands. Without this the child has no idea which of
   eight identical squares they're filling. */
.wu-spell-active {
  border-style: solid;
  border-color: var(--colour-accent);
  background: color-mix(in srgb, var(--colour-accent) 16%, transparent);
}

.wu-spell-filled {
  border-color: transparent;
  background: color-mix(in srgb, var(--colour-success) 18%, transparent);
  color: var(--colour-success);
}

/* Spaces and hyphens are pre-filled — the child spells letters, not
   punctuation — so they render as plain text with no slot around them. */
.wu-spell-sep {
  display: inline-block;
  min-width: 0.3em;
  color: var(--colour-text-secondary);
}

.wu-spell-win .wu-spell-filled {
  animation: wu-spell-pop 0.35s ease;
}

@keyframes wu-spell-pop {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.16); }
  100% { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .wu-spell-win .wu-spell-filled { animation: none; }
}

/* ── Matching: 👀 study peek (v276) ─────────────────────────────────────
   The pairs shown ALIGNED before the target column shuffles — this is where
   Flashcard's study half went. Tinted so it reads as a different phase from
   the round proper, and the columns are joined by a faint rule so the pairing
   is legible at a glance rather than needing to be traced. */
.wu-match-study .wu-match-btn {
  cursor: default;
  border-color: color-mix(in srgb, var(--colour-accent) 40%, var(--colour-border));
  background: color-mix(in srgb, var(--colour-accent) 7%, var(--colour-surface));
}

.wu-match-ready {
  margin: var(--space-md) auto 0;
  display: block;
  min-width: 180px;
}

/* ── Balance: letter variables (v276, hard tier) ────────────────────────
   A letter in the equation must not look like a number, or the child reads
   "A" as an operand and computes nothing. Coloured and italic, matching the
   key above. */
.wu-bal-var {
  color: var(--colour-accent);
  font-style: italic;
}

/* The key sits above the equation because it has to be read first. */
.wu-bal-key {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-sm);
}

.wu-bal-keyitem {
  padding: 4px 14px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--colour-accent) 10%, var(--colour-surface));
  border: 1px solid color-mix(in srgb, var(--colour-accent) 40%, transparent);
  font-family: var(--font-mono);
  font-size: var(--font-size-md);
  color: var(--colour-text-primary);
}

.wu-bal-keyitem b {
  color: var(--colour-accent);
  font-style: italic;
}

/* Language chip flags (v279) — the flag emoji is the content, so it needs to
   carry at the HUD's small size rather than sit at label weight. */
.wu-lang-flag {
  font-size: 1.25em;
  line-height: 1;
  vertical-align: -0.08em;
}

/* ✓ confirms the language sheet (v279, owner: "replace the X on the top right
   with a Check symbol"). It replaced ✕ because the sheet no longer applies on
   selection — there IS something to confirm now, so the corner control is a
   commit, not a dismissal. Green to say "done", not "close". */
.wu-sheet-confirm {
  color: var(--colour-success);
  font-size: var(--font-size-lg);
  font-weight: 800;
}

/* ── 🔢 Times-table cover (v281) ────────────────────────────────────────
   A heat STRIP, not a list: the question the owner asked is about spread
   ("wide range cover of the numbers, not a repeat of always the same one"),
   and spread is something you see rather than count. The fill height carries
   the count so an untouched table is obvious at a glance. */
.wu-tbl-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: var(--space-sm) 0;
}

.wu-tbl-chip {
  position: relative;
  flex: 1 1 52px;
  min-width: 46px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  padding: 6px 2px;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-button);
  overflow: hidden;
  background: var(--colour-surface);
}

/* The fill rises from the bottom in proportion to the count. */
.wu-tbl-chip::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--fill, 0%);
  background: color-mix(in srgb, var(--colour-success) 42%, transparent);
  pointer-events: none;
}

.wu-tbl-chip b,
.wu-tbl-chip small {
  position: relative;
  z-index: 1;
}

.wu-tbl-chip b {
  font-family: var(--font-mono);
  font-size: var(--font-size-md);
  color: var(--colour-text-primary);
}

.wu-tbl-chip small {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--colour-text-secondary);
}

/* Offered at this level but never practised — amber outline so a genuine gap
   stands out from a table the level simply doesn't teach yet. */
.wu-tbl-none {
  border-color: color-mix(in srgb, var(--colour-warning) 60%, transparent);
  background: color-mix(in srgb, var(--colour-warning) 8%, var(--colour-surface));
}

/* Not in the child's current difficulty. Shown, not hidden: "not offered yet"
   is different information from "never done", and hiding these would make an
   easy-level child look like full coverage. */
.wu-tbl-off {
  opacity: 0.38;
}

/* ── Verb table: the heading IS the language toggle (v283) ─────────────
   No separate control (owner: "don't make another element… keep clean UI").
   The heading already reads "ser · to be", i.e. it already names both
   languages — so it carries a faint button affordance and a ⇄ hint, and a tap
   swaps which side the paradigm is written in. Local to this screen: a study
   flip must not silently re-configure every other vocab activity.

   v285: the faint outline + small ⇄ read as decoration, not as a control
   (owner: "the swap arrows are too subtle… maybe a pill outline to both verbs
   with a bolder arrow"). Both sides now sit in their own pill with a solid
   arrow between them, which also SHOWS the direction: the filled pill is the
   language the paradigm is written in, the outlined one is the gloss — so the
   control states the current setting instead of only hinting a tap. */
.wu-vt-inf-tap {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  border: none;
  background: transparent;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

/* Type is declared HERE, not inherited: `.wu-vt-inf-tap` carries `font: inherit`
   to reset the button, and being the later rule that beat `.wu-vt-inf`'s display
   type — so the heading had quietly been rendering at plain body 16px, part of
   why it read as a label rather than a control. */
.wu-vt-pill {
  border: 2px solid var(--colour-accent);
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--colour-accent) 12%, var(--colour-surface));
  color: var(--colour-text-primary);
  font-family: var(--font-display);
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-bold);
  padding: 4px 16px;
  white-space: nowrap;
  transition: background var(--transition-fast), border-color var(--transition-fast);
}

/* The gloss side: same shape and SAME SIZE — a smaller font here made the two
   pills different heights, which reads as a mistake rather than a hierarchy.
   Weight and colour carry the difference instead. */
.wu-vt-pill-alt {
  border-color: var(--colour-border);
  background: var(--colour-surface);
  color: var(--colour-text-secondary);
  font-weight: normal;
}

.wu-vt-swap {
  color: var(--colour-accent);
  font-weight: var(--font-weight-bold);
  font-size: var(--font-size-2xl);
  line-height: 1;
  transition: transform var(--transition-fast);
}

.wu-vt-inf-tap:hover .wu-vt-pill,
.wu-vt-inf-tap:active .wu-vt-pill {
  background: color-mix(in srgb, var(--colour-accent) 22%, var(--colour-surface));
}

.wu-vt-inf-tap:hover .wu-vt-pill-alt,
.wu-vt-inf-tap:active .wu-vt-pill-alt {
  background: var(--colour-bg);
}

.wu-vt-inf-tap:active .wu-vt-swap {
  transform: scale(1.25);
}

@media (prefers-reduced-motion: reduce) {
  .wu-vt-swap { transition: none; }
  .wu-vt-inf-tap:active .wu-vt-swap { transform: none; }
}

/* 🔊 on the Path prompt (v283, owner). Reads the PROMPT, not the answer —
   speaking the target would just read out the tiles the child is hunting. */
.wu-path-say {
  flex: none;
  border: none;
  background: transparent;
  padding: 0 6px;
  font-size: var(--font-size-md);
  line-height: 1;
  cursor: pointer;
  /* 32 → 44px (v291 audit). 🔊 reads the prompt aloud — for a child who needs
     it, it is the most important control on the bar. */
  min-width: 44px;
  min-height: 44px;
}

.wu-path-say:active { opacity: 0.6; }

/* SHORT LANDSCAPE — compress the rail instead of scrolling it (owner, v289:
   "scrolling is not great, may need to make a call on compromise of information
   visible on mobile"). Measured at 851×393 the rail needed 398px in 336px, and
   the whole 62px excess sits in ONE card: the piece card stacks PLACE above
   NEXT at 149px. Laid side by side it is ~70px, which fits the column with room
   to spare — so nothing has to be hidden. That is the better call than dropping
   the legend, which is the only thing on screen that says which times table
   each colour is drilling, i.e. the game's actual subject.
   `overflow-y: auto` stays on the rail as a safety net: it shows a scrollbar
   only if a future card overflows again, which beats spilling over the board. */
@media (orientation: landscape) and (max-height: 560px) {
  .wu-game-rail .wu-stk-bar {
    flex-direction: row;
    align-items: center;
    flex-wrap: wrap;
    column-gap: var(--space-sm);
  }
  .wu-game-rail .wu-stk-lbl-next {
    margin-top: 0;
    margin-left: var(--space-sm);
  }
  /* …and the rail moves to the RIGHT, which is what actually buys the room.
     Its 86px top padding exists solely to clear the HUD — a quarter of the
     column on a 393px screen — and the HUD only occupies the LEFT. The v239
     reason for a left rail was that the Android status pill hid the ✕ pinned
     inside it; that ✕ moved into the HUD's game pill in v243, so the constraint
     is gone. On the right the rail needs no clearance at all, which is the 86px
     that lets everything fit with nothing hidden. Only in short landscape —
     tall screens keep the left rail, where the padding costs nothing. */
  .wu-game-layer:has(.wu-game-rail) {
    flex-direction: row-reverse;
  }
  .wu-game-rail {
    gap: var(--space-xs);
    /* On the right it needs no HUD clearance — but it does need the SYSTEM
       band. v242 dropped the reserve entirely on the reasoning that the ✕ had
       moved to the HUD; true for the ✕, not for the score card, which then sat
       under the Android pill. Same protected zone as the crossword keys, one
       token (owner, 2026-08-17).
       ⚠️ The BAND, not --support-top. That token bundles the band with
       --space-md, which is right for chrome floating in a pane and wrong here:
       the layer has already inset the rail, so the gutter was counted twice and
       the first card sat 40px from the top against a 24px gap on the right
       (owner, 2026-09-23: "not consistent padding from the top and right").
       Dropping the double count makes the two equal at every size and still
       clears a cluster-drawing shell, whose pill ends at 60px. */
    padding-top: calc(var(--safe-top) + var(--system-band));
  }
  /* The mirror of the games.css rule — here the rail is on the RIGHT, so the
     outer side it re-takes is that one. */
  .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) var(--rail-regain-x) var(--rail-regain-y) 0;
  }
  .wu-game-rail .wu-pop-status {
    font-size: 11px;
    line-height: 1.25;
  }
}

/* ── ✳️ Cross Words (v286, decision #85) ─────────────────────────────────
   A sparse criss-cross on the hex board. Three states carry the whole puzzle:
   cells not in any word are INERT comb, the active word is highlighted, and a
   solved word locks green. The letter strip sits under the board in the pane
   (not in the drill layer) so the board stays inside the hex fitter's box. */
/* Pane puzzles reserve room for their own clue bar (v287). Path's bar floats
   over the top of its board — measured as a real overlap at 1024×768 and
   851×393, where it sat across the top row of stones. The board is sized from
   the wrap minus padding, so reserving here shrinks it to fit instead.
   Excluded for Cross Words, which moves its clue into the right-hand column on
   a tall screen and reserves its own space (below). Note the :has() selector
   outranks `.wu-xw-open .wu-honeycomb-wrap`, hence the explicit :not(). */
.wu-honeycomb-pane:has(.wu-path-bar):not(.wu-xw-open) .wu-honeycomb-wrap {
  /* Derived from the bar, not guessed: 16px top offset + a 44px control + 8px
     of breathing room. It was 58px, sized against a 42px bar — then growing the
     ✕ to the 44px tap minimum took the bar to 54px and it clipped the board's
     top row again by 11px. Tying the number to the control's own minimum means
     the next change to the bar's contents cannot silently re-open this. Two
     --space-md, not one plus a --space-sm: the bar is positioned from the PANE
     and this padding from the WRAP, and the pane's own inset is the difference
     (it landed exactly 1px short otherwise). */
  padding-top: calc(var(--space-md) * 2 + 44px);
}

/* …and the bar itself sits at the TOP EDGE. Both clue bars always mount in a
   `.wu-honeycomb-pane`, so this is the only `top` that ever applies to them.
   (A max-width:1100px rule that dropped the bar below the old HUD pill stack
   lost to this on specificity every time; deleted v418.) */
.wu-honeycomb-pane:has(.wu-path-bar) .wu-path-bar {
  top: calc(var(--space-md) + var(--safe-top));
}

/* The key strip RESERVES its space in the flower's own container rather than
   floating over the board: createHoneycomb sizes the hex from
   stage.parentElement minus its padding, so padding the wrap is what makes the
   board shrink to fit instead of colliding with the keys.

   The layout splits on HEIGHT, not orientation (owner, v286: "the input is
   above the grid… maybe one to recommend doing vertically"). A 26-key block is
   ~330px as a column and ~105px as a two-row strip, so:
     • TALL (≥600px) — column down the right, clue above it. "What to type"
       directly above "where to type", and the board owns the whole left side.
     • SHORT — strip along the bottom. On a 393px-tall phone in landscape the
       column plus the clue is taller than the screen; the strip costs the board
       a third of the height the column would. */
.wu-xw-keys {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(var(--space-md) + var(--safe-bottom));
  padding: 0 var(--space-md);
  z-index: 5;
}

/* Derived from the key block's real height (vocab.js writes --xw-keys-h), plus
   the gap it is offset by. The 190px it replaces was ~200px short in phone
   portrait, where the strip wraps to six rows — the board simply rendered
   underneath the letters. The fallback keeps this sane for one frame before the
   first measurement lands. */
.wu-xw-open .wu-honeycomb-wrap {
  padding-bottom: calc(var(--xw-keys-h, 190px) + var(--space-md) * 2 + var(--safe-bottom));
  /* …and the same reserve Path keeps for ITS clue bar. In the strip layout the
     clue sits at the top edge, exactly like Path's, so it needs exactly Path's
     clearance — without it the bar drew across the board's top row (measured: 2
     tiles underneath at 1024×768). Derived from the control's own 44px minimum,
     not a measured constant, for the reasons in #89/#90. The column layout puts
     the clue UNDER the board instead and takes this back. */
  padding-top: calc(var(--space-md) * 2 + 44px);
}

/* (A max-width:699px rule used to anchor the Cross Words clue to the right
   edge, first to dodge the HUD pill stack and later with a max-width that
   subtracted --rail-w inside a pane that already sits right of the rail.
   Deleted v418: once `.wu-path-bar` centred with both insets, that cap was
   what cut long clues off, measured at 375×812 "the strawberries" 82px
   of text in a 59px box with it, fitting without it.) */

/* Short landscape, too narrow even for the key column (under 560px wide). The
   strip is the only option left and it is a poor one — the board pays heavily —
   but nothing may OVERLAP. The hardcoded `padding-bottom: 124px` that used to
   live here was the bug: the strip stands ~216px on a screen this shape, so the
   board simply rendered underneath the letters (measured at 667×375: all 24
   cells covered). It is gone; the derived --xw-keys-h reserve in the base rule
   is correct by construction and applies here too. */
@media (orientation: landscape) and (max-height: 599px) and (max-width: 559px) {
  .wu-xw-open .wu-honeycomb-wrap {
    padding-left: 76px;  /* clears the collapsed ★ only (HUD lite, v287) */
  }
}

/* The side column is gated on WIDTH ALONE (v292). It was `min-height: 600px`
   too, so a phone in landscape fell back to the bottom strip — which costs
   124px of the scarce axis and squeezed the board to 35px hexes, i.e. 9.6px
   letters. This is the same finding as Stack's rail: in landscape, height is
   what you have least of and width is what you have most of. A short screen
   just needs the key block laid wider and flatter (below) so the clue still
   fits beneath it. */
/* ⚠️ The side column is now the EXCEPTION, not the wide-screen default (owner,
   2026-08-28: "the spell it text input layout is so much cleaner than what we
   have on the crosswords — can it be adopted?").

   It can, nearly everywhere. ✏️ Spell it's band IS the base `.wu-alpha-strip`
   style — a wide flat A-Z, ~13 per row, two rows, centred. Cross Words only ever
   diverged because this media query swapped it for a column at ≥700px wide. The
   two exercises answer from the same strip by design (#85), so they should look
   the same wherever the geometry allows.

   The gate is therefore HEIGHT, not width: the strip costs vertical space, and
   only a SHORT screen can't pay. Measured at 851×393 the strip takes the board
   from 45px hexes to 27px even laid out wide and flat — in landscape height is
   what you have least of, which is the same finding as Stack's rail (#88). On a
   tall screen there is room to spare and the strip wins on consistency.

   (This is not a revert of v292, which read `min-height: 600px` and sent phone
   landscape to the NARROW strip — 6 rows deep, the worst of both. The column
   survives exactly where v292 needed it, and nowhere else.) */
@media (min-width: 560px) and (max-height: 560px) {
  /* The key column's width is COMPUTED from the keys it has to hold, not typed
     in as a round number. It was 252px against five 48px keys and four 8px gaps
     — which need 272 — so every row overflowed its container by 10px and the
     lone Z on the last row poked out far enough to sit on the clue pill
     (measured at 768×1024). A column narrower than its own contents is a bug
     waiting on whichever row happens to reach the edge.

     Six across at 44px (still the tap floor, #88) rather than five at 48: five
     rows instead of six, which is what buys the top clearance on the only screen
     shape that gets here. */
  .wu-xw-open {
    --xw-key: 44px;
    --xw-gap: 8px;
    --xw-cols: 6;
    --xw-col-w: calc(var(--xw-cols) * var(--xw-key) + (var(--xw-cols) - 1) * var(--xw-gap));
    --xw-gutter: calc(var(--xw-col-w) + var(--space-md) + 4px);
  }
  .wu-xw-open .wu-honeycomb-wrap {
    padding-right: var(--xw-gutter);
    padding-left: 76px;  /* clears the collapsed ★ only (HUD lite, v287) */
    /* The clue is at the BOTTOM here, so the strip layout's top reserve is pure
       waste — and on a 393px-tall screen 76px of waste is most of a board row. */
    padding-top: var(--space-md);
    /* Reserve for the clue, which sits under the board at every side-column
       size now (below). Derived, not measured: two gaps + the gesture-bar inset
       + the 44px control minimum — the third time a hardcoded reserve here broke
       when a control's minimum changed (#89, #90). */
    padding-bottom: calc(var(--space-md) * 2 + var(--safe-bottom) + 44px);
  }
  /* The clue sits at the BOTTOM of the right column, not the top (v291, owner:
     "the crossword pad location and hint is hitting the android pill with the
     network and battery element"). CLAUDE.md §6 is explicit that the top band
     belongs to the Android system overlay and that the HUD side is the safe
     anchor — putting the clue top-right walked straight into it, and the
     browser cannot reproduce the overlay, which is why it shipped. The keys
     take the top of the column and the clue reads directly above the board's
     baseline, which is also where the eye already is. */
  /* Selector matches the v287 Path rule's specificity on purpose: that rule is
     `.wu-honeycomb-pane:has(.wu-path-bar) .wu-path-bar { top: … }`, and a plain
     `.wu-xw-bar` lost to it — leaving top AND bottom set, which stretched the
     bar into a 658px column down the whole right edge. */
  .wu-honeycomb-pane:has(.wu-path-bar) .wu-xw-bar {
    box-sizing: border-box;
    left: 76px;                 /* clears the collapsed rail, like the wrap */
    right: var(--xw-gutter);    /* stops short of the key column */
    top: auto;
    bottom: var(--support-bottom);
    transform: none;
    width: auto;
    max-width: none;
    justify-content: center;
  }
  /* The keys are ANCHORED TO THE BOTTOM of the column, not the top (owner,
     2026-08-28: "the text input is overlapping with the android pill… the
     letter pad on the right hand side does not work").

     Both halves of that report are one bug. v313 reserved --system-band (34px)
     to push a top-anchored block clear of the Android status overlay, but that
     is a GUESSED constant standing in for something the web platform will not
     tell us: env(safe-area-inset-top) reports the display CUTOUT, not the
     overlay, so any device whose status bar is taller than 34px puts the top
     row of keys underneath it — where they are both invisible AND inert,
     because the system takes those touches. Tapping A-E does nothing, and the
     pad "does not work".

     Growing the constant would only move the guess. Anchoring to the bottom
     removes it: the block grows UPWARD from an edge whose inset the platform
     does report honestly (--safe-bottom is a physical gesture bar, not an
     overlay drawn on top), so it cannot reach the top band at any key count, on
     any device, whatever the pill's height. Same principle as "the ✕ belongs on
     the HUD side" (#88) — pick the corner the system never occupies. It also
     survives immersive mode (#101) either way: with the bar hidden the reserve
     simply stops mattering, rather than becoming wrong in the other direction.

     Bonus, and not a small one on a tablet: the keys are now the closest thing
     on screen to where a child's thumbs already are. */
  .wu-xw-keys {
    left: auto;
    right: var(--space-md);
    top: auto;
    bottom: var(--support-bottom);
    transform: none;
    width: var(--xw-col-w);
    padding: 0;
  }
  /* Fixed-width columns, not 1fr: 1fr divides the container and a key's own
     min-width then overflows the track it was given. Sizing the track from the
     key means the two can never disagree. */
  .wu-xw-keys .wu-alpha-strip {
    display: grid;
    grid-template-columns: repeat(var(--xw-cols), var(--xw-key));
    justify-content: center;
    gap: var(--xw-gap);
    max-width: none;
  }
  .wu-xw-keys .wu-alpha-key {
    min-width: var(--xw-key);
    min-height: var(--xw-key);
  }
}

/* Short + wide: 6 keys across instead of 5, so the block is ~275px rather than
   328px and the clue clears it at the bottom of the same column. */
/* Short + wide: 6 keys across instead of 5, so the block is ~275px tall rather
   than 328px. Only the WIDTHS change now — the clue already sits under the board
   and both blocks are already bottom-anchored in the rule above, which is what
   collapsed this branch from five rules to three. */


/* NOTE: the ✓ badge is stripped from the DOM by crossword.js, not hidden here —
   honeycomb.css loads after this file, so `.wu-confirmed .wu-hex-check` wins any
   equal-specificity rule written at this end. */

/* Letters ride the ICON slot — display type, not caption type. A lone letter
   in the small label slot read as a footnote in the first build. */
.wu-xw-board .wu-hex-icon {
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
}

.wu-xw-next {
  flex: none;
  border: none;
  background: transparent;
  color: var(--colour-accent);
  font-size: var(--font-size-lg);
  line-height: 1;
  cursor: pointer;
  /* 24 → 44px (v290 audit). It is how the child reaches the other clue, so it
     is a primary control despite looking like a chevron. */
  min-width: 44px;
  min-height: 44px;
  padding: 0;
}
.wu-xw-next:active { transform: scale(1.2); }

@media (prefers-reduced-motion: reduce) {
  .wu-hex-wrap.wu-xw-cursor .wu-hex { animation: none; }
  .wu-hex-wrap.wu-xw-just .wu-hex { animation: none; }
}

/* ── 📚 Reading library (decision #107) ─────────────────────────────────────
   Covers are GENERATED SVG, never photographs: a 150 KB JPEG would eat over
   half the sync blob's remaining headroom before the host WAF starts rejecting
   pushes, and a camera is the biggest change this app could make to its privacy
   posture. See src/data/books.js. */
.wu-lib { display: flex; flex-direction: column; gap: var(--space-lg); }
.wu-lib-shelf { display: flex; flex-direction: column; gap: var(--space-sm); }

/* GRID, not a row (owner, 2026-08-17). A horizontal strip was right when a shelf
   held two books; it is wrong the moment it holds twelve, because the ones that
   matter most — the finished pile the child is proud of — scroll off the edge
   and are never seen. A grid shows the whole shelf at once, which is the entire
   point of having one. auto-fill keeps the card size fixed and lets the COLUMN
   COUNT respond, so a phone gets 3 and a laptop 8 without a breakpoint. */
.wu-lib-row {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(108px, 1fr));
  gap: var(--space-md);
  padding-bottom: var(--space-xs);
  justify-items: center;
}
.wu-lib-card {
  width: 108px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  font: inherit;
}
/* The cover and its ×N badge — the badge is positioned against THIS, not the
   card, so it rides the cover's hover lift instead of floating free of it. */
.wu-lib-cover {
  position: relative;
  display: block;
  line-height: 0;
}
/* Read count, from the second read only. Deliberately looks like a sticker on
   the book rather than a data field: it is the shelf's only status symbol and
   it is EARNED (see readCount in books.js — there is no favourite flag). */
.wu-lib-times {
  position: absolute;
  right: -6px;
  top: -6px;
  min-width: 26px;
  height: 26px;
  padding: 0 5px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  background: var(--colour-brand-deep);
  color: #fff;
  border: 2px solid var(--colour-surface);
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: var(--font-weight-bold);
  line-height: 1;
}
/* The library sheet is CHILD-accessible, and every `.wu-set-toggle` in it
   measured 36px — under the Phase 6 floor (#88-#90). Scoped rather than raising
   the class globally: its other home is the parent settings panels, where a
   change would reflow a lot of grown-up UI for no reported problem. That
   global 36px stands as a finding, not a fix. */
.wu-lib-sheet .wu-set-toggle {
  min-height: 44px;
}
.wu-lib-count {
  margin: 0 0 var(--space-sm);
  color: var(--colour-brand-deep);
  font-size: var(--font-size-sm);
}
.wu-lib-card .wu-bookcover {
  width: 108px;
  height: auto;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.14);
  transition: transform var(--transition-fast);
}
.wu-lib-card:hover .wu-bookcover { transform: translateY(-3px); }
.wu-lib-meta {
  font-size: var(--font-size-xs);
  color: var(--colour-text-secondary);
  text-align: center;
  line-height: 1.25;
}
.wu-lib-add {
  justify-content: center;
  min-height: 154px;
}
.wu-lib-add .wu-lib-plus {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 108px;
  height: 154px;
  border: 2px dashed var(--colour-border);
  border-radius: 8px;
  font-size: 34px;
  color: var(--colour-text-secondary);
}
.wu-lib-add:hover .wu-lib-plus { border-color: var(--colour-accent); color: var(--colour-accent); }

/* Sheet — wider than the standard one: the cover-maker is a preview beside its
   controls, and stacking them would put the dials off-screen on a phone. */
/* ⚠️ 642, not 560, BECAUSE THE BASE SHEET IS NOW BORDER-BOX. This rule always
   meant 560px of CONTENT — the preview beside its controls — and content-box
   delivered it by letting the 40px padding either side sit outside the number.
   Under border-box the same 560 would have given 478 of content, and the motif
   grid below has a hard 494px floor at its 10-column desktop layout (10 x 44px
   swatches plus gaps), so the pickers overflowed the sheet by 15px each side.
   642 = 560 + the padding and border that used to be free. */
.wu-lib-sheet { max-width: 642px; width: min(92vw, 642px); }
.wu-lib-detail { display: flex; justify-content: center; margin-bottom: var(--space-sm); }
.wu-lib-detail .wu-bookcover { width: 132px; height: auto; border-radius: 8px; }
.wu-lib-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  justify-content: center;
  margin: var(--space-md) 0 var(--space-sm);
}
.wu-lib-edit { width: 100%; }
/* Logging is the DAILY act on a book detail and the status buttons are the
   occasional one, so this is a filled primary button rather than a fourth
   toggle. Full width and above 🎨 for the same reason. */
.wu-lib-read {
  width: 100%;
  margin-bottom: var(--space-sm);
  min-height: 44px;
}

/* Cover beside the fields that make it; pickers full-width beneath. */
.wu-lib-maker {
  display: flex; gap: var(--space-md); align-items: flex-start; text-align: left;
  margin-bottom: var(--space-md);
}
.wu-lib-preview { flex: 0 0 132px; }
.wu-lib-preview .wu-bookcover {
  width: 132px; height: auto; border-radius: 8px;
  box-shadow: 0 3px 12px rgba(0, 0, 0, 0.16);
}
.wu-lib-fields {
  flex: 1; min-width: 0; display: flex; flex-direction: column;
  gap: var(--space-sm); align-self: stretch; justify-content: center;
}
.wu-lib-pickers { text-align: left; }

/* GRIDS, not wrapped flex rows. auto-fit with 1fr stretches the last row's
   items to fill the width, so a partial row never leaves a ragged gap — the
   thing the owner was seeing with 30 emoji in a flex wrap. The counts are
   chosen to divide evenly at the common column counts (30 motifs: 10/6/5;
   8 patterns: 4/8). */
.wu-lib-swatches {
  display: grid; gap: 6px;
  grid-template-columns: repeat(auto-fit, minmax(44px, 1fr));
}
.wu-lib-motifs {
  display: grid; gap: 6px;
  grid-template-columns: repeat(10, 1fr);
}
.wu-lib-patterns {
  display: grid; gap: 6px;
  grid-template-columns: repeat(4, 1fr);
}
@media (max-width: 560px) {
  .wu-lib-motifs { grid-template-columns: repeat(6, 1fr); }
}
/* 44px targets throughout — the floor from the #88–#90 audit. */
.wu-lib-swatch {
  width: 100%; min-width: 44px; height: 44px; border-radius: var(--radius-btn);
  border: 3px solid transparent; cursor: pointer; padding: 0;
}
.wu-lib-motif {
  width: 100%; min-width: 44px; height: 44px; border-radius: var(--radius-btn);
  border: 2px solid var(--colour-border); background: var(--colour-surface);
  font-size: 22px; line-height: 1; cursor: pointer; padding: 0;
}
.wu-lib-swatch.wu-lib-on { border-color: var(--colour-text-primary); }
.wu-lib-motif.wu-lib-on {
  border-color: var(--colour-accent);
  background: color-mix(in srgb, var(--colour-accent) 12%, var(--colour-surface));
}
.wu-lib-patterns .wu-cal-tab { min-height: 36px; text-transform: capitalize; }

/* Phone: the maker stacks, and the preview shrinks rather than pushing the
   dials off-screen — same rule as every other reserve in this project. */
@media (max-width: 560px) {
  .wu-lib-maker { flex-direction: column; align-items: center; }
  .wu-lib-preview { position: static; flex: 0 0 auto; }
  .wu-lib-preview .wu-bookcover { width: 104px; }
  .wu-lib-fields { width: 100%; }
}

/* ── Audit fixes, v319 (first full re-run since v295) ────────────────────────
   All four are TAP TARGETS under the 44px Phase 6 floor — the same category
   that #88–#90 kept finding, which is why the audit tests for it directly.
   Hit area grown with a transparent ::after so the paint is unchanged. */
.wu-cal-nav,
.wu-debug-dl,
.wu-debug-help-toggle {
  position: relative;
}
.wu-cal-nav::after,
.wu-debug-dl::after,
.wu-debug-help-toggle::after {
  content: "";
  position: absolute;
  inset: 50% auto auto 50%;
  width: 44px;
  height: 44px;
  min-width: 100%;
  transform: translate(-50%, -50%);
}
/* The APK download is a <button> now, not an <a>: it runs the shared downloader
   so it can report progress (app-update.js). It keeps the link LOOK, because
   nothing about the action changed from the reader's side. */
button.wu-debug-dl {
  display: inline-block;
  min-height: 24px;
  padding: 10px 0;
  border: none;
  background: none;
  color: var(--colour-accent);
  font: inherit;
  text-align: left;
  text-decoration: underline;
  cursor: pointer;
}
button.wu-debug-dl[disabled] {
  color: var(--colour-text-secondary);
  text-decoration: none;
  cursor: default;
}
.wu-debug-dlstate {
  display: block;
}

/* Diagnostics links sit in prose, so they get height rather than a 44px box —
   a full square would overlap the line above and steal its taps. */
.wu-debug a {
  display: inline-block;
  min-height: 24px;
  padding: 10px 0;
}

/* 🔤 Variables letter pad — the answer IS a letter, so the pad holds letters
   rather than digits. One tap submits: there is nothing to type, so a ✓ would
   only add a way to get stuck. Sized to the same 44px floor as the numpad. */
/* ONE ROW, whatever the letter count (owner, 2026-08-17) — auto-flow column
   with 1fr auto-columns, so 5 letters and 6 letters both fill the width without
   the pad needing to know which. A 3-column grid wrapped them onto two rows and
   made the pad read as a keypad rather than a set of choices. */
.wu-varpad {
  display: grid;
  grid-auto-flow: column;
  grid-template-columns: none;
  /* SQUARE cells (owner, 2026-08-17). Plain 1fr columns inherit the numpad's
     narrow container and gave 37x88 slivers — tall, thin, and under the 44px
     tap floor on the axis that matters for a fingertip. A bounded auto-column
     plus aspect-ratio:1 keeps them square at any letter count, and the row
     centres rather than stretching to fill. */
  grid-auto-columns: minmax(56px, 92px);
  justify-content: center;
  gap: var(--space-sm);
  max-width: none;
}
.wu-varpad button {
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  aspect-ratio: 1;
  min-height: 56px;
  height: auto;
  font-size: clamp(22px, 4.5vmin, 34px);
}
.wu-bal-var {
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  color: var(--colour-accent);
}
.wu-bal-blank {
  display: inline-grid;
  place-items: center;
  min-width: 1.6em;
  color: var(--colour-text-secondary);
}

/* 📚 Live-book picker in the reading sheet (#107 slice 2) — 1–2 covers, so a row
   rather than a grid. Picking fills the Book field; typing still works, because
   a book not yet on the shelf must never be un-loggable. */
.wu-bookpick { display: flex; gap: var(--space-sm); justify-content: center; margin-bottom: var(--space-sm); flex-wrap: wrap; }
.wu-bookpick-b {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  width: 84px; padding: 6px; border: 2px solid transparent; border-radius: var(--radius-card);
  background: none; cursor: pointer; font: inherit;
  font-size: var(--font-size-xs); color: var(--colour-text-secondary);
}
.wu-bookpick-b .wu-bookcover { width: 68px; height: auto; border-radius: 6px; }
.wu-bookpick-b span { line-height: 1.2; text-align: center; }
.wu-bookpick-on {
  border-color: var(--colour-accent);
  background: color-mix(in srgb, var(--colour-accent) 10%, transparent);
  color: var(--colour-text-primary);
}

/* ── Crossword cursor: which cell the next letter lands in ───────────────────
   Owner: "the next word selection is working nicely but it is difficult to read
   the highlight." The cursor cell was carried only by the tile's own tint —
   and on an ACTIVE word every cell is already tinted, so there was nothing
   separating "this word" from "this cell".

   A stylesheet cannot simply repaint it: createHoneycomb writes --tile-color as
   an INLINE style, which beats any class rule (the same trap noted in
   crossword.js). And .wu-hex is clip-path'd, so box-shadow and outline are
   clipped away too. filter + transform survive both. */
/* The selected WORD. Colour alone could not carry this: a pending hex fills at
   8% of its tile colour, so "active blue" and "rest blue" arrived as two pale
   washes the owner could not tell apart. Amber (set inline by crossword.js) plus
   a fill at the same 26% the board uses for a completed tile makes the run read
   as one object. Filled cells inside it stay at their own 55% `wu-confirmed`
   level — honeycomb.css loads after this file, so that rule wins at equal
   specificity, which is exactly the wanted order: filled > active > rest. */
.wu-hex-wrap.wu-xw-active .wu-hex::before { background: var(--tile-color); }
.wu-hex-wrap.wu-xw-active .wu-hex::after {
  background: color-mix(in srgb, var(--tile-color) 26%, var(--colour-surface));
}

.wu-hex-wrap.wu-xw-cursor { z-index: 3; }
.wu-hex-wrap.wu-xw-cursor .wu-hex {
  filter: saturate(1.7) brightness(0.9);
  transform: scale(1.07);
}
.wu-hex-wrap.wu-xw-cursor .wu-hex-label,
.wu-hex-wrap.wu-xw-cursor .wu-hex-icon {
  font-weight: 800;
  color: var(--colour-text-primary);
}
@media (prefers-reduced-motion: reduce) {
  .wu-hex-wrap.wu-xw-cursor .wu-hex { transform: none; }
}

/* Activity colour dot in a settings panel heading — answers "which colour is
   this on the flower?" without opening the panel (owner, 2026-08-17). */
.wu-set-dot {
  display: inline-block;
  width: 10px; height: 10px;
  border-radius: 50%;
  margin-right: 6px;
  vertical-align: middle;
}

/* 📚 The library door inside the reading sheet — same chip shape as a book so
   it reads as one more thing on the shelf, but outlined rather than filled so
   it is clearly not a book you can pick. */
.wu-bookpick-more { color: var(--colour-text-secondary); }
.wu-bookpick-plus {
  display: grid;
  place-items: center;
  width: 68px;
  height: 97px;
  border: 2px dashed var(--colour-border);
  border-radius: 6px;
  font-size: 26px;
}
.wu-bookpick-more:hover .wu-bookpick-plus {
  border-color: var(--colour-accent);
}

/* ── Reading sheet: book first, then progress (owner, 2026-08-17) ────────────
   "How far did you get?" cannot be answered before the book is known, so stage
   two stays hidden until a cover is picked. Class-driven, so nothing re-renders
   and no input loses focus mid-flow.
   The BOOK field itself is always hidden here — the covers ARE the input, and a
   text box beside them invites typing a title that already exists. It still
   holds the value that collect() reads. */
/* STEP 1 is the choice and NOTHING else — progress, the timer and Done are all
   absent until a book exists to attach them to (owner). Driven off the presence
   of .wu-book-chosen, so the two steps cannot both be on screen. */
.wu-sheet:has(.wu-bookpick):not(.wu-book-chosen) .wu-sheet-label,
.wu-sheet:has(.wu-bookpick):not(.wu-book-chosen) .wu-sheet-actions,
.wu-sheet:has(.wu-bookpick):not(.wu-book-chosen) .wu-timer-notes {
  display: none;
}
/* STEP 2 replaces the chooser rather than appending to it. */
.wu-sheet.wu-book-chosen .wu-bookpick { display: none; }

/* The picked book's own text field never shows — the cover IS the answer, and a
   text box beside the covers invites retyping a title that already exists. It
   still carries the value collect() reads. */
.wu-sheet .wu-sheet-label:has(.wu-sheet-input[data-key="book"]) {
  display: none !important;
}

/* The chosen book, standing in for the chooser it replaced. */
.wu-book-head {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm);
  margin-bottom: var(--space-sm);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-card);
  text-align: left;
}
.wu-book-head[hidden] { display: none; }
.wu-book-head .wu-bookcover { width: 44px; height: auto; border-radius: 4px; flex: 0 0 44px; }
.wu-book-head-t {
  flex: 1; min-width: 0;
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
}
.wu-book-change {
  flex: none;
  width: 44px; height: 44px;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-btn);
  background: var(--colour-surface);
  font-size: 18px;
  cursor: pointer;
}
.wu-bookpick-more .wu-bookpick-plus { font-size: 30px; }

/* Add-a-book keeps the standard top-right ✕ and commits with a NAMED button at
   the foot — see library.js for why a bare ✓ is wrong for a create sheet.
   ⚠️ `.wu-sheet-go` NEVER HAD A RULE OF ITS OWN — this line, setting only width
   and margin, was the whole of it, so the button that commits a new book
   rendered as a bare UA default: **23px tall**, half the 44px floor (#88-#90),
   on the primary action of the sheet. It carries `.wu-sheet-btn
   .wu-sheet-primary` in the markup now and this stays as the width hook only.
   A class name that LOOKS like a component but matches no rule is invisible in
   review and only a measurement finds it. */
.wu-lib-sheet .wu-sheet-go { width: 100%; margin-top: var(--space-md); }


