:root {
    --bg: #1a1614;
    --fg: #f4d2a3;
    --board: #f0c98c;
    --board-edge: #6b4a1f;
    --grid: #5a3b1c;
    --red: #b22222;
    --black: #1a1a1a;
    --green: #228b22;
    --accent: #d4a55c;
    --muted: #7a6c5a;
    --select-bg: rgba(80, 60, 20, 0.85);
    --legal-bg: rgba(40, 80, 40, 0.65);
    --tile-back: #d8a96b;
    --tile-disc: #fffbe6;
    --river: #5a8fbf;
    font-family: ui-sans-serif, "Noto Sans CJK SC", "Noto Sans CJK TC", system-ui, sans-serif;
}

* { box-sizing: border-box; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--fg);
    min-height: 100vh;
    display: grid;
    place-items: start center;
}

.boot-loader {
    width: min(100%, 760px);
    min-height: 100vh;
    padding: 2rem 1rem;
    display: grid;
    place-items: center;
    transition: opacity 0.18s ease, transform 0.18s ease;
}
.boot-loader--done {
    opacity: 0;
    transform: translateY(-0.35rem);
}
.boot-card {
    width: min(100%, 32rem);
    padding: clamp(1.5rem, 6vw, 2.4rem);
    border: 1px solid rgba(212, 165, 92, 0.45);
    border-radius: 1rem;
    background:
        radial-gradient(circle at 20% 15%, rgba(178, 34, 34, 0.22), transparent 36%),
        linear-gradient(145deg, rgba(244, 210, 163, 0.09), rgba(0, 0, 0, 0.2));
    box-shadow: 0 18px 56px rgba(0, 0, 0, 0.35);
}
.boot-mark {
    width: 4rem;
    height: 4rem;
    display: grid;
    place-items: center;
    border: 2px solid var(--red);
    border-radius: 999px;
    background: var(--tile-disc);
    color: var(--red);
    font-size: 2.15rem;
    font-weight: 800;
    box-shadow: 0 0 0 0.45rem rgba(178, 34, 34, 0.12);
}
.boot-kicker {
    margin: 1.25rem 0 0.35rem;
    color: var(--accent);
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}
.boot-card h1 {
    margin: 0;
    font-size: clamp(1.9rem, 7vw, 3.4rem);
    line-height: 0.95;
}
.boot-copy {
    max-width: 30rem;
    margin: 1rem 0 1.25rem;
    color: var(--muted);
    line-height: 1.5;
}
.boot-progress {
    height: 0.55rem;
    overflow: hidden;
    border: 1px solid rgba(212, 165, 92, 0.36);
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.28);
}
.boot-progress span {
    display: block;
    width: 45%;
    height: 100%;
    border-radius: inherit;
    background: linear-gradient(90deg, var(--red), var(--accent), var(--tile-disc));
    animation: boot-progress 1.2s ease-in-out infinite;
}
.boot-noscript {
    max-width: 34rem;
    margin: 1rem;
    padding: 1rem;
    border: 1px solid var(--red);
    border-radius: 0.5rem;
    background: rgba(178, 34, 34, 0.12);
}
@keyframes boot-progress {
    0% { transform: translateX(-105%); }
    55%, 100% { transform: translateX(235%); }
}
@media (prefers-reduced-motion: reduce) {
    .boot-loader { transition: none; }
    .boot-progress span { animation: none; transform: none; width: 100%; }
}

.app-shell {
    width: min(1100px, 100%);
    padding: 2rem 1rem;
}

.picker h1 { font-size: clamp(1.8rem, 5vw, 3rem); margin: 0 0 0.5rem; }
.subtitle { color: var(--muted); margin: 0 0 2rem; }

.picker-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 1rem;
}

.variant-card {
    display: block;
    padding: 1.25rem;
    background: rgba(244, 210, 163, 0.06);
    border: 1px solid var(--grid);
    border-radius: 0.75rem;
    color: inherit;
    text-decoration: none;
    transition: background 0.12s ease, border-color 0.12s ease;
}
.variant-card:hover { background: rgba(244, 210, 163, 0.12); border-color: var(--accent); }
.variant-card h2 { margin: 0 0 0.25rem; font-size: 1.25rem; }
.variant-card p { margin: 0; color: var(--muted); font-size: 0.92rem; }
.variant-card--online { background: rgba(178, 34, 34, 0.1); }
.variant-card--online:hover { background: rgba(178, 34, 34, 0.2); }

.variant-card--form {
    cursor: default;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}
.variant-card--form:hover { background: rgba(244, 210, 163, 0.06); border-color: var(--grid); }
.card-fieldset {
    border: 1px solid var(--grid);
    border-radius: 0.4rem;
    padding: 0.5rem 0.75rem;
    margin: 0;
}
.card-fieldset legend {
    color: var(--accent);
    font-size: 0.8rem;
    padding: 0 0.35rem;
}
.radio-row, .check-row {
    display: flex;
    align-items: flex-start;
    gap: 0.45rem;
    font-size: 0.9rem;
    line-height: 1.3;
    padding: 0.15rem 0;
    cursor: pointer;
}
.radio-row input, .check-row input { margin-top: 0.2rem; flex-shrink: 0; }
.preset-row { display: flex; gap: 0.4rem; flex-wrap: wrap; }
.btn-ghost {
    background: transparent;
    border: 1px solid var(--grid);
    color: var(--fg);
    padding: 0.25rem 0.6rem;
    font-size: 0.85rem;
}
.btn-ghost:hover { border-color: var(--accent); background: rgba(244, 210, 163, 0.08); }
.hint {
    margin: 0.35rem 0 0;
    color: var(--muted);
    font-size: 0.78rem;
    line-height: 1.35;
}
.hint code {
    background: rgba(244, 210, 163, 0.1);
    padding: 0.05rem 0.25rem;
    border-radius: 0.2rem;
}
.card-cta { margin-top: auto; align-self: flex-start; }

.game-page {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 280px;
    gap: 1.5rem;
    align-items: start;
}
.game-page--single { grid-template-columns: 1fr; }
@media (max-width: 720px) { .game-page { grid-template-columns: 1fr; } }

/* Non-mirror viewport fit (desktop / two-column layout). Cap the board
 * pane to the available viewport height (minus `.app-shell`'s 2rem top +
 * 2rem bottom padding) so the page never scrolls. The pane becomes a
 * flex column whose SVG board takes whatever vertical space remains
 * after the captured strip + any optional banners. Mobile / portrait
 * (single-column at <=720px) is exempt — the sidebar stacks below and
 * the page is allowed to scroll. */
