/* AlphaBridge play-table styles (extracted from play.html).
   The play UI's visual contract (visual_qa/play_table.py) spans
   play.html + play.css + play.js as one surface. */
:root {
  color-scheme: light;
}
body[data-theme="dark"] {
  color-scheme: dark;
}
* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background: var(--ab-page-backdrop);
  background-attachment: fixed;
  color: var(--ab-ink);
  /* The play page is a viewport-height flex column: appbar + command bar at the
     top (flex:none), the play shell fills the rest. A *definite* page height is
     load-bearing — it lets .play-shell-main resolve `height:100%`, which in turn
     lets the dock drawers' `height:100%` inner scroll internally instead of
     leaking their tall content (Seat Views / Settings) into the flex row and
     inflating the felt past the screen. Guarded by check_play_ui_rendered.mjs
     (felt fits the viewport, no page scroll). */
  height: 100vh;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
}
/* The top bar is the shared design-system component .ab-appbar (logo + wordmark
   + identical nav), defined in app/static/design/alphabridge.css. The play
   table's old bespoke header/.brand/.app-nav rules were removed when it adopted
   the standard bar; page-specific actions moved to .play-commandbar. */
:focus-visible {
  outline: 2px solid var(--ab-blue);
  outline-offset: 2px;
}
h1 { margin: 0; font-size: 22px; letter-spacing: -0.01em; }
h2 { margin: 0 0 10px; font-size: 16px; letter-spacing: -0.005em; }
h3 { margin: 0 0 8px; font-size: 14px; letter-spacing: 0; }
button, select, input {
  font: inherit;
  border-radius: 10px;
  border: 1px solid var(--ab-line);
  background: var(--ab-panel);
  color: var(--ab-ink);
  min-height: 36px;
}
button {
  padding: 7px 12px;
  cursor: pointer;
  border-color: var(--ab-felt-dark2);
  background: linear-gradient(180deg, var(--ab-btn-hi), var(--ab-felt) 55%, var(--ab-felt-dark2, var(--ab-felt)));
  color: var(--ab-white);
  font-weight: 700;
  box-shadow: inset 0 1px 0 var(--ab-white-a20), 0 1px 2px var(--ab-shadow-a18);
  transition: filter 120ms ease, transform 80ms ease, box-shadow 120ms ease;
}
button:hover:not(:disabled) {
  filter: brightness(1.07);
  box-shadow: inset 0 1px 0 var(--ab-white-a20), 0 6px 16px -6px var(--ab-feltglow-a55);
}
button:active:not(:disabled) {
  transform: translateY(1px);
}
button.secondary {
  background: var(--ab-panel);
  border-color: var(--ab-line);
  color: var(--ab-felt-dark);
  box-shadow: 0 1px 2px var(--ab-shadow-a6);
}
button.secondary:hover:not(:disabled) {
  filter: none;
  background: var(--ab-accent-soft);
}
button:disabled {
  cursor: not-allowed;
  opacity: 0.48;
  box-shadow: none;
}
select, input { padding: 7px 9px; }
input:focus, select:focus {
  border-color: var(--ab-felt);
  box-shadow: 0 0 0 3px var(--ab-felt-a14);
}
label {
  display: grid;
  gap: 4px;
  font-size: 11px;
  color: var(--ab-muted);
  font-weight: 750;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.toolbar, .selectors {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: end;
}
.play-commandbar {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  justify-content: space-between;
  padding: 8px 18px;
  border-bottom: 1px solid var(--ab-line);
  background: var(--ab-rail-panel);
}
.play-commandbar .toolbar {
  align-items: center;
  flex: 1;
}
/* Right-clustered page-specific actions (Watch, Table Guide, Theme): they live
   in the command bar, never on the shared top bar. */
.commandbar-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
}
/* The command bar's switch pills share one 40px height. Everything else
   (viewer, seed, theme) lives in the settings rail or the board panel. */
.play-commandbar .ab-toggle {
  min-height: 40px;
}
/* Board-action cluster (new / undo / bot-step / auto / auto-complete / skip): a
   tight row of square icon buttons that lives in the command bar, left of Table
   Guide, so the controls are always one click away instead of buried in a drawer.
   Auto is the design-system .ab-toggle folded in as a labeled mini-switch (see
   .cmd-action--auto below). */
.cmd-actions {
  display: inline-flex;
  gap: 4px;
  padding: 3px;
  border: 1px solid var(--ab-line);
  border-radius: 12px;
  background: var(--ab-rail-panel-soft);
}
.cmd-action {
  display: inline-grid;
  place-items: center;
  width: 38px;
  min-width: 38px;
  height: 38px;
  min-height: 38px;
  padding: 0;
  border-radius: 9px;
  border: 1px solid transparent;
  background: transparent;
  color: var(--ab-felt-dark);
  box-shadow: none;
}
.cmd-action:hover:not(:disabled) {
  filter: none;
  background: var(--ab-accent-soft);
}
.cmd-action.primary {
  background: var(--ab-felt);
  border-color: var(--ab-felt-dark2);
  color: var(--ab-white);
}
.cmd-action.primary:hover:not(:disabled) {
  filter: brightness(1.06);
  background: var(--ab-felt);
}
.cmd-action:disabled {
  opacity: 0.4;
}
.cmd-symbol {
  width: 19px;
  height: 19px;
}
/* Auto: the design-system .ab-toggle folded into the cluster as a labeled pill
   (loop icon + "Auto" + a compact sliding switch). The real <input id=autoAdvance>
   is kept, so every play.js hook (autoAdvanceEnabled / the change listener) is
   unchanged — only the chrome around it is restyled. The orange ON-state is
   inherited straight from the locked .ab-toggle package (its `input:checked ~
   .ab-toggle__track` paints the track), so this layer adds no hardcoded color. */
.cmd-actions .cmd-action--auto {
  display: inline-flex;
  flex-direction: row;
  align-items: center;
  width: auto;
  min-width: 0;
  height: 38px;
  min-height: 38px;
  gap: 7px;
  padding: 0 9px 0 10px;
  /* Match the sibling button.secondary chip (panel fill + --ab-line border +
     soft shadow) so the pill reads as one of the cluster in both themes — a
     <label> doesn't pick up the button.secondary rule the icon buttons use. */
  border: 1px solid var(--ab-line);
  border-radius: 9px;
  background: var(--ab-panel);
  box-shadow: 0 1px 2px var(--ab-shadow-a6);
  font-family: var(--ab-font-sans);
  font-size: 13px;
  font-weight: 800;
  /* Reset the bare label{} rule (uppercase / 11px / wide tracking) so the pill
     keeps its mixed-case "Auto" label and cluster-button metrics. */
  text-transform: none;
  letter-spacing: normal;
  color: var(--ab-felt-dark);
  white-space: nowrap;
}
.cmd-actions .cmd-action--auto:hover {
  background: var(--ab-accent-soft);
}
/* The ON-state cue is the orange switch alone. Override the package's
   `.ab-toggle:has(input:checked)` green border so the pill border never diverges
   from its neutral cluster siblings (most visible in dark mode). */
.cmd-actions .cmd-action--auto.ab-toggle:has(input:checked) {
  border-color: var(--ab-line);
  color: var(--ab-felt-dark);
}
.cmd-action--auto .cmd-label {
  line-height: 1;
}
.cmd-action--auto .cmd-symbol {
  flex: none;
}
/* Compact switch: reuse the package track (its checked rule supplies the orange
   ON), resized to ride inside a cluster button. px-only — no color added here. */
.cmd-actions .cmd-action--auto .ab-toggle__track {
  flex: none;
  width: 28px;
  height: 16px;
  background: var(--ab-panel-soft);
  box-shadow: inset 0 0 0 1px var(--ab-line);
}
.cmd-actions .cmd-action--auto .ab-toggle__knob {
  width: 12px;
  height: 12px;
  top: 2px;
  left: 2px;
  box-shadow: 0 1px 1.5px var(--ab-shadow-a6);
}
.cmd-actions .cmd-action--auto .ab-toggle__knob::after {
  display: none;
}
.cmd-actions .cmd-action--auto input:checked ~ .ab-toggle__track .ab-toggle__knob {
  transform: translateX(12px);
}
/* Boolean controls are the design-system .ab-toggle component (label + sliding
   track). The table only drives the Watch button's track from aria-pressed,
   since that control is a <button> with no checkbox for the component's
   input:checked selectors to key off. The track is 52px wide → 26px travel. */
.watch-toggle.ab-toggle[aria-pressed="true"] .ab-toggle__track {
  background: linear-gradient(180deg, var(--ab-felt), var(--ab-felt-dark));
}
.watch-toggle.ab-toggle[aria-pressed="true"] .ab-toggle__track .ab-toggle__knob {
  transform: translateX(26px);
}
.watch-toggle.ab-toggle[aria-pressed="true"] .ab-toggle__track .ab-toggle__state--on { opacity: 1; }
.watch-toggle.ab-toggle[aria-pressed="true"] .ab-toggle__track .ab-toggle__state--off { opacity: 0; }
.seed-row {
  display: grid;
  grid-template-columns: minmax(92px, 1fr) auto;
  gap: 6px;
  align-items: end;
}
/* "Watch AI vs AI" self-play showcase. The command bar holds only the
   switch pill — it stays in the same spot in both modes so toggling
   never reflows the bar. Pause/Step/Speed/Next/Exit live in the left
   rail's commentary section. */
