/* OGS live play — hub, session screen, profile.
 *
 * Built on the same tokens and rhythm as the vs-AI play screen so
 * moving between "play a bot" and "play a person" doesn't feel like
 * changing products. Everything themes through tokens.css; no colour
 * is declared only inside a media query.
 *
 * Naming is scoped (.ogs-*) on purpose. There is no global .btn in
 * this codebase — each surface defines its own button, the way
 * play.css does with .play-rail-btn — so these carry their own chrome
 * rather than inheriting something that doesn't exist.
 */

/* ── Buttons ──────────────────────────────────────────────────── */

.ogs-btn,
a.ogs-btn:link,
a.ogs-btn:visited {
  /* :link/:visited pinned for the same reason play.css pins them —
   * base.css gives anchors an amber hover that otherwise bleeds
   * through button-shaped links in dark mode. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 10px 16px;
  border-radius: 999px;
  border: 1px solid var(--color-divider);
  background: var(--color-surface-card);
  color: var(--color-text);
  text-decoration: none;
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  transition:
    border-color var(--dur-short) var(--ease-out),
    background var(--dur-short) var(--ease-out);
}
.ogs-btn:hover,
a.ogs-btn:hover { border-color: var(--color-wood); color: var(--color-text); }
.ogs-btn:disabled,
.ogs-btn[disabled] { opacity: 0.45; cursor: not-allowed; }
.ogs-btn:disabled:hover { border-color: var(--color-divider); }

.ogs-btn-primary,
a.ogs-btn-primary:link,
a.ogs-btn-primary:visited {
  background: var(--color-text);
  color: var(--color-surface-card);
  border-color: var(--color-text);
}
/* ⚠️ This hover was UNREADABLE in dark mode and fine in light, which is
   why it survived: `--color-wood-dark` (#4A3B2A) is one of the few
   tokens that does NOT flip between themes, while `--color-surface-card`
   does. In light mode that paints near-white on deep brown; in dark it
   painted #2C2620 on #4A3B2A, about 1.2:1.

   Both tokens below are theme-invariant, so this pair resolves
   identically in either theme and cannot drift apart again. Amber
   ground, deep brown label: ~5:1, and it reads as the key accent, which
   a primary action should. */
.ogs-btn-primary:hover,
a.ogs-btn-primary:hover {
  background: var(--color-wood);
  border-color: var(--color-wood);
  color: var(--color-wood-dark);
}

/* ── Busy ─────────────────────────────────────────────────────────
   A button that fires a network call has to say so on the press, not
   when the answer lands. Matchmaking is the worst case: "Find a game"
   opens a socket first, so there was a real gap where nothing had
   visibly happened and pressing again looked like the right move. */
.ogs-btn.is-busy,
.ogs-btn[aria-busy="true"] {
  cursor: progress;
  /* Keep the label in place; the spinner is additive, so the button
     doesn't resize and shift everything next to it. */
  color: var(--color-text-secondary);
}
.ogs-btn.is-busy::before,
.ogs-btn[aria-busy="true"]::before {
  content: "";
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-top-color: transparent;
  animation: ogs-spin 0.7s linear infinite;
  flex: 0 0 auto;
}
.ogs-btn-primary.is-busy,
.ogs-btn-primary[aria-busy="true"] { color: var(--color-surface-card); }
@keyframes ogs-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .ogs-btn.is-busy::before,
  .ogs-btn[aria-busy="true"]::before { animation: none; opacity: 0.6; }
}

/* ── Focus ────────────────────────────────────────────────────────
   A neutral ring on the OGS pages, never the site's amber (round 9, the
   user: the orange outline on a focused field, "same for dropdown. and
   everything else really"). A keyboard still sees where it is; a text
   field says it with its border and a faint halo, as it has a caret
   besides. Scoped to the pages' roots and their page-level dialogs, so
   the site header keeps the site's own ring. */
:root { --ogs-focus: color-mix(in srgb, var(--color-text) 42%, transparent); }

/* ── Metrics (mobile review, 2026-10-08) ───────────────────────────
   One value per measure, so that a column has one card edge and one
   content edge, and a pill is one height on every page.
     --ogs-pad     a card's inner padding: where its content starts
     --ogs-radius  a card's corners
     --ogs-pill-h  the head row's account pill, and so the row itself */
:root {
  --ogs-pad: 20px;
  --ogs-radius: 16px;
  --ogs-pill-h: 34px;
}
@media (max-width: 520px) { :root { --ogs-pad: 16px; } }
:is(.ogs-hub, #ogs-session, #ogs-spectate, .ogs-modal-scrim, #ogs-messages) :focus-visible {
  outline-color: var(--ogs-focus);
}
:is(.ogs-hub, #ogs-session, #ogs-spectate, .ogs-modal-scrim, #ogs-messages)
  :is(input:not([type]), input[type="text"], input[type="search"], input[type="number"],
      input[type="url"], textarea):is(:focus, :focus-visible) {
  outline: none;
  border-color: color-mix(in srgb, var(--color-text) 38%, transparent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-text) 7%, transparent);
}

/* ── Pressed ──────────────────────────────────────────────────────
   Every control answers the press, not only the hover (user, round 5:
   a button with "no special hover or pressed state" feels dead). The
   site's press (capybara.css .cb-act): down a pixel and a touch smaller
   for a button, down a pixel for a row. Movement rather than colour, so
   it reads on every theme; a dimming instead where motion is unwanted. */
:is(.ogs-btn, .ogs-action, .ogs-icon-btn, .ogs-clock-btn, .ogs-chat-send,
    .ogs-seg-opt, .ogs-opp-pill, .ogs-link-btn, .ogs-quick-phrase, .ogs-speed-tile,
    .ogs-chat-channel, .ogs-watchers-sort):active:not(:disabled) {
  transform: translateY(1px) scale(0.98);
}
:is(.ogs-menu-item, .ogs-gameinfo-toggle, .ogs-more-toggle,
    .ogs-watchers-toggle, .ogs-switch-row, .ogs-opp-option,
    .ogs-online-row, .ogs-chat-line.is-jump):active:not(:disabled) {
  transform: translateY(1px);
}
@media (prefers-reduced-motion: reduce) {
  :is(.ogs-btn, .ogs-action, .ogs-icon-btn, .ogs-clock-btn, .ogs-chat-send,
      .ogs-seg-opt, .ogs-opp-pill, .ogs-link-btn, .ogs-quick-phrase, .ogs-speed-tile,
      .ogs-chat-channel, .ogs-watchers-sort, .ogs-menu-item,
      .ogs-gameinfo-toggle, .ogs-more-toggle, .ogs-watchers-toggle,
      .ogs-switch-row, .ogs-opp-option, .ogs-online-row,
      .ogs-chat-line.is-jump):active:not(:disabled) {
    transform: none;
    filter: brightness(0.94);
  }
}

.ogs-btn-quiet { border-color: transparent; color: var(--color-text-secondary); }
.ogs-btn-quiet:hover { border-color: var(--color-divider); }

/* Resign / destructive. Terracotta, not signal red — the house
 * palette never uses traffic-light colour for state. Matches the
 * existing .play-rail-btn-danger on the vs-AI screen. */
.ogs-btn-danger { color: var(--color-mq-mistake); border-color: var(--color-mq-mistake); }
.ogs-btn-danger:hover {
  background: var(--color-mq-mistake);
  border-color: var(--color-mq-mistake);
  color: var(--color-surface-card);
}

.ogs-btn-sm { padding: 7px 12px; font-size: 12px; }

/* ── Session screen ───────────────────────────────────────────── */

.ogs-session-page {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  gap: var(--space-5);
  max-width: 1440px;
  margin: 0 auto;
  padding: var(--space-3) var(--space-4) var(--space-5);
  align-items: start;
}
/* minmax(0, 1fr), never bare 1fr: a long opponent name in the rail
 * would otherwise set the column's min-content width, the board would
 * measure that inflated value and render past the viewport. Same trap
 * documented at length in play.css. */
@media (max-width: 1024px) {
  .ogs-session-page { grid-template-columns: minmax(0, 1fr); }
  /* One column: board, then players, then everything else — the order
     the DOM is already in, and the order it has always rendered in. The
     page gap between the two rails is a little wider than the gap
     between cards inside one, which reads as the seam it is. */
}
/* ⚠️ The grid placement for the two wider bands lives at the END of this
   file, with the app shell — not here. Anything that re-declares the
   board stage has to come after its base rule below: for the cascade,
   and because tests/test_ogs_review.py finds that rule by taking the
   first mention of the selector in this file and expects the base one.
   (Which is also why this note spells the selector out in prose.) */
@media (max-width: 720px) {
  .ogs-session-page {
    padding: var(--space-2) 0 var(--space-3);
    gap: var(--space-3);
  }
  /* The rail spans the column, as the board and the head row over it do. */
  .ogs-rail { width: 100%; }
}

/* ⚠️ A COLUMN, and it has to be one. This was a flex ROW, which was
   fine for as long as the board was the only thing in it — and then
   the variation bar, the compose bar, the branch picker and the move
   bar were added as siblings and every one of them landed BESIDE the
   board, squeezing it. Nothing errored and the board still drew.
   The tell was in the CSS all along: every one of those bars sets
   `margin-top`, which only means anything to something stacked. */
.ogs-board-stage {
  position: relative;
  display: flex;
  flex-direction: column;
  /* Stretch, not centre: the bars below the board are full-width rows
     with their own internal alignment, and the board centres itself
     inside `.ogs-board-canvas`. Centring here would shrink-wrap each
     bar to its text. */
  align-items: stretch;
  justify-content: flex-start;
  width: 100%;
}
.ogs-board-canvas {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  box-sizing: border-box;
}
/* This is not the game. Enough of a shift to be unmistakable at a
   glance, not so much that the stones get hard to read — somebody is
   trying to study the line, not just notice it. */
.ogs-board-canvas.is-variation {
  outline: 2px dashed var(--color-wood);
  outline-offset: 4px;
  border-radius: 4px;
}

.ogs-variation-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: var(--space-3);
  padding: 8px 12px;
  border: 1px solid var(--color-divider);
  border-radius: 8px;
  font-size: 13px;
}
.ogs-variation-what { font-weight: 600; }
.ogs-variation-by { color: var(--color-text-tertiary); }
.ogs-variation-moved {
  color: var(--color-text-secondary);
  border-left: 1px solid var(--color-divider);
  padding-left: 8px;
}
.ogs-variation-bar .ogs-btn { margin-left: auto; }

.ogs-rail {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
}
.ogs-rail-card {
  background: var(--color-surface-card);
  border: 1px solid var(--color-divider);
  border-radius: 12px;
  padding: var(--space-3);
  min-width: 0;
}
.ogs-rail-card h2 {
  margin: 0 0 0.6rem;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}

/* ── Players + clocks ─────────────────────────────────────────── */

.ogs-players {
  padding: var(--space-2);
  background: color-mix(in srgb, var(--color-wood) 4%, var(--color-surface-card));
}
.ogs-players-rule {
  height: 1px;
  background: var(--color-divider);
  margin: 2px 8px;
}

.ogs-player-card {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  border-radius: 8px;
  border-left: 2px solid transparent;
  transition: border-color var(--dur-short) var(--ease-out);
}
/* Active turn: a faint wood rule on the leading edge. Same signal the
 * vs-AI scoreboard uses, so "whose turn" reads identically on both. */
.ogs-player-card.is-active { border-left-color: var(--color-wood); }
.ogs-player-card.is-disconnected { opacity: 0.6; }

.ogs-player-stone {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  border-radius: 50%;
}
.ogs-player-stone-black {
  background: radial-gradient(circle at 33% 30%,
    var(--color-stone-black-hi), var(--color-stone-black-mid) 55%,
    var(--color-stone-black-lo));
}
.ogs-player-stone-white {
  background: radial-gradient(circle at 33% 30%,
    var(--color-stone-white-hi), var(--color-stone-white-mid) 60%,
    var(--color-stone-white-lo));
}

/* Two rows: who (name, rank), then how they stand (clock, captures,
 * komi). The name owns its whole row, so a long one ellipsises only
 * against the card's own edge and never against a clock. */
.ogs-player-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.ogs-player-line { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.ogs-player-name {
  font-size: 14px;
  font-weight: 600;
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ogs-player-rank {
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}
.ogs-player-status {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 2px 12px;
  min-width: 0;
}
.ogs-player-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 2px 10px;
  min-width: 0;
  font-size: 12px;
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}
.ogs-player-meta:empty { display: none; }
/* With a clock on the row the standing figures sit at its far end; on a
 * finished game there is no clock and they simply start the row. */
.ogs-player-status > .ogs-clock + .ogs-player-meta { margin-left: auto; }
.ogs-player-away { color: var(--color-mq-inaccuracy); }
/* "Passed", beside the name of the player whose last move was a pass:
   until the next move, so it is still there for whoever looks up late. */
.ogs-player-passed {
  flex: 0 0 auto;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--color-surface-warm);
  color: var(--color-text-secondary);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* Captures as the stones themselves: a small stone of the colour taken,
 * then the count. The sentence ("3 captured") is the accessible name. */
.ogs-player-captures {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
.ogs-cap-stone {
  flex: 0 0 auto;
  width: 10px;
  height: 10px;
  border-radius: 50%;
}
.ogs-cap-stone.is-black {
  background: radial-gradient(circle at 33% 30%,
    var(--color-stone-black-hi), var(--color-stone-black-mid) 55%,
    var(--color-stone-black-lo));
  /* A ring, or a black stone on the dark page is a hole. */
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-text) 38%, transparent);
}
.ogs-cap-stone.is-white {
  background: radial-gradient(circle at 33% 30%,
    var(--color-stone-white-hi), var(--color-stone-white-mid) 60%,
    var(--color-stone-white-lo));
  box-shadow: 0 0 0 1px rgba(23, 10, 2, 0.22);
}
/* Either colour (random, or OGS decides): one stone split on the
 * diagonal, lit like the other two, with the black one's ring so it
 * holds on both grounds. */
.ogs-cap-stone.is-either {
  background:
    radial-gradient(circle at 33% 30%, rgba(255, 255, 255, 0.3), transparent 55%),
    linear-gradient(135deg,
      var(--color-stone-black-mid) 0 50%, var(--color-stone-white-mid) 50% 100%);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-text) 38%, transparent);
}

/* Clocks. Tabular figures so the digits don't jitter as they count
 * down — on a byo-yomi period that shift is very visible. */
.ogs-clock {
  flex: 0 0 auto;
  display: flex;
  align-items: baseline;
  gap: 3px;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-secondary);
}
.ogs-clock-main { font-size: 16px; font-weight: 600; color: var(--color-text); }
.ogs-clock-sep { font-size: 11px; color: var(--color-text-tertiary); }
.ogs-clock-period { font-size: 15px; font-weight: 600; color: var(--color-text); }
.ogs-clock-periods { font-size: 11px; color: var(--color-text-tertiary); }
/* The periods waiting behind a running main time: "+ 5×30s". */
.ogs-clock-reserve {
  margin-left: 2px;
  font-size: 13px;
  font-weight: 500;
  color: var(--color-text-secondary);
  white-space: nowrap;
}

.ogs-clock.is-active .ogs-clock-main,
.ogs-clock.is-active .ogs-clock-period { color: var(--color-text); }
/* In overtime the main time is gone; the period clock is the whole
 * story, so it takes the emphasis, running or not. (After the rule
 * above: it used to lose to it, so your own overtime never showed.) */
.ogs-clock.is-overtime .ogs-clock-period { color: var(--color-mq-inaccuracy); }
/* Last period. Terracotta rather than red, and paired with the "SD"
 * label so the meaning doesn't rest on colour alone. */
.ogs-clock .is-sudden-death { color: var(--color-mq-mistake); }
.ogs-clock.is-paused { opacity: 0.55; }

/* The clock before the first move: a short, separate allowance to get
 * the game under way. Named rather than merely shown, because the
 * number alone would read as an absurdly fast time control. */
.ogs-clock-note {
  font-size: 10px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-secondary);
}

/* Your move is on the wire. Three rising bars, drawn rather than typed
 * so no platform can decide it is an emoji. It pulses because the whole
 * point is "this is still happening", and stops the moment it goes. */
.ogs-clock-transmitting {
  align-self: center;
  display: inline-block;
  width: 9px;
  height: 8px;
  background:
    linear-gradient(var(--color-text-tertiary), var(--color-text-tertiary))
      0 100% / 2px 3px no-repeat,
    linear-gradient(var(--color-text-tertiary), var(--color-text-tertiary))
      3.5px 100% / 2px 5.5px no-repeat,
    linear-gradient(var(--color-text-tertiary), var(--color-text-tertiary))
      7px 100% / 2px 8px no-repeat;
  animation: ogs-transmit 1.1s ease-in-out infinite;
}
@keyframes ogs-transmit {
  0%, 100% { opacity: 0.35; }
  50%      { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .ogs-clock-transmitting { animation: none; opacity: 0.8; }
}

/* Why the clock stopped: Paused, Weekend, Vacation, Paused by the
 * server. Quiet, because it is a state and not an alert. */
.ogs-clock-pause {
  font-size: 10px;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--color-text-secondary);
}
.ogs-clock-note,
.ogs-clock-pause {
  align-self: center;
  margin-left: 4px;
  padding: 2px 7px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--color-text) 9%, transparent);
  line-height: 1.3;
  white-space: nowrap;
}

.ogs-pause-note {
  margin: 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--color-text-secondary);
}

/* ── Hover ghost on the live board ────────────────────────────────
 * The same weight as the review viewer and the play-vs-AI board, so
 * an empty intersection under the pointer looks the same everywhere.
 * Shudan's own faint ghost is much wispier and the difference was
 * noticeable when moving between our surfaces. */
.ogs-board-canvas .shudan-vertex .shudan-ghost,
.ogs-board-canvas .shudan-vertex .shudan-ghost::before {
  width: 0.86em;
  height: 0.86em;
}
.ogs-board-canvas .shudan-vertex .shudan-ghost { opacity: 1; }
.ogs-board-canvas .shudan-vertex .shudan-ghost::before {
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  box-shadow: none !important;
}
.ogs-board-canvas .shudan-vertex.shudan-ghost_1 .shudan-ghost::before {
  background: rgba(18, 14, 10, 0.62);
  border: 1px solid rgba(18, 14, 10, 0.35);
}
.ogs-board-canvas .shudan-vertex.shudan-ghost_-1 .shudan-ghost::before {
  background: rgba(255, 250, 240, 0.78);
  border: 1px solid rgba(24, 18, 12, 0.22);
}

/* ── Territory ────────────────────────────────────────────────────
 *
 * Squares, not shading: a small square in the owner's colour on every
 * point of territory, on a dead stone too (it is territory now, and it
 * is drawn faint underneath so the shape that died stays legible). That
 * is what Go Cafe draws and what a player counting a board reads without
 * being taught; the shaded regions were "a sea of transparency" (user,
 * 2026-10-07). The paint layer (OGSPlay.territoryPaint, the count and the
 * estimate alike) is redrawn here, on these boards only: the review
 * screen keeps its shading. */
.ogs-board-canvas .shudan-vertex[class*="shudan-paint_"] .shudan-paint::before {
  left: 33%;
  top: 33%;
  right: 33%;
  bottom: 33%;
  opacity: 1;
  border: none;
  border-radius: 1px;
}
/* White on wood needs an edge to be seen at all. */
.ogs-board-canvas .shudan-vertex.shudan-paint_-1 .shudan-paint::before {
  box-shadow: 0 0 0 1px rgba(23, 10, 2, 0.45);
}

/* ── Text on a stone ──────────────────────────────────────────────
 *
 * ⚠️ A label is TEXT, so Shudan's contrast STROKE does not colour it.
 * Without these rules every label on this board inherits the ink used
 * on an empty intersection, which on a black stone is black on black.
 * The review surface has the same three rules scoped to its own mount
 * (game-detail.css); this board never got them, so the branch numbers
 * on a shared variation have been unreadable on Black's stones the
 * whole time. */
.ogs-board-canvas .shudan-vertex.shudan-marker_label.shudan-sign_1
  .shudan-marker {
  color: var(--shudan-black-foreground-color);
  fill: var(--shudan-black-foreground-color);
}
.ogs-board-canvas .shudan-vertex.shudan-marker_label.shudan-sign_-1
  .shudan-marker {
  color: var(--shudan-white-foreground-color);
  fill: var(--shudan-white-foreground-color);
}
/* A ghost stone is translucent and has no sign of its own, so the two
 * rules above miss it. Same colours, chosen by the ghost. */
.ogs-board-canvas .shudan-vertex.shudan-ghost_1.shudan-marker_label
  .shudan-marker {
  color: var(--shudan-black-foreground-color);
  fill: var(--shudan-black-foreground-color);
}
.ogs-board-canvas .shudan-vertex.shudan-ghost_-1.shudan-marker_label
  .shudan-marker {
  color: var(--shudan-white-foreground-color);
  fill: var(--shudan-white-foreground-color);
}

/* ⭐ The countdown, which is the one label on this board that has to be
 * read without being looked at. Bigger and heavier than a branch
 * number, and never jittering as the digit changes.
 *
 * ⚠️ The hook is the marker's `preview` flag, because there is no other
 * way to tell this label apart in CSS: the countdown sits on a ghost
 * stone or on the staged stone, and a branch number can sit on either
 * kind of stone too. See the note where the layer is built. */
.ogs-board-canvas .shudan-vertex.shudan-marker-preview.shudan-marker_label
  .shudan-marker {
  font-size: 0.62em;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* ── Controls ─────────────────────────────────────────────────── */

.ogs-controls { display: flex; flex-direction: column; gap: 10px; }
.ogs-turn-line {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--color-text-secondary);
}
.ogs-turn-dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--color-text-tertiary);
  flex: 0 0 auto;
}
.ogs-turn-dot.is-yours {
  background: var(--color-wood);
  animation: ogs-pulse 2s var(--ease-out) infinite;
}
@keyframes ogs-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.35; }
}
@media (prefers-reduced-motion: reduce) {
  .ogs-turn-dot.is-yours { animation: none; }
}

.ogs-control-row { display: flex; gap: 8px; }
.ogs-control-row > * { flex: 1 1 0; }
.ogs-submit-row { display: flex; gap: 8px; }
.ogs-submit-row > * { flex: 1 1 0; }