@media (min-width: 721px) {
    .board-pane {
        display: flex;
        flex-direction: column;
        max-height: calc(100dvh - 4rem);
        min-height: 0;
        box-sizing: border-box;
    }
    .board-pane > svg.board {
        flex: 1 1 auto;
        width: 100%;
        height: 100%;
        max-width: 100%;
        max-height: 100%;
        min-height: 0;
    }
}

/* Mirror mode (xiangqi pass-and-play with two players sitting opposite).
 * Board pane is a flex column whose total height is the viewport minus
 * `.app-shell`'s 2rem top + 2rem bottom padding. The captured strips and
 * resign buttons size to content; the board SVG takes whatever vertical
 * space remains via `flex: 1`. The default `.board { width: 100% }`
 * combined with `preserveAspectRatio="xMidYMid meet"` then letterboxes
 * the SVG so it always fits without forcing the pane taller. */
.board-pane--mirror {
    display: flex;
    flex-direction: column;
    height: calc(100dvh - 4rem);
    max-height: calc(100dvh - 4rem);
    min-height: 0;
    box-sizing: border-box;
}
/* Override the global `.board { height: auto; max-height: 80vh }` so the
 * SVG can shrink inside the flex column instead of forcing 80vh. */
.board-pane--mirror > svg.board {
    flex: 1 1 auto;
    width: 100%;
    height: 100%;
    max-width: 100%;
    max-height: 100%;
    min-height: 0;
}

/* Per-seat resign buttons in mirror mode. They sit as flex items at the
 * top and bottom of `.board-pane--mirror`, aligned to the right edge.
 * The top button is rotated 180° so the Black player reads it upright.
 * Inline (not absolute) so they don't overlap the captured strips. */
.resign-btn {
    align-self: flex-end;
    flex: 0 0 auto;
    background: rgba(244, 210, 163, 0.08);
    color: var(--muted);
    border: 1px solid var(--grid);
    border-radius: 0.4rem;
    padding: 0.25rem 0.65rem;
    margin: 0.25rem;
    font: inherit;
    font-size: 0.85rem;
    cursor: pointer;
}
.resign-btn:hover:not(:disabled) {
    background: rgba(248, 113, 113, 0.18);
    color: #f87171;
    border-color: #f87171;
}
.resign-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.resign-btn--top {
    transform: rotate(180deg);
    transform-origin: center;
    align-self: flex-start;
}

/* Right-hand column on the play page stacks the sidebar and chat panel. */
.right-column {
    display: grid;
    gap: 1rem;
    align-content: start;
}

.chat-panel {
    background: rgba(244, 210, 163, 0.04);
    border: 1px solid var(--grid);
    border-radius: 0.5rem;
    display: grid;
    grid-template-rows: 1fr auto;
    height: 320px;
    overflow: hidden;
}
.chat-log {
    overflow-y: auto;
    padding: 0.5rem 0.75rem;
    font-size: 0.88rem;
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
}
.chat-line {
    margin: 0;
    line-height: 1.35;
    word-wrap: break-word;
}
.chat-line .ts {
    color: var(--muted);
    font-size: 0.78rem;
    margin-right: 0.4rem;
    font-variant-numeric: tabular-nums;
}
.chat-line .from-red { color: var(--red); font-weight: 600; }
.chat-line .from-black { color: var(--fg); font-weight: 600; }
.chat-line .from-green { color: var(--green); font-weight: 600; }
.chat-line .text { color: var(--fg); }
.chat-empty { color: var(--muted); margin: 0.4rem 0; font-size: 0.85rem; }
.chat-input {
    display: flex;
    gap: 0.4rem;
    padding: 0.4rem 0.5rem;
    border-top: 1px solid var(--grid);
    background: rgba(0, 0, 0, 0.18);
}
.chat-input input {
    flex: 1;
    /* Without min-width:0 the flex item refuses to shrink below its
       intrinsic placeholder width on narrow containers (iPad sidebar
       column), pushing the Send button outside the .chat-panel's
       overflow:hidden clip area so it renders as half-cut. */
    min-width: 0;
    background: rgba(244, 210, 163, 0.06);
    color: var(--fg);
    border: 1px solid var(--grid);
    border-radius: 0.3rem;
    padding: 0.35rem 0.55rem;
    font: inherit;
}
.chat-input input:focus { outline: 1px solid var(--accent); border-color: var(--accent); }
.chat-input input:disabled { opacity: 0.5; cursor: not-allowed; }
.chat-input .btn {
    /* Pair with the input's min-width:0 — keep Send at its intrinsic
       width even on tight columns instead of letting flex shrink it. */
    flex-shrink: 0;
}
.spectator-badge { color: var(--accent); font-weight: 600; }
.room-actions {
    display: flex;
    gap: 0.35rem;
}

.back-link {
    display: inline-block;
    color: var(--muted);
    text-decoration: none;
}
.back-link:hover { color: var(--accent); }

/* Board pane + SVG. */
.board-pane {
    position: relative;
    background: var(--board);
    border: 4px solid var(--board-edge);
    border-radius: 0.5rem;
    padding: 0.5rem;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
}

/* Captured-pieces strip rendered directly under .board (inside .board-pane).
 * Two stacked rows — Black dead on top (Red's trophies), Red dead below
 * (Black's trophies) — each piece a small disc-icon mirroring board style. */
.captured-strip {
    margin-top: 0.6rem;
    padding: 0.55rem 0.75rem 0.65rem;
    border-top: 1px dashed var(--grid);
    display: grid;
    gap: 0.35rem;
}
.captured-header {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.5rem;
}
.captured-header h4 {
    margin: 0;
    font-size: 0.85rem;
    color: var(--muted);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}
.captured-toggle {
    font-size: 0.75rem;
    padding: 0.1rem 0.55rem;
    line-height: 1.25;
}
.captured-row {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    min-height: 1.7rem;
}
.captured-label {
    color: var(--muted);
    font-size: 0.85rem;
    width: 1.4rem;
    flex-shrink: 0;
    text-align: center;
}
.captured-row.red .captured-label { color: var(--red); }
.captured-pieces {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
    align-items: center;
    flex: 1;
}
.captured-piece {
    display: inline-grid;
    place-items: center;
    width: 1.65rem;
    height: 1.65rem;
    border-radius: 50%;
    background: var(--tile-disc);
    border: 1px solid var(--grid);
    line-height: 1;
}
.captured-piece-glyph {
    font-size: 0.95rem;
    font-weight: 700;
    user-select: none;
}
.captured-row.red .captured-piece-glyph { color: var(--red); }
.captured-row.black .captured-piece-glyph { color: var(--black); }
.captured-empty { color: var(--muted); font-size: 0.85rem; }

