/* ---------------------------------------------------------------------------
   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;
  /* Set from js/native.js on iPadOS 26, where the system's window controls are drawn
     over the top-left corner of the app's own content and no env() describes them.
     Zero everywhere else, which is what makes it safe to add unconditionally. */
  --win-ctl-left: 0px;
  --win-ctl-right: 0px;
}

* { 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 .bi-tag, body.text-large .bi-hcp { font-size: 14px; }
body.text-large .ag-head { font-size: 17px; }
body.text-large .ag-cell { font-size: 25px; }
body.text-large .bi-text { font-size: 17px; }
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); }

/* The side insets are zero in portrait and only pay for themselves when the
   phone is turned: landscape puts the Dynamic Island and the rounded corners
   against the LEFT or RIGHT edge, and every screen in this file bleeds its
   content to that edge. Inset once here rather than in each screen — #app is
   the only box every screen shares, the strip it gives up shows --bg-deep,
   and a screen that wants to paint under the island can still undo it. */
#app {
  height: 100dvh; display: flex; flex-direction: column;
  padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right);
}
/* 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;
  padding-left: calc(12px + var(--win-ctl-left));
  padding-right: calc(12px + var(--win-ctl-right));
  flex-shrink: 0;
}
/* One line, always. The web build hangs a fourth control (the ✕) off this bar,
   and on a 320px screen "Deal 12 · Practice" between four 36px buttons is close
   enough to the edge to wrap — which would make the bar taller and take the
   height out of the table below it. Truncating is the better failure. */
.top-bar h2 {
  font-size: 17px; font-weight: 700; flex: 1; text-align: center; color: var(--text);
  min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.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); }
/* 36px is a deliberate size, not an oversight — the comment on .top-bar above
   explains why four controls have to fit a 320px screen — but 36 is short of
   the 44px a thumb actually needs, and this bar carries ≡ and ✕ side by side,
   where a mis-tap means "close the game" instead of "open the menu".
   So the BOX stays 36 and only the hit area grows, the same trick
   .home-site-link uses a few rules down: an inset overlay, which reserves no
   width and therefore cannot reintroduce the wrap the .top-bar comment is
   guarding against. The gap between two icon buttons is 8px, so ±4px makes
   neighbours meet exactly rather than overlap — the boundary lands midway
   between them, which is where it belongs. */
@media (pointer: coarse) {
  .icon-btn { position: relative; }
  .icon-btn::after {
    content: ''; position: absolute; inset: -4px;
    /* no background: this is a touch target, not a visible control */
  }
}

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

/* Top to bottom: a title-bar-sized brand, the table card (score + the one
   button most visits are for), then the other destinations in three labelled
   groups. Everything shares one 430px column on a phone. */
.home-screen {
  background: linear-gradient(180deg, var(--bg) 0%, var(--bg-deep) 100%);
  align-items: center;
  padding: max(14px, env(safe-area-inset-top)) 18px calc(10px + env(safe-area-inset-bottom));
  overflow-y: auto;
  justify-content: flex-start;
  gap: 22px;
}
.home-head, .home-body { width: min(430px, 100%); }
.home-head {
  display: flex; align-items: center; gap: 12px; padding-top: 6px;
  padding-left: var(--win-ctl-left); padding-right: var(--win-ctl-right);
}
.home-brand { display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0; }
.home-brand .rase-tile { flex-shrink: 0; filter: drop-shadow(0 6px 16px rgba(0,0,0,.45)); }
.home-brand h1 { line-height: 0; }
.home-brand .tagline { margin-top: 6px; font-size: 12.5px; }
.home-help { font-size: 18px; font-weight: 800; }
.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; }

.home-body { display: flex; flex-direction: column; gap: 22px; }

.home-play {
  display: flex; flex-direction: column; gap: 12px;
  padding: 14px;
  background: var(--surface); border: 1px solid rgba(198,163,83,.4); border-radius: 18px;
}
.hp-head { display: flex; flex-direction: column; gap: 3px; padding: 0 2px; }
.hp-kicker { font-size: 11px; font-weight: 800; letter-spacing: .8px; text-transform: uppercase; color: var(--gold); }
.hp-line { font-size: 13px; color: var(--text-2); line-height: 1.4; }
.hp-score {
  display: flex; align-items: stretch;
  padding: 8px 4px; border-radius: 12px; background: rgba(0,0,0,.22);
}
.hp-score:active { background: rgba(0,0,0,.34); }
.hp-score .hs { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; }
.hp-score b { font-size: 20px; font-weight: 800; color: var(--gold); font-variant-numeric: tabular-nums; }
.hp-score i { font-style: normal; font-size: 10.5px; font-weight: 700; letter-spacing: .4px; text-transform: uppercase; color: var(--text-2); }
.hp-score .hs-pad { border-left: 1px solid var(--stroke); font-size: 12.5px; font-weight: 700; color: var(--gold-soft); }
.home-play .action-btn.subtle { height: 36px; }

/* A group is one rounded list, its rows split by hairlines. The hairlines are
   the list's own background showing through a 1px grid gap, so the same rule
   draws them between stacked rows on a phone and between columns on a tablet. */
.home-group h2 {
  margin: 0 4px 8px;
  font-size: 12px; font-weight: 800; letter-spacing: .8px; text-transform: uppercase; color: var(--text-2);
}
.hg-list {
  display: grid; gap: 1px; overflow: hidden;
  background: var(--stroke); border: 1px solid var(--stroke); border-radius: var(--radius);
}
.hg-list .menu-row { border: none; border-radius: 0; padding: 12px 13px; }
.hg-list .menu-row:active { transform: none; background: var(--surface-raised); }
.hg-list .menu-icon { width: 38px; height: 38px; font-size: 18px; }
.home-foot { display: flex; flex-direction: column; align-items: center; gap: 2px; margin-top: auto; }

.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-row:disabled { opacity: .45; }
.menu-row:disabled:active { transform: none; }
.menu-row:disabled .menu-chevron { visibility: hidden; }
.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 controls that are real links rather than dispatched actions: 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; }
a.icon-btn { text-decoration: none; }
a.action-btn {
  display: inline-flex; align-items: center; justify-content: center;
  text-decoration: none;
}

/* Quiet: 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 {
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 12px; color: var(--text-2); opacity: .75; text-decoration: none;
  margin-top: 8px; padding: 6px 16px; border-radius: 20px;
  border: 1px solid var(--stroke);
}
.home-site-link:active { background: rgba(255,255,255,.06); }
/* A pointer hits a 28px pill; a thumb does not. The link stays the same size
   to READ either way — only the box it sits in grows, which is the difference
   between a quiet link and one nobody can reliably tap. */
@media (pointer: coarse) {
  .home-site-link { min-height: 44px; padding: 6px 20px; }
}
.home-version { font-size: 11px; color: var(--text-2); opacity: .5; padding-bottom: 8px; margin-top: 4px; letter-spacing: .3px; }

/* ---------------------------------------------------------------- guide

   The first-run tours (js/guide.js). One fixed layer over the whole app: the
   spotlight is a transparent box whose enormous shadow is the dimming, so the
   target shows through untouched, and the card is placed beside it from JS. */

.guide { position: fixed; inset: 0; z-index: 90; }
.guide.no-spot { background: rgba(3,10,7,.74); }
.guide-spot {
  position: fixed; border-radius: 14px; pointer-events: none;
  box-shadow: 0 0 0 200vmax rgba(3,10,7,.74);
  outline: 2px solid var(--gold); outline-offset: 2px;
  animation: guidePulse 1.8s ease-in-out infinite;
}
@keyframes guidePulse { 50% { outline-color: rgba(198,163,83,.3); } }
.guide-card {
  position: fixed; left: 16px; top: 16px;
  width: min(340px, calc(100vw - 24px));
  padding: 15px 16px 12px;
  background: var(--bg); border: 1px solid rgba(198,163,83,.55); border-radius: 18px;
  box-shadow: 0 16px 50px rgba(0,0,0,.6);
  animation: guideIn .2s ease;
}
.guide-card.centered {
  left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(380px, calc(100vw - 40px));
  text-align: center; padding: 22px 20px 14px;
}
@keyframes guideIn { from { opacity: 0; } }
.guide-brand { display: flex; justify-content: center; margin-bottom: 14px; }
.guide-kicker { font-size: 11px; font-weight: 800; letter-spacing: .7px; text-transform: uppercase; color: var(--gold); margin-bottom: 5px; }
.guide-card h3 { font-size: 18px; font-weight: 800; color: var(--text); margin-bottom: 6px; }
.guide-card.intro h3 { font-size: 22px; }
.guide-card p { font-size: 14.5px; line-height: 1.5; color: rgba(242,239,230,.88); }
.guide-actions { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
.guide-card.intro .guide-actions { flex-direction: column; align-items: stretch; gap: 2px; margin-top: 18px; }
.g-btn {
  min-height: 44px; padding: 0 18px; border-radius: 12px;
  background: var(--surface-raised); border: 1px solid var(--stroke);
  font-size: 14.5px; font-weight: 800; color: var(--text);
}
.g-btn.primary { background: var(--gold); border-color: var(--gold); color: var(--ink-on-gold); }
.guide-card.intro .g-btn.primary { margin-bottom: 6px; }
.g-link { min-height: 44px; padding: 0 4px; font-size: 13.5px; font-weight: 600; color: var(--text-2); }
.guide-actions .g-link:first-child { margin-right: auto; }
.g-link.quiet { font-size: 12.5px; opacity: .75; }
body.text-large .guide-card h3 { font-size: 20px; }
body.text-large .guide-card p { font-size: 16.5px; }
/* A sheet that slides up would be measured mid-slide and lit 60px too low. */
body.guide-on .sheet { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .guide-spot, .guide-card { animation: none; }
}

.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 */

/* The two card sizes are REGISTERED, and it matters.

   An ordinary custom property is a token stream, not a value: nothing inside it
   is resolved until it is finally substituted into a real property, and it is
   resolved against whatever element does the substituting. Both sizes are
   written against the table's own box — 100cqh, 100cqw of .table-area — and the
   fan and the dummy tray, which read them, are direct children of it, so that
   worked. The trick is not: .table-center sits inside .table-middle, which is a
   size container in its own right, so the same tokens re-resolved against the
   MIDDLE BAND. The cross asked for a card the width of a third of the band it
   was standing in, got 34px, and drew a 39px trick card beside a 97px hand.

   Registering them with a <length> syntax makes them compute to a length at the
   element that declares them, which is the element whose container the numbers
   were written about. Everything downstream then inherits a plain px value that
   means the same thing wherever it is read. (A browser too old for @property
   ignores this block, keeps the old token behaviour, and gets the small cross
   it always had — a degradation, not a break.) */
@property --card-player {
  syntax: "<length>"; inherits: true; initial-value: 70px;
}
@property --card-dummy {
  syntax: "<length>"; inherits: true; initial-value: 70px;
}

.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;
  /* The bottom inset is the home indicator's. The fan is the lowest thing on
     the screen and it is a control, not decoration: without this the bottom of
     every card sits under the bar, and the >=700px twin of this rule has
     always said so. The height it costs is height the layout never had —
     .game-screen.is-bidding already sizes its rows against a --usable-h with
     the same inset subtracted. */
  padding: 6px 8px calc(2px + env(safe-area-inset-bottom)); 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;
}
/* The concealed hands, down the sides of the auction and across the top. The
   overlap is set per band (--auc-lap) so the column fills the height the table
   has, rather than overflowing a short screen or stopping halfway up a tall
   one; overflow:hidden is the guard for the bands in between. */
.seat-zone.in-auction {
  overflow: hidden; justify-content: flex-start;
  /* Thirteen backs are a fixed height and the band they stand in is not, so on
     some windows the column is longer than the room for it. Fading the last
     inch reads as a hand continuing past the edge; cutting it reads as a card
     drawn wrong. The mask costs nothing when nothing overflows. */
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 26px), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 26px), transparent);
}
/* One letter and the turn dot: all that fits, and all that is needed beside a
   record whose columns are headed with the same letters. */