/* ── Notices (reconnect, undo request) ────────────────────────── */

.ogs-rail-notice {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  padding: var(--space-3);
  border-radius: 12px;
  border: 1px solid var(--color-divider);
  background: var(--color-surface-warm);
  font-size: 13px;
  color: var(--color-text-secondary);
}
.ogs-rail-notice strong {
  display: block;
  color: var(--color-text);
  font-size: 13px;
  font-weight: 600;
}
.ogs-rail-notice p { margin: 4px 0 0; font-size: 12px; }
.ogs-rail-notice.is-warning { border-color: var(--color-mq-inaccuracy); }
.ogs-rail-notice-dot {
  flex: 0 0 auto;
  width: 7px; height: 7px;
  margin-top: 5px;
  border-radius: 50%;
  background: var(--color-mq-inaccuracy);
  animation: ogs-pulse 1.4s var(--ease-out) infinite;
}
@media (prefers-reduced-motion: reduce) {
  .ogs-rail-notice-dot { animation: none; }
}
.ogs-rail-notice-actions { display: flex; gap: 8px; margin-top: 8px; }

/* ── Scoring ──────────────────────────────────────────────────── */

.ogs-removal p { margin: 0 0 10px; font-size: 13px; color: var(--color-text-secondary); }
.ogs-removal-actions { display: flex; gap: 8px; }
.ogs-removal-actions > * { flex: 1 1 0; }
/* The running count, sized like the result it is about to become. */
.ogs-removal-score {
  font-size: 16px;
  font-weight: 600;
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
  margin-bottom: 6px;
}
/* OGS accepts on the player's behalf when this expires, so it reads as
   a deadline rather than decoration — but quietly, since most of the
   time it simply runs out because both sides already agreed. */
.ogs-removal-countdown {
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-secondary);
  padding: 6px 0 10px;
}
.ogs-removal .ogs-btn[disabled] { opacity: 0.55; pointer-events: none; }

/* WHERE the borders are open. Set in the same tabular figures as the
   clocks, because it is read as a list of places on a grid. */
.ogs-sealing-where {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--color-text) !important;
  margin-bottom: 4px !important;
}

/* Who has accepted. ⚠️ No red cross: "not yet" is not an error, and a
   cross beside a name reads as one. The waiting side gets a hollow
   ring and a dimmed row; the accepted side gets a tick and full ink. */
.ogs-removal-accept {
  list-style: none;
  margin: 0 0 10px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.ogs-removal-accept-row {
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-size: 13px;
  color: var(--color-text-tertiary);
}
.ogs-removal-accept-row.is-accepted { color: var(--color-text); }
.ogs-removal-accept-mark {
  width: 1em;
  text-align: center;
  font-size: 11px;
}
.ogs-removal-accept-who { overflow-wrap: anywhere; }

/* ── Game over ────────────────────────────────────────────────── */

/* The result in words (round 16b): who won, big, then how, in bold, and
   "Now 4 kyu!" beside a win that lifted your rank. Then the two actions,
   one under the other, each on one line, and the rematch as a quiet line.
   Confetti falls across the card on your win (relative + clipped). */
.ogs-gameover {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 14px;
  overflow: hidden;
  isolation: isolate;
}
.ogs-gameover-head { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.ogs-gameover-line { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }
.ogs-gameover-result {
  font-size: 24px;
  font-weight: 700;
  letter-spacing: -0.015em;
  line-height: 1.15;
  color: var(--color-text);
}
.ogs-gameover-how { font-size: 16px; font-weight: 600; color: var(--color-text); }
.ogs-gameover-rankup {
  display: inline-flex;
  align-items: center;
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--color-wood);
  color: var(--color-on-wood);
  font-size: 12.5px;
  font-weight: 700;
  white-space: nowrap;
  animation: ogs-rankup-in 0.5s cubic-bezier(0.3, 1.6, 0.5, 1) both;
}
@keyframes ogs-rankup-in { from { opacity: 0; transform: scale(0.6); } to { opacity: 1; transform: scale(1); } }
.ogs-gameover-actions { display: flex; flex-direction: column; gap: 8px; }
.ogs-gameover-actions > .ogs-btn {
  width: 100%;
  min-height: 44px;
  justify-content: flex-start;
  gap: 8px;
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
}
.ogs-gameover-cta > .ogs-icon:last-child { margin-left: auto; opacity: 0.75; }
.ogs-gameover-logo {
  flex: none;
  width: 18px;
  height: 18px;
  border-radius: 5px;
  background: url("../brand/favicon-32.png") center / cover no-repeat;
}
.ogs-gameover-error { margin: 0; font-size: 13px; color: var(--color-text-secondary); text-align: center; }
.ogs-gameover-error:empty { display: none; }

/* Confetti: paper falling across the card, once (GameOverCard Confetti). */
.ogs-confetti { position: absolute; inset: 0; pointer-events: none; z-index: 2; }
.ogs-confetti i {
  position: absolute;
  top: -12px;
  width: 6px;
  height: 10px;
  border-radius: 2px;
  opacity: 0;
  /* Nearly steady once falling, as paper is: an ease-out this slow
     looked like it stopped in mid-air. */
  animation: ogs-confetti-fall var(--fall-time) cubic-bezier(0.3, 0.45, 0.6, 1) var(--fall-delay) forwards;
}
@keyframes ogs-confetti-fall {
  0% { opacity: 1; transform: translate(0, 0) rotate(0); }
  85% { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--fall-x), 300px) rotate(var(--fall-turn)); }
}
@media (prefers-reduced-motion: reduce) {
  .ogs-confetti { display: none; }
  .ogs-gameover-rankup { animation: none; }
}
.ogs-gameover-body {
  margin: 0;
  font-size: 13px;
  color: var(--color-text-secondary);
}
.ogs-gameover-pending {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 10px;
  font-size: 13px;
  color: var(--color-text-tertiary);
}


/* ── The result did not count ─────────────────────────────────── */

/* Struck through, upstream's own treatment, and the right one: the
 * headline still says who won, because that happened, and the line
 * through it says the record does not carry it. */
.ogs-gameover-result.is-annulled,
.ogs-rail-card h2.is-annulled {
  text-decoration: line-through;
  text-decoration-thickness: 1px;
  color: var(--color-text-secondary);
}
.ogs-annulled {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 8px 10px;
  border-left: 2px solid var(--color-divider);
  font-size: 13px;
  color: var(--color-text-secondary);
}
.ogs-annulled strong { color: var(--color-text); }
.ogs-annulled p { margin: 0; }

/* ── Where the points came from ───────────────────────────────── */

.ogs-player-score {
  /* Inherits the meta row's size and colour whether it came out as a
   * button or a bare span, so the two forms are indistinguishable
   * until the pointer is over one. */
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  text-align: left;
}
.ogs-player-score.is-toggle { cursor: pointer; }
/* ⚠️ `color`, not `background`: an amber row highlight here would read
 * as a link, which this is not. See the alignment conventions note. */
.ogs-player-score.is-toggle:hover,
.ogs-player-score.is-toggle:focus-visible,
.ogs-player-score.is-open { color: var(--color-text); }
.ogs-player-points { font-variant-numeric: tabular-nums; }

.ogs-score-breakdown {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 6px;
  padding: 6px 8px;
  border-radius: 4px;
  background: var(--color-surface-warm);
  border: 1px solid var(--color-divider);
  font-size: 11px;
  color: var(--color-text-secondary);
}
.ogs-score-row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
}
.ogs-score-value { font-variant-numeric: tabular-nums; }
.ogs-score-row.is-head {
  padding-bottom: 4px;
  margin-bottom: 2px;
  border-bottom: 1px solid var(--color-divider);
}
.ogs-score-row.is-total {
  padding-top: 4px;
  margin-top: 2px;
  border-top: 1px solid var(--color-divider);
  color: var(--color-text);
  font-weight: 600;
}

/* ── What game this is ────────────────────────────────────────── */

/* The flag is an emoji, so its size is the font's business and not
 * ours. Nudged up a fraction because the regional-indicator pair sits
 * low against a baseline shared with the name. */
.ogs-player-flag {
  flex: 0 0 auto;
  font-size: 12px;
  line-height: 1;
  position: relative;
  top: -1px;
}
/* The circled handicap number and the komi are both standing
 * adjustments to this card's score, so they read as one pair with the
 * capture count rather than as separate badges. */
/* A touch larger than the row: at eleven pixels the circled numeral is
 * a smudge, and a number you cannot read is decoration. */
.ogs-player-handicap {
  flex: 0 0 auto;
  font-size: 13px;
  line-height: 1;
}
.ogs-player-komi {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
}

.ogs-gameinfo { padding: 0; }
.ogs-gameinfo-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  padding: var(--space-3);
  background: none;
  border: 0;
  cursor: pointer;
  text-align: left;
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}
.ogs-gameinfo-toggle:hover,
.ogs-gameinfo-toggle:focus-visible,
.ogs-gameinfo-toggle.is-open { color: var(--color-text); }
/* The hover ground is the site's button hover (base.html), neutral, never
 * amber (an amber row reads as a link), and shaped by this card: its
 * inner corners while it is the whole card; open, it is the card's head,
 * so its bottom edge is square and the rows start a little below it
 * (user, 2026-10-07). */
.ogs-gameinfo-toggle { border-radius: 11px; }
.ogs-gameinfo-toggle:hover { background: var(--color-surface-overlay); }
.ogs-gameinfo-toggle.is-open {
  border-radius: 11px 11px 0 0;
  margin-bottom: 8px;
}
/* The drawn chevron (OGSPlay.Chevron): one size and one turn for every
 * disclosure on these pages. Its own size, never the label's: an
 * uppercase 12px label would make it a speck again. */
.ogs-chevron {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  max-width: none;               /* base.css squeezes svg to its box */
  transition: transform var(--dur-short) var(--ease-out);
}
@media (prefers-reduced-motion: reduce) { .ogs-chevron { transition: none; } }
.ogs-gameinfo-chevron { margin-left: auto; }
.ogs-gameinfo-toggle.is-open .ogs-gameinfo-chevron { transform: rotate(180deg); }

.ogs-gameinfo-rows {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 0;
  padding: 0 var(--space-3) var(--space-3);
  font-size: 12px;
}
/* Label left, value right, and the value wraps rather than pushing the
 * rail wider — the clock sentence is two lines on a narrow column. */
.ogs-gameinfo-row {
  display: grid;
  grid-template-columns: minmax(0, 5.5rem) minmax(0, 1fr);
  gap: 10px;
  align-items: baseline;
}
.ogs-gameinfo-row dt { color: var(--color-text-tertiary); }
.ogs-gameinfo-row dd {
  margin: 0;
  color: var(--color-text-secondary);
  overflow-wrap: anywhere;
}

/* ── The dock's pieces ─────────────────────────────────────────────
   Upstream's dock was a card on the old rail; its rows, hints and the
   sound line now sit inside the game menu and the report form. Quiet
   by default: a player passes over them a hundred times a game without
   wanting anything from them. */

.ogs-dock-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}
.ogs-dock-hint {
  margin: 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--color-text-tertiary);
}
.ogs-dock-note {
  margin: 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--color-text-secondary);
}

/* Calling a moderator. Unfolded on demand, so the fields only exist
   once somebody has decided to write one. */
.ogs-dock-report {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--color-divider);
}
.ogs-dock-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 12px;
  color: var(--color-text-tertiary);
}
.ogs-dock-field textarea {
  width: 100%;
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid var(--color-divider);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  font-size: 13px;
}
.ogs-dock-field textarea { resize: vertical; line-height: 1.45; }
/* ⚠️ Not a red panel. A refusal here is the client saying "that report
   would not stand up", which is help rather than an error, and the
   house rule is ink over traffic lights. */
.ogs-dock-refusal {
  margin: 0;
  padding: 8px 10px;
  border-left: 2px solid var(--color-text-tertiary);
  font-size: 12px;
  line-height: 1.45;
  color: var(--color-text-secondary);
}

/* ── Chat ─────────────────────────────────────────────────────── */

.ogs-chat { display: flex; flex-direction: column; min-height: 0; }
.ogs-chat-scroll {
  max-height: 260px;
  overflow-y: auto;
  overflow-x: hidden;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding-right: 4px;
}
/* Nothing said yet: centred in the log's room (margin auto inside the
   scroller's column), an icon over one line of invitation. */
.ogs-chat-empty {
  margin: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  max-width: 240px;
  padding: var(--space-5) var(--space-2);
  text-align: center;
}
.ogs-chat-empty p {
  margin: 0;
  font-size: 14px;
  line-height: 1.5;
  color: var(--color-text-secondary);
}
.ogs-chat-empty-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--color-wood) 16%, transparent);
  color: var(--color-wood-ink);
}
.ogs-chat-empty-icon .ogs-icon { width: 22px; height: 22px; max-width: none; }
/* ⭐ One row per line, one grid for the whole log (round 12, the user:
   move, time, name and channel "all of it on one line", and "regardless
   of name length, tag, etc everything should be aligned vertically and
   not be skewed"). The rows are subgrids of .ogs-chat-rows, so the four
   columns (the move, the name, the words, the time) are shared by every
   row and each is as wide as its widest entry; the words wrap inside
   their own column. A name past the cap ends in an ellipsis. Words at
   14px in the full text ink: design round 2 raised them from 13px for
   the dark theme, and 15px left the words too narrow a column in the
   rail's 300px. */
.ogs-chat-rows {
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr) auto;
  column-gap: 10px;
  align-content: start;
}
.ogs-chat-line {
  position: relative;
  display: grid;
  grid-template-columns: subgrid;
  grid-column: 1 / -1;
  align-items: baseline;
  padding: 2px 2px 2px 8px;
  border-radius: 6px;
  font-size: 14px;
  line-height: 1.45;
}
/* A new speaker starts a little below the run before. */
.ogs-chat-rows > .ogs-chat-line:not(.is-continued) { margin-top: 6px; }
.ogs-chat-rows > .ogs-chat-line:first-child { margin-top: 0; }
.ogs-chat-line:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }
/* The row goes to its move (round 14). */
.ogs-chat-line.is-jump { cursor: pointer; }
.ogs-chat-cell { min-width: 0; }
.ogs-chat-rows .ogs-chat-who,
.ogs-chat-rows a.ogs-chat-who:link,
.ogs-chat-rows a.ogs-chat-who:visited {
  justify-self: start;
  max-width: 6em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--color-text-secondary);
  font-size: 12.5px;
  font-weight: 600;
}
.ogs-chat-body {
  min-width: 0;
  color: var(--color-text);
  overflow-wrap: anywhere;
}

/* When it was said, at the end of the row. Quiet enough to skip when
   you are reading the conversation, there when you are reading the
   game. Tabular figures so the column does not shimmer. */
.ogs-chat-time {
  justify-self: end;
  color: var(--color-text-secondary);
  opacity: 0.8;
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
}

/* The day changed. A rule with the date sitting on it, across the
   whole grid. */
.ogs-chat-day {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 10px 0 4px;
  font-size: 11px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--color-text-secondary);
}
.ogs-chat-rows > .ogs-chat-day:first-child { margin-top: 0; }
.ogs-chat-day::before,
.ogs-chat-day::after {
  content: "";
  flex: 1 1 auto;
  border-top: 1px solid var(--color-divider);
}

/* ⭐ The move it was said at, first in the row, where the move changes.
   A button, because pressing it takes the board there; a small tag so a
   bare number reads as a move and not a count. Every tag as wide as the
   column (round 14, the user: the numbers were "all different widths
   visually"), so "7" and "189" are one shape down the log. */
.ogs-chat-move {
  justify-self: stretch;
  text-align: center;
  appearance: none;
  border: 0;
  margin: 0;
  padding: 0 4px;
  border-radius: 4px;
  background: color-mix(in srgb, var(--color-text) 7%, transparent);
  font: inherit;
  font-size: 11px;
  font-weight: 600;
  line-height: 17px;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-secondary);
  cursor: pointer;
  transition: background var(--dur-short) var(--ease-out),
              color var(--dur-short) var(--ease-out);
}
.ogs-chat-move:hover,
.ogs-chat-move:focus-visible {
  background: color-mix(in srgb, var(--color-wood) 30%, transparent);
  color: var(--color-text);
}
.ogs-chat-move.is-static { cursor: default; }
.ogs-chat-move.is-static:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); color: var(--color-text-secondary); }

/* The channel, when it is not the room everybody is in: a bar at the
   start of the row, in the colour the "Send to" picker shows beside the
   channel's name. Its hit area is wider than the bar, for the hover
   that names it. */
.ogs-chat-malkovich,
.ogs-chat-channel[data-channel="malkovich"] { --ogs-chan: var(--color-tier-deep); }
.ogs-chat-personal,
.ogs-chat-channel[data-channel="personal"] { --ogs-chan: var(--color-wood); }
.ogs-chat-spectator,
.ogs-chat-channel[data-channel="spectator"] { --ogs-chan: color-mix(in srgb, var(--color-text) 30%, transparent); }
.ogs-chat-mark {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 8px;
}
.ogs-chat-mark::before {
  content: "";
  position: absolute;
  top: 3px;
  bottom: 0;
  left: 2px;
  width: 3px;
  border-radius: 2px;
  background: var(--ogs-chan, transparent);
}
.ogs-chat-line.is-continued .ogs-chat-mark::before { top: 0; border-top-left-radius: 0; border-top-right-radius: 0; }
.ogs-chat-line:has(+ .ogs-chat-line.is-continued) .ogs-chat-mark::before { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }

/* A `/me` line is narration, not speech: the name in its column, the
   rest in italics. */
.ogs-chat-line.is-action .ogs-chat-body {
  font-style: italic;
  color: var(--color-text-secondary);
}

/* A board point somebody named. Dotted, so it reads as something to
   put the pointer on rather than something to click. */
.ogs-chat-coord {
  border-bottom: 1px dotted var(--color-text-tertiary);
  cursor: default;
}
.ogs-chat-coord:hover {
  color: var(--color-wood);
  border-bottom-color: var(--color-wood);
}

/* OGS will not take a message from this account. Said once, where the
   box would have been. */
.ogs-chat-blocked {
  margin: 10px 0 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--color-text-tertiary);
}

/* Five things you say over and over. */
.ogs-quick-wrap { margin-top: 8px; }
.ogs-quick-chat { margin-top: 8px; }
.ogs-quick-list {
  list-style: none;
  margin: 0 0 8px;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}
.ogs-quick-list li { display: flex; min-width: 0; }
.ogs-quick-phrase {
  appearance: none;
  border: 1px solid var(--color-divider);
  background: transparent;
  color: var(--color-text);
  font: inherit;
  font-size: 12px;
  padding: 3px 10px;
  border-radius: 999px;
  cursor: pointer;
}
.ogs-quick-phrase:hover,
.ogs-quick-phrase:focus-visible {
  border-color: var(--color-wood);
  background: color-mix(in srgb, var(--color-wood) 12%, transparent);
}
.ogs-quick-list input {
  width: 100%;
  min-width: 0;
  padding: 3px 8px;
  border-radius: 6px;
  border: 1px solid var(--color-divider);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 12px;
}
/* One per row while editing: a phrase is a sentence, and five sentences
   sharing a wrapped row cannot be read, let alone edited. ⚠️ Driven by
   a class the component sets, not by `:has(input)` — the selector would
   be prettier and would silently do nothing wherever `:has` is not
   supported, which is the kind of failure nobody notices in review. */
.ogs-quick-chat.is-editing .ogs-quick-list { flex-direction: column; }
.ogs-quick-actions { display: flex; gap: 6px; }

/* Thinking-out-loud reads quieter than speech, because it is not
   addressed to anybody. A private note quieter still. */
.ogs-chat-malkovich .ogs-chat-body { font-style: italic; }
.ogs-chat-personal .ogs-chat-body { color: var(--color-text-secondary); }

/* A shared line is the most interesting thing anybody says in a game
   chat, so it looks like something to press rather than a label. */
.ogs-chat-variation {
  appearance: none;
  white-space: nowrap;
  border: 1px solid var(--color-divider);
  background: transparent;
  color: var(--color-text);
  font: inherit;
  padding: 1px 8px;
  border-radius: 999px;
  cursor: pointer;
  transition: border-color var(--dur-short) var(--ease-out),
              background-color var(--dur-short) var(--ease-out);
}
.ogs-chat-variation:hover,
.ogs-chat-variation:focus-visible {
  border-color: var(--color-wood);
  background: color-mix(in srgb, var(--color-wood) 12%, transparent);
}

/* A shared REVIEW, which goes somewhere rather than doing something.
   Same pill as a shared line because they read as the same offer, but
   an anchor, so it middle-clicks and copies like the link it is. */
.ogs-chat-review {
  display: inline-block;
  border: 1px solid var(--color-divider);
  color: var(--color-text);
  text-decoration: none;
  padding: 1px 8px;
  border-radius: 999px;
  transition: border-color var(--dur-short) var(--ease-out),
              background-color var(--dur-short) var(--ease-out);
}
.ogs-chat-review:hover,
.ogs-chat-review:focus-visible {
  border-color: var(--color-wood);
  background: color-mix(in srgb, var(--color-wood) 12%, transparent);
  /* ⚠️ Repeated, not inherited. A bare `a:hover` elsewhere in the
     cascade turns links amber, which on a pill reads as a warning. */
  color: var(--color-text);
  text-decoration: none;
}

/* ⭐ The composer is ONE field in three segments (round 14, the user):
   where the message goes, the words, the send button. Rounded only at
   its outer corners, in the move bar's and the estimate row's radius
   (the pill was "too rounded"), the segments parted by hairlines. The
   field clips its segments, so the focus ring is the field's own. */