.watch-toggle {
  font-weight: 850;
  box-shadow: 0 1px 2px var(--ab-shadow-a6);
}
.watch-toggle:hover:not(:disabled) {
  filter: none;
  background: var(--ab-accent-soft);
  box-shadow: 0 1px 2px var(--ab-shadow-a6);
}
.watch-toggle[aria-pressed="true"] {
  border-color: var(--ab-felt-a35);
  color: var(--ab-felt-dark);
}
.watch-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: end;
}
.watch-controls button {
  min-height: 36px;
}
.watch-controls .watch-speed {
  display: grid;
  gap: 3px;
}
.watch-controls[hidden] {
  display: none;
}
.watch-status {
  min-height: 24px;
  display: inline-flex;
  align-items: center;
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--ab-accent-soft);
  color: var(--ab-felt-dark);
  font-size: 12px;
  font-weight: 800;
  white-space: nowrap;
}
.watch-commentary[hidden] {
  display: none;
}
.watch-log {
  display: grid;
  gap: 6px;
  max-height: 360px;
  overflow-y: auto;
}
.watch-entry {
  padding: 6px 8px;
  border-radius: 6px;
  background: var(--ab-rail-thought-bg);
  color: var(--ab-rail-text);
  font-size: 12px;
  line-height: 1.35;
  overflow-wrap: anywhere;
}
.watch-entry.table-note {
  background: var(--ab-rail-note-bg);
  color: var(--ab-rail-note-text);
  border: 1px solid var(--ab-rail-note-border);
  font-weight: 700;
}
/* --- Design-system PlayShell adoption (the Drawer standard) ------------------
   <main> is .ab-play--bare (token base + drawer JS hook, no standalone-card
   chrome). Inside it .ab-play__main is the flex row holding the left edge-tab
   strip, the three dock drawers (Seat Views / Settings / Assistant), and the
   felt. Dock drawers are width:0 closed and reflow the felt when open; below
   900px they slide over a scrim (component CSS). The felt is the flex anchor;
   play.js adds the accordion (one drawer open at a time) over the engine's
   open/close/toggle API. */
main {
  width: 100%;
  margin: 0 auto;
  padding: 8px 0;
  /* Fill the viewport column below the appbar + command bar (flex:none). */
  flex: 1 1 auto;
  min-height: 0;
}
.play-shell-main {
  /* Definite height (not min-height) so the felt and the dock drawers' inner
     panels resolve their `height:100%` and stay within the screen. See the body
     rule above for why min-height alone let the drawers inflate the felt. */
  height: 100%;
  min-height: 0;
  align-items: stretch;
}
/* Drawer panel width when open (and the inner panel width). */
.play-shell-main .ab-drawer { --w: 304px; }
/* The left edge-tab strip: three rotated rounded tabs, always visible, that
   open/close the drawers (accordion). Mirrors the design-system .ab-drawer-handle
   vocabulary as one contiguous vertical strip on the far-left edge. */
.drawer-tabs {
  order: -2;
  flex: none;
  align-self: stretch;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 10px;
  padding: 16px 0;
  background: var(--ab-page);
  border-right: 1px solid var(--ab-line);
  z-index: 4;
}
.drawer-tab {
  writing-mode: vertical-rl;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 11px;
  min-height: 0;
  padding: 26px 11px;
  border: 1px solid var(--ab-line);
  border-left: 0;
  border-radius: 0 13px 13px 0;
  background: var(--ab-panel);
  color: var(--ab-green-dark);
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  box-shadow: 0 2px 6px var(--ab-shadow-a6);
  transition: background 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
}
.drawer-tab__icon {
  writing-mode: horizontal-tb;
  font-size: 19px;
  line-height: 1;
}
.drawer-tab:hover {
  background: var(--ab-accent-soft);
}
.drawer-tab[aria-pressed="true"] {
  background: var(--ab-accent-soft);
  border-color: var(--ab-felt);
  color: var(--ab-felt-dark);
  box-shadow: inset 0 0 0 1px var(--ab-felt-a35);
}
/* The table IS the felt: one green surface (position: relative so the corner
   widgets — board tile NW, bidding box NE, trick counts SE — and the board
   picker popover dock to it). The 3x3 grid lays the four seats (fixed compass:
   N top-centre, S bottom-centre, W/E mid-sides) around the centre .felt; the
   four corner cells host the docked widgets. The centre column stays >=300px
   (the one-window budget the visual contract checks). */
.table {
  /* The felt is the flex anchor inside .ab-play__main — it fills the row and
     reflows when a drawer opens beside it. */
  flex: 1 1 auto;
  width: 100%;
  max-width: 1320px;
  margin: 0 auto;
  /* Fill the row top-to-bottom: the felt IS the flex anchor, so it must stretch
     to the shell height, not float centered. `align-self: center` left a short
     felt (auction phase, hands hidden) vertically centered inside the tall
     .play-shell-main, dumping all the slack above it as a big empty top margin
     the player had to scroll past. Guarded by check_play_ui_rendered.mjs (felt
     hugs the top of the shell). */
  align-self: stretch;
  position: relative;
  display: grid;
  /* The right column's min matches the auction-history width (.center-auction
     below) so the absolutely-docked auction always fits INSIDE its own column
     and never spills left over the centre column's North seat — the "North's
     face-up hand overlaps the auction box" bug at narrow widths / browser zoom. */
  grid-template-columns: minmax(150px, 0.9fr) minmax(320px, 1.7fr) minmax(208px, 0.9fr);
  /* Fixed top/bottom seat bands (not `auto`): North row-1 and South row-3 hold a
     reserved height that fits both the auction-phase name banner AND the play-
     phase 13-card hand, so the felt centre (row 2) keeps a CONSTANT position and
     height through every phase. With `auto` rows the bands grew when hands were
     dealt and shrank as cards were played, sliding the felt — and with it the
     bidding box and contract card — up and down. Guarded by
     check_play_ui_rendered.mjs (felt top/height stable across auction→play).
     The row band + row gap ride custom properties because the auction dock's
     height cap (.center-auction max-height) is derived from them: the dock may
     grow only within row 1, never down over the East seat panel in row 2. */
  --felt-row1: 156px;
  --felt-rowgap: 10px;
  grid-template-rows: var(--felt-row1) minmax(240px, 1fr) var(--felt-row1);
  gap: var(--felt-rowgap) 14px;
  align-items: stretch;
  padding: 16px;
  border-radius: 22px;
  border: 1px solid var(--ab-feltedge-a55);
  background: var(--ab-felt-fill);
  color: var(--ab-white);
  box-shadow: var(--ab-shadow-felt);
}
.side-panel {
  background: var(--ab-panel);
  border: 1px solid var(--ab-line);
  border-radius: 14px;
  padding: 12px;
  min-width: 0;
  box-shadow: 0 1px 2px var(--ab-shadow-a4), 0 18px 40px -30px var(--ab-shadow-a50);
}
/* Every seat sits on the felt inside the SAME faint transparent border the felt
   centre uses, so all four read as contained hands on the open green (not just
   the viewer's). The ring is inset (box-shadow) so it never bleeds past the grid
   cell into the felt centre. */
.seat-panel {
  /* --seat-radius / --seat-pad drive the concentric-corner relationship with the
     banner inside (see .seat-banner): the padding is the uniform inset, so the
     banner's radius = --seat-radius − --seat-pad keeps the two curves parallel
     (Apple's nesting standard). Uniform padding (not 8/12/10) is required for the
     corners to actually be concentric on every side. */
  --seat-radius: 16px;
  --seat-pad: 8px;
  background: transparent;
  border: 0;
  border-radius: var(--seat-radius);
  padding: var(--seat-pad);
  min-width: 0;
  box-shadow: inset 0 0 0 1px var(--ab-white-a14);
}
/* The viewer's own hand (and the visible dummy) read a touch stronger — the
   hand they actually play from. */
.seat-panel.own-hand {
  background: var(--ab-white-a10);
  box-shadow: inset 0 0 0 1px var(--ab-white-a18);
}
/* Bidding box at the BOTTOM of the felt centre (felt grid row 3). The
   contract/call-meaning card sits at the TOP (row 1) and the trick fills the 1fr
   middle (row 2); because the box is pinned to the bottom and the middle absorbs
   the card's growth, previewing a call grows the card downward into that middle
   filler and can never push the box — the old hover flicker loop stays gone.
   Auction only; renderCalls hides it once play starts and the trick takes the
   centre. The .bid-stage confirm panel pops to its RIGHT into open felt. The box's
   card surface + inner grid come from the design-system .ab-biddingbox co-class
   on #bidBox; .bid-box is position: relative so the confirm panel anchors to it. */
.bid-box {
  grid-row: 3;
  align-self: end;
  justify-self: center;
  width: 100%;
  max-width: 360px;
  min-width: 0;
  position: relative;
  z-index: 3;
}
/* When a call is staged the confirm panel pops out to the box's RIGHT
   (.bid-stage, half = (112px width + 10px gap) / 2 = 61px). Nudging the box
   left by that half keeps the box+confirm assembly visually centred on the felt
   instead of letting the pair read right-heavy. The .bid-stage stays to the
   right of the box (its rendered-geometry contract), it just travels with it. */
.bid-box.staging {
  transform: translateX(-61px);
}
@media (prefers-reduced-motion: no-preference) {
  .bid-box { transition: transform 160ms cubic-bezier(0.22, 1, 0.36, 1); }
}
.bid-box[hidden] {
  display: none;
}
.bid-box .panel-heading {
  margin-bottom: 0;
}
.bid-box h2 {
  margin: 0;
  color: var(--ab-card-label);
  font-size: 11px;
  line-height: 1.2;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.seat-panel {
  display: grid;
  /* Intrinsic rows (not auto + 1fr): the panel fills its grid cell (see the
     pos-* rules), and align-content positions the banner + hand toward the felt
     within that fixed frame — so the border holds its size whether or not a hand
     is dealt, instead of shrink-wrapping the banner during the auction. */
  grid-template-rows: auto auto;
  gap: 8px;
}
/* Seat name bar = the design-system seat view (.ab-seatview), made
   interactive: click to change the pair agreement. The component supplies the
   card surface, name/detail typography, and the gold .ab-seatview--turn ring
   (the single "whose turn" signal); the table only adds the interaction
   affordances and the chip + tags + HCP head row. */
.seat-banner {
  max-width: 100%;
  min-width: 0;
  /* Concentric corners (Apple's nesting standard): the banner sits inside the
     seat panel inset by --seat-pad on every side, so its radius = the panel's
     radius − that inset keeps the two curves parallel instead of a tight little
     box rattling inside a big rounded frame. Both vars are inherited from
     .seat-panel, so this stays concentric at every breakpoint. */
  border-radius: calc(var(--seat-radius) - var(--seat-pad));
  font-weight: 750;
  font-size: 13px;
  cursor: pointer;
  transition: border-color 120ms ease, background 120ms ease;
}
.seat-banner:hover {
  border-color: var(--ab-felt);
}
.seat-banner-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 7px;
  min-width: 0;
}
.seat-chip {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: 22px;
  height: 22px;
  border-radius: 6px;
  background: var(--ab-felt);
  color: var(--ab-white);
  font-size: 12px;
  font-weight: 850;
}
/* The seat WORD is gone (the chip already names the seat). The bidding system
   (.ab-seatview__detail.seat-system) is now the banner's primary label: it
   rides the single head row right after the chip, growing to fill the row and
   truncating before it can crowd the tags or HCP badge. */
