*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* HIDDEN MEANS HIDDEN.
 *
 * The browser's own `[hidden] { display: none }` has the specificity of a single
 * attribute selector, so ANY rule that sets `display` on the same element beats
 * it — `#mode-grid { display: grid }` and `.rg-row { display: flex }` both did.
 * The element then keeps its `hidden` attribute, reports `.hidden === true` to
 * every script that asks, and stays on the screen.
 *
 * This was rediscovered six separate times, each fixed with its own
 * `#thing[hidden] { display: none }` and a comment explaining the trap, and then
 * a seventh time when the pieces panel and the custom-clock panel shipped
 * permanently open — a collapsible section that could not collapse. One rule
 * here retires the whole class of bug, so the per-element patches are gone and
 * should not come back.
 *
 * !important is load-bearing, not decoration: without it this loses to exactly
 * the same rules the UA stylesheet loses to. Nothing in this file wants to show
 * an element that is marked hidden. */
[hidden] { display: none !important; }

/* ── THE PAGE IS TWO STACKED BLOCKS, AND IT USED TO BE A FLEX ROW ───────────
 *
 * The two things in the flow are #game-container and #site-footer (#site-corner,
 * the toasts and the modal overlays are all position: fixed and in nobody's
 * way). They stack, and each is centred by `margin: auto` against its own
 * max-width. That is the whole layout.
 *
 * It was a centred flex row with flex-wrap, and the footer was `flex: 0 0 100%`
 * to force itself onto a line of its own. That is a promise flex does not quite
 * keep. A flex item's hypothetical size — the number the line-breaking actually
 * uses — is the basis CLAMPED BY max-width, and both items have one: 1240px
 * here and 1040px on the footer. So on a wide enough screen the two fit side by
 * side, nothing wraps, and the footer sails up beside the board with the content
 * shoved off centre. Reported 2026-09-02 with a screenshot of exactly that on
 * the home screen: "Terms of Service · Privacy" at the top right of an empty
 * page, the menu hard left.
 *
 * Blocks cannot do that to each other, whatever the widths are. The bug is not
 * tuned out, it is unavailable — which is the reason to prefer this to a wider
 * cap or a cleverer basis, both of which leave the same trap set for the next
 * max-width somebody adds.
 *
 * (The old rule carried a comment warning that losing flex-wrap would put the
 * footer alongside the content. It was right about the symptom and wrong about
 * the cause: flex-wrap was there all along.) */
body {
  font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
  background: #1a1a2e;
  color: #e0e0e0;
  min-height: 100vh;
  padding: 1rem;
}

#game-container {
  /* Wide enough for the two boards and the move-history panel side by side.
     On narrower screens #play-area wraps the panel below the boards. */
  max-width: 1240px;
  width: 100%;
  margin: 0 auto;
  /* A flex item establishes a block formatting context, so nothing inside this
     box ever collapsed a margin out through its edges. It is not a flex item any
     more, and #setup-screen's 3rem top margin would otherwise escape and be
     applied to the container instead — the same gap in the same place today, and
     a trap the first child with a bottom margin would fall into. flow-root keeps
     the containment the old layout gave for free.
     The phone game overrides this with display: flex, and may: it is a BFC too. */
  display: flow-root;
}

header {
  text-align: center;
  margin-bottom: 1rem;
}

header h1 {
  font-size: 1.8rem;
  color: #e0e0e0;
  letter-spacing: 0.05em;
  margin-bottom: 0.4rem;
}

#turn-indicator {
  font-size: 1.1rem;
  font-weight: 600;
  padding: 0.3rem 1rem;
  border-radius: 4px;
  display: inline-block;
}

#turn-indicator.white {
  background: #f0f0f0;
  color: #1a1a2e;
}

#turn-indicator.black {
  background: #333;
  color: #f0f0f0;
}

#game-status {
  font-size: 1rem;
  margin-top: 0.3rem;
  min-height: 1.4em;
}

.status-check { color: #ffa726; font-weight: 700; }
.status-checkmate { color: #ef5350; font-weight: 700; }
.status-stalemate { color: #78909c; font-weight: 700; }

/* ---- play area (boards + move history) ---- */

#play-area {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: 1.2rem;
  flex-wrap: wrap;
}

#move-history {
  /* 190px until Alpha 1.1.5, which was already a squeeze: at 0.9rem Courier the
     two move columns get ~62px each, and "g7xh8=Q#" is 69px — the longest
     notations have always wrapped. Naming the captured piece adds a character to
     every officer capture, so the panel is widened to fit the worst case
     (9 characters, ~78px) rather than made to wrap more often. #play-area still
     drops it below the boards when the screen is too narrow for both. */
  width: 230px;
  max-height: 72vh;
  display: flex;
  flex-direction: column;
  background: #20203a;
  border: 1px solid #2f2f4a;
  border-radius: 8px;
  overflow: hidden;
}

.mh-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.4rem 0.5rem 0.4rem 0.8rem;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: #90a4ae;
  border-bottom: 1px solid #2f2f4a;
}

.mh-copy {
  font-family: inherit;
  font-size: 0.7rem;
  text-transform: none;
  letter-spacing: normal;
  padding: 0.18rem 0.55rem;
  border: 1px solid #546e7a;
  border-radius: 4px;
  background: transparent;
  color: #90a4ae;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}

.mh-copy:hover { background: #546e7a; color: #fff; }

.mh-list {
  overflow-y: auto;
  padding: 0.3rem 0.35rem;
  font-family: 'Courier New', monospace;
  font-size: 0.9rem;
}

.mh-row {
  display: grid;
  grid-template-columns: 2.2em 1fr 1fr;
  gap: 0.3rem;
  padding: 0.14rem 0.35rem;
  border-radius: 3px;
}

.mh-row:nth-child(odd) { background: rgba(255, 255, 255, 0.03); }
.mh-num { color: #6b7a8a; }
.mh-move { color: #e0e0e0; }
.mh-empty { color: #6b7a8a; padding: 0.4rem; font-style: italic; }

/* How long that side spent on the move. Dimmed and unitalicised (it is an <em>
   for the meaning, not the slant) so the move itself still reads first — the
   time is there to be found, not to compete with the notation.

   On its own line rather than beside the move, because there is no width for it
   beside the move: the columns are barely wide enough for the notation alone.
   The tightened line-height keeps a row well under two full lines, so a long
   game's scoresheet does not double in height for an annotation. */
.mh-time {
  display: block;
  text-align: right;
  color: #6b7a8a;
  font-style: normal;
  font-size: 0.78em;
  line-height: 1.1;
  white-space: nowrap;
}
.mh-move.mh-current .mh-time { color: rgba(255, 255, 255, 0.75); }

/* Once the game is over the log becomes a way to navigate it: each move jumps
   the board to the position right after it. */
.mh-list.mh-clickable .mh-move[data-ply] {
  cursor: pointer;
  border-radius: 3px;
  padding: 0 0.2rem;
  margin: 0 -0.2rem;
}
.mh-list.mh-clickable .mh-move[data-ply]:hover { background: rgba(186, 104, 200, 0.22); }
.mh-move.mh-current {
  background: #8e24aa;
  color: #fff;
  font-weight: bold;
}

/* ---- replay controls (shown under the boards once a game has finished) ---- */

#review-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.9rem;
  width: 100%;   /* always its own row inside #play-area, never beside the boards */
}

#review-bar.hidden { display: none; }

/* When the layout stacks — a narrow screen, or phone mode on a big one — the
   move log drops below the boards, and the controls would end up below that:
   a whole scoresheet of scrolling away from the board they drive. Reorder so
   they sit directly under the board instead. */
@media (max-width: 700px) {
  #review-bar { order: 1; }
  #move-history { order: 2; }
}
#play-area:has(#boards-container.phone-mode) #review-bar { order: 1; }
#play-area:has(#boards-container.phone-mode) #move-history { order: 2; }

#review-bar button {
  min-width: 2.6rem;
  padding: 0.4rem 0.6rem;
  font-size: 1rem;
  line-height: 1;
  background: #37474f;
  color: #eceff1;
  border: 1px solid #546e7a;
  border-radius: 5px;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
#review-bar button:hover:not(:disabled) { background: #546e7a; color: #fff; }
#review-bar button:disabled { opacity: 0.35; cursor: default; }

#review-bar .rv-play {
  min-width: 6rem;
  font-weight: bold;
  background: #6a1b9a;
  border-color: #8e24aa;
}
#review-bar .rv-play:hover:not(:disabled) { background: #8e24aa; }

.rv-status {
  margin-left: 0.5rem;
  color: #90a4ae;
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
  min-width: 8em;
}

#review-bar .rv-save {
  min-width: 7rem;
  font-size: 0.85rem;
  border-color: #ab47bc;
  color: #f3e5f5;
}
#review-bar .rv-save:hover:not(:disabled) { background: #6a1b9a; }
#review-bar .rv-save.hidden { display: none; }

/* The outcome of a shared game, which its final position often can't show. */
.status-saved { color: #ce93d8; font-weight: 700; }

/* Share link + Copy, side by side in the save dialog. */
.share-row {
  display: flex;
  gap: 0.5rem;
  align-items: stretch;
  margin-bottom: 0.9rem;
}
.share-row .share-input {
  flex: 1 1 auto;
  margin-bottom: 0;
  font-size: 0.85rem;
}
.share-row .share-copy {
  flex: 0 0 auto;
  margin: 0;
  width: auto;
}

/* ---- boards layout ---- */

/* Safety net for --cell-size. Every wrapper sets its own from --board-size;
   this only catches a layout path where one is missing. It matters because the
   glyph rule multiplies it: an undefined value there would either invalidate the
   declaration (glyph falls back to body text) or, with a generous fallback,
   render a piece far too large — which is the failure this whole section exists
   to remove. Modest on purpose. */
#boards-container, #tutorial-screen { --cell-size: 2rem; }

#boards-container {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: 0;
  flex-wrap: wrap;
}

.board-wrapper {
  --board-size: clamp(260px, 40vw, 440px);
  /* The side of one square. Every glyph, peek strip and grid track below
     derives from this rather than recomputing it — see the note on .piece. */
  --cell-size: calc((var(--board-size) - 4px) / 8);
  display: flex;
  flex-direction: column;
  align-items: center;
}

.board-label {
  font-size: 0.85rem;
  color: #90a4ae;
  margin-bottom: 0.3rem;
  letter-spacing: 0.03em;
}

.board {
  display: grid;
  /* minmax(0, …) rather than a bare 1fr: a plain 1fr track refuses to shrink
     below its content, so on a narrow board the piece glyphs held the columns
     open, the grid overflowed its own width, and `overflow: hidden` quietly
     ate the h-file. */
  grid-template-columns: repeat(8, minmax(0, 1fr));
  grid-template-rows: repeat(8, minmax(0, 1fr));
  width: var(--board-size);
  height: var(--board-size);
  border: 2px solid #546e7a;
  border-radius: 4px;
  overflow: hidden;
}

/* ---- Möbius "peek" strips (optional display mode) ---- */

/* A 3-rank silhouette of what lies across each seam: near + mid at full height,
   then a half-height far rank that fades out. Hidden unless the reflection
   toggle (#boards-container.reflections-on) is active. */
/* --peek-rows (0–2.5, set at runtime by the slider) controls how many ranks
   are revealed. The grid always holds 3 full-height ranks, but the strip's
   visible height is peek-rows × cell; align-content anchors the near rank to
   the board edge so the outer ranks clip and fade. */
:root { --peek-rows: 1.5; }

.board-peek {
  /* Match the board's inner grid (its 2px border insets the content each side)
     so peek columns line up with the board columns. */
  --cell: var(--cell-size);
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  grid-template-rows: repeat(3, var(--cell));
  width: calc(var(--board-size) - 4px);
  height: 0;            /* collapsed until reflection is on — grows/fades in */
  opacity: 0;
  overflow: hidden;
  transition: height 0.2s ease, opacity 0.18s ease;
}

.board-peek.peek-top {
  align-content: end;   /* near rank at the bottom (touching the board), clip the top */
  -webkit-mask-image: linear-gradient(to top, #000 52%, transparent 100%);
          mask-image: linear-gradient(to top, #000 52%, transparent 100%);
}
.board-peek.peek-bottom {
  align-content: start; /* near rank at the top (touching the board), clip the bottom */
  -webkit-mask-image: linear-gradient(to bottom, #000 52%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 52%, transparent 100%);
}

#boards-container.reflections-on .board-peek {
  height: calc(var(--peek-rows) * var(--cell));
  opacity: 1;
}

/* Interactive: peeked squares are real squares across the seam — clicking one
   selects/moves the piece there. A cool purple tint marks it as the reflection
   (the "other side"), distinct from the brown board but still fully playable. */
.board-peek .cell { cursor: pointer; overflow: hidden; }
.board-peek .cell.light { background: #cdc2dc; }
.board-peek .cell.dark  { background: #9b8db3; }
.board-peek .cell.peek-near { opacity: 0.62; }
.board-peek .cell.peek-mid  { opacity: 0.4; }
.board-peek .cell.peek-far  { opacity: 0.22; }

/* Mirror the labels so the strip reads like the reverse side of the board. */
.peek-label { display: inline-block; transform: scaleX(-1); }

.refl-piece.white { color: #f5f5f5; }
.refl-piece.black { color: #2a2a2a; }

/* Reflection view keeps the boards full size; the clocks and captured piles
   move into side gutters (White left, Black right) and the container widens so
   nothing has to shrink or wrap. */
#game-container.reflection-layout { max-width: 1320px; }

.side-gutter { display: none; }

#boards-container.reflections-on .side-gutter {
  display: flex;
  flex-direction: column;
  align-items: center;
  align-self: center;
  gap: 0.8rem;
  padding: 0 0.3rem;
  min-width: 150px;
}

.gutter-clock { display: flex; justify-content: center; width: 100%; }
.gutter-clock .clock-box { margin-bottom: 0; }

.captured-side { display: flex; flex-direction: column; align-items: center; gap: 0.25rem; }
.captured-side .captured-label { font-size: 0.72rem; }
.captured-side .captured-pieces {
  font-size: 1.45rem;
  line-height: 1.2;
  max-width: 3.4ch;
  text-align: center;
  word-break: break-all;
}

/* In the split reflection view the gutters replace the bottom info-panel. The
   unified layout keeps the info-panel (reflection-layout is set only for the
   desktop split view — see buildDOM). */
#game-container.reflection-layout #info-panel { display: none; }

/* Reflection mode is ~320px wider than the plain view (a gutter either side), so
   the normal 1240px cap left no room for the move log beside the boards on any
   screen, however large. Give this layout its own cap so a big monitor can
   actually show boards, gutters and log on one row. */
#game-container.reflection-layout { max-width: 1600px; }

/* Keep everything on one row and vertically centered (desktop). The mobile
   layout below stacks into a column instead. */
@media (min-width: 701px) {
  #boards-container.reflections-on { flex-wrap: nowrap; align-items: center; }

  /* Reflection mode adds a ~185px gutter either side, which made the boards
     wider than the play area on their own — so the move log wrapped underneath
     them instead of sitting on the right. The page width is fixed, so the boards
     take what's left after the gutters, the twist column and the log: full size
     wherever there's room (roughly 1590px and up, i.e. no change on a large
     screen), shrinking only as far as it must below that.

     The reserve counts a gutter at its FULL width — the clock in it is now a
     fixed size that already fits the tenths (see .clock-time), so this number
     covers the last ten seconds as well as the rest of the game. Reserving the
     narrower figure is what let the log jump below the boards mid-game. */
  #boards-container.reflections-on .board-wrapper {
    --board-size: clamp(260px, calc((min(100vw - 32px, 1600px) - 662px) / 2), 440px);
  }
}

#flip-btn {
  margin-top: 0.6rem;
  margin-right: 0.5rem;
  padding: 0.35rem 0.6rem;
  font-size: 0.95rem;
  line-height: 1;
  font-family: inherit;
  border: 1px solid #546e7a;
  border-radius: 4px;
  background: transparent;
  cursor: pointer;
  transition: background 0.15s;
}

#flip-btn:hover { background: #546e7a; }

#display-btn {
  margin-top: 0.6rem;
  margin-right: 0.5rem;
  padding: 0.35rem 0.9rem;
  font-size: 0.85rem;
  line-height: 1;
  font-family: inherit;
  border: 1px solid #546e7a;
  border-radius: 4px;
  background: transparent;
  color: #90a4ae;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}

#display-btn:hover { background: #546e7a; color: #fff; }

#display-btn.active {
  border-color: #ab47bc;
  background: rgba(171, 71, 188, 0.2);
  color: #ce93d8;
}

#peek-range {
  vertical-align: middle;
  margin-top: 0.6rem;
  margin-right: 0.5rem;
  width: 110px;
  accent-color: #ab47bc;
  cursor: pointer;
}


/* ---- display tray ----
   The look-and-feel controls used to sit loose in the header, where seven of
   them wrapped into rows above the board. They now fold away behind ⚙ Display;
   the tray is the last child of <header>, so as a block in that centered inline
   row it lands on its own line directly beneath the button. */

#customize-btn {
  margin-top: 0.6rem;
  margin-right: 0.5rem;
  padding: 0.35rem 0.9rem;
  font-size: 0.85rem;
  line-height: 1;
  font-family: inherit;
  border: 1px solid #546e7a;
  border-radius: 4px;
  background: transparent;
  color: #90a4ae;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}

#customize-btn:hover { background: #546e7a; color: #fff; }

#customize-btn.active {
  border-color: #ab47bc;
  background: rgba(171, 71, 188, 0.2);
  color: #ce93d8;
}

#customize-btn .caret {
  display: inline-block;
  font-size: 0.7em;
  transition: transform 0.15s;
}

#customize-btn[aria-expanded="true"] .caret { transform: rotate(180deg); }

#display-panel {
  /* Block, so it breaks the header's inline row and starts a line of its own;
     fit-content + auto margins keep it wrapped around its controls and centred
     rather than spanning the page. */
  display: block;
  width: fit-content;
  margin: 0.7rem auto 0;
  padding: 0.35rem 0.9rem 0.7rem;
  border: 1px solid #3a3a5a;
  border-radius: 8px;
  background: rgba(171, 71, 188, 0.06);
  max-width: 100%;
}


/* Inside the tray the controls keep their own spacing rules; they just don't
   need the top margin that separated them from the title row out in the open. */
#display-panel #display-btn,
#display-panel #unify-btn,
#display-panel #peek-range,
#display-panel .autodecline-opt { margin-top: 0.35rem; }

/* ---- twist connector ---- */

#twist-connector {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 0 0.6rem;
  min-width: 60px;
}

.twist-label {
  font-size: 0.8rem;
  color: #ab47bc;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  margin-bottom: 0.4rem;
}