.ogs-chat-form { display: flex; flex-wrap: wrap; margin-top: 10px; }
.ogs-chat-input {
  display: flex;
  align-items: stretch;
  flex: 1 1 100%;
  min-width: 0;
  min-height: 40px;
  border: 1px solid color-mix(in srgb, var(--color-text) 14%, transparent);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  overflow: hidden;
}
.ogs-chat-input:focus-within {
  border-color: color-mix(in srgb, var(--color-text) 38%, transparent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-text) 7%, transparent);
}
:is(#ogs-session, #ogs-spectate) .ogs-chat-input input,
:is(#ogs-session, #ogs-spectate) .ogs-chat-input input:is(:focus, :focus-visible) {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  padding: 0 12px;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  outline: none;
  color: var(--color-text);
  font-size: 15px;
  text-overflow: ellipsis;
}
.ogs-chat-input input::placeholder { color: var(--color-text-secondary); opacity: 0.85; }
/* Where it goes: the room's name, and before it the bar its lines are
   marked with, so the button and the log say it the same way. One
   press, the next room. It never shrinks: a name cut short is a room
   you cannot see. */
.ogs-chat-channel {
  appearance: none;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  margin: 0;
  padding: 0 12px;
  border: 0;
  border-right: 1px solid var(--color-divider);
  border-radius: 0;
  background: color-mix(in srgb, var(--color-text) 4%, transparent);
  color: var(--color-text);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--dur-short) var(--ease-out);
}
.ogs-chat-channel:hover { background: var(--color-surface-overlay); }
.ogs-chat-channel:focus-visible { outline: 2px solid var(--ogs-focus); outline-offset: -2px; }
.ogs-chat-channel[data-channel]:not([data-channel="main"])::before {
  content: "";
  flex: none;
  width: 3px;
  height: 14px;
  margin-right: 7px;
  border-radius: 2px;
  background: var(--ogs-chan);
}
/* Send: the last segment, ink while there is something to send. */
.ogs-chat-input .ogs-chat-send {
  appearance: none;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  margin: 0;
  padding: 0;
  border: 0;
  border-left: 1px solid var(--color-divider);
  border-radius: 0;
  cursor: pointer;
}
.ogs-chat-input .ogs-chat-send:disabled { cursor: default; }
.ogs-chat-input .ogs-chat-send:focus-visible { outline: 2px solid var(--ogs-focus); outline-offset: -2px; }
/* Who will see it (Note, Private), under the field, whole, with the bar
   its lines are marked with. */
.ogs-chat-hint {
  display: flex;
  align-items: center;
  flex: 1 1 100%;
  margin: 6px 0 0;
  font-size: 12.5px;
  line-height: 1.35;
  color: var(--color-text-secondary);
}
.ogs-chat-hint::before {
  content: "";
  flex: none;
  width: 3px;
  height: 12px;
  margin-right: 7px;
  border-radius: 2px;
  background: var(--ogs-chan);
}
.ogs-chat-hint[data-channel="malkovich"] { --ogs-chan: var(--color-tier-deep); }
.ogs-chat-hint[data-channel="personal"] { --ogs-chan: var(--color-wood); }
/* A segment answers the press in place: shrinking would open a gap in
   the field's edge. */
.ogs-chat-input :is(.ogs-chat-channel, .ogs-chat-send):active:not(:disabled) {
  transform: none;
  filter: brightness(0.94);
}

/* ── Toast ────────────────────────────────────────────────────── */

.ogs-toast {
  position: absolute;
  left: 50%;
  bottom: 16px;
  transform: translateX(-50%);
  padding: 9px 16px;
  border-radius: 999px;
  background: var(--color-text);
  color: var(--color-surface-card);
  font-size: 13px;
  box-shadow: 0 6px 18px rgba(23, 10, 2, 0.24);
  pointer-events: none;
  z-index: 5;
  max-width: 90%;
  text-align: center;
}

/* ── Loading + fallback ───────────────────────────────────────── */

.ogs-session-fallback {
  max-width: 560px;
  margin: 0 auto;
  padding: var(--space-6) var(--space-4);
  text-align: center;
}

/* The board while the page opens (play/ogs/_opening.html, and
   OGSPlay.OpeningBoard once the app is up): the reader's board wood as
   a square filling the board's area, so the board arrives where it
   already is. Sized here alone: the server's copy comes before any
   script could measure. Below the shell the board is as wide as its
   column; in the shell it is as tall as the frame (see below). */
.ogs-opening-board {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  aspect-ratio: 1 / 1;
}
.ogs-opening-label {
  max-width: 80%;
  padding: 10px 16px;
  border-radius: 10px;
  background: color-mix(in srgb, var(--color-surface-card) 88%, transparent);
  color: var(--color-text-secondary);
  font-size: 14px;
  line-height: 1.4;
  text-align: center;
  animation: ogs-breathe 2.4s var(--ease-out) infinite;
}
.ogs-opening-label p { margin: 0; }
.ogs-opening-label noscript p { margin: 8px 0; }
@keyframes ogs-breathe {
  0%, 100% { opacity: 0.7; }
  50%      { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .ogs-opening-label { animation: none; }
}
.ogs-session-fallback h1 { font-size: 20px; margin: 0 0 0.5rem; }
.ogs-session-fallback p { color: var(--color-text-secondary); font-size: 14px; }
.ogs-session-fallback-actions {
  display: flex;
  gap: 10px;
  justify-content: center;
  margin-top: var(--space-4);
}

/* ── Modal ────────────────────────────────────────────────────── */

.ogs-modal-scrim {
  position: fixed;
  inset: 0;
  background: rgba(23, 10, 2, 0.42);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  z-index: 60;
}
.ogs-modal {
  background: var(--color-surface-card);
  border-radius: 14px;
  padding: var(--space-4);
  max-width: 380px;
  width: 100%;
  box-shadow: 0 18px 48px rgba(23, 10, 2, 0.3);
}
.ogs-modal h2 {
  margin: 0 0 0.5rem;
  font-size: 17px;
  color: var(--color-text);
  text-transform: none;
  letter-spacing: 0;
}
.ogs-modal p {
  margin: 0 0 var(--space-4);
  font-size: 14px;
  color: var(--color-text-secondary);
}
.ogs-modal-actions { display: flex; gap: 8px; justify-content: flex-end; }

/* ── Hub ──────────────────────────────────────────────────────── */

/* No max-width of its own: base.html already wraps content in
 * .container (1200px). Setting a narrower one here made the hub sit
 * visibly inset from the header and footer, which read as unfinished
 * rather than as a deliberate measure. */
.ogs-hub {
  padding: var(--space-5) 0 var(--space-6);
}
/* ── The head row (play/ogs/_head.html) ───────────────────────────
   Every OGS page opens with it: the way back or the page's title at the
   left edge, your OGS account at the right, the two centred on each
   other (mobile review P1-3: four pages had four head rows, every one
   of them floating somewhere on a phone). The right-hand group wraps
   whole and keeps to the right edge. The row is as tall as the pill, so
   a visitor's row, which has none, stands as tall as everyone else's. */
.ogs-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-4);
  min-height: var(--ogs-pill-h);
  margin-bottom: var(--space-5);
}
.ogs-head-title,
.ogs-page-title {
  min-width: 0;
  margin: 0;
  font-size: 26px;
  line-height: 1.2;
  text-wrap: balance;
}
/* A page with a parent and a title: the title under the row. */
.ogs-page-title { margin-bottom: var(--space-5); }
.ogs-head-end {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-left: auto;
}
/* The one paragraph some heads carry (the lobby for a visitor with no
   account, the watch list's invitation to one), under the row. */
.ogs-hub-lede {
  max-width: 56ch;
  margin: calc(-1 * var(--space-2)) 0 var(--space-5);
  color: var(--color-text-secondary);
  font-size: 15px;
}
/* Under the lobby's two doors for a visitor who is signed out: both
   start with signing in. */
.ogs-hub-fine {
  margin: var(--space-4) 0 0;
  color: var(--color-text-tertiary);
  font-size: 13px;
}
/* An admin's preview of the empty states (?preview=, _head.html): a
   quiet bar above the head row, dashed so it never reads as part of the
   page a visitor would see. */
.ogs-preview-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  margin: 0 0 var(--space-4);
  padding: 8px 14px;
  border: 1px dashed color-mix(in srgb, var(--color-wood) 55%, transparent);
  border-radius: 10px;
  background: color-mix(in srgb, var(--color-wood) 8%, transparent);
  font-size: 13px;
  color: var(--color-text-secondary);
}
.ogs-preview-bar-label {
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-size: 11px;
}
.ogs-preview-bar strong { color: var(--color-text); font-weight: 600; }
/* A game screen draws the row from its template (P.ScreenHead) as a
   region of the layout, whose gap spaces it. */
.ogs-screen-head .ogs-head { margin-bottom: 0; }

/* The way in sits under the chat form, quiet: it is an offer, not a
   control competing with Send. */
.ogs-compose-open { align-self: flex-start; margin-top: var(--space-2); }

/* Writing a line ("Show a line"): a card in the command rail with what
   the line is, the review screen's tool palette (styled by
   sgf-viewer.css, .gd-tools*), and Undo / Share / Cancel. The board
   itself only says "this is not the game" (.is-variation). */
.ogs-compose-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.ogs-compose-what {
  margin: 0;
  font-size: 13px;
  line-height: 1.45;
  color: var(--color-text-secondary);
}
.ogs-compose-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}
.ogs-compose-actions .ogs-btn { margin: 0; }
.ogs-compose-actions .ogs-btn-primary { flex: 1 1 auto; }

/* Stepping through the game: the review screen's own move buttons
   (KifuGoban.NavSegment, styled by sgf-viewer.css), in the command rail
   under Pass and the rest. Here the segment spans the rail and its
   buttons share the width, as on the review screen's phone layout: in a
   column 300-odd pixels wide, a small pill floating in the middle looked
   like an afterthought. */
/* ⚠️ Every rule carries both strip classes: the review's phone rules are
   scoped `.sgf-controls-strip .sgf-nav-seg .sgf-nav-btn` and won at
   ≤1279px (38px minimum buttons pushed a three-digit counter's row out of
   a 340px rail, and the strip's row-break pseudo-element added a blank
   second row under it). */
.sgf-controls-strip.ogs-nav-strip {
  padding: var(--space-2) 0 0;
  max-width: none;
}
.ogs-command .sgf-controls-strip.ogs-nav-strip { padding-top: 0; }
.sgf-controls-strip.ogs-nav-strip::before { content: none; }
.sgf-controls-strip.ogs-nav-strip .sgf-nav-seg {
  width: 100%;
  height: 36px;
}
.sgf-controls-strip.ogs-nav-strip .sgf-nav-seg .sgf-nav-btn { flex: 1 1 0; min-width: 0; padding: 0; }
.sgf-controls-strip.ogs-nav-strip .sgf-nav-seg .sgf-nav-counter { flex: 0 0 auto; }

/* The pen needs the pointer to mean "draw", and it needs the browser
   to stop trying to select the board while you do. */
.ogs-board-canvas.is-drawing { cursor: crosshair; }
.ogs-board-canvas.is-drawing,
.ogs-board-canvas.is-drawing * {
  user-select: none;
  -webkit-user-select: none;
  /* ⚠️ Touch only: without this a stroke scrolls the page instead of
     drawing, and on a phone that is the whole feature gone. */
  touch-action: none;
}

/* Rematch. The waiting row reads as a status, not a control, so the
   withdraw sits quietly beside it rather than competing. */
.ogs-rematch-waiting {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-top: var(--space-2);
}
.ogs-rematch-waiting-text {
  font-size: 13px;
  color: var(--color-text-secondary);
}
.ogs-rematch-error {
  margin: var(--space-2) 0 0;
  font-size: 12px;
  color: var(--color-text-secondary);
}

/* Filters sit above the list, quiet enough that the games stay the
   subject of the page. */
.ogs-watch-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.ogs-watch-filter {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--color-text-secondary);
}
/* "Board size" broke across two lines and shoved the select down half a
   row. A label beside a control never wraps: this one is two words in
   English and longer in most locales ("Brettgröße", "Dimensione del
   goban"), so the English fitting proves nothing. */
.ogs-watch-filter > span { white-space: nowrap; }
.ogs-watch-filter-check { cursor: pointer; }
.ogs-watch-filter-check input { cursor: pointer; }

/* A card that leads with a number. Keeps the shorter cards from
 * reading as empty and puts the useful thing first. */
.ogs-card-figure { display: flex; align-items: baseline; gap: 6px; }
.ogs-card-figure-value {
  font-size: 30px;
  font-weight: 600;
  line-height: 1;
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
}
.ogs-card-figure-unit { font-size: 14px; color: var(--color-text-tertiary); }

.ogs-hub-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--space-4);
  /* The two doors are one pair: the same height side by side (the user,
   * round 16: one card was visibly bigger than the other), with each
   * button pinned to the foot so both sit on one line. The shorter
   * card's slack goes above its button, where a pair reads it as
   * alignment, not as a void under the text. */
  align-items: stretch;
}
.ogs-hub-grid > .ogs-card .ogs-card-actions { margin-top: auto; }
.ogs-card {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: var(--ogs-pad);
  border-radius: var(--ogs-radius);
  border: 1px solid var(--color-divider);
  background: var(--color-surface-card);
}
.ogs-card h2 { margin: 0; font-size: 16px; color: var(--color-text); }
.ogs-card p {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--color-text-secondary);
}
.ogs-card-actions { display: flex; gap: 8px; flex-wrap: wrap; }

/* The connect/create card carries the whole "not linked" state, so it
 * gets the wood tint that marks a primary action elsewhere in the app. */
.ogs-card-feature {
  background: color-mix(in srgb, var(--color-wood) 7%, var(--color-surface-card));
  border-color: color-mix(in srgb, var(--color-wood) 30%, var(--color-divider));
}

/* Empty states. Deliberately not apologetic: they say what this is
 * for and give one obvious way in. */
.ogs-empty {
  padding: var(--space-5) var(--space-4);
  text-align: center;
  border-radius: 14px;
  border: 1px dashed var(--color-divider);
  background: var(--color-surface-warm);
}
.ogs-empty h2 { margin: 0 0 0.4rem; font-size: 16px; text-transform: none; letter-spacing: 0; color: var(--color-text); }
.ogs-empty p {
  margin: 0 auto var(--space-4);
  max-width: 44ch;
  font-size: 13px;
  color: var(--color-text-secondary);
}
/* The empty pool points at the way in: left, to Quick match and Create a
 * game, or up once the lobby is one column (.ogs-lobby-grid, 900px). */
.ogs-empty-pointer {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.ogs-empty-pool .ogs-empty-pointer { margin-bottom: 0; }
.ogs-pointer-arrow { flex: none; color: var(--color-wood); }
@media (max-width: 900px) {
  .ogs-pointer-arrow { transform: rotate(90deg); }
}

/* Matchmaking form bits */
.ogs-field-row { display: flex; gap: 10px; }
.ogs-field-row label {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}
/* A labelled dropdown (ogs_hub.js `field`): the small label above, the
   app's own dropdown (.kf-select) under it, full width. */
.ogs-field {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}
.ogs-field-label {
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}
.ogs-check {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--color-text-secondary);
  cursor: pointer;
}
/* accent-color, not a hand-built control: it keeps the native
 * semantics and focus behaviour and only changes the fill. Without it
 * the checkbox renders system blue, the one off-palette colour on the
 * page. */
.ogs-check input[type="checkbox"] {
  accent-color: var(--color-wood-dark);
  width: 15px;
  height: 15px;
  cursor: pointer;
}
.ogs-searching {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--color-text-secondary);
}
.ogs-searching-dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--color-wood);
  animation: ogs-pulse 1.4s var(--ease-out) infinite;
}
@media (prefers-reduced-motion: reduce) {
  .ogs-searching-dot { animation: none; }
}
/* Pushed to the end so the digits sit in one place while they tick, and
   tabular figures so the line doesn't twitch on every change of width. */
.ogs-searching-elapsed {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-tertiary, var(--color-text-secondary));
}

/* The two cards that only ever appear when a game has gone wrong. Given
   the same weight as the scoring card rather than a notice's: each one
   carries a decision, not an FYI. */
.ogs-antigrief p { margin: 0 0 var(--space-2); font-size: 13px; }
.ogs-antigrief-detail {
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}
.ogs-antigrief-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.ogs-hub-section { margin-top: var(--space-5); }
.ogs-section-title {
  margin: 0 0 var(--space-3);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}
.ogs-muted { color: var(--color-text-tertiary); font-size: 13px; }

/* Your games, and what is waiting on an answer: a card of rows (see
   "Cards of rows" with the lobby below), not a box per row. */
.ogs-games { display: flex; flex-direction: column; }
.ogs-game-row,
a.ogs-game-row:link,
a.ogs-game-row:visited {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px var(--ogs-pad);
  border-bottom: 1px solid var(--color-divider);
  text-decoration: none;
  color: var(--color-text);
  transition: background var(--dur-short) var(--ease-out);
}
a.ogs-game-row:hover {
  background: color-mix(in srgb, var(--color-text) 4%, transparent);
  color: var(--color-text);
  text-decoration: none;
}
.ogs-game-opponent { flex: 1 1 auto; min-width: 0; font-weight: 600; font-size: 14px; }
.ogs-game-meta {
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--color-text-tertiary);
  font-variant-numeric: tabular-nums;
}
.ogs-game-turn {
  flex: 0 0 auto;
  font-size: 11px;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--color-wood);
  color: var(--color-on-wood);
  font-weight: 600;
}

/* Win / loss. Deliberately NOT green/red — this codebase doesn't use
 * traffic-light colour for state. Weight and ink carry the result;
 * the word itself is the signal. */
.ogs-result {
  flex: 0 0 auto;
  min-width: 4.5em;
  text-align: right;
  font-size: 12px;
  font-weight: 600;
}
.ogs-result.is-win { color: var(--color-text); }
.ogs-result.is-loss { color: var(--color-text-tertiary); font-weight: 400; }
.ogs-result.is-unfinished { color: var(--color-wood); font-weight: 400; }

.ogs-games-footer {
  display: flex;
  justify-content: center;
  margin-top: var(--space-3);
}

/* ── The open challenge pool ──────────────────────────────────────
 *
 * A list whose rows arrive and vanish on their own, so everything here
 * is about staying readable while it moves: the section title and its
 * two tabs share a baseline, the row keeps its three parts in fixed
 * places, and a row that has gone stays put and fades rather than
 * pulling the ones below it up under the cursor. */

.ogs-section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}
.ogs-section-head .ogs-section-title { margin-bottom: 0; }
.ogs-open-challenges > .ogs-muted { margin: 0 0 var(--space-3); }

/* ⚠️ Deliberately NOT a red warning. Three ordinary settings trip this
 * — a custom komi, a set handicap and a fast clock — and every one of
 * them is also how a normal teaching game is posted. Ink and a dotted
 * underline say "read this"; a red badge would say "this person is
 * cheating", which we do not know. See the no-traffic-light rule. */
.ogs-open-warning {
  flex: 0 0 auto;
  font-size: 11px;
  font-weight: 600;
  color: var(--color-text-secondary);
  border-bottom: 1px dotted var(--color-text-tertiary);
  cursor: help;
}

/* Custom-challenge form. Sits inside an .ogs-card and replaces its
 * body, so it inherits the card's padding and needs only its own
 * internal rhythm. */
.ogs-custom-form { display: flex; flex-direction: column; gap: 12px; }
.ogs-field-full {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}
.ogs-field-full input {
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid var(--color-divider);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 13px;
  /* The label is uppercase small-caps; the value the user types is not. */
  text-transform: none;
  letter-spacing: normal;
}
.ogs-field-full input:focus-visible {
  outline: 2px solid var(--ogs-focus);
  outline-offset: 1px;
}
/* Form-level failure. Terracotta, not signal red — same rule as
 * everywhere else in this palette. */
.ogs-form-error {
  margin: 0;
  font-size: 12px;
  color: var(--color-mq-mistake);
}

/* ── Watch: the live wall ─────────────────────────────────────────
   Round 7, from the user: "a basic boring list … make the view
   impressive". The strongest game as the top board, then a wall of
   cards, each a board that plays on as you watch (ogs_watch.js draws
   them on a canvas over the reader's own board wood). */

.ogs-watch-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}
.ogs-watch-toolbar .ogs-watch-filters { margin-bottom: 0; }
.ogs-watch-count {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}

/* The site's one "right now" colour, breathing. */
.ogs-live-dot {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-live);
  animation: ogs-live-breathe 2.4s var(--ease-out) infinite;
}
@keyframes ogs-live-breathe {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-live) 50%, transparent); }
  70%  { box-shadow: 0 0 0 7px color-mix(in srgb, var(--color-live) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-live) 0%, transparent); }
}

/* A card: the board, then who is playing, then the game.
 * ⚠️ Layout on the bare class only: on `a.ogs-watch-card:link` it would
 * outrank any variant class (round 7 had a top-board variant come out as
 * a narrow column that way). The link states only pin the colour:
 * base.css turns bare anchors amber on hover, and a card is a surface,
 * not a link in prose. */
.ogs-watch-card {
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
  padding: 12px;
  border-radius: 16px;
  border: 1px solid var(--color-divider);
  background: var(--color-surface-card);
  transition: border-color var(--dur-short) var(--ease-out),
              box-shadow var(--dur-short) var(--ease-out),
              transform var(--dur-short) var(--ease-out);
}
a.ogs-watch-card:link,
a.ogs-watch-card:visited {
  color: var(--color-text);
  text-decoration: none;
}
a.ogs-watch-card:hover {
  color: var(--color-text);
  border-color: var(--color-wood);
  box-shadow: 0 10px 24px -16px rgba(0, 0, 0, 0.45);
  transform: translateY(-2px);
}
a.ogs-watch-card:active { transform: translateY(0) scale(0.99); }
/* A move lands on its own stone (drawn on the board's canvas, ogs_watch.js
 * drawFlare); the card itself stays still. */
.ogs-watch-card.is-over { opacity: 0.7; }

.ogs-watch-board-wrap { position: relative; min-width: 0; }
/* A plain square of the board's wood. Shudan's own .shudan-goban adds a
 * border and padding for its vertex grid, which pushed the canvas out of
 * square; this outranks it (same weight, later). */
.shudan-goban.ogs-watch-board {
  display: block;
  box-sizing: border-box;
  width: 100%;
  aspect-ratio: 1 / 1;
  padding: 0;
  border: 0;
  border-radius: 6px;
  overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
}
/* `color` is the grid's ink: the board theme's own line colour. */
.ogs-watch-canvas {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  color: var(--shudan-board-foreground-color, #3E2723);
}
.ogs-watch-status {
  position: absolute;
  top: 8px;
  left: 8px;
  padding: 2px 9px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--color-surface-card) 90%, transparent);
  color: var(--color-text);
  font-size: 11px;
  font-weight: 600;
}

