/* Conquer — the shell.
   Chamfered panels and an angular display face, laid out mobile-portrait
   first; on wider screens the whole app stays a phone-shaped column with the
   background bleeding around it.

   The palette lives in the token block below and is overridden wholesale by
   `themes.js`. The values here are **Daybreak**, the default: a white ground
   with a blue accent and a deep teal for selection. The original dark navy
   scheme is still available as "Nightfall" through the Developer Mode picker,
   along with four others. */

/* --- Fonts ---------------------------------------------------------------
   Self-hosted so the look does not depend on a network the player may not
   have. Every rule below names a system fallback, so a missing file degrades
   to a plainer shell rather than a broken one. */
@font-face {
  font-family: 'Chakra Petch';
  src: url('assets/fonts/chakra-petch-600.woff2') format('woff2');
  font-weight: 600;
  font-display: swap;
}
@font-face {
  font-family: 'Chakra Petch';
  src: url('assets/fonts/chakra-petch-700.woff2') format('woff2');
  font-weight: 700;
  font-display: swap;
}
@font-face {
  font-family: 'Rajdhani';
  src: url('assets/fonts/rajdhani-500.woff2') format('woff2');
  font-weight: 500;
  font-display: swap;
}
@font-face {
  font-family: 'Rajdhani';
  src: url('assets/fonts/rajdhani-600.woff2') format('woff2');
  font-weight: 600;
  font-display: swap;
}

:root {
  /* Spacing and type scale, so every screen shares one rhythm. */
  --s1: 0.35rem;
  --s2: 0.6rem;
  --s3: 1rem;
  --s4: 1.5rem;
  --s5: 2.25rem;

  --t-sm: 0.9rem;
  /* Compact chrome — the Developer Mode badge — held at
     the old smallest size. They share the topbar with the screen title, and
     raising --t-sm for body copy squeezed that title into an ellipsis. */
  --t-chip: 0.82rem;
  --t-md: 1rem;
  --t-lg: 1.25rem;
  --t-xl: 2rem;

  --display: 'Chakra Petch', 'Segoe UI', system-ui, sans-serif;
  --body: 'Rajdhani', 'Segoe UI', system-ui, sans-serif;

  /* --- Theme ---------------------------------------------------------------
     Named by role rather than by colour. `themes.js` overrides exactly this
     set, so anything a theme must be able to change has to be a token here.

     Colours needed at more than one alpha are declared as a space-separated
     triple and composed at the point of use — `rgb(var(--accent-rgb) / 0.4)`.
     One token then covers every alpha: the panel fill alone appeared as a
     dozen separate `rgba(22, 43, 71, …)` literals before this, none of which a
     theme could reach.

     Where a colour is wanted both ways, the triple is the single source and
     the plain token is derived from it. Declaring both independently would let
     a theme set the accent bar to one colour and its glow to another — a
     subtle, easy mistake with no obvious symptom.

     **These values are Daybreak, the default theme, and must stay in step with
     it.** `themes.js` re-applies the same tokens from JS, so if the two ever
     disagree the page paints one palette for a frame and then swaps — a visible
     flash on every load. `themes.test.js` compares this block against
     DEFAULT_THEME to make that impossible to do by accident. */
  --accent-rgb: 20 102 255;
  --select-rgb: 5 109 128;
  --p1-rgb: 217 43 69;
  --surface-rgb: 238 243 250;
  --panel-rgb: 255 255 255;

  --accent: rgb(var(--accent-rgb));
  --select: rgb(var(--select-rgb));
  --p1: rgb(var(--p1-rgb));
  --bg-raised: rgb(var(--surface-rgb));
  --bg-panel: rgb(var(--panel-rgb));

  /* Only ever used flat. */
  --bg-deep: #ffffff;
  --bg-mid: #f4f7fb;
  --edge: #d3dfee;
  --edge-bright: #9fb8d4;
  --ink: #0d1b2a;
  --ink-dim: #526379;
  --p2: #1868c9;
  --good: #177245;
  /* The capture celebration's gold. A token like everything else, so a theme
     that wanted a colder win could have one. */
  --celebrate: #ffd46b;
  --bad: #c0392b;

  /* Only ever used with an alpha. */
  --panel-lo-rgb: 244 247 251; /* the darker end of a panel gradient */
  --scrim-rgb: 18 30 46; /* behind an overlay — still dark on a light theme */
  --shadow-rgb: 30 45 70; /* drop and text shadows */
  --grid-rgb: 120 150 190; /* the ambient engineering grid */

  /* The ambient radial wash on the body. */
  --wash-a: #e8f0fb;
  --wash-b: #dfe9f7;

  /* The primary button, and the ink that must read on top of it. A light
     theme's accent needs pale text where a yellow one needs near-black. */
  --accent-ink: #ffffff;
  --accent-hi: #3d81ff;
  --accent-lo: #1466ff;
  --accent-hover-hi: #5c95ff;
  --accent-hover-lo: #0f57e0;
  --accent-edge: #7ba9ff;

  /* A selected control — a board size, a difficulty, the equipped skin. The
     fill is a whole background value rather than a colour so a light theme can
     make it solid while a dark one keeps a translucent tint: a mid colour on a
     pale wash of itself has almost no contrast, which is exactly what made the
     light themes' chips unreadable. */
  --select-fill: linear-gradient(
    180deg,
    rgb(var(--select-rgb)),
    rgb(var(--select-rgb) / 0.94)
  );
  --select-ink: #ffffff;

  /* Body copy inside `.prose`, a touch softer than --ink. */
  --prose-ink: #2c3d52;

  /* The self-playing board behind the title. Both are per-theme: the values
     that sit it nicely behind a navy ground wash it out entirely on white. */
  --demo-opacity: 0.5;
  --demo-filter: saturate(0.85);

  /* Legacy aliases: a few rules still speak in these older names. */
  --bg: var(--bg-deep);
  --line: var(--edge);
  --red: var(--p1);
  --blue: var(--p2);

  /* The board's own surface. Overwritten per skin from JS; these defaults are
     the "Terminal" skin, which is what the board has always looked like. */
  --board-surface: #fafafa;
  --board-frame: #2f5a8c;

  --radius: 4px;
  /* The chamfer that gives every panel its angular corner. */
  --chamfer: 12px;
  --chamfer-sm: 8px;

  /* 44px is the smallest comfortable touch target. */
  --tap: 44px;
  /* The app is a phone-shaped column even on a desktop monitor. */
  --column: 480px;
}