.twist-arrows {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.twist-map {
  font-size: 0.7rem;
  color: #ce93d8;
  text-align: center;
  font-family: monospace;
}

/* ---- cells ---- */

.cell {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  transition: background 0.12s;
}

.cell.light { background: #e8d5b5; }
.cell.dark  { background: #b58863; }

.cell.selected        { background: #f6d94e !important; }
.cell.last-move-from  { background: #c8e6a0 !important; }
.cell.last-move-to    { background: #aad576 !important; }
.cell.in-check        { background: #ef5350 !important; }

.cell.legal-move::after {
  content: '';
  position: absolute;
  width: 28%;
  height: 28%;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.25);
  pointer-events: none;
}

/* The three inset shadows a cell can carry live here as tokens because they have
   to be *combined* in places — box-shadow does not stack across rules, so a cell
   that earns two of them needs one rule listing both. Naming them once keeps
   those combinations from drifting out of step with the originals. */
:root {
  --capture-ring: inset 0 0 0 4px rgba(239, 83, 80, 0.7);
  --preview-ring: inset 0 0 0 4px rgba(200, 95, 120, 0.8);
  --seam-marker:  inset 0 2px 0 0 rgba(206, 147, 216, 0.9);
}

.cell.legal-capture {
  box-shadow: var(--capture-ring);
}

/* Move preview (look-only): a piece that isn't yours to move right now. Blue,
   and marked with an X rather than the solid dot/ring of a playable move, so it
   reads unmistakably as "you cannot move here yet". */
.cell.preview-from { background: #d98a99 !important; }

.cell.preview-move::after {
  content: '';
  position: absolute;
  width: 38%;
  height: 38%;
  pointer-events: none;
  /* Thicker bands with a soft feathered ramp on each edge, plus a gentle blur,
     so the X reads as a soft marker rather than a crisp glyph. */
  background:
    linear-gradient(45deg,  transparent 34%, rgba(214, 48, 49, 0.75) 44%, rgba(214, 48, 49, 0.75) 56%, transparent 66%),
    linear-gradient(-45deg, transparent 34%, rgba(214, 48, 49, 0.75) 44%, rgba(214, 48, 49, 0.75) 56%, transparent 66%);
  filter: blur(0.6px);
}

/* On a capturable piece, ring the square too so the target piece stays readable. */
.cell.preview-capture {
  box-shadow: var(--preview-ring);
}

.cell:hover {
  filter: brightness(1.08);
}

/* The same square, drawn somewhere else — its reflection across a seam, or the
   row a peek strip repeats. js/ui.js linkHover adds this to every cell carrying
   the hovered file+rank, so the pair lights together and reads as one field
   rather than two that resemble each other. Deliberately the SAME effect as
   :hover above: a reflection that announced itself differently would be a third
   thing to learn, and the point is that there is nothing to learn — it is the
   same square. Keep the two declarations in step. */
.cell.hover-link {
  filter: brightness(1.08);
}

/* "Hover circle" in the Display tray, off by default. A ring on the square under
   the pointer AND on every other drawing of it, so the pair is marked rather
   than merely brightened — which is what the reflection strips need, since a
   peek-far row sits at 0.22 opacity and a 1.08 brightening there is close to
   invisible.

   ::before, because ::after is the legal-move dot and the preview X. A square
   that is both hovered and a legal target therefore carries both: ring at the
   edge, marker in the middle.

   Scoped to #boards-container, and that scope is the whole of "not in the
   tutorial" — the tutorial's boards have no such ancestor, and `.cell.tut-path`
   already owns ::before over there. Nothing else is needed to keep them apart.

   Deliberate, and the rule generalises: a display setting that is OFF by default
   does not follow a reader onto /how. The tutorial teaches the board a new
   player is about to be handed, and every optional flourish it picks up is one
   more thing on a page whose whole job is to have few of them.

   Purple, and a brighter one than the seam marker's #ce93d8. Same family — the
   ring is about the seam, so it belongs in the seam's colour — but the marker
   is a 2px bar sitting flush on a board square, while this is a hairline circle
   that also has to survive a peek row at 0.22 opacity. It is lifted and fully
   opaque to make up the difference.

   The dark outer shadow is not decoration, and purple needs it more than white
   did: the ring has to read on four grounds at once, and two of them are the
   peek strips' own light purples (#cdc2dc, #9b8db3), which sit close enough to
   it in hue that the edge would otherwise go soft exactly where the ring
   matters most. */
#boards-container.hover-ring .cell.hover-link::before {
  content: '';
  position: absolute;
  inset: 11%;
  border-radius: 50%;
  border: 2px solid #e2a4ff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
  pointer-events: none;
}

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

/* Belt and braces with the U+FE0E in js/pieces.js: every surface that draws a
   piece glyph asks for the text presentation explicitly, so the black pawn
   (the one chess codepoint that is also an emoji) cannot be hijacked by the
   colour emoji font and rendered as an oversized square. Ignored by browsers
   that don't know the property — the variation selector still covers those. */
.piece,
.refl-piece,
.captured-pieces,
.promo-btn,
.mode-preview,
.bg-glyph {
  font-variant-emoji: text;
}

/* ONE rule for every piece glyph, on a board or in a reflection.
 *
 * A glyph is sized from its own square and nothing else. No viewport units: the
 * square already tracks the viewport through --board-size, so bringing vw in
 * again only lets the two disagree.
 *
 * This was two rules that drifted apart, and both halves of the drift were
 * visible bugs. .piece was given a "never bigger than its square" cap; the
 * identical .refl-piece kept a bare clamp(1.6rem, 3.5vw, 3rem), and the 1.6rem
 * FLOOR is the sting — a reflection glyph would not shrink below 25.6px however
 * small the square became. On a phone the unified board sizes itself from the
 * viewport height and reflection mode adds three more rows, which pushes squares under ~29px:
 * the board glyphs duly shrank and the reflection glyphs stayed put and burst
 * out of their cells. The other direction was a tutorial override capping
 * reflections at 1.9rem against the board's 3rem, so they looked shrunken.
 *
 * The two must therefore stay in ONE declaration. Splitting them again to give
 * reflections their own size is how this comes back; tests/header.test.mjs
 * fails if .refl-piece ever gets a font-size of its own.
 *
 * 0.88 is the same ratio the capped rule settled on, and at the default desktop
 * board it was already the binding constraint — so the normal view is unchanged
 * to the pixel. */
.piece,
.refl-piece {
  font-size: calc(var(--cell-size, 2rem) * 0.88);
  line-height: 1;
  pointer-events: none;
}

.piece {
  text-shadow: 0 1px 3px rgba(0,0,0,0.3);
  z-index: 1;
}

.piece.white { color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,0.5); }
.piece.black { color: #222; text-shadow: 0 1px 2px rgba(255,255,255,0.2); }

/* Reverse-side orientation ("Reverse pieces" in the header). The negative ranks
   are the far side of the strip, reached by crossing a seam — which turns
   everything through 180°. `data-rank` starts with '-' on exactly those squares
   in every layout, so one selector covers board 2, the phone extension rows and
   the reflection peeks alike. The glyphs are inline spans, and transforms don't
   apply to inline boxes, hence inline-block. */
/* Board squares. `.piece` is only ever used on a real board cell (renderBoard);
   the reflection strips use `.refl-piece`, which is what lets the two disagree
   below. */
#boards-container.flip-all .cell[data-rank^="-"] .piece,
#boards-container.flip-pawns .cell[data-rank^="-"] .piece.pc-pawn,

/* Reflection squares, "Reflections show the far side" OFF — the old behaviour,
   where a strip repeats the board's orientation. */
#boards-container.flip-all:not(.reflect-invert) .cell[data-rank^="-"] .refl-piece,
#boards-container.flip-pawns:not(.reflect-invert) .cell[data-rank^="-"] .refl-piece.pc-pawn,

/* Reflection squares, the checkbox ON (the default). A strip shows the far side
   of the seam, so it inverts: the rank test is negated, and it is the POSITIVE
   ranks that get turned over here. Net effect — a pawn is drawn upside down in
   exactly one of the two places you can see it, never both and never neither. */
#boards-container.flip-all.reflect-invert .cell:not([data-rank^="-"]) .refl-piece,
#boards-container.flip-pawns.reflect-invert .cell:not([data-rank^="-"]) .refl-piece.pc-pawn {
  display: inline-block;
  transform: rotate(180deg);
}

/* ---- labels ---- */

.rank-label {
  position: absolute;
  top: 2px;
  left: 3px;
  font-size: clamp(0.5rem, 1vw, 0.7rem);
  color: rgba(0, 0, 0, 0.45);
  pointer-events: none;
  font-weight: 600;
  z-index: 2;
}

.file-label {
  position: absolute;
  bottom: 2px;
  right: 3px;
  font-size: clamp(0.5rem, 1vw, 0.7rem);
  color: rgba(0, 0, 0, 0.45);
  pointer-events: none;
  font-weight: 600;
  z-index: 2;
}

/* ---- info panel ---- */

#info-panel {
  display: flex;
  justify-content: center;
  gap: 2rem;
  margin-top: 1rem;
  flex-wrap: wrap;
}

.captured-group {
  text-align: center;
}

.captured-label {
  font-size: 0.8rem;
  color: #90a4ae;
  display: block;
  margin-bottom: 0.2rem;
}

.captured-pieces {
  font-size: 1.3rem;
  min-height: 1.5em;
  letter-spacing: 0.1em;
}

/* ---- modals ---- */

.modal {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.7);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
}

.modal.hidden { display: none; }

.modal-content {
  position: relative;
  background: #263238;
  padding: 2rem;
  border-radius: 8px;
  text-align: center;
  min-width: 200px;
}

.modal-close {
  position: absolute;
  top: 0.4rem;
  right: 0.6rem;
  border: none;
  background: transparent;
  color: #90a4ae;
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
  padding: 0.1rem 0.35rem;
}

.modal-close:hover { color: #fff; }

.modal-actions {
  display: flex;
  gap: 0.8rem;
  justify-content: center;
  flex-wrap: wrap;
}

.modal-btn-secondary {
  padding: 0.6rem 1.4rem;
  font-size: 1.1rem;
  font-family: inherit;
  border: 1px solid #546e7a;
  border-radius: 4px;
  cursor: pointer;
  background: transparent;
  color: #cfd8dc;
  transition: background 0.15s, color 0.15s;
}

.modal-btn-secondary:hover { background: #546e7a; color: #fff; }

/* Resign confirm — destructive, so it reads as a warning. */
#resign-confirm {
  padding: 0.6rem 1.4rem;
  font-size: 1.1rem;
  font-family: inherit;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  background: #b3413a;
  color: #fff;
  transition: background 0.15s;
}

#resign-confirm:hover { background: #c85a52; }

.modal-content p {
  margin-bottom: 1rem;
  font-size: 1.1rem;
}

.promo-btn, #new-game-btn, #request-accept, #resume-btn {
  padding: 0.6rem 1.4rem;
  font-size: 1.1rem;
  font-family: inherit;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  background: #546e7a;
  color: #fff;
  transition: background 0.15s;
}

.promo-btn:hover, #new-game-btn:hover, #request-accept:hover, #resume-btn:hover {
  background: #78909c;
}

/* ---- unified single board (phone layout, or the Single-board toggle) ---- */

.unified-wrapper {
  --board-size: min(94vw, 360px);
  --cell-size: calc((var(--board-size) - 4px) / 8);
}

/* Explicit square cells (8 wide × 16 tall) — deterministic, no aspect-ratio or
   1fr rounding that could clip the last column/row. */
.board-unified {
  --ucell: var(--cell-size);
  grid-template-columns: repeat(8, var(--ucell));
  grid-template-rows: repeat(16, var(--ucell));
  width: var(--board-size);
  height: auto;
}

/* Mark the two internal twist seams so the mirrored rows are legible. */
.board-unified .cell.seam-edge { box-shadow: var(--seam-marker); }

/* …but the capture rings are box-shadows too, and box-shadow does not stack
   across rules: the seam marker out-specified them and REPLACED them, so a
   capturable piece on rank 8 or −8 got no red ring — and no dot either, since a
   capture square never earns .legal-move. It looked unmarked while staying
   perfectly playable. The seam rows are exactly where a capture is most
   surprising, so restate the pair together. Seam line first: it paints on top,
   keeping the twist boundary readable through the ring. */
.board-unified .cell.seam-edge.legal-capture  { box-shadow: var(--seam-marker), var(--capture-ring); }
.board-unified .cell.seam-edge.preview-capture { box-shadow: var(--seam-marker), var(--preview-ring); }

/* Extension = the wrapped-around (negative-rank) rows. "Mirrored extension"
   (on by default) flips their labels like reflection mode's peek labels;
   "Purple extension" (off by default) tints those rows the same purple as the
   reflection peeks. The selectors out-specify .cell.light / .cell.dark. */
.board-unified.mirror-ext .ext-label { transform: scaleX(-1); }
.board-unified.purple-ext .cell.ext-cell.light { background: #cdc2dc; }
.board-unified.purple-ext .cell.ext-cell.dark  { background: #9b8db3; }

/* Rainbow mode: give each wrapped-around (negative-rank) row its own hue so the
   eight "other side" ranks are easy to tell apart. Works for both the reflection
   peeks and the phone extension rows — both carry data-rank + .light/.dark. The
   light/dark pair per rank keeps the checkerboard readable; the #boards-container
   id keeps these ahead of the base shade and purple-ext rules. Red → violet. */
#boards-container.rainbow-on .cell[data-rank="-1"].light { background: hsl(2 70% 80%); }
#boards-container.rainbow-on .cell[data-rank="-1"].dark  { background: hsl(2 58% 67%); }
#boards-container.rainbow-on .cell[data-rank="-2"].light { background: hsl(28 74% 78%); }
#boards-container.rainbow-on .cell[data-rank="-2"].dark  { background: hsl(28 62% 64%); }
#boards-container.rainbow-on .cell[data-rank="-3"].light { background: hsl(48 72% 75%); }
#boards-container.rainbow-on .cell[data-rank="-3"].dark  { background: hsl(46 60% 61%); }
#boards-container.rainbow-on .cell[data-rank="-4"].light { background: hsl(135 42% 76%); }
#boards-container.rainbow-on .cell[data-rank="-4"].dark  { background: hsl(138 36% 59%); }
#boards-container.rainbow-on .cell[data-rank="-5"].light { background: hsl(188 50% 76%); }
#boards-container.rainbow-on .cell[data-rank="-5"].dark  { background: hsl(190 44% 59%); }
#boards-container.rainbow-on .cell[data-rank="-6"].light { background: hsl(214 62% 81%); }
#boards-container.rainbow-on .cell[data-rank="-6"].dark  { background: hsl(214 50% 66%); }
#boards-container.rainbow-on .cell[data-rank="-7"].light { background: hsl(258 50% 82%); }
#boards-container.rainbow-on .cell[data-rank="-7"].dark  { background: hsl(258 42% 70%); }
#boards-container.rainbow-on .cell[data-rank="-8"].light { background: hsl(300 46% 82%); }
#boards-container.rainbow-on .cell[data-rank="-8"].dark  { background: hsl(300 38% 70%); }

/* Peeks on the unified board must size their columns exactly like the board
   (explicit cell width, not 1fr) or fractional-pixel rounding drifts them out of
   line with the board columns on the small phone cells. */
.board-peek.unified-peek {
  grid-template-columns: repeat(8, var(--cell-size));
}

/* The unified layout stacks vertically (clock, board, clock) — that's the phone
   arrangement, and the fallback everywhere too narrow for the flanking rule below. */
#boards-container.phone-mode {
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
}

.phone-mode .clock-box { margin: 0; min-width: 130px; }

/* Unified board on a screen with room beside it (i.e. the Single-board toggle on
   a desktop, not an actual phone). The 8×16 board is ~715px tall, so bracketing
   it above and below pushes the lower clock off the bottom of a laptop screen —
   you simply never see your own clock. Here the clocks move into the empty side
   margins instead and stick to the top of the viewport while you scroll.

   They live in their own grid columns, so "floating" can never put a clock over
   the board: column 2 is the board's and nothing else is ever placed in it. */
@media (min-width: 760px) {
  #boards-container.phone-mode {
    display: grid;
    grid-template-columns: 1fr auto 1fr; /* equal margins keep the board centred */
    align-items: start;
    column-gap: 1rem;
  }
  .phone-mode > .clock-top,
  .phone-mode > .clock-bottom {
    grid-row: 1;
    position: sticky;
    top: 0.75rem;
    margin: 0;
  }
  .phone-mode > .clock-top       { grid-column: 1; justify-self: end; }
  .phone-mode > .unified-wrapper { grid-column: 2; grid-row: 1; }
  .phone-mode > .clock-bottom    { grid-column: 3; justify-self: start; }
}

#unify-btn {
  margin-top: 0.6rem;
  margin-right: 0.5rem;
  padding: 0.35rem 0.9rem;
  font-size: 0.85rem;
  line-height: 1;
  font-family: inherit;
  border: 1px solid #546e7a;
  border-radius: 4px;
  background: transparent;
  color: #90a4ae;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}

#unify-btn:hover { background: #546e7a; color: #fff; }

#unify-btn.active {
  border-color: #ab47bc;
  background: rgba(171, 71, 188, 0.2);
  color: #ce93d8;
}

/* ---- responsive ---- */

@media (max-width: 700px) {
  #boards-container {
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
  }

  .board-wrapper {
    --board-size: clamp(280px, 85vw, 440px);
  }

  /* ── THE HEIGHT BUDGET, AND WHY IT IS A VARIABLE ──────────────────────────
   *
   * --fit-vh is the height the board is allowed to fill. Two values, and which
   * one is in force is the whole of this note:
   *
   * UNLOCKED it is 100svh — the viewport with the browser's bars SHOWING, the
   * smallest the screen ever gets. The page scrolls in this state, so the bars
   * come and go under your thumb; a board sized to anything larger would grow
   * and shrink the whole way down the page, which is the dvh trade that was
   * made once and undone (tests/header.test.mjs pins it).
   *
   * LOCKED it is a MEASURED number of pixels, written by js/ui.js
   * (_measureFitViewport) from window.innerHeight. This is not dvh coming back
   * in through the side door: locked, the page does not scroll at all, so the
   * bars cannot move and the number cannot move with them. It is read once per
   * build and once per resize.
   *
   * The difference is a whole band of screen. svh assumes the bars are showing;
   * arrive at the board with them already collapsed — which is what a phone
   * does after any scrolling on the way in — and they will now stay collapsed,
   * because the lock is on. The board was then sized for a screen about a URL
   * bar shorter than the one it was actually on, and that gap showed up as
   * exactly what issue #79 reported second: a band of dead page under the
   * board, and a board too small for the screen holding it.
   *
   * The fallback chain is svh's, so a browser that has neither unit still gets
   * plain vh and the property is never undefined — `min-height: var(--fit-vh)`
   * below has no fallback of its own on purpose, because a var() fallback of
   * `100svh` would be invalid-at-computed-value-time on those same browsers and
   * take the whole declaration with it, which is worse than the unit it patches.
   *
   * On #boards-container rather than on #game-container, because that is the
   * element whose own min-height reads it and the element JS writes it to. */
  #boards-container {
    --fit-vh: 100vh;
    --fit-vh: 100svh;
  }

  /* THE BOARD IS THE SCREEN (issue #79).
   *
   * A 16-rank board is exactly 1:2 and a phone is about 1:2.16, so the board can
   * be the full width of the screen and still stand shorter than it — 390 × 780
   * in a 390 × 844 viewport. That is what this rule asks for, and nothing is
   * subtracted from it, because on a phone nothing else is on the screen: the
   * header, the captured piles and the move log are all below the fold, and the
   * two clocks are fixed over the board's own ends rather than bracketing it.
   *
   * What stood here subtracted a measured header — 150–250px of wrapped buttons
   * — and capped the width at 85vw on top. Together they took about a third of
   * the board, which is the complaint this replaces: the clocks had become
   * visible by making the board too small to play on.
   *
   * The height it is allowed is --fit-vh, and which viewport that stands for —
   * svh unlocked, the measured screen locked — is the note on its own
   * definition above. Not lvh, and not dvh; both were tried and both are argued
   * there and in tests/header.test.mjs.
   *
   * No 85vw cap and no 240px floor. The cap kept a short wide screen from
   * running off the sides, which is now the min() against 100vw; the floor let a
   * very short screen scroll rather than shrink, and a board that overflows
   * because of a floor is this bug wearing a hat.
   *
   * With reflection on, the strips add peek-rows at each end — 19 rows at the
   * default 1.5 — and the board shrinks to keep the whole strip on the screen.
   * That is the trade the toggle is for, and it is the player's to make.
   *
   * --clock-room is the ONE thing subtracted here, it is 0 unless the player has
   * ticked "Clocks off the board", and it is measured (js/ui.js
   * _measureClockRoom) rather than guessed — a seat chip wraps to a second line
   * on a narrow screen, which is more than a rank's difference. Everything the
   * old rule subtracted was chrome the board should never have been paying for;
   * this is a row the player has asked to spend. */

  /* The -4px/+4px pair is the board's own 2px border, top and bottom. Without
     it the cell comes out a third of a pixel too big per rank and the strip
     stands about five pixels proud of its budget — a sixth of a rank, invisible
     until it is the sixth that overflows. Taking the border off before the
     divide and putting it back after makes the wrapper exactly --fit-vh tall —
     at a board size of 1, which is where the next paragraph starts. */

  /* --board-zoom is the one thing here allowed to overflow the screen, and it
     multiplies the FINISHED size rather than joining the min(): the slider
     promises a board a fifth bigger, so it has to mean that whichever of the two
     terms was binding. On a phone in portrait it is always the height one — 8
     columns against 19 rows leaves a third of the width unused — so what the
     extra size costs is ranks off the top, and js/ui.js (_anchorNearEnd) is what
     decides it comes off the top rather than off your own end. */
  #boards-container.phone-mode .unified-wrapper {
    --unified-rows: 16;
    --board-size: calc(min(100vw, calc((var(--fit-vh) - var(--clock-room, 0px) - 4px) / var(--unified-rows) * 8 + 4px)) * var(--board-zoom, 1));
  }
  #boards-container.phone-mode.reflections-on .unified-wrapper {
    --unified-rows: calc(16 + 2 * var(--peek-rows, 0));
  }

}

/* THE TWO SIZE CONTROLS ARE HIDDEN WHERE THEY WOULD DO NOTHING.
 *
 * Both are answered entirely inside the phone breakpoint — --board-zoom by the
 * rule above, and "Clocks off the board" by the .clocks-clear block further down
 * (with _measureClockRoom, which bails unless phoneGame). On a wide screen
 * neither moves anything at all.
 *
 * They were still in the tray there, because js/ui.js hides them on the LAYOUT
 * (`this.phone`) and a desktop can ask for the phone layout with the Single-board
 * toggle. A control that does nothing reads as broken rather than as not
 * applicable — and these two are the only ones with the problem: the extension
 * toggles beside them drive rules that are not in any media query, so they work
 * in the unified layout at every width and stay where they are.
 *
 * The same breakpoint, so a control exists exactly where the rules it drives do.
 * In CSS rather than in the header, for the same reason the layout is: a header
 * is rebuilt on a resize only when the breakpoint crossing changes the layout,
 * so a pinned Single board dragged wider would keep a stale answer, while a media
 * query has none to keep. And it only ever hides — the `hidden` attribute the
 * tray puts on both in the split layout is not something this can undo. */
@media (min-width: 701px) {
  #clocks-off-opt,
  #board-zoom-opt { display: none; }
}

/* ══ THE PHONE GAME: A BOARD, TWO CLOCKS, AND NOTHING ELSE ═══════════════════
 *
 * Issue #79 began as "the clock is not visible" and the first answer made room
 * for the clocks by shrinking the board, which is the wrong trade twice: the
 * board is the game, and a board sized around a header full of buttons is small
 * for a reason no player can see.
 *
 * So the screen holds the board at its full size and the two clocks lying over
 * its ends. Everything else — the header and its controls, the captured piles,
 * the replay bar, the move log — is BELOW THE FOLD, one scroll away and no
 * longer costing the board a rank. Reading order down the page: board, controls,
 * captured, replay, log.
 *
 * `body.phone-game` and `#game-container.phone-game` are set together from
 * js/ui.js (buildDOM), and both are taken off when the board is left, so the
 * screen that replaces it gets its padding and its ordinary flow back. */

/* EVERY RULE HERE IS INSIDE THE PHONE MEDIA QUERY, and that is not tidiness.
 * .phone-game rides on the unrolled board, which a desktop can also ask for with
 * the Single-board toggle — so an unguarded `body.phone-game { padding: 0 }`
 * would strip a 1400px screen of its margins to solve a 390px screen's problem.
 * The board is only ever too big for the screen it is on. */
@media (max-width: 700px) {
  /* The page's 1rem of padding is 2rem of board width, which on a 390px screen
     is most of a file. The phone game is the one screen that spends it on the
     game instead; the footer and the corner badge are positioned, not padded, so
     they are unaffected. */
  body.phone-game { padding: 0; }

  /* A flex column so `order` below can put the board above the header. No
     padding at the foot: both clocks are in one bar at the top now, so nothing
     is pinned over the end of the page for the last control to hide under. */
  #game-container.phone-game {
    display: flex;
    flex-direction: column;
  }

  /* The board is the FIRST thing on the page, and the header — which is still
     the header, with every game control it ever had — comes after it.
   *
   * display: contents is what makes that a two-line rule instead of a rebuild:
   * #play-area stops generating a box, so the board, the replay bar and the move
   * log become flex children of #game-container alongside the header, and
   * `order` can interleave them. The negative orders are deliberate — the review
   * bar (1) and the move log (2) already carry orders from the stacking rules
   * above, and lifting two elements above the default 0 leaves those, and the
   * captured piles at 0, exactly where they were relative to each other. */
  #game-container.phone-game > #play-area { display: contents; }

  /* THE BOARD SITS ON THE BOTTOM EDGE, and the slack goes to the top.
   *
   * The board has a fixed shape — 8 squares by 16, or by 19 with the reflection
   * strips — so it can be as wide as the screen or as tall as it, never both
   * unless the two happen to agree. Whichever runs out first sets the size, and
   * on a tall phone that is the WIDTH: the board is a full 100vw across and
   * still stands short of the bottom, leaving a band of nothing under it.
   *
   * That band is real and it has to go somewhere. It goes to the TOP, because
   * the top is where the clock bar already is: give the slack to the top and the
   * bar stops lying on the board's first rank and floats in empty space instead.
   * The same pixels buy a board that reaches the near edge — the end you are
   * looking at, where your own pieces are — and a clock that covers nothing.
   *
   * min-height rather than height: when the board is the taller of the two it
   * already fills the screen and this changes nothing at all.
   *
   * AND ON A PHONE THAT IS THE USUAL CASE NOW. With reflection on — the default
   * — the strip is 19 rows to the screen's 8 columns, so height runs out long
   * before width does: the board reaches both ends and the band above is
   * nothing at all. The paragraph above is what happens on the other shape of
   * screen, a short wide one, and it is still what should happen there. */
  #game-container.phone-game #boards-container {
    order: -2;
    min-height: var(--fit-vh);
    justify-content: flex-end;
  }
  #game-container.phone-game > #header { order: -1; margin-bottom: 0.6rem; }

  /* THE TITLE AND THE TURN LINE COME OFF THE PHONE GAME. Both are below the
     fold, so this buys the board no height at all — it is the header not
     repeating what the top of the screen already says. The active clock is lit
     for whoever is to move and carries their name, which is the same fact in
     the place you are already looking; and one board on a screen with nothing
     else on it does not need to be told which site it is on.
     Hidden here rather than dropped from _headerHTML: the sandbox already skips
     both by another condition, and a second one on the same two lines is one
     more thing that can come to disagree with itself. renderTurnIndicator keeps
     writing to a hidden node, which costs nothing and keeps the split layout's
     copy of it honest. */
  body.phone-game #header h1,
  body.phone-game #header #turn-indicator { display: none; }
}

/* ── The lock ────────────────────────────────────────────────
 *
 * It locks SCROLLING — nothing else. The board is at its full size whether this
 * is on or off; what the lock decides is whether the page under it can be
 * scrolled to. Set from js/ui.js (_armFitEscape) and taken off when the board is
 * left, so the next screen scrolls normally.
 *
 * (It said "one line" here until 2026-09-02, and the one line was the bug — see
 * the block inside the rule. A desktop locks on overflow alone; a phone needs to
 * be told that a touch is not a pan.)
 *
 * On the body rather than on the container, because the body is the element that
 * scrolls and no selector reaches it from a class on one of its descendants.
 *
 * With nothing to scroll to, a swipe is free to mean "let me out", which is what
 * makes a locked default defensible: two of them unlock. */
body.fit-locked {
  overflow: hidden;
  /* ── AND overflow: hidden IS NOT A LOCK ON A PHONE ─────────────────────────
   *
   * That is the whole of this rule on a desktop and it was the whole of it here,
   * which is why the lock did not hold: reported 2026-09-02 as "I can still
   * scroll while locked in", with the hint toast appearing and the page moving
   * under it anyway.
   *
   * The spec propagates the body's overflow to the viewport, so one line ought
   * to do it — and it does, for a wheel and for the keyboard. WebKit does not
   * apply it to TOUCH PANNING, which on a phone is the only kind of scrolling
   * there is. Nothing else was wrong: the class was on, the listeners were
   * armed, the toast fired. The page simply scrolled.
   *
   * touch-action is the declaration that means it. It says a touch here is not
   * for panning — which is exactly the claim the lock makes, and the same claim
   * the escape gesture depends on, because a swipe can only mean "let me out"
   * on a page where it does not already mean "scroll". The escape still sees it:
   * touch-action governs the browser's own response to a gesture, not whether
   * touchmove is delivered, so the two-swipe unlock is untouched.
   *
   * TWO DECLARATIONS, AND THE SECOND IS NOT A TYPO. `none` blocks the pan and
   * the pinch both, and it is the value every browser that has touch-action
   * understands. `pinch-zoom` blocks only the pan, so a player who needs a
   * closer look at the board still gets one — but a browser that does not know
   * the value drops that line and keeps the `none` above it. The order is the
   * fallback: the weaker, better-supported value first.
   *
   * overscroll-behavior is the third thing a phone does that a desktop does not:
   * the rubber-band at the end of a page, which is not a scroll but still moves
   * the board under your thumb. */
  touch-action: none;
  touch-action: pinch-zoom;
  overscroll-behavior: none;
}