.ogs-watch-info { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.ogs-watch-players { display: flex; flex-direction: column; gap: 2px; }
.ogs-watch-player {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
  margin: 0 -6px;
  padding: 3px 6px;
  border-radius: 8px;
  font-size: 14px;
}
/* Whose move it is: the wood wash the game screen's seats use. */
.ogs-watch-player.is-to-move {
  background: color-mix(in srgb, var(--color-wood) 16%, transparent);
}
.ogs-watch-player .ogs-player-stone { width: 14px; height: 14px; }
.ogs-watch-name {
  min-width: 0;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ogs-watch-rank {
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}
.ogs-watch-clock { flex: 0 0 auto; margin-left: auto; }
.ogs-watch-clock .ogs-clock-main,
.ogs-watch-clock .ogs-clock-period { font-size: 13px; }
.ogs-watch-clock .ogs-clock-reserve,
.ogs-watch-clock .ogs-clock-periods { font-size: 11px; }
.ogs-watch-clock .ogs-clock:not(.is-active) .ogs-clock-main {
  font-weight: 500;
  color: var(--color-text-secondary);
}

/* One line, always: what does not fit wraps onto a second row that is
 * clipped away, so a fact drops out whole instead of the card growing a
 * line (round 8). The items are one height, so the row is exact. */
.ogs-watch-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 6px;
  height: 19px;
  overflow: hidden;
  font-size: 12px;
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}
.ogs-watch-move,
.ogs-watch-tag { box-sizing: border-box; height: 19px; line-height: 17px; white-space: nowrap; }
.ogs-watch-move { margin-right: 2px; font-weight: 600; color: var(--color-text); }
.ogs-watch-tag {
  padding: 0 8px;
  border-radius: 999px;
  border: 1px solid var(--color-divider);
  font-size: 11px;
}

/* The wall. */
.ogs-watch-wall {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--space-4);
}
/* A card narrow enough to cut the names short gives up the clock's
   reserve ("+ 5×30s") first, by its own width: three cards a row at 768
   cut "Penguin54324" to five letters (mobile review P3-5). */
.ogs-watch-wall .ogs-watch-card { container: watchcard / inline-size; }
@container watchcard (max-width: 250px) {
  .ogs-clock-reserve { display: none; }
}

/* A phone: two boards a row, the cards pared to the board and players. */
@media (max-width: 560px) {
  .ogs-watch-wall { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .ogs-watch-wall .ogs-watch-card { gap: 8px; padding: 8px; border-radius: 12px; }
  .ogs-watch-wall .ogs-watch-player { gap: 5px; font-size: 12px; }
  .ogs-watch-wall .ogs-watch-rank { font-size: 11px; }
  .ogs-watch-wall .ogs-watch-clock .ogs-clock-main,
  .ogs-watch-wall .ogs-watch-clock .ogs-clock-period { font-size: 12px; }
  .ogs-watch-wall .ogs-clock-reserve,
  .ogs-watch-wall .ogs-watch-tag { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .ogs-live-dot { animation: none; }
  a.ogs-watch-card:hover, a.ogs-watch-card:active { transform: none; }
}

.ogs-watch-more {
  display: flex;
  justify-content: center;
  margin-top: var(--space-5);
}

/* ── Player avatar: face, stone, presence ─────────────────────── */

/* Three things in one 36px square: who they are, which colour they
 * hold, and whether they're around. The stone sits bottom-left and the
 * presence dot top-right, so neither ever covers a face — the same
 * arrangement OGS uses, for the same reason.
 *
 * The footprint is fixed. With no avatar the stone grows to fill it,
 * which keeps every card the same shape whether the image is there,
 * still loading, or permanently missing. A layout that reflows when a
 * third-party image lands is worse than one that never had the image. */
.ogs-avatar {
  position: relative;
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  border-radius: 10px;
  background: color-mix(in srgb, var(--color-wood) 12%, var(--color-surface));
  overflow: visible;
}
.ogs-avatar-img {
  width: 100%;
  height: 100%;
  border-radius: inherit;
  object-fit: cover;
  display: block;
}

.ogs-avatar-stone {
  position: absolute;
  left: -2px;
  bottom: -2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  /* Reads against a photograph of anything. */
  box-shadow: 0 0 0 2px var(--color-surface-card);
}

/* No avatar: the stone stops being a badge and becomes the whole
 * thing, centred in the footprint the image would have taken. */
.ogs-avatar.is-bare {
  background: transparent;
}
.ogs-avatar.is-bare .ogs-avatar-stone {
  left: 50%;
  bottom: 50%;
  width: 26px;
  height: 26px;
  transform: translate(-50%, 50%);
  box-shadow: none;
}

/* Presence. Live green for here (the site's one "right now" colour,
 * tokens.css --color-live; the user asked for live dots to be green),
 * hollow for away. Absent entirely until OGS has actually told us,
 * because a dot that defaults to one state is a claim we haven't
 * earned. */
.ogs-avatar-presence {
  position: absolute;
  right: -2px;
  top: -2px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  box-shadow: 0 0 0 2px var(--color-surface-card);
  background: var(--color-text-tertiary);
}
.ogs-avatar.is-online .ogs-avatar-presence {
  background: var(--color-live);
}
.ogs-avatar.is-offline .ogs-avatar-presence {
  background: var(--color-surface);
  box-shadow: 0 0 0 2px var(--color-surface-card),
              inset 0 0 0 1.5px var(--color-text-tertiary);
}

/* ── Matchmaking pickers ──────────────────────────────────────────
   Board size and clock are toggle buttons rather than selects,
   because in OGS's "multiple" mode the same controls become a
   multi-selection and a control must not change shape between modes.
   `is-on` is the shared pressed state; it uses the same
   theme-invariant amber pair as the primary button, for the same
   reason (see .ogs-btn-primary:hover above). */
.ogs-picker { display: flex; flex-direction: column; gap: 6px; }
.ogs-picker-label {
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}
.ogs-picker-select { align-self: flex-start; min-width: 11rem; }
.ogs-picker-select .kf-select-button { width: 100%; justify-content: space-between; }
.ogs-mode-note { margin: 0; font-size: 12px; line-height: 1.4; }

.ogs-toggle-row { display: flex; flex-wrap: wrap; gap: 6px; }
.ogs-toggle,
.ogs-clock-btn {
  padding: 7px 12px;
  border-radius: 8px;
  border: 1px solid var(--color-divider);
  background: var(--color-surface-card);
  color: var(--color-text);
  font-size: 13px;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition:
    border-color var(--dur-short) var(--ease-out),
    background var(--dur-short) var(--ease-out);
}
.ogs-toggle:hover,
.ogs-clock-btn:hover { border-color: var(--color-wood); }
.ogs-toggle.is-on,
.ogs-clock-btn.is-on {
  background: var(--color-wood);
  border-color: var(--color-wood);
  color: var(--color-wood-dark);
  font-weight: 600;
}
.ogs-toggle:disabled,
.ogs-clock-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.ogs-toggle:disabled:hover,
.ogs-clock-btn:disabled:hover { border-color: var(--color-divider); }
.ogs-toggle.is-on:disabled:hover,
.ogs-clock-btn.is-on:disabled:hover { border-color: var(--color-wood); }

/* One row per speed: what it costs you in wall-clock time, then the
   two clocks OGS will actually give you. */
.ogs-speed-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.ogs-speed-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 4px;
}
.ogs-speed-name {
  font-size: 12px;
  font-weight: 600;
  color: var(--color-text-secondary);
}
.ogs-speed-est { font-size: 11px; color: var(--color-text-tertiary); }
.ogs-speed-clocks { display: flex; align-items: center; gap: 6px; }
.ogs-speed-or {
  font-size: 11px;
  color: var(--color-text-tertiary);
  /* Reserves its width whether or not the word is there, so the
     buttons do not jump sideways when the mode changes. */
  min-width: 1.4rem;
  text-align: center;
}
.ogs-speed-clocks .ogs-clock-btn { flex: 1 1 0; }

/* ── The custom-game clock editor ──────────────────────────────── */
.ogs-tc-editor { display: flex; flex-direction: column; gap: 10px; }
.ogs-tc-fields { flex-wrap: wrap; }
.ogs-tc-count {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}
.ogs-tc-count input {
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid var(--color-divider);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  /* The label above is uppercased; the value must not be. */
  text-transform: none;
  letter-spacing: normal;
  width: 100%;
}
.ogs-tc-count input:disabled { opacity: 0.5; }
.ogs-tc-summary {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin: 0;
  font-size: 13px;
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
}
.ogs-tc-summary-label {
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}

/* Narrow screens: the three clock fields stack rather than shrinking
   into unreadable slivers. */
@media (max-width: 520px) {
  .ogs-tc-fields { flex-direction: column; }
  .ogs-speed-clocks { flex-wrap: wrap; }
}

/* ── The rest of the custom-game form ──────────────────────────── */

/* The footnote under "No analysis during the game". It has to say that
 * the switch also turns off conditional moves, because a player who
 * finds that out later has no way to connect the two. */
.ogs-check-note {
  margin: -6px 0 0 23px;
  font-size: 11px;
  line-height: 1.4;
  color: var(--color-text-tertiary);
}

/* An option a rule forbids is shown, greyed, rather than removed: a
 * control that vanishes teaches nobody why. */
.ogs-field-row select option:disabled { color: var(--color-text-tertiary); }

/* Narrow screens: the paired selects stack instead of shrinking. */
@media (max-width: 520px) {
  .ogs-custom-form .ogs-field-row { flex-direction: column; }
}

/* ── Invitation links ──────────────────────────────────────────── */

.ogs-invite-link {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
/* The fallback when the clipboard refuses us. A readonly field rather
 * than plain text: it can be selected with one click and, unlike a
 * <p>, it does not wrap a long URL into an unreadable block. */
.ogs-invite-url {
  flex: 1 1 220px;
  min-width: 0;
  padding: 6px 8px;
  border-radius: 8px;
  border: 1px solid var(--color-divider);
  background: var(--color-surface);
  color: var(--color-text-secondary);
  font-size: 12px;
}
.ogs-invite-row { flex-wrap: wrap; gap: 8px; }

/* The landing page is one card and nothing else: whoever followed the
 * link came for one decision. */
.ogs-invite-page { max-width: 620px; }
.ogs-invite-settings {
  margin: 0 0 var(--space-3);
  font-size: 13px;
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}

/* ── Who else is in the room ───────────────────────────────────── */

.ogs-chat-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.ogs-chat-head h2 { margin: 0; }
.ogs-watchers { position: relative; }
.ogs-watchers-toggle {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px;
  border-radius: 999px;
  border: 1px solid var(--color-divider);
  background: transparent;
  color: var(--color-text-secondary);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
/* The theme-invariant pair, same reason as the primary button: this
 * chip sits on the card in both themes and an accent that only reads
 * in one of them disappears in the other. */
.ogs-watchers-toggle.is-on {
  background: var(--color-wood);
  border-color: var(--color-wood-dark);
  color: var(--color-wood-dark);
}
.ogs-watchers-icon { font-size: 11px; line-height: 1; }
.ogs-watchers-list {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  z-index: 5;
  min-width: 190px;
  max-height: 260px;
  overflow-y: auto;
  padding: 6px;
  border-radius: 10px;
  border: 1px solid var(--color-divider);
  background: var(--color-surface);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
}
.ogs-watchers-sort {
  display: block;
  width: 100%;
  padding: 4px 6px;
  margin-bottom: 4px;
  border: 0;
  border-bottom: 1px solid var(--color-divider);
  background: transparent;
  color: var(--color-text-tertiary);
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  text-align: left;
  cursor: pointer;
}
.ogs-watchers-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  padding: 3px 6px;
  font-size: 13px;
  color: var(--color-text);
}
.ogs-watchers-rank {
  font-size: 11px;
  color: var(--color-text-tertiary);
  font-variant-numeric: tabular-nums;
}
.ogs-watchers-empty {
  margin: 0;
  padding: 4px 6px;
  font-size: 12px;
  color: var(--color-text-tertiary);
}

/* ⚠️ From here up the review viewer is the old two-column page: the
   board spans both rows, the head row (P.ScreenHead, first in the DOM so
   that it heads a phone's single column) takes the top of the right-hand
   column and the rail the rest. The playing and watching boards place
   their own regions by area (`.ogs-game`, which outranks this). */
@media (min-width: 1025px) {
  .ogs-session-page { grid-template-rows: auto minmax(0, 1fr); }
  .ogs-board-stage { grid-column: 1; grid-row: 1 / span 2; }
  .ogs-session-page > .ogs-screen-head { grid-column: 2; grid-row: 1; }
}

/* ═══════════════════════════════════════════════════════════════════
   DESKTOP APP SHELL  (≥1280px; tablet and phone untouched)

   Operator, 2026-08-24: "board should be full screen as much as
   possible. and the rule we use elsewhere on the site: board never
   moves and is not scrollable. left/right rails can scroll
   independently."

   That rule already exists — `body.gd-review` in game-detail.css — so
   this is the same shell, not a second one. A live game wants it more
   than a review does: the board must never be pushed or resized by
   async chrome, because on this surface the chrome arrives DURING the
   game. A resync notice, an undo request, a pause card and a variation
   bar all appear mid-move, and every one of them used to shove the
   board down the page while somebody was reaching for a point.

   The frame is 100dvh under the sticky header and never scrolls. The
   two side tracks are the SAME width, so the board sits dead centre
   (the same choice the review shell makes, and what lets a stream crop
   to just the board). The right track holds the rail and scrolls on its
   own; the left is the matching spacer that centres the board.

   ⚠️ Keep the metrics below in sync with `liveBoardMetrics()` in
   web/static/js/ogs_play.js. They are mirrored, not shared: the CSS
   needs them at parse time and the sizer needs them before first paint,
   so there is nowhere they could both read from.
   ═══════════════════════════════════════════════════════════════════ */
@media (min-width: 1280px) {
  body.ogs-shell { overflow: hidden; }
  body.ogs-shell > main.container {
    max-width: none;
    margin: 0;
    padding: 0;
  }
  /* The footer is the main thing that forced a scroll below the fold,
     and a live game is a workspace rather than a page to read to the
     end of. Same call as the review shell. */
  body.ogs-shell .site-footer { display: none; }

  /* The frame. Both live surfaces mount into one of these two roots. */
  body.ogs-shell #ogs-session,
  body.ogs-shell #ogs-spectate {
    height: calc(100dvh - var(--header-h, 64px));
    overflow: hidden;
  }

  body.ogs-shell .ogs-session-page {
    /* Side tracks. Tuned like the review shell's: wide enough for the
       rail to stay usable and to scale on big monitors, narrow enough
       that the board stays height-bound rather than width-bound (which
       would leave it small with tall gaps above and below). */
    --ogs-side: clamp(280px, 21vw, 400px);
    --ogs-shell-gap: clamp(16px, 1.4vw, 32px);
    --ogs-shell-pad: clamp(10px, 0.9vw, 20px);
    --ogs-shell-pad-y: clamp(4px, 0.4vw, 10px);

    height: 100%;
    max-width: none;
    margin: 0;
    padding: var(--ogs-shell-pad-y) var(--ogs-shell-pad);
    /* [ spacer | board | rail ]. The two side tracks are the SAME width
       so the board sits dead centre. The middle is `minmax(0, 1fr)` and
       NOT `auto`: an auto column would hug its widest child, which is
       sometimes a wrapped compose bar rather than the goban — and worse,
       it would make the column's width depend on the board, which is
       exactly the feedback loop the sizer is written to avoid (see
       `liveBoardVertexSize`). A viewport-derived column keeps the
       board's box independent of the board. */
    grid-template-columns: var(--ogs-side) minmax(0, 1fr) var(--ogs-side);
    gap: var(--ogs-shell-gap);
    align-items: stretch;
    overflow: hidden;
    box-sizing: border-box;
  }

  /* ── The board column ──────────────────────────────────────────
     Explicitly placed rather than auto-flowed: the stage is the FIRST
     child in the DOM (it is what the page is about, and on a phone it
     must come before the rail), so left to itself it would land in the
     spacer track. */
  body.ogs-shell .ogs-board-stage {
    grid-column: 2;
    grid-row: 1;
    min-width: 0;
    min-height: 0;
    overflow: hidden;
    /* ⚠️ Room for the browsing OUTLINE, which is painted 6px outside the
       canvas (2px dashed at a 4px offset) and would otherwise be shaved
       off by the `overflow: hidden` above. That outline is the whole
       "this is not the game" signal — the one whose false positive on
       every move the operator caught two commits ago — and clipping it
       breaks it silently, in the one layout where the canvas fills its
       column exactly. Measured: zero slack before this. */
    padding: 8px;
  }
  /* ⭐ This is what makes the board sizeable without a feedback loop.
     `flex: 1 1 auto` + `min-height: 0` gives the canvas exactly the
     height the bars below it did not take — a box handed down by the
     frame, with no dependence on the goban inside it. The sizer reads
     that box and fits the board to it. Take `min-height: 0` away and the
     canvas grows to its content instead, the measured height becomes the
     board's own height, and the board can never change size again. */
  body.ogs-shell .ogs-board-canvas {
    flex: 1 1 auto;
    min-height: 0;
    /* Top-aligned, level with the top of the rail. */
    align-items: flex-start;
    overflow: hidden;
  }

  /* The chat log is the one card that can grow without limit, so give it
     a floor and let it take the space the rail has left rather than
     collapsing to nothing behind a game-over card. */
  body.ogs-shell .ogs-chat { min-height: 220px; }

}

/* ═══════════════════════════════════════════════════════════════════
   THE GAME LAYOUT  (`.ogs-session-page.ogs-game`, the playing board)

   Operator, 2026-10-04: "controls have no logic, just piled up at
   random places … think about what user wants most often and make it
   smooth and intuitive. rest should also be accessible easily when
   needed."

   What a player wants, in order of how often: the board; whose move it
   is and how much time is left; pass / undo / resign; the chat. Then,
   rarely, everything else. So: five regions, placed by grid AREAS so
   that the DOM order is the phone order and each wider band only
   rearranges them, under the head row every OGS page has.

     head       the way back · your OGS account (play/ogs/_head.html)
     seat-opp   opponent + clock, across the board from you
     board
     seat-me    you + your clock, on your side of the board
     command    whose turn · Pass / Undo / Resign · More (everything else)
     talk       game information + chat

   On a phone the seats sit either side of the board, as at a real one.
   Beside the board (a rail) they sit together at the top, one under the
   other: the two names and clocks are read together, and with the board
   between them they were a screen apart (user's walk, 2026-10-05).

   ⚠️ Specificity: every rule here carries `.ogs-game`, so it outranks
   the older `.ogs-session-page` placement rules above, which the
   spectator board still uses.
   ═══════════════════════════════════════════════════════════════════ */

.ogs-session-page.ogs-game {
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "head" "opp" "board" "me" "command" "talk";
  gap: var(--space-2);
  max-width: 760px;
}
.ogs-game > .ogs-screen-head { grid-area: head; }
.ogs-game > .ogs-seat-opp { grid-area: opp; }
/* As wide as the board (--ogs-board-w, P.markBoardWidth): a board the
   screen's height keeps narrower than the column (a portrait tablet)
   has the cards over and under it at its own width, not 26px wider
   each side. */
.ogs-game > .ogs-screen-head,
.ogs-game > .ogs-seat,
.ogs-game > .ogs-command,
.ogs-game > .ogs-talk {
  width: 100%;
  max-width: var(--ogs-board-w, none);
  justify-self: center;
}
.ogs-game > .ogs-board-stage { grid-area: board; }
.ogs-game > .ogs-seat-me { grid-area: me; }
.ogs-game > .ogs-command { grid-area: command; }
.ogs-game > .ogs-talk { grid-area: talk; }

.ogs-game > .ogs-command,
.ogs-game > .ogs-talk {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}

/* ── Seats ─────────────────────────────────────────────────────── */

.ogs-seat { min-width: 0; }
.ogs-seat .ogs-player-card {
  background: var(--color-surface-card);
  border: 1px solid var(--color-divider);
  border-radius: 14px;
  padding: 10px 12px;
  gap: 12px;
  transition:
    border-color var(--dur-short) var(--ease-out),
    box-shadow var(--dur-short) var(--ease-out);
}
/* Whose clock is running: the card outlined in wood, and its clock at
   full ink while the other one waits in a quieter grey. The user asked
   for the outline alone (2026-10-07: the wood bar on the leading edge
   went, and the clock's wood ground "looks like selected text"). */
.ogs-seat .ogs-player-card.is-active {
  border-color: var(--color-wood);
  box-shadow: 0 0 0 1px var(--color-wood);
}
.ogs-seat .ogs-clock:not(.is-active) .ogs-clock-main,
.ogs-seat .ogs-clock:not(.is-active):not(.is-overtime) .ogs-clock-period {
  color: var(--color-text-secondary);
}
.ogs-seat .ogs-clock-main { font-size: 22px; line-height: 1.15; }
.ogs-seat .ogs-clock-period { font-size: 20px; line-height: 1.15; }
.ogs-seat .ogs-player-name { font-size: 15px; }

/* ── Command card ──────────────────────────────────────────────── */

.ogs-game .ogs-controls { gap: var(--space-3); }
.ogs-game .ogs-turn-line {
  font-size: 15px;
  font-weight: 600;
  color: var(--color-text);
}
.ogs-game .ogs-turn-text { flex: 1 1 auto; min-width: 0; }
.ogs-game .ogs-turn-dot { width: 9px; height: 9px; }

.ogs-icon-btn {
  appearance: none;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 50%;
  background: none;
  color: var(--color-text-secondary);
  cursor: pointer;
  transition: background var(--dur-short) var(--ease-out);
}
.ogs-icon-btn:hover,
.ogs-icon-btn.is-open {
  background: var(--color-surface-overlay);
  color: var(--color-text);
}
.ogs-icon-btn:focus-visible {
  outline: 2px solid var(--ogs-focus);
  outline-offset: 1px;
}
.ogs-icon-btn:disabled { opacity: 0.35; cursor: default; background: none; }
.ogs-icon { display: block; flex: 0 0 auto; }

/* The three moves of every game, as equal tiles. Icon over label, so
   each one is a big target and none of them is a word in a sentence. */
.ogs-action-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-2);
}
.ogs-action {
  appearance: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 64px;
  padding: 10px 6px;
  /* Outlines on the card, not darker boxes inside it (design round 2:
     "dark on dark on dark" was the lobby's problem and this was the
     same pattern). */
  border: 1px solid color-mix(in srgb, var(--color-text) 14%, transparent);
  border-radius: 12px;
  background: transparent;
  color: var(--color-text);
  font: inherit;
  font-size: 13px;
  line-height: 1.2;
  text-align: center;
  cursor: pointer;
  transition:
    border-color var(--dur-short) var(--ease-out),
    background var(--dur-short) var(--ease-out);
}
.ogs-action:hover { border-color: var(--color-wood); }
.ogs-action:focus-visible {
  outline: 2px solid var(--ogs-focus);
  outline-offset: 1px;
}
.ogs-action:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  border-color: var(--color-divider);
}
/* Resign is the one with no way back: terracotta on hover only, so it
   does not shout from the card for the whole game. */
