/* Grandpa's Checkers — the web client.
 *
 * The desktop build keeps its own, older-looking interface. This one does
 * not: it is the page strangers land on from a link, and most of them on a
 * phone, so it is built for being read and tapped.
 *
 * What that means in practice: a system font at a size somebody with tired
 * eyes can read, touch targets you do not have to aim at, real contrast, and
 * the board as the only thing competing for attention. The board itself keeps
 * its cream and green, because that is what a checkers board looks like.
 */

:root {
  /* Surfaces, from the page up. */
  --ink: #16181d;
  --ink-soft: #5b6270;
  --ink-faint: #8b93a1;
  --page: #171a21;
  --page-2: #1f232c;
  --card: #ffffff;
  --card-2: #f4f6f9;
  --line: #e2e6ec;

  /* The red of the pieces, used for anything that is the next thing to do. */
  --accent: #d6402f;
  --accent-dark: #a62a1c;
  --accent-soft: #fdecea;

  /* Darker than it looks like it needs to be: this green is only ever read on
     a white card, and the brighter #1f8a4c came to 4.37:1 there — under AA by
     a hair, which is the worst place to be. This is 4.97:1. */
  --good: #1b8046;
  --warn: #b8860b;

  /* Slate and ivory: a cool dark square makes the warm red and the cream
     pieces carry. */
  --board-light: #e9e5d8;
  --board-dark: #44506a;
  --board-frame: #2a3142;
  --red-a: #e0483c;
  --red-b: #9c1a13;
  --white-a: #fbf8ee;
  --white-b: #bdb49a;
  --highlight: #ffd34d;

  --radius: 14px;
  --radius-sm: 9px;
  --shadow: 0 1px 2px rgba(16, 20, 28, .06), 0 8px 24px rgba(16, 20, 28, .10);
  --shadow-lg: 0 10px 40px rgba(0, 0, 0, .35);

  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
          Arial, "Noto Sans", sans-serif;
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  font-family: var(--font);
  /* 16px. Small text is the first thing to go wrong for an older reader. */
  font-size: 16px;
  line-height: 1.45;
  color: var(--ink);
  background: radial-gradient(1200px 800px at 50% -10%, var(--page-2), var(--page));
  -webkit-font-smoothing: antialiased;
}

/* The game is a fixed layout the height of the window (.app), and the few
   lines about it (.about) are a scroll below it, for somebody arriving from
   a search. The board is measured against the window, and a scrollbar
   appearing would change the measurement it was sized from, so the room for
   one is kept whether or not it shows. Sideways nothing scrolls. On a touch
   screen a swipe on the board never moves the page (.board, below). */
html {
  overflow-x: hidden;
  overflow-y: auto;
  scrollbar-gutter: stable;
}
/* Not a scroller of its own: with overscroll-behavior: none (below) one
   would keep the wheel from ever reaching the page. */
body {
  height: auto;
  min-height: 100%;
}

/* No line ends with one word left alone on it (owner, 4 November 2026:
   "never leave one word dangling"): headings, titles and the words on
   buttons are balanced, and running text is set "pretty", which keeps a
   paragraph's last line from being a single word. */
h1, h2, h3, h4, button, label, figcaption, .dialog-title, .agreement-title { text-wrap: balance; }
p, li, dd, td, blockquote { text-wrap: pretty; }

/* The written pages are documents, and documents scroll.

   They share this stylesheet with the game and so inherited its lock. It did
   not show while the licences were a box with its own scrollbar inside a page
   that fitted; rendering them properly made the page eight thousand pixels
   tall, and none of it could be reached. Terms and privacy had the same fault
   waiting for a short enough window.

   Scoped from the ancestor rather than by putting a class on every page, so
   that a fourth one cannot be added without it. */
html:has(> body.page),
html:has(> body.page) body {
  height: auto;
  overflow: visible;
}

.app {
  display: grid;
  /* Three children: the header, the playing area, the status strip. The chat
     used to be a fourth and is now a column inside the playing area, so this
     came down from four rows. A row declared for a child that is not there
     is taken out of the 1fr that the board lives in — see .board-area, where
     the same mistake has its own paragraph. */
  grid-template-rows: auto minmax(0, 1fr) auto;
  height: 100vh;
  /* The small viewport: the screen with the browser's bars showing, which
     does not change as you scroll. 100dvh grew as Safari tucked its bars
     away on a scroll down to the lines under the game, and the board grew
     and shrank with it (owner, 10 November 2026: "the board changes size
     when I scroll"). */
  height: 100svh;
  max-width: 1180px;
  margin: 0 auto;
  padding: 0 14px 10px;
  gap: 10px;
}

/* ---------- Header ---------- */

/* One line: the name on the left, the menus beside it, the connection light
   pushed to the far right. */
.topbar {
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 12px 4px 2px;
  min-width: 0;
}

.titlebar {
  display: flex;
  align-items: center;
  gap: 10px;
  color: #fff;
  flex: none;
}

