/* BrightKidz Hub — games: THE CONSTRUCTION SHEET
   =========================================================================

   THESIS. A game is a specimen sheet with its construction grid left showing.
   Every piece, every quantity and every path is built on one visible cellular
   armature, and the armature is not cleaned up afterwards — it is the proof of
   how the thing was made. This refuses what the games shipped first and what
   the category always ships: a small illustrated diagram parked above a row of
   grey buttons and a paragraph of instructions.

   OWN-WORLD. Board stock ruled with a faint construction grid. The subject ink
   owns whole planes — the sheet header, the active cell, the filled meter —
   never a lone tinted node. Structure is drawn twice, at two weights: hairline
   for the armature, 3px for anything you can touch. Quantity is never a
   numeral; it is CELLS FILLED, so a child reads amount as length. Pieces are
   die-cut: 4px corners, hard 2px offset shadow, no blur. One diagonal per
   sheet marks whose turn it is, and it is the only diagonal the system allows.

   STORY. A child sees ruled paper with a shape being built on it. They press a
   cell; it fills, and the meter beside it grows by one cell. Nothing is a
   number they must decode — the size of the thing IS the amount.

   FIRST VIEWPORT. The sheet fills the mount edge to edge and breaks out of the
   reading measure. Turn cells top-left, board centred and LARGE, meters and
   tray on the armature beside it, status printed along the bottom edge. Title
   and prose sit BELOW the board: the artifact leads.

   FORM. Crouwel grid specimen — the surviving challenger, owner-chosen over
   the assigned punch-out sheet (seed 8ab0aee0), staged on board stock rather
   than Crouwel's stark white because PRODUCT.md pins the warm ground and the
   six subject inks as brand commitments.

   -------------------------------------------------------------------------
   WHAT THIS FILE MAY AND MAY NOT DO

   Loaded ONLY on pages carrying a game — six game pages, the games index, and
   the six articles that mount one. Not on all 84. It is separate from site.css
   for that reason, and because site.css is shared ground several agents edit.

   THE CLASS CONTRACT IS CLOSED. Games use these and add none:
     .game-mount .game-intro .game-stagebar .game-board .game-grid .game-tray
     .game-controls .game-btn .game-status .game-cell .game-tile .game-readout
     .game-note
   plus, new with this world and available to every game:
     .game-meter / .game-meter-cell   quantity as cells filled, never a numeral
     .game-sheet-label                the small tracked label naming a region

   Colour comes from the ink tokens and nothing else, so a game is in its
   subject's colour with no per-game CSS. `--ink` is bound by data-subject on
   the page root, exactly as every other component works.

   Every state that uses colour ALSO uses shape or text: some of these readers
   are colour-blind, and a child cannot tell "I am bad at this" from "this is
   unfair" (DESIGN.md accessibility floor). */

/* ---------------------------------------------------------------- tokens -- */

.game-mount,
.game-page {
  /* Derived from the ink so a game never introduces a colour.
     DECLARED ON BOTH, once. .game-title sits OUTSIDE the mount -- it is the
     sheet's header, not part of the sheet -- and it draws the sheet's own
     hairline. A second copy of these three values would be two things to keep
     in agreement, and one of them would go stale. */
  --sheet-rule: color-mix(in srgb, var(--ink) 22%, transparent);
  --sheet-rule-strong: color-mix(in srgb, var(--ink) 42%, transparent);
  --cell: 48px;          /* the armature's unit AND the tap-target floor */
}

/* ----------------------------------------------------------- the sheet -- */