.ogs-action.is-danger:hover {
  border-color: var(--color-mq-mistake);
  color: var(--color-mq-mistake);
}

.ogs-btn-lg { padding: 14px 18px; font-size: 14px; }

.ogs-pause-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 10px 12px;
  border-radius: 10px;
  background: var(--color-surface-warm);
}
.ogs-pause-row .ogs-pause-note {
  flex: 1 1 auto;
  margin: 0;
  font-size: 13px;
  color: var(--color-text-secondary);
}
.ogs-pause-row .ogs-btn { flex: 0 0 auto; white-space: nowrap; }

/* The estimate and Play variation (round 14), side by side under the
   moves, half the row each, or the whole row alone. Each a control that
   keeps its place whether it asks, is on ("B+12.5 ×", pressed) or says
   why it can't, in the move bar's chrome above it (36px, the same
   corners and edge), its icon on the cards' text edge: as a pill of its
   own width the estimate floated under the bar, 4px in from everything
   else (mobile review P2-3). */
.ogs-board-tools { display: flex; gap: 8px; }
.ogs-board-tools:empty { display: none; }
.ogs-board-tools > * { flex: 1 1 0; min-width: 0; }
.ogs-board-tools .ogs-btn span { min-width: 0; line-height: 1.25; }
.ogs-estimate-ask,
.ogs-variation-ask { display: flex; }
:is(.ogs-estimate-ask, .ogs-variation-ask) .ogs-btn {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 8px;
  width: 100%;
  min-height: 36px;
  margin: 0;
  padding: 0 12px;
  border: 1px solid var(--color-divider);
  border-radius: var(--radius-sm);
  background: var(--color-surface-card);
  text-align: left;
}
/* Hover and on: the overlay ground, as the move bar's buttons. */
:is(.ogs-estimate-ask, .ogs-variation-ask) .ogs-btn:hover:not(:disabled) { background: var(--color-surface-overlay); }
:is(.ogs-estimate-ask, .ogs-variation-ask) .ogs-icon { flex: none; width: 16px; height: 16px; max-width: none; }
:is(.ogs-estimate-ask, .ogs-variation-ask) .ogs-btn.is-on {
  background: var(--color-surface-overlay);
  color: var(--color-text);
}
.ogs-estimate-ask .ogs-btn.is-on strong {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* The × says pressing it again takes it away. */
:is(.ogs-estimate-ask, .ogs-variation-ask) .ogs-btn.is-on .ogs-icon:last-child {
  width: 14px;
  height: 14px;
  margin-left: 2px;
  opacity: 0.7;
}
/* Can't be asked for: the reason in the pill, quietly. */
.ogs-estimate-ask .ogs-btn:disabled {
  opacity: 1;
  color: var(--color-text-tertiary);
  cursor: default;
}

/* ── Talk ──────────────────────────────────────────────────────── */

/* The composer is its own band under the log, so what has been said and
   what you are about to say never run together. */
.ogs-game .ogs-chat-form {
  margin-top: var(--space-2);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-divider);
}
.ogs-chat-send {
  background: var(--color-text);
  color: var(--color-surface-card);
}
.ogs-chat-send:hover { background: var(--color-wood); color: var(--color-on-wood); }
.ogs-chat-send:disabled,
.ogs-chat-send:disabled:hover {
  background: var(--color-surface-overlay);
  color: var(--color-text-tertiary);
  opacity: 1;
}
.ogs-chat-send .ogs-icon { width: 18px; height: 18px; }
.ogs-chat-tools {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  margin-top: 4px;
}
.ogs-chat-tools:empty { display: none; }
.ogs-chat-tools .ogs-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding-left: 8px;
  padding-right: 10px;
  margin-top: 0;
  background: transparent;
}
.ogs-chat-tools .ogs-icon { width: 16px; height: 16px; max-width: none; }

/* ── The game menu ─────────────────────────────────────────────── */

.ogs-menu-scrim {
  position: fixed;
  inset: 0;
  z-index: 70;
  background: transparent;
}
.ogs-menu-scrim.is-sheet {
  background: rgba(23, 10, 2, 0.38);
  display: flex;
  align-items: flex-end;
}
.ogs-menu {
  position: fixed;
  box-sizing: border-box;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 6px;
  background: var(--color-surface-card);
  border: 1px solid var(--color-divider);
  border-radius: 14px;
  box-shadow: 0 14px 44px rgba(23, 10, 2, 0.22);
  color: var(--color-text);
  animation: ogs-menu-in 140ms var(--ease-out, ease-out);
}
.ogs-menu.is-sheet {
  position: relative;
  width: 100%;
  max-height: 82vh;
  border-radius: 18px 18px 0 0;
  border-bottom: 0;
  padding: 8px 8px calc(env(safe-area-inset-bottom, 0px) + 14px);
  animation-name: ogs-sheet-in;
}
/* The grab bar: says "this slides" without a word. */
.ogs-menu.is-sheet::before {
  content: "";
  display: block;
  width: 36px;
  height: 4px;
  margin: 2px auto 8px;
  border-radius: 2px;
  background: var(--color-surface-overlay);
}
@keyframes ogs-menu-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}
@keyframes ogs-sheet-in {
  from { transform: translateY(24px); opacity: 0.6; }
  to   { transform: none; opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .ogs-menu, .ogs-menu.is-sheet { animation: none; }
}

.ogs-menu-section + .ogs-menu-section {
  border-top: 1px solid var(--color-divider);
  margin-top: 6px;
  padding-top: 6px;
}
.ogs-menu-title {
  padding: 6px 10px 4px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}
.ogs-menu-item,
a.ogs-menu-item:link,
a.ogs-menu-item:visited {
  appearance: none;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  width: 100%;
  min-height: 42px;
  padding: 9px 10px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--color-text);
  font: inherit;
  font-size: 14px;
  line-height: 1.3;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
button.ogs-menu-item:hover,
a.ogs-menu-item:hover { background: var(--color-surface-overlay); color: var(--color-text); }
.ogs-menu-item:focus-visible {
  outline: 2px solid var(--ogs-focus);
  outline-offset: -2px;
}
.ogs-menu-item:disabled { cursor: default; }
.ogs-menu-item:disabled .ogs-menu-label { color: var(--color-text-tertiary); }
button.ogs-menu-item:disabled:hover { background: none; }
.ogs-menu-label {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.ogs-menu-hint {
  font-size: 12px;
  line-height: 1.35;
  color: var(--color-text-tertiary);
}
/* Touch placement in the game menu (OGSDock TouchRow): the label, the
   choice across the menu's width, what the chosen one does. */
.ogs-menu-touch {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 9px 10px;
  font-size: 14px;
}

.ogs-switch {
  position: relative;
  flex: 0 0 auto;
  width: 34px;
  height: 20px;
  border-radius: 999px;
  background: var(--color-surface-overlay);
  transition: background var(--dur-short) var(--ease-out);
}
.ogs-switch::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--color-surface-card);
  box-shadow: 0 1px 2px rgba(23, 10, 2, 0.25);
  transition: transform var(--dur-short) var(--ease-out);
}
.ogs-switch.is-on { background: var(--color-wood); }
.ogs-switch.is-on::after { transform: translateX(14px); }

.ogs-modal.ogs-modal-report { max-width: 440px; }

/* ── Phone ─────────────────────────────────────────────────────── */

@media (max-width: 720px) {
  .ogs-seat .ogs-player-card { padding: 7px 12px; gap: 10px; border-radius: 12px; }
  .ogs-seat .ogs-avatar { width: 40px; height: 40px; border-radius: 10px; }
  .ogs-seat .ogs-player-body { gap: 0; }
  .ogs-seat .ogs-clock-main { font-size: 19px; }
  .ogs-seat .ogs-clock-period { font-size: 17px; }
}

/* ── Landscape and small laptops: board left, one rail right ──────
   From 1025px, and on any screen wider than tall from 640px: in one
   column a landscape tablet (1024×768) had a board taller than the
   screen and its own card and controls below the fold, and a phone on
   its side had room for the board or the cards, never both (mobile
   review P1-2). Beside the board the side column takes 340px, the board
   the rest, and the board is sized to the screen's height too
   (boardBudget in ogs_play.js). */

@media (min-width: 1025px), (min-width: 640px) and (orientation: landscape) {
  /* The board's track is the board's width (--ogs-board-w) once it is
     drawn, and the two columns centre together: a board the screen's
     height keeps narrower than its column sat off-centre in it, the gap
     to the side column twice the page's margin. The board is sized from
     the page, not this track (besideColumnWidth), so it can still grow. */
  .ogs-session-page.ogs-game {
    max-width: 1440px;
    grid-template-columns: var(--ogs-board-w, minmax(0, 1fr)) 340px;
    justify-content: center;
    grid-template-rows: auto auto auto auto minmax(0, 1fr);
    grid-template-areas:
      "board head"
      "board opp"
      "board me"
      "board command"
      "board talk";
    column-gap: var(--space-5);
    row-gap: var(--space-2);
  }
  .ogs-game > .ogs-board-stage { grid-area: board; }
  .ogs-game > .ogs-screen-head,
  .ogs-game > .ogs-seat,
  .ogs-game > .ogs-command,
  .ogs-game > .ogs-talk { max-width: none; justify-self: stretch; }
  /* The board is sized to the screen's height here, so the page's 24px
     under the site header is board taken away; the game page's own top
     padding is the gap. A phone on its side has 390px in all, and the
     board hit its smallest size with the header gap in (the app shell
     zeroes it too). */
  body.ogs-shell > main.container { padding-top: 0; }

  /* The two seats, stacked in the rail, are one block: the row gap
     taken back (and the shared border laid over itself), the inner
     corners square. The running seat's outline sits on top, so its
     whole edge shows on the shared side too. */
  .ogs-game > .ogs-seat-me { margin-top: calc(-1 * var(--space-2) - 1px); }
  .ogs-game > .ogs-seat-opp .ogs-player-card {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
  }
  .ogs-game > .ogs-seat-me .ogs-player-card {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
  }
  .ogs-game > .ogs-seat .ogs-player-card.is-active {
    position: relative;
    z-index: 1;
  }
}

/* ── Desktop app shell: [ talk | board | seats + command ] ─────── */

@media (min-width: 1280px) {
  /* ⭐ The board's column is the board's width, and the three columns
     sit together in the middle: on a big screen (or zoomed out) the
     chat and the seats stay beside the board, a gap's width away,
     instead of out at the screen's edges (user, 2026-10-07). The board
     is NOT sized from this column, which would lock it (see
     `boardBudget` in ogs_play.js): its width budget is the page less
     the side tracks, and its height is the frame's. */
  body.ogs-shell .ogs-session-page.ogs-game {
    grid-template-columns: var(--ogs-side) auto var(--ogs-side);
    justify-content: center;
    grid-template-rows: auto auto auto minmax(0, 1fr);
    grid-template-areas:
      "talk board head"
      "talk board opp"
      "talk board me"
      "talk board command";
    column-gap: var(--ogs-shell-gap);
    row-gap: var(--space-2);
  }
  body.ogs-shell .ogs-game > .ogs-board-stage { grid-area: board; }
  /* A bar under the board takes the board's width and never sets it:
     zero width for sizing the column, the full column once it is
     sized. Otherwise "Showing a shared line…" would push the side
     panels apart, and the board's size would depend on a bar's text. */
  body.ogs-shell .ogs-game .ogs-variation-bar {
    box-sizing: border-box;
    width: 0;
    min-width: 100%;
    /* …and one line, whatever that width: a bar that wrapped more as
       the board narrowed would take height the board then lost. */
    flex-wrap: nowrap;
  }
  body.ogs-shell .ogs-game .ogs-variation-bar > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  body.ogs-shell .ogs-game .ogs-variation-bar .ogs-btn { flex: 0 0 auto; white-space: nowrap; }
  /* While the page opens: a square as tall as the frame, never wider
     than the board's column could be (the page less the two side
     tracks, their gaps, the page's padding and the stage's 8px).
     ⚠️ From the viewport, not `height: 100%`: the column hugs its
     content and is sized before a percentage height can resolve, so a
     percentage gave a tall sliver as wide as the label. These are the
     frame's own numbers (the #ogs-session rule and the shell block). */
  body.ogs-shell .ogs-game .ogs-opening-board {
    --ogs-opening-side: min(
      calc(100dvh - var(--header-h, 64px) - 2 * var(--ogs-shell-pad-y) - 16px),
      calc(100vw - 2 * var(--ogs-side) - 2 * var(--ogs-shell-gap)
           - 2 * var(--ogs-shell-pad) - 16px));
    width: var(--ogs-opening-side);
    height: var(--ogs-opening-side);
  }
  /* The stage keeps 8px of room for the browsing outline; the head row
     matches it so the side column starts level with the board. */
  body.ogs-shell .ogs-game > .ogs-screen-head { padding-top: 8px; }
  /* The middle of the right rail and the whole left one scroll inside
     their own tracks; nothing they do can reach the board. */
  body.ogs-shell .ogs-game > .ogs-command,
  body.ogs-shell .ogs-game > .ogs-talk {
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
  }
  body.ogs-shell .ogs-game > .ogs-command::-webkit-scrollbar,
  body.ogs-shell .ogs-game > .ogs-talk::-webkit-scrollbar { display: none; }
  body.ogs-shell .ogs-game > .ogs-talk { padding: 8px 0; }
  /* Chat takes all the height the left track has; its log scrolls. */
  body.ogs-shell .ogs-game .ogs-chat {
    flex: 1 1 auto;
    min-height: 260px;
  }
  body.ogs-shell .ogs-game .ogs-chat-scroll {
    flex: 1 1 auto;
    max-height: none;
    min-height: 0;
  }

  body.ogs-shell .ogs-seat .ogs-player-card { padding: 12px 14px; gap: 14px; }
  body.ogs-shell .ogs-seat .ogs-avatar { width: 56px; height: 56px; border-radius: 14px; }
  body.ogs-shell .ogs-seat .ogs-avatar-stone { width: 20px; height: 20px; }
  body.ogs-shell .ogs-seat .ogs-player-name { font-size: 17px; }
  body.ogs-shell .ogs-seat .ogs-player-rank { font-size: 13px; }
  body.ogs-shell .ogs-seat .ogs-player-meta { font-size: 13px; }
  body.ogs-shell .ogs-seat .ogs-clock-main { font-size: 26px; }
  body.ogs-shell .ogs-seat .ogs-clock-period { font-size: 22px; }
  body.ogs-shell .ogs-seat .ogs-clock-periods,
  body.ogs-shell .ogs-seat .ogs-clock-sep { font-size: 13px; }
  body.ogs-shell .ogs-seat .ogs-clock-reserve { font-size: 14px; }
}

/* ═══════════════════════════════════════════════════════════════════
   THE LOBBY  (`.ogs-lobby`, the OGS hub when an account is linked)

   Round 1 (2026-10-04): incoming → your games → [start a game | pool].
   Round 2, same evening, operator: "lobby is too complicated and too
   dark. Open challenges items look like a mess of text and are dark on
   dark on dark. I don't like the tabs style … quick match is somewhat
   simplified but custom game is complicated."

   So: one surface deep. The page, then at most one card (the Quick
   match panel); the open pool is a list ON the page, rows separated by
   hairlines, not boxes inside a box. Choices are the site's pills (the
   bands on /play). Labels use the secondary ink, which clears AA on the
   dark ground where the tertiary did not. The custom game is a dialog.
   ═══════════════════════════════════════════════════════════════════ */

/* ── The head row's links ──────────────────────────────────────── */

a.ogs-hub-watch,
a.ogs-hub-watch:link,
a.ogs-hub-watch:visited {
  font-size: 14px;
  color: var(--color-text-secondary);
  text-decoration: none;
}
a.ogs-hub-watch:hover { color: var(--color-text); text-decoration: none; }
a.ogs-hub-back,
a.ogs-hub-back:link,
a.ogs-hub-back:visited {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  color: var(--color-text-secondary);
  text-decoration: none;
}
/* The arrow is drawn, not a glyph: "\2190" sits on the text baseline and
   each system font puts it somewhere else (low beside the label on a
   phone). A mask centres on the flex line in any font. */
a.ogs-hub-back::before {
  content: "";
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M13.5 8h-11M7 3.5 2.5 8 7 12.5'/></svg>") center / contain no-repeat;
          mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M13.5 8h-11M7 3.5 2.5 8 7 12.5'/></svg>") center / contain no-repeat;
}
a.ogs-hub-back:hover { color: var(--color-text); text-decoration: none; }
/* "OGS" in the lobby's title goes to OGS: the title's own ink, the
   site's underline on hover. */
a.ogs-hub-title-link,
a.ogs-hub-title-link:link,
a.ogs-hub-title-link:visited { color: inherit; text-decoration: none; }
a.ogs-hub-title-link:hover {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: var(--color-wood);
  text-decoration-thickness: 2px;
  text-underline-offset: 4px;
}
/* ── Sections ──────────────────────────────────────────────────── */

.ogs-lobby > .ogs-hub-section:first-child,
.ogs-lobby > .ogs-rail-notice + .ogs-hub-section { margin-top: 0; }
.ogs-lobby > .ogs-rail-notice { margin-bottom: var(--space-4); }
.ogs-lobby-sub { margin-top: var(--space-4); }
.ogs-lobby-sub h3 {
  margin: 0 0 var(--space-2);
  padding-inline: calc(var(--ogs-pad) + 1px);
  font-size: 13px;
  font-weight: 600;
  color: var(--color-text-secondary);
}
/* Every section of the lobby heads a card, so its title starts where
   the card's content does (mobile review P1-4): one card edge and one
   content edge down each column, as an inset grouped list has. */
.ogs-lobby .ogs-section-title {
  margin-bottom: var(--space-2);
  padding-inline: calc(var(--ogs-pad) + 1px);
  color: var(--color-text-secondary);
}
.ogs-lobby .ogs-section-head { margin-bottom: var(--space-2); }
.ogs-lobby .ogs-games + .ogs-games { margin-top: 8px; }

/* Somebody is waiting on an answer: the one place on the page that gets
   the wood ground. */
.ogs-lobby-incoming .ogs-games {
  background: color-mix(in srgb, var(--color-wood) 9%, var(--color-surface-card));
  border-color: color-mix(in srgb, var(--color-wood) 45%, var(--color-divider));
}
.ogs-lobby-incoming .ogs-game-row { border-bottom-color: color-mix(in srgb, var(--color-wood) 30%, var(--color-divider)); }

/* A game in progress. The whole row is the link. */
.ogs-mygame-who {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: 6px;
}
.ogs-mygame-who .ogs-game-opponent { flex: 0 1 auto; }
.ogs-mygame-go {
  flex: 0 0 auto;
  font-size: 20px;
  line-height: 1;
  color: var(--color-text-secondary);
  transition: transform var(--dur-short) var(--ease-out);
}
a.ogs-mygame:hover .ogs-mygame-go { transform: translateX(2px); color: var(--color-text); }
.ogs-lobby .ogs-game-meta { color: var(--color-text-secondary); }

/* A challenge this page is holding open: what it is waiting for, what
   was sent, and the two things you can do about it. */