* {
  box-sizing: border-box;
}

/* --- Scrollbars -----------------------------------------------------------
   The platform scrollbar is a wide light-grey slab that reads as a foreign
   object on this palette, and it sat hard against the text. Thin, themed, and
   held off the content by the track's own border. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--edge-bright) transparent;
}

::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  /* The transparent border is what insets the thumb: it keeps a 3px gutter on
     both sides so the bar never touches the words beside it. */
  background-clip: padding-box;
  border: 3px solid transparent;
  border-radius: 999px;
  background-color: var(--edge-bright);
}

::-webkit-scrollbar-thumb:hover {
  background-color: var(--select);
}

::-webkit-scrollbar-corner {
  background: transparent;
}

html,
body {
  margin: 0;
  height: 100%;
  overflow: hidden; /* the shell never scrolls; only content inside a screen may */
  overscroll-behavior: none;
}

body {
  font-family: var(--body);
  font-size: 1.05rem;
  background: var(--bg-deep);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}

/* --- Ambient background ---------------------------------------------------
   Three stacked layers, all pure CSS: a radial wash, a faint engineering
   grid, and the drifting "data fragments". They sit behind the app column and
   are what fills a desktop window either side of it. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(ellipse 120% 80% at 50% -10%, var(--wash-a) 0%, transparent 60%),
    radial-gradient(ellipse 100% 60% at 50% 110%, var(--wash-b) 0%, transparent 65%),
    linear-gradient(180deg, var(--bg-mid) 0%, var(--bg-deep) 100%);
}

body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.5;
  background-image:
    linear-gradient(rgb(var(--grid-rgb) / 0.14) 1px, transparent 1px),
    linear-gradient(90deg, rgb(var(--grid-rgb) / 0.14) 1px, transparent 1px);
  background-size: 44px 44px;
  /* Fades the grid out towards the bottom so it reads as depth, not wallpaper. */
  -webkit-mask-image: linear-gradient(180deg, #000 0%, transparent 85%);
  mask-image: linear-gradient(180deg, #000 0%, transparent 85%);
}

/* The drifting motes. Decorative only, so it is inert to pointers and
   removed entirely for players who ask for less motion. */
#bg-fx {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

#bg-fx i {
  position: absolute;
  display: block;
  border-radius: 50%;
  border: 1px solid rgb(var(--select-rgb) / 0.5);
  background: rgb(var(--select-rgb) / 0.06);
  animation: drift linear infinite;
}

@keyframes drift {
  from {
    transform: translateY(12vh);
    opacity: 0;
  }
  15%,
  75% {
    opacity: 1;
  }
  to {
    transform: translateY(-112vh);
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  #bg-fx {
    display: none;
  }
}

/* --- Shell ---------------------------------------------------------------
   svh rather than dvh: dvh re-resolves every time the browser address bar
   hides, which would resize the board mid-move. svh is the stable smallest
   height. */
#app {
  position: relative;
  z-index: 1;
  height: 100svh;
  max-width: var(--column);
  margin-inline: auto;
  display: grid;
  grid-template-rows: auto auto auto 1fr auto auto;
  overflow: hidden;
  padding-top: env(safe-area-inset-top);
}

/* On a screen wider than the column, edge the column so it reads as a
   deliberate frame rather than a stranded block of content.

   The fill runs panel to panel-lo, not panel to scrim. `--scrim-rgb` is the
   backdrop behind an overlay and stays dark on a light theme deliberately, so
   using it here painted 60% of a near-black navy down the whole column the
   moment the window passed 520px: the same screen was white on a phone and
   slate on a desktop, and Daybreak's sign-in note fell to 2.4:1. `--panel-lo`
   is the darker end of a panel gradient, which is what this actually is, and
   it moves with the theme. On the dark themes the change is about three points
   of channel and invisible. */
@media (min-width: 520px) {
  #app {
    border-inline: 1px solid var(--edge);
    box-shadow: 0 0 80px rgb(var(--shadow-rgb) / 0.6);
    background: linear-gradient(180deg, rgb(var(--panel-rgb) / 0.6), rgb(var(--panel-lo-rgb) / 0.6));
  }
}

#topbar {
  position: relative;
  display: grid;
  /* The right cell is `auto` rather than a fixed tap target so the Developer
     Mode badge can sit beside the menu button. With it hidden the cell
     collapses back to the button's width. */
  grid-template-columns: var(--tap) 1fr auto;
  align-items: center;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--edge);
  background: linear-gradient(180deg, rgb(var(--surface-rgb) / 0.85), rgb(var(--panel-rgb) / 0.4));
  min-height: calc(var(--tap) + var(--s2));
}

/* A thin cyan rule along the bottom of the header band. */
#topbar::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--select), transparent);
  opacity: 0.45;
  pointer-events: none;
}

/* Placed explicitly, because `.hidden` is `display: none` — without this the
   back button leaving the flow shifts the title and the right-hand cluster
   each one column to the left, and the title renders inside the 44px cell. */
#back-btn {
  grid-column: 1;
}

#topbar-title {
  grid-column: 2;
}

.topbar-right {
  grid-column: 3;
}

#topbar-title {
  margin: 0;
  font-family: var(--display);
  font-size: var(--t-lg);
  font-weight: 700;
  text-align: center;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.topbar-right {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s2);
  min-width: var(--tap);
}

/* Deliberately loud: it must never be ambiguous whether Developer Mode is on.
   Loud is the red pill, not the word — spelled out, "Developer Mode" took 110px
   of a 375px bar, and once the title grew to 20px it pushed "Extra Mode" into
   an ellipsis beside it. The full phrase is still on the element's `title`. */
.dev-badge {
  padding: 2px var(--s2);
  border: 1px solid var(--p1);
  border-radius: 999px;
  color: var(--p1);
  font-size: var(--t-chip);
  font-weight: 600;
  letter-spacing: 0.04em;
  white-space: nowrap;
}