/* Mirror mode: above-board strip is rotated 180° so glyphs read upright
 * for the player sitting on the opposite seat. The strip sits BEFORE the
 * board in source order (so it's at the top of the device) and we drop
 * the dashed top-border + flip the margin to a bottom-margin so the
 * visual separator still lives between the strip and the board. */
.captured-strip--mirrored-above {
    margin-top: 0;
    margin-bottom: 0.6rem;
    border-top: none;
    border-bottom: 1px dashed var(--grid);
}
.captured-row--mirrored {
    transform: rotate(180deg);
    transform-origin: center;
}

/* Three-kingdom WIP overlay. */
.wip-overlay {
    position: absolute;
    inset: 0;
    background: rgba(26, 22, 20, 0.78);
    display: grid;
    place-items: center;
    border-radius: 0.5rem;
    pointer-events: none;
}
.wip-banner {
    background: var(--bg);
    color: var(--fg);
    border: 1px solid var(--accent);
    border-radius: 0.5rem;
    padding: 1.25rem 1.5rem;
    max-width: 80%;
    text-align: center;
    pointer-events: auto;
}
.wip-banner h3 { margin: 0 0 0.4rem; color: var(--accent); }
.wip-banner p { margin: 0; color: var(--muted); font-size: 0.92rem; }
.wip-banner code {
    background: rgba(244, 210, 163, 0.1);
    padding: 0.05rem 0.3rem;
    border-radius: 0.2rem;
    color: var(--fg);
}
.board {
    display: block;
    width: 100%;
    height: auto;
    max-height: 80vh;
    user-select: none;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
    touch-action: manipulation;
}
.board-bg { fill: var(--board); }
.grid { stroke: var(--grid); stroke-width: 1.5; }
.palace { stroke: var(--grid); stroke-width: 1.2; opacity: 0.85; }
.river-text {
    fill: var(--river);
    font-size: 28px;
    letter-spacing: 0.6em;
    font-weight: 600;
    user-select: none;
    pointer-events: none;
}

/* ICCS-style coordinate labels around the board. Live in the
   LABEL_MARGIN strip OUTSIDE the playing area (board.rs extends the
   viewBox + bg into negative coords) so they never overlap pieces.
   Slightly higher opacity than before since they now sit on the
   wood frame, not on the grid where they competed visually. */
.board-coord {
    fill: var(--river);
    font-size: 12px;
    font-weight: 600;
    opacity: 0.7;
    user-select: none;
    pointer-events: none;
    font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}

/* Pieces — discs with a centered glyph. */
.tile { pointer-events: none; }
.tile-disc { fill: var(--tile-disc); stroke: var(--grid); stroke-width: 1.5; }
.tile-back {
    fill: var(--tile-back);
    stroke: var(--board-edge);
    stroke-width: 1.8;
}
.tile-glyph { font-size: 30px; font-weight: 700; user-select: none; }
.tile.piece.red .tile-glyph { fill: var(--red); }
.tile.piece.black .tile-glyph { fill: var(--black); }
.tile.piece.green .tile-glyph { fill: var(--green); }

.selection {
    fill: rgba(255, 200, 60, 0.18);
    stroke: #ff8c1a;
    stroke-width: 4;
    pointer-events: none;
    filter: drop-shadow(0 0 5px rgba(255, 140, 26, 0.6));
}
.move-dot {
    fill: var(--legal-bg);
    pointer-events: none;
}
.move-target {
    fill: rgba(60, 140, 60, 0.20);
    stroke: rgba(80, 200, 80, 0.85);
    stroke-width: 2.5;
    pointer-events: none;
}
.chain-lock {
    fill: none;
    stroke: #f5a623;
    stroke-width: 3.5;
    stroke-dasharray: 5 3;
    pointer-events: none;
}
/* Threat highlight (Display setting). Three flavors so the user can
 * tell which mode is firing without consulting the picker:
 *   .threat-mark         — base ring for modes A (Attacked) and B
 *                          (NetLoss). Red, similar geometry to
 *                          .move-target so it reads as a 'warning'
 *                          version of the same affordance.
 *   .threat-mark--attack — same as base; alias kept for class-list
 *                          clarity in the SVG renderer if a future
 *                          revision wants to differentiate A vs B
 *                          visually (e.g. dashed for A) without
 *                          touching the renderer.
 *   .threat-mark--mate   — magenta, thicker — only shown when mode C
 *                          (MateThreat) is active and points at the
 *                          opponent piece(s) participating in the
 *                          checkmate-in-1 threat.
 *   .threat-mark--hover  — softer red, semi-transparent — used by the
 *                          'hover preview' secondary toggle to show
 *                          what an opponent could take if the
 *                          hovered/selected piece stayed put. Drawn
 *                          on top of the static threat ring so both
 *                          can stack without one swallowing the other. */
.threat-mark {
    fill: none;
    stroke: #d63838;
    stroke-width: 3;
    pointer-events: none;
    opacity: 0.85;
}
.threat-mark--mate {
    stroke: #b300b3;
    stroke-width: 4;
    stroke-dasharray: 4 2;
}
.threat-mark--hover {
    stroke: #ff7a7a;
    stroke-width: 2.5;
    stroke-dasharray: 3 3;
    opacity: 0.7;
}
/* "Highlight latest move" overlay (Display setting). Soft sky-blue
 * tint behind the from/to squares of the most recent move so the
 * user sees at a glance what the opponent just played. Color picked
 * to be distinct from existing highlights:
 *   - selection / chain-lock   → orange / amber
 *   - move-target / move-dot   → green
 *   - threat / threat-hover    → red / soft red
 *   - debug-from / debug-to    → yellow / blue (deeper)
 *   - last-move-from / -to     → light steel-blue (this rule)
 *
 * Drawn UNDER the piece glyph so the piece itself sits cleanly on
 * top of the highlight (the marker mounts in the `overlay-layer`
 * SVG group, between palace lines and pieces — see board.rs). The
 * destination ring is slightly stronger than the origin so direction
 * is readable without an explicit arrow. */
.last-move-from {
    fill: rgba(80, 140, 200, 0.16);
    stroke: rgba(80, 140, 200, 0.45);
    stroke-width: 1.5;
    pointer-events: none;
}
.last-move-to {
    fill: rgba(80, 140, 200, 0.30);
    stroke: rgba(80, 140, 200, 0.65);
    stroke-width: 2;
    pointer-events: none;
}
/* Floating toast — positioned ABOVE the board pane (negative top) so
 * its mount/unmount doesn't reflow the flex column or snap the SVG to
 * a different size, AND it doesn't cover the top row of pieces / tiles.
 * Previous `top: 0.6rem` placed it inside .board-pane's padding which
 * overlapped the first board row (banqi top-rank tiles, xiangqi rank 9
 * pieces). The negative offset floats it in the empty space above the
 * board card — visible against the dark page background, no overlap. */