.seat-banner .seat-system {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 13px;
  font-weight: 800;
  color: var(--ab-ink);
}
.app-rail .seat-banner .seat-system { color: var(--ab-ink); }
/* Hidden / not-yet-dealt hands keep the SAME fixed frame as a full hand — the
   panel fills its grid cell rather than shrink-wrapping its card-back ladder, so
   the seat border stays in place across the auction and play. */
.seat-panel.hidden-hand {
  width: auto;
  min-width: 0;
  max-width: none;
  justify-self: stretch;
}
.seat-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
  min-width: 0;
}
/* The HCP badge anchors the right edge whether or not role tags are
   present (the tags collapse entirely when empty). The shared .ab-hcp chip is
   sized for headers; on the compact seat banner it reads too large, so the
   table tightens its metrics here (look stays, footprint shrinks). */
.seat-banner-head .ab-hcp {
  margin-left: auto;
  flex: none;
  min-height: 19px;
  padding: 1px 7px;
  gap: 2px;
  border-radius: 6px;
}
.seat-banner-head .ab-hcp__value { font-size: 12px; }
.seat-banner-head .ab-hcp__unit { font-size: 9px; }
.seat-tags:empty {
  display: none;
}
.seat-tag {
  display: inline-flex;
  align-items: center;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--ab-accent-soft);
  color: var(--ab-felt-dark);
  font-size: 10px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
}
/* A staged agreement change shows on the banner immediately, marked PENDING
   in gold until a new board brings it into play. */
.seat-tag.pending-agreement {
  background: var(--ab-rail-turn-bg);
  border: 1px solid var(--ab-gold);
  color: var(--ab-gold);
}
.seat-banner.agreement-pending {
  border-color: var(--ab-gold);
}
/* Agreement-change reminder toast: floats over the felt (fixed, so it never
   reflows the one-window layout) and persists until the board is restarted. */
.agreement-notice {
  position: fixed;
  z-index: 40;
  left: 50%;
  bottom: 22px;
  transform: translateX(-50%);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 14px;
  max-width: min(560px, calc(100vw - 32px));
  padding: 12px 16px;
  border-radius: 14px;
  background: var(--ab-rail-note-bg);
  border: 1px solid var(--ab-rail-note-border);
  color: var(--ab-rail-note-text);
  box-shadow: 0 18px 40px -18px var(--ab-shadow-a55);
  font-weight: 750;
  font-size: 13px;
}
.agreement-notice[hidden] { display: none; }
.agreement-notice-text { flex: 1 1 220px; line-height: 1.32; }
.agreement-notice-actions { display: inline-flex; gap: 8px; flex: none; }
.agreement-notice-actions button { min-height: 34px; }
/* The HCP badge is the shared design-system component (.ab-hcp): a minted
   dark-green chip with a gold tabular figure and an "HCP" unit. The table
   only positions it (see ".seat-banner-head .ab-hcp" above); its look lives
   in the locked package, not here. */
/* Seat panels sit at a FIXED compass (pos-* classes set by render): North is
   always top-centre, South bottom-centre, West/East the mid-sides — the table
   never rotates with the viewer. Taking over a partner's declarer hand switches
   which hand you control and see (a legal declarer view), but North-declares
   simply means the declarer is the top seat. North hugs the felt's top edge
   (align-self: end) so the tall auction row keeps the board tile / bidding box
   in the corners with North just above the centre. */
/* Each seat panel FILLS its grid cell (a fixed frame), and align-content packs
   the banner + hand within it. EVERY seat now puts its name banner at the TOP of
   its box (its header), so all four read consistently: North pushes the hand to
   the bottom (hugging the felt) via space-between; South, West, and East keep the
   banner on top with the hand directly beneath (start). The frame holds its
   position and size across the auction and play either way. */
.pos-top { grid-column: 2; grid-row: 1; align-self: stretch; justify-self: stretch; align-content: space-between; }
.pos-left { grid-column: 1; grid-row: 2; align-self: stretch; justify-self: stretch; align-content: start; }
.pos-right { grid-column: 3; grid-row: 2; align-self: stretch; justify-self: stretch; align-content: start; }
.pos-left:has(.card-backs) .card-backs,
.pos-right:has(.card-backs) .card-backs { flex: 1; align-content: center; align-items: center; justify-content: center; }
/* Top/bottom hidden fans (e.g. the face-down dummy) are a horizontal flex row
   that stretches the full panel width — centre the backs within it so the hand
   sits in the middle of the seat box instead of hugging the left edge. */
.pos-top .card-backs,
.pos-bottom .card-backs { justify-content: center; }
.pos-bottom { grid-column: 2; grid-row: 3; align-self: stretch; justify-self: stretch; align-content: start; }
/* N/S hands (single fan or suit groups) center horizontally in the now full-width
   top/bottom frames. */
.pos-top .hand .cards,
.pos-bottom .hand .cards { justify-content: center; }
/* The felt centre (table grid c2 r2): the focus area of the now-whole-table
   felt. The table paints the green; the centre gets only a faint inner ring so
   it reads as the play zone without becoming a separate box. A two-row stack —
   contract/explanation card on top, then the centred swap region below it where
   the auction history (bidding) and the trick zone (play) trade places. */
.felt {
  grid-column: 2;
  grid-row: 2;
  min-height: 300px;
  border-radius: 16px;
  background: radial-gradient(130% 90% at 50% 18%, var(--ab-white-a10), transparent 62%);
  box-shadow: inset 0 0 0 1px var(--ab-white-a10);
  color: var(--ab-white);
  padding: 14px;
  display: grid;
  /* Three stacked rows: call-meaning / contract card (r1, top), trick (r2, the
     flexible 1fr filler), bidding box (r3, bottom). The box is pinned to the
     BOTTOM and the 1fr middle absorbs any growth of the card above, so previewing
     a call grows the card downward INTO the middle filler and never moves the box
     (the hover flicker loop stays structurally impossible — the box just lives at
     the bottom now). During play the card empties (r1 collapses) and the box
     hides (r3 collapses), so the trick fills the centre. */
  grid-template-rows: auto minmax(0, 1fr) auto;
  justify-items: center;
  align-content: stretch;
  gap: 12px;
  position: relative;
}
/* Board tile docked to the felt's NW corner (table grid c1 r1). The tile itself
   is the design-system .ab-boardtile component; this class only places it. */
.board-tile {
  grid-column: 1;
  grid-row: 1;
  align-self: start;
  justify-self: start;
}
/* Hover cue is a shadow-only raise (and so needs no reduced-motion gate): a
   translate would retreat the tile's own bottom edge from under the cursor
   and flicker the hover state. */
.board-tile:hover {
  box-shadow: 0 5px 12px var(--ab-black-a40);
}
/* Contract dock: during play the contract reads as a compact chip docked
   RIGHT NEXT TO the board tile in the felt's NW corner (out of grid flow, like
   the auction in the NE), instead of a wide banner squatting in the felt centre
   over the trick. Empty (and so display:none) during the auction — the felt
   centre's .par-box owns the call-meaning preview then. It sits in the board
   tile's vertical band and clears the North seat to its right. */
.contract-dock {
  position: absolute;
  top: 16px;
  left: 88px;
  z-index: 3;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 1px;
  min-height: 72px;
  max-width: 104px;
  padding: 6px 11px;
  border-radius: 10px;
  background: linear-gradient(180deg, var(--ab-card-hi), var(--ab-card-face2));
  border: 1px solid var(--ab-card-edge);
  color: var(--ab-suit-black);
  box-shadow: 0 1px 0 var(--ab-white-a50) inset, 0 4px 14px var(--ab-black-a22);
}
.contract-dock:empty { display: none; }
.contract-dock .metric {
  color: var(--ab-card-label);
  font-size: 9px;
  line-height: 1.1;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}
.contract-dock .contract-dock__deal {
  display: flex;
  align-items: baseline;
  gap: 5px;
  font-weight: 850;
  line-height: 1.05;
}
.contract-dock .contract-dock__call { font-size: 21px; }
.contract-dock .contract-dock__by {
  font-size: 12px;
  font-weight: 800;
  color: var(--ab-card-note);
}
.contract-dock .contract-dock__score {
  font-size: 13px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}
/* Call-meaning card: the felt centre's TOP row (above the trick and the bidding
   box). It is the call-meaning / "your call" preview surface — hovering a bid in
   the box below grows this card DOWNWARD into the 1fr trick filler beneath it, so
   the box (pinned to the bottom) never moves (no reflow flicker). During play it
   is emptied (the contract reads from the NW dock) and collapses via :empty. */
.par-box {
  grid-row: 1;
  align-self: start;
  /* Centered in the felt so the card sits in the middle of the play zone. */
  justify-self: center;
  max-width: 100%;
  min-height: 52px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2px 14px;
  background: linear-gradient(180deg, var(--ab-card-hi), var(--ab-card-face2));
  border: 1px solid var(--ab-card-edge);
  border-radius: 10px;
  color: var(--ab-suit-black);
  padding: 5px 12px;
  font-weight: 800;
  line-height: 1.16;
  font-size: 13px;
  overflow-wrap: anywhere;
  box-shadow: 0 1px 0 var(--ab-white-a50) inset, 0 4px 14px var(--ab-black-a22);
}
/* During play the contract reads from the NW dock, so the felt-centre card is
   emptied — collapse it then (its min-height + card surface would otherwise
   leave an empty pill floating above the trick). */