/* Placed explicitly, for the same reason the topbar's columns are: a grid
   child removed with `display: none` does not leave a hole — every later child
   moves up a row. Hiding the topbar on the title screen dropped `#stage` into
   the `auto` row, where it collapsed to zero because everything inside it is
   absolutely positioned. */
#topbar {
  grid-row: 1;
}

#tabstrip {
  grid-row: 2;
}

#tab-blurb {
  grid-row: 3;
}

#stage {
  grid-row: 4;
  position: relative;
  overflow: hidden;
  min-height: 0; /* lets the row actually shrink inside the grid */
}

#bottombar {
  grid-row: 5;
}

#tabbar {
  grid-row: 6;
}

#bottombar {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  padding-bottom: calc(var(--s2) + env(safe-area-inset-bottom));
}

/* Set by `go()` when no bottom button belongs to the current screen. `:empty`
   was the obvious test and the wrong one — the buttons are always children,
   just hidden, so it never matched and the bar kept its padding everywhere. */
#bottombar.is-empty {
  display: none;
}

/* The title screen has its own wordmark and no back, pause or title
   text, so its topbar was an empty bar with a rule under it. */
#app.title-mode #topbar {
  display: none;
}

/* --- Tabs -----------------------------------------------------------------
   The five main tabs along the bottom, and the sub-tabs across the top of
   whichever is open. Both are painted by nav-ui.js from the table in nav.js,
   and both hide with `is-empty` rather than by being removed — see the note
   above about grid children and holes. */

#tabbar {
  display: flex;
  border-top: 1px solid var(--edge);
  background: linear-gradient(180deg, rgb(var(--panel-rgb) / 0.4), rgb(var(--surface-rgb) / 0.85));
  padding-bottom: env(safe-area-inset-bottom);
}

#tabbar.is-empty,
#tabstrip.is-empty,
#tab-blurb.is-empty {
  display: none;
}

/* One sentence saying what the open screen is, under the strip. Quiet: it is
   read once and then ignored, and it sits directly above the controls it is
   describing, which have to stay the loudest thing on the screen. */
#tab-blurb {
  margin: 0;
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--edge);
  background: rgb(var(--panel-rgb) / 0.35);
  color: var(--prose-ink);
  font-size: 0.95rem;
  line-height: 1.35;
  text-wrap: pretty;
}

.tab {
  /* `auto` rather than 0. Equal fifths were fine at 10.9px; at 12.5px "Extra
     Mode" needs 69 of the 68 it gets on a 360px phone and clips to "Extra M…".
     Sized from their labels the long one borrows from "Play" and "Shop", and
     the leftover is still shared evenly — the same fix the sub-tabs above
     already use, for the same reason. */
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: var(--tap);
  padding: var(--s1) 2px;
  border: 0;
  background: none;
  /* `--prose-ink`, not `--ink-dim`. The bar was 10.9px of the palest ink in
     the app, which is a caption's colour on something that is a main control.
     The active tab is still told apart by the accent, the icon fill and the
     heavier weight — it never needed the others to be faint. */
  color: var(--prose-ink);
  font-family: var(--display);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  cursor: pointer;
}

.tab-icon {
  width: 24px;
  height: 24px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* Filled while its tab is the open one, an outline the rest of the time. Play
   used to be solid always, which read as "this is the button" rather than as
   "this is where you are". Only the silhouette fills — the bag's handle stays
   a line, and the gear's hole stays a hole. */
.tab.is-active .tab-icon .is-solid {
  fill: currentColor;
}

.tab.is-active {
  color: var(--select);
  font-weight: 700;
}

.tab .tab-label {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#tabstrip {
  display: flex;
  flex-direction: column;
  border-bottom: 1px solid var(--edge);
  background: rgb(var(--surface-rgb) / 0.55);
}

.tab-row {
  display: flex;
}

/* `1 1 auto`, not `1 1 0`: with four tabs under Learn, equal widths clipped
   "Tips & Tricks" to an ellipsis at 375px while "Puzzles" sat in 20px of empty
   room beside it. Sized from their labels, a long one borrows from a short one
   and the leftover is still shared evenly. Equal widths were never doing any
   work here — the underline is what marks the open tab. */
.subtab {
  flex: 1 1 auto;
  min-width: 0;
  min-height: var(--tap);
  padding: var(--s1);
  border: 0;
  /* The underline is on the button rather than a separate element, so an
     active tab cannot drift out of line with its own marker. */
  border-bottom: 2px solid transparent;
  background: none;
  color: var(--prose-ink);
  font-family: var(--display);
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
}

.subtab.is-active {
  color: var(--select);
  border-bottom-color: var(--select);
}

/* The second row — Offence and Defence under Tips & Tricks — is quieter than
   the row above it, so the two read as a hierarchy rather than as six equal
   choices stacked up. */
.tab-row-nested {
  background: rgb(var(--panel-rgb) / 0.35);
}

.tab-row-nested .subtab {
  min-height: 38px;
  font-size: 0.85rem;
}

/* --- Screens -------------------------------------------------------------- */

.screen {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s3);
  padding: var(--s3);
  animation: fade-in 0.18s ease-out;
}

@keyframes fade-in {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.hidden {
  display: none !important;
}

/* --- The chamfered corner ------------------------------------------------
   The most recognisable device in the reference: corners cut on the diagonal
   rather than rounded. */
.cut {
  clip-path: polygon(
    var(--chamfer) 0,
    100% 0,
    100% calc(100% - var(--chamfer)),
    calc(100% - var(--chamfer)) 100%,
    0 100%,
    0 var(--chamfer)
  );
}

/* --- Title ---------------------------------------------------------------- */

#screen-title {
  padding: var(--s3);
  justify-content: center;
  gap: var(--s5);
}

/* The demo board is square, like the board in play. Stretching it to the
   screen made it a tall elongated grid that looked nothing like the game.
   `max` rather than `min` so the square *covers* the viewport and is cropped
   at the sides — `min` fitted it inside and left 182px of dead space above and
   below it on a phone. `#stage` already clips the overflow. */