.game-mount {
  margin: 2.4rem 0;
  border: var(--stroke) solid var(--rule);
  border-radius: var(--die-cut);
  background: var(--stock);
  box-shadow: var(--shadow);
  padding: clamp(.9rem, 3vw, 1.5rem);

  /* THE BOARD BREAKS OUT OF THE READING MEASURE.
     An article's <main> caps at ~676px, which starved the board: measured, a
     12-column game asked for 595px and was offered 569, so its cells missed
     the 48px floor by 2px even on a 1280px screen. The mount said
     `max-width: none` but the PARENT was the cap, so that intent was never
     delivered. A game is an artifact, not a paragraph; it gets the width.

     THE BREAKOUT IS BOUNDED BY MEASURED PADDING, NOT GUESSED.
     Measured on the built pages: <main> pads 16px at 320-390px and 27-28px at
     900-1280px. A first cut asked for 2rem (32px) each side — more padding
     than exists — and put an 11px horizontal scrollbar on the page at 900px.
     A vw-based clamp does not rescue it either, because vw includes the
     scrollbar. So the breakout never exceeds the smallest padding at its own
     breakpoint. */
  width: calc(100% + 2rem);
  margin-inline: -1rem;
  max-width: none;
}

@media (min-width: 48rem) {
  .game-mount { width: calc(100% + 3rem); margin-inline: -1.5rem; }
}

/* The construction grid, left showing.
   A repeating-linear-gradient here is RULING, not a gradient standing in for
   artwork (which DESIGN.md prohibits) — it is the same device as the printed
   grid on graph paper, and it is what makes every alignment in the game
   legible as deliberate. */
.game-mount[data-bk-state="live"] {
  background-image:
    repeating-linear-gradient(to right,
      var(--sheet-rule) 0 1px, transparent 1px var(--cell)),
    repeating-linear-gradient(to bottom,
      var(--sheet-rule) 0 1px, transparent 1px var(--cell));
  background-position: 0 0;
}

/* ONCE THE GAME IS LIVE, THE BOARD COMES FIRST.
   Measured at 320px: several fallbacks ran to three paragraphs and pushed the
   board off the bottom of a phone — the "instructions first" anti-pattern, and
   a seven-year-old will not read them. Ordering rather than HIDING: the prose
   is the no-JS document and carries real content, so it moves below the board
   rather than disappearing. With JavaScript off the rule never applies. */
.game-mount[data-bk-state="live"] {
  display: flex;
  flex-direction: column;
}
/* HIDDEN once the game runs, not merely moved. Reordering was the first fix
   and it solved the wrong half: it got the prose out of the way of the board
   but left it ON SCREEN, and MEASURED, the fallback for five of the twelve
   games STATES THE ANSWER the game exists to make a child find --
   cloud-ladder's says "you cannot name a cloud by looking at its shape alone",
   which is its stage 3; close-the-loop's says "being joined to the battery is
   not what lights a bulb"; ten-minus-one's explains why the trick works and
   what happens past ten rows. Measured live: 604px of it, display:block,
   visible, sitting under the board the child is playing.

   That is not a reason to rewrite the prose. It is the JavaScript-off
   document and it MUST teach the idea, because a child without scripts
   cannot play and would otherwise be handed a question with no answer.
   The same text simply has two audiences and only one of them should see it
   at a time. This rule applies ONLY under [data-bk-state="live"], so with
   JavaScript off it never fires and the prose is untouched.

   The element STAYS in the DOM: every game driver asserts it survives
   mounting, guarding against a game wiping its own fallback. */
.game-mount[data-bk-state="live"] .game-intro { display: none; }

.game-intro {
  margin: 0 0 1rem;
  max-width: var(--measure);
}
.game-intro > :first-child { margin-top: 0; }
.game-intro > :last-child { margin-bottom: 0; }
.game-mount[data-bk-state="inert"] .game-intro { margin-bottom: 0; }

/* The small tracked label that names a region. Crouwel's specimen sheets name
   their parts; so does a rulebook. */
.game-sheet-label {
  display: block;
  font-family: var(--font-read);
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-edge);
  margin: 0 0 .4rem;
}

/* ------------------------------------------------------------ the turns -- */

/* Turn markers are CELLS of the armature, filled. Not pills: a pill is a
   generic chip, a filled cell is this system saying "one unit of the grid". */
