/* ---------------------------------------------------------------------------
   RASE BRIDGE — web preview
   Palette and components mirror the native SwiftUI app (Theme.swift, dark).
--------------------------------------------------------------------------- */

:root {
  --bg: #0B1D15;
  --bg-deep: #06110C;
  --surface: #102A1F;
  --surface-raised: #163527;
  --stroke: #2C4A3B;
  --gold: #C6A353;
  --gold-soft: #E6D5A8;
  --text: #F2EFE6;
  --text-2: #9DB2A6;
  --felt-top: #14382A;
  --felt-bottom: #0A2018;
  --positive: #2E7D4F;
  --negative: #B03A34;
  --info: #2F6FBF;
  --ink-on-gold: #14261C;
  --ivory: #FFFEF6;
  /* per-suit shades: distinguishable within each colour family */
  --spade: #000000;      /* jet black */
  --club: #47525A;       /* soft slate charcoal */
  --heart: #EE1C25;      /* vivid scarlet */
  --diamond: #EF6A13;    /* warm orange-red */
  --card-blue: #1F5FBF;
  --card-green: #1E7A3C;
  --card-black: #000000;
  --card-red: #EE1C25;
  --radius: 15px;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html, body { height: 100%; overscroll-behavior: none; }

body {
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Rounded", "Segoe UI", Roboto, sans-serif;
  background: var(--bg-deep);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}

/* The setting increases reading and decision cues rather than scaling the whole
   interface, which would make a short landscape table overflow. It still has to
   be VISIBLY different — a setting whose effect you cannot see is one you cannot
   choose — so every cue the Text Size sample shows moves with it. */
body.text-large .status-bar { font-size: 14px; }
body.text-large .status-context { font-size: 14px; }
body.text-large .status-turn { font-size: 21px; }
body.text-large .conv-row .ct, body.text-large .system-option .ot { font-size: 17px; }
body.text-large .conv-row .cb, body.text-large .system-option .ob,
body.text-large .section-caption { font-size: 14px; }
body.text-large .rules-text { font-size: 16px; }
body.text-large .rs-input { font-size: 17px; }
body.text-large .rs-snip, body.text-large .rs-status { font-size: 13px; }
body.text-large .seg { font-size: 15px; }
body.text-large .result-title { font-size: 26px; }
body.text-large .result-sub, body.text-large .result-debrief { font-size: 15px; }
body.text-large .tc-pill .l { font-size: 13px; }
body.text-large .tc-pill .v { font-size: 22px; }

/* A live sample of the two lines the table leads with, so choosing a size shows
   what it does instead of describing it. */
.text-sample {
  margin-top: 10px; padding: 10px 12px;
  background: rgba(0,0,0,.25); border: 1px solid rgba(44,74,59,.6); border-radius: 11px;
}
.ts-strip { display: flex; flex-direction: column; gap: 1px; }
.ts-context { font-size: 12px; font-weight: 600; color: rgba(230,213,168,.72); }
.ts-turn { font-size: 17px; font-weight: 800; color: var(--gold); }
.ts-note { margin-top: 7px; font-size: 12px; color: var(--text-2); line-height: 1.4; }
body.text-large .ts-context { font-size: 14px; }
body.text-large .ts-turn { font-size: 21px; }
body.text-large .ts-note { font-size: 14px; }
body.text-large .seat-badge { font-size: 12px; }
body.text-large .bp-profile { font-size: 17px; }
body.text-large .bp-badge, body.text-large .bp-hcp { font-size: 14px; }
body.text-large .ag-head { font-size: 14px; }
body.text-large .ag-cell { font-size: 20px; }
body.text-large .meaning-text, body.text-large .meaning-hint, body.text-large .bp-waiting { font-size: 14px; }
body.text-large .hint-pill { font-size: 13px; }
body.text-large .user-hand .pcard .idx { font-size: calc(var(--uw) * 0.38); }
body.text-large .dummy-cols .pcard .idx { font-size: calc(var(--dw) * 0.46); }

#app { height: 100dvh; display: flex; flex-direction: column; }
/* Above 1100px the app stops growing and centres instead. A card table that
   keeps stretching is not a bigger table — it is the same table with
   everything on it pulled further apart, and past about 1200px the fan, the
   trick and the bidding panel stop reading as one object. 1180px is the width
   the widest layout in this file was drawn for: the landscape rail, a table
   plus a 400px panel beside it. The deep background carries the rest.
   Everything inside is sized from .table-area's container query rather than
   from vw, so the cards come down with the frame — a vw-derived card inside a
   capped frame (15vw of a 1920px window in a 1180px table) is precisely the
   bug this cap would otherwise introduce. */
@media (min-width: 1100px) {
  #app { width: 100%; max-width: 1180px; margin-inline: auto; }
}
.screen { flex: 1; display: flex; flex-direction: column; min-height: 0; }

button { font-family: inherit; cursor: pointer; border: none; background: none; color: inherit; }
button:disabled { cursor: default; }

.brand-font { font-weight: 800; letter-spacing: .3px; }

/* ---------------------------------------------------------------- brand mark

   js/brand.js draws the logo as inline SVG without any fills of its own, so the
   mark is coloured from the palette above and stays in step with the theme.   */

.rase-mark, .rase-word, .rase-club, .rase-tile { display: block; overflow: visible; }
.mk-club { fill: var(--gold); }
.mk-r, .mk-word { fill: var(--ivory); }

/* The gold-ruled tile from the app icon, reused wherever the app signs its name. */
.rase-tile .tile-face {
  fill: var(--surface);
  stroke: var(--gold);
}

/* A hairline with the club set into it — a divider that carries the brand. */
.club-rule { display: flex; align-items: center; gap: 10px; width: min(300px, 70%); }
.club-rule span { flex: 1; height: 1px; background: rgba(198,163,83,.45); }
.club-rule span:first-child { background: linear-gradient(90deg, transparent, rgba(198,163,83,.5)); }
.club-rule span:last-child { background: linear-gradient(90deg, rgba(198,163,83,.5), transparent); }
.club-rule .rase-club { opacity: .85; }

/* The mark standing in for the app's name in a screen title bar. */
.top-brand { width: 36px; display: flex; justify-content: center; opacity: .7; flex-shrink: 0; }

/* Faint mark for empty states and the felt. */
.mark-ghost .mk-r { fill: rgba(255,254,246,.62); }
.mark-ghost .mk-club { fill: rgba(198,163,83,.72); }

/* ---------------------------------------------------------------- top bar */

.top-bar {
  display: flex; align-items: center; gap: 8px;
  padding: max(10px, env(safe-area-inset-top)) 12px 8px;
  flex-shrink: 0;
}
.top-bar h2 { font-size: 17px; font-weight: 700; flex: 1; text-align: center; color: var(--text); }
.top-spacer { width: 36px; }

.icon-btn {
  width: 36px; height: 36px; border-radius: 10px;
  background: rgba(255,255,255,.08);
  color: var(--gold-soft);
  font-size: 17px; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.icon-btn:active { background: rgba(255,255,255,.18); }

/* ---------------------------------------------------------------- home */

.home-screen {
  background: linear-gradient(180deg, var(--bg) 0%, var(--bg-deep) 100%);
  align-items: center;
  padding: 24px 20px;
  overflow-y: auto;
  justify-content: flex-start;
  gap: 26px;
}
.home-hero {
  text-align: center; padding-top: 30px;
  display: flex; flex-direction: column; align-items: center; gap: 15px;
}
.home-hero .rase-tile { filter: drop-shadow(0 10px 26px rgba(0,0,0,.5)); }
.home-hero h1 { line-height: 0; }
.tagline { font-size: 14px; font-weight: 600; color: var(--text-2); }
.home-sub { margin-top: 5px; font-size: 12px; color: var(--text-2); opacity: .8; }

.chips-row { display: flex; gap: 10px; width: min(430px, 100%); }
.chip {
  flex: 1; text-align: center; padding: 11px 4px;
  background: var(--surface); border: 1px solid var(--stroke); border-radius: 12px;
}
.chip .chip-value { font-size: 20px; font-weight: 800; color: var(--gold); }
.chip .chip-title { font-size: 10px; font-weight: 600; color: var(--text-2); margin-top: 3px; letter-spacing: .3px; }

.home-buttons { display: flex; flex-direction: column; gap: 12px; width: min(430px, 100%); }

.btn-primary {
  border-radius: 16px; padding: 15px 20px; text-align: center;
  background: linear-gradient(135deg, var(--gold-soft), var(--gold));
  color: var(--ink-on-gold);
  box-shadow: 0 5px 18px rgba(198,163,83,.28);
  transition: transform .06s ease;
}
.btn-primary:active { transform: scale(.98); }
.btn-primary .bt { font-size: 17px; font-weight: 800; }
.btn-primary .bs { font-size: 12px; font-weight: 600; opacity: .72; margin-top: 3px; }

.menu-row {
  display: flex; align-items: center; gap: 13px;
  background: var(--surface); border: 1px solid var(--stroke); border-radius: var(--radius);
  padding: 13px; text-align: left; width: 100%;
  transition: transform .06s ease;
}
.menu-row:active { transform: scale(.985); }
.menu-icon {
  width: 42px; height: 42px; border-radius: 11px; flex-shrink: 0;
  background: rgba(198,163,83,.12); color: var(--gold);
  display: flex; align-items: center; justify-content: center; font-size: 19px;
}
.menu-row .mt { font-size: 15px; font-weight: 700; color: var(--text); }
.menu-row .ms { font-size: 11.5px; font-weight: 500; color: var(--text-2); margin-top: 2px; }
.menu-chevron { margin-left: auto; color: var(--text-2); opacity: .6; font-size: 15px; font-weight: 700; }

/* The one .menu-row that is a real link rather than a dispatched action: on the
   web, /play/ is a page of a website and has to lead back out of itself. An <a>
   carries browser defaults a <button> does not, so they are undone here rather
   than by reaching for a button that fakes navigation. */
a.menu-row { text-decoration: none; color: inherit; box-sizing: border-box; }

.home-footer { font-size: 12px; color: var(--text-2); opacity: .7; }
/* Quieter than the footer above it: the way out of the app is worth being able
   to find and not worth being the first thing a player sees on the home screen. */
.home-site-link {
  font-size: 12px; color: var(--text-2); opacity: .75; text-decoration: none;
  margin-top: 8px; padding: 6px 12px; border-radius: 20px;
  border: 1px solid var(--stroke);
}
.home-site-link:active { background: rgba(255,255,255,.06); }
.home-version { font-size: 11px; color: var(--text-2); opacity: .5; padding-bottom: 18px; margin-top: 4px; letter-spacing: .3px; }

.about-card {
  padding: 22px 14px 10px;
  display: flex; flex-direction: column; align-items: center; gap: 9px; text-align: center;
}
.about-card .about-tag { font-size: 12.5px; color: var(--text-2); font-style: italic; }
.about-card .about-meta { font-size: 11.5px; color: var(--text-2); opacity: .6; }
/* third-party attribution: quieter than the app's own metadata, and allowed to wrap */
.about-card .credit { max-width: 300px; line-height: 1.45; opacity: .45; }
.about-card .club-rule { margin: 3px 0; }

/* ---------------------------------------------------------------- shared cards/panels */

.section-card {
  background: var(--surface); border: 1px solid var(--stroke); border-radius: var(--radius);
  padding: 14px; width: 100%;
}
.section-card h3 { font-size: 15px; font-weight: 800; color: var(--gold); margin-bottom: 8px; }

/* An empty state signs itself with the mark instead of leaving a bare sentence. */
.section-card.brand-empty {
  padding: 24px 16px 20px;
  display: flex; flex-direction: column; align-items: center; text-align: center; gap: 9px;
}
.section-card.brand-empty h3, .section-card.brand-empty .section-caption { margin: 0; }
.section-caption { font-size: 11.5px; color: var(--text-2); margin-bottom: 12px; line-height: 1.5; }

.scroll-body {
  flex: 1;
  min-height: 0;               /* allow the flex child to shrink so overflow scrolls */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 12px 16px calc(28px + env(safe-area-inset-bottom));
  display: flex; flex-direction: column; gap: 14px;
  background: var(--bg);
}
/* Keep cards at their natural height so tall content overflows (and scrolls)
   instead of being squished by the flex column. */
.scroll-body > * { flex-shrink: 0; }

/* ---------------------------------------------------------------- settings */

.system-option {
  display: flex; align-items: center; gap: 12px;
  background: rgba(255,255,255,.03);
  border: 1px solid var(--stroke); border-radius: 11px;
  padding: 11px; margin-bottom: 8px; width: 100%; text-align: left;
}
.system-option.selected { background: rgba(198,163,83,.12); border-color: rgba(198,163,83,.6); }
.system-option .ot { font-size: 14px; font-weight: 700; color: var(--text); }
.system-option .ob { font-size: 11.5px; color: var(--text-2); margin-top: 2px; line-height: 1.45; }
.radio-dot {
  margin-left: auto; width: 20px; height: 20px; border-radius: 50%;
  border: 2px solid var(--gold); flex-shrink: 0; position: relative;
}
.system-option.selected .radio-dot::after {
  content: ""; position: absolute; inset: 3px; border-radius: 50%; background: var(--gold);
}

.conv-row {
  display: flex; align-items: center; gap: 12px; padding: 10px 0;
  border-bottom: 1px solid rgba(44,74,59,.5);
}
.conv-row:last-child { border-bottom: none; }
.conv-row .ct { font-size: 13px; font-weight: 600; color: var(--text); }
.conv-row .cb { font-size: 11px; color: var(--text-2); margin-top: 2px; line-height: 1.4; }
.conv-text { flex: 1; }

.segmented {
  display: flex; gap: 4px; margin-top: 9px;
  background: rgba(255,255,255,.05); border-radius: 9px; padding: 3px;
  width: fit-content;
}
.seg {
  padding: 6px 13px; border-radius: 7px;
  font-size: 12px; font-weight: 700; color: var(--text-2);
}
.seg.sel { background: var(--gold); color: var(--ink-on-gold); }

.toggle {
  width: 46px; height: 28px; border-radius: 14px;
  background: #2a4335; position: relative; flex-shrink: 0;
  transition: background .15s ease; cursor: pointer; border: none;
}
.toggle::after {
  content: ""; position: absolute; top: 2px; left: 2px;
  width: 24px; height: 24px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.35);
  transition: left .15s ease;
}
.toggle.on { background: var(--gold); }
.toggle.on::after { left: 20px; }

/* ---------------------------------------------------------------- rules

   Two levels of native <details>: a chapter is the card, and its sections are
   hairline-separated rows inside it. The whole library therefore fits one
   scroll of chapter headings, with any single answer two taps away.          */

.rules-chapter {
  background: var(--surface); border: 1px solid var(--stroke);
  border-radius: var(--radius); overflow: hidden;
}
.rules-chapter > summary {
  display: flex; align-items: center; gap: 10px;
  padding: 14px 34px 14px 16px; cursor: pointer; list-style: none; position: relative;
}
.rules-chapter > summary::-webkit-details-marker { display: none; }
.rules-chapter > summary::after {
  content: "›"; position: absolute; right: 14px; top: 50%;
  font-size: 20px; line-height: 0; color: var(--text-2);
  transform: translateY(-50%) rotate(90deg); transition: transform .15s;
}
.rules-chapter[open] > summary::after { transform: translateY(-50%) rotate(-90deg); }
.rules-chapter[open] > summary { border-bottom: 1px solid var(--stroke); }
.rc-head { flex: 1; min-width: 0; }
.rc-title { display: block; font-size: 15px; font-weight: 800; color: var(--gold); }
.rc-caption { display: block; font-size: 11.5px; line-height: 1.45; color: var(--text-2); margin-top: 2px; }
.rc-count {
  flex: none; min-width: 22px; text-align: center; padding: 2px 6px;
  border-radius: 9px; background: var(--surface-raised); border: 1px solid var(--stroke);
  font-size: 11px; font-weight: 700; color: var(--text-2);
}

.rules-section {
  background: var(--surface); border: 1px solid var(--stroke);
  border-radius: var(--radius); overflow: hidden;
}
/* Inside a chapter the section is a row, not a card of its own. */
.rules-chapter .rules-section {
  border: none; border-radius: 0; background: none;
}
.rules-chapter .rules-section + .rules-section { border-top: 1px solid var(--stroke); }
.rules-section summary {
  padding: 14px 16px; font-weight: 700; font-size: 15px; color: var(--text);
  cursor: pointer; list-style: none; position: relative;
}
.rules-chapter .rules-section summary { font-size: 14px; padding: 12px 34px 12px 16px; }
.rules-section summary::-webkit-details-marker { display: none; }
.rules-section summary::after {
  content: "›"; position: absolute; right: 16px; top: 11px;
  font-size: 20px; color: var(--text-2); transform: rotate(90deg); transition: transform .15s;
}
.rules-chapter .rules-section summary::after { right: 14px; top: 9px; }
.rules-section[open] summary::after { transform: rotate(-90deg); }
.rules-section[open] > summary { color: var(--gold-soft); }
.rules-text { padding: 2px 16px 16px; font-size: 13.5px; line-height: 1.62; color: var(--text-2); }
.rules-text p { margin-bottom: 10px; }
.rules-text strong, .rules-text em { color: var(--text); }
.rules-text ul { margin: 0 0 10px 18px; }
.rules-text li { margin-bottom: 7px; }
.rules-table { overflow-x: auto; -webkit-overflow-scrolling: touch; margin: 6px 0 12px; }
.rules-text table { border-collapse: collapse; margin: 6px 0 12px; width: 100%; }
.rules-table table { margin: 0; min-width: 100%; }
.rules-text th, .rules-text td { border: 1px solid var(--stroke); padding: 6px 10px; font-size: 12.5px; text-align: left; }
.rules-text th { background: var(--surface-raised); color: var(--gold-soft); }

/* Search sits between the bar and the scroll, so it stays put while results
   move under it. Filtering is done in the DOM by ui.js rather than by a
   re-render, which would replace the field mid-word. */
.rules-search {
  display: flex; align-items: center; gap: 8px; flex-shrink: 0;
  margin: 0 16px; padding: 0 10px;
  background: var(--surface); border: 1px solid var(--stroke); border-radius: 11px;
}
.rules-search:focus-within { border-color: var(--gold); }
.rs-icon { flex: none; font-size: 13px; opacity: .55; }
.rs-input {
  flex: 1; min-width: 0; padding: 10px 0;
  background: none; border: none; outline: none; -webkit-appearance: none;
  font-family: inherit; font-size: 15px; color: var(--text);
  user-select: text; -webkit-user-select: text;
}
.rs-input::placeholder { color: var(--text-2); opacity: .8; }
.rs-input::-webkit-search-cancel-button { display: none; }   /* .rs-clear is ours */
.rs-clear {
  flex: none; width: 26px; height: 26px; border-radius: 50%;
  background: var(--surface-raised); color: var(--text-2); font-size: 11px;
  display: inline-flex; align-items: center; justify-content: center;
}
.rs-clear:active { background: rgba(255,255,255,.18); }
.rs-status { flex-shrink: 0; margin: 8px 18px 0; font-size: 11.5px; color: var(--text-2); }

/* Filtered-out chapters and sections, and the excerpt under a matched heading. */
.rules-chapter[hidden], .rules-section[hidden], .rs-clear[hidden],
.rs-status[hidden], .rs-snip[hidden], .about-card[hidden], .rs-empty[hidden] { display: none; }
.rs-t { display: block; }
.rs-snip {
  display: block; margin-top: 4px; padding-right: 4px;
  font-size: 11.5px; font-weight: 400; line-height: 1.5; color: var(--text-2);
}
.rules-text mark, .rs-snip mark {
  background: rgba(198,163,83,.26); color: var(--gold-soft);
  border-radius: 3px; padding: 0 1px;
}

/* ---------------------------------------------------------------- game table */

.game-screen {
  /* The auction hand's card width. The panel below sits clear of the hand by
     deriving its offset from this, rather than a px guess that only holds on
     one screen size. Do NOT raise: 13 overlapped cards are 6.04 card-widths,
     so 16.5vw is exactly one screen wide. */
  --uw-bid: clamp(52px, 16.5vw, 84px);
  /* The player's card width in PLAY, hoisted to the screen so the dummy can be
     sized as a proportion of it rather than by an independent px guess. Those
     two drifted apart badly — a 120px player card next to a 58px dummy is a
     hand you can read beside one you cannot. Dummy now derives from this. */
  --card-player: clamp(52px, 15vw, 78px);
  --card-dummy: clamp(52px, calc(var(--card-player) * 0.84), 88px);
  background: radial-gradient(120% 90% at 50% 38%, var(--felt-top) 0%, var(--felt-bottom) 78%);
}

/* Two-line status strip. Line 1 is the standing context (deal, dealer,
   vulnerability); line 2 is the one thing the player needs right now — the
   contract and whose move it is — set larger and brighter so the current
   decision is readable without parsing the four seat badges. */
.status-bar {
  display: flex; align-items: center; gap: 8px;
  margin: 0 10px; padding: 7px 13px;
  background: rgba(0,0,0,.25);
  border: 1px solid rgba(44,74,59,.6);
  border-radius: 11px;
  color: rgba(230,213,168,.9);
  width: calc(100% - 20px);
  text-align: left;
}
.status-lines { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.status-context { font-size: 12px; font-weight: 600; color: rgba(230,213,168,.72); }
.status-turn {
  font-size: 17px; font-weight: 800; color: var(--gold);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.status-turn:empty { display: none; }
.status-chev { color: rgba(230,213,168,.6); font-size: 11px; flex: none; }

.table-area {
  flex: 1; display: flex; flex-direction: column;
  justify-content: space-between;
  padding: 6px 8px 2px; min-height: 0; gap: 2px; position: relative;
  /* Contain the z-index:1 below. Without this the table's children are stacked
     against the SCREEN, not against the table, so .table-middle painted over
     the absolutely-positioned .bidding-panel (z-index auto) and every bid
     control became untappable on phone portrait. Isolating keeps that 1 local
     and costs nothing else. */
  isolation: isolate;
}

/* One decorative watermark belongs to the table, not to the trick.  Keeping it
   in this absolute layer means rotation, a completed trick, or the bidding
   panel can never move it or create a second overlapping mark.

   Its position is fixed to the TOP of the table rather than centred on it:
   .table-area changes height between bidding and play (the composer takes the
   lower half), so a centred mark slid up and down as the hand progressed. An
   anchor that does not move is the point of a watermark. The size is fixed for
   the same reason — a vw-derived width redrew it on every rotation. */
.table-watermark {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 240px; pointer-events: none; opacity: .04; z-index: 0;
}
/* The mark lives inside .table-middle, so "centred" means centred on the band
   that holds the trick — which is the same thing in every layout, however the
   table is arranged around it. Anchored to .table-area instead it drifted with
   the table's own height: 9px off the cross on a phone, 69px in landscape and
   157px on a tablet, where it read as a smudge below the cards. */
.table-middle { position: relative; }
.table-middle > :not(.table-watermark) { position: relative; z-index: 1; }
.table-watermark .rase-mark { width: 100%; height: auto; }
.table-area > :not(.table-watermark) { position: relative; z-index: 1; }
.table-middle { display: flex; flex: 1; align-items: center; min-height: 0; }
/* The bidding panel occupies the lower table. Keep the side-seat labels in the
   clear upper region instead of centring them into that panel. */
.game-screen.is-bidding .table-middle { align-items: flex-start; padding-top: 12px; }
/* --tw lives here, not on .trick-area, so the felt print resolves to the same
   width during the auction (no trick on the table) as it does in play. */
.table-center {
  flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 8px; min-width: 0; position: relative; --tw: 196px;
}

/* Both centrings are declared twice, plain and `safe`. When a seat's contents
   are taller than the band it stands in — thirteen card backs in a short
   window, four dummy fans in a squat one — plain centring spends half the
   overflow ABOVE the box, where it is either clipped or painted over the
   neighbour. That is what sliced "West · Robot" in half at 1280x800 and
   printed it across the dummy's cards at 820x825. `safe` falls back to
   flex-start in exactly that case, so the badge stays whole and the overflow
   is spent at the far end of the stack, where it costs nothing. A browser
   without the keyword drops the second declaration and keeps the first. */
.seat-zone {
  display: flex; flex-direction: column;
  align-items: center; align-items: safe center;
  gap: 4px; min-height: 0;
}
.seat-W, .seat-E {
  width: 74px; flex-shrink: 0;
  justify-content: center; justify-content: safe center;
}
.seat-zone.bare { gap: 0; }
.seat-W.bare, .seat-E.bare { width: auto; }

.seat-badge {
  font-size: 10.5px; font-weight: 700; color: var(--text-2);
  background: rgba(0,0,0,.28);
  padding: 4px 10px; border-radius: 20px;
  display: inline-flex; align-items: center; gap: 5px;
  white-space: nowrap;
}
.seat-badge .dot { width: 6px; height: 6px; border-radius: 50%; background: rgba(157,178,166,.4); }
/* whose turn: the active seat's badge flips to solid gold so the side to play
   is unmistakable at a glance */
.seat-badge.active { color: var(--ink-on-gold); background: var(--gold); font-weight: 800; }
/* side (E/W) zones are narrow: let the badge text wrap instead of clipping */
.seat-W .seat-badge, .seat-E .seat-badge {
  white-space: normal; max-width: 100%;
  text-align: center; line-height: 1.4; border-radius: 12px;
}
.seat-badge.active .dot { background: var(--ink-on-gold); box-shadow: none; animation: turnpulse 1.2s ease-in-out infinite; }
@keyframes turnpulse { 50% { opacity: .25; } }

/* playing cards */
.pcard {
  background: var(--ivory);
  border-radius: 8px;
  border: 1px solid rgba(0,0,0,.14);
  box-shadow: 0 1.5px 3px rgba(0,0,0,.3);
  position: relative;
  display: inline-block;
  color: var(--card-black);
  flex-shrink: 0;
}
.pcard .idx {
  position: absolute; top: 5%; left: 8%;
  display: flex; flex-direction: column; align-items: center; line-height: 1.02;
  font-weight: 800;
}
.pcard .pip {
  position: absolute; left: 55%; top: 58%; transform: translate(-50%, -50%);
  opacity: .85;
}
.pcard.hint-glow {
  border: 2.5px solid var(--gold);
  box-shadow: 0 0 12px rgba(198,163,83,.9);
}
.pcard.dimmed { opacity: .45; }
.pcard:disabled { cursor: default; }

.card-back {
  background: linear-gradient(135deg, #1C4534 0%, #0E2A1E 100%);
  border-radius: 6px;
  border: 1px solid rgba(0,0,0,.25);
  box-shadow: 0 1px 3px rgba(0,0,0,.3), inset 0 0 0 1.5px rgba(198,163,83,.5);
  flex-shrink: 0;
}

.hidden-hand { display: flex; }
.hidden-hand .card-back { width: 32px; height: 46px; margin-left: -21px; }
.hidden-hand .card-back:first-child { margin-left: 0; }
.hidden-hand.vertical { flex-direction: column; }
.hidden-hand.vertical .card-back { width: 44px; height: 30px; margin-left: 0; margin-top: -19px; }
.hidden-hand.vertical .card-back:first-child { margin-top: 0; }

/* user hand row (bidding: single compact row) */
.user-hand { --uw: clamp(52px, 14.5vw, 72px); display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 3px 2px 4px; }
.user-hand .hand-line-row { display: flex; justify-content: center; }
/* bidding: the 13-card row spans the full screen width (13 overlapped cards
   ≈ 6.04 card-widths, so 16.5vw fills 100vw once the side padding is cancelled) */
.user-hand:not(.play2) {
  flex-direction: row;
  --uw: var(--uw-bid);
  margin: 0 -8px;                 /* cancel .table-area side padding */
  padding: 4px 0 5px;
}
.user-hand .pcard {
  width: var(--uw);
  height: calc(var(--uw) * 1.45);
  margin-left: calc(var(--uw) * -0.58);
  transition: transform .15s ease;
}
.user-hand .pcard:first-child,
.user-hand .hand-line-row .pcard:first-child { margin-left: 0; }
.user-hand .pcard .idx { font-size: calc(var(--uw) * 0.34); }
/* "10" is two characters wide — shrink and nudge it so the overlapping next
   card never covers the value (visible strip in the bidding fan = 0.42 uw) */
.user-hand:not(.play2) .pcard.r10 .idx { font-size: calc(var(--uw) * 0.28); left: 5%; }
.user-hand .pcard .pip { font-size: calc(var(--uw) * 0.5); }
/* A card in a fan shows only its left strip, but .pip is centred at 55% of the
   card — far enough right to be covered by the next card, near enough that a
   sliver of it leaks into the strip. Thirteen of those slivers read as a row
   of smudges under the ranks, and the larger the cards get the more they look
   like a rendering fault: at 820x825 the fan is 115px cards and each leak is a
   10px half-moon sitting on the index above it. The index already carries rank
   AND suit, so the centre pip belongs only to a card whose face is actually
   exposed — the last of a fan, or one followed by a suit gap. Only the play
   fan has suit gaps: the auction packs all thirteen at the same 58% overlap,
   so there the exemption would leave a sliver behind on the last card of every
   suit. The dummy's vertical columns have always worked this way
   (`.dummy-cols .pcard .pip` is display:none); this is the same rule for the
   fans that overlap sideways. */
.user-hand:not(.play2) .pcard:has(+ .pcard) .pip { display: none; }
.user-hand.play2 .pcard:has(+ .pcard:not(.suit-gap)) .pip { display: none; }
.dummy-cols:not(.side) .dummy-col .pcard:has(+ .pcard) .pip { display: none; }
.user-hand .pcard.playable:active { transform: translateY(-10px); }
.user-hand .pcard.hint-glow { transform: translateY(-8px); z-index: 2; }

/* play mode: two rows → bigger cards than bidding, less overlap, suits grouped */
.user-hand.play2 { --uw: var(--card-player); gap: 4px; }
.user-hand.play2 .pcard { margin-left: calc(var(--uw) * -0.46); }
.user-hand.play2 .pcard.suit-gap { margin-left: calc(var(--uw) * -0.1); }

/* dummy columns (per suit, stacked downward)
   --dw is the dummy card width and carries the brief's floor: 52px on phone,
   70px on tablet (raised in the 700px query below). Dummy may be smaller than
   the player's hand, never illegibly so — a hand you have to squint at is one
   you misplay. */
.dummy-cols { --dw: var(--card-dummy); display: flex; align-items: flex-start; gap: 6px; }
.dummy-col { display: flex; flex-direction: column; }
/* overlap tuned so each card's rank+suit index stays fully visible (not clipped) */
.dummy-col .pcard { margin-top: calc(var(--dw, 42px) * -0.82); }
.dummy-col .pcard:first-child { margin-top: 0; }
.dummy-cols .pcard {
  width: var(--dw, 42px); height: calc(var(--dw, 42px) * 1.45);
}
/* horizontal index (rank beside suit) fits the visible strip without overlap */
.dummy-cols .pcard .idx {
  flex-direction: row; align-items: center; gap: calc(var(--dw, 42px) * 0.04);
  top: 8%; left: 12%; font-size: calc(var(--dw, 42px) * 0.42); font-weight: 800;
}
.dummy-cols .pcard .idx span:last-child { font-size: calc(var(--dw, 42px) * 0.46); }
/* the two-character "10" index shrinks so rank and suit both stay on the card */
.dummy-cols .pcard.r10 .idx { left: 6%; font-size: calc(var(--dw, 42px) * 0.34); gap: calc(var(--dw, 42px) * 0.02); }
.dummy-cols .pcard.r10 .idx span:last-child { font-size: calc(var(--dw, 42px) * 0.40); }
.dummy-cols .pcard .pip { display: none; }
/* side (E/W) dummy: wrap suits into a 2×2 grid so it fits the narrow seat zone.
   The width was a flat 33px, which is what a dummy sitting East or West still
   rendered at in portrait — two thirds of the 52px floor and barely legible.
   It now takes the same token as the top tray, and the seat zone widens to
   hold two columns of it. */
.dummy-cols.side {
  --dw: var(--card-dummy);
  flex-wrap: wrap;
  width: calc(var(--dw) * 2 + 6px);
  justify-content: center;
}
.seat-W:has(.dummy-cols.side), .seat-E:has(.dummy-cols.side) {
  width: calc(var(--card-dummy) * 2 + 14px);
}
/* A phone cannot afford a widened dummy tray AND a full-size column of card
   backs opposite it — between them they squeezed the trick under its own
   minimum. The backs stay, but as a SILHOUETTE: a narrow column the play
   layouts below size and place. Only short landscape, where the height is
   genuinely gone, drops them entirely. */
@media (max-width: 699px) {
  /* A narrow column, not `auto`: left to size itself the badge grew into the
     middle and sat on a played card. This reserves just enough for the name. */
  .game-screen:not(.is-bidding) .seat-zone.is-hidden.seat-W,
  .game-screen:not(.is-bidding) .seat-zone.is-hidden.seat-E { width: 58px; }
  .game-screen:not(.is-bidding) .seat-zone.is-hidden .seat-badge {
    font-size: 11px; padding: 4px 7px; text-align: center;
  }
}

/* trick area — every measurement derives from --tw so the whole cross scales
   with one variable (phones keep the original 196px geometry) */
.trick-area { position: relative; width: var(--tw); height: calc(var(--tw) * 0.878); }
.trick-card {
  position: absolute; width: calc(var(--tw) * 0.286); height: calc(var(--tw) * 0.408);
  background: var(--ivory); border-radius: 8px; border: 1px solid rgba(0,0,0,.15);
  box-shadow: 0 2.5px 7px rgba(0,0,0,.4);
}
.trick-card .tc-idx {
  position: absolute; top: 6%; left: 9%;
  font-weight: 800; font-size: calc(var(--tw) * 0.097); line-height: 1;
}
.trick-card .tc-pip {
  position: absolute; left: 56%; top: 60%; transform: translate(-50%, -50%);
  font-size: calc(var(--tw) * 0.173); line-height: 1;
}
/* Entry motion belongs to the card that has just ARRIVED, never to the trick.
   The whole view re-renders on every action, so an animation on .trick-card
   itself restarted on all four cards every time anything happened: the trick
   flickered as a unit and said nothing about who had just played. That is why
   it was cut back to a bare fade — the fade was not the fix, the re-render
   was. render() now diffs the trick against the previous frame and tags only
   the newly played card with `tc-new`, so the motion runs once, on one card.

   Each card slides in from the edge its own seat sits at, which is the cue the
   cross exists to give: the card comes FROM the hand that played it. Every
   keyframe carries its position's rotation, because a transform keyframe
   replaces the .pos-* transform outright — a plain translate would drop the
   tilt for the length of the animation and snap it back at the end. The
   travel is a percentage of the card, so it scales with --tw and stays inside
   the layout the way the old scale(1.18) did not. */
.trick-card.tc-new {
  animation-duration: .26s;
  animation-timing-function: cubic-bezier(.22,.85,.3,1);
}
.trick-card.tc-new.pos-N { animation-name: tcInN; }
.trick-card.tc-new.pos-S { animation-name: tcInS; }
.trick-card.tc-new.pos-W { animation-name: tcInW; }
.trick-card.tc-new.pos-E { animation-name: tcInE; }
@keyframes tcInN { from { opacity: 0; transform: translateY(-80%) rotate(-3deg); } }
@keyframes tcInS { from { opacity: 0; transform: translateY(80%) rotate(3deg); } }
@keyframes tcInW { from { opacity: 0; transform: translateX(-90%) rotate(-6deg); } }
@keyframes tcInE { from { opacity: 0; transform: translateX(90%) rotate(6deg); } }
/* Reduce Motion keeps the arrival — the card appears, it just does not travel. */
@keyframes trickIn { from { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .trick-card.tc-new.pos-N, .trick-card.tc-new.pos-S,
  .trick-card.tc-new.pos-W, .trick-card.tc-new.pos-E { animation-name: trickIn; }
}
.trick-card.pos-N { top: 0; left: calc(var(--tw) * 0.357); transform: rotate(-3deg); }
.trick-card.pos-S { bottom: 0; left: calc(var(--tw) * 0.357); transform: rotate(3deg); }
.trick-card.pos-W { top: calc(var(--tw) * 0.235); left: calc(var(--tw) * 0.026); transform: rotate(-6deg); }
.trick-card.pos-E { top: calc(var(--tw) * 0.235); right: calc(var(--tw) * 0.026); transform: rotate(6deg); }

.trick-counters { display: flex; gap: 10px; }
.tc-pill {
  display: flex; align-items: center; gap: 5px;
  padding: 4px 11px; border-radius: 20px; background: rgba(0,0,0,.3);
}
.tc-pill .l { font-size: 10px; font-weight: 700; color: var(--text-2); }
.tc-pill .v { font-size: 15px; font-weight: 800; color: var(--gold-soft); }

/* suit colours */
.s-S { color: var(--spade); }
.s-C { color: var(--club); }
.s-H { color: var(--heart); }
.s-D { color: var(--diamond); }
.fourcolor .s-D { color: var(--card-blue); }
.fourcolor .s-C { color: var(--card-green); }

/* suit symbols inline in text on dark surfaces (auction grid, review tables):
   the card-face shades disappear against the felt, so use brighter ones */
.cs { font-weight: 800; }
.cs.s-S { color: var(--text); }
.cs.s-C { color: #9FB0AE; }
.cs.s-H { color: #F1615A; }
.cs.s-D { color: #EFA15A; }
.fourcolor .cs.s-D { color: #6FA0F0; }
.fourcolor .cs.s-C { color: #62C287; }

/* hint pill */
.hint-pill {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 4px 11px; border-radius: 20px;
  background: rgba(198,163,83,.22);
  border: 1px solid rgba(198,163,83,.55);
  color: var(--gold-soft); font-size: 11px; font-weight: 700;
}
.undo-pill {
  background: rgba(176,58,52,.2);
  border-color: rgba(176,58,52,.55);
  color: #E8A9A4;
}
.undo-pill:active { background: rgba(176,58,52,.4); }
.claim-pill {
  background: rgba(47,111,191,.22);
  border-color: rgba(47,111,191,.6);
  color: #90BAEF;
  font-weight: 800;
}
.claim-pill:active { background: rgba(47,111,191,.4); }
.concede-pill {
  background: rgba(176,58,52,.2);
  border-color: rgba(176,58,52,.55);
  color: #E8A9A4;
  font-weight: 800;
}
.concede-pill:active { background: rgba(176,58,52,.4); }
/* toggled-on pill (e.g. Last trick peek is showing) */
.hint-pill.active {
  background: rgba(198,163,83,.5);
  color: var(--ivory);
}
/* The pills are grouped either side of the seat badge in the markup, but on a
   phone the groups are not wanted: there is no width to arrange anything with,
   and the best a 390px row can do is what it has always done — wrap the lot,
   centred. `display: contents` takes the two spans out of the layout entirely,
   so this stays the row it was. The tablet band below is where the grouping
   earns its place. */
.badge-row { display: flex; align-items: center; gap: 8px; justify-content: center; flex-wrap: wrap; }
.badge-row .br-side { display: contents; }

/* last-trick peek */
.trick-area.peeking .trick-card, .trick-area.peeking .trick-card.tc-new { animation-name: none; opacity: .96; }
.trick-card.tc-win {
  box-shadow: 0 0 0 2px var(--gold-soft), 0 3px 9px rgba(0,0,0,.5);
  z-index: 2;
}
.trick-peek-label {
  font-size: 11px; font-weight: 700; color: var(--gold-soft);
  padding: 4px 11px; border-radius: 20px; background: rgba(0,0,0,.3);
  text-align: center;
}

/* ---------------------------------------------------------------- bidding panel */

.bidding-panel {
  /* phone: floats above the frozen hand fan (tablets dock it — see below) */
  position: absolute; left: 12px; right: 12px;
  bottom: calc(var(--uw-bid) * 1.45 + 39px);
  background: rgba(16,42,31,.97);
  border: 1px solid var(--stroke); border-radius: 14px;
  box-shadow: 0 8px 26px rgba(0,0,0,.45);
  padding: 7px;
  display: flex; flex-direction: column; gap: 5px;
  /* the hand stays frozen below the panel; the panel itself scrolls if its
     content outgrows the space between the status bar and the cards */
  min-height: calc(60dvh - (var(--uw-bid) * 1.45 + 14px));
  max-height: calc(100% - 210px);
  overflow-y: auto;
  /* No entry animation: the whole view re-renders on every action, which would
     replay the slide each time and make the buttons jump under a double-tap. */
}

.bp-header { display: flex; align-items: center; gap: 8px; }
.bp-profile { font-size: 14.5px; font-weight: 700; color: var(--gold-soft); }
.bp-badge {
  font-size: 12px; font-weight: 800; color: var(--gold);
  background: rgba(198,163,83,.18); border-radius: 20px; padding: 4px 9px;
}
.bp-hcp {
  margin-left: auto;
  font-size: 13px; font-weight: 800; color: var(--ink-on-gold);
  background: var(--gold-soft); border-radius: 20px; padding: 4px 11px;
  white-space: nowrap;
}

.auction-grid-wrap {
  background: rgba(22,53,39,.55); border-radius: 10px; padding: 9px 10px;
  /* Absorbs whatever the panel has spare, so it fills the space from the first
     call and the buttons below never shift down as the auction grows. */
  flex: 1 1 auto; min-height: 96px; overflow-y: auto;
}
.auction-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px 8px; }
/* 5-column variant adds a round-number column on the left */
.auction-grid.ag5 { grid-template-columns: 22px repeat(4, 1fr); gap: 6px 6px; }
.ag-rnd-head { }
.ag-rnd {
  font-size: 11px; font-weight: 700; color: rgba(157,178,166,.45);
  display: flex; align-items: center; justify-content: center;
}
.ag-head {
  font-size: 13px; font-weight: 700; color: var(--text-2);
  text-align: center; letter-spacing: .4px; padding-bottom: 6px;
  border-bottom: 1px solid rgba(157,178,166,.25);
}
.ag-head.dealer { color: var(--gold); }
.ag-cell {
  font-size: 18px; font-weight: 600; text-align: center; color: var(--gold-soft);
  min-height: 30px; line-height: 30px; border-radius: 6px;
}
.ag-cell.pass { color: var(--text-2); font-weight: 400; }
.ag-cell.sel { background: rgba(198,163,83,.2); }
.ag-explain {
  display: flex; gap: 6px; align-items: flex-start;
  margin-top: 7px; padding: 8px;
  background: rgba(198,163,83,.1); border-radius: 8px;
  font-size: 11.5px; color: var(--text-2); line-height: 1.5; text-align: left;
}
.ag-explain b { color: var(--gold-soft); }
.ag-explain { cursor: pointer; }                       /* tap anywhere to dismiss */
.ag-close { margin-left: auto; padding-left: 8px; color: var(--text-2); font-size: 12px; flex-shrink: 0; }

.level-row { display: flex; gap: 6px; }
/* Sized for a thumb, not for the space that was left over: the auction hides the
   other seats' card backs, and that room goes here. */
.level-chip {
  /* 48px is the brief's minimum touch target; the row is the primary control
     of the whole screen, so it takes the floor rather than the leftovers. */
  flex: 1; height: 48px; border-radius: 10px;
  background: var(--surface-raised); color: var(--text);
  font-size: 18px; font-weight: 700;
}
.level-chip.sel { background: var(--gold); color: var(--ink-on-gold); }
.level-chip:disabled { opacity: .3; }

.strain-row { display: flex; gap: 6px; }
.strain-btn {
  flex: 1; height: 48px; border-radius: 10px;
  background: var(--ivory); font-size: 27px; font-weight: 700;
  line-height: 1; display: flex; align-items: center; justify-content: center;
}
.strain-btn.nt { font-size: 19px; color: var(--card-black); }
.strain-btn.sel { background: var(--gold); color: var(--ink-on-gold) !important; }
.strain-btn:disabled { opacity: .4; }

.special-row { display: flex; gap: 6px; flex-wrap: nowrap; }
.special-btn {
  flex: 1 1 0; min-width: 0; height: 48px; border-radius: 10px;
  color: #fff; font-size: 15px; font-weight: 700;
}
.special-btn.pass { background: var(--positive); }
.special-btn.dbl { background: var(--negative); }
.special-btn.rdbl { background: var(--info); }
.special-btn.sel { outline: 2px solid var(--gold-soft); outline-offset: -2px; }
.special-btn:disabled { opacity: .32; }
/* The lightbulb stays — it is what the tutorials and the play-screen Hint pill
   both point at — but it carries its name as well, so the action is
   discoverable without decoding an icon. */
.suggest-btn {
  min-width: 48px; height: 48px; border-radius: 10px; flex: none;
  padding: 0 12px; display: flex; align-items: center; gap: 6px;
  background: rgba(198,163,83,.16);
  border: 1px solid rgba(198,163,83,.5);
  color: var(--gold-soft); font-size: 20px; font-weight: 800;
  white-space: nowrap;
}
.suggest-btn .sb-label { font-size: 15px; font-weight: 700; }
.bid-utility-row { min-height: 0; display: flex; justify-content: flex-end; }
.undo-bid-btn {
  min-height: 44px; padding: 0 14px; border-radius: 10px;
  color: #E8A9A4; background: rgba(176,58,52,.2);
  border: 1px solid rgba(176,58,52,.55); font-size: 15px; font-weight: 800;
}
.undo-bid-btn:active { background: rgba(176,58,52,.4); }

/* natural height: the slot sits above the buttons, so growing it extends the
   bottom-anchored panel upward without moving the rows below */
.meaning-slot { display: flex; flex-direction: column; justify-content: center; }
.meaning-panel {
  background: rgba(22,53,39,.7); border-radius: 10px; padding: 7px 8px;
  display: flex; flex-direction: column; gap: 5px;
}
.meaning-head { display: flex; align-items: flex-start; gap: 8px; flex: 1; min-height: 0; overflow: hidden; }
.meaning-call { font-size: 14px; font-weight: 800; color: var(--gold); white-space: nowrap; }
.meaning-text {
  flex: 1; font-size: 12px; color: var(--text-2); line-height: 1.4;
  display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.meaning-tag {
  font-size: 9.5px; font-weight: 800; color: var(--gold);
  background: rgba(198,163,83,.18); border-radius: 20px; padding: 3px 7px;
  white-space: nowrap;
}
.meaning-tag.suggested { background: rgba(198,163,83,.4); }
.place-btn {
  height: 34px; border-radius: 11px;
  background: var(--gold); color: var(--ink-on-gold);
  font-size: 14.5px; font-weight: 800;
}
/* Instructional text, not metadata — the brief's 14px floor applies. */
.meaning-hint { font-size: 14px; line-height: 1.35; color: var(--text-2); text-align: center; padding: 0; }

.bp-waiting {
  display: flex; align-items: center; justify-content: center; gap: 9px;
  padding: 7px 0 4px; color: var(--text-2); font-size: 12.5px; font-weight: 500;
}
.spinner {
  width: 15px; height: 15px; border-radius: 50%;
  border: 2px solid rgba(198,163,83,.25); border-top-color: var(--gold);
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------------------------------------------------------------- overlays / sheets */

.overlay {
  position: fixed; inset: 0; z-index: 40;
  background: rgba(4,12,8,.6);
  display: flex; align-items: flex-end; justify-content: center;
}
.overlay.center { align-items: center; padding: 20px; }
.sheet {
  background: var(--bg);
  border-radius: 20px 20px 0 0;
  box-shadow: 0 -8px 40px rgba(0,0,0,.5);
  padding: 8px 18px calc(20px + env(safe-area-inset-bottom));
  width: min(560px, 100%);
  max-height: 88dvh;
  overflow-y: auto;
  animation: sheetUp .3s ease;
}
.overlay.center .sheet { border-radius: 20px; animation: none; }
@keyframes sheetUp { from { transform: translateY(60px); opacity: .4; } }
.grab { width: 42px; height: 5px; border-radius: 3px; background: var(--stroke); margin: 6px auto 14px; }

.sheet h3 { font-size: 17px; font-weight: 800; color: var(--text); display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.sheet h3 .h3-brand { display: inline-flex; align-items: center; gap: 9px; }

/* table menu (≡) — reuses the home screen's menu rows */
.game-menu { display: flex; flex-direction: column; gap: 10px; padding-bottom: 6px; }
.game-menu .menu-row { background: rgba(255,255,255,.03); }

/* result sheet */
.result-title { font-size: 25px; font-weight: 800; color: var(--text); text-align: center; }
.result-sub { font-size: 13.5px; color: var(--text-2); text-align: center; margin: 8px 0 14px; line-height: 1.5; }
.result-entries {
  background: rgba(22,53,39,.6); border-radius: 12px; padding: 10px 12px;
  display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px;
}
.result-entry { display: flex; align-items: baseline; gap: 8px; font-size: 13px; }
.result-entry .lab { flex: 1; color: var(--text-2); }
.result-entry .amt { font-weight: 800; }
.result-entry .amt.we { color: var(--gold); }
.result-entry .amt.they { color: var(--text); }
.result-entry .line-tag { font-size: 10px; color: var(--text-2); opacity: .75; }
.result-event { font-size: 14px; font-weight: 700; color: var(--gold); text-align: center; margin: 8px 0; }
.result-buttons { display: flex; flex-direction: column; gap: 9px; margin-top: 14px; }
.result-buttons .row2 { display: flex; gap: 9px; }
.action-btn {
  height: 48px; box-sizing: border-box; border-radius: 13px;   /* border-box → all buttons exactly 48px */
  background: var(--surface-raised); border: 1px solid var(--stroke);
  color: var(--text); font-size: 14.5px; font-weight: 700;
}
.result-buttons .row2 .action-btn { flex: 1; }                  /* split the row evenly */
.action-btn.prominent { width: 100%; background: var(--gold); border: none; color: var(--ink-on-gold); }
.action-btn.subtle { height: 40px; background: none; border: none; color: var(--text-2); font-weight: 600; font-size: 13.5px; }

/* scorepad */
.pad-stats { display: flex; gap: 10px; margin-bottom: 14px; }
.pad-table { background: var(--surface); border: 1px solid var(--stroke); border-radius: 16px; padding: 13px; }
.pad-cols {
  display: grid; grid-template-columns: 1fr 84px 84px; gap: 8px;
  font-size: 11px; font-weight: 800; color: var(--gold);
  padding-bottom: 8px;
}
.pad-cols span:nth-child(n+2) { text-align: right; }
.pad-section { font-size: 12px; font-weight: 800; color: var(--text); padding: 7px 0 4px; }
.pad-section small { font-weight: 500; color: var(--text-2); margin-left: 5px; }
.pad-row {
  display: grid; grid-template-columns: 1fr 84px 84px; gap: 8px;
  font-size: 12.5px; padding: 4px 0; align-items: baseline;
  font-variant-numeric: tabular-nums;
}
.pad-row .lab { color: var(--text-2); font-weight: 500; }
.pad-row.bold .lab { color: var(--text); font-weight: 700; }
.pad-row .num { text-align: right; font-weight: 600; color: var(--gold-soft); }
.pad-row.bold .num { font-weight: 800; }
.pad-empty { font-size: 11.5px; color: rgba(157,178,166,.55); padding: 4px 0; }
.gold-line { border: none; height: 2px; background: rgba(198,163,83,.85); margin: 7px 0; }
.gold-line.double { box-shadow: 0 4px 0 -2px rgba(198,163,83,.85); margin-bottom: 11px; }
.gold-line.thin { height: 1px; background: rgba(198,163,83,.5); }
.pad-status {
  margin-top: 12px; font-size: 12.5px; color: var(--text-2); line-height: 1.55;
  background: var(--surface-raised); padding: 10px 12px; border-radius: 10px;
}
.danger-btn { color: #E07A74; font-size: 13px; font-weight: 600; padding: 14px; width: 100%; text-align: center; }

/* contract card in recap */
.contract-card {
  background: var(--surface); border: 1px solid var(--stroke); border-radius: var(--radius);
  padding: 13px 14px; margin-bottom: 12px;
}
.contract-line { display: flex; align-items: center; gap: 8px; }
.contract-big { font-size: 22px; font-weight: 800; color: var(--gold); }
.contract-detail { font-size: 12px; color: var(--text-2); margin-top: 6px; }
.your-contract {
  margin-left: auto; font-size: 9px; font-weight: 800; color: var(--gold);
  background: rgba(198,163,83,.2); border-radius: 20px; padding: 3px 8px;
}

/* ---------------------------------------------------------------- review */

.review-screen { background: var(--bg); }
.review-loading { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; color: var(--text-2); font-size: 13px; }
.review-loading .rase-mark { opacity: .75; margin-bottom: 4px; }
.progress { width: min(280px, 70%); height: 6px; background: var(--surface-raised); border-radius: 3px; overflow: hidden; }
.progress div { height: 100%; background: var(--gold); border-radius: 3px; transition: width .2s; }

.deal-diagram { display: flex; flex-direction: column; gap: 12px; }
.dd-mid { display: flex; justify-content: space-between; gap: 12px; }
.hand-block .hb-title { font-size: 11px; font-weight: 800; color: var(--gold); margin-bottom: 4px; }
.hand-line { display: flex; gap: 6px; font-size: 12px; align-items: baseline; padding: 1px 0; }
.hand-line .hs { width: 14px; font-weight: 700; }
.hand-line .hr { font-family: ui-monospace, "SF Mono", Consolas, monospace; font-weight: 600; color: var(--text-2); letter-spacing: .5px; }
.hand-line .hs.s-S { color: var(--text); }
.hand-line .hs.s-C { color: #9FB0AE; }
.hand-line .hs.s-H { color: #F1615A; }
.hand-line .hs.s-D { color: #EFA15A; }


.ra-table { display: flex; flex-direction: column; }
.ra-row {
  display: grid; grid-template-columns: 58px 52px 52px 1fr; gap: 8px;
  padding: 7px 4px; font-size: 12px; align-items: baseline;
  border-bottom: 1px solid rgba(44,74,59,.5);
}
.ra-row:last-child { border-bottom: none; }
.ra-row.head { font-size: 10px; font-weight: 800; color: var(--text-2); text-transform: uppercase; letter-spacing: .5px; }
.ra-row.user-row { background: rgba(198,163,83,.06); }
.ra-row.deviation { background: rgba(217,142,50,.1); }
/* A defensible alternative sits between the two: marked, but not flagged. */
.ra-row.sound { background: rgba(198,163,83,.09); }
.ra-sound {
  display: inline-block; margin-left: 6px; padding: 1px 5px; border-radius: 5px;
  background: rgba(198,163,83,.16); color: var(--text-2);
  font-size: 9.5px; font-weight: 800; letter-spacing: .4px; text-transform: uppercase;
  vertical-align: 1px;
}
.ra-row .mono { font-weight: 700; color: var(--gold-soft); font-variant-numeric: tabular-nums; }
.ra-row .exp { color: var(--text-2); font-size: 11px; line-height: 1.45; }

.rp-trick {
  background: rgba(22,53,39,.5); border-radius: 10px; padding: 8px 11px;
  display: flex; flex-wrap: wrap; gap: 4px 11px; align-items: baseline; font-size: 12.5px;
  margin-bottom: 6px;
}
.rp-no { font-weight: 800; color: var(--text-2); min-width: 17px; }
.rp-card { font-weight: 600; color: var(--text); }
.rp-card.winner { text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--gold); }
.rp-note { font-size: 11.5px; color: var(--text-2); width: 100%; padding-left: 28px; }
.rp-note.good { color: #6FBF8B; }
.rp-note.bad { color: #E07A74; font-weight: 600; }
.rp-reason { font-size: 11.5px; color: var(--gold-soft); width: 100%; padding-left: 28px; font-style: italic; line-height: 1.4; }
/* tricks the double-dummy tracker says changed hands in this trick — the same
   losses the grade is built from, so the list and the percentage agree */
.rp-loss { font-size: 11.5px; width: 100%; padding-left: 28px; line-height: 1.4; font-weight: 600; }
.rp-loss.mine { color: #E07A74; }
.rp-loss.theirs { color: var(--text-2); font-weight: 500; }

/* play grade ring — the closed face of the breakdown disclosure */
.grade-card { display: flex; align-items: center; gap: 16px; }
.grade-details > summary.grade-card { cursor: pointer; list-style: none; }
.grade-details > summary.grade-card::-webkit-details-marker { display: none; }
.grade-details > summary::after {
  content: '▾'; margin-left: auto; padding-left: 8px; flex-shrink: 0;
  font-size: 12px; color: var(--text-2); transition: transform .15s;
}
.grade-details[open] > summary::after { transform: rotate(180deg); }
.grade-details[open] > summary.grade-card { margin-bottom: 12px; }
/* the disclosure hint doubles as the closed-state summary, so keep it legible */
.grade-details .grade-basis { font-weight: 600; }
.grade-ring {
  width: 92px; height: 92px; border-radius: 50%; flex-shrink: 0;
  background: conic-gradient(var(--gold) calc(var(--pct) * 1%), rgba(255,255,255,.09) 0);
  display: flex; align-items: center; justify-content: center;
}
.grade-pct {
  width: 72px; height: 72px; border-radius: 50%; background: var(--surface);
  /* grid: content centred in the disc, number and % aligned on their baseline */
  display: grid; grid-auto-flow: column; align-content: center; justify-content: center;
  align-items: baseline; line-height: 1;
  font-size: 26px; font-weight: 800; color: var(--gold-soft); font-variant-numeric: tabular-nums;
}
.grade-pct span { font-size: 13px; margin-left: 1px; }
.grade-meta { flex: 1; min-width: 0; }
.grade-label { font-size: 19px; font-weight: 800; color: var(--text); }
.grade-basis { font-size: 12px; color: var(--text-2); margin-top: 4px; line-height: 1.45; }

/* the two halves behind the ring: bidding and play, each with what it cost */
.scorecard .sc-row { margin-top: 12px; }
.scorecard .sc-row:first-of-type { margin-top: 4px; }
.sc-head { display: flex; justify-content: space-between; align-items: baseline; }
.sc-name { font-size: 13px; font-weight: 700; color: var(--text); letter-spacing: .02em; }
.sc-pct { font-size: 13px; font-weight: 800; color: var(--gold-soft); font-variant-numeric: tabular-nums; }
.sc-bar {
  height: 6px; border-radius: 3px; margin: 5px 0 6px;
  background: rgba(255,255,255,.09); overflow: hidden;
}
.sc-bar > div { height: 100%; border-radius: 3px; background: var(--gold); }
.sc-detail { font-size: 12px; color: var(--text-2); line-height: 1.5; }
.sc-detail b { color: var(--gold-soft); font-weight: 700; }

/* one-line debrief on the result sheet when our contract goes down */
.result-debrief {
  margin: 8px auto 0; max-width: 340px;
  padding: 9px 12px; border-radius: 9px;
  background: rgba(198,163,83,.1);
  font-size: 12.5px; line-height: 1.5; color: var(--gold-soft);
  text-align: center;
}

/* the verdict that opens "What to work on" — the one sentence a player reads
   if they read nothing else, so it leads the card rather than sitting alone */
.improve-lead {
  font-size: 13.5px; line-height: 1.5; color: var(--gold-soft);
  padding-bottom: 9px; margin-bottom: 2px;
}
.improve-lead:not(:last-child) { border-bottom: 1px solid rgba(44,74,59,.5); }

/* analysis: the deal stays pinned at the top while the review scrolls under
   it, so any note can be compared with the layout; collapsible when not needed */
.deal-pin {
  /* lives between the top bar and the scroll area — structurally always
     visible, with the review scrolling in its own region below */
  flex-shrink: 0; z-index: 5;
  border-radius: 0; border-left: none; border-right: none;
  box-shadow: 0 10px 22px rgba(0,0,0,.5);
  padding: 10px 16px;
}
.deal-pin summary {
  font-size: 15px; font-weight: 800; color: var(--gold);
  cursor: pointer; list-style: none;
  display: flex; align-items: center;
}
.deal-pin summary::-webkit-details-marker { display: none; }
/* Title and result share a line where there is room and stack where there is
   not — the chevron sits outside the wrapping group so it stays put either way. */
.deal-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px; flex: 1; min-width: 0; }
.deal-title { flex-shrink: 0; }
.deal-pin .deal-meta { font-size: 11.5px; font-weight: 600; color: var(--text-2); }
.deal-pin .deal-meta b { color: var(--text); font-weight: 700; }
.deal-pin .deal-meta b.deal-score { color: var(--gold-soft); }
/* dealer and vulnerability: context you check occasionally, so it gives up the
   title to the contract and its score and sits at the head of the body instead */
.deal-context { font-size: 11px; color: var(--text-2); margin-bottom: 8px; }
/* dealer chip beside the hand's title in the diagram */
/* high-card points, sat at the end of the hand's title line. Deliberately not
   gold: the dealer badge next to it is, and the two must not read as one mark. */
.hb-hcp { margin-left: auto; padding-left: 8px; font-weight: 700; color: var(--text-2); white-space: nowrap; }
.hand-block .hb-title { display: flex; align-items: baseline; }

.hb-dealer {
  display: inline-block; min-width: 13px; text-align: center;
  font-size: 9px; font-weight: 800; line-height: 13px;
  color: var(--ink-on-gold); background: var(--gold); border-radius: 4px;
  vertical-align: 1px;
}
.deal-pin summary::after { content: '▾'; margin-left: auto; font-size: 11px; color: var(--text-2); transition: transform .15s; }
.deal-pin:not([open]) summary::after { transform: rotate(-90deg); }
.deal-pin[open] summary { margin-bottom: 8px; }
/* compact rendering inside the pinned card so it doesn't dominate the screen */
.deal-pin .deal-diagram { gap: 6px; }
.deal-pin .hand-line { font-size: 11px; padding: 0; }
.deal-pin .hand-block .hb-title { font-size: 10px; margin-bottom: 2px; }
.deal-pin .dd-tricks { margin-top: 8px; padding-top: 8px; }
.deal-pin .ddo-note { display: none; }
.deal-pin .ddt-cell { font-size: 12px; }

/* tricks-available grid — every hand as declarer, every strain */
.dd-tricks { margin-top: 12px; padding-top: 10px; border-top: 1px solid rgba(44,74,59,.6); }
.ddt-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 5px; }
.ddt-head .ddo-title { margin-bottom: 0; }
/* the grid folds away by default — it is reference material, and on a phone it
   is most of the panel's height when the hands are what you came to see */
.dd-tricks > summary.ddt-head { cursor: pointer; list-style: none; }
.dd-tricks > summary::-webkit-details-marker { display: none; }
.dd-tricks > summary::after {
  content: '▾'; margin-left: auto; font-size: 11px; color: var(--text-2); transition: transform .15s;
}
.dd-tricks:not([open]) > summary::after { transform: rotate(-90deg); }
.dd-tricks[open] > summary { margin-bottom: 7px; }
.ddt-grid { overflow-x: auto; }         /* a narrow phone scrolls the grid, not the page */
/* The grid reads as a table, so the columns stay close enough to scan across.
   Left free to stretch it would sprawl over the full width of the panel on a
   tablet and the rows would stop lining up to the eye. */
.ddt-row { display: flex; align-items: baseline; min-width: 220px; max-width: 340px; }
.ddt-seat { width: 34px; flex-shrink: 0; font-size: 12px; font-weight: 800; color: var(--text-2); }
.ddt-cell {
  flex: 1; text-align: center; padding: 2px 0; font-size: 13px; font-weight: 600;
  color: var(--text); font-variant-numeric: tabular-nums;
}
/* points sit past a gap, off the end of the trick columns — they are a
   different quantity and must not read as a sixth strain */
.ddt-hcp-cell {
  width: 34px; flex-shrink: 0; margin-left: 10px; text-align: right;
  font-size: 12px; font-weight: 700; color: var(--text-2); font-variant-numeric: tabular-nums;
  border-left: 1px solid rgba(44,74,59,.6); padding-left: 8px;
}
/* the hand that has to declare, where it makes a difference to the count */
.ddt-by {
  font-size: .7em; font-weight: 700; color: var(--text-2);
  margin-left: 2px; vertical-align: 2px; letter-spacing: 0;
}
.ddt-cell.played .ddt-by { color: var(--gold-soft); }
.ddt-row.head .ddt-cell { font-size: 12.5px; font-weight: 800; padding-bottom: 3px; }
.ddt-row.head .ddt-cell.nt { color: var(--text-2); }
.ddt-row.head .ddt-hcp-cell { font-size: 10px; letter-spacing: .04em; }
.ddt-cell.none { color: var(--text-2); opacity: .45; font-weight: 500; }
/* The best contract each side had, ringed in place. An inset shadow rather than
   a border: a border would add two pixels to one cell and pull the column out
   of line with the rows above and below it. */
.ddt-cell.best {
  box-shadow: inset 0 0 0 1px var(--gold); border-radius: 4px;
  color: var(--gold-soft); font-weight: 800;
}
.ddt-cell.played {
  color: var(--gold); font-weight: 800;
  background: rgba(198,163,83,.14); border-radius: 4px;
}
/* A marked cell must read even where the count is too low to bid — a contract
   played and gone down lands on exactly such a cell. */
.ddt-cell.none.played, .ddt-cell.none.best { opacity: 1; }

/* what the two marks mean. Unlike .ddo-note this survives on a phone: with the
   optimum list gone, the marks carry that meaning on their own. */
.ddt-key { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 7px; }
.ddt-key-item { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; color: var(--text-2); }
.ddt-key-item i { width: 15px; height: 15px; border-radius: 4px; flex-shrink: 0; }
.ddt-key-item i.k-best { box-shadow: inset 0 0 0 1px var(--gold); }
.ddt-key-item i.k-played { background: rgba(198,163,83,.14); }

.ddo-title { font-size: 12px; font-weight: 800; color: var(--gold-soft); text-transform: uppercase; letter-spacing: .5px; margin-bottom: 5px; }
.ddo-note { font-size: 11px; color: var(--text-2); line-height: 1.4; margin-top: 6px; }

/* live scorepad entry form */
.lp-row { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }
.lp-label { width: 56px; flex-shrink: 0; font-size: 11px; font-weight: 700; color: var(--text-2); text-transform: uppercase; letter-spacing: .3px; }
.lp-row .segmented { margin-top: 0; width: 100%; flex-wrap: wrap; }
.lp-row .seg { flex: 1; padding: 6px 2px; text-align: center; min-width: 30px; white-space: nowrap; }
.lp-preview {
  margin: 10px 0; padding: 8px 12px; border-radius: 9px;
  background: rgba(198,163,83,.1);
  font-size: 12.5px; font-weight: 600; color: var(--gold-soft); text-align: center;
}
.lp-events { padding: 10px 14px; }
.lp-note { font-size: 10.5px; color: var(--text-2); line-height: 1.45; margin: 2px 0 4px; }
.lp-notice { font-size: 13px; font-weight: 700; color: #6FBF8B; }
/* two segmented switches sharing one row (honours amount + side) */
.lp-duo { display: flex; gap: 6px; flex: 1; min-width: 0; }
.lp-duo .segmented { width: auto; flex: 1; }
.lp-duo .lp-dim { opacity: .4; }
.lp-timer-card { display: flex; align-items: center; gap: 10px; padding: 10px 14px; }
.lp-timer { flex: 1; font-size: 19px; font-weight: 800; color: var(--text-2); font-variant-numeric: tabular-nums; }
.lp-timer.run { color: var(--gold); }
.lp-timer-btn { height: 36px; padding: 0 14px; font-size: 12.5px; }
.lp-void .archive-title, .lp-void .archive-sub { opacity: .45; text-decoration: line-through; }
.lp-editing { background: rgba(198,163,83,.08); border-radius: 8px; }

/* hand archive */
.archive-row { display: flex; align-items: center; gap: 8px; border-bottom: 1px solid rgba(44,74,59,.5); }
.archive-row:last-child { border-bottom: none; }
.archive-main {
  flex: 1; min-width: 0; padding: 10px 0;
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px; text-align: left;
}
.archive-main:active { opacity: .7; }
.archive-title { font-size: 14.5px; font-weight: 700; color: var(--text); }
.archive-sub { font-size: 11.5px; color: var(--text-2); }
.archive-grade {
  flex-shrink: 0; font-size: 12px; font-weight: 800; color: var(--gold-soft);
  background: rgba(0,0,0,.28); border-radius: 12px; padding: 4px 8px;
  font-variant-numeric: tabular-nums;
}
.archive-del {
  flex-shrink: 0; width: 30px; height: 30px; border-radius: 50%;
  color: #E07A74; background: rgba(224,122,116,.12); font-size: 13px;
}
.archive-del:active { background: rgba(224,122,116,.28); }

/* key improvements */
.improve-row { display: flex; gap: 10px; padding: 9px 0; border-bottom: 1px solid rgba(44,74,59,.5); align-items: baseline; }
.improve-row:last-child { border-bottom: none; }
.improve-label {
  font-size: 10.5px; font-weight: 800; color: var(--gold);
  background: rgba(198,163,83,.15); border-radius: 6px; padding: 3px 8px;
  white-space: nowrap; flex-shrink: 0;
}
.improve-text { font-size: 13px; color: var(--text-2); line-height: 1.45; }

.wide-btn {
  width: 100%; height: 46px; border-radius: 13px; margin-top: 16px;
  background: var(--gold); color: var(--ink-on-gold); font-size: 15px; font-weight: 800;
}

/* confirm dialog */
.confirm-box { text-align: center; padding: 6px 2px; }
.confirm-box .cq { font-size: 16px; font-weight: 800; color: var(--text); margin-bottom: 6px; }
.confirm-box .cm { font-size: 12.5px; color: var(--text-2); line-height: 1.5; margin-bottom: 16px; }
.confirm-box .action-btn.destructive { background: var(--negative); border: none; color: #fff; }

/* ---------------------------------------------------------------- tablet (iPad)

   Breakpoints key off the *window* width, not the device, so Split View, Slide
   Over and Stage Manager windows fall back to the phone layout automatically
   once they get narrow. Three tiers:

     < 700px            phone (everything above)
     >= 700px           tablet: full-bleed screens, bigger cards, the bidding
                        panel docked as an in-flow band under the table
     >= 920px landscape tablet landscape: bidding panel becomes a right-hand
                        rail beside the table; analysis becomes two columns

   Card sizes use vmin so a card is the same physical size in portrait and
   landscape, with a vh cap so a short landscape window can't overflow.
------------------------------------------------------------------------------ */

@media (min-width: 700px) {
  #app { background: var(--bg-deep); }
  .screen { width: 100%; }

  /* Tablet card scale. The dummy keeps the same 80% relationship to the hand as
     on a phone — the extra screen goes to BOTH, not to the player's cards
     alone, which is how the two ended up at 120px against 58px. */
  .game-screen {
    --card-player: min(clamp(70px, 13.5vmin, 120px), 13.5vh);
    --card-dummy: clamp(70px, calc(var(--card-player) * 0.84), 96px);
  }

  /* --- chrome ------------------------------------------------------------ */
  .top-bar { padding: max(14px, env(safe-area-inset-top)) 20px 10px; gap: 12px; }
  .top-bar h2 { font-size: 20px; }
  .icon-btn { width: 42px; height: 42px; border-radius: 12px; font-size: 19px; }
  .top-brand { width: 42px; }
  .top-brand .rase-mark { width: 25px; height: auto; }

  /* --- home -------------------------------------------------------------- */
  .home-screen { padding: 30px 40px; gap: 30px; }
  .home-hero { padding-top: 46px; gap: 20px; }
  /* The lockup is SVG, so it scales by height and keeps its own proportions. */
  .home-hero .rase-tile { width: 116px; height: auto; }
  .home-hero .rase-word { height: 46px; width: auto; }
  .tagline { font-size: 17px; }
  .home-sub { font-size: 14px; }
  .chips-row { width: min(720px, 100%); gap: 14px; }
  .chip { padding: 15px 6px; }
  .chip .chip-value { font-size: 26px; }
  .chip .chip-title { font-size: 12px; }
  /* the four secondary destinations pair up under a full-width play button */
  .home-buttons {
    width: min(720px, 100%);
    display: grid; grid-template-columns: 1fr 1fr; gap: 14px;
  }
  .home-buttons .btn-primary { grid-column: 1 / -1; padding: 19px 24px; }
  .btn-primary .bt { font-size: 20px; }
  .btn-primary .bs { font-size: 14px; }
  .menu-row { padding: 16px; gap: 15px; height: 100%; }
  .menu-icon { width: 48px; height: 48px; font-size: 22px; }
  .menu-row .mt { font-size: 17px; }
  .menu-row .ms { font-size: 13px; }
  .home-footer { font-size: 13.5px; }
  .home-site-link { font-size: 13px; }

  /* --- reading screens: one generous, centred measure --------------------- */
  .scroll-body { padding: 20px 24px calc(40px + env(safe-area-inset-bottom)); gap: 18px; }
  .scroll-body > * { width: 100%; max-width: 860px; margin-left: auto; margin-right: auto; }
  .section-card { padding: 18px 20px; }
  .section-card h3 { font-size: 17px; margin-bottom: 10px; }
  .section-caption { font-size: 13px; }
  .system-option { padding: 14px; }
  .system-option .ot { font-size: 16px; }
  .system-option .ob { font-size: 13px; }
  .conv-row { padding: 13px 0; }
  .conv-row .ct { font-size: 15px; }
  .conv-row .cb { font-size: 12.5px; }
  .seg { font-size: 13px; padding: 7px 15px; }
  .rules-section summary { font-size: 17px; padding: 16px 20px; }
  .rules-chapter > summary { padding: 16px 38px 16px 20px; }
  .rules-chapter .rules-section summary { font-size: 16px; padding: 14px 38px 14px 20px; }
  .rc-title { font-size: 17px; }
  .rc-caption { font-size: 13px; }
  .rules-text { font-size: 15px; padding: 2px 20px 20px; }
  /* Match the centred measure the scroll body uses on a tablet. */
  .rules-search { width: calc(100% - 48px); max-width: 860px; margin: 0 auto; }
  .rs-input { font-size: 16px; padding: 12px 0; }
  .rs-status { width: calc(100% - 48px); max-width: 860px; margin: 10px auto 0; font-size: 13px; }
  .rs-snip { font-size: 13px; }
  .rules-text th, .rules-text td { font-size: 14px; padding: 8px 12px; }
  .archive-title { font-size: 16px; }
  .archive-sub { font-size: 13px; }
  .improve-text { font-size: 14.5px; }

  /* --- sheets become centred dialogs ------------------------------------- */
  .overlay { align-items: center; padding: 32px; }
  .sheet {
    width: min(680px, 100%);
    border-radius: 20px;
    max-height: 84dvh;
    padding: 14px 26px calc(26px + env(safe-area-inset-bottom));
    animation: none;
  }
  .grab { display: none; }
  .sheet h3 { font-size: 19px; }
  .result-title { font-size: 30px; }
  .result-sub { font-size: 15px; }
  .result-entry { font-size: 14.5px; }
  .action-btn { height: 52px; font-size: 16px; }
  .pad-cols, .pad-row { grid-template-columns: 1fr 110px 110px; }
  .pad-cols { font-size: 12.5px; }
  .pad-row { font-size: 14px; }
  .pad-status { font-size: 14px; }

  /* --- game table -------------------------------------------------------- */
  /* grid so the bidding panel can be placed as a band (or, wide, a rail)
     without the markup changing */
  .game-screen {
    display: grid; min-height: 0;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto minmax(0, 1fr) auto;
    grid-template-areas: "top" "status" "table" "rail";
  }
  .game-screen > .top-bar { grid-area: top; }
  .game-screen > .status-bar { grid-area: status; }
  .game-screen > .table-area { grid-area: table; }
  .game-screen > .bidding-panel { grid-area: rail; }

  .status-bar { width: auto; margin: 0 20px; padding: 11px 18px; font-size: 14px; }
  .table-area { padding: 10px 20px calc(8px + env(safe-area-inset-bottom)); gap: 6px; }
  /* During the auction the table holds four badges and a fan, and nothing in
     the middle — so `space-between`, which is right in play where the trick
     fills that space, pushed the four seats into the corners of an empty felt
     and left the player's own hand marooned at the bottom of it. On a phone
     the bidding panel covers that region and the question never arises; on
     anything wider the compass has to gather into one object, centred on the
     table it is describing. */
  .game-screen.is-bidding .table-area { justify-content: center; gap: 16px; }
  /* A definite basis, not `flex: 0 1 auto`. .table-middle is a size container
     (see the budget block below), so its own contents no longer contribute to
     its size — asking it to shrink-wrap them collapsed the band to zero and
     took both opponents' badges off the table with it. During the auction the
     band holds two badges and the felt print, and 96px is the whole of that.
     It stays shrinkable, because the cluster can be taller than the row it is
     centred in — Larger Text pushes it about 30px past — and an empty band is
     the right thing to spend first, rather than sliding the player's hand
     under the bidding panel. It stops at the height of a badge, which is the
     one thing in there that has to be read. */
  .game-screen.is-bidding .table-middle {
    flex: 0 1 96px; min-height: 40px; padding-top: 0; align-items: center;
  }
  /* the top seat and the hand keep their natural height; only the middle
     (trick / felt mark) gives way when the window is short */
  .table-area > .seat-zone { flex-shrink: 0; }
  .table-center { gap: 14px; }

  .seat-badge { font-size: 15px; padding: 8px 17px; gap: 8px; border-radius: 22px; }
  .seat-badge .dot { width: 9px; height: 9px; }
  /* Larger Text was written against the phone scale, where 12px IS larger —
     here it is three points smaller than the size this band already sets, so
     turning the setting on shrank every seat badge and the trick counters'
     labels. A setting called Larger Text may never make anything smaller. */
  body.text-large .seat-badge { font-size: 17px; }
  body.text-large .tc-pill .l { font-size: 14.5px; }
  .seat-W, .seat-E { width: 116px; }
  /* An opponent's dummy turns the table into a compass, and the cross only
     lands on the centre line if BOTH side columns reserve the same width. The
     tray's side was sized by :has(.dummy-cols.side) and the card-backs' side
     was not, so the trick sat 36px right of centre at 768x1024. Capping the
     height matters as much: four vertical suit columns run 560px against the
     ~530px the band has, and the overflow reached down into the player's hand. */
  .game-screen.dummy-at-W .seat-W, .game-screen.dummy-at-W .seat-E,
  .game-screen.dummy-at-E .seat-W, .game-screen.dummy-at-E .seat-E {
    /* Sized to FIT, not capped and scrolled — a scrollbar here just hides the
       ♠Q and the ♥A above the fold, which is worse than a tight fit.
       A 2x2 block of VERTICAL columns runs 7.31 card-widths tall (1.45 for the
       first card in a column, 0.63 for each one under it, over two rows), so
       even at 64px it wanted ~474px against the 395px this band actually has.
       Horizontal fans cost one card-height per suit whatever its length —
       4 x 1.45 = 5.8 card-widths for the whole hand — which fits with room.

       "The band" was 395px because the sum was worked out on a 768x1024
       tablet. A 900x620 browser window leaves 201, and there 58px fans stood
       348px tall: the dummy's last suit was painted over the player's own hand
       and off the bottom of the screen. So the width is taken from the band
       rather than assumed — 5.8 card-widths is the same equation, solved for
       the card instead of for the tray — down to the brief's 52px legibility
       floor. Below that the tray is bounded and scrolls: hiding a suit behind
       a scroll is bad, and it is still better than printing it over the hand,
       which is the only other thing a box can do with content it cannot
       hold. */
    --dw-side: max(52px, min(58px, calc((100cqh - 12px) / 5.8)));
    width: calc(var(--dw-side) * 3.6);
    max-height: 100%;
    overflow: auto; overscroll-behavior: contain;
  }
  .game-screen.dummy-at-W .dummy-cols.side,
  .game-screen.dummy-at-E .dummy-cols.side {
    --dw: var(--dw-side, 58px);
    display: flex; flex-direction: column; flex-wrap: nowrap;
    width: auto; gap: 4px;
  }
  .game-screen.dummy-at-E .dummy-cols.side { align-items: flex-end; }
  .game-screen.dummy-at-W .dummy-cols.side .dummy-col,
  .game-screen.dummy-at-E .dummy-cols.side .dummy-col { flex-direction: row; }
  .game-screen.dummy-at-W .dummy-cols.side .dummy-col .pcard,
  .game-screen.dummy-at-E .dummy-cols.side .dummy-col .pcard {
    margin-top: 0; margin-left: calc(var(--dw) * -0.42);
  }
  .game-screen.dummy-at-W .dummy-cols.side .dummy-col .pcard:first-child,
  .game-screen.dummy-at-E .dummy-cols.side .dummy-col .pcard:first-child { margin-left: 0; }
  /* the cross gives back what the two side columns now take */
  .game-screen.dummy-at-W .table-center,
  .game-screen.dummy-at-E .table-center { --tw: min(44vmin, 290px, var(--tw-fit, 9999px)); }
  .seat-zone { gap: 8px; }

  .hidden-hand .card-back { width: 46px; height: 66px; margin-left: -30px; }
  /* Thirteen backs at 17px of exposed edge stand 247px tall, and with the seat
     badge above them that is more than the band has once Larger Text puts the
     badge on two lines — so the zone, which scrolls, grew a scrollbar down each
     side of the table and clipped the last back in half. They are a
     silhouette: nobody scrolls them, and there is nothing to read at the
     bottom. The exposed edge is taken from the band instead, so the stack
     closes up until it fits, and never opens wider than the 17px it was
     drawn at. */
  .hidden-hand.vertical .card-back {
    width: 62px; height: 43px;
    margin-top: min(-26px, calc((100cqh - 103px) / 12 - 43px));
  }

  /* Dummy at the top lays each suit out horizontally — like the player's own
     fan — instead of in tall vertical columns. A long suit in a column costs
     ~5x its card height, which is more than any tablet can spare; a row costs
     one card height whatever the length. */
  .dummy-cols:not(.side) { --dw: var(--card-dummy); gap: 12px; }
  .dummy-cols:not(.side) .dummy-col { flex-direction: row; }
  .dummy-cols:not(.side) .dummy-col .pcard {
    margin-top: 0; margin-left: calc(var(--dw) * -0.58);
  }
  .dummy-cols:not(.side) .dummy-col .pcard:first-child { margin-left: 0; }
  .dummy-cols:not(.side) .pcard .idx {
    flex-direction: column; align-items: center; gap: 0;
    top: 5%; left: 8%; font-size: calc(var(--dw) * 0.34);
  }
  .dummy-cols:not(.side) .pcard .idx span:last-child { font-size: calc(var(--dw) * 0.34); }
  .dummy-cols:not(.side) .pcard.r10 .idx,
  .dummy-cols:not(.side) .pcard.r10 .idx span:last-child {
    left: 5%; font-size: calc(var(--dw) * 0.28);
  }
  .dummy-cols:not(.side) .pcard .pip { display: block; font-size: calc(var(--dw) * 0.5); }

  /* A side (E/W) dummy keeps vertical columns — that zone is a tall, narrow
     strip — wrapped 2x2 as on the phone. */
  /* A tablet has the room, so a side dummy takes the same token as the top
     tray rather than a flat 46px against a 120px hand. */
  .dummy-cols.side { --dw: calc(var(--card-dummy) * 0.8); width: calc(var(--dw) * 2 + 9px); }

  .table-center { --tw: min(44vmin, 340px, var(--tw-fit, 9999px)); }
  .tc-pill { padding: 8px 18px; gap: 7px; }
  .tc-pill .l { font-size: 13.5px; }
  .tc-pill .v { font-size: 22px; }
  .trick-peek-label { font-size: 14px; padding: 6px 15px; }

  .hint-pill { font-size: 15.5px; padding: 10px 19px; gap: 6px; }
  .badge-row { gap: 11px; }

  /* The last term is a SHARE of the table, not a size: thirteen overlapped
     cards are 6.04 card-widths, so 10cqw holds the fan to about 60% of the
     table's width. Without it a 124px card made a 750px slab of an 820px
     window and the table above it read as the fan's margin. It is cqw rather
     than vw because the frame stops growing at 1180px and the fan has to stop
     with it — the two vmin/vh terms stay on the window, since how TALL a card
     may be has nothing to do with how wide the frame was allowed to get. */
  .user-hand:not(.play2) {
    --uw: min(clamp(72px, 14vmin, 124px), 15vh, 10cqw);
    margin: 0; padding: 6px 0 8px;
  }
  .user-hand.play2 { --uw: var(--card-player); gap: 8px; }

  /* --- bidding panel: docked band under the table ------------------------ */
  .bidding-panel {
    position: static;
    max-height: none; overflow: visible;
    width: min(760px, calc(100% - 40px));
    margin: 0 auto calc(12px + env(safe-area-inset-bottom));
    padding: 12px; gap: 8px;
    box-shadow: 0 -6px 26px rgba(0,0,0,.35);
  }
  .bp-profile { font-size: 16px; }
  .bp-badge { font-size: 13.5px; padding: 5px 11px; }
  .bp-hcp { font-size: 15px; padding: 5px 13px; }
  .auction-grid-wrap { max-height: 230px; padding: 9px 13px; }
  .ag-head { font-size: 13.5px; padding-bottom: 5px; }
  .ag-cell { font-size: 17.5px; min-height: 28px; }
  .ag-rnd { font-size: 11px; }
  .ag-explain { font-size: 14.5px; padding: 11px; }
  .level-row, .strain-row, .special-row { gap: 6px; }
  .level-chip { height: 46px; font-size: 19px; }
  .strain-btn { height: 54px; font-size: 26px; }
  .strain-btn.nt { font-size: 19px; }
  .special-btn { height: 54px; font-size: 17px; }
  /* Width, not min-width, was 36px short of the button's own contents: the
     lightbulb, the 6px gap and the word "Suggest" measure ~99px, so the label
     hung outside the button — and in the landscape rail, where the panel sits
     against the right edge, it hung off the screen with it. The button hugs
     its content the way the phone rule already lets it; 64px stays as the
     floor for the icon-only state. */
  .suggest-btn { min-width: 64px; height: 54px; font-size: 20px; padding: 0 14px; }
  .place-btn { height: 50px; font-size: 19px; }
  .meaning-call { font-size: 18px; }
  .meaning-text { font-size: 15.5px; -webkit-line-clamp: 3; line-clamp: 3; }
  .meaning-tag { font-size: 11.5px; padding: 4px 9px; }
  .meaning-hint { font-size: 14.5px; }
  .bp-waiting { font-size: 15.5px; }

  /* --- analysis ---------------------------------------------------------- */
  .deal-pin { padding: 12px 24px; }
  .deal-pin summary { font-size: 17px; }
  .deal-pin .hand-line { font-size: 13.5px; }
  .deal-pin .hand-block .hb-title { font-size: 12px; }
  .deal-pin .ddt-cell { font-size: 14px; }
  .deal-pin .ddt-seat { font-size: 13px; width: 38px; }
  .deal-pin .ddt-hcp-cell { font-size: 13px; }
  .ra-row { grid-template-columns: 72px 64px 64px 1fr; font-size: 13.5px; padding: 9px 6px; }
  .ra-row .exp { font-size: 12.5px; }
  .rp-trick { font-size: 14px; padding: 10px 14px; }
  .rp-note, .rp-reason { font-size: 12.5px; }
  .wide-btn { height: 52px; font-size: 16.5px; }
}

/* ------------------------------------------------- tablet, landscape/wide */

@media (min-width: 920px) and (orientation: landscape) {
  /* Bidding docks to a right-hand rail: the auction grid and the call buttons
     stay visible alongside the full 13-card fan instead of covering it. */
  .game-screen {
    /* Landscape is short, so both cards are height-bound — but they shrink
       TOGETHER. The brief's floor for phone landscape is the strictest in the
       document (80% of the player card) and it is enforced here. */
    --card-player: min(clamp(66px, 9.5vw, 118px), 17vh);
    --card-dummy: clamp(52px, calc(var(--card-player) * 0.84), 96px);
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: auto auto minmax(0, 1fr);
    grid-template-areas:
      "top    top"
      "status status"
      "table  rail";
  }
  .bidding-panel {
    width: clamp(320px, 30vw, 400px);
    /* In the rail the panel is a GRID ITEM, not the floating overlay the phone
       uses. It kept the base rule's absolute positioning, so `align-self: end`
       did nothing and the panel was placed from the bottom of the screen by a
       phone-shaped offset — which is how its lowest controls ended up past the
       fold. Returning it to flow lets the grid area bound it. */
    position: relative; left: auto; right: auto; bottom: auto;
    /* hugs its content at the foot of the rail and grows upward as the
       auction fills, so the call buttons never move under a double-tap */
    align-self: end;
    /* The PANEL must not scroll — only its auction history may. It used to
       carry overflow-y:auto itself, so on a short landscape phone the
       Pass/Dbl/Rdbl row and Suggest sat 77px below the fold and four controls
       were unreachable. Clamping the panel and letting the grid absorb the
       squeeze keeps every control on screen at any height. */
    min-height: 0; max-height: 100%; overflow: hidden;
    margin: 0 20px calc(16px + env(safe-area-inset-bottom)) 0;
    box-shadow: 0 8px 26px rgba(0,0,0,.45);
  }
  /* A short landscape rail cannot hold the header on two lines, a full-height
     meaning slot AND three rows of 48px+ controls. The controls are the
     priority (they are the decision), so the header stays on one line and the
     two explanatory blocks become the flexible ones. */
  .bp-header { flex-wrap: nowrap; min-width: 0; }
  .bp-header .bp-profile { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .bidding-panel {
    gap: 4px; padding: 6px;
  }
  .bidding-panel .auction-grid-wrap {
    flex: 1 1 auto; min-height: 0; max-height: none; overflow-y: auto;
  }
  .bidding-panel .meaning-slot {
    flex: 0 1 auto; min-height: 0; overflow-y: auto;
  }

  /* Landscape is short (an 11" iPad is only 834pt tall), so the top dummy's
     rows shrink with the window, and a side dummy unwraps into a single row of
     four columns — the 2x2 wrap would be twice as tall as there is room for. */
  /* Never derive dummy width solely from the short edge: that reduced cards
     to 28px wide on a landscape iPhone.  A readable minimum wins; suit fans
     are compact enough to fit the available horizontal table space. */
  .dummy-cols:not(.side) { --dw: var(--card-dummy); }
  /* The rule above this used to be `min(38px, 4.6vh)`, which is precisely the
     "derive from the short edge" the comment forbids: on a 430pt-tall phone
     4.6vh is 20px, so the side dummy rendered at 20x29 — worse than the 28x40
     the brief called critical. The floor now wins, and the seat widens to hold
     four columns of it. Only the dummy's own side reserves the width; the
     opposite seat is .bare and stays auto. */
  .dummy-cols.side { --dw: max(52px, calc(var(--card-dummy) * 0.75)); flex-wrap: nowrap; width: auto; }
  /* The tablet block (min-width: 700px) also matches here, and its stacked
     horizontal fans are written with the dummy-at-* class, which outranks the
     one-class rule above — so a side dummy came out 348px tall against the
     307px this band has, and scrolled four of its cards out of sight.
     Landscape has the width for all four columns side by side; restore that
     at matching specificity. */
  .game-screen.dummy-at-W .dummy-cols.side,
  .game-screen.dummy-at-E .dummy-cols.side {
    --dw: max(52px, calc(var(--card-dummy) * 0.75));
    display: flex; flex-direction: row; flex-wrap: nowrap;
    width: auto; gap: 6px; align-items: flex-start;
  }
  .game-screen.dummy-at-E .dummy-cols.side { justify-content: flex-end; }
  .game-screen.dummy-at-W .dummy-cols.side .dummy-col,
  .game-screen.dummy-at-E .dummy-cols.side .dummy-col { flex-direction: column; }
  .game-screen.dummy-at-W .dummy-cols.side .dummy-col .pcard,
  .game-screen.dummy-at-E .dummy-cols.side .dummy-col .pcard {
    margin-left: 0; margin-top: calc(var(--dw) * -0.82);
  }
  .game-screen.dummy-at-W .dummy-cols.side .dummy-col .pcard:first-child,
  .game-screen.dummy-at-E .dummy-cols.side .dummy-col .pcard:first-child { margin-top: 0; }
  /* likewise the tablet's narrowed cross: this layout sizes its own. */
  .game-screen.dummy-at-W .table-center,
  .game-screen.dummy-at-E .table-center { --tw: min(44vmin, 340px, var(--tw-fit, 9999px)); }
  /* Thirteen legible cards in four vertical columns are taller than a landscape
     phone has to give (a 4-card suit alone is 172px against ~127px of room), so
     the zone is bounded and scrolls its overflow. Scrolling is the lesser evil:
     the previous behaviour shrank the cards to 20px wide, and simply letting
     them grow spilled them over the Undo pill and the player's own hand.
     The brief's proper remedy is a top-centre dummy tray in this orientation —
     a layout change rather than a sizing one. */
  .seat-W, .seat-E {
    width: calc(var(--card-dummy) * 0.75 * 4 + 24px); min-width: 226px;
    max-height: 100%; overflow-y: auto; overscroll-behavior: contain;
  }
  /* Same equalisation as the tablet: the dummy's column was pinned narrower by
     the :has() rule while the card-backs opposite kept the full width, which
     put the cross 22px off centre — and off centre the other way when the
     dummy changed sides. */
  .game-screen.dummy-at-W .seat-W, .game-screen.dummy-at-W .seat-E,
  .game-screen.dummy-at-E .seat-W, .game-screen.dummy-at-E .seat-E {
    width: calc(var(--card-dummy) * 0.75 * 4 + 24px);
  }
  .table-middle { align-items: stretch; }

  /* The trick is a cross, so its box is ~0.88 of its width TALL. On a landscape
     phone the middle row only gets ~140px, and a 40vh cross wanted 151 — so it
     spilled onto the seat badges, the WE/THEY counters and the utility pills,
     printing them over a played card. Deriving the width from the height the
     row actually has keeps the cross inside its own space. */
  .table-center { --tw: min(44vmin, 360px, 30vh, var(--tw-fit, 9999px)); gap: 6px; min-height: 0; }
  .trick-area { flex: 0 0 auto; }
  /* 13cqw, not the 10cqw the stacked layouts use: here the table area is only
     the part of the frame left over by the rail, so the same share of the
     window is a much larger share of this box. */
  .user-hand:not(.play2) { --uw: min(clamp(72px, 13vmin, 124px), 17vh, 13cqw); }

  /* Width to spare: the hand plays as ONE row of 13 — the two rendered rows
     are joined end to end. Bigger cards, and the height of a whole row handed
     back to the table. */
  .user-hand.play2 {
    --uw: min(var(--card-player), calc((100cqw - 24px) / 8.8));
    flex-direction: row; gap: 0;
  }
  .user-hand.play2 .hand-line-row + .hand-line-row .pcard:first-child {
    margin-left: calc(var(--uw) * -0.1);   /* the join is always a suit change */
  }

  /* Analysis: the deal diagram becomes a permanent left rail so any note can
     be read against the layout without scrolling. */
  .review-screen {
    display: grid; min-height: 0;
    grid-template-columns: minmax(320px, 380px) minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
    grid-template-areas: "top top" "pin body";
  }
  .review-screen > .top-bar { grid-area: top; }
  .review-screen > .deal-pin {
    grid-area: pin; overflow-y: auto;
    box-shadow: none; border-right: 1px solid var(--stroke);
  }
  .review-screen > .scroll-body { grid-area: body; }
  .deal-pin .ddo-note { display: block; }
  .deal-pin .deal-diagram { gap: 10px; }
}

/* --------------------------------------------- the panel under a mouse

   Every control in the bidding panel is sized for a thumb: 48px is the brief's
   minimum touch target and the tablet band takes several of them past it, to
   54. A pointer is not a thumb. It lands where it is aimed, so the same
   controls only have to be big enough to READ — and at 54px each, three rows
   of them plus the auction grid is most of a browser window's lower half for
   what is, on a desktop, five clicks a deal.

   The query is `pointer: fine`, not a width: what changes here is the input
   device, and there is no width at which a screen stops being touched. An iPad
   reports a coarse pointer even with a trackpad attached, and the app inside
   Capacitor always does, so both keep the touch sizes whatever the window is
   doing. The 700px floor keeps the phone-width layout — which sizes the panel
   against the viewport — out of it entirely. */
@media (min-width: 700px) and (pointer: fine) {
  .bidding-panel { padding: 11px; gap: 7px; }
  .level-row, .strain-row, .special-row { gap: 5px; }
  .level-chip { height: 36px; font-size: 16px; }
  .strain-btn { height: 42px; font-size: 22px; }
  .strain-btn.nt { font-size: 16px; }
  .special-btn { height: 40px; font-size: 15px; }
  .suggest-btn { min-width: 56px; height: 40px; font-size: 17px; padding: 0 12px; }
  .suggest-btn .sb-label { font-size: 14px; }
  .place-btn { height: 36px; font-size: 15px; }

  /* The same argument, applied to the sheets.

     Everything in the >=700px band is sized to be read at arm's length across
     a tablet held in two hands: the result title goes 25px -> 30px, the
     buttons 48px -> 52px, and the type under them up with it. On a laptop the
     sheet is the same 680px card, but it is half a metre away on a screen the
     mouse can hit to the pixel, and at that distance the scale reads as a
     phone screenshot blown up — a 30px headline over 13px rows, and three
     button slabs the width of the dialog to say "Next Deal".

     So the sheets step back down to about the phone's scale, which is what a
     desktop dialog wants anyway. Only the type and the control heights move;
     the sheet keeps its width and its spacing, so nothing reflows. */
  .sheet h3 { font-size: 16.5px; }
  .result-title { font-size: 22px; }
  .result-sub { font-size: 13.5px; margin: 7px 0 12px; }
  .result-debrief { font-size: 12px; padding: 8px 11px; }
  .result-entry { font-size: 13px; }
  .result-event { font-size: 13px; }
  .action-btn { height: 42px; font-size: 14px; border-radius: 11px; }
  .action-btn.subtle { height: 34px; font-size: 13px; }
  .pad-cols { font-size: 11.5px; }
  .pad-row { font-size: 13px; }
  .pad-status { font-size: 13px; }
  .confirm-box .cq { font-size: 16px; }
  .confirm-box .cm { font-size: 13px; }

  /* A press is not a tap.

     The playable card lifts 10px while it is held — a touch affordance, and a
     good one: a thumb covers the card it is on, and the lift is what tells you
     which one you have got before you commit. A mouse click is over in about
     70ms. The lift is a 150ms ease, so the click never buys more than a third
     of it before the card leaves the hand and the element is destroyed: the
     card twitches upward a few pixels and vanishes mid-travel, which is the
     part that looks wrong.

     A pointer does not need to be told what it is over — it needs to be told
     what it is ABOUT to play, and it has a state for that. The lift moves to
     hover, where it is a steady position rather than an animation racing the
     click, and it raises the card out of the fan so the whole face shows
     instead of the 46% strip. The press then adds no movement at all, so the
     card goes from lifted to played with nothing half-finished in between. */
  .user-hand .pcard.playable:hover,
  .user-hand .pcard.playable:active {
    transform: translateY(-10px);
    z-index: 2;
  }
  /* The dummy's cards never had a press state at all — they are not in
     .user-hand, so the lift above has never applied to them. They cannot
     borrow it either: the tray lays them out in overlapping rows in one
     layout and downward columns in another, and a card that lifts in a
     column simply hides under its neighbour. An outline reads the same
     whichever way the tray is stacked. */
  .dummy-cols .pcard.playable:hover {
    outline: 2px solid var(--gold-soft); outline-offset: -2px; z-index: 2;
  }
}

/* ------------------------------- the stacked table, sized by the room it has

   Everything above sizes the table's three bands — dummy tray, trick, hand —
   against the VIEWPORT, and each cap was tuned on a device where the sum came
   out right: a phone, an 11" iPad, a 768x1024 portrait tablet. A browser
   window is neither. It can be 820x825, or 1366x640 with the chrome taken off
   a laptop screen, and there the sum stops fitting: the tray and the hand are
   flex-shrink:0 and take their full height, the middle band gets whatever is
   left — 107px at 820x825, 13px at 900x620 — and the cross, which has an
   intrinsic height, spills equally out of both ends of it. That is the bug in
   the reported screenshots: a played card printed across the dummy's spades,
   card-back columns running up over the status strip, the counters lost behind
   the tray.

   No vh cap can fix that, because the middle band is not a fraction of the
   viewport — it is what the other two bands leave behind. So the sizes are
   taken from the table itself, with container queries:

     .table-area   is the whole table  → 100cqh for the card budget
     .table-middle is the middle band  → 100cqh for the cross

   The budget solves the same equation the tuned numbers were solving by hand.
   With the hand on one row, the three bands cost, in units of the player's
   card width cp:

     hand   1.45cp + 45px      (one row of cards, badge, padding)
     tray   1.22cp + 41px      (0.84cp dummy cards, badge, gap)
     cross  2.11cp + 64px      (a cross 2.4cp wide, plus the counters)
     -----------------------------------------------------------------
            4.78cp + 150px  ≤  the table's height

   so cp ≤ (100cqh - 150px) / 4.78. It is only ever a CAP: on a tall window
   (768x1024, an iPad in portrait) the vmin term is far smaller and nothing
   changes. It bites exactly where the layout used to overflow.

   Short landscape (max-height: 520px) is excluded: that band has its own
   arrangement below, with its own budget worked out in its own comment. */
@media (min-width: 700px) and (min-height: 521px) {
  .table-area { container-type: size; }
  .table-middle { container-type: size; }

  /* Declared on the table's children, not on .game-screen: cq units resolve
     against an ANCESTOR container, so the value has to be computed inside the
     container it is measuring. Custom properties inherit, so the hand and the
     dummy read it from here exactly as they read it from .game-screen before. */
  .table-area > * {
    --card-player: min(clamp(70px, 13.5vmin, 120px), 13.5vh, calc((100cqh - 150px) / 4.78));
    --card-dummy: clamp(70px, calc(var(--card-player) * 0.84), 96px);
  }
  /* Belt and braces. The budget above assumes the tray and the hand cost what
     they usually cost; --tw-fit measures what the middle band ACTUALLY came out
     as and converts it into the widest cross that fits (the box is 0.878 of its
     width tall, and the counters under it take ~64px). Every stacked layout's
     --tw carries it as a final min() term, so however the bands land, the trick
     stays inside its own band instead of printing over its neighbours. */
  .table-center { --tw-fit: calc((100cqh - 64px) / 0.878); }

  /* And the corollary nobody drew out when --tw-fit was added: if the cross is
     measured from what the other two bands LEAVE, then anything that changes
     their height rescales the cross. The pill row is exactly such a thing. It
     gains and loses Hint every turn and Undo every play, and at a laptop
     window — where the middle band is ~200px and --tw-fit is the term that
     actually binds, not the 290px cap — one extra wrapped line of pills takes
     19px off the band and shrinks the trick from 181px to 160px. It grows back
     on the next card. That is the "zoom in and out on every card": not an
     animation, a relayout, twice a trick, in the middle of the table.

     Holding the row to one line holds it to the height of the seat badge,
     which is the tallest thing in it and never changes. The band is then a
     constant, and so is the cross.

     Two things hold it there. The sides become real boxes again — one line
     each, packed toward the badge — and the row becomes a three-column grid
     the full width of the seat, so the badge sits dead centre and a pill
     appearing pushes only its own side outward instead of sliding the whole
     strip 100px sideways. There is no min-width:0 on the sides: a 1fr track is
     minmax(auto, 1fr), and that auto floor is what stops a side being handed
     less room than its pills need. Without it the sides are squeezed to half
     the row whatever is in them and the pills stack — which is the failure
     this block exists to prevent. With it, the columns are equal whenever the
     row has the width for it, and when it does not the badge gives ground by
     exactly the shortfall.

     Below 700px none of this applies — the phone layouts give --tw a fixed
     value and never consult --tw-fit — so there the row stays the plain
     centred wrapping strip it has always been. */
  .badge-row {
    display: grid; grid-template-columns: 1fr auto 1fr;
    align-items: center; gap: 8px; width: 100%;
  }
  .badge-row .br-side {
    display: flex; align-items: center; gap: 8px; flex-wrap: nowrap;
  }
  .badge-row .br-side.left { justify-content: flex-end; }
  .badge-row .br-side.right { justify-content: flex-start; }
  .badge-row .hint-pill, .badge-row .seat-badge { flex-shrink: 0; }
  .badge-row .hint-pill { white-space: nowrap; }
}

/* The wide-landscape block below joins the hand into one row and sizes cards
   from the width; inside this band the budget above has to apply on top of it,
   so it is restated here with the same terms plus the cap. */
@media (min-width: 920px) and (orientation: landscape) and (min-height: 521px) {
  .table-area > * {
    --card-player: min(clamp(66px, 9.5vw, 118px), 17vh, calc((100cqh - 150px) / 4.78));
    --card-dummy: clamp(52px, calc(var(--card-player) * 0.84), 96px);
  }
}

/* One row of thirteen, whenever the window is too short for two.

   Landscape ≥920px has done this since the rail was built — the two rendered
   rows are joined end to end — and the reason given there was width to spare.
   The stronger reason is height: the second row costs a whole card height,
   ~160px at tablet sizes, and it is the single largest item in the budget
   above. A window under 960px tall does not have it to spend, whichever way
   round it is: 820x825 portrait and 900x620 landscape both overflowed by
   roughly one row.

   The width cap keeps the joined row inside the window, and it has to count
   the suit gaps: twelve joins at 46% overlap are 0.54 card-widths each, but
   the three that fall on a suit change (including the join between the two
   rendered rows) are only 10% overlapped and cost 0.9 — so a four-suit hand
   measures 8.56 card-widths, not the 7.48 the overlap alone suggests. Sizing
   on 7.48 is what pushed the row 26px off both edges of an 820px window. */
@media (min-width: 700px) and (min-height: 521px) and (max-height: 959px) {
  .user-hand.play2 {
    --uw: min(var(--card-player), calc((100cqw - 24px) / 8.8));
    flex-direction: row; gap: 0;
  }
  .user-hand.play2 .hand-line-row + .hand-line-row .pcard:first-child {
    margin-left: calc(var(--uw) * -0.1);   /* the join is always a suit change */
  }
}

/* ------------------------------------------------- short landscape (phone) */

/* A landscape phone gives the middle row about 140px. The trick's cross is
   ~0.88 of its width tall, so a trick whose cards meet the brief's 64x92
   minimum would need a 197px-tall box — it cannot be done, and shrinking the
   cross until it fits produced 42x56 cards, smaller than the dummy beside them,
   which the brief explicitly forbids. Laying the four cards in a ROW costs one
   card height instead of one and a half, so they keep full size and stop
   colliding with the seat badges and counters. */
@media (orientation: landscape) and (max-height: 520px) {
  /* The row still needs ~16px that the middle row does not have, so the hand
     gives it up: 15vh keeps the player's cards at the brief's 64px floor while
     freeing the height the trick and the badges were fighting over. */
  .game-screen {
    --card-player: min(clamp(64px, 9.5vw, 118px), 15vh);
    --card-dummy: clamp(52px, calc(var(--card-player) * 0.84), 96px);
  }
  /* --- the brief's landscape play arrangement -------------------------------
     dummy tray left · trick and score centre · utilities right · hand across
     the bottom. Previously all three of those competed for one horizontal row
     between the status bar and the hand, which is why a legible dummy, a
     full-size trick and the utility pills could not coexist: the dummy zone
     (226px), the trick row (274px) and the opposite seat all wanted the same
     band. Naming the regions and laying them out as a grid gives each its own
     space instead of trading pixels between them.

     `display: contents` dissolves the two wrapper divs so their children can be
     placed directly — the markup keeps its nesting for every other layout. */
  .game-screen:not(.is-bidding) .table-area {
    /* The opponents get a column each, flanking the cross. 38px is what a
       card back needs to read as a card back and nothing more — see the
       silhouette note below. */
    --side: 38px;
    display: grid;
    grid-template-columns: minmax(0, auto) var(--side) 132px var(--side) minmax(0, auto);
    grid-template-rows: minmax(0, 1fr) auto;
    grid-template-areas:
      "dummy left trick right util"
      "hand  hand hand  hand  hand";
    align-items: center;
    column-gap: 6px; row-gap: 2px;
  }
  .game-screen:not(.is-bidding) .table-middle,
  .game-screen:not(.is-bidding) .seat-zone.seat-S { display: contents; }
  .game-screen:not(.is-bidding) .table-watermark { grid-area: trick; }

  .game-screen:not(.is-bidding) .seat-zone.is-dummy {
    grid-area: dummy; align-self: center; justify-self: start;
    max-height: 100%; overflow-y: auto; overscroll-behavior: contain;
  }
  /* The centre column is the cross plus the two counters — 166px of content
     against a 141px band. Centred, it overflowed EQUALLY in both directions:
     the North card was clipped by the status strip above and the counters sat
     on the player's own hand below. Anchoring it to the top of the band, and
     giving the counters the phone's proportions instead of the tablet's
     (this width matches the tablet query too), lands the whole column inside
     the space between the strip and the hand. */
  .game-screen:not(.is-bidding) .table-center {
    grid-area: trick; align-self: stretch; justify-content: flex-start; gap: 3px;
  }
  .game-screen:not(.is-bidding) .tc-pill { padding: 3px 9px; gap: 4px; }
  .game-screen:not(.is-bidding) .tc-pill .l { font-size: 10px; }
  .game-screen:not(.is-bidding) .tc-pill .v { font-size: 14px; }
  .game-screen:not(.is-bidding) .badge-row {
    grid-area: util; flex-direction: column; align-items: stretch;
    gap: 6px; justify-self: end; max-height: 100%; overflow-y: auto;
  }
  /* The rail is sized by its widest item, and that was "South · You ·
     Declarer" at 150px — half again the width of every button beside it, and
     the single largest claim on a band that now has to seat two more columns.
     It is also the one thing on this screen said twice: the status strip
     directly above reads "3NT by S — your play", and the hand it names is the
     one spread across the bottom of the screen. Dropping it here holds the
     rail to the width of its buttons (~100px) and leaves the dummy tray its
     full row. Every other layout keeps the badge. */
  .game-screen:not(.is-bidding):not(.dummy-at-W):not(.dummy-at-E)
    .badge-row > .seat-badge { display: none; }
  .game-screen:not(.is-bidding) .user-hand { grid-area: hand; }
  /* The opponents, as a silhouette down each side of the cross — the same cue
     portrait gets, on the budget this orientation has. What goes instead is
     their NAME: the badge costs 34px out of a 141px band, and in a 38px column
     "West · Robot" wraps to three lines taller than the stack it labels. The
     seat is still unambiguous — the stack sits on the side of the cross the
     seat plays into, and the status strip names the seat to play in words. */
  .game-screen:not(.is-bidding) .seat-zone.seat-W { grid-area: left; }
  .game-screen:not(.is-bidding) .seat-zone.seat-E { grid-area: right; }
  .game-screen:not(.is-bidding) .seat-zone.seat-W,
  .game-screen:not(.is-bidding) .seat-zone.seat-E {
    display: flex; max-height: 100%; gap: 0;
  }
  /* `.is-hidden` is carried only to match the phone block's 58px reservation,
     which is a wider selector than this one and otherwise wins — a 58px zone
     in a 38px column overhangs into the cross on one side and the utility
     rail on the other. */
  .game-screen:not(.is-bidding) .seat-zone.is-hidden.seat-W,
  .game-screen:not(.is-bidding) .seat-zone.is-hidden.seat-E { width: var(--side); }
  .game-screen:not(.is-bidding):not(.dummy-at-W):not(.dummy-at-E)
    .seat-zone.is-hidden .seat-badge { display: none; }
  .game-screen:not(.is-bidding) .seat-zone.is-hidden .hidden-hand { display: flex; }
  /* 6px of exposed edge — half what portrait affords. Thirteen of these stand
     96px tall, which is what clears the 120px cross beside them. */
  .game-screen:not(.is-bidding) .seat-zone.seat-W .hidden-hand.vertical .card-back,
  .game-screen:not(.is-bidding) .seat-zone.seat-E .hidden-hand.vertical .card-back {
    width: 34px; height: 24px; margin-top: -18px;
  }
  .game-screen:not(.is-bidding) .seat-zone.seat-W .hidden-hand.vertical .card-back:first-child,
  .game-screen:not(.is-bidding) .seat-zone.seat-E .hidden-hand.vertical .card-back:first-child {
    margin-top: 0;
  }
  /* The dummy tray now shares the band with two more columns. Sized `auto` it
     would win the argument and push the cross off the screen, so it wraps
     instead: a second row of fans inside a zone that already scrolls. Only the
     widest trays ever reach it. */
  .game-screen:not(.is-bidding) .seat-zone.is-dummy .dummy-cols:not(.side) { flex-wrap: wrap; }

  /* Each suit fans HORIZONTALLY. This is the tray the tablet query already
     builds, but that query starts at 700px — so a 667px landscape phone fell
     back to tall vertical columns, and a six-card suit stood 239px high in a
     171px band: the dummy's own long suit scrolled out of sight. A row costs
     one card height whatever the suit's length, which is the only shape this
     band can hold. Repeating it here binds it to the ORIENTATION rather than
     to a width, and is a no-op on the wide landscape phones the tablet query
     was already covering. */
  /* Row-gap is tighter than column-gap: the second row only exists on the
     narrowest landscape phones, where every pixel of it is height the band
     does not have. */
  .dummy-cols:not(.side) { --dw: var(--card-dummy); gap: 4px 10px; flex-wrap: wrap; }
  .dummy-cols:not(.side) .dummy-col { flex-direction: row; }
  .dummy-cols:not(.side) .dummy-col .pcard {
    margin-top: 0; margin-left: calc(var(--dw) * -0.58);
  }
  .dummy-cols:not(.side) .dummy-col .pcard:first-child { margin-left: 0; }
  .dummy-cols:not(.side) .pcard .idx {
    flex-direction: column; align-items: center; gap: 0;
    top: 5%; left: 8%; font-size: calc(var(--dw) * 0.34);
  }
  .dummy-cols:not(.side) .pcard .idx span:last-child { font-size: calc(var(--dw) * 0.34); }
  .dummy-cols:not(.side) .pcard.r10 .idx,
  .dummy-cols:not(.side) .pcard.r10 .idx span:last-child {
    left: 5%; font-size: calc(var(--dw) * 0.28);
  }
  .dummy-cols:not(.side) .pcard .pip { display: block; font-size: calc(var(--dw) * 0.5); }

  .table-center { gap: 4px; }
  /* The trick is a CROSS here whoever holds the dummy, not a left-to-right
     row. A row is perfectly readable and says nothing about WHO played each
     card, which is the one thing a trick has to say. 52x74 is under the
     brief's floor for a trick card and is what buys the separation inside the
     120px of height this layout has: at 92px tall the North and South cards
     cannot be told apart. */
  .trick-area {
    display: block; position: relative;
    width: 132px; height: 120px;
  }
  .trick-area .felt-print { display: none; }
  /* `relative`, not `static`: the rank and pip inside are absolutely positioned
     against the card, so removing the card as a containing block sent them to
     the nearest positioned ancestor — a stray K in one corner of the table and
     a pip floating over a blank card. Relative keeps it a flex item AND keeps
     its own contents anchored to it. */
  .trick-card {
    position: absolute; inset: auto;
    width: 52px; height: 74px;
  }
  .trick-card.pos-N { top: 0; left: 40px; bottom: auto; right: auto; transform: rotate(-3deg); }
  .trick-card.pos-S { bottom: 0; left: 40px; top: auto; right: auto; transform: rotate(3deg); }
  .trick-card.pos-W { top: 23px; left: 0; bottom: auto; right: auto; transform: rotate(-6deg); }
  .trick-card.pos-E { top: 23px; right: 0; bottom: auto; left: auto; transform: rotate(6deg); }
  .trick-card .tc-idx { font-size: 17px; top: 5%; left: 8%; }
  .trick-card .tc-pip { font-size: 26px; }

  /* ---- an OPPONENT's dummy: the compass table ------------------------------
     The flat row above is readable but says nothing about WHO played each
     card, which is the one thing a trick has to say. Here the four seats take
     their compass positions and the trick goes back to a cross, so every card
     sits toward the hand that played it.

     The budget is exact and worth recording. 250px of table height, less
     padding, leaves 236: 24 (partner strip) + 124 (cross) + 88 (hand). 124px
     cannot separate two 92px cards, so the trick card drops to 52x74 here —
     under the brief's floor, and the price of a cross you can read a seat off.
     The hand goes to ONE row for the same reason; two rows cost 90px that the
     cross needs more. */
  .game-screen.dummy-at-W:not(.is-bidding),
  .game-screen.dummy-at-E:not(.is-bidding) { --card-player: 46px; }

  .game-screen.dummy-at-W:not(.is-bidding) .table-area,
  .game-screen.dummy-at-E:not(.is-bidding) .table-area {
    padding: 8px 14px calc(6px + env(safe-area-inset-bottom));
    grid-template-rows: 24px minmax(0, 1fr) auto;
    column-gap: 6px; row-gap: 0;
    align-items: center; justify-items: center;
  }
  /* Area names are SCREEN positions, never seat roles — "left" is always the
     left-hand column. Only the widths swap, and the utility rail swaps with
     them: parked on a fixed edge it stacked against the wide dummy column and
     shoved the cross 150px off centre. Mirrored, the cross lands within 9px of
     the screen's centre line either way. */
  .game-screen.dummy-at-W:not(.is-bidding) .table-area {
    grid-template-columns: 312px 182px 156px minmax(0, 1fr);
    grid-template-areas:
      "top  top   top   util"
      "left trick right util"
      "hand hand  hand  util";
  }
  .game-screen.dummy-at-E:not(.is-bidding) .table-area {
    grid-template-columns: minmax(0, 1fr) 156px 182px 312px;
    grid-template-areas:
      "util top  top   top"
      "util left trick right"
      "util hand hand  hand";
  }
  .game-screen.dummy-at-W:not(.is-bidding) .seat-zone.seat-W,
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.seat-W { grid-area: left; display: flex; }
  .game-screen.dummy-at-W:not(.is-bidding) .seat-zone.seat-E,
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.seat-E { grid-area: right; display: flex; }
  .game-screen.dummy-at-W:not(.is-bidding) .seat-zone.seat-N,
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.seat-N {
    grid-area: top; display: flex; flex-direction: row; align-items: center; gap: 8px;
  }
  .game-screen.dummy-at-W:not(.is-bidding) .seat-zone.is-hidden .hidden-hand,
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.is-hidden .hidden-hand { display: flex; }

  /* The tricks-won counters are a scoreboard, not table furniture: moving them
     out of the centre frees the whole middle row for the cross. display:
     contents dissolves .table-center so both children place directly, the
     same trick .table-middle already uses. */
  .game-screen.dummy-at-W:not(.is-bidding) .table-center,
  .game-screen.dummy-at-E:not(.is-bidding) .table-center { display: contents; }
  .game-screen.dummy-at-W:not(.is-bidding) .trick-counters,
  .game-screen.dummy-at-E:not(.is-bidding) .trick-counters {
    grid-area: top; justify-self: end; align-self: center; display: flex; gap: 6px;
  }
  .game-screen.dummy-at-W:not(.is-bidding) .trick-counters .tc-pill,
  .game-screen.dummy-at-E:not(.is-bidding) .trick-counters .tc-pill,
  .game-screen.dummy-at-W:not(.is-bidding) .seat-zone .seat-badge,
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone .seat-badge {
    font-size: 10px; padding: 2px 8px;
  }

  /* the cross restored, with the spread the flat row was hiding */
  .game-screen.dummy-at-W:not(.is-bidding) .trick-area,
  .game-screen.dummy-at-E:not(.is-bidding) .trick-area {
    grid-area: trick; display: block; position: relative;
    width: 174px; height: 124px;
  }
  .game-screen.dummy-at-W:not(.is-bidding) .trick-card.pos-N,
  .game-screen.dummy-at-E:not(.is-bidding) .trick-card.pos-N { top: 0; left: 61px; transform: rotate(-3deg); }
  .game-screen.dummy-at-W:not(.is-bidding) .trick-card.pos-S,
  .game-screen.dummy-at-E:not(.is-bidding) .trick-card.pos-S { bottom: 0; left: 61px; transform: rotate(3deg); }
  .game-screen.dummy-at-W:not(.is-bidding) .trick-card.pos-W,
  .game-screen.dummy-at-E:not(.is-bidding) .trick-card.pos-W { top: 25px; left: 0; transform: rotate(-6deg); }
  .game-screen.dummy-at-W:not(.is-bidding) .trick-card.pos-E,
  .game-screen.dummy-at-E:not(.is-bidding) .trick-card.pos-E { top: 25px; right: 0; transform: rotate(6deg); }

  /* ONE row, for every dummy position. Two rows cost ~90px of a 232px table,
     which is the whole reason a North dummy was squeezed into a 52px scroll
     box against the 117px its four fans need — eleven of its cards were off
     screen. 812px of width has room for thirteen cards in a line. */
  .game-screen:not(.is-bidding) .user-hand.play2 {
    flex-direction: row; align-items: flex-end; gap: 10px;
  }

  /* Dummy: 2 x 2 grid of HORIZONTAL fans — one card-height per suit whatever
     its length, which is what fits 13 cards in the 124px middle row. `auto`
     columns, not 1fr: a singleton suit beside a five-card fan left a 120px
     hole. Card sizes across the table are hand 46 / dummy 40 / trick 52; they
     were 56 / 36 / 52, and that 20px spread is what read as compressed. */
  .game-screen.dummy-at-W:not(.is-bidding) .seat-zone.seat-W.is-dummy,
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.seat-E.is-dummy {
    width: auto; max-height: none; overflow: visible;
    align-self: center; justify-self: start; align-items: flex-start;
  }
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.seat-E.is-dummy {
    justify-self: end; align-items: flex-end;
  }
  .game-screen.dummy-at-W:not(.is-bidding) .dummy-cols.side,
  .game-screen.dummy-at-E:not(.is-bidding) .dummy-cols.side {
    --dw: 40px;
    display: grid; grid-template-columns: repeat(2, auto);
    width: auto; column-gap: 14px; row-gap: 2px;
  }
  .game-screen.dummy-at-W:not(.is-bidding) .dummy-cols.side .dummy-col,
  .game-screen.dummy-at-E:not(.is-bidding) .dummy-cols.side .dummy-col { flex-direction: row; }
  .game-screen.dummy-at-W:not(.is-bidding) .dummy-cols.side .dummy-col .pcard,
  .game-screen.dummy-at-E:not(.is-bidding) .dummy-cols.side .dummy-col .pcard {
    margin-top: 0; margin-left: calc(var(--dw) * -0.34);
  }
  .game-screen.dummy-at-W:not(.is-bidding) .dummy-cols.side .dummy-col .pcard:first-child,
  .game-screen.dummy-at-E:not(.is-bidding) .dummy-cols.side .dummy-col .pcard:first-child { margin-left: 0; }

  /* the declarer's stack opposite, and the partner's strip across the top */
  .game-screen.dummy-at-W:not(.is-bidding) .seat-zone.seat-E,
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.seat-W { width: 156px; }
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.seat-W { justify-self: end; }
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.seat-W .hidden-hand { justify-content: flex-end; }
  .game-screen.dummy-at-W:not(.is-bidding) .seat-zone.seat-E .hidden-hand,
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.seat-W .hidden-hand { flex-direction: row; }
  .game-screen.dummy-at-W:not(.is-bidding) .seat-zone.seat-E .hidden-hand .card-back,
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.seat-W .hidden-hand .card-back {
    width: 38px; height: 55px; margin-top: 0; margin-left: -28px;
  }
  .game-screen.dummy-at-W:not(.is-bidding) .seat-N .hidden-hand .card-back,
  .game-screen.dummy-at-E:not(.is-bidding) .seat-N .hidden-hand .card-back {
    width: 20px; height: 22px; margin-left: -12px;
  }

  /* The tray is centred in its row and runs a few px taller than it, so its
     lowest card finished flush against the top of the player's hand — touching
     exactly, gap 0. A margin lifts it clear without touching the card sizes. */
  .game-screen.dummy-at-W .seat-zone.seat-W.is-dummy,
  .game-screen.dummy-at-E .seat-zone.seat-E.is-dummy { margin-bottom: 12px; }

  /* the rail gives back ~38px, which goes into the cross'+String.fromCharCode(39)+'s spread */
  .game-screen.dummy-at-W:not(.is-bidding) .badge-row .hint-pill,
  .game-screen.dummy-at-E:not(.is-bidding) .badge-row .hint-pill {
    font-size: 11px; padding: 6px 10px; gap: 4px;
  }
}

/* ----------------------------------------------------- phone portrait play */

/* The brief's portrait arrangement: dummy tray at the top, trick in the centre,
   a utility row, then the two-row hand. Previously the dummy sat wherever the
   compass put it — a tall 2x2 block at the side when East or West declared —
   which squeezed the centre and let the trick counters collide with the cards.
   Naming the regions and laying them out in rows gives each its own band, the
   same fix the landscape rail got. */
@media (max-width: 699px) and (orientation: portrait) {
  .game-screen:not(.is-bidding) .table-area {
    /* 58px matches the column width the phone block reserves for a concealed
       seat, so the zone fills its grid cell exactly instead of overhanging it. */
    --side: 58px;
    display: grid;
    grid-template-columns: var(--side) minmax(0, 1fr) var(--side);
    grid-template-rows: auto minmax(0, 1fr) auto auto;
    grid-template-areas:
      "dummy dummy dummy"
      "left  trick right"
      "util  util  util"
      "hand  hand  hand";
    row-gap: 4px; column-gap: 4px; align-items: center; justify-items: center;
  }
  .game-screen:not(.is-bidding) .table-middle,
  .game-screen:not(.is-bidding) .seat-zone.seat-S { display: contents; }
  /* display:contents dissolves the band, so the mark becomes a grid item and
     has to name its cell — the trick's — to stay centred on the cross. */
  .game-screen:not(.is-bidding) .table-watermark { grid-area: trick; }

  .game-screen:not(.is-bidding) .seat-zone.is-dummy {
    grid-area: dummy; width: 100%; max-height: 42vh;
    overflow-y: auto; overscroll-behavior: contain;
  }
  /* With the dummy in a top tray and the empty side seats gone, the centre has
     the whole width — enough for the trick to reach the brief's 64x92 floor
     instead of the 60x83 it was squeezed to. */
  .game-screen:not(.is-bidding) .table-center { grid-area: trick; --tw: min(228px, 53vw); }
  .game-screen:not(.is-bidding) .badge-row { grid-area: util; }
  .game-screen:not(.is-bidding) .user-hand { grid-area: hand; width: 100%; }

  /* The opponents, as a stack of backs down each edge. They carry no
     information — every concealed hand holds the same count — and that is why
     they used to be dropped here. What went with them was the table itself:
     with the dummy hoisted into the top band, nothing on screen said that the
     cards arriving from the left and right belong to two players sitting
     there, and a trick played into empty margins reads as a solitaire layout
     rather than as two sides. The column is deliberately thin: a silhouette to
     place the seat by, not a hand anyone is meant to read. */
  .game-screen:not(.is-bidding) .seat-zone.seat-W { grid-area: left; }
  .game-screen:not(.is-bidding) .seat-zone.seat-E { grid-area: right; }
  .game-screen:not(.is-bidding) .seat-zone.seat-W,
  .game-screen:not(.is-bidding) .seat-zone.seat-E {
    display: flex; width: var(--side); max-height: 100%; gap: 5px;
  }
  .game-screen:not(.is-bidding) .seat-zone.is-hidden .hidden-hand { display: flex; }
  /* Overlapped to ~9px of exposed edge: enough that each card reads as its own
     outline, short enough that thirteen of them clear the trick's height. */
  .game-screen:not(.is-bidding) .seat-zone.seat-W .hidden-hand.vertical .card-back,
  .game-screen:not(.is-bidding) .seat-zone.seat-E .hidden-hand.vertical .card-back {
    width: 46px; height: 32px; margin-top: -23px;
  }
  .game-screen:not(.is-bidding) .seat-zone.seat-W .hidden-hand.vertical .card-back:first-child,
  .game-screen:not(.is-bidding) .seat-zone.seat-E .hidden-hand.vertical .card-back:first-child {
    margin-top: 0;
  }
  .game-screen:not(.is-bidding) .seat-zone.seat-W .seat-badge,
  .game-screen:not(.is-bidding) .seat-zone.seat-E .seat-badge {
    font-size: 10px; padding: 3px 5px; letter-spacing: -.1px;
  }

  /* In a wide, short tray each suit fans HORIZONTALLY — a vertical column costs
     about five card-heights for a long suit, a row costs one whatever its
     length. Four fans do not fit across 430px, so they wrap two-by-two. */
  .game-screen:not(.is-bidding) .dummy-cols,
  .game-screen:not(.is-bidding) .dummy-cols.side {
    --dw: var(--card-dummy);
    display: flex; flex-wrap: wrap; width: auto; max-width: 100%;
    justify-content: center; align-items: flex-start; gap: 6px 14px;
  }
  .game-screen:not(.is-bidding) .dummy-cols .dummy-col { flex-direction: row; }
  .game-screen:not(.is-bidding) .dummy-cols .dummy-col .pcard {
    margin-top: 0; margin-left: calc(var(--dw) * -0.58);
  }
  .game-screen:not(.is-bidding) .dummy-cols .dummy-col .pcard:first-child { margin-left: 0; }
  /* vertical index (rank above suit), as the tablet tray uses */
  .game-screen:not(.is-bidding) .dummy-cols .pcard .idx {
    flex-direction: column; align-items: center; gap: 0;
    top: 5%; left: 8%; font-size: calc(var(--dw) * 0.34);
  }
  .game-screen:not(.is-bidding) .dummy-cols .pcard .idx span:last-child { font-size: calc(var(--dw) * 0.34); }
  .game-screen:not(.is-bidding) .dummy-cols .pcard.r10 .idx,
  .game-screen:not(.is-bidding) .dummy-cols .pcard.r10 .idx span:last-child {
    left: 5%; font-size: calc(var(--dw) * 0.28);
  }
  .game-screen:not(.is-bidding) .dummy-cols .pcard .pip { display: block; font-size: calc(var(--dw) * 0.5); }

  /* ---- an OPPONENT's dummy: the compass table ------------------------------
     The band arrangement above hoists the dummy to the top row whichever seat
     holds it, which throws away the one cue that says WHICH opponent you are
     looking at. When the dummy belongs to the opposition it goes to its own
     edge instead, the top band is free for the partner's concealed hand, and
     the declarer takes the column opposite the tray — the compass, as at a
     table. `dummy-at-W`/`dummy-at-E` are set in gameScreen().

     The side columns are deliberately EQUAL. Sized to their contents the trick
     landed 26px off the screen's centre line; at 92px each it sits on it. */
  .game-screen.dummy-at-W:not(.is-bidding) .table-area,
  .game-screen.dummy-at-E:not(.is-bidding) .table-area {
    --side: 92px;
    grid-template-columns: var(--side) minmax(0, 1fr) var(--side);
    grid-template-rows: auto minmax(0, 1fr) auto auto;
    grid-template-areas:
      "top   top   top"
      "left  trick right"
      "util  util  util"
      "hand  hand  hand";
    column-gap: 6px; align-items: center; justify-items: center;
  }
  .game-screen.dummy-at-W:not(.is-bidding) .seat-zone.seat-W,
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.seat-W { grid-area: left; }
  .game-screen.dummy-at-W:not(.is-bidding) .seat-zone.seat-E,
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.seat-E { grid-area: right; }
  .game-screen.dummy-at-W:not(.is-bidding) .seat-zone.seat-W,
  .game-screen.dummy-at-W:not(.is-bidding) .seat-zone.seat-E,
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.seat-W,
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.seat-E {
    display: flex; width: var(--side); max-height: 100%;
  }
  .game-screen.dummy-at-W:not(.is-bidding) .seat-zone.seat-N,
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.seat-N {
    grid-area: top; display: flex; flex-direction: row;
    align-items: center; justify-content: center; gap: 10px; width: 100%;
  }
  .game-screen.dummy-at-W:not(.is-bidding) .table-center,
  .game-screen.dummy-at-E:not(.is-bidding) .table-center {
    grid-area: trick; --tw: min(159px, 43vw);
  }
  /* Every seat present. The count is identical in each concealed hand — one
     card leaves every hand per trick — so the backs carry no information the
     status strip lacks. They earn their place by making the table read as the
     game: two empty chairs opposite does not. */
  .game-screen.dummy-at-W:not(.is-bidding) .seat-zone.is-hidden .hidden-hand,
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.is-hidden .hidden-hand { display: flex; }

  /* The dummy's four suits as a 2 x 2 grid of VERTICAL columns, in ♠♥ / ♣♦
     reading order. A grid rather than flex-wrap: a singleton suit beside a
     five-card fan left a ragged hole. Two columns is what keeps the tray
     inside 92px and the trick on the centre line. */
  .game-screen.dummy-at-W:not(.is-bidding) .dummy-cols.side,
  .game-screen.dummy-at-E:not(.is-bidding) .dummy-cols.side {
    --dw: 42px;
    display: grid; grid-template-columns: repeat(2, var(--dw));
    justify-content: center; align-items: start;
    column-gap: 6px; row-gap: 4px; width: auto;
  }
  .game-screen.dummy-at-W:not(.is-bidding) .dummy-cols.side .dummy-col,
  .game-screen.dummy-at-E:not(.is-bidding) .dummy-cols.side .dummy-col { flex-direction: column; }
  .game-screen.dummy-at-W:not(.is-bidding) .dummy-cols.side .dummy-col .pcard,
  .game-screen.dummy-at-E:not(.is-bidding) .dummy-cols.side .dummy-col .pcard {
    margin-left: 0; margin-top: calc(var(--dw) * -0.72);
  }
  .game-screen.dummy-at-W:not(.is-bidding) .dummy-cols.side .dummy-col .pcard:first-child,
  .game-screen.dummy-at-E:not(.is-bidding) .dummy-cols.side .dummy-col .pcard:first-child { margin-top: 0; }
  /* the horizontal index (rank beside suit) reads in the ~30px strip a
     vertical column leaves visible; the stacked one clipped "10" to "1(" */
  .game-screen.dummy-at-W:not(.is-bidding) .dummy-cols.side .pcard .idx,
  .game-screen.dummy-at-E:not(.is-bidding) .dummy-cols.side .pcard .idx {
    flex-direction: row; align-items: center; gap: 1px;
    top: 5%; left: 10%; font-size: calc(var(--dw) * 0.44);
  }
  .game-screen.dummy-at-W:not(.is-bidding) .dummy-cols.side .pcard .idx span:last-child,
  .game-screen.dummy-at-E:not(.is-bidding) .dummy-cols.side .pcard .idx span:last-child {
    font-size: calc(var(--dw) * 0.46);
  }
  .game-screen.dummy-at-W:not(.is-bidding) .dummy-cols.side .pcard.r10 .idx,
  .game-screen.dummy-at-E:not(.is-bidding) .dummy-cols.side .pcard.r10 .idx,
  .game-screen.dummy-at-W:not(.is-bidding) .dummy-cols.side .pcard.r10 .idx span:last-child,
  .game-screen.dummy-at-E:not(.is-bidding) .dummy-cols.side .pcard.r10 .idx span:last-child {
    left: 6%; font-size: calc(var(--dw) * 0.34);
  }
  .game-screen.dummy-at-W:not(.is-bidding) .dummy-cols.side .pcard .pip,
  .game-screen.dummy-at-E:not(.is-bidding) .dummy-cols.side .pcard .pip { display: none; }

  /* The declarer's stack opposite. Tightened so it does not out-run the dummy
     beside it — but the FIRST card keeps its margin, or the whole stack rides
     up over its own seat badge. */
  .game-screen.dummy-at-W:not(.is-bidding) .hidden-hand.vertical .card-back,
  .game-screen.dummy-at-E:not(.is-bidding) .hidden-hand.vertical .card-back {
    width: 52px; height: 34px; margin-top: -25px;
  }
  .game-screen.dummy-at-W:not(.is-bidding) .hidden-hand.vertical .card-back:first-child,
  .game-screen.dummy-at-E:not(.is-bidding) .hidden-hand.vertical .card-back:first-child { margin-top: 0; }

  .game-screen.dummy-at-W:not(.is-bidding) .seat-zone.seat-W .seat-badge,
  .game-screen.dummy-at-W:not(.is-bidding) .seat-zone.seat-E .seat-badge,
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.seat-W .seat-badge,
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.seat-E .seat-badge {
    font-size: 11px; padding: 3px 7px; text-align: center; gap: 4px;
  }
}