.title-demo-frame {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: max(100%, 100svh);
  aspect-ratio: 1;
  pointer-events: none;
}

/* Bright enough to read who is taking ground — that is the whole point of the
   demo — but still clearly behind the wordmark. The text over it carries its
   own shadow and the buttons are opaque, so this is the only surface where the
   two compete. */
#title-demo {
  display: block;
  width: 100%;
  height: 100%;
  opacity: var(--demo-opacity);
  /* Tints the demo board into the theme instead of letting a bright white
     grid sit behind the wordmark. */
  filter: var(--demo-filter);
  /* Fades the board out at its edges so the square never reads as a panel on
     the background. A mask rather than a gradient overlay: this fades the
     canvas to *transparent*, so whatever is behind shows through correctly at
     every point. The overlay it replaces faded to a fixed `--bg-deep`, which
     did not match the body's gradient at that height and left the rectangle's
     edges plainly visible. */
  -webkit-mask-image: radial-gradient(ellipse at center, #000 35%, transparent 78%);
  mask-image: radial-gradient(ellipse at center, #000 35%, transparent 78%);
}

.title-overlay {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s5);
  text-align: center;
  width: 100%;
}

/* Shrinks to the width of the word inside it, which is what gives the subtitle
   below a width to match. */
.wordmark {
  display: inline-block;
  width: fit-content;
  max-width: 100%;
}

/* The wordmark: heavy angular type over a solid yellow bar, the way the
   reference stacks its logo. */
.game-title {
  margin: 0;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(2.75rem, 15vw, 4.25rem);
  line-height: 0.95;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
  text-shadow:
    0 0 24px rgb(var(--select-rgb) / 0.45),
    0 3px 18px rgb(var(--shadow-rgb) / 0.85);
}

.game-title::after {
  content: '';
  display: block;
  height: 10px;
  margin: var(--s2) 0 0;
  width: 100%;
  background: var(--accent);
  box-shadow: 0 0 22px rgb(var(--accent-rgb) / 0.55);
  clip-path: polygon(2% 0, 100% 0, 98% 100%, 0 100%);
}

/* Exactly as wide as the word above it. `textLength` on the <text> makes the
   browser fit the letters to the box, so this holds for any font that loads
   and at any screen width — no per-breakpoint font size to keep in sync. */
.game-subtitle {
  display: block;
  width: 100%;
  margin-top: var(--s2);
  overflow: visible;
}

.game-subtitle text {
  font-family: var(--display);
  font-weight: 600;
  /* In viewBox units. Chosen so the spacing adjustment stays small, which
     keeps the tracking looking deliberate rather than stretched. */
  font-size: 88px;
  fill: var(--ink);
  opacity: 0.82;
}

/* --- Title screen sign-in (mock-up) --------------------------------------- */

.auth {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  width: min(100%, 340px);
}

.btn-sso {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
}

/* These are meant to be read — they say what sign-in will offer once it
   exists. The default 0.4 disabled opacity let the demo board show straight
   through them, which looked broken rather than unavailable.
   `.btn.btn-sso` rather than `.btn-sso`: this block sits above the Buttons
   section, so at equal specificity `.btn:disabled` would win on source order. */
.btn.btn-sso:disabled {
  opacity: 1;
  color: var(--ink-dim);
  background: rgb(var(--panel-lo-rgb) / 0.92);
  border-style: dashed;
  border-color: var(--edge);
}

.auth-note {
  margin: var(--s1) 0 0;
  color: var(--ink-dim);
  font-size: var(--t-sm);
  line-height: 1.5;
  text-shadow: 0 2px 10px rgb(var(--shadow-rgb) / 0.9);
}

/* --- Buttons -------------------------------------------------------------- */

.btn {
  position: relative;
  min-height: var(--tap);
  padding: var(--s2) var(--s4);
  font-family: var(--display);
  font-size: var(--t-md);
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--ink);
  background: linear-gradient(180deg, var(--bg-raised), var(--bg-panel));
  border: 1px solid var(--edge);
  border-radius: 0;
  cursor: pointer;
  transition:
    background 0.14s ease,
    box-shadow 0.14s ease,
    color 0.14s ease;
  clip-path: polygon(
    var(--chamfer-sm) 0,
    100% 0,
    100% calc(100% - var(--chamfer-sm)),
    calc(100% - var(--chamfer-sm)) 100%,
    0 100%,
    0 var(--chamfer-sm)
  );
}

.btn:hover:not(:disabled) {
  background: linear-gradient(180deg, var(--edge), var(--bg-raised));
  box-shadow: inset 0 0 0 1px rgb(var(--select-rgb) / 0.35);
}

/* Scale rather than a downward nudge. A transform contributes to an element's
   scrollable overflow, and `translateY(1px)` on a button in a `.setup` whose
   content already fits exactly pushed its scrollHeight one pixel past its
   height — so a scrollbar flashed on the right for as long as the button was
   held. Scaling *down* stays inside the original box and cannot do that. */
.btn:active:not(:disabled) {
  transform: scale(0.98);
}

.btn:focus-visible {
  outline: 2px solid var(--select);
  outline-offset: 2px;
}

/* The affirmative action on a screen: yellow, the accent colour, because it
   is the one thing that should be found without reading. */
.btn-primary {
  color: var(--accent-ink);
  background: linear-gradient(180deg, var(--accent-hi), var(--accent-lo));
  border-color: var(--accent-edge);
  font-weight: 700;
}

.btn-primary:hover:not(:disabled) {
  background: linear-gradient(180deg, var(--accent-hover-hi), var(--accent-hover-lo));
  box-shadow: 0 0 20px rgb(var(--accent-rgb) / 0.4);
}

.btn-ghost {
  background: transparent;
  border-color: transparent;
  color: var(--ink-dim);
}

.btn-ghost:hover:not(:disabled) {
  background: rgb(var(--surface-rgb) / 0.6);
  color: var(--ink);
}

.btn-wide {
  width: 100%;
}

.btn-small {
  min-height: 36px;
  padding: var(--s1) var(--s3);
  font-size: var(--t-sm);
  --chamfer-sm: 6px;
}