.titlebar-icon {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: radial-gradient(circle at 34% 28%, #ff7a6d, var(--red-a) 45%, var(--red-b) 100%);
  box-shadow: 0 1px 3px rgba(0, 0, 0, .5);
  flex: none;
}

.titlebar-text {
  margin: 0;
  font-size: 19px;
  font-weight: 650;
  letter-spacing: -0.01em;
}

/* Sound on and off (index.html): a speaker, crossed out while muted, in the
   room between the menus and the connection. */
.sound-button {
  margin-left: auto;
  flex: none;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  padding: 0;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, .12);
  background: rgba(255, 255, 255, .08);
  color: rgba(255, 255, 255, .8);
  cursor: pointer;
}
.sound-button:hover { background: rgba(255, 255, 255, .14); }
.sound-button:focus-visible { outline: 3px solid var(--highlight, #ffd34d); outline-offset: 2px; }
.sound-button svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.sound-button .speaker { fill: currentColor; stroke: none; }
.sound-button[aria-pressed="true"] .cross,
.sound-button[aria-pressed="false"] .waves { display: none; }

.titlebar-status {
  margin-left: 8px;
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 13px;
  color: rgba(255, 255, 255, .72);
  background: rgba(255, 255, 255, .08);
  border: 1px solid rgba(255, 255, 255, .12);
  padding: 4px 11px;
  border-radius: 999px;
}

.dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--ink-faint);
  flex: none;
}
.dot-on { background: #3ddc84; box-shadow: 0 0 8px rgba(61, 220, 132, .7); }
.dot-off { background: #ff6b5e; }

/* ---------- Menus ---------- */

.menubar {
  display: flex;
  gap: 6px;
  min-width: 0;
}

.menu { position: relative; }

.menu-title {
  font: inherit;
  font-size: 14px;
  font-weight: 550;
  color: rgba(255, 255, 255, .8);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 999px;
  /* A target you do not have to aim at. */
  padding: 8px 15px;
  cursor: pointer;
  transition: background .15s, color .15s;
}
.menu-title u { text-decoration: none; }
@media (hover: hover) { .menu-title:hover { background: rgba(255, 255, 255, .1); color: #fff; } }
.menu.open .menu-title { background: rgba(255, 255, 255, .16); color: #fff; }

.menu-items {
  display: none;
  position: absolute;
  z-index: 40;
  top: calc(100% + 6px);
  left: 0;
  min-width: 232px;
  margin: 0;
  padding: 7px;
  list-style: none;
  background: var(--card);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
}
.menu.open .menu-items { display: block; }

.menu-items button {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  font: inherit;
  font-size: 15px;
  text-align: left;
  color: var(--ink);
  background: none;
  border: 0;
  border-radius: 7px;
  padding: 10px 12px;
  cursor: pointer;
}
@media (hover: hover) { .menu-items button:hover:not(:disabled) { background: var(--card-2); } }
/* Links to the written pages, dressed as the items around them. */
.menu-items a {
  display: flex;
  font-size: 15px;
  color: var(--ink);
  text-decoration: none;
  border-radius: 7px;
  padding: 10px 12px;
}
@media (hover: hover) { .menu-items a:hover { background: var(--card-2); } }
.menu-items button:disabled { color: var(--ink-faint); cursor: default; }
.menu-items .sep {
  height: 1px;
  margin: 6px 4px;
  background: var(--line);
}
.check { width: 1em; color: var(--accent); font-weight: 700; }

/* ---------- The table ---------- */

.table {
  display: grid;
  /* Both players down the left, the board, the messages down the right.
     Having a player either side of the board read well and cost two columns
     to say four short lines in each, while the chat was a strip along the
     bottom with a log two messages tall. One column each way says the same
     thing and hands the board the height the strip was using. */
  grid-template-columns: 200px minmax(0, 1fr) 236px;
  gap: 14px;
  min-height: 0;
  align-items: stretch;
}

/* The opponent level with the top of the board and you level with the bottom
   of it, which is how the two of you are sitting on the board itself.
   
   The column is as tall as the whole board area, and that includes the
   caption below the board — so without the foot the bottom panel finished
   thirty-one pixels below the board's edge, level with a line of text instead
   of with the playing surface. */
.players {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 12px;
  min-height: 0;
  padding-bottom: calc(var(--caption-h) + var(--caption-gap));
}

/* ---------- Players ---------- */

.player-panel {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 16px 14px;
  min-width: 0;
  background: var(--card);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  transition: box-shadow .2s;
}

/* Whose turn it is, said with light rather than a label alone. The ring is
   green and not the accent red, because red is also a player: the human plays
   Red, so a red ring round White's panel said "Red" and "White to move" in
   the same breath. */
.player-panel.active {
  box-shadow: var(--shadow), 0 0 0 2px var(--good);
}

.player-name-plate {
  width: 100%;
  text-align: center;
  font-weight: 650;
  font-size: 15px;
  padding: 7px 10px;
  border-radius: 999px;
  color: #fff;
}
.player-name-plate.red { background: linear-gradient(180deg, #e8564a, var(--red-b)); }
.player-name-plate.white { background: linear-gradient(180deg, #8d8878, #5f5b4f); }

.player-name {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.player-piece { padding: 2px; }
.player-piece .piece { width: 46px; height: 46px; }

.player-tally {
  font-size: 14px;
  color: var(--ink-soft);
  min-height: 1.4em;
  white-space: nowrap;
}

/* A fixed height, in pixels not ems, because the states below are different
   sizes — "Your Turn" is 19px and loud, the rest are smaller — and a height
   measured in ems grew and shrank the whole panel every time the turn
   changed. The box is now tall enough for the loudest state and holds that
   height for all of them, with the text centred in it. */
.player-turn {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  font-weight: 650;
  color: var(--good);
  min-height: 26px;
}

/* The three states the client sets here. For a while it set them and the
   stylesheet had none of them, so "Your Turn", "Opponent's Turn", "Thinking…"
   and "Disconnected…" all came out as the same small green line — four
   different situations wearing one face.

   Being told it is your move is the whole reason to look at this panel, so it
   is the largest thing in it. The other two are deliberately quieter, and
   told apart by colour rather than by size alone: a lost connection is not
   the same news as somebody taking their time. */
.player-turn.your-turn {
  font-size: 19px;
  font-weight: 700;
  line-height: 1.2;
  color: var(--good);
}

.player-turn.waiting {
  font-size: 14px;
  font-weight: 600;
  color: var(--ink-soft);
}

.player-turn.offline {
  font-size: 14px;
  font-weight: 700;
  color: var(--accent-dark);
}

/* The move clock for whoever is on it. A fixed height so showing or hiding it
   never resizes the panel, monospaced so the digits do not jiggle as they
   count. */
.player-clock {
  min-height: 20px;
  font-variant-numeric: tabular-nums;
  font-size: 15px;
  font-weight: 650;
  color: var(--ink-soft);
}
.player-clock.urgent { color: var(--accent-dark); }

.player-actions {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
}

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

.btn {
  font: inherit;
  font-size: 15px;
  font-weight: 550;
  color: var(--ink);
  background: var(--card-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 11px 16px;
  cursor: pointer;
  transition: background .15s, border-color .15s, transform .05s;
}
@media (hover: hover) { .btn:hover:not(:disabled) { background: #e9edf3; border-color: #cfd6e0; } }
.btn:active:not(:disabled) { transform: translateY(1px); }
.btn:disabled { color: var(--ink-faint); background: var(--card-2); cursor: default; opacity: .6; }

.btn.primary {
  color: #fff;
  background: var(--accent);
  border-color: var(--accent);
}
@media (hover: hover) { .btn.primary:hover:not(:disabled) { background: var(--accent-dark); border-color: var(--accent-dark); } }

/* ---------- Board ---------- */

.board-area {
  display: grid;
  /* The board takes whatever is left after the caption, so the caption can
     never squeeze it into a non-square shape — and, less obviously, so that
     the box below has a height at all. Laying this out as a centred flex
     column instead leaves that box sized to its content, its content sized
     from 100cqh, and 100cqh measured against a box with no height: the board
     resolves to nothing and renders as a dot.

     Two rows, and therefore exactly two children that may be in flow. A grid
     hands anything beyond that an implicit auto row, which it takes out of
     the board's 1fr — so every square gets smaller and nothing on the page
     says why. That has now happened three times, each time to somebody adding
     a notice below the board in good faith. Anything new either replaces a
     row here or is positioned out of flow, as .board-notices is. */
  grid-template-rows: minmax(0, 1fr) auto;
  gap: 10px;
  min-height: 0;
  min-width: 0;
  /* The containing block for those floating notices. */
  position: relative;
}

.board-fit {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  min-width: 0;
  /* Makes this box the reference for the cq units on the frame. */
  container-type: size;
}

.board-frame {
  aspect-ratio: 1 / 1;

  /* The board must fit the tighter of the two dimensions, or the squares stop
     being square and the pieces render as ovals. The first rule is the
     fallback for browsers without container query units; the second wins
     wherever they are supported. */
  width: min(100%, 70svh);
  width: min(100cqw, 100cqh);
  height: auto;
  margin-inline: auto;

  padding: 10px;
  background: linear-gradient(145deg, #39445c, var(--board-frame));
  border-radius: var(--radius);
  box-shadow: 0 12px 34px rgba(0, 0, 0, .45);
}

.board {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  grid-template-rows: repeat(8, 1fr);
  width: 100%;
  height: 100%;
  border-radius: 5px;
  overflow: hidden;
  position: relative;
}

/* The board stood down, while there is no connection to play over.
   It is dimmed and drained of colour rather than hidden or covered: the
   position is still worth looking at, and a board that has gone is alarming
   in a way that a board that has gone quiet is not. Nothing is laid over it,
   so a click still reaches the square underneath and the client can answer
   for itself why nothing happened. */
.board.standby {
  opacity: .62;
  filter: saturate(.3) brightness(.92);
  cursor: default;
}
.board.standby .sq,
.board.standby .sq.playable { cursor: default; }
.board.standby .sq.playable:hover { filter: none; }
.board.standby .sq.must > .piece { animation: none; }

.sq {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  /* A square is a <button>, and a button carries a browser default padding of
     1px 6px. That leaves the content box 10px narrower than it is tall, so the
     piece's 78% below resolves to a different number on each axis and the
     checkers come out oval. The square itself still looks square, which is
     what makes this one hard to spot. */
  padding: 0;
}

.sq.light { background: var(--board-light); }
.sq.dark { background: var(--board-dark); }
.sq.playable { cursor: pointer; }

/* The square a piece just came from and landed on. */
.sq.last::before {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(255, 211, 77, .28);
}

/* A piece the player has picked up. */
.sq.selected::after {
  content: "";
  position: absolute;
  inset: 0;
  box-shadow: inset 0 0 0 4px var(--highlight);
}

/* Somewhere the selected piece may go. */
.sq.target::after {
  content: "";
  position: absolute;
  width: 36%;
  height: 36%;
  border-radius: 50%;
  background: rgba(255, 255, 255, .9);
  box-shadow: 0 0 0 2px rgba(0, 0, 0, .35), 0 0 12px rgba(255, 255, 255, .55);
}

/* A destination that captures something. */
.sq.target.capture::after {
  width: 78%;
  height: 78%;
  background: rgba(255, 90, 70, .2);
  border: 3px solid rgba(255, 118, 98, .95);
  box-shadow: 0 0 10px rgba(255, 90, 70, .7);
}

@media (hover: hover) { .sq.playable:hover { filter: brightness(1.1); } }
.sq:focus-visible { outline: 3px solid #fff; outline-offset: -3px; }

.sq-number {
  position: absolute;
  z-index: 2;
  top: 1px;
  left: 1px;
  /* A small dark tab in the corner, over the piece rather than under it, so
     it reads on a dark square, a red piece and a white one alike. */
  font-size: 9px;
  line-height: 1.15;
  padding: 0 2px;
  border-radius: 3px;
  color: #fff;
  background: rgba(20, 24, 33, .62);
  pointer-events: none;
}

/* A piece that is obliged to move, because jumps are mandatory. */
.sq.must > .piece { animation: nudge 1.4s ease-in-out infinite; }

@keyframes nudge {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-7%); }
}

/* A square whose piece has just been taken.
 
   board.js has always added this class and the stylesheet never had a rule
   for it, so a capture removed the piece between one frame and the next with
   nothing to mark it — the comment above the code says it flashes the squares
   whose pieces were taken, and it did not. It cannot fade the piece, because
   the board has already been repainted without it by the time this is added;
   it marks the square it came from, which is the thing still there to mark. */
@keyframes taken {
  from { box-shadow: inset 0 0 0 3px rgba(255, 255, 255, .85); }
  to   { box-shadow: inset 0 0 0 3px rgba(255, 255, 255, 0); }
}
.sq.vanish { animation: taken .5s ease-out; }
.lite .sq.vanish, .board.standby .sq.vanish { animation: none; }

/* ---------- Pieces ---------- */

.piece {
  width: 78%;
  height: 78%;
  border-radius: 50%;
  position: relative;
  /* The stacked shadows give the disc its thickness and gloss. */
  box-shadow:
    0 3px 3px rgba(0, 0, 0, .45),
    inset 0 -3px 5px rgba(0, 0, 0, .35),
    inset 0 3px 5px rgba(255, 255, 255, .4);
}

.piece::before {
  /* The milled ring around the rim of a checker. */
  content: "";
  position: absolute;
  inset: 16%;
  border-radius: 50%;
  border: 2px solid rgba(0, 0, 0, .18);
}

.piece.red { background: radial-gradient(circle at 34% 28%, #ff7a6d, var(--red-a) 45%, var(--red-b) 100%); }
.piece.white { background: radial-gradient(circle at 34% 28%, #ffffff, var(--white-a) 45%, var(--white-b) 100%); }

.piece.king::after {
  /* Kings carry a crown. */
  content: "";
  position: absolute;
  inset: 22%;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  background-image: url("data:image/svg+xml;utf8,\
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 18'>\
<path d='M2 15h20l1.2-11-6 4.2L12 1 6.8 8.2.8 4z' fill='%23f2c33c' stroke='%238a6414' stroke-width='1.1' stroke-linejoin='round'/>\
<rect x='2' y='14.4' width='20' height='2.6' rx='1' fill='%23f2c33c' stroke='%238a6414' stroke-width='1.1'/>\
</svg>");
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .5));
}

/* ---------- Board caption ---------- */

/* The caption's height and the gap above it are named, because the players
   column has to leave exactly that much room at its foot to finish level with
   the board rather than with the caption underneath it. Two rules that have
   to agree on a number should not each be told it separately. */
:root {
  --caption-h: 21px;
  --caption-gap: 10px;
}

.board-caption {
  text-align: center;
  font-size: 15px;
  font-weight: 600;
  color: rgba(255, 255, 255, .88);
  min-height: var(--caption-h);
}
/* Above the phone breakpoint the players column reserves exactly one caption
   line (--caption-h) so the bottom panel finishes level with the board. A
   caption that wrapped to two lines broke that alignment, so here — where the
   board is wide enough that the real messages fit anyway — it is held to one
   line. On a phone the panels stack and the reservation does not apply, so the
   caption may wrap freely. */
@media (min-width: 861px) {
  .board-caption {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

/* ---------- Chat ---------- */

.chat {
  display: grid;
  /* A column beside the board now rather than a strip beneath it, so this
     stacks: the switch, then the log taking whatever height is going, then
     the picker. No fixed height — the column is as tall as the board. */
  grid-template-columns: minmax(0, 1fr);
  /* Four children, four rows: the switch, the log, the note saying why the
     picker is greyed, and the picker. The note is usually empty and hidden,
     which takes it out of flow and collapses its row to nothing. */
  grid-template-rows: auto minmax(0, 1fr) auto auto;
  gap: 10px;
  min-height: 0;
  padding: 12px;
  background: var(--card);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.chat-log-wrap {
  grid-row: 2;
  min-height: 0;
  overflow: hidden;
  background: var(--card-2);
  border-radius: var(--radius-sm);
}

.chat-log {
  height: 100%;
  overflow-y: auto;
  padding: 9px 11px;
  font-size: 14px;
}
.chat-log .system { color: var(--ink-soft); font-style: italic; }
.chat-log .who { font-weight: 650; }

/* The switch sits above the log, on one line, because stacked down a 104px
   column was the only reason that column existed. */
.chat-side {
  grid-row: 1;
  grid-column: 1;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 12px;
  min-height: 0;
}

.chat-side-title {
  font-size: 12px;
  font-weight: 650;
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: .04em;
}

.radio {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 14px;
  cursor: pointer;
}

.chat-note {
  grid-row: 3;
  grid-column: 1;
  margin: 0;
  font-size: 13px;
  color: var(--ink-soft);
}
.chat-note[hidden] { display: none; }

.chat-send { grid-row: 4; grid-column: 1; }

/* The move list sits under the chat and shares the column's height with the
   log. Options ▸ Move List takes it away, and the log has the lot again. */
.chat:not(.no-moves) { grid-template-rows: auto minmax(0, 1fr) auto auto minmax(0, 1fr); }
.moves {
  grid-row: 5;
  grid-column: 1;
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-height: 0;
}
.moves[hidden] { display: none; }
.moves-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.btn-small { min-height: 0; padding: 3px 10px; font-size: 12px; }
.moves-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  background: var(--card-2);
  border-radius: var(--radius-sm);
  padding: 6px 10px;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
  display: grid;
  grid-template-columns: 2.4em 1fr 1fr;
  align-content: start;
  column-gap: 6px;
  row-gap: 2px;
}
.moves-list .n { color: var(--ink-faint); text-align: right; }
.moves-list .last { font-weight: 700; }
.moves-buttons { display: inline-flex; gap: 6px; }
.moves-buttons [hidden] { display: none; }
.moves-list .empty { grid-column: 1 / -1; color: var(--ink-soft); font-style: italic; }
.export-text {
  width: 100%;
  box-sizing: border-box;
  font: 13px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--ink);
  background: var(--card-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 8px;
  resize: vertical;
}

/* Shut while there is nobody to talk to — waiting for an opponent, or not in
   a game at all. Dimmed rather than hidden: a control that vanishes and comes
   back is harder to find again than one that greys. */
/* The moves are not the chat, and stay at full strength. */
.chat.shut > :not(.moves):not(.chat-tabs) { opacity: .55; }
.chat.shut .chat-log-wrap { background: var(--card-2); }
.chat-send select:disabled { cursor: not-allowed; }

.chat-send select {
  width: 100%;
  font: inherit;
  /* A point down from the 15px it had as a strip along the bottom: the column
     is 236px and the placeholder is a whole sentence, which at 15px lost its
     last word behind the arrow. */
  font-size: 14px;
  color: var(--ink);
  background: var(--card-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 11px 12px;
  cursor: pointer;
}

/* ---------- Status ---------- */

.statusbar {
  display: flex;
  gap: 16px;
  padding: 0 6px;
  font-size: 13px;
  color: rgba(255, 255, 255, .55);
}
/* The record, the mode and the legal links sit together on the right;
   everything to the left of them stays where it is. */
#status-mode { margin-left: auto; }

/* What the site is, in a sentence, for anyone (and any search engine) who
   reads the page rather than plays it. A quiet line of its own under the
   status cells; it goes with the strip wherever the strip goes. */
.statusbar { flex-wrap: wrap; row-gap: 1px; }
.status-about {
  flex: 1 0 100%;
  margin: 0;
  font-size: 12px;
  text-align: center;
  color: rgba(255, 255, 255, .4);
}
.status-about a { color: rgba(255, 255, 255, .6); }
.status-about a:hover { color: #fff; }

/* ---------- About, under the game ---------- */

/* Not while a game is on the board (app.js, renderPanels). Still in the page
   as it is sent, for anyone who has not started one and for search. */
body.in-game .about { display: none; }

.about {
  max-width: 44rem;
  margin: 0 auto;
  padding: 40px 20px max(56px, env(safe-area-inset-bottom, 0px));
  color: rgba(255, 255, 255, .72);
  font-size: 16px;
  line-height: 1.6;
}
.about h2 {
  color: #fff;
  font-size: 22px;
  letter-spacing: -0.01em;
  margin: 0 0 12px;
}
.about h3 {
  color: #fff;
  font-size: 16px;
  margin: 26px 0 8px;
}
.about p { margin: 0 0 12px; }
/* The three paragraphs folded under the heading, which opens them: a chevron
   beside it, turned up when open, and the heading the whole target. */
.about-more summary {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  cursor: pointer;
  list-style: none;
}
.about-more summary::-webkit-details-marker { display: none; }
.about-more summary h2 { margin: 0; }
.about-more summary::after {
  content: "";
  flex: none;
  width: 8px;
  height: 8px;
  margin-top: -4px;
  border: solid rgba(255, 255, 255, .6);
  border-width: 0 2px 2px 0;
  rotate: 45deg;
  transition: rotate .15s ease;
}
.about-more[open] summary::after { rotate: 225deg; margin-top: 4px; }
.about-more[open] summary { margin-bottom: 8px; }
.about-more summary:focus-visible { outline: 2px solid rgba(255, 255, 255, .7); outline-offset: 3px; border-radius: 4px; }
@media (prefers-reduced-motion: reduce) { .about-more summary::after { transition: none; } }
.about-more + h3 { margin-top: 14px; }
.about-links {
  margin: 0;
  padding-left: 1.2em;
}
.about-links li { margin-bottom: 6px; }
.about a { color: rgba(255, 255, 255, .88); }
.about a:hover { color: #fff; }

/* ---------- Dialogs ---------- */

/* Over the game, the height of the window, and scrolled away with it: the
   lines under the game (.about) are never covered by a dialog left open
   above them. A dialog that opens brings the page back up (openDialog). */
.overlay {
  position: absolute;
  inset: 0 0 auto;
  height: 100vh;
  height: 100dvh;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 18px;
  background: rgba(10, 12, 16, .62);
  backdrop-filter: blur(3px);
}
.overlay[hidden] { display: none; }

.dialog {
  width: min(520px, 100%);
  max-height: 86vh;
  /* The visible height, toolbars and all, and never more than the overlay. */
  max-height: min(86dvh, 100%);
  display: flex;
  flex-direction: column;
  background: var(--card);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  animation: rise .16s ease-out;
}

/* A small lift into place, and solid from the first frame: no fade, so a
   dialog is never see-through with the board showing through it. */
@keyframes rise {
  from { transform: translateY(8px) scale(.99); }
  to { transform: none; }
}

.dialog-title {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 16px 18px 6px;
  font-size: 18px;
  font-weight: 650;
}

/* A full-sized target, which this can afford anywhere: the title row is
   already about as tall, and a dialog is not competing with the board for
   height the way the header is. The faint grey it used to wear came to 2.9:1
   on the card, under the 3:1 a control has to meet to be seen at all. */
.dialog-close {
  margin-left: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  font: inherit;
  font-size: 19px;
  line-height: 1;
  color: var(--ink-soft);
  background: none;
  border: 0;
  border-radius: 7px;
  padding: 6px 9px;
  cursor: pointer;
}
@media (hover: hover) { .dialog-close:hover { background: var(--card-2); color: var(--ink); } }
/* The display above would otherwise beat the hidden attribute, and a dialog
   that cannot be closed would still show a button that closes it. */
.dialog-close[hidden] { display: none; }

/* min-height: 0 so that, in the dialog's column, the body shrinks and scrolls
   rather than pushing its buttons out of the bottom of the dialog. */
.dialog-body { padding: 8px 18px 18px; overflow-y: auto; min-height: 0; }
.dialog-body p { margin: 0 0 12px; }
.dialog-body .muted { color: var(--ink-soft); font-size: 14px; }
/* The "newer version" line at the foot of a dialog between games: a quiet
   sentence with its Reload as a link, not another button to weigh up. */
.dialog-body .reload-note { margin: 14px 0 0; }
.link-button {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--accent-dark);
  text-decoration: underline;
  cursor: pointer;
}

.dialog-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 9px;
  justify-content: flex-end;
  margin-top: 14px;
}

/* The result of a game is announced in the middle of the dialog, so its
   buttons belong in the middle too. Right-aligned under centred text reads
   as a layout that has come apart. */
.dialog[data-kind="result"] .dialog-actions { justify-content: center; }

/* Three buttons will not sit in a row on a phone. Stacking them full width
   also makes each one a target you cannot miss, which is the more useful
   property at the moment a game has just ended. */
@media (max-width: 480px) {
  .dialog-actions { flex-direction: column-reverse; }
  .dialog-actions .btn { width: 100%; }
}

.choice-grid { display: flex; flex-direction: column; gap: 8px; }

/* The front door while the lobby is off: one button, and it plays. It is
   large because it is the only thing to do, and because the person most
   likely to be looking at it is not looking for small targets. */
.play-button {
  display: block;
  width: 100%;
  font-size: 22px;
  font-weight: 650;
  padding: 18px;
  margin-bottom: 10px;
}

.play-strength {
  display: block;
  width: 100%;
  font-size: 15px;
}

.choice {
  display: flex;
  align-items: center;
  gap: 13px;
  width: 100%;
  font: inherit;
  text-align: left;
  color: var(--ink);
  background: var(--card-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 14px 15px;
  cursor: pointer;
  transition: background .15s, border-color .15s, transform .05s;
}
@media (hover: hover) { .choice:hover:not(:disabled) { background: #e9edf3; border-color: #cbd3de; } }
.choice:active:not(:disabled) { transform: translateY(1px); }
.choice:disabled { opacity: .55; cursor: default; }
.choice .glyph { font-size: 23px; line-height: 1; flex: none; }
.choice .label { display: block; font-weight: 650; font-size: 16px; }
.choice .sub { display: block; font-size: 14px; color: var(--ink-soft); }
/* The one choice to lead with (Play a Friend's Start a Game): the accent, as
   .btn.primary. */
.choice.primary { color: #fff; background: var(--accent); border-color: var(--accent); }
.choice.primary .sub { color: rgba(255, 255, 255, .88); }
@media (hover: hover) { .choice.primary:hover:not(:disabled) { background: var(--accent-dark); border-color: var(--accent-dark); } }

/* The computer's strength, 1 to 10: a slider with its number shown large,
   a line saying what that number is like to play, and Play under it. The
   slider is a native range, so the keyboard and screen readers work as they
   do anywhere; only its looks are ours. */
.level-picker {
  background: var(--card-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 12px 15px 10px;
}
.level-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
.level-head label { font-weight: 650; font-size: 16px; }
.level-readout { white-space: nowrap; }
.level-number {
  font-size: 40px;
  font-weight: 750;
  line-height: 1;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.level-of { margin-left: 6px; font-size: 14px; color: var(--ink-soft); }

#level-slider {
  --fill: 44.4%;
  --thumb: 30px;
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 44px;
  margin: 4px 0 0;
  background: transparent;
  cursor: pointer;
}
#level-slider::-webkit-slider-runnable-track {
  height: 10px;
  border-radius: 5px;
  background: linear-gradient(to right, var(--accent) var(--fill), #d3d9e2 var(--fill));
}
#level-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: var(--thumb);
  height: var(--thumb);
  margin-top: calc((10px - var(--thumb)) / 2);
  border-radius: 50%;
  background: #fff;
  border: 3px solid var(--accent);
  box-shadow: 0 1px 4px rgba(0, 0, 0, .25);
}
#level-slider::-moz-range-track { height: 10px; border-radius: 5px; background: #d3d9e2; }
#level-slider::-moz-range-progress { height: 10px; border-radius: 5px; background: var(--accent); }
#level-slider::-moz-range-thumb {
  width: var(--thumb);
  height: var(--thumb);
  box-sizing: border-box;
  border-radius: 50%;
  background: #fff;
  border: 3px solid var(--accent);
  box-shadow: 0 1px 4px rgba(0, 0, 0, .25);
}
#level-slider:focus { outline: none; }
#level-slider:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; border-radius: 8px; }

.level-scale {
  display: flex;
  justify-content: space-between;
  font-size: 13px;
  color: var(--ink-soft);
}
.dialog-body .level-blurb {
  margin: 6px 0 0;
  text-align: center;
  font-size: 17px;
  font-weight: 600;
}
.level-play {
  display: block;
  width: 100%;
  min-height: 48px;
  margin-top: 12px;
  font-size: 19px;
  font-weight: 650;
}
.choice-grid .level-play { margin-top: 4px; }

/* Who goes first against the computer: three radio buttons drawn as one
   segmented control. The radios stay real (and in the tab order, visually
   hidden) so arrow keys, focus and screen readers behave as for any radio
   group; each segment is at least 44px tall for a finger. */
.first-picker {
  margin: 10px 0 0;
  padding: 0;
  border: 0;
  min-width: 0;
}
.first-picker legend {
  padding: 0;
  margin-bottom: 6px;
  font-weight: 650;
  font-size: 15px;
}
.first-options {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  background: var(--card-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 3px;
  gap: 3px;
}
.first-option {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 6px;
  border-radius: calc(var(--radius-sm) - 3px);
  font-size: 16px;
  font-weight: 600;
  color: var(--ink-soft);
  cursor: pointer;
  text-align: center;
  user-select: none;
}
.first-option input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
@media (hover: hover) { .first-option:hover { color: var(--ink); background: #e9edf3; } }
.first-option:has(input:checked) {
  background: var(--accent);
  color: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .2);
}
.first-option:has(input:focus-visible) { outline: 3px solid var(--accent); outline-offset: 2px; }

/* A finger, not a pointer: the thumb is as large as the target it has to be,
   and the slider tall enough to hold it. */
@media (pointer: coarse), (max-width: 560px) {
  #level-slider { --thumb: 44px; height: 52px; }
}

/* The people waiting, listed above the choices because when there is anybody
   there it is the only thing worth reading. */
.tables-title { font-weight: 650; margin: 0 0 8px; }
#open-tables { margin-bottom: 16px; }
#open-tables .choice { margin-bottom: 6px; }

.code-display {
  font-size: 34px;
  font-weight: 700;
  letter-spacing: .16em;
  text-align: center;
  padding: 16px;
  margin-bottom: 12px;
  background: var(--accent-soft);
  color: var(--accent-dark);
  border-radius: var(--radius-sm);
}

/* An invitation link is as long as the address it was served from and has to
   wrap rather than run off the dialog. */
.code-link {
  font-size: 15px;
  font-weight: 500;
  letter-spacing: 0;
  word-break: break-all;
  line-height: 1.4;
  background: var(--card-2);
  color: var(--ink);
}

/* The result of a game, which has to be the thing you see. */
.result-headline {
  font-size: 27px;
  font-weight: 700;
  text-align: center;
  margin: 6px 0 2px;
}
.result-reason { text-align: center; color: var(--ink-soft); margin-bottom: 18px; }
/* Where Play Again stands: the other side wants another, you are waiting,
   they have left, or the minute is up (js/rematch.js). */
.result-rematch { text-align: center; font-weight: 600; margin: -8px 0 14px; }
.result-rematch[hidden] { display: none; }
/* Being tried out (?test=invite): a game with somebody you know, offered
   under the buttons as a sentence with a link, not another button to weigh
   against Play Online. */
.dialog-body .result-invite { text-align: center; color: var(--ink-soft); font-size: 14px; margin: 16px 0 0; text-wrap: balance; }
/* Play Again counts its minute down on the button; even digits keep it from
   jittering as they change. */
#btn-rematch, #result-again { font-variant-numeric: tabular-nums; }
.btn[hidden] { display: none; }

label { display: block; font-size: 14px; font-weight: 600; margin-bottom: 6px; }

input[type="text"], select {
  width: 100%;
  font: inherit;
  font-size: 16px;
  padding: 11px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--card-2);
  color: var(--ink);
}
input[type="text"]:focus, select:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* A friend's four-letter code, typed big and spaced like the code it is.
   Here rather than in a style attribute, which the page's policy refuses.
   Join a Game's one field, so it is big: a target nobody can miss, and the
   letters read back at a glance. The spacing also falls after the last
   letter, so as much is put before the first to keep them centred. */
.join-field { margin-top: 4px; }
.join-field label { font-weight: 500; font-size: 15px; color: var(--ink-soft); margin-bottom: 10px; text-wrap: balance; }
input.join-code {
  text-transform: uppercase;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 34px;
  font-weight: 800;
  letter-spacing: 10px;
  padding: 14px 12px 14px 22px;
  text-align: center;
}
.join-refusal {
  margin: 10px 0 0;
  color: #b3261e;
  font-size: 15px;
  font-weight: 600;
  text-align: center;
  text-wrap: balance;
}
.dialog[data-kind="play"] .press-note { color: var(--cream); opacity: .85; }
.join-refusal[hidden] { display: none; }
/* "You're doing that too much.": under the buttons of the dialog it was
   pressed in, for a few seconds (js/presses.js). */
.press-note {
  margin: 12px 0 0;
  color: var(--ink-soft);
  font-size: 14px;
  font-weight: 600;
  text-align: center;
  text-wrap: balance;
}

/* ---------- Licences ---------- */

/* Help → Licences: a card for each thing in the game that is not ours, as
   the app's Open-Source Licences page lays them out. */
.licences-intro { color: var(--ink-soft); font-size: 15px; }
.licence {
  margin: 0 0 12px;
  padding: 14px 16px;
  background: var(--card-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
.dialog-body .licence p { margin: 10px 0 0; font-size: 15px; }
.licence-head { display: flex; align-items: center; gap: 12px; }
.licence-icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 10px;
  color: #fff;
}
.licence-icon-go { background: linear-gradient(135deg, var(--red-a), var(--red-b)); }
.licence-icon-dbip { background: linear-gradient(135deg, #39445c, var(--board-dark)); }
.licence-name { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
.licence-name strong { font-size: 16px; }
.licence-name span { color: var(--ink-soft); font-size: 14px; }
.licence-badge {
  margin-left: auto;
  flex: none;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent-dark);
  font-size: 12px;
  font-weight: 650;
  white-space: nowrap;
}

.licence-text { margin-top: 10px; }
.licence-text summary {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 32px;
  color: var(--accent-dark);
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  list-style: none;
}
.licence-text summary::-webkit-details-marker { display: none; }
.licence-chevron { display: inline-flex; transition: transform .15s ease; }
.licence-text[open] .licence-chevron { transform: rotate(180deg); }
.licence-body {
  margin-top: 8px;
  padding: 12px 14px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--ink-soft);
  font-size: 13px;
  line-height: 1.5;
}
/* Each condition of the licence on a line of its own (licences.js). */
.dialog-body .licence-body p { margin: 0 0 10px; font-size: 13px; white-space: pre-line; }
.dialog-body .licence-body p:last-child { margin-bottom: 0; }

.dialog-body .licence-links { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.licence-links a {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 6px 12px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent-dark);
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
}
@media (hover: hover) { .licence-links a:hover { text-decoration: underline; } }
.dialog-body .licences-foot { margin: 4px 0 0; text-align: center; color: var(--ink-soft); font-size: 13px; }

@media (prefers-reduced-motion: reduce) { .licence-chevron { transition: none; } }

/* ---------- Narrow screens ---------- */

@media (max-width: 860px) {
  /* Everything here is in service of one number: how big a square ends up.
     A phone has about 750 pixels of height and the board only gets what the
     chrome leaves, so the chrome is kept to what carries information. */
  .app { padding: 0 8px 6px; gap: 6px; }

  .topbar { padding: 7px 2px 0; gap: 10px; }
  .titlebar { gap: 8px; }
  .titlebar-text { font-size: 16px; }
  .titlebar-icon { width: 18px; height: 18px; }
  .titlebar-status { font-size: 12px; padding: 3px 9px; }

  /* Narrow means a phone, and on a phone this is a thumb rather than a
     pointer — and with the title bar gone on a short screen the menus are the
     only way to Help, to Options and to a new game, so they are the last
     controls that should be hard to hit. Forty-four pixels is the smallest
     target anybody recommends. */
  .menu-title { min-height: 44px; padding: 6px 14px; font-size: 14px; }

  /* Which only helps if they can be reached at all. The header is one flex
     row holding the name, the menus and the connection light, and all three
     were rigid: at 390 pixels they come to more than the row is wide, so the
     row overflowed. A flex box that is allowed to shrink shrinks its own
     box and not what is inside it, so .menubar collapsed to four pixels and
     the three menus spilled straight out of it — under the connection pill,
     which painted over Help and left it untappable on the commonest phone
     there is.

     So the order in which things give way is now said out loud. The menus
     never give way: they are the only route to a new game, to Help and to
     Options. The name gives way first — you know which program you opened —
     and then the pill's label, which still leaves the coloured dot saying
     the same thing in less room. */
  .titlebar { flex: 0 1 auto; min-width: 0; }
  .titlebar-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .menubar { flex: none; }
  .titlebar-status { flex: 0 1 auto; min-width: 0; overflow: hidden; }
  .titlebar-status span:not(.dot) {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* One column, so the board gets the width. The players column dissolves
     with display: contents, which puts both panels back to being children of
     this grid rather than of a box that would stack them in a corner of it.
     They sit together above the board: on a phone there is no left and right
     to put them either side of, and together is how they now read everywhere
     else.

     Four children in flow here — two players, the board, the messages — and
     therefore four rows. */
  .players { display: contents; }
  .table {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto minmax(0, 1fr) auto;
    gap: 8px;
  }

  .player-panel {
    flex-direction: row;
    align-items: center;
    gap: 9px;
    padding: 6px 10px;
    border-radius: var(--radius-sm);
  }
  /* min-width: 0 made this the first thing to give way, and it gave way all
     the way: the plate came out 20px wide with the name inside it clipped to
     nothing, so on a phone neither player had a name. Same shape as the
     header collision above — a flex item allowed to shrink shrinks its box
     and not its contents. It may still shrink, but not past legibility. */
  .player-name-plate {
    width: auto;
    flex: 1 1 auto;
    min-width: 68px;
    font-size: 13px;
    padding: 4px 10px;
  }
  /* The tally and the turn keep their right to wrap. Stopping them looked
     tidier and cost about fifty pixels the row did not have, which pushed
     the Draw button off the side of the panel. */
  .player-piece { padding: 0; }
  .player-piece .piece { width: 26px; height: 26px; }
  .player-tally { font-size: 13px; }
  .player-turn { font-size: 12px; min-height: 0; }
  .player-actions { margin-top: 0; flex-direction: row; width: auto; gap: 6px; }
  /* Full-sized targets, because the board here is limited by the width of the
     screen rather than its height — a taller panel costs the squares nothing.
     On a short screen it does, and the rule below takes it back. */
  .player-actions .btn { min-height: 44px; padding: 8px 14px; font-size: 14px; }

  .chat {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto auto auto;
    gap: 6px;
    padding: 8px;
    border-radius: var(--radius-sm);
  }
  /* Capped rather than given the column's height, because here it has no
     column — it is a strip under the board and every pixel it takes is a
     pixel off the squares. */
  .chat-log-wrap { height: 54px; }
  .chat-log { padding: 6px 9px; font-size: 13px; }
  .chat:not(.no-moves) { grid-template-rows: auto auto auto auto auto; }
  .moves-list { height: 66px; flex: none; font-size: 13px; }
  .chat-send select { min-height: 44px; padding: 9px 11px; font-size: 14px; }

  .board-area { gap: 6px; }
  .board-frame { padding: 6px; border-radius: 10px; }
  .board-caption { font-size: 14px; }

  /* The status strip goes. Connection state is in the header pill, how busy
     the lobby is now appears on the New Game screen where the choice is
     actually made, and the mode is obvious from the game in front of you —
     so on a phone it was seventeen pixels of the board spent on nothing. */
  .statusbar { display: none; }

  /* A grid does not drop a row just because nothing is in it. The status
     strip is display: none just above, and the chat is now inside .table
     rather than a row of its own, which leaves two children here — declaring
     any more takes the height out of the board's 1fr and it collapses. */
  .app { grid-template-rows: auto minmax(0, 1fr); }

  .code-display { font-size: 28px; }
}

/* Narrower than about a large phone held upright, and the name cannot be
   afforded at all: the menus and the connection light need the whole line
   between them. This is the same trade the short-screen rules below make,
   arrived at from the other direction. */
@media (max-width: 560px) {
  .titlebar { display: none; }

  /* The panel is one row and at this width it cannot hold all of it: giving
     the name plate enough room to be read pushed the Draw button off the
     edge. The piece count goes instead. It is the one thing in the row that
     can be had by looking at the board, and a name nobody can read is worse
     than a count nobody needs. */
  .player-tally { display: none; }
}

/* A short phone as well as a narrow one: the log is the first thing that can
   go, since the picker still sends messages and the caption still says whose
   move it is. */
@media (max-width: 860px) and (max-height: 720px) {
  .chat-log-wrap { display: none; }
  .moves-list { height: 44px; }
  .player-panel { padding: 5px 10px; }
  .player-piece .piece { width: 22px; height: 22px; }

  /* And the roomy targets go back to being merely adequate ones. There are
     two player panels and a picker between the board and the bottom of the
     screen, so eleven pixels each is nearly two pixels off every square — and
     a square you cannot see is worse than a button you have to aim at. */
  .player-actions .btn { min-height: 38px; padding: 8px 12px; font-size: 13px; }
  .chat-send select { min-height: 38px; }
}

/* Shorter still — a phone from about 2016, where the chrome takes nearly half
   the display. Everything that is decoration rather than information goes. */
@media (max-width: 860px) and (max-height: 600px) {
  .titlebar { display: none; }

  /* The menu buttons are 44px tall on a phone and this is the screen where
     that is most needed and least affordable, so the header pays for it out
     of its own padding and the caption's gap rather than out of the board. */
  .topbar { padding: 2px 2px 0; }
  .board-area { gap: 4px; }

  /* The name goes; the menus stay, because they are the only way back to a
     new game. They share the top line, so the row count does not change. */
  .app { grid-template-rows: auto minmax(0, 1fr); }

  .chat-side { display: none; }
  .menu-title { padding: 6px 12px; font-size: 13px; }
  .player-panel { padding: 3px 8px; gap: 7px; }
  .player-name-plate { font-size: 12px; padding: 3px 8px; }
  .player-tally, .player-turn { font-size: 12px; }
  .board-caption { font-size: 13px; }
  .statusbar { font-size: 11px; }
}

/* ---------- Reduced effects ----------

   For an old machine, a small single-board computer, or anyone who has asked
   their system for less motion. The expensive parts are the layered shadows,
   the radial gradients on every piece, and the continuous animation on a
   piece that must move; all three become flat colour. */

.lite .piece { box-shadow: inset 0 0 0 2px rgba(0, 0, 0, .35); }
.lite .piece.red { background: var(--red-a); }
.lite .piece.white { background: var(--white-a); }
.lite .piece::before { border-color: rgba(0, 0, 0, .3); }
.lite .sq.must > .piece { animation: none; }
.lite .board-frame { box-shadow: none; }
.lite .player-panel, .lite .chat, .lite .dialog { box-shadow: none; border: 1px solid var(--line); }
.lite .overlay { backdrop-filter: none; }
.lite .dialog { animation: none; }

@media (prefers-reduced-motion: reduce) {
  .searching-dot { animation: none; opacity: .8; }
  .sq.must > .piece, .sq.vanish, .dialog { animation: none; }
  .btn, .choice, .menu-title { transition: none; }
}

.status-legal a { color: inherit; text-decoration: none; }
/* White, not --ink: --ink is for the light cards, and on the dark page it
   all but vanished under the pointer (owner, 5 November 2026). */
.status-legal a:hover { color: #fff; text-decoration: underline; }

.record-score {
  font-size: 34px;
  font-weight: 650;
  letter-spacing: .02em;
  font-variant-numeric: tabular-nums;
  margin: 2px 0 4px;
}

/* ---------- The notices that float over the board ----------

   Both of these were children of the board's grid, which declares two rows,
   so each one that appeared was given a row of its own and paid for it out of
   the board: at 1280 by 820 a square went from 63.7 pixels to 55.9 with the
   searching strip up, and every game now starts by looking for an opponent.
   They are taken out of flow instead and stacked over the middle of the
   board, which is where somebody playing is already looking.

   Nothing here may take a click. The offer was a modal dialog once: every
   click landed on the overlay rather than on a piece, and from the other side
   of the screen that is indistinguishable from the game having died. The
   container passes clicks straight through to the board and only the buttons
   catch them, so do not give this box a background, a hit area, or
   pointer-events of its own. */
.board-notices {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 8px;
  pointer-events: none;
}
.board-notices .btn { pointer-events: auto; }

/* The welcome button, hovering over the empty board before a game starts.
   Centred like the other notices; a soft panel so it reads over the squares,
   but a real button — the first thing on the board is the thing to do. */
.welcome {
  pointer-events: auto;
  cursor: pointer;
  max-width: 80%;
  padding: 12px 26px;
  border-radius: var(--radius-sm);
  background: rgba(38, 44, 56, .92);
  border: 1px solid rgba(255, 255, 255, .18);
  box-shadow: 0 10px 28px rgba(0, 0, 0, .5);
  text-align: center;
  font: inherit;
  font-size: 17px;
  font-weight: 600;
  color: #eff2f7;
  transition: background .12s ease, border-color .12s ease, transform .06s ease;
}
@media (hover: hover) { .welcome:hover { background: rgba(52, 60, 76, .96); border-color: rgba(255, 255, 255, .3); } }
.welcome:active { transform: translateY(1px); }
.welcome:focus-visible { outline: 2px solid #eff2f7; outline-offset: 2px; }
.welcome[hidden] { display: none; }

/* Shown while the server is being asked to find a real opponent.
   Opaque, because the cream and green behind it would otherwise take whatever
   contrast the text had — which is how this strip came to be grey-on-dark at
   2.4:1, i.e. not readable at all. */
/* The words and Stop looking are one row, wrapping on a narrow phone; the
   tick box, when there is one, a line of its own under them. A column, so
   the strip is as wide as the wider of the two, not of all three side by
   side. */
.searching {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  max-width: 100%;
  padding: 8px 14px;
  border-radius: var(--radius-sm);
  background: #262c38;
  border: 1px solid rgba(255, 255, 255, .22);
  box-shadow: 0 10px 28px rgba(0, 0, 0, .5);
  font-size: 15px;
  color: #eff2f7;
}
.searching[hidden] { display: none; }
.searching-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  flex-wrap: wrap;
}

/* Tell Me When Someone's Waiting (js/notify.js), in the strip: a line of its
   own under the words and Stop looking, quieter than both, so it never
   pushes the button off a phone's line. */
.searching-notify {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 36px;
  margin: 0;
  padding-top: 6px;
  border-top: 1px solid rgba(255, 255, 255, .12);
  font-size: 14px;
  font-weight: 600;
  line-height: 1.25;
  color: rgba(239, 242, 247, .86);
  text-wrap: balance;
  cursor: pointer;
}
.searching-notify[hidden] { display: none; }
.searching-notify input { flex: none; width: 18px; height: 18px; margin: 0; accent-color: var(--accent); cursor: pointer; }
@media (pointer: coarse) { .searching-notify { min-height: 44px; } }

/* "You go first" / "Your opponent goes first", over the middle of the board
   as a game begins, then gone by itself. Never takes a click or the focus.
   Out of flow, so while it is empty — it stays in the page between games, a
   live region has to exist before it speaks — it moves nothing else. */
.first-move {
  position: absolute;
  top: 50%;
  left: 8px;
  right: 8px;
  margin-inline: auto;
  width: fit-content;
  translate: 0 -50%;
  pointer-events: none;
  max-width: calc(var(--board-size, 100%) - 32px);
  padding: 14px 26px;
  border-radius: var(--radius-sm);
  background: rgba(38, 44, 56, .96);
  border: 1px solid rgba(255, 255, 255, .22);
  box-shadow: 0 10px 28px rgba(0, 0, 0, .5);
  color: #eff2f7;
  font-size: clamp(20px, calc(var(--board-size, 400px) * .065), 32px);
  font-weight: 700;
  line-height: 1.2;
  text-align: center;
  opacity: 0;
  visibility: hidden;
}
.first-move.show {
  visibility: visible;
  animation: first-move 2.9s ease forwards;
}
@keyframes first-move {
  0%   { opacity: 0; scale: .94; }
  8%   { opacity: 1; scale: 1; }
  80%  { opacity: 1; }
  100% { opacity: 0; }
}
.lite .first-move.show { animation: none; opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .first-move.show { animation: none; opacity: 1; }
}

/* A slow pulse rather than a spinner. Nothing is being waited for — there is
   a game going on behind this — so it should read as a light left on, not as
   something holding the page up. */
.searching-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  animation: searching-pulse 1.8s ease-in-out infinite;
}

@keyframes searching-pulse {
  0%, 100% { opacity: .25; }
  50%      { opacity: 1; }
}

.lite .searching-dot { animation: none; opacity: .8; }

/* An offer of a real opponent: the loudest thing on the page short of a
   dialog, since it is the only route from playing the computer to playing a
   person, and it expires. It had a red wash and no colour of its own, so the
   text fell back to the near-black the body uses on its white cards and came
   out at 1.3:1 on a dark page — present in the DOM and invisible on screen. */
/* Pinned to the top of the board rather than its centre. The container is
   click-transparent but these buttons are not, and centred over the middle
   squares they covered pieces the player might be trying to move in the solo
   game behind the offer. The top row is the opponent's back rank — the least
   busy place to sit. The searching strip stays centred: it has no live game
   under it. */
.offer {
  position: absolute;
  top: 10px;
  /* Both edges and an auto margin, not left: 50% and a transform: a box
     positioned at 50% can only grow into the half to the right of it, so on a
     phone the offer came out half the board wide and three lines tall. */
  left: 8px;
  right: 8px;
  margin-inline: auto;
  width: fit-content;
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
  max-width: calc(100% - 20px);
  padding: 10px 14px;
  border-radius: var(--radius-sm);
  background: #4a211b;
  border: 1px solid rgba(255, 141, 124, .6);
  box-shadow: 0 10px 28px rgba(0, 0, 0, .5);
  font-size: 16px;
  font-weight: 550;
  color: #ffeae6;
}
.offer[hidden] { display: none; }

/* White on the accent red is 4.4:1, under AA for text this size, and this is
   the button the whole arrangement exists for. The darker red is 7.1:1, and
   hovering still lightens so the button answers the pointer. */
.offer .btn.primary { background: var(--accent-dark); border-color: var(--accent-dark); }
@media (hover: hover) { .offer .btn.primary:hover:not(:disabled) { background: #c0301f; border-color: #c0301f; } }

/* A thumb, not a cursor: these two strips are read on a phone more than
   anywhere else, so their buttons are full-sized targets. Floating clear of
   the grid, they cost the board nothing. */
.searching .btn,
.offer .btn {
  min-height: 44px;
  padding: 10px 16px;
  font-size: 15px;
}

/* ---------- The pages that are not the game ---------- */

/* Terms and privacy. Plain reading width, the site's colours, and nothing
   that needs JavaScript — they have to be readable with the game broken. */

body.page {
  display: block;
  background: var(--bg);
  color: var(--ink-soft);
}

.prose {
  max-width: 44rem;
  margin: 0 auto;
  padding: 48px 20px 80px;
  font-size: 17px;
  line-height: 1.65;
}

.prose h1 {
  color: #fff;
  font-size: 30px;
  letter-spacing: -0.015em;
  margin: 0 0 6px;
}

.prose h2 {
  color: #fff;
  font-size: 19px;
  margin: 34px 0 10px;
}

.prose h3 {
  color: #fff;
  font-size: 16px;
  margin: 22px 0 6px;
}

.prose p, .prose ul { margin: 0 0 14px; }
.prose ul { padding-left: 1.3em; }
.prose li { margin-bottom: 7px; }
.prose strong { color: #fff; font-weight: 650; }

.prose code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .9em;
  background: rgba(255, 255, 255, .08);
  padding: 1px 5px;
  border-radius: 4px;
}

.prose a { color: var(--accent); }
.prose a.btn { display: inline-block; text-decoration: none; }
.prose a.btn.primary { color: #fff; }

.prose-lead {
  font-size: 19px;
  color: #fff;
}

.prose-date {
  color: var(--ink-faint);
  font-size: 14px;
  margin: 0 0 28px;
}

.prose-back {
  font-size: 15px;
  margin-bottom: 28px;
}

@media (max-width: 600px) {
  .prose { padding: 28px 18px 60px; font-size: 16px; }
  .prose h1 { font-size: 25px; }
}

/* ---------- Phones: touch, safe areas, landscape ----------

   A tap is not a click with a mouse. No double-tap zoom on a board that is
   tapped twice per move, no grey flash on every square, no text selection or
   callout from a long press, and no pull-to-refresh that throws a game away. */
html { touch-action: manipulation; -webkit-tap-highlight-color: transparent; }
html, body { overscroll-behavior: none; }
.dialog-body, .moves-list, .chat-log { overscroll-behavior: contain; }
/* The page scrolls to the lines under the game, but never from the board,
   where a swipe is a player reaching for a piece. */
.board { touch-action: pinch-zoom; }
.board, .menubar, .player-panel {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* Installed to a home screen the page runs under the status bar, the notch
   and the home indicator (viewport-fit=cover), so it keeps clear of them. */
.app {
  padding-top: env(safe-area-inset-top, 0px);
  padding-bottom: max(10px, env(safe-area-inset-bottom, 0px));
  padding-left: max(14px, env(safe-area-inset-left, 0px));
  padding-right: max(14px, env(safe-area-inset-right, 0px));
}
.overlay {
  padding: max(18px, env(safe-area-inset-top, 0px)) max(18px, env(safe-area-inset-right, 0px))
           max(18px, env(safe-area-inset-bottom, 0px)) max(18px, env(safe-area-inset-left, 0px));
}

/* A menu that would run off the right edge is pulled back in when it opens
   (see the menus in app.js); this keeps a very narrow one from being wider
   than the screen. */
.menu-items { max-width: calc(100vw - 16px); }

/* A short screen with a dialog up is usually a phone with its keyboard
   coming: keep the dialog at the top, where the keyboard does not reach. */
@media (max-height: 700px) and (max-width: 860px), (max-height: 700px) and (pointer: coarse) {
  .overlay { align-items: flex-start; }
}

/* A touch screen of any size: under 16px, iOS zooms the page in when a field
   takes focus, and this page has no way to scroll back out. */
@media (pointer: coarse) {
  .chat-send select, .export-text { font-size: 16px; }
}
.export-text { max-height: 40vh; }

@media (max-width: 860px) {
  .app {
    padding-bottom: max(6px, env(safe-area-inset-bottom, 0px));
    padding-left: max(8px, env(safe-area-inset-left, 0px));
    padding-right: max(8px, env(safe-area-inset-right, 0px));
  }

  /* Whose turn it is changes every move, and a turn line that wrapped to two
     lines made one panel taller than the other — so the board resized under
     the player's finger each turn. One line, one size. */
  .player-turn.your-turn,
  .player-turn.waiting,
  .player-turn.offline { font-size: 13px; white-space: nowrap; }
  /* Thumb-sized, within what the board can spare. */
  .btn-small { min-height: 32px; padding: 5px 12px; font-size: 13px; }
  .radio { min-height: 32px; }
}

/* One line, always: a caption that wrapped took a second line from the board
   and gave it back a few seconds later, so the squares shrank and grew. */
@media (max-width: 860px) {
  .board-caption { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}


/* A phone on its side: too short to stack anything above or below the board,
   so back to three columns — players, board, messages — with everything
   compacted to the height there is. This also catches the largest phones,
   which are wide enough for the desktop layout but far too short for it,
   and a window narrower than a phone on its side (from 420px): stacked, as
   at 500×400, the board had 20px.

   The credit to DB-IP (at every size, below) goes under the messages, in
   their column, not in a row of its own across the foot, which was a row
   of the board's height: the table's columns are the page's (a subgrid),
   so the credit takes the foot of the last one, and the players and the
   board run the whole height beside it. */
@media (orientation: landscape) and (max-height: 500px) and (min-width: 420px) {
  .titlebar { display: none; }
  .topbar { padding: 2px 2px 0; }
  .menu-title { padding: 6px 12px; font-size: 13px; }
  .app {
    /* Sized from the width, so a small phone keeps most of it for the board. */
    grid-template-columns: clamp(130px, 22vw, 190px) minmax(0, 1fr) clamp(150px, 26vw, 230px);
    grid-template-rows: auto minmax(0, 1fr) auto;
    gap: 4px 8px;
  }
  .masthead { grid-column: 1 / -1; }

  .table {
    grid-column: 1 / -1;
    grid-row: 2 / 4;
    /* The same columns where a browser has no subgrid (Safari before 16),
       the credit then over the foot of the messages rather than under. */
    grid-template-columns: clamp(130px, 22vw, 190px) minmax(0, 1fr) clamp(150px, 26vw, 230px);
    grid-template-columns: subgrid;
    grid-template-rows: minmax(0, 1fr);
    grid-template-rows: subgrid;
    gap: 4px 8px;
  }
  .players, .board-area { grid-row: 1 / -1; }
  /* On the shortest screens (an iPhone SE on its side, Safari's bars and
     the App Store's bar all showing) the messages have less room than their
     controls: kept inside their card rather than drawn over the credit. */
  .chat { grid-column: 3; grid-row: 1; overflow-y: clip; }
  .statusbar { grid-column: 3; grid-row: 3; position: relative; z-index: 1; }
  /* Wrapped between the links, never inside one: "DB-" and "IP" apart was
     a line with one word on it. */
  .status-about { text-wrap: balance; }
  .status-about a { white-space: nowrap; }
  .players {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 6px;
    padding-bottom: 0;
  }
  .player-panel {
    flex-direction: column;
    align-items: stretch;
    gap: 3px;
    padding: 6px 8px;
    border-radius: var(--radius-sm);
  }
  .player-name-plate { font-size: 12px; padding: 3px 8px; min-width: 0; }
  .player-piece, .player-tally { display: none; }
  .player-turn { min-height: 20px; }
  .player-turn, .player-turn.your-turn, .player-turn.waiting, .player-turn.offline {
    font-size: 13px; white-space: nowrap;
  }
  .player-clock { min-height: 0; font-size: 13px; text-align: center; }
  .player-actions { flex-direction: row; gap: 6px; margin-top: 0; }
  .player-actions .btn { flex: 1; min-height: 34px; padding: 6px 8px; font-size: 13px; }
  /* A finished game: Resign and Draw are dead, and Play Again wrapped onto
     two lines beside them. */
  .player-actions.over #btn-resign,
  .player-actions.over #btn-draw { display: none; }

  .board-area { gap: 4px; }
  .board-frame { padding: 5px; border-radius: 8px; }
  .board-fit { align-items: center; }
  .board-caption { font-size: 13px; min-height: 18px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

  .chat, .chat:not(.no-moves) {
    grid-template-rows: auto minmax(0, 1fr) auto auto minmax(0, 1fr);
    gap: 6px;
    padding: 8px;
    border-radius: var(--radius-sm);
  }
  .chat.no-moves { grid-template-rows: auto minmax(0, 1fr) auto auto; }
  .chat-side { display: flex; }
  .chat-log-wrap { display: block; height: auto; }
  .chat-log { font-size: 13px; padding: 5px 8px; }
  .chat-send select { min-height: 34px; padding: 6px 9px; }
  .moves-list { height: auto; flex: 1 1 auto; font-size: 13px; }
}

/* A narrow phone: with the turn held to one line, the row has no room for
   the decorative piece beside the name, so it goes — the plate's colour
   already says which side — and the buttons pull in a little. */
@media (max-width: 560px) {
  .player-piece { display: none; }
  .player-name-plate { min-width: 56px; }
  .player-actions .btn { padding-left: 10px; padding-right: 10px; }
}

/* The narrowest phones (320px): the connection pill keeps its light and drops
   the word, and the panel buttons pull in so Draw stays inside its panel.
   Out of sight rather than gone: a screen reader still needs the word, and
   the light alone says nothing to it. */
@media (max-width: 380px) {
  #connection-text {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .player-panel { gap: 6px; }
  .player-actions .btn { padding-left: 8px; padding-right: 8px; }
}

/* Short enough that the chat switch and log are both hidden: the empty rows
   they leave must not keep their gaps, or the card opens on a blank band. */
@media (max-width: 860px) and (max-height: 600px) {
  .chat, .chat:not(.no-moves) { row-gap: 0; }
  .chat-send { margin-bottom: 6px; }
}

/* The notices over the board are no wider than the board: on a phone the
   column is far wider than the squares, and a strip hanging off both sides of
   the board read as broken. --board-size is kept by trackBoardSize in app.js. */
.searching, .offer {
  max-width: calc(var(--board-size, 100%) - 16px);
  text-align: center;
}

/* A phone short enough that the chat log is hidden: the empty rows it leaves
   must not keep their gaps, or the card opens on a blank band. */
@media (max-width: 860px) and (max-height: 720px) {
  .chat, .chat:not(.no-moves) { row-gap: 0; }
  .chat-side, .chat-note, .chat-send { margin-bottom: 6px; }
}

/* The light stays beside the words, never on a line of its own. */
.searching-line { display: inline-flex; align-items: center; gap: 10px; }

.searching-line { white-space: nowrap; }

/* ---------- Players on a phone held upright ----------

   Not two white cards with pill labels, which read as buttons and fought the
   dark page: a slim row for each player on the page itself — a small piece
   in their colour, the name, and whose turn it is. The opponent sits above
   the board and you below it, where your own pieces are, with your buttons
   on your row. The side to move glows softly. */
@media (max-width: 860px) and (orientation: portrait) {
  .table { grid-template-rows: auto minmax(0, 1fr) auto auto; }
  .player-panel { order: 0; }
  .board-area { order: 1; }
  .player-panel.mine { order: 2; }
  .chat { order: 3; }

  .player-panel {
    flex-wrap: wrap;
    gap: 4px 10px;
    padding: 4px 10px;
    min-height: 40px;
    background: transparent;
    box-shadow: none;
    border-radius: 12px;
    transition: background .2s;
  }
  .player-panel.active {
    background: rgba(74, 222, 128, .10);
    box-shadow: inset 0 0 0 1px rgba(74, 222, 128, .35);
  }
  .player-piece { display: block; order: -1; padding: 0; }
  .player-piece .piece { width: 20px; height: 20px; }
  .player-name-plate,
  .player-name-plate.red,
  .player-name-plate.white {
    background: none;
    color: #eef1f6;
    width: auto;
    min-width: 0;
    flex: 0 1 auto;
    padding: 0;
    text-align: left;
    font-size: 16px;
    font-weight: 650;
  }
  .player-tally { color: rgba(238, 241, 246, .55); font-size: 13px; }
  .player-turn, .player-turn.your-turn { color: #4ade80; font-size: 13px; min-height: 0; }
  .player-panel.mine { margin-bottom: 4px; }
  .player-turn.waiting { color: rgba(238, 241, 246, .65); }
  .player-turn.offline { color: #ff8a7a; }
  .player-clock { color: rgba(238, 241, 246, .75); font-size: 14px; min-height: 0; }
  .player-actions { margin-left: auto; margin-top: 0; }
  .player-actions .btn { min-height: 36px; padding: 6px 12px; font-size: 14px; }
  /* Once the game has a result Resign and Draw only take room — disabled,
     and pushing Play Again onto a line of its own under the name. */
  .player-actions.over #btn-resign,
  .player-actions.over #btn-draw { display: none; }
}

/* ---------- Chat and Moves as tabs on a phone held upright ----------

   There is not the height for a chat log and a move list under the board on a
   phone, and the log was the one that lost — hidden outright on most phones.
   So they share the card as two tabs; the chat shows its log again, and a dot
   on the Chat tab says a message arrived while Moves was showing. */
.chat-tabs { display: none; }
@media (max-width: 860px) and (orientation: portrait) {
  .chat, .chat:not(.no-moves), .chat.no-moves {
    display: flex;
    flex-direction: column;
    gap: 6px;
    row-gap: 6px;
  }
  .chat-tabs {
    display: flex;
    order: -1;
    gap: 4px;
    padding: 3px;
    border-radius: 10px;
    background: var(--card-2);
  }
  .chat.no-moves .chat-tabs { display: none; }
  .chat-tabs button {
    flex: 1;
    min-height: 34px;
    border: 0;
    border-radius: 8px;
    background: none;
    font: inherit;
    font-size: 14px;
    font-weight: 650;
    color: var(--ink-soft);
    cursor: pointer;
  }
  .chat-tabs button[aria-selected="true"] {
    background: var(--card);
    color: var(--ink);
    box-shadow: 0 1px 3px rgba(0, 0, 0, .15);
  }
  .chat-tabs button.unread::after {
    content: "";
    display: inline-block;
    width: 7px;
    height: 7px;
    margin-left: 6px;
    border-radius: 50%;
    background: var(--accent);
    vertical-align: middle;
  }

  .chat-side { order: 0; margin: 0; }
  .chat-side .chat-side-title { display: none; }
  .chat-log-wrap { order: 1; }
  .chat-note { order: 2; margin: 0; }
  .chat-send { order: 3; margin: 0; }
  .moves { order: 4; }
  .moves-head .chat-side-title { display: none; }
  .moves-head { justify-content: flex-end; }

  /* One height for the card whichever tab shows, so switching tabs never
     resizes the board; the log or the list takes whatever is left. */
  .chat, .chat:not(.no-moves), .chat.no-moves { height: 236px; }
  .chat[data-tab="chat"] .chat-log-wrap { display: block; flex: 1 1 auto; height: auto; min-height: 0; }
  .chat[data-tab="chat"] .moves { display: none; }
  .chat[data-tab="moves"] .chat-side,
  .chat[data-tab="moves"] .chat-log-wrap,
  .chat[data-tab="moves"] .chat-note,
  .chat[data-tab="moves"] .chat-send { display: none; }
  .chat[data-tab="moves"] .moves { flex: 1 1 auto; min-height: 0; }
  .chat[data-tab="moves"] .moves-list { flex: 1 1 auto; height: auto; min-height: 0; }
}
@media (max-width: 860px) and (orientation: portrait) and (max-height: 720px) {
  .chat, .chat:not(.no-moves), .chat.no-moves { height: 200px; }
}
@media (max-width: 860px) and (orientation: portrait) and (max-height: 600px) {
  .chat, .chat:not(.no-moves), .chat.no-moves { height: 164px; }
}

/* Pass and play: the two are at this one screen and talk to each other, so
   the card is the move list alone, at every size; with the move list off too
   it keeps its place but shows nothing. After the phone's tabs, to win. */
.chat.no-chat { grid-template-rows: minmax(0, 1fr); }
.chat.no-chat > :not(.moves) { display: none; }
.chat.no-chat .moves:not([hidden]) { grid-row: 1; display: flex; flex: 1 1 auto; min-height: 0; }
.chat.no-chat .moves-head .chat-side-title { display: inline; }
.chat.no-chat .moves-head { justify-content: space-between; }
.chat.no-chat .moves-list { flex: 1 1 auto; height: auto; min-height: 0; }
.chat.no-chat.no-moves { visibility: hidden; }

/* ---------- A phone held upright: as the app ----------

   The iPhone app's phone layout (PhoneLayout, ChatPanel), which the owner
   asked the page to match (10 November 2026: "match app closer"). Phones
   only: a tablet held upright keeps the layout above.

   The board as wide as the screen whenever that still leaves the card under
   it a useful height, and otherwise as big as the height allows: the board's
   row may grow to the column's width (and its caption), and the card takes
   what is left, never less than --card-min. A grid grows a track to its
   largest before it hands out the 1fr, which is exactly that order.

   With no game there is nothing under your row, and nothing to resign: the
   board and the two rows sit in the middle of the screen, the card's room
   (less the gap the ::before brings) shared out above and below them, the
   ::before being the room above; so the board is the same size before a
   game as in one. */
@media (max-width: 560px) and (orientation: portrait) {
  .table {
    --board-row: calc(100vw - 16px + var(--caption-h) + 6px);
    --card-min: 150px;
    gap: 6px;
    grid-template-rows: auto minmax(0, var(--board-row)) auto minmax(var(--card-min), 1fr);
  }
  body.no-game .table {
    --half: calc((var(--card-min) - 6px) / 2);
    grid-template-rows: minmax(var(--half), 1fr) auto minmax(0, var(--board-row)) auto minmax(var(--half), 1fr);
  }
  body.no-game .table::before { content: ""; order: -1; }
  body.no-game .chat,
  body.no-game .player-actions { display: none; }

  /* The card: dark, in the page's colours, with a hairline edge (the app's
     ChatInk). The tabs, the log, then the picker with the switch beside it;
     the moves in the log's place. Placed by hand, and spaced by margins
     rather than the grid's gap, so a row with nothing in it takes no room. */
  .chat, .chat:not(.no-moves), .chat.no-moves {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: auto minmax(0, 1fr) auto auto;
    gap: 0 10px;
    height: auto;
    min-height: 0;
    padding: 8px;
    color: #eef1f6;
    color-scheme: dark;
    background: #232833;
    border: 1px solid rgba(255, 255, 255, .12);
    box-shadow: 0 6px 20px rgba(0, 0, 0, .3);
  }
  .chat-tabs { grid-area: 1 / 1 / 2 / -1; background: #171a21; }
  .chat-tabs button { color: #aab2c0; }
  .chat-tabs button[aria-selected="true"] { background: #3a4150; color: #eef1f6; }
  .chat-log-wrap { grid-area: 2 / 1 / 3 / -1; margin: 6px 0 0; }
  .chat.no-moves .chat-log-wrap { margin-top: 0; }
  .chat-note { grid-area: 3 / 1 / 4 / -1; margin: 6px 0 0; color: #aab2c0; }
  .chat-send { grid-area: 4 / 1; margin: 6px 0 0; }
  .chat-side { grid-area: 4 / 2; margin: 6px 2px 0 0; gap: 12px; display: flex; }
  .moves { grid-area: 2 / 1 / 5 / -1; margin: 6px 0 0; }

  .chat-log-wrap, .chat.shut .chat-log-wrap, .moves-list { background: #171a21; }
  .chat-log, .moves-list { color: #eef1f6; }
  .chat-log .system, .moves-list .empty { color: #aab2c0; }
  .moves-list .n, .chat-side-title { color: #7f8796; }
  .chat-send select {
    color: #eef1f6;
    background: #171a21;
    border-color: rgba(255, 255, 255, .12);
    color-scheme: dark;
  }
  .radio { color: #eef1f6; }
  .radio input { accent-color: var(--accent); }
  .chat .btn-small:disabled {
    color: #7f8796;
    background: transparent;
    border-color: rgba(255, 255, 255, .12);
    opacity: 1;
  }

  /* Against the computer there is nobody to talk to, so, as in the app, the
     card is the moves alone, under their title; with the move list off too,
     nothing. Pass and play and a restricted chat region are the same. */
  .chat.vs-computer > :not(.moves) { display: none; }
  .chat.vs-computer .moves:not([hidden]),
  .chat.no-chat .moves:not([hidden]) { grid-area: 1 / 1 / -1 / -1; margin: 0; display: flex; }
  .chat.vs-computer .moves-head .chat-side-title { display: inline; }
  .chat.vs-computer .moves-head { justify-content: space-between; }
  .chat.vs-computer .moves-list { flex: 1 1 auto; height: auto; min-height: 0; }
  .chat.vs-computer.no-moves { visibility: hidden; }
}
@media (max-width: 560px) and (orientation: portrait) and (max-height: 720px) {
  .table { --card-min: 130px; }
}
@media (max-width: 560px) and (orientation: portrait) and (max-height: 600px) {
  .table { --card-min: 110px; }
}

/* ---------- The credit to DB-IP, at every size ----------

   The country database is DB-IP's, under CC BY 4.0, whose one condition is
   the credit: so where the status strip goes (a phone, a tablet, a phone on
   its side) its credit line stays, alone and on one line, with How to play
   beside it; the sentence before them goes, and the status cells. On its
   side the line is not a row of its own but the foot of the messages'
   column (above), where it wraps, balanced. */
@media (max-width: 860px), (orientation: landscape) and (max-height: 500px) {
  .app { grid-template-rows: auto minmax(0, 1fr) auto; }
  .statusbar { display: flex; justify-content: center; padding: 0 4px; }
  .statusbar .status-cell, .status-blurb { display: none; }
  .status-about { font-size: 11px; line-height: 1.3; }
}

/* ---------- The message to players ----------

   The operator's banner (js/motd.js), above the board on every screen. It
   shares the header's grid row with the menus — .masthead holds both — so
   the row counts the layouts above declare stay true whether or not it is
   up. Plain words in the tone's colour, and a close button big enough for a
   thumb. */
.masthead { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.motd {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 6px 6px 14px;
  border-radius: var(--radius-sm);
  font-size: 15px;
  line-height: 1.35;
  color: #fff;
  background: #46536a;
  border-left: 5px solid #8fa3c2;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
}
.motd[hidden] { display: none; }
.motd-maintenance { background: #f2b33d; color: #2a1d03; border-left-color: #a8700a; }
.motd-warning { background: #b3261e; color: #fff; border-left-color: #ff8a80; }
.motd-text { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.motd-close {
  flex: none;
  width: 36px;
  height: 36px;
  border: 0;
  border-radius: 8px;
  background: rgba(0, 0, 0, .12);
  color: inherit;
  font: inherit;
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
}
@media (hover: hover) { .motd-close:hover { background: rgba(0, 0, 0, .25); } }
.motd-close:focus-visible { outline: 2px solid currentColor; outline-offset: 1px; }

/* On a phone every line of it is a line off the board, so a long message is
   cut to a few lines with an ellipsis, and a tap on the words opens it out
   (js/motd.js toggles .open). The shorter the screen, the fewer lines. */
@media (max-width: 860px), (orientation: landscape) and (max-height: 500px) {
  .motd-text {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    line-clamp: 3;
    overflow: hidden;
    cursor: pointer;
  }
  .motd.open .motd-text { display: block; -webkit-line-clamp: unset; line-clamp: none; }
}
@media (max-width: 860px) {
  .masthead { gap: 5px; }
  .motd { font-size: 14px; padding: 3px 3px 3px 11px; border-left-width: 4px; }
  .motd-close { width: 38px; height: 38px; }
}
@media (max-width: 860px) and (max-height: 720px) {
  .motd { font-size: 13px; line-height: 1.3; }
  .motd-text { -webkit-line-clamp: 2; line-clamp: 2; }
  .motd-close { width: 34px; height: 34px; }
}
/* Picking the computer's strength on a phone on its side. Stacked, the
   slider alone filled the dialog and Play — the one thing the step is for —
   was below the bottom of it, in a dialog nothing said could be scrolled.
   Side by side instead: the slider on the left, who goes first and Play on
   the right, and the front door's two lines of greeting put away. */
@media (orientation: landscape) and (max-height: 500px) {
  .dialog-title { padding-top: 8px; padding-bottom: 0; }
  .dialog-body:has(.choice-grid[data-step="level"]) > p:not(.reload-note) { display: none; }
  .choice-grid[data-step="level"] {
    display: grid;
    grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
    grid-template-rows: auto 1fr;
    gap: 10px 14px;
  }
  .choice-grid[data-step="level"] .level-picker { grid-row: 1 / 3; padding: 8px 12px 6px; }
  .choice-grid[data-step="level"] .level-number { font-size: 32px; }
  .choice-grid[data-step="level"] .first-picker { margin: 0; }
  .choice-grid[data-step="level"] .first-options { grid-template-columns: 1fr; }
  .choice-grid[data-step="level"] .first-option { min-height: 40px; }
  .choice-grid[data-step="level"] .level-play { align-self: end; margin-top: 0; }
}

@media (orientation: landscape) and (max-height: 500px) and (min-width: 420px) {
  .masthead { gap: 3px; }
  .motd { font-size: 13px; line-height: 1.3; padding: 2px 3px 2px 10px; }
  .motd-text { -webkit-line-clamp: 1; line-clamp: 1; }
  .motd-close { width: 32px; height: 32px; }
}

/* ---------- The app, offered on an iPhone or iPad ----------

   js/appnotice.js. It floats over the page, held to the bottom of the
   screen while the page scrolls under it (owner, 10 November 2026: "should
   stack locked the bottom and hover over the page"). The game stops short of
   it — the page is that much shorter (--app-notice-room, measured by app.js)
   — so it never covers the board or its buttons, and the page has that much
   room after its last line, so nothing under the game is left behind it.
   Closed, it is gone and the room with it. Like the App Store's own banner:
   the icon, the name, and Get; the close button as big a target as the
   menus.

   A dialog's overlay stops short of it (app.js measures the room it takes),
   where it would otherwise dim it with the page and take its taps: at the
   front door it looked switched off, and Get and ✕ did nothing. */
.overlay {
  height: calc(100vh - var(--app-notice-room, 0px));
  height: calc(100dvh - var(--app-notice-room, 0px));
}
.app {
  height: calc(100vh - var(--app-notice-room, 0px));
  height: calc(100svh - var(--app-notice-room, 0px));
}
body { padding-bottom: var(--app-notice-room, 0px); }
.app-notice {
  position: fixed;
  z-index: 90;
  left: 50%;
  bottom: max(8px, env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 10px;
  width: min(560px, calc(100% - 16px));
  padding: 5px 2px 5px 8px;
  border-radius: var(--radius-sm);
  color: #fff;
  background: var(--board-frame);
  border: 1px solid rgba(255, 255, 255, .12);
  box-shadow: 0 6px 20px rgba(0, 0, 0, .45);
}
.app-notice[hidden] { display: none; }
.app-notice-icon { flex: none; width: 44px; height: 44px; border-radius: 10px; }
.app-notice-text {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  line-height: 1.25;
}
.app-notice-text strong { font-size: 15px; }
.app-notice-text span { font-size: 13px; color: rgba(255, 255, 255, .72); }
.app-notice-get {
  flex: none;
  min-height: 44px;
  padding: 8px 18px;
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}
/* A plain ✕, as on the dialogs: in a box of its own it read as a second,
   greyed-out button. */
.app-notice-close {
  flex: none;
  width: 44px;
  height: 44px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: rgba(255, 255, 255, .7);
  font: inherit;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}
@media (hover: hover) { .app-notice-close:hover { background: rgba(255, 255, 255, .1); color: #fff; } }
.app-notice-close:focus-visible { outline: 2px solid currentColor; outline-offset: 1px; }

/* A short phone: every pixel here is a pixel off the board, so smaller,
   as the other targets there are. */
@media (max-width: 860px) and (max-height: 720px) {
  .app-notice { gap: 8px; padding: 3px 3px 3px 6px; }
  .app-notice-icon { width: 36px; height: 36px; border-radius: 8px; }
  .app-notice-get { min-height: 38px; padding: 6px 14px; font-size: 14px; }
  .app-notice-close { width: 38px; height: 38px; }
}
/* On its side, there is width to spare and no height: one line. */
@media (orientation: landscape) and (max-height: 500px) {
  .app-notice-text { flex-direction: row; align-items: baseline; gap: 8px; }
  .app-notice-text strong { flex: none; }
  .app-notice-text span { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .app-notice-icon { width: 32px; height: 32px; }
  .app-notice-get { min-height: 34px; padding: 5px 14px; }
  .app-notice-close { width: 34px; height: 34px; }
}

/* ---------- The front door: the Mac app's title card ----------

   While the lobby is off the front door is the Mac app's title card, drawn
   for the page (MacTitleCard in the Mac app): dark polished wood with a thin
   gold edge, sitting over the board rather than a white box in front of it.
   The two pieces and the name, a pill with a live light saying who is about,
   Play Online as the big red button, practice against the computer as the
   quieter line under it, and the record as badges.

   It is still the dialog underneath (#dialog, data-kind="play"), so focus,
   Escape and the close button behave as for every other one; only its looks
   are here. app.js nudges it over the board with --card-dx/--card-dy. */

:root {
  --gold: #f2c33c;
  --wood-a: #3a2516;
  --wood-b: #24160c;
  --cream: #f6ead2;
  --parchment: #e8d9bb;
  --live: #3ddc84;
  --font-rounded: ui-rounded, "SF Pro Rounded", var(--font);
}

/* The board shows through: the card sits on the table, it does not put the
   table out. */
.overlay:has(> .dialog[data-kind="play"]) {
  background: rgba(10, 12, 16, .28);
  backdrop-filter: none;
}

.dialog[data-kind="play"] {
  --s: 1;
  position: relative;
  width: min(440px, 100%);
  color: var(--cream);
  font-family: var(--font-rounded);
  text-align: center;
  border: 1.5px solid transparent;
  border-radius: 22px;
  background:
    linear-gradient(var(--wood-a), var(--wood-b)) padding-box,
    linear-gradient(rgba(242, 195, 60, .75), rgba(184, 134, 11, .35)) border-box;
  box-shadow: 0 12px 44px rgba(0, 0, 0, .55);
  translate: var(--card-dx, 0px) var(--card-dy, 0px);
}

/* The name is drawn in the card, so the title row keeps only its ✕, in the
   corner; the words stay for a screen reader, which names the dialog by
   them. */
.dialog[data-kind="play"] .dialog-title {
  position: absolute;
  top: 6px;
  right: 6px;
  z-index: 1;
  padding: 0;
}
.dialog[data-kind="play"] #dialog-title {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.dialog[data-kind="play"] .dialog-close { color: rgba(232, 217, 187, .75); border-radius: 50%; }
@media (hover: hover) {
  .dialog[data-kind="play"] .dialog-close:hover { background: rgba(255, 255, 255, .08); color: var(--cream); }
}
.dialog[data-kind="play"] :focus-visible { outline: 3px solid var(--gold); outline-offset: 3px; }

.dialog[data-kind="play"] .dialog-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: calc(26px * var(--s)) calc(30px * var(--s)) calc(24px * var(--s));
}
.dialog[data-kind="play"] .dialog-body p { margin: 0; }

/* The two pieces, tipped towards each other, the king bobbing gently. */
.title-logo {
  position: relative;
  margin: 0 auto;
  width: calc(150px * var(--s));
  height: calc(90px * var(--s));
}
.title-logo .piece { position: absolute; }
.title-logo .piece.white {
  width: calc(62px * var(--s));
  height: calc(62px * var(--s));
  left: calc(14px * var(--s));
  top: calc(22px * var(--s));
  rotate: -8deg;
}
.title-logo .piece.red {
  width: calc(70px * var(--s));
  height: calc(70px * var(--s));
  left: calc(64px * var(--s));
  top: calc(6px * var(--s));
  rotate: 6deg;
}

.title-name {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: calc(12px * var(--s));
  line-height: 1.08;
}
.title-small {
  font-size: calc(22px * var(--s));
  font-weight: 650;
  color: var(--gold);
}
.title-big {
  font-size: calc(46px * var(--s));
  font-weight: 850;
  color: #fff4dc;
  background: linear-gradient(#fff4dc, #f0d9a8);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 2px 0 rgba(0, 0, 0, .6));
}

/* Who is about, with a live light. Red, and no ring, while the line is
   down. */
.dialog[data-kind="play"] .dialog-body .lobby-pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-top: calc(16px * var(--s));
  padding: 5px 13px;
  border-radius: 999px;
  background: rgba(0, 0, 0, .35);
  border: 1px solid rgba(61, 220, 132, .4);
  font-size: 14px;
  font-weight: 650;
  line-height: 1.3;
  color: var(--cream);
  font-variant-numeric: tabular-nums;
}
.dialog[data-kind="play"] .dialog-body .lobby-pill[hidden] { display: none; }
.pill-dot {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--live);
  box-shadow: 0 0 6px rgba(61, 220, 132, .9);
  animation: pill-pulse 2.1s ease-in-out infinite;
}
@keyframes pill-pulse {
  0%, 100% { box-shadow: 0 0 6px rgba(61, 220, 132, .9); }
  50% { box-shadow: 0 0 2px rgba(61, 220, 132, .25); }
}
.dialog[data-kind="play"] .dialog-body .lobby-pill.off { border-color: transparent; }
.lobby-pill.off .pill-dot { background: #ff6b5e; box-shadow: none; animation: none; }

/* Play Online, the big red button; under it, practice, quieter. Both are
   .choice for the scripts that press them, and drawn here instead. */
.dialog[data-kind="play"] .title-actions {
  align-items: center;
  gap: calc(6px * var(--s));
  margin-top: calc(18px * var(--s));
  width: 100%;
}
.dialog[data-kind="play"] .title-play {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: auto;
  min-width: calc(210px * var(--s));
  min-height: 50px;
  padding: 10px 24px;
  font-family: var(--font-rounded);
  font-size: calc(21px * var(--s));
  font-weight: 700;
  color: #fff;
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  box-shadow: 0 2px 8px rgba(0, 0, 0, .35);
}
@media (hover: hover) {
  .dialog[data-kind="play"] .title-play:hover:not(:disabled) { background: var(--accent-dark); border-color: var(--accent-dark); }
}
.title-play svg { width: 1.15em; height: 1.15em; flex: none; }

.dialog[data-kind="play"] .title-practice {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: auto;
  min-height: 44px;
  padding: 6px 12px;
  text-align: center;
  font-family: var(--font-rounded);
  font-size: calc(16px * var(--s));
  font-weight: 650;
  color: var(--parchment);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
}
@media (hover: hover) {
  .dialog[data-kind="play"] .title-practice:hover:not(:disabled) { background: rgba(255, 255, 255, .06); color: #fff4dc; }
}
.title-practice svg { width: 1.2em; height: 1.2em; flex: none; }
/* Under Play Online, one line of two quieter buttons, a pair: More Ways to
   Play… at the left, practice at the right, each half the width however long
   its words, with its picture above them (owner, 9 November 2026: "make this
   look nice and take up equal space"). While
   somebody waits practice steps aside and More Ways stands alone. Outlined
   rather than filled, so Play Online stays what the eye goes to. Named from
   .title-actions, to win over .title-more's and .title-practice's own. */
.title-row {
  display: flex;
  align-items: stretch;
  justify-content: center;
  gap: 10px;
  width: 100%;
  margin-top: 4px;
}
.dialog[data-kind="play"] .title-actions .title-row > .choice {
  flex: 1 1 0;
  min-width: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  min-height: 48px;
  padding: 8px 11px;
  font-family: var(--font-rounded);
  font-size: calc(14px * var(--s));
  font-weight: 650;
  line-height: 1.2;
  text-align: center;
  text-wrap: balance;
  text-decoration: none;
  color: var(--parchment);
  background: rgba(255, 255, 255, .04);
  border: 1px solid rgba(232, 217, 187, .28);
  border-radius: var(--radius-sm);
}
.dialog[data-kind="play"] .title-actions .title-row > .choice:only-child { flex: 0 1 auto; padding-left: 22px; padding-right: 22px; }
@media (hover: hover) {
  .dialog[data-kind="play"] .title-actions .title-row > .choice:hover:not(:disabled) {
    color: #fff4dc;
    background: rgba(255, 255, 255, .09);
    border-color: rgba(232, 217, 187, .5);
  }
}
.title-row svg { width: 1.15em; height: 1.15em; flex: none; }
/* Two side by side: the picture at the same height in both, the words centred
   in what is left under it, however many lines either takes. */
.dialog[data-kind="play"] .title-actions .title-row > .choice:not(:only-child) {
  flex-direction: column;
  justify-content: flex-start;
  gap: 5px;
  padding-top: 10px;
  padding-bottom: 10px;
}
.title-row > .choice:not(:only-child) svg { width: 1.5em; height: 1.5em; }
.title-row > .choice:not(:only-child) .label { flex: 1 1 auto; display: flex; align-items: center; justify-content: center; }
.title-row .label { min-width: 0; }
/* More Ways to Play…, and Back from the list it opens: smaller and quieter
   still than practice, so Play Online stays what the eye goes to. */
.dialog[data-kind="play"] .title-more {
  width: auto;
  min-height: 40px;
  padding: 4px 12px;
  font-family: var(--font-rounded);
  font-size: calc(14px * var(--s));
  font-weight: 600;
  color: rgba(232, 217, 187, .8);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: rgba(232, 217, 187, .35);
}
@media (hover: hover) {
  .dialog[data-kind="play"] .title-more:hover:not(:disabled) { color: #fff4dc; background: rgba(255, 255, 255, .06); }
}
/* The other ways to play, each with a line saying what it is. */
.dialog[data-kind="play"] .title-mode { justify-content: flex-start; text-align: left; min-width: calc(250px * var(--s)); }
.title-mode .label { display: flex; flex-direction: column; }
.title-mode .sub { font-size: calc(13px * var(--s)); font-weight: 500; color: rgba(232, 217, 187, .72); }
.title-actions .label { font-size: inherit; font-weight: inherit; }

/* The record: online first, as badges; practice after, in a quieter line. */
.title-record {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  margin-top: calc(14px * var(--s));
}
.title-record[hidden] { display: none; }
.record-badges {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 8px;
}
.record-online { font-size: 13px; font-weight: 850; color: var(--gold); }
.record-badge {
  --tint: var(--gold);
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 10px;
  border-radius: 999px;
  background: rgba(0, 0, 0, .3);
  border: 1px solid color-mix(in srgb, var(--tint) 45%, transparent);
  font-size: 14px;
  font-weight: 650;
  color: var(--cream);
}
.record-badge svg { width: 15px; height: 15px; color: var(--tint); flex: none; }
.record-badge.loss { --tint: #e86b5c; }
.record-badge.draw { --tint: #9fc7ae; }
.dialog[data-kind="play"] .record-none { font-size: 14px; font-weight: 550; color: rgba(232, 217, 187, .85); }
.dialog[data-kind="play"] .record-practice { font-size: 13px; font-weight: 550; color: rgba(232, 217, 187, .72); }

.dialog[data-kind="play"] .dialog-body .reload-note { margin-top: 14px; color: rgba(232, 217, 187, .8); }

/* The language (languageControl): a word and a list, each language named in
   itself. 16px keeps iOS from zooming the page as the list opens. */
.lang-picker { display: flex; align-items: center; gap: 10px; margin: 0 0 14px; font-size: 15px; font-weight: 600; }
.lang-picker > span { flex: none; }
.lang-picker select { flex: 1; min-height: 40px; font: inherit; font-size: 16px; font-weight: 500; }
@media (pointer: coarse) { .lang-picker select { min-height: 44px; } }

/* The welcome (showAgreementDialog): a greeting, not a form. The language a
   small pill in the corner, the pieces, one question, the two documents as
   links, and one big Continue. */
.dialog[data-kind="agreement"] { --s: .78; }
.dialog[data-kind="agreement"] .dialog-body { text-align: center; }
.agreement-top { display: flex; justify-content: flex-end; margin: -6px 0 0; }
.lang-compact {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  padding: 0 4px 0 11px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--card-2);
  color: var(--ink-soft);
}
.lang-compact svg { width: 16px; height: 16px; flex: none; }
.lang-compact select {
  min-height: 34px;
  padding: 0 4px;
  border: 0;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 16px;
  font-weight: 600;
}
/* The title bar stays for a screen reader; the sheet shows the title itself. */
.dialog[data-kind="agreement"] .dialog-title {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.dialog[data-kind="agreement"] .dialog-body { padding-top: 14px; }
.dialog[data-kind="agreement"] .title-logo { margin: -8px auto 6px; }
.dialog-body .agreement-title { margin: 0 0 14px; font-size: 22px; font-weight: 800; line-height: 1.25; }

/* Japanese may break a line between any two characters, which left the last
   one of a heading alone on a line: 「ようこそ」 as 「ようこ」 and 「そ」.
   Its headings are balanced instead, and broken between phrases where the
   browser can; so are the front door's two smaller buttons, whose
   「コンピューターと練習」 broke as 「コンピュー」「ターと練習」. */
:lang(ja) :is(.agreement-title, .dialog-title, h1, h2, h3, .title-row .label) {
  text-wrap: balance;
  word-break: auto-phrase;
}
.dialog-body .agreement-question { margin: 0 0 10px; font-size: 17px; font-weight: 650; }
.birth { display: grid; gap: 8px; max-width: 300px; margin: 0 auto 18px; }
.birth.with-month { grid-template-columns: 1fr 1.3fr; }
.birth select { width: 100%; min-height: 46px; font: inherit; font-size: 17px; font-weight: 600; text-align: center; }
.birth select[hidden] { display: none; }
.dialog-body .agreement-lead { margin: 0 0 6px; color: var(--ink-soft); font-size: 15px; }
.dialog-body .agreement-links { display: flex; flex-wrap: wrap; justify-content: center; gap: 4px 18px; margin: 0 0 18px; }
.agreement-links a { color: var(--accent-dark); font-weight: 650; }
#agreement-continue { width: 100%; min-height: 50px; font-size: 18px; font-weight: 700; }
#agreement-continue:disabled { opacity: .45; }
.dialog-body .agreement-note { margin: 10px 0 0; font-size: 13px; }

/* Under the age for online play, the front door says why (renderPlayChoices). */
.title-note { margin: 6px 0 0; font-size: 14px; line-height: 1.4; text-align: center; }
.dialog[data-kind="play"] .title-note { color: rgba(232, 217, 187, .85); }


/* Showing which country you're from (countryControl): a tick box, off to
   begin with, and the list under it once it is ticked. At the foot of the
   front door, quiet, and never in the way of Play. */
/* The words are sized to the box, not the screen, so they stay on one line
   on a phone: 4.9% of the box's width, which leaves Safari's slightly wider
   letters room, and never more than 15px. Should a box ever be narrower
   still, they break into two even lines beside the tick rather than leave
   one word on its own. */
.country-box {
  container-type: inline-size;
  align-self: stretch;
  margin-top: calc(14px * var(--s, 1));
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
.country-tick {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 36px;
  margin: 0;
  font-size: clamp(12px, 4.9cqw, 15px);
  font-weight: 600;
  line-height: 1.25;
  text-align: left;
  text-wrap: balance;
  cursor: pointer;
}
.country-tick input { flex: none; width: 18px; height: 18px; margin: 0; accent-color: var(--accent); cursor: pointer; }
.country-choice { margin: 4px 0 4px; }
.country-choice[hidden] { display: none; }
/* 16px keeps iOS from zooming the page as the list opens. */
#country-select { width: 100%; min-height: 44px; font: inherit; font-size: 16px; }
.dialog[data-kind="play"] .country-box { border-color: rgba(242, 195, 60, .25); background: rgba(0, 0, 0, .22); }
.dialog[data-kind="play"] .country-tick { color: var(--cream); }
.dialog[data-kind="country"] .country-box { border: 0; padding: 0; margin: 0; }
.dialog[data-kind="country"] .country-tick { justify-content: flex-start; }
@media (pointer: coarse) { .country-tick { min-height: 44px; } }
.dialog[data-kind="play"] .link-button { color: var(--gold); }

/* Picking the computer's strength, on the wood. The pill, the record and
   the pieces are put away for it; the name stays, so it is still the same
   card. */
.dialog[data-kind="play"] .dialog-body:has(.choice-grid[data-step="level"]) > :is(.lobby-pill, .title-record) { display: none; }
.dialog[data-kind="play"] .dialog-body:has(.choice-grid[data-step="level"]) .title-logo { display: none; }
.dialog[data-kind="play"] .choice-grid[data-step="level"] { align-items: stretch; text-align: left; gap: 10px; }
.dialog[data-kind="play"] .level-picker {
  background: rgba(0, 0, 0, .28);
  border-color: rgba(242, 195, 60, .25);
  color: var(--cream);
}
.dialog[data-kind="play"] .level-number { color: var(--gold); }
.dialog[data-kind="play"] .level-of,
.dialog[data-kind="play"] .level-scale { color: rgba(232, 217, 187, .78); }
.dialog[data-kind="play"] #level-slider::-webkit-slider-runnable-track {
  background: linear-gradient(to right, var(--accent) var(--fill), rgba(255, 255, 255, .2) var(--fill));
}
.dialog[data-kind="play"] #level-slider::-moz-range-track { background: rgba(255, 255, 255, .2); }
.dialog[data-kind="play"] .first-picker legend { color: var(--cream); }
.dialog[data-kind="play"] .first-options {
  background: rgba(0, 0, 0, .28);
  border-color: rgba(242, 195, 60, .25);
}
.dialog[data-kind="play"] .first-option { color: var(--parchment); }
@media (hover: hover) {
  .dialog[data-kind="play"] .first-option:hover { color: #fff4dc; background: rgba(255, 255, 255, .08); }
}
.dialog[data-kind="play"] .first-option:has(input:checked) { color: #fff; background: var(--accent); }
.dialog[data-kind="play"] .first-option:has(input:focus-visible) { outline-color: var(--gold); }
.dialog[data-kind="play"] .level-play { font-family: var(--font-rounded); }

/* Smaller screens, a smaller card. */
@media (max-width: 400px), (max-height: 720px) {
  .dialog[data-kind="play"] { --s: .86; }
}
@media (max-width: 340px) {
  .overlay:has(> .dialog[data-kind="play"]) { padding-left: 8px; padding-right: 8px; }
  .dialog[data-kind="play"] { --s: .8; }
  .dialog[data-kind="play"] .dialog-body { padding-left: 14px; padding-right: 14px; }
  .dialog[data-kind="play"] .title-practice { padding-left: 6px; padding-right: 6px; }
}

/* A phone on its side: the name on the left, the rest on the right, so the
   card fits the height there is. */
@media (orientation: landscape) and (max-height: 520px) {
  .dialog[data-kind="play"] { --s: .8; width: min(640px, 100%); }
  .dialog[data-kind="play"] .dialog-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
    align-items: center;
    justify-items: center;
    column-gap: 18px;
    padding: 18px 22px;
  }
  .dialog[data-kind="play"] .title-head { grid-column: 1; grid-row: 1 / span 4; }
  .dialog[data-kind="play"] .dialog-body > :not(.title-head) { grid-column: 2; }
  .dialog[data-kind="play"] .dialog-body .lobby-pill { margin-top: 0; }
  .dialog[data-kind="play"] .title-actions { margin-top: 12px; }
  /* Choosing a strength wants the whole width. */
  .dialog[data-kind="play"] .dialog-body:has(.choice-grid[data-step="level"]) { display: block; }
  .dialog[data-kind="play"] .dialog-body:has(.choice-grid[data-step="level"]) .title-head { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .pill-dot { animation: none; }
}
.lite .pill-dot { animation: none; }

/* ---------- Face to face ----------

   Pass and play on a tablet lying flat between two people, being tried out
   (?test=passplay; faceToFaceNow in app.js). The screen is in two halves:
   Red's panel the right way up along the bottom edge, White's turned round
   along the top, and the board between them as big as the rest allows. Each
   panel says to its own player when it is their turn, so the caption under
   the board, which could face only one of them, is not shown; nor are the
   messages, with nobody to send them to. Laid out the same way whatever the
   size, so these rules come last and name the body. */
body.face-to-face .app { max-width: none; }
body.face-to-face .status-cell,
body.face-to-face .status-blurb,
body.face-to-face .chat,
body.face-to-face .board-caption,
body.face-to-face #btn-draw,
body.face-to-face .player-clock { display: none; }
body.face-to-face .players { display: contents; }
body.face-to-face .table {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr) auto;
  gap: 10px;
}
body.face-to-face .player-panel { order: 0; }
body.face-to-face .board-area { order: 1; }
body.face-to-face .player-panel.mine { order: 2; margin: 0; }
body.face-to-face .player-panel {
  flex-direction: row;
  flex-wrap: nowrap;
  align-items: center;
  gap: 14px;
  width: min(100%, 760px);
  justify-self: center;
  min-height: 66px;
  padding: 10px 18px;
  background: var(--card);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
body.face-to-face .player-panel.active {
  background: var(--card);
  box-shadow: var(--shadow), inset 0 0 0 3px #4ade80;
}
/* White's half, turned to the player sitting across the table. */
body.face-to-face .player-panel:not(.mine) { rotate: 180deg; }
body.face-to-face .player-piece { order: -1; padding: 0; }
body.face-to-face .player-piece .piece { width: 38px; height: 38px; }
body.face-to-face .player-name-plate {
  width: auto;
  min-width: 112px;
  flex: none;
  padding: 7px 16px;
  font-size: 18px;
  text-align: center;
  color: #fff;
}
body.face-to-face .player-name-plate.red { background: linear-gradient(180deg, #e8564a, var(--red-b)); }
body.face-to-face .player-name-plate.white { background: linear-gradient(180deg, #8d8878, #5f5b4f); }
body.face-to-face .player-tally { color: var(--ink-soft); font-size: 16px; }
body.face-to-face .player-turn,
body.face-to-face .player-turn.your-turn,
body.face-to-face .player-turn.waiting {
  font-size: 19px;
  font-weight: 700;
  white-space: nowrap;
  min-height: 0;
}
body.face-to-face .player-turn.your-turn { color: #15803d; }
body.face-to-face .player-turn.waiting { color: var(--ink-soft); }
body.face-to-face .player-actions { margin: 0 0 0 auto; flex-direction: row; width: auto; }
body.face-to-face .player-actions .btn { min-height: 44px; padding: 8px 16px; font-size: 15px; }
/* White's kings wear their crowns the right way up for White. */
body.face-to-face .piece.white.king::after { rotate: 180deg; }
/* A question for White — Resign — asked the right way up for them. */
body.face-to-face .dialog.turned { rotate: 180deg; }