.game-stagebar {
  display: flex;
  flex-wrap: wrap;
  gap: 0;
  margin: 0 0 1.2rem;
  padding: 0;
  list-style: none;
  font-size: .8rem;
  font-weight: 700;
  border: 1px solid var(--sheet-rule-strong);
  width: fit-content;
  max-width: 100%;
  background: var(--stock);
}
.game-stagebar li {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  padding: .5rem .8rem;
  min-height: 40px;
  border-right: 1px solid var(--sheet-rule-strong);
  color: var(--board);
}
.game-stagebar li:last-child { border-right: 0; }

/* THE CURRENT TURN IS AN INK PLANE with a die-cut corner.
   Colour is never the only signal: the notch is a shape, and the item is also
   the only one marked aria-current for a screen reader.

   ⚠ The corner, NOT a diagonal across the cell. The first cut ran the system's
   permitted diagonal corner-to-corner and it read as a STRIKETHROUGH — "1.
   Find a way" looked cancelled, which is the opposite of "you are here". A
   device that says the wrong thing loudly is worse than no device. The
   diagonal survives as the cut corner, where it marks the piece without
   crossing its label. */
.game-stagebar li[aria-current="step"] {
  background:
    linear-gradient(to bottom left,
      var(--stock) 0 9px, transparent 9px),
    var(--ink);
  color: var(--stock);
}
.game-stagebar li[data-done="yes"]::before { content: "\2713\00a0"; }

/* ------------------------------------------------------------- the board -- */

.game-board {
  display: block;
  margin: 0 0 1.1rem;
  padding: clamp(.5rem, 2vw, 1rem);
  border: 1px solid var(--sheet-rule-strong);
  background: var(--stock);
  overflow-x: auto;              /* a board never makes the PAGE scroll */
  -webkit-overflow-scrolling: touch;
}
.game-board svg { display: block; max-width: 100%; height: auto; margin: 0 auto; }

.game-grid {
  display: grid;
  gap: 4px;
  margin: 0 auto;
  width: max-content;
  max-width: 100%;
}

/* --------------------------------------------------------------- pieces -- */

.game-cell,
.game-tile {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--cell);
  min-height: var(--cell);
  padding: 0;
  font: inherit;
  font-family: var(--font-display);
  font-size: 1.2rem;
  color: var(--board);
  background: var(--stock);
  border: var(--stroke) solid var(--rule);
  border-radius: var(--die-cut);
  cursor: pointer;
  transition: transform .14s cubic-bezier(.16, 1, .3, 1),
              background-color .14s ease;
}
.game-cell:hover,
.game-tile:hover { background: var(--ink-soft); }

.game-cell:focus-visible,
.game-tile:focus-visible,
.game-btn:focus-visible {
  outline: var(--stroke) solid var(--ink-edge);
  outline-offset: 3px;
}

/* A piece in your hand is LIFTED — the hard offset shadow is the world's own
   "this one is off the sheet" device (DESIGN.md material rules). */
.game-tile[aria-pressed="true"] {
  background: var(--ink);
  color: var(--stock);
  border-color: var(--ink);
  box-shadow: var(--shadow-lift);
  transform: translate(-2px, -2px);
}

/* Printed on the sheet rather than dealt to the player: it keeps the ruling
   showing through, so a fixed cell reads as part of the armature. */
.game-cell[data-fixed="yes"] {
  background:
    repeating-linear-gradient(-45deg,
      var(--sheet-rule) 0 1px, transparent 1px 6px),
    var(--stock-deep);
  cursor: default;
}
/* There is deliberately NO `color: transparent` on an empty cell. The first
   cut had one, and it resolved `currentColor` to nothing inside any SVG a
   game draws in an empty cell: Same Name's signs were invisible until the
   cell was solved, in a game about looking at signs. Fifteen writes '' into
   an empty cell, so the rule was never load-bearing; it only hid other
   games' artwork. Blank a cell by writing nothing into it. */

.game-tray {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin: 0 0 1rem;
  padding: .6rem;
  border: 1px solid var(--sheet-rule-strong);
  min-height: 62px;
  align-items: center;
}
.game-tray:empty::before {
  content: "empty";
  font-weight: 700;
  opacity: .6;
}