.par-box:empty { display: none; }
.par-box .stat {
  display: grid;
  gap: 0;
  justify-items: start;
}
.par-box .metric {
  display: block;
  color: var(--ab-card-label);
  font-size: 10px;
  line-height: 1.2;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.par-box .big {
  display: block;
  font-size: 16px;
  line-height: 1.05;
}
.par-box .disclosure {
  flex-basis: 100%;
  color: var(--ab-card-note);
  font-size: 12px;
  font-weight: 650;
  line-height: 1.22;
}
/* The auction history docks to the felt's NE corner (table grid c3 r1), visible
   in both phases — during play it shows the final auction that set the contract.
   Row 1 grows downward with long auctions. The panel surface + 4-column grid come
   from the design-system .ab-auction component nested inside; the behavioural
   co-classes below (vulnerable seat, turn cell) layer the table's state signals
   over the component look. */
/* Auction history docks to the felt's NE corner. It is positioned ABSOLUTELY
   (not in the grid flow) on purpose: as calls accumulate it grows taller, and a
   grid-flow element in row 1 would inflate that `auto` row and push North, the
   felt, and the bidding box downward every time you bid — the "fixed elements
   move when I bid" bug. Out of flow, it grows over the empty felt corner without
   moving anything; a max-height + internal scroll keeps a long auction from
   running into the East seat. Guarded by check_play_ui_rendered.mjs (bidding box
   and North stay put across the whole auction). */
.center-auction {
  position: absolute;
  top: 16px;
  right: 16px;
  z-index: 3;
  /* Capped to the right grid column's min (208px, set on .table) so the dock
     stays within its own column and can never overlap the centre column's North
     seat — its hand fans inside c2 with a gap to spare. */
  width: min(208px, 38%);
  /* Height cap derived from the felt's OWN row metrics: the dock starts 16px
     below the felt top and the East seat panel (row 2) starts at
     padding + row1 + rowgap, so this is the exact budget before the dock would
     cover East's name banner. The old calc(50% - 24px) cap was measured against
     the felt height, not row 1, and a 5+-row auction buried the East banner
     (role tags, HCP badge) under the dock at every common laptop size. -10px
     keeps clear of East across breakpoints (dock top stays 16px while the felt
     padding shrinks to 12px on short viewports). Long auctions scroll inside
     the dock; renderAuction pins the scroll to the newest call (scrolling up
     reviews the earlier rounds and the W/N/E/S header), so nothing is lost. */
  max-height: calc(var(--felt-row1, 156px) + var(--felt-rowgap, 10px) - 10px);
  overflow-y: auto;
  opacity: 0.97;
}
.center-auction .ab-auction {
  min-width: 0;
  width: 100%;
}
/* Vertical separators between the four W/N/E/S columns so the seats read as
   distinct columns (the component only draws horizontal row dividers). Drop the
   rule on the last (S) column so the grid's outer edge stays clean. */
.center-auction .ab-auction__h,
.center-auction .ab-auction__c {
  border-right: 1px solid var(--ab-line);
}
.center-auction .ab-auction__h:nth-child(4n),
.center-auction .ab-auction__c:nth-child(4n) {
  border-right: 0;
}
.ab-auction .auction-seat.not-vulnerable { color: var(--ab-ink); }
.ab-auction .auction-seat.vulnerable {
  background: var(--ab-auction-vul);
  color: var(--ab-white);
}
.ab-auction .auction-cell.turn-cell {
  background: var(--ab-auction-turn);
  box-shadow: inset 0 -3px 0 var(--ab-call-pass);
}
.status {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}
.pill {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 3px 8px;
  border-radius: 999px;
  background: var(--ab-white-a15);
  color: inherit;
  font-size: 12px;
  font-weight: 750;
}
/* The trick zone is the felt centre's MIDDLE row (felt grid row 2, the flexible
   1fr filler). The bidding box hides during play, so the trick owns the centre.
   It centres its content — the played-card layout is the design-system component
   .ab-trickarea (four absolute N/E/S/W seat slots around a centre dot). */
.trick {
  grid-row: 2;
  /* Stretch across the felt width (but stay vertically centred) so it has a
     DEFINITE width — the trick area inside sizes itself with `min(…, 100%)` and
     centres within it. (place-self: center here would shrink-wrap, making the
     inner 100% resolve to 0.) */
  align-self: center;
  justify-self: stretch;
  display: grid;
  place-items: center;
  pointer-events: none;
}
.felt:not(.play-mode) .trick {
  display: none;
}
/* The trick is the focal point of play, so the table scales the design-system
   .ab-trickarea up from its compact default: a bigger play field, bigger dashed
   seat slots, bigger played cards (with their rank/suit scaled to match), and
   bigger, brighter seat letters. The played-card overrides keep a card the same
   size as the slot it lands in. */
/* Wider than tall: the four cards sit at the edges (W left, E right, N/S centred),
   so the WIDTH governs whether the side cards clear the centre ones. At the old
   square 320 the 70px cards + 36px side letters collided; widening to ~400 (capped
   to the felt so it never overflows a narrow column) gives the side cards a clear
   gap from the centre pair. Height stays compact for the one-window budget. */
.ab-trickarea { width: min(400px, 100%); height: 300px; }
.ab-trickarea__empty { width: 70px; height: 98px; border-radius: 8px; }
.ab-trickarea .ab-card--picture {
  width: 70px; min-width: 70px; height: 98px; min-height: 98px;
}
.ab-trickarea .ab-card--picture .ab-card__rank { font-size: 20px; }
.ab-trickarea .ab-card--picture .ab-card__suit { font-size: 40px; }
/* Seat letters are the prominent labels for the play field — sized up well beyond
   the slot/card scale (the dashed outlines keep their size; only the letters grow). */
.ab-trickarea__label { font-size: 27px; line-height: 1; font-weight: var(--ab-weight-heavy); color: var(--ab-cream-a85); }
/* Board-done card: fills the center swap region once the held trick
   clears, so a finished board never leaves an empty stretch of felt. */
.result-card {
  grid-column: 1 / -1;
  grid-row: 1 / -1;
  place-self: center;
  max-width: 100%;
  display: grid;
  gap: 3px;
  justify-items: center;
  background: linear-gradient(180deg, var(--ab-card-hi), var(--ab-card-face2));
  border: 1px solid var(--ab-card-edge);
  border-radius: 12px;
  color: var(--ab-suit-black);
  padding: 14px 24px;
  font-weight: 800;
  text-align: center;
  overflow-wrap: anywhere;
  box-shadow: 0 1px 0 var(--ab-white-a50) inset, 0 12px 30px var(--ab-black-a35);
}
.result-card .metric {
  color: var(--ab-card-label);
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.result-card .big {
  font-size: 20px;
  line-height: 1.1;
}
.result-card .note {
  font-size: 13px;
  font-weight: 700;
  color: var(--ab-card-note);
}
/* Trick-count chips dock to the felt's SE corner (table grid c3 r3), shown only
   during play (renderTableStatus leaves them empty during the auction — the old
   Calls/Result boxes are gone). */
.scoreline {
  grid-column: 3;
  grid-row: 3;
  display: flex;
  gap: 8px;
  align-self: end;
  justify-self: end;
}
.scorebox {
  min-height: 30px;
  min-width: 78px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  border: 1px solid var(--ab-cream-a40);
  border-radius: 999px;
  box-shadow: inset 0 1px 0 var(--ab-white-a10), 0 3px 10px var(--ab-black-a30);
  background: linear-gradient(180deg, var(--ab-mint1-a88), var(--ab-mint2-a94));
  color: var(--ab-scorebox-text);
  padding: 3px 12px;
  font-weight: 850;
}
.scorebox span {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0;
  opacity: 0.88;
}
.scorebox strong {
  font-family: var(--ab-font-display);
  font-size: 17px;
  line-height: 1;
  color: var(--ab-gold-bright);
}
/* Status row removed from the felt (its whose-turn / contract text duplicated
   the auction turn-highlight, the contract card, and the result card). The
   element stays in the DOM (id required by the visual contract) but never
   renders. */
.table-status {
  display: none;
}
.hand {
  display: grid;
  gap: 4px;
}
/* Shared hand styling for all four seat panels (N/E/S/W). The taller
   East/West panels stretch with the felt row, so card rows must pack at
   the top instead of stretching apart: one class keeps font size,
   suit-symbol size, spacing, and alignment identical on every seat. */
.seat-hand {
  align-content: start;
  font-size: 14px;
  min-width: 0;
}
.seat-hand .suit-row {
  align-content: start;
}
.suit-row {
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr);
  gap: 5px;
  align-items: start;
  min-height: 34px;
}
.suit-label {
  font-weight: 800;
  color: var(--ab-ink);
  line-height: 34px;
  font-size: 20px;
}
.suit-label.red { color: var(--ab-red); }
.cards {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
  min-width: 0;
}
/* Cards are the design-system component (.ab-card chip / --picture / --legal),
   emitted by playingCardHtml(). Each suit row's .cards is also an .ab-card-hand
   so the cards overlap into a hover WAVE fan (picture mode) or sit as a flat
   chip row (text mode). The component's 24px hover headroom is too tall for the
   compact seat panels, so tame it; the sorted fan keeps its own (more specific)
   padding below. */
.seat-hand .ab-card-hand {
  padding-top: 4px;
}
/* Card-hover DESELECT fix. The design-system wave lifts the hovered card (and
   its neighbours) with translateY. On an interactive hand that re-renders when a
   card is staged (confirm-cards), the lift moves the card out from under a
   stationary cursor: the pointer drops into the gap, the card snaps back and
   flickers, and the confirming click lands on empty felt or a neighbour — so the
   staged card reads as "deselected". On the play table's own hands we drop the
   positional lift (the hit target never moves) and signal hover with a
   non-moving raise instead (elevation + brightness + z-index), so a selection
   always survives the hover and the second click confirms. */
/* Kill the wave's transform on EVERY seat-hand card, hovered or not, with
   `!important`. This is the one case that genuinely needs it: the package's wave
   is a chain of selectors whose specificity GROWS with distance — `:hover`
   (0,3,0), `:hover + .ab-card` (0,3,0), `:hover + .ab-card + .ab-card` (0,5,0),
   `…+ .ab-card` (0,6,0). A plain override has to out-specify the *deepest* link or
   the far neighbours leak and lift while the near ones stay put — the lone
   "jumping card" bug. `!important` wins regardless of how deep the chain goes, so
   no card can ever lift, in any browser. Hover feedback is the non-moving raise
   below (shadow + brightness), not motion; the transition drops `transform` so
   nothing animates a lift either. */
.seat-panel .seat-hand .ab-card-hand > .ab-card {
  transform: none !important;
  transition: box-shadow var(--ab-dur-wave, 160ms) ease, filter var(--ab-dur-wave, 160ms) ease;
}
/* The package wave also ships a ::after hover hit-extender covering the strip
   a lifted card vacates (its anti-flicker guard). With the lift disabled here
   the card never vacates that strip, so the extender would only widen the
   hover/click zone 18px below an unmoved card — drop it with the wave. */
.seat-panel .seat-hand .ab-card-hand > .ab-card:hover::after {
  content: none;
}
.seat-hand .ab-card-hand > .ab-card.ab-card--legal:hover {
  z-index: 40;
  filter: brightness(1.04);
  box-shadow: 0 10px 22px -8px var(--ab-shadow-a50);
}
/* --- Grouped ("Suits") hand view --------------------------------------------
   The four suits are drawn as compact rank chips, oriented to fit each seat's
   slot. A suit's chips never wrap mid-suit (so each suit reads as one clean
   block); the outer hand wraps between suits where needed. */
.hand.grouped .ab-card {
  min-width: 24px;
}
.hand.grouped .cards {
  flex-wrap: nowrap;
  padding-top: 0;
}
/* Side seats (E/W): a narrow, tall column → FOUR stacked suit rows, one suit
   per row, label gutter on the left. This is the "EW shows four rows" layout. */
.pos-left .hand.grouped,
.pos-right .hand.grouped {
  display: grid;
  gap: 4px;
  justify-content: start;
}
.pos-left .hand.grouped .suit-row,
.pos-right .hand.grouped .suit-row {
  min-height: 30px;
}
/* Top/bottom seats (N/S): a wide, short band → the four suit groups laid out
   side by side, so the hand stays short and never overflows the band. */
.pos-top .hand.grouped,
.pos-bottom .hand.grouped {
  display: flex;
  flex-flow: row wrap;
  justify-content: center;
  align-items: flex-start;
  gap: 5px 18px;
}
.pos-top .hand.grouped .suit-row,
.pos-bottom .hand.grouped .suit-row {
  display: flex;
  align-items: center;
  width: auto;
  min-height: 32px;
  gap: 5px;
}
.pos-top .hand.grouped .suit-label,
.pos-bottom .hand.grouped .suit-label {
  line-height: 1;
  font-size: 19px;
}
/* Sorted 13: one BBO-style fan of overlapping card pictures, with a small
   gap marking each suit boundary. */
.hand.sorted-hand {
  align-content: start;
}
.hand.sorted-hand .cards {
  gap: 0;
  row-gap: 6px;
  padding-top: 2px;
}
.hand.sorted-hand .cards .ab-card.suit-start {
  margin-left: 7px;
}
/* Sorted 13 — side seats (E/W): four picture-card rows, one per suit, stacked
   and vertically centered in the tall side column. Cards are a touch smaller so
   four overlapping rows fit without inflating the felt row. */
.hand.sorted-rows {
  display: grid;
  gap: 5px;
  align-content: center;
}
.hand.sorted-rows .suit-row {
  grid-template-columns: 20px minmax(0, 1fr);
  align-items: center;
  min-height: 0;
}
.hand.sorted-rows .cards {
  gap: 0;
  row-gap: 0;
  padding-top: 0;
  flex-wrap: nowrap;
}
.hand.sorted-rows .ab-card--picture {
  min-width: 38px;
  min-height: 54px;
}
/* Looser overlap than the main fan so two-digit ranks (10) stay legible on the
   smaller side cards. */
.hand.sorted-rows .ab-card-hand > .ab-card--picture + .ab-card--picture {
  margin-left: -18px;
}
.hand.sorted-rows .ab-card--picture .ab-card__suit {
  font-size: 22px;
}
.hand.sorted-rows .suit-label {
  line-height: 1;
  font-size: 18px;
}
/* "10" is the only two-glyph rank index and the fan overlap clips its zero
   (it read as "1C" / "1(" in every fanned hand). Tighten the pair and pull it
   into the card's corner padding so the full "10" fits the visible strip. */
.ab-card-hand .ab-card__rank.rank-ten {
  letter-spacing: -0.12em;
  margin-left: -3px;
}
/* A disabled segmented option (e.g. the "By suit" view, off for now) reads as
   greyed and non-interactive. */
.ab-seg__opt:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
/* Confirm-cards: the first tap outlines the staged card; the second tap (or
   Enter) plays it. */
.ab-card.pending {
  outline: 3px solid var(--ab-gold);
  outline-offset: 1px;
}
/* --- Dark theme refinements (play table) ------------------------------------
   A real dark theme: the playing cards get DARK faces (charcoal) with light /
   bright pips instead of the package's constant white card stock, the felt's
   inner ring drops from white to a dark edge, and the felt-centre information
   cards drop onto the same dark surface as the (already-dark) bidding box and
   auction grid. */
body[data-theme="dark"] .ab-card {
  background: var(--ab-panel-soft);
  border-color: var(--ab-rail-line);
  color: var(--ab-cream);
}
body[data-theme="dark"] .ab-card--picture {
  background: linear-gradient(180deg, var(--ab-panel-soft), var(--ab-panel));
}
body[data-theme="dark"] .ab-card--red {
  color: var(--ab-red);
}
/* Playable (legal) cards keep a clear affordance — a blue ring on the dark face. */
body[data-theme="dark"] .ab-card--legal {
  background: var(--ab-green-soft);
  border-color: var(--ab-blue);
  color: var(--ab-cream);
}
body[data-theme="dark"] .ab-card--legal.ab-card--red {
  color: var(--ab-red);
}
/* The felt's inner ring + the seat-panel rings are faint WHITE in light mode;
   in dark that reads as a glowing white border around the felt. Use a dark edge. */
body[data-theme="dark"] .felt {
  box-shadow: inset 0 0 0 1px var(--ab-black-a30);
}
body[data-theme="dark"] .table {
  border-color: var(--ab-black-a30);
}
body[data-theme="dark"] .par-box,
body[data-theme="dark"] .result-card,
body[data-theme="dark"] .contract-dock {
  background: var(--ab-panel);
  border-color: var(--ab-line);
  color: var(--ab-ink);
}
body[data-theme="dark"] .par-box .metric,
body[data-theme="dark"] .par-box .disclosure,
body[data-theme="dark"] .result-card .metric,
body[data-theme="dark"] .result-card .note,
body[data-theme="dark"] .contract-dock .metric,
body[data-theme="dark"] .contract-dock .contract-dock__by {
  color: var(--ab-muted);
}
/* The level (1–7) picker keeps a pale "rail" look by design; in dark mode pull
   it onto a dark neutral so the bidding box reads as one cohesive dark panel. */
body[data-theme="dark"] .ab-bidcall.level-call {
  background: var(--ab-panel-soft);
  border-color: var(--ab-line);
  color: var(--ab-ink);
}
body[data-theme="dark"] .ab-bidcall.level-call:disabled {
  background: var(--ab-rail-panel-soft);
  border-color: var(--ab-rail-line);
  color: var(--ab-muted);
}
/* Hidden hands as face-down card backs (count = 13 minus the seat's publicly
   played cards, so only public information is conveyed). The backs are the
   design-system .ab-cardback fan; .display:flex here only beats .hand's grid
   so the component's overlap/--v stacking takes effect. */
.card-backs {
  display: flex;
  min-height: 50px;
}
/* A hidden hand reads as a real HELD hand: one tight overlapping fan of backs at
   the face-up card scale — portrait everywhere — and crucially UNGROUPED (a
   single uniform fan, no per-suit split), so the suit shape stays secret. This
   mirrors the viewer's own fan, just flipped to backs, instead of the old
   spread-out ladder of separate tiles. Top/bottom (dummy) fan horizontally; the
   narrow, tall side seats (E/W) fan vertically. The extra .card-backs class makes
   these win over the design package's looser defaults. */
/* Backs match the face-up picture-card size (46×64, the size North shows), so a
   hidden hand reads at the same scale as the visible ones. Top/bottom seats keep
   them PORTRAIT in a horizontal fan (-24px ⇒ 22px showing, mirroring the face-up
   fan). */
.card-backs .ab-cardback {
  width: 46px;
  height: 64px;
  border-radius: 6px;
}
/* Side seats (E/W): the cards face the player sitting to your side, so they read
   turned 90° — LANDSCAPE backs (the 46×64 card rotated), stacked into the
   vertical fan. */
.card-backs .ab-cardback--landscape {
  width: 64px;
  height: 46px;
  border-radius: 6px;
}
.card-backs.ab-cardback-fan .ab-cardback + .ab-cardback { margin-left: -24px; }
.card-backs.ab-cardback-fan--v .ab-cardback + .ab-cardback { margin-left: 0; margin-top: -32px; }
aside {
  display: grid;
  gap: 12px;
  align-content: start;
}
/* The drawers are dark rail panels (the play table's rail aesthetic) built on
   the design-system .ab-drawer structure: .ab-drawer is the engine-driven outer
   (dock width 0 -> --w when open, overflow-clipped), .ab-drawer__inner the
   padded, scrollable content. .app-rail supplies the dark palette every content
   rule below keys off; the outer panel look is set here. */
.app-rail { color: var(--ab-rail-text); }
.play-shell-main .ab-drawer.app-rail {
  background: var(--ab-rail);
  border: 0;
  border-right: 1px solid var(--ab-rail-border);
  border-radius: 0;
  padding: 0;
  box-shadow: var(--ab-rail-shadow);
}
.play-shell-main .ab-drawer__inner {
  padding: 12px;
  gap: 11px;
}
/* Drawer head: title + × close, on the dark rail. */
.rail-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 40px;
  margin-bottom: 2px;
}
.ab-drawer__title.rail-title {
  color: var(--ab-rail-text);
  font-family: var(--ab-font-display);
  font-weight: 600;
  font-size: 22px;
  line-height: 1.05;
  letter-spacing: -0.01em;
}
.rail-icon-button {
  width: 30px;
  min-width: 30px;
  height: 30px;
  min-height: 30px;
  padding: 0;
  border-radius: 8px;
  border: 1px solid var(--ab-rail-line);
  background: transparent;
  color: var(--ab-rail-text);
  font-size: 18px;
  line-height: 1;
}
.rail-icon-button:hover {
  background: var(--ab-rail-hover);
}
.app-rail .side-panel,
.app-rail details.side-panel {
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 0;
  color: var(--ab-rail-text);
}
.app-rail h2 {
  color: var(--ab-rail-text);
  font-size: 14px;
  margin: 0 0 8px;
}
.app-rail label {
  color: var(--ab-rail-muted);
}
.app-rail select,
.app-rail input {
  background: var(--ab-panel);
  border-color: var(--ab-rail-line);
  color: var(--ab-rail-text);
}
.rail-section {
  display: grid;
  gap: 8px;
  padding: 10px;
  border-radius: 12px;
  background: var(--ab-rail-panel-soft);
  border: 1px solid var(--ab-rail-line);
}
.rail-section.primary {
  background: var(--ab-rail-panel);
}
.rail-action {
  width: 100%;
  justify-content: start;
  min-height: 40px;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: var(--ab-rail-text);
  text-align: left;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  font-size: 14px;
  font-weight: 750;
}
.rail-action.primary {
  background: var(--ab-rail-action-primary);
}
.rail-action.secondary {
  background: transparent;
  color: var(--ab-rail-text);
}
.rail-action .rail-symbol {
  width: 22px;
  display: inline-grid;
  place-items: center;
  color: var(--ab-rail-action-accent);
  font-size: 17px;
}
/* Icon-only action row: five equal tooltip buttons on one line. */
.rail-section.action-row {
  display: flex;
  gap: 4px;
  padding: 6px;
}
.action-row .rail-action {
  flex: 1 1 0;
  min-width: 0;
  justify-content: center;
  padding: 8px 0;
}
.action-row .rail-action .rail-symbol {
  width: 18px;
  height: 18px;
  flex: none;
}
.settings-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 10px;
}
/* Settings-drawer footer: the "this table only" note + Reset to defaults, plus
   the convention-card picker button (mirrors the live pair agreement). */
