/* THE ROOT SELECTOR IS THE MOUNT CONTRACT, NOT A CLASS NAME.
   This file styled `.arcade` while the build emits
   `<div class="arcade-mount" data-arcade="long-way-round">`, so the root rule
   matched NOTHING -- and every colour in the game is derived inside it:
   --arc-trail, --arc-rule, --arc-wall, --arc-dot, --arc-ground. With those
   undefined, `.arc-ribbon { stroke: var(--arc-trail) }` had no stroke, so THE
   LINE THE WHOLE GAME IS ABOUT WAS INVISIBLE, and all four themes rendered
   identically because .arcade[data-theme] never matched either.
   Neither author could see it: the stylesheet was written before the page
   existed, and the page was emitted by someone who never read the stylesheet.
   `[data-arcade]` is the selector `arcade.json` already declares as `mount`
   and the one the module itself queries, so it cannot drift when a layout
   class is renamed. */
/* BrightKidz Hub — arcade: THE CONSTRUCTION SHEET, PLAYED
   =========================================================================

   THESIS. The arcade is the games' construction sheet with the armature doing
   real work: the ruling is not decoration behind the board, it IS the board,
   and the line a child draws is measured against it square by square.

   WHY THIS FILE EXISTS AT ALL, rather than a few more classes in games.css.
   That file's class vocabulary is CLOSED and belongs to the twelve games; a
   game needing a new class is a game inventing a second visual language. The
   arcade is not a game in that sense — it has no host article, no `teaches`
   sentence and no 24 KB ceiling — so it gets its own sheet and inherits the
   world from DESIGN.md rather than borrowing the games' vocabulary.

   WHAT IT INHERITS UNCHANGED: the warm board ground, the six subject inks,
   3px rules on anything you can touch, hard 2px offset shadows with no blur,
   4px die-cut corners, inline SVG only, and the whole accessibility floor.

   WHAT IS THEIRS ALONE: the played armature, the head TILE, and the pip strip
   that shows puzzles cleared as filled cells rather than as a numeral.

   THE THEME IS THE ONLY THING THAT MOVES. Four themes are pure data over
   identical geometry, and every one of them binds `--ink` to a subject ink
   that already exists. A theme changes ink, ground and a few words. It never
   changes a rule, a size, a control or a target — which is what makes one of
   them free and the rest merely nice to have.

   COLOUR IS NEVER THE ONLY SIGNAL. Wall carries a die-cut tile, a portal pair
   carries a SHAPE that tells one pair from the other, a cleared puzzle carries
   a count in its title, and every state change is also spoken in the status
   line. Some of these readers are colour-blind, and a child cannot tell "I am
   bad at this" from "this is unfair".

   -------------------------------------------------------------------------
   2026-08-29 — THE BOARD HAD NO PRESENCE, AND THE NUMBER SAYS SO.

   MEASURED on the built page at a 1280px viewport: the sheet was 917.2px wide
   and the board was 304px. The artifact this whole surface exists to show was
   a THIRD of its own frame, ringed by six hundred pixels of empty ruling —
   and a 4x4 board hit that 304px cap because arcade.js sets `--arc-cols` per
   stage and the cell was capped at 76px. So the board did not merely look
   small, it was FORBIDDEN from being larger by this file.

   Two changes, and they work together. The cell cap now targets a BOARD WIDTH
   rather than a cell size (`480px / --arc-cols`), so a four-column board and a
   five-column board arrive at the same authority instead of the four-column
   one shrinking by a fifth; and the sheet itself is capped and centred, so the
   frame fits the artifact rather than stranding it.

   A RAIL WAS DESIGNED AND IS NOT HERE, and the reason is worth recording so
   nobody spends the afternoon I spent. DESIGN.md's games FIRST VIEWPORT puts
   "meters and tray on the armature BESIDE it" — a right-hand rail — and this
   sheet has the width for one. It is not reachable from CSS alone with this
   DOM. The rail's contents (pips, score, worlds, themes) are FLAT SIBLINGS of
   the board, arriving AFTER it, with no wrapper. Grid cannot stack them
   independently of the play column's row heights without one — a board given
   a multi-row span redistributes its height across the rail's rows and pulls
   them apart — and floats cannot either, because a float is placed at or below
   the block position where it is ENCOUNTERED, and by then the board is already
   laid out above it. The honest fix is one wrapper element around those four,
   which belongs to whoever owns arcade.js. Until then the single column is not
   a compromise dressed up: it is the whole of what this file can truthfully do.

   RHYTHM IS THE ARMATURE'S, NOT THE ARTICLE'S. Spacing is in multiples of
   `--arc-unit`, the same 24px step the sheet's own ruling is drawn on, so the
   gaps between things line up with the printed grid behind them. That is the
   Crouwel argument in one variable: the sheet should look measured because it
   WAS measured, not because the values were nudged until they looked right. */

/* ---------------------------------------------------------------- tokens -- */

