/*
 * play-coach.css — layout + interaction styling for the /play page (issue #397).
 *
 * The board itself is styled entirely by chesswick-diagram.css (the shared
 * .chesswick-board* classes and --cw-* variables); this file only adds the page
 * shell, the coach strip, the board sizing, the toolbar + settings menu, the move
 * list, the promotion picker, the move-category badges, and the playable-board
 * cursor/drag affordances that CVN diagrams do not need. No inline styles — CSP
 * forbids them.
 *
 * Layout goal (all viewports): a compact, app-like, single column — Coach Chesswick's
 * avatar + live feedback ABOVE the board, the board sized to fit the viewport, and a
 * single row of primary controls below — so the core play loop needs no scrolling.
 * Session-local settings (coach persona, arrow hints, flip), the move list, and the
 * subtle "new game" action live behind the ⚙ settings menu.
 */

/* ── Page shell: a full-height flex column so the footer sits at the bottom and the
   play surface fits the viewport without scrolling. ─────────────────────────────── */
.play-shell {
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    /* Home-indicator / gesture-bar inset on notched phones (#542). Declared once here so every
       bottom-anchored surface on the play page — the column's own padding, the sticky toolbar,
       the bottom-sheet menus, and the decision action bar — can keep clear of it in lockstep. */
    --play-safe-bottom: env(safe-area-inset-bottom, 0px);
}

.profile-shell--board {
    flex: 0 0 auto;
}

/* The play column is a top-aligned flex stack: feedback card → board (plates + board) → toolbar. It is
   a `container-type: size` query container, so `100cqh` below resolves to the column's OWN height — the
   space left between the header and footer, which is STABLE and does NOT change with the coach note's
   length. The board is sized from that stable height (not from the live remaining flex space), and the
   feedback region has a fixed MINIMUM height plus flex-grow: it absorbs whatever slack the (often
   width-bound) board leaves in the column, and its note scrolls internally beyond that — so the note
   area is as large as the viewport allows while the board still never shifts as feedback streams.
   The board group therefore sits at the bottom of the column, directly above the toolbar. */
.play-layout {
    flex: 1 1 auto;
    min-height: 0;
    container-type: size;


    /* ── The column's vertical budget, DERIVED from its real parts (#542) ───────────────
       Everything in the column except the board host. Each part below is also the value the
       rule it names actually uses (see .play-coach-panel, .play-player, .play-board,
       .play-toolbar and the `gap`s here), so the reserve can never drift away from the layout
       it is meant to describe — the previous hand-tuned 19rem constant under-counted the eval
       gutter and any plate that grew past one row, and the toolbar paid for it by sliding
       below the fold. */
    --play-feedback-height: 10.5rem;   /* .play-coach-panel height (fixed region) */
    --play-plate-height: 1.5rem;       /* .play-player min-height, one above + one below */
    --play-stage-gap: 0.3rem;          /* .play-board gap: plate·board·plate */
    --play-toolbar-height: 2.75rem;    /* .play-toolbar min-height (= a 44px touch target) */
    --play-column-gap: 0.55rem;        /* this column's gap: card·stage·toolbar */
    --play-pane-gap: 0.6rem;           /* .play-pane--board gap (two-column layouts only) */
    /* The eval bar's gutter, reserved by .play-board__host. --no-eval zeroes both below, and
       because they are named here the reserve and the padding move together. */
    --play-board-gutter: 1.5rem;
    --play-board-gutter-top: 1.3rem;
    /* How much of the reserve the feedback card accounts for, and which gaps sit between the
       column's items. Both are overridden by the two-column layouts, where the card moves out
       of the board's column entirely. */
    --play-reserve-feedback: var(--play-feedback-height);
    --play-reserve-gaps: calc(2 * var(--play-column-gap));
    /* Deliberate slack on top of the measured parts. Zero in the stacked layout — every pixel there
       belongs to the board — but the two-column layouts keep some so the centred [board · toolbar]
       group is not flush against the grid's edges. */
    --play-board-breathing: 0px;
    /* The smallest board we will ever render. Below this the board stops shrinking (a 5rem
       board is not a game) and the sticky toolbar — not the sizing math — is what keeps the
       controls reachable. */
    --play-board-min: 9rem;

    --play-board-reserve: calc(
        var(--play-reserve-feedback) +
        2 * var(--play-plate-height) +
        2 * var(--play-stage-gap) +
        var(--play-toolbar-height) +
        var(--play-reserve-gaps) +
        var(--play-board-gutter-top) +
        var(--play-board-breathing));

    /* The board host's width — the square's side PLUS the left eval gutter (border-box). Sized
       from BOTH axes so the column mathematically fits the viewport: the width the column offers
       (100cqw) and the height left over after the reserve, converted back into a host width by
       adding the gutter the host itself contributes. Shrinking the board is always preferable to
       pushing the toolbar or a decision off-screen, so the height branch wins whenever it is the
       tighter of the two. Floored at --play-board-min so a very short viewport shrinks toward a
       playable size rather than toward zero. */
    --play-board-size: max(
        var(--play-board-min),
        min(100cqw, calc(100cqh - var(--play-board-reserve) + var(--play-board-gutter))));

    /* The width the board host (and its player plates) actually renders at: the size above, capped by
       the column. One shared variable so the host and the plates always agree — and so reading mode
       (below) can shrink both in lockstep by overriding just this one value. */
    --play-board-width: min(100%, var(--play-board-size, 100%));

    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--play-column-gap);
    width: 100%;
    max-width: 36rem;
    margin: 0 auto;
    /* Wide enough for the footer to still be showing, so the home-indicator inset is the footer's
       to carry (see .play-shell .app-site-footer); on phones the footer is dropped and the mobile
       block below moves the inset onto this padding instead. */
    padding: 0.5rem 0.9rem 0.35rem;
}

/* Evaluation bar hidden (the default): reclaim its gutter. Zeroing the two gutter variables
   drops the host's padding, pulls the plate names flush to the board edge, AND takes the top
   gutter back out of the board reserve — one switch, three call sites, no drift. */
.play-layout--no-eval {
    --play-board-gutter: 0px;
    --play-board-gutter-top: 0px;
}

/* ── Game-status line (its own slim row in the feedback card) ──────────────────────
   "whose move / thinking… / check / result", sitting just under the avatar+name head so it never
   competes with the severity badge for room. */
.play-status {
    margin: 0.2rem 0 0;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--color-text-muted, #7A6E5F);
    min-height: 1.1em;
}

.play-status--busy { font-style: italic; }

/* When there's nothing to say (e.g. a mistake/blunder pause, where the note + decision buttons speak
   for themselves), collapse the status row so it doesn't reserve a blank line above the note. */
.play-status:empty { display: none; }

/* ── Chesswick's asides ────────────────────────────────────────────────────────────
   His personality lines, in their own slot between the status and the opening caption. Set as speech —
   italic, a quiet quotation rule down the left — so it reads as the coach remarking rather than as
   another piece of game state. Empty almost all the time, and it collapses completely when it is, so
   it costs the card no height except in the few seconds one is actually showing. */