/* -------------------------------------------------------------- quantity -- */

/* AMOUNT IS LENGTH, NOT A NUMERAL.
   "Cloud 0" is a dashboard reading; a row of cells that fills is the world.
   A child sees how much there is without decoding a digit, which is the whole
   argument for the grid: the armature already gives them a ruler. */
.game-meter {
  display: flex;
  align-items: center;
  gap: 3px;
  padding: 0;
  margin: 0;
  list-style: none;
}
.game-meter-cell {
  width: 14px;
  height: 22px;
  border: 1px solid var(--sheet-rule-strong);
  background: var(--stock);
}
.game-meter-cell[data-on="yes"] {
  background: var(--ink);
  border-color: var(--ink);
}
/* The newest cell carries the diagonal, so "what just changed" is visible
   without colour and without motion. */
.game-meter-cell[data-new="yes"] {
  background:
    linear-gradient(to top right,
      var(--ink) calc(50% - 1px), var(--stock) calc(50% - 1px),
      var(--stock) calc(50% + 1px), var(--ink) calc(50% + 1px));
}

/* --------------------------------------------------------------- readout -- */

.game-readout {
  font-family: var(--font-display);
  font-size: .95rem;
  color: var(--board);
}
.game-readout[data-state="ok"] { color: var(--ink-edge); }
.game-readout[data-state="ok"]::after { content: "\00a0\2713"; }
.game-readout[data-state="no"]::after { content: "\00a0\00d7"; }

/* -------------------------------------------------------------- controls -- */

.game-controls {
  display: flex;
  flex-wrap: wrap;
  gap: .55rem;
  margin: 1rem 0 0;
}

.game-btn {
  font: inherit;
  font-family: var(--font-display);
  font-size: .9rem;
  text-transform: uppercase;
  letter-spacing: .04em;
  min-height: var(--cell);
  min-width: var(--cell);
  padding: .5rem 1.05rem;
  color: var(--board);
  background: var(--stock);
  border: var(--stroke) solid var(--rule);
  border-radius: var(--die-cut);
  box-shadow: var(--shadow);
  cursor: pointer;
}
.game-btn:hover { background: var(--ink); color: var(--stock); }
.game-btn:active { transform: translate(2px, 2px); box-shadow: none; }
.game-btn[disabled] {
  cursor: default;
  box-shadow: none;
  color: var(--ink-edge);
  /* Not colour alone: a disabled control loses its shadow, so it reads as
     flat on the sheet rather than pressable, and takes the armature's own
     hatching so it looks printed rather than dealt. */
  background:
    repeating-linear-gradient(-45deg,
      var(--sheet-rule) 0 1px, transparent 1px 6px),
    var(--stock-deep);
}
.game-btn[data-primary="yes"] {
  background: var(--ink);
  color: var(--stock);
  border-color: var(--ink);
}

/* ---------------------------------------------------------------- status -- */

/* Printed along the foot of the sheet like the caption on a box back.
   ALWAYS VISIBLE, never screen-reader-only: the announcement and the sentence
   a sighted child reads are the same string, because two texts for one fact
   is two things to keep in agreement and one of them goes stale. */
.game-status {
  margin: 1.1rem 0 0;
  padding: .8rem 1rem;
  min-height: 3.2em;
  font-weight: 700;
  background: var(--ink-soft);
  border: 1px solid var(--sheet-rule-strong);
  border-radius: var(--die-cut);
}

.game-note {
  margin: .6rem 0 0;
  font-size: .9rem;
  color: var(--board);
  opacity: .85;
}

/* ------------------------------------------------------------ the section -- */

.games-board { margin: 0; }

