:root {
  --bg: #16171b;
  --panel: #1e1f25;
  --ink: #d8d9e0;
  --dim: #8a8c98;
  --line: #2f3038;
  --accent: #79b98a;
  --danger: #d97070;

  /* The gilt moulding, shared by the World Record card and the Hall of Fame
     popup so the two cannot drift apart. **It crosses light-dark-light-dark
     rather than fading evenly** -- that alternation is what reads as a turned
     frame instead of a painted stripe -- and it is lit from the top left like
     everything else on the page.

     Painted into a transparent border by the two-layer background trick; see
     .world-card.has-record for how, and use --gilt-lip with it. */
  --gilt: linear-gradient(145deg,
    #f7e7b8 0%,
    #d9b451 14%,
    #9d7526 34%,
    #eed697 52%,
    #b18c33 70%,
    #7d5f1c 86%,
    #d9b451 100%);

  /* The lip where the frame meets the picture, the frame's own outer edge, and
     the depth that hangs it off the page. */
  --gilt-lip: inset 0 0 0 1px rgba(0, 0, 0, 0.55),
              0 0 0 1px rgba(0, 0, 0, 0.55);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  /* Bottom room for the fixed scale control, so it never covers page content
     once the sidebar wraps below the board on a narrow screen. */
  /* The top figure clears the centred .site-head -- the logo with the H1 under
     it -- which is fixed and so takes no room of its own. It was 133px while
     the logo stood alone; the H1 moved under it on 2026-08-31 and added about
     fifteen pixels. See the note on .site-head. */
  padding: 152px 16px 84px;

  /* The level and its heading sit in the middle of the window rather than
     hanging off the top. `safe center` is repeated after plain `center` on
     purpose: browsers that understand it will not push an over-tall board off
     the top of the page where it cannot be scrolled to, and browsers that do
     not simply keep the plain value. */
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  justify-content: safe center;
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.5 "Segoe UI", system-ui, -apple-system, sans-serif;
}

.app {
  max-width: max-content;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

.dim { color: var(--dim); }

/* ---------------------------------------------------------------- header */

.meta { text-align: center; margin: 0; }

/* The level's own name. An h2 in the markup now -- the page's h1 is the line
   above it, which names the game rather than the board on screen -- but it is
   still the biggest thing in the header and keeps every bit of its size. */
/* **The page's H1**, above the level name and set like a tagline rather than
   like a heading: small caps, well spaced, and quiet enough that it reads as
   part of the logo above it instead of competing with the level title below.

   The page needs an H1 that says what the site is -- the level name that used
   to hold the tag is filled in by script, so a crawler found it empty -- and
   the game needs its board. One line at this size is what those two came to:
   about sixteen pixels, which fitCell takes off the board's height. Anything
   longer belongs on one of the four written pages. */
/* The game's H1, and the game's alone -- the four written pages carry the logo
   but not this line, which is why it can be positioned outright here without
   anything to undo elsewhere.

   **Fixed under the logo rather than flowing after it.** The logo is fixed, so
   there is nothing in the flow for this to follow; 130px is where the logo's
   box ends at 210px wide, and 79px is where it ends at the 120px the phone gets
   -- the two numbers are the logo's size and nothing else, so they move when it
   does. body's padding-top clears both together. */
.site-tag {
  position: fixed;
  top: 130px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 5;
  pointer-events: none;
  white-space: nowrap;
  margin: 0;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  line-height: 1.2;
  color: var(--dim);
  opacity: 0.8;
}

@media (max-width: 720px) {
  .site-tag { font-size: 9.5px; letter-spacing: 0.16em; top: 79px; }
}

#level-name {
  margin: 0;
  font-size: 22px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  /* Tight, so the credit under it reads as part of the same block. At the
     body's inherited 1.5 the 22px name carried a 33px line box and the author
     line sat adrift below it, which made the two look like two separate
     announcements rather than a title and its byline.

     1.3 rather than the 1.15 it was first set to: closed right up the two
     touched, and a byline wants a little air under the name it belongs to.
     Jeff's call, 2026-08-30. */
  line-height: 1.3;
}

/* The level's place in the Levels list, in front of its name.
   `text-transform: none` on purpose: #level-name is set in caps, and the number
   is a label rather than part of the title -- left in caps it read as a second
   name shouting over the first. Grey and lighter for the same reason, so the
   eye lands on RUBICON and picks the number up afterwards. The trailing space
   is in the text, not a margin, so it collapses with the name when the number
   is blank. */
.level-no {
  color: var(--dim);
  /* Smaller than the name it labels. At the heading's full 22px the number was
     the same size as the level and competed with it; a caption in front of a
     title is what it is. 0.6em was the first try and read a shade too quiet
     next to a 22px name -- 0.66em still gives way to it and is easier to pick
     out. Jeff's call, 2026-08-31. */
  font-size: 0.66em;
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0.06em;
  /* Lifted a pixel off the shared baseline. Two sizes of type on one line sit
     on that baseline correctly and still look wrong: the smaller one reads as
     having sagged, because the eye lines up the middles of the words and not
     their feet. One pixel is the whole correction. */
  position: relative;
  top: -1px;
}

/* The credit under the level's name. The top margin was 3px, which sat the
   byline right up against the heading once the number was added in front of it;
   8px is the little bit of air Jeff asked for on 2026-08-31 and no more -- the
   two are still one block, a title and its byline, and any more space here
   makes them read as two announcements. It is also height taken off the board,
   since fitCell subtracts what .meta measures. */
.meta p { margin: 8px 0 0; font-size: 13px; line-height: 1.35; }

/* The credit's flag, a size up from the name beside it and with its own gap.
   At the inherited size the chip read as punctuation rather than as a flag. */
.meta .flag-code {
  font-size: 11px;
  padding: 1px 5px;
  margin-right: 3px;
}

.meta .flag { font-size: 1.3em; margin-right: 3px; }

/* The origin mark rides a little higher here than the base rule sets it, and
   sits closer to the name.

   That baseline drop is tuned for the score list, where the mark sits in a
   dense row of small type; in this credit line, alone under the level name
   and with the board's empty space below it, the same drop reads as the mark
   having slipped off the line.

   The gap goes for the same reason. The base rule's 0.16em is on top of the
   space character the markup already puts after the mark, which in a dense row
   is right; here, at the credit's larger type and with nothing crowding it, the
   two together left the mark floating away from the name it belongs to. 0.02em
   keeps a hair of separation and lets the space character do the rest. Jeff's
   call, 2026-08-31. Scoped to .meta, so the score list and the info card are
   untouched. */
.meta .origin-mark { vertical-align: -0.12em; margin-right: 0.02em; }

/* ------------------------------------------------------- board and layout */

.play {
  display: flex;
  align-items: flex-start;
  gap: 28px;
  flex-wrap: wrap;
  justify-content: center;
}

/* No panel, border or padding: the area outside the level's walls should be
   nothing but page background, so the level reads as its own silhouette. */
.board-wrap {
  overflow: auto;
  max-width: 100%;
  /* The anchor for #move-hint, which is drawn over the board and has to line
     up with a particular square on it. No offsets, so nothing else moves. */
  position: relative;
}

#board {
  display: block;
  touch-action: none;   /* swipes drive the game, not the page */
}

/* ------------------------------------------------------ first-run move hint */

/* Shown once, to a player who has never been here before, straight after the
   About card is closed. It is the only thing on the page that says the board
   can be touched at all: the arrow keys are not written anywhere else, and a
   swipe is invisible by nature.

   Everything here is decoration over a live board, so it never takes the
   pointer -- a tap or a swipe on the hint has to reach the canvas underneath
   it, or the very gesture being taught would be swallowed by the thing
   teaching it. Below the cards (10) and the scale control (5) so anything the
   player opens covers it.

   --hint-size is set from script: it is the board cell size, floored so the
   arrow is still legible on a board scaled right down. */
.move-hint {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 3;
  /* Only ever seen for the frame before positionMoveHint sizes it against
     the board's own cell. */
  --hint-size: 24px;
}

/* Positioned by script: `left` and `top` put its top-centre on the edge of
   the player's square that the move leaves by, and the rotation turns it to
   face that way. transform-origin pins the turn to that same point, so all four
   directions pivot about the player rather than swinging away from them. */
.move-hint-arrow {
  position: absolute;
  width: var(--hint-size);
  height: calc(var(--hint-size) * 1.375);   /* the viewBox's own 32:44 */
  transform-origin: 50% 0;
  transform: translateX(-50%);
}

.move-hint-arrow svg {
  display: block;
  width: 100%;
  height: 100%;
  /* The move itself, on a loop: out of the player's square, across the one
     next to it, and gone. A static arrow says which way; this says go. */
  animation: move-hint-step 1.6s ease-in-out infinite;
}

.move-hint-arrow path {
  fill: #57d47c;
  /* Dark edge and a green glow around it. The floor is a photograph, so the
     arrow has to carry its own contrast rather than trust what is behind it. */
  stroke: rgba(0, 0, 0, 0.65);
  stroke-width: 2;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 6px rgba(87, 212, 124, 0.75));
}

@keyframes move-hint-step {
  0%   { transform: translateY(-16%); opacity: 0; }
  18%  { transform: translateY(-8%);  opacity: 1; }
  64%  { transform: translateY(52%);  opacity: 1; }
  100% { transform: translateY(80%);  opacity: 0; }
}

/* Which end of the board the caption sits at is script's call -- it goes to
   whichever end the arrow is not using -- so both edges are left to the inline
   `top`/`bottom` it sets. */
.move-hint-text {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  margin: 10px 0;
  max-width: 92%;
  width: max-content;
  padding: 8px 14px;
  border: 1px solid rgba(87, 212, 124, 0.55);
  border-radius: 999px;
  background: rgba(12, 14, 16, 0.88);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
  color: var(--ink);
  font-size: 13px;
  line-height: 1.35;
  text-align: center;
  text-wrap: balance;
}

.move-hint-text b {
  color: #7fe39b;
  font-weight: 600;
  white-space: nowrap;
}

/* Asked not to be animated: keep the arrow, drop the travel. It still points
   the way, which is the half of the message that cannot be written down. */
@media (prefers-reduced-motion: reduce) {
  .move-hint-arrow svg { animation: none; }
}

/* The right-hand column: World Record, the player's score, the status line, the
   arrow pad and the controls.

   **Centred down its own height rather than stacked from the top.** `.play`
   aligns its children to flex-start, so this column has to opt into the row's
   full height itself -- `align-self: stretch` -- before `justify-content` has
   anything to centre within. Without the stretch the column is only as tall as
   its contents and centring does nothing at all.

   The top padding that used to hold it off the board is gone with it; it was
   there to align the first card with the board's top edge, which is not where
   the column starts any more. */
.side {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  align-self: stretch;
  gap: 14px;
}


/* Match the HUD, so the column has one edge rather than three. Follows
   .hud's min-width; change them together. */
.status, .hint { max-width: 300px !important; }

.status {
  margin: 0;
  min-height: 1.5em;
  max-width: 190px;
  font-size: 13px;
  text-align: center;
}

.status.won { color: var(--accent); font-weight: 600; }
.status.error { color: var(--danger); }

/* ------------------------------------------------------------------- hud */

.hud {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px 22px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 6px;
  /* Wide enough for the World row -- flag, moves and time on one line -- so the
     panel does not resize when a record arrives from the server. Matches
     .scoreboard's width; see the note there. */
  min-width: 300px;
}

/* Card title, matching the World Record card's, and ruled off from the rows
   it heads so the three numbers still read as one block. */
.hud-title {
  text-align: center;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--line);
  color: var(--ink);
}

.stat {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
}

.stat .label {
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--dim);
}

/* ------------------------------------------------------------ button pad */

/* A true cross. Down used to share the middle row with left and right, which
   put it in the centre of the pad rather than below it; it now has a row of
   its own so the four arrows are symmetrical about the middle cell, and that
   middle cell holds the sound indicator. */
.pad {
  display: grid;
  grid-template-columns: repeat(3, 52px);
  grid-template-rows: repeat(3, 44px);
  gap: 6px;
}

.arrow.up    { grid-area: 1 / 2; }
.arrow.left  { grid-area: 2 / 1; }
.arrow.right { grid-area: 2 / 3; }
.arrow.down  { grid-area: 3 / 2; }

/* The sound switch in the middle of the cross.

   **It carries none of the chrome the arrows around it have** -- no panel
   background, no border, no radius -- and that is deliberate: it is a
   different kind of thing to the four arrows, it is not part of moving, and
   giving it the same box would read as a fifth direction. It answers to the
   pointer instead, which is what says it can be pressed.

   Sits dim while sound is off and takes the accent green when it is on: the
   colour that means "yours" everywhere else on the page. The pulse below is a
   brighter green again -- #57d47c, the first-move arrow's -- because it is
   asking to be looked at from across the page rather than reporting a state. The struck-through
   speaker says the same thing again for anyone who cannot see the difference.

   The `button` reset further down this file is undone rather than worked
   around, because everything it sets -- padding, border, background -- is
   wrong here. It loses on specificity rather than on order: a class beats a
   bare type selector wherever the two sit. */
.pad-audio {
  grid-area: 2 / 2;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 4px;
  background: none;
  /* The resting colour, held in a property rather than written straight into
     `color`, because the pulse below has to return to it. A keyframe cannot
     ask what colour the element would otherwise be, so hard-coding --dim at
     0% and 100% would have the pulse end by turning an ON switch grey and
     leaving it there until the class came off. */
  --pad-audio-rest: var(--dim);
  color: var(--pad-audio-rest);
  cursor: pointer;
  transition: color 120ms ease;
}

.pad-audio:hover,
.pad-audio:active { background: none; color: var(--ink); }
/* The global `button:active` rule nudges every button down a pixel. Here that
   would jog the centre of the cross while the four arrows around it held
   still, which reads as a misprint rather than as a press. */
.pad-audio:active { transform: none; }

.pad-audio:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* On, which is the default -- see js/audio.js. The slash goes and the speaker
   takes the accent, so the two states differ in shape as well as in colour. */
.pad-audio.is-on { --pad-audio-rest: var(--accent); }
.pad-audio.is-on:hover { color: var(--accent); }
.pad-audio.is-on .slash { display: none; }

/* Beats display: flex above, which would otherwise keep the switch on screen
   on a browser with no WebAudio for js/main.js to hide it from. */
.pad-audio[hidden] { display: none; }

/* Three green pulses when the board the player is going to play on first comes
   into view, so somebody who has never noticed there is sound in the game
   notices there is a switch for it. App.attractAudio adds the class and takes
   it off at the end, or earlier if the pointer reaches the button -- and it
   runs once a session and never again, because an indicator that never settles
   stops being an invitation and becomes a fault to be ignored.

   **A drop-shadow on the icon rather than a ring around the button.** The four
   arrows around this are solid boxes and it is not one; a spread box-shadow
   would draw exactly the panel this control deliberately does without, and put
   a green rectangle in the middle of the cross. Filtered, the glow takes the
   shape of the speaker itself and spills out past it, which is the "glow
   outward" that was asked for. It costs no layout either way, so nothing in
   the pad moves while it runs. */
@keyframes audio-attract {
  0% {
    color: var(--pad-audio-rest);
    filter: drop-shadow(0 0 0 rgba(87, 212, 124, 0));
    transform: scale(1);
  }
  35% {
    color: #57d47c;
    filter: drop-shadow(0 0 16px rgba(87, 212, 124, 1));
    transform: scale(1.22);
  }
  100% {
    color: var(--pad-audio-rest);
    filter: drop-shadow(0 0 0 rgba(87, 212, 124, 0));
    transform: scale(1);
  }
}

.pad-audio.attract { animation: audio-attract 1.5s ease-out 3; }