.seat-badge.sb-letter {
  font-size: 12px; font-weight: 800; padding: 4px 8px; gap: 4px;
  white-space: nowrap;
}
.seat-zone.in-auction .hidden-hand.vertical .card-back { margin-top: var(--auc-lap); }
.seat-zone.in-auction .hidden-hand.vertical .card-back:first-child { margin-top: 0; }

.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;
}
/* There is no centre pip. The index already prints rank AND suit, so a second
   suit symbol on the face is the same information twice, and at these sizes it
   does not read as a playing-card pip — it reads as a stray duplicate sitting
   under the index. One suit per card. */
.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.playable:active { transform: translateY(-10px); }
.user-hand .pcard.hint-glow { transform: translateY(-8px); z-index: 2; }
/* The hole a played card leaves. It keeps the box — width, height and the
   negative margin that overlaps it — and gives up everything else, so the
   cards around it stay exactly where they were dealt. `visibility: hidden`
   rather than `opacity: 0`: it takes the card out of hit-testing and out of
   the accessibility tree while leaving the layout untouched. */
.user-hand .pcard.spent { visibility: hidden; }

/* 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); }
/* 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;
}

/* ---------------------------------------------------------------- the auction

   Three parts in three places: what the call MEANS, beside North's cards at the
   top of the table; the RECORD, in the middle where the trick will stand; the
   CALLS, across the bottom where the thumb is. See biddingInfo(),
   centerContent() and biddingPanel() in ui.js.

   The panel's height is a sum rather than a guess — four rows and the gaps
   between them. Naming it --bp-h is what lets the table above reserve exactly
   that band, so the record can fill everything down to the top of the buttons
   without either one needing to know where the other ends. */

.game-screen.is-bidding {
  --bp-lv: 50px;      /* level row             */
  --bp-st: 58px;      /* strain row            */
  --bp-sp: 50px;      /* Pass / Dbl / Rdbl / Suggest */
  --bp-pl: 54px;      /* Place                 */
  --bp-gap: 7px;
  --bp-pad: 8px;
  --bp-h: calc(var(--bp-lv) + var(--bp-st) + var(--bp-sp) + var(--bp-pl)
             + 3 * var(--bp-gap) + 2 * var(--bp-pad));
  --auc-lap: -13px;   /* how far apart the concealed hands lie */
}

/* --- the top band: North's hand, and what your call means beside it -------- */

.table-top { display: flex; align-items: stretch; gap: 8px; }
.table-top > .seat-zone { flex: none; }

.bidding-info {
  flex: 1; min-width: 0;
  background: rgba(22,53,39,.72); border: 1px solid rgba(44,74,59,.55);
  border-radius: 12px; padding: 8px 10px;
  display: flex; flex-direction: column; gap: 5px;
}
.bi-head { display: flex; align-items: center; gap: 7px; }
.bi-call {
  flex: none; font-size: 19px; font-weight: 800; color: var(--gold);
  white-space: nowrap;
}
.bi-tag {
  min-width: 0; flex: 0 1 auto;
  font-size: 10px; font-weight: 800; color: var(--gold);
  background: rgba(198,163,83,.18); border-radius: 20px; padding: 3px 7px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bi-tag.sug { background: rgba(198,163,83,.42); color: var(--gold-soft); }
.bi-hcp {
  margin-left: auto; flex: none;
  font-size: 13px; font-weight: 800; color: var(--ink-on-gold);
  background: var(--gold-soft); border-radius: 20px; padding: 3px 9px;
  white-space: nowrap;
}
/* The meaning itself. Three lines in a column this wide is a whole sentence;
   past that it scrolls, rather than growing the band the table is built
   around.

   The scrolling needs the cap to exist. `overflow-y: auto` on its own never
   fired: .table-top is `flex: none` and the card's height is its content, so
   the box grew to whatever the sentence needed and scrollHeight never exceeded
   clientHeight. A 3-HCP preemptive raise over a takeout double runs to twelve
   lines on a phone, which took the band from 85px to 342px and pushed the top
   of the table down through the bidding panel. --bi-lines is the cap, in ems
   so that each band's own font-size carries it. */
.bi-text {
  flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  --bi-lines: 3;
  max-height: calc(1.32em * var(--bi-lines));
  font-size: 15px; line-height: 1.32; color: var(--text-2);
}
.bidding-info.thinking .bi-call { color: var(--text-2); }
.bidding-info.thinking .bi-text { display: flex; align-items: center; gap: 8px; }

/* --- the record ----------------------------------------------------------- */

.auction-grid-wrap {
  width: 100%; flex: 1 1 auto; min-height: 0; overflow-y: auto;
  /* Opaque, because the column headings inside it are sticky and the rows have
     to pass behind them cleanly. It also gives the record an edge on the felt,
     which the 55% it used to be did not on a big screen. */
  background: var(--surface-raised); border-radius: 10px; padding: 9px 10px;
}
.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: 5px 6px; }
.ag-rnd-head { }
.ag-rnd {
  font-size: 12px; font-weight: 700; color: rgba(157,178,166,.45);
  display: flex; align-items: center; justify-content: center;
}
/* The headings stay put while the rounds scroll under them. A long auction
   scrolls this box — and it is scrolled to the newest call, so without this the
   one row that says which column is which seat is the first thing to leave. */
.ag-head, .ag-rnd-head {
  position: sticky; top: -9px;                 /* cancels the wrap's padding */
  background: var(--surface-raised); z-index: 1;
  /* An auction longer than the box is scrolled to its newest call, so the round
     at the top is usually a part-round — and a call cut through the middle of
     its glyphs reads as a rendering fault rather than as a record continuing
     above. The shadow is the surface's own colour, so what passes under the
     headings fades out instead, the same answer the concealed hands give when
     their column outgrows its band. */
  box-shadow: 0 9px 9px -5px var(--surface-raised);
}
.ag-head {
  font-size: 15px; font-weight: 700; color: var(--text-2);
  text-align: center; letter-spacing: .4px; padding: 0 0 5px;
  border-bottom: 1px solid rgba(157,178,166,.25);
}
.ag-head.dealer { color: var(--gold); }
/* Calls are set at the size of the rank on a card, because they are the same
   class of thing: the one item in the box you read a value off. */
.ag-cell {
  font-size: 22px; font-weight: 600; text-align: center; color: var(--gold-soft);
  min-height: 34px; line-height: 34px; 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: 13px; color: var(--text-2); line-height: 1.45; text-align: left;
  cursor: pointer;                                   /* tap anywhere to dismiss */
}
.ag-explain b { color: var(--gold-soft); }
.ag-close { margin-left: auto; padding-left: 8px; color: var(--text-2); font-size: 12px; flex-shrink: 0; }

/* --- the calls ------------------------------------------------------------ */

.bidding-panel {
  /* phone: floats over the band the table has reserved for it between the
     record and the frozen hand fan, so it covers nothing */
  position: absolute; left: 12px; right: 12px;
  /* The hand below is 1.45 card-heights plus 33px of badge, gap and padding,
     and the table sets an 8px gap between its bands — so this offset puts the
     panel's foot exactly where the table's own layout puts the top of the
     hand, and its head exactly 10px under the record. The two agree by
     construction rather than by a pair of guesses that matched on one screen
     size and drifted on every other.
     The safe-area term is the same one .table-area pads its foot with. An
     absolutely positioned box is offset from its ancestor's PADDING box, so
     that padding moves the hand in flow and leaves this panel where it was:
     without repeating the inset here the panel's foot lands on the seat badge
     and the top of the fan, which is the one thing this offset exists to
     prevent. */
  bottom: calc(var(--uw-bid) * 1.45 + 41px + env(safe-area-inset-bottom));
  height: var(--bp-h);
  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: var(--bp-pad);
  display: flex; flex-direction: column; gap: var(--bp-gap);
  overflow: hidden;
  /* 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. */
}
/* Nothing to tap while a robot is thinking, and the top band is already saying
   so — the band goes quiet rather than holding an empty box. */
.bidding-panel.waiting { background: none; border-color: transparent; box-shadow: none; }
.bp-idle { flex: 1; display: flex; align-items: center; justify-content: center; }
/* The meaning of the call, drawn a second time at the top of the panel. Only the
   landscape rails show it — see "The meaning of the call, in the landscape
   rails" below; everywhere else the card on the table carries the sentence. */
.bp-explain { display: none; }

.level-row { display: flex; gap: 6px; height: var(--bp-lv); }
.level-chip {
  flex: 1; height: 100%; border-radius: 10px;
  background: var(--surface-raised); color: var(--text);
  font-size: 22px; 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; height: var(--bp-st); }
/* Set at the size of the pip on a card, for the reason the record is set at the
   size of the rank. */
.strain-btn {
  flex: 1; height: 100%; border-radius: 10px;
  background: var(--ivory); font-size: 32px; font-weight: 700;
  line-height: 1; display: flex; align-items: center; justify-content: center;
}
.strain-btn.nt { font-size: 22px; 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; height: var(--bp-sp); }
.special-btn {
  flex: 1 1 0; min-width: 0; height: 100%; border-radius: 10px;
  color: #fff; font-size: 17px; 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: 100%; 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: 16px; font-weight: 700; }

/* Rendered whether or not a call is chosen: a button that appeared would push
   the three rows above it down between the tap that picks a strain and the tap
   that confirms it, and would change a panel height the table has reserved. */
.place-btn {
  width: 100%; height: var(--bp-pl); border-radius: 11px;
  background: var(--gold); color: var(--ink-on-gold);
  font-size: 18px; font-weight: 800;
}
.place-btn:disabled { background: rgba(198,163,83,.18); color: rgba(230,213,168,.5); }
/* Undo shares the Place row: it stretches to Place's height in every band. */
.place-row { display: flex; gap: 6px; }
.place-row .place-btn { flex: 1; width: auto; min-width: 0; }
.place-row .undo-bid-btn { flex: none; min-height: 0; align-self: stretch; }

.undo-bid-btn {
  min-height: 44px; padding: 0 16px; border-radius: 10px;
  color: #E8A9A4; background: rgba(176,58,52,.35);
  border: 1px solid rgba(176,58,52,.7); font-size: 15px; font-weight: 800;
}
.undo-bid-btn:active { background: rgba(176,58,52,.5); }

.spinner {
  width: 15px; height: 15px; border-radius: 50%; flex: none;
  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; }
/* A charge that only moved overtricks: real, measured, and not worth red. */
.rp-loss.safe { color: var(--text-2); font-weight: 500; }
/* Partner's lead, explained. Neither a grade nor a charge — nothing at this
   seat is scored — so it takes neither the red nor the green, and sits a shade
   quieter than the notes it shares a line with. */
.rp-partner { font-size: 11.5px; color: var(--text-2); width: 100%; padding-left: 28px; line-height: 1.4; opacity: .92; }

/* 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;
}
/* Vulnerable, as a hand record prints it: red, and never gold — the gold marks
   are the dealer and the player's own seat, and a third gold badge would blur all three. */
.hb-vul {
  display: inline-block; min-width: 13px; text-align: center;
  font-size: 9px; font-weight: 800; line-height: 13px;
  color: var(--ivory); background: var(--negative); border-radius: 4px;
  vertical-align: 1px; margin-left: 3px;
}
.dd-row { display: flex; justify-content: center; }
.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; }

/* Pinch to zoom on the deal (deal-zoom.js). Unzoomed, the frame is an ordinary
   box and a finger on it still scrolls whatever is underneath. Zoomed, it takes
   a height cap from the script, scrolls, and owns the touch: the script pans it
   itself, so a native scroll can never fight a pinch for the position. */
.dd-zoom { position: relative; }
.dd-zoom-frame { touch-action: pan-x pan-y; }
.dd-zoom.is-zoomed .dd-zoom-frame { overflow: auto; overscroll-behavior: contain; touch-action: none; }
/* As wide as the hands need rather than as wide as the frame, so a long suit
   runs on into the scroll instead of breaking onto a second line; never
   narrower, so West and East still sit at the edges until they meet. */