.play-aside {
    margin: 0.3rem 0 0;
    padding-left: 0.5rem;
    border-left: 2px solid var(--color-border, #E2D9CC);
    font-size: 0.82rem;
    font-style: italic;
    line-height: 1.35;
    color: var(--color-text-muted, #7A6E5F);
}

.play-aside:empty { display: none; }

/* The line fades in rather than snapping: it arrives while the student is still looking at the move
   they just played, and a hard swap in the corner of the eye reads as an error message. */
@media (prefers-reduced-motion: no-preference) {
    .play-aside { animation: play-aside-in 220ms ease-out both; }
}

@keyframes play-aside-in {
    from { opacity: 0; transform: translateY(-2px); }
    to { opacity: 1; transform: none; }
}

/* ── Opening caption (#579) ────────────────────────────────────────────────────────
   The opening the game is in, under the status line: quieter and smaller than the status itself, so it
   reads as a label on the game rather than as another thing the coach is saying. The same block is
   reused in the post-game summary's headline area. */
.play-opening {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.3rem;
    margin: 0.15rem 0 0;
    font-size: 0.74rem;
    color: var(--color-text-muted, #7A6E5F);
}

.play-opening[hidden] { display: none; }

/* "Opening:" — present for the reading, dimmed so the name is what the eye lands on. */
.play-opening__label { opacity: 0.75; }

.play-opening__name {
    font-weight: 600;
    color: var(--color-text, #2a2a2a);
}

/* Linked only when the name resolves to a published term/article; an unresolved opening renders as a
   <span> with no affordance at all, so nothing suggests a page that isn't there. Wears the dotted
   cross-reference underline .dict-xref/.article-xref use, because that is exactly what it is. */
a.play-opening__name {
    color: var(--color-teal, #1B6B77);
    text-decoration: underline;
    text-decoration-style: dotted;
    text-underline-offset: 0.15em;
}

a.play-opening__name:hover,
a.play-opening__name:focus-visible { text-decoration-style: solid; }

/* The ECO code — a study handle, so it stays legible but never competes with the name. */
.play-opening__eco {
    font-size: 0.68rem;
    letter-spacing: 0.04em;
    opacity: 0.75;
}

.play-opening--summary { margin-top: 0.25rem; }

/* ── Coach note (streamed from /play/coach) + hint (two-stage /play/hint) ──────────
   Both live inside the coach feedback card — the hint sits just below the note —
   and share a quiet card look. */
.play-coach-note,
.play-hint {
    font-size: 0.86rem;
    line-height: 1.5;
    color: var(--color-text, #2a2a2a);
    margin: 0;
}

.play-coach-note > :first-child,
.play-hint > :first-child { margin-top: 0; }
.play-coach-note > :last-child,
.play-hint > :last-child { margin-bottom: 0; }
.play-coach-note p,
.play-hint p { margin: 0 0 0.4rem; }

.play-coach-note__placeholder {
    color: var(--color-text-muted, #7A6E5F);
    font-style: italic;
}

/* Space the note (and the streaming/typing indicator) from the status line above it, but only when
   something is actually showing — an idle empty note adds no gap. (The whole card scrolls within the
   fixed feedback region, so the note itself needs no separate max-height/scroll.) */
.play-coach-note[data-empty="false"],
.play-coach-note--busy {
    margin-top: 0.45rem;
}

/* The hint sits just below the coach note inside the panel card. paintCoachPanel() owns when it
   shows (live position only, never under a reviewed move's note) by toggling .play-hint--shown,
   so display is JS-driven rather than keyed off [data-empty] alone. */
.play-hint { display: none; }

/* The on-demand hint reads as a distinct, intentional callout (a soft green "help" block with a
   labelled header), rather than a dashed afterthought tacked under the note. */
.play-hint--shown {
    display: block;
    margin-top: 0.55rem;
    padding: 0.45rem 0.6rem;
    border-radius: 9px;
    background: var(--cw-good-soft, rgba(47, 138, 78, 0.1));
    border: 1px solid var(--cw-good-soft, rgba(47, 138, 78, 0.2));
}

.play-hint--shown::before {
    content: '💡 Hint';
    display: block;
    margin-bottom: 0.2rem;
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-accent-strong, #2f7d4f);
}

/* When the hint is the only thing showing (no note above it), it needs no extra top gap. */
.play-coach-note[data-empty="true"] + .play-hint--shown {
    margin-top: 0;
}

.play-hint__placeholder {
    color: var(--color-text-muted, #7A6E5F);
    font-style: italic;
}

/* ── Post-game summary card (#546) ────────────────────────────────────────────────
   Shown inside the coach feedback card the moment a game finishes: the result, the honest counts,
   the two or three key moments (each row jumps the review cursor to that ply), and Chesswick's
   streamed closing lesson above the two ways onward. It reads as a distinct panel within the card —
   a boxed-off "the game is over, here is what happened" — rather than as another note. */
.play-summary {
    margin-top: 0.55rem;
    padding: 0.6rem 0.7rem;
    border: 1px solid var(--color-border, #e3dacc);
    border-radius: 10px;
    /* The coach card itself is --color-bg, so the summary sits on --color-surface to read as an inset
       panel within it — "the game is over, here is what happened" — in both themes. */
    background: var(--color-surface, #fff);
}

.play-summary[hidden] { display: none; }

.play-summary__headline {
    margin: 0;
    font-size: 0.95rem;
    font-weight: 700;
    color: var(--color-text, #2a2a2a);
}

/* The strength the game was played at (#550): a quiet caption under the result, not a headline of
   its own — it qualifies the result rather than competing with it. */
.play-summary__level {
    margin: 0.15rem 0 0;
    font-size: 0.74rem;
    color: var(--color-text-muted, #7A6E5F);
}

.play-summary__level[hidden] { display: none; }

/* The counts, on one wrapping row. Each tile carries its own severity tint via --cw-c (set by the
   chesswick-color-* class play-coach.js adds), and a count of zero stays neutral — nothing to flag. */
.play-summary__stats {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin: 0.5rem 0 0;
    padding: 0;
    list-style: none;
}

.play-summary__stat {
    display: flex;
    flex-direction: column;
    align-items: center;
    min-width: 3.6rem;
    padding: 0.25rem 0.45rem;
    border: 1px solid var(--cw-c, var(--color-border, #e3dacc));
    border-radius: 8px;
    background: var(--cw-cs, transparent);
}

.play-summary__stat-value {
    font-size: 1rem;
    font-weight: 700;
    line-height: 1.1;
    color: var(--color-text, #2a2a2a);
}

.play-summary__stat-label {
    font-size: 0.62rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-text-muted, #7A6E5F);
}

/* The reward for a game the engine never faulted — shown instead of an empty "key moments" list. */
.play-summary__clean {
    margin: 0.5rem 0 0;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--color-accent-strong, #2f7d4f);
}

.play-summary__clean[hidden],
.play-summary__moments[hidden] { display: none; }

.play-summary__moments { margin-top: 0.55rem; }

.play-summary__subhead {
    margin: 0 0 0.3rem;
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-text-muted, #7A6E5F);
}

.play-summary__moment-list {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}

/* One key moment: the move, its severity badge/label, and what the engine would have played. The whole
   row is the button — tapping it reviews that position, so it needs a comfortable touch target. */
.play-summary__moment {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0.35rem;
    width: 100%;
    min-height: 40px;
    padding: 0.3rem 0.45rem;
    border: 1px solid var(--cw-c, var(--color-border, #e3dacc));
    border-left-width: 3px;
    border-radius: 8px;
    background: var(--color-bg, #faf7f2);
    font: inherit;
    font-size: 0.8rem;
    text-align: left;
    color: var(--color-text, #2a2a2a);
    cursor: pointer;
}

/* The row for the position currently on the board, so the card tracks the review cursor. */
.play-summary__moment--current {
    background: var(--cw-cs, var(--color-surface, #fff));
}

.play-summary__moment-move {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-weight: 600;
}

.play-summary__moment-badge {
    font-weight: 700;
    color: var(--cw-c, var(--color-text-muted, #7A6E5F));
}

.play-summary__moment-label {
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--cw-c, var(--color-text-muted, #7A6E5F));
}

.play-summary__moment-best {
    margin-left: auto;
    font-size: 0.72rem;
    color: var(--color-text-muted, #7A6E5F);
}

/* Chesswick's closing lesson (streamed). Same prose treatment as a coach note, with the same
   three-dot "still composing" cue while it is on its way — and simply absent if it never arrives. */
.play-summary__lesson {
    font-size: 0.86rem;
    line-height: 1.5;
    color: var(--color-text, #2a2a2a);
}

.play-summary__lesson > :first-child { margin-top: 0; }
.play-summary__lesson > :last-child { margin-bottom: 0; }
.play-summary__lesson p { margin: 0 0 0.4rem; }

.play-summary__lesson[data-empty="false"],
.play-summary__lesson--busy { margin-top: 0.55rem; }

.play-summary__lesson--busy::after {
    content: '';
    display: inline-block;
    width: 1.5rem;
    height: 0.4rem;
    margin-top: 0.35rem;
    vertical-align: middle;
    background-image:
        radial-gradient(circle closest-side, var(--color-text-muted, #7A6E5F) 90%, transparent),
        radial-gradient(circle closest-side, var(--color-text-muted, #7A6E5F) 90%, transparent),
        radial-gradient(circle closest-side, var(--color-text-muted, #7A6E5F) 90%, transparent);
    background-size: 0.4rem 0.4rem;
    background-position: 0 0, 0.55rem 0, 1.1rem 0;
    background-repeat: no-repeat;
    animation: play-coach-typing 1s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
    .play-summary__lesson--busy::after { animation: none; }
}

/* The two ways onward: the deep chat review of this game, and the rematch. */
.play-summary__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-top: 0.6rem;
}

.play-summary__action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 40px;
    padding: 0 0.8rem;
    border: 1px solid var(--color-accent-strong, #2f7d4f);
    border-radius: 9px;
    background: var(--color-bg, #faf7f2);
    font: inherit;
    font-size: 0.8rem;
    font-weight: 700;
    color: var(--color-accent-strong, #2f7d4f);
    cursor: pointer;
}

.play-summary__action[hidden] { display: none; }

.play-summary__action--primary {
    background: var(--color-accent-strong, #2f7d4f);
    color: #fff;
}

/* Copy PGN / copy FEN (#578). Pressing one swaps its label for a short "Copied ✓" beat, so the control
   reserves enough width for either label — otherwise the summary's action row and the ⚙ toggle row
   reflow under the player's finger the instant they tap. */
.play-copy {
    min-width: 8.5rem;
    white-space: nowrap;
}

.play-hint__reveal {
    font-weight: 600;
    color: var(--color-accent-strong, #2f7d4f);
}

/* Contextual resign offer (#545) — a quiet aside under the note, not a callout competing with the
   hint block: Chesswick is observing that resigning would be reasonable, not urging it. Shown only
   when play-coach.js unhides it; [hidden] alone would be overridden by a display rule here. */
.play-resign-offer {
    margin: 0.55rem 0 0;
    padding-top: 0.45rem;
    border-top: 1px dashed var(--cw-board-border, #d6c9b4);
    font-size: 0.82rem;
    line-height: 1.5;
    font-style: italic;
    color: var(--color-text-muted, #7A6E5F);
}

.play-resign-offer[hidden] { display: none; }

/* ── Adaptive bot strength (#550) — Chesswick's offer to change the level ───────────
   Sits with the note, sharing the quick-reply chips' furniture (#543): a line in his voice and two
   one-tap answers. It is deliberately a *card within the card* rather than another paragraph — the
   only thing in the coach panel asking for a decision about the next game rather than this move. */
.play-level-offer {
    margin: 0.55rem 0 0;
    padding: 0.5rem 0.6rem;
    border: 1px dashed var(--cw-board-border, #d6c9b4);
    border-radius: 10px;
    background: var(--color-surface, #fffdf8);
}

.play-level-offer[hidden] { display: none; }

.play-level-offer__text {
    margin: 0;
    font-size: 0.82rem;
    line-height: 1.5;
    color: var(--color-text, #2a2a2a);
}

.play-level-offer__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    margin-top: 0.45rem;
}

.play-level-offer__actions[hidden] { display: none; }

/* Same pill as the ask chips: this is an answer to something Chesswick said, and it should read as
   the same kind of gesture. The accept side is the only one tinted — declining is the safe default
   and must never look like the discouraged choice. */
.play-level-offer__action {
    padding: 0.28rem 0.6rem;
    border: 1px solid var(--cw-board-border, #d6c9b4);
    border-radius: 999px;
    background: var(--color-surface, #fffdf8);
    font: inherit;
    font-size: 0.76rem;
    font-weight: 600;
    line-height: 1.3;
    color: var(--color-text, #2a2a2a);
    cursor: pointer;
}

.play-level-offer__action:hover { border-color: var(--color-accent-strong, #2f7d4f); }

.play-level-offer__action--accept {
    border-color: var(--color-accent-strong, #2f7d4f);
    background: var(--color-accent-strong, #2f7d4f);
    color: #fff;
}

/* ── "Ask Chesswick" (#543) — the student's half of the conversation ───────────────
   A Q&A thread for the position on the board, then (at the live position only) the quick-reply chips
   and the question box. Sits at the foot of the coach card, below the note/hint, so the reading order
   is: what Chesswick said → what you asked → what he answered → ask him something else. */
.play-ask-thread { margin: 0; }

.play-ask-thread[data-empty="false"] {
    margin-top: 0.55rem;
    padding-top: 0.5rem;
    border-top: 1px dashed var(--cw-board-border, #d6c9b4);
}

.play-ask-entry + .play-ask-entry { margin-top: 0.55rem; }

/* The student's own words, set apart from Chesswick's prose: quieter, tighter, and marked with a
   leading rule so a glance at the card tells you who is speaking without a name label per line. */
.play-ask-entry__q {
    margin: 0 0 0.25rem;
    padding-left: 0.5rem;
    border-left: 2px solid var(--cw-board-border, #d6c9b4);
    font-size: 0.8rem;
    font-weight: 600;
    line-height: 1.4;
    color: var(--color-text-muted, #7A6E5F);
}

.play-ask-entry__a {
    font-size: 0.86rem;
    line-height: 1.5;
    color: var(--color-text, #2a2a2a);
}

.play-ask-entry__a > :first-child { margin-top: 0; }
.play-ask-entry__a > :last-child { margin-bottom: 0; }
.play-ask-entry__a p { margin: 0 0 0.4rem; }

/* Same three-dot "composing" cue the note uses, so a streaming answer reads identically to a
   streaming note rather than inventing a second waiting idiom. */
.play-ask-entry__a--busy::after {
    content: '';
    display: inline-block;
    width: 1.5rem;
    height: 0.4rem;
    margin-top: 0.35rem;
    vertical-align: middle;
    background-image:
        radial-gradient(circle closest-side, var(--color-text-muted, #7A6E5F) 90%, transparent),
        radial-gradient(circle closest-side, var(--color-text-muted, #7A6E5F) 90%, transparent),
        radial-gradient(circle closest-side, var(--color-text-muted, #7A6E5F) 90%, transparent);
    background-size: 0.4rem 0.4rem;
    background-position: 0 0, 0.55rem 0, 1.1rem 0;
    background-repeat: no-repeat;
    animation: play-coach-typing 1s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
    .play-ask-entry__a--busy::after { animation: none; }
}

.play-ask-thread__error {
    margin: 0.4rem 0 0;
    font-size: 0.8rem;
    font-style: italic;
    color: var(--color-text-muted, #7A6E5F);
}

.play-ask { margin-top: 0.55rem; }
.play-ask[hidden] { display: none; }

/* Chips wrap rather than scroll: a horizontally-scrolling row hides options on exactly the phone
   viewport this feature is aimed at. */
.play-ask__chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
}

.play-ask__chips:empty { display: none; }

.play-ask__chip {
    padding: 0.28rem 0.6rem;
    border: 1px solid var(--cw-board-border, #d6c9b4);
    border-radius: 999px;
    background: var(--color-surface, #fffdf8);
    font: inherit;
    font-size: 0.76rem;
    font-weight: 600;
    line-height: 1.3;
    color: var(--color-text, #2a2a2a);
    cursor: pointer;
}

.play-ask__chip:hover:not(:disabled) { border-color: var(--color-accent-strong, #2f7d4f); }
.play-ask__chip:disabled { opacity: 0.5; cursor: default; }

.play-ask__row {
    display: flex;
    gap: 0.35rem;
    margin-top: 0.4rem;
}

.play-ask__input {
    flex: 1 1 auto;
    min-width: 0;
    padding: 0.35rem 0.55rem;
    border: 1px solid var(--cw-board-border, #d6c9b4);
    border-radius: 8px;
    background: var(--color-surface, #fffdf8);
    font: inherit;
    /* 16px keeps iOS Safari from zooming the whole board document when the box takes focus. */
    font-size: 16px;
    color: var(--color-text, #2a2a2a);
}

.play-ask__input:disabled { opacity: 0.6; }

.play-ask__send {
    flex: 0 0 auto;
    padding: 0.35rem 0.75rem;
    border: 1px solid var(--color-accent-strong, #2f7d4f);
    border-radius: 8px;
    background: var(--color-accent-strong, #2f7d4f);
    font: inherit;
    font-size: 0.8rem;
    font-weight: 700;
    color: #fff;
    cursor: pointer;
}

.play-ask__send:disabled { opacity: 0.5; cursor: default; }

/* A clean three-dot "typing" indicator while the note streams in — one cue, not a second sentence
   duplicating the "Thinking…" status line. Shown after whatever text has streamed so far, so it reads
   as "still composing". Built from a stepped background gradient so it needs no extra DOM. */
.play-coach-note--busy::after {
    content: '';
    display: inline-block;
    width: 1.5rem;
    height: 0.4rem;
    margin-top: 0.35rem;
    vertical-align: middle;
    /* Three explicit gradient layers (one per dot) rather than relying on a single gradient being
       repeated across the three background-positions — some engines render only the first otherwise. */
    background-image:
        radial-gradient(circle closest-side, var(--color-text-muted, #7A6E5F) 90%, transparent),
        radial-gradient(circle closest-side, var(--color-text-muted, #7A6E5F) 90%, transparent),
        radial-gradient(circle closest-side, var(--color-text-muted, #7A6E5F) 90%, transparent);
    background-size: 0.4rem 0.4rem;
    background-position: 0 0, 0.55rem 0, 1.1rem 0;
    background-repeat: no-repeat;
    animation: play-coach-typing 1s ease-in-out infinite;
}

@keyframes play-coach-typing {
    0%, 100% { opacity: 0.35; }
    50% { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .play-coach-note--busy::after { animation: none; }
}

/* ── Player plates (name + captured material, above & below the board) ─────────────
   Each plate names the player whose pieces sit on that edge of the board and shows the men that
   player has captured plus a "+N" material lead. They are tied to board orientation, so a flip swaps
   them (handled in play-coach.js). Kept to a single compact row so the board still fits the viewport. */
.play-player {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    /* Match the board's computed width (same --play-board-width as .play-board__host) so the plate names
       line up with the board edges whether the board is width- or height-bound — including reading mode. */
    width: var(--play-board-width, 100%);
    /* A plate is exactly one row tall — that is the height --play-board-reserve budgets for it
       (--play-plate-height), so it must not grow. */
    min-height: var(--play-plate-height, 1.5rem);
    max-height: var(--play-plate-height, 1.5rem);
    /* Left padding matches the board host's eval gutter (.play-board__host padding-left) so the name
       sits over the board's left edge rather than the reserved eval column. When the eval bar is
       hidden, .play-layout--no-eval zeroes --play-board-gutter and both indents drop in lockstep. */
    padding: 0 0.15rem 0 var(--play-board-gutter, 1.5rem);
    font-size: 0.85rem;
    font-weight: 700;
    color: var(--color-text, #2a2a2a);
}

.play-player__name {
    flex: 0 0 auto;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 12rem;
}

/* The row of small captured-piece glyphs. Deliberately a single non-wrapping row that clips its
   tail rather than growing a second line: a taller plate would silently eat into the board's
   height budget (--play-plate-height) and push the toolbar off-screen late in a capture-heavy
   game. The "+N" material lead is a sibling, so the load-bearing number always survives. */
.play-player__captured {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    flex-wrap: nowrap;
    overflow: hidden;
    gap: 0;
    min-width: 0;
    min-height: 1.1rem;
}

/* Captured men render as the shared board SVG pieces, scaled down and slightly overlapped like
   Lichess/Chess.com plates. They live outside the grid, so override the board piece's absolute
   positioning (.chesswick-piece is position:absolute; inset:4%) and lay them out inline instead. */
.play-player__piece {
    position: static;
    inset: auto;
    width: 1.15rem;
    height: 1.15rem;
    margin-right: -0.28rem;
    flex: 0 0 auto;
    filter: none;
}

.play-player__adv {
    flex: 0 0 auto;
    font-size: 0.8rem;
    font-weight: 700;
    color: var(--color-text-muted, #7A6E5F);
}

/* ── Board stage ─────────────────────────────────────────────────────────────────
   A natural-height group of [top plate · board · bottom plate] so the plates stay glued to the board
   edges. Its height is driven by the board, which is sized (below) from the column's STABLE height —
   so the stage is a fixed size and the centred play-layout never reflows when the coach note changes.
   flex: 0 0 auto so the variable-height feedback card above can't stretch or shrink it. */
.play-board {
    width: 100%;
    min-width: 0;
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--play-stage-gap, 0.3rem);
}

/* width: the board is square (aspect-ratio 1); cap its width to the smaller of the stage width or the
   stage height minus the room the two plates need, so the whole [plate·board·plate] group fits. Reserve
   the top/left gutter the eval bar + readout float into (mirrors .chesswick-diagram--has-eval) so the
   board never shifts when an eval lands; --no-eval drops the gutter again below. */
.play-board__host {
    position: relative;
    width: var(--play-board-width, 100%);
    padding-top: var(--play-board-gutter-top, 1.3rem);
    padding-left: var(--play-board-gutter, 1.5rem);
}

.play-board__host .chesswick-board__eval {
    width: 0.5rem;
    transform: translateX(-0.95rem);
}

.play-board__host .chesswick-board__eval-readout {
    font-size: 0.66rem;
    transform: translate(-1.75rem, -1.35rem);
}

/* Evaluation bar is hidden by default and revealed from the ⚙ settings (Evaluation bar toggle).
   The --no-eval state lives on .play-layout so the gutter, the eval bar, the player plates AND the
   board's height reserve stay in lockstep — it zeroes --play-board-gutter/--play-board-gutter-top up
   in .play-layout--no-eval, which reclaims the board's left/top gutter, pulls the plate names +
   captured pieces flush to the board edge, and takes the top gutter back out of the reserve. Only
   the bar itself needs hiding here. */
.play-layout--no-eval .chesswick-board__eval,
.play-layout--no-eval .chesswick-board__eval-readout {
    display: none;
}

.play-board__mount {
    position: relative;
}

/* ── Reading mode (single-column layouts only) ─────────────────────────────────────
   When Chesswick has more to say than the feedback card can show, play-coach.js puts
   .play-layout--reading on the layout: the board shrinks (staying centred, fully in view,
   just above the toolbar) and — because the feedback region flex-grows — every freed pixel
   goes straight to the note, so longer messages read without squinting into a tiny scroll
   box. The mode holds until the note clears (the student plays on / takes back / starts a
   new game), so the board never oscillates mid-note.

   The query is the repo's "wherever the STACKED layout is in effect" one (same as the decision
   bar below) — the two two-column layouts (desktop >=900px and the landscape-phone split) put
   the note beside the board in its own fixed-height column, where shrinking the board would
   free no room for it. */
@media (max-width: 899px) and (min-height: 561px), (max-width: 559px) {
    .play-layout--reading {
        /* 74% of the board's NORMAL width, whichever constraint is binding: scaling both terms of the
           base min() keeps a height-bound board (--play-board-size smaller than the column) shrinking
           too — capping at a flat 74% of the column would leave such a board untouched and free no
           height for the note. The 100% fallback keeps the unset case at 74% of the column. */
        --play-board-width: min(74%, calc(var(--play-board-size, 100%) * 0.74));
    }

    /* Let the board glide between its two sizes (the plates ride along via the shared
       width variable). Skipped under prefers-reduced-motion below. */
    .play-board__host,
    .play-player {
        transition: width 220ms ease;
    }
}

@media (prefers-reduced-motion: reduce) {
    .play-board__host,
    .play-player {
        transition: none;
    }
}

/* A playable board: squares are interactive, not zoom-to-enlarge like CVN. */
.play-board__shell .chesswick-board__grid {
    cursor: default;
}

.play-square--grab .chesswick-piece {
    cursor: grab;
}

.chesswick-piece.play-dragging {
    cursor: grabbing;
    pointer-events: none;
    will-change: transform;
}

/* ── Keyboard move entry (issue #548) ────────────────────────────────────────────
   The playable board is an ARIA grid, which needs row elements the 8×8 CSS grid does
   not: `display: contents` dissolves them back out of layout so the squares stay direct
   grid items and every existing square rule is untouched. */
.play-board__shell .chesswick-board__row {
    display: contents;
}

/* The focus cursor. Inset rather than outset so the ring stays inside the board edge on
   the a-file/first-rank squares, and drawn above the piece so it is never half-hidden. */
.play-board__shell .chesswick-square:focus-visible {
    outline: 3px solid var(--color-teal, #1b6b77);
    outline-offset: -3px;
    z-index: 4;
}

/* Programmatic focus during keyboard play should look the same; a pointer press must not
   paint a ring, hence :focus-visible above and nothing for plain :focus. */
@media (forced-colors: active) {
    .play-board__shell .chesswick-square:focus-visible {
        outline: 3px solid Highlight;
    }
}

/* ── Toolbar (below the board) ────────────────────────────────────────────────────
   One row of controls: review back (‹), Undo, review forward (›), the labelled "Get help"
   coaching-assist menu (✦), and the ⚙ settings menu. Back/Undo/Forward share a look so
   navigation reads as a unit; the hint/arrows/eval affordances are grouped behind "Get
   help" (#458); New game is deliberately NOT here (it lives in the settings menu) to keep
   the primary path about playing, not restarting. */
.play-toolbar {
    display: flex;
    align-items: stretch;
    justify-content: center;
    gap: var(--play-toolbar-gap, 0.4rem);
    width: 100%;
    /* One row tall — the height --play-board-reserve budgets for it (--play-toolbar-height). */
    min-height: var(--play-toolbar-height, 2.75rem);
    /* Backstop pin to the column bottom. In practice the feedback region's flex-grow absorbs the
       column's slack, so the board group and this toolbar both sit at the bottom (thumb zone) with
       nothing left for this margin — it only bites if the panel is ever capped. */
    margin-top: auto;
    /* Backstop (#542). The sizing math above makes the column fit, but it cannot know about a user
       font scale, a mid-game change in browser chrome, or a viewport we never measured — and a
       toolbar below the fold has no scroll cue, so the player just sees controls that vanished.
       Sticky keeps the row parked at the bottom of the scrollport in exactly those cases, offset
       by the home-indicator inset. In the normal (fitting) case it is inert: the row is already at
       the bottom of the column, the background matches the shell, and nothing moves. */
    position: sticky;
    bottom: var(--play-safe-bottom, 0px);
    z-index: 30;
    background: var(--color-bg, #fff);
}

/* A soft fade above the toolbar so content scrolling underneath it in the backstop case reads as
   "there is more up there", not as an abrupt cut. Sized to exactly the column gap it lives in, so
   it never tints the plate above it when the column fits. */
.play-toolbar::before {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 100%;
    height: var(--play-column-gap, 0.55rem);
    pointer-events: none;
    background: linear-gradient(to top, var(--color-bg, #fff), transparent);
}

.play-tool {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.35rem;
    background: var(--color-bg-alt, #f2ede4);
    border: 1px solid var(--cw-board-border, #d6c9b4);
    color: var(--color-text, #2a2a2a);
    border-radius: 10px;
    padding: 0.5rem 0.7rem;
    /* 2.75rem == the 44px minimum touch target, named so .play-toolbar and the board's height
       reserve are computed from the same number. */
    min-height: var(--play-toolbar-height, 2.75rem);
    font-size: 0.85rem;
    font-weight: 600;
    cursor: pointer;
    transition: background 140ms ease, border-color 140ms ease, opacity 140ms ease;
}

.play-tool:hover { background: var(--color-bg, #fff); }

.play-tool:focus-visible {
    outline: 2px solid var(--color-teal, #1b6b77);
    outline-offset: 2px;
}

.play-tool:disabled {
    opacity: 0.4;
    cursor: default;
}

.play-tool__glyph { font-size: 1.05rem; line-height: 1; }

/* Review arrows (‹ ›) are the primary way to step through the game, so they take the lion's share of the
   row — they grow to fill whatever Undo/Hint/gear leave behind. */
.play-tool--nav {
    flex: 1 1 auto;
    min-width: 64px;
    padding-left: 0.5rem;
    padding-right: 0.5rem;
}

.play-tool--nav .play-tool__glyph { font-size: 1.5rem; }

/* The compact, square, icon-only secondary actions — Undo and the gear. */
.play-tool--undo,
.play-tool--gear {
    flex: 0 0 auto;
    width: 44px;
    padding-left: 0.5rem;
    padding-right: 0.5rem;
}

.play-tool--undo .play-tool__glyph { font-size: 1.2rem; }

/* "Get help" coaching-assist entry (#458): a labelled (glyph + text) button — wider than the icon-only
   tools so the affordance is discoverable. data-active="true" tints it while any grouped assist (hint
   in progress, arrows, or the eval bar) is engaged, so the umbrella control signals help is on. */
.play-tool--assist {
    flex: 0 0 auto;
    padding-left: 0.85rem;
    padding-right: 0.85rem;
}

.play-tool__label { white-space: nowrap; }

.play-tool--assist[data-active="true"] {
    border-color: var(--color-accent-strong, #2f7d4f);
    color: var(--color-accent-strong, #2f7d4f);
    background: var(--cw-good-soft, rgba(47, 138, 78, 0.14));
}

/* Struggle cue (#544): when a run of moves has each made things worse — or the student has been
   thinking a long time in a lost position — the button pulses ONCE to say the help is here. It never
   opens itself, and play-coach.js fires it at most once a game: a cue that keeps firing is nagging.
   Under reduced motion it holds a static tint for the same beat instead of animating. */
.play-tool--pulse {
    animation: play-assist-pulse 850ms ease-in-out 3;
}

@keyframes play-assist-pulse {
    0%, 100% {
        border-color: var(--cw-board-border, #d6c9b4);
        box-shadow: none;
    }
    50% {
        border-color: var(--color-accent-strong, #2f7d4f);
        box-shadow: 0 0 0 3px var(--cw-good-soft, rgba(47, 138, 78, 0.22));
    }
}

@media (prefers-reduced-motion: reduce) {
    .play-tool--pulse {
        animation: none;
        border-color: var(--color-accent-strong, #2f7d4f);
        box-shadow: 0 0 0 3px var(--cw-good-soft, rgba(47, 138, 78, 0.22));
    }
}

/* ── Settings menu (⚙) ────────────────────────────────────────────────────────────
   A popover anchored under the toolbar holding the session-local controls + move list
   + the subtle new-game action. Constrained height with internal scroll so it never
   reintroduces page scroll. */
/* Floated as a bottom sheet (position: fixed) so opening it never reflows the board
   or reintroduces page scroll; outside-click / Escape close it (wired in play-coach.js).
   It sits ABOVE the toolbar row rather than over it (#542): the ⚙ / "Get help" triggers a
   player just pressed have to stay visible and pressable to toggle the sheet back shut, and
   the home-indicator inset is added on so the sheet clears the gesture bar on notched phones. */
.play-menu {
    position: fixed;
    left: 50%;
    /* The sheet's resting offset above the toolbar, named once so the `bottom` anchor and the
       height ceiling below are derived from the same value and cannot drift apart. */
    --play-sheet-bottom: calc(var(--play-toolbar-height, 2.75rem) + 0.95rem + var(--play-safe-bottom, 0px));
    bottom: var(--play-sheet-bottom);
    transform: translateX(-50%);
    z-index: 40;
    width: calc(100% - 1.2rem);
    max-width: 26rem;
    background: var(--color-bg, #fff);
    border: 1px solid var(--color-border, #e3dacc);
    border-radius: 12px;
    box-shadow: 0 8px 28px rgba(0, 0, 0, 0.22);
    padding: 0.8rem 0.9rem;
    /* The sheet may grow into all the height the viewport actually has above the toolbar (its
       own bottom offset plus a 0.75rem top margin) — the old min(70dvh, 26rem) cap forced
       internal scrolling of the move list / coaching knobs on tall phones while half the screen
       above the sheet sat empty. It still only takes what its content needs (overflow-y: auto). */
    max-height: calc(100dvh - var(--play-sheet-bottom) - 0.75rem);
    overflow-y: auto;
    /* Internal scrolling must not chain into the page behind the sheet (mobile rubber-banding). */
    overscroll-behavior: contain;
}

.play-menu[hidden] { display: none; }

.play-menu__group {
    padding: 0.55rem 0;
    border-bottom: 1px solid var(--color-border, #e3dacc);
}

.play-menu__group:first-child { padding-top: 0.1rem; }

.play-menu__group--foot {
    border-bottom: none;
    padding-bottom: 0.1rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
}

.play-menu__close { flex: 0 0 auto; }

.play-menu__row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.85rem;
    font-weight: 600;
}

.play-menu__label {
    flex: 0 0 auto;
    color: var(--color-text-muted, #7A6E5F);
}

.play-menu__heading {
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--color-text-muted, #7A6E5F);
    margin: 0 0 0.4rem;
}

.play-menu__group--toggles {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}

/* Coaching knobs (#544): four labelled select rows stacked with a little air between them, so the
   group reads as one setting with three dials rather than four unrelated controls. */
.play-menu__group--coaching {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

/* The labels are short and the selects carry the meaning, so give every row the same label column
   width — otherwise "Style"/"Pause"/"Notes"/"Arrows" ragged-edge the selects out of alignment. */
.play-menu__group--coaching .play-menu__label {
    width: 3.6rem;
}

/* ── "Get help" coaching-assist surface (#458) ────────────────────────────────────
   The hint, suggested-move/threat arrows, and eval-bar toggle, grouped as full-width
   rows in the assist popover. Each row is [glyph] [title + one-line description], with a
   trailing checkbox for the eval toggle. Buttons and the label share one look so the
   three read as one coherent "show me more help" system rather than scattered icons. */
.play-assist-menu__group {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.play-assist-item {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    width: 100%;
    text-align: left;
    background: var(--color-bg-alt, #f2ede4);
    border: 1px solid var(--cw-board-border, #d6c9b4);
    color: var(--color-text, #2a2a2a);
    border-radius: 10px;
    padding: 0.55rem 0.7rem;
    min-height: 44px;
    font: inherit;
    cursor: pointer;
    transition: background 140ms ease, border-color 140ms ease, opacity 140ms ease;
}

.play-assist-item:hover { background: var(--color-bg, #fff); }

.play-assist-item:focus-visible {
    outline: 2px solid var(--color-teal, #1b6b77);
    outline-offset: 2px;
}

.play-assist-item:disabled {
    opacity: 0.45;
    cursor: default;
}

.play-assist-item__glyph {
    flex: 0 0 auto;
    width: 1.6rem;
    text-align: center;
    font-size: 1.2rem;
    line-height: 1;
}

.play-assist-item__text {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    min-width: 0;
    flex: 1 1 auto;
}

.play-assist-item__label {
    font-size: 0.9rem;
    font-weight: 600;
}

.play-assist-item__sub {
    font-size: 0.78rem;
    line-height: 1.35;
    color: var(--color-text-muted, #7A6E5F);
}

/* Hint reads as the primary "ask for help" affordance — a soft accent on its glyph. */
.play-assist-item--hint:not(:disabled) .play-assist-item__glyph {
    color: var(--color-accent-strong, #2f7d4f);
}

/* Three-state arrow toggle: off (default), best move (green), best move + threat (red) — tinted with
   the same semantic colours the arrows themselves use, so the row's state matches what's on the board. */
.play-assist-item--arrows[data-state="1"] {
    border-color: var(--cw-good, #2f8a4e);
    background: var(--cw-good-soft, rgba(47, 138, 78, 0.16));
}

.play-assist-item--arrows[data-state="1"] .play-assist-item__glyph { color: var(--cw-good, #2f8a4e); }

.play-assist-item--arrows[data-state="2"] {
    border-color: var(--cw-bad, #b62a2a);
    background: var(--cw-bad-soft, rgba(182, 42, 42, 0.16));
}

.play-assist-item--arrows[data-state="2"] .play-assist-item__glyph { color: var(--cw-bad, #b62a2a); }

.play-assist-item__check {
    flex: 0 0 auto;
    width: 1.1rem;
    height: 1.1rem;
    cursor: pointer;
}

.play-persona__select {
    flex: 1 1 auto;
    min-width: 0;
    background: var(--color-bg-alt, #f2ede4);
    border: 1px solid var(--cw-board-border, #d6c9b4);
    color: var(--color-text, #2a2a2a);
    border-radius: 8px;
    padding: 0.45rem 0.6rem;
    font-size: 0.85rem;
    font-weight: 600;
    cursor: pointer;
    min-height: 44px;
}

.play-persona__select:focus-visible {
    outline: 2px solid var(--color-teal, #1b6b77);
    outline-offset: 2px;
}

.play-persona-blurb {
    font-size: 0.8rem;
    line-height: 1.5;
    font-style: italic;
    color: var(--color-text-muted, #7A6E5F);
    margin: 0.5rem 0 0;
}

.play-btn {
    background: var(--color-bg-alt, #f2ede4);
    border: 1px solid var(--cw-board-border, #d6c9b4);
    color: var(--color-text, #2a2a2a);
    border-radius: 8px;
    padding: 0.5rem 0.85rem;
    font-size: 0.85rem;
    font-weight: 600;
    cursor: pointer;
    min-height: 44px;
    transition: background 140ms ease, border-color 140ms ease;
}

.play-btn:hover { background: var(--color-bg, #fff); }

.play-btn:focus-visible {
    outline: 2px solid var(--color-teal, #1b6b77);
    outline-offset: 2px;
}

/* "Back to chat" exit link in the settings menu — a .play-btn styled <a>, made full-width so it reads
   as the clear way out of the (header-less, full-screen on mobile) play surface. */
.play-menu__leave {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    text-decoration: none;
}

/* New game: deliberately subtle — a quiet text button, not a primary call to action. */
.play-menu__newgame {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    background: transparent;
    border: none;
    color: var(--color-text-muted, #7A6E5F);
    font-size: 0.82rem;
    font-weight: 600;
    cursor: pointer;
    padding: 0.5rem 0.2rem;
    min-height: 44px;
    text-decoration: underline;
    text-underline-offset: 2px;
}

.play-menu__newgame:hover { color: var(--color-text, #2a2a2a); }

.play-menu__newgame:focus-visible {
    outline: 2px solid var(--color-teal, #1b6b77);
    outline-offset: 2px;
    border-radius: 6px;
}

/* Resign: the same quiet text-button weight as New game — a real but deliberately undramatic exit,
   never a primary call to action. Disabled until there is a move to concede. */
.play-menu__resign {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    background: transparent;
    border: none;
    color: var(--color-text-muted, #7A6E5F);
    font-size: 0.82rem;
    font-weight: 600;
    cursor: pointer;
    padding: 0.5rem 0.2rem;
    min-height: 44px;
    text-decoration: underline;
    text-underline-offset: 2px;
}

.play-menu__resign:hover:not(:disabled) { color: var(--cw-bad, #b62a2a); }

.play-menu__resign:disabled {
    opacity: 0.45;
    cursor: default;
    text-decoration: none;
}

.play-menu__resign:focus-visible {
    outline: 2px solid var(--color-teal, #1b6b77);
    outline-offset: 2px;
    border-radius: 6px;
}

/* ── Move list (inside the settings menu) ────────────────────────────────────────── */
.play-movelist {
    list-style: none;
    margin: 0;
    padding: 0;
    max-height: 9rem;
    overflow-y: auto;
    overscroll-behavior: contain;
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.85rem;
}

.play-movelist__row {
    display: grid;
    grid-template-columns: 2.2rem 1fr 1fr;
    gap: 0.4rem;
    padding: 0.12rem 0.2rem;
    border-radius: 4px;
}

.play-movelist__row:nth-child(odd) {
    background: rgba(0, 0, 0, 0.03);
}

.play-movelist__num { color: var(--color-text-muted, #7A6E5F); }

/* SAN cells are buttons: click one to review that move (its feedback shows in the coach card). */
.play-movelist__san {
    appearance: none;
    text-align: left;
    border: 1px solid transparent;
    background: transparent;
    padding: 0 0.2rem;
    border-radius: 4px;
    font: inherit;
    color: var(--color-text, #2a2a2a);
    cursor: pointer;
}

.play-movelist__san:hover { background: var(--color-bg-alt, #f7f3ec); }

/* The current move keeps its teal highlight even on hover (hover would otherwise win on specificity). */
.play-movelist__san--current,
.play-movelist__san--current:hover {
    background: var(--color-teal, #1b6b77);
    color: #fff;
    font-weight: 600;
}

.play-movelist__san:focus-visible {
    outline: 2px solid var(--color-teal, #1b6b77);
    outline-offset: 1px;
}

/* ── Promotion picker ──────────────────────────────────────────────────────────── */
.play-promo-host {
    position: absolute;
    inset: 0;
    display: none;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.32);
    border-radius: 4px;
    z-index: 6;
}

.play-promo-host--open { display: flex; }

.play-promo {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    align-items: center;
    background: var(--color-bg, #fff);
    border: 1px solid var(--cw-board-border, #d6c9b4);
    border-radius: 10px;
    padding: 0.6rem;
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.25);
}

.play-promo__choice {
    width: 52px;
    height: 52px;
    padding: 4px;
    background: var(--color-bg-alt, #f2ede4);
    border: 1px solid var(--cw-board-border, #d6c9b4);
    border-radius: 8px;
    cursor: pointer;
}

.play-promo__choice .chesswick-piece {
    width: 100%;
    height: 100%;
}

.play-promo__choice:hover { background: var(--color-bg, #fff); }

.play-promo__choice:focus-visible,
.play-promo__cancel:focus-visible {
    outline: 2px solid var(--color-teal, #1b6b77);
    outline-offset: 2px;
}

.play-promo__cancel {
    background: transparent;
    border: none;
    color: var(--color-text-muted, #7A6E5F);
    font-size: 0.8rem;
    cursor: pointer;
    padding: 0.3rem 0.5rem;
}

/* ── Move-category badge on the just-moved square ──────────────────────────────────
   The badge's colour comes from the CVN semantic-colour primitive: play-coach.js
   adds a `chesswick-color-{good|interesting|bad|focus}` class (mapped from the move
   category) which sets `--cw-c`, exactly as the CVN `labels` annotation does. */
.play-badge {
    position: absolute;
    top: 1px;
    right: 1px;
    z-index: 3;
    min-width: 1.05rem;
    height: 1.05rem;
    padding: 0 2px;
    border-radius: 4px;
    font-size: 0.62rem;
    font-weight: 700;
    line-height: 1.05rem;
    text-align: center;
    color: #fff;
    background: var(--cw-c, var(--cw-neutral, #2e6f99));
    pointer-events: none;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}

/* The badge must not counter-rotate with the flipped board — keep it upright. */
.chesswick-board--flipped .play-badge {
    transform: rotate(180deg);
}

/* ── Coach annotations (#541) ───────────────────────────────────────────────────────
   The server-computed marks that go with a coaching note (a ring on what is hanging, an arrow along
   the refutation). They are drawn with the ordinary board-core highlight/arrow primitives, so all this
   adds is the one-shot attention cue: the set that has just landed pulses ONCE, then sits still. A
   permanently animated board is noise, and a cue that never stops signalling stops being a cue —
   play-coach.js spends the pulse flag on the first paint, so a re-render never replays it. */
/* Square marks (ring / dot / corners) bounce in. Only `transform` is animated: the fx primitives set their
   own resting opacity (0.85 for dot/corners), and animating opacity to a different value would snap back
   when the one-shot animation ends and fill-mode hands the property back to the base rule. */
.chesswick-square__fx.play-annotation--pulse {
    animation: play-annotation-pulse 620ms ease-out 1;
    transform-origin: center;
}

@keyframes play-annotation-pulse {
    0%   { transform: scale(0.72); }
    55%  { transform: scale(1.1); }
    100% { transform: scale(1); }
}

/* Arrows fade in instead of scaling: an SVG transform resolves against the whole overlay view-box, so a
   scaled arrow would zoom from the middle of the board rather than from itself. The end frame restates the
   arrow primitive's own 0.92 opacity so the pulse lands exactly on the resting value. */
.chesswick-arrow.play-annotation--pulse {
    animation: play-annotation-fade 620ms ease-out 1;
}

@keyframes play-annotation-fade {
    0%   { opacity: 0; }
    100% { opacity: 0.92; }
}

@media (prefers-reduced-motion: reduce) {
    .chesswick-square__fx.play-annotation--pulse,
    .chesswick-arrow.play-annotation--pulse { animation: none; }
}

/* ── Coach feedback card (the single feedback surface) ──────────────────────────────
   One place for everything Chesswick says, merging the old slim status strip and the floating
   note card into a single surface: the game-status line, his per-move note, the two-stage hint,
   and the mistake/blunder decision. It sits at the top of the board stack and wears the soft
   note-card look.

   Sizing: the region has a fixed MINIMUM (--play-feedback-height) and additionally absorbs the
   column's leftover slack via flex-grow. When the board is width-bound (typical portrait phone)
   that slack used to sit as dead space between the board and the toolbar; giving it to the
   feedback region instead makes the note area as large as the viewport allows and pushes the
   board down to sit just above the toolbar (thumb zone). Because the slack is a property of the
   viewport — not of the note's length — the board still never shifts as a note streams: the card
   is bottom-anchored (hugging the board) and scrolls internally when the note outgrows the
   region. Always present — the status line lives in the card head, so the card shows even before
   there's a note. */
.play-coach-panel {
    width: 100%;
    flex: 1 1 auto;
    min-height: var(--play-feedback-height, 10.5rem);
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-end;
    gap: 0.5rem;
}

/* The card surface — avatar + name (+ severity) in the head, the game status on its own line, then
   the streamed note. Tinted on its left edge by the move's severity colour, with a soft elevation.
   flex/min-height/overflow let it shrink and scroll within the fixed region so the decision buttons
   below it stay put and the board never shifts. */
.play-coach-panel__card {
    width: 100%;
    flex: 0 1 auto;
    min-height: 0;
    overflow-y: auto;
    /* The card scrolls internally; don't let that chain into the page on touch devices. */
    overscroll-behavior: contain;
    padding: 0.6rem 0.75rem;
    background: var(--color-bg, #fff);
    border: 1px solid var(--color-border, #e3dacc);
    border-left: 3px solid var(--cw-c, var(--color-border, #e3dacc));
    border-radius: 12px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 6px 16px rgba(0, 0, 0, 0.07);
    font-size: 0.85rem;
    line-height: 1.45;
    color: var(--color-text, #2a2a2a);
    transition: border-color 200ms ease;
}

/* ── Stacked layouts: the card owns the whole feedback region ──────────────────────
   The region absorbs the column's slack (see .play-coach-panel above), but the card used to
   bottom-anchor inside it, so on a tall phone every spare pixel rendered as dead page
   background ABOVE the card — a third of the screen of nothing, while a long note still
   scrolled inside the card. Stretch the card over the region instead: the coach pane reads
   as one stable surface from the top of the screen down to the board, the spare room lives
   INSIDE the card where the note can grow into it, and the ask composer anchors to the
   card's foot (chat-style, nearest the thumbs). The two-column layouts keep the compact
   bottom-anchored card — their region is a fixed-height box beside the board, not slack. */
@media (max-width: 899px) and (min-height: 561px), (max-width: 559px) {
    .play-coach-panel__card {
        flex: 1 1 auto;
        display: flex;
        flex-direction: column;
        align-items: stretch;
    }

    /* The card is the scroll container: its children must keep their natural height (not be
       squashed by flex) so overflow actually overflows and scrolls. */
    .play-coach-panel__card > * { flex: 0 0 auto; }

    /* The composer rides at the card's foot; the auto margin absorbs the card's free space, so
       the blank sits between the coach's words and the input rather than above the card. The
       padding keeps the 0.55rem breathing gap once content grows and the auto margin hits 0. */
    .play-coach-panel__card .play-ask {
        margin-top: auto;
        padding-top: 0.55rem;
    }
}

.play-coach-panel__head {
    display: flex;
    align-items: center;
    gap: 0.45rem;
}

/* Chesswick's name — the stable anchor of the head, so the card always reads as the coach speaking
   even before there's a move to assess. */
.play-coach-panel__name {
    flex: 0 0 auto;
    font-size: 0.9rem;
    font-weight: 700;
    color: var(--color-text, #2a2a2a);
}

.play-coach-panel__avatar {
    flex: 0 0 auto;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    border: 1px solid var(--color-border, #e3dacc);
    background: var(--color-bg-alt, #f7f3ec);
}

.play-coach-panel__badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.25rem;
    height: 1.25rem;
    padding: 0 0.3rem;
    border-radius: 5px;
    font-size: 0.7rem;
    font-weight: 700;
    line-height: 1;
    color: #fff;
    background: var(--cw-c, var(--cw-neutral, #2e6f99));
}

.play-coach-panel__badge:empty { display: none; }

/* Severity word (e.g. "Good move", "Brilliant", "Mistake"). Content-width so the status line beside
   it takes the rest of the head row; hidden when there's no severity to name. */
.play-coach-panel__label {
    flex: 0 1 auto;
    min-width: 0;
    font-size: 0.9rem;
    font-weight: 700;
    color: var(--color-text, #2a2a2a);
}

.play-coach-panel__label:empty { display: none; }

/* Decision buttons (mistake/blunder): "Play on" always, with an optional "Take it back". A single
   content-width row sitting just below the card, so they never stretch the full card/screen width.
   align-self: flex-start keeps the row content-width (the panel otherwise stretches its children);
   flex-wrap lets them stack only if a very narrow screen can't fit both on one line. */
.play-coach-panel__actions {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    flex: 0 0 auto;
    align-self: flex-start;
    gap: 0.5rem;
}

.play-coach-panel__actions[hidden] { display: none; }

.play-coach-panel__ack,
.play-coach-panel__undo {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.3rem;
    min-height: 40px;
    padding: 0 0.9rem;
    border-radius: 9px;
    font-size: 0.88rem;
    font-weight: 700;
    cursor: pointer;
    transition: filter 140ms ease;
}

/* "Play on" — primary action, filled with the severity tint. */
.play-coach-panel__ack {
    background: var(--cw-c, var(--cw-bad, #b62a2a));
    border: 1px solid transparent;
    color: #fff;
}

/* "Take it back" — secondary action, outlined in the severity tint. Filled with the surface colour
   (not transparent) so it reads as a solid button rather than a cut-out of the card behind it. */
.play-coach-panel__undo {
    background: var(--color-bg, #fff);
    border: 1px solid var(--cw-c, var(--cw-bad, #b62a2a));
    color: var(--cw-c, var(--cw-bad, #b62a2a));
}

.play-coach-panel__undo[hidden] { display: none; }

/* On blunders the takeback is the recommended response, so flip the emphasis: "Take it back"
   becomes the filled primary button and "Play on" the outlined secondary one. */
.play-coach-panel--takeback .play-coach-panel__undo {
    background: var(--cw-c, var(--cw-bad, #b62a2a));
    border: 1px solid transparent;
    color: #fff;
}

.play-coach-panel--takeback .play-coach-panel__ack {
    background: var(--color-bg, #fff);
    border: 1px solid var(--cw-c, var(--cw-bad, #b62a2a));
    color: var(--cw-c, var(--cw-bad, #b62a2a));
}

/* ── Decision actions in the thumb zone on narrow viewports (#542) ─────────────────
   In the single-column layout the decision row renders inside the feedback card at the TOP of
   the column, while the player's eyes and thumb are on the board and toolbar at the bottom —
   and the game is *paused on* those buttons, so an off-screen "Play on" reads as a frozen game
   rather than as a prompt. When a decision is live, float the row as a bottom action bar in the
   thumb zone. It deliberately sits over the toolbar row: while deciding, "Take it back" is the
   better-labelled twin of the toolbar's Undo, and hiding the rest keeps the choice unambiguous.
   The board itself stays fully visible above, and nothing reflows — the bar is out of flow, so
   the board never resizes mid-game.

   The query is "wherever the STACKED layout is in effect" — the two two-column layouts (desktop
   >=900px and the landscape-phone split) put the card beside the board with its actions already
   in view, so they keep the in-card placement.

   Keyboard-open state (#580): the document asks for interactive-widget=resizes-content, so the
   layout viewport this bar is anchored to shrinks with the on-screen keyboard and the bar rides up
   to sit on top of it rather than behind it — the same as the .play-menu bottom sheet, whose
   `bottom` offset and 100dvh height ceiling both track the same shrunken viewport. Nothing here
   needs its own keyboard handling; it just must not be re-anchored to anything but the layout
   viewport (no `position: fixed` on a transformed ancestor, no svh/lvh in the offsets). */
@media (max-width: 899px) and (min-height: 561px), (max-width: 559px) {
    .play-coach-panel__actions:not([hidden]) {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 45;
        justify-content: center;
        padding: 0.55rem 0.9rem calc(0.55rem + var(--play-safe-bottom, 0px));
        background: var(--color-bg, #fff);
        border-top: 1px solid var(--color-border, #e3dacc);
        box-shadow: 0 -6px 20px rgba(0, 0, 0, 0.14);
        /* One-shot attention cue as the bar arrives — the player was looking at the board, not
           at the card, so the row has to announce itself. */
        animation: play-decision-rise 240ms ease-out;
    }

    /* Full-width-ish targets in the bar (they are content-width inside the card). */
    .play-coach-panel__actions:not([hidden]) .play-coach-panel__ack,
    .play-coach-panel__actions:not([hidden]) .play-coach-panel__undo {
        flex: 1 1 0;
        min-height: 46px;
        max-width: 13rem;
    }
}

@keyframes play-decision-rise {
    from { transform: translateY(100%); opacity: 0; }
    to { transform: translateY(0); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .play-coach-panel__actions:not([hidden]) { animation: none; }
}

/* "Show me why" (#541) — the third, non-committal action. Styled as a quiet ghost button rather than a
   third coloured one: it does not resolve the decision, so it must not compete with the two that do. */
.play-coach-panel__why {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.3rem;
    min-height: 40px;
    padding: 0 0.9rem;
    border-radius: 9px;
    font-size: 0.88rem;
    font-weight: 600;
    cursor: pointer;
    background: transparent;
    border: 1px dashed var(--color-border, #d6cbb8);
    color: var(--color-text-muted, #5d5548);
    transition: filter 140ms ease;
}

.play-coach-panel__why[hidden] { display: none; }

.play-coach-panel__ack:hover,
.play-coach-panel__undo:hover,
.play-coach-panel__why:hover { filter: brightness(1.06); }

.play-coach-panel__ack:focus-visible,
.play-coach-panel__why:focus-visible,
.play-coach-panel__undo:focus-visible,
.play-coach-panel__handle:focus-visible,
.play-coach-panel__collapse:focus-visible,
.play-coach-panel__card:focus-visible {
    outline: 2px solid var(--color-teal, #1b6b77);
    outline-offset: 2px;
}

/* ── "Playing as" colour modal ─────────────────────────────────────────────────────
   A full-screen scrim with a centred card sitting on top of the board, shown before each game so the
   student picks a side. Toggled by the [hidden] attribute from play-coach.js. */
.play-color-modal {
    position: fixed;
    inset: 0;
    z-index: 80;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    background: rgba(0, 0, 0, 0.5);
}

.play-color-modal[hidden] { display: none; }

.play-color-modal__panel {
    width: 100%;
    max-width: 24rem;
    background: var(--color-bg, #fff);
    border: 1px solid var(--color-border, #e3dacc);
    border-radius: 14px;
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
    padding: 1.2rem 1.1rem 1.3rem;
    text-align: center;
}

.play-color-modal__title {
    font-family: 'Playfair Display', Georgia, serif;
    font-size: 1.35rem;
    margin: 0 0 0.25rem;
    color: var(--color-text, #2a2a2a);
}

.play-color-modal__sub {
    margin: 0 0 1rem;
    font-size: 0.85rem;
    color: var(--color-text-muted, #7A6E5F);
}

/* "Chesswick will play at level 3 of 8." (#550) — the strength, stated where the game is started.
   Quieter than the sub-line above it: a fact about the opponent, not an instruction. */
.play-color-modal__strength {
    margin: -0.7rem 0 1rem;
    font-size: 0.76rem;
    font-weight: 600;
    color: var(--color-text-muted, #7A6E5F);
}

.play-color-modal__strength[hidden] { display: none; }

.play-color-modal__choices {
    display: flex;
    gap: 0.6rem;
    justify-content: center;
}

.play-color-choice {
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.2rem;
    background: var(--color-bg-alt, #f2ede4);
    border: 1px solid var(--cw-board-border, #d6c9b4);
    border-radius: 12px;
    padding: 0.85rem 0.5rem;
    cursor: pointer;
    transition: background 140ms ease, border-color 140ms ease, transform 120ms ease;
}

.play-color-choice:hover {
    background: var(--color-bg, #fff);
    border-color: var(--color-accent-strong, #2f7d4f);
    transform: translateY(-2px);
}

.play-color-choice:focus-visible {
    outline: 2px solid var(--color-teal, #1b6b77);
    outline-offset: 2px;
}

.play-color-choice__piece {
    font-size: 2rem;
    line-height: 1;
}

.play-color-choice__piece--white { color: #fff; text-shadow: 0 0 1px #000, 0 1px 2px rgba(0, 0, 0, 0.4); }
.play-color-choice__piece--black { color: #1a1a1a; }

.play-color-choice__label {
    font-size: 0.9rem;
    font-weight: 700;
    color: var(--color-text, #2a2a2a);
}

.play-color-choice__hint {
    font-size: 0.7rem;
    color: var(--color-text-muted, #7A6E5F);
}

/* ── Resume prompt (#547) ──────────────────────────────────────────────────────────
   The colour modal's other face, shown when an unfinished game survived a reload. It reuses the
   choice buttons wholesale — two options, same shape, same target size — so coming back to a game
   looks like the decision it is rather than a new piece of chrome. Toggled by [hidden] from
   play-coach.js, which hides #playColorStart while this is up. */
.play-resume[hidden],
#playColorStart[hidden] { display: none; }

/* The resume options are a pair, not a trio, so they read better a little narrower than the sides. */
.play-resume .play-color-modal__choices { max-width: 18rem; margin: 0 auto; }

.play-resume-choice .play-color-choice__piece {
    font-size: 1.7rem;
    color: var(--color-accent-strong, #2f7d4f);
}

/* Resume is the reason the prompt exists, so it carries the same "suggested" tint the last-played
   side does — it still has to be pressed. */
#playResumeYes {
    border-color: var(--color-accent-strong, #2f7d4f);
    background: var(--cw-good-soft, rgba(47, 138, 78, 0.14));
}

/* The side chosen last time (#544) reads as the suggested one, without being pre-committed: the
   student still has to press it, they just don't have to hunt for it. */
.play-color-choice[data-last="true"] {
    border-color: var(--color-accent-strong, #2f7d4f);
    background: var(--cw-good-soft, rgba(47, 138, 78, 0.14));
}

/* ── Coaching-intensity presets on the colour modal (#544) ─────────────────────────
   One row under the colour choices: three bundles of the ⚙ → Coaching knobs, chosen at
   the moment the student is already deciding what kind of game this is. The selected one
   is tinted (data-selected, mirrored on aria-pressed for assistive tech). */
.play-color-modal__label {
    margin: 1.1rem 0 0.45rem;
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--color-text-muted, #7A6E5F);
}

.play-preset-row {
    display: flex;
    gap: 0.45rem;
    justify-content: center;
}

.play-preset {
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.15rem;
    min-height: 44px;
    background: var(--color-bg-alt, #f2ede4);
    border: 1px solid var(--cw-board-border, #d6c9b4);
    border-radius: 10px;
    padding: 0.5rem 0.35rem;
    font: inherit;
    cursor: pointer;
    transition: background 140ms ease, border-color 140ms ease;
}

.play-preset:hover { background: var(--color-bg, #fff); }

.play-preset:focus-visible {
    outline: 2px solid var(--color-teal, #1b6b77);
    outline-offset: 2px;
}

.play-preset[data-selected="true"] {
    border-color: var(--color-accent-strong, #2f7d4f);
    background: var(--cw-good-soft, rgba(47, 138, 78, 0.16));
}

.play-preset__label {
    font-size: 0.78rem;
    font-weight: 700;
    line-height: 1.2;
    color: var(--color-text, #2a2a2a);
}

.play-preset__hint {
    font-size: 0.66rem;
    line-height: 1.25;
    color: var(--color-text-muted, #7A6E5F);
}

.play-color-modal__note {
    margin: 0.7rem 0 0;
    font-size: 0.72rem;
    color: var(--color-text-muted, #7A6E5F);
}

/* ── Footer: keep it a thin strip on the play page so the column fits the viewport. ──
   Where the footer is showing it is the page's bottom edge, so it carries the home-indicator
   inset (#542) — the play column above it then needs none. */
.play-shell .app-site-footer {
    padding-top: 0.4rem;
    padding-bottom: calc(0.4rem + var(--play-safe-bottom, 0px));
    font-size: 0.72rem;
}

/* ── Mobile: an immersive, full-screen play surface ────────────────────────────────
   On phones the play loop should own the whole viewport like a native app, so drop the site footer
   nav and — on the standalone play page — the site header too, leaving only the game. Profile mode
   (/u/{name}/play) keeps its header: it carries the player's @handle and the section nav, which are
   real identity/navigation, not redundant chrome. */
@media (max-width: 640px) {
    .play-shell .app-site-footer { display: none; }
    .play-shell:not(.play-shell--profile) .app-header { display: none; }

    /* Profile mode keeps its header (the @handle + section nav); trim its padding on mobile so the
       still-visible strip stays thin and the board gets the room. (Harmless on the standalone play
       page, where the header above is hidden outright.) */
    .play-shell .app-header {
        padding-top: 0.45rem;
        padding-bottom: 0.45rem;
    }

    /* The footer is gone here, so this column IS the page's bottom edge: it takes the
       home-indicator inset onto its own padding. That also shrinks 100cqh by the inset, so the
       board is sized against the room that is actually usable rather than against pixels the
       gesture bar owns. --play-column-gap (not `gap`) so the board's reserve tracks the change. */
    .play-layout {
        padding: 0.45rem 0.6rem calc(0.6rem + var(--play-safe-bottom, 0px));
        --play-column-gap: 0.45rem;
    }

    /* A slightly tighter card on small screens. */
    .play-coach-panel__card { padding: 0.55rem 0.65rem; }
}

/* ── Narrow phones: keep the whole toolbar row on ONE line ─────────────────────────
   The row does not wrap (wrapping would double its height and silently blow the board's
   reserve), so on a 360px-wide phone the five controls have to be squeezed instead: tighter
   gaps, a narrower floor for the review arrows, and less padding around "Get help". Below
   ~340px even that is not enough, so the assist button drops its label and becomes the same
   44px square as Undo and the gear — its aria-label still names it. */
@media (max-width: 400px) {
    .play-toolbar { --play-toolbar-gap: 0.3rem; }

    .play-tool--nav { min-width: 52px; }

    .play-tool--assist {
        padding-left: 0.6rem;
        padding-right: 0.6rem;
    }
}

@media (max-width: 340px) {
    .play-tool--assist {
        width: 44px;
        padding-left: 0.5rem;
        padding-right: 0.5rem;
    }

    .play-tool--assist .play-tool__label { display: none; }
}

/* ── Short viewports (landscape phones, or profile mode where the header/nav stay) ──
   When the column height is small, the fixed feedback region + plates + toolbar can leave little or
   no room for the board. Shrink the parts the reserve is built from — the feedback region and the
   plates — so the board keeps a usable size instead of collapsing toward zero. (The reserve itself
   is no longer overridden: it is derived from these parts, so it follows automatically. Anything
   short AND wide enough goes to the two-column layout below instead, which is a far better use of
   a landscape viewport than a stack.) */
@media (max-height: 560px) {
    .play-layout {
        --play-feedback-height: 7rem;
        --play-plate-height: 1.3rem;
        --play-board-min: 7rem;
    }
}

/* ── Wider screens: the centred column gets a touch more room and a bit more air. ─── */
@media (min-width: 720px) {
    .play-layout {
        max-width: 38rem;
        --play-column-gap: 0.9rem;
        padding-top: 1rem;
    }
}

/* ── Desktop: two-column layout (#457) ─────────────────────────────────────────────
   On wide viewports the centred ~38rem column leaves big empty margins, the review arrows
   stretch into awkward wide bars, and the move list / persona / eval / new-game all hide
   behind the ⚙ and "Get help" sheets. At >=900px we split into a board column (left: the
   board stage + the compact primary toolbar) and a persistent coaching column (right: the
   feedback card, the move list, and the secondary controls), so the horizontal space is used
   and those controls are surfaced rather than tucked behind a gear.

   The .play-pane wrappers are `display: contents` by default, so below this breakpoint they
   are invisible to layout and the single-column mobile/tablet flow above is untouched. They
   only become real columns inside the grid here.

   The same split is also the right answer for a SHORT and wide viewport — a landscape phone has
   no vertical room to stack a feedback card above a board above a toolbar, which is exactly how
   the stacked layout used to squeeze the board toward nothing (#542). So the second query below
   opts landscape phones into this grid too; the block after it tunes the proportions down. */
.play-pane { display: contents; }

@media (min-width: 900px), (min-width: 560px) and (max-height: 560px) {
    .play-layout {
        display: grid;
        grid-template-columns: minmax(0, 1fr) clamp(18rem, 30%, 23rem);
        grid-template-rows: auto minmax(0, 1fr);
        grid-template-areas:
            "board feedback"
            "board side";
        align-items: stretch;
        column-gap: 1.6rem;
        row-gap: 0.8rem;
        max-width: 64rem;
        padding: 1rem 1.2rem calc(0.6rem + var(--play-safe-bottom, 0px));
        /* The feedback card is not stacked above the board in this layout (it moves to the right
           column), so it drops out of the board's reserve and the only gap left in the board
           column is .play-pane--board's own. --play-board-breathing carries the remaining slack:
           6.95rem of measured parts + 2.05rem is the 9rem reserve this layout has always used, so
           the desktop board keeps exactly the size it shipped with. */
        --play-reserve-feedback: 0px;
        --play-reserve-gaps: var(--play-pane-gap);
        --play-board-breathing: 2.05rem;
        /* The feedback card no longer drives the board height, so it can be a touch taller here. */
        --play-feedback-height: 13rem;
    }

    /* The panes become real flex columns (they are `display: contents` at narrower widths). */
    .play-pane--board {
        grid-area: board;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: var(--play-pane-gap);
        min-width: 0;
        min-height: 0;
    }

    /* The board column centres its own [board · toolbar] group, so the toolbar hugs the board
       rather than pinning to the very bottom of the (now taller) column. It keeps its sticky
       backstop — inert while the column fits, which on a desktop-sized window it always does. */
    .play-pane--board .play-toolbar { margin-top: 0; }

    /* The fade belongs in whatever gap sits above the toolbar; in this layout that is the pane's
       gap, not the stacked column's. */
    .play-pane--board .play-toolbar::before { height: var(--play-pane-gap); }

    /* Review nav (‹ ›) no longer stretches to fill a full-width row — in the compact board-column
       toolbar it reads as a normal-sized control beside Undo (fixing the wide empty bars). */
    .play-pane--board .play-tool--nav {
        flex: 0 0 auto;
        min-width: 48px;
    }

    /* The feedback card sits at the top of the right column. Its height is FIXED here (the
       mobile flex-grow that absorbs column slack doesn't apply inside the grid, and an
       auto-sized row would let a long note push the whole side column down instead of
       scrolling the card). Reading mode never engages board-shrink at this width either —
       the note has its own column. */
    #playCoachPanel {
        grid-area: feedback;
        height: var(--play-feedback-height);
        min-height: var(--play-feedback-height);
    }

    /* Right column: the coaching panel stack — the now-persistent assist + settings panels,
       scrolling internally if they outgrow the column height. */
    .play-pane--side {
        grid-area: side;
        display: flex;
        flex-direction: column;
        gap: 0.9rem;
        min-width: 0;
        min-height: 0;
        overflow-y: auto;
    }

    /* The ⚙ settings and "Get help" sheets stop being floating bottom-sheet popovers and become
       always-visible panels in the right column. (The grouped controls keep their ids + handlers,
       so persona/flip/arrows/hint/eval/new-game all work exactly as before — only the container's
       presentation changes.) */
    .play-pane--side .play-menu {
        position: static;
        transform: none;
        left: auto;
        bottom: auto;
        z-index: auto;
        width: 100%;
        max-width: none;
        max-height: none;
        box-shadow: none;
    }

    /* Surface the panels even though they start with the [hidden] popover attribute. */
    .play-pane--side .play-menu[hidden] { display: block; }

    /* Their trigger buttons and the Close / Back-to-chat chrome are redundant once the panels are
       persistent (the desktop header still carries the Chat link). */
    #playAssist,
    #playSettings { display: none; }

    .play-pane--side .play-menu__close { display: none; }
    .play-pane--side .play-assist-menu .play-menu__group--foot { display: none; }
    .play-pane--side .play-menu__group--leave { display: none; }

    /* A little more room for the move list now that it has a persistent home. */
    .play-pane--side .play-movelist { max-height: 16rem; }
}

/* ── Landscape phones: the same two-column split, scaled down (#542) ────────────────
   A 740x360 (or 568x320) viewport has plenty of width and almost no height, so the desktop
   proportions above are wrong for it: a 23rem coaching column and 13rem feedback card would
   leave the board nothing. Narrow the right column, thin out the gaps, and let the board take
   what height is left — the toolbar stays under the board, on-screen, without scrolling.
   Bounded at 899px so a genuinely desktop-width window keeps the desktop proportions it ships
   with, however short it happens to be. */
@media (min-width: 560px) and (max-width: 899px) and (max-height: 560px) {
    /* Every row of chrome costs the board directly here, and the footer is pure site nav — the
       same trade the phone breakpoint already makes. The header stays: at this width it is still
       showing, and it carries the Chat link out of the play surface. */
    .play-shell .app-site-footer { display: none; }

    .play-layout {
        /* The board is height-bound here (a 360-tall viewport caps it around 13rem), so the old
           1fr board column just centred a small board in a sea of empty margin while the
           coaching column was squeezed to 13–18rem and clipped its own card. Flip the ratio:
           the coaching column takes the width the board cannot use. (Fractions, not `auto` —
           the board host's width is min(100%, …), which is circular during intrinsic track
           sizing and collapses the board.) */
        /* The min() cap on the coaching column bounds the top of this range (a ~899px-wide
           short window would otherwise hand the coach 500px+ while a width-bound board shrank
           below what 1fr used to give it); the 60% term keeps the cap from starving the board
           column on the narrowest landscape phones, where 30rem would exceed the whole row. */
        grid-template-columns: minmax(0, 2fr) minmax(13rem, min(60%, 30rem));
        column-gap: 1rem;
        row-gap: 0.5rem;
        /* viewport-fit=cover exposes the horizontal insets too: in landscape a notched phone
           puts the sensor housing at the left or right edge, exactly where this edge-to-edge
           grid parks the board column and the toolbar. The max() keeps the normal gutter on
           un-notched hardware. */
        padding: 0.5rem max(0.8rem, env(safe-area-inset-right, 0px))
                 calc(0.5rem + var(--play-safe-bottom, 0px)) max(0.8rem, env(safe-area-inset-left, 0px));
        --play-board-breathing: 0.5rem;
    }

    /* The old 7rem fixed card was smaller than its own resting furniture (head + status + ask
       row), so it scrolled even when empty. Let it size to content instead, bounded by the
       layout height (cqh — .play-layout is the size container) so a long note scrolls inside
       the card rather than pushing the whole side column off-screen. (In the two-column
       layouts the feedback card is out of the board's reserve — --play-reserve-feedback is
       0 — so a flexible height here cannot move the board.) */
    #playCoachPanel {
        height: auto;
        min-height: 0;
        max-height: 62cqh;
    }

    .play-pane--side { gap: 0.5rem; }

    /* The column is only a couple of hundred pixels tall, so let each panel keep its natural
       height and scroll the column as a whole — otherwise flex squeezes every panel into its own
       sliver with its own scrollbar and none of them is readable. */
    .play-pane--side .play-menu { flex: 0 0 auto; }

    /* The right column is only a couple of hundred pixels tall here; it scrolls internally, so
       the move list must not try to claim a third of it. */
    .play-pane--side .play-movelist { max-height: 7rem; }
}