.ogs-waiting-row { flex-wrap: wrap; row-gap: 8px; }
.ogs-waiting-row .ogs-searching-dot { flex: 0 0 auto; }
.ogs-waiting-text {
  flex: 1 1 260px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.ogs-waiting-text strong { font-size: 14px; font-weight: 600; color: var(--color-text); }
.ogs-waiting-text .ogs-game-meta { white-space: normal; line-height: 1.4; }
.ogs-waiting-actions {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
}
.ogs-waiting-actions .ogs-invite-link { margin: 0; }

/* ── Grid: Quick match beside the open pool ────────────────────── */

/* The DOM is a phone's order (Play, the pool, Online now, the live
   games); beside each other, Play and Online now share the left column
   and the pool runs down the right. The second row is `1fr` so that it,
   not the first, takes whatever height the pool has beyond Play's: Play
   and Online now stay a gap apart however long the pool gets. */
.ogs-lobby-grid {
  display: grid;
  grid-template-columns: minmax(320px, 380px) minmax(0, 1fr);
  grid-template-rows: auto 1fr;
  gap: var(--space-6);
  align-items: start;
  margin-top: var(--space-6);
}
.ogs-lobby > .ogs-lobby-grid:first-child,
.ogs-lobby > .ogs-muted + .ogs-lobby-grid { margin-top: 0; }
.ogs-lobby > .ogs-muted { margin: 0 0 var(--space-4); }
.ogs-lobby-grid > .ogs-hub-section { margin-top: 0; }
.ogs-lobby-grid > .ogs-start-block { grid-column: 1; grid-row: 1; }
.ogs-lobby-grid > .ogs-open-challenges { grid-column: 2; grid-row: 1 / span 2; }
.ogs-lobby-grid > .ogs-online { grid-column: 1; grid-row: 2; }
@media (max-width: 900px) {
  .ogs-lobby-grid { grid-template-columns: minmax(0, 1fr); }
  .ogs-lobby-grid > .ogs-start-block,
  .ogs-lobby-grid > .ogs-open-challenges,
  .ogs-lobby-grid > .ogs-online { grid-column: auto; grid-row: auto; }
}

/* ── Cards of rows (mobile review P1-4) ────────────────────────────
   The open pool, Online now and your games: the Play card's chrome,
   their rows edge to edge inside it with hairlines between, each row's
   text on the card's content edge (--ogs-pad) and its hover filling the
   card's width. No overflow clip, because the pool's dropdowns open out
   of the card, so the end rows round their own corners for the hover. */
.ogs-pool-card,
.ogs-online-list,
.ogs-games {
  border: 1px solid var(--color-divider);
  border-radius: var(--ogs-radius);
  background: var(--color-surface-card);
}
.ogs-open-list > :last-child,
.ogs-online-list > li:last-child,
.ogs-games > :last-child { border-bottom: 0; }
.ogs-open-list > :last-child,
.ogs-online-list > li:last-child > .ogs-online-row,
.ogs-games > :last-child,
.ogs-pool-card > .ogs-empty {
  border-bottom-left-radius: calc(var(--ogs-radius) - 1px);
  border-bottom-right-radius: calc(var(--ogs-radius) - 1px);
}
.ogs-online-list > li:first-child > .ogs-online-row,
.ogs-games > :first-child {
  border-top-left-radius: calc(var(--ogs-radius) - 1px);
  border-top-right-radius: calc(var(--ogs-radius) - 1px);
}

/* The live games, as the lobby's last row on a phone (LiveGamesRow):
   the head row there has room for the title and your account only.
   Wider, "Browse live games" stays in the head row and this is not
   drawn. */
.ogs-live-row { display: none; }
@media (max-width: 640px) {
  .ogs-head .ogs-hub-watch { display: none; }
  a.ogs-live-row,
  a.ogs-live-row:link,
  a.ogs-live-row:visited {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 48px;
    padding: 0 var(--ogs-pad);
    border: 1px solid var(--color-divider);
    border-radius: var(--ogs-radius);
    background: var(--color-surface-card);
    color: var(--color-text);
    font-size: 15px;
    font-weight: 600;
    text-decoration: none;
  }
  a.ogs-live-row:hover { color: var(--color-text); text-decoration: none; }
}
.ogs-live-row-dot {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-live);
}
.ogs-live-row-go {
  flex: none;
  margin-left: auto;
  color: var(--color-text-secondary);
  transition: transform var(--dur-short) var(--ease-out);
}
a.ogs-live-row:hover .ogs-live-row-go { transform: translateX(2px); color: var(--color-text); }
a.ogs-live-row:focus-visible { outline: 2px solid var(--ogs-focus); outline-offset: 2px; }

/* ── A segmented choice (P.Segmented) ────────────────────────────
 * The site's segmented group (the review editor's .gd-seg, the board
 * menu's .kf-seg): ONE outer border and radius, straight seams between
 * the segments, the chosen one filled with wood (round 9, the user:
 * "straight separators"). The native input stays, hidden, so keys and
 * screen readers get a real radio. No weight change on the chosen one:
 * a segment that grows when picked moves its neighbours. */
.ogs-seg {
  display: inline-flex;
  align-items: stretch;
  max-width: 100%;
  overflow: hidden;
  border: 1px solid var(--color-divider);
  border-radius: var(--radius-sm, 6px);
  background: var(--color-surface-card);
}
.ogs-seg.is-block { display: flex; width: 100%; box-sizing: border-box; }
.ogs-seg-opt {
  position: relative;
  display: flex;
  flex: 0 1 auto;
  min-width: 0;
  cursor: pointer;
  user-select: none;
}
.ogs-seg-opt + .ogs-seg-opt { border-left: 1px solid var(--color-divider); }
.ogs-seg.is-block .ogs-seg-opt { flex: 1 1 0; }
.ogs-seg-opt input {
  position: absolute;
  width: 0;
  height: 0;
  opacity: 0;
  pointer-events: none;
}
.ogs-seg-text {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  padding: 7px 14px;
  color: var(--color-text-secondary);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.25;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  transition:
    color var(--dur-short) var(--ease-out),
    background var(--dur-short) var(--ease-out),
    box-shadow var(--dur-short) var(--ease-out);
}
.ogs-seg-opt:hover .ogs-seg-text {
  color: var(--color-text);
  background: var(--color-surface-overlay);
}
/* `--color-on-wood` is a fixed dark ink, right on the wood in both
 * themes (same pair as /play's bands). */
.ogs-seg-opt.is-on .ogs-seg-text {
  background: var(--color-wood);
  color: var(--color-on-wood);
}
.ogs-seg-opt input:focus-visible + .ogs-seg-text {
  outline: 2px solid var(--ogs-focus);
  outline-offset: -2px;
}
.ogs-seg-opt.is-disabled { cursor: not-allowed; }
.ogs-seg-opt.is-disabled .ogs-seg-text { opacity: 0.5; }
.ogs-seg-opt.is-disabled:hover .ogs-seg-text { background: none; color: var(--color-text-secondary); }
.ogs-seg.is-small .ogs-seg-text { padding: 5px 11px; font-size: 13px; }

/* Labels everywhere in the lobby read in the secondary ink. */
.ogs-lobby .ogs-picker-label,
.ogs-lobby .ogs-field-row label,
.ogs-lobby .ogs-field-label,
.ogs-lobby .ogs-tc-summary-label {
  font-size: 11.5px;
  color: var(--color-text-secondary);
}
.ogs-create .ogs-check { color: var(--color-text); font-size: 14px; }
.ogs-create .ogs-check-note { color: var(--color-text-secondary); font-size: 12px; margin: -8px 0 0 23px; }
/* Under the opponent choice: the same note, not indented under a box. */
.ogs-create .ogs-picker > .ogs-check-note { margin: 0; }

/* ── The Quick match panel ─────────────────────────────────────── */

.ogs-findgame {
  gap: var(--space-4);
  /* The tiles below narrow by the card's width, not the screen's: the
     card is as narrow beside the pool on a landscape tablet as it is on
     a phone (mobile review P2-1). */
  container: quick / inline-size;
}
/* Quick match | Custom game: the site's tab bar (.kf-tabs in
   components.css, the review's Comments | Stats), half the card each
   (round 10; round 9's underline sat under the text and curved up at
   the ends like a bracket). Only the size and the idle ink are the
   card's own: these head the card, so they read at a heading's size,
   and the idle one takes the secondary ink like every label here (the
   tertiary fails AA on the dark card; round 2). */
.ogs-findgame-tabs { margin-top: -4px; }
.ogs-findgame-tabs .kf-tab {
  padding: 4px 8px 10px;
  font-size: 15px;
  letter-spacing: -0.005em;
  color: var(--color-text-secondary);
}
.ogs-findgame-tabs .kf-tab:hover,
.ogs-findgame-tabs .kf-tab.is-on { color: var(--color-text); }
.ogs-findgame-tabs .kf-tab:disabled:hover { color: var(--color-text-secondary); }
.ogs-quick { display: flex; flex-direction: column; gap: var(--space-4); }
.ogs-picker { gap: 8px; }

/* The speed, as three tiles that print the clock you will get and how
   long the game usually takes. Those two numbers are the decision.
   Outlines on the card, not filled boxes on it. */
.ogs-speed-tiles {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
}
.ogs-speed-tile {
  appearance: none;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  padding: 11px 11px 10px;
  border: 1px solid color-mix(in srgb, var(--color-text) 14%, transparent);
  border-radius: 12px;
  background: transparent;
  color: var(--color-text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--dur-short) var(--ease-out),
              background var(--dur-short) var(--ease-out);
}
.ogs-speed-tile:hover { border-color: var(--color-wood); }
.ogs-speed-tile.is-on {
  border-color: var(--color-wood);
  background: color-mix(in srgb, var(--color-wood) 13%, transparent);
  box-shadow: inset 0 0 0 1px var(--color-wood);
}
.ogs-speed-tile:focus-visible { outline: 2px solid var(--ogs-focus); outline-offset: 1px; }
.ogs-speed-tile-name { font-size: 15px; font-weight: 600; }
/* Allowed to wrap at its spaces: German's "20Min + 5×30s" is wider
   than a third of the panel. */
.ogs-speed-tile-clock {
  font-size: 13px;
  line-height: 1.3;
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
}
.ogs-speed-tile-est { font-size: 12px; line-height: 1.3; color: var(--color-text-secondary); }
.ogs-clock-main-part { white-space: nowrap; }

/* "More options", folded to one line that says what it is set to. */
.ogs-more {
  border-top: 1px solid var(--color-divider);
  border-bottom: 1px solid var(--color-divider);
}
.ogs-more-toggle {
  appearance: none;
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  /* Wider than the card's column by its own padding each side, pulled
     back by the margin: the text stays on the content edge with the
     controls above it, and the hover highlight has room around the text
     (round 11; it sat flush). */
  width: calc(100% + 20px);
  margin: 0 -10px;
  padding: 12px 10px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--color-text);
  font: inherit;
  font-size: 14px;
  text-align: left;
  cursor: pointer;
  transition: background var(--dur-short) var(--ease-out);
}
.ogs-more-toggle:hover { background: color-mix(in srgb, var(--color-text) 5%, transparent); }
.ogs-more-toggle:focus-visible { outline: 2px solid var(--ogs-focus); outline-offset: 0; }
.ogs-more-label { flex: 0 0 auto; font-weight: 600; }
.ogs-more-summary {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
  color: var(--color-text-secondary);
}
.ogs-more-chevron {
  margin-left: auto;
  color: var(--color-text-secondary);
}
.ogs-more.is-open .ogs-more-chevron { transform: rotate(180deg); }
.ogs-more-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: 4px 2px 16px;
}

.ogs-btn-block { width: 100%; padding-top: 14px; padding-bottom: 14px; font-size: 15px; }

/* A live search: the whole pane says so, with the one way out. */
.ogs-search-live {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas: "pulse text time" "stop stop stop";
  align-items: center;
  gap: var(--space-3) var(--space-3);
  padding: var(--space-3) 0 0;
}
.ogs-search-pulse { grid-area: pulse; position: relative; width: 28px; height: 28px; }
.ogs-search-pulse span {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--color-wood);
  animation: ogs-ripple 1.8s var(--ease-out) infinite;
}
.ogs-search-pulse span + span { animation-delay: 0.9s; }
@keyframes ogs-ripple {
  0%   { transform: scale(0.35); opacity: 0.9; }
  100% { transform: scale(1);    opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .ogs-search-pulse span { animation: none; transform: scale(0.5); opacity: 0.8; }
}
.ogs-search-text {
  grid-area: text;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.ogs-search-text strong { font-size: 15px; color: var(--color-text); }
.ogs-search-text span { font-size: 13px; color: var(--color-text-secondary); }
.ogs-search-elapsed {
  grid-area: time;
  font-size: 18px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--color-text);
}
.ogs-search-live > .ogs-btn { grid-area: stop; }

.ogs-text-input {
  box-sizing: border-box;
  width: 100%;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid color-mix(in srgb, var(--color-text) 16%, transparent);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  font-size: 15px;
}
.ogs-text-input::placeholder { color: var(--color-text-secondary); opacity: 0.85; }
.ogs-text-input:focus-visible { outline: 2px solid var(--ogs-focus); outline-offset: 1px; }

/* A narrow card: tighter tiles, and every tile's clock on two lines, the
   added time under the main time, so the three tiles keep one height and
   their "about N minutes" lines stay level. */
@container quick (max-width: 420px) {
  .ogs-speed-tile { padding: 9px 8px 8px; }
  .ogs-speed-tile-clock { font-size: 12px; }
  .ogs-speed-tile-clock .ogs-clock-more-part { display: block; }
}

/* ── The open pool: a list on the page, not boxes in a box ─────── */

/* The rows switch to their compact layout, and the toolbar folds, by
   the pool's own width: the pool is narrower beside Play on a landscape
   tablet than it is alone on a portrait one (mobile review P1-1, P2-2). */
.ogs-open-challenges { container: pool / inline-size; }
.ogs-pool-card > .ogs-muted {
  margin: 0;
  padding: 14px var(--ogs-pad) 16px;
  border-top: 1px solid var(--color-divider);
  font-size: 13px;
  color: var(--color-text-secondary);
}
/* The pool's toolbar (round 9): its two halves, the size, ranked or not,
 * and on the right the switch, one strip across the top of the card,
 * every control the same 30px high so nothing crowds the list below. */
.ogs-pool-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 12px var(--ogs-pad);
}
.ogs-pool-toolbar .ogs-seg-text { padding-top: 0; padding-bottom: 0; height: 28px; }
.ogs-pool-toolbar .kf-select-button {
  box-sizing: border-box;
  height: 30px;
  padding: 0 10px;
  font-size: 13px;
}
/* ⭐ One row on a desktop (round 14, the user: all filters on one row,
   "must"). The pool is about 706px inside at most (the page stops at
   1200) and the four controls need about 680 in English, so the label
   says "Only games I can accept" (the switch is the "show"). The push
   (.ogs-pool-push, no width of its own) takes the row's spare room, so
   the switch ends at the content edge, over the Accept buttons; where
   the words are longer than the row (French, Spanish), the push stays
   behind and the switch starts the second row at the content edge,
   never stranded at the far end of it. */
.ogs-pool-push { flex: 1 0 0; min-width: 0; }
.ogs-pool-acceptable {
  flex: 0 0 auto;
  gap: 8px;
}
.ogs-pool-acceptable .ogs-switch-text {
  flex: 0 1 auto;
  font-size: 13px;
  color: var(--color-text-secondary);
}
.ogs-pool-acceptable .ogs-switch,
.ogs-watch-filter-check .ogs-switch { width: 30px; height: 18px; }
.ogs-pool-acceptable .ogs-switch::after,
.ogs-watch-filter-check .ogs-switch::after { width: 14px; height: 14px; }
.ogs-pool-acceptable .ogs-switch.is-on::after,
.ogs-watch-filter-check .ogs-switch.is-on::after { transform: translateX(12px); }
/* A phone's width: the two halves across the top, the two dropdowns
 * sharing the line under them, each half of it. */
@container pool (max-width: 560px) {
  .ogs-pool-push { display: none; }
  .ogs-pool-acceptable { flex: 1 1 100%; padding-top: 2px; }
  .ogs-pool-toolbar .ogs-pool-tabs { flex: 1 1 100%; }
  .ogs-pool-toolbar .ogs-pool-tabs .ogs-seg-opt { flex: 1 1 0; }
  .ogs-pool-toolbar .kf-select { flex: 1 1 0; min-width: 0; }
  .ogs-pool-toolbar .kf-select-button { justify-content: center; width: 100%; }
  .ogs-pool-toolbar .kf-select-value {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}
/* What the filters held back, with the one way to see it. */
.ogs-pool-hidden {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: 4px 10px;
  margin: 0;
  font-size: 13px;
  color: var(--color-text-secondary);
}
.ogs-open-foot { padding: 12px var(--ogs-pad); }
.ogs-empty-pool .ogs-pool-hidden { margin-top: var(--space-3); }
/* Empty, in the card: plain words under the toolbar, not a dashed box
   inside a box. */
.ogs-pool-card > .ogs-empty {
  border: 0;
  border-top: 1px solid var(--color-divider);
  border-radius: 0;
  background: none;
}
.ogs-link-btn {
  appearance: none;
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-text);
  font: inherit;
  font-weight: 600;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--color-wood) 70%, transparent);
  text-underline-offset: 3px;
  cursor: pointer;
}
.ogs-link-btn:hover { text-decoration-color: var(--color-wood); }
.ogs-link-btn:focus-visible { outline: 2px solid var(--ogs-focus); outline-offset: 2px; border-radius: 3px; }

/* ⭐ One grid for the whole list (round 14, the user: aligned
   "dynamically, not based on some thresholds"). Every row is a subgrid
   of it and its facts a subgrid of the row, so the board, the clock,
   Ranked and the action are each a column as wide as its widest entry,
   on every row, in every language: no column widths guessed for the
   longest label we ship. The game takes what is left. */
.ogs-open-list {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto auto auto;
  column-gap: var(--space-5);
  border-top: 1px solid var(--color-divider);
}
.ogs-open-row {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  align-items: center;
  row-gap: 4px;
  padding: 12px var(--ogs-pad);
  border-bottom: 1px solid var(--color-divider);
  transition: background var(--dur-short) var(--ease-out);
}
.ogs-open-list > .ogs-open-foot { grid-column: 1 / -1; }
.ogs-open-main { grid-column: 1; }
.ogs-open-actions { grid-column: 5; }
.ogs-open-row:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }
.ogs-open-main {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.ogs-open-who {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.ogs-open-who .ogs-cap-stone { width: 12px; height: 12px; }
.ogs-open-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 15px;
  font-weight: 600;
  color: var(--color-text);
}
.ogs-open-who .ogs-player-rank { font-size: 13px; color: var(--color-text-secondary); }
/* Your own rank, in gold (round 12). A tinted pill rather than gold
   letters, which would not read on the light card; the ink flips bright
   in the dark theme. */
.ogs-open-who .ogs-player-rank.is-your-rank {
  padding: 0 7px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--color-wood) 34%, transparent);
  color: var(--color-wood-ink);
  font-weight: 600;
}
.ogs-open-sub {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 10px;
  font-size: 13px;
  line-height: 1.4;
  color: var(--color-text-secondary);
}
.ogs-open-title { color: var(--color-text); }
/* Fixed columns, so board, clock and ranked line up down the list and
   the eye can run down one of them: scanning for "19×19, about 10
   minutes" is the whole job of this list. */
.ogs-open-facts {
  grid-column: 2 / span 3;
  display: grid;
  grid-template-columns: subgrid;
  align-items: center;
  font-variant-numeric: tabular-nums;
}
.ogs-open-facts .ogs-fact.is-ranked { justify-self: start; }
.ogs-fact {
  font-size: 14px;
  color: var(--color-text-secondary);
  white-space: nowrap;
}
.ogs-fact.is-size { color: var(--color-text); font-weight: 600; }
.ogs-fact.is-clock { color: var(--color-text); }
/* A Canadian clock ("10m + 10m per 20 stones") on a narrow pool: it
 * breaks between its halves (P.clockLines) rather than run under the
 * Ranked chip. */
.ogs-open-facts .ogs-fact.is-clock { white-space: normal; line-height: 1.3; }
.ogs-fact.is-ranked {
  padding: 1px 9px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--color-wood) 60%, transparent);
  color: var(--color-text);
  font-size: 12px;
}
.ogs-open-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 6px;
  min-width: 0;
}
/* The sparks' anchor (AcceptControl): the pill's own box, so the
   stream rises off it and nowhere else. */
.ogs-accept-sparks { position: relative; display: inline-flex; }

/* ── Accept, then Start (AcceptControl) ───────────────────────────
   One pill, two faces. Both faces sit in one grid cell, so the pill is
   the wider face's width in either state and the list never reflows.
   Start keeps 30px clear on its right for the ×, which lies over the
   pill's right end, behind a hairline. */
.ogs-accept {
  position: relative;
  display: inline-flex;
  border-radius: 999px;
}
.ogs-accept-main {
  display: inline-grid;
  place-items: center;
  overflow: hidden;
}
.ogs-accept-face {
  grid-area: 1 / 1;
  white-space: nowrap;
  transition:
    transform var(--dur-short) var(--ease-out),
    opacity var(--dur-instant) var(--ease-out);
}
.ogs-accept-face.is-start {
  padding-right: 30px;
  opacity: 0;
  transform: translateX(45%);
}
.ogs-accept.is-confirming .ogs-accept-face.is-accept { opacity: 0; transform: translateX(-60%); }
.ogs-accept.is-confirming .ogs-accept-face.is-start { opacity: 1; transform: none; }
.ogs-accept-dismiss {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 30px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-left: 1px solid color-mix(in srgb, var(--color-surface-card) 30%, transparent);
  border-radius: 0 999px 999px 0;
  background: transparent;
  color: var(--color-surface-card);
  cursor: pointer;
  overflow: hidden;
  opacity: 0;
  transition:
    opacity var(--dur-instant) var(--ease-out),
    background var(--dur-short) var(--ease-out);
}
/* The × slides in inside its own segment, never past the pill's end. */
.ogs-accept-dismiss svg {
  transform: translateX(14px);
  transition: transform var(--dur-short) var(--ease-out);
}
.ogs-accept-dismiss:disabled { pointer-events: none; }
.ogs-accept.is-confirming .ogs-accept-dismiss { opacity: 1; }
.ogs-accept.is-confirming .ogs-accept-dismiss svg { transform: none; }
.ogs-accept-dismiss:hover { background: color-mix(in srgb, var(--color-surface-card) 16%, transparent); }
/* On Start's amber hover the × takes the hover's ink. */
.ogs-accept-main:hover + .ogs-accept-dismiss {
  color: var(--color-wood-dark);
  border-left-color: color-mix(in srgb, var(--color-wood-dark) 30%, transparent);
}
/* The press moves the whole pill, the × with it, not Start alone. */
.ogs-accept:has(> .ogs-accept-main:active, > .ogs-accept-dismiss:active:not(:disabled)) { transform: translateY(1px) scale(0.98); }
.ogs-accept > .ogs-accept-main:active { transform: none; }
/* Starting: the spinner in the ×'s place, so nothing changes width. */
.ogs-accept-main[aria-busy="true"]::before { content: none; }
.ogs-accept-main[aria-busy="true"]:hover { color: var(--color-wood-dark); }
.ogs-accept.is-busy .ogs-accept-dismiss { opacity: 1; cursor: progress; }
.ogs-accept.is-busy .ogs-accept-dismiss svg { display: none; }
.ogs-accept.is-busy .ogs-accept-dismiss::before {
  content: "";
  width: 11px;
  height: 11px;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-top-color: transparent;
  animation: ogs-spin 0.7s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .ogs-accept-face,
  .ogs-accept-dismiss svg { transition: opacity var(--dur-short) linear; transform: none !important; }
  .ogs-accept:has(> .ogs-accept-main:active, > .ogs-accept-dismiss:active:not(:disabled)) { transform: none; filter: brightness(0.94); }
  .ogs-accept.is-busy .ogs-accept-dismiss::before { animation: none; opacity: 0.6; }
}
.ogs-open-reason {
  font-size: 13px;
  line-height: 1.35;
  color: var(--color-text-secondary);
  text-align: right;
}
/* Not for you: a rank window you are outside. Still readable, plainly
   not the thing to press. Each part dimmed by name: on a narrow pool
   .ogs-open-main has no box of its own (display: contents), and opacity
   on it would dim nothing. */