.dd-zoom.is-zoomed .deal-diagram { width: max-content; min-width: 100%; }
.dd-zoom-reset {
  display: none; position: absolute; top: 0; right: 0;
  min-height: 26px; padding: 4px 12px; border-radius: 20px;
  background: var(--surface-raised); border: 1px solid rgba(198,163,83,.55);
  color: var(--gold-soft); font: inherit; font-size: 11px; font-weight: 700;
  box-shadow: 0 2px 8px rgba(0,0,0,.4); cursor: pointer;
}
.dd-zoom.is-zoomed .dd-zoom-reset { display: block; }

/* 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, and ONE card size for every face-up card on the table.
     The dummy used to be sized at 84% of the hand and the trick at whatever the
     middle band had left (~80%), so the same card was drawn at three sizes
     depending on which of the three bands it happened to be in — a played card
     visibly shrank on its way from the fan to the cross and grew again when the
     dummy followed it. The three bands now share --card-player, and the budget
     further down is what keeps the sum inside the table.

     The side (E/W) dummy is the exception and keeps its own token: that zone is
     a narrow strip whose width, not the table's height, decides what fits. */
  .game-screen {
    --card-player: min(clamp(70px, 15vmin, 130px), 15vh);
    --card-dummy: var(--card-player);
  }

  /* --- chrome ------------------------------------------------------------ */
  .top-bar {
    padding: max(14px, env(safe-area-inset-top)) 20px 10px; gap: 12px;
    padding-left: calc(20px + var(--win-ctl-left));
    padding-right: calc(20px + var(--win-ctl-right));
  }
  .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; }

  /* The table's chrome, wound in as far as it will go.

     Every band on this screen is sized from what the other two leave behind, so
     padding here is not decoration — it is card size. The reading screens keep
     the generous 14/20/10 above, because there the margin IS the design; the
     game screen spends it on the cards, which is the only thing on it anyone is
     looking at. Between the deal title, the status strip and the table's own
     inset this hands about 40px back to the middle of the table, and pulls the
     four seats out to the edges the felt actually has. */
  .game-screen > .top-bar {
    padding: max(6px, env(safe-area-inset-top)) 14px 4px;
    padding-left: calc(14px + var(--win-ctl-left));
    padding-right: calc(14px + var(--win-ctl-right));
  }
  .game-screen > .status-bar { margin: 0 10px; padding: 7px 14px; width: auto; }

  /* --- home -------------------------------------------------------------- */
  .home-screen { padding: max(24px, env(safe-area-inset-top)) 40px 24px; gap: 28px; }
  .home-head, .home-body { width: min(760px, 100%); }
  /* The lockup is SVG, so it scales by height and keeps its own proportions. */
  .home-brand { gap: 16px; }
  .home-brand .rase-tile { width: 64px; height: auto; }
  .home-brand .rase-word { height: 32px; width: auto; }
  .tagline { font-size: 16px; }
  .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 table card spans the width; the two short groups sit side by side
     under it, and Set up & learn spans again. Its three rows stay stacked:
     as three columns of a 760px column they wrap every title and caption. */
  .home-body { display: grid; grid-template-columns: 1fr 1fr; gap: 26px 18px; align-items: start; }
  .home-play, .home-group.hg-wide { grid-column: 1 / -1; }
  .home-play { padding: 18px; gap: 14px; }
  .hp-kicker { font-size: 12.5px; }
  .hp-line { font-size: 15px; }
  .hp-score { padding: 11px 6px; }
  .hp-score b { font-size: 26px; }
  .hp-score i { font-size: 12px; }
  .hp-score .hs-pad { font-size: 14px; }
  .home-play .btn-primary { padding: 19px 24px; }
  .home-group h2 { font-size: 13px; }
  .btn-primary .bt { font-size: 20px; }
  .btn-primary .bs { font-size: 14px; }
  .menu-row { padding: 16px; gap: 15px; }
  /* height:100% belongs to the home grid and only to it: there the groups sit
     in two columns and a row should match its neighbour rather than its own
     text. Unscoped, it reached every other .menu-row too — and the ones on the
     Diagrams screen are direct children of .scroll-body, which is as tall as
     the screen, so each row grew to fill it: three rows of 642px at 1024x768
     and 986px on an iPad, each holding one line of text in the middle of an
     empty card. Invisible below 700px, where this block does not apply, which
     is why every phone check missed it. */
  .home-body .menu-row { height: 100%; }
  .menu-icon { width: 48px; height: 48px; font-size: 22px; }
  .hg-list .menu-row { padding: 15px 16px; }
  .hg-list .menu-icon { width: 44px; height: 44px; font-size: 20px; }
  .menu-row .mt { font-size: 17px; }
  .menu-row .ms { font-size: 13px; }
  .home-site-link { font-size: 13px; }
  .guide-card { width: min(380px, calc(100vw - 24px)); }
  .guide-card h3 { font-size: 20px; }
  .guide-card p { font-size: 16px; }

  /* --- 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; }
  /* Side inset of 8px, not 20: the dummy tray and the two opponents sit at the
     ends of this box, so its padding is the gap between them and the cross. */
  .table-area { padding: 4px 8px calc(2px + env(safe-area-inset-bottom)); gap: 2px; }
  /* 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: 10px; }
  /* 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. */
  /* The middle now holds the record between two hands, not two badges over an
     empty felt, so it takes the room rather than giving it up. */
  .game-screen.is-bidding .table-middle {
    flex: 1 1 auto; min-height: 0; padding-top: 0; align-items: stretch;
  }
  /* On a screen this tall the record does NOT fill the band — a 490px box
     ruled for one round of bidding is a box with nothing in it. It takes the
     height its rounds need, sits centred between the two hands, and scrolls
     once the auction is longer than the band. The phone does the opposite,
     because there the band is barely bigger than the box. */
  .game-screen.is-bidding .auction-grid-wrap { flex: 0 1 auto; max-height: 100%; }
  .game-screen.is-bidding .table-area { justify-content: flex-start; }
  .game-screen.is-bidding .seat-zone.seat-W,
  .game-screen.is-bidding .seat-zone.seat-E { width: 70px; }
  /* 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: 6px; }

  .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.

       The 58px ceiling that used to sit in this min() was the last place on the
       table where a card was drawn at a size of its own: the hand and the trick
       are both --card-player now, and a dummy at 58px beside them is the same
       mismatch this whole pass is about, just moved to the side of the table.
       The ceiling is therefore --card-player, and the two terms under it are
       the two things that can genuinely stop it — the band's height, by the 5.8
       card-widths four fans stand, and the table's width, which has to seat two
       of these columns (3.6 card-widths each) either side of a cross 3.5 wide.
       On a laptop none of the three binds and the dummy matches the hand; on a
       tablet the width does, and it lands between 58 and the full size. */
    --dw-side: max(52px, min(var(--card-player),
                             calc((100cqh - 12px) / 5.8),
                             calc((100cqw - var(--card-player) * 3.5) / 7.2)));
    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 — that is what
     --tw-room measures, so no separate cap is needed here any more */
  .game-screen.dummy-at-W .table-center,
  .game-screen.dummy-at-E .table-center {
    --tw: min(calc(var(--card-player) / 0.286),
              var(--tw-fit, 9999px), var(--tw-room, 9999px));
  }
  .seat-zone { gap: 4px; }

  .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);
  }

  /* 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); }

  /* The cross is now sized BY the card rather than the card by the cross: a
     trick card is exactly --card-player wide (0.286 of the box is one card), so
     the card you play is the same size in the middle of the table as it was in
     your hand a moment earlier. --tw-fit and --tw-room stay as the two guards —
     the band's height and the width the side seats leave — so a window that
     cannot hold the full-size cross shrinks the cross rather than overflowing
     it. The budget block below is what stops that happening in the first
     place. */
  .table-center {
    --tw: min(calc(var(--card-player) / 0.286),
              var(--tw-fit, 9999px), var(--tw-room, 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 auction fan spreads to the width of the table.

     How BIG a card may be is a question about height — it stands above the
     bidding panel and both have to fit — so the size terms stay as they were,
     bar the last: 10cqw used to hold the fan to about 60% of the table's width,
     which was the right answer when a rail took the other 40% and the wrong one
     now the panel is a band underneath. It becomes a plain overflow guard (13
     overlapped cards are 6.04 card-widths).

     What fills the table is the OVERLAP, not the size. --strip is the width of
     one card's exposed edge, solved for directly: (room - one whole card) / 12
     is what each of the other twelve may take. It is clamped at both ends —
     never tighter than the 0.42 the phone's fan exposes, and never looser than
     0.62 of a card, past which thirteen cards stop reading as one hand. Where
     the sum cannot be computed at all (a short landscape window, which is not a
     container) --fan-room is absent, the middle term collapses, and the clamp
     returns the phone's fan unchanged.

     Everything else is then measured in --strip rather than guessed: the
     overlap is strip-minus-a-card.

     The last size term is the one that stops the fan running off the bottom of the
     table and onto the bidding panel below it. Every other term is about the
     window; this one is about the box, and it is the box that runs out first on
     a short landscape window, where the panel takes the lower half.
     --fan-chrome is what the table spends before the fan, measured: two seat
     badges, the middle band at its 40px minimum, the two gaps between them, the
     zone gap and the fan's own padding. It is a token because a badge sized for
     a thumb is 34px and one sized for a mouse is 25, and two of them either way
     is the difference between a fan that clears the panel and one that laps
     over its top corner. The
     52px floor under it is the legibility floor: below that the term has
     stopped solving anything and is only making the cards unreadable, and a fan
     that overlaps the panel by a few pixels is the lesser of those two. */
  .user-hand:not(.play2) {
    --uw: min(clamp(72px, 14vmin, 124px), 15vh, calc(100cqw / 6.04),
              max(52px, calc((100cqh - var(--fan-chrome, 148px)) / 1.45)));
    --strip: clamp(calc(var(--uw) * 0.42),
                   calc((var(--fan-room, 0px) - var(--uw)) / 12),
                   calc(var(--uw) * 0.62));
    margin: 0; padding: 6px 0 8px;
  }
  .user-hand:not(.play2) .pcard { margin-left: calc(var(--strip) - var(--uw)); }
  .user-hand:not(.play2) .pcard:first-child { margin-left: 0; }
  .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);
  }
  .bi-call { font-size: 22px; }
  .bi-tag { font-size: 12px; padding: 4px 9px; }
  .bi-hcp { font-size: 15px; padding: 4px 12px; }
  .bi-text { font-size: 16.5px; }
  .auction-grid-wrap { padding: 9px 13px; }
  .ag-head { font-size: 16px; padding-bottom: 5px; }
  .ag-cell { font-size: 24px; min-height: 38px; line-height: 38px; }
  .ag-rnd { font-size: 12px; }
  .ag-explain { font-size: 14.5px; padding: 11px; }
  .bidding-panel { --bp-lv: 52px; --bp-st: 60px; --bp-sp: 56px; --bp-pl: 56px; --bp-gap: 8px; }
  .level-row, .strain-row, .special-row { gap: 6px; }
  .level-chip { font-size: 21px; }
  .strain-btn { font-size: 30px; }
  .strain-btn.nt { font-size: 21px; }
  .special-btn { font-size: 18px; }
  /* 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; font-size: 20px; padding: 0 14px; }
  .place-btn { font-size: 20px; }

  /* --- analysis ---------------------------------------------------------- */

  /* The deal panel was drawn to fit beside a hand of cards on a phone, and it
     kept those sizes onto screens with four times the room: a 340px table of
     10-14px numbers and an 11px note, adrift in a 380px rail on a laptop. The
     numbers ARE the analysis — the whole panel exists to be read against the
     deal — so they take the room that is going spare rather than leaving it
     empty. Everything below moves together and keeps its proportions; nothing
     here changes the phone, which does not have the room and never did. */
  .deal-pin { padding: 12px 24px; }
  .deal-pin summary { font-size: 18.5px; }
  .deal-pin .deal-meta { font-size: 13px; }
  .deal-pin .deal-context { font-size: 13px; }
  .deal-pin .hand-line { font-size: 15.5px; }
  .deal-pin .hand-block .hb-title { font-size: 13.5px; }
  .deal-pin .ddo-title { font-size: 13.5px; }
  /* The cap is what keeps the columns close enough to scan across; it moves up
     with the type rather than holding it at a phone's measure. */
  .deal-pin .ddt-row { max-width: 420px; }
  .deal-pin .ddt-cell { font-size: 17px; }
  .deal-pin .ddt-row.head .ddt-cell { font-size: 15px; }
  .deal-pin .ddt-seat { font-size: 15px; width: 44px; }
  .deal-pin .ddt-hcp-cell { font-size: 15px; width: 40px; }
  .deal-pin .ddt-row.head .ddt-hcp-cell { font-size: 11.5px; }
  .deal-pin .ddt-key-item { font-size: 13px; }
  .deal-pin .ddt-key-item i { width: 17px; height: 17px; }
  .deal-pin .ddo-note { font-size: 13.5px; line-height: 1.55; }
  .dd-zoom-reset { font-size: 13px; min-height: 30px; }
  /* The body moves with the rail. Sizing one of the two columns up and leaving
     the other where it was would only move the mismatch: the reviewed auction
     is the thing the trick table is being read AGAINST, and a 13.5px row
     beside a 17px cell reads as two screens stitched together. Scoped to the
     review, because these headings and captions are shared with settings and
     the library, which are prose at a comfortable measure already. */
  .ra-row { grid-template-columns: 78px 68px 68px 1fr; font-size: 15px; padding: 10px 8px; }
  .ra-row .exp { font-size: 13.5px; }
  .rp-trick { font-size: 15.5px; padding: 11px 14px; }
  .rp-note, .rp-reason, .rp-partner { font-size: 13.5px; }
  .review-screen .section-card h3 { font-size: 18.5px; }
  .review-screen .section-caption { font-size: 14px; }
  .review-screen .improve-lead { font-size: 15px; }
  .review-screen .sc-detail { font-size: 13.5px; }
  .wide-btn { height: 52px; font-size: 16.5px; }
}

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