/* A chosen option in a setup row. Cyan means "selected" everywhere in the app. */
.btn.selected {
  color: var(--select-ink);
  border-color: var(--select);
  background: var(--select-fill);
  box-shadow:
    0 0 18px rgb(var(--select-rgb) / 0.25),
    inset 0 0 12px rgb(var(--select-rgb) / 0.12);
}

/* A selected control must keep its fill under the cursor.
   `.btn:hover:not(:disabled)` scores (0,3,0) against `.btn.selected`'s (0,2,0),
   so without this the hover background wins and the selection disappears while
   being pointed at — which on a light theme meant the chip you had just clicked
   turned into the hover gradient. Hover still reads, through the glow. */
.btn.selected:hover:not(:disabled) {
  background: var(--select-fill);
  box-shadow:
    0 0 24px rgb(var(--select-rgb) / 0.4),
    inset 0 0 12px rgb(var(--select-rgb) / 0.12);
}

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

.icon-btn {
  min-width: var(--tap);
  min-height: var(--tap);
  display: grid;
  place-items: center;
  font-size: var(--t-lg);
  color: var(--ink);
  background: transparent;
  border: none;
  border-radius: var(--radius);
  cursor: pointer;
}

.icon-btn:hover {
  background: rgb(var(--surface-rgb) / 0.7);
  color: var(--select);
}

.icon-btn:focus-visible {
  outline: 2px solid var(--select);
  outline-offset: -2px;
}

/* --- Menu rows -----------------------------------------------------------
   A menu entry is a full-width bar with a yellow left rule; hovering or
   selecting it washes cyan across the bar. This is the reference's
   main-menu behaviour. */
.menu-list {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  width: min(100%, 380px);
}

.menu-list > .btn {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  text-align: left;
  text-transform: uppercase;
  padding-left: var(--s4);
  border-left: 3px solid var(--accent);
  overflow: hidden;
}

.menu-list > .btn::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 0;
  background: linear-gradient(90deg, rgb(var(--select-rgb) / 0.35), transparent 85%);
  transition: width 0.18s ease;
}

.menu-list > .btn:hover:not(:disabled)::before,
.menu-list > .btn.selected::before {
  width: 100%;
}

.menu-list > .btn:hover:not(:disabled),
.menu-list > .btn.selected {
  color: var(--ink);
  border-left-color: var(--select);
}

.menu-list > .btn.btn-primary {
  border-left-color: var(--accent-ink);
}

/* A ghost button is transparent, which reads as *disabled* once it is a
   full-width menu row on a dark ground rather than a small link. In a menu it
   keeps a panel behind it and only steps its text down a shade. */
.menu-list > .btn.btn-ghost {
  border-left-color: var(--edge-bright);
  color: var(--ink);
  background: linear-gradient(180deg, rgb(var(--surface-rgb) / 0.55), rgb(var(--panel-rgb) / 0.55));
  border-color: var(--edge);
}

.menu-list > .btn.btn-ghost:hover:not(:disabled) {
  background: linear-gradient(180deg, var(--edge), var(--bg-raised));
}

/* The row's own content has to sit above the cyan wash, which is a
   pseudo-element on the same stacking context. */
.menu-list > .btn > * {
  position: relative;
}

/* On the primary button the sub-line sits on the *accent*, not on a panel, so
   --ink-dim is the wrong colour for it — that is picked to read against the
   panel. --accent-ink is the one guaranteed to contrast with the accent (the
   theme tests hold it to 4.5:1), softened rather than recoloured. This was
   already weak on Nightfall's yellow; the light themes made it obvious. */
.btn-primary .row-sub {
  color: var(--accent-ink);
  opacity: 0.75;
}

/* A second line of explanation under a menu row, for entries whose meaning is
   not obvious from two words. */
.row-sub {
  display: block;
  margin-top: 2px;
  font-family: var(--body);
  font-size: var(--t-sm);
  font-weight: 500;
  letter-spacing: 0.02em;
  text-transform: none;
  color: var(--ink-dim);
}

/* --- Setup screens -------------------------------------------------------- */

.setup {
  width: min(100%, 420px);
  max-height: 100%;
  overflow-y: auto;
  /* Nothing here is meant to scroll sideways. Without this a control that
     overshoots by a pixel or two — a range input's default margin did exactly
     that — puts a horizontal scrollbar across the bottom of the screen.
     Content that genuinely needs width scrolls inside its own box instead
     (see `.matches-scroll`). */
  overflow-x: hidden;
  /* Room for the scrollbar so it never sits on top of the text. */
  padding-right: var(--s1);
}

/* Section headings carry a solid yellow tick, the reference's way of
   labelling a group. */
.setup h3,
.prose h3 {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin: var(--s4) 0 var(--s2);
  font-family: var(--display);
  font-size: var(--t-sm);
  font-weight: 700;
  color: var(--ink);
  text-transform: uppercase;
  letter-spacing: 0.12em;
}

.setup h3::before,
.prose h3::before {
  content: '';
  width: 4px;
  height: 1em;
  background: var(--accent);
  flex: 0 0 auto;
}

/* A section that speaks instead of showing — "Objective" is one sentence and no
   board. Set as body copy rather than as a caption: it is the first thing on
   the screen and the one line that says what the game is for. */
.prose .section-intro {
  margin: 0 0 var(--s3);
  color: var(--prose-ink);
  font-size: var(--t-md);
  line-height: 1.5;
}

/* A heading that follows prose draws its own rule above itself. Every other
   heading on the screen already has one -- the card above it ends with a
   `.lesson` border -- so "5 Basic Rules", sitting directly under the
   objective's sentence, was the only group that ran on from the one before it
   without a break. */
.prose .section-intro + h3 {
  padding-top: var(--s3);
  border-top: 1px solid var(--edge);
}

.setup h3:first-child {
  margin-top: 0;
}

.option-row {
  display: flex;
  gap: var(--s2);
  flex-wrap: wrap;
}

.option-row .btn {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: var(--s2);
}

.option-row .btn small {
  font-family: var(--body);
  font-size: var(--t-sm);
  font-weight: 500;
  letter-spacing: 0.02em;
  text-transform: none;
  color: var(--ink-dim);
}