/* Anyone who has asked not to be waved at is not waved at: the pulsing goes
   and a still green halo stands in for it, so the switch is still pointed out.

   **The halo, and not the accent fill .help-fab.attract falls back to.** Green
   IS the on state here -- see .is-on -- so recolouring the speaker while it is
   still struck through would have the switch claiming to be in the state it is
   asking to be put into. A glow around a dim speaker says "look here" without
   saying anything untrue about the sound.

   With no animation there is no animationend either, which is why
   App.attractAudio does not rely on one to take the class off again. */
@media (prefers-reduced-motion: reduce) {
  .pad-audio.attract {
    animation: none;
    filter: drop-shadow(0 0 13px rgba(87, 212, 124, 0.95));
  }
}

/* Sized against the arrow glyphs around it rather than to a round number:
   at 23px it read as a smaller sibling of the four arrows instead of as the
   centre of the cross. `overflow: visible` matters here -- the wave arc and
   the slash are drawn slightly outside the 24 viewBox. */
.pad-audio svg { width: 28px; height: 28px; overflow: visible; }
.pad-audio .cone { fill: currentColor; }
.pad-audio .wave {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
}
/* Drawn last and a shade heavier, so it reads as a line struck through the
   speaker rather than as one more sound wave. */
.pad-audio .slash {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}