.games-board .board-region {
  margin: 0 0 2.2rem;
  padding: 1.1rem 1.2rem 1.4rem;
  border: var(--stroke) solid var(--rule);
  border-radius: var(--die-cut);
  box-shadow: var(--shadow);
  background: var(--stock);
  background-image:
    repeating-linear-gradient(to right,
      color-mix(in srgb, var(--ink) 16%, transparent) 0 1px, transparent 1px 48px),
    repeating-linear-gradient(to bottom,
      color-mix(in srgb, var(--ink) 16%, transparent) 0 1px, transparent 1px 48px);
}
/* The second kind's name, over the board; same rhythm as the first kind's
   heading in play-shelf.css so the two read as a pair. */
.play-attached > h2 { margin-bottom: .35rem; }

/* h2 OR h3 -- /play/ demotes the six regions when it nests them under a kind.
   LEVEL IS SEMANTICS, SIZE IS PRESENTATION: the flat h3 size would shrink the
   headings over fifteen of that page's twenty cards, so the size is held at
   site.css's h2 ramp instead. Pinned by test_play_index.py. */
.games-board .board-region :is(h2, h3) {
  margin: 0 0 .9rem;
  font-size: clamp(1.4rem, 3.6vw, 1.9rem);
  color: var(--ink);
}

/* A LONE ENTRY TAKES THE WHOLE ROW.
   The shared article-list is a `repeat(auto-fill, minmax(17rem, 1fr))` grid,
   which is right for a category page holding sixteen articles and wrong here:
   with one game per subject it left a small card marooned in a field of empty
   ruling, which reads as unfinished rather than spacious. A specimen sheet
   lists its entries as full-width ruled rows, and that is also what scales —
   a second game in a subject simply sits beside the first. */
.games-board .article-list > li:only-child { grid-column: 1 / -1; }
.games-board .article-list > li:only-child .summary { max-width: 52ch; }
.games-board .empty {
  margin: 0;
  font-weight: 700;
  opacity: .75;
}

/* "See all 7 Science games" -- shown only when a region holds more than the
   board previews. It is a way OUT of the board, so it is set apart from the
   cards by the region's own ruling rather than dressed up as a seventh card:
   a link that looks like a game is a link a child presses expecting a game. */
.games-board .region-more {
  margin: 1rem 0 0;
  padding-top: .75rem;
  border-top: 1px solid var(--sheet-rule-strong);
  font-weight: 700;
}

/* ------------------------------------------------ the page IS the sheet -- */

/* A GAME PAGE IS NOT A DOCUMENT WITH A GAME IN IT.

   MEASURED on the built site at 320x568, before this section existed: the play
   surface began at a MEDIAN OF 558px OF A 568px VIEWPORT, and not one of the
   fifteen games showed a playable board without scrolling. At 390 and 414 no
   game at all showed its board AND its controls together. A child opened
   `Fifteen` and saw a wordmark, a nav row, a title, a three-line sentence and
   a meta line -- then the top edge of the sheet. Not one tile. `Shift the
   Wheel` was five lines of prose and no game.

   Where the 539px above the board went, at 320:

       masthead                 140     (site.css; not this file's to spend)
       h1 + margin               48
       .game-teaches + margin   107
       .game-meta + margin       65
       .game-mount top margin    38
       mount border + padding    17
       .game-stagebar + margin  124

   THE FIRST VIEWPORT CONTRACT AT THE TOP OF THIS FILE ALREADY SAID WHAT TO DO
   -- "Title and prose sit BELOW the board: the artifact leads" -- and this
   file had already delivered its own half, hiding .game-intro the moment a
   game is live. It could not deliver the other half, because the heading and
   the two caption paragraphs are emitted by the page shell ABOVE the mount,
   where no selector here can reach them. emit.game_page_html now emits them in
   the order this file always described; the rules below are what that order
   buys.

   .game-page and .game-title are PAGE furniture and NOT an addition to the
   closed game class contract at the top of this file. A game still uses those
   thirteen classes and adds none. These two frame the sheet, in the same
   category as .game-teaches and .game-meta, which have been here from the
   start. */