.settings-note {
  margin: 2px 0 0;
  font-size: 11px;
  font-weight: 650;
  text-transform: none;
  letter-spacing: 0;
  color: var(--ab-rail-muted);
}
.reset-settings { width: 100%; }
.convention-card {
  width: 100%;
  justify-content: center;
  font-weight: 800;
}
/* Long toggle labels (e.g. "Auto-alert conventions") wrap rather than forcing
   the docked rail wider than its track — the flex text item must be allowed to
   shrink below its single-line min-content. */
.ab-toggle--block > span:not(.ab-toggle__track) {
  min-width: 0;
  overflow-wrap: anywhere;
}
/* The rails are a flex column; their sections must shrink to the docked track
   width (default flex min-width:auto would let a section's min-content — e.g. a
   wide segmented picker — overflow the rail's right edge and get clipped). With
   min-width:0 the .ab-seg pickers stay bounded by the rail and wrap to a second
   band when their options don't fit, instead of spilling past the edge. */
main.ab-play .app-rail > *,
main.ab-play .rail-section,
main.ab-play details.side-panel,
main.ab-play .settings-grid > * {
  min-width: 0;
}
/* Rail settings share one EXACT 44px row height — every row is a
   segmented picker or a switch pill — so nothing renders taller or
   shorter than its neighbours (a picker may wrap to a second 44px-ish
   band when its options overflow the rail). */