button {
  font: inherit;
  color: var(--ink);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 8px 14px;
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

button:hover { background: #272932; }
button:active { background: #303341; transform: translateY(1px); }

.arrow { padding: 0; font-size: 15px; }

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

/* Undo and Restart, both the width of the wider of the two labels.

   **Grid, not flex, and the reason matters.** .side lays its children out with
   `align-items: center`, so this row shrink-wraps to its content: there is no
   free space for flex to hand out, and every flex item's default
   `min-width: auto` floors it at its own text width. `flex: 1 1 0` therefore
   changes nothing -- "Restart" stays at its natural width and "Undo" takes
   whatever is left, which is the very thing being fixed.

   Two `1fr` tracks in an auto-width grid are sized to the widest item in
   either of them, so both come out at "Restart"'s width and neither is
   squeezed. Give the row a definite width instead and flex would work too,
   but the row would then stretch to the 300px cards above it. */
.controls {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  align-items: center;
  gap: 10px;
}

/* Fixed to the bottom-right of the viewport rather than sitting in the sidebar.
   The slider resizes the board, so in flow it reflowed the page on every drag
   and moved out from under the pointer. Fixed positioning is relative to the
   viewport, so neither the board size nor scrolling can shift it. */
.scale {
  position: fixed;
  right: 16px;
  bottom: 16px;
  z-index: 5;              /* below the sign-in gate, which is 10 */
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 14px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 6px;
  font-size: 12px;
  color: var(--dim);
}

/* accent-color recolours the filled track and the thumb together. Left to the
   browser default this is a saturated blue, the loudest thing on a page that
   is otherwise greys and one green -- and it is a comfort control, not part
   of the game. */
.scale input {
  width: 164px;
  accent-color: var(--dim);
}

/* The widest reading is "72 px". Reserving that width stops the slider from
   shifting sideways as the number gains or loses a digit mid-drag. */
#scale-value { min-width: 40px; text-align: right; }

.hint {
  margin: 0;
  max-width: 190px;
  font-size: 12px;
  text-align: center;
}

/* How many world records the player holds, in the card's top corner. Out of
   the flow so the centred name below it stays centred. */
.world-count {
  position: absolute;
  top: 10px;
  right: 11px;
  display: flex;
  align-items: center;
  gap: 3px;
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
}

.world-count[hidden] { display: none; }

.count-trophy {
  width: 13px;
  height: 13px;
  fill: #d9b451;   /* the World Record card's gold, meaning the same thing */
}

/* ---------------------------------------------------------------- player */

/* Head of the scoreboard card, above the level list and ruled off from it. */
.player {
  margin: 0 0 10px;
  padding: 0 0 10px;
  font-size: 13px;
  display: block;
  text-align: center;
  border-bottom: 1px solid var(--line);
}

.player-nick {
  display: block;
  font-weight: 600;
  color: var(--ink);
}

.player-nick .flag-code {
  color: var(--ink);
  font-size: 10px;
  padding: 1px 5px;
  margin-right: 4px;
}

.player-nick .flag { font-size: 1.25em; margin-right: 4px; }

.player-place {
  display: block;
  margin-top: 3px;
  font-size: 11px;
  line-height: 1.45;
  color: var(--dim);
}

/* An author `display` beats the UA stylesheet's rule for [hidden], so any
   element that both sets display and toggles hidden needs this. */
.player[hidden] { display: none; }

/* ------------------------------------------------------------------ link */

.link {
  background: none;
  border: 0;
  padding: 0;
  font-size: 12px;
  color: var(--dim);
  text-decoration: underline;
  cursor: pointer;
}

.link:hover { color: var(--ink); background: none; }

/* ------------------------------------------------------------ scoreboard */

/* Left of the board: the player's levels, most recently played first. */
.scoreboard {
  /* Same as the HUD, so the board sits between equal columns. Both were 236px
     until the rows had to hold a level name, a flag and an author credit on
     one line, 268px until every row grew a gutter on its left, and 284px until
     that gutter was widened to set the row numbers apart (2026-08-22). 300px
     is what it takes for the wider gutter to cost the level names nothing:
     300 - 12 padding - 16 scrollbar gutters - 45 row border and padding leaves
     227px for name, credit and controls, which is what they had at 284px with
     the old 21px gutter.
     **Widen them together or the board stops being centred** -- this, .hud's
     min-width, and --side-w in the pinned-panel media query. */
  width: 300px;
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  /* As tall as the board beside it. `.play` aligns its children to the top, so
     this one opts into the row's full height on its own; the level list inside
     takes the slack and scrolls when there are more levels than fit. Wrapped
     below the board on a narrow window it simply matches its own row. */
  align-self: stretch;
  position: relative;   /* anchors .world-count */
  /* Left padding cut to match the right on 2026-08-22 so the row numbers can
     sit further out. It was 14px, which held them in against the panel edge.
     The width above is unchanged and `box-sizing` is border-box, so the board
     between the two columns stays centred. */
  padding: 12px 6px 12px 6px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 6px;
}

.scoreboard h2 {
  /* Space above it separates the card's two halves -- who is playing, and what
     they have played. */
  margin: 8px 8px 8px 0;
  text-align: center;
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--dim);
  flex: 0 0 auto;
}

.scoreboard ul {
  margin: 0;
  padding: 0;
  list-style: none;
  overflow-y: auto;
  flex: 1 1 auto;
  scrollbar-width: thin;
  /* **This is what makes the rows sit the same distance from both edges of the
     card.** The scrollbar takes real width out of the right-hand side, so a
     list padded evenly still reads lopsided -- the rows ended up 8px from the
     left and 16px from the right. `both-edges` reserves the bar's width on the
     left as well, whether or not the bar is showing, so the two margins match
     and stay matched when the list is short enough not to scroll. */
  scrollbar-gutter: stable both-edges;
}

/* What the row says on the left, its controls on the right. Every row carries
   the current-level rule as a transparent border, so marking one does not
   shift the text of the others. */
.scoreboard li {
  position: relative;   /* anchors .score-num */
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  /* The left padding is a gutter for the row's number, which is positioned into
     it rather than laid out in the row -- so the number costs the name and the
     credit beside it no width, and both of the row's lines keep one left edge.
     It held the solved tick until 2026-08-22; see refreshScores for why that
     went. Take the gutter away and the numbers land on top of the names.

     **The gutter is wider than the number on purpose.** 36px of it against a
     16px number leaves 20px of clear air before the name starts, which is what
     sets the numbers apart as their own column rather than reading as part of
     the level's name. Jeff's call, 2026-08-22; it was 21px, which left 5px and
     was far too tight. The card was widened to 300px to pay for it. */
  padding: 7px 5px 7px 36px;
  /* All four sides reserved, not just the left: the current level is framed
     rather than ruled, and a border that only appeared on the marked row would
     shift every other row's text as the selection moved. */
  border: 2px solid transparent;
  border-radius: 5px;
  /* The divider between rows, drawn as an inset shadow rather than a border,
     because the border is now spoken for by the frame. */
  box-shadow: inset 0 -1px 0 var(--line);
}

.score-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

/* Centred against both of the row's lines, not aligned to the first. */
.score-icons {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 4px;
}

.scoreboard li:last-child { box-shadow: none; }

/* The whole row is the target, so it has to look like one. */
.scoreboard li { cursor: pointer; }
.scoreboard li:hover { background: #23242c; }

/* A locked row still answers a click -- with the reason -- but should not
   promise a level it will not open. */
.score-locked { cursor: not-allowed; }
.score-locked:hover { background: none; }

/* The scrollbar is furniture, not part of the list, so it stays out of sight
   until the pointer is on the card.

   **Only the thumb fades -- the width stays reserved.** A bar that appeared on
   hover would narrow every row the moment the pointer arrived, shifting the
   name the reader was reaching for out from under it. `scrollbar-gutter` above
   holds the space open; this only paints into it. Both spellings are needed:
   the -webkit- rules for Chrome, `scrollbar-color` for Firefox. */
.scoreboard ul::-webkit-scrollbar { width: 8px; }
.scoreboard ul::-webkit-scrollbar-track { background: transparent; }
.scoreboard ul::-webkit-scrollbar-thumb {
  background: transparent;
  border-radius: 4px;
}
.scoreboard ul { scrollbar-color: transparent transparent; }

.scoreboard:hover ul::-webkit-scrollbar-thumb { background: var(--line); }
.scoreboard:hover ul { scrollbar-color: var(--line) transparent; }

/* Level name, then the trophy and the replay control, on one line. */
.score-head {
  display: flex;
  align-items: center;
  gap: 6px;
}

/* The row's number, quiet and small: it is a position in the list, not the
   thing being picked, and it must not compete with the level's name.

   Absolutely positioned into the row's left gutter -- where the solved tick
   used to be -- so it takes no part in the layout, costs the name and credit
   beside it no width, and is centred against both of the row's lines rather
   than sitting on the first one.

   Right-aligned in a fixed box so 1 and 18 both end at the same x and every
   name in the list starts in the same place. Tabular figures for the same
   reason: a proportional 1 is narrower than a 7, which would make the column
   wobble as the list re-sorted. Both matter more than usual here, because these
   numbers change under the reader as records are set. */
.score-num {
  position: absolute;
  left: 0;
  width: 16px;
  top: 50%;
  transform: translateY(-50%);
  text-align: right;
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  color: var(--dim);
}

/* The level on the board is the one the reader is looking for when they glance
   back at the list, so its number is brought up with its name. */
.score-current .score-num { color: var(--ink); }

/* Takes the row's spare width so the icons stay pinned to the right edge, and
   clips rather than wrapping -- a long level name or author must not push a
   two-line row out of a list where every other row is two lines. */
/* The gap is the space between the level's name and its credit, so the flag
   that starts the credit is not read as part of the name. */
.score-title {
  display: flex;
  align-items: baseline;
  gap: 9px;
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
}

/* Shrinks last: when the row runs out of width the credit gives way before
   the name does, since the name is what is being picked. */
.score-level {
  color: var(--ink);
  flex: 0 1 auto;
  flex-shrink: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Who made the level, beside its name. Quieter and smaller: it is a credit,
   not the thing being picked. */
.score-author {
  flex: 0 1 auto;
  flex-shrink: 12;
  min-width: 0;
  font-size: 10px;
  color: var(--dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The credit's flag is set a size up from the words beside it, and carries its
   own gap: at 10px the chip was small enough to read as punctuation. */
.score-author .flag-code {
  color: var(--dim);
  font-size: 10px;
  padding: 1px 5px;
  margin-right: 4px;
}

.score-author .flag {
  font-size: 1.25em;
  margin-right: 4px;
}

/* The origin mark gets the same treatment as the flag it replaces in this row,
   plus the nudge the base rule explains. */
.score-author .origin-mark {
  margin-right: 4px;
}

/* The same gold as the World Record card, because it means the same thing. */
.score-trophy {
  width: 13px;
  height: 13px;
  fill: #d9b451;
  flex: 0 0 auto;
}

/* A button, not a link: it changes what is on the board. Sized to stay a
   comfortable tap target on a phone while the icon inside it stays small. */
.score-replay {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 4px;
  color: var(--dim);
  cursor: pointer;
}

/* The one level the player may start next, and only that one. Green is what
   "yours"/"go" means everywhere else on the page, so it reads as the thing to
   do without needing a label. Solved rows keep the grey replay arrow: they are
   somewhere to go back to, not somewhere to go. */
.score-replay.is-next { color: var(--accent); }

.score-replay:hover, .score-replay:focus-visible {
  color: var(--accent);
  background: var(--bg);
}

.score-replay svg {
  width: 14px;
  height: 14px;
  fill: currentColor;
}

/* Moves, time and when they got it, on one line under the name. The gap does
   the spacing: a leading space inside a flex item is collapsed away, so the
   separator would otherwise sit hard against the time. */
.score-stats {
  display: flex;
  align-items: baseline;
  gap: 5px;
  min-width: 0;
}

/* Solved: a green disc with a white tick, at the head of the score line and
   just left of the moves.

   It lived in the row's left gutter until 2026-08-22, when the row numbers took
   that space; this is where it came back. Against the score it is doing
   something the gutter version was not -- marking the number rather than
   repeating a fact the row already carried twice.

   `align-self: center` because .score-stats aligns its children on the
   baseline, and an <svg> has no baseline to align on: left to itself it hangs
   low against the digits beside it. */
.score-solved {
  flex: 0 0 auto;
  align-self: center;
  width: 12px;
  height: 12px;
}

.score-solved .solved-disc { fill: var(--accent); }

/* Stroked, not filled, and white rather than the panel colour: the disc is
   small enough that a hole punched through it would read as grey. */
.score-solved .solved-tick {
  fill: none;
  stroke: #fff;
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.score-value { color: var(--ink); font-size: 11px; flex: 0 0 auto; }

/* A level they have not solved. Dimmer than a score and italic, so a glance
   down the column separates "played" from "not played" without reading it. */
.score-unsolved {
  color: var(--dim);
  font-size: 10.5px;
  font-style: italic;
}

/* Not a button, so it is sized like one only to keep the rows aligned. */
.score-lock {
  width: 12px;
  height: 12px;
  margin-right: 5px;
  fill: var(--dim);
  opacity: 0.7;
  flex: 0 0 auto;
}

/* A level they have not reached yet. Dimmed as a whole rather than by parts,
   so the row reads as out of play at a glance. */
.score-locked .score-level { color: var(--dim); }

/* The level currently on the board: a green frame round the whole row, and a
   wash of the same green behind it. The wash is kept very low -- the list is
   also the level picker, so the row has to read as "this one" without
   competing with the accent-coloured scores inside it. The divider is dropped
   on this row so the frame closes rather than being cut by a line. */
.score-current {
  border-color: var(--accent);
  background: rgba(121, 185, 138, 0.10);
  box-shadow: none;
}

/* Clipped rather than wrapped, same reason as the level name. */
.score-when {
  color: var(--dim);
  font-size: 10.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The World Record cell. Not the accent green -- that colour means "yours"
   everywhere else on the page, and this record usually belongs to someone
   else. */
/* The World Record card is a picture frame, and it is one whether or not the
   level has been claimed -- plain moulding when it is empty, gilt when it is
   not. **The 3px frame is on the card at all times and only its colour
   changes**, which is what stops the panel resizing under the reader when a
   record arrives from the server a moment after the first paint. */
.world-card {
  /* Anchors .card-pin, the Hall of Fame button in the card's top right corner,
     which is absolutely positioned. It also anchored the old gold line, which
     is gone -- this must not go with it, or the button pins itself to whatever
     is positioned further up. */
  position: relative;
  gap: 6px;
  text-align: center;
  border-width: 3px;
  border-color: var(--line);
  border-radius: 7px;   /* 3px of frame leaves a 4px radius on the face inside */
}

/* Gilt. A real frame is not a gold line -- it is a moulding with a lit side and
   a shaded one, a lip where it meets the picture, and enough depth to stand off
   the wall. All four are here and none of them is an image:

   * **The moulding** is a gradient painted into the border box while the border
     itself is transparent -- the two background layers below, the first
     clipped to the padding box to keep the card's own face, the second to the
     border box so it fills the frame. Lit from the top left like everything
     else on the page, and it crosses light-dark-light-dark rather than fading
     evenly, because that alternation is what reads as a turned moulding rather
     than as a painted stripe.
   * **The lip** is the inset shadow: a dark hairline just inside the frame,
     where a real one would cast onto the picture.
   * **The outer edge** is the first spread shadow, which keeps the gold from
     bleeding into the dark page behind it.
   * **The depth** is the last shadow, offset downward, so the frame hangs.

   Only on a level that has a record; refreshWorldBest puts the class on and
   takes it off. */
.world-card.has-record {
  border-color: transparent;
  background-image: linear-gradient(var(--panel), var(--panel)), var(--gilt);
  background-origin: border-box;
  background-clip: padding-box, border-box;
  box-shadow: var(--gilt-lip), 0 3px 10px rgba(0, 0, 0, 0.45);
}

/* Trophy stacked above the words rather than beside them, so it reads as the
   card's emblem instead of as a bullet on the heading. */
.world-card > .label {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}

/* The level's name, sitting over "World Record". Set the size of the stat
   labels rather than of a heading: it says which level the record belongs to,
   and the holder and the score under it are what the card is for. Always one
   line -- a long name is cut with an ellipsis rather than wrapped, so the card
   keeps the same height whatever a level is called. */
.world-level {
  max-width: 100%;
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Before a level is loaded there is no name to show, and an empty line would
   still take the column's gap and push the card open by 6px. */
.world-level:empty { display: none; }

.trophy {
  width: 26px;
  height: 26px;
  fill: #d9b451;   /* gold, and the only warm colour on the page */
  flex: 0 0 auto;
}

.world-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

.world-holder {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 6px;
  font-size: 15px;
  color: var(--ink);
}

.world-holder .flag-code { color: var(--ink); }

/* The holder's country, under their name. Smaller than the score: it says who
   they are, and the score is what the player is here to read. */
.world-place { font-size: 11px; color: var(--dim); }
/* Same as .wr-where: recordPlace returns "" for a record with no place worth
   printing, and an empty line should take no room rather than a little. */
.world-place:empty { display: none; }

/* The score itself is the number on this card that matters, so it is the only
   line on it set in the ink colour. */
.world-score { font-size: 13px; color: var(--ink); }

/* The date the record was set. Quieter than the score above it: it is how long
   the record has stood, which is context, not the number being chased. */
.world-when { font-size: 11px; color: var(--dim); opacity: 0.8; }

/* Nobody has claimed it. Reads as an invitation, so it is not styled as
   missing data. */
.world-empty {
  font-size: 12px;
  font-style: italic;
  color: var(--dim);
}

/* ------------------------------------------------------------- sign-in gate */

.gate {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(10, 11, 14, 0.88);
  z-index: 10;
}

.gate[hidden] { display: none; }

.gate-card {
  position: relative;   /* anchors .card-close */
  width: 420px;
  max-width: 100%;
  display: flex;
  flex-direction: column;
  padding: 24px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
}

.gate-card h2 {
  margin: 0 0 6px;
  font-size: 18px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.gate-card p { margin: 0 0 14px; font-size: 12px; }

.gate-card label,
.gate-card .field-label {
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
  margin-bottom: 4px;
}

.gate-card input,
.gate-card select {
  font: inherit;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 8px 10px;
  margin-bottom: 12px;
}

.gate-card input::placeholder { color: var(--dim); }

.gate-card input:focus,
.gate-card select:focus {
  outline: none;
  border-color: var(--accent);
}

/* The dropdown list itself is painted by the OS, so it needs a colour pair
   that works on a light popup as well as our dark card. */
.gate-card select option { background: var(--bg); color: var(--ink); }

.gate-privacy {
  margin: 0 0 16px !important;
  padding: 10px 12px;
  font-size: 11px;
  line-height: 1.55;
  color: var(--dim);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 4px;
}

.gate-privacy strong { color: var(--ink); }

.gate-card button[type="submit"] {
  margin-top: 4px;
  margin-bottom: 18px;
  padding: 11px 14px;
  font-weight: 600;
  color: #0f1a12;
  background: var(--accent);
  border-color: var(--accent);
}

.gate-card button[type="submit"]:hover { background: #8fcc9c; }
.gate-card button[type="submit"]:active { background: #6aa87a; }

.gate-error { margin: 0 0 8px !important; color: var(--danger); font-size: 12px; }
.gate-error:empty { display: none; }

.gate-note { margin: 10px 0 0 !important; font-size: 11px; }
.gate-note:empty { display: none; }

/* ------------------------------------------------------------ welcome back */

/* Reuses .gate for the backdrop and .gate-card for the panel, so the two cards
   cannot drift apart, and inherits `.gate[hidden] { display: none }` with it. */
/* The reCAPTCHA widget's own box. The iframe Google renders is a fixed 304px
   wide and cannot be restyled, so this only centres it and gives it air; do not
   try to scale it -- a transformed widget stops accepting clicks in Safari. It
   overflows on the narrowest phones, which is why the parent card scrolls. */
.recaptcha-box {
  display: flex;
  justify-content: center;
  margin: 4px 0 14px;
  min-height: 78px;
}

/* **Required, not tidiness.** display:flex above is a class rule and beats the
   browser's own [hidden] { display: none }, so without this the box keeps its
   78px even when js/recaptcha.js hides it -- which it always does in score
   mode, where there is no widget to draw. That left a hole in the sign-in card
   under a key that renders nothing. */
.recaptcha-box[hidden] { display: none; }

/* Same shape as the welcome card: this is the same kind of interruption. */
.human-card {
  max-width: 360px;
  text-align: center;
}

.human-note {
  margin: 0 0 12px !important;
  min-height: 16px;
  font-size: 12px;
  color: var(--dim);
}

/* The locked-level card. Same shape as the welcome card -- narrow, centred,
   with room at the top for the corner X -- because they are the same kind of
   thing: a short interruption with one way out. */
.locked-card {
  max-width: 340px;
  text-align: center;
  padding-top: 34px;
}

.locked-icon {
  width: 44px;
  height: 44px;
  margin: 2px auto 12px;
  display: block;
  color: #d9b451;   /* the World Record card's gold; see line 338 and 822 */
}

/* The level to solve next. Given its own weight because it is the one piece of
   the card the player actually has to read. */
.locked-next {
  margin: 0 0 16px !important;
  color: var(--ink);
  font-weight: 600;
  letter-spacing: 0.02em;
}

#locked-ok { width: 100%; }

.welcome-card {
  max-width: 320px;
  text-align: center;
  /* Room for the corner X above the logo, which is centred and would otherwise
     sit under it. */
  padding-top: 34px;
}

/* Plain inline flow, not a flex row: as flex, the name and the place became
   separate items and each wrapped inside its own column, breaking "Jeff B"
   across two lines while there was still room beside it. */
.welcome-who {
  margin: 0 0 4px !important;
  line-height: 1.45;
  color: var(--ink);
}

.welcome-name {
  display: block;
  margin-bottom: 6px;
  font-size: 17px;
  font-weight: 600;
}

.welcome-place {
  display: block;
  font-size: 13px;
  color: var(--dim);
}

.welcome-place .flag-code { color: var(--dim); }

#welcome-play {
  margin-top: 16px;
  width: 100%;
  padding: 11px 14px;
  font-weight: 600;
  color: #0f1a12;
  background: var(--accent);
  border-color: var(--accent);
}

#welcome-play:hover { background: #8fcc9c; }
#welcome-play:active { background: #6aa87a; }

/* Destructive, so it keeps the dashed, dim `.forget` treatment rather than
   competing with Play. */
#welcome-clear { width: 100%; margin-top: 10px; }

/* ----------------------------------------------------------------- flags */

.flag {
  font-size: 1.05em;
  line-height: 1;
}

/* The mark for a level with a non-country origin, in place of a flag.

   White, not currentColor: the credit sets --ink (#d8d9e0) and a mark this small
   drawn in it sinks into the caption, where the flags it stands beside are
   full-colour artwork holding their own weight.

   Sized and spaced by eye rather than to match the flag glyph, because it is a
   drawn mark and not a glyph with its own side bearings: a little larger to carry
   the same weight, pulled left to close the gap its SVG box leaves, and given a
   real gap on the right, which the space character in the markup only half
   supplies. */
.origin-mark {
  width: 1.25em;
  height: 1.25em;
  margin-left: -0.10em;
  margin-right: 0.16em;
  vertical-align: -0.24em;
  fill: #ffffff;
}

/* Fallback where the platform has no flag glyphs (Windows): a small code chip,
   so it reads as deliberate rather than as stray letters. */
.flag-code {
  display: inline-block;
  padding: 1px 4px;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.06em;
  vertical-align: 1px;
  color: var(--ink);
  background: var(--line);
  border-radius: 3px;
}

/* The country field shows what we detected, with the dropdown behind a click. */
.country-detected {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 10px;
  margin-bottom: 12px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 4px;
}

.country-detected[hidden] { display: none; }

.country-detected-value {
  display: flex;
  align-items: center;
  gap: 7px;
}

.gate-card select[hidden] { display: none; }

.gate-card .field-label { display: block; }

/* Start-over control on the welcome-back card: available, not inviting. */
.forget {
  margin-top: 2px;
  padding: 8px 10px;
  font-size: 12px;
  color: var(--dim);
  background: none;
  border: 1px dashed var(--line);
}

.forget[hidden] { display: none; }

.forget:hover {
  color: var(--danger);
  border-color: var(--danger);
  background: none;
}

.forget span { font-size: 14px; vertical-align: -1px; }

/* Corner dismiss on the sign-in card. Deliberately quiet: looking around
   without signing in is allowed, but it is not the path being offered. */
.card-close {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 30px;
  height: 30px;
  padding: 0;
  font-size: 18px;
  line-height: 1;
  color: var(--dim);
  background: none;
  border: 0;
}

.card-close:hover { color: var(--ink); background: #272932; }

/* --------------------------------------------------------------- footer */

.site-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 30px;            /* clear of the window's bottom edge, not on it */
  z-index: 5;              /* same layer as the scale control, below the cards */
  text-align: center;
  font-size: 12px;
  color: var(--dim);
}

.site-footer a { color: var(--dim); }
.site-footer a:hover { color: var(--ink); }

/* The two written pages, on their own line above the credit. Brighter than the
   credit line under them because these are the only two links on the page that
   go anywhere a reader might want to go, and quieter than the game so they do
   not compete with it. The row is the home page's only link to /about/ -- the
   corner has the ? for the card, not the page -- so it is also the link a
   crawler follows to find it. */
.footer-links {
  display: block;
  margin-bottom: 4px;
  font-size: 12px;
}

.site-footer .footer-links a {
  color: var(--ink);
  text-decoration: none;
  padding: 2px 2px;   /* a tap target rather than a line of type on a phone */
}

.site-footer .footer-links a:hover { color: var(--accent); }

/* The notice under the credit line. A step smaller and a step quieter: it is
   the smallest print on the page and should read that way. */
.site-copyright {
  display: block;
  margin-top: 2px;
  font-size: 10.5px;
  opacity: 0.75;
}

/* Below roughly 960px a centred footer would run into the scale control on the
   right, so it steps up above it and the page gets the extra bottom room to
   match. The breakpoint is where the two boxes would actually start to touch,
   not a round number. */
/* ------------------------------------------------- pinned side columns ---
   Above 1000px the two panels are taken out of the flow and pinned to fixed
   points, left and right, running a fixed vertical band. In the flow they were
   centred as a group with the board, so every level change moved them
   sideways -- the list you were reading slid across the screen because the
   next board happened to be wider. Pinned, they stay put and the board is
   centred in what is left between them.

   THE INSET AND `.play`'s GAP ARE A PAIR. `App.fitCell` sizes the board by
   subtracting each sibling's width plus that gap from the window, so the space
   actually left beside a pinned panel is `gap + body padding - inset`. At
   40 + 16 - 20 that is a 36px channel either side of the board. Change one of
   the three and the board will either overlap a panel or stop short of it.

   Below 1000px none of this applies: `.play` becomes a single centred column
   with the board at the top of it -- see the stacked-layout block below --
   which is what a phone needs, and pinned panels there would simply lie on top
   of the level. */
@media (min-width: 1000px) {
  /* The three numbers that decide this layout, in one place. `fitCell` sizes
     the board by subtracting each sibling's width plus `.play`'s gap from the
     window, so the channel actually left beside a pinned panel works out as
     gap + body side padding - inset. Deriving the gap from the other two with
     calc() keeps them from drifting apart: raise the inset and the gap follows,
     and the channel stays put. */
  :root {
    /* Panel to window edge. Grows with the window rather than sitting at one
       value: a fixed inset that looks generous at 1900px squeezes the board at
       1000px, where the two 300px panels already account for well over half
       the width. The `8vw - 40px` form is chosen so it still lands on 40px at
       the 1000px breakpoint -- widening the margin must not make the narrowest
       pinned layout worse than it was. Caps at 140px so it stops growing on a
       very wide monitor instead of stranding the board in the middle. */
    --side-inset: clamp(40px, calc(8vw - 40px), 140px);
    --side-w: 300px;      /* matches .scoreboard/.hud width */
    --board-gap: 36px;    /* panel to board */
    --body-pad: 16px;     /* body's own left/right padding */
  }

  .play { gap: calc(var(--side-inset) + var(--board-gap) - var(--body-pad)); }

  .scoreboard, .side {
    position: fixed;
    /* Clear of the fixed furniture: the help button at top right, and the
       scale control and footer along the bottom. */
    top: 70px;
    bottom: 80px;
    width: var(--side-w);
    z-index: 4;            /* under the scale control and the cards */
  }

  /* With the panels pinned, the scale control only resizes the board -- which
     means a board scaled up has nothing to push against and would slide under
     a panel. Cap the board's column at the channel between them so it scrolls
     inside it instead. */
  .board-wrap {
    max-width: calc(100vw - 2 * (var(--side-inset) + var(--side-w) + var(--board-gap)));
  }

  .scoreboard {
    left: var(--side-inset);
    align-self: auto;      /* height comes from top/bottom now, not the row */
    overflow: hidden;      /* keeps the list inside the rounded border */
  }

  /* Centre the BOARD, not the column it sits in. Everything above it -- the
     top padding that reserves room for the fixed logo, plus the level title
     and the gap under it -- has to be mirrored below, or the board sits low by
     half the difference. 133px of padding and roughly 66px of title and gap is
     what puts it in the middle of the window. */
  body { padding-bottom: 199px; }

  .side {
    right: var(--side-inset);
    padding-top: 0;
    /* Centred down the pinned band, so the column sits in the middle of the
       page rather than hanging from the top of it. Jeff's call, 2026-08-23 --
       it used to be flex-start.

       `safe` matters because this box also scrolls: on a short window the
       column can be taller than the band, and a plain `center` would push its
       top out of the scrollport where it could not be reached. `safe center`
       centres when there is room and falls back to the top when there is not.
       A browser that does not understand it drops the declaration and inherits
       the plain `center` from the base rule, which is the same intent. */
    justify-content: safe center;
    overflow-y: auto;
    scrollbar-width: thin;
  }
}

/* ------------------------------------------------- the stacked layout ---
   Below 1000px the panels are not pinned, and everything under that width is
   one centred column: the board, then the controls and the two score cards,
   then the level list.

   **999px, not 960.** The pinned block above starts at 1000, so any other
   number leaves a band that is neither pinned nor stacked. Between 960 and
   1000 that band put the level list BESIDE the board and wrapped only .side
   below it -- on a 960px window the board sat right of a 300px list at x483,
   with the arrow pad 578px further down the page. Measured, 2026-09-02.

   **The order is a DOM order, not a flex `order`.** .board-wrap is first in
   index.html so the tab order matches what is on screen; reordering with
   `order` here would have left a keyboard reaching the 65-row level list
   before the board it is meant to play. It costs the pinned layout nothing,
   because up there .scoreboard and .side are position: fixed and take their
   places from their insets rather than from the document. */
@media (max-width: 999px) {
  .play {
    flex-direction: column;
    align-items: center;
    gap: 18px;
  }

  /* Under the board: what you press, then what you scored, then the record to
     beat. The level list is last because it is the thing you leave the board
     to go and use, and the hint is last inside the column because it is
     mostly desktop advice. These are flex `order`s rather than another DOM
     move because .side's own reading order -- your score, then the record,
     then the controls -- is the right one for a screen reader either way. */
  .side .status     { order: 1; }
  .side .pad        { order: 2; }
  .side .controls   { order: 3; }
  .side #score-card { order: 4; }
  .side .world-card { order: 5; }
  .side .hint       { order: 6; }

  /* Nothing is beside anything any more, so the two panels stop reserving a
     column of their own and take the page instead. **min-width has to go with
     the width** or the 300px floor keeps its value: at 320px that floor plus
     body's 32px of padding came to 332 against 305 of room, and the whole
     page scrolled sideways. Capped at 340 so a tablet does not get one very
     wide card. */
  /* **.app has to be told to yield to the window.** Its `max-width:
     max-content` caps the column at its content and nothing else caps it at
     the page, so a shrink-to-fit chain -- app, play, side -- sized itself to
     the widest unbreakable thing inside it. On a 320px screen that was the
     player's own line, "Richmond, Virginia, United States": the column stood
     322px wide against 273px of room and took the whole page sideways with
     it. Capping here rather than unpicking the nowraps keeps the overflow
     where it belongs, inside the list's own scroller. Measured, 2026-09-02. */
  .app, .play { width: 100%; max-width: 100%; }

  .scoreboard, .hud { width: 100%; max-width: 340px; min-width: 0; }
  .status, .hint { max-width: 340px !important; }

  /* **The fixed furniture comes out of the flow and joins the page.** Fixed,
     the footer and the scale control were painted straight over the board and
     the arrow pad -- at 390px the footer sat at y708-766 and the board at
     y724-920. On one screen at a desk that is a strip of chrome; on a page
     1731px tall it is two opaque bars parked across the middle of it.

     Static, they also stop having to be paid for. body's bottom padding was
     124px purely to reserve room for them, and fitCell subtracts that padding
     from the height it has to fit the board into -- so giving it back is a
     bigger board on every phone. */
  body { padding-bottom: 24px; }

  .site-footer {
    position: static;
    order: 2;
    margin-top: 30px;
  }

  .scale {
    position: static;
    order: 1;
    margin-top: 26px;
    max-width: 100%;
  }

  /* The slider gives up width before the label or the reading does: at 320px
     the control stood 285px wide against 305px of room. */
  .scale input { width: min(164px, 42vw); }

  /* **The level list still gets a height of its own.** Above 1000px
     .scoreboard is pinned with top and bottom, so the list has a definite box
     to scroll inside and `flex: 1 1 auto` fills it. Down here the panel is
     back in the flow, and a flex item that is the only thing in its line
     resolves `stretch` to its own content height -- so the list simply grew a
     row per level. With 62 levels the panel stood 3389px tall and the page
     nearly 5000.

     That was not only ugly. **fitCell subtracts a wrapped panel's height from
     the height it has to fit the board into**, so a 3389px panel put the
     budget thousands of pixels below zero and the board fell back to the 36px
     default -- 612px across on a 390px phone, which is what pushed the page
     off centre and let it scroll left and right. The cap is what makes that
     arithmetic mean something again. Reported from a real phone, 2026-08-27.

     A share of the window rather than a pixel count: the panel sits under the
     board at every width down here, and half the window reads as "a list you
     scroll" on a phone and on a tablet alike. */
  .scoreboard ul { max-height: 48vh; }
}


/* ----------------------------------------------------------------- logo */

/* The masthead: the logo, with the game's H1 under it.

   **It groups them in the document and imposes nothing on them in the layout.**
   `display: contents` on purpose: the logo and the tagline are each positioned
   against the viewport in their own right, below, and a real box here would put
   a containing block between them and it. It matters because .site-logo is NOT
   the game's alone -- /about/, /how-to-play/, /history/ and /ai-restoration/
   each carry one too, with no masthead around it, and whatever this rule did to
   the image would have to be undone on all four. Nothing here touches it. */
.site-head { display: contents; }

.site-logo {
  position: fixed;
  top: 12px;
  /* Centred at the head of the page rather than tucked into a corner. Still
     fixed and still out of the flow, so it costs the board no width; the room
     it needs is bought with body's top padding instead, which is what fitCell
     already subtracts when it sizes the board. Change one and change the
     other, or the level's title moves up under the art.

     The four written pages buy the same room with .page-inner's padding-top.
     They share this rule; they do not share the tagline below it. */
  left: 50%;
  transform: translateX(-50%);
  z-index: 5;
  width: 210px;
  height: auto;
  /* Decorative. It sits over the top of the page, so it must never be the
     thing that swallows a click meant for what is underneath. .site-logo-link
     gives this back on the four pages, where the logo is the way home. */
  pointer-events: none;
}

/* ------------------------------------------- masthead, stacked layouts ---
   **The logo leaves the middle wherever the panels are not pinned.** It is
   centred by design -- see the rule above -- but centred it cannot share the
   top strip with the corner nav, and below about 822px the two genuinely
   collide: a 210px logo centred on a 740px window runs x257-467 and the nav's
   box starts at x425, so "How to Play" sat on the MUSE mark. It did so at
   every width from 320 up. Measured, 2026-09-02.

   **This has to live after .site-logo's own rule, not up with the rest of the
   stacked layout.** A media query adds no specificity, so the base rule's
   `left: 50%` simply won from further down the file -- the same trap the
   pinned-panel block carries a note about. Written once here and once, a
   size smaller, in the phone block below.

   The nav gets a hard left edge rather than a max-width; the phone block is
   where that reasoning is written out. */
@media (max-width: 999px) {
  .site-logo { left: 16px; transform: none; }
  .corner-nav { left: 240px; max-width: none; pointer-events: none; }
  .corner-nav > * { pointer-events: auto; }
}

/* **On a phone the logo leaves the middle and pins to the left edge.** It is
   centred everywhere else on purpose -- see the note above -- but centred it
   cannot share the top strip with the corner nav, and the two simply do not
   fit: 120px of logo, 236px of nav and their two 10px insets come to 376
   against the 375 a 390px phone has. The overlap was real at every width from
   320 up to about 820, with "How to Play" sitting on the MUSE mark. Measured,
   2026-09-02; Jeff's call between this and dropping two of the four pills.

   104px rather than 120 so the logo's box ends at x114 and the nav, whose own
   left edge is nailed to x124 below, always has ten pixels of air against it.
   The eight pixels this gives up against 112 are what keep the four pills on
   ONE row at 390px, which is the width most phones actually are.
   The four written pages share .site-logo and so move with it, which is what
   is wanted -- one masthead, not two. They do not share .site-tag, which stays
   centred: the logo's own art stops 90% of the way down its box, at y73 here,
   so the tagline at y79 is clear of the mark even where it crosses the box. */
@media (max-width: 720px) {
  .site-logo { width: 104px; left: 10px; }
  /* Smaller logo, less room to buy back. */
  body { padding-top: 100px; }
}

/* ------------------------------------------- a phone lying on its side ---
   A landscape phone is about 360px tall, and the masthead is sized for a
   window that is not. body's 152px of top padding reserves room for a 210px
   logo, fitCell subtracts that padding from the height it has to fit the board
   into, and what was left of a 360px window came to 118px -- seven rows of
   level in 118px is a 15px cell, which the step rounds down to the 12px floor.
   The board came out 132px across on a 740px screen. Measured, 2026-09-02.

   So the masthead comes down to the size the window can afford. The logo is
   the whole of the cost: 84px of it stands 48px tall, which buys back 76px of
   padding and takes the cell from 12 to 25.

   **Deliberately not applied above 999px.** A short window on a desktop has
   the pinned panels, whose top and bottom insets are measured against this
   same furniture, and a wide-and-short desktop window is not what this is
   for -- it is for a phone turned sideways. */
@media (max-width: 999px) and (max-height: 560px) {
  .site-logo { width: 84px; left: 10px; }
  .site-tag { top: 58px; font-size: 9.5px; letter-spacing: 0.16em; }
  .corner-nav { left: 104px; }
  body { padding-top: 76px; }

  /* The level list is measured against the window, and half of a 360px window
     is not a list. It gives up its share here so the board keeps its own. */
  .scoreboard ul { max-height: 60vh; }
}

/* The logo's own art starts 10% down its box and ends 90% down, so at 210px
   wide the visible mark ends about 118px below the top of the window. */

/* Logo at the head of both cards. Decorative -- the heading underneath already
   names the game -- so the alt is empty and it is not a link. */
.card-logo {
  display: block;
  width: 168px;
  /* The art has transparent margins of its own, so a tight bottom margin still
     leaves a visible gap above the heading. */
  height: auto;
  margin: 0 auto 4px;
}

/* Publication date, behind a pin on the author line. It is a footnote about the
   level, not part of the credit, so it stays out of the line until asked for. */
.info-pin {
  width: 11px;
  height: 11px;
  padding: 0;
  font-size: 7px;
  font-style: italic;
  font-weight: 700;
  line-height: 9px;
  /* Rides above the baseline rather than sitting on it -- it annotates the
     line, it is not a word in it. */
  vertical-align: 5px;
  color: var(--ink);
  background: var(--panel);
  border: 1px solid var(--dim);
  border-radius: 50%;
}

.info-pin:hover {
  color: #0f1a12;
  background: var(--accent);
  border-color: var(--accent);
}

/* The pin in the World Record card's top corner, which opens the whole table.
   Absolutely positioned so it takes no room from the centred emblem under it,
   and inside the gold frame rather than over it. */
.card-pin {
  position: absolute;
  top: 6px;
  right: 6px;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: transparent;
  color: var(--dim);
  cursor: pointer;
}

.card-pin:hover {
  color: var(--ink);
  border-color: var(--dim);
}

.pin-icon {
  width: 12px;
  height: 12px;
  fill: currentColor;
}

/* The player's own card, while a level is actually in play: ringed in the
   accent green that means "yours" everywhere else, so it is obvious which of
   the two cards is counting. Dropped the moment the level falls, which is what
   makes the gold World Record card above it the one to look at.

   A colour change on the border .hud already draws, not a second border, so
   nothing moves when it goes on or off. */
.hud.playing {
  border-color: var(--accent);
}

/* ------------------------------------------------------------ solved flash */

/* "Level Solved!" across the middle of the window, for a beat and a half,
   before the score card opens and before any fireworks. Jeff's call,
   2026-08-22.

   **pointer-events: none is load-bearing.** It covers the whole window, and a
   player who has just finished a level is usually already moving toward the
   next one; without this it would swallow that click. */
.solved-flash {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}

/* Beats display: flex above, which would otherwise keep it on screen. */
.solved-flash[hidden] { display: none; }

.solved-flash span {
  font-size: 64px;
  font-weight: 700;
  letter-spacing: 0.03em;
  /* A half-step under #57d47c, the "look at this" green the first-move arrow
     and the sound switch's pulse are drawn in.

     **Deliberately not the identical value, because this is not an identical
     job.** Those two are small shapes -- a 22px arrow, a 28px icon -- and this
     is thirteen characters of solid fill at 64px. The same green over an area
     that size reads brighter than it does in a glyph, so holding the number
     fixed would not hold the *appearance* fixed. Stepping the lightness down
     about a tenth is what makes the flash look like the same green as the
     arrow rather than measure like it.

     Settled by walking it: #2b7a48 first, dark enough that the thick white
     stroke did all the work and the fill only kept it from being a white word;
     then #57d47c, which at this size glared. Jeff's calls, 2026-09-04.

     **Never --accent**, whichever way this drifts. That is the light sage
     meaning "yours" on the score cards and the level list, and against a
     stroke this heavy it washes out -- fill and outline end up at nearly the
     same weight, and the letters read as a white word with a green tint. */
  color: #4cbf6e;
  /* White stroke, then the dark halo outside it. Two layers doing two jobs:
     the outline separates the fill from whatever tile it lands on, the glow
     carries that separation out into the board. Jeff's call, 2026-09-04.

     **paint-order puts the stroke behind the fill**, so the letters keep their
     full weight and only the outer half of the stroke shows. Without it the
     stroke straddles the outline and eats into the glyph, which at this weight
     closes up the counters in the o and the e -- and at 0.12em, thick enough
     that the word carries its own outline across a busy board, it would close
     them entirely. Width is in em so the 40px phone size below scales with it
     rather than wearing a desktop outline. */
  -webkit-text-stroke: 0.12em #fff;
  paint-order: stroke fill;
  /* Dark halo rather than a panel: it has to stay legible over the board,
     which is light where the walls are. */
  text-shadow: 0 2px 26px rgba(0, 0, 0, 0.85), 0 0 6px rgba(0, 0, 0, 0.6);
  animation: solved-flash 3s ease-out both;
}

/* In fast, hold, out. The overshoot and the settle are what make it land
   rather than merely appear.

   **The percentages were rescaled when the flash went from 1.6s to 3s, not
   left where they were.** They are proportions of the whole, so carrying them
   over would have stretched the arrival from 240ms to 450ms and the departure
   with it -- and a congratulation that eases in over nearly half a second does
   not land, it swells. The in and out keep the timings they always had, in
   milliseconds; all of the extra time went into the hold, which is the part
   Jeff asked for more of. 8% and 13% of 3s are the old 15% and 25% of 1.6s,
   and 85% starts the same 450ms fade the old 72% did. */
@keyframes solved-flash {
  0%   { opacity: 0; transform: scale(0.72); }
  8%   { opacity: 1; transform: scale(1.06); }
  13%  { transform: scale(1); }
  85%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.05); }
}

@media (prefers-reduced-motion: reduce) {
  .solved-flash span { animation: solved-flash-still 3s steps(1, end) both; }
  @keyframes solved-flash-still { 0%, 85% { opacity: 1; } 100% { opacity: 0; } }
}

@media (max-width: 720px) {
  /* 40px, not the 64px above: at 0.52em of advance per character "Level
     Solved!" is about 280px wide here once the stroke is counted, which still
     clears a 320px phone on one line. The flash has no padding of its own to
     give up, so the next size up would wrap it. */
  .solved-flash span { font-size: 40px; }

  /* On a phone the card is nearly the whole screen, so every millimetre of
     padding is a line of text lost, and the backdrop round it shrinks to a
     sliver -- which is why the sticky X and Close button above matter here
     rather than on a desktop. */
  .gate { padding: 10px; }

  .help-card {
    max-height: 88vh;
    padding: 0;
  }

  /* Tighter margins on a phone, where 24px of padding is a line of text lost.
     The top still has to clear the X, which is anchored to the card and sits
     over this box rather than in it. */
  .help-scroll { padding: 40px 16px 10px; }

  /* Closer into the corner to match the smaller frame. */
  .help-card .card-close { top: 4px; right: 4px; }
  /* Lifted like the desktop one, with a skirt to match the smaller gap. */
  #help-ok { bottom: 4px; margin-top: 22px; box-shadow: 0 0 0 16px var(--panel); }

  /* The reversed section bars keep their weight but stop eating the width. */
  .help-card h3,
  .prose h3 { margin: 20px 0 10px; padding: 5px 8px; }

  /* Both are already narrow at this width; let them have the column. */
  .help-photo, .help-scan { width: 100%; }
}

/* ------------------------------------------------------ world records card */

/* Wider than the level-info card: this one is a table of every level, and the
   holder's name and where they played from need the room. */
/* .gate-card sets a fixed 420px, which is too narrow for three columns -- the
   holder's place wrapped onto a second line. Overridden, not raised: the width
   is the thing being changed, and max-width alone would never have won. */
.records-card {
  width: 560px;
  max-width: 100%;
  text-align: center;

  /* **The card is never taller than the window.** Without this the trophy, the
     heading, the podium, the fixed-height list and the Close button simply
     added up, and on a phone the sum ran off both ends of the screen -- the X
     above the top edge and the Close button below the bottom one, with no way
     out of the card but Escape, which a phone does not have. Reported from a
     real phone, 2026-08-27.

     Capped rather than shortened: the list keeps the height it was given
     wherever there is room for it, and .records-scroll gives that height up
     first when there is not.

     A percentage, not a vh figure: it resolves against .gate, so the cap is
     the window less .gate's own padding -- 20px a side on a desktop and 10px
     on a phone -- and stays right without either number being repeated here.
     A vh cap would have had to guess at that padding, and guessed wrong at one
     of the two widths. */
  max-height: 100%;

  /* The same gilt frame the World Record card wears, for the same reason: this
     is that card opened up, every level's record in one table. The two share
     --gilt and --gilt-lip so they cannot drift apart.

     Deeper drop shadow than the card in the sidebar, because this one is a
     dialog standing over a dimmed page rather than a panel sitting in it. */
  border-width: 3px;
  border-color: transparent;
  border-radius: 11px;   /* 3px of frame leaves the 8px face .gate-card sets */
  background-image: linear-gradient(var(--panel), var(--panel)), var(--gilt);
  background-origin: border-box;
  background-clip: padding-box, border-box;
  box-shadow: var(--gilt-lip), 0 14px 40px rgba(0, 0, 0, 0.55);
}

/* **Everything in the card but the list holds its height.** The card is a
   flex column, and a flex item shrinks by default: with a max-height on the
   card, the browser took the height it needed out of every child at once
   rather than out of the list. The trophy came out at 7px tall instead of 56
   -- a gold smear above the heading -- and the podium rows closed up with it.

   The list is the one thing here that can be shorter without losing anything;
   see .records-scroll, which is the only child that stays shrinkable. */
.records-card > * { flex: 0 0 auto; }

/* ---------------------------------------------- the Hall of Fame arriving */

/* **The card is not there and then it is.** It used to appear between one
   frame and the next, and on entry it opens itself 260ms after the board is
   painted (see showHallOnEntry) -- the player is looking at the level they
   were about to play when a full-screen scoreboard takes the window. Reported
   as disorienting, 2026-08-30.

   So it grows into place instead: the page dims first, the card zooms up from
   88% behind that dim, and its contents rise in after it, a row at a time.
   The eye is led from the dim, to the emblem, to the list -- which is the
   order it would read them in anyway.

   Driven off `display`, not a class: `.gate[hidden] { display: none }` means
   every one of these restarts on its own each time the card is shown, so
   openRecords needs to do nothing to arm them. Only the *close* needs help
   from JS, because the card has to stay displayed while it plays; see
   .records-closing at the foot of this block. */

@keyframes records-dim-in {
  from { background-color: rgba(10, 11, 14, 0); }
  to   { background-color: rgba(10, 11, 14, 0.88); }
}

@keyframes records-zoom-in {
  from { opacity: 0; transform: scale(0.88) translateY(14px); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}

/* The contents, a beat behind the card they are in. Small on purpose: this is
   the settling after the zoom, not a second entrance. */
@keyframes records-rise-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

#records { animation: records-dim-in 280ms ease-out both; }

/* Overshoot-free ease-out -- the card is a scoreboard, not a toy, so it slows
   into place rather than bouncing past it. */
#records .records-card {
  animation: records-zoom-in 420ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

/* Staggered from the emblem down. The delays start while the card's own zoom
   is still finishing, so the two read as one movement rather than two. */
#records .records-card > * {
  animation: records-rise-in 320ms ease-out both;
  animation-delay: 200ms;
}
#records .records-trophy-big { animation-delay: 200ms; }
#records .records-card h2 { animation-delay: 240ms; }
#records .records-summary { animation-delay: 280ms; }
#records .records-podium { animation-delay: 320ms; }
#records .records-scroll { animation-delay: 360ms; }
#records #records-ok { animation-delay: 400ms; }

/* **Leaving.** Quicker than arriving and without the stagger: an exit that
   takes as long as the entrance reads as a card that will not go away. The
   class is put on by closeRecords, which hides the card for real when this has
   played -- it has to stay displayed until then. */
#records.records-closing {
  animation: records-dim-in 180ms ease-in reverse both;
  pointer-events: none;   /* no second click into a card that is leaving */
}
#records.records-closing .records-card {
  animation: records-zoom-in 180ms ease-in reverse both;
}
#records.records-closing .records-card > * { animation: none; }

/* Nothing moves, and closeRecords reads the same query so the card does not
   sit there through 180ms of an exit that is not being drawn. */
@media (prefers-reduced-motion: reduce) {
  #records,
  #records .records-card,
  #records .records-card > *,
  #records.records-closing,
  #records.records-closing .records-card { animation: none; }
}

/* The card's emblem, at the size the solved card uses. Every level in the game
   is on this list, so it is worth an emblem. */
.records-trophy-big {
  width: 56px;
  height: 56px;
  margin: 2px auto 8px;
  fill: #d9b451;
}

.records-summary {
  margin: 0 0 12px !important;
  font-size: 11.5px;
}

/* ------------------------------------------------------------- podium */

/* The three players holding the most records. An <ol>, because the order is
   the point, but the numbering is carried by the trophies' colours rather than
   by markers -- gold, silver and bronze say first, second and third without a
   column of digits. */
.records-podium {
  /* Air above the rule that .records-scroll draws as its top border, matched by
     the padding under it, so the podium and the list read as two blocks rather
     than as one list with a line through it. */
  margin: 0 0 18px;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.records-podium[hidden] { display: none; }

.podium {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.03);
  font-size: 12.5px;
  text-align: left;
}

/* First place sits a step brighter than the other two, so the eye lands on it
   before it reads any of the names. Only the background and the trophy say so
   -- the three names are set at one size, and the flag chips with them, or the
   column reads as three different lists rather than as one standing. */
.podium-1 {
  background: rgba(217, 180, 81, 0.10);
}

.podium-trophy {
  width: 20px;
  height: 20px;
  flex: 0 0 auto;
}

/* One size for all three, set on .podium-trophy above: the places are told
   apart by colour alone. A bigger first trophy made the row it sat in taller
   than the two under it, which broke the column the three names line up in. */
.podium-1 .podium-trophy { fill: #d9b451; }
.podium-2 .podium-trophy { fill: #c2c6cf; }
.podium-3 .podium-trophy { fill: #bd7f4a; }

.podium-who {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The chip is a flag, not the first syllable of the name -- it needs a gap of
   its own to read as one. Both selectors, since a platform with real flag
   glyphs gets .flag and Windows gets the .flag-code chip. */
.podium-who .flag,
.podium-who .flag-code {
  margin-right: 4px;
}

/* The signed-in player, in the accent that means "yours" everywhere else. */
.podium-mine .podium-who { color: var(--accent); }

.podium-count {
  flex: 0 0 auto;
  color: var(--dim);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}

/* The table scrolls, the heading and the count do not. Sized in vh rather than
   px because the constraint is the window, not the list: a hundred records must
   not push the Close button off the bottom of it.

   **A fixed height, not a cap.** The list holds only the levels that have a
   record on them, so it is short early on and grows as the game is played --
   and against a max-height that meant a card whose height moved every time
   somebody claimed a level, one that stood a third as tall on a fresh site as
   it will once the levels are claimed. Every record is listed however many
   there are; the box scrolls rather than the card growing. Jeff's call,
   2026-08-23. */
.records-scroll {
  height: min(64vh, 560px);
  /* The card's max-height above is met by shrinking this box and nothing else
     -- see .records-card > * below, which is what pins the rest. This is the
     only part of the card that can lose height without losing a control.
     `min-height: 0` and `flex-shrink: 1` are what let it: a flex item's
     automatic minimum is its content, which would have held the box at its
     full height and pushed the Close button out of the card again. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  margin: 0 -4px 14px;
  /* The top padding is the other half of the gap around the separator. It
     scrolls away with the content, which is what should happen: once the list
     is scrolled the first visible row belongs under the line. */
  padding: 12px 4px 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  /* Firefox; Chrome and Safari take the ::-webkit rules below. */
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
}

.records-scroll::-webkit-scrollbar { width: 8px; }
.records-scroll::-webkit-scrollbar-track { background: transparent; }
.records-scroll::-webkit-scrollbar-thumb {
  background: var(--line);
  border-radius: 4px;
}
.records-scroll::-webkit-scrollbar-thumb:hover { background: var(--dim); }

.records-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
  /* The card is centred; the table inside it is a list to read down. */
  text-align: left;
}

.records-table td {
  padding: 7px 6px;
  border-top: 1px solid var(--line);
  vertical-align: middle;
}

.records-table tr:first-child td { border-top: 0; }

/* Banding, very low: three columns of small type need a line for the eye to
   follow, and a rule per row would out-shout the names. */
.records-table tr:nth-child(even) td { background: rgba(255, 255, 255, 0.02); }

/* The player's own records get the accent that means "yours" everywhere else,
   and a rule down the left edge -- the same pair the current level's row uses
   in the Levels card, so the two lists say "this one is yours" the same way. */
.records-mine td { background: rgba(121, 185, 138, 0.08); }
.records-mine .records-level {
  color: var(--accent);
  box-shadow: inset 2px 0 0 var(--accent);
}

/* The rank number, so the list reads as a standing rather than as rows that
   happen to be in an order. Dim: it is an index, not a score. */
.records-rank {
  width: 1%;
  padding-right: 0 !important;
  color: var(--dim);
  font-size: 10.5px;
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}

.records-level {
  color: var(--ink);
  white-space: nowrap;
  padding-left: 8px !important;
}

.records-holder { width: 100%; }

/* Under the holder's name rather than beside it: the name is what is being
   read, and the place is a footnote to it. */
.records-place {
  display: block;
  margin-top: 1px;
  font-size: 10.5px;
  color: var(--dim);
}

/* Tabular figures so the move counts line up down the column even though the
   rows are different widths. */
.records-score {
  color: var(--ink);
  white-space: nowrap;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* Stands in for the whole table when no record has been set anywhere. Centred
   in the fixed-height box rather than sitting at the top of it, where against
   560px of empty panel it would read as a list that failed to load. */
.records-none {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  margin: 0 !important;
  padding: 0 24px;
  text-align: center;
  font-style: italic;
}

.records-trophy {
  width: 11px;
  height: 11px;
  fill: #d9b451;
  margin-right: 4px;
  vertical-align: -1px;
}

/* **The Hall of Fame on a phone.** The cap on .records-card above is what
   keeps both ways out of the card on screen; these are the numbers that buy
   the list back the room the cap takes off it. Everything above the list is
   fixed furniture -- trophy, heading, count, podium -- and on a 360px-wide
   screen that furniture was most of the card, leaving the scroller a couple of
   rows. Reported from a real phone, 2026-08-27. */
@media (max-width: 720px) {
  /* .gate-card's 24px, trimmed. The top keeps enough headroom for the X, which
     is anchored to the card and sits over this box rather than in it.

     **88vh, the same cap the About card wears at this width**, in place of the
     percentage the desktop uses. The two want the same thing and the About
     card is the one with the mileage -- it is the card on this site that has
     always sized right on a phone, which is why its number is the one copied
     here rather than a new one guessed at. vh is also the safer unit against a
     mobile URL bar: it is the tall-viewport height, so the cap does not grow
     when the bar slides away and leave the card standing past the screen. */
  .records-card {
    padding: 16px 12px 14px;
    max-height: 88vh;
  }

  /* Closer into the corner to match the smaller frame, the same move the About
     card makes at this width. */
  .records-card .card-close { top: 4px; right: 4px; }

  /* The emblem still reads at this size, and the 16px it gives back is a row
     of the list. */
  .records-trophy-big { width: 40px; height: 40px; margin: 0 auto 6px; }

  .records-card h2 { font-size: 15px; }
  .records-summary { margin-bottom: 10px !important; }
  .records-podium { margin-bottom: 12px; }

  /* **The list takes whatever is left, rather than a height of its own.** On a
     phone the window is the constraint, not the list, so the fixed 64vh is
     dropped for a flex fill: the card stands as tall as it is allowed and the
     scroller ends exactly where the Close button begins.

     No floor under it. A floor is a height the card is not allowed to give
     up, and any height the card cannot give up is one that pushes the Close
     button back off the bottom of a short window -- which is the bug this
     whole block exists to fix. A two-row list that scrolls is a worse card
     than a five-row one; a card you cannot close is not a card. */
  .records-scroll {
    height: auto;
    flex: 1 1 auto;
    margin-bottom: 12px;
  }

  /* Four columns of small type in a 340px-wide card: the cells give up their
     padding before the type gives up its size. */
  .records-table td { padding: 6px 4px; }
  .records-level { padding-left: 6px !important; }

  /* **The score column was being cut off the right edge** -- "81 moves · 0:"
     with the seconds past the card. .records-scroll scrolls on y, and a box
     that scrolls on one axis clips on the other, so the table simply ran out
     of the card rather than fitting into it. The width was never there to be
     had: two of the four columns refuse to wrap, and a level name and a score
     set on one line each leave the holder's name and town nothing.

     So the level name wraps at this width, and the clock moves under the move
     count -- the same shape .records-place already gives the holder's town,
     under the name it belongs to. The rows were two lines tall before this, so
     it costs no height. Reported from a real phone, 2026-08-27.

     The score is *not* simply let loose to wrap: `white-space: normal` on the
     whole cell broke it wherever it ran out of room, which was in the middle
     of "81 moves" -- three ragged lines per row. openRecords builds the cell
     out of three nodes for exactly this; the separator is what goes. */
  .records-level { white-space: normal; }
  .records-dot { display: none; }
  .records-secs {
    display: block;
    margin-top: 1px;
    font-size: 10.5px;
    color: var(--dim);
  }

  /* The rank stays on one line -- it is two characters -- but it stops
     reserving a column of its own width. */
  .records-rank { font-size: 10px; }
}

/* A phone held in landscape, or a short desktop window. The furniture above
   the list is nearly the whole card at this height, so the two pieces of it
   that are decoration rather than information go: the big trophy, and the air
   around the heading. The podium stays -- it is the standing the card is named
   for, and it is the part a player on a phone is most likely to have opened
   the card to see. */
@media (max-height: 560px) {
  .records-trophy-big { display: none; }
  .records-card h2 { font-size: 14px; }
  .records-summary { margin-bottom: 8px !important; }
  .records-podium { margin-bottom: 10px; }
}

/* ------------------------------------------------------------- solved card */

/* Wider than the other gate cards: this one carries a trophy, a score, the
   standing record and two buttons, and at 380 the record line wrapped. */
/* Wider than the other gate cards: this one carries a trophy, a score, a
   two-line world-record panel, a ten-chip rating row and two buttons. It was
   440px while the record was one line; the record grew a second line and the
   rating row arrived on 2026-09-02, and at 440 the chips came out under 40px
   each and the record rows wrapped.

   **The height cap and the scroll are load-bearing now, not tidiness.** The
   card can be held shut until a rating is given -- see rateGate in
   js/main.js -- so a card taller than the window would be a card whose only
   way out had been pushed off the bottom of the screen. 88vh is the cap the
   About and Hall of Fame cards already use at this width, and vh rather than
   % because it is the tall-viewport height and so does not grow when a mobile
   URL bar slides away. */
.solved-card {
  max-width: 520px;
  max-height: 88vh;
  overflow-y: auto;
  scrollbar-width: thin;
  text-align: center;
}

.solved-card h2 { margin-bottom: 2px; }

/* The moment the scoreboard exists for, so it is drawn at the size that says
   so. Hidden for a solve that did not take the record. */
.solved-trophy {
  width: 64px;
  height: 64px;
  margin: 2px auto 6px;
  fill: #d9b451;
}

.solved-trophy[hidden] { display: none; }

.solved-headline {
  margin: 0 0 14px !important;
  font-size: 13px;
  color: var(--dim);
}

.solved-record {
  color: #d9b451;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  /* Set apart from the score above it. It is a different claim -- the score is
     what this player did, this is where it stands against everyone -- and run
     tight against the numbers it read as another line of them. */
  margin-top: 12px;
}

/* The world-record display, laid over everything including the cards. Drawn by
   js/fireworks.js, which creates and removes this element itself.

   `pointer-events: none` is load-bearing rather than tidy: the canvas covers
   the whole viewport -- both card buttons included -- for the ten seconds it
   runs, and without it neither can be clicked for that whole time. */
.fireworks {
  position: fixed;
  inset: 0;
  z-index: 20;            /* over .gate, which is 10 */
  pointer-events: none;
}

/* Left-aligned inside a centred card: a term/definition list reads as a table,
   and a centred one gives the eye no column to follow. */
.solved-card .info-list { text-align: left; }

/* **The record, in a panel of its own with a ruled heading.** It used to be a
   single flex row -- trophy, holder, place -- which was the right shape while
   it only ever said one thing. It now says two on a solve that took the record,
   the new one over the one it displaced, and two claims in a row read as one
   run-on sentence. Ruled and stacked, it reads as a table of two entries.

   Wider than it was, too: .solved-card went from 440 to 520 on 2026-09-02 to
   carry this and the rating row under it without either wrapping. */
.solved-wr {
  margin: 0 0 16px;
  padding: 12px 14px 13px;
  text-align: left;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.02);
}

/* A `hidden` attribute loses to a `display` value set on the same element, and
   this block is laid out rather than left inline -- the same reason
   .solved-trophy needs its own. */
.solved-wr[hidden] { display: none; }

/* Gold when the record is this player's own, the signal .records-mine gives in
   the Hall of Fame. Kept to the border and a wash rather than the gold text a
   new record gets: this panel also appears on a solve that took nothing, and it
   must not read as a win. */
.solved-wr-mine {
  border-color: rgba(217, 180, 81, 0.45);
  background: rgba(217, 180, 81, 0.08);
}

.solved-wr-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 10px;
  padding-bottom: 9px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--dim);
  border-bottom: 1px solid var(--line);
}

.solved-wr-cup { flex: none; line-height: 0; }

/* Between the 64px celebration trophy and the 11px one in the Hall of Fame
   table: big enough to mark the panel at a glance, small enough that it cannot
   be mistaken for the big one on a solve that did not take the record. Came
   down from 26px when it moved into the heading, where it stands beside 10px
   type rather than beside a two-line block. */
.solved-wr-icon {
  display: block;
  width: 20px;
  height: 20px;
  fill: #d9b451;
}

.solved-wr-rows {
  display: flex;
  flex-direction: column;
  gap: 9px;
}

.wr-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
}

/* A column of its own rather than a run-in label, so NEW and PREVIOUS line up
   and the two records start at the same x. Fixed width because the two words
   are the only two it ever holds. */
.wr-tag {
  flex: 0 0 60px;
  font-size: 9.5px;
  letter-spacing: 0.1em;
  line-height: 1.6;
  text-transform: uppercase;
  color: var(--dim);
}

.wr-body {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.wr-who { font-size: 13px; color: var(--ink); }
.wr-where { font-size: 11px; color: var(--dim); }

/* recordPlace returns "" for a record with no usable place on it, and an empty
   line still takes the flex gap above. */
.wr-where:empty { display: none; }

/* The record just taken, and the one it beat. **The new one is marked, the old
   one steps back** -- without the pair reading as an order the two lines look
   like two records the level somehow has at once. Green rather than gold on the
   tag: gold is the panel's own colour when the record is this player's, and a
   gold tag inside a gold panel says nothing. */
.wr-new .wr-tag { color: var(--accent); }
.wr-new .wr-who { font-weight: 600; }
.wr-old .wr-who { color: var(--dim); }

/* "None -- you set the first", where a displaced record would go. */
.wr-who-none { color: var(--dim); font-style: italic; }

/* ------------------------------------------------- the difficulty rating */

/* **One tap, and the card will not close without it.** Built to match the
   record panel above -- same border, same wash, same ruled heading -- because
   the two are the same kind of thing: a block of the card that is about the
   level rather than about this solve. */
.solved-rate {
  margin: 0 0 16px;
  padding: 12px 14px 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.02);
}

.solved-rate[hidden] { display: none; }

/* The section is focused when the card opens owing a rating -- see the tail of
   openSolved -- so the keyboard starts on the row rather than on a disabled
   button. It is a programmatic target and never a tab stop, and a ring drawn
   round the whole panel would only ask what it meant. */
.solved-rate:focus { outline: none; }

.solved-rate-label {
  margin: 0 0 10px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-align: center;
  color: var(--dim);
}

/* **`flex: 1 1 0` on the chips, not a width.** Ten of them have to fit one row
   at every width the card is ever drawn at, and a row of ten fixed chips wraps
   to 5+5 somewhere around 360px -- which turns a single continuous scale into
   two groups and makes 5 to 6 look like a bigger step than it is. Sharing the
   row instead, they are about 46px each on a desktop card and about 24px on a
   320px phone, and the scale stays one line everywhere. */
.rate-chips {
  display: flex;
  gap: 4px;
}

.rate-chip {
  flex: 1 1 0;
  min-width: 0;
  padding: 7px 0;
  font: inherit;
  font-size: 12px;
  /* The row must not twitch as the two-digit 10 sits beside the one-digit 9. */
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  background: var(--panel);
  border: 1px solid #43454f;
  border-radius: 5px;
  cursor: pointer;
}

/* **:not(.rate-chip-on) is load-bearing, not tidiness.** This selector is
   0-3-0 and .rate-chip-on below is 0-1-0, so without it the hover won on the
   chip the player had just tapped -- and it sets a background but no colour,
   so the chip kept .rate-chip-on's near-black text and painted it on a dark
   grey. The chosen number went black under the pointer that chose it, and
   stayed black until the mouse moved away. Reported 2026-09-02.

   Not fixed by raising .rate-chip-on to 0-2-0, which still loses. The chosen
   chip is simply not a hover target: it is already what it is going to be. */
.rate-chip:hover:not(:disabled):not(.rate-chip-on) {
  border-color: var(--dim);
  background: #272932;
}

.rate-chip-on {
  font-weight: 700;
  color: #0f1a12;
  background: var(--accent);
  border-color: var(--accent);
}

/* **The chip flares gold and settles into green.** Put on the one chip just
   tapped, by renderRating, and cleared straight afterwards so it plays once
   rather than on every redraw of the row.

   It picks the sweep's gold up and hands it to the accent green that means
   "yours" everywhere else on this site -- so the tap answers the light that
   ran along the row asking for it, instead of simply turning a square green.

   **No fill.** The keyframes end on exactly what .rate-chip-on paints, so
   letting the animation stop and the base rule take over is the same picture
   with nothing held -- and a `forwards` fill here would sit on top of the
   class for ever, which would matter the moment either colour changed.

   The text stays dark throughout: it is dark on the gold and dark on the
   green, so there is nothing to cross-fade and no moment where it disappears
   into its own background. */
.rate-chip-picked {
  animation: rate-pick 620ms ease-out 1 none;
}

@keyframes rate-pick {
  0% {
    color: #1a1508;
    background: #f2dc9b;
    border-color: #f7ead0;
    box-shadow: 0 0 20px rgba(242, 220, 155, 0.95);
    transform: scale(1.16);
  }
  35% {
    color: #1a1508;
    background: #d9b451;
    border-color: #f2dc9b;
    box-shadow: 0 0 15px rgba(217, 180, 81, 0.7);
    transform: scale(1.09);
  }
  100% {
    color: #0f1a12;
    background: var(--accent);
    border-color: var(--accent);
    box-shadow: none;
    transform: none;
  }
}

/* **A run of yellow light along the row, out and back, once when the card
   opens.** The rating is the one thing on this card that is asked of the player
   rather than told to them, and it sits under a trophy and a score that are both
   louder. This is what points at it.

   **Two passes of one keyframe, staggered off the chip's own index.** Each chip
   carries `--i` (0 to 9, set in renderRating), and the two delays are worked out
   from it here rather than written down as a list -- so the timing has one home
   and widening the scale costs nothing. The first pass runs left to right off
   `--i`; the second runs right to left off `9 - --i`, starting once the first has
   set off along the whole row.

   Yellow rather than the accent green: green is what a CHOSEN chip turns, and a
   sweep in the same colour would read as ten selections being made in turn. It is
   the gold the trophy and the world-record frame already use.

   **It turns rather than restarting.** The two passes are one traversal of the
   row and back, counted in steps: the outward pass lights chip `i` at step `i`,
   the return lights it at step `2 * last - i`, and the far chip -- where those
   two are the same step -- is the turn. There is no pause at the end of the row
   because there is no second run to start; the light simply comes back. It was
   written as a fixed 900ms delay first, which left a dark frame at the far end
   and read as two separate sweeps. Jeff report, 2026-09-02.

   `--last` is the index of the far chip, set from renderRating alongside `--i`,
   so the arithmetic here still has no idea how long the scale is and widening
   it goes on costing nothing.

   Timing: 55ms a step. The light reaches the far end at 495ms, is back at the
   near end at 990ms and done by about 1.4s -- long enough to follow out and
   back, over well before anyone has read the record above it and reached for a
   number. */
.rate-attend .rate-chip:not(.rate-chip-on) {
  animation:
    rate-attend 380ms ease-out calc(var(--i) * 55ms) 1 none,
    rate-attend 380ms ease-out calc((2 * var(--last) - var(--i)) * 55ms) 1 none;
}

/* **The fill is none, and that is load-bearing rather than a default left
   alone.** Written as `both` at first, the return pass held its own 0% frame
   through its 560ms delay -- and being the later of the two animations on the
   property, it won the cascade and painted the resting colour over the outward
   pass for its whole run. The sweep simply did not appear. With no fill, an
   animation contributes nothing before it starts or after it ends, so the two
   passes hand the row over cleanly. Caught on 2026-09-02 by freezing the chips
   mid-animation and reading their computed background back.

   0% and 100% still restate the resting colours so each pass lands exactly
   where it set off. */
/* **The flash is a solid gold, not a wash.** It was a 22% tint over the panel
   at first, which measured as a colour change and read as nothing at all on a
   card that has a trophy on it -- the chip is 24px wide on a phone, and a fifth
   of a colour across 24px for a third of a second is not something anyone
   notices. Reported as "no highlight seen", 2026-09-02. It now fills, lifts and
   throws a glow, which is about as loud as ten small squares can be. */
@keyframes rate-attend {
  0% {
    border-color: #43454f;
    background: var(--panel);
    color: var(--ink);
    box-shadow: none;
    transform: none;
  }
  45% {
    border-color: #f2dc9b;
    background: #d9b451;
    color: #1a1508;
    box-shadow: 0 0 15px rgba(217, 180, 81, 0.75);
    /* Transform, so the lift costs no layout: the chips are flex items sharing
       the row and nothing else may move as the light goes past. */
    transform: translateY(-3px) scale(1.07);
  }
  100% {
    border-color: #43454f;
    background: var(--panel);
    color: var(--ink);
    box-shadow: none;
    transform: none;
  }
}

/* The sweep is a pointer, not information -- the caption under the row already
   says a rating is wanted -- so it is dropped outright rather than slowed. */
@media (prefers-reduced-motion: reduce) {
  .rate-attend .rate-chip:not(.rate-chip-on) { animation: none; }
  /* The chip still turns green -- .rate-chip-on does that on its own. Only
     the flare on the way there goes. */
  .rate-chip-picked { animation: none; }
}

/* Read-only, for a player who has already rated this level. The row is left in
   place rather than replaced by a sentence, so what they see is the same row
   they used with their own answer standing in it. */
.rate-chip:disabled { cursor: default; opacity: 0.45; }

/* ...and the one they picked stays fully lit, or the panel would show a row of
   ten equally grey chips with nothing to read. Beats `button:disabled`'s own
   opacity on specificity, which is the whole job of this rule. */
.rate-chip-on:disabled { opacity: 1; }

/* `.gate-card p` sets a margin and a size on every paragraph in a card and is
   more specific than a bare class, so this is scoped to win rather than left
   to source order. */
.solved-rate .rate-word {
  margin: 9px 0 0;
  font-size: 12px;
  text-align: center;
}

.solved-rate .rate-word-ask { color: var(--dim); font-style: italic; }
.solved-rate .rate-word-set { color: var(--ink); font-weight: 600; }

/* Replay and Next Level share the row evenly. Both are always present -- see
   the note in index.html -- so `openSolved` being called twice cannot shuffle
   them under the pointer. On a narrow phone they stack instead of squeezing to
   half a word each. */
.solved-actions {
  display: flex;
  gap: 10px;
  margin-top: 4px;
}

.solved-actions button {
  flex: 1 1 0;
  padding: 11px 14px;
  font-weight: 600;
}

/* Next Level is where the player is going, so it carries the accent; Replay is
   the quieter of the two and keeps the panel treatment. */
#solved-next {
  color: #0f1a12;
  background: var(--accent);
  border-color: var(--accent);
}

#solved-next:hover { background: #8fcc9c; }
#solved-next:active { background: #6aa87a; }

/* Disabled it is not going anywhere, so it should not still look like the
   brightest thing on the card. */
#solved-next:disabled,
#solved-next:disabled:hover {
  color: var(--ink);
  background: var(--panel);
  border-color: var(--line);
}

.btn-quiet { color: var(--dim); }
.btn-quiet:hover { color: var(--ink); }

@media (max-width: 420px) {
  .solved-actions { flex-direction: column; }

  /* **Ten chips across a phone are narrow by arithmetic** -- the row is the
     card divided by ten, which is about 24px on a 390px screen and there is no
     width to be had. So the tap target is bought in height instead, where
     there is room to spend it: 11px of padding stands the chip at roughly
     38px, which is a finger. The type comes down half a point so the
     two-digit 10 keeps its air at the narrowest the row ever gets. */
  .rate-chips { gap: 3px; }
  .rate-chip { padding: 9px 0; font-size: 11.5px; }
}

/* Nothing left to play. Kept visible rather than removed, for the reason
   nextLevel gives. */
button:disabled {
  opacity: 0.4;
  cursor: default;
}

button:disabled:hover { background: var(--panel); }
button:disabled:active { transform: none; }

/* --------------------------------------------------------- level info card */

.info-card { max-width: 380px; }

.info-list {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 8px 16px;
  margin: 4px 0 18px;
  font-size: 13px;
}

.info-list dt {
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--dim);
  padding-top: 2px;
}

.info-list dd { margin: 0; color: var(--ink); }

/* The card is a reference sheet rather than a row in a list, so its flags are
   set a size up from the ones in the scoreboard: at the inherited 9px the chip
   read as punctuation beside the name it belongs to. */
.info-list .flag-code {
  font-size: 11px;
  padding: 2px 6px;
}

.info-list .flag { font-size: 1.35em; }

/* Keeps a flag and the name it belongs to on one line. */
.info-inline {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
}

/* The world record's heading inside the level-info card.

   Left-aligned with the list under it rather than centred like the card's own
   <h2>: this is a divider inside the card, not a second title for it. The rule
   above it does the separating, so the type itself can stay quiet -- the same
   small uppercase as an .info-list dt, one shade brighter, because it heads
   those terms rather than standing beside them. */
.info-heading {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 0 0 2px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink);
}

/* The same gold as the World Record card and the Hall of Fame, at the size the
   scoreboard's rows use: it is a marker on a heading, not the card's emblem. */
.info-trophy {
  width: 13px;
  height: 13px;
  fill: #d9b451;
  flex: 0 0 auto;
}

/* Stands in for the record list on a level nobody has solved. Takes the list's
   own margin so the Close button below it does not move between the two
   states. */
.info-none {
  margin: 4px 0 18px;
  font-size: 13px;
  color: var(--dim);
}

#info-ok { width: 100%; }

/* ------------------------------------------------------------------- help */

/* Opposite corner to the logo, on the same fixed layer as the scale control. */
/* The Hall of Fame button, beside the help button in the same corner.
   
   **A pill, not a circle, and the count sits beside the trophy.** Under it, in
   a 32px button, the number lands at about 9px and reads as a smudge; beside it
   both halves are full size and the button says what it is at a glance. The
   height, border, colours and hover are the help button's, so the two read as
   one pair of controls rather than two unrelated ornaments -- only the width
   and the radius differ, and the radius is a full pill so the shape still
   answers the circle next to it.

   right: 56px is the help button's 16px plus its 32px plus 8px of air. The two
   are pinned rather than laid out, so that number is the only thing keeping
   them apart: move one and the other has to follow. */
.records-fab {
  /* **No longer pinned on its own.** It sits in .corner-nav with the About and
     How to Play links and the help button, which is what now holds the corner
     and what spaces the four of them. The `right: 56px` that used to keep this
     clear of the help button went with it -- a hand-kept number that had to be
     changed whenever anything else arrived in the corner, and something else
     has now arrived twice. */
  display: flex;
  align-items: center;
  gap: 5px;
  height: 32px;
  /* Wider on the right than the left: the count is the last thing on the
     button and a digit needs more air off a curved edge than the trophy's
     square-ish silhouette does. Jeff's call, 2026-08-23. */
  padding: 0 15px 0 11px;
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  color: var(--dim);
  background: var(--panel);
  border: 1px solid #43454f;   /* the help button's, and for its reason */
  border-radius: 999px;
}

.records-fab .pin-icon {
  width: 15px;
  height: 15px;
  /* The same gold as every other trophy on the page -- the World Record card's
     emblem, the Hall of Fame's, the podium's first place and the badge on the
     scoreboard. **Not currentColor**: this button lightens its text on hover
     and the trophy must not go with it, or it would be the one trophy in the
     game that changes colour when a pointer passes over it. */
  fill: #d9b451;
  /* Sits a touch low against the digit beside it rather than centred on the
     button: the art's cup is top-heavy, so optical centre is below geometric.
     Jeff's call, 2026-08-23. */
  margin-top: 1px;
}

.records-fab-count { font-variant-numeric: tabular-nums; }

.records-fab:hover {
  color: var(--ink);
  border-color: var(--dim);
  background: #272932;
}

.help-fab {
  /* Pinned by .corner-nav now, along with the rest of the corner. */
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  padding: 0;
  font-size: 16px;
  font-weight: 700;
  line-height: 30px;
  color: var(--dim);
  background: var(--panel);
  /* A step lighter than the --line every other edge on the page uses. This one
     is a lone circle on the background rather than the edge of a panel, so at
     --line (#2f3038) it barely separated from the dark behind it. About a fifth
     of the way from --line to --dim, which leaves the hover state -- a full
     --dim border -- clearly further on. Jeff's call, 2026-08-22. */
  border: 1px solid #43454f;
  border-radius: 50%;
}

/* On load the button waves once or twice, in the green that means "yours"
   elsewhere, so a first-time player notices there is a help card at all. Three
   rings and then it stops for good -- an indicator that never settles stops
   being an invitation and becomes a fault to be ignored.
   `App.bindHelp` adds the class, and takes it off early if the card is opened
   before the rings finish, since by then it has done its job.

   The ring is a spread box-shadow rather than a scaled element: it expands
   outside the button without taking any layout space, so nothing beside it
   moves, and the button is `position: fixed` with the board under it. */
@keyframes help-attract {
  0%   { box-shadow: 0 0 0 0 rgba(121, 185, 138, 0.55); }
  70%  { box-shadow: 0 0 0 13px rgba(121, 185, 138, 0); }
  100% { box-shadow: 0 0 0 0 rgba(121, 185, 138, 0); }
}

.help-fab.attract {
  border-color: var(--accent);
  color: var(--accent);
  animation: help-attract 1.5s ease-out 3;
}

/* Anyone who has asked not to be waved at is not waved at. The button keeps the
   accent colour so it still stands out on first load; only the motion goes. */
@media (prefers-reduced-motion: reduce) {
  .help-fab.attract { animation: none; }
}

.help-fab:hover {
  color: var(--ink);
  border-color: var(--dim);
  background: #272932;
  transform: none;   /* the global button:active nudge would jitter a fixed dot */
}

/* Wider than the other cards: this one is prose, and the history reads badly
   in a 420px column. Overrides .gate-card's fixed width, not just its max.

   **The card itself no longer scrolls -- .help-scroll inside it does** -- so
   the padding .gate-card sets is taken off here and put back on the scroller.
   Padding on a scroll container is padding on the *frame*, which would leave
   the copy running out from under a 24px band at the top rather than starting
   below it, and it would push the scrollbar 24px in from the card's edge. */
.help-card {
  width: 620px;
  max-width: 100%;
  /* Long enough to need scrolling on a short window; the backdrop is fixed, so
     the card has to do its own. */
  max-height: 82vh;
  padding: 0;
  overflow: hidden;   /* keeps the scroller inside the card's rounded corners */
}

.help-scroll {
  /* Inherits .gate-card's column layout, which #help-ok's full width and the
     ordinary block flow of the copy both rely on. */
  display: flex;
  flex-direction: column;
  min-height: 0;      /* or a flex item refuses to shrink below its content */
  overflow-y: auto;
  /* .gate-card's 24px, with headroom at the top for the corner X, which now
     sits on the card above this box and overlaps its first 30px.

     **The 12px at the bottom is the Close button's skirt budget, not spacing.**
     A sticky element cannot leave its containing block, so #help-ok comes to
     rest on this box's content edge and the padding below it is a strip of
     card the copy scrolls through. The button's box-shadow has to cover that
     strip, so the two numbers are a pair -- see #help-ok. */
  padding: 46px 24px 12px;
}

/* Matches the scoreboard's, so the two scrolling panels look like one idea. */
.help-scroll::-webkit-scrollbar { width: 8px; }
.help-scroll::-webkit-scrollbar-thumb {
  background: var(--line);
  border-radius: 4px;
}
.help-scroll::-webkit-scrollbar-track { background: transparent; }
.help-scroll { scrollbar-width: thin; scrollbar-color: var(--line) transparent; }

/* **Both ways out of this card stay on screen while it scrolls.**

   The About copy is long -- four sections and two photographs -- and on a phone
   that is many screens of scrolling. The X used to scroll away with the content
   and the Close button sat at the very bottom, so the only ways out were to
   scroll back to the top, scroll all the way down, tap the sliver of backdrop
   round the card, or press Escape. On a phone the first two are a chore, the
   third is a very small target and the fourth does not exist. Reported from a
   real phone, 2026-08-23.

   The Close button is sticky: it sticks to the scrollport it lives in, so it
   stays inside the card's rounded corners and moves with it if the window
   resizes. It needs a background of its own -- the text scrolls underneath it,
   not behind the card.

   **The X is not sticky, it is absolute against the card**, which is the whole
   reason .help-scroll exists. Sticky put it in the scrollport, and there the
   scrollbar's 8px lane held it a visible step in from the corner it is
   supposed to occupy; nudging it further out only painted it over the thumb.
   Anchored to the card instead it sits in the true corner, over the scrollbar
   rather than beside it, and never moves. Jeff's call, 2026-08-23. */
.help-card .card-close {
  position: absolute;
  top: 6px;
  right: 6px;
  margin: 0;
  /* Above the sticky Close button, and above the copy that scrolls under it. */
  z-index: 3;
  background: var(--panel);
  border-radius: 4px;
}

.help-card h2 { text-align: center; }

/* The four section titles, reversed out: dark text on a light bar rather than
   dim text on the card. The About copy is long enough now that a quiet label
   was not enough of a break -- The Story of Muse, How to Play, Scoring and How
   We Brought Muse Back each start a different subject, and the reader should
   see where one ends without reading it. */
.help-card h3,
.prose h3 {
  margin: 26px 0 12px;
  padding: 6px 10px;
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--panel);
  background: var(--ink);
  border-radius: 4px;
}

/* The first one follows the card's own title, so it does not need the full gap
   above it -- that space is already there. */
.help-card h3:first-of-type,
.prose h3:first-of-type { margin-top: 14px; }

.help-card p,
.prose p {
  margin: 0 0 11px;
  font-size: 15px;
  line-height: 1.55;
  color: var(--ink);
}

.help-list {
  margin: 0;
  padding-left: 18px;
  font-size: 15px;
  line-height: 1.55;
  color: var(--ink);
}

.help-list li { margin-bottom: 7px; }

.help-card kbd,
.prose kbd {
  display: inline-block;
  min-width: 19px;
  margin: 0 1px;
  padding: 1px 5px;
  font: inherit;
  font-size: 13px;
  text-align: center;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 3px;
}

/* The animated player, centred under the prose and above the Close button.
   Square, and sized here rather than on the element: the canvas backs itself at
   the device pixel ratio, so the CSS box is the only size the rest of the code
   talks about. Decorative, so it is out of the pointer's way -- the Close
   button sits directly beneath it. */
.help-player {
  display: block;
  width: 72px;
  height: 72px;
  margin: 20px auto 0;
  pointer-events: none;
}

/* A line on its own, given room. The new About copy is written in short
   beats -- "Until now.", "Fewest moves wins." -- and each is a full stop in the
   argument rather than another paragraph, so it gets air above and below and a
   size up from the prose around it. */
.help-beat {
  margin: 16px 0 16px !important;
  font-size: 16px;
  text-align: center;
  color: var(--ink);
}

/* The way out of a section of the About card and on to the page that tells it
   properly. Quiet, right-aligned and small: it is an offer at the end of a
   passage, not a call to action, and the reader who wanted the short version
   has already had it by the time they reach one. */
.help-link {
  margin: 4px 0 2px !important;
  font-size: 12.5px !important;
  text-align: right;
}

.help-link a {
  color: var(--accent);
  text-decoration: none;
}

.help-link a:hover { text-decoration: underline; }

/* The address under the Get in Touch heading, set like .help-beat rather than
   like the prose: it is the one thing in that section a player is meant to act
   on, and buried in a paragraph at 13px on a phone it was neither easy to see
   nor easy to hit. Centred, a size up, and in the accent so it reads as a link
   without an underline having to say so.

   Generous padding for the same reason -- a mailto on a phone is a tap target,
   and the line box of a 15px link is not one. */
.help-contact {
  margin: 12px 0 0 !important;
  text-align: center;
}

.help-contact a {
  display: inline-block;
  padding: 8px 14px;
  font-size: 15px;
  color: var(--accent);
  text-decoration: none;
  border: 1px solid var(--line);
  border-radius: 4px;
}

.help-contact a:hover {
  color: var(--ink);
  border-color: var(--accent);
}

/* The two photographs in the About card: Jeff and Perry, and the scanned
   printout of an original level. Full width of the card and framed, so each
   reads as a photograph set into the page rather than as part of it. */
.help-photo,
.help-scan {
  display: block;
  width: 100%;
  height: auto;
  margin: 14px auto 16px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: #000;
}

/* Both photographs run narrower than the column and are centred in it --
   `margin: auto` above is what centres them. Neither wants the full width: the
   portrait competed with the prose at 100%, and the scan of the level printout
   reads better with air round it than filling the card edge to edge. */
.help-photo { width: 80%; }
.help-scan { width: 80%; }

/* Always reachable, whatever has been scrolled past. It used to stick flush to
   the card's bottom edge at bottom: -24px; it now sits 12px clear of it, which
   was Jeff's call on 2026-08-23.

   **The box-shadow is not decoration and must not be dropped.** Lifting the
   button opens a 12px strip between it and the bottom of the card, and the
   text scrolls *underneath* a sticky element -- so without something opaque in
   that strip, lines of the About copy slide through the gap below the button.
   A spread-only shadow paints the panel colour across it. It skirts all four
   sides, which also keeps text off the button's shoulders; margin-top is what
   stops it eating into the copy above. */
#help-ok {
  width: 100%;
  /* Wider than the skirt below, so the paragraph above is not painted over
     when the card is scrolled to the end. */
  margin-top: 26px;
  position: sticky;
  bottom: 6px;
  z-index: 2;
  /* A shade off the card behind it. The base button colour is var(--panel),
     which is the card's own background, so on this card alone the button read
     as a bare outline. Jeff's call, 2026-08-23. */
  background: #272932;
  /* 20px, against 12px of padding below the button: the skirt has to reach the
     card's bottom edge or the copy slides through the strip underneath. */
  box-shadow: 0 0 0 20px var(--panel);
}

#help-ok:hover { background: #303341; }
#help-ok:active { background: #383b4a; }

/* Marks a page loaded with the unlock code in its URL, next to the Levels
   heading. Deliberately quiet but never invisible: an unlocked list is
   indistinguishable from a finished one otherwise. See UNLOCK_CODE in
   js/main.js. */
.unlock-tag {
  margin-left: 8px;
  padding: 1px 6px;
  border: 1px solid var(--dim);
  border-radius: 999px;
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
  vertical-align: middle;
}

/* ------------------------------------------------------------- corner nav */

/* **Everything in the top right corner, in one row.**

   It holds four things now -- How to Play, About, the Hall of Fame count and
   the ? button -- and before this each one was pinned with its own top/right
   pair, spaced by hand-kept numbers. The stylesheet said so out loud: "move
   one and the other has to follow". Two links arrived and there was nothing
   sensible to move them to.

   So the corner is a flex row and the buttons are its children. The gap is the
   only spacing figure left, the row grows leftward as things are added to it,
   and #records-fab hiding itself while the records are in flight closes up
   behind it rather than leaving a hole.

   Wrapping is insurance rather than a layout: at the sizes below the four fit
   on one line down to about 340px. If a longer label ever pushes past that the
   row breaks onto a second line instead of running under the centred logo. */
.corner-nav {
  position: fixed;
  top: 16px;
  right: 16px;
  z-index: 5;              /* the layer the two buttons used to hold */
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 8px;
  /* Never wider than the window less its own inset, so a wrap is a wrap and
     not a row hanging off the left edge. */
  max-width: calc(100vw - 32px);
}

/* The page links, cut to the Hall of Fame pill's pattern: the same height, the
   same border, the same colours and the same hover, because they stand in the
   same row and anything else would read as two sets of controls rather than
   one. Only the radius is shared with the pill rather than the circle -- a
   word needs a pill.

   text-decoration: none and the explicit colour because these are anchors
   among buttons: left alone they would be the only underlined blue thing on
   the page. */
.nav-pill {
  display: flex;
  align-items: center;
  height: 32px;
  padding: 0 13px;
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;     /* "How to Play" is three words and one control */
  color: var(--dim);
  text-decoration: none;
  background: var(--panel);
  border: 1px solid #43454f;
  border-radius: 999px;
}

.nav-pill:hover {
  color: var(--ink);
  border-color: var(--dim);
  background: #272932;
}

/* The page you are already on. Marked rather than removed -- a nav that drops
   its current entry moves the others under the pointer between pages. */
.nav-pill[aria-current="page"] {
  color: var(--ink);
  border-color: var(--dim);
}

/* Beats display: flex above, which would otherwise keep a hidden pill on
   screen. The About pill wears `hidden` on every page as of 2026-09-30. */
.nav-pill[hidden] { display: none; }

/* On a phone the corner has to hold the same four things in a third of the
   width. Everything comes down a step together, so the row still reads as one
   set of controls. */
@media (max-width: 720px) {
  /* **The row is given a left edge, not a max-width.** A max-width does not
     stop a wrapped flex box from claiming its full width -- it only decides
     where the wrap happens -- so at 360px the nav duly broke onto two rows and
     then sat its 220px box down on the logo anyway. Measured, 2026-09-02.

     Nailing `left` instead makes the clearance an invariant rather than an
     arithmetic hope: the box starts at x124, the logo ends at x114, and no
     window width can bring them together. The pills stay hard right because
     the base rule already sets justify-content: flex-end, and they wrap up off
     the right-hand end when the room runs out. Two rows stand 62px tall, which
     body's 100px of top padding still clears.

     **pointer-events must come off the box.** Spanning from x124 to the right
     edge it now covers the top of the board, and a fixed box with no
     background still swallows what it covers -- the same trap .site-logo
     carries a note about. The pills take theirs back one line down. */
  .corner-nav {
    top: 10px;
    right: 10px;
    left: 124px;
    gap: 6px;
    max-width: none;
    pointer-events: none;
  }

  .corner-nav > * { pointer-events: auto; }
  .nav-pill { height: 28px; padding: 0 10px; font-size: 11px; }
  .records-fab { height: 28px; padding: 0 12px 0 9px; font-size: 11px; }
  .help-fab { width: 28px; height: 28px; font-size: 14px; line-height: 26px; }
}

/* -------------------------------- the game's two corner controls, off the game */

/* On the written pages the Hall of Fame pill and the ? are anchors, not
   buttons: there is no game under those pages for them to open a card on, so
   they lead into one (/#hall-of-fame and /#about, read by urlCard in
   js/main.js). They wear the game's own classes so the corner is the same
   corner, and these are the three things an anchor does not inherit from a
   button.

   Written as `a.records-fab` rather than a class of their own so that the two
   can never drift: change the pill in the game and the link changes with it.

   **`span.help-fab` is in here for the same reason and it is not decoration.**
   On /about/ the ? is a spent <span> rather than an anchor -- see the rule
   below -- and a span inherits even less from a button than an anchor does. It
   was left out when this rule was written and the glyph duly sat hard against
   the left edge of its circle on that one page, because the centring a button
   gets free is `text-align: center` from the UA stylesheet and a span has
   none. Reported 2026-09-04. Any third element that ever wears .help-fab needs
   to be named here too; the alternative is putting these three lines on
   .help-fab itself, which would take the game's own button off the pixel
   line-height it is tuned on at both breakpoints. */
a.records-fab,
a.help-fab,
span.help-fab {
  text-decoration: none;
  /* A button centres its label; an anchor does not, and the ? sat high in its
     circle. Flex rather than the button's line-height, which is set on
     .help-fab in pixels and would have to be kept in step with the height by
     hand at every breakpoint. */
  display: flex;
  align-items: center;
  justify-content: center;
}

/* **Nothing wears this as of 2026-09-30**: the ? on /about/ is a live anchor
   again, because with the About pill hidden it is the only About control in
   the corner. Kept, with span.help-fab above, in case the spent ? comes back.

   The ? on the About page, where the card it opens is the page being read. A
   live control that reopens what is already on screen is worse than one that
   is plainly spent, so it stays in the corner, keeps the row's shape, and says
   -- in colour, in the cursor and in aria-disabled -- that it has nothing to
   do here.

   Quieter than the controls beside it but not invisible: at full --dim it read
   as available, and hidden it would have changed the shape of the corner
   between the game and this page, which is the whole thing this row exists to
   avoid. */
.help-fab.is-disabled {
  color: #4c4e58;
  border-color: #33353d;
  cursor: default;
  /* It is a <span>, so there is no hover state to suppress; this stops a
     stray drag from selecting the glyph as text. */
  user-select: none;
}

/* ----------------------------------------------- About / How to Play pages */

/* The two pages that are not the game: /about/ and /how-to-play/. They exist
   to be read and to be indexed -- a search engine and an answer engine are
   handed prose on a URL of its own rather than a paragraph inside a dialog
   that only opens on a click.

   They borrow this stylesheet whole rather than carrying one of their own, so
   the game and the pages about it cannot end up looking like two products.
   What they add is a reading column: the game is a fixed, unscrolling board
   and these are documents, so the page has to give scrolling back. */
.page {
  /* body is a centring flex column built to hold one board in the middle of
     the window. A document is not that: it starts at the top and runs down.
     Both of those have to be given back explicitly, or a long page centres
     itself and its first paragraph is above the top of the window. */
  display: block;
  min-height: 100vh;
  padding: 0 16px 80px;
}

/* The logo is the way back to the game on these pages, so it is wrapped in a
   link -- and .site-logo sets `pointer-events: none`, which on the game is
   right (it lies over the board and must never swallow a move) and here would
   make the link untappable. Given back on these pages only. */
.site-logo-link { pointer-events: auto; cursor: pointer; }

/* The game's footer is fixed to the window because the game never scrolls.
   These pages do, and a fixed footer on a document is a band that sits over
   the last paragraph forever. It goes back into the flow at the end of the
   page, where a footer belongs. */
.site-footer-static {
  position: static;
  margin: 46px auto 0;
  padding: 0 16px;
}

/* A measure, not a width. Prose set the full width of a monitor is not read,
   it is skimmed, and this is the one place on the site with something to say
   at length. The top padding clears the fixed logo the same way the game's
   body padding does -- see .site-logo, which is fixed and so buys its room
   from whatever is under it. */
.page-inner {
  max-width: 720px;
  margin: 0 auto;
  padding-top: 132px;
}

@media (max-width: 720px) {
  .page-inner { padding-top: 92px; }
}

/* The page title, set like the cards' own heading a size up: the pages and the
   dialogs are the same voice. */
.page-title {
  margin: 0 0 6px;
  font-size: 26px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-align: center;
  color: var(--ink);
}

/* One line under the title saying what the page is, for a reader and for a
   snippet alike. Deliberately a sentence and not a slogan. */
.page-lead {
  margin: 0 0 26px;
  font-size: 15px;
  line-height: 1.6;
  text-align: center;
  color: var(--dim);
}

/* The reading column itself. Framed like a card, because everything else on
   this site that holds words is one. */
.page-card {
  padding: 26px 30px 30px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
}

@media (max-width: 720px) {
  .page-title { font-size: 20px; }
  .page-card { padding: 18px 16px 22px; }
}

/* The section heads on these pages, set like the bars the About card uses for
   the same job -- see .help-card h3, which .prose h3 joins. An h2 here sits
   above those, so it is the quieter of the two: a rule and small caps, against
   the reversed bar underneath it. */
.page-card h2 {
  margin: 30px 0 14px;
  padding-top: 18px;
  font-size: 13px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink);
  border-top: 1px solid var(--line);
}