/* "Quick" / "Medium" / "Official" sits *inside* the selected chip, so it takes
   the chip's own ink rather than the selection colour — the same mistake the
   primary button's sub-line had. Softened with opacity, not recoloured, so it
   keeps whatever contrast the label above it was measured to have. */
.option-row .btn.selected small {
  color: var(--select-ink);
  opacity: 0.8;
}

/* The padlock and the way out of it, beside a heading rather than under it —
   the whole section is what is locked, and a note below would be a note about
   a block of controls that is not rendered at all. Set as body copy, because
   the heading's own display face is uppercase and letter-spaced and this is a
   sentence. */
.setting-lock {
  margin-left: var(--s1);
  font-family: var(--body);
  font-size: var(--t-sm);
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-dim);
}

/* --- Handicap ------------------------------------------------------------- */

/* Extra opening dots, one row per side. A stepper rather than a row of chips
   because the range runs to nine and ten chips would wrap into three lines on a
   phone -- and because the two rows have to line up with each other, which a
   wrapping row cannot promise. */
.handicap {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}

.handicap-row {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

/* The name takes the slack, so both steppers sit hard against the right edge
   and the two numbers are directly above one another. */
.handicap-who {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--display);
  font-size: var(--t-sm);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink);
}

.handicap-step {
  flex: 0 0 auto;
  width: 2.75rem;
  padding: var(--s2) 0;
  font-size: var(--t-md);
  line-height: 1;
}

/* Fixed width, because a stepper whose buttons shuffle sideways between 9 and
   10 is a stepper that is hard to press twice. */
.handicap-value {
  flex: 0 0 auto;
  min-width: 2.5rem;
  text-align: center;
  font-family: var(--display);
  font-size: var(--t-md);
  font-weight: 700;
  color: var(--ink);
}

.handicap-note {
  margin: 0 0 var(--s1);
}

.hint {
  margin: var(--s2) 0 0;
  color: var(--ink-dim);
  font-size: var(--t-sm);
  line-height: 1.55;
}

/* --- Prose ---------------------------------------------------------------- */

/* The one screen whose content legitimately scrolls, inside itself. */
.prose {
  width: min(100%, 560px);
  max-height: 100%;
  overflow-y: auto;
  line-height: 1.6;
  font-size: var(--t-md);
}

/* A call to action above a `.prose` block. It needs `flex: 0 0 auto` because
   the prose beside it scrolls and will otherwise take every pixel: as a second
   `.prose` this was squeezed to a ten-pixel strap with its label clipped away,
   which looked like a broken divider rather than a button.

   The rule under it is the one thing that says the page continues. Without it
   the button sat hard against the first heading and the screen read as ending
   there, with no hint that the rules were below and scrollable. */
.lead-action {
  flex: 0 0 auto;
  width: min(100%, 560px);
  margin-bottom: var(--s2);
  padding-bottom: var(--s3);
  border-bottom: 1px solid var(--edge);
}

.lead-action .btn {
  width: 100%;
}

.prose p,
.prose ul {
  margin: 0 0 var(--s3);
  color: var(--prose-ink);
}

.prose li {
  margin-bottom: var(--s2);
}

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

/* --- Play screen ---------------------------------------------------------- */

/* The score row stays hard under the topbar, where it has always been. Only
   `.play-stage` below it is centred. */
#screen-play {
  justify-content: flex-start;
  gap: var(--s2);
  padding: var(--s2);
}

.hud {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  width: 100%;
  padding: var(--s2) var(--s3);
  background: linear-gradient(180deg, rgb(var(--surface-rgb) / 0.9), rgb(var(--panel-rgb) / 0.65));
  border: 1px solid var(--edge);
  font-size: var(--t-sm);
  flex: 0 0 auto;
  clip-path: polygon(
    var(--chamfer-sm) 0,
    100% 0,
    100% calc(100% - var(--chamfer-sm)),
    calc(100% - var(--chamfer-sm)) 100%,
    0 100%,
    0 var(--chamfer-sm)
  );
}

.score {
  display: flex;
  align-items: center;
  gap: var(--s1);
  font-family: var(--display);
  font-weight: 600;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}

.score .dot {
  width: 0.75rem;
  height: 0.75rem;
  border-radius: 50%;
  flex: 0 0 auto;
}

.score.player-1 .dot {
  background: var(--hud-p1, var(--p1));
  box-shadow: 0 0 10px var(--hud-p1, var(--p1));
}

.score.player-2 .dot {
  background: var(--hud-p2, var(--p2));
  box-shadow: 0 0 10px var(--hud-p2, var(--p2));
}

.turn-indicator {
  font-family: var(--display);
  font-weight: 600;
  font-size: var(--t-sm);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--select);
  text-align: center;
}

.hud-puzzle {
  flex-direction: column;
  gap: var(--s1);
  text-align: center;
}

/* The tutorial's instruction line. The height is FIXED, and that is the whole
   point of it: the play screen is a grid, so a step whose text runs to three
   lines where the one before it ran to two would resize the board area -- and
   the board answers a resize by re-fitting its zoom, which would silently throw
   away the pinch the last step exists to teach. Three lines is enough for the
   longest step; a fourth would be a step that needs rewriting. */
.hud-tutorial {
  flex-direction: column;
  align-items: stretch;
  justify-content: center;
  gap: var(--s1);
  text-align: left;
  /* Fixed, and that is the point of it: the play screen is a grid, so a step
     whose text wraps to one more line than the last would resize the board
     area -- and the board answers a resize by re-fitting its zoom, which would
     throw away the pinch. Measured against the longest step with the
     confirmation line showing -- 110px on a 375px screen, so 8em at 115px
     leaves a margin. If a new step needs more room, shorten the step. */
  min-height: 8em;
}

.tutorial-text {
  font-size: var(--t-sm);
  line-height: 1.35;
  color: var(--prose-ink);
}

/* Only ever shown once the step's move has actually been made, so it is the
   board saying "that was it" rather than a label that was always there. Set at
   the title's size, not the detail's: at `--t-sm` it read as a footnote to the
   instruction, when it is the answer to it. */