.settings-grid select {
  height: 44px;
  width: 100%;
  padding: 4px 10px;
  font-size: 16px;
  font-weight: 650;
}
/* Segmented pickers: every fixed option is visible and one tap away —
   the source <select> stays in the DOM (hidden) as the value of record. */
select.seg-source {
  display: none;
}
/* The segmented pickers (built by enhanceSegmentedSelects from each hidden
   <select>) and the full-width settings toggles are the design-system .ab-seg
   and .ab-toggle--block components — their look lives in the locked package. */
/* The picker is a 4-column grid of design-system .ab-boardtile cells (each a
   fixed 66px square), centered within the popover. */
.board-grid {
  display: grid;
  grid-template-columns: repeat(4, 66px);
  justify-content: center;
  gap: 8px;
}
.board-panel {
  display: none;
  position: absolute;
  left: 10px;
  top: 88px;
  width: min(382px, calc(100% - 20px));
  padding: 14px;
  border: 1px solid var(--ab-rail-border);
  border-radius: 14px;
  background: var(--ab-panel);
  background: color-mix(in srgb, var(--ab-panel) 82%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  color: var(--ab-ink);
  z-index: 8;
  box-shadow: 0 24px 56px -16px var(--ab-black-a45);
}
.board-panel.open {
  display: grid;
  gap: 10px;
}
.board-mode-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 8px;
  align-items: center;
}
/* Assistant drawer: a short blurb above the cheat aids. */
.assistant-blurb {
  margin: 0;
  font-size: 11px;
  font-weight: 650;
  line-height: 1.4;
  color: var(--ab-rail-muted);
}
.panel-heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 10px;
}
.panel-heading h2 { margin: 0; }
/* "Cheating" aids: controls that reveal hidden information or hand the player a
   computed answer (Bot thoughts, the Hands reveal, Double Dummy, the in-play
   bidding assistant). A red hairline + a ⚠ on the label flags them as an
   unfair-advantage aid — usable, but not fair play. The selectors are specific
   enough to beat .secondary's border-color and the toggle component's border. */
button.cheat,
.ab-toggle--block.cheat {
  border: 1.5px solid var(--ab-danger);
}
.cheat-label::before {
  content: "⚠ ";
  color: var(--ab-danger);
  font-weight: 900;
}
.assist-btn { width: 100%; justify-content: center; }
/* The assistant's suggestion drops in just under its button, red-bordered to
   keep reading as a cheat. */
.assist-result {
  display: grid;
  gap: 3px;
  padding: 8px 10px;
  border-radius: 10px;
  border: 1px solid var(--ab-danger);
  background: var(--ab-rail-panel-soft);
  color: var(--ab-rail-text);
  font-size: 12px;
  line-height: 1.35;
}
.assist-result[hidden] { display: none; }
.assist-result .assist-call { font-size: 16px; font-weight: 850; }
.assist-result .assist-meaning { color: var(--ab-rail-muted); overflow-wrap: anywhere; }
.assist-result .assist-note { color: var(--ab-rail-muted); font-style: italic; }
.insight-list {
  display: grid;
  gap: 5px;
}
.insight-card {
  display: grid;
  gap: 5px;
  padding: 7px 8px;
  border: 1px solid var(--ab-line);
  border-left: 4px solid var(--ab-insight-edge);
  border-radius: 10px;
  background: var(--ab-rail-panel-soft);
}
.app-rail .insight-card {
  border: 0;
  border-left: 4px solid var(--ab-insight-edge-rail);
  border-radius: 12px;
  background: var(--ab-rail-panel);
  color: var(--ab-rail-text);
}
.app-rail .insight-card.turn {
  border-left-color: var(--ab-gold);
  background: var(--ab-rail-turn-bg);
}
.app-rail .insight-card.viewer {
  border-left-color: var(--ab-rail-viewer-border);
}
.insight-card.turn {
  border-left-color: var(--ab-gold);
  background: var(--ab-insight-turn-bg);
}
.insight-card.viewer {
  border-left-color: var(--ab-blue);
}
/* Card header mirrors the table's seat banner: seat chip, seat word
   with the system name as a subline, role pushed to the right. */
.insight-top {
  display: flex;
  gap: 7px;
  align-items: center;
  font-weight: 800;
}
.insight-id {
  display: grid;
  min-width: 0;
  line-height: 1.15;
}
.insight-id span {
  color: var(--ab-rail-muted);
  font-size: 11px;
  font-weight: 650;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.insight-role {
  margin-left: auto;
  color: var(--ab-muted);
  font-size: 11px;
  font-weight: 750;
  text-align: right;
  white-space: nowrap;
}
.app-rail .insight-role {
  color: var(--ab-rail-muted);
}
.insight-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}
.mini-pill {
  display: inline-flex;
  min-height: 22px;
  align-items: center;
  padding: 3px 8px;
  border-radius: 999px;
  background: var(--ab-rail-hover);
  color: var(--ab-ink);
  font-size: 11px;
  font-weight: 750;
}
.app-rail .mini-pill {
  background: var(--ab-rail-hover);
  color: var(--ab-rail-text);
}
.app-rail .mini-pill.alert {
  background: var(--ab-pill-alert-bg-rail);
  color: var(--ab-pill-alert-text-rail);
}
.mini-pill.alert {
  background: var(--ab-pill-alert-bg);
  color: var(--ab-pill-alert-text);
}
.thought {
  display: grid;
  gap: 3px;
  padding: 6px;
  border-radius: 6px;
  background: var(--ab-thought-bg);
  color: var(--ab-thought-text);
  font-size: 12px;
  line-height: 1.35;
}
.app-rail .thought {
  background: var(--ab-rail-thought-bg);
  color: var(--ab-rail-text);
}
.app-rail .thought strong {
  color: var(--ab-rail-text);
}
.thought strong {
  color: var(--ab-thought-strong);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0;
}
/* Disclosed call meanings: strain/call colour now rides the .ab-auction__*
   spans (component); the table only keeps every call cell hoverable/tappable
   and marks alertable (conventional) calls with a visible corner marker. */