.page-card h2:first-child { margin-top: 0; padding-top: 0; border-top: 0; }

/* **The prose styles are shared with the About card, not copied from it.**
   The rules a few hundred lines up are scoped to .help-card; each one names
   .prose as well, and these pages put that class on the column.

   **The words are NOT shared, and this comment used to say they were.** It
   claimed the About copy was "literally the same file in both places
   (partials/about-body.html)". That was true of the arrangement before
   2026-08-30 and has not been true since: there is no partials/ folder on the
   drive, the card was cut to roughly a third of its old length, and /about/
   was rewritten from scratch. Corrected 2026-09-03, because a comment saying
   one edit updates both surfaces would lead somebody to recreate exactly the
   duplication the split was made to end -- the same fifteen hundred words at
   two URLs, competing with each other, neither ranking.

   So the reason to keep the styles shared is no longer that one string is
   drawn twice. It is that the card and the pages have to read as one product:
   the same measure, the same heading bars, the same colour of a link in
   running text. Two stylesheets for two surfaces is how a site starts looking
   like two sites. See CLAUDE.md, "The written pages, and why each one exists".

   The one string that genuinely is duplicated is the Get in Touch paragraph,
   word for word in index.html and /about/. It is four lines long and not worth
   a second version; if it is ever edited, edit both. */