/* The lock button. Sized and coloured like the flip button beside it — it is the
   same kind of thing, a control over how the board is shown rather than over the
   game — and it says which state it is in with the same .active the display
   toggles use. */
#fit-btn {
  font-size: 1rem;
  line-height: 1;
  padding: 0.3rem 0.5rem;
  border: 1px solid transparent;
  border-radius: 6px;
  background: transparent;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
#fit-btn:hover { border-color: #546e7a; }
#fit-btn.active { border-color: #ab47bc; background: rgba(171, 71, 188, 0.18); }

/* ── BOTH CLOCKS, IN ONE BAR, AT THE TOP ─────────────────────────────────────
 *
 * Fixed to the top of the VIEWPORT, not to the board, and side by side rather
 * than one at each end. Two things follow from that, and the second is the
 * reason for it:
 *
 * Out of the flow, they cost the board no height at all — which is the whole of
 * the original bug. Bracketing a board a screen and a half long put at least one
 * clock off the edge at every scroll position you would actually play from, and
 * sizing the board around them instead made it a third too small. Fixed, they do
 * neither, and they stay put while you scroll down to the controls and the move
 * log, so both clocks are on screen for the whole game.
 *
 * And in ONE bar they cover one end of the board instead of two. That is a rank
 * given back at the near end, where your own pieces are and where you are
 * looking, in exchange for reading both times in one glance at the far end.
 *
 * WHICH IS WHOSE: YOUR clock is on the left, your opponent's on the right.
 *
 * The board is already oriented so that your own home rank is at the BOTTOM —
 * that is what this.flipped settles, from your seat — so .clock-bottom is your
 * clock and .clock-top is theirs, whatever colour either of you has. Laying the
 * pair out bottom-then-top turns the board's near/far into the bar's left/right,
 * and it needs no idea of who is who to do it.
 *
 * Two players on one screen fall out of the same rule rather than needing their
 * own: nobody is seated, the board is unflipped, so White is at the bottom and
 * lands on the left with Black on the right. Flip the board and they swap, which
 * is right — the left clock is always the near side's.
 *
 * The chips mirror around the pair, each name on its own outer edge, which is
 * the reverse of the order the flowed clocks use (.clock-bottom's chip sits
 * below its time when the clock is under the board, and to the LEFT of it when
 * the clock is the left half of a bar).
 *
 * Half-transparent, over the board's top end. With reflection on that end is a
 * peek strip, which is the right thing to cover: a strip is a hint about what
 * lies across the seam, and there is a whole board underneath it that is not.
 *
 * pointer-events: none, so a clock lying over the board cannot swallow a tap
 * meant for the square beneath it. Nothing in a clock is clickable during a
 * game — the seat name only becomes a link once the game is over.
 *
 * env(safe-area-inset-*) keeps them clear of a notch and a home indicator; the
 * fallback is 0, which is every phone that does not have one. */
@media (max-width: 700px) {
  body.phone-game .phone-mode:not(.clocks-clear) > .clock-top,
  body.phone-game .phone-mode:not(.clocks-clear) > .clock-bottom {
    position: fixed;
    /* top: 0, with the notch clearance moved into PADDING below.
       A fixed element with no top falls back to its own static position, and for
       the lower clock that is under the board — which is precisely the layout
       this replaces. So the one declaration that does the pinning must not be
       one a browser can drop, and env() inside a top takes the whole
       declaration with it wherever it is not understood. The failure is silent
       and looks exactly like the rule was never written. */
    top: 0;
    z-index: 6;
    margin: 0;
    padding: 0.1rem 0.7rem;
    gap: 0.45rem;
    min-width: 0;
    background: rgba(32, 32, 58, 0.76);
    border-color: rgba(120, 144, 156, 0.35);
    backdrop-filter: blur(3px);
    pointer-events: none;
  }
  /* Butted against the middle from either side, so the pair is centred however
     wide each one gets — no wrapper element, and no arithmetic that a longer
     name or a switch to tenths could falsify. */
  body.phone-game .phone-mode:not(.clocks-clear) > .clock-bottom { right: 50%; margin-right: 0.15rem; }
  body.phone-game .phone-mode:not(.clocks-clear) > .clock-top { left: 50%; margin-left: 0.15rem; }

  /* ── NAMES OUTWARD, TIMES INWARD — AND THIS PAIR GOVERNS BOTH BARS ────────
   *
   * The left clock's chip on its left, the right clock's on its right, so the
   * two TIMES meet at the centre line and read in one glance.
   *
   * BOTH ARE WRITTEN OUT, and .clock-bottom's now agrees with the default it
   * once had to override. It used to differ: the flowed pair mirrored, so
   * .clock-bottom's chip came last, and only .clock-top needed restating here.
   * The flowed pair was standardised on the chip first (see .seat-chip below),
   * which leaves this line saying what is already true — kept, because the axis
   * this pair controls is left/right rather than above/below, and a rule that
   * reads as half a mirror is how the next person breaks the other half.
   *
   * These two lines say #boards-container.phone-mode and NOT
   * :not(.clocks-clear), which every other rule in this section does, because
   * the mirror is not a property of the overlaid bar — it is a property of two
   * clocks side by side, and "Clocks off the board" is also two clocks side by
   * side. Scoped away from it, that bar ran the FLOWED order instead: name
   * inward, time outward, both times pinned to the two screen edges. Which is
   * the edge a long name pushes them over — reported 2026-09-02 with the box
   * ticked, the opponent's clock reading "5:" and the rest of it off the screen.
   * The cap in the clocks-clear block below is the other half of that fix. */
  body.phone-game #boards-container.phone-mode > .clock-bottom .seat-chip { order: -1; }
  body.phone-game #boards-container.phone-mode > .clock-top .seat-chip { order: 1; }

  /* The active side is the one you are looking for, so it keeps its full weight
     while the idle one recedes. Under time pressure the clock still goes orange
     and then red (.low, .flagged) — those rules are untouched by any of this. */
  body.phone-game .phone-mode:not(.clocks-clear) > .clock-box:not(.active) { background: rgba(32, 32, 58, 0.6); }

  /* ── AND AS SMALL AS IT CAN BE READ AT ─────────────────────────────────────
   *
   * Every pixel of an overlaid clock is a pixel of board, so this one is not the
   * clock the rest of the site draws. What made it big was the seat chip:
   * .seat-chip is `flex: 0 0 100%`, which gives it a line of its own, so each
   * clock stood two lines and about 70px tall — 140px of covered board between
   * them, six ranks on a phone.
   *
   * One line instead. The chip goes inline beside the time (keeping its order,
   * so it stays on the board's outer edge), the word WHITE/BLACK goes entirely —
   * a clock at the top of the board is the top player's and the chip already
   * names them — and the time comes down to a size that is still the largest
   * thing in the strip. About 26px each, so roughly a rank apiece.
   *
   * The 6ch on .clock-time is NOT touched: under ten seconds the clock switches
   * to tenths, and a box that grows at the ten-second mark is a board that
   * reflows exactly when you can least afford to look away. */
  body.phone-game .phone-mode:not(.clocks-clear) > .clock-box {
    gap: 0.4rem;
    padding: 0.05rem 0.55rem;
    /* The notch and the dynamic island, cleared from inside the bar rather than
       by moving the bar down, so it still reaches the top edge and its blur runs
       up under the status bar. A browser that does not know env() drops this
       line whole, which costs the clearance and nothing else. */
    padding-top: calc(0.05rem + env(safe-area-inset-top, 0px));
  }
  body.phone-game #boards-container.phone-mode .clock-label { display: none; }
  body.phone-game #boards-container.phone-mode .clock-time { font-size: 1.05rem; }
  body.phone-game #boards-container.phone-mode .seat-chip {
    flex: 0 1 auto;
    font-size: 0.72rem;
    max-width: 9rem;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  /* THE BADGES COME OFF THE BAR. Three of them ride in each seat chip and they
     are the widest thing in it — and two clocks now sit side by side in a bar
     360px wide, so they are the difference between a name and an ellipsis. They
     are on the player's page, they are on the chip in every other layout, and
     nobody reads them mid-move. */
  body.phone-game #boards-container.phone-mode .seat-badges { display: none; }

  /* AND SO DOES THE RATING, for the height rather than the width. It sits on a
     line of its own under the name (.seat-stack), and this bar is drawn OVER the
     board — so a second line here is a second line of board gone, on a layout
     whose whole argument is that every pixel of it is a pixel of board. It is on
     the player's page, on the leaderboard, and on the chip in every other
     layout. With it hidden .seat-stack collapses back to one line by itself. */
  body.phone-game #boards-container.phone-mode .seat-rating { display: none; }

  /* One line, both bars: the chip is `flex: 0 0 100%` by default, which gives it
     a line of its own and makes every clock two lines tall. */
  body.phone-game #boards-container.phone-mode > .clock-box { flex-wrap: nowrap; }

  /* ── LOCKED, THE BAR IS SMALLER STILL ─────────────────────────────────────
   *
   * Locked, the bar is the only thing on the screen that is not the board, and
   * every pixel of it is a pixel of board. So it gives up what it can:
   *
   * THE BADGES GO. Three of them ride in each seat chip, and they are the widest
   * part of it — a whole rank's worth of top edge, on the two ends of the board,
   * for something nobody is reading mid-move. They are on the player's page,
   * they are on the chip everywhere else, and they come straight back the moment
   * you unlock.
   *
   * The name, the time and the lit box for whose move it is all stay. Those are
   * the clock; the rest is decoration that costs board. */
  body.fit-locked.phone-game #boards-container.phone-mode > .clock-box {
    gap: 0.3rem;
    padding: 0 0.45rem;
    padding-top: env(safe-area-inset-top, 0px);
  }
  body.fit-locked.phone-game #boards-container.phone-mode .clock-time { font-size: 0.95rem; }
  body.fit-locked.phone-game #boards-container.phone-mode .seat-chip {
    font-size: 0.68rem;
    max-width: 7rem;
  }

  /* ── "Clocks off the board" ────────────────────────────────────────────────
   *
   * The same one bar at the top, put back into the flow: the board is sized
   * around it (--clock-room) instead of being drawn under it, which is what the
   * option is for. ONE bar rather than two is what makes the option cheap — the
   * board gives up a single clock's height, about a rank, rather than two.
   *
   * A grid, because a flex column cannot put two of three children on one row.
   * The clocks take row 1, a column each; the board takes the whole of row 2.
   *
   * Sticky, not static: in the flow, and still pinned to the top of the screen
   * while you scroll down to the controls and the move log. A clock that scrolls
   * away is the bug this whole layout is about, and it does not come back
   * through a checkbox.
   *
   * Everything else about them is the ordinary clock: opaque, full weight, and
   * clickable once the seat name becomes a link at the end of the game. Nothing
   * here is over the board, so there is nothing to see through and no tap to
   * pass on.
   *
   * ── AND EVERY SELECTOR HERE NAMES #boards-container. IT HAS TO. ────────────
   *
   * This block shipped as `body.phone-game .phone-mode.clocks-clear` — three
   * classes and an element, which loses to `#boards-container { display: flex }`
   * because ONE id outranks any number of classes. So display: grid never
   * applied, the container stayed a flex column, and the option put one clock
   * above the board and one below it: the exact layout it exists to replace,
   * failing silently and looking like the rule had never been written.
   *
   * It was reported from a phone with the box ticked, and it cost several rounds
   * to find, because every part of it reads correctly. Any rule here that sets a
   * property #boards-container also sets — display, align-items, gap,
   * justify-content — must carry the id or it does nothing. */
  body.phone-game #boards-container.phone-mode.clocks-clear {
    display: grid;
    /* minmax(0, …), not a bare 1fr. A bare fr track is minimum AUTO — it refuses
       to go below its content — so a clock wider than half the screen does not
       get cut off, it makes its COLUMN wider, the grid wider than the container,
       and hangs the surplus off the right-hand edge of the phone. A name is the
       only thing in there that can vary, and a name is exactly what a player
       chooses. minmax(0, 1fr) makes half the bar a hard half: the box has to fit,
       so the ellipsis on .seat-chip finally has something to bite on. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    justify-items: center;
    align-items: start;
    column-gap: 0.3rem;
    row-gap: 0.25rem;
    /* Bar at the top, board on the bottom edge, the slack between them — the
       same deal as the overlaid bar above, spelled for a grid. justify-content
       is the flex spelling and does nothing here, so the two cases each need
       their own line. */
    align-content: space-between;
  }
  body.phone-game #boards-container.phone-mode.clocks-clear > .clock-bottom {
    grid-area: 1 / 1;
    justify-self: end;
  }
  body.phone-game #boards-container.phone-mode.clocks-clear > .clock-top {
    grid-area: 1 / 2;
    justify-self: start;
  }
  body.phone-game #boards-container.phone-mode.clocks-clear > .unified-wrapper {
    grid-area: 2 / 1 / 3 / -1;
  }
  body.phone-game #boards-container.phone-mode.clocks-clear > .clock-top,
  body.phone-game #boards-container.phone-mode.clocks-clear > .clock-bottom {
    position: sticky;
    top: 0;                     /* see the note on the fixed bar above */
    z-index: 6;
    margin: 0;
    /* .phone-mode .clock-box asks for 130px, which is a floor the cap above
       cannot get under — half of a 320px screen is 160px and two chips' worth of
       padding is more than the 30px left over. The clock shrinks instead; the
       time inside it is a fixed 6ch and never does. */
    min-width: 0;
    padding-top: calc(0.35rem + env(safe-area-inset-top, 0px));
  }
}

/* ---- setup / mode-selection screen ---- */

#setup-screen {
  max-width: 860px;
  margin: 3rem auto 0;
  text-align: center;
  padding: 1rem;
  animation: setup-fade 0.4s ease;
}

@keyframes setup-fade {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0); }
}

#setup-title {
  font-size: clamp(2rem, 6vw, 3.2rem);
  letter-spacing: 0.06em;
  color: #f0f0f0;
  margin-bottom: 0.3rem;
}

#setup-subtitle {
  color: #ab47bc;
  font-size: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  margin-bottom: 2rem;
}

#mode-grid {
  display: grid;
  /* minmax(0, 1fr), NOT 1fr. A bare 1fr is minmax(auto, 1fr), so a card whose
     content will not shrink — King + Pawns is nine glyphs on one line — widens
     its own column and pushes the others off the edge of the panel. That is
     what cropped "All Pieces" to "All Pi". */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1rem;
}

@media (max-width: 600px) {
  #mode-grid { grid-template-columns: repeat(2, 1fr); }
}

.mode-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  padding: 1.4rem 0.8rem;
  background: linear-gradient(160deg, #29293f, #20203a);
  border: 1px solid #3a3a5a;
  border-radius: 10px;
  cursor: pointer;
  color: #e0e0e0;
  font-family: inherit;
  transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s;
}

.mode-card:hover {
  transform: translateY(-3px);
  border-color: #ab47bc;
  box-shadow: 0 6px 20px rgba(171, 71, 188, 0.25);
}

.mode-preview {
  font-size: 1.7rem;
  line-height: 1;
  color: #ce93d8;
  letter-spacing: 0.05em;
}

.mode-name {
  font-size: 0.92rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}

/* ---- header reset button ---- */

#reset-btn {
  margin-top: 0.6rem;
  padding: 0.35rem 1rem;
  font-size: 0.85rem;
  font-family: inherit;
  border: 1px solid #546e7a;
  border-radius: 4px;
  background: transparent;
  color: #90a4ae;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}

#sound-btn {
  margin-top: 0.6rem;
  margin-right: 0.5rem;
  padding: 0.35rem 0.6rem;
  font-size: 0.95rem;
  line-height: 1;
  font-family: inherit;
  border: 1px solid #546e7a;
  border-radius: 4px;
  background: transparent;
  cursor: pointer;
  transition: background 0.15s;
}

#sound-btn:hover { background: #546e7a; }

#reset-btn:hover {
  background: #546e7a;
  color: #fff;
}

/* ---- in-game action buttons (offer draw / pause) ---- */