.tutorial-done {
  font-size: var(--t-md);
  font-weight: 700;
  color: var(--good);
}

.puzzle-prompt {
  margin: 0;
  font-size: var(--t-md);
}

.puzzle-status {
  margin: 0;
  min-height: 2.4em;
  font-size: var(--t-sm);
  color: var(--ink-dim);
}

.puzzle-status.good {
  color: var(--good);
}

.puzzle-status.bad {
  color: var(--bad);
}

.puzzle-actions {
  display: flex;
  gap: var(--s2);
  flex-wrap: wrap;
  justify-content: center;
  flex: 0 0 auto;
}

/* The board fills whatever space the stage has left, and must stay square.
   `flex: 1 1 auto` on the viewport itself does not do that: flex-grow sets the
   height directly, which overrides `aspect-ratio` and leaves the skin painting
   a tall rectangle behind a square board. So the *area* takes the leftover
   height and the viewport is sized to the largest square that fits inside it. */
/* The board and its bar, centred in whatever height is left under the score
   row. The screen itself stays top-aligned so that row does not drift down
   with them. */
.play-stage {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
}

/* A square box rather than "whatever height is going". It used to be `flex: 1`
   with the board centred inside it, so on a tall screen a 400px board sat in a
   765px area — 180px of nothing above it and 180px below, which pushed the
   review controls to the bottom of the screen instead of under the board.
   Sized from the width, clamped by the height, and shrinkable: the viewport
   inside still measures `min(100cqw, 100cqh)`, so a short screen scales the
   board down exactly as before. The spare height now falls outside the stack,
   which `.screen` centres. */
.board-area {
  flex: 0 1 auto;
  min-height: 0;
  width: 100%;
  aspect-ratio: 1;
  max-height: 100%;
  display: grid;
  place-items: center;
  container-type: size;
}

/* Its surface and frame come from the active skin, set as custom properties
   from JS. */
.board-viewport {
  position: relative;
  /* Falls back to the full width where container units are unsupported. */
  width: 100%;
  width: min(100cqw, 100cqh);
  aspect-ratio: 1;
  max-width: 100%;
  max-height: 100%;
  overflow: hidden;
  background: var(--board-surface);
  border: 2px solid var(--board-frame);
  box-shadow: 0 0 24px rgb(var(--select-rgb) / 0.15);
  touch-action: none; /* we handle drag and pinch ourselves */
}

canvas#board,
canvas#board-marks {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
  will-change: transform;
}

/* The tutorial's pointing layer. Transparent everywhere the arrow is not, so
   the board shows through, and never in the way of a tap -- the viewport under
   it is what listens. */
canvas#board-marks {
  pointer-events: none;
}

/* --- Overlays ------------------------------------------------------------- */

.overlay {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  padding: var(--s3);
  background: rgb(var(--scrim-rgb) / 0.82);
  backdrop-filter: blur(3px);
  animation: fade-in 0.14s ease-out;
  z-index: 10;
}

.panel {
  width: min(100%, 360px);
  /* A panel taller than the screen would otherwise run off both ends with no
     way to reach its buttons — the level detail is the one that can get long,
     and a short phone in landscape is where it bites. */
  max-height: 100%;
  overflow-y: auto;
  padding: var(--s4);
  background: linear-gradient(180deg, var(--bg-panel), rgb(var(--panel-lo-rgb)));
  border: 1px solid var(--edge-bright);
  box-shadow: 0 0 40px rgb(var(--select-rgb) / 0.12);
  text-align: center;
  clip-path: polygon(
    var(--chamfer) 0,
    100% 0,
    100% calc(100% - var(--chamfer)),
    calc(100% - var(--chamfer)) 100%,
    0 100%,
    0 var(--chamfer)
  );
}

.panel h2 {
  margin: 0 0 var(--s3);
  padding-bottom: var(--s2);
  font-family: var(--display);
  font-size: var(--t-lg);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  border-bottom: 1px solid var(--edge);
}

.panel p {
  margin: 0 0 var(--s3);
  color: var(--ink-dim);
}

.panel .menu-list {
  width: 100%;
}

/* --- Developer Mode ------------------------------------------------------- */

.dev-readout {
  margin-top: var(--s1);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--t-sm);
  color: var(--ink-dim);
}

.dev-moves {
  width: 100%;
  margin-bottom: var(--s2);
  padding: var(--s2);
  border: 1px solid var(--edge);
  background: var(--bg-deep);
  color: var(--ink);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--t-sm);
  white-space: pre;
  overflow: auto;
  resize: vertical;
}

/* Four jumps and a counter. At 375px they used to wrap wherever they ran out,
   which put Next and Last on a second row and left the counter stranded at the
   end of the first — the reading order broken exactly where the order is the
   whole point. The counter takes its own line instead, and the four buttons
   keep theirs, on a narrower padding than a button gets elsewhere. */
.dev-review {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  flex-wrap: wrap;
}

.dev-review .btn-small {
  padding-inline: var(--s1);
}

.dev-review-count {
  order: -1;
  flex-basis: 100%;
}

.dev-review-count {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--t-sm);
  color: var(--ink-dim);
  min-width: 7ch;
  text-align: center;
}

/* Unmistakable when the board is showing the past rather than the game. */
.reviewing #board-viewport {
  outline: 2px dashed var(--p1);
  outline-offset: 4px;
}

/* --- Previous Matches ---------------------------------------------------- */

/* The table needs the full column width, not the 420px the setup screens use.
 *
 * It also owns its own scrolling, both ways, which is why `.setup`'s vertical
 * scroll is turned off here. When the box that scrolls sideways is as tall as
 * the whole table, its horizontal scrollbar sits at the bottom of the *table*
 * — below the fold, reachable only by scrolling to the last row — and the
 * columns off to the right look like they are simply not there. Bounding the
 * box puts that scrollbar at the bottom of what you can see, always. */
#screen-matches .setup {
  width: 100%;
  display: flex;
  flex-direction: column;
  overflow-y: hidden;
  /* A flex child will not shrink below its content without this, which would
     hand the height back to the table and undo the whole point. */
  min-height: 0;
}