[data-arcade] {
  /* Derived from the ink so the arcade never introduces a colour of its own,
     exactly as games.css derives its sheet rules. */
  --arc-rule: color-mix(in srgb, var(--ink) 20%, transparent);
  --arc-rule-strong: color-mix(in srgb, var(--ink) 45%, transparent);
  --arc-wall: color-mix(in srgb, var(--ink) 72%, var(--board));
  --arc-trail: var(--ink);
  --arc-dot: color-mix(in srgb, var(--ink) 30%, var(--stock));
  --arc-ground: color-mix(in srgb, var(--ink) 5%, var(--stock));

  /* THE BOARD'S RULING IS NOT THE SHEET'S RULING, and they had been one token.
     The sheet's grid is texture behind everything; the board's grid is the
     RULER a child reads distance with — arcade.js calls it that — so it is
     printed heavier. It stays deliberately subordinate to the dots and the
     ribbon: see the note on the armature at the foot of this file for why 3:1
     is not reachable here without turning the board into a cage. */
  --arc-grid: color-mix(in srgb, var(--ink) 55%, var(--stock));

  /* THE DOT NEEDED AN EDGE, AND THAT IS A CONTRAST FIX, NOT A FLOURISH.
     MEASURED against --arc-ground: the fill alone was 1.59:1 in the comb
     theme — the dots are the thing a child counts down, and they were the
     palest mark on the board. A darker FILL is the wrong repair: dots mean
     "still to visit", and if they read as strongly as the ribbon the board
     looks finished before it is started. So the fill stays quiet and the dot
     gets a printed EDGE at --ink-edge (measured 7.4:1 on the ground), which
     is DESIGN.md's own token-disc device. Amount is still read as area
     covered, because a visited dot drops edge AND fill together. */
  --arc-dot-edge: var(--ink-edge);

  /* THE ARMATURE'S STEP. The sheet's ruling is drawn at 24px, so every margin
     in this file is a multiple of it and lands on a printed line. */
  --arc-unit: 24px;

  /* ⚠ THE RULE-MARKS ARE UNREADABLE ON THE RIBBON AND THIS FILE CANNOT FIX IT.
     MEASURED: --rule on the board ground is 13.6:1 and --rule on the INK
     RIBBON is 2.17:1 in the night theme — under 3:1, at exactly the place a
     child looks to check whether they may leave a square, and only AFTER they
     have drawn over it, which is why nobody had seen it.

     NO OPAQUE COLOUR FIXES BOTH, and that is arithmetic rather than taste:
     against the comb ink a mark needs relative luminance <= 0.233 to clear the
     pale ground and >= 0.478 to clear the ribbon, and no colour is both.

     A PRINTER'S TRAP WAS BUILT FOR THIS AND WAS REJECTED BY MEASUREMENT. Four
     zero-blur drop-shadows in --stock did put a light choke around each mark
     — 2,979 extra near-stock pixels on the same mark, same ink, same window —
     but the offsets resolve in CSS px and not in the board's user units, so
     the choke does NOT scale with the board, and Chromium quantises it: 0.22px
     and 0.42px render IDENTICALLY while 1.2px is fatter. A device that cannot
     be tuned smaller than "too big" is a device that cannot be tuned, and at
     every size it reached it ATE THE RIBBON where the two crossed — the one
     unbroken line being the thing §3b exists to protect. Rendered, sampled,
     and taken back out; the sweep is in the report.

     THE FIX IS ONE ATTRIBUTE AND IT BELONGS TO arcade.js. The dots already get
     `data-done="yes"` when their cell is visited. If the rule-marks got the
     same stamp, this sheet could print them in --rule on the bare ground and
     switch them to --stock the moment the ribbon arrives underneath — 13.6:1
     before and 5.0:1 after, both clear, with no outline and no filter. Until
     then the marks stay --rule, which is right on the ground they spend most
     of their life on and wrong for the moment after a child draws through. */
  /* THE CELL FLOOR IS MEASURED, NOT CHOSEN, and the first measurement moved
     the generator rather than this file. In Chrome at a 320px viewport <main>
     is 305px (a scrollbar takes 15) and the board is offered 245px once this
     sheet's padding and rules are paid — so six columns give 40.8px, under
     DESIGN.md's floor, and five give 49px. The generator caps at five for
     that reason. Do not raise it here: the cap that matters is in arcade.js,
     and this variable only follows what the board reports. */
  --arc-cols: 5;

  /* THE CAP TARGETS A BOARD, NOT A CELL, and that is the whole fix for a
     board that floated. `480px / --arc-cols` asks every stage for about the
     same WIDTH, so the 4-wide boards this generator opens on stop arriving a
     fifth smaller than the 5-wide ones they grow into.

     13vh KEEPS THE TALL STAGES HONEST: SIZES tops out at 5x7, so a cap set on
     width alone would hand a short laptop window a 730px-tall board and push
     the controls a child is mid-drag with off the bottom of the screen.

     THERE WAS AN 8.6vw GUARD HERE AND IT WAS MEASURED OUT AGAIN. The idea was
     to keep a phone honest, but it was doing the opposite: at 305-390px it
     resolved BELOW the clamp floor, so the cap pinned to that floor and the
     board came out 230px WIDE AT EVERY ONE of those widths — 4 cells of 54.5px
     on a 358px sheet, which is a board refusing to use two thirds of the room
     it had. It was also redundant, and that is the load-bearing half: overflow
     is prevented by `min(100%, ...)` on the board itself, which cannot be
     escaped, so a viewport-width term here could only ever make the board
     SMALLER than the space it was already guaranteed to fit inside. Removing
     it takes a 390px phone from 56px cells to about 79px. It remains a CAP and
     never a floor. */
  --arc-cell-max: clamp(48px, min(calc(480px / var(--arc-cols)), 13vh), 120px);

  /* The plate: ground, then a hairline, then the drawing. See .arcade-board. */
  --arc-plate: 8px;
}

/* Four themes, four inks that already exist in the system. */
[data-arcade][data-theme="comb"]     { --ink: var(--ink-math); }
[data-arcade][data-theme="reef"]     { --ink: var(--ink-science); }
[data-arcade][data-theme="rooftops"] { --ink: var(--ink-arts); }
[data-arcade][data-theme="night"]    { --ink: var(--ink-technology); }

/* AND THE TWO INKS DERIVED FROM IT HAVE TO BE RE-DERIVED HERE.
   site.css declares --ink-edge and --ink-soft ON :root, so the var(--ink)
   inside them resolves against :ROOT's ink — the page's subject — and not
   against whatever a descendant later sets. Rebinding --ink on [data-arcade]
   therefore moved the board and left every mark drawn in --ink-edge behind:
   MEASURED on a reef board, the one-way arrows, the door bars, the key and
   the sheet label all came out in the page's amber while the ribbon was teal.
   It reads as two games overlaid, and it was invisible until the themes were
   rendered and looked at. Restating both here re-derives them from the theme. */