.game-action {
  margin-top: 0.6rem;
  margin-right: 0.5rem;
  padding: 0.35rem 0.9rem;
  font-size: 0.85rem;
  line-height: 1;
  font-family: inherit;
  border: 1px solid #546e7a;
  border-radius: 4px;
  background: transparent;
  color: #90a4ae;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}

.game-action:hover:not(:disabled) { background: #546e7a; color: #fff; }

.game-action:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* The way back into the game-over dialog (issue #122). A .game-action like its
   neighbours, tinted the purple the site uses for "this is about the result"
   (the rating line, the badge discs, Save game) so it reads as the one button in
   the row that is offering something rather than ending something.
   Display:none rather than visibility, so the row closes up while the game is
   still being played and nothing shifts when it appears. */
#result-btn {
  border-color: #8e24aa;
  color: #ce93d8;
}
#result-btn:hover:not(:disabled) { background: #6a1b9a; border-color: #ab47bc; color: #fff; }
#result-btn.hidden { display: none; }

/* ---- auto-decline checkboxes ---- */

.autodecline-opt {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  margin-top: 0.6rem;
  margin-right: 0.5rem;
  font-size: 0.8rem;
  color: #90a4ae;
  cursor: pointer;
  user-select: none;
}

/* The one dropdown among the header options ("Reverse pieces"), styled to sit
   with the buttons rather than as a raw OS control. */
.autodecline-opt select {
  font-family: inherit;
  font-size: 0.8rem;
  line-height: 1;
  padding: 0.2rem 0.3rem;
  border: 1px solid #546e7a;
  border-radius: 4px;
  background: #263238;
  color: #cfd8dc;
  cursor: pointer;
}

.autodecline-opt select:hover { border-color: #ab47bc; color: #fff; }

/* The `hidden` attribute must win over the inline-flex above (used to show the
   single-board extension toggles only after Single board is selected). */

.autodecline-opt input {
  accent-color: #ab47bc;
  cursor: pointer;
}

/* ---- pause bar (non-blocking; keeps the whole board visible) ---- */

.pause-bar {
  position: fixed;
  left: 50%;
  bottom: 1.5rem;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.35rem;
  padding: 0.6rem 0.75rem 0.7rem 1.1rem;
  background: rgba(38, 50, 56, 0.97);
  border: 1px solid #ab47bc;
  border-radius: 8px;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.45);
  z-index: 130;
}

.pause-bar.hidden { display: none; }

.pause-bar-label {
  font-size: 1rem;
  font-weight: 600;
  color: #ce93d8;
  letter-spacing: 0.02em;
}

/* The label and Resume share the top row; the notes stack under them. */
.pause-bar-main {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  justify-content: space-between;
}

/* Empty for a game nobody is keeping (unrated bot, hotseat) — :empty drops the
   row entirely so the bar closes up rather than showing a hole. */
.pause-bar-note {
  font-size: 0.8rem;
  color: #b0bec5;
  letter-spacing: 0.02em;
}
.pause-bar-note:empty { display: none; }

/* The allowance: paused time counting down, and the pause count once it is
   nearly gone. Bigger than the note above it and on its own line, because it is
   the only thing in this box that is changing and the only thing with a
   consequence. Tabular figures so a ticking clock doesn't jitter. */
.pause-bar-left {
  font-size: 1rem;
  font-weight: 600;
  color: #cfd8dc;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
}
.pause-bar-left:empty { display: none; }

/* Three left, two, one. Red because running out ends the game, and a player who
   only notices at zero has been told too late. */
.pause-bar-left.urgent { color: #ff7043; }

/* Spent. Grey rather than red: there is nothing left to warn about, and a red
   line that cannot be acted on is just alarm. */
.pause-bar-left.spent { color: #90a4ae; font-weight: 500; }

.pause-bar #resume-btn {
  padding: 0.35rem 1rem;
  font-size: 0.95rem;
}

/* ---- transient toast notice ---- */

#toast {
  position: fixed;
  left: 50%;
  bottom: 1.5rem;
  transform: translate(-50%, 1rem);
  background: #37474f;
  color: #eceff1;
  padding: 0.6rem 1.1rem;
  border-radius: 6px;
  border: 1px solid #546e7a;
  font-size: 0.9rem;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s, transform 0.2s;
  z-index: 120;
}

#toast.show {
  opacity: 1;
  transform: translate(-50%, 0);
}

/* ---- setup: selected state, time control, start ---- */

.mode-card.selected {
  border-color: #ab47bc;
  box-shadow: 0 0 0 2px rgba(171, 71, 188, 0.5), 0 6px 20px rgba(171, 71, 188, 0.25);
  background: linear-gradient(160deg, #33334d, #262642);
}

.setup-section-label {
  margin-top: 2rem;
  margin-bottom: 0.9rem;
  color: #90a4ae;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
}

#time-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.8rem;
  max-width: 560px;
  margin: 0 auto;
}

.time-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  padding: 0.9rem 0.6rem;
  background: linear-gradient(160deg, #29293f, #20203a);
  border: 1px solid #3a3a5a;
  border-radius: 10px;
  cursor: pointer;
  color: #e0e0e0;
  font-family: inherit;
  transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s;
}

.time-card:hover {
  transform: translateY(-2px);
  border-color: #ab47bc;
}

.time-card.selected {
  border-color: #ab47bc;
  box-shadow: 0 0 0 2px rgba(171, 71, 188, 0.5);
  background: linear-gradient(160deg, #33334d, #262642);
}

.time-short {
  font-size: 1.2rem;
  font-weight: 700;
  color: #ce93d8;
  font-family: 'Courier New', monospace;
  letter-spacing: 0.04em;
}

.time-name {
  font-size: 0.78rem;
  color: #b0bec5;
}

#start-btn {
  margin-top: 2.2rem;
  padding: 0.8rem 2.6rem;
  font-size: 1.1rem;
  font-weight: 600;
  font-family: inherit;
  letter-spacing: 0.04em;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  color: #fff;
  background: linear-gradient(135deg, #ab47bc, #7b1fa2);
  box-shadow: 0 6px 18px rgba(123, 31, 162, 0.4);
  transition: transform 0.15s, box-shadow 0.15s;
}

#start-btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 9px 24px rgba(123, 31, 162, 0.5);
}

/* ---- profile: the record card (per-colour totals + career facts) ---- */

.pf-record { margin-top: 0.4rem; display: flex; flex-direction: column; gap: 0.6rem; }

/* The three figures the card leads with. Equal columns so the numbers sit on a
   shared baseline and can be compared at a glance. */
.pf-hero {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.4rem;
}
.pf-hero-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0.1rem;
  padding: 0.55rem 0.35rem;
  border-radius: 6px;
  background: #1b1b31;
}
.pf-hero-v {
  font-size: 1.5rem;
  line-height: 1.1;
  font-weight: 600;
  color: #eceff1;
  font-variant-numeric: tabular-nums;
}
.pf-hero-k {
  font-size: 10px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: #90a4ae;
}
/* Always present, empty or not, so the three tiles stay the same height. */
.pf-hero-n {
  font-size: 0.72rem;
  color: #78909c;
  min-height: 1em;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The colour split is a footnote to the figures above it now, not the headline —
   the rule is what says so. */
.pf-colours {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding-top: 0.6rem;
  border-top: 1px solid #2a2a45;
}

/* Fixed columns rather than a flex row: White's figures have to line up under
   Black's to be comparable at a glance, which is the whole point of the card.
   The name column carries the result colour the same way .rg-row does. */
.pf-cr {
  display: grid;
  grid-template-columns: 3.4rem 1fr 4.2rem 2.6rem 3rem;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.45rem 0.55rem;
  border-radius: 6px;
  background: #1b1b31;
  font-variant-numeric: tabular-nums;
}
/* A bot's arena games, tucked under the colour they belong to: same grid, quieter
   ink, so the row above stays the headline and this reads as its footnote. */
.pf-cr-arena {
  background: transparent;
  padding-top: 0.1rem;
  padding-bottom: 0.35rem;
  font-size: 0.92em;
}
.pf-cr-arena .pf-cr-name { color: #78909c; font-weight: 400; }
.pf-cr-arena .pf-cr-wdl,
.pf-cr-arena .pf-cr-rate { color: #90a4ae; }

.pf-cr-name  { color: #eceff1; font-weight: 600; }
.pf-cr-games { color: #90a4ae; font-size: 0.82rem; }
.pf-cr-wdl   { color: #cfd8dc; }
.pf-cr-rate  { color: #cfd8dc; text-align: right; }
.pf-cr-net   { text-align: right; font-weight: 600; }
.pf-cr-net.up   { color: #aad576; }
.pf-cr-net.down { color: #ef5350; }
.pf-cr-net.even { color: #78909c; }

.pf-facts { display: flex; flex-direction: column; gap: 0.25rem; }
.pf-fact {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0 0.45rem;
  font-size: 0.86rem;
}
.pf-fact-k {
  flex: none;
  min-width: 6.5rem;
  font-size: 10px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: #90a4ae;
}
.pf-fact-v { color: #eceff1; font-variant-numeric: tabular-nums; }
.pf-fact-n { color: #78909c; font-size: 0.78rem; }

/* Phones: five columns stop fitting, so split into two rows — who and how many
   above, then the figures. Every column is placed explicitly rather than left to
   auto-flow, which would strand the net points on a third row of its own. */
@media (max-width: 430px) {
  /* The tiles stay three across — they are the point of the card — but the
     figure shrinks so a four-digit rating cannot wrap out of its box. */
  .pf-hero { gap: 0.3rem; }
  .pf-hero-tile { padding: 0.5rem 0.2rem; }
  .pf-hero-v { font-size: 1.25rem; }
  .pf-hero-k { font-size: 9px; letter-spacing: 0.04em; }
  .pf-hero-n { font-size: 0.66rem; }

  .pf-cr { grid-template-columns: 1fr auto auto; row-gap: 0.15rem; }
  .pf-cr-name  { grid-column: 1; }
  .pf-cr-games { grid-column: 2 / -1; text-align: right; }
  .pf-cr-wdl   { grid-column: 1; }
  .pf-cr-rate  { grid-column: 2; }
  .pf-cr-net   { grid-column: 3; min-width: 2.8rem; }
}

/* ---- profile: recent games ---- */

.pf-recent { margin-top: 0.4rem; }
.rg-list { display: flex; flex-direction: column; gap: 0.35rem; }

.rg-row {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.45rem 0.55rem;
  border-radius: 6px;
  background: #1b1b31;
  border-left: 3px solid #546e7a;   /* the result, readable before you read anything */
}
.rg-row.win  { border-left-color: #aad576; }
.rg-row.loss { border-left-color: #ef5350; }
.rg-row.draw { border-left-color: #78909c; }

.rg-mark {
  flex: none;
  width: 1.4rem;
  text-align: center;
  font-weight: 700;
  font-size: 0.95rem;
  line-height: 1.5;
  color: #90a4ae;
}
.rg-row.win .rg-mark  { color: #aad576; }
.rg-row.loss .rg-mark { color: #ef5350; }

/* A name that opens somebody's page. Styled as text rather than as a button so
   a list of opponents still reads as a list — the underline on hover is what
   says it is pressable, and :focus-visible keeps it reachable by keyboard. */
.pf-plink {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-align: inherit;
}
.pf-plink:hover { text-decoration: underline; }
.pf-plink:focus-visible { outline: 2px solid #7cc4ff; outline-offset: 2px; border-radius: 3px; }

/* The one-line tally under a player's name on their public page. */
.pf-tally {
  margin: 0.4rem 0 0;
  color: #90a4ae;
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
}

/* The side you played, as a disc in its own column. The row is align-items:
   flex-start, so the margin lifts it onto the first text line rather than the
   top of a two-line block. */
.rg-seat {
  flex: none;
  width: 0.62rem;
  height: 0.62rem;
  margin-top: 0.4rem;
  border-radius: 50%;
  border: 1px solid #90a4ae;
}
.rg-seat.white { background: #e8eaed; }
.rg-seat.black { background: #12121f; }

.rg-main { flex: 1; min-width: 0; }
.rg-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.rg-opp { font-size: 0.92rem; color: #e0e0e0; }
.rg-bot {
  margin-left: 0.35rem;
  padding: 0.05rem 0.3rem;
  border: 1px solid #546e7a;
  border-radius: 3px;
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: #90a4ae;
}
.rg-end { font-size: 0.75rem; color: #90a4ae; }

/* An ending that has a tooltip worth reading — the two "abandoned" reasons,
   which are a word apart and mean different things. The dotted underline is the
   whole affordance: without it a title attribute is a secret. Deliberately
   quiet, since most rows have nothing extra to say and a list of underlines
   would read as links. */
.rg-why {
  border-bottom: 1px dotted #607d8b;
  cursor: help;
}

/* "today" and "3 days ago" carry the day they stand for as a title (issue
   #100). NO dotted underline, unlike .rg-why above: that one marks the handful
   of rows with something extra to say, and this is on every row — a column of
   dots would read as a column of links. The cursor is the whole hint, which is
   the right weight for a tooltip that only confirms what the row already says. */
.rg-when { cursor: help; }

/* Both ladders on one line where there's room, wrapping to two when there
   isn't — four numbers is a lot to fit on a phone. */
.rg-runs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.9rem;
  margin-top: 0.15rem;
  font-size: 0.78rem;
  color: #90a4ae;
  font-variant-numeric: tabular-nums;
}
.rg-who { color: #78909c; }
.rg-run b { color: #e0e0e0; font-weight: 600; }
.rg-delta.up   { color: #aad576; }
.rg-delta.down { color: #ef5350; }
.rg-delta.even { color: #78909c; }
/* A seat still in its dark-horse era: the name of the tier where the rating
   would be, and the countdown behind it (#111). Quiet on purpose — it sits in
   the same column as a rating and must not read as one. The label keeps a
   little more light than the count: it is the answer, the count is the aside. */
.rg-dh { color: #b0bec5; font-weight: 600; }
.rg-togo { color: #78909c; font-variant-numeric: normal; }

.pf-more { margin-top: 0.5rem; }

/* ---- in-game clocks ---- */

.clock-box {
  display: flex;
  flex-wrap: wrap;          /* so the seat chip can take a line of its own */
  align-items: baseline;
  justify-content: center;
  gap: 0.6rem;
  margin-bottom: 0.4rem;
  padding: 0.35rem 1rem;
  border-radius: 6px;
  background: #20203a;
  border: 1px solid #2f2f4a;
  min-width: 150px;
  transition: background 0.2s, border-color 0.2s, box-shadow 0.2s;
}

.clock-label {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: #90a4ae;
}

.clock-time {
  font-family: 'Courier New', monospace;
  font-size: 1.5rem;
  font-weight: 700;
  color: #e0e0e0;
  font-variant-numeric: tabular-nums;
  /* Sized for the WIDEST reading it can ever show, not the usual one. Under ten
     seconds the clock switches to tenths (js/clock.js), so "0:09" becomes
     "0:09.4" — two characters more. Reserving only four meant the box grew at
     the ten-second mark, and in the reflection view that widened both gutters
     enough to wrap the move log below the boards, mid-game, exactly when you
     were least able to afford looking away. Six also covers a long game's
     "120:00", so the box is one fixed size in every state it has. */
  min-width: 6ch;
  text-align: right;
}

.clock-box.active {
  border-color: #aad576;
  background: #25351f;
  box-shadow: 0 0 12px rgba(170, 213, 118, 0.3);
}

/* Who is behind the clock: name, rating and their three show-off badges. A
   full-width flex line inside the clock box, so it stacks rather than sitting
   beside the time.

   ALWAYS ABOVE THE TIME, BOTH CLOCKS. This used to mirror — `.clock-bottom`'s
   chip took `order: 1`, which put each name on the OUTER edge of the pair and
   left the two times closest to the board. That reads as symmetry in a mockup
   and as a mistake on a screen: with one clock above the board and one below it,
   your own name sat under your time and your opponent's sat over theirs, so the
   same two facts came in a different order on each side and neither could be
   found by position. Standardised on the chip first, which is the order every
   other layout already used.

   The phone bar restates both clocks for a different axis entirely — names
   outward, times meeting at the centre line — and is unaffected by this. See the
   `#boards-container.phone-mode` pair above. */
.seat-chip {
  flex: 0 0 100%;
  order: -1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  min-width: 0;
  font-size: 0.78rem;
  color: #cfd8dc;
}

.seat-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* The opponent's name, once the game is over: a button that looks like the text
   it replaced until there is something to press. Disabled is the live game —
   plain text, no cursor, nothing to invite a click mid-move. */
button.seat-name {
  border: 0;
  padding: 0;
  background: none;
  font: inherit;
  color: inherit;
}
button.seat-name:disabled { cursor: default; }
button.seat-name:not(:disabled) {
  cursor: pointer;
  color: #d9c2e4;
  text-decoration: underline dotted rgba(206, 147, 216, 0.5);
  text-underline-offset: 2px;
  transition: color 0.15s;
}
button.seat-name:not(:disabled):hover { color: #ce93d8; }
button.seat-name:focus-visible { outline: 2px solid #ce93d8; outline-offset: 2px; }

/* The name and the rating, stacked. A row that wraps rather than a column, so
   the bot tag stays beside the name and only the rating — the one child given a
   full line below — drops under it. `min-width: 0` is what keeps .seat-name's
   ellipsis working from in here. */
/* The name with its rating under it. A column, and therefore exactly as wide as
   the name — which is the whole reason the bot tag is a sibling of this rather
   than a child (see buildSeatChipDOM): inside, it widened the box and the number
   centred under "Kami Kaze bot" instead of under the name.

   Baseline-aligned within the chip, so the tag that follows it sits on the
   NAME's line rather than halfway down a two-line block. The avatar and the
   badges stay centred against the whole thing. */
.seat-stack {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 0;
}
.seat-chip > .seat-stack,
.seat-chip > .rg-bot { align-self: baseline; }

/* What the seat is rated (issue #98). Deliberately QUIETER than the name and
   quieter than the badges: it is a fact about the player, not a thing they
   chose.

   A GREYED PURPLE — the midpoint of the two this tried first. The house purple
   (#ce93d8, what the badges beside it wear) made the number the loudest thing on
   a chip whose name is plain grey, so it pulled the eye before the person did.
   The blue-grey it replaced (#8f9bb3) read as chrome and lost the family
   resemblance to the badges entirely. This keeps the hue and drops the
   saturation, which is the half that was shouting. Tabular figures so the two
   clocks' numbers line up under each other rather than shimmering by a pixel.

   Only ever drawn once a player has a published rating at all — see
   seatRatingDOM in js/ui.js for why an unrated one is withheld here rather than
   marked, and api/_mm.js for why the number never reaches the browser either. */
.seat-rating {
  line-height: 1.25;
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  color: #af97c6;
}
/* Provisional, on a clock: the `?` is in the text, this is the rest of the mark.
   Greyed out of the purple family entirely, because on a clock the number has no
   caption beside it to do the qualifying. */
.seat-rating.provisional { color: #90a4ae; }

/* The show-off badges wear the same purple they do on the profile: the point of
   them is to be recognised across the two screens, and a grey disc on the clock
   read as chrome rather than as something somebody earned. */
.seat-badges { display: flex; gap: 0.25rem; flex: none; }
.seat-badge {
  position: relative;        /* the tip hangs off this */
  width: 1.05rem;
  height: 1.05rem;
  padding: 0;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 35%, #462f52, #2b2340);
  border: 1px solid #ab47bc;
  box-shadow: 0 0 0 1px rgba(171, 71, 188, 0.2);
  font-family: inherit;
  font-size: 0.62rem;
  line-height: 0.95rem;
  text-align: center;
  color: #ce93d8;
  cursor: pointer;
  overflow: visible;         /* a button must not clip the tip hanging off it */
  -webkit-appearance: none;
  -webkit-tap-highlight-color: transparent;
}
.seat-badge .bg-svg { width: 0.7rem; height: 0.7rem; vertical-align: -0.1rem; }
.seat-badge:focus-visible { outline: 2px solid #ce93d8; outline-offset: 2px; }

/* …except a gold one, which is gold here too. Same palette as the grid — see
   the note by .bg-slot.gold. A disc this small cannot carry a second signal, so
   the colour is the whole of it. */
.seat-badge.gold {
  border-color: #ffc94d;
  background: radial-gradient(circle at 50% 35%, #5a4318, #33280f);
  box-shadow: 0 0 0 1px rgba(255, 201, 77, 0.3);
  color: #ffd77a;
}
.seat-badge.gold:focus-visible { outline-color: #ffc94d; }

/* …or a glass one, whose body comes from the shared rule by .bg-slot.earned.glass
   — at 17px the specular catch is most of what survives, and it is enough. The
   glint deliberately does not run here; see the note there. */
.seat-badge.glass {
  color: #f2f8ff;
  /* No blur on this one. It is 17px, so there is nothing to see through it, and
     it sits on a clock that repaints every second — a backdrop filter there has
     to re-sample the region each time for an effect nobody can make out. */
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
.seat-badge.glass:focus-visible { outline-color: #e9f4ff; }

/* A 17px disc is a fine thing to look at and a poor thing to hit with a thumb,
   which is exactly what happens on a phone. The disc keeps its size; the target
   around it does not. */
.seat-badge::after {
  content: '';
  position: absolute;
  inset: -6px;
  border-radius: 50%;
}

/* What the badge is. Above the disc, flipped below by placeBadgeTip when the
   clock sits too near the top of the window, and nudged sideways there too so a
   full-sentence criterion cannot run off a narrow screen. */
.seat-tip {
  position: absolute;
  bottom: calc(100% + 0.45rem);
  left: 50%;
  transform: translateX(-50%);
  z-index: 40;
  width: max-content;
  max-width: min(17rem, 70vw);
  padding: 0.4rem 0.55rem;
  border-radius: 0.4rem;
  background: #1c1830;
  border: 1px solid #6a4c78;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
  color: #e6dcec;
  font-size: 0.72rem;
  line-height: 1.35;
  text-align: left;
  white-space: normal;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;      /* never eats the tap that would dismiss it */
  transition: opacity 0.12s;
}
.seat-tip.tip-below { bottom: auto; top: calc(100% + 0.45rem); }
.seat-badge:hover .seat-tip,
.seat-badge:focus-visible .seat-tip,
.seat-badge.tip-open .seat-tip { opacity: 1; visibility: visible; }

/* Desktop has the room, and the clock line is the one place a badge is seen by
   the person you are playing — so it is worth the extra millimetre there. The
   phone keeps the small discs: that row already competes with the board. */
@media (min-width: 701px) {
  .seat-badges { gap: 0.25rem; }
  .seat-badge {
    width: 1.55rem;
    height: 1.55rem;
    font-size: 0.9rem;
    line-height: 1.45rem;
  }
  .seat-badge .bg-svg { width: 1rem; height: 1rem; vertical-align: -0.15rem; }
}

/* The showcase on a profile page, and the picker's state in the badge grid. */
.bg-showcase { display: flex; gap: 0.25rem; margin-top: 0.35rem; }
.bg-badge.bg-mini { width: 1.5rem; height: 1.5rem; font-size: 0.8rem; line-height: 1.5rem; }

/* A mini disc has no .bg-slot around it, so it never picked up the lit ring the
   same badge wears in the grid — it drew with the unearned grey border and a
   glyph at 0.3 opacity, greyscaled. On somebody else's profile that is the only
   place their showcase appears, and a badge you had to earn was rendering as
   the "not yet" state. `earned` is set by showcaseRow (every disc there is, by
   definition), and these two rules are what it means without a slot. */
.bg-badge.bg-mini.earned {
  border-color: #ab47bc;
  background: radial-gradient(circle at 50% 35%, #462f52, #2b2340);
  box-shadow: 0 0 0 1px rgba(171, 71, 188, 0.25);
}
.bg-badge.bg-mini.earned .bg-glyph { opacity: 1; filter: none; }

.bg-badge.bg-mini.earned.gold {
  border-color: #ffc94d;
  background: radial-gradient(circle at 50% 35%, #5a4318, #33280f);
  box-shadow: 0 0 0 1px rgba(255, 201, 77, 0.3);
}
.bg-badge.bg-mini.earned.gold .bg-glyph { color: #ffd77a; }

.bg-chosen { margin: 0.2rem 0 0.6rem; }
.bg-picker .bg-chosen, .modal-card .bg-chosen { text-align: left; }
.bg-slot.picked .bg-badge {
  border-color: #7cc4ff;
  box-shadow: 0 0 0 2px rgba(124, 196, 255, 0.35);
}
/* The pick's position, not a tick — the order is the order they appear in.
   A column rather than one marker, because a graded badge is ONE circle in the
   grid and several ids in the showcase: wear its purple and its bronze and both
   positions belong to the same slot. At most three, so the column never outgrows
   the disc it hangs off. */
.bg-picks {
  position: absolute;
  z-index: 1;          /* above the hover shine, which is a ::after on the disc */
  top: -0.2rem;
  right: -0.2rem;
  display: flex;
  flex-direction: column;
  gap: 0.12rem;
}
.bg-pick {
  width: 1rem;
  height: 1rem;
  border-radius: 50%;
  background: #7cc4ff;
  color: #0f1216;
  font-size: 0.62rem;
  font-weight: 700;
  line-height: 1rem;
  text-align: center;
}

.clock-box.active .clock-time { color: #c8e6a0; }

.clock-box.low .clock-time { color: #ffa726; }

.clock-box.active.low {
  border-color: #ffa726;
  background: #3a2a14;
  box-shadow: 0 0 12px rgba(255, 167, 38, 0.35);
}

.clock-box.flagged {
  border-color: #ef5350;
  background: #3a1a1a;
}

.clock-box.flagged .clock-time { color: #ef5350; }

/* ---- online play: setup buttons ---- */

#start-row {
  display: flex;
  gap: 1rem;
  justify-content: center;
  flex-wrap: wrap;
  margin-top: 2.2rem;
}

#start-row #start-btn { margin-top: 0; }

#online-btn {
  padding: 0.8rem 2.6rem;
  font-size: 1.1rem;
  font-weight: 600;
  font-family: inherit;
  letter-spacing: 0.04em;
  border-radius: 8px;
  cursor: pointer;
  color: #ce93d8;
  background: transparent;
  border: 1px solid #ab47bc;
  transition: transform 0.15s, background 0.15s, color 0.15s;
}

#online-btn:hover {
  transform: translateY(-2px);
  background: rgba(171, 71, 188, 0.15);
  color: #fff;
}

/* ---- bots: setup button, picker, in-game indicator ---- */

#bot-btn, #bot-back-btn {
  padding: 0.8rem 2.6rem;
  font-size: 1.1rem;
  font-weight: 600;
  font-family: inherit;
  letter-spacing: 0.04em;
  border-radius: 8px;
  cursor: pointer;
  color: #ce93d8;
  background: transparent;
  border: 1px solid #ab47bc;
  transition: transform 0.15s, background 0.15s, color 0.15s;
}

#bot-btn:hover, #bot-back-btn:hover {
  transform: translateY(-2px);
  background: rgba(171, 71, 188, 0.15);
  color: #fff;
}

/* ==== the unrated-game screen, Alpha 1.1.7 ==============================
   Two halves separated by a rule: a queue with fixed terms, then everything
   you set up yourself. The divider is load-bearing — Find Unrated Game ignores
   the pickers below it, so the two must not read as one form. */

.setup-mm {
  max-width: 560px;
  margin: 0 auto 1.6rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.7rem;
}

/* A RUNG OF THEIR OWN, between the pickers and the ladder.
   
   Three fills, in order of how much purple is in them:
   
     .mode-card / .time-card   #29293f  a choice being recorded
     these four                #33294a  a button that takes you somewhere
     .home-big.primary         #7e3f9e  Find Rated Game, the one loud thing
   
   The step up from the pickers is small but it is the whole point: these DO
   something — start a game, join a queue — where the cards above them only
   remember what you picked. The step down from the rated button is large, and
   deliberately: the site has one headline action and nothing a screen deeper may
   compete with it.
   
   Purple otherwise stays reserved for what is SELECTED and what is under the
   cursor, which is where it carries information. */
#find-unrated-btn {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  padding: 0.9rem 1.4rem;
  border: 1px solid #4a3a5e;
  border-radius: 10px;
  cursor: pointer;
  font-family: inherit;
  color: #e8e0ef;
  background: linear-gradient(160deg, #33294a, #272038);
  transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s;
}
#find-unrated-btn:hover {
  transform: translateY(-2px);
  border-color: #ab47bc;
  box-shadow: 0 6px 20px rgba(171, 71, 188, 0.25);
}
.mm-title { font-size: 1.1rem; font-weight: 600; letter-spacing: 0.04em; }
/* The terms this button plays on, and the answer to "why did I not get the
   clock I picked". Dimmed, but on the button rather than beside it — a caption
   somewhere else would be read after the click rather than before it. */
.mm-terms {
  font-size: 0.78rem;
  color: #90a4ae;
  font-family: 'Courier New', monospace;
  letter-spacing: 0.03em;
}
.setup-mm .mm-toggle { justify-content: center; text-align: left; }

.setup-divider {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  max-width: 560px;
  margin: 0 auto 1.4rem;
  color: #6b7a8a;
  font-size: 0.82rem;
}
.setup-divider::before,
.setup-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: #2f2f4a;
}

/* ---- a section folded away behind the answer it currently holds ---- */

.setup-fold {
  /* Full width of the screen: it holds a three-column grid of cards, and at
     480px inside an 860px page that grid had nowhere to go. */
  max-width: 100%;
  margin: 0 auto 1.4rem;
  border: 1px solid #2f2f4a;
  border-radius: 10px;
  overflow: hidden;
  background: #20203a;
}

.fold-head {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 0.8rem;
  padding: 0.7rem 0.9rem;
  background: none;
  border: none;
  cursor: pointer;
  color: #e0e0e0;
  font-family: inherit;
  font-size: 0.95rem;
  text-align: left;
}
.fold-head:hover { background: rgba(171, 71, 188, 0.12); }

.fold-label {
  color: #90a4ae;
  font-size: 0.78rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  flex: 0 0 auto;
}
/* The current answer, which is the whole reason the panel can stay shut. */
.fold-value {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-width: 0;
}
.fold-preview {
  color: #ce93d8;
  font-size: 1.05rem;
  letter-spacing: 0.04em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.fold-caret { color: #6b7a8a; flex: 0 0 auto; }

/* The six cards, when opened. Tighter than they were standing alone — inside a
   panel they are a menu rather than the subject of the page. */
.fold-body { padding: 0.9rem; border-top: 1px solid #2f2f4a; }
.fold-body#mode-grid { gap: 0.6rem; }
.fold-body .mode-card { padding: 0.9rem 0.5rem; gap: 0.4rem; }
/* The previews have to be able to shrink, or they re-create the blowout inside
   a column that is now the right width. Nine glyphs is the widest any card
   holds, so this is sized for King + Pawns and everything else has room. */
.fold-body .mode-preview {
  font-size: 1.35rem;
  letter-spacing: 0.02em;
  max-width: 100%;
  overflow: hidden;
}
/* Names on one line where they fit, and legible where they do not — "King +
   Pawns + Rooks" broke over four lines in the cropped column. */
.fold-body .mode-name { font-size: 0.82rem; line-height: 1.25; }

@media (max-width: 700px) {
  .fold-body#mode-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ---- the clock, now with a custom option ---- */

#time-row { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 600px) {
  #time-row { grid-template-columns: repeat(2, 1fr); }
}

#custom-time {
  max-width: 560px;
  margin: 0.8rem auto 0;
  padding: 0.9rem;
  border: 1px solid #2f2f4a;
  border-radius: 10px;
  background: #20203a;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.custom-field {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 0.9rem;
  color: #b0bec5;
}
.custom-field-label { flex: 1 1 auto; text-align: left; }
.custom-field-input {
  flex: 0 0 5rem;
  font-family: 'Courier New', monospace;
  font-size: 1rem;
  padding: 0.4rem 0.5rem;
  border-radius: 6px;
  border: 1px solid #546e7a;
  background: #1a1a2e;
  color: #f0f0f0;
  text-align: center;
}
.custom-field-input:focus { outline: none; border-color: #ab47bc; }
.custom-field-unit { flex: 0 0 auto; color: #6b7a8a; font-size: 0.82rem; }

/* What the two fields add up to, echoed in the same notation the rest of the
   site uses for a clock. Turns red and says the range when the pair is one this
   board will not accept, rather than letting the player find that out by
   pressing a button and getting no clock. */
.custom-echo {
  margin: 0;
  text-align: center;
  font-family: 'Courier New', monospace;
  font-size: 1.05rem;
  color: #ce93d8;
  letter-spacing: 0.04em;
}
.custom-echo.bad {
  color: #ef9a9a;
  font-family: inherit;
  font-size: 0.82rem;
  letter-spacing: normal;
}

/* ---- who you are playing ----
   Three peers now, each naming an opponent rather than a mechanism, so they
   are laid out as equal cards instead of one filled button and two outlines.
   The id selectors above still carry each one's colour; these override the
   layout they assumed, and need the id in the selector to do it. */

#start-row { gap: 0.8rem; }

/* THREE PEERS, STYLED AS THREE PEERS.
 *
 * "Play locally" wore the filled purple alone until Alpha 1.1.7 — inherited from
 * when it was "Play on This Computer" and hotseat was the only way to play at
 * all. It had become the least likely of the three and still looked like the
 * answer, which is what made the row read as one recommendation and two
 * alternatives.
 *
 * All three are filled now, matching Find Unrated Game above them. They are four
 * ways to start a game and no one of them is the answer, so they carry the same
 * weight: choosing an opponent is a choice between equals.
 *
 * Scoped to the row on purpose. #start-btn is ALSO the bot picker's "Start Game"
 * (js/ui.js BotPicker), where the gradient means something different — it is the
 * one primary on that screen — so this overrides inside #start-row and nowhere
 * else. */
#start-row .start-card {
  flex: 1 1 10rem;
  max-width: 14rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.2rem;
  padding: 0.8rem 1rem;
  font-size: 1rem;
  color: #e8e0ef;
  background: linear-gradient(160deg, #33294a, #272038);
  border: 1px solid #4a3a5e;
  box-shadow: none;
}
#start-row .start-card:hover {
  transform: translateY(-2px);
  border-color: #ab47bc;
  box-shadow: 0 6px 20px rgba(171, 71, 188, 0.25);
}
/* The sub-label reads against the fill rather than beside it, so it needs to be
   dimmed white rather than simply transparent. */
#start-row .start-card .start-note { color: #90a4ae; opacity: 1; }
/* A refused button must not look live, and must not lift on hover — these are
   disabled while the custom clock is unreadable. */
#start-row .start-card:disabled,
#start-row .start-card:disabled:hover {
  opacity: 0.4;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
  border-color: #4a3a5e;
  background: linear-gradient(160deg, #33294a, #272038);
}
.start-label { font-size: 1rem; font-weight: 600; letter-spacing: 0.03em; }
.start-note {
  font-size: 0.74rem;
  font-weight: 400;
  letter-spacing: normal;
  opacity: 0.75;
}

/* ---- home: the second door ---- */

#home-unrated {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  margin-top: 1.6rem;
}

#bot-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.8rem;
  margin-top: 1.2rem;
}

@media (max-width: 560px) { #bot-grid { grid-template-columns: 1fr; } }

.bot-card {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 0.9rem 1rem;
  text-align: left;
  border: 1px solid #2f2f4a;
  border-radius: 10px;
  background: #20203a;
  color: #e0e0e0;
  cursor: pointer;
  font-family: inherit;
  transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s;
}

.bot-card:hover {
  transform: translateY(-2px);
  border-color: #ab47bc;
  box-shadow: 0 6px 20px rgba(171, 71, 188, 0.2);
}

.bot-card.selected {
  border-color: #ab47bc;
  box-shadow: 0 0 0 2px rgba(171, 71, 188, 0.5);
  background: linear-gradient(160deg, #33334d, #262642);
}

/* A family heading inside the bot grid. Spans both columns so the cards below
   it read as a set rather than as the first two of a longer list. */
.bot-family {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  margin-top: 0.6rem;
  padding-bottom: 0.35rem;
  border-bottom: 1px solid #33334d;
}
.bot-family:first-child { margin-top: 0; }
.bot-family-name {
  font-size: 0.95rem;
  font-weight: 700;
  color: #ce93d8;
  letter-spacing: 0.02em;
}
.bot-family-note { font-size: 0.8rem; color: #90a4ae; }

.bot-name { font-size: 1.05rem; font-weight: 700; color: #ce93d8; }
.bot-blurb { font-size: 0.85rem; color: #b0bec5; line-height: 1.35; }

.bot-level { display: flex; gap: 0.3rem; margin-top: 0.2rem; }
.bot-level .pip {
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  border: 1px solid #546e7a;
}
.bot-level .pip.on { background: #ab47bc; border-color: #ab47bc; }

/* ---- the bot picker's second step: which side ----
 *
 * A panel, not a section. The picker asks its two questions on two screens now
 * (js/ui.js BotPicker), and this is the whole of the second one — the bot you
 * chose, the side, and the terms. There is no start button: tapping a side IS
 * starting the game, so .color-card is the primary action on this screen rather
 * than a selection waiting on one. It is sized to fit a phone screen with
 * nothing below the fold, because being below the fold is the fault it exists
 * to fix. */

#bot-confirm {
  max-width: 32rem;
  margin: 0 auto;
}

.bot-chosen {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  margin-top: 1.2rem;
  padding: 1rem 1.1rem;
  border: 1px solid #4a3a5e;
  border-radius: 12px;
  background: linear-gradient(160deg, #33334d, #262642);
}

.bot-chosen .bot-blurb { text-align: center; max-width: 26rem; }
.bot-chosen .bot-level { justify-content: center; }

#bot-color-row {
  display: flex;
  gap: 0.8rem;
  justify-content: center;
  margin-top: 0.6rem;
}

.color-card {
  flex: 1 1 0;
  max-width: 12rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  padding: 0.9rem 0.8rem;
  font-size: 1rem;
  font-family: inherit;
  border: 1px solid #2f2f4a;
  border-radius: 12px;
  background: #20203a;
  color: #cfd8dc;
  cursor: pointer;
  transition: transform 0.15s, border-color 0.15s, background 0.15s, box-shadow 0.15s;
}

.color-card:hover {
  transform: translateY(-2px);
  border-color: #ab47bc;
}

.color-card.selected {
  border-color: #ab47bc;
  box-shadow: 0 0 0 2px rgba(171, 71, 188, 0.5);
  background: linear-gradient(160deg, #33334d, #262642);
}

/* BOTH KINGS IN THE SAME INK, ON THE SAME LIGHT DISC — which looks wrong for
 * half a second and is right. ♔ is an outline glyph and ♚ a solid one, so a
 * diagram tells the sides apart by SHAPE, not by colour; drawing the black king
 * in a light colour on a dark chip makes it read as the white one. This is how
 * every printed chess position has done it. */
.color-king {
  width: 2.6rem;
  height: 2.6rem;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.9rem;
  line-height: 1;
  border-radius: 50%;
  background: #e8eaf0;
  color: #16161f;
}

.color-name { font-size: 1.05rem; font-weight: 600; color: #e0e0e0; letter-spacing: 0.03em; }
.color-note { font-size: 0.74rem; color: #90a4ae; line-height: 1.3; text-align: center; }

/* The settings the screen two steps back already decided. Quiet — it is there
   to be checked, not read. */
#bot-terms {
  margin-top: 1.2rem;
  font-size: 0.76rem;
  color: #90a4ae;
  text-transform: uppercase;
  letter-spacing: 0.14em;
}

#bot-indicator,
#opp-indicator {
  font-size: 0.9rem;
  color: #90a4ae;
  margin-top: 0.2rem;
}

#opp-indicator:empty { display: none; } /* room links carry no identity */

#bot-indicator.thinking { color: #ce93d8; }

/* ---- online play: lobby / share screen ---- */

#lobby-screen {
  max-width: 620px;
  margin: 3rem auto 0;
  text-align: center;
  padding: 1rem;
  animation: setup-fade 0.4s ease;
}

.lobby-instructions {
  color: #b0bec5;
  font-size: 1rem;
  line-height: 1.5;
  max-width: 460px;
  margin: 0 auto 1.6rem;
}

.share-row {
  display: flex;
  gap: 0.6rem;
  max-width: 520px;
  margin: 0 auto;
}

.share-input {
  flex: 1;
  padding: 0.7rem 0.9rem;
  font-family: 'Courier New', monospace;
  font-size: 0.95rem;
  color: #e0e0e0;
  background: #20203a;
  border: 1px solid #3a3a5a;
  border-radius: 8px;
}

.lobby-btn {
  padding: 0.7rem 1.6rem;
  font-size: 1rem;
  font-weight: 600;
  font-family: inherit;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  color: #fff;
  background: linear-gradient(135deg, #ab47bc, #7b1fa2);
  box-shadow: 0 6px 18px rgba(123, 31, 162, 0.4);
  transition: transform 0.15s, box-shadow 0.15s;
}

.lobby-btn:hover { transform: translateY(-2px); }

.copy-feedback {
  min-height: 1.2rem;
  margin: 0.6rem 0 0;
  color: #66bb6a;
  font-size: 0.85rem;
  letter-spacing: 0.02em;
}

/* --- the two seats in a link game's room (Lobby.showRoom) --- */
.seat-row {
  display: flex;
  gap: 0.9rem;
  justify-content: center;
  max-width: 520px;
  margin: 0 auto 1.6rem;
}

.seat-card {
  flex: 1;
  padding: 1.1rem 0.9rem;
  border: 1px solid #3a3a5a;
  border-radius: 10px;
  background: #20203a;
  text-align: center;
}

/* Your own seat, so the side you are on is readable at a glance rather than
   from the button text. Same purple as the primary action everywhere else. */
.seat-card.seat-mine {
  border-color: #ab47bc;
  box-shadow: 0 0 0 1px rgba(171, 71, 188, 0.35);
}

.seat-label {
  font-size: 1.05rem;
  font-weight: 600;
  color: #e0e0e0;
  margin-bottom: 0.25rem;
}

.seat-who {
  font-size: 0.85rem;
  color: #90a4ae;
  margin-bottom: 0.9rem;
  min-height: 1.1rem;
}

.seat-btn {
  width: 100%;
  padding: 0.55rem 0.8rem;
  font-size: 0.92rem;
}

/* A seat somebody else is in, and a Leave that cannot be pressed because the
   game has started. Both are "there is nothing to do here", so both look the
   same rather than inventing a second disabled style. */
.seat-btn:disabled {
  background: #2a2a44;
  color: #6b7a85;
  box-shadow: none;
  cursor: default;
}

.seat-btn:disabled:hover { transform: none; }

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

.waiting-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.7rem;
  margin-top: 2rem;
  color: #90a4ae;
  font-size: 0.95rem;
}

.spinner {
  width: 16px;
  height: 16px;
  border: 2px solid #3a3a5a;
  border-top-color: #ab47bc;
  border-radius: 50%;
  display: inline-block;
  animation: spin 0.8s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

.lobby-cancel {
  margin-top: 2rem;
  padding: 0.4rem 1.2rem;
  font-size: 0.85rem;
  font-family: inherit;
  border: 1px solid #546e7a;
  border-radius: 4px;
  background: transparent;
  color: #90a4ae;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}

.lobby-cancel:hover { background: #546e7a; color: #fff; }

/* ---- online play: in-game seat / connection indicators ---- */

#seat-indicator {
  margin-top: 0.3rem;
  font-size: 0.9rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  color: #b0bec5;
}

#seat-indicator.white { color: #eceff1; }
#seat-indicator.black { color: #b0bec5; }
#seat-indicator.your-turn { color: #ce93d8; }

#conn-indicator {
  min-height: 1rem;
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  color: #ffa726;
}

#conn-indicator.conn-lost::before { content: '● '; }

/* ================= Home / landing screen ================= */

#home-screen {
  max-width: 720px;
  margin: 2.5rem auto 0;
  padding: 1rem 1.2rem 3rem;
  text-align: center;
  animation: setup-fade 0.4s ease;
}

#home-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 2.6rem;
}

/* Animated brand: "Chessbius" that opens out into "Chess Möbius" on hover. The
   hidden span sits in the MIDDLE of the word rather than after the first letter,
   so the reveal separates the two names that were overlapping. */
.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  cursor: default;
  user-select: none;
  font-size: clamp(1.6rem, 5vw, 2.4rem);
  letter-spacing: 0.05em;
  color: #f0f0f0;
  font-weight: 700;
}
.brand .brand-text { display: inline-flex; align-items: baseline; }
/* The board mark, in front of the name. Sized in `em` so it tracks the clamped
   font-size above and needs no breakpoint of its own. The file is 192px for the
   sake of high-density screens; it draws at roughly a third of that. */
.brand .brand-mark {
  width: 1.5em;
  height: 1.5em;
  flex: none;
  object-fit: contain;
}
.brand .b-ext {
  display: inline-block;
  max-width: 0;
  overflow: hidden;
  opacity: 0;
  white-space: pre;      /* preserve the trailing space */
  color: #ce93d8;
  transition: max-width 0.45s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.35s ease;
}
.brand:hover .b-ext,
.brand:focus-within .b-ext {
  /* Sized for " Mö" — was 6ch when the hidden text was "öbius ". Too small
     clips the M; too large and the animation finishes early, then coasts. */
  max-width: 4ch;
  opacity: 1;
}

/* Login / profile corner. A page-level element (index.html) rather than part of
   any screen, so it survives navigation — every screen replaces the whole of
   #game-container, which is how it used to disappear on the leaderboard.
   Fixed, so it stays put while a long standings table scrolls under it. */
#site-corner {
  position: fixed;
  top: 1rem;
  right: 1rem;
  z-index: 40;
  display: flex;
  flex-direction: column;   /* account on top, feedback under it */
  align-items: flex-end;
  gap: 0.35rem;
}

/* The account row, unchanged from when it was the whole corner. */
.corner-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-height: 2.2rem;
}

/* Quiet on purpose. It sits on every page at the top right, so anything with a
   fill or a bright edge would read as the primary action on screens where the
   primary action is "play". Borderless until hovered. */
.corner-feedback {
  font-family: inherit;
  font-size: 0.75rem;
  color: #78909c;
  padding: 0.15rem 0.5rem;
  border: 1px solid transparent;
  border-radius: 6px;
  background: transparent;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.corner-feedback:hover {
  color: #e1bee7;
  border-color: rgba(206, 147, 216, 0.45);
  background: rgba(206, 147, 216, 0.1);
}

/* The version badge, and the door to the changelog. Quieter again than the
   feedback button above it — it is a fact about the build first and a link
   second, and it should read as a footnote to the corner rather than a third
   thing competing in it. */
.corner-version {
  font-size: 0.7rem;
  color: #5f7182;
  text-decoration: none;
  padding: 0 0.5rem;
  letter-spacing: 0.02em;
  border-bottom: 1px solid transparent;
}
.corner-version:hover { color: #ce93d8; border-bottom-color: rgba(206, 147, 216, 0.4); }

/* --- the changelog page ------------------------------------------------ */

#changelog-screen {
  max-width: 640px;
  margin: 0 auto;
  padding: 1rem 1rem 3rem;
}
.cl-title {
  font-size: 1.6rem;
  color: #eceff1;
  margin-bottom: 0.4rem;
}
.cl-sub {
  color: #90a4ae;
  font-size: 0.92rem;
  line-height: 1.5;
  margin-bottom: 2rem;
}
.cl-release {
  border-top: 1px solid #2f2f4a;
  padding-top: 1.2rem;
  margin-bottom: 1.8rem;
}
/* Version and date on one line, pushed apart. The eye scans the left edge for
   the release it wants and never has to read the date to do it. */
.cl-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 0.7rem;
}
.cl-version { font-size: 1.15rem; color: #ce93d8; font-weight: 600; }
.cl-date { font-size: 0.8rem; color: #6d7d88; white-space: nowrap; }

/* "New" and "Fixed" as labels rather than headings — small, coloured, and the
   only thing distinguishing the two lists, so they carry the colour. */
.cl-kind {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin: 0.9rem 0 0.4rem;
  font-weight: 600;
}
.cl-kind.cl-new { color: #80cbc4; }
.cl-kind.cl-fixed { color: #ffb74d; }

.cl-list {
  list-style: none;
  padding: 0;
  margin: 0;
}
.cl-list li {
  position: relative;
  padding-left: 1.1rem;
  margin-bottom: 0.45rem;
  color: #cfd8dc;
  font-size: 0.92rem;
  line-height: 1.55;
}
.cl-list li::before {
  content: '—';
  position: absolute;
  left: 0;
  color: #546e7a;
}

/* A release's aside, under its bullets. Sits inside .cl-release rather than at
   the foot of the page, so it travels down with its own entry as newer releases
   are added above it. */
.cl-egg {
  color: #5f7182;
  font-size: 0.82rem;
  font-style: italic;
  line-height: 1.5;
  margin: 1rem 0 0;
}
.cl-home { display: block; margin: 1.8rem auto 0; }


/* The home screen's own top row keeps the brand and nothing else now, so it no
   longer needs to reserve the right-hand side for the corner. */
#home-top { padding-right: 9rem; }

@media (max-width: 560px) {
  #site-corner { top: 0.5rem; right: 0.5rem; }
  #home-top { padding-right: 4.5rem; }
  /* On a narrow screen the name and rating give way to the avatar alone —
     there isn't room for both beside the brand. */
  #site-corner .auth-name { display: none; }
}
.auth-login,
.auth-name {
  font-family: inherit;
  font-size: 0.9rem;
  color: #e0e0e0;
  background: transparent;
  border: 1px solid #546e7a;
  border-radius: 6px;
  padding: 0.4rem 0.9rem;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.auth-login:hover,
.auth-name:hover { background: #546e7a; color: #fff; }
.auth-name {
  border-color: #ab47bc;
  display: flex;
  flex-direction: column;   /* name over rating */
  align-items: flex-end;
  gap: 0.1rem;
  line-height: 1.15;
  padding: 0.32rem 0.9rem;
  text-align: right;
}
.auth-who { font-size: 0.9rem; }
.auth-rating {
  font-size: 0.72rem;
  color: #ce93d8;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.03em;
}
/* A provisional rating (5–14 rated games) is shown, but not dressed up as a
   real number. Under five there is no number to dress: `.unrated` is the
   countdown that stands in its place, quieter still because it is an
   instruction rather than a figure. */
.auth-rating.provisional { color: #90a4ae; }
.auth-rating.unrated { color: #78909c; font-style: italic; }
.auth-name:hover .auth-rating { color: #f3e5f5; }
.auth-avatar {
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  border: none;
  cursor: pointer;
  font-family: inherit;
  font-weight: 700;
  color: #fff;
  background: linear-gradient(150deg, #ab47bc, #7e3f9e);
  transition: transform 0.15s;
}
.auth-avatar:hover { transform: scale(1.06); }

/* primary matchmaking buttons */
#home-actions {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  max-width: 460px;
  margin: 0 auto;
}
.home-big {
  font-family: inherit;
  font-size: 1.15rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: #e0e0e0;
  padding: 1.1rem 1rem;
  border-radius: 10px;
  border: 1px solid #3a3a5a;
  background: linear-gradient(160deg, #29293f, #20203a);
  cursor: pointer;
  transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s;
}
.home-big:hover {
  transform: translateY(-2px);
  border-color: #ab47bc;
  box-shadow: 0 6px 20px rgba(171, 71, 188, 0.25);
}
.home-big.primary {
  color: #fff;
  border-color: #ab47bc;
  background: linear-gradient(160deg, #7e3f9e, #5e2f78);
}

/* The saved game, offered above the Find buttons. Warm rather than purple: it
   is the one thing on this screen with a clock already running on it, and it
   should not read as just another way to start something. */
#home-resume {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.5rem;
  max-width: 460px;
  margin: 0 auto 1.4rem;
  padding: 1rem;
  border: 1px solid #6d4c41;
  border-radius: 12px;
  background: linear-gradient(160deg, #2e2620, #241f1c);
}
#home-resume .home-big.resume {
  border-color: #ffb74d;
  background: linear-gradient(160deg, #8a5a1e, #6b4416);
}
#home-resume .home-big.resume:hover {
  border-color: #ffcc80;
  box-shadow: 0 6px 20px rgba(255, 183, 77, 0.22);
}
.resume-note {
  margin: 0;
  color: #bcaaa4;
  font-size: 0.82rem;
  letter-spacing: 0.02em;
  text-align: center;
}
.home-small.discard {
  align-self: center;
  font-size: 0.85rem;
  padding: 0.45rem 1.1rem;
  color: #a1887f;
  border-color: #5d4037;
}
.home-small.discard:hover { background: #5d4037; color: #fff; }

/* matchmaking options */
.mm-note {
  margin: 1.1rem auto 0.4rem;
  color: #90a4ae;
  font-size: 0.82rem;
  letter-spacing: 0.03em;
}
.mm-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: #b0bec5;
  font-size: 0.85rem;
  cursor: pointer;
}
.mm-toggle input { accent-color: #ab47bc; width: 1rem; height: 1rem; cursor: pointer; }

/* How long a search holds out for a human before a bot is offered (issue #96).
   Sits under the toggle it depends on and matches its weight — the same dimmed
   caption size, because it is a setting and not a headline.

   A ROW OF ITS OWN, which is why this is `flex` and the toggle above it is
   `inline-flex`. The home screen stacks these two by dropping them into a
   centred block, so an inline-flex slider simply joined the checkbox on its
   line whenever the window was wide enough to fit both — under it on a phone
   and beside it on a laptop, from one rule. Block-level is what makes "under"
   the answer at every width.

   It is on screen only while that toggle is ticked, which it is by default.
   js/home.js hides it with the `hidden` attribute, and the display here is why
   that needs the global `[hidden] { display: none !important }` rule to bite. */
.mm-patience {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.6rem;
  margin-top: 0.35rem;
  color: #b0bec5;
  font-size: 0.85rem;
  cursor: pointer;
}
.mm-patience input[type="range"] {
  width: 120px;
  accent-color: #ab47bc;
  cursor: pointer;
}
/* Fixed width so the row does not jump a pixel sideways every time the handle
   crosses a step — the number is the only thing in it that changes length. */
.mm-patience-label { min-width: 15.5em; text-align: right; }

/* secondary row */
#home-secondary {
  display: flex;
  justify-content: center;
  gap: 1rem;
  margin-top: 2rem;
}
.home-small {
  font-family: inherit;
  font-size: 0.95rem;
  color: #cfd8dc;
  background: transparent;
  border: 1px solid #546e7a;
  border-radius: 8px;
  padding: 0.7rem 1.4rem;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.home-small:hover { background: #546e7a; color: #fff; }

.setup-back {
  position: absolute;
  left: 1rem;
  top: 1rem;
  font-family: inherit;
  font-size: 0.85rem;
  color: #90a4ae;
  background: transparent;
  border: 1px solid #546e7a;
  border-radius: 6px;
  padding: 0.35rem 0.8rem;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.setup-back:hover { background: #546e7a; color: #fff; }

/* THE WAY HOME. It wears the positioning class of the button it replaced
   (.setup-back here, .tut-home in the tutorial), so it lands exactly where the
   "← Home" button used to sit — but it is a brand, not a button, so it sheds the
   border and the padded box those classes bring with them. */
.home-link {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  text-decoration: none;
  color: #e0e0e0;
  font-size: 1.02rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  line-height: 1;
  border: none;
  background: transparent;
  padding: 0.2rem 0.1rem;
  opacity: 0.9;
  transition: opacity 0.15s, color 0.15s;
}
.home-link:hover { opacity: 1; color: #ce93d8; background: transparent; }
.home-link-mark { width: 1.7rem; height: 1.7rem; flex: 0 0 auto; object-fit: contain; }
.home-link-text { white-space: nowrap; }

/* On a narrow screen the mark carries it alone, as the corner drops the account
   name at the same width. */
@media (max-width: 560px) {
  .home-link-text { display: none; }
  .home-link-mark { width: 1.5rem; height: 1.5rem; }
}

/* Two escapes where one absolute corner button used to sit: the stack takes the
   corner, and what is inside it sits in flow.

   STACKED, NOT SIDE BY SIDE. The first row is the brand, and the brand has to
   land in the same place on this page as on every other one — a way home that
   moves depending on which screen you are looking at is most of the way back to
   the five different Home buttons it replaced. Back therefore goes underneath
   rather than beside, which also stops a short button being stretched to the
   brand's height by the row. */
.setup-nav {
  position: absolute;
  left: 1rem;
  top: 1rem;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.4rem;
}
.setup-nav .setup-back { position: static; left: auto; top: auto; }

/* toast */
.home-toast {
  position: fixed;
  left: 50%;
  bottom: 2rem;
  transform: translate(-50%, 1rem);
  background: #263238;
  color: #eceff1;
  border: 1px solid #455a64;
  border-radius: 8px;
  padding: 0.7rem 1.2rem;
  font-size: 0.9rem;
  opacity: 0;
  transition: opacity 0.28s ease, transform 0.28s ease;
  z-index: 1000;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}
.home-toast.show { opacity: 1; transform: translate(-50%, 0); }

/* ================= Auth modals ================= */

.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.65);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  z-index: 1100;
  animation: setup-fade 0.2s ease;
}
.modal-card {
  position: relative;
  background: linear-gradient(160deg, #29293f, #20203a);
  border: 1px solid #3a3a5a;
  border-radius: 12px;
  padding: 1.8rem 1.6rem;
  width: min(400px, 100%);
  text-align: center;
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
}
.modal-x {
  position: absolute;
  top: 10px;
  right: 12px;
  background: none;
  border: none;
  color: #90a4ae;
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  padding: 4px;
  transition: color 0.15s;
}
.modal-x:hover { color: #f0f0f0; }
.modal-title {
  color: #f0f0f0;
  font-size: 1.4rem;
  margin-bottom: 0.5rem;
}
.modal-sub {
  color: #b0bec5;
  font-size: 0.9rem;
  line-height: 1.5;
  margin-bottom: 1.2rem;
}
.modal-input {
  width: 100%;
  box-sizing: border-box;
  font-family: inherit;
  font-size: 1rem;
  padding: 0.7rem 0.9rem;
  border-radius: 8px;
  border: 1px solid #546e7a;
  background: #1a1a2e;
  color: #f0f0f0;
  margin-bottom: 0.4rem;
}
.modal-input:focus { outline: none; border-color: #ab47bc; }
/* The five-digit login code. Big, centred and spaced out because it is copied by
   eye from another app — the spacing is what keeps a digit from being read into
   its neighbour, and monospace is what stops 1 and 7 arguing about their width. */
.modal-input.code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 1.6rem;
  text-align: center;
  letter-spacing: 0.4em;
  text-indent: 0.4em; /* letter-spacing pads the right of the last digit; undo the lean */
  padding: 0.6rem 0.5rem;
}
/* The game-log box. Monospace because a game log IS monospace — its two columns
   only line up in one, and someone checking what they pasted should see it laid
   out the way it was written. Vertical resize only: widening it past the card
   would break the modal rather than help. */
.modal-textarea {
  font-family: 'Courier New', monospace;
  font-size: 0.85rem;
  line-height: 1.45;
  resize: vertical;
  min-height: 9rem;
  white-space: pre;
  overflow-wrap: normal;
  overflow-x: auto;
}
.modal-error {
  color: #ef9a9a;
  font-size: 0.82rem;
  min-height: 1.1rem;
  margin-bottom: 0.6rem;
}
.modal-btn {
  display: block;
  width: 100%;
  font-family: inherit;
  font-size: 0.95rem;
  padding: 0.7rem 1rem;
  border-radius: 8px;
  border: 1px solid #546e7a;
  background: transparent;
  color: #e0e0e0;
  cursor: pointer;
  margin-top: 0.55rem;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.modal-btn:hover { background: #546e7a; color: #fff; }
.modal-btn.primary {
  border-color: #ab47bc;
  background: linear-gradient(160deg, #7e3f9e, #5e2f78);
  color: #fff;
}
.modal-btn.primary:hover { background: linear-gradient(160deg, #9548bb, #6d3789); }
.modal-btn.primary:disabled { opacity: 0.6; cursor: default; }
.modal-btn.ghost { border-color: transparent; color: #90a4ae; }
.modal-btn.ghost:hover { background: transparent; color: #cfd8dc; }
.modal-btn.dev {
  margin-top: 1rem;
  border-style: dashed;
  border-color: #546e7a;
  color: #80cbc4;
  font-size: 0.85rem;
}
.modal-btn.dev:hover { background: rgba(128, 203, 196, 0.12); color: #a7ffeb; }

/* Sign in with Google. An <a>, not a <button> — the login is a full-page trip
   to accounts.google.com and back — so it restates the bits .modal-btn gets
   from the browser's button defaults: centring, and no underline.
   Colours follow Google's dark button variant so the control looks like the
   one people have been taught to look for. */
.modal-btn.google {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  text-decoration: none;
  box-sizing: border-box;
  border-color: #8e918f;
  background: #131314;
  color: #e3e3e3;
  font-weight: 500;
}
.modal-btn.google:hover { background: #1e1f20; color: #fff; border-color: #a8abaa; }
.modal-btn.google svg { flex: 0 0 auto; }

/* "or" between the two ways in, with a rule running out either side. */
.modal-or {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  margin: 0.9rem 0 0.2rem;
  color: #78909c;
  font-size: 0.8rem;
}
.modal-or::before,
.modal-or::after {
  content: '';
  flex: 1 1 auto;
  height: 1px;
  background: #37474f;
}

.mm-searching { display: flex; justify-content: center; padding: 8px 0 14px; }

/* live search feedback: elapsed timer + the current rating window */
.mm-timer {
  font-variant-numeric: tabular-nums;
  font-size: 1.6rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: #e6e6f0;
  text-align: center;
  margin: 2px 0 4px;
}
.mm-range {
  font-size: 0.85rem;
  color: #9a9ac0;
  text-align: center;
  min-height: 1.1em;
  margin: 0 0 12px;
}
.mm-range:empty { margin: 0; }

/* post-game rating card (inside the game-over dialog) */
#game-over-extra:empty { display: none; }
.rr-rating {
  margin: 0.7rem auto 0.2rem;
  max-width: 320px;
  border-top: 1px solid #3a3a5a;
  padding-top: 0.6rem;
}
.rr-line { display: flex; justify-content: space-between; gap: 1rem; padding: 0.3rem 0; font-size: 0.95rem; }
.rr-name { color: #b0bec5; }
.rr-nums { color: #eceff1; }
.rr-nums b { color: #fff; }
.rr-delta { font-weight: 700; margin-left: 5px; }
.rr-delta.up { color: #66bb6a; }
.rr-delta.down { color: #ef5350; }
.rr-delta.even { color: #90a4ae; }
.rr-prov { margin-top: 0.6rem; color: #ab47bc; font-size: 0.85rem; }
.rr-prov b { color: #ce93d8; }

/* The game-over dialog's one-line note when there are no figures to show:
   an unrated game, a report still in flight, or one that failed. */
.rr-note { margin: 0.6rem 0 0; font-size: 0.9rem; color: #b0bec5; }
.rr-note.pending { font-style: italic; }

/* ---- a badge landing in the game-over dialog ---- */

.bg-earned { position: relative; margin-top: 0.9rem; padding-top: 0.85rem; border-top: 1px solid #3a3a5a; }
.bg-earned-title {
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  color: #ce93d8;
  margin-bottom: 0.65rem;
}
.bg-earned-row { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.7rem; }
.bg-earned .bg-slot { cursor: default; }

/* One turn of the coin. `both` holds the end state, which is a full 360° — i.e.
   face-on — so the badge is left exactly as it would have been drawn. */
@keyframes bg-flip {
  from { transform: rotateY(0deg); }
  to   { transform: rotateY(360deg); }
}
.bg-flip { animation: bg-flip 0.75s cubic-bezier(0.35, 0, 0.2, 1) 1 both; }

/* Confetti falls past the dialog's buttons rather than being boxed into the
   badge row — hence the fixed band and pointer-events: none, so nothing it
   drifts over stops being clickable. */
.bg-confetti {
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 250px;
  overflow: hidden;
  pointer-events: none;
}
.bg-confetti-bit {
  position: absolute;
  top: -14px;
  width: 7px;
  height: 11px;
  border-radius: 1px;
  animation: bg-confetti 1.6s ease-in 1 forwards;
}
@keyframes bg-confetti {
  0%   { opacity: 1; transform: translate(0, 0) rotate(0deg); }
  100% { opacity: 0; transform: translate(var(--drift, 0), 250px) rotate(var(--spin, 360deg)); }
}

/* Asked to keep still: the badge simply appears, and the confetti doesn't. */
@media (prefers-reduced-motion: reduce) {
  .bg-flip { animation: none; }
  .bg-confetti { display: none; }
}

/* Rating change pinned in the move-log panel, so it outlives the game-over
   dialog. Collapses entirely until there's something to say. */
.mh-rating:empty { display: none; }
.mh-rating {
  padding: 0.45rem 0.55rem;
  border-top: 1px solid #2f2f4a;   /* it sits under the moves now, not above */
  background: #1b1b31;
  font-size: 0.85rem;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.4rem;
}
.mh-rating-label { color: #90a4ae; text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.7rem; }
.mh-rating-nums { color: #eceff1; font-variant-numeric: tabular-nums; }
.mh-rating-nums b { color: #fff; }
.mh-rating.unavailable { color: #90a4ae; font-style: italic; }
/* The two states a finished game starts in, before (or instead of) figures. */
.mh-rating.unrated { color: #90a4ae; }
.mh-rating.pending { color: #b0bec5; font-style: italic; }

/* leaderboard screen */
#leaderboard-screen { max-width: 960px; margin: 2.5rem auto 3rem; padding: 1rem 1.2rem 3rem; animation: setup-fade 0.4s ease; }
.lb-title { font-size: clamp(1.8rem, 5vw, 2.6rem); color: #f0f0f0; letter-spacing: 0.04em; margin: 0 0 1.2rem; text-align: center; }
.lb-status { text-align: center; color: #90a4ae; }
.lb-card { background: linear-gradient(160deg, #29293f, #20203a); border: 1px solid #3a3a5a; border-radius: 14px; padding: 16px 18px; margin-bottom: 20px; }
.lb-card h2 { font-size: 15px; margin: 0 0 4px; color: #f0f0f0; }
.lb-cap { font-size: 12.5px; color: #b0bec5; margin: 0 0 12px; }
.lb-empty { color: #90a4ae; font-size: 13px; }
.lb-table { width: 100%; border-collapse: collapse; font-size: 13.5px; font-variant-numeric: tabular-nums; }
.lb-table th, .lb-table td { text-align: left; padding: 9px 12px; white-space: nowrap; }
.lb-table thead th { border-bottom: 2px solid #546e7a; font-size: 11px; letter-spacing: 0.07em; text-transform: uppercase; color: #90a4ae; font-weight: 600; }
.lb-table tbody td { color: #eceff1; border-bottom: 1px solid #33334d; }
.lb-table tbody tr:nth-child(even) td { background: rgba(130, 130, 130, 0.06); }
/* You, on the board. After the striping rule on purpose — same specificity, so
   source order is what keeps the tint from being eaten on even rows. */
.lb-table tbody tr.lb-me td { background: rgba(171, 71, 188, 0.18); color: #fff; }
.lb-table tbody tr.lb-me td:first-child { box-shadow: inset 3px 0 0 #ab47bc; }
.lb-you { font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; color: #ab47bc; border: 1px solid #ab47bc; border-radius: 4px; padding: 1px 5px; margin-left: 6px; }
.lb-table .num { text-align: right; }
.lb-table .muted { color: #90a4ae; }
.lb-rank { color: #90a4ae; width: 30px; }
.lb-tag { font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; color: #ce93d8; border: 1px solid #7e3f9e; border-radius: 4px; padding: 1px 5px; }
/* A CARD'S FILTERS — one strip in the standings, one in the chart, each
   governing the card it is in. Distinct from .lb-controls, which is the chart's
   own row of view controls and sits below the caption rather than above it.
   Directly under each heading: the strip used to float between the cards, where
   a bordered row belonging to neither of them reads as a stray control the
   moment it is not at the top of the page. So the top margin clears the h2
   rather than a card, and the rule cannot assume it is first in the document. */
.lb-filters {
  display: flex; flex-wrap: wrap; align-items: center; gap: 20px;
  margin: 10px 0 14px; padding: 0 2px 12px; border-bottom: 1px solid #33334d;
}

/* A provisional rating ranked beside settled ones. Muted and `?`-suffixed, the
   same treatment the profile card gives it: the point is that it must not read
   as the same kind of claim as the number above it. */
.lb-prov { color: #b0bec5; font-weight: 600; }

/* Dark Horses: no rating column at all, so the card is narrow and the heading
   carries the explanation. The parenthetical is part of the heading rather than
   a caption — "Dark Horses" alone is a nice phrase and a poor label. */
.lb-dh-sub { color: #90a4ae; font-weight: 400; font-size: 12px; margin-left: 6px; }
.lb-dh-table { max-width: 420px; }

/* "unrated", where a rating change would otherwise be — the game-over dialog and
   the move-log strip. Italic and quiet: it is the absence of a figure, and must
   not sit in the column looking like one. */
.rr-none { color: #78909c; font-style: italic; font-variant-numeric: normal; }
.lb-chart-box { overflow-x: auto; }
.lb-chart { display: block; width: 100%; height: auto; overflow: visible; }
.lb-gl { stroke: #33334d; stroke-width: 1; }
.lb-ax { fill: #6f7d86; font-size: 11px; }
.lb-line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
/* Today's carried-over rating: hollow, so it doesn't read as a game played. */
.lb-today { fill: none; stroke-width: 2; }
.lb-end { font-size: 12px; font-weight: 600; }
/* Leader from a dot to a name that had to move clear of its neighbour. Thin and
   half-lit: it is a pointer, and must not read as another rating line. */
.lb-lead { stroke-width: 1; opacity: 0.55; }
/* Your own series: same colour, more weight — found at a glance, still comparable. */
.lb-line.lb-me { stroke-width: 3.5; }
.lb-today.lb-me { stroke-width: 2.5; }
.lb-end.lb-me { font-size: 13px; font-weight: 800; }
.lb-note { margin: 8px 0 0; }
.lb-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 18px; margin: 2px 0 12px; }
.lb-seg-group { display: inline-flex; gap: 14px; }
.lb-seg, .lb-ci-toggle { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; color: #b0bec5; cursor: pointer; }
.lb-seg input, .lb-ci-toggle input { accent-color: #ab47bc; cursor: pointer; }
/* The window picker. Only one of the two is displayed — the other belongs to the
   mode you are not in, and JS toggles them. */
.lb-win { padding-left: 18px; border-left: 1px solid #33334d; }
.lb-lines { display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px; color: #b0bec5; cursor: pointer; }
/* Fixed width so the row does not jitter as the number changes under the drag. */
.lb-lines-n { min-width: 46px; }
.lb-lines input[type="range"] { width: 96px; accent-color: #ab47bc; cursor: pointer; }

/* ---- profile screen ---- */
/* Reuses the leaderboard's card shell (.lb-card) so the two screens read as one
   family; .pf-* is only what the account page adds. */

#profile-screen {
  max-width: 720px;
  margin: 2.5rem auto 3rem;
  padding: 1rem 1.2rem 3rem;
  animation: setup-fade 0.4s ease;
}

.pf-head { display: flex; align-items: center; gap: 1rem; margin: 0.5rem 0 1.6rem; }
.pf-avatar {
  width: 3.6rem;
  height: 3.6rem;
  flex: none;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.7rem;
  font-weight: 700;
  color: #fff;
  background: linear-gradient(150deg, #ab47bc, #7e3f9e);
}
.pf-who { min-width: 0; }
.pf-name {
  font-size: clamp(1.5rem, 4vw, 2.1rem);
  color: #f0f0f0;
  letter-spacing: 0.02em;
  margin: 0;
  overflow-wrap: anywhere;
}
.pf-email { color: #90a4ae; font-size: 0.85rem; margin: 0.15rem 0 0; overflow-wrap: anywhere; }
.pf-inline-btn {
  margin-top: 0.4rem;
  padding: 0;
  font-family: inherit;
  font-size: 0.85rem;
  color: #ce93d8;
  background: none;
  border: none;
  cursor: pointer;
}
.pf-inline-btn:hover { color: #f3e5f5; text-decoration: underline; }

.pf-card { text-align: left; }

/* Rating: the number carries the block, the counts sit beside it. */
.pf-rating-row { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.6rem 1.4rem; margin: 0.5rem 0 0.7rem; }
.pf-rating { font-size: 2.4rem; font-weight: 700; color: #eceff1; font-variant-numeric: tabular-nums; line-height: 1; }
.pf-rating.provisional { color: #b0bec5; }
/* The em dash where a rating will be. Dimmer than provisional and lighter in
   weight, so a card with no number does not shout a placeholder. */
.pf-rating.unrated { color: #78909c; font-weight: 400; }
.pf-stats { display: flex; flex-wrap: wrap; gap: 0 1.1rem; }
.pf-stat { display: flex; flex-direction: column; align-items: center; }
.pf-stat-v { font-size: 1rem; color: #eceff1; font-variant-numeric: tabular-nums; }
.pf-stat-k { font-size: 10px; letter-spacing: 0.07em; text-transform: uppercase; color: #90a4ae; }

/* ---- badges ---- */

/* Circles, earned and unearned side by side. The unearned ones keep their ring
   at full contrast and dim only the glyph inside: greying the whole disc makes
   a new player's profile look broken rather than unfinished, and the lit rings
   are what let the grid be read as a set of slots at a glance. */

.bg-count {
  float: right;
  font-size: 11px;
  letter-spacing: 0.07em;
  color: #90a4ae;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

.bg-group { margin-top: 0.9rem; }

.bg-group-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem;
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #90a4ae;
  margin-bottom: 0.45rem;
}

.bg-lock { color: #7e6a86; text-transform: none; letter-spacing: 0.02em; font-size: 11px; }

/* Names sit under their circles, so the cells are a fixed width and the rows
   align at the top — a two-line name must not shove its neighbours down. */
.bg-grid { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 0.6rem 0.35rem; }

/* The slot is the button: circle, plus the name once it's earned. */
.bg-slot {
  width: 4.4rem;
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.32rem;
  padding: 0;
  border: 0;
  border-radius: 10px;
  background: none;
  font-family: inherit;
  cursor: pointer;
}

.bg-badge {
  width: 3.1rem;
  height: 3.1rem;
  flex: none;
  position: relative;   /* anchors .bg-pick, the picker's position marker */
  border-radius: 50%;
  border: 2px solid #4a4a6a;
  background: radial-gradient(circle at 50% 35%, #32324e, #24243c);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.12s, border-color 0.15s, box-shadow 0.15s;
}

.bg-slot:hover .bg-badge { transform: translateY(-2px); }

/* Earned: the ring lights up and the glyph comes to full strength. */
.bg-slot.earned .bg-badge {
  border-color: #ab47bc;
  background: radial-gradient(circle at 50% 35%, #462f52, #2b2340);
  box-shadow: 0 0 0 1px rgba(171, 71, 188, 0.25), 0 2px 8px rgba(171, 71, 188, 0.18);
}

/* Gold: the rare ones. A different colour rather than a bigger circle, so a
   grid of badges stays a grid — the eye finds it without the layout moving. The
   glow is stronger than the purple one because the whole job of this ring is to
   be spotted from across a page somebody is scrolling past. */
.bg-slot.gold.earned .bg-badge {
  border-color: #ffc94d;
  background: radial-gradient(circle at 50% 35%, #5a4318, #33280f);
  box-shadow: 0 0 0 1px rgba(255, 201, 77, 0.3), 0 2px 14px rgba(255, 201, 77, 0.3);
}
.bg-slot.gold.earned .bg-glyph { color: #ffd77a; }
.bg-slot.gold.earned .bg-name { color: #ffd77a; }

/* Nothing for `.gold` on its own, on purpose. A locked gold badge wears exactly
   what every other locked badge wears — the muted gold ring that used to sit
   here marked the rare ones out before they were won, which turns the grid into
   a list of what you are missing. Gold is the reward, not the advertisement. */

/* GLASS — the Unique group. Not earned by playing well, so it does not wear a
   metal. Every other badge on this page is a painted disc; this one is a lens
   with the page visible through it.

   THE TRAP THIS IS WRITTEN AROUND. An unearned badge is drawn faint, and "pale
   and washed out" is exactly what a careless glass ring would be — so the two
   would be the same picture, and the rarest badge on the board would read as one
   the player has not got. Four things separate them, and none is decoration:

     * it is TRANSPARENT, so whatever card it sits on shows through, and it
       changes with the surface. A dimmed badge is flat and always the same;
     * a SPECULAR catch at upper left, which is the single cue that turns a
       circle into a sphere;
     * light BOUNCING back up off what it rests on, low and cool;
     * a lit rim. Nothing else here is lit from inside.

   Sitting after the gold rules, since glass is the tone a badge carries INSTEAD
   of gold (toneOf in js/badges.js returns one name, never two). */
.bg-slot.earned.glass .bg-badge,
.bg-badge.bg-mini.earned.glass,
.pf-show-slot.filled.glass .bg-badge,
.seat-badge.glass {
  border-color: rgba(236, 246, 255, 0.62);
  background:
    /* the catch of light — soft, high, and off to one side */
    radial-gradient(50% 36% at 30% 20%, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0) 72%),
    /* the kicker: a small hard secondary, lower and to the other side. A real
       sphere has two, and having two is most of what says "glass" rather than
       "pale" — it is also what replaced the glint that used to run here. */
    radial-gradient(16% 12% at 68% 70%, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 76%),
    /* the streak a raking light leaves across the middle */
    linear-gradient(107deg, rgba(255, 255, 255, 0) 32%, rgba(255, 255, 255, 0.3) 43%,
                    rgba(255, 255, 255, 0.07) 52%, rgba(255, 255, 255, 0) 62%),
    /* bounce off whatever it is resting on */
    radial-gradient(62% 32% at 54% 95%, rgba(184, 222, 255, 0.34), rgba(184, 222, 255, 0) 72%),
    /* and the body of the glass itself, which is barely there on purpose */
    linear-gradient(160deg, rgba(217, 239, 255, 0.15), rgba(138, 172, 210, 0.07));
  /* Real glass distorts what is behind it. Unsupported browsers simply get the
     clear version, which is still transparent and still reads correctly. */
  -webkit-backdrop-filter: blur(2.5px) saturate(1.2);
  backdrop-filter: blur(2.5px) saturate(1.2);
  box-shadow:
    inset 0 1.5px 4px rgba(255, 255, 255, 0.5),
    inset 0 -4px 9px rgba(160, 202, 244, 0.2),
    0 0 0 1px rgba(233, 244, 255, 0.2),
    0 2px 16px rgba(206, 232, 255, 0.24);
}
.bg-slot.earned.glass .bg-glyph,
.bg-slot.earned.glass .bg-name,
.bg-badge.bg-mini.earned.glass .bg-glyph,
.pf-show-slot.filled.glass .bg-glyph { color: #f2f8ff; }

/* ── the hover shine ─────────────────────────────────────────────────────────
 *
 * A pass of light across the disc when you point at it, on every badge that has
 * a ring worth catching light: glass, gold, silver, bronze. It rides with the
 * bob those two surfaces already do on hover.
 *
 * ON HOVER, AND ONLY ON HOVER. There was an ambient version — a glint every
 * seven seconds — and it came out again, which is worth writing down so it does
 * not come back by accident. A badge is worn where the wearer is NOT the
 * subject: beside a name on a clock while the opponent is trying to think, and
 * in the game-over dialog with the board still on screen. Nobody can dismiss a
 * decoration on somebody else's seat, so a badge that moves by itself asks to be
 * looked at every few seconds for the length of a game. Hover is the opposite:
 * it is the viewer's own deliberate act, on their own screen, and it never
 * happens unasked. That is the whole difference, and it is why this is fine and
 * the ambient one was not.
 *
 * Phones have no hover, so a good share of players will never see this. That is
 * accepted: the resting rings are the design, and this is a flourish for people
 * who go looking.
 *
 * A METAL REFLECTS ITS OWN COLOUR. A white streak across gold reads as glass
 * rather than as gold, so the colour is per tone. `--shine` is set on the disc
 * and inherits into the ::after, which is what keeps ONE layer rule instead of
 * one per tone per surface — and `--shine-peak` defaults to 0, so an ordinary
 * purple badge gets a layer that is transparent at every stop. Only a ring with
 * something to catch shines, which is also what stops the effect from meaning
 * nothing by being everywhere.
 *
 * A NEW RANK NEEDS A LINE HERE, beside the one it needs in the grade block
 * above. Forgetting it is the mild failure: the badge simply does not shine. */
.bg-slot.earned.glass        .bg-badge,
.pf-show-slot.filled.glass   .bg-badge { --shine: 255, 255, 255; --shine-peak: 0.6; }
.bg-slot.earned.gold         .bg-badge,
.pf-show-slot.filled.gold    .bg-badge,
.bg-slot.earned.grade-gold   .bg-badge,
.pf-show-slot.grade-gold     .bg-badge { --shine: 255, 233, 170; --shine-peak: 0.5;  --polish: 0.3; }
.bg-slot.earned.grade-silver .bg-badge,
.pf-show-slot.grade-silver   .bg-badge { --shine: 231, 244, 255; --shine-peak: 0.52; --polish: 0.34; }
.bg-slot.earned.grade-bronze .bg-badge,
.pf-show-slot.grade-bronze   .bg-badge { --shine: 255, 199, 138; --shine-peak: 0.48; --polish: 0.26; }

/* The resting polish, and the reason the metals need one at all: glass carries
   its highlights in its own background stack, so beside it a metal ring was a
   flat disc that only came alive when pointed at. This is what a struck metal
   looks like standing still — a catch high and to one side, and a softer bounce
   off the surface underneath, both in the metal's OWN colour.
 *
 * Weaker than glass on purpose. Metal is opaque: it reflects, it does not
 * transmit, so the same strength that reads as a lens here reads as a smudge.
 *
 * `--polish` defaults to 0, so glass (whose highlights are already in its
 * background, and would otherwise be drawn twice) and every plain purple badge
 * get a layer that is transparent at every stop. Behind the glyph, since it is
 * ::before — a shine on a badge is under the thing the badge is a picture of. */
.bg-slot.earned .bg-badge::before,
.pf-show-slot.filled .bg-badge::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  pointer-events: none;
  background:
    radial-gradient(52% 38% at 32% 22%,
      rgba(var(--shine, 255, 255, 255), var(--polish, 0)),
      rgba(var(--shine, 255, 255, 255), 0) 72%),
    radial-gradient(58% 26% at 52% 96%,
      rgba(var(--shine, 255, 255, 255), calc(var(--polish, 0) * 0.5)),
      rgba(var(--shine, 255, 255, 255), 0) 74%);
}

/* The layer. Parked beyond the right edge, so it rests invisible and ENDS
   invisible — a one-shot animation snaps back to its resting state when it
   finishes, and with nothing to see at either end there is nothing to snap.
   Flicking the pointer away mid-sweep cancels it, which is why it is short.

   Clipped by its own border-radius rather than by an overflow rule on the disc:
   .bg-picks hangs outside the circle, and `overflow: hidden` would cut the
   showcase position markers off. */
.bg-slot.earned .bg-badge::after,
.pf-show-slot.filled .bg-badge::after {
  content: '';
  position: absolute;
  inset: -1px;
  border-radius: 50%;
  pointer-events: none;
  background: linear-gradient(102deg,
    rgba(var(--shine, 255, 255, 255), 0) 40%,
    rgba(var(--shine, 255, 255, 255), var(--shine-peak, 0)) 47%,
    rgba(var(--shine, 255, 255, 255), calc(var(--shine-peak, 0) * 0.26)) 52%,
    rgba(var(--shine, 255, 255, 255), 0) 59%);
  background-size: 250% 100%;
  background-repeat: no-repeat;
  background-position: 150% 0;
}

@media (prefers-reduced-motion: no-preference) {
  .bg-slot.earned:hover .bg-badge::after,
  .bg-slot.earned:focus-visible .bg-badge::after,
  .pf-show-slot.filled:hover .bg-badge::after,
  .pf-show-slot.filled:focus-visible .bg-badge::after {
    animation: bg-shine-sweep 700ms ease-out;
  }
}

@keyframes bg-shine-sweep {
  from { background-position: 150% 0; }
  to   { background-position: -60% 0; }
}

/* GRADES — bronze at ten, silver at fifty, gold at a hundred. The rank rides on
   the ring rather than on a badge of its own, which is what keeps a graded badge
   one circle in the grid instead of four, and what makes three ranks of the same
   badge readable side by side in a showcase.

   A FOURTH RANK NEEDS A BLOCK HERE. This is the one place the rank names are
   written out, and it has to be: each needs its own colour, which no amount of
   deriving will invent. Everything else — awarding, drawing, naming, proving —
   is built from GRADES in js/badges.js, so forgetting this file is the mild
   failure rather than the silent one: the badge simply draws with the ordinary
   earned ring instead of its own.

   Written after the purple rule so it wins on an earned slot, and BEFORE the
   gold-badge rules above stay untouched — a grade-gold and a rare-gold ring
   deliberately look the same. Gold means gold however it was reached, which is
   also why a gold grade satisfies The Gold Standard. */
.bg-slot.earned.grade-bronze .bg-badge {
  border-color: #c9803f;
  background: radial-gradient(circle at 50% 35%, #4d3421, #2e2317);
  box-shadow: 0 0 0 1px rgba(201, 128, 63, 0.3), 0 2px 10px rgba(201, 128, 63, 0.22);
}
.bg-slot.earned.grade-bronze .bg-glyph,
.bg-slot.earned.grade-bronze .bg-name { color: #e0a071; }

.bg-slot.earned.grade-silver .bg-badge {
  border-color: #c2ccd6;
  background: radial-gradient(circle at 50% 35%, #414a55, #272d35);
  box-shadow: 0 0 0 1px rgba(194, 204, 214, 0.3), 0 2px 12px rgba(194, 204, 214, 0.24);
}
.bg-slot.earned.grade-silver .bg-glyph,
.bg-slot.earned.grade-silver .bg-name { color: #dde4ea; }

.bg-slot.earned.grade-gold .bg-badge {
  border-color: #ffc94d;
  background: radial-gradient(circle at 50% 35%, #5a4318, #33280f);
  box-shadow: 0 0 0 1px rgba(255, 201, 77, 0.3), 0 2px 14px rgba(255, 201, 77, 0.3);
}
.bg-slot.earned.grade-gold .bg-glyph,
.bg-slot.earned.grade-gold .bg-name { color: #ffd77a; }

/* The progress bar, and it lives in the detail line rather than under every
   circle. Only one badge is open at a time, so it costs one row of the card
   instead of twenty — and the badges with nothing to count never draw an empty
   one, which is most of them. */
.bg-bar-wrap {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.35rem;
}
.bg-bar {
  flex: 1 1 auto;
  max-width: 14rem;
  height: 5px;
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
}
.bg-bar-fill {
  display: block;
  height: 100%;
  border-radius: 3px;
  background: #ab47bc;
  transition: width 0.25s ease;
}
.bg-bar.grade-bronze .bg-bar-fill { background: #c9803f; }
.bg-bar.grade-silver .bg-bar-fill { background: #c2ccd6; }
.bg-bar.grade-gold   .bg-bar-fill { background: #ffc94d; }
.bg-bar-note { font-size: 0.75rem; opacity: 0.75; white-space: nowrap; }

/* The rungs of a graded badge, each with the day it was reached. Under the bar,
   because it answers the question the bar raises: the bar says how far to the
   next rank, this says when the ones behind it happened.

   A row rather than a stacked list — three items of two words each, and stacking
   them would push the recent-games card down by three lines every time a badge is
   opened. The earned ones take their rank's colour; the rest stay dim and show
   their threshold, so the ladder still reads as a ladder. */
.bg-rungs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 1rem;
  margin-top: 0.4rem;
  font-size: 0.75rem;
}
.bg-rung {
  display: inline-flex;
  align-items: baseline;
  gap: 0.35rem;
  opacity: 0.5;                       /* not yet earned: present, but quiet */
}
.bg-rung.is-earned { opacity: 1; }
.bg-rung-name { font-weight: 600; letter-spacing: 0.02em; }
.bg-rung.grade-bronze .bg-rung-name { color: #c9803f; }
.bg-rung.grade-silver .bg-rung-name { color: #c2ccd6; }
.bg-rung.grade-gold   .bg-rung-name { color: #ffc94d; }
/* The badge's own rung, which has no metal — it is the purple the badge wears. */
.bg-rung.is-base.is-earned .bg-rung-name { color: #ce93d8; }

/* WHAT THE RUNG COST, kept beside it after it is reached. It used to be replaced
   by the date, so earning bronze was also the moment the grid stopped being able
   to say what bronze had wanted — and for a graded badge that number is nowhere
   else, since the criterion cannot name it without naming all four. */
.bg-rung-needs {
  color: #90a4ae;
  font-variant-numeric: tabular-nums;
}
.bg-rung-when { opacity: 0.8; white-space: nowrap; }
/* The separator belongs to the date, not to the markup: an unearned rung has no
   date and must not be left with a dangling dot. */
.bg-rung-when:not(:empty)::before { content: '· '; opacity: 0.65; }

/* The rank chooser (js/profile.js chooseRank): pressing a graded badge in the
   showcase picker opens an overlay of the ranks you hold, one row each.

   IT REPLACED A ROW OF CHIPS, and the reason is layout rather than taste. The
   chips were appended to the group's shared detail line at the foot of the grid,
   so pressing a badge near the top of a long group put the choice most of a
   screen below the press — the badge looked as if it had printed a description
   and equipped nothing, which is how #71 was reported. An overlay cannot land
   off-screen. */
.bg-choose-card { max-width: 22rem; }

.bg-choose { display: flex; flex-direction: column; gap: 0.4rem; margin: 0.6rem 0 0; }

.bg-choose-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  font: inherit;
  text-align: left;
  padding: 0.45rem 0.6rem;
  border-radius: 0.6rem;
  border: 1px solid rgba(255, 255, 255, 0.16);
  background: transparent;
  color: inherit;
  cursor: pointer;
}
.bg-choose-row:hover { border-color: rgba(255, 255, 255, 0.4); }

/* The circle is the badge as it will actually be worn, ring and all, so the
   choice is made on the thing itself rather than on the word for it. */
.bg-choose-row .bg-badge { width: 2.2rem; height: 2.2rem; flex: none; }

.bg-choose-text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.bg-choose-name { font-weight: 600; }
.bg-choose-when { font-size: 0.72rem; opacity: 0.7; }
.bg-choose-when:empty { display: none; }

/* The same blue position marker the grid circle wears, so the two agree about
   which rank is sitting in which slot. */
.bg-choose-pick {
  margin-left: auto;
  min-width: 1.1rem;
  height: 1.1rem;
  border-radius: 50%;
  background: #7cc4ff;
  color: #0f1216;
  font-size: 0.65rem;
  font-weight: 700;
  line-height: 1.1rem;
  text-align: center;
  flex: none;
}

.bg-choose-row.grade-bronze { border-color: #c9803f; }
.bg-choose-row.grade-silver { border-color: #c2ccd6; }
.bg-choose-row.grade-gold   { border-color: #ffc94d; }
.bg-choose-row.picked { background: rgba(171, 71, 188, 0.28); border-color: #ce93d8; }
.bg-choose-row.picked.grade-bronze { background: rgba(201, 128, 63, 0.28); }
.bg-choose-row.picked.grade-silver { background: rgba(194, 204, 214, 0.22); }
.bg-choose-row.picked.grade-gold   { background: rgba(255, 201, 77, 0.25); }

/* Only ever set when a press did nothing — the picker's own status line, which
   would normally say so, is behind this card. */
.bg-choose-note { margin: 0.5rem 0 0; }
.bg-choose-note:empty { display: none; }

/* The same three rings wherever a showcased badge is drawn small: the mini disc
   on somebody else's profile, the slot in your own showcase strip, and the disc
   on a clock. A rank has to look like itself on every screen for the same reason
   gold does — a colour that changes by page stops meaning anything. */
.bg-badge.bg-mini.earned.grade-bronze,
.pf-show-slot.grade-bronze .bg-badge,
.seat-badge.grade-bronze {
  border-color: #c9803f;
  background: radial-gradient(circle at 50% 35%, #4d3421, #2e2317);
  box-shadow: 0 0 0 1px rgba(201, 128, 63, 0.3);
}
.bg-badge.bg-mini.earned.grade-bronze .bg-glyph,
.pf-show-slot.grade-bronze .bg-glyph,
.seat-badge.grade-bronze .seat-glyph { color: #e0a071; }

.bg-badge.bg-mini.earned.grade-silver,
.pf-show-slot.grade-silver .bg-badge,
.seat-badge.grade-silver {
  border-color: #c2ccd6;
  background: radial-gradient(circle at 50% 35%, #414a55, #272d35);
  box-shadow: 0 0 0 1px rgba(194, 204, 214, 0.3);
}
.bg-badge.bg-mini.earned.grade-silver .bg-glyph,
.pf-show-slot.grade-silver .bg-glyph,
.seat-badge.grade-silver .seat-glyph { color: #dde4ea; }

.bg-badge.bg-mini.earned.grade-gold,
.pf-show-slot.grade-gold .bg-badge,
.seat-badge.grade-gold {
  border-color: #ffc94d;
  background: radial-gradient(circle at 50% 35%, #5a4318, #33280f);
  box-shadow: 0 0 0 1px rgba(255, 201, 77, 0.3);
}
.bg-badge.bg-mini.earned.grade-gold .bg-glyph,
.pf-show-slot.grade-gold .bg-glyph,
.seat-badge.grade-gold .seat-glyph { color: #ffd77a; }

.seat-badge.grade-bronze:focus-visible { outline-color: #c9803f; }
.seat-badge.grade-silver:focus-visible { outline-color: #c2ccd6; }
.seat-badge.grade-gold:focus-visible { outline-color: #ffc94d; }

.bg-slot.selected .bg-badge { border-color: #ce93d8; box-shadow: 0 0 0 2px rgba(206, 147, 216, 0.35); }
.bg-slot:focus-visible { outline: 2px solid #ce93d8; outline-offset: 3px; }

/* Selected AND gold: the ring goes gold and the glow deepens, rather than
   flipping to purple. Selection has to read as "this one" without overwriting
   the tier — a gold badge that turns purple the moment you press it looks
   demoted, which is the opposite of what winning it meant.

   Deliberately NOT gated on `.earned`, unlike the resting styles above. Pressing
   a badge is what asks "what is this one?", and the answer for these includes
   that it is rare — so the locked silhouette stays plain in the grid, and only
   says so once you actually reach for it. The note above still holds for the
   resting state: gold is not an advertisement you have to scroll past. */
.bg-slot.gold.selected .bg-badge {
  border-color: #ffd77a;
  box-shadow: 0 0 0 2px rgba(255, 215, 122, 0.45), 0 0 18px rgba(255, 201, 77, 0.5);
}
.bg-slot.gold:focus-visible { outline-color: #ffc94d; }

.bg-glyph {
  font-size: 1.5rem;
  line-height: 1;
  color: #ce93d8;
  opacity: 0.3;              /* the dim half of "not yet" — the ring stays lit */
  filter: grayscale(1);
  transition: opacity 0.15s, filter 0.15s;
}

.bg-slot.earned .bg-glyph { opacity: 1; filter: none; }

/* Rung numbers need to be legible at four digits inside a 3.1rem circle. */
.bg-glyph.bg-tier { font-size: 0.95rem; font-weight: 700; font-variant-numeric: tabular-nums; }

/* A run of characters rather than one glyph — Lenny. Small enough to sit inside
   the circle, and `nowrap` because the face breaking across two lines is a
   sadder sight than a small face. The combining marks it is built from need a
   little vertical room, hence the line-height. */
.bg-glyph.bg-wide {
  font-size: 0.62rem;
  line-height: 1.35;
  white-space: nowrap;
  letter-spacing: -0.02em;
}
.bg-badge.bg-mini .bg-glyph.bg-wide { font-size: 0.3rem; }
.bg-badge.bg-show .bg-glyph.bg-wide { font-size: 0.5rem; }
.seat-glyph.bg-wide { font-size: 0.3rem; white-space: nowrap; }

.bg-svg { width: 1.6rem; height: 1.6rem; display: block; }

/* Only earned badges are captioned. Long names wrap rather than truncate — a
   half-shown badge name is worse than a two-line one. */
.bg-name {
  font-size: 9.5px;
  line-height: 1.25;
  text-align: center;
  color: #ce93d8;
  letter-spacing: 0.01em;
  overflow-wrap: anywhere;
}

.bg-detail { margin: 0.55rem 0 0; }
.bg-detail b { color: #eceff1; }

/* ---- the showcase strip beside the rating ---- */

/* Three slots, filled or hollow, pressed to open the picker. They sit at the
   right-hand end of the rating row and drop below it once the row runs out of
   width, rather than squeezing the figures they sit next to. */
.pf-showcase {
  display: flex;
  gap: 0.4rem;
  margin-left: auto;
  align-self: center;
}
.pf-show-slot {
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  line-height: 0;
  cursor: pointer;
}
.pf-show-slot:hover .bg-badge { transform: translateY(-2px); }
.pf-show-slot:focus-visible { outline: 2px solid #ce93d8; outline-offset: 3px; }

.bg-badge.bg-show { width: 2.5rem; height: 2.5rem; }
.bg-badge.bg-show .bg-glyph { font-size: 1.2rem; }
.bg-badge.bg-show .bg-svg { width: 1.3rem; height: 1.3rem; }

/* Filled: the same lit ring an earned badge wears in the grid below, so the
   thing on your clock and the thing in your collection are visibly one badge. */
.pf-show-slot.filled .bg-badge {
  border-color: #ab47bc;
  background: radial-gradient(circle at 50% 35%, #462f52, #2b2340);
  box-shadow: 0 0 0 1px rgba(171, 71, 188, 0.25), 0 2px 8px rgba(171, 71, 188, 0.18);
}
.pf-show-slot.filled .bg-glyph { opacity: 1; filter: none; }

/* And gold when the badge is gold — this strip sits at the top of your own
   profile, which is the first place you look after earning one. */
.pf-show-slot.filled.gold .bg-badge {
  border-color: #ffc94d;
  background: radial-gradient(circle at 50% 35%, #5a4318, #33280f);
  box-shadow: 0 0 0 1px rgba(255, 201, 77, 0.3), 0 2px 10px rgba(255, 201, 77, 0.28);
}
.pf-show-slot.filled.gold .bg-glyph { color: #ffd77a; }
.pf-show-slot.gold:focus-visible { outline-color: #ffc94d; }

.pf-show-slot.glass:focus-visible { outline-color: #e9f4ff; }

/* Empty: a dashed ring around a hollow outline. Not a plus sign — the slot is
   for a badge, and an outline of one says so without adding an icon that has to
   be learned. */
.bg-badge.bg-empty {
  border-style: dashed;
  border-color: #46466b;
  background: none;
  box-shadow: none;
}
.bg-badge.bg-empty .bg-glyph { color: #8f8fb5; opacity: 0.4; filter: none; }
.pf-show-slot:hover .bg-badge.bg-empty { border-color: #6d6d9c; }

/* ---- the picker modal ---- */

/* Wider than a login box because it holds a grid, and scrolling rather than
   growing so the Done button stays on screen on a phone. */
.bg-picker-card { width: min(460px, 100%); }
.bg-picker {
  max-height: min(50vh, 400px);
  overflow-y: auto;
  text-align: left;
  margin-bottom: 0.4rem;
}
.bg-picker .bg-group:first-child { margin-top: 0; }
.bg-picker-card .modal-btn { margin-top: 0.4rem; }

.pf-saved-title { color: #eceff1; font-size: 1rem; margin-top: 0.3rem; }
.pf-saved-meta { color: #90a4ae; font-size: 12.5px; margin-top: 0.2rem; }
.pf-saved-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.9rem; }
.pf-saved-actions .home-small { font-size: 0.85rem; padding: 0.45rem 0.9rem; }
/* Separated by a rule, because it is not another thing to do to your saved game
   — it opens somebody else's, and sits on this card only because that is where
   games live. */
.pf-import {
  border-top: 1px solid #2f2f4a;
  padding-top: 0.9rem;
  margin-top: 1.1rem;
}
.pf-share-url { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; margin-top: 0.8rem; font-size: 11.5px; }
.pf-share-url span { letter-spacing: 0.07em; text-transform: uppercase; color: #90a4ae; }
.pf-share-url code { color: #b0bec5; overflow-wrap: anywhere; }

.pf-danger { border-color: #7a4a4a; color: #ef9a9a; }
.pf-danger:hover { background: #7a4a4a; color: #fff; }

/* The two shortcuts this page exists for. Side by side on a desktop, stacked
   once there isn't room for both. */
.pf-links { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 0.7rem; margin-top: 0.6rem; }
.pf-link {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  width: 100%;
  text-align: left;
  font-family: inherit;
  padding: 0.9rem 1rem;
  border: 1px solid #546e7a;
  border-radius: 10px;
  background: transparent;
  color: #cfd8dc;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, transform 0.15s;
}
.pf-link:hover { background: rgba(171, 71, 188, 0.12); border-color: #ab47bc; transform: translateY(-1px); }
.pf-link-icon { font-size: 1.4rem; line-height: 1; flex: none; }
.pf-link-text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.pf-link-text b { color: #eceff1; font-size: 0.98rem; font-weight: 600; }
.pf-link-text span { color: #90a4ae; font-size: 12px; }
.pf-link-arrow { margin-left: auto; color: #90a4ae; flex: none; }
.pf-link:hover .pf-link-arrow { color: #ce93d8; }

.pf-account { display: flex; flex-wrap: wrap; gap: 0.6rem; justify-content: center; }

/* ==================================================================
   The tutorial (/how) and the first-visit card that points at it
   ================================================================== */

/* --- first-visit card on the home screen --- */
/* Sits below the resume card and above the Find buttons. Purple rather than the
   resume card's warm brown: this is an invitation, not a clock running out. */
#home-intro {
  position: relative;   /* the ✕ sits in the corner, out of the column below */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.8rem;
  max-width: 460px;
  margin: 0 auto 1.6rem;
  padding: 1rem;
  border: 1px solid #4a3a5e;
  border-radius: 12px;
  background: linear-gradient(160deg, #262038, #1f1b30);
}
#home-intro-clip {
  width: 100%;
  max-width: 100%;
  border-radius: 8px;
  background: #1a1a2e;
  display: block;
}
/* The clip failed to load. Collapse it rather than leaving a black slab — the
   sentence and the button below are the part that has to survive. */
#home-intro.no-clip #home-intro-clip { display: none; }
#home-intro .intro-note {
  font-size: 0.92rem;
  color: #b0bec5;
  text-align: center;
  line-height: 1.5;
  margin: 0;
}
#home-intro .intro-buttons { display: flex; gap: 0.6rem; flex-wrap: wrap; justify-content: center; }
#home-intro .intro-open { border-color: #ab47bc; color: #f0e4f5; }
#home-intro .intro-open:hover { background: rgba(171, 71, 188, 0.18); }

/* Putting the card away, in its own corner rather than beside the offer to
   explain. Quiet by design: it replaced a button reading "I know how it works",
   which sat as an equal to "How it works" and made the card look like it was
   asking a question. Declining an explanation is not the same size of decision
   as accepting one.

   A generous hit area at a small glyph size — this is the control a returning
   visitor reaches for, often on a phone. */
#home-intro .intro-close {
  position: absolute;
  top: 0.35rem;
  right: 0.45rem;
  z-index: 1;
  font-family: inherit;
  font-size: 0.85rem;
  line-height: 1;
  padding: 0.35rem 0.5rem;
  color: #8a7f9c;
  background: transparent;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: color 0.15s, background 0.15s;
}
#home-intro .intro-close:hover { color: #f0e4f5; background: rgba(171, 71, 188, 0.22); }

/* The spin, for returning visitors — the other half of #home-intro, and never
   on screen at the same time (js/home.js renders exactly one of the two). No
   card chrome: this one is the shop window rather than an explanation, so it is
   the board on the page and nothing around it. */
#home-spin {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.6rem;
  max-width: 460px;
  margin: 0 auto 1.6rem;
}
#home-spin-clip {
  width: 100%;
  max-width: 100%;
  border-radius: 8px;
  background: #1a1a2e;
  display: block;
}
#home-spin.no-clip { display: none; }

/* The 2D / 3D pair and the pause button, on one row. */
#home-spin .spin-toggle,
.clip-controls {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  justify-content: center;
}
.home-small.spin-cut,
.home-small.clip-motion {
  padding: 0.25rem 0.7rem;
  font-size: 0.82rem;
  opacity: 0.75;
}
.home-small.spin-cut:hover,
.home-small.clip-motion:hover { opacity: 1; }
/* The selected cut. `on` rather than :focus — the choice has to stay legible
   after the button loses focus, which is most of the time. */
.home-small.spin-cut.on {
  opacity: 1;
  border-color: #ab47bc;
  color: #f0e4f5;
  background: rgba(171, 71, 188, 0.18);
}
/* Paused. Reusing the selected look would read as "2D and Play are both on";
   this says only that the clip is being held still. */
.home-small.clip-motion[aria-pressed="true"] {
  opacity: 1;
  border-color: #6d5a82;
}

/* --- the tutorial screen --- */
#tutorial-screen {
  max-width: 1040px;
  margin: 2rem auto 0;
  padding: 0 1.2rem 3rem;
  animation: setup-fade 0.4s ease;
}
.tut-title {
  font-size: clamp(1.6rem, 5vw, 2.2rem);
  color: #f0f0f0;
  letter-spacing: 0.03em;
  text-align: center;
  font-weight: 700;
}
.tut-sub {
  text-align: center;
  color: #90a4ae;
  margin: 0.5rem 0 2rem;
  font-size: 1rem;
}

/* The hook. Still capped below the page width — a 15-second loop is an
   illustration, not a feature film — but generous, because the whole argument
   of the page is in it and the captions run underneath.
 *
   The source is 854x480, so this is about as wide as it goes before the
   placeholder softens; the 1080p render has plenty of room. */
.tut-hero {
  max-width: 760px;
  margin: 0 auto 2.6rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.7rem;
}
.tut-hero.tut-hero-off { display: none; }

/* Both cuts of a clip share one grid cell, so the box is the size of the larger
   of them and switching render moves nothing else on the page. Absolute
   positioning would stack them just as well but takes the pair out of flow, and
   then the caption underneath has nothing to sit under. */
.tut-stack {
  display: grid;
  width: 100%;
}
.tut-stack > video {
  grid-area: 1 / 1;
  width: 100%;
  display: block;
  transition: opacity 0.3s ease;
}
/* Faded, not removed. display:none would drop the element out of the grid and
   collapse the cell to the other cut's height for a frame — a jump, on a swap
   whose whole selling point is that nothing jumps. */
.tut-stack > video.tut-off { opacity: 0; }

.tut-video {
  width: 100%;
  border-radius: 10px;
  border: 1px solid #3a3a5a;
  background: #1a1a2e;
  display: block;
}

/* The render switch. Quieter than .tut-choice on purpose: those buttons are the
   lesson, this one is a preference about how the lesson looks. It stays hidden
   until the 3D file is known to exist — see clipPair() in js/tutorial.js. */
.tut-render-switch {
  display: inline-flex;
  border: 1px solid #3f4b56;
  border-radius: 8px;
  overflow: hidden;
  margin: 0 auto;
}
.tut-render-switch.tut-off { display: none; }
.tut-render-opt {
  font-family: inherit;
  font-size: 0.8rem;
  color: #90a4ae;
  padding: 0.32rem 0.85rem;
  border: 0;
  background: transparent;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.tut-render-opt + .tut-render-opt { border-left: 1px solid #3f4b56; }
.tut-render-opt:hover { background: rgba(171, 71, 188, 0.14); color: #cfd8dc; }
.tut-render-opt.on { background: rgba(171, 71, 188, 0.22); color: #f0e4f5; }
/* Pressed, and the first frame of the other cut has not arrived yet. */
.tut-render-switch.is-busy { opacity: 0.6; cursor: progress; }
.tut-render-switch.is-busy .tut-render-opt { pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
  .tut-stack > video { transition: none; }
}
/* Reserved height, so the caption appearing and clearing between beats does not
   shunt the whole page up and down while the clip loops. */
.tut-caption {
  min-height: 1.5em;
  text-align: center;
  color: #ce93d8;
  font-size: 0.95rem;
  letter-spacing: 0.02em;
  margin: 0;
}

.tut-panel {
  border-top: 1px solid #2f2f4a;
  padding-top: 1.8rem;
  margin-bottom: 2.4rem;
}
.tut-head { display: flex; align-items: baseline; gap: 0.7rem; margin-bottom: 0.6rem; }
.tut-num {
  font-size: 0.8rem;
  font-weight: 700;
  color: #ab47bc;
  border: 1px solid #ab47bc;
  border-radius: 50%;
  width: 1.7rem;
  height: 1.7rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
}
.tut-head h2 { font-size: 1.15rem; color: #eceff1; font-weight: 600; }

/* The per-step turn control (js/tutorial.js turnButton, issue #74).
   `margin-left: auto` pushes it to the far end of the heading, so it sits in the
   same place on every step and never competes with the title for attention — it
   is an aside the reader can take or leave, not the point of the panel. */
.tut-turn {
  margin-left: auto;
  flex: none;
  align-self: center;
  font: inherit;
  font-size: 0.72rem;
  line-height: 1;
  padding: 0.3rem 0.6rem;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: transparent;
  color: #b0bec5;
  cursor: pointer;
  white-space: nowrap;
}
.tut-turn:hover { border-color: rgba(255, 255, 255, 0.42); color: #eceff1; }
/* On = board 2 is turned, which is how the page arrives. Marked in the same
   purple the step numbers use, so "this is switched on" reads the same here as
   everywhere else on the page. */
.tut-turn.on { border-color: #ab47bc; color: #ce93d8; }

/* Narrow screens still get the two-board page if the reader asks for it, and
   there the heading has no room for a control beside the title. */
@media (max-width: 30rem) {
  .tut-head { flex-wrap: wrap; }
  .tut-turn { margin-left: 0; order: 3; width: 100%; text-align: center; }
}
.tut-blurb { color: #b0bec5; line-height: 1.65; margin-bottom: 1.1rem; }
.tut-blurb strong { color: #eceff1; }
.tut-blurb em { color: #ce93d8; font-style: normal; }

.tut-boards {
  display: flex;
  gap: 1.4rem;
  justify-content: center;
  flex-wrap: wrap;
  margin: 1rem 0;
}
/* Big enough to read a glyph and a rank label at arm's length. Two of these
   plus the gap has to fit #tutorial-screen, which is why the page is wider
   than the rest of the site. */
.tut-wrap {
  --board-size: clamp(240px, 34vw, 380px);
  --cell-size: calc((var(--board-size) - 2px) / 8);   /* 1px border, not 2 */
}
.tut-board { border-width: 1px; }

/* The unrolled board is 8 wide and SIXTEEN tall, so width is the wrong thing to
   size it by: at .tut-wrap's width it would stand two screens high, and panel 5's
   whole argument is a route you can take in at one glance. So size it by HEIGHT
   and let the width fall out — the same trick #boards-container.phone-mode plays
   for the real board, and lvh for the same reason: see the long note there.
   Short version — dvh changes as iOS shows and hides the URL bar, so a board
   sized in it visibly grows and shrinks the whole way down a scroll; lvh is the
   stable, larger figure. This page scrolls by design, so overflowing the screen
   by a bar-height costs it nothing at all.
   .board-unified brings the explicit 8x16 tracks and height:auto with it.
 *
   These out-specify both .tut-wrap above and the 860px media query below, which
   are single-class rules — the width caps are repeated here so nothing is lost
   by winning. */
.tut-wrap.tut-unified {
  --tut-rows: 16;
  --board-size: max(200px, min(86vw, 340px, calc((100vh  - 170px) / var(--tut-rows) * 8)));
  --board-size: max(200px, min(86vw, 340px, calc((100lvh - 170px) / var(--tut-rows) * 8)));
}
/* Reflections add peek-rows above and below the board, and they have to be in
   the count or the strips push the board itself off the bottom of the screen. */
.tut-wrap.tut-unified.reflect {
  --tut-rows: calc(16 + 2 * var(--peek-rows, 0));
}

/* The tutorial's own highlight: "the page is pointing at this square", which is
   a different thing from selected, playable, or last-moved.
 *
 * Cyan, and the choice is forced rather than decorative — every other colour on
 * a cell already carries a meaning, and three of them appear on these same
 * boards:
 *
 *   purple  reflections and seam markers  (the strips are literally purple)
 *   yellow  .cell.selected                (panels 4 and 5 show a selected piece)
 *   green   last-move-from / last-move-to (panel 4 marks where a piece landed)
 *   red     in-check and the capture ring (panel 5 uses both)
 *
 * Purple was the original pick and it was the wrong one: highlighting a purple
 * reflection in purple reads as more reflection, not as emphasis. Cyan was the
 * only hue left unspoken for.
 *
 * Both marks below share the token and differ by SHAPE instead — a ring means
 * "this square", a filled block means "the piece travels through here" — so the
 * page adds one colour to the vocabulary rather than two. */
:root { --tut-mark: 77, 208, 225; }   /* cyan; swap for 246, 217, 78 to go yellow */

.cell.tut-flag {
  box-shadow: inset 0 0 0 2px rgba(var(--tut-mark), 0.95);
}

.tut-note {
  color: #90a4ae;
  font-size: 0.92rem;
  line-height: 1.6;
  text-align: center;
  max-width: 620px;
  margin: 0.8rem auto 0;
  min-height: 3em;      /* the message swaps as you click; keep the page still */
}
.tut-note strong { color: #eceff1; }
.tut-note em { color: #ce93d8; font-style: normal; }

.tut-choices { display: flex; gap: 0.6rem; justify-content: center; flex-wrap: wrap; margin-top: 0.9rem; }
.tut-choice {
  font-family: inherit;
  font-size: 0.9rem;
  color: #cfd8dc;
  padding: 0.5rem 0.9rem;
  border: 1px solid #546e7a;
  border-radius: 8px;
  background: transparent;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.tut-choice:hover { background: rgba(171, 71, 188, 0.14); border-color: #ab47bc; }
.tut-choice.on { border-color: #ab47bc; color: #f0e4f5; background: rgba(171, 71, 188, 0.2); }

.tut-outro {
  border-top: 1px solid #2f2f4a;
  padding-top: 1.8rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.9rem;
  text-align: center;
}
.tut-outro .home-big { width: 100%; max-width: 320px; }

@media (max-width: 860px) {
  .tut-wrap { --board-size: min(86vw, 340px); }
  .tut-boards { gap: 1.6rem; }
}

/* --- tutorial: reflections, route trail, nav, closing loop --- */

/* The peek strips are built into every tutorial board but have no height until
   the wrapper earns .reflect — so panel 2's toggle is a transition rather than a
   rebuild, and the boards never have to be re-rendered. Mirrors the game's own
   rule (#boards-container.reflections-on), with the cell maths adjusted for the
   1px border the tutorial boards use instead of 2px. */
.tut-wrap .board-peek {
  width: calc(var(--board-size) - 2px);
}
.tut-wrap.reflect .board-peek {
  height: calc(var(--peek-rows) * var(--cell));
  opacity: 1;
}
/* The game's own default. It was briefly 2 here, on the theory that this is the
   one page whose job is showing across the seam — but a deeper strip crowds the
   board it is meant to annotate, and matching the default means the tutorial
   shows people exactly what they will get in a game. */
#tutorial-screen { --peek-rows: 1.5; }

/* Non-interactive panels shouldn't offer a pointer on a square that does nothing. */
.tut-wrap .board-peek .cell { overflow: hidden; }

/* The long way round, traced. Distinct from a legal-move dot: this is not
   somewhere the piece may go, it is the ground it covers getting there. */
.cell.tut-path::before {
  content: '';
  position: absolute;
  inset: 18%;
  border-radius: 3px;
  background: rgba(var(--tut-mark), 0.34);
  box-shadow: inset 0 0 0 1px rgba(var(--tut-mark), 0.8);
  pointer-events: none;
}

/* Home, at the top as well as the bottom — the page is long. */
.tut-nav {
  margin-bottom: 0.6rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
.tut-home {
  font-family: inherit;
  font-size: 0.9rem;
  color: #90a4ae;
  background: transparent;
  border: none;
  padding: 0.3rem 0;
  cursor: pointer;
  transition: color 0.15s;
}
.tut-home:hover { color: #ce93d8; }

/* The layout toggle, opposite Home. Outlined rather than bare, because unlike
   Home it changes the page you are reading rather than leaving it — but quiet,
   because the page has already chosen the right one for this screen and most
   readers should never need it. */
.tut-layout {
  font-family: inherit;
  font-size: 0.82rem;
  color: #90a4ae;
  background: transparent;
  border: 1px solid #3f4a56;
  border-radius: 999px;
  padding: 0.3rem 0.8rem;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s;
}
.tut-layout:hover { color: #ce93d8; border-color: #ab47bc; }

.tut-layout-note {
  text-align: center;
  color: #6d7d88;
  font-size: 0.85rem;
  margin: -1.4rem 0 1.6rem;   /* tucks under .tut-sub's bottom margin */
}

/* The closing loop: the finished strip, turning, after the explaining is done. */
.tut-spin {
  border-top: 1px solid #2f2f4a;
  padding-top: 1.8rem;
  margin-bottom: 2.4rem;
  text-align: center;
}
.tut-spin h2 {
  font-size: 1.15rem;
  color: #eceff1;
  font-weight: 600;
  margin-bottom: 1rem;
}
.tut-spin-video-stack {
  max-width: 560px;
  margin: 0 auto 1rem;
}
.tut-spin-video {
  width: 100%;
  border-radius: 10px;
  background: #1a1a2e;
  display: block;
}
.tut-spin .tut-render-switch { margin-bottom: 1.4rem; }
.tut-spin.tut-spin-off .tut-spin-video-stack,
.tut-spin.tut-spin-off .tut-render-switch { display: none; }
.tut-spin .tut-blurb { max-width: 620px; margin: 0 auto; text-align: left; }

/* The piece picker in the "crossing a seam" panel. A glyph beside each name,
   since the glyph is what they will be hunting for on the board a second later. */
.tut-picker { margin-bottom: 0.2rem; }
.tut-choice.piece-choice {
  font-size: 1rem;
  padding: 0.45rem 0.8rem;
  min-width: 6.2rem;
}

/* Piece orientation in the tutorial.
 *
 * Hard-coded to the game's DEFAULTS rather than read from the player's saved
 * preferences. The tutorial is teaching what the board looks like out of the
 * box, and somebody who has turned the flips off would otherwise get a lesson
 * that contradicts its own diagrams. Only pawns ever turn over — knights and
 * officers stay upright everywhere, exactly as in a default game.
 *
 * SPLIT LAYOUT ONLY, since 2026-09-08. "The default" stopped being one value
 * that day: pieceFlipPref() in js/ui.js now answers 'pawns' on the split layout
 * and 'none' on the single board, because the unrolled strip already reads as
 * the far side without turning anything over. This rule kept flipping them on
 * both, so the single-board page was drawing a board no player is given — the
 * exact fault the paragraph above says it exists to avoid, arrived at from the
 * other direction. Scoped on the WRAP because the reflection strips are its
 * children too, and the second selector below is about them.
 *
 * The two rules are opposites on purpose: negative ranks on the BOARD, positive
 * ranks in a REFLECTION. A pawn is upside down in exactly one of the two places
 * it appears. */
#tutorial-screen .tut-wrap:not(.tut-unified) .cell[data-rank^="-"] .piece.pc-pawn,
#tutorial-screen .tut-wrap:not(.tut-unified) .cell:not([data-rank^="-"]) .refl-piece.pc-pawn {
  display: inline-block;
  transform: rotate(180deg);
}

/* No font-size here: .piece and .refl-piece share one rule that sizes from
   --cell-size, which .tut-wrap sets for these smaller boards. Giving the strips
   their own size is precisely the bug that rule exists to prevent. */
#tutorial-screen .refl-piece.white { color: #f5f5f5; }
#tutorial-screen .refl-piece.black { color: #2a2a2a; }

/* ---- site footer (index.html, filled by js/home.js) ---- */

/* Not fixed: on a long page (the tutorial, the leaderboard) a pinned footer eats
   a strip of every screenful for a line most people read once. It sits at the
   end of the document and waits to be scrolled to. */
#site-footer {
  /* Under the content, always: it is a block and the container is a block. No
     `flex: 0 0 100%` any more — see the note on body. */
  max-width: 1040px;
  margin: 2.5rem auto 0;
  padding: 1.4rem 1.2rem 2rem;
  border-top: 1px solid #2f2f4a;
  text-align: center;
}
.site-footer-line {
  color: #78909c;
  font-size: 0.88rem;
  line-height: 1.6;
  margin: 0;
}
.site-footer-mail {
  color: #ce93d8;
  text-decoration: none;
  border-bottom: 1px solid rgba(206, 147, 216, 0.35);
}
.site-footer-mail:hover {
  color: #e1bee7;
  border-bottom-color: #e1bee7;
}
/* The feedback form's opener: a button dressed as the address beside it. */
.site-footer-form {
  background: none;
  border: 0;
  border-bottom: 1px solid rgba(206, 147, 216, 0.35);
  padding: 0;
  font: inherit;
  cursor: pointer;
}
/* The legal pair, under the contact line. Quieter than the address above them:
   they are the things you go looking for, not the thing being offered. */
.site-footer-legal {
  margin-top: 0.5rem;
  font-size: 0.82rem;
}
.site-footer-link {
  color: #90a4ae;
  text-decoration: none;
  border-bottom: 1px solid rgba(144, 164, 174, 0.3);
}
.site-footer-link:hover { color: #ce93d8; border-bottom-color: rgba(206, 147, 216, 0.4); }

/* --- the terms and privacy pages ---------------------------------------- */

/* Same column as the changelog: these are read top to bottom, and a measure
   wider than this stops being prose and starts being a contract. The about
   page borrows the column, and the rest of the lg- type with it. */
#legal-screen, #about-screen {
  max-width: 640px;
  margin: 0 auto;
  padding: 1rem 1rem 3rem;
}
.lg-title {
  font-size: 1.6rem;
  color: #eceff1;
  margin-bottom: 0.3rem;
}
/* The date is the first thing worth knowing about a document like this — a
   notice with no date is one nobody can tell has gone stale. */
.lg-updated {
  color: #78909c;
  font-size: 0.82rem;
  margin: 0 0 1rem;
}
.lg-sub {
  color: #90a4ae;
  font-size: 0.92rem;
  line-height: 1.6;
  margin-bottom: 2rem;
}
.lg-section {
  border-top: 1px solid #2f2f4a;
  padding-top: 1.1rem;
  margin-bottom: 1.6rem;
}
.lg-h {
  font-size: 1.05rem;
  color: #ce93d8;
  margin: 0 0 0.6rem;
}
.lg-p {
  color: #b0bec5;
  font-size: 0.92rem;
  line-height: 1.65;
  margin: 0 0 0.8rem;
}
.lg-p:last-child { margin-bottom: 0; }
/* The line introducing a list of prohibitions. Brighter as well as bolder —
   bold alone against #b0bec5 reads as a rendering accident rather than as
   emphasis, and it is the only bold text on either page. */
.lg-lead {
  color: #cfd8dc;
  margin-bottom: 0.5rem;
}
.lg-lead strong { font-weight: 700; }
.lg-list {
  list-style: none;
  padding: 0;
  margin: 0 0 0.8rem;
}
.lg-list li {
  color: #b0bec5;
  font-size: 0.92rem;
  line-height: 1.6;
  padding-left: 1.1rem;
  position: relative;
  margin-bottom: 0.45rem;
}
.lg-list li::before {
  content: '—';
  position: absolute;
  left: 0;
  color: #546e7a;
}
.lg-other, .lg-home { display: block; margin: 1.2rem auto 0; }
.lg-other { margin-top: 2rem; }

/* --- the about page ------------------------------------------------------ */

.ab-story { font-size: 1rem; }
.ab-devs { margin-top: 1.8rem; }
/* The two dev badges, drawn as earned grid slots so their glass is the glass
   of the profile grid. Uncaptioned, so the slot is just the disc. */
.ab-badges { justify-content: center; gap: 0.8rem 2rem; margin: 0.4rem 0 1rem; }
.ab-badges .bg-slot { width: auto; cursor: default; }
.ab-ladder { text-align: center; }
.ab-ladder strong { color: #eceff1; font-weight: 700; }
.ab-link { color: #ce93d8; text-decoration: underline; text-underline-offset: 2px; }
.ab-link:hover { color: #e1bee7; }

/* The line under the login box pointing at both documents. Small, but a link in
   it has to look like one — this is the only place the pages are offered at the
   moment somebody is deciding whether to sign up. */
.modal-fine {
  color: #78909c;
  font-size: 0.78rem;
  line-height: 1.5;
  margin: 1rem 0 0;
  text-align: center;
}
.modal-fine a {
  color: #90a4ae;
  border-bottom: 1px solid rgba(144, 164, 174, 0.35);
  text-decoration: none;
}
.modal-fine a:hover { color: #ce93d8; }

/* --- the feedback form ------------------------------------------------- */

/* Left-aligned inside a centred card. Labels above their controls, and prose
   the reader has to fill in should start where their eye already is. */
.fb-form { text-align: left; margin-bottom: 0.4rem; }
.fb-label {
  display: block;
  color: #90a4ae;
  font-size: 0.8rem;
  margin: 0.7rem 0 0.3rem;
}
.fb-kind, .fb-text, .fb-mail {
  width: 100%;
  font-family: inherit;
  font-size: 0.9rem;
  color: #e0e0e0;
  background: #1f1b30;
  border: 1px solid #45566b;
  border-radius: 8px;
  padding: 0.5rem 0.6rem;
}
.fb-text { resize: vertical; min-height: 6.5rem; line-height: 1.5; }
.fb-kind:focus, .fb-text:focus, .fb-mail:focus {
  outline: none;
  border-color: #ce93d8;
}
.fb-count {
  color: #6d7d88;
  font-size: 0.75rem;
  text-align: right;
  margin: 0.25rem 0 0;
  min-height: 1em;   /* reserved, so appearing and clearing does not jog the card */
}

/* The honeypot. Hidden without display:none — a field that is not rendered at
   all is one a form-filler can notice is not rendered. This one is in the
   layout, off-screen, and left alone by anything with eyes. */
.fb-pot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.fb-error {
  color: #ef9a9a;
  font-size: 0.82rem;
  line-height: 1.5;
  margin: 0.6rem 0 0;
  min-height: 1em;
}
.fb-escape { color: #ce93d8; }
.fb-actions { margin-top: 0.6rem; }


/* ─── the board setup page (/board, issue #84) ────────────────────────────────
 *
 * A real board with a page built around it: the game's own header and boards in
 * the middle, this page's heading above and its saved-setups panel below. The
 * board and the display tray bring their own styles with them — everything here
 * is the furniture, and it deliberately borrows the tutorial's measurements so
 * the two pages that teach the board look like the same pair of hands. */
/* No width of its own: this IS #game-container, which is already sized for the
   board it is holding — and the board is the widest thing on the page, in
   reflection view especially. The text below sets its own measure instead. */
.boardsetup-page { padding-bottom: 2.5rem; }
.bs-head {
  padding-top: 0.2rem;
  text-align: center;
}
/* Home on the left of its own row, clear of the fixed site corner on the right.
   Its own row rather than beside the title, so a narrow screen never has the two
   competing for the same line. */
.bs-nav {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  margin-bottom: 0.4rem;
}
.bs-title {
  font-size: clamp(1.6rem, 5vw, 2.2rem);
  color: #f0f0f0;
  letter-spacing: 0.03em;
  font-weight: 700;
  margin: 0 0 0.4rem;
}
.bs-note {
  color: #6d7d88;
  font-size: 0.85rem;
  margin: 0.3rem auto 0;
  max-width: 60ch;
}

/* Save/Load, on the board's own header row beside sound, Display and flip. A
   span of inline buttons, so they wrap with the rest of that row rather than
   claiming a line of their own — and a rule keeping them off the switches. */
.bs-acct {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  vertical-align: middle;
  margin-left: 0.6rem;
  padding-left: 0.7rem;
  border-left: 1px solid #37474f;
}

.bs-btn {
  font-family: inherit;
  font-size: 0.8rem;
  color: #eceff1;
  background: #37474f;
  border: 1px solid #455a64;
  border-radius: 8px;
  padding: 0.45rem 0.9rem;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s, opacity 0.15s;
}
.bs-btn:hover:not(:disabled) { background: #455a64; border-color: #ab47bc; }
.bs-btn.ghost { background: transparent; color: #90a4ae; }
.bs-btn.ghost:hover:not(:disabled) { color: #ce93d8; border-color: #ab47bc; background: transparent; }
.bs-btn:disabled { opacity: 0.45; cursor: default; }

/* What just happened. Announced (role="status"), so it is read as well as seen —
   pressing Save and being told nothing is the failure this line exists for. */
.bs-status {
  text-align: center;
  font-size: 0.85rem;
  color: #90a4ae;
  margin: 0.5rem auto 0;
}
.bs-status.ok { color: #81c784; }
.bs-status.err { color: #ef9a9a; }

.bs-outro {
  border-top: 1px solid #2f2f4a;
  margin-top: 2rem;
  padding-top: 1.8rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.9rem;
  text-align: center;
}
.bs-outro .home-big { width: 100%; max-width: 340px; }
.bs-outro-note { margin-top: -0.3rem; }

@media (max-width: 560px) {
  /* Nothing to hang a divider off once the row has wrapped. */
  .bs-acct { margin-left: 0; padding-left: 0; border-left: none; }
}