@media (min-width: 920px) and (orientation: landscape) {
  /* The auction used to dock into a right-hand RAIL here, on the reasoning that
     a rail leaves the fan uncovered. It does — and it also takes a third of the
     window away from the table for the whole of the auction, so the thirteen
     cards the panel exists to bid on were squeezed into the left half of a
     1280px screen and drawn at 96px while the felt around them stood empty.

     The panel belongs UNDER the cards, which is where the tablet band already
     puts it: a band across the foot of the screen, the hand spread the full
     width above it. Nothing is covered either way, and the fan gets the whole
     table instead of what a rail leaves. So this band inherits the stacked
     grid, the docked panel and the panel's own sizing from the 700px block
     above, and only states what is genuinely different about a wide landscape
     window — the dummy's shape, the joined play fan, and the analysis rail. */

  /* 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 holds card backs and stays auto. */
  /* A side dummy is four VERTICAL columns side by side here, not four stacked
     fans, so it gets its own --dw-side rather than the stacked band's — and it
     is declared on the seat, which is where the stacked band declares it, or
     the value there would win by being closer to the cards. It used to be
     written as a fraction of --card-dummy, which was a fraction of the hand;
     now that the dummy TRAY is the same size as the hand that chain would make
     a side dummy full-size too, and four full-size columns are wider than the
     strip they stand in. This is the size the band has always resolved to.
     (The declaration is on the dummy-at-* seat rule further down, so that it
     outranks the stacked band's --dw-side on the very same element.) */
  .dummy-cols.side { --dw: var(--dw-side, 58px); 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: var(--dw-side);
    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(calc(var(--card-player) / 0.286),
              var(--tw-fit, 9999px), var(--tw-room, 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 {
    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 {
    --dw-side: clamp(52px, calc(var(--card-player) * 0.63), 72px);
    width: calc(var(--dw-side) * 4 + 24px); min-width: 226px;
  }
  .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(calc(var(--card-player) / 0.286),
              var(--tw-fit, 9999px), var(--tw-room, 9999px));
    gap: 6px; min-height: 0;
  }
  .trick-area { flex: 0 0 auto; }
  /* 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. */
  .game-screen { --fan-cap: calc((100cqw - 24px) / 8.8); }
  .user-hand.play2 { --uw: var(--card-player); 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;
    /* Wider than it was: the rail holds a four-hand diagram and a five-column
       table, and at 380px both were sized down to fit rather than the rail
       being sized up to hold them. A share rather than a flat number, so the
       920px window where this layout starts is not handed the same 460px as
       the 1180px one the frame caps at — there it would be nearly half the
       screen. The body keeps the rest, and is capped at an 860px measure
       anyway, so this spends window that was going to be margin. */
    grid-template-columns: minmax(340px, min(460px, 40%)) 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 again, applied to the table's own furniture.

     The seat badge, the WE/THEY counters and the utility pills are all sized as
     touch targets, and between them they are 137px of the table's height — the
     constant in the card budget, and therefore the difference between a 60px
     card and a 70px one on a laptop window. None of the three is a target at
     all: the badge and the counters are labels nobody taps, and the pills are
     hit to the pixel by a mouse. They step back to a size meant for reading,
     the budget's constant comes down with them, and the cards take the
     difference. A tablet keeps every one of these at 48px+. */
  .game-screen { --table-chrome: 118px; --counter-room: 40px; --fan-chrome: 130px; }
  .game-screen .seat-badge { font-size: 13px; padding: 5px 13px; gap: 6px; }
  body.text-large .game-screen .seat-badge { font-size: 15px; }
  .game-screen .badge-row .hint-pill { font-size: 13px; padding: 6px 13px; }
  .game-screen .tc-pill { padding: 4px 13px; gap: 6px; }
  .game-screen .tc-pill .l { font-size: 12px; }
  .game-screen .tc-pill .v { font-size: 18px; }
  body.text-large .game-screen .tc-pill .l { font-size: 13px; }

  /* 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 — and cp is now ALL THREE cards, the dummy's and the trick's
   as well as the hand's, so the equation has to buy a full-size cross rather
   than whatever was left over:

     hand   1.45cp + 53px      (one row of cards, the pill row, padding)
     tray   1.45cp + 38px      (dummy fans at full size, badge, gap)
     cross  3.07cp + 50px      (a cross 3.5cp wide is 0.878 of that tall,
                                plus the WE/THEY counters under it — that last
                                term is --counter-room, which --tw-fit uses too,
                                so the budget and the guard agree)
     -----------------------------------------------------------------
            5.97cp + 137px  ≤  the table's height

   so cp ≤ (100cqh - 150px) / 5.97, taking 150 rather than the bare 137 so the
   sum has a little slack in it: the term that would otherwise bite is --tw-fit,
   and --tw-fit biting means the cross alone shrinks — which is the one outcome
   this whole arrangement exists to prevent.

   The constants are MEASURED, not estimated: 53px is the pill row plus the
   fan's own padding, 38px the dummy's seat badge and the gap under it, 46px the
   WE/THEY counters. Anything that changes them changes the card size, which is
   why the chrome above was wound in rather than left alone. The per-band constants came down with the
   chrome (see the .game-screen padding rules in the 700px band): winding the
   inset in is worth about a card-width of size here, which is the whole reason
   for doing it. 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. */
  /* The last term is the WIDTH the dummy tray needs, and it belongs to the
     player's card because the two are now the same size. A thirteen-card hand
     in four horizontal fans measures 7.78 card-widths (four suit starts at full
     width, nine followers at the 0.42 the overlap exposes) plus three 12px
     gaps: on a 768pt tablet that is what binds, not the height, and without it
     the dummy's last suit simply runs off the side of the table.

     --fan-cap is the matching term for the layouts that join the hand into ONE
     row of thirteen: with three suit gaps that row measures 8.56 card-widths,
     and where the table is not that wide the row is what decides the size — so
     the trick and the dummy follow it down instead of standing a head taller
     than the hand they are meant to match. It is unset, and therefore infinite,
     wherever the hand keeps its two rows. */
  .table-area > * {
    --card-player: min(clamp(70px, 15vmin, 130px), 15vh,
                       calc((100cqh - var(--table-chrome, 150px)) / 5.97),
                       calc((100cqw - 44px) / 7.78), var(--fan-cap, 9999px));
    --card-dummy: var(--card-player);
  }
  /* A side dummy is bounded by the width of its strip, not by the table's
     height, so it keeps the proportion it always had rather than following
     --card-dummy up to full size.

     The card's own width term changes with it. The dummy-at-a-side table is
     three columns across — tray, cross, tray — and the trays cannot go under
     52px, which is 7.2 card-widths for the pair plus their gaps, or 384px. The
     cross is 3.5 card-widths, so what is left over divided by 3.5 is the widest
     card this arrangement can seat, and taking it here means the card you play
     is still the size it was in your hand rather than the size the leftovers
     allowed. The height terms are unchanged; on any table with the width, this
     one does not bind. */
  .game-screen.dummy-at-W .table-area > *,
  .game-screen.dummy-at-E .table-area > * {
    --card-player: min(clamp(70px, 15vmin, 130px), 15vh,
                       calc((100cqh - var(--table-chrome, 150px)) / 5.97),
                       var(--fan-cap, 9999px), calc((100cqw - 384px) / 3.5));
    --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 --counter-room). --tw-room is the same
     idea across: the cross may not be wider than the centre column it stands
     in. Every stacked layout's --tw carries both as final min() terms, so
     however the bands land, the trick stays inside its own band instead of
     printing over its neighbours. */
  /* --tw-room is the WIDTH the cross has, and the only box that knows it is the
     centre column itself — what the two side seats leave changes completely
     between a pair of card-back silhouettes and a loaded dummy tray, and a
     constant subtracted from the table was a guess at both. Making
     .table-center an inline-size container measures it instead: an element is
     never its own query container, so the 100cqw below resolves where --tw is
     finally used, on .trick-area, and that is .table-center. --tw-fit's 100cqh
     skips it (an inline-size container has no block axis) and still lands on
     .table-middle, which is the band it was written about. */
  .table-center {
    container-type: inline-size;
    --tw-fit: calc((100cqh - var(--counter-room, 50px)) / 0.878);
    --tw-room: calc(100cqw - 8px);
  }
  /* The width the auction fan has to spread across — see .user-hand above. */
  .table-area > * { --fan-room: calc(100cqw - 8px); }

  /* 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 docked panel, on a window that is short rather than small.

   Under the cards, the panel is a fixed cost the table pays out of its height.
   It used to carry the auction and the meaning of the call as well, and on a
   600px-tall window that came to 327 of 600 — leaving the table 158, less than
   the 220 a legible fan and four seat badges need, with the fan drawn straight
   over the panel's own header.

   Moving the record and the meaning out onto the table took most of that cost
   away: what is left is four rows of buttons, and here they come down a size
   each. The record gives nothing up, because it is now part of the table it
   was competing with. */
@media (min-width: 700px) and (max-height: 860px) {
  /* The panel is four rows of buttons now — there is no history inside it to
     shrink, so the rows themselves give up the difference, and the record on
     the table absorbs the rest through its own scroll. */
  .bidding-panel { --bp-lv: 46px; --bp-st: 52px; --bp-sp: 48px; --bp-pl: 48px; --bp-gap: 6px; --bp-pad: 9px; }
}

/* The panel's height, summed from the rows this band really draws.

   --bp-h is declared once, on .game-screen.is-bidding, and a custom property's
   var()s are substituted where it is DECLARED — so there it is summed from the
   base 50/58/50/54 and 7px gaps, whatever a descendant later says. The two
   bands above set the rows' sizes on .bidding-panel, which is a descendant: the
   rows grew, the panel's height (var(--bp-h), inherited already summed) did not.
   On an iPad Pro 13" in portrait the rows came to 52+60+56+56 inside a 249px
   panel, 13px short, and the panel's border cut through the Place button.
   The phone's blocks never had the problem — they declare their sizes on
   .game-screen.is-bidding, beside --bp-h, so the sum follows them.

   Nothing at this width reads --bp-h but the panel itself (the table reserves
   its band by it only under 700px), so it is restated here, on the panel, where
   the band's own sizes are. The +2px is the 1px border the phone's sum has
   never counted.

   Coarse pointers only, which is every device that has the problem. A mouse's
   panel is sized by the pointer block, whose Place row is 36px: its rows come
   to exactly 249 already, and a panel that was right is left alone. */
@media (min-width: 700px) and (pointer: coarse) {
  .bidding-panel {
    --bp-h: calc(var(--bp-lv) + var(--bp-st) + var(--bp-sp) + var(--bp-pl)
               + 3 * var(--bp-gap) + 2 * var(--bp-pad) + 2px);
  }
}

/* ...and the rail, kept for the windows the band cannot fit in.

   Under the cards the panel costs the table a fixed ~346px — three rows of
   controls, the auction, and the meaning of the call being composed, none of
   which can be given up — and the table itself needs about 220 for four seat
   badges and a legible fan. With the top bar and the status strip that is a
   780px window before anything is comfortable. Above it the band is plainly
   better: the fan gets the whole width, which is the point of the change. Below
   it the band leaves the table 139px, and the honest answer is the layout this
   band shipped with — the panel in a rail down the right-hand side, the table
   keeping its full height beside it.

   So the split is by HEIGHT, which is the thing that actually runs out, rather
   than by width, which is what the rail was originally keyed to. The second
   query is the same argument at its extreme: a landscape phone between 700 and
   920px wide never qualified for the rail, so it docked a 350px panel under a
   390px-tall window and the fan was drawn 115px past the bottom of the table
   and 100px off the screen. It is narrow for a rail; it is not as narrow as
   that.

   The same was true of everything between: the iPhone Duo opened flat is
   890x626, too tall for the short band and too narrow for the 920px one, so
   it got the docked panel under a ~600px table — the 139px case above. The
   height is what decides, so from 700px wide it is the only test. */
@media (min-width: 700px) and (orientation: landscape) and (max-height: 779px) {
  .game-screen {
    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);
    /* A grid ITEM here, not the phone's floating overlay: it kept the base
       rule's absolute positioning once, and `align-self: end` then did nothing
       while the panel was placed from the bottom of the screen by a
       phone-shaped offset — which put its lowest controls past the fold. */
    position: relative; top: auto; left: auto; right: auto; bottom: auto;
    align-self: end;
    /* The PANEL must not scroll — only its auction history may — or the
       Pass/Dbl/Rdbl row ends up below the fold and unreachable. */
    min-height: 0; max-height: 100%; overflow: hidden;
    margin: 0 16px calc(12px + env(safe-area-inset-bottom)) 0;
    box-shadow: 0 8px 26px rgba(0,0,0,.45);
    gap: 4px; padding: 7px;
  }
  /* The fan now shares its row with the rail, so --fan-room — which is measured
     from .table-area, and .table-area is the grid column beside the rail —
     already spreads it across exactly the width that is left. Only the SIZE
     term needs restating, against a landscape window's height. */
  .user-hand:not(.play2) {
    --uw: min(clamp(72px, 13vmin, 124px), 17vh,
              max(52px, calc((100cqh - var(--fan-chrome, 148px)) / 1.45)));
  }
}

/* The rail at its shortest, where it has to choose what to show.

   A 390px-tall landscape phone gives the rail 287px, and the panel's parts want
   315: a two-line header, the auction so far, the meaning of the call with its
   Place button, and three rows of controls. Something has to give, and the
   order is not negotiable — the controls are the decision, the Place button is
   how the decision is made, and the auction is a RECORD that is one tap away on
   the status strip anyway. So the system badge goes (the profile name beside it
   says the same thing), the meaning block stops flexing and takes the height it
   needs, and the auction grid absorbs the whole squeeze inside its own scroll.
   Before this the squeeze was shared, which cut the Place button in half.

   Under 700px there is no rail — the panel is still the phone's floating
   overlay, which scrolls — so this is scoped away from it rather than applied
   to a layout it was not measured against. */
@media (min-width: 700px) and (orientation: landscape) and (max-height: 520px) {
  /* A 393px-tall rail holds four rows of buttons and nothing else, so the calls
     go there and the table keeps the rest. But the table here is 134px all in,
     and the meaning of the call was taking 95 of it. So the card folds onto one
     line — the call, its meaning as far as it goes, the point count — which
     leaves the record enough for its headings and a round. The sentence goes
     down into the panel — see .bp-explain, below. */
  .bidding-panel { --bp-lv: 40px; --bp-st: 46px; --bp-sp: 42px; --bp-pl: 42px; --bp-gap: 5px; --bp-pad: 6px; }
  .bidding-info { flex-direction: row; align-items: center; gap: 8px; padding: 6px 9px; }
  .bidding-info .bi-head { flex: none; }
  .bidding-info .bi-hcp { margin-left: 0; }
  .bi-text {
    font-size: 13.5px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  }
  .game-screen.is-bidding .ag-cell { font-size: 19px; min-height: 26px; line-height: 26px; }
  .game-screen.is-bidding .ag-head { font-size: 13px; }
  /* Thirteen backs need 100px of column and there are 30 — a clipped sliver of
     card is not a hand, it is a rendering fault. The badges stay and say where
     the seats are. */
  .game-screen.is-bidding .seat-zone.in-auction .hidden-hand { display: none; }
}

/* 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) {
  .game-screen { --fan-cap: calc((100cqw - 24px) / 8.8); }
  .user-hand.play2 { --uw: var(--card-player); 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);
  }

  .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’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;
  }
}

/* The auction on a landscape phone: the rail, rebuilt without the tablet.

   Under 700px the calls are still the portrait phone's floating panel, placed
   from the bottom of the screen by the height of the hand. That offset was
   worked out for a column 700px tall. A 667x375 SE puts the panel's top edge
   36px ABOVE the screen, and its 249px covers the top bar, the status strip and
   the record, which leaves only the hand to see. The 678x466 outer screen of
   the Duo fails the same way.

   The rail block above is the answer, but it cannot simply start lower down.
   It relies on two things the 700px tablet block builds: the grid that names
   the rail's area, and a docked panel in place of the floating one. Without
   them .table-area collapses to 9px. So this block builds only those two here,
   at the phone's own type scale, and scopes both to the auction. Play keeps the
   flex column and the grid of its own that the block above gives it.

   The budget at 667x375, measured: 105px of top bar and status strip leaves the
   table 270. The rail and its margins take 308px, so the table column is 359
   wide. The panel's four rows take the short rail's numbers, 197px, which fits
   in the 270 with room to spare. On the table, the top band is 36, the hand is
   121 with its badge, and the gaps and padding are 24. That leaves the record
   90px: its headings and two whole rounds. At 678x440 the record gets 152px,
   which is three rounds. */
@media (max-width: 699px) and (orientation: landscape) {
  .game-screen.is-bidding {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: auto auto minmax(0, 1fr);
    grid-template-areas:
      "top    top"
      "status status"
      "table  rail";
    --bp-lv: 40px; --bp-st: 46px; --bp-sp: 42px; --bp-pl: 42px; --bp-gap: 5px; --bp-pad: 6px;
  }
  .game-screen.is-bidding > .top-bar { grid-area: top; }
  .game-screen.is-bidding > .status-bar { grid-area: status; }
  /* A width container, so the fan below can be sized to the column it is
     given rather than to the screen, which is 290px wider. */
  .game-screen.is-bidding > .table-area {
    grid-area: table; container-type: inline-size;
    gap: 8px; justify-content: flex-start;
  }
  /* 290px holds seven level chips at 34px each, and Pass, Dbl and Rdbl at 47px
     each beside a Suggest button that does not shrink. `end` keeps the calls
     level with the hand, which is where the thumb already is. The fan runs to
     the edge of its column, so the 8px on the left is the gap between its last
     card and the rail. Without it the two touch. */
  .game-screen.is-bidding > .bidding-panel {
    grid-area: rail; align-self: end;
    position: relative; top: auto; left: auto; right: auto; bottom: auto;
    width: 290px; max-height: 100%;
    margin: 0 10px calc(8px + env(safe-area-inset-bottom)) 8px;
  }

  /* Only the record may give. The top band and the hand keep their height, and
     a long auction scrolls inside the record, which has a scrollbar for it. */
  .game-screen.is-bidding .table-top,
  .game-screen.is-bidding .seat-zone.seat-S { flex: none; }
  .game-screen.is-bidding .table-middle {
    flex: 1 1 auto; min-height: 0; align-items: stretch; padding-top: 0;
  }
  .game-screen.is-bidding .table-center { justify-content: stretch; }

  /* The concealed hands go, as they do on the short rail above. North's backs
     are 164px of a 365px column, which leaves the meaning of the call two
     words. The side columns are 90px tall and thirteen backs need 162. What
     stays are the badges, and the record's column headings name the same
     seats. */
  .game-screen.is-bidding .seat-zone.in-auction .hidden-hand { display: none; }
  .game-screen.is-bidding .seat-zone.seat-W,
  .game-screen.is-bidding .seat-zone.seat-E { width: 40px; }

  /* The meaning goes on one line, as on the short rail: the call, as much of
     the sentence as fits, and the points. The two-line card cost 70px of the
     270, more than a round of the record. The sentence goes down into the panel —
     see .bp-explain, below. The system tag goes as it does in portrait, because
     180px cannot hold both it and the call and the points. */
  .game-screen.is-bidding .bidding-info {
    flex-direction: row; align-items: center; gap: 8px; padding: 6px 9px;
  }
  .game-screen.is-bidding .bidding-info .bi-head { flex: none; }
  .game-screen.is-bidding .bidding-info .bi-hcp { margin-left: 0; }
  .game-screen.is-bidding .bi-tag:not(.sug) { display: none; }
  .game-screen.is-bidding .bi-text {
    font-size: 13.5px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  }
  .game-screen.is-bidding .ag-cell { font-size: 19px; min-height: 26px; line-height: 26px; }
  .game-screen.is-bidding .ag-head { font-size: 13px; }

  /* The fan fills the table column, not the screen. --uw-bid is 84px on a 667px
     phone, and thirteen cards at that size are 507px wide, which is 148px more
     than the column has. The fan measures 6.04 card-widths and spans the column
     plus the 16px of padding it cancels, so both test sizes get a 59-61px card.
     17vh is the same height cap the short rail uses. It binds only on a window
     shorter than about 350px, where a 59px card would crowd out the record. */
  .game-screen.is-bidding .user-hand:not(.play2) {
    --uw: max(52px, min(calc((100cqw + 16px) / 6.04), 17vh, 84px));
  }
}

/* The meaning of the call, in the landscape rails.

   Both rails fold the card on the table to one line, and a line is not room for
   a sentence: the engine's reasoning for a suggestion read "Five-card …" in
   78px, and reading why is the one thing Suggest is for. Nothing on the table
   can give it more, so the sentence moves down into the panel, where the rail
   has the height — a 402px phone leaves about 90px under the controls. When
   there is less, it is the sentence that gives, scrolling; never a button.

   It goes at the TOP of the panel, above the rows. The rows hold the bottom,
   where the thumb is, so a longer sentence grows the panel upward and nothing
   under the thumb moves between the tap that picks a strain and the tap that
   places it. Its height is fixed at three lines for that reason, and for the
   one the card has always had: past three lines a sentence scrolls, it does
   not grow. The card keeps the call, the tag and the points, and loses the line
   it could not finish.

   The panel's height is its content here, not --bp-h: that is the band the
   table reserves for a panel docked under it, and a rail reserves nothing. Only
   while it waits on a robot does it keep that height, so the Undo button does
   not move.

   Not under 346px tall (a 568x320 SE of the first generation). The rows alone
   are 204 of it and the sentence would be left a sliver of one line — a clipped
   half-line at the top of the panel reads as a rendering fault, where the
   truncated card at least reads as a sentence that goes on. Those screens keep
   the card as it was. */
@media (min-width: 700px) and (orientation: landscape) and (min-height: 346px) and (max-height: 520px),
       (max-width: 699px) and (orientation: landscape) and (min-height: 346px) {
  .bidding-info .bi-meaning { display: none; }
  .bidding-panel:not(.waiting) { height: auto; }
  .bidding-panel:not(.waiting) > :not(.bp-explain) { flex: none; }
  .bp-explain {
    display: block; flex: 0 1 auto; min-height: 0;
    height: calc(1.32em * 3 + 4px); overflow-y: auto; overscroll-behavior: contain;
    font-size: 13.5px; line-height: 1.32; color: var(--text-2);
    padding: 0 2px 4px; border-bottom: 1px solid var(--stroke);
  }
  body.text-large .bp-explain { font-size: 15.5px; }

  /* The head of the card — the call, the tag, the points — is `flex: none` in
     these rails so that the SENTENCE takes the squeeze: it is the part that can
     be cut short. The sentence has left the card, so the head is all that is in
     it, and `flex: none` now means only that it cannot give way either. The
     tag's own `text-overflow` never got its turn and the head ran out of the
     card, under the panel docked beside it.

     On an iPhone 17 Pro in landscape (874x402) the card is 241px. With a
     16-HCP pill, "Your call" and either system's tag — "2/1 Game Force" and
     "ACOL (weak NT)" are both over 110px — make a head of about 300px, and the
     panel covered 55-61px of the pill's 78. A Double or a Redouble did the same
     under any long tag, and under SUGGESTED too (22px and 47px). Of the 18
     pairs of lead and tag measured, 14 had the pill under the panel.

     So the head may shrink, which hands the squeeze to the one part of it
     built to take it: the tag clips with an ellipsis, the call and the points
     never do. The selector matches the under-700px rule that set `flex: none`
     first, and comes after it. */
  .game-screen.is-bidding .bidding-info .bi-head { flex: 0 1 auto; min-width: 0; }
}

/* ...and on a phone the system tag goes.

   Shrinking alone leaves the idle card's tag about 40px wide — "2/…" — which
   says nothing. The tag names the system the meaning beside it is written
   under, and the meaning is no longer beside it: it is in the panel. So on a
   phone the tag goes, as it already does under 700px and in portrait.
   SUGGESTED stays; it says whose call this is.

   900px is the line between the 874px phones, whose card cannot hold it, and
   the 932px and 956px ones, whose card nearly or quite can: nothing there
   reached the panel, even with the tag. Those, and any wider window, keep it. */
@media (min-width: 700px) and (max-width: 900px) and (orientation: landscape) and (min-height: 346px) and (max-height: 520px) {
  .game-screen.is-bidding .bi-tag:not(.sug) { display: none; }
}

/* The compass table on a landscape phone under 700px wide.

   The columns above were measured on an 812px phone, and they are fixed:
   312 + 182 + 156 is 650px before the rail. A 667x375 SE has 621px once the
   padding (28) and three gaps (18) are gone, so the grid ran to a 682px
   scrollWidth. The rail's `minmax(0, 1fr)` was the only column that could
   give, and it went to 0px. So the rail's 86px of pills sat on top of the
   declarer's backs, and the far column ran off the other edge. That column
   is the dummy tray with the dummy East, or the E backs and the trick
   counters with the dummy West. The iPhone Duo's outer screen at 678x466
   hits the same block and is 11px better off, which is not enough.

   The budget, at 621px: rail `auto` (86px, the width of its widest pill),
   declarer's stack 110, cross 156, and whatever is left goes to the dummy
   (269px at 667, 280 at 678). Each of the three gives something:
   - the backs show 6px of edge instead of 10: 38 + 12 x 6 = 110px, the same
     exposure the side stacks of the flat layout already use;
   - the cross loses 18px of spread, and the North and South cards now sit
     exactly between the West and East cards instead of 9px clear of them;
   - the dummy's fans step 18px a card instead of 26, and their gap is 10px
     instead of 14. A 5-5 tray, the one measured here, was 305px and is now
     234. A 6-5 tray (252px) fits. A 6-6 tray (270px) is 1px over at 667, and
     it spills toward the cross, not off the screen. At 18px a card, the
     14px index still shows a whole "10".
   The dummy column takes the slack, not the rail, so the cross stays
   between the two hands. It sits 34px off the centre line, where the
   812px layout had it within 9px. */
@media (max-width: 699px) and (orientation: landscape) and (max-height: 520px) {
  .game-screen.dummy-at-W:not(.is-bidding) .table-area {
    grid-template-columns: minmax(0, 1fr) 156px 110px auto;
  }
  .game-screen.dummy-at-E:not(.is-bidding) .table-area {
    grid-template-columns: auto 110px 156px minmax(0, 1fr);
  }
  .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: 110px; }
  .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 {
    margin-left: -32px;
  }
  .game-screen.dummy-at-W:not(.is-bidding) .seat-zone.seat-E .hidden-hand .card-back:first-child,
  .game-screen.dummy-at-E:not(.is-bidding) .seat-zone.seat-W .hidden-hand .card-back:first-child {
    margin-left: 0;
  }
  .game-screen.dummy-at-W:not(.is-bidding) .trick-area,
  .game-screen.dummy-at-E:not(.is-bidding) .trick-area { width: 156px; }
  .game-screen.dummy-at-W:not(.is-bidding) .trick-card.pos-N,
  .game-screen.dummy-at-E:not(.is-bidding) .trick-card.pos-N,
  .game-screen.dummy-at-W:not(.is-bidding) .trick-card.pos-S,
  .game-screen.dummy-at-E:not(.is-bidding) .trick-card.pos-S { left: 52px; }
  .game-screen.dummy-at-W:not(.is-bidding) .dummy-cols.side,
  .game-screen.dummy-at-E:not(.is-bidding) .dummy-cols.side { column-gap: 10px; }
  .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: calc(var(--dw) * -0.55);
  }
  .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;
  }
}

/* Short landscape sizes its cross by hand, not by the card.

   Every band from 700px up now derives --tw from --card-player, so a played
   card is the size it was in the fan. That trade buys its height from the
   table, and this orientation has none to sell: the middle row is ~140px and
   the trick's own grid column is 132px wide. So the two caps this layout has
   always used are restated here, after the blocks that would otherwise reach
   in — 44vmin for a tablet-width window, and 30vh as well once the window is
   wide enough that vmin stops being the short edge. Under 700px nothing is
   restated and the 196px base value stands, as it always has. */
@media (min-width: 700px) and (orientation: landscape) and (max-height: 520px) {
  .game-screen:not(.is-bidding) .table-center { --tw: min(44vmin, 340px); }
}
@media (min-width: 920px) and (orientation: landscape) and (max-height: 520px) {
  .game-screen:not(.is-bidding) .table-center { --tw: min(44vmin, 360px, 30vh); }
}

/* ----------------------------------------------------- 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) {
  /* --- the auction, laid out on the table ---------------------------------

     The markup puts the parts in their places; this band hands out the height.
     The panel's own height is the sum of its four rows (--bp-h), so the table
     reserves exactly that band and the record fills everything above it.
     Nothing here has to guess where anything else ends. */
  .game-screen.is-bidding .table-area { gap: 8px; justify-content: flex-start; }

  /* The height the table ACTUALLY has, which is not the height a media query
     sees. The panel's band is a sum of four button rows, and those rows stepped
     down at a 719px viewport — a number blind to the notch and the home bar. A
     393x852 iPhone reports 852 and has about 759 to lay out in, so it took the
     tall phone's numbers and the ~90px it does not have came out of the one
     item that can give: the record, left showing a round and a half with the
     round above it sliced in half by the sticky headings. That is the whole of
     the bug — it shows up first on a diagram because "You sit East" wraps the
     status line to a second row and takes another 14px with it.

     env() is readable in calc() even though a media query cannot read it, so
     the rows are sized against the real height instead of a step: they run
     between the two sets of numbers that were already tuned — the tall phone's
     at the top, the short phone's at the floor — and the step block below still
     takes over further down, where it is smaller again. */
  .game-screen.is-bidding {
    --usable-h: calc(100vh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px));
    --bp-lv: clamp(46px, calc(var(--usable-h) * 0.0616), 50px);
    --bp-st: clamp(52px, calc(var(--usable-h) * 0.0714), 58px);
    --bp-sp: clamp(46px, calc(var(--usable-h) * 0.0616), 50px);
    --bp-pl: clamp(48px, calc(var(--usable-h) * 0.0665), 54px);
    --bp-gap: clamp(5px, calc(var(--usable-h) * 0.0086), 7px);
    --bp-pad: clamp(6px, calc(var(--usable-h) * 0.0099), 8px);
  }
  /* 100vh is the window with the browser's chrome at its SMALLEST, which
     overstates a phone browser's first paint by the height of its toolbar.
     Where svh exists it is the honest number; where it does not (the WebView
     this app ships in, on the iOS versions Capacitor still targets) there is no
     collapsing toolbar and the two are the same. */
  @supports (height: 100svh) {
    .game-screen.is-bidding {
      --usable-h: calc(100svh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px));
    }
  }
  /* A round of the record comes down with the buttons — the same argument, and
     it is what decides how many rounds are on screen: at 30px, the height the
     short phone's block already sets, a 759px window holds three whole rounds
     and its headings rather than two and a sliced one. */
  .game-screen.is-bidding .ag-cell {
    min-height: clamp(30px, calc(var(--usable-h) * 0.0395), 34px);
    line-height: clamp(30px, calc(var(--usable-h) * 0.0395), 34px);
  }
  /* Thirteen backs across the top say "there is a hand there", which the badge
     beside them says too. On a short window they give up their last dozen
     pixels before the record gives up a round. */
  .game-screen.is-bidding .seat-N .hidden-hand .card-back {
    height: clamp(34px, calc(var(--usable-h) * 0.0505), 46px);
  }
  /* Only the middle may give. A long auction makes the record taller than the
     band it has, and if the hand and the top band shrink alongside it the cards
     are the thing that gets clipped — the record has a scrollbar for exactly
     this and they do not. */
  .game-screen.is-bidding .table-top,
  .game-screen.is-bidding .seat-zone.seat-S { flex: none; }
  .game-screen.is-bidding .table-middle {
    flex: 1 1 auto; min-height: 0; align-items: stretch; padding-top: 0; gap: 7px;
    /* the band the floating panel stands in, and the gap above it */
    margin-bottom: calc(var(--bp-h) + clamp(6px, calc(var(--usable-h) * 0.0123), 10px));
  }
  /* 44px is the width of a card back lying on its side, which is what these
     two columns are. */
  .game-screen.is-bidding .seat-zone.seat-W,
  .game-screen.is-bidding .seat-zone.seat-E { width: 44px; }
  .game-screen.is-bidding .table-center { justify-content: stretch; }
  /* North's thirteen are 164px across, leaving 205 for the meaning beside them
     — about 26 characters to a line, three lines to a sentence. */
  .game-screen.is-bidding .bi-text { font-size: 15px; }
  /* "2/1 Game Force" beside a call and a point count in 205px is three words
     ellipsised to two, which tells the player nothing they cannot read on the
     home screen. SUGGESTED is a different matter — it changes what the sentence
     under it is — so that one stays. */
  .game-screen.is-bidding .bi-tag:not(.sug) { display: none; }

  .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); }
  /* And the guard the tablet bands have had all along. The trick row is the
     1fr that the tray, the pills and the hand leave, and on a short phone that
     is less than 228px: 142 on a 647px SE, 159 on the iPhone Duo's outer
     screen, and the cross printed over the pill row and the hand under it.
     Making the cell a size container lets --tw read the row it actually got —
     it is unregistered, so the cq units resolve on .trick-area, whose
     container this is. Size containment also means the cross no longer
     props the row open, which is the point: the row decides, the cross fits.
     34px is the counters under the cross and the gap above them. */
  @supports (container-type: size) {
    .game-screen:not(.is-bidding) .table-center {
      container-type: size; align-self: stretch; justify-self: stretch;
      --tw: min(228px, 53vw, calc((100cqh - 34px) / 0.878), calc(100cqw - 8px));
    }
  }
  .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);
  }

  /* ---- 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);
  }

  /* 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;
  }
}

/* A phone 720px tall or less, where the table and the calls are competing for a
   screen with about 180px less of everything in it.

   The budget on a 667px screen: 437px of table between the status strip and the
   hand, of which the calls want 249 at the sizes above. That leaves 188 for the
   meaning, North's hand and the record together — and the record ends up with
   none of it. So the buttons come down to the brief's 48px floor and lose a
   little of their gaps, the meaning sets a point smaller, and the concealed
   hands lie closer together. Between them that gives the record back about
   90px, which is the difference between showing a round and showing nothing. */