.chain-banner {
    position: absolute;
    top: -2.4rem;
    left: 50%;
    transform: translateX(-50%);
    z-index: 25;
    padding: 0.4rem 0.8rem;
    background: rgba(245, 166, 35, 0.92);
    border: 1px solid #f5a623;
    border-radius: 0.4rem;
    color: #1a1614;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
    pointer-events: auto;
    white-space: nowrap;
    font-weight: 600;
}
.ai-thinking-banner {
    background: rgba(212, 165, 92, 0.92);
    border-color: var(--accent);
}
.btn-sm { font-size: 0.85rem; padding: 0.2rem 0.6rem; }

/* Hit-test cells. Transparent but clickable.
   pointer-events: all stops iOS Safari from skipping the rect when fill is
   transparent. The :hover rule is gated on hover-capable devices because
   iOS otherwise treats the first tap as a hover (consuming the click). */
.cell-hit {
    fill: transparent;
    cursor: pointer;
    touch-action: manipulation;
    pointer-events: all;
}
@media (hover: hover) and (pointer: fine) {
    .cell-hit:hover { fill: rgba(212, 165, 92, 0.18); }
}

/* Sidebar. */
.sidebar {
    background: rgba(244, 210, 163, 0.04);
    border: 1px solid var(--grid);
    border-radius: 0.5rem;
    padding: 1rem 1.25rem;
    display: grid;
    gap: 0.65rem;
}
.variant-label { margin: 0; font-size: 1.1rem; }
.turn-row { display: flex; align-items: baseline; gap: 0.4rem; }
.turn-prefix { color: var(--muted); }
.turn-red { color: var(--red); font-weight: 700; }
.turn-black { color: var(--fg); font-weight: 700; }
.turn-green { color: var(--green); font-weight: 700; }
.turn-awaiting { color: #d4a017; font-weight: 700; font-style: italic; }
.status { margin: 0; font-weight: 600; }
.status.ongoing { color: var(--muted); font-weight: 400; }
.status.won { color: var(--accent); }
.status.drawn { color: var(--muted); }
.muted { color: var(--muted); margin: 0; font-size: 0.9rem; }
.sidebar-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.btn {
    background: rgba(244, 210, 163, 0.08);
    color: var(--fg);
    border: 1px solid var(--grid);
    border-radius: 0.4rem;
    padding: 0.4rem 0.8rem;
    cursor: pointer;
    font: inherit;
}
.btn:hover { border-color: var(--accent); background: rgba(244, 210, 163, 0.16); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-primary {
    background: rgba(178, 34, 34, 0.6);
    border-color: var(--red);
    color: #fffbe6;
    font-weight: 600;
}
.btn-primary:hover { background: rgba(178, 34, 34, 0.78); }

/* AI hint toggle button (sidebar). Distinguishable from generic .btn
   so the user spots the on-demand affordance immediately, but visually
   subordinate to .btn-primary. The "on" state shifts to a muted blue
   matching the .hints-banner net-mode banner. */
.btn-hint {
    margin-top: 0.5rem;
    width: 100%;
    background: rgba(80, 130, 200, 0.1);
    border-color: rgba(80, 130, 200, 0.45);
    color: #d6e3f3;
    font-weight: 500;
}
.btn-hint:hover {
    background: rgba(80, 130, 200, 0.22);
    border-color: rgba(80, 130, 200, 0.7);
}
.btn-hint--on {
    background: rgba(80, 130, 200, 0.32);
    border-color: rgba(120, 170, 230, 0.85);
    color: #ffffff;
}
.btn-hint--on:hover {
    background: rgba(80, 130, 200, 0.45);
}

/* Lobby. */
.lobby { display: grid; gap: 1rem; }
.lobby h2 { margin: 0; }
.lobby h3 { margin: 1rem 0 0; }
.lobby-form {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) auto auto;
    gap: 0.5rem;
    align-items: stretch;
}
@media (max-width: 600px) {
    .lobby-form { grid-template-columns: 1fr; }
}
.text-input {
    background: rgba(244, 210, 163, 0.06);
    color: var(--fg);
    border: 1px solid var(--grid);
    border-radius: 0.4rem;
    padding: 0.5rem 0.75rem;
    font: inherit;
    width: 100%;
    max-width: 100%;
    box-sizing: border-box;
}
.text-input:focus { outline: 1px solid var(--accent); border-color: var(--accent); }
/* Search-depth number input: by default <input type=number> in the
 * variant card uses 100% of grid width (which is narrow). Force a
 * comfortable minimum so placeholder + 2-digit values fit without
 * clipping. */
.depth-input {
    width: 100%;
    min-width: 6rem;
    box-sizing: border-box;
}

/* Custom-difficulty inline inputs (Search depth + Node budget +
 * Variation). Mounted below the Custom radio inside the Difficulty
 * fieldset. The variant card is as narrow as 220 px on multi-card
 * layouts (iPad landscape), so this block must stay inside its
 * parent's content width — `min-width: 0` defeats the default flex/
 * grid intrinsic-min-content behaviour that would otherwise overflow.
 *
 * Layout: each row stacks the label above the input (1-column flex
 * column) instead of a label-input-cap grid; the previous 3-column
 * grid required ~6 rem for the input + however-much for the cap text
 * + however-much for the label, which blew out at narrow widths and
 * caused the inputs to overlap with each other (see screenshot
 * report 2026-05-10). */
.custom-difficulty-inputs {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    margin-left: 1.6rem;          /* aligns with the radio's label text */
    padding: 0.5rem 0.6rem;
    border-left: 2px solid rgba(60, 80, 120, 0.18);
    background: rgba(60, 80, 120, 0.04);
    border-radius: 0 4px 4px 0;
    min-width: 0;
    max-width: 100%;
    box-sizing: border-box;
}
.custom-input-row {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    min-width: 0;
}
.custom-input-label {
    color: var(--muted, #888);
    font-size: 0.85rem;
}
.custom-input-row .text-input {
    width: 100%;
    box-sizing: border-box;
    min-width: 0;                 /* allow shrinking inside narrow card */
}
/* Sub-fieldset that mirrors the standalone Variation fieldset's
 * options when Custom is active. Tighter spacing than the top-level
 * radio rows since it lives inside an already-indented block. */
.custom-variation {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    margin: 0.2rem 0 0;
    padding: 0;
    border: 0;
}
.custom-variation legend {
    padding: 0;
    color: var(--muted, #888);
    font-size: 0.85rem;
}
.custom-variation .radio-row {
    font-size: 0.88rem;
    line-height: 1.25;
}

.rooms-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.4rem; }
.room-row {
    display: grid;
    grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) auto auto auto auto;
    gap: 0.75rem;
    align-items: center;
    padding: 0.6rem 0.9rem;
    background: rgba(244, 210, 163, 0.04);
    border: 1px solid var(--grid);
    border-radius: 0.4rem;
}
.room-row:hover { border-color: var(--accent); }
.room-id { font-weight: 600; }
.room-variant { color: var(--muted); font-size: 0.9rem; }
.room-seats { color: var(--muted); font-size: 0.9rem; }
.room-lock { font-size: 0.85rem; }
.room-hints { font-size: 0.85rem; }
.room-status { font-size: 0.8rem; padding: 0.1rem 0.5rem; border-radius: 0.2rem; }
.room-status.lobby { background: rgba(122, 108, 90, 0.3); color: var(--fg); }
.room-status.playing { background: rgba(40, 120, 60, 0.4); color: #d6f3d8; }
.room-status.finished { background: rgba(178, 34, 34, 0.3); color: #f3d6d6; }

/* AI hints banner above the board in net mode (server says
   hints_allowed). Tells both players + spectators that the AI panel
   is sanctioned for this room. */
.hints-banner {
  background: rgba(80, 130, 200, 0.18);
  color: #d6e3f3;
  padding: 0.4rem 0.75rem;
  border-radius: 0.3rem;
  font-size: 0.85rem;
  margin-bottom: 0.5rem;
  text-align: center;
}

/* Connection banner. */
.conn-banner { margin: 0; padding: 0.4rem 0.75rem; border-radius: 0.3rem; font-size: 0.9rem; }
.conn-banner.ok { background: rgba(40, 120, 60, 0.18); color: #d6f3d8; }
.conn-banner.pending { background: rgba(212, 165, 92, 0.15); color: var(--accent); }
.conn-banner.error { background: rgba(178, 34, 34, 0.2); color: #f3d6d6; }

/* Online play placeholder + toast. */
.conn-placeholder {
    aspect-ratio: 9 / 10;
    display: grid;
    place-items: center;
    text-align: center;
    color: var(--muted);
    padding: 2rem;
}
.toast {
    position: absolute;
    left: 50%;
    bottom: 1rem;
    transform: translateX(-50%);
    background: rgba(178, 34, 34, 0.92);
    color: #fffbe6;
    padding: 0.6rem 1rem;
    border-radius: 0.4rem;
    font-weight: 500;
    cursor: pointer;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}

/* ─────────────────────────────────────────────────────────────────────
 * End-of-game overlay + confetti + check banner. See `prefs.rs` for
 * the localStorage toggles that gate these.
 * ───────────────────────────────────────────────────────────────────── */

/* Centered VICTORY/DEFEAT/DRAW banner over the board area. The container
 * is `position: absolute` so it lives inside `.board-pane`'s positioning
 * context (same as `.wip-overlay`). The banner card uses the existing
 * accent palette so it reads as part of the theme rather than a popover. */
.endgame-overlay {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    z-index: 40;
    pointer-events: none;
    background: rgba(26, 22, 20, 0.45);
    border-radius: 0.5rem;
    animation: endgame-fade-in 0.25s ease-out;
}
/* Mirror-mode halves: each overlay covers the half of the board pane that
 * faces a single seat. Top is rotated 180° so it reads upright from the
 * far seat (Black). */
.endgame-overlay--top {
    inset: 0 0 50% 0;
    transform: rotate(180deg);
    transform-origin: center;
}
.endgame-overlay--bottom {
    inset: 50% 0 0 0;
}
.endgame-banner {
    background: var(--bg);
    border: 2px solid var(--accent);
    border-radius: 0.6rem;
    padding: 1.25rem 2rem;
    text-align: center;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55);
    animation: pulse-banner 1.6s ease-in-out infinite;
}
.endgame-title {
    margin: 0;
    font-size: clamp(2rem, 6vw, 3.5rem);
    letter-spacing: 0.18em;
    font-weight: 800;
}
.endgame-title--victory { color: #fcd34d; text-shadow: 0 0 12px rgba(252, 211, 77, 0.5); }
.endgame-title--defeat  { color: #f87171; text-shadow: 0 0 12px rgba(248, 113, 113, 0.5); }
.endgame-title--draw    { color: var(--muted); }
.endgame-title--red     { color: var(--red); }
.endgame-title--black   { color: #f4d2a3; }
.endgame-title--green   { color: #6ad28d; }
.endgame-reason {
    margin: 0.5rem 0 0;
    color: var(--muted);
    font-size: 0.95rem;
}

/* Confetti — pure CSS particles. The container fills the viewport so
 * particles can fall past the board edge naturally. Each particle reads
 * `--x` (start x %), `--drift` (lateral drift in px), `--delay` (stagger),
 * `--rotate-end` (final rotation), `--color` (fill). */
.confetti-container {
    position: fixed;
    inset: 0;
    pointer-events: none;
    z-index: 60;
    overflow: hidden;
}
.confetti-particle {
    position: absolute;
    top: -16px;
    left: var(--x);
    width: 8px;
    height: 14px;
    background: var(--color);
    border-radius: 1px;
    transform-origin: 50% 50%;
    animation: confetti-fall 3s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
    animation-delay: var(--delay);
    opacity: 0.95;
}

@keyframes confetti-fall {
    0%   { transform: translate(0, 0) rotate(0deg); opacity: 1; }
    80%  { opacity: 1; }
    100% { transform: translate(var(--drift), 110vh) rotate(var(--rotate-end)); opacity: 0; }
}

@keyframes pulse-banner {
    0%, 100% { transform: scale(1.0); }
    50%      { transform: scale(1.04); }
}

@keyframes endgame-fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}

/* "將軍 / CHECK" sidebar badge — only mounts when the side-to-move's
 * general is under attack (xiangqi only) and the user hasn't disabled it. */
.check-badge {
    margin: 0.4rem 0 0.6rem;
    padding: 0.3rem 0.6rem;
    background: rgba(178, 34, 34, 0.18);
    border-left: 3px solid var(--red);
    color: #ffd3d3;
    font-weight: 700;
    border-radius: 0.25rem;
    animation: pulse-banner 1.4s ease-in-out infinite;
}

/* Picker "⚙ Display settings" expandable. Native <details> so collapsed
 * by default, no JS. Lives at the bottom of the picker page. */
.picker-settings {
    margin: 1.5rem auto 0;
    max-width: 640px;
    padding: 0.6rem 0.9rem;
    border: 1px solid var(--grid);
    border-radius: 0.5rem;
    background: rgba(244, 210, 163, 0.04);
}
.picker-settings > summary {
    cursor: pointer;
    user-select: none;
    color: var(--muted);
    font-size: 0.95rem;
}
.picker-settings[open] > summary {
    margin-bottom: 0.5rem;
    color: var(--fg);
}

/* FX toggle row. Used in the picker's display-settings expandable. */
.fx-toggles {
    margin-top: 0.6rem;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    font-size: 0.85rem;
    color: var(--muted);
}
.fx-toggles label {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    cursor: pointer;
}
/* Threat-highlight selector lives inside the same .fx-toggles
 * container as the FX checkboxes; it gets its own row layout so the
 * select-box doesn't squish the bilingual label onto two lines on
 * narrow screens. The dropdown styling matches the captured-sort
 * toggle elsewhere in the sidebar. */
.threat-mode-row {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-wrap: wrap;
    cursor: default;
}
.threat-mode-select {
    background: rgba(0, 0, 0, 0.25);
    color: var(--text);
    border: 1px solid rgba(255, 255, 255, 0.18);
    border-radius: 4px;
    padding: 0.15rem 0.35rem;
    font-size: 0.85rem;
    cursor: pointer;
}
.threat-mode-select:focus {
    outline: 2px solid rgba(255, 140, 26, 0.55);
    outline-offset: 1px;
}

/* Move history panel — sits between action buttons and FX toggles in
 * the sidebar. Scrollable so a long game doesn't push the rest of the
 * sidebar off-screen. ICCS notation is short (4 chars) so a 22ch
 * column fits comfortably with the side glyph + ply number prefix. */
.move-history {
    margin-top: 0.8rem;
    padding-top: 0.6rem;
    border-top: 1px dashed rgba(255, 255, 255, 0.1);
}
.move-history__title {
    font-size: 0.95rem;
    margin: 0 0 0.4rem 0;
    color: var(--muted);
    font-weight: 600;
}
.move-history__empty {
    font-size: 0.85rem;
    margin: 0;
}
.move-history__list {
    list-style: none;
    margin: 0;
    padding: 0;
    max-height: 16rem;
    overflow-y: auto;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    font-size: 0.8rem;
    line-height: 1.5;
}
.move-history__entry {
    display: grid;
    grid-template-columns: 2.4rem 1.4rem 1fr auto;
    gap: 0.3rem;
    padding: 0.05rem 0.2rem;
    border-radius: 3px;
    align-items: baseline;
}
.move-history__entry:hover {
    background: rgba(255, 255, 255, 0.04);
}
.move-history__entry--red .move-history__side {
    color: var(--red);
}
.move-history__entry--black .move-history__side {
    color: var(--muted);
}
.move-history__num {
    color: var(--muted);
    text-align: right;
}
.move-history__side {
    text-align: center;
    font-weight: 600;
}
.move-history__text {
    text-align: left;
    color: var(--text);
}

/* AI debug panel — shown only when ?debug=1 is in the URL. Sits as a
 * third column to the right of the sidebar (or wraps under the board
 * on narrow viewports). The panel itself is scrollable so a 40-move
 * scored list doesn't push the layout. */
.debug-panel {
    border: 1px dashed rgba(245, 166, 35, 0.4);
    border-radius: 0.6rem;
    padding: 0.8rem 0.9rem;
    background: rgba(245, 166, 35, 0.04);
    min-width: 16rem;
    max-width: 22rem;
    color: var(--fg);
    align-self: start;
}
.debug-panel__title {
    margin: 0 0 0.2rem 0;
    font-size: 1rem;
    color: var(--accent);
}
.debug-panel__subtitle {
    margin: 0 0 0.6rem 0;
    font-size: 0.78rem;
    color: var(--muted);
    font-style: italic;
}
.debug-panel__empty {
    font-size: 0.85rem;
    margin: 0;
}
.debug-panel__meta {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 0.15rem 0.6rem;
    margin: 0 0 0.6rem 0;
    font-size: 0.8rem;
}
.debug-panel__meta > div {
    display: contents;
}
.debug-panel__meta dt {
    color: var(--muted);
}
.debug-panel__meta dd {
    margin: 0;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
}
.debug-panel__table {
    width: 100%;
    border-collapse: collapse;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    font-size: 0.78rem;
    max-height: 26rem;
    display: block;
    overflow-y: auto;
}
.debug-panel__table thead th {
    text-align: left;
    color: var(--muted);
    font-weight: 600;
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
    padding: 0.2rem 0.4rem;
    position: sticky;
    top: 0;
    background: rgba(40, 30, 20, 0.92);
}
.debug-panel__row {
    cursor: pointer;
    transition: background 0.05s ease;
}
.debug-panel__row:hover {
    background: rgba(245, 166, 35, 0.16);
}
.debug-panel__row td {
    padding: 0.18rem 0.4rem;
    border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}
.debug-panel__row--chosen {
    background: rgba(245, 166, 35, 0.1);
    font-weight: 600;
}
.debug-panel__row--good .debug-panel__score {
    color: #6ade8a;
}
.debug-panel__row--blunder .debug-panel__score {
    color: #f06464;
}
.debug-panel__rank {
    color: var(--muted);
    text-align: right;
    width: 2.2rem;
}
.debug-panel__mv {
    text-align: left;
}
.debug-panel__score {
    text-align: right;
    width: 5rem;
}

/* Board overlay highlights for the debug panel: dashed connector +
 * yellow rings on from / to. Pure visual; SVG layer doesn't capture
 * pointer events so click-through still works. */
.board .debug-from {
    fill: none;
    stroke: rgba(245, 166, 35, 0.85);
    stroke-width: 2;
    stroke-dasharray: 3 3;
    pointer-events: none;
}
.board .debug-to {
    fill: rgba(245, 166, 35, 0.18);
    stroke: rgba(245, 166, 35, 0.85);
    stroke-width: 2;
    pointer-events: none;
}
.board .debug-line {
    stroke: rgba(245, 166, 35, 0.7);
    stroke-width: 2.5;
    stroke-dasharray: 6 4;
    pointer-events: none;
}
/* PV continuation chain: thinner + bluish so it visually subordinates
 * to the chosen move. Opacity is set per-element from the search
 * depth so the predicted line fades as it gets deeper. */
.board .debug-pv-from {
    fill: none;
    stroke: rgba(116, 184, 255, 0.7);
    stroke-width: 1.5;
    stroke-dasharray: 2 2;
    pointer-events: none;
}
.board .debug-pv-to {
    fill: rgba(116, 184, 255, 0.14);
    stroke: rgba(116, 184, 255, 0.7);
    stroke-width: 1.5;
    pointer-events: none;
}
.board .debug-pv-line {
    stroke: rgba(116, 184, 255, 0.55);
    stroke-width: 1.5;
    stroke-dasharray: 4 3;
    pointer-events: none;
}

/* PV continuation rendered inline next to each row's main move. */
.debug-panel__mv-main {
    font-weight: 600;
}
.debug-panel__mv-pv {
    color: var(--muted);
    font-size: 0.72rem;
    font-weight: 400;
}

/* Game-page layout already grid-positions board + sidebar. When the
 * debug panel mounts, it becomes a third grid column. The page falls
 * back to 1 column on narrow viewports. */
.game-page:has(.debug-panel) {
    grid-template-columns: minmax(0, 1fr) auto auto;
}

/* Picker hero — a tiny header above the variant grid so the page feels
 * like a landing screen rather than a bare form. SVG icon scales with
 * the heading; tagline stays muted. */
.picker-hero {
    display: flex;
    align-items: center;
    gap: 1rem;
    margin-bottom: 1.5rem;
    padding: 1rem 1.25rem;
    background: linear-gradient(135deg, rgba(212, 165, 92, 0.15), rgba(178, 34, 34, 0.05));
    border: 1px solid rgba(212, 165, 92, 0.25);
    border-radius: 0.6rem;
}
.picker-hero__glyph {
    flex: 0 0 auto;
    width: 64px;
    height: 64px;
    border: 2px solid var(--red);
    border-radius: 50%;
    display: grid;
    place-items: center;
    background: var(--bg);
    color: var(--red);
    font-size: 2.2rem;
    font-weight: 700;
    box-shadow: 0 4px 12px rgba(178, 34, 34, 0.3);
}
.picker-hero__copy h1 {
    margin: 0 0 0.25rem;
    font-size: 1.6rem;
    color: var(--accent);
    letter-spacing: 0.05em;
}
.picker-hero__copy p {
    margin: 0;
    color: var(--muted);
    font-size: 0.9rem;
}
.picker-hero__source {
    flex: 0 0 auto;
    margin-left: auto;
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    color: var(--muted);
    text-decoration: none;
    transition: color 150ms ease, background 150ms ease, transform 150ms ease;
}
.picker-hero__source:hover,
.picker-hero__source:focus-visible {
    color: var(--accent);
    background: rgba(212, 165, 92, 0.18);
    transform: translateY(-1px);
}
.picker-hero__source svg { display: block; }

/* ---- PWA install / update / offline UI -------------------------- */

/* Install banner — shown above the picker hero on devices that can
   install (Chrome/Edge fired beforeinstallprompt, or iOS Safari needs
   the manual instruction variant). */
.pwa-banner {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    margin: 0 0 1.4rem;
    padding: 0.9rem 1rem;
    border-radius: 12px;
    border: 1px solid rgba(212, 165, 92, 0.35);
    background: linear-gradient(135deg, rgba(178, 34, 34, 0.18), rgba(212, 165, 92, 0.10));
    color: var(--fg);
}
.pwa-banner__icon {
    flex: 0 0 auto;
    width: 44px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--tile-back);
    color: var(--red);
    font-weight: 900;
    font-size: 1.6rem;
    font-family: "Noto Serif TC", "Songti SC", serif;
}
.pwa-banner__copy {
    flex: 1 1 auto;
    min-width: 0;
}
.pwa-banner__copy strong {
    display: block;
    color: var(--accent);
    font-size: 1rem;
    margin-bottom: 0.15rem;
}
.pwa-banner__copy p {
    margin: 0;
    color: var(--muted);
    font-size: 0.88rem;
    line-height: 1.4;
}
.pwa-banner__share {
    display: inline-block;
    padding: 0 0.3rem;
    border: 1px solid var(--accent);
    border-radius: 4px;
    color: var(--accent);
    font-size: 0.85rem;
}
.pwa-banner__actions {
    display: flex;
    align-items: center;
    gap: 0.4rem;
}
.pwa-banner__dismiss {
    border: none;
    background: transparent;
    color: var(--muted);
    cursor: pointer;
    font-size: 1rem;
    padding: 0.25rem 0.5rem;
}
.pwa-banner__dismiss:hover { color: var(--fg); }
.pwa-banner--ios { background: linear-gradient(135deg, rgba(34, 139, 34, 0.18), rgba(212, 165, 92, 0.10)); }

/* Sidebar install button — match other .btn styles, just with a
   primary tint when the prompt is hot. */
.pwa-install-btn {
    margin-top: 0.4rem;
    border-color: rgba(178, 34, 34, 0.55);
    color: var(--fg);
    background: rgba(178, 34, 34, 0.18);
}
.pwa-install-btn:hover {
    background: rgba(178, 34, 34, 0.32);
    border-color: var(--red);
}

/* Update toast — fixed bottom-centre. The whole strip is clickable
   for a fat-finger-friendly hit target on mobile. */
.pwa-toast {
    position: fixed;
    left: 50%;
    bottom: max(1rem, env(safe-area-inset-bottom, 1rem));
    transform: translateX(-50%);
    z-index: 1000;
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.6rem 0.8rem 0.6rem 1rem;
    border-radius: 999px;
    background: rgba(26, 22, 20, 0.96);
    border: 1px solid var(--accent);
    color: var(--fg);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
    font-size: 0.9rem;
    max-width: calc(100vw - 2rem);
}
.pwa-toast__msg { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pwa-toast__btn {
    border: 1px solid var(--accent);
    background: var(--accent);
    color: #1a1614;
    border-radius: 999px;
    padding: 0.3rem 0.8rem;
    font-weight: 600;
    cursor: pointer;
}
.pwa-toast__btn:hover { background: #e7bd72; }
.pwa-toast__close {
    border: none;
    background: transparent;
    color: var(--muted);
    cursor: pointer;
    font-size: 0.95rem;
    padding: 0.25rem 0.4rem;
}
.pwa-toast__close:hover { color: var(--fg); }

/* Online / offline indicator — tucked into a corner of the shell so
   it's visible from any route but not in the way. The label is hidden
   when online (a green pip is enough); offline expands to text. */
.pwa-online-dot {
    position: fixed;
    right: 0.8rem;
    bottom: 0.8rem;
    z-index: 900;
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.25rem 0.55rem;
    border-radius: 999px;
    font-size: 0.78rem;
    color: var(--muted);
    background: rgba(26, 22, 20, 0.7);
    border: 1px solid rgba(122, 108, 90, 0.35);
    pointer-events: auto;
}
.pwa-online-dot__pip {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--green);
    box-shadow: 0 0 0 3px rgba(34, 139, 34, 0.18);
}
.pwa-online-dot--off .pwa-online-dot__pip {
    background: var(--red);
    box-shadow: 0 0 0 3px rgba(178, 34, 34, 0.22);
}
.pwa-online-dot--on .pwa-online-dot__label { display: none; }
.pwa-online-dot--off { color: var(--fg); border-color: rgba(178, 34, 34, 0.5); }

/* LAN-multiplayer indicator states (Phase 6). The label always
 * shows so the user knows which page state they're in (otherwise
 * the dot is indistinguishable from "Online"). */
.pwa-online-dot--lan { color: var(--fg); border-color: rgba(96, 165, 250, 0.55); }
.pwa-online-dot--lan .pwa-online-dot__pip {
    background: #60a5fa;
    box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.22);
}
.pwa-online-dot--lan-waiting { color: var(--fg); border-color: rgba(217, 158, 62, 0.55); }
.pwa-online-dot--lan-waiting .pwa-online-dot__pip {
    background: #d99e3e;
    box-shadow: 0 0 0 3px rgba(217, 158, 62, 0.22);
    animation: pwa-lan-pulse 1.6s ease-in-out infinite;
}
.pwa-online-dot--lan-dropped { color: var(--fg); border-color: rgba(178, 34, 34, 0.55); }
.pwa-online-dot--lan-dropped .pwa-online-dot__pip {
    background: var(--red);
    box-shadow: 0 0 0 3px rgba(178, 34, 34, 0.22);
}
@keyframes pwa-lan-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.45; }
}
@media (prefers-reduced-motion: reduce) {
    .pwa-online-dot--lan-waiting .pwa-online-dot__pip { animation: none; }
}

@media (max-width: 480px) {
    .pwa-banner { flex-direction: column; align-items: stretch; text-align: left; }
    .pwa-banner__actions { justify-content: flex-start; }
}

/* ============================================================
 * AI win-rate display (?evalbar=1) — two pieces:
 *   1. `.eval-badge`   — sidebar `紅 % • 黑 %` pill (live during play)
 *   2. `.eval-chart`   — SVG line chart (live during play, also visible
 *                        post-game when the trend is fully populated)
 * Plus per-row `.move-history__delta` annotations on history rows.
 *
 * History: an earlier revision had a third piece — a vertical SVG eval
 * bar attached to the right edge of the board. It was removed
 * 2026-05-10 because (a) it duplicated the sidebar badge's information
 * and (b) the layout-glue required to align its height with the board
 * SVG was fragile across viewports (broke click hit-testing on iPad
 * landscape). Sidebar badge is now the single live-display surface.
 * ============================================================ */

/* --- Sidebar `紅 % • 黑 %` badge --- */

.eval-badge {
    margin: 0.2rem 0 0.5rem;
    padding: 0.25rem 0.4rem 0.35rem;
    border: 1px solid var(--grid);
    border-radius: 0.4rem;
    background: rgba(244, 210, 163, 0.04);
}
.eval-badge__row {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 0.45rem;
    font-size: 0.85rem;
    margin-bottom: 0.2rem;
}
.eval-badge__side {
    font-variant-numeric: tabular-nums;
}
.eval-badge__side--red {
    color: #f87171;
    font-weight: 600;
}
.eval-badge__side--black {
    color: #d8d4cc;
    font-weight: 600;
}
.eval-badge__sep {
    color: var(--muted);
    font-size: 0.75rem;
}
.eval-badge__bar {
    display: flex;
    height: 6px;
    width: 100%;
    border-radius: 3px;
    overflow: hidden;
    background: #2b2624;
}
.eval-badge__fill {
    height: 100%;
    transition: width 0.4s ease;
}
.eval-badge__fill--red {
    background: linear-gradient(90deg, #d05a3a, #b86b3d);
}
.eval-badge__fill--black {
    background: linear-gradient(90deg, #4a4540, #2b2624);
}

/* --- End-of-game (or live) trend chart --- */

.eval-chart {
    border: 1px solid var(--grid);
    border-radius: 0.6rem;
    padding: 0.6rem 0.7rem 0.5rem;
    background: rgba(244, 210, 163, 0.04);
    min-width: 16rem;
    max-width: 22rem;
    align-self: start;
}
.eval-chart__title {
    margin: 0 0 0.4rem 0;
    font-size: 0.92rem;
    color: var(--accent);
}
.eval-chart__svg {
    display: block;
    width: 100%;
    height: auto;
    max-height: 160px;
    background: transparent;
}
.eval-chart__bg-black {
    fill: rgba(43, 38, 36, 0.7);
}
.eval-chart__bg-red {
    fill: rgba(184, 107, 61, 0.18);
}
.eval-chart__gridline {
    stroke: var(--accent);
    stroke-width: 0.4;
    stroke-dasharray: 2 2;
    opacity: 0.5;
}
.eval-chart__axis-label {
    fill: var(--muted);
    font-size: 0.42rem; /* small relative to viewBox 260×140 */
    font-family: ui-monospace, SFMono-Regular, monospace;
}
.eval-chart__axis-label--ply {
    font-size: 0.5rem;
}
.eval-chart__area {
    fill: rgba(245, 166, 35, 0.16);
    stroke: none;
}
.eval-chart__line {
    fill: none;
    stroke: var(--accent);
    stroke-width: 1.2;
    stroke-linejoin: round;
    stroke-linecap: round;
}
.eval-chart__dot {
    fill: var(--accent);
    opacity: 0.6;
}
.eval-chart__dot:hover {
    opacity: 1;
    r: 3;
}
.eval-chart__summary {
    margin: 0.3rem 0 0;
    font-size: 0.78rem;
    text-align: center;
}

/* --- Per-row delta annotation in the move history list --- */

.move-history__delta {
    padding-left: 0.3rem;
    font-size: 0.72rem;
    font-variant-numeric: tabular-nums;
    text-align: right;
    white-space: nowrap;
}
.move-history__delta--up {
    color: #65bf65; /* gentle green */
}
.move-history__delta--down {
    color: #d77a6a; /* muted red */
}
.move-history__delta--flat {
    color: var(--muted);
}

/* The history row uses CSS grid (4 columns: number, side, text, delta).
 * The 4th column is `auto`-sized so when a row has no delta annotation
 * (typically the very first move before the initial-position sample
 * lands), the column shrinks to 0 and the layout looks identical to
 * the pre-evalbar version. */