/* Wide content scrolls inside its own box, so the page body never does.
 * `flex: 0 1 auto` rather than `1`: a short list keeps its natural height
 * instead of stretching an empty bordered box down the screen. */
.matches-scroll {
  overflow: auto;
  flex: 0 1 auto;
  min-height: 0;
  border: 1px solid var(--edge);
}

.matches-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-sm);
  white-space: nowrap;
}

/* The header stays put while the rows scroll under it.
 *
 * `box-shadow` rather than `border-bottom` for the rule beneath it, and that is
 * not a preference: with `border-collapse: collapse` the table owns the cell
 * borders, not the cell, so a stuck `th` leaves its border behind at the top of
 * the table and the header sits on the rows with nothing between them. An inset
 * shadow is painted by the cell itself and travels with it.
 *
 * The background must stay opaque for the same reason — it is the only thing
 * hiding the rows passing underneath. */
.matches-table th {
  position: sticky;
  top: 0;
  z-index: 1;
  text-align: left;
  padding: var(--s2);
  color: var(--ink-dim);
  font-family: var(--display);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  box-shadow: inset 0 -1px 0 var(--edge);
  background: var(--bg-raised);
}

.matches-table td {
  padding: var(--s2);
  border-bottom: 1px solid var(--edge);
}

.matches-table tbody tr:last-child td {
  border-bottom: 0;
}

.matches-table tbody tr {
  cursor: pointer;
}

.matches-table tbody tr:hover {
  background: var(--bg-raised);
}

/* The match number and the engine are what a game is identified by later, so
   they are the two columns that must stay legible at a glance. */
.matches-table td:first-child {
  font-variant-numeric: tabular-nums;
  color: var(--ink-dim);
}

/* The one column worth scanning down. Blue for a win, red for a loss, and
   nothing for a draw or a local game — `is-none` deliberately leaves the row's
   own colour alone rather than picking a side between two players at one
   device. */
.matches-table td.outcome {
  font-weight: 700;
  letter-spacing: 0.04em;
}

.matches-table td.outcome.is-win {
  color: var(--p2);
}

.matches-table td.outcome.is-loss {
  color: var(--bad);
}

.matches-empty {
  color: var(--ink-dim);
  font-size: var(--t-sm);
  margin-top: var(--s3);
}

/* A colour choice in a settings row: the swatch is drawn from the live theme
   token, so it always shows the colour the board will actually use. */
.choice-dot {
  display: inline-block;
  width: 0.8em;
  height: 0.8em;
  margin-right: 0.45em;
  border-radius: 50%;
  vertical-align: -0.05em;
}

/* A button that is nothing but its swatch. The dot grows to carry the control
   on its own and loses the gap it only needed to stand off a label. */
.btn.is-swatch .choice-dot {
  width: 1.5em;
  height: 1.5em;
  margin: 0;
  vertical-align: -0.35em;
}

/* The About screen is prose rather than controls, so it reads as a page. */
.prose-about p {
  margin: 0 0 var(--s3);
  color: var(--prose-ink);
  font-size: var(--t-md);
  line-height: 1.55;
}

/* What the puzzle is asking for, and how much room is left — stated above the
   board rather than buried in the prompt. The count is the half that changes,
   so it takes the accent while the objective stays plain ink. */
.puzzle-goal {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s2);
  width: 100%;
  font-family: var(--display);
  font-weight: 700;
}

.puzzle-objective {
  font-size: var(--t-md);
  color: var(--ink);
  letter-spacing: 0.04em;
}

.puzzle-moves {
  font-size: var(--t-sm);
  color: var(--accent);
  white-space: nowrap;
}

/* Retry restarts the puzzle rather than stepping through it, so it takes its
   own row under the jumps instead of sitting in the middle of them. */
.puzzle-actions .puzzle-retry {
  flex-basis: 100%;
  margin-inline: auto;
  max-width: 14rem;
}

/* Developer Mode's per-move search report.
 *
 * Scrolls in its own box, both ways. The play screen is a fixed grid that never
 * scrolls, and a report is twelve wide rows of monospace — without `overflow`
 * here it would either stretch the grid or push a horizontal scrollbar across
 * the whole app, which a stray two pixels on a range input has already done
 * once in this project.
 *
 * `pre` rather than a div because the table is aligned with spaces, and
 * `white-space: pre` keeps the columns without needing a real table. */
.dev-report {
  margin: var(--s1) 0 0;
  padding: var(--s1);
  /* Half the screen was too much of it: the board is what a player needs to
   * see while the search report is only read between moves, and it scrolls. */
  max-height: 20vh;
  overflow: auto;
  /* Both of these, and neither is optional. A `pre` is sized by its content,
   * and a flex or grid child's default `min-width: auto` is its content's
   * intrinsic width — so a twelve-column monospace table widens its parent
   * and puts a horizontal scrollbar across the whole app instead of scrolling
   * inside its own box. That is the same trap the shop cards and the range
   * input already hit here. `overflow: auto` alone does not prevent it. */
  min-width: 0;
  max-width: 100%;
  white-space: pre;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
  line-height: 1.45;
  color: var(--ink-dim);
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

/* The skin picker on the setup screens: a name between two arrows. Eight board
   previews will not fit across a phone, and the shop is where a skin is looked
   at — here it only has to be named and changed. */
.skin-pick {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

/* The name takes the slack, so the two arrows sit at the ends of the row and
   the name stays centred however long it is. */
.skin-pick-name {
  flex: 1 1 auto;
  min-width: 0;
  text-align: center;
  font-family: var(--display);
  font-size: var(--t-sm);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink);
}

.btn.skin-step {
  flex: 0 0 auto;
  min-width: 2.6em;
  padding-inline: var(--s2);
  font-size: var(--t-md);
  line-height: 1;
}

/* --- Signing in ----------------------------------------------------------
   While a Google sign-in is finished, the title screen shows one line and no
   buttons. It used to show every button for about a second -- long enough to
   press Continue with Google again. */
.auth-status {
  display: none;
  margin: 0;
  text-align: center;
  color: var(--ink-dim);
}

html.signing-in .auth > :not(.auth-status) {
  display: none;
}

html.signing-in .auth-status {
  display: block;
}