/* Set wider here than in the card: a page has the measure for it. */
.prose p { font-size: 15.5px; }

/* **"This is a firsthand account."** One line at the head of the history and
   the restoration pages saying who is speaking. It is not decoration: an
   answer engine weighs a primary source differently from a summary of one, and
   it can only do that if the page says which it is. Set apart from the prose
   so a reader takes it as a note about the page rather than the first sentence
   of it. */
.page-source {
  margin: 0 0 22px !important;
  padding: 10px 14px;
  font-size: 13px !important;
  font-style: italic;
  line-height: 1.5;
  color: var(--dim) !important;
  background: var(--bg);
  border-left: 3px solid var(--accent);
  border-radius: 0 4px 4px 0;
}

/* A photograph with a caption that says what it is. The caption carries as
   much weight as the alt text does and is read by everybody, so it is written
   to describe the thing rather than to label it. */
.page-figure {
  margin: 20px 0 24px;
  text-align: center;
}

.page-figure img {
  display: block;
  width: 84%;
  height: auto;
  margin: 0 auto;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: #000;
}

.page-figure figcaption {
  max-width: 84%;
  margin: 10px auto 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--dim);
}

/* The way on to the page that carries the next part of the story. Ends a
   section rather than interrupting it, so it sits right and quiet. */