.ogs-open-row.is-closed :is(.ogs-open-who, .ogs-open-sub, .ogs-open-facts) { opacity: 0.55; }
.ogs-open-row.is-gone { opacity: 0.45; }
.ogs-open-row.is-gone:hover { background: none; }
/* The row waiting on Start keeps its hover tint, so it is plain which
   game is about to begin once the pointer has moved off it. */
.ogs-open-row.is-confirming { background: color-mix(in srgb, var(--color-text) 4%, transparent); }

/* ⭐ A phone's pool (round 14, the user: each row "a mess of unaligned
   text", worst where a reason stood in for Accept). Still one grid: the
   board, the clock and Ranked keep their columns, now on the row's
   second line across the full width, the clock whole; the name and
   Accept share the first line; the notes come last. A row you cannot
   take has no Accept, so its reason is a line of its own under the rest
   at the content edge, instead of a narrow column squeezing the facts. */
@container pool (max-width: 640px) {
  .ogs-open-list {
    grid-template-columns: auto auto minmax(0, 1fr) auto;
    column-gap: var(--space-3);
  }
  .ogs-open-main { display: contents; }
  .ogs-open-who { grid-column: 1 / 4; grid-row: 1; }
  .ogs-open-actions { grid-column: 4; grid-row: 1; }
  .ogs-open-facts { grid-column: 1 / -1; grid-row: 2; }
  .ogs-open-sub { grid-column: 1 / -1; grid-row: 3; }
  .ogs-open-row.is-closed .ogs-open-who { grid-column: 1 / -1; }
  .ogs-open-row.is-closed > .ogs-open-actions { grid-column: 1 / -1; grid-row: 4; }
  .ogs-open-row.is-closed .ogs-open-actions { justify-content: flex-start; }
  .ogs-open-row.is-closed .ogs-open-reason { text-align: left; }
  .ogs-fact { font-size: 13px; }
}

/* ── A player's name as a link (P.PlayerName, round 11) ───────────
 * Reads as the name it was; the underline comes on hover. Colour set
 * on :hover too, or the site's a:hover amber wins. */
a.ogs-name-link,
a.ogs-name-link:link,
a.ogs-name-link:visited {
  color: inherit;
  text-decoration: none;
}
a.ogs-name-link:hover {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--color-wood) 75%, transparent);
  text-decoration-thickness: 1.5px;
  text-underline-offset: 3px;
}
a.ogs-name-link:focus-visible { outline: 2px solid var(--ogs-focus); outline-offset: 2px; border-radius: 3px; }

/* ── Online now (round 10) ─────────────────────────────────────────
 * Under the card: friends, then recent opponents, whom OGS reports
 * online. A list on the page like the open pool (rows and hairlines, not
 * a second card); a row is one press, and the player flies over to
 * Custom game (flyIntoForm in ogs_hub.js). */
.ogs-online-list {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  list-style: none;
}
.ogs-online-list > li { border-bottom: 1px solid var(--color-divider); }
.ogs-online-row {
  appearance: none;
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 9px var(--ogs-pad);
  border: 0;
  border-radius: 0;
  background: none;
  color: var(--color-text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background var(--dur-short) var(--ease-out);
}
.ogs-online-row:hover:not(:disabled) {
  background: color-mix(in srgb, var(--color-text) 4%, transparent);
}
.ogs-online-row:disabled { opacity: 0.5; cursor: not-allowed; }
.ogs-online-row:focus-visible { outline: 2px solid var(--ogs-focus); outline-offset: -2px; }
.ogs-online-who {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}
.ogs-online-face { position: relative; flex: none; display: inline-flex; }
/* Here, in the live green: the only reason anyone is on this list. */
.ogs-online-dot {
  position: absolute;
  right: -1px;
  bottom: -1px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--color-live);
  box-shadow: 0 0 0 2px var(--color-surface-card);
}
.ogs-online-row .ogs-opp-tag { margin-left: 0; }
.ogs-online-go {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 2px;
  margin-left: auto;
  font-size: 13px;
  font-weight: 600;
  color: var(--color-text-secondary);
  transition: color var(--dur-short) var(--ease-out);
}
.ogs-online-go svg { transition: transform var(--dur-short) var(--ease-out); }
.ogs-online-row:hover:not(:disabled) .ogs-online-go { color: var(--color-text); }
.ogs-online-row:hover:not(:disabled) .ogs-online-go svg { transform: translateX(2px); }
/* The last row when more are online than fit: "Show 4 more", quieter
   than the people above it, its chevron turning over once open. */
.ogs-online-toggle {
  gap: 4px;
  font-size: 13px;
  font-weight: 600;
  color: var(--color-text-secondary);
}
.ogs-online-toggle:hover:not(:disabled) { color: var(--color-text); }
.ogs-online-toggle-icon { transition: transform var(--dur-short) var(--ease-out); }
.ogs-online-toggle[aria-expanded="true"] .ogs-online-toggle-icon { transform: rotate(180deg); }

/* The copy that flies: the row's face and name, lifted off the page as
 * a chip in the chosen colour. Fixed, above everything, never in the
 * way of a press. Its padding plus border is FLY_PAD in ogs_hub.js. */
.ogs-fly {
  position: fixed;
  z-index: 1000;
  pointer-events: none;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin: 0;
  padding: 4px 14px 4px 4px;
  border: 1px solid var(--color-wood);
  border-radius: 999px;
  background: var(--color-surface-card);
  white-space: nowrap;
  will-change: transform, opacity;
}
.ogs-fly .ogs-online-dot { box-shadow: 0 0 0 2px var(--color-surface-card); }

/* ── Create a game, the Quick match card's second tab ─────────────── */
.ogs-create .ogs-more { margin-top: 0; }
@media (max-width: 520px) {
  .ogs-create .ogs-field-row { flex-direction: column; }
}
/* The last row: Ranked on the left, the button on the right. The button
   keeps Find a game's height so the two tabs end on the same weight. */
.ogs-create-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}
.ogs-create-foot .ogs-switch-row { flex: 0 1 auto; min-width: 0; padding: 2px 0; }
.ogs-create-foot .ogs-switch-text { flex: 0 1 auto; }
.ogs-create-foot .ogs-create-submit {
  flex: 0 0 auto;
  padding: 14px 24px;
  font-size: 15px;
  white-space: nowrap;
}

/* ── Create a game: who to challenge ──────────────────────────────
 * A search field whose answers drop down under it (face, name, rank),
 * the chosen player shown in its place, and the quick choices under it. */
.ogs-opp { display: flex; flex-direction: column; gap: 10px; }
.ogs-opp-field { position: relative; }
.ogs-opp-icon {
  position: absolute;
  left: 12px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--color-text-secondary);
  pointer-events: none;
}
.ogs-text-input.ogs-opp-input { padding-left: 36px; }
.ogs-opp-list {
  position: absolute;
  z-index: 5;
  left: 0;
  right: 0;
  top: calc(100% + 6px);
  max-height: 264px;
  margin: 0;
  padding: 4px;
  overflow-y: auto;
  list-style: none;
  border: 1px solid var(--color-divider);
  border-radius: 12px;
  background: var(--color-surface-card);
  box-shadow: 0 16px 36px -14px rgba(23, 10, 2, 0.4);
  animation: ogs-opp-drop 120ms var(--ease-out, ease-out);
}
@keyframes ogs-opp-drop {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}
.ogs-opp-option {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  padding: 7px 10px;
  border-radius: 8px;
  cursor: pointer;
}
.ogs-opp-option.is-active { background: color-mix(in srgb, var(--color-wood) 18%, transparent); }
.ogs-opp-none { padding: 10px 12px; font-size: 13px; color: var(--color-text-secondary); }
.ogs-opp-face {
  flex: none;
  box-sizing: border-box;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  object-fit: cover;
  background: color-mix(in srgb, var(--color-text) 8%, transparent);
}
.ogs-opp-face.is-bare {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  font-weight: 600;
  color: var(--color-text-secondary);
}
.ogs-opp-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 15px;
  font-weight: 600;
  color: var(--color-text);
}
.ogs-opp-rank {
  flex: none;
  font-size: 13px;
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}
.ogs-opp-tag {
  flex: none;
  margin-left: auto;
  padding: 1px 8px;
  border-radius: 999px;
  border: 1px solid var(--color-divider);
  font-size: 11px;
  color: var(--color-text-secondary);
}
/* The one being challenged: the field's place, in the chosen colour. */
.ogs-opp-picked {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  padding: 6px 6px 6px 8px;
  border: 1px solid var(--color-wood);
  border-radius: 10px;
  background: color-mix(in srgb, var(--color-wood) 12%, transparent);
}
.ogs-opp-picked .ogs-opp-face { width: 30px; height: 30px; }
.ogs-opp-picked .ogs-opp-tag { margin-left: 0; }
.ogs-opp-clear { margin-left: auto; font-size: 20px; line-height: 1; }
.ogs-opp-quick {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}
.ogs-opp-quick-label {
  margin-right: 2px;
  font-size: 11.5px;
  color: var(--color-text-secondary);
}
.ogs-opp-pill {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  padding: 3px 11px 3px 3px;
  border: 1px solid color-mix(in srgb, var(--color-text) 14%, transparent);
  border-radius: 999px;
  background: transparent;
  color: var(--color-text);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: border-color var(--dur-short) var(--ease-out),
              background var(--dur-short) var(--ease-out);
}
.ogs-opp-pill .ogs-opp-face { width: 22px; height: 22px; font-size: 11px; }
.ogs-opp-pill .ogs-opp-rank { font-size: 12px; }
.ogs-opp-pill:hover { border-color: var(--color-wood); }
.ogs-opp-pill.is-on {
  border-color: var(--color-wood);
  background: color-mix(in srgb, var(--color-wood) 16%, transparent);
}
.ogs-opp-pill:focus-visible { outline: 2px solid var(--ogs-focus); outline-offset: 2px; }
.ogs-opp-pill:disabled { opacity: 0.5; cursor: not-allowed; }
.ogs-opp-pill-name {
  max-width: 14ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}
@media (prefers-reduced-motion: reduce) {
  .ogs-opp-list { animation: none; }
}

/* A real checkbox, drawn as the switch the game menu uses. */
.ogs-switch-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  cursor: pointer;
}
.ogs-switch-input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.ogs-switch-input:focus-visible ~ .ogs-switch { outline: 2px solid var(--ogs-focus); outline-offset: 2px; }
.ogs-switch-text {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 15px;
  color: var(--color-text);
}
.ogs-switch-row.is-locked { cursor: default; opacity: 0.6; }

.ogs-submit-row .ogs-btn { white-space: nowrap; }

/* What next, after a game: the same opponent again, as a quiet line
   under the card's two actions (round 16b). Watching: another game. */
.ogs-gameover-next { display: flex; justify-content: center; }
.ogs-gameover-next > * { min-width: 0; }
.ogs-gameover-next > a.ogs-btn { width: 100%; }     /* the watch screen's "Watch another game" */
.ogs-gameover-next .ogs-rematch-btn {
  min-height: 32px;
  padding: 4px 10px;
  border-color: transparent;
  background: none;
  color: var(--color-text-secondary);
  font-weight: 500;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, var(--color-text-secondary) 45%, transparent);
}
.ogs-gameover-next .ogs-rematch-btn:hover {
  background: none;
  color: var(--color-text);
  text-decoration-color: var(--color-wood);
}

/* Contrast in both themes: an OFF switch read as nothing at all on the
   dark card, and the minutes on a chosen tile faded into its wood. */
.ogs-switch { background: color-mix(in srgb, var(--color-text) 20%, transparent); }
.ogs-switch.is-on { background: var(--color-wood); }
.ogs-speed-tile.is-on .ogs-speed-tile-est { color: var(--color-text-secondary); }


/* ── The watch screen, on the shared game layout (#13) ─────────── */

/* A guest's chat has a way in where the input would be: the sentence,
   then the button, never jammed together. */
.ogs-chat-guest {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  margin-top: var(--space-3);
  font-size: 14px;
  color: var(--color-text-secondary);
}
.ogs-chat-guest .ogs-btn { flex: 0 0 auto; white-space: nowrap; }
.is-watching .ogs-gameover .ogs-turn-line { align-items: flex-start; }


/* ═══════════════════════════════════════════════════════════════════
   AN OGS PLAYER'S PROFILE  (`.ogs-pf`, play/ogs/profile.html, round 11)

   The user: "profile page is very bare. make it more awesome somehow".
   A sibling of the pro player page (player-detail.css: hero, chips,
   the year sparkline) in the lobby's voice: a hero with the face and
   who they are, five numbers, the rating as a chart, then their games
   beside who they are on OGS. Win and loss are the pro page's own
   colours (--color-mq-excellent / --color-mq-mistake), never a red X.
   ═══════════════════════════════════════════════════════════════════ */

.ogs-pf { --pf-won: var(--color-mq-excellent); --pf-lost: var(--color-mq-mistake); }

/* ── The hero ──────────────────────────────────────────────────── */

.ogs-pf-hero {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  padding: var(--space-6);
  border: 1px solid var(--color-divider);
  border-radius: 20px;
  background:
    radial-gradient(120% 140% at 0% 0%,
      color-mix(in srgb, var(--color-wood) 16%, transparent) 0%, transparent 55%),
    var(--color-surface-card);
}
.ogs-pf-face {
  position: relative;
  flex: none;
  width: 104px;
  height: 104px;
  border-radius: 50%;
  box-shadow: 0 0 0 4px var(--color-surface-card),
              0 0 0 5px color-mix(in srgb, var(--color-wood) 55%, transparent),
              0 10px 24px -12px rgba(0, 0, 0, 0.35);
}
.ogs-pf-face img {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
  background: color-mix(in srgb, var(--color-text) 8%, transparent);
}
.ogs-pf-face-letter {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: color-mix(in srgb, var(--color-wood) 22%, var(--color-surface-card));
  color: var(--color-text);
  font-size: 44px;
  font-weight: 650;
}
/* On OGS right now: the live green; away: hollow. No dot until OGS
   has said (the socket), the rule every presence dot here keeps. */
.ogs-pf-presence {
  position: absolute;
  right: 4px;
  bottom: 4px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 3px solid var(--color-surface-card);
  background: var(--color-surface-card);
  box-shadow: inset 0 0 0 2px var(--color-text-tertiary);
}
.ogs-pf-presence.is-online { background: var(--color-live); box-shadow: none; }
.ogs-pf-presence[hidden] { display: none; }

.ogs-pf-id { min-width: 0; flex: 1 1 auto; }
.ogs-pf-name-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 10px;
}
.ogs-pf-name {
  margin: 0;
  font-size: 2.1rem;
  line-height: 1.1;
  letter-spacing: -0.015em;
  overflow-wrap: anywhere;
}
.ogs-pf-rank {
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--color-wood);
  color: #2b1d0e;
  font-size: 14px;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}
.ogs-pf-badge {
  padding: 2px 9px;
  border: 1px solid var(--color-divider);
  border-radius: 999px;
  font-size: 12px;
  color: var(--color-text-secondary);
}
.ogs-pf-badge.is-pro {
  border-color: color-mix(in srgb, #d4a02a 55%, var(--color-divider));
  color: #a37817;
}
.ogs-pf-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 18px;
  margin: 8px 0 0;
  font-size: 14px;
  color: var(--color-text-secondary);
}
.ogs-pf-meta > span { white-space: nowrap; }
.ogs-pf-flag { margin-right: 6px; }
.ogs-pf-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 10px;
  margin-top: var(--space-4);
}
a.ogs-pf-out,
a.ogs-pf-out:link,
a.ogs-pf-out:visited {
  font-size: 14px;
  color: var(--color-text-secondary);
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--color-text) 25%, transparent);
  text-underline-offset: 3px;
}
a.ogs-pf-out:hover { color: var(--color-text); text-decoration-color: var(--color-wood); }
/* A little more room between the buttons and the link, on the button's
   right: a left margin on the link indented it wherever it starts a line
   (alone on your own profile; wrapped under the buttons on a phone),
   while a margin at a line's end takes no room. */
.ogs-pf-actions .ogs-btn:has(+ .ogs-pf-out) { margin-right: 6px; }

/* ── Five numbers ──────────────────────────────────────────────── */

/* One row of equal tiles, however many there are (two to five); two
   columns below 900px, an odd last tile across both. auto-fit left one
   tile alone on a row of its own (mobile review P2-4: 2+2+1 on a phone,
   4+1 at 768). */
.ogs-pf-stats,
.ogs-pf-stats-row {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 12px;
}
@media (max-width: 899px) {
  .ogs-pf-stats,
  .ogs-pf-stats-row { grid-auto-flow: row; grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .ogs-pf-stats-row > .ogs-pf-stat:last-child:nth-child(odd) { grid-column: 1 / -1; }
}
.ogs-pf-stats { margin: var(--space-4) 0 var(--space-6); }
.ogs-pf-stats > .ogs-pf-stats-row { grid-column: 1 / -1; }
.ogs-pf-stat {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  padding: 14px 16px 13px;
  border: 1px solid var(--color-divider);
  border-radius: 14px;
  background: var(--color-surface-card);
}
.ogs-pf-stat.is-placeholder {
  min-height: 92px;
  background: linear-gradient(90deg,
    var(--color-surface-card) 0%,
    color-mix(in srgb, var(--color-text) 4%, var(--color-surface-card)) 50%,
    var(--color-surface-card) 100%);
  background-size: 200% 100%;
  animation: ogs-pf-shimmer 1.4s linear infinite;
}
@keyframes ogs-pf-shimmer { to { background-position: -200% 0; } }
.ogs-pf-stat-label {
  font-size: 11.5px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-secondary);
}
.ogs-pf-stat-value {
  font-size: 26px;
  line-height: 1.15;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  color: var(--color-text);
}
.ogs-pf-stat-sub { font-size: 12.5px; color: var(--color-text-secondary); }
.ogs-pf-stat.is-won .ogs-pf-stat-value { color: var(--pf-won); }
.ogs-pf-stat.is-lost .ogs-pf-stat-value { color: var(--pf-lost); }
.ogs-pf-winbar {
  display: block;
  height: 4px;
  margin-top: 6px;
  border-radius: 2px;
  background: color-mix(in srgb, var(--pf-lost) 45%, var(--color-divider));
  overflow: hidden;
}
.ogs-pf-winbar-won { display: block; height: 100%; background: var(--pf-won); }

/* ── Main column and side column ───────────────────────────────── */

/* The rating and the games down the left; the side column holds the
   cards about you and them, then who they are on OGS. The second row is
   `1fr` so that the games' height goes there, not between the two
   groups of side cards. */
.ogs-pf-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  grid-template-rows: auto 1fr;
  grid-template-areas: "main top" "main rest";
  gap: var(--space-4) var(--space-6);
  align-items: start;
}
.ogs-pf-main { grid-area: main; min-width: 0; display: flex; flex-direction: column; gap: var(--space-6); }
.ogs-pf-main > .ogs-hub-section { margin-top: 0; }
.ogs-pf-side { min-width: 0; display: flex; flex-direction: column; gap: var(--space-4); }
.ogs-pf-side-top { grid-area: top; }
.ogs-pf-side-rest { grid-area: rest; }
/* No cards about the two of you: the rest of the side column starts at
   the top. */
.ogs-pf-main:first-child ~ .ogs-pf-side-rest { grid-row: 1 / span 2; }
.ogs-pf-card {
  padding: 16px 18px;
  border: 1px solid var(--color-divider);
  border-radius: 16px;
  background: var(--color-surface-card);
}
.ogs-pf-card-title {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 12px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-secondary);
}

/* ── The rating chart ──────────────────────────────────────────── */

.ogs-pf-chart-card { position: relative; padding-bottom: 10px; }
.ogs-pf-chart-body { position: relative; }
.ogs-pf-range { position: absolute; top: -40px; right: 0; }
.ogs-pf-chart-svg { display: block; overflow: visible; touch-action: pan-y; }
.ogs-pf-rankline line {
  stroke: color-mix(in srgb, var(--color-text) 12%, transparent);
  stroke-dasharray: 3 4;
}
.ogs-pf-rankline text,
.ogs-pf-axis {
  fill: var(--color-text-tertiary);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}
.ogs-pf-band { fill: color-mix(in srgb, var(--color-wood) 12%, transparent); }
.ogs-pf-line {
  fill: none;
  stroke: var(--color-wood);
  stroke-width: 2.2;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.ogs-pf-end { fill: var(--color-wood); stroke: var(--color-surface-card); stroke-width: 2; }
.ogs-pf-hover line { stroke: color-mix(in srgb, var(--color-text) 28%, transparent); }
.ogs-pf-hover circle { fill: var(--color-wood); stroke: var(--color-surface-card); stroke-width: 2; }
.ogs-pf-hover circle.is-won { fill: var(--pf-won); }
.ogs-pf-hover circle.is-lost { fill: var(--pf-lost); }
.ogs-pf-tip {
  position: absolute;
  top: 6px;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  padding: 6px 10px;
  border: 1px solid var(--color-divider);
  border-radius: 10px;
  background: var(--color-surface-card);
  box-shadow: 0 8px 20px -10px rgba(0, 0, 0, 0.35);
  font-size: 12px;
  white-space: nowrap;
  pointer-events: none;
}
.ogs-pf-tip-date { color: var(--color-text-secondary); }
.ogs-pf-tip-rating { font-weight: 650; font-variant-numeric: tabular-nums; }
.ogs-pf-tip-result.is-won { color: var(--pf-won); }
.ogs-pf-tip-result.is-lost { color: var(--pf-lost); }
.ogs-pf-chart-skeleton { height: 240px; border-radius: 10px; background: color-mix(in srgb, var(--color-text) 3%, transparent); }
.ogs-pf-chart-msg { margin: var(--space-6) 0; text-align: center; }

/* ── Their games ───────────────────────────────────────────────── */

/* A card of rows, as the lobby's lists are, whose rows share one set of
   columns (subgrid): the result chips are all as wide as the widest, so
   "Annulled" no longer runs into the next column and the names start on
   one line (mobile review P2-5). The board, the clock and ranked or not
   are a column each (round 14, the user: "everything should be aligned
   vertically and dynamically"): the result, the opponent, the three
   facts, the date. */
.ogs-pf-games {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr) auto auto auto auto;
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--color-divider);
  border-radius: var(--ogs-radius);
  background: var(--color-surface-card);
}
.ogs-pf-games-section .ogs-section-title { padding-inline: calc(var(--ogs-pad) + 1px); }
.ogs-pf-game {
  position: relative;
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: center;
  gap: 4px 14px;
  padding: 10px var(--ogs-pad);
  border-bottom: 1px solid var(--color-divider);
  transition: background var(--dur-short) var(--ease-out);
}
.ogs-pf-game:last-child { border-bottom: 0; }
.ogs-pf-game:first-child { border-radius: calc(var(--ogs-radius) - 1px) calc(var(--ogs-radius) - 1px) 0 0; }
.ogs-pf-game:last-child { border-bottom-left-radius: calc(var(--ogs-radius) - 1px); border-bottom-right-radius: calc(var(--ogs-radius) - 1px); }
.ogs-pf-game:only-child { border-radius: calc(var(--ogs-radius) - 1px); }
.ogs-pf-game:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }
.ogs-pf-game.is-busy { opacity: 0.7; cursor: progress; }
/* The row's own link, stretched over it; the opponent's name sits
   above it as a link of its own. */