[data-arcade][data-theme] {
  --ink-edge: color-mix(in srgb, var(--ink) 62%, var(--board));
  --ink-soft: color-mix(in srgb, var(--ink) 14%, var(--stock));
}

/* ----------------------------------------------------------- the sheet -- */

[data-arcade] {
  /* CAPPED AND CENTRED, because the frame should fit the artifact. The arcade
     page is `data-kind="listing"`, so <main> hands this mount 917px at a
     1280px viewport — width a five-column board has no use for, and which
     read as an empty field with a stamp in the middle. 47rem holds the widest
     board this generator can draw with a printed margin around it. */
  max-width: 47rem;
  margin: calc(var(--arc-unit) * 1.5) auto calc(var(--arc-unit) * 2);
  border: var(--stroke) solid var(--rule);
  border-radius: var(--die-cut);
  background: var(--stock);
  box-shadow: var(--shadow);
  padding: clamp(.9rem, 3vw, 1.4rem);
  /* The armature, printed on the sheet itself and continuing under the board:
     the point of a construction sheet is that the grid was not cleaned up. */
  background-image:
    repeating-linear-gradient(90deg, transparent 0 23px, var(--arc-rule) 23px 24px),
    repeating-linear-gradient(0deg, transparent 0 23px, var(--arc-rule) 23px 24px);
}

/* ------------------------------------------------------------- the head -- */

/* The masthead of the sheet, ruled off from the board the way a rulebook rules
   off its title. It was a flex row of two text runs at article weight; the
   rule and the display size are what make it read as a plate heading. */
.arcade-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .1rem 1rem;
  margin-bottom: var(--arc-unit);
  padding-bottom: calc(var(--arc-unit) / 3);
  border-bottom: 1px solid var(--arc-rule-strong);
}

.arcade-sheet-label {
  display: block;
  font-family: var(--font-display);
  font-size: .7rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-edge);
  margin: var(--arc-unit) 0 calc(var(--arc-unit) / 3);
}
.arcade-head .arcade-sheet-label { margin: 0; flex: 1 0 100%; }

.arcade-world {
  font-family: var(--font-display);
  /* Up from clamp(1.3rem, 4.5vw, 1.8rem). A world's name is this sheet's
     title and it was set smaller than the article heading above it. */
  font-size: clamp(1.5rem, 5.2vw, 2.15rem);
  line-height: 1.05;
  letter-spacing: -0.015em;
  margin: 0;
  color: var(--ink);
}

/* ----------------------------------------------------------- the board -- */

.arcade-board {
  /* The board is the artifact and it leads. It is capped so a cell never
     grows past --arc-cell-max on a wide screen, and it shrinks to fit below
     that — never the other way round, because a board that overflows is a
     board a child cannot reach the edge of. The plate padding is ADDED to the
     cap so the drawing keeps its full size rather than paying for its frame. */
  max-width: min(100%,
    calc(var(--arc-cols) * var(--arc-cell-max) + var(--arc-plate) * 2));
  margin: 0 auto;
  padding: var(--arc-plate);
  background: var(--arc-ground);
  border: var(--stroke) solid var(--rule);
  border-radius: var(--die-cut);
  /* A PRINTED PLATE, NOT A PANEL: the 3px rule, a band of ground, then a
     hairline, then the drawing. It is the double rule a printed board carries
     around its play area, drawn with inset shadows so it costs no element and
     cannot sit over the play surface and eat a drag. */
  box-shadow:
    var(--shadow),
    inset 0 0 0 calc(var(--arc-plate) - 2px) var(--arc-ground),
    inset 0 0 0 calc(var(--arc-plate) - 1px) var(--arc-rule-strong);
  overflow: hidden;
}

.arcade-svg {
  display: block;
  width: 100%;
  height: auto;
  touch-action: none;          /* a drag draws the line, it does not scroll */
  cursor: pointer;
}
.arcade-svg:focus { outline: none; }
.arcade-svg:focus-visible { outline: none; }

/* A tabindexed <svg> gets no reliable focus ring, and "I cannot see where I
   am" reads to a child as "the keyboard does nothing". */
.arcade-board:focus-within {
  outline: var(--stroke) solid var(--ink);
  outline-offset: 2px;
}