/* The sheet's own header, at the weight of a label rather than a headline.
   site.css sets h1 at clamp(2rem, 6vw, 3rem) with a .4em margin: right for a
   spread a child sits down to read, and 48px of a 428px usable phone screen
   for a page whose entire subject is the board beneath it. A specimen sheet
   names its subject ON the sheet and hands the room to the specimen, so the
   header takes the sheet's own hairline and stops competing with it. */
.game-page .game-title {
  font-size: clamp(1.3rem, 4.6vw, 1.9rem);
  line-height: 1.05;
  margin: 0 0 .55rem;
  padding-bottom: .35rem;
  border-bottom: 1px solid var(--sheet-rule-strong);
}

/* The sheet starts where its header ends. 2.4rem of separation is right for a
   game mounted mid-article -- a turn interrupting a read -- and wrong for the
   page whose whole subject it is. Scoped, so the article mount keeps its. */
.game-page .game-mount { margin-top: 0; }

/* THE CAPTION, printed under the specimen instead of in front of it.

   The same two paragraphs carrying the same two facts; what changed is that a
   child meets them after the thing they describe. Both keep their size -- they
   cost nothing below the board, and the sentence naming what a game teaches is
   real content, not chrome. The rule above them is the sheet's own device: it
   says "this belongs to that" without spending a word on it. */
.game-page .game-teaches {
  font-size: 1.15rem;
  font-weight: 700;
  max-width: var(--measure);
  margin: 1.5rem 0 .35rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--sheet-rule-strong);
}
.game-page .game-meta {
  font-size: .92rem;
  margin: 0;
  max-width: var(--measure);
}

/* ONE ROW OF TURNS, NOT THREE.

   MEASURED at 320: 105px for a turn bar that wrapped to two rows, 157px for
   one that wrapped to three -- for three short labels, in front of the board.
   One row that scrolls sideways is the device site.css already chose for this
   exact problem at this exact width ("One row that scrolls sideways, not three
   that stack"), so this is that behaviour rather than a second one to keep in
   agreement with it. Compacted as well as unwrapped, so most turn bars now fit
   with no scrolling at all -- and the cost becomes a flat ~40px however many
   turns a game has, where a wrapped row grew without limit.

   The items are not tap targets: they carry no role, no href and no tabindex,
   and the measured smallest target inside every mount stayed 48px. The 44px
   floor governs things a thumb presses, and a turn marker is not one.

   BOTH predicates, because a landscape phone is short without being narrow and
   the bar costs the same there. */
@media (max-width: 34rem), (max-height: 46rem) {
  .game-stagebar {
    flex-wrap: nowrap;
    width: 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scroll-snap-type: x proximity;
    margin-bottom: .8rem;
  }
  .game-stagebar li {
    flex: 0 0 auto;
    scroll-snap-align: start;
    min-height: 36px;
    padding: .35rem .6rem;
    font-size: .74rem;
  }
  /* The cut corner marking the current turn is sized to the cell it notches;
     at this padding the 9px original overhangs the label it must not cross. */
  .game-stagebar li[aria-current="step"] {
    background:
      linear-gradient(to bottom left,
        var(--stock) 0 7px, transparent 7px),
      var(--ink);
  }

  /* The caption tightens with everything else, and keeps its rule. */
  .game-page .game-teaches { margin-top: 1.1rem; padding-top: .8rem; }

  /* The status is the game's own voice and STAYS AT READING SIZE -- some of
     these readers are dyslexic, and this is the sentence telling them what
     just happened and what to do next. Only its frame gives ground. */
  .game-status { margin-top: .7rem; padding: .6rem .75rem; }
}