.ogs-pf-game-link { position: absolute; inset: 0; z-index: 0; border-radius: 4px; }
.ogs-pf-game-link:focus-visible { outline: 2px solid var(--ogs-focus); outline-offset: -2px; }
.ogs-pf-game-name { position: relative; z-index: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ogs-pf-result {
  justify-self: stretch;
  text-align: center;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 650;
  color: var(--color-text-secondary);
  background: color-mix(in srgb, var(--color-text) 6%, transparent);
}
.ogs-pf-result.is-won { color: var(--pf-won); background: color-mix(in srgb, var(--pf-won) 14%, transparent); }
.ogs-pf-result.is-lost { color: var(--pf-lost); background: color-mix(in srgb, var(--pf-lost) 13%, transparent); }
.ogs-pf-result.is-playing { color: var(--color-live); background: color-mix(in srgb, var(--color-live) 13%, transparent); }
.ogs-pf-game-who {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  font-size: 14.5px;
  font-weight: 600;
}
.ogs-pf-game-who .ogs-player-stone { width: 12px; height: 12px; flex: none; }
/* A white stone this small needs its edge on a light page. */
.ogs-pf-game-who .ogs-player-stone-white { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-text) 30%, transparent); }
.ogs-pf-game-who .ogs-opp-face { width: 26px; height: 26px; font-size: 12px; }
.ogs-pf-result { grid-column: 1; }
.ogs-pf-game-who { grid-column: 2; }
.ogs-pf-game-facts {
  grid-column: 3 / span 3;
  display: grid;
  grid-template-columns: subgrid;
  align-items: center;
  font-size: 13px;
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.ogs-pf-tag {
  justify-self: start;
  padding: 1px 7px;
  border: 1px solid var(--color-divider);
  border-radius: 999px;
  font-size: 11px;
}
.ogs-pf-game-date {
  grid-column: 6;
  justify-self: end;
  font-size: 13px;
  color: var(--color-text-secondary);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.ogs-pf-more { margin-top: var(--space-4); }
.ogs-pf-games-msg { margin-top: var(--space-3); }
.ogs-pf-empty { padding: var(--space-6); text-align: center; }

/* ── The side column ───────────────────────────────────────────── */

.ogs-pf-vs-score {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 18px;
}
.ogs-pf-vs-side { display: flex; flex-direction: column; align-items: center; min-width: 0; }
.ogs-pf-vs-n { font-size: 34px; line-height: 1.1; font-weight: 700; font-variant-numeric: tabular-nums; }
.ogs-pf-vs-who {
  max-width: 120px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12.5px;
  color: var(--color-text-secondary);
}
/* Level with the numbers, not with the number-and-name column. */
.ogs-pf-vs-dash { align-self: flex-start; font-size: 26px; line-height: 1.45; color: var(--color-text-tertiary); }
.ogs-pf-vs-draws { margin: 6px 0 0; text-align: center; font-size: 12.5px; color: var(--color-text-secondary); }
.ogs-pf-vs-recent {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--color-divider);
}
.ogs-pf-vs-recent-label { font-size: 12.5px; color: var(--color-text-secondary); }
.ogs-pf-dots { display: flex; gap: 6px; margin: 0; padding: 0; list-style: none; }
.ogs-pf-dot {
  display: block;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--color-text-tertiary);
  transition: transform var(--dur-short) var(--ease-out);
}
.ogs-pf-dot.is-won { background: var(--pf-won); }
.ogs-pf-dot.is-lost { background: var(--pf-lost); }
.ogs-pf-dot:hover { transform: scale(1.2); }
.ogs-pf-dot.is-busy { animation: ogs-pulse 0.9s var(--ease-out) infinite; }
.ogs-pf-dot:focus-visible { outline: 2px solid var(--ogs-focus); outline-offset: 2px; }

.ogs-pf-list { margin: 0; padding: 0; list-style: none; }
a.ogs-pf-list-row,
a.ogs-pf-list-row:link,
a.ogs-pf-list-row:visited {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 0;
  border-top: 1px solid var(--color-divider);
  color: var(--color-text);
  text-decoration: none;
  font-size: 14px;
}
.ogs-pf-list li:first-child a.ogs-pf-list-row { border-top: 0; }
a.ogs-pf-list-row:hover { color: var(--color-text); }
a.ogs-pf-list-row:hover .ogs-pf-list-main { text-decoration: underline; text-decoration-color: var(--color-wood); text-underline-offset: 3px; }
.ogs-pf-list-main { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ogs-pf-vs-word, .ogs-pf-list-meta { color: var(--color-text-secondary); font-size: 13px; }

.ogs-pf-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; list-style: none; }
a.ogs-pf-chip,
a.ogs-pf-chip:link,
a.ogs-pf-chip:visited {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  padding: 3px 10px 3px 4px;
  border: 1px solid var(--color-divider);
  border-radius: 999px;
  color: var(--color-text);
  font-size: 13px;
  text-decoration: none;
}
a.ogs-pf-chip:hover { color: var(--color-text); border-color: var(--color-wood); }
.ogs-pf-chip img { width: 20px; height: 20px; border-radius: 50%; object-fit: cover; }
.ogs-pf-chip span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.ogs-pf-trophies { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 8px; }
.ogs-pf-trophies li { display: flex; gap: 10px; align-items: baseline; font-size: 13.5px; line-height: 1.4; }
.ogs-pf-trophy {
  flex: none;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  transform: translateY(1px);
  background: radial-gradient(circle at 35% 35%, #f1d27a, #b5841c 70%);
}
.ogs-pf-about {
  margin: 0 0 8px;
  font-size: 14px;
  line-height: 1.55;
  white-space: pre-line;
  overflow-wrap: anywhere;
}

/* ── Narrower screens ──────────────────────────────────────────── */

@media (max-width: 960px) {
  .ogs-pf-grid {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: none;
    grid-template-areas: none;
    row-gap: var(--space-6);
  }
  .ogs-pf-main,
  .ogs-pf-side-top,
  .ogs-pf-side-rest,
  .ogs-pf-main:first-child ~ .ogs-pf-side-rest { grid-area: auto; }
}
@media (max-width: 620px) {
  /* The face beside the name, smaller, not stacked over it: the hero
     was a third of the first screen (mobile review P3-3). */
  .ogs-pf-hero { gap: var(--space-4); padding: var(--space-5) var(--ogs-pad); }
  .ogs-pf-face { width: 64px; height: 64px; }
  .ogs-pf-face-letter { font-size: 28px; }
  .ogs-pf-presence { right: 0; bottom: 0; width: 15px; height: 15px; }
  .ogs-pf-name { font-size: 1.6rem; }
  .ogs-pf-actions { margin-top: var(--space-3); }
  .ogs-pf-stat-value { font-size: 22px; }
  .ogs-pf-range { position: static; margin-bottom: 10px; }
  /* Two lines a row: the result down the left of both, the opponent and
     the date over the three facts, which keep their shared columns. */
  .ogs-pf-games { grid-template-columns: max-content auto auto minmax(0, 1fr) auto; }
  .ogs-pf-result { grid-column: 1; grid-row: 1 / span 2; }
  .ogs-pf-game-who { grid-column: 2 / 5; grid-row: 1; }
  .ogs-pf-game-date { grid-column: 5; grid-row: 1; }
  .ogs-pf-game-facts { grid-column: 2 / span 3; grid-row: 2; font-size: 12px; }
}
@media (prefers-reduced-motion: reduce) {
  .ogs-pf-stat.is-placeholder, .ogs-pf-dot.is-busy { animation: none; }
}


/* ═══════════════════════════════════════════════════════════════════
   OGS MESSAGES  (ogs_messages.js, play/ogs/_messages.html, round 11)

   The OGS account pill in the site header (OGS pages only), a sheet
   from the right with the conversations and one open thread, and a
   toast when one arrives with the sheet closed. Mounted on <body>, so
   nothing on the page can clip or out-stack it.
   ═══════════════════════════════════════════════════════════════════ */

/* The pill (round 12, the user: messages "should be next to OGS username
   (in the same pill)", not the site's username): your OGS name, to your
   profile, and the messages button with its count. At the right end of
   every OGS page's head row (play/ogs/_head.html), never in the site
   header, which has no room once OGS is a menu item of its own (round
   12b). */
.ogs-account-pill {
  display: inline-flex;
  align-items: stretch;
  flex: 0 0 auto;
  height: var(--ogs-pill-h);
  border: 1px solid color-mix(in srgb, var(--color-text) 14%, transparent);
  border-radius: 999px;
  background: var(--color-surface-card);
}
a.ogs-account-pill-name,
a.ogs-account-pill-name:link,
a.ogs-account-pill-name:visited {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
  padding: 0 10px 0 14px;
  border-radius: 999px 0 0 999px;
  color: var(--color-text);
  font-size: 14px;
  text-decoration: none;
  transition: background var(--dur-short) var(--ease-out);
}
a.ogs-account-pill-name:hover {
  background: var(--color-surface-overlay);
  color: var(--color-text);
  text-decoration: none;
}
.ogs-account-pill-user {
  max-width: 16ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}
.ogs-account-pill-rank {
  padding: 1px 8px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--color-text) 7%, transparent);
  color: var(--color-text-secondary);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.ogs-account-pill-msg {
  display: inline-flex;
  align-items: center;
  padding: 0 14px 0 12px;
  border-left: 1px solid color-mix(in srgb, var(--color-text) 14%, transparent);
  border-radius: 0 999px 999px 0;
  color: var(--color-text);
  transition: background var(--dur-short) var(--ease-out),
              color var(--dur-short) var(--ease-out);
}
.ogs-account-pill-msg:hover { background: var(--color-surface-overlay); color: var(--color-wood-ink); }
/* The count on the bubble's shoulder, ringed in the pill's own ground so
   it reads as sitting on top (the round 11 header button's badge, which
   the user liked). */
.ogs-account-pill-icon { position: relative; display: block; }
.ogs-account-pill-icon svg { display: block; width: 23px; height: 23px; }
.ogs-account-pill-icon .ogs-msg-badge {
  position: absolute;
  top: -5px;
  right: -8px;
  box-shadow: 0 0 0 2px var(--color-surface-card);
}
.ogs-account-pill-msg:hover .ogs-msg-badge { box-shadow: 0 0 0 2px var(--color-surface-overlay); }
.ogs-account-pill :focus-visible { outline: 2px solid var(--ogs-focus); outline-offset: -2px; }
.ogs-msg-badge {
  min-width: 15px;
  height: 15px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--color-wood);
  color: var(--color-on-wood);
  font-size: 9.5px;
  font-weight: 700;
  line-height: 15px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.ogs-msg-badge[hidden] { display: none; }

.ogs-msg-scrim {
  position: fixed;
  inset: 0;
  z-index: 1100;
  background: rgba(23, 10, 2, 0.28);
  animation: ogs-msg-fade 160ms var(--ease-out);
}
.ogs-msg-panel {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 1101;
  display: flex;
  flex-direction: column;
  width: min(420px, 100vw);
  border-left: 1px solid var(--color-divider);
  background: var(--color-surface-card);
  box-shadow: -18px 0 40px -24px rgba(0, 0, 0, 0.4);
  animation: ogs-msg-slide 220ms var(--ease-out);
}
@keyframes ogs-msg-fade { from { opacity: 0; } }
@keyframes ogs-msg-slide { from { transform: translateX(24px); opacity: 0; } }
.ogs-msg-panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 10px 12px 18px;
  border-bottom: 1px solid var(--color-divider);
}
.ogs-msg-title { margin: 0; font-size: 17px; font-weight: 650; }
.ogs-msg-close { font-size: 22px; line-height: 1; }

/* ── The list ──────────────────────────────────────────────────── */

.ogs-msg-list-view { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; overflow-y: auto; }
.ogs-msg-new { padding: 12px 16px 8px; }
.ogs-msg-search { width: 100%; }
.ogs-msg-results {
  margin: 6px 0 0;
  padding: 4px;
  list-style: none;
  border: 1px solid var(--color-divider);
  border-radius: 12px;
}
.ogs-msg-result {
  appearance: none;
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 6px 8px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--color-text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.ogs-msg-result:hover { background: color-mix(in srgb, var(--color-wood) 16%, transparent); }
.ogs-msg-none { padding: 8px 10px; font-size: 13px; color: var(--color-text-secondary); }
.ogs-msg-convs { margin: 0; padding: 4px 0; list-style: none; }
.ogs-msg-conv {
  appearance: none;
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 10px 16px;
  border: 0;
  background: none;
  color: var(--color-text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.ogs-msg-conv:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }
.ogs-msg-conv:focus-visible,
.ogs-msg-result:focus-visible { outline: 2px solid var(--ogs-focus); outline-offset: -2px; }
.ogs-msg-face { position: relative; flex: none; width: 40px; height: 40px; }
.ogs-msg-face img,
.ogs-msg-face-letter {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
  background: color-mix(in srgb, var(--color-wood) 18%, var(--color-surface-card));
  color: var(--color-text);
  font-weight: 650;
}
.ogs-msg-result .ogs-msg-face { width: 28px; height: 28px; font-size: 12px; }
.ogs-msg-online {
  position: absolute;
  right: -1px;
  bottom: -1px;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--color-live);
  box-shadow: 0 0 0 2px var(--color-surface-card);
}
.ogs-msg-conv-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
.ogs-msg-conv-top { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.ogs-msg-row-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 14.5px;
  font-weight: 600;
}
.ogs-msg-row-rank { flex: none; font-size: 12.5px; color: var(--color-text-secondary); }
.ogs-msg-conv-when { flex: none; margin-left: auto; font-size: 12px; color: var(--color-text-tertiary); }
.ogs-msg-conv-last {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
  color: var(--color-text-secondary);
}
.ogs-msg-conv.is-unread .ogs-msg-conv-last { color: var(--color-text); font-weight: 600; }
.ogs-msg-conv-unread {
  flex: none;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  border-radius: 10px;
  background: var(--color-wood);
  color: var(--color-on-wood);
  font-size: 11.5px;
  font-weight: 700;
  line-height: 20px;
  text-align: center;
}
.ogs-msg-empty { padding: 36px 28px; text-align: center; font-size: 13.5px; color: var(--color-text-secondary); }
.ogs-msg-empty p { margin: 0 0 6px; }
.ogs-msg-empty-title { font-size: 15px; font-weight: 600; color: var(--color-text); }

/* ── One conversation ──────────────────────────────────────────── */

.ogs-msg-thread { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
.ogs-msg-thread-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px 10px 8px;
  border-bottom: 1px solid var(--color-divider);
}
.ogs-msg-thread-head .ogs-msg-face { width: 36px; height: 36px; }
.ogs-msg-thread-who { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.ogs-msg-thread-name { font-size: 15px; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ogs-msg-thread-sub { font-size: 12px; color: var(--color-text-secondary); min-height: 1em; }
.ogs-msg-challenge { flex: none; }
.ogs-msg-lines {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  margin: 0;
  padding: 12px 16px;
  list-style: none;
  display: flex;
  flex-direction: column;
}
.ogs-msg-day {
  align-self: center;
  margin: 10px 0 6px;
  font-size: 12px;
  color: var(--color-text-tertiary);
}
.ogs-msg-line { display: flex; flex-direction: column; align-items: flex-start; margin-top: 8px; }
.ogs-msg-line.is-continued { margin-top: 2px; }
.ogs-msg-line.is-mine { align-items: flex-end; }
.ogs-msg-bubble {
  max-width: 82%;
  padding: 7px 12px;
  border-radius: 16px;
  background: var(--color-surface-overlay);
  color: var(--color-text);
  font-size: 14px;
  line-height: 1.45;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.ogs-msg-line.is-mine .ogs-msg-bubble { background: color-mix(in srgb, var(--color-wood) 30%, var(--color-surface-card)); }
.ogs-msg-line.is-pending .ogs-msg-bubble { opacity: 0.6; }
.ogs-msg-line.is-failed .ogs-msg-bubble { box-shadow: inset 0 0 0 1px var(--color-mq-mistake); }
.ogs-msg-bubble a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.ogs-msg-meta { margin: 2px 6px 0; font-size: 11px; color: var(--color-text-tertiary); }
.ogs-msg-meta.is-failed { color: var(--color-mq-mistake); }
.ogs-msg-compose {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  padding: 10px 12px 12px;
  border-top: 1px solid var(--color-divider);
}
.ogs-msg-input {
  flex: 1 1 auto;
  min-height: 40px;
  max-height: 140px;
  padding: 9px 14px;
  border-radius: 20px;
  resize: none;
  font: inherit;
  font-size: 14px;
  line-height: 1.4;
}
.ogs-msg-send { width: 40px; height: 40px; }

/* ── The toast ─────────────────────────────────────────────────── */

.ogs-msg-toast {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 1102;
  width: min(360px, calc(100vw - 32px));
  border: 1px solid var(--color-divider);
  border-radius: 16px;
  background: var(--color-surface-card);
  box-shadow: 0 16px 40px -18px rgba(0, 0, 0, 0.45);
  animation: ogs-msg-rise 220ms var(--ease-out);
}
@keyframes ogs-msg-rise { from { transform: translateY(12px); opacity: 0; } }
.ogs-msg-toast-main {
  appearance: none;
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 12px 40px 12px 14px;
  border: 0;
  border-radius: 16px;
  background: none;
  color: var(--color-text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.ogs-msg-toast-text { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.ogs-msg-toast-who { font-size: 14px; font-weight: 650; }
.ogs-msg-toast-said {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: 13px;
  color: var(--color-text-secondary);
}
.ogs-msg-toast-reply { flex: none; font-size: 13px; font-weight: 650; color: var(--color-wood-ink); }
.ogs-msg-toast-close { position: absolute; top: 6px; right: 6px; width: 28px; height: 28px; font-size: 18px; }

:is(.ogs-msg-conv, .ogs-msg-result, .ogs-msg-toast-main):active:not(:disabled) { transform: translateY(1px); }

@media (max-width: 560px) {
  .ogs-msg-panel { width: 100vw; border-left: 0; }
  /* At the top on a phone, where a notification is expected: at the
     bottom it covered the ⋯ sheet's last rows and Create challenge
     (mobile review P2-7). */
  .ogs-msg-toast {
    top: 8px;
    right: 8px;
    bottom: auto;
    left: 8px;
    width: auto;
    animation-name: ogs-msg-drop;
  }
}
@keyframes ogs-msg-drop { from { transform: translateY(-12px); opacity: 0; } }
/* Never over a sheet or a dialog, which has the reader's attention; the
   count on the account pill still says a message came. */
body:has(.ogs-menu-scrim, .ogs-modal-scrim, .bm-scrim:not([hidden])) .ogs-msg-toast { display: none; }
@media (prefers-reduced-motion: reduce) {
  .ogs-msg-scrim, .ogs-msg-panel, .ogs-msg-toast { animation: none; }
  :is(.ogs-msg-conv, .ogs-msg-result, .ogs-msg-toast-main):active:not(:disabled) {
    transform: none;
    filter: brightness(0.94);
  }
}

/* ═══════════════════════════════════════════════════════════════════
   A FINGER, NOT A POINTER  (mobile review P2-8, P2-9)

   On a touch screen a control is at least 40px tall, and the row
   actions a thumb reaches for most (Accept, the move bar, Estimate)
   44px: grown by padding, never by bigger type, and one height for one
   kind of control (segments were 33.5, 30 and 28px; dropdowns 30 and
   35). A chip whose size is part of what it says (the watchers count,
   a captures toggle, a link in a sentence) keeps its size and gets a
   larger touch area around it instead. Last in the file: it adjusts
   rules from every section above.
   ═══════════════════════════════════════════════════════════════════ */
@media (pointer: coarse) {
  :root { --ogs-pill-h: 40px; }
  .ogs-btn-sm,
  .ogs-switch-row,
  a.ogs-hub-back,
  a.ogs-hub-watch { min-height: 40px; }
  a.ogs-hub-back,
  a.ogs-hub-watch { display: inline-flex; align-items: center; }
  .ogs-seg-text,
  .ogs-pool-toolbar .ogs-seg-text,
  .kf-select-button,
  .ogs-pool-toolbar .kf-select-button { min-height: 40px; height: auto; }
  .ogs-icon-btn { width: 40px; height: 40px; }
  .ogs-open-actions .ogs-btn,
  .ogs-estimate-ask .ogs-btn,
  .ogs-variation-ask .ogs-btn,
  .ogs-chat-input,
  .sgf-controls-strip.ogs-nav-strip .sgf-nav-seg { min-height: 44px; }
  .ogs-link-btn,
  .ogs-watchers-toggle,
  .ogs-player-score.is-toggle,
  .ogs-pf-out { position: relative; }
  .ogs-link-btn::after,
  .ogs-watchers-toggle::after,
  .ogs-player-score.is-toggle::after,
  .ogs-pf-out::after {
    content: "";
    position: absolute;
    inset: -10px -6px;
  }
}