.arc-rule-path {
  fill: none;
  stroke: var(--arc-grid);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

.arc-wall-tile {
  fill: var(--arc-wall);
  stroke: var(--rule);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

/* The token disc: a quiet fill inside a printed edge. The edge is the part
   that has to be seen (see --arc-dot-edge); the fill only says "not yet". */
.arc-dot-mark {
  fill: var(--arc-dot);
  stroke: var(--arc-dot-edge);
  /* .34 of the armature's 10, and the first value was .55, which was rendered
     and rejected: at a 108px cell sixteen ringed hexagons out-weighed the
     ribbon and the board read as busy before a child had drawn anything. The
     RATIO is a property of the colour, not of the width, so thinning costs the
     legibility argument nothing — the edge still measures 7.2:1 against the
     board ground. It buys back the hierarchy: ribbon first, dots second. */
  stroke-width: .34;
}
/* BOTH, or a visited cell keeps a ring and the board stops reading as area
   covered — which is the one thing the dots are for. */
.arc-dot-mark[data-done="yes"] { fill: none; stroke: none; }

/* THE TRAIL IS A RIBBON OF EVEN WIDTH WITH NO SWELLING ANYWHERE.
   §3b, and it is structural: a stroke that is the same width from end to end
   cannot draw a shaft with a bulb on it, whatever route a child takes. The
   head is a die-cut SQUARE tile for the same reason — a square bounded by one
   cell is not elongated and cannot be made so. */
.arc-ribbon {
  fill: none;
  stroke: var(--arc-trail);
  /* 4.2 of the armature's 10, so a run of the line is NARROWER than the gap
     beside it (5.8) and the ruling still reads between two parallel runs. At
     5.4 — the first value — a finished board closed up into a blocky maze and
     the one unbroken LINE the whole game is about stopped reading as a line.
     Rendered and looked at, per world and per theme, before it was changed. */
  stroke-width: 4.2;
  stroke-linecap: round;
  stroke-linejoin: round;
  /* WAS .92, AND FLAT PRINTED INK DOES NOT HAVE AN OPACITY. It cost the line
     contrast against the ground for nothing gained: the marks that sit over it
     are legible because they are trapped in --stock, not because the ribbon
     was let through. */
  opacity: 1;
}

/* The head is a die-cut piece: a hard offset shadow, no blur, exactly as every
   other touchable thing in this system. Measured need, not decoration — at
   320px a flat head tile and a wall tile are both filled rounded squares, and
   a child must be able to find themselves on the board at a glance. */
.arc-head-shadow { fill: var(--rule); stroke: none; }
.arc-head-tile {
  fill: var(--arc-trail);
  stroke: var(--rule);
  stroke-width: 1.2;
  vector-effect: non-scaling-stroke;
}
.arc-mark {
  fill: none;
  stroke: var(--stock);
  stroke-width: 1;
  stroke-linecap: round;
  /* ADOPTED FROM arcade.js, WHICH ASKED FOR IT BY NAME. This class carried
     non-scaling-stroke, which pins a stroke to one DEVICE width at any board
     size — right for the armature, wrong for a mark drawn INSIDE the head
     plate at a 48px cell, where it held the character to a scratch. The
     module sets `vector-effect:none` inline as a fix and asks the sheet to
     adopt it so the inline style becomes redundant rather than contradicted. */
  vector-effect: none;
}
.arc-mark-fill { fill: var(--stock); stroke: none; }

/* A shape cut OUT of the head plate — a stripe, a pupil, a window. It is
   filled in the ink the plate is printed in, so it reads as a HOLE in the
   piece rather than as a third colour arriving on the board. */
.arc-mark-cut { fill: var(--arc-trail); stroke: none; }

/* THE RULE-MARKS SCALE WITH THE BOARD, AND THEY ARE PRINTED IN THE RULE INK.
   Two fixes, both found by rendering the themes and looking at them.

   They used to carry vector-effect: non-scaling-stroke, which pins a stroke to
   the same screen width at any zoom. That is right for the armature — a
   hairline should stay a hairline — and wrong for a mark a child has to READ:
   at a 374px board it held them to 1.4 device pixels and the one-way arrows
   came out as scratches. Without it, 1.4 of the armature's 10 units is about
   a tenth of a cell and grows with the board.

   And they are drawn in --rule, the printed near-black, rather than in the
   ink: a mark sits on the pale ground BEFORE the line arrives and on the ink
   ribbon AFTER it, and only the printing colour reads on both.

   ⚠ THAT LAST CLAUSE WAS HALF TRUE AND THE MEASUREMENT SAYS WHICH HALF.
   --rule on the ground is 13.6:1; --rule on the ink RIBBON is 2.17:1 in the
   night theme — under 3:1, at exactly the place a child looks to check whether
   they may leave a square, and only once they have drawn over it, which is why
   nobody saw it. No opaque colour fixes both grounds; the trap does. */
.arc-oneway {
  fill: none;
  stroke: var(--rule);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.arc-portal {
  fill: none;
  stroke: var(--rule);
  stroke-width: 1.3;
  stroke-dasharray: 2.4 1.8;
}
.arc-portal-pip { fill: var(--rule); stroke: none; }

.arc-door {
  fill: none;
  stroke: var(--rule);
  stroke-width: 1.7;
  stroke-linecap: round;
}
/* An opened door keeps its bars as a dashed ghost rather than vanishing: a
   mark that disappears reads as a mistake, and the child should be able to see
   what they unlocked. */
.arc-door[data-open="yes"] { stroke-dasharray: 1.6 1.8; opacity: .5; }
.arc-key {
  fill: none;
  stroke: var(--rule);
  stroke-width: 1.5;
  stroke-linecap: round;
}

/* THE SQUARE THE LINE MUST FINISH ON. arcade.js lists this as already styled
   and it was NOT — it has been carrying its own inline style since it shipped,
   which is the only reason it looked right. Adopting it here makes that inline
   style redundant rather than contradicted, and gives it the trap every other
   rule-mark needs: the head STANDS on the finish square, so this outline
   spends its last and most important moment sitting on the ink. */
.arc-goal {
  fill: none;
  stroke: var(--rule);
  stroke-width: 1.1;
  stroke-dasharray: 2 1.6;
}

/* ------------------------------------------------------------ the world -- */

/* THE BACKDROP IS SCENERY, AND IT IS PRINTED IN THE RULING.
   arcade.js draws the world behind the board — a comb, a reef, a skyline — and
   named these three in its class contract. They belong to the armature's
   weight, not the ink's: scenery that competes with the line is scenery that
   hid the game. The module drops the whole group under prefers-contrast: more
   from its own side, as this sheet already drops its own ruling there; the
   rule below is the sheet keeping its half of that bargain. */
.arc-sky { opacity: .45; pointer-events: none; }
.arc-sky-line { fill: none; stroke: var(--arc-rule); stroke-width: 1.1; }
.arc-sky-fill { fill: var(--arc-rule); stroke: none; }

/* THE WIN MOMENT. arcade.js owns the timing — the group is empty except for
   about 700ms — so this file only says what the piece is MADE of and never how
   long it lasts. The finished line is the reward; the sweep is a light run
   along it in the sheet's own stock, with no blur and no colour the sheet did
   not already have. (An earlier version of this comment described confetti and
   die-cut rings, because the contract at the time named an .arc-burst. It does
   not any more, and a stylesheet describing pieces that no longer exist is how
   the next reader ends up styling a class nothing emits.)

   Nothing here is required for the win to register: the status line says it,
   the pip fills, and the Next puzzle control ungreys. That is what makes the
   reduced-motion block at the foot of this file honest rather than apologetic. */
.arc-spark { pointer-events: none; }
.arc-sweep {
  fill: none;
  stroke: var(--stock);
  /* 2.4, WHICH IS THE MODULE'S OWN VALUE AND NOT A GUESS. The first cut here
     said 4.2 — the ribbon's full width — reasoning that a light running ALONG
     the line should be as wide as it. Rendered, that is a light running
     THROUGH the line: at full width the sweep covers the ink completely and
     the finished path reads as a hollow tube for the length of the moment,
     which is the one unbroken line §3b protects, broken at the exact instant
     a child has just earned it. It never actually shipped that way — an
     inline style beats a stylesheet, so arcade.js's own 2.4 was winning and
     hiding my error — which is precisely the arrangement the class contract
     warns about: the sheet must agree with the inline value, not merely be
     overridden by it, or the day someone deletes the inline style the sheet
     does the damage. Matched deliberately. */
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ---------------------------------------------------------- the words -- */

.arcade-note {
  margin: calc(var(--arc-unit) / 2) 0 0;
  font-size: .95rem;
  line-height: 1.5;
  color: var(--ink-edge);
  max-width: 60ch;
}

/* THE STATUS WAS A STATUS LINE, AND THIS SURFACE IS NOT A DASHBOARD.
   "15 squares still to visit" is the game speaking — the only voice it has
   between one press and the next — and it was set as bold body copy in the
   flow, indistinguishable from the instruction paragraph directly above it. It
   is now what games.css already makes of the same sentence: a caption printed
   along the foot of the sheet, on the ink's own plane, with a die-cut register
   mark at its head so it reads as PRINTED rather than as text that happened.

   Still one string, still `role=status`, still always visible: the sentence a
   sighted child reads and the sentence a screen reader announces are the same
   sentence, because two texts for one fact is two things to keep in agreement
   and one of them goes stale.

   The reserved height stays. It is why the board never jumps under a thumb. */
.arcade-status {
  display: flex;
  align-items: flex-start;
  gap: .6rem;
  margin: var(--arc-unit) 0 0;
  padding: calc(var(--arc-unit) / 2) calc(var(--arc-unit) * .6);
  min-height: 3.2em;           /* reserved, so the board never jumps */
  font-size: 1.02rem;
  font-weight: 700;
  line-height: 1.4;
  color: var(--board);
  background: var(--ink-soft);
  border: 1px solid var(--arc-rule-strong);
  border-radius: var(--die-cut);
  max-width: 62ch;
}
/* The register mark: a die-cut square of ink, the world's own ornament, and
   the thing that tells a glance "this line is the game talking". */
.arcade-status::before {
  content: "";
  flex: 0 0 auto;
  width: .62em;
  height: .62em;
  margin-top: .42em;
  background: var(--ink);
  border-radius: 2px;
}

.arcade-ask {
  margin: 0 0 calc(var(--arc-unit) / 2);
  max-width: 58ch;
  font-size: .95rem;
  line-height: 1.5;
  color: var(--board);
}
/* Dealt onto the sheet rather than ruled out of it: the chooser is the one
   thing standing between a child and the board, so it is kept tight and it
   sits on the ink's plane instead of ringing a large empty rectangle. */
.arcade-chooser {
  border: var(--stroke) dashed var(--arc-rule-strong);
  border-radius: var(--die-cut);
  padding: calc(var(--arc-unit) * .6);
  margin-bottom: var(--arc-unit);
  background: var(--ink-soft);
}

.arcade-score {
  margin: calc(var(--arc-unit) / 3) 0 0;
  font-size: .95rem;
  color: var(--ink-edge);
}

/* --------------------------------------------------------- the controls -- */

.arcade-controls,
.arcade-worldbar,
.arcade-themebar {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin-top: calc(var(--arc-unit) / 2);
}
.arcade-controls { margin-top: var(--arc-unit); }

/* THE BUTTONS WERE ARTICLE BUTTONS AND THE GAME NEEDED GAME BUTTONS.
   Three controls set identically, so the one that MOVES YOU ON looked exactly
   like the one that undoes — and, because Next puzzle is disabled until the
   board is solved, the primary was the WEAKEST thing in the row for all but
   the last second of a puzzle. Two fixes and they are separate: the voice (the
   games' display caps, which is what tells a child this is a control on a
   board and not a link in an article) and the hierarchy (the primary grows to
   take the row; the two undo controls stay at their content width). */
.arcade-btn {
  font: inherit;
  font-family: var(--font-display);
  font-size: .9rem;
  text-transform: uppercase;
  letter-spacing: .04em;
  font-weight: 700;
  min-height: 48px;            /* the widget floor; this will be tapped fast */
  padding: .55rem 1rem;
  background: var(--stock);
  color: var(--board);
  border: var(--stroke) solid var(--rule);
  border-radius: var(--die-cut);
  box-shadow: var(--shadow);
  cursor: pointer;
}
.arcade-btn:hover:not(:disabled) { background: var(--ink-soft); }
.arcade-btn:active:not(:disabled) {
  transform: translate(2px, 2px);
  box-shadow: none;
}
.arcade-btn[data-primary="yes"] {
  background: var(--ink);
  color: var(--stock);
  border-color: var(--ink);
}
/* SCOPED THREE WAYS, AND EACH ONE IS A DEFECT I RENDERED FIRST.
   TO THE CONTROL ROW, because `data-primary` is also how the world bar and the
   theme bar mark the one you are ON, and a selected world has no business
   stretching to fill its row.
   TO THE ENABLED STATE, because Next puzzle is disabled for all of a puzzle
   except its last second — so an unscoped rule made the one control a child
   CANNOT use the widest thing on the sheet, which is the hierarchy backwards.
   It claims the row at the moment it becomes pressable, which turns a layout
   rule into part of the win.
   AND TO THE ROW ITSELF: below 26rem this container is a COLUMN, where
   flex-grow grows the main axis — measured at 320px, the disabled primary came
   out an 11rem-TALL hatched slab. Media queries add no specificity, so the
   narrow override below could not have reached this rule; the min-width guard
   here is what keeps it in its own layout. */
@media (min-width: 26.0625rem) {
  .arcade-controls .arcade-btn[data-primary="yes"]:not(:disabled) { flex: 1 1 11rem; }
}

/* PRINTED ON THE SHEET RATHER THAN DEALT TO THE PLAYER, which is games.css's
   treatment and the reason this one changed. `opacity: .55` over an ink plane
   MEASURED 1.59:1 for the label — the least readable text anywhere on the page
   was the button a child is waiting to be allowed to press. A locked control
   now loses its shadow (so it reads flat rather than pressable), takes the
   armature's own hatching (so it reads printed rather than dealt), and keeps a
   label at 5.9:1. Never colour alone: it also SAYS it is locked. */
.arcade-btn:disabled {
  cursor: not-allowed;
  box-shadow: none;
  color: var(--ink-edge);
  border-color: var(--arc-rule-strong);
  background:
    repeating-linear-gradient(-45deg,
      var(--arc-rule) 0 1px, transparent 1px 6px),
    var(--stock-deep);
}
.arcade-btn:focus-visible {
  outline: var(--stroke) solid var(--ink);
  outline-offset: 2px;
}
.arcade-lock {
  font-weight: 400;
  font-size: .85em;
  text-transform: none;
  letter-spacing: 0;
}

/* ------------------------------------------------------------- the pips --
   Quantity as CELLS FILLED, never a numeral — the games' rule, and the reason
   the strip is here at all. The digit inside a pip is a NAME (which puzzle),
   not an amount; the amount is how many pips are filled.

   IT DID NOT READ THAT WAY, AND THE GAP IS WHY. Twelve separately outlined
   boxes with a numeral in each, spaced 3px apart, is a table of contents — a
   child reads twelve THINGS and then reads their numbers. Butting the cells
   into one ruled strip is what turns them into a measure: the filled run has a
   LENGTH, and length is the thing the armature has already taught them to
   read. It is the same device as the games' .game-stagebar, for the same
   reason, which is also why it is not a new invention. */

.arcade-pips {
  display: flex;
  flex-wrap: wrap;
  gap: 0;                       /* butted: one strip, not twelve boxes */
  list-style: none;
  margin: 0;
  padding: 0;
  width: fit-content;
  max-width: 100%;
  border: 1px solid var(--rule);
  border-radius: var(--die-cut);
  background: var(--stock);
  overflow: hidden;             /* so the fills are die-cut by the strip */
}
.arcade-pip {
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-size: .78rem;
  /* The divider is the armature between two cells, not a box around one — and
     it is printed at --ink-edge because --arc-rule-strong MEASURED 1.89:1 on
     the strip's own ground, which is a ruler you cannot read the gradations
     of. Inside a filled run it goes quiet against the ink on purpose: a meter
     whose filled cells stay visibly separate is twelve boxes again, and the
     length is the thing being read. games.css does the same for the same
     reason (.game-meter-cell takes the ink as its border once it is on). */
  border-right: 1px solid var(--ink-edge);
  background: var(--stock);
  color: var(--ink-edge);
}
.arcade-pip:last-child { border-right: 0; }
/* MEASURED, and the middle rung was the one that failed: --stock on the 58%
   mix was 2.34:1. The printing colour clears every mix (10.3:1 at one star,
   6.4:1 at two) and only the solid ink needs the stock back (5.0:1), so the
   rule is "board on a mix, stock on the ink". */
.arcade-pip[data-stars="1"] {
  background: color-mix(in srgb, var(--ink) 28%, var(--stock));
  color: var(--board);
}
.arcade-pip[data-stars="2"] {
  background: color-mix(in srgb, var(--ink) 58%, var(--stock));
  color: var(--board);
}
.arcade-pip[data-stars="3"] { background: var(--ink); color: var(--stock); }
/* THE CURRENT PUZZLE IS A CUT CORNER AND NOTHING ELSE, and the deleted line
   is the reason this comment is longer than the rule. It also set
   `color: var(--board)`, which is a later rule at equal specificity to the
   star fills — so the moment the puzzle you are ON is also a puzzle you have
   THREE-STARRED, the label lost its --stock and MEASURED 2.17:1 on solid ink:
   the single least readable thing on the sheet, on the one cell a child is
   looking for. The star rules own the label colour; this rule owns the shape.
   The games' first cut ran the diagonal corner to corner and the active turn
   read as cancelled, which is why it is a corner and not a stroke. */
.arcade-pip[aria-current="step"] {
  clip-path: polygon(0 0, 100% 0, 100% 72%, 72% 100%, 0 100%);
  font-size: .86rem;
}

/* ------------------------------------------------------- the page around -- */

/* THE PROSE AROUND THE BOARD WAS THE ARTICLE'S, NOT THE GAME'S. These are
   emitted by the page builder rather than by the module, and they were
   inheriting 19px/1.65 body copy — so the sheet arrived after two paragraphs
   set exactly like the 84 explainers, which is the register the games surface
   exists to refuse. The summary is the game's one claim; the meta is a
   caption. Neither is reading matter. */
.arcade-page { margin: 0; }
.arcade-page .arcade-summary {
  font-size: 1.12rem;
  font-weight: 700;
  line-height: 1.4;
  max-width: 54ch;
  margin: 0 0 calc(var(--arc-unit) / 3);
}
.arcade-page .arcade-meta {
  font-family: var(--font-display);
  font-size: .7rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-edge);
  margin: 0;
}

/* -------------------------------------------------------------- motion -- */

/* THE MOVEMENT ON THIS SHEET IS THE PRESS AND THE WIN, AND NOTHING ELSE.
   An earlier version of this block said there was no animation here at all,
   and that was true when it was written: a square fills the instant it is
   pressed and the ribbon is redrawn, so state arrives without travel for
   everybody. arcade.js has since added a win moment — .arc-spark and the light
   that runs the finished line inside it, alive for about 700ms — so the
   claim would now be false, and a comment asserting a property the file no
   longer has is worse than no comment.

   THE TIMING IS THE MODULE'S, SO THIS BLOCK IS NOT THE WHOLE FIX and does not
   pretend to be: it turns off the 2px press offset and any transition or
   animation this sheet could put on the win pieces. If the module drives that
   moment on a timer rather than in CSS, honouring the preference is its call
   to make. It is written down here so the next reader looks there rather than
   assuming this block covered it.

   Nothing is HIDDEN under reduced motion. The win is already carried by the
   status line, by the pip that fills and by the control that ungreys, so a
   child who has asked for stillness is told they won in words and in the
   strip — which is the whole of DESIGN.md's rule: state still changes, it
   simply arrives without travel. */
@media (prefers-reduced-motion: reduce) {
  .arcade-btn:active:not(:disabled) { transform: none; }
  .arc-spark,
  .arc-sweep {
    transition: none !important;
    animation: none !important;
  }
}

/* THE ONE MARK ON THIS SHEET THAT DOES NOT CLEAR 3:1, STATED RATHER THAN
   QUIETLY LEFT OUT OF THE REPORT. The board's armature measures about 1.8:1
   against the board ground, and it cannot be fixed by choosing a better
   colour: the ground is near-stock at relative luminance ~0.80, so 3:1 needs
   a line at <= 0.233 — a mid-dark grid in every cell of every board. That is
   a cage drawn over the game, and it would also outrank the dots and the
   ribbon, which ARE the marks a child has to read (7.2:1 and 4.7:1).
   It is judged as ruling rather than as an informational mark: every cell
   boundary it draws is also carried by the dot printed inside that cell, by
   the wall tiles, and by the ribbon's own geometry, so nothing is conveyed by
   the grid ALONE. Under prefers-contrast: more the sheet's ruling is dropped
   entirely and the dots go solid, which is the setting where a reader who
   needs the boundaries gets them from the marks instead. */

@media (prefers-contrast: more) {
  [data-arcade] { background-image: none; border-width: 4px; }
  .arc-dot-mark { fill: var(--ink-edge); stroke: none; }
  /* The board's ruling goes up here and only here. Everywhere else it is
     deliberately subordinate to the marks (see the note above); a reader who
     has asked the operating system for more contrast has asked for exactly
     this, and it costs nothing anywhere else. */
  .arc-rule-path { stroke: var(--ink-edge); }
  .arcade-board { box-shadow: var(--shadow); }
  .arcade-pip { border-right-color: var(--rule); }
  .arcade-pips { border-width: 2px; }
  .arcade-status { background: var(--stock); border-width: 2px; }
  .arcade-btn:disabled { background: var(--stock-deep); text-decoration: line-through; }
  .arc-sky { display: none; }
}

/* ------------------------------------------------------------- narrow -- */

@media (max-width: 400px) {
  /* .55rem, and the third decimal place is the point. MEASURED: at 320px the
     board is offered 245px at .7rem padding, which is 47.7px per square on a
     five-wide board — over DESIGN.md's 44px floor but a third of a pixel under
     the 48px one it sets for anything inside a widget. Trimming the sheet's
     own padding by 2.4px a side buys 48.8px and clears it outright. The board
     is the artifact; the margin around it is the thing that gives way.

     THE PLATE GOES ENTIRELY, OUT OF THE SAME BUDGET AND FOR THE SAME REASON.
     MEASURED on a REAL 5x7 board — the widest this generator can draw, reached
     by seeding the module's own save to rung 12 rather than by reasoning about
     a 4-wide one — at a 305px viewport, the scrollbar case the paragraph above
     is about: a 3px plate each side left the cell at 47.48px. Over DESIGN.md's
     44px floor, half a pixel under the 48px one it sets inside a widget, and
     half a pixel is still under. With the plate at 0 the same board measures
     48.68 x 48.68, and 51.68 at a 320px viewport, in all four themes.

     AND THE CELL IS THE TAP TARGET, not the mark drawn inside it. arcade.js
     hit-tests with `floor(((ev.clientX - rect.left) / rect.width) * stage.w)`
     over the WHOLE svg rect, so a press anywhere in a cell's rectangle lands
     on that cell — which is why this number, and not the size of a dot, is the
     one that has to clear the floor.

     A printed margin is worth less than a square a thumb can hit. The double
     rule is a nicety; the phone does without it rather than the child doing
     without the square. */
  [data-arcade] { padding: .55rem; background-image: none; --arc-plate: 0px; }
  /* With no plate there is no band for the inner hairline to sit in, so the
     board keeps only its offset shadow rather than drawing a rule flush
     against the 3px border, which reads as a thicker border and not a plate. */
  .arcade-board { box-shadow: var(--shadow); }
  .arcade-btn { padding: .5rem .7rem; }
  /* THE STRIP STAYS ONE STRIP. At 320px twelve 30px cells need 360 and are
     offered about 264, so they wrapped to 8 + 4 — and a ruler broken across
     two lines is twelve boxes again, which is the exact reading this component
     was rebuilt to stop. They share the row instead: the cells narrow, the
     LENGTH of the filled run still means the same thing, and the digit (a
     name, not an amount) gives up the space. */
  .arcade-pip {
    width: auto;
    height: 30px;
    flex: 1 1 0;
    min-width: 0;
    font-size: .68rem;
  }
  .arcade-pips { width: 100%; }
}

@media (max-width: 26rem) {
  /* The controls become full-width rows: a 48px target a thumb misses is a
     48px target that failed. games.css does the same at the same width. */
  .arcade-controls { flex-direction: column; align-items: stretch; }
  .arcade-controls .arcade-btn { width: 100%; flex: 0 0 auto; }
}

/* THE JAVASCRIPT-OFF DOCUMENT IS FOR READERS WHO HAVE JAVASCRIPT OFF.
   With the game live it was still on screen, so a child playing the arcade
   read a paragraph telling them they need JavaScript to play the thing they
   were playing -- above the board, taking the top third of a phone screen.
   The twelve games had the identical defect and it was fixed there this
   morning; nobody asked whether the arcade shared it. It did.
   The prose is NOT deleted and the element stays in the DOM: it is the real
   no-JS document, it carries real content, and the drivers assert it survives
   mounting. It is hidden only once something is actually running. */
[data-arcade][data-arcade-state="live"] .arcade-intro { display: none; }

/* ==========================================================================
   THE /play/ CARD — the longer kind, sitting beside the article games
   ==========================================================================

   WHY THIS IS NOT `.card`. /play/ merges what used to be two nav tabs, and
   the only real information that split carried is that these two things are
   different: a game teaches one article's idea in two minutes, this one has
   five worlds and remembers where you got to. If both were drawn as the same
   card in the same grid, the merge would have thrown that away and left a
   child guessing which of sixteen tiles is the long one.

   So it is a BANNER, not a tile: full width, mark on the left, text to the
   right, one per row. The games below stay a three-column grid of vertical
   tiles. The difference is legible before a word is read, which is the whole
   point, and it is why this file grew a card rather than cards.css growing a
   modifier — `.card` belongs to the listings and this is deliberately not one.

   THE INK IS --board, AND THAT IS THE LOAD-BEARING CHOICE. This card carries
   no subject, so with no rule it would inherit :root's --ink (#1f6f78), which
   is one hex step from --ink-science (#12676f) — it would read as a slightly
   wrong Science card. The tempting fix, handing the record a category so it
   earns a subject motif for free, is worse: it would bind standalone play to
   a subject ink and teach a child that the six-colour model means nothing.
   --board is the site's own printed near-black, belongs to no region by
   construction, and runs 14.6:1 on stock. */

.play-arcade-list {
  list-style: none;
  margin: 0 0 var(--gap);
  padding: 0;
  display: grid;
  gap: var(--gap);
}

.play-arcade-card {
  --ink: var(--board);
  display: grid;
  grid-template-columns: 7.5rem minmax(0, 1fr);
  align-items: stretch;
  background: var(--stock);
  border: var(--stroke) solid var(--rule);
  border-radius: var(--die-cut);
  box-shadow: var(--shadow);
  overflow: hidden;
  position: relative;
}

/* The ink block, bled to the card's edges exactly as .card-mark is, so the
   two kinds are plainly the same site drawn to different plans. */
.play-arcade-mark {
  display: grid;
  place-items: center;
  background: var(--ink);
  border-right: var(--stroke) solid var(--rule);
  padding: 1rem;
}

.play-arcade-mark svg {
  display: block;
  width: 3.6rem;
  height: 3.6rem;
  color: var(--stock);
}

.play-arcade-text { padding: 1rem; display: grid; align-content: start; gap: .45rem; }

.play-arcade-text > a {
  font-family: var(--font-display);
  font-size: 1.25rem;
  color: var(--board);
  text-decoration: none;
}

/* The whole banner is the target, matching .article-list .card > a::after. */
.play-arcade-text > a::after { content: ""; position: absolute; inset: 0; }
.play-arcade-text > a:hover { color: var(--ink-edge); text-decoration: underline; }
.play-arcade-card:focus-within { outline: var(--stroke) solid var(--board); outline-offset: 3px; }
.play-arcade-text > a:focus-visible { outline: none; }

/* What you actually DO. Bigger than a game card's summary because it is the
   fact a child chooses on, and it is a sentence rather than a category. */
.play-arcade-do {
  margin: 0;
  font-size: 1rem;
  color: color-mix(in srgb, var(--board) 82%, var(--stock));
}

/* Longer, keeps your place, not timed — the three things that make it the
   other kind. Same weight and size as .card-subject so the two card shapes
   agree about what a small bold fact line looks like. */
.play-arcade-facts {
  margin: .15rem 0 0;
  font-size: .8rem;
  font-weight: 700;
  color: var(--ink);
}

.play-longer > h2 { margin-bottom: .35rem; }

/* Phone: the two-column banner becomes a short ink band above the text, which
   is the same rearrangement .card already makes and keeps the mark visible
   rather than dropping it. Matches the breakpoint site.css uses. */
@media (max-width: 34rem) {
  .play-arcade-card { grid-template-columns: minmax(0, 1fr); }
  .play-arcade-mark {
    border-right: 0;
    border-bottom: var(--stroke) solid var(--rule);
    padding: .6rem;
  }
  .play-arcade-mark svg { width: 2.6rem; height: 2.6rem; }
}