.page-next {
  margin: 6px 0 4px !important;
  font-size: 13.5px !important;
  text-align: right;
}

.page-next a { color: var(--accent); text-decoration: none; }
.page-next a:hover { text-decoration: underline; }

/* Links inside the running prose of a page: the accent, without an underline
   until a pointer is on them. The About card's copy has almost none; these
   pages are full of them, because each one hands the reader on to the page
   that answers the next question. */
.prose p > a,
.prose li > a {
  color: var(--accent);
  text-decoration: none;
}

.prose p > a:hover,
.prose li > a:hover { text-decoration: underline; }

/* Sits at the foot of both pages: the pages are a detour from the game and
   this is the way back to it. Given real weight, because it is the one thing
   on the page that is an action rather than something to read. */
.page-cta {
  display: block;
  margin: 30px auto 0;
  padding: 13px 20px;
  max-width: 320px;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-align: center;
  text-transform: uppercase;
  color: #12331a;
  text-decoration: none;
  background: var(--accent);
  border-radius: 6px;
}

.page-cta:hover { background: #8fcc9c; }

/* The other page, offered under the button in a quieter voice. */
.page-more {
  margin: 16px 0 0;
  font-size: 13px;
  text-align: center;
  color: var(--dim);
}

.page-more a { color: var(--ink); }

/* --------------------------------------------------------------- diagrams */

/* The boards on the How to Play page.

   **They are real boards, drawn by the game's own renderer** -- see
   js/diagrams.js. Nothing here draws a piece or names a piece colour; the
   canvas does that, with the same code the board in the game uses, which is
   the point. What is left for the stylesheet is the frame round each one, the
   captions, and the two marks on the deadlock board.

   The canvas carries its own width and height as inline styles, set by
   Renderer.resize from the board's size and the cell size chosen in
   diagrams.js. Those sizes are picked to fit a phone, so nothing here scales
   them; the one guard is max-width, in case a board is ever widened. */
.diagram {
  margin: 18px 0 22px;
  padding: 16px;
  text-align: center;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 6px;
}

/* Wraps one canvas, and is what the marks below are positioned against: it
   shrinks to the canvas exactly, so a percentage across it is a percentage
   across the board. Starts `hidden`; js/diagrams.js takes that off when the
   board has drawn, so scripting off leaves the caption without an empty
   rectangle over it. */
.board-figure {
  position: relative;
  display: inline-block;
  line-height: 0;          /* no descender gap under the canvas */
}

.board-figure canvas {
  max-width: 100%;
  border-radius: 3px;
}

/* Before / After, under the board each names. */
.board-label {
  display: block;
  margin-top: 8px;
  font-size: 13px;
  line-height: 1.4;
  color: var(--dim);
}

/* The two boards either side of one move, and the arrow between them. Wraps on
   a narrow window rather than shrinking the boards: two half-size boards say
   less than two full-size ones stacked. */
.board-pair {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 14px 18px;
}

.board-arrow {
  display: block;
  width: 54px;
  line-height: 0;
}

.board-arrow svg {
  display: block;
  width: 44px;
  height: 24px;
  margin: 0 auto;
}

.board-arrow-text {
  display: block;
  margin-top: 6px;
  font-size: 12px;
  line-height: 1.4;
  color: var(--accent);
}

/* On a phone the pair stacks, and an arrow pointing right between two boards
   that now sit one above the other is pointing at nothing. Turned a quarter
   turn so it goes on meaning "and then". */
@media (max-width: 560px) {
  .board-arrow svg { transform: rotate(90deg); }
}

/* The crosses on the two squares a stuck block would have to be pushed from.
   Positioned in percentages by the markup -- see the note there -- and pulled
   back by half their own size so the percentage lands on the middle of the
   glyph rather than its top left corner.

   Red, and the only red on either page: it is the one mark on these boards
   that is not part of the game. */
.board-mark {
  position: absolute;
  transform: translate(-50%, -50%);
  font-size: 22px;
  line-height: 1;
  font-weight: 700;
  color: #d05a5a;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
  pointer-events: none;
}

.diagram figcaption {
  margin: 14px 0 0;
  font-size: 13px;
  line-height: 1.5;
  text-align: center;
  color: var(--dim);
}

.diagram figcaption strong { color: var(--ink); }

/* The key under the piece board: a name for each thing on it, in the order the
   board puts them. A list rather than labels inside the drawing, because at a
   phone's width those labels would be smaller than the pieces they name. */
.legend-key {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 8px 18px;
  margin: 16px 0 0;
  padding: 0;
  list-style: none;
  font-size: 13.5px;
  line-height: 1.45;
  text-align: left;
  color: var(--dim);
}

.legend-key strong { color: var(--ink); font-weight: 600; }

/* ------------------------------------------------------------- sr-only */

/* In the document, out of the picture.

   The home page's H1 and the sentence defining the game are marked up this
   way: the board is what the page is for, and both attempts at showing that
   text -- above the board and below it -- were too much disruption for what
   they were worth to a reader. See the note in index.html for the trade, and
   for why nothing marked up as an FAQ is ever hidden this way.

   The clip-rect form rather than display:none or visibility:hidden, both of
   which take the text out of the accessibility tree as well as off the screen.
   This leaves it exactly where a screen reader expects a heading to be. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