.ab-auction .auction-call {
  position: relative;
  cursor: pointer;
}
.ab-auction .auction-call.alertable::after {
  content: "!";
  position: absolute;
  top: 1px;
  right: 3px;
  font-size: 10px;
  font-weight: 900;
  line-height: 1;
  color: var(--ab-alert-marker);
}
.call-popover {
  display: none;
  position: fixed;
  z-index: 40;
  width: min(320px, calc(100vw - 24px));
  padding: 14px;
  border: 1px solid var(--ab-rail-border);
  border-radius: 14px;
  background: var(--ab-panel);
  background: color-mix(in srgb, var(--ab-panel) 82%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  color: var(--ab-ink);
  box-shadow: 0 24px 56px -16px var(--ab-black-a50);
}
.call-popover.open {
  display: grid;
  gap: 6px;
}
.call-popover .popover-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  font-size: 14px;
}
.call-popover .popover-close {
  min-width: 30px;
  min-height: 30px;
  padding: 2px 8px;
  background: var(--ab-panel);
  border-color: var(--ab-line);
  color: var(--ab-ink);
  font-size: 16px;
  line-height: 1;
}
.call-popover .popover-meaning {
  font-size: 13px;
  font-weight: 650;
  line-height: 1.35;
  overflow-wrap: anywhere;
}
.call-popover .popover-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}
/* Partnership agreement picker (opens from the seat name bars). */
.call-popover .strategy-options {
  display: grid;
  gap: 5px;
}
.call-popover .strategy-option {
  min-height: 36px;
  justify-content: start;
  text-align: left;
  padding: 6px 10px;
  background: var(--ab-panel);
  border: 1px solid var(--ab-line);
  border-radius: 8px;
  color: var(--ab-ink);
  font-weight: 750;
  box-shadow: none;
}
.call-popover .strategy-option:hover:not(:disabled) {
  filter: none;
  background: var(--ab-accent-soft);
}
.call-popover .strategy-option.current {
  border-color: var(--ab-felt);
  background: var(--ab-accent-soft);
  color: var(--ab-felt-dark);
}
/* Compact BBO-style entry: pick a level (1-7), then a strain. Three
   fixed rows keep the box short enough to live on the felt. */
.bid-buttons {
  display: grid;
  gap: 6px;
}
.bid-actions {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 5px;
}
.bid-matrix {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 4px;
  align-items: stretch;
}
.bid-strains {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 4px;
  align-items: stretch;
}
/* The level picker (1–7) is a bespoke pale-card variant of the .ab-bidcall
   component — the strain buttons carry the suit fills, the level buttons read
   as the neutral "pick a level" rail. */
.ab-bidcall.level-call {
  background: linear-gradient(180deg, var(--ab-white), var(--ab-level-bg2));
  border-color: var(--ab-level-border);
  color: var(--ab-suit-black);
  font-variant-numeric: tabular-nums;
}
.ab-bidcall.level-call.selected {
  background: var(--ab-felt);
  border-color: var(--ab-felt-dark2);
  color: var(--ab-white);
  box-shadow: inset 0 -1px 0 var(--ab-black-a18);
}
.ab-bidcall.level-call:disabled {
  background: var(--ab-level-disabled-bg);
  border-color: var(--ab-level-disabled-border);
  color: var(--ab-level-disabled-text);
}
/* Confirm panel: just Confirm over Cancel, popping into the felt to the RIGHT of
   the bidding box (the box is centred on the felt, so either side is open green;
   the panel opens to the right by contract) and matching the box's full height
   (top/bottom pinned). What the staged call means shows in the contract card,
   not here. Cancel clears only the staged strain (the level stays selected).
   Guarded by check_play_ui_rendered.mjs (confirm panel left >= bid-box right). */
.bid-stage {
  position: absolute;
  left: calc(100% + 10px);
  top: 0;
  bottom: 0;
  z-index: 5;
  width: 112px;
  display: grid;
  /* An explicit single full-width column is required: without it the grid falls
     back to a content-sized implicit column, so the Confirm/Cancel buttons don't
     fill the panel and sit off-centre. minmax(0,1fr) makes the column fill the
     panel and the buttons stretch to it (justify-items stretch by default). */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: 1fr 1fr;
  justify-items: stretch;
  gap: 7px;
  padding: 9px;
  border: 1px solid var(--ab-stage-border);
  border-radius: 10px;
  background: var(--ab-note-bg);
  color: var(--ab-stage-text);
  font-size: 13px;
  font-weight: 750;
  box-shadow: 0 10px 24px var(--ab-black-a30);
}
.bid-stage button {
  width: 100%;
  min-height: 0;
  height: 100%;
  padding: 4px 12px;
  font-size: 15px;
}
/* The bid buttons' surface, suit fills, pending outline, and disabled state
   are the design-system .ab-bidcall component (set on every bid button by
   renderBidButton/renderStrainButton). Only the level-picker variant above is
   table-specific. */
details.side-panel {
  padding: 0;
}
details.side-panel > summary {
  min-height: 42px;
  padding: 12px;
  cursor: pointer;
  font-weight: 800;
  list-style: none;
}
.app-rail details.side-panel > summary {
  padding: 10px;
  border-radius: 10px;
  background: var(--ab-rail-panel);
  color: var(--ab-rail-text);
}
details.side-panel > summary::-webkit-details-marker {
  display: none;
}
details.side-panel > .details-body {
  padding: 0 12px 12px;
}
.left-toggle { white-space: nowrap; }
@media (prefers-reduced-motion: no-preference) {
  @keyframes rise-in {
    from { opacity: 0; transform: translateY(14px); }
    to { opacity: 1; transform: none; }
  }
  @keyframes pop-in {
    from { opacity: 0; transform: scale(0.96) translateY(4px); }
    to { opacity: 1; transform: none; }
  }
  .play-commandbar { animation: rise-in 420ms cubic-bezier(0.22, 1, 0.36, 1) backwards; }
  .drawer-tabs { animation: rise-in 480ms cubic-bezier(0.22, 1, 0.36, 1) 60ms backwards; }
  .table { animation: rise-in 480ms cubic-bezier(0.22, 1, 0.36, 1) 120ms backwards; }
  .board-panel.open,
  .call-popover.open { animation: pop-in 260ms cubic-bezier(0.34, 1.56, 0.64, 1); }
}
/* Mid-width: tighten the edge-tab strip so the felt keeps more room. The drawers
   stay dock until 900px (the component flips them to scrim overlays below that). */
@media (max-width: 1180px) {
  main { padding: 4px 0; }
  .play-shell-main .ab-drawer { --w: 280px; }
  .drawer-tabs { padding: 12px 5px; gap: 7px; }
  .drawer-tab { padding: 14px 7px; }
}
@media (max-width: 920px) {
  .table {
    grid-template-columns: 1fr;
    grid-template-rows: auto;
    padding: 12px;
  }
  /* Stack the whole table into one column (source order): board tile, North,
     auction history, West, felt centre (box + card + trick), East, South, trick
     counts. No element keeps a side column, so the table never overflows
     horizontally on a phone. */
  .board-tile, .north, .center-auction, .west, .east, .south, .felt, .scoreline {
    grid-column: 1;
    grid-row: auto;
    align-self: auto;
    justify-self: stretch;
  }
  .felt { min-height: 300px; }
  /* Stacked single-column phone layout: the auction history flows back into the
     column (the desktop absolute-corner dock would float it over the felt). */
  .center-auction { position: static; max-height: none; overflow: visible; width: 100%; justify-self: stretch; }
  .board-tile { width: 64px; justify-self: start; }
  /* Stacked phone layout: the contract chip flows in the column (the desktop
     NW absolute dock would float over the board tile). */
  .contract-dock { position: static; left: auto; top: auto; max-width: none; min-height: 0; flex-direction: row; align-items: center; gap: 12px; justify-self: stretch; }
  .bid-box { max-width: none; }
  /* Single column: the confirm panel stacks below the box, so no centring nudge. */
  .bid-box.staging { transform: none; }
  .seat-panel.hidden-hand {
    width: 100%;
    justify-self: stretch;
  }
  .bid-stage {
    position: static;
    inset: auto;
    width: auto;
    grid-template-rows: none;
    grid-template-columns: 1fr 1fr;
  }
  .bid-stage button { min-height: 38px; height: auto; }
  .scoreline { justify-self: end; width: min(170px, 100%); }
}
/* Short viewports (laptops at 720-780px) get a compact vertical mode so
   the one-window rule — four seats + felt + bidding box without
   scrolling — holds during the auction. Same layout, tighter metrics. */
@media (max-height: 780px) {
  .play-commandbar { padding: 6px 18px; }
  .play-commandbar .ab-toggle { min-height: 36px; }
  .cmd-action:not(.cmd-action--auto) { width: 34px; min-width: 34px; height: 34px; min-height: 34px; }
  .cmd-actions .cmd-action--auto { height: 34px; min-height: 34px; padding: 0 8px 0 9px; }
  main { padding: 2px 0; }
  /* Let the felt shrink back to its compact center row on short laptops: the
     taller default middle-row floor (minmax(300px,…)) would otherwise pin the
     felt at 300px here (it stretches into the row) and break the one-window
     rule the rest of this block protects. */
  /* Tighter fixed seat bands on short laptops — still fixed (not auto) so the
     felt stays put across phases, just smaller to keep the one-window budget. */
  .table { --felt-row1: 128px; --felt-rowgap: 8px; gap: var(--felt-rowgap) 12px; grid-template-rows: var(--felt-row1) minmax(180px, 1fr) var(--felt-row1); padding: 12px; }
  /* Tighter inset on short laptops; still uniform so the banner's concentric
     corner radius (--seat-radius − --seat-pad) stays parallel to the box. */
  .seat-panel { --seat-pad: 6px; }
  .seat-banner { min-height: 26px; }
  .card-backs { min-height: 42px; }
  .suit-row { min-height: 30px; }
  .suit-label { line-height: 30px; font-size: 18px; }
  /* The felt's top row (the call-meaning preview card) is allowed to YIELD here:
     minmax(0, auto) lets the grid shrink it toward 0 under pressure, while the
     bidding box (row 3, `auto`) keeps its intrinsic height. On a short laptop the
     felt cell can be squeezed below parBox+bidBox; without this the box (pinned to
     the felt's bottom) overflowed the felt and was drawn on top of the South seat
     banner — the "south panel collides with the centre box" bug. Now the secondary
     preview card gives way and the essential box always stays inside the felt.
     Guarded by check_play_ui_rendered.mjs (a seat panel never overlaps the felt
     centre / its bidding box). */
  .felt { grid-template-rows: minmax(0, auto) minmax(0, 1fr) auto; min-height: 0; padding: 8px; }
  /* Stretch (not start) + min-height:0 + clip so the preview card's BOX tracks the
     (shrinking) row instead of keeping its content height and overflowing downward
     onto the bidding box. When the felt has room the row is sized to the card's
     content, so it looks unchanged; only when squeezed does the card give way. */
  .par-box { align-self: stretch; min-height: 0; overflow: hidden; }
  /* The trick area is a fixed-size cross whose N/S card slots are anchored to its
     top/bottom edges. On short viewports the felt centre (table grid row 2)
     shrinks below a fixed 296px, so the area used to stick out past the felt and
     into the North/South seat bands (the play-mode sibling of the auction "south
     panel collides with the centre box" bug). Cap the height to the space the
     felt actually has: `100vh − 436px` is the felt-centre height in this short
     regime (viewport minus the appbar, command bar, the two 128px seat bands and
     the table/felt gutters — measured stable to ±~4px across the 3-column width
     range because the command bar stays one row), with ~12px of headroom so the
     centred area keeps clear of both bands. So the trick can never overflow the
     felt; the graduated card-size tiers below then shrink the cards in step so the
     cross stays collision-free as the height drops. (Below 620px the seat bands
     tighten to 116px — see the next block — giving the capped trick even more
     clearance.) Guarded by check_play_ui_rendered.mjs
     (trick_no_seat_overlap_short + responsive_sweep). */
  .ab-trickarea { width: min(380px, 100%); height: min(296px, calc(100vh - 436px)); }
  .ab-bidcall { min-height: 32px; }
  .bid-box { padding: 8px; gap: 5px; }
}
/* Very short windows (≈600–620px tall — a non-maximised laptop window still wide
   enough to keep the 3-column table): at 128px bands the full-height bidding box
   no longer fits inside the squeezed felt and would poke past it toward South.
   Reclaim the last ~24px from the over-sized seat bands (a 13-card fan is only
   ~66px, so a 116px band still holds the hand + banner) and tighten the felt's
   inner gap, so the bidding box stays fully inside the felt centre. Below this the
   table restacks to one column (max-width:920) on any real phone. */