@media (max-width: 699px) and (orientation: portrait) and (max-height: 719px) {
  .game-screen.is-bidding {
    /* 46 and 48, not the 48 and 50 this block first set: the brief's floor is
       the 44px a thumb needs, and the four pixels between those two numbers and
       that floor are worth more to the record than they are to a button which
       is already comfortably hit. Nothing here goes below 44. */
    --bp-lv: 46px; --bp-st: 48px; --bp-sp: 46px; --bp-pl: 48px;
    --bp-gap: 5px; --bp-pad: 6px;
    --auc-lap: -24px;
    /* 16.5vw is the width that makes thirteen overlapped cards exactly one
       screen across, and on a 667px window the height that buys — 90px of it —
       is what left the record showing a single round. At 14vw the fan stops
       short of the edges by a thumb's width and the cards are 52px, which is
       the brief's legibility floor rather than a number picked to fit. The
       record gets the 15px back, and the panel's offset follows on its own
       because it is derived from this. */
    --uw-bid: clamp(52px, 14vw, 84px);
  }
  /* The record's own chrome, at the sizes the landscape rail proved when it had
     the same argument: a call set at 20px in a 26px row is still the one item
     in the box you read a value off, and three rounds of it fit where two did.
     The headings' offset has to follow the padding it cancels. */
  .game-screen.is-bidding .auction-grid-wrap { padding: 7px 10px; }
  .game-screen.is-bidding .ag-head, .game-screen.is-bidding .ag-rnd-head { top: -7px; }
  /* The box scrolls to the newest call, so what sits at its top is whatever is
     left of the oldest round still on screen — on a long auction, a few pixels
     of one, cut through the middle. It reads as a rendering fault rather than
     as more above.

     Rounding the box up to whole rounds is the obvious fix and there is no room
     for it: two rounds and the heading need 96px, and the pad leaves 79. Coming
     DOWN to a whole boundary costs a whole round, because 75px already shows
     two of them and it is a third that is peeking. So keep the rows and fade
     the edge they arrive at, which is what a scrolling box is supposed to do.

     The first 20px stay opaque: the column headings are sticky there — they
     stick to -7px, cancelling the padding, so they sit at the very top of the
     border box the mask is measured from — and they must not fade with the rows
     passing behind them. Opaque again by 30px so the first WHOLE round is never
     touched; only the sliver between them fades.

     Gated on .is-scrolled, set in ui.js, because an auction that FITS the box
     has nothing above it — an unconditional mask ghosts the top of round one
     on every short auction, which is a worse artefact than the one it fixes. */
  .game-screen.is-bidding .auction-grid-wrap.is-scrolled {
    -webkit-mask-image: linear-gradient(to bottom, #000 0 20px, rgba(0,0,0,0) 23px, #000 30px);
            mask-image: linear-gradient(to bottom, #000 0 20px, rgba(0,0,0,0) 23px, #000 30px);
  }
  .game-screen.is-bidding .ag-head { font-size: 13px; padding-bottom: 4px; }
  .game-screen.is-bidding .table-area { gap: 6px; }
  /* North's row of backs is the one thing on this screen that can go without
     taking a decision with it: it costs the top band 50px, and West's and
     East's columns already say that there are three hands out there. Losing it
     also widens the meaning beside it from 26 characters a line to about 38,
     which is what turns a three-line sentence into two. */
  .game-screen.is-bidding .seat-N .hidden-hand { display: none; }
  /* Four pixels of the reserved band handed back: the hand's own badge row is
     the same height whatever the screen, so it is a larger share of a 667px one
     and the table was ending four pixels past the bottom of itself. */
  .game-screen.is-bidding .table-middle { margin-bottom: calc(var(--bp-h) + 4px); }
  .game-screen.is-bidding .bi-text { font-size: 14.5px; }
  .game-screen.is-bidding .strain-btn { font-size: 28px; }
  .game-screen.is-bidding .ag-cell { font-size: 20px; min-height: 26px; line-height: 26px; }
}

/* A short, WIDE phone: the iPhone Duo's outer screen, 466x678.

   The block above was measured on a 667px iPhone SE, which gives 20px of that
   to its status bar. The Duo gives about 96 to the Dynamic Island and the home
   bar, and a media query reads 678 either way — so it takes the SE's numbers
   and the record is left with its heading and no rows. What the query CAN tell
   apart is the shape: every phone that reaches this height band is narrower
   than 5:8, and the Duo is 0.69. So the shape picks the Duo out, and the room
   comes from the places that still had some:

   - the fan is 14vw above, which is 65px on a 466px screen and 52 on the SE it
     was written for. The 52 was the floor; a wider phone does not need a
     bigger card to read one.
   - every button row goes to the 44px a thumb needs, and no lower.
   - the status strip, deal title and meaning give up a few pixels of padding.

   Measured at 466x582 (the outer screen less its insets): the record goes from
   27px to 75px, which is a round and a half, the same as the SE shows. */
@media (max-width: 699px) and (orientation: portrait) and (max-height: 719px) and (min-aspect-ratio: 5/8) {
  .game-screen.is-bidding {
    --bp-lv: 44px; --bp-st: 46px; --bp-sp: 44px; --bp-pl: 44px;
    --bp-gap: 4px; --bp-pad: 5px;
    --uw-bid: 52px;
  }
  .game-screen.is-bidding > .top-bar { padding-bottom: 4px; }
  .game-screen.is-bidding > .status-bar { padding-top: 4px; padding-bottom: 4px; }
  .game-screen.is-bidding .bidding-info { padding: 6px 10px; }
  /* In play the same 15vw costs the hand two rows of 70px cards and the dummy
     a second line of suits, and the trick is what pays for both. The SE's
     56/52 give the cross 159px of row instead of 18. */
  .game-screen { --card-player: 56px; --card-dummy: 52px; }
}


/* ---------------------------------------------------------------------------
   End-of-rubber report
   -------------------------------------------------------------------------*/

/* The pad's verdict. Tinted by outcome, but only just: a lost rubber is a
   normal event and a red card shouting about it every third rubber would wear
   badly on a learner who is, by definition, going to lose plenty of them. */
.rr-hero { text-align: center; }
.rr-hero.rr-won { border-color: rgba(46,125,79,.55); background: linear-gradient(180deg, rgba(46,125,79,.14), transparent 70%), var(--surface); }
.rr-hero.rr-lost { border-color: rgba(176,58,52,.45); background: linear-gradient(180deg, rgba(176,58,52,.12), transparent 70%), var(--surface); }
.rr-hero.rr-open { border-color: rgba(198,163,83,.45); background: linear-gradient(180deg, rgba(198,163,83,.10), transparent 70%), var(--surface); }
.rr-hero-title { font-size: 17px; font-weight: 800; color: var(--gold-soft); }
.rr-hero-sub { font-size: 12.5px; color: var(--text-2); margin-top: 6px; }
.rr-score { display: flex; align-items: center; justify-content: center; gap: 18px; margin-top: 10px; }
.rr-side { min-width: 78px; }
.rr-num { font-size: 30px; font-weight: 800; color: var(--text); font-variant-numeric: tabular-nums; line-height: 1.05; }
.rr-lab { font-size: 10px; font-weight: 800; letter-spacing: .09em; color: var(--text-2); margin-top: 2px; }
/* A rule rather than a centred dot: the two columns are number-over-label, so
   anything vertically centred between them lands under the numbers and reads as
   a stray mark rather than a separator. */
.rr-vs { width: 1px; align-self: stretch; background: rgba(198,163,83,.35); }

/* The grade, set apart from the score above it — see rrGrade(). */
.rr-overall { display: flex; align-items: baseline; gap: 10px; margin: 12px 0 2px; }
.rr-overall-num { font-size: 34px; font-weight: 800; color: var(--gold); font-variant-numeric: tabular-nums; line-height: 1; }
.rr-overall-lab { font-size: 12px; color: var(--text-2); }
.rr-trend { font-size: 12px; color: var(--text-2); margin: 2px 0 12px; line-height: 1.45; }
.rr-trend.up { color: #7FBF95; }
.rr-trend.down { color: #D9938E; }

.rr-chips { width: 100%; }
.rr-chips .chip { flex: 1; }

/* Hands the report has no grade for. Framed as an offer, not a warning. */
.rr-missing {
  margin-top: 12px; padding: 10px 12px; border-radius: 10px;
  background: rgba(198,163,83,.08); border: 1px solid rgba(198,163,83,.22);
  font-size: 12.5px; color: var(--text-2); display: flex; flex-direction: column; gap: 8px;
}
.rr-missing .progress { width: 100%; }
.rr-missing .action-btn { height: 38px; }
.rr-missing .section-caption { margin: 0; }

.rr-lesson { display: flex; gap: 10px; padding: 9px 0; border-bottom: 1px solid rgba(44,74,59,.5); align-items: baseline; }
.rr-lesson:last-child { border-bottom: none; }
.rr-lesson-lab {
  flex-shrink: 0; font-size: 10.5px; font-weight: 800; border-radius: 6px; padding: 3px 8px;
  color: var(--gold); background: rgba(198,163,83,.15);
}
.rr-lesson.good .rr-lesson-lab { color: #8FCBA5; background: rgba(46,125,79,.20); }
.rr-lesson-text { font-size: 13px; line-height: 1.5; color: var(--text); }

/* A whole row that is one button, unlike the archive's row-plus-delete. */
button.rr-hand { width: 100%; background: none; text-align: left; }
button.rr-hand:active { opacity: .7; }
button.rr-hand .archive-main { display: flex; }

/* ---------------------------------------------------------------- diagrams */

/* The set-up compass: North and South centred across the full width, West and
   East side by side between them — the diagram's own shape, without an empty
   middle column eating a third of a phone. */
.dg-compass {
  display: grid; gap: 10px;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  grid-template-areas: "n n" "w e" "s s";
}
.dg-at-N { grid-area: n; } .dg-at-W { grid-area: w; } .dg-at-E { grid-area: e; } .dg-at-S { grid-area: s; }
.dg-at-N, .dg-at-S { justify-self: center; width: min(100%, 230px); }
.dg-hand {
  background: rgba(255,255,255,.03); border: 1px solid var(--stroke); border-radius: 11px;
  padding: 8px 9px; display: flex; flex-direction: column; gap: 6px; min-width: 0; height: 100%;
}
.dg-hand.is-you { border-color: rgba(198,163,83,.6); }
/* A vulnerable hand in the set-up compass: a red wash behind it, so the board's
   vulnerability reads off the diagram itself and not only off the Vul row. The
   gold border still marks the player's own seat. */
.dg-hand.is-vul { background: rgba(176,58,52,.13); border-color: rgba(176,58,52,.45); }
.dg-hand.is-vul.is-you { border-color: rgba(198,163,83,.6); }
.dg-board-line { text-align: center; font-size: 11px; font-weight: 700; color: var(--text-2); margin-top: 10px; }
.dg-hand-head { display: flex; justify-content: space-between; align-items: baseline; gap: 6px; }
.dg-seat { font-size: 11.5px; font-weight: 800; color: var(--gold); white-space: nowrap; }
.dg-count { font-size: 10.5px; font-weight: 600; color: var(--text-2); white-space: nowrap; }
.dg-lines {
  display: block; width: 100%; text-align: left; border-radius: 7px;
  padding: 3px 4px; background: rgba(0,0,0,.2); flex: 1;
}
.dg-lines:active { background: rgba(198,163,83,.14); }
.dg-lines .hand-line .hr { overflow-wrap: anywhere; }
.dg-hidden { font-size: 11.5px; color: var(--text-2); font-style: italic; padding: 12px 3px; text-align: center; flex: 1; }
.dg-kind { margin-top: 0; width: 100%; }
.dg-kind .seg { flex: 1; padding: 5px 2px; font-size: 11px; text-align: center; }
.dg-status { margin-top: 12px; font-size: 12px; font-weight: 600; color: var(--text-2); text-align: center; }
.dg-tools { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.dg-tools .action-btn { flex: 1; min-width: 140px; height: 38px; font-size: 12.5px; }
.dg-tools .danger-btn { padding: 8px; }
.dg-note { margin: 8px 0 0; }
.dg-center { text-align: center; }
.dg-continue { width: 100%; margin-top: 8px; }
.wide-btn:disabled { opacity: .4; }

.dg-picker { display: flex; flex-direction: column; gap: 8px; }
.dg-suit-row { display: flex; align-items: center; gap: 6px; }
.dg-suit { width: 18px; font-size: 17px; font-weight: 700; text-align: center; flex-shrink: 0; }
.dg-suit.s-S { color: var(--text); }
.dg-suit.s-C { color: #9FB0AE; }
.dg-suit.s-H { color: #F1615A; }
.dg-suit.s-D { color: #EFA15A; }
.dg-ranks { display: grid; grid-template-columns: repeat(13, minmax(0, 1fr)); gap: 3px; flex: 1; min-width: 0; }
.dg-rank {
  position: relative; height: 36px; border-radius: 6px; padding: 0;
  background: rgba(255,255,255,.07); color: var(--text);
  font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums;
}
.dg-rank.sel { background: var(--gold); color: var(--ink-on-gold); }
.dg-rank.held { color: var(--text-2); background: rgba(255,255,255,.025); }
.dg-holder { position: absolute; right: 2px; bottom: 1px; font-size: 8px; font-weight: 800; color: var(--gold-soft); line-height: 1; }
.dg-notice { margin-top: 10px; font-size: 12.5px; font-weight: 700; color: #E07A74; text-align: center; }
.dg-row2 { display: flex; gap: 8px; margin-top: 12px; }
.dg-row2 .action-btn { flex: 1; }

.dg-score { display: flex; align-items: baseline; justify-content: center; gap: 8px; margin: 10px 0 4px; }
.dg-score-num { font-size: 26px; font-weight: 800; color: var(--gold); font-variant-numeric: tabular-nums; }
.dg-score-lab { font-size: 12px; color: var(--text-2); }

/* diagrams: reading a photo */
.dg-photo { border-color: rgba(198,163,83,.45); }
.dg-check { display: flex; gap: 8px; align-items: baseline; font-size: 12.5px; color: var(--text); padding: 4px 0; line-height: 1.45; }
.dg-check-mark {
  width: 18px; height: 18px; flex-shrink: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 800; background: rgba(255,255,255,.08); color: var(--text-2);
}
.dg-check.ok .dg-check-mark { background: rgba(111,191,139,.2); color: #6FBF8B; }
.dg-check.bad .dg-check-mark { background: rgba(224,122,116,.2); color: #E07A74; }
.dg-issues { margin: 8px 0 0 18px; font-size: 11.5px; color: var(--text-2); line-height: 1.45; }
/* A card the photo reader guessed at: outlined in amber until its hand is checked. */
.dg-unsure { outline: 1.5px solid #E0A24A; outline-offset: 1px; border-radius: 3px; color: #F2C57C; }
.dg-rank.unsure { box-shadow: inset 0 0 0 2px #E0A24A; }
/* A set-up choice that does not apply to this deal (a printed lead nobody can play). */
.lp-row .seg:disabled { opacity: .4; }
.dg-part-row .segmented { display: grid; grid-template-columns: repeat(9, minmax(0, 1fr)); }
@media (max-width: 520px) { .dg-part-row .segmented { grid-template-columns: repeat(5, minmax(0, 1fr)); } }
.dg-lead-row { margin-top: 12px; }
.dg-part-head { font-size: 11px; font-weight: 700; color: var(--text-2); text-transform: uppercase; letter-spacing: .3px; margin: 12px 0 7px; }
/* The lead picker: only the leader's cards, so a void says so. */
.dg-lead-picker { margin: 10px 0 12px; }
.dg-void { grid-column: 1 / -1; color: var(--text-2); font-size: 13px; padding: 6px 2px; }
.dg-scanning {
  display: flex; align-items: center; justify-content: center; gap: 12px;
  padding: 22px; max-width: 300px; font-size: 14px; font-weight: 700; color: var(--text);
}
.toggle:disabled { opacity: .35; cursor: default; }

/* diagrams: checking a photo — what was picked up, and cropping it */
.dg-count { display: flex; align-items: baseline; justify-content: center; gap: 8px; margin: 2px 0 8px; }
.dg-count-num { font-size: 30px; font-weight: 800; font-variant-numeric: tabular-nums; color: #E0A24A; }
.dg-count.ok .dg-count-num { color: #6FBF8B; }
.dg-count-lab { font-size: 13px; color: var(--text-2); }
.dg-seat-chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin-bottom: 12px; }
.dg-seat-chip {
  font-size: 12px; padding: 3px 9px; border-radius: 999px; color: var(--text-2);
  border: 1px solid var(--stroke); background: rgba(0,0,0,.18);
}
.dg-seat-chip b { font-variant-numeric: tabular-nums; margin-left: 2px; }
.dg-seat-chip.ok { border-color: rgba(111,191,139,.5); } .dg-seat-chip.ok b { color: #6FBF8B; }
.dg-seat-chip.short { border-color: rgba(224,162,74,.6); } .dg-seat-chip.short b { color: #E0A24A; }
.dg-seat-chip.none { border-style: dashed; }
.dg-shot-wrap { display: flex; justify-content: center; margin: 6px 0 10px; }
.dg-shot { position: relative; line-height: 0; border-radius: 6px; background: #000; }
.dg-shot img {
  display: block; max-width: 100%; max-height: 62vh; border-radius: 6px;
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
/* A suit row the reader picked up, and how many cards it read in it. */
.dg-hl {
  position: absolute; pointer-events: none; border-radius: 3px;
  outline: 2px solid rgba(111,191,139,.95); outline-offset: 2px; background: rgba(111,191,139,.16);
}
.dg-hl.unsure { outline-color: rgba(224,162,74,.95); background: rgba(224,162,74,.18); }
.dg-hl-n {
  position: absolute; left: calc(100% + 6px); top: 50%; transform: translateY(-50%);
  min-width: 16px; padding: 0 4px; border-radius: 8px; line-height: 16px; text-align: center;
  font-size: 10.5px; font-weight: 800; color: #10231A; background: #6FBF8B;
}
.dg-hl.unsure .dg-hl-n { background: #E0A24A; }
/* A suit row that was not read, where it should be: the missing cards are there. */
.dg-hl.missing { outline: 2px dashed rgba(224,162,74,.95); background: rgba(224,162,74,.10); }
.dg-hl.missing .dg-hl-n { background: #E0A24A; }
.dg-hl-seat {
  position: absolute; pointer-events: none; transform: translateY(calc(-100% - 5px));
  padding: 0 6px; border-radius: 4px; line-height: 17px; white-space: nowrap;
  font-size: 11px; font-weight: 800; color: #10231A; background: #6FBF8B;
}
.dg-hl-seat.short { background: #E0A24A; }
.dg-hl-seat.beside { transform: translateX(36px); }
/* Zoomed in on a diagram that is a small part of the photo: the photo and its
   outlines are enlarged together inside a clipped frame, while the counts and
   tags keep their size, so they cover less of the cards. */
.dg-zoom { position: relative; overflow: hidden; max-width: 100%; border-radius: 6px; background: #000; }
.dg-zoom .dg-shot { position: absolute; border-radius: 0; }
.dg-zoom .dg-shot img { width: 100%; max-width: none; max-height: none; border-radius: 0; }
.dg-zoom-toggle {
  display: block; margin: -4px auto 6px; padding: 6px 12px; background: none; border: 0;
  color: var(--gold); font: inherit; font-size: 13px; font-weight: 700; cursor: pointer;
}
.dg-advice { text-align: center; }
.dg-whole-btn, .dg-use-btn { width: 100%; margin-top: 10px; }
.dg-use-btn:disabled { opacity: .4; cursor: default; }
/* The crop frame: everything outside it dimmed, a corner handle to drag at each corner. */
.dg-crop-area { overflow: hidden; touch-action: none; }
.dg-crop-box {
  position: absolute; cursor: move; touch-action: none;
  border: 2px solid var(--gold); box-shadow: 0 0 0 9999px rgba(0,0,0,.58);
  background-image:
    linear-gradient(to right, transparent calc(33.33% - .5px), rgba(230,213,168,.35) calc(33.33% - .5px), rgba(230,213,168,.35) calc(33.33% + .5px), transparent calc(33.33% + .5px), transparent calc(66.66% - .5px), rgba(230,213,168,.35) calc(66.66% - .5px), rgba(230,213,168,.35) calc(66.66% + .5px), transparent calc(66.66% + .5px)),
    linear-gradient(to bottom, transparent calc(33.33% - .5px), rgba(230,213,168,.35) calc(33.33% - .5px), rgba(230,213,168,.35) calc(33.33% + .5px), transparent calc(33.33% + .5px), transparent calc(66.66% - .5px), rgba(230,213,168,.35) calc(66.66% - .5px), rgba(230,213,168,.35) calc(66.66% + .5px), transparent calc(66.66% + .5px));
}
.dg-crop-h { position: absolute; width: 20px; height: 20px; border: 0 solid var(--gold-soft); }
/* The handle is drawn small but answers a thumb-sized area around it. */
.dg-crop-h::after { content: ''; position: absolute; inset: -14px; }
.dg-crop-h.nw { left: -4px; top: -4px; border-width: 4px 0 0 4px; cursor: nwse-resize; }
.dg-crop-h.ne { right: -4px; top: -4px; border-width: 4px 4px 0 0; cursor: nesw-resize; }
.dg-crop-h.sw { left: -4px; bottom: -4px; border-width: 0 0 4px 4px; cursor: nesw-resize; }
.dg-crop-h.se { right: -4px; bottom: -4px; border-width: 0 4px 4px 0; cursor: nwse-resize; }

/* diagrams: the cloud reader */
.dg-cloud-row { margin-top: 10px; border-top: 1px solid rgba(44,74,59,.5); border-bottom: none; }
.dg-cloud-row .segmented { flex-wrap: wrap; }
.dg-cloud-btn { width: 100%; margin-top: 12px; }

/* diagrams: paste a deal, fit to the auction */
.dg-paste-input {
  width: 100%; margin-top: 4px; padding: 10px; border-radius: 10px; resize: vertical;
  background: rgba(0,0,0,.28); border: 1px solid var(--stroke); color: var(--text);
  font: 12.5px/1.45 ui-monospace, "SF Mono", Consolas, monospace;
}
.dg-paste-input:focus { outline: 2px solid rgba(198,163,83,.6); outline-offset: 1px; }
.dg-fit-row { margin-top: 10px; border-top: 1px solid rgba(44,74,59,.5); border-bottom: none; }