/* A BOARD NEVER PUSHES ITS OWN CONTROLS OFF THE SCREEN.

   Keyed on viewport HEIGHT, not width: the question this answers is "is there
   room", and a 320-wide phone in a 900px-tall window has room while a landscape
   844x390 does not. It engages only where it must, so a board that already fits
   never gains a scrollbar.

   .game-board ALREADY computes `overflow: auto / auto` -- one axis being `auto`
   forces the other off `visible`, measured on all eight games probed -- so a
   max-height engages the model that is already here rather than introducing
   one. Nothing scales, and that is the point: MEASURED, several games put their
   touch targets INSIDE the SVG (pole-to-pole 3, close-the-loop 2), so a board
   shrunk to fit would take its own tap targets under the 44px floor with it,
   silently, with every declared number unchanged. A board taller than the room
   left scrubs inside its own frame instead, and the controls beneath it stay
   where the thumb expects them.

   DERIVED, not guessed: 20rem is the furniture this page still spends above the
   frame (masthead, header, mount chrome, one turn row) plus one row of controls
   below it. `max()` keeps a floor, so a short landscape phone gets a small board
   rather than a slot; the vh line is the fallback for an engine without dvh, and
   the dvh line wins wherever it is understood. */
@media (max-height: 46rem) {
  .game-board {
    max-height: max(11rem, calc(100vh - 20rem));
    max-height: max(11rem, calc(100dvh - 20rem));

    /* THE SCROLLBAR IS PAID FOR OUT OF THE FRAME, NEVER OUT OF THE BOARD.

       MEASURED, and it is why these two lines exist: with the cap alone, a
       board that overflowed grew a classic 15px scrollbar, the SVG inside
       obeyed its own `max-width: 100%` and rescaled by 5.8%, and close-the-
       loop's touch targets went 48px -> 46px. Nothing declared a smaller
       number anywhere; the board simply got narrower and took its targets
       with it. DESIGN.md sets 44px as the floor and 48px INSIDE A WIDGET, so
       46 is a breach, and a silent one.

       `scrollbar-gutter: stable` reserves the strip whether or not it is
       needed, so a capped board and an uncapped one at the same width have
       the SAME content box and no board ever changes size by scrolling. The
       reserved strip is then taken back out of the frame's own inline padding
       -- 8px a side at this width, so the drawing keeps essentially the width
       it had (239px before, 240px after, measured). On a phone, where
       scrollbars are overlays and the gutter is zero, the board gets those
       16px BACK and its targets grow rather than shrink. */
    scrollbar-gutter: stable;
    padding-inline: 0;
  }
}

.card-kind {
  display: inline-block;
  margin-left: .5rem;
  padding: .1rem .45rem;
  font-size: .72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--stock);
  background: var(--ink);
  border-radius: var(--die-cut);
}

/* ------------------------------------------------------------------ a11y -- */

@media (prefers-reduced-motion: reduce) {
  /* State still CHANGES; it simply arrives without travel (DESIGN.md motion).
     Nothing here hides a change — that would be worse than animating it. */
  .game-cell,
  .game-tile,
  .game-btn,
  .game-meter-cell,
  .game-board * {
    transition: none !important;
    animation: none !important;
  }
  .game-tile[aria-pressed="true"],
  .game-btn:active { transform: none; }
}

@media (prefers-contrast: more) {
  .game-mount { border-width: 4px; background-image: none; }
  .games-board .board-region { border-width: 4px; background-image: none; }
  .game-status { background: var(--stock); border-width: 2px; }
  .game-btn[disabled] { background: var(--stock-deep); text-decoration: line-through; }
  .game-stagebar li,
  .game-board,
  .game-tray,
  .game-meter-cell { border-color: var(--rule); }
  /* The sheet's header rule and the caption's rule are structure, not
     decoration -- they are what say "this title belongs to that sheet" and
     "this caption belongs above". A reader who asked for more contrast is the
     last one who should be shown them at 42% ink. */
  .game-page .game-title,
  .game-page .game-teaches { border-color: var(--rule); }
}

@media (max-width: 26rem) {
  /* On the narrowest phones the controls become full-width rows: a 48px
     target a thumb misses is a 48px target that failed. Games laying out
     their own control cluster (Mirror Wing's cursor pad) opt out by setting
     their own display on the container. */
  .game-controls { flex-direction: column; align-items: stretch; }
  .game-btn { width: 100%; }
  .game-mount { margin-inline: -0.6rem; width: calc(100% + 1.2rem); }
}