@media (max-height: 620px) {
  .table { --felt-row1: 116px; grid-template-rows: var(--felt-row1) minmax(150px, 1fr) var(--felt-row1); }
  .felt { gap: 8px; }
  .seat-banner { min-height: 24px; }
}
/* Graduated SHORT-HEIGHT trick-card tiers (wide-but-short windows, the 3-column
   layout). The min() height above keeps the trick inside the felt, but as it
   shrinks the default 70x98 cards — whose cross needs ~264px of height not to
   collide — would overlap each other. So drop the card (and its empty slot,
   rank/suit and seat-letter) size in lock-step with the falling felt, exactly as
   the max-width:480 phone block does for narrow WIDTHS. Each tier is sized for
   the felt at its own short end so the four cards keep a clear gap (asserted by
   responsive_sweep + trick_no_seat_overlap_short). Scoped to min-width:481px so
   they never touch the phone block below (which owns width<=480 at any height). */
@media (max-height: 712px) and (min-width: 481px) {
  .ab-trickarea__empty,
  .ab-trickarea .ab-card--picture {
    width: 56px; min-width: 56px; height: 78px; min-height: 78px;
  }
  .ab-trickarea .ab-card--picture .ab-card__rank { font-size: 16px; }
  .ab-trickarea .ab-card--picture .ab-card__suit { font-size: 32px; }
  .ab-trickarea__label { font-size: 22px; }
}
@media (max-height: 660px) and (min-width: 481px) {
  .ab-trickarea__empty,
  .ab-trickarea .ab-card--picture {
    width: 46px; min-width: 46px; height: 64px; min-height: 64px;
  }
  .ab-trickarea .ab-card--picture .ab-card__rank { font-size: 13px; }
  .ab-trickarea .ab-card--picture .ab-card__suit { font-size: 26px; }
  .ab-trickarea__label { font-size: 19px; }
}
@media (max-height: 624px) and (min-width: 481px) {
  .ab-trickarea__empty,
  .ab-trickarea .ab-card--picture {
    width: 40px; min-width: 40px; height: 56px; min-height: 56px;
  }
  .ab-trickarea .ab-card--picture .ab-card__rank { font-size: 11px; }
  .ab-trickarea .ab-card--picture .ab-card__suit { font-size: 23px; }
  .ab-trickarea__label { font-size: 16px; }
}
/* Small phones: the stacked felt is only ~270px wide, too narrow for three 70px
   trick cards plus the seat letters (they collide — caught by the responsive
   sweep in check_play_ui_rendered.mjs at 360px). Scale the trick down so the four
   cards keep a clear gap; the rank/suit fonts shrink with the card. */
@media (max-width: 480px) {
  .ab-trickarea { width: min(330px, 100%); height: 248px; }
  .ab-trickarea__empty,
  .ab-trickarea .ab-card--picture {
    width: 52px; min-width: 52px; height: 73px; min-height: 73px;
  }
  .ab-trickarea .ab-card--picture .ab-card__rank { font-size: 15px; }
  .ab-trickarea .ab-card--picture .ab-card__suit { font-size: 29px; }
  .ab-trickarea__label { font-size: 22px; }
}
/* --- Table Guide spotlight tour --------------------------------------- */
/* A click-catching backdrop, a cut-out ring that frames the current
   landmark (the page dims through the ring's huge spread shadow), and a
   felt-green step card with Skip / Back / Next. */
.tour-backdrop {
  position: fixed;
  inset: 0;
  z-index: 58;
  background: transparent;
}
.tour-spotlight {
  position: fixed;
  z-index: 59;
  pointer-events: none;
  border: 2px solid var(--ab-gold-bright);
  border-radius: 14px;
  box-shadow:
    0 0 0 9999px var(--ab-tourdim-a55),
    0 0 24px var(--ab-goldglow-a45);
}
.tour-card {
  position: fixed;
  z-index: 60;
  width: min(310px, calc(100vw - 24px));
  display: grid;
  gap: 6px;
  padding: 14px;
  border: 1px solid var(--ab-feltedge-a60);
  border-radius: 14px;
  background:
    radial-gradient(120% 90% at 50% 0%, var(--ab-white-a12), transparent 60%),
    linear-gradient(170deg, var(--ab-felt-hi), var(--ab-tour-grad2));
  color: var(--ab-cream);
  box-shadow: 0 24px 56px -16px var(--ab-black-a55);
}
.tour-card[hidden],
.tour-backdrop[hidden],
.tour-spotlight[hidden] {
  display: none;
}
.tour-card strong {
  font-size: 15px;
  letter-spacing: -0.005em;
}
.tour-card p {
  margin: 0;
  font-size: 13px;
  font-weight: 600;
  line-height: 1.4;
  color: var(--ab-tour-body);
}
.tour-foot {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 4px;
}
.tour-skip {
  min-height: 30px;
  padding: 3px 8px;
  border: 0;
  background: transparent;
  color: var(--ab-tour-soft);
  font-size: 12px;
  font-weight: 700;
  box-shadow: none;
}
.tour-skip:hover:not(:disabled) {
  filter: none;
  background: var(--ab-white-a12);
  box-shadow: none;
}
.tour-count {
  margin-left: auto;
  font-size: 12px;
  font-weight: 750;
  color: var(--ab-tour-soft);
  white-space: nowrap;
}
.tour-nav {
  display: inline-flex;
  gap: 6px;
}
.tour-nav button {
  min-height: 32px;
  padding: 4px 12px;
}
.tour-nav button.secondary {
  background: transparent;
  border-color: var(--ab-cream-a50);
  color: var(--ab-cream);
}
.tour-nav button.secondary:hover:not(:disabled) {
  background: var(--ab-white-a12);
}
.tour-nav #tourNext {
  background: var(--ab-cream);
  border-color: var(--ab-cream);
  color: var(--ab-tour-grad2);
  box-shadow: none;
}
@media (prefers-reduced-motion: no-preference) {
  .tour-spotlight {
    transition: top 240ms cubic-bezier(0.22, 1, 0.36, 1), left 240ms cubic-bezier(0.22, 1, 0.36, 1),
      width 240ms cubic-bezier(0.22, 1, 0.36, 1), height 240ms cubic-bezier(0.22, 1, 0.36, 1);
  }
  .tour-card {
    transition: top 240ms cubic-bezier(0.22, 1, 0.36, 1), left 240ms cubic-bezier(0.22, 1, 0.36, 1);
  }
}

/* Double-dummy analysis panel ------------------------------------------------
   Lives in a controls-rail side-panel; uses the rail palette so it reads as a
   quiet reference card, with one felt/gold accent on the optimal-card callout. */
.dd-body { display: grid; gap: 10px; }
.dd-blurb {
  margin: 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--ab-rail-muted);
}
.dd-run { width: 100%; }
.dd-status {
  margin: 0;
  font-size: 12px;
  font-style: italic;
  color: var(--ab-rail-muted);
}
.dd-bestcard {
  display: grid;
  gap: 2px;
  padding: 10px 12px;
  border: 1px solid var(--ab-rail-border);
  border-left: 3px solid var(--ab-rail-action-accent);
  border-radius: 8px;
  background: var(--ab-rail-panel-soft);
}
/* The author display above beats the UA's [hidden] { display: none }, so the
   empty best-card box rendered as a stray bordered pill under the Analyze
   button before any analysis ran. Re-assert the hidden contract. */
.dd-bestcard[hidden] {
  display: none;
}
.dd-bestcard-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ab-rail-muted);
}
.dd-bestcard-cards {
  font-size: 20px;
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--ab-rail-text);
}
.dd-bestcard-meta { font-size: 11px; color: var(--ab-rail-muted); }
.dd-par-line {
  margin: 0;
  font-size: 12px;
  font-weight: 600;
  color: var(--ab-gold);
}
.dd-grid {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.dd-grid th,
.dd-grid td {
  padding: 3px 6px;
  text-align: center;
  border-bottom: 1px solid var(--ab-rail-line);
}
.dd-grid thead th {
  color: var(--ab-rail-muted);
  font-weight: 600;
}
.dd-grid th[scope="row"] { text-align: left; color: var(--ab-rail-text); }
.dd-grid tbody td { color: var(--ab-rail-text); }
