/* cribbageweb — /play/ only (WEB-054, governed by W-0067).
 *
 * The site's second stylesheet, and the only one that is not site.css. It is
 * loaded by /play/ alone through the `extraStylesheet` frontmatter hook in
 * base.njk (W-0067 §4): 15–20 KB of game CSS would be dead weight on every
 * content page, and a future embed/iframe port needs the game's styles without
 * the site chrome.
 *
 * Because of that port this file is written SELF-CONTAINED. Every colour, face
 * and radius it uses is aliased below as `var(--site-token, literal)`, so the
 * game surface renders correctly with site.css present (tokens win) and without
 * it (fallbacks win). Never reference a site token directly further down.
 *
 * Grid: every authored size is a multiple of 4, with 14 and 18 as the two
 * sanctioned off-grid values (docs/DESIGN.md §Scale). Three documented
 * exceptions, all ratios rather than authored dimensions: the playing-card
 * proportion (1 : 1.375, so a 112px card is 154px tall), the card-corner ratio
 * (24/108 horizontally, 24/148 vertically — the art's own radius, see
 * .play-card), and mono letter-spacing, which the grid rule exempts.
 *
 * Seat colours are deliberately reversed from the Nobs app — human red, bot
 * blue (W-0067 §6). Do not "correct" this in either direction.
 *
 * LAYOUT STABILITY (W-0068). The surface sizes from the screen width alone and
 * never changes height during a game. That is bought with one rule: every child
 * of .play__area has a height that no game state can change —
 *
 *   .play__row     var(--card-h)   a hand row, whatever it is holding
 *   .play__centre  230px           starter | count, both columns shorter
 *   .play__strip   80px + 44       status 28 + gap 8 + actions 44, and a margin
 *                                  that centres the dialogue under the crib
 *
 * so their sum, and the surface, are constants. No row here has a min-height,
 * none grows with its contents, and a `hidden` button leaves a fixed box
 * behind it. When a hand holds more cards than its track can lay flat the cards
 * FAN — a negative margin overlaps them and the row height stays put. The two
 * seat chips are pinned to the area's corners rather than carried by the rows,
 * and the rows reserve a fixed 224px gutter for them. Anything added here needs
 * a state-independent height, or the guarantee is gone.
 *
 * Class contract for the game modules (cards.js / board.js write these):
 *   .play-card                 base card box, sized from --card-w
 *   .play-card--back           face-down card; the styling here paints the
 *                              no-JS placeholder <span> only, runtime backs
 *                              being self-contained SVG art
 *   .play-card--slot           a playable card, always a <button>
 *   .play-card--pile           a card in the played pile (overlapped)
 *   .play-card--starter        a card in the starter fan (absolutely placed)
 *   .play-card__face           the composed face inside a slot
 *   .play-ghost                a card in flight (motion.js, inside .play-flight)
 *   .play-deck__card           one of the travelling deck's three backs
 *   .play-crib--centre         the crib at its discard well, on the play axis
 *   .play-crib--dealer-you     the crib docked beside the reader's plaque
 *   .play-crib--dealer-bot     the crib docked beside the opponent's plaque
 *   .play-crib__label          the crib fixture's mono caption
 *   .play-crib__slots          the four slots' box (a row, or the dock's fan)
 *   .play-crib__slot           one crib slot, empty or holding a back
 *   .play-stage__headline      the cut ceremony's serif line (stages.js)
 *   .play-stage__sub           its mono subtext
 *   .play-stage__seat          one side of the ceremony: plaque over slot
 *   .play-stage__plaque--you   the ceremony's own seat plaque, human
 *   .play-stage__plaque--bot   the ceremony's own seat plaque, opponent
 *   .play-stage__slot          where a cut card lands
 *   .play-stage__fan           the deck to cut: one <button> the whole width
 *                              of the felt, with the backs dealt across it
 *   .play-stage__back          one back in that deck, a decorative <span>
 *   .play-stage__flier         a cut card between the fan and its slot
 *   .play-show__notches        the three counts, as progress bars (show.js)
 *   .play-show__notch          one of those bars
 *   .play-show__divider        the "THE SHOW" caption between two hairlines
 *   .play-show__rule           one of those hairlines
 *   .play-show__divider-text   the caption itself
 *   .play-show__body           everything that is swapped per section
 *   .play-show__name           whose hand is being counted, in the serif
 *   .play-show__cards          the cut card and the four being counted
 *   .play-show__starter        the cut card's block: a peek back under a face
 *   .play-show__peek           that back, offset out from under it
 *   .play-show__starter-face   the cut card itself
 *   .play-show__card           one card box, holding both of its sides
 *   .play-show__hand           the four overlapped cards of the count
 *   .play-show__total          the HAND TOTAL block
 *   .play-show__total-label    its mono caption
 *   .play-show__total-num      the running total, in the serif
 *   .play-show__rows           the scrolling list of what the hand scores
 *   .play-show__row            one line of it, always a <button>
 *   .play-show__row-label      what scored
 *   .play-show__row-value      how much, tinted by who is counting
 *   .play-show__rows--you      that tint, when the reader is counting
 *   .play-show__rows--bot      the same, for the opponent
 *   .play-show__note           "COUNTED 8 OF 12", when a win cut the count off
 *   .play-show__next           the panel's one control (a .play-btn--ghost)
 *   .play-pill__chip           one score pill (pill.js), inside .play-pill
 *   .play-pill__chip--you      the same pill when the reader scored it
 *   .is-illegal                a card that would take the count past 31
 *   .is-selected               a card chosen for the crib
 *   .is-sorting                a hand card mid sort-pass (deal's rank glide)
 *   .is-slots                  the pile in pegging's 8-fixed-slot mode
 *   .is-spent                  a pile card from a finished set, greyed in place
 *   .is-active                 the seat whose turn it is (on .play-chip), or
 *                              the show row the highlight is on
 *   .is-muted                  sound is off (on .play__mute)
 *   .is-shown                  the travelling deck, or the crib, is on the felt
 *   .is-rifling                the travelling deck is being shuffled
 *   .is-filled                 a crib slot holding a card
 *   .is-yours                  the crib on screen is the reader's. Currently
 *                              unstyled — the caption stopped carrying a seat
 *                              colour in W-0079 — and kept as the hook.
 *   .is-open                   the cut ceremony, or the show panel, is on the
 *                              felt
 *   .is-done                   a count the show has reached (on a notch)
 *   .is-dim                    a card outside the highlighted show row
 *   .is-nodiv                  a show row whose hairline is transparent, so the
 *                              highlight can round a corner without moving it
 *   .is-grayed                 a show row whose points were never awarded
 *   .is-zero                   the show's muted "nothing" row
 *   .is-loser                  the seat that drew high, dimmed (on a stage seat)
 *   .is-entering               a score pill for the frame before it arrives
 *   .is-leaving                a score pill floating up and out on its own
 *   .is-fast                   a score pill cleared by the one replacing it
 *   .is-hidden                 generic hide utility
 *
 * STAGES. game.js writes `data-stage` on the surface — cut | deal | discard |
 * pegging | show — and the rules under §Stages read it. They may only ever
 * change OPACITY: a stage that moved or removed furniture would break the
 * height guarantee above. An absent attribute (the opening table, before an
 * opponent is chosen) means everything is visible.
 */

/* ---------- Local token aliases (the self-containment layer) ---------- */
:root {
  --play-felt-top: var(--felt-top, #0e1d36);
  --play-felt-bottom: var(--felt-bottom, #01050b);
  --play-ink: var(--nobs-ink, #f3f9ff);
  --play-lead: var(--nobs-lead, #c4d5e6);
  --play-blue: var(--nobs-accent, #5fa4f5);
  --play-red: var(--red, #c4382e);
  --play-red-hover: var(--red-hover, #a02d25);
  --play-linen: var(--linen, #f6f3ed);
  --play-edge: var(--edge, #e5dfd3);
  --play-muted: var(--ink-muted, #6e675c);
  --play-font-display: var(--font-display, 'Instrument Serif', Georgia, serif);
  --play-font-ui: var(--font-ui, 'Inter', 'Helvetica Neue', Arial, sans-serif);
  --play-font-mono: var(--font-mono, 'Spline Sans Mono', ui-monospace, monospace);
  --play-radius-card: var(--radius-card, 16px);
  --play-radius-figure: var(--radius-figure, 12px);
  --play-radius-pill: var(--radius-pill, 999px);
  --play-shadow-onfelt: var(--shadow-onfelt, 0 12px 32px rgba(1, 5, 11, 0.55));
  --play-shadow-lift: var(--shadow-lift, 0 8px 24px rgba(26, 24, 21, 0.1));
  --play-shadow-cta: var(--shadow-cta, 0 6px 18px rgba(196, 56, 46, 0.22));

  /* Felt-only derivatives. The felt is dark enough that the light palette has
     no equivalents, so these are literals by necessity, not by omission. */
  --play-hairline: rgba(243, 249, 255, 0.12);
  --play-well: rgba(243, 249, 255, 0.05);
  --play-seat-off: rgba(243, 249, 255, 0.16);
  --play-seat-on: rgba(243, 249, 255, 0.92);
  --play-scrim: rgba(1, 5, 11, 0.72);

  /* The app's own card shadows (theme/shadows.ts cardLg / cardMd), literals for
     the same reason as the block above: the light palette has no equivalent. */
  --play-shadow-card: 0 12px 20px rgba(0, 0, 0, 0.4);
  --play-shadow-card-sm: 0 8px 16px rgba(0, 0, 0, 0.4);

  /* The app's own accent, kept at its own value rather than aliased to
     --play-blue (W-0104). --play-blue is the site's brightened #5FA4F5, chosen
     to clear AA as TEXT on the felt; these three are a button's FILL and the
     hairline round its outlined twin, where the app's darker #2C8EF3 is what
     carries white text at 4.5:1 and what the end-game screen actually uses.
     Felt-only literals for the same reason as the block above. */
  --play-blue-fill: #2c8ef3;
  --play-blue-fill-hover: #1a7de6;
  --play-blue-wash: rgba(44, 142, 243, 0.1);
  --play-blue-wash-hover: rgba(44, 142, 243, 0.2);
  --play-blue-hairline: rgba(95, 164, 245, 0.24);

  /* The show's row highlight: the accent at a sixteenth, which is the only
     fill on the felt that has to read as "this row is the one" without moving
     anything. A literal for the same reason as the block above — an alpha of
     --play-blue is not expressible without color-mix, and the felt has no
     palette entry to alias. */
  --play-accent-fill: rgba(95, 164, 245, 0.16);

  /* The score pill, the app's toast palette carried over as felt-only literals.
     The two text tints keep the app's own values; which SEAT gets which is the
     web's decision, not the app's — human red, bot blue (W-0067 §6). */
  --play-pill-fill: rgba(0, 0, 0, 0.75);
  --play-pill-text-you: #ffb7b2;
  --play-pill-text-bot: #a2cdfc;
  --play-shadow-pill: 0 4px 12px rgba(0, 0, 0, 0.4);

  /* Motion vocabulary, mirroring MOTION in src/assets/js/play/motion.js: the
     table's flights are scheduled in JS and its chrome in CSS, and the two are
     meant to read as one system. The fourth easing is `linear` (pinches and
     dims) and needs no token. Durations are runtime values, which the 4pt grid
     exempts. */
  --play-ease-travel: cubic-bezier(0.645, 0.045, 0.355, 1);
  --play-ease-arrive: cubic-bezier(0.215, 0.61, 0.355, 1);
  --play-ease-exit: cubic-bezier(0.55, 0.055, 0.675, 0.19);
  --play-dur-chrome: 250ms;
  --play-dur-grey: 300ms;
  --play-dur-riffle: 550ms;
  --play-dur-stage: 500ms;
  /* The one moment of the hand that gets a screen to itself takes the longest
     fade on the surface, and the furniture behind it comes back a little
     faster than the screen goes away. */
  --play-dur-furniture: 400ms;
  /* The score pill arrives and leaves in the same 150ms; the faster one is the
     preempt, where a pill is only clearing the way for the one replacing it. */
  --play-dur-pill: 150ms;
  --play-dur-pill-fast: 100ms;
}

/* ---------- Utilities ---------- */
.play-page [hidden],
.play [hidden],
.is-hidden {
  display: none !important;
}

/* Every fixed height below is authored border-box. site.css sets that globally,
   but the iframe port ships this file alone, so the game states it for itself —
   scoped to the game's own roots, never `*`, so the port cannot rewrite a host
   page's box model. */
.play-page,
.play-page *,
.play-page *::before,
.play-page *::after,
.play,
.play *,
.play *::before,
.play *::after {
  box-sizing: border-box;
}

/* ---------- Page container ----------
   1660px, deliberately wider than the site's 1180px, so the game surface, the
   board panel and the 300px rail all clear the fold (W-0067). The header and
   footer keep their own 1180px container and are not touched. */
.play-page {
  width: 100%;
  max-width: 1660px;
  margin: 0 auto;
  padding: 16px 32px 96px;
}

.play-shell {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
  gap: 24px;
  align-items: start;
}

/* ---------- Game surface ---------- */
.play {
  /* One custom property sets the scale for every card on the surface, so the
     responsive ladder only ever has to change --card-w, and --card-h carries it
     into every row height on the surface. The 1.375 factor is the playing-card
     proportion, which is why a 112px card is 154px tall. A card is never sized
     by how many of them a hand is holding: it is a function of the viewport
     width and of nothing else, which is what keeps one surface height per
     width (W-0068).

     The clamp is the tablet's whole mechanism (W-0082): 8.8vw - 2.4px is the
     line through (1300px, 112px), so a card is exactly 112 at every width the
     desktop layout was drawn at and shrinks with the window below that, down
     to 65.2 at the 768px floor. The phone rung swaps in its own line. */
  --card-w: clamp(64px, 8.8vw - 2.4px, 112px);
  --card-h: calc(var(--card-w) * 1.375);
  --hand-gap: 16px;
  --pile-w: calc(var(--card-w) - 32px);
  /* The travelling deck, as a fraction of a card rather than an authored size:
     a dealt card grows from it, and the growth has to read the same at every
     rung of the responsive ladder. A ratio, so grid-exempt like the 1.375. */
  --play-deck-w: calc(var(--card-w) * 0.72);
  /* What the starter cluster is drawn in ratios OF. A card, everywhere the
     starter has a column of its own to stand in; a fraction of one on the
     phone, where it shares a band with the count (W-0087). One token, so the
     fan, its caption and the cut card can never disagree about the scale. */
  --play-starter-w: var(--card-w);
  /* The pegging row's 8 fixed slots: an inset off either end of the track, and
     the step that lands slot 0 flush left and slot 7 flush right of what is
     left. The 100% resolves against .play__pile, so the row is a function of
     the play area's width alone. */
  --pile-inset: 16px;
  --pile-step: calc((100% - 2 * var(--pile-inset) - var(--pile-w)) / 7);
  /* The left column: a seat plaque plus a 20px gap. Every centred thing on the
     surface offsets by this same gutter, so one axis runs through the hands,
     the count, the played row and the dialogue. The plaque is sized FROM this
     token rather than the other way round (see .play-chip), so there is still
     only one number to change.

     Capped the same way --card-w is: 12.8vw + 57.6px is the line through
     (1300px, 224px), so the gutter is 204 + 20 at every desktop width and
     narrows to 155.9 at the 768px floor (W-0082). It falls to 0 on a phone,
     which is the switch game.js reads to dock the crib (§Responsive). */
  --play-gutter: clamp(148px, 12.8vw + 57.6px, 224px);

  position: relative;
  display: flex;
  gap: 24px;
  padding: 28px;
  border-radius: 20px;
  background: linear-gradient(var(--play-felt-top), var(--play-felt-bottom));
  box-shadow: var(--play-shadow-lift);
  color: var(--play-ink);
  font-family: var(--play-font-ui);
  overflow: hidden;
}

/* The positioning context for the two pinned seat chips. It is the area and not
   the surface on purpose: below 1300px the score panel stacks above the area,
   and a chip pinned to the surface would land on top of it.

   The gap is 8 rather than 16 because the dialogue's own bottom margin is
   carrying the spacing instead (see .play__strip): the thing the game's voice
   has to stand in the middle of is the band between the crib well's slots and
   the reader's cards, and the crib is out of the flow, so no gap here can see
   where it ends. A uniform 8 plus the strip's 44 is what lands it — 52px above
   the words, 52px below the buttons (W-0079). Below 768px the seat plaques
   rejoin the flow between the dialogue and the hand, which is a different
   budget and keeps the 16 (§Responsive). */
.play__area {
  position: relative;
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* A hand row is exactly one card tall and holds nothing but the hand. The left
   gutter is the pinned chip's 204px plus a 20px gap; the hand centres in what
   is left, which is what the seat chips being out of flow buys. */
.play__row {
  flex: none;
  display: flex;
  height: var(--card-h);
  padding-left: var(--play-gutter);
}

.play__hand {
  flex: 1;
  min-width: 0;
  display: flex;
  justify-content: center;
  gap: var(--hand-gap);
}

/* The fan. Six cards do not lie flat beside a seat chip at the design width, and
   the alternative — shrinking the cards to fit — changes the row height with the
   count, which is the thing this layout exists to prevent. So the cards keep
   their size and overlap instead: the margin is whatever it takes for six cards
   plus five gaps to land inside the track, and clamps to zero the moment they
   already fit. Only a hand of five or more can overflow, so only a hand of five
   or more takes the rule — the four-card hands of the play stay flat and
   gapped. `:nth-last-child(n + 5):first-child` is the count test: it matches the
   first card only when four more follow it. */
.play__hand > :nth-last-child(n + 5):first-child ~ .play-card {
  margin-left: min(0px, calc((100% - 6 * var(--card-w) - 5 * var(--hand-gap)) / 5));
}
/* Overlap makes paint order visible: DOM order stacks each card over the one
   before it — leftmost at the bottom, rightmost on top — and that order is
   invariant: hovering or selecting a card raises and lights it, never re-stacks
   it (the app's rule: selection is elevation only). The one exception is
   keyboard focus, because a focus ring half-covered by the next card is an
   accessibility regression, and focus is not hover. */
.play__hand > .play-card {
  position: relative;
}
.play__hand > .play-card--slot:focus-visible {
  z-index: 1;
}

/* A slot the deal has not reached yet. All six are in the row before the first
   card leaves the deck, so the centring above — and the overlap margin, which
   engages for the whole row at once at five cards — is settled before anything
   lands, and no card that has landed ever moves again. `visibility` is the only
   hide that does this: the box keeps its place in the flex line and still
   measures, which is what motion.fly needs to aim a flight at it. Scoped to the
   hand's own children because a human card flies as a deep clone of the slot it
   is heading for; the clone carries this class but lives in the motion layer,
   where the rule cannot reach it, so it stays visible in the air. */
.play__hand > .is-unrevealed {
  visibility: hidden;
}

/* Seat chips are pinned to the corners of the area, so no game state can move
   them: opponent top-left, you bottom-left, both against the surface's own 28px
   padding. Below 768px they go back into the flow — see §Responsive. */
.play-chip--bot,
.play-chip--you {
  position: absolute;
  left: 0;
}
.play-chip--bot {
  top: 0;
}
.play-chip--you {
  bottom: 0;
}

/* Mono labels. Letter-spacing is grid-exempt (docs/DESIGN.md §Type). */
.play__label {
  font-family: var(--play-font-mono);
  font-weight: 500;
  font-size: 12px;
  line-height: 16px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--play-lead);
}
.play__label--accent {
  color: var(--play-blue);
}

/* Mute. It lives in the panel foot beside New game, not floating over the felt:
   a corner-pinned control lands on the score tiles at wide widths and on the
   compact score strip at narrow ones. */
.play__mute {
  position: relative;
  flex: none;
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid var(--play-seat-off);
  border-radius: var(--play-radius-pill);
  background: var(--play-well);
  color: var(--play-ink);
  font-size: 18px;
  cursor: pointer;
}
.play__mute:hover:not(:disabled) {
  background: rgba(243, 249, 255, 0.12);
}
.play__mute:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
/* Muted: the note dims and takes a slash across it. The state has to read from
   the glyph itself, because the button's own dimming is what "disabled" says.
   The slash is a stroke, which the 4pt rule exempts. */
.play__mute.is-muted > span {
  opacity: 0.55;
}
.play__mute.is-muted::after {
  content: '';
  position: absolute;
  width: 24px;
  height: 2px;
  border-radius: var(--play-radius-pill);
  background: currentcolor;
  transform: rotate(-45deg);
}

/* ---------- Game speed ----------
   Three words on their own line of the panel foot (W-0074). Words and not
   icons because this page's readers skew older than the app's; a line of their
   own because three 44px targets plus New game and mute want about 500px, and
   the panel column is 340.

   The pressed state is drawn from `aria-pressed` rather than from a class, so
   the thing a screen reader is told and the thing a reader is shown cannot
   drift apart — the mute button keeps its own class only because its state is
   a slash drawn across a glyph. It reads as a fill and not just a brighter
   tint: on a dark felt a tint alone does not say "this is the live one". */
/* The phone's face for that same control (W-0084): one button saying which of
   the three is live, opening the group as a sheet. It ships hidden and only the
   phone rung turns it on — everywhere else the three words are already on the
   row, and a trigger would be a second way to say the same thing. */
.play__speed-trigger {
  display: none;
}
/* The trigger's own caption. The phone's control row has one line for the game
   speed and the live value both, so the trigger says which setting this is
   above the value it holds (W-0087). Off everywhere the three words are already
   on the row and the trigger itself is hidden. */
.play__speed-cap {
  display: none;
}
.play__speed-backdrop {
  display: none;
}

.play__speed {
  /* The whole width of the foot, which is what puts it on the second line.
     This group is also the phone's sheet (W-0084, §Responsive), but it carries
     no [popover] attribute any more (W-0104), so there is no UA styling to
     undo here — the eleven-line reset this rule used to need went with it. */
  flex: 1 1 100%;
  display: flex;
  gap: 8px;
}
.play__speed-opt {
  /* Equal thirds that can give width back: at 320px the track is narrower than
     the three words' natural widths and the padding is what yields first. */
  flex: 1 1 0;
  min-width: 0;
  min-height: 44px;
  padding: 8px 4px;
  border: 1px solid var(--play-seat-off);
  border-radius: var(--play-radius-pill);
  background: var(--play-well);
  color: var(--play-lead);
  font-family: var(--play-font-ui);
  font-weight: 600;
  font-size: 14px;
  line-height: 20px;
  cursor: pointer;
}
.play__speed-opt:hover:not(:disabled) {
  background: rgba(243, 249, 255, 0.12);
  color: var(--play-ink);
}
/* The page's focus idiom, and the z-index for the page's reason: a ring set
   4px clear of its button would otherwise be drawn under the neighbour the
   8px gap puts right beside it. */
.play__speed-opt:focus-visible {
  position: relative;
  z-index: 1;
  outline: 2px solid var(--play-seat-on);
  outline-offset: 4px;
}
.play__speed-opt[aria-pressed='true'] {
  border-color: var(--play-seat-on);
  background: rgba(243, 249, 255, 0.16);
  color: var(--play-ink);
}
.play__speed-opt:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* ---------- Seat chips ----------
   Both seats render from this one block so they cannot drift: 204×92 at the
   design width, a 56px avatar ringed in the seat colour, name, and a mono
   DEALER / PONE line. The active seat takes a near-white border and a soft glow
   while keeping its seat tint; the inactive seat sits on a faint border over
   near-transparent fill. The handoff's 17px name was normalised to 16px on the
   grid (W-0056).

   The width is the gutter less its 20px gap rather than an authored 204, so the
   plaque and the column it stands in cannot drift apart as the window narrows
   (W-0082) — one token moves both. The avatar has its own line because it is
   the one part that would read as a bug at plaque scale: it caps at 56 from
   ~1267px up, so every desktop width keeps today's ring. */
.play-chip {
  flex: none;
  display: flex;
  align-items: center;
  gap: 16px;
  width: calc(var(--play-gutter) - 20px);
  height: 92px;
  padding: 16px;
  border: 2px solid var(--play-seat-off);
  border-radius: var(--play-radius-card);
  background: rgba(243, 249, 255, 0.04);
  transition:
    border-color 0.16s ease,
    box-shadow 0.16s ease,
    background 0.16s ease,
    opacity var(--play-dur-furniture) var(--play-ease-arrive);
}
.play-chip__avatar {
  flex: none;
  width: clamp(40px, 3vw + 18px, 56px);
  height: clamp(40px, 3vw + 18px, 56px);
  border-radius: var(--play-radius-pill);
  border: 2px solid var(--play-seat-off);
  background: var(--play-felt-top);
  object-fit: cover;
}
.play-chip__text {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.play-chip__name {
  font-weight: 600;
  font-size: 16px;
  line-height: 24px;
  color: var(--play-ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.play-chip__role {
  font-family: var(--play-font-mono);
  font-weight: 500;
  font-size: 12px;
  line-height: 16px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  white-space: nowrap;
  color: rgba(196, 213, 230, 0.8);
}
/* The chip's own readout, and the phone's only one: the board panel's score
   tiles are off screen at that rung, so each seat tile carries its number under
   its name (W-0084). Hidden at every width that shows the tiles, which is every
   other one — renderScores writes both pairs and play.css picks.

   Mono where the tiles are the display serif: a 20px numeral inside an 80px
   tile is a readout and not a headline, and the tabular figures are the tiles'
   own convention, so a score never shifts on the way from 9 to 10. */
.play-chip__score {
  display: none;
  font-family: var(--play-font-mono);
  font-weight: 700;
  font-size: 20px;
  line-height: 24px;
  font-variant-numeric: tabular-nums;
  color: var(--play-ink);
}
.play-chip--you .play-chip__avatar {
  border-color: var(--play-red);
}
.play-chip--bot .play-chip__avatar {
  border-color: var(--play-blue);
}
.play-chip.is-active {
  border-color: var(--play-seat-on);
  box-shadow: 0 0 18px rgba(243, 249, 255, 0.28);
}
.play-chip--you.is-active {
  background: rgba(196, 56, 46, 0.18);
  box-shadow:
    0 0 0 4px rgba(196, 56, 46, 0.28),
    0 0 18px rgba(243, 249, 255, 0.28);
}
.play-chip--bot.is-active {
  background: rgba(95, 164, 245, 0.16);
  box-shadow:
    0 0 0 4px rgba(95, 164, 245, 0.26),
    0 0 18px rgba(243, 249, 255, 0.28);
}
.play-chip--you.is-active .play-chip__avatar {
  box-shadow: 0 0 12px var(--play-red);
}
.play-chip--bot.is-active .play-chip__avatar {
  box-shadow: 0 0 12px var(--play-blue);
}
.play-chip.is-active .play-chip__role {
  color: var(--play-ink);
}

/* ---------- Cards ----------
   A card is exactly --card-w by --card-h and never shrinks: it is the same size
   whether the hand holds six or one, which is what keeps a row's height a
   constant. Hands that cannot lay their cards flat overlap them instead (see the
   fan rule above). `aspect-ratio` is inert while both dimensions are set and is
   kept for the starter fan, which sizes from its height alone. */
.play-card {
  flex: none;
  display: block;
  width: var(--card-w);
  height: var(--card-h);
  aspect-ratio: 1 / 1.375;
  padding: 0;
  border: 0;
  /* The art's own corner, not a site radius: those percentages are the FRAME
     constants in cards.js (radius 24 on a 108 x 148 card). A horizontal
     percentage resolves against the box width and a vertical one against its
     height, so with the SVG's preserveAspectRatio="none" filling the box the
     CSS corner sits exactly on the drawn corner at every card size. Any
     decoration on a card element — shadow, ring, glow — must follow this
     radius; a fixed px value paints outside the art and shows a light shell. */
  border-radius: calc(100% * 24 / 108) / calc(100% * 24 / 148);
  background: none;
  box-shadow: var(--play-shadow-card);
}
/* The no-JS placeholder's paint only: a runtime back is a self-contained SVG
   that draws its own field. Kept on `span` because a CSS border on a replaced
   element shrinks the art's box by 1px and then paints outside its corners. */
span.play-card--back {
  background:
    repeating-linear-gradient(
      45deg,
      rgba(243, 249, 255, 0.08) 0 8px,
      rgba(243, 249, 255, 0) 8px 16px
    ),
    linear-gradient(160deg, #1d3358, #0a1730);
  border: 1px solid var(--play-hairline);
}
.play-card__face {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: inherit;
  overflow: hidden;
}
.play-card--slot {
  cursor: pointer;
  transition:
    transform 0.16s ease,
    filter 0.16s ease,
    box-shadow 0.16s ease;
}
/* Hover is a subtle raise and glow, never a re-stack. `:not(.is-selected)`
   matters: this selector out-specifies .is-selected, and without it hovering a
   selected card would drop it from -14px to -8px and swap its red ring for the
   white glow.

   TWO GATES, because a raised card is a claim (W-0098). Nothing here recommends
   a play — the human hand has no advice path at all — but during the play the
   cards that would take the count past 31 sit at brightness(0.5), so a single
   LEGAL card raised and glowing beside them reads as the table naming it. The
   lift is only honest when a pointer is actually resting on the card, and both
   gates exist because a lift can arrive without one:

   (hover: hover) and (pointer: fine) — a tap leaves :hover latched on the card
   that was tapped, with nothing to move it off. On a phone one card simply
   stays up. The phone's .is-selected override further down already assumed this
   query was here ("there is no hover state under it to out-rise"); it never was.

   :where(.play__hand:not(.is-recentred)) — the hand is centred, so it re-centres
   every time it shrinks, sliding a new card under a cursor that never moved.
   game.js sets .is-recentred on each rebuild and the next real pointermove
   clears it, so the lift waits for a gesture. `:where()` keeps this at zero
   specificity: the reduced-motion override at the foot of the file cancels this
   transform by source order at EQUAL weight, and would lose if the selector
   gained any. */
@media (hover: hover) and (pointer: fine) {
  :where(.play__hand:not(.is-recentred))
    > .play-card--slot:hover:not(:disabled):not(.is-illegal):not(.is-selected) {
    transform: translateY(-8px);
    box-shadow:
      0 0 0 1px var(--play-seat-on),
      0 0 16px rgba(243, 249, 255, 0.35),
      var(--play-shadow-card);
  }
}
.play-card--slot:focus-visible {
  outline: 2px solid var(--play-seat-on);
  outline-offset: 4px;
}
.play-card--slot.is-selected {
  transform: translateY(-14px);
  box-shadow:
    0 0 0 2px var(--play-red),
    var(--play-shadow-card);
}
/* A card that would take the count past 31: a 50% black FILL over it, and never
   a lowered opacity (W-0089). These cards overlap by design — the fan slides
   each one under the next — and a translucent card shows the card BEHIND it
   through its own face, so the rank and pips of the one underneath read
   straight through the one in front. That is what the owner caught: two cards
   dimmed beside each other stopped being two cards.

   brightness() rather than an ::after layer because on an opaque card the two
   composite to the same pixels — a black fill at 50% over a source is
   `src x 0.5 + 0 x 0.5`, which is exactly what `brightness(0.5)` computes — and
   the filter dims the cream rim and the drop shadow along with the face, which
   an overlay clipped to the card's box cannot. It is also the idiom already on
   the felt: .play-card--pile.is-spent greys a finished set the same way. */
.play-card--slot.is-illegal {
  filter: brightness(0.5);
  cursor: not-allowed;
}
.play-card--slot:disabled {
  cursor: default;
}
/* The deal's sort pass: game.js FLIPs each card from its dealt position to its
   rank-sorted one on transform alone. Declared after the base rule so this
   transition wins while the class is on. */
.play-card--slot.is-sorting {
  transition: transform 0.25s ease;
  will-change: transform;
}
/* An empty slot in the inert shell: a hairline ghost, not a blank rectangle. */
.play-card--slot:disabled .play-card__face:empty {
  border: 1px dashed var(--play-hairline);
  background: rgba(243, 249, 255, 0.03);
}

/* ---------- Centre row: starter, count, pile, status ---------- */
/* The count column at its tallest: label 16, gap 16, numeral 72, gap 16, and a
   pile that is a card less 44. Everything above the pile is type and holds
   still, so the sum is a card's height plus 76 — 230 at the design width, and
   the same arithmetic at every narrower one (W-0082). The starter column is
   the fan plus a 16 gap and a 16 label, and is pinned out of the row entirely
   (see .play__starter). Neither has a state that changes either number. */
.play__centre {
  flex: none;
  height: calc(var(--card-h) + 76px);
  display: grid;
  /* First column + the 24px gap = the gutter, so the count column starts on
     the same axis as the hands and the dialogue strip. */
  grid-template-columns: calc(var(--play-gutter) - 24px) minmax(0, 1fr);
  gap: 24px;
  align-items: center;
}

/* The starter cluster is pinned to the middle of the play area rather than
   riding its cell in the row, so the deck — and the card cut off it — sits
   exactly between the two seat plaques, which are themselves pinned to the
   corners. Same idiom as the chips, and out of the flow for the same reason
   that is safe: nothing here can change a height (W-0068). It leaves the
   centre row's first column empty, which costs nothing — that column is the
   gutter spacer, and it is an explicit track, so the count column beside it
   does not move. The travelling deck follows on its own: placeDeck measures
   the fan's middle card wherever it has ended up.

   Below 768px the plaques rejoin the flow and there are no corners to sit
   between, so this goes back into its cell with them (§Responsive). */
.play__starter {
  position: absolute;
  left: 0;
  top: 50%;
  width: calc(var(--play-gutter) - 24px);
  transform: translateY(-50%);
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}
/* The cluster is drawn in ratios of a card rather than in authored pixels, the
   same bargain --play-deck-w makes: the deck a card is cut off has to read the
   same size beside the hand at every width the surface runs at. 19/14, 10/7 and
   17/14 of a card are exactly the 152, 160 and 136 the design width was drawn
   at, so a desktop is untouched and a tablet is the same picture, smaller
   (W-0082). Written as fractions and not decimals so the identity is exact and
   not merely close. The DECK_FAN offsets in game.js are already percentages of
   this box, so nothing in script has to know. */
.play__starter-fan {
  position: relative;
  width: calc(var(--play-starter-w) * 19 / 14);
  height: calc(var(--play-starter-w) * 10 / 7);
}
/* The fan sizes from its height, so `width: auto` and the base `aspect-ratio`
   between them give a starter card its width. */
.play-card--starter {
  position: absolute;
  left: 0;
  top: 0;
  height: calc(var(--play-starter-w) * 17 / 14);
  width: auto;
}

/* The caption and the numeral over the pile. Nothing at all above the phone
   rung: the count column lays out as one flex column of three, exactly as it
   always has, and this wrapper only becomes a box where the pile has to leave
   the column and take the whole screen width (W-0087, §Responsive). */
.play__count-head {
  display: contents;
}

/* Named rather than auto-placed: the starter beside it is out of the flow, so
   this is the row's only in-flow item and auto-placement would drop it into
   the gutter track. */
.play__count {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}
/* The caption belongs to the number under it, not to the deck above it, and at
   the column's uniform 16 it read as the deck's. So it drops 8px onto the
   numeral and hands the 8 back — the margins cancel, the numeral and the pile
   below it do not move, and the column is still exactly 230. */
.play__count .play__label {
  margin-top: 8px;
  margin-bottom: -8px;
}
.play__count-num {
  font-family: var(--play-font-display);
  font-weight: 400;
  font-size: 80px;
  line-height: 72px;
  font-variant-numeric: tabular-nums;
  color: var(--play-ink);
}
/* A fixed height, not a floor: the pile is empty between rounds and holds five
   cards during the show, and the count column may not move either way.
   --pile-w is a card less 32, so its 1.375 height is a card's height less 44. */
.play__pile {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  height: calc(var(--card-h) - 44px);
}
/* Pile cards overlap by design, so they keep a fixed size and do not shrink. */
.play-card--pile {
  width: var(--pile-w);
  height: calc(var(--pile-w) * 1.375);
  /* Smaller card, smaller shadow — the app's cardMd. */
  box-shadow: var(--play-shadow-card-sm);
  margin-left: -24px;
}
.play-card--pile:first-child {
  margin-left: 0;
}
/* Pegging lays into 8 fixed slots instead of the centred row: a card's place
   depends only on its landing index (--i, written by renderPile), never on
   later plays, so nothing already on the table ever moves. Grayed sets stay
   put and the next set continues rightward — the app's stackSlotCenter rule. */
.play__pile.is-slots {
  position: relative;
  width: 100%;
}
.play__pile.is-slots .play-card--pile {
  position: absolute;
  bottom: 0;
  left: calc(var(--pile-inset) + var(--i) * var(--pile-step));
  margin-left: 0;
}
/* A finished set, greyed in place. brightness(0.4) is a black wash at the
   app's overlayPlayed 0.6, and unlike an overlay it also dims the cream rim
   and the shadow. The wash eases, but nothing moves: a landed card's position
   is fixed for the rest of the hand (W-0069). renderPile rebuilds these cards
   already grey, so game.js starts them clear for one frame to give the
   transition something to run from. */
.play-card--pile.is-spent {
  filter: brightness(0.4);
  transition: filter var(--play-dur-grey) linear;
}

/* ---------- The dialogue strip ----------
   What the game is saying, and the buttons it is saying it about, in one fixed
   box between the centre and the reader's cards. Requirement: every word and
   every control renders ABOVE the hand, and nothing below it. 28 + 8 + 44 = 80,
   whichever of the two buttons is showing, or neither.

   The 44px bottom margin is what CENTRES the dialogue in the band the reader is
   actually looking at during the discard (W-0079). The crib well is out of the
   flow, so the flow cannot see it: its slots end 44px above the bottom of the
   centre row, and 44 + the area's 8px gap on either side of this box puts 52px
   of felt above the words and 52px below the buttons. A margin and not extra
   height on purpose — the box stays exactly what it holds, so nothing here has
   to know how the contents are aligned inside it. `center` rather than the old
   `flex-end` for that same reason: the two agree today, and if either the box
   or its contents ever changes, concentric is the answer that stays right. */
.play__strip {
  position: relative; /* the score pill hangs off this box */
  flex: none;
  height: 80px;
  margin-bottom: 44px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  /* Same gutter as the hand rows and the count column: the words and the
     buttons centre on the play area's one axis, not on the surface. */
  padding-left: var(--play-gutter);
}

/* The single place turn state is spelled out in words. Everything else on the
   surface signals turn with light — so when this line does speak, it is saying
   the thing the reader cannot work out from the felt, and 20/28 is the size that
   admits it (W-0079). One line at every width the desktop layout runs at; the
   phone reserves two and stays at 16 (see §Responsive). */
/* THE VOICE ROW (W-0088). One box, holding the words and the score pill, which
   never both have it: a pill takes the row and hands it back when the last chip
   has left. Its height is the line's height at every rung and the pill lies
   absolutely inside it, so a score can never move the reader's cards (W-0068).
   The 32px chip overhangs this 28px box by two either side, into the strip's own
   8px gap, which holds nothing. */
.play__voice {
  position: relative;
  flex: none;
  width: 100%;
  height: 28px;
}

.play__status {
  margin: 0;
  height: 100%;
  font-size: 20px;
  line-height: 28px;
  text-align: center;
  text-wrap: pretty;
  color: var(--play-lead);
}
/* game.js writes exactly one of these, whatever the line is. It exists because
   .play__status is a flex box at the narrow rungs, a flex box makes an anonymous
   item of every run of text, and an anonymous item loses its trailing space —
   which is what ate the space before the crib's owner. One item, one inline
   flow, nothing to trim. */
.play__status-line {
  display: inline;
}
/* The crib's owner, picked out: the one word on the felt a reader has to READ
   rather than glance at, because a discard sent to the wrong crib cannot be
   taken back. Inherits everywhere the sentence fits on one line; the phone
   weights it (§Responsive). */
.play__status-em {
  font-weight: inherit;
}

/* Off screen, and the game's one voice for a reader who cannot see the felt
   (W-0088). The row above says only the handful of things the app's row says;
   everything the table used to spell out is announced here instead. Same clip
   idiom as site.css's .skip-link and .nav-toggle__sr. */
.play__announce {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.play__actions {
  display: flex;
  justify-content: center;
  gap: 16px;
  height: 44px;
}

/* ---------- The score pill ----------
   What just pegged, as a number: "+2 - FIFTEEN". It fills .play__voice rather
   than floating over the buttons (W-0088, superseding W-0072's placement): the
   pill and the words are the same announcement in two registers, so they stand
   in the same place and take turns. Still absolute, and for the original
   reason — the strip's height is part of the layout guarantee (W-0068) and a
   pill that took up room would move the reader's hand every time somebody
   scored. Still silent, too: the hidden region has already said it in words.

   pill.js writes the chips; it never writes a style. One at a time in normal
   play, but a leaving chip and its replacement stack for up to 100ms during a
   preempt, which is why the chip and not the slot is the absolute one. */
.play-pill {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 2;
}
.play-pill__chip {
  position: absolute;
  height: 32px;
  display: inline-flex;
  align-items: center;
  padding: 0 20px;
  border: 0.5px solid var(--play-blue);
  border-radius: var(--play-radius-pill);
  background: var(--play-pill-fill);
  box-shadow: var(--play-shadow-pill);
  font-family: var(--play-font-mono);
  font-weight: 500;
  font-size: 14px;
  line-height: 24px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--play-pill-text-bot);
  transition:
    opacity var(--play-dur-pill) var(--play-ease-arrive),
    transform var(--play-dur-pill) var(--play-ease-arrive);
}
/* Coloured by whoever scored, not by whose turn it is. */
.play-pill__chip--you {
  border-color: var(--play-red);
  color: var(--play-pill-text-you);
}
.play-pill__chip.is-entering {
  opacity: 0;
  transform: scale(0.92);
}
.play-pill__chip.is-leaving {
  opacity: 0;
  transform: translateY(-8px);
}
.play-pill__chip.is-fast {
  opacity: 0;
  transition-duration: var(--play-dur-pill-fast);
}

/* ---------- Buttons ---------- */
.play-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 12px 24px;
  border: 0;
  border-radius: var(--play-radius-pill);
  font-family: var(--play-font-ui);
  font-weight: 600;
  font-size: 16px;
  line-height: 20px;
  text-decoration: none;
  cursor: pointer;
}
.play-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
.play-btn--primary,
.play-btn--cta {
  background: var(--play-red);
  color: #ffffff;
  box-shadow: var(--play-shadow-cta);
}
.play-btn--primary:hover:not(:disabled),
.play-btn--cta:hover {
  background: var(--play-red-hover);
}
.play-btn--ghost {
  background: transparent;
  border: 1px solid rgba(243, 249, 255, 0.35);
  color: var(--play-ink);
}
.play-btn--ghost:hover:not(:disabled) {
  background: rgba(243, 249, 255, 0.1);
}
/* The end-of-game pair, and the felt's only blue controls (W-0104). Every other
   button on this site is red, deliberately — the game reversed the app's seat
   colours (W-0067 §6) and red is the reader's. This exception is scoped to the
   post-game modal and to nothing else: it is the one fixture drawn from the
   app's own end-game screen rather than from the site's palette, and its blue,
   its crown and its wreath all come from there together. Never reach for these
   two outside .play-modal.

   The fill and the inset pair are the app's LargeButton verbatim: dark shade
   below, light above, which is what gives the primary its pressed-metal face.
   The radius is the felt's pill rather than the app's 20, so the modal's CTA
   is the same shape as every other button on this page. */
.play-btn--blue {
  min-height: 56px;
  padding: 12px 32px;
  background: var(--play-blue-fill);
  color: #ffffff;
  font-size: 18px;
  line-height: 24px;
  box-shadow:
    inset 0 -4px 12px rgba(0, 0, 0, 0.25),
    inset 0 8px 4px rgba(255, 255, 255, 0.15);
}
.play-btn--blue:hover:not(:disabled) {
  background: var(--play-blue-fill-hover);
}
.play-btn--outline {
  min-height: 56px;
  padding: 12px 32px;
  border: 1px solid var(--play-blue-fill);
  background: var(--play-blue-wash);
  color: var(--play-ink);
  font-size: 18px;
  line-height: 24px;
}
.play-btn--outline:hover:not(:disabled) {
  background: var(--play-blue-wash-hover);
}
.play-btn--pill {
  flex: none;
  padding: 12px 20px;
  background: transparent;
  border: 1px solid rgba(243, 249, 255, 0.35);
  color: var(--play-ink);
  font-family: var(--play-font-mono);
  font-weight: 500;
  font-size: 12px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}
.play-btn--pill:hover:not(:disabled) {
  background: rgba(243, 249, 255, 0.1);
}

/* ---------- Board panel ---------- */
/* The panel is the third column and the board's only home, so it narrows with
   the window rather than ever being taken away: 24.8vw + 49.6px is the line
   through (1300px, 372px), and it bottoms out at 240 — the width the board's
   300×620 viewBox is still legible in — at the 768px floor (W-0082). */
.play__panel {
  flex: none;
  width: clamp(240px, 24.8vw + 49.6px, 372px);
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding: 20px 16px;
  border: 1px solid var(--play-hairline);
  border-radius: var(--play-radius-card);
  background: var(--play-well);
}
.play__scores {
  display: flex;
  gap: 12px;
}
.play-score {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 12px 16px;
  border-radius: var(--play-radius-figure);
}
.play-score--you {
  background: rgba(196, 56, 46, 0.14);
  border: 1px solid rgba(196, 56, 46, 0.45);
}
.play-score--bot {
  background: rgba(95, 164, 245, 0.12);
  border: 1px solid rgba(95, 164, 245, 0.4);
}
.play-score__label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--play-font-mono);
  font-weight: 500;
  font-size: 12px;
  line-height: 16px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--play-ink);
}
.play-score__dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: var(--play-radius-pill);
}
.play-score--you .play-score__dot {
  background: var(--play-red);
}
.play-score--bot .play-score__dot {
  background: var(--play-blue);
}
.play-score__num {
  font-family: var(--play-font-display);
  font-weight: 400;
  font-size: 40px;
  line-height: 44px;
  font-variant-numeric: tabular-nums;
  color: var(--play-ink);
}

/* New game and mute on one line, the speed control on the next: .play__speed
   asks for the full width, so the wrap is what places it rather than a second
   container. */
.play__panel-foot {
  flex: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
}
.play__panel-foot .play-btn--pill {
  flex: 1;
}

/* board.js mounts a 300×620 viewBox SVG here; it scales to the height left. */
.play__board {
  position: relative;
  flex: 1;
  min-height: 0;
}
.play__board > svg {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

/* ---------- Opponent select ----------
   The page's initial state, so it ships visible and inert. */
.play-select {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 32px;
  background: var(--play-scrim);
  /* The surface is a fixed height and clips its overflow, so the safety valve
     for a short window has to be a scroll rather than a taller surface. */
  overflow-y: auto;
}
@supports (backdrop-filter: blur(4px)) {
  .play-select {
    backdrop-filter: blur(4px);
  }
}
.play-select__panel {
  margin-block: auto;
  width: min(640px, 100%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
  text-align: center;
}
/* Two lines, one message: the mono eyebrow says what the page is, the line
   under it says what to do next. The eyebrow keeps the old label's size — it is
   the quiet half — and the instruction carries the weight. */
.play-select__heading {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}
.play-select__eyebrow {
  font-family: var(--play-font-mono);
  font-weight: 500;
  font-size: 12px;
  line-height: 16px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--play-lead);
}
.play-select__title {
  font-family: var(--play-font-ui);
  font-weight: 600;
  font-size: 24px;
  line-height: 32px;
  color: var(--play-ink);
}
.play-select__bots {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 16px;
}
.play-select__bot {
  width: 192px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 24px 16px;
  border: 1px solid var(--play-hairline);
  border-radius: var(--play-radius-card);
  background: rgba(243, 249, 255, 0.06);
  color: var(--play-ink);
  font-family: var(--play-font-ui);
  cursor: pointer;
  transition:
    border-color 0.16s ease,
    background 0.16s ease;
}
.play-select__bot:hover:not(:disabled) {
  border-color: var(--play-seat-on);
  background: rgba(243, 249, 255, 0.12);
}
.play-select__bot:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
.play-select__avatar {
  width: 88px;
  height: 88px;
  border-radius: var(--play-radius-pill);
  border: 2px solid var(--play-blue);
  background: var(--play-felt-top);
  object-fit: cover;
}
.play-select__name {
  font-weight: 700;
  font-size: 20px;
  line-height: 28px;
}
.play-select__level {
  font-family: var(--play-font-mono);
  font-weight: 500;
  font-size: 16px;
  line-height: 20px;
  /* Tracking eases off as the size goes up: 0.18em was set for 12px, and at
     16px the same ratio pushes MEDIUM past the tile. */
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--play-lead);
}

/* The no-JS note, the calculator's .hc__nojs move: a reader without JavaScript
   sees an explained, inert board and two links out, never a dead table. */
.play__nojs {
  margin: 0;
  max-width: 46ch;
  font-size: 16px;
  line-height: 24px;
  color: var(--play-lead);
}
.play__nojs a {
  color: var(--play-blue);
}

/* ---------- Flight layer ----------
   Where cards travel. motion.js builds a ghost here at its DESTINATION box and
   animates it back from its source, so the layer holds nothing between
   flights and holding nothing is its resting state. Absolute and inset to the
   surface's padding box: every source and every destination on the table is
   inside .play, so a flight never meets the surface's overflow clip. It sits
   over the felt and under the opponent select (z-index 4). */
.play-flight {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}
.play-flight .play-ghost {
  position: absolute;
  will-change: transform;
}
/* Shadow off in the air, on at landing — the shadow belongs to the table, not
   to the card. The real element under the ghost carries it the whole time. */
.play-flight .play-ghost,
.play-flight .play-ghost * {
  box-shadow: none !important;
}

/* ---------- The travelling deck ----------
   Three composed backs that stand in for the deck during the deal and the
   discard, when the in-flow starter fan is faded out (see §Stages). Out of the
   flow like the flight layer, and positioned by game.js from measured rects:
   it is decoration, so nothing in the layout may depend on where it lands. */
.play-deck {
  position: absolute;
  z-index: 2;
  width: var(--play-deck-w);
  height: calc(var(--play-deck-w) * 1.375);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--play-dur-chrome) var(--play-ease-exit);
}
.play-deck.is-shown {
  opacity: 1;
  transition-timing-function: var(--play-ease-arrive);
}
/* Declared after .play-card so these win: a deck card fills the fixture rather
   than taking a hand card's size, and only the top one casts a shadow — three
   stacked shadows read as three cards floating, not as one deck. */
.play-deck__card {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  box-shadow: none;
}
.play-deck__card > svg {
  display: block;
  width: 100%;
  height: 100%;
}
.play-deck__card:nth-child(1) {
  transform: translate(-8px, 8px);
}
.play-deck__card:nth-child(2) {
  transform: translate(-4px, 4px);
}
.play-deck__card:nth-child(3) {
  box-shadow: var(--play-shadow-card-sm);
}
/* The riffle: seven diminishing wiggles about the deck's own centre, the app's
   shuffle. Amplitudes run 1, 1, 0.65, 0.65, 0.3, 0.3, 0 of 4px and 2deg. The
   transform is on the fixture, so the three cards' own offsets compose with it
   and the stack shuffles as one object. */
.play-deck.is-rifling {
  animation: play-deck-riffle var(--play-dur-riffle) ease-in-out;
}
@keyframes play-deck-riffle {
  0% {
    transform: translateX(0) rotate(0deg);
  }
  14.3% {
    transform: translateX(-4px) rotate(-2deg);
  }
  28.6% {
    transform: translateX(4px) rotate(2deg);
  }
  42.9% {
    transform: translateX(-2.6px) rotate(-1.3deg);
  }
  57.1% {
    transform: translateX(2.6px) rotate(1.3deg);
  }
  71.4% {
    transform: translateX(-1.2px) rotate(-0.6deg);
  }
  85.7% {
    transform: translateX(1.2px) rotate(0.6deg);
  }
  100% {
    transform: translateX(0) rotate(0deg);
  }
}

/* ---------- The crib ----------
   Where the four discards live between the discard and the show. Out of the
   flow like the deck and the flight layer, and for the same reason: it appears
   mid-hand, crosses the table and goes away, and the surface's height may not
   notice any of it (W-0068).

   Two homes. The WELL is a row of four slots on the play axis, over the count
   column's real estate, which the discard stage has no other use for; the DOCK
   is a tight fan of the four backs beside the dealer's own seat plaque, in the
   left gutter column, which is empty at every width that has one. The move
   between them is a single group FLIP (motion.js `flyGroup`) with the four
   backs travelling as one object.

   Both homes are positioned by game.js from measured rects, exactly as the
   travelling deck is: the responsive ladder moves the count column and the
   seat plaques, and a measurement always knows where they went. Nothing in the
   layout may depend on where this lands. */
.play-crib {
  position: absolute;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--play-dur-chrome) var(--play-ease-exit);
}
.play-crib.is-shown {
  opacity: 1;
  transition-timing-function: var(--play-ease-arrive);
}
/* Whose crib it is. White and bold at 14 rather than the surface's usual 12/500
   mono label, and no longer painted in the owner's seat colour (W-0079): this is
   the one caption on the felt the reader has to READ rather than glance at, and
   a discard sent to the wrong crib cannot be taken back. It deliberately no
   longer matches .play__label or .play-show__total-label — those name furniture,
   this names a consequence. The phone rung pins the size back to 12 (§Responsive):
   the caption is nowrap and the fixture is centred on its own measured width. */
.play-crib__label {
  font-family: var(--play-font-mono);
  font-weight: 700;
  font-size: 14px;
  line-height: 20px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--play-ink);
}
/* A slot is a card-shaped box that is either empty or holding a back, so it
   takes the card art's own corner (see .play-card) rather than a site radius. */
.play-crib__slot {
  display: block;
  border-radius: calc(100% * 24 / 108) / calc(100% * 24 / 148);
}
.play-crib__slot > svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* The well: pile-sized slots, so a discard lands at the size a played card
   lands at. An empty one is the shell's own hairline ghost, not a hole. */
.play-crib--centre .play-crib__slots {
  display: flex;
  gap: 12px;
}
.play-crib--centre .play-crib__slot {
  width: var(--pile-w);
  height: calc(var(--pile-w) * 1.375);
}
.play-crib--centre .play-crib__slot:not(.is-filled) {
  border: 1px dashed var(--play-hairline);
  background: var(--play-well);
}

/* The dock. The four backs are absolutely placed inside an 84 x 72 box from
   one index each (`--i`, written once when the slots are built), so the whole
   fan — 12px apart, tilting -9 / -3 / +3 / +9 — is three declarations rather
   than eight. The heights are the card ratio (48 x 1.375 = 66), which the 4pt
   grid exempts.

   No caption. Docked, the crib is standing against its owner's own seat
   plaque, which has already said whose it is; and because game.js centres the
   FIXTURE on that plaque from its own measured width, a caption beside the
   backs is also what pushed the backs off centre. With the label gone the dock
   is exactly the 84-wide fan, and centring it centres what the reader sees.
   The well out on the play axis keeps its caption: nothing else there says
   whose crib it is. */
.play-crib--dealer-bot .play-crib__label,
.play-crib--dealer-you .play-crib__label {
  display: none;
}
.play-crib--dealer-bot .play-crib__slots,
.play-crib--dealer-you .play-crib__slots {
  position: relative;
  width: 84px;
  height: 72px;
}
.play-crib--dealer-bot .play-crib__slot,
.play-crib--dealer-you .play-crib__slot {
  position: absolute;
  top: 4px;
  left: calc(var(--i) * 12px);
  width: 48px;
  height: 66px;
  transform: rotate(calc(var(--i) * 6deg - 9deg));
  box-shadow: var(--play-shadow-card-sm);
}

/* ---------- The cut ceremony ----------
   The one moment of the game with a screen to itself: an opaque felt overlay
   over the whole surface, holding a headline, the two seats, an empty slot
   under each and a fan of card backs to press. Absolutely placed and out of
   the flow, which is what lets it exist at all (W-0068).

   It sits UNDER the opponent select (z-index 4) and under the flight layer
   (3), so the scrim can fade off a stage that is already standing and cards
   can fly over both. The background is the felt's own gradient rather than a
   scrim: the ceremony is not something laid over the table, it is what the
   table is doing.

   Open and closed are one class. The `hidden` attribute is the other half of
   it — stages.js drops it before opening so the fan can measure real boxes,
   and puts it back at the end of the dissolve. The visibility delay is what
   keeps a closed stage out of the tab order without cutting the fade short. */
.play-stage {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 32px;
  overflow: hidden;
  background: linear-gradient(var(--play-felt-top), var(--play-felt-bottom));
  opacity: 1;
  visibility: visible;
  transition: opacity var(--play-dur-stage) var(--play-ease-exit);
}
.play-stage:not(.is-open) {
  opacity: 0;
  visibility: hidden;
  transition:
    opacity var(--play-dur-stage) var(--play-ease-exit),
    visibility 0s var(--play-dur-stage);
}
.play-stage__head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  text-align: center;
}
.play-stage__headline {
  font-family: var(--play-font-display);
  font-weight: 400;
  font-size: 48px;
  line-height: 56px;
  color: var(--play-ink);
}
.play-stage__sub {
  font-family: var(--play-font-mono);
  font-weight: 500;
  font-size: 14px;
  line-height: 20px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--play-lead);
}

/* Two seats side by side, each with the slot its cut card lands in directly
   under its own plaque. The plaques are .play-chip outright — same box, same
   avatar ring, same compact treatment on a phone — minus the --you / --bot
   modifiers, which pin the real chips to the corners of the play area.

   They sit UNDER the deck here, and further under it than the headline sits
   over it: 16 from the stage's own gap above the deck, 40 below it once this
   margin is added to the same gap. The deck is what the reader is being asked
   to touch, so it keeps the headline close and pushes the outcome away. */
.play-stage__seats {
  display: flex;
  align-items: flex-start;
  gap: 24px;
  margin-top: 24px;
}
.play-stage__seat {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  transition: opacity 450ms linear;
}
/* The cut is settled: the seat that drew high dims, plaque and card together,
   and the one that deals stays lit. */
.play-stage__seat.is-loser {
  opacity: 0.55;
}
.play-stage__plaque--you .play-chip__avatar {
  border-color: var(--play-red);
}
.play-stage__plaque--bot .play-chip__avatar {
  border-color: var(--play-blue);
}
/* An empty slot is a hairline ghost at card size, on the card art's own
   corner (see .play-card); a full one is a card and needs neither. */
.play-stage__slot {
  position: relative;
  width: var(--card-w);
  height: var(--card-h);
  border-radius: calc(100% * 24 / 108) / calc(100% * 24 / 148);
}
.play-stage__slot:empty {
  border: 1px dashed var(--play-hairline);
  background: var(--play-well);
}

/* The deck to cut: one button holding all fifty-two backs, each absolutely
   placed from one transform (stages.js writes it), so a back leaving is a
   transform change on the ones that are left and the gap closes itself.

   One control, not fifty-two. A deck is a single thing to press — the reader
   chooses a place in it, not an item from a list — so the button carries the
   label and the focus ring, and the backs inside it are decoration a pointer
   happens to be able to aim at. The button is stripped back to its box: it
   has to be the strip, one card tall, with nothing of its own to paint. */
.play-stage__fan {
  position: relative;
  width: 100%;
  height: var(--card-h);
  display: block;
  appearance: none;
  border: 0;
  padding: 0;
  background: none;
  font: inherit;
  cursor: pointer;
}
.play-stage__fan:disabled {
  cursor: default;
}
.play-stage__fan:focus-visible {
  outline: 2px solid var(--play-seat-on);
  outline-offset: 4px;
}
.play-stage__back {
  position: absolute;
  top: 0;
  left: 50%;
  margin-left: calc(var(--card-w) / -2);
  transition: transform var(--play-dur-chrome) var(--play-ease-arrive);
}
.play-stage__back > svg {
  display: block;
  width: 100%;
  height: 100%;
}
/* Hover lights the back the pointer is over, which is how a deck this tightly
   overlapped says where the cut would land. Dead once the cut is taken: the
   deck is the button, so the button's disabled state is what turns it off. */
.play-stage__fan:not(:disabled) .play-stage__back:hover {
  box-shadow:
    0 0 0 1px var(--play-seat-on),
    0 0 16px rgba(243, 249, 255, 0.35),
    var(--play-shadow-card);
}

/* The cut card in the air, between the fan and the slot. The flight layer's
   ghost, one screen up: shadow off while it travels, the outer box carrying
   the travel and the inner one the pinch. */
.play-stage__flier {
  position: absolute;
  z-index: 1;
  pointer-events: none;
  will-change: transform;
}
.play-stage__flier,
.play-stage__flier * {
  box-shadow: none !important;
}
.play-stage__flier-inner {
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

/* ---------- The show ----------
   The hand count: three sections — pone's hand, dealer's hand, dealer's crib —
   on an opaque felt panel over the PLAY AREA only. Not the whole surface, the
   way the cut ceremony covers it: the board beside it is exactly what a reader
   wants to watch while somebody pegs eight points, so the panel stops at the
   area's edge and the panel gets an edge of its own to say so.

   Everything else is the ceremony's machinery, reused. Absolute and out of the
   flow, which is what lets it exist at all (W-0068); one `.is-open` class with
   the visibility delay behind it; `hidden` landing from a timer in show.js
   rather than from a transitionend.

   The height guarantee survives one flexible box, and there is exactly one:
   the rows list. A twelve-point crib scrolls its own list rather than growing
   the felt. Everything above and below it is fixed. */
.play-show {
  /* The starter is the smaller card, as a FRACTION of the hand's rather than an
     authored size, so the block follows --card-w down the responsive ladder. A
     ratio, and so grid-exempt like the 1.375 card proportion. */
  --play-show-starter-w: calc(var(--card-w) * 0.72);

  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 16px 24px;
  overflow: hidden;
  border: 1px solid var(--play-hairline);
  border-radius: var(--play-radius-figure);
  background: linear-gradient(var(--play-felt-top), var(--play-felt-bottom));
  opacity: 1;
  visibility: visible;
  transition: opacity var(--play-dur-stage) var(--play-ease-exit);
}
.play-show:not(.is-open) {
  opacity: 0;
  visibility: hidden;
  transition:
    opacity var(--play-dur-stage) var(--play-ease-exit),
    visibility 0s var(--play-dur-stage);
}

/* Three counts, three bars, filled cumulatively: where the hand has got to,
   without a word. The radius is the pill token rather than an authored 2 — on a
   4px bar the two draw the same shape and only one of them is on the grid. */
.play-show__notches {
  flex: none;
  display: flex;
  gap: 8px;
}
.play-show__notch {
  width: 24px;
  height: 4px;
  border-radius: var(--play-radius-pill);
  background: var(--play-seat-off);
}
.play-show__notch.is-done {
  background: var(--play-blue);
}

/* — THE SHOW — the mono caption between two hairlines, the app's own header. */
.play-show__divider {
  flex: none;
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  max-width: 480px;
}
.play-show__rule {
  flex: 1;
  height: 1px;
  background: var(--play-hairline);
}
.play-show__divider-text {
  font-family: var(--play-font-mono);
  font-weight: 500;
  font-size: 14px;
  line-height: 20px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--play-lead);
}

/* Everything that changes with the section, in one box: show.js fades this and
   nothing else between counts, so the notches and the divider hold still while
   the hand under them is replaced. */
.play-show__body {
  flex: 1;
  min-height: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}
.play-show__name {
  flex: none;
  font-family: var(--play-font-display);
  font-weight: 400;
  font-size: 28px;
  line-height: 36px;
  color: var(--play-ink);
}

/* The five cards of a count: the cut card on the left at three-quarter size
   with a face-down back peeking out from under it — that is how the panel says
   this one came off the deck rather than out of a hand — then the four the
   count is about, overlapped so four of them read as one hand. No radius is
   authored on any of these boxes: a card's corner is the art's own (W-0070,
   .play-card). */
.play-show__cards {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
}
.play-show__card {
  position: relative;
  /* The reveal ripple scales these boxes, and what is inside each one is a
     79-path SVG under a shadow with three more overlapping it. Promoted rather
     than re-rasterised every frame of the flip, on the same terms as
     .play-stage__flier and .play-card--slot.is-sorting: these boxes exist only
     while the panel is open. */
  will-change: transform;
}
/* The grey a highlight lays over every card outside the row, drawn over the art
   and inside the art's own corner. See §the highlight for why it is a pane and
   not a filter. */
.play-show__card::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: #000;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--play-dur-grey) linear;
}
/* Both sides of a card are rendered at once and concealed with `visibility`, so
   they stack rather than replace each other and the flip has both in hand at
   its midpoint. A face-up section builds no back at all (show.js): the flip is
   the only thing the second side is for. */
.play-show__card > svg {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}
.play-show__starter {
  position: relative;
  flex: none;
}
.play-show__starter .play-show__card {
  width: var(--play-show-starter-w);
  height: calc(var(--play-show-starter-w) * 1.375);
  box-shadow: var(--play-shadow-card-sm);
}
.play-show__starter .play-show__peek {
  position: absolute;
  top: 4px;
  left: 4px;
}
.play-show__hand {
  display: flex;
}
/* The overlap is two sevenths of a card — exactly the 32px the design width was
   drawn at — so four cards read as one hand at every width the panel opens at
   rather than clamping together as the card shrinks (W-0082). The phone keeps
   the authored 32 (§Responsive): its cards are small enough that a proportional
   overlap barely overlaps at all. */
.play-show__hand > .play-card + .play-card {
  margin-left: calc(var(--card-w) * -2 / 7);
}

/* HAND TOTAL, and the number under it. Display face and tabular figures: the
   numeral changes on every row and a proportional font would shuffle the
   column each time. It JUMPS to each value and pulses (show.js) — a count is a
   sum of whole scores, not a dial. */
.play-show__total {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}
.play-show__total-label {
  font-family: var(--play-font-mono);
  font-weight: 500;
  font-size: 12px;
  line-height: 16px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--play-blue);
}
.play-show__total-num {
  font-family: var(--play-font-display);
  font-weight: 400;
  font-size: 48px;
  line-height: 48px;
  font-variant-numeric: tabular-nums;
  color: var(--play-ink);
}

/* The reasons, one per line. The ONE flexible box on the panel, and the only
   one allowed to be: it scrolls inside itself, so a long crib cannot move the
   felt (W-0068). 480 is about twelve characters of label either side of the
   longest run name at 16px, and wider than that the two ends of a row stop
   reading as one line. */
.play-show__rows {
  /* Positioned so it is its own rows' offsetParent: show.js scrolls the newest
     row into view from offsetTop, and without this the panel is the nearest
     positioned ancestor and every one of those offsets is measured from the
     wrong box. */
  position: relative;
  flex: 1;
  min-height: 0;
  width: 100%;
  max-width: 480px;
  overflow-y: auto;
}

/* A row is a real button: hovering it lights the cards that made those points,
   clicking it makes that stay. Dead until the section settles — a list that
   answers the pointer while it is still being written invites the reader to
   chase it — so the cursor is the default until then. */
.play-show__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  width: 100%;
  height: 48px;
  padding: 0 12px;
  border: 0;
  border-bottom: 1px solid var(--play-hairline);
  background: none;
  font-family: var(--play-font-ui);
  text-align: left;
  cursor: default;
}
.play-show__row:not(:disabled) {
  cursor: pointer;
}
/* The ring turns INWARD here, unlike everywhere else on the felt: the list
   clips its own overflow, so a ring outside a row's box would be cut off on
   the first and last of them. */
.play-show__row:focus-visible {
  outline: 2px solid var(--play-seat-on);
  outline-offset: -4px;
}
.play-show__row-label {
  font-weight: 600;
  font-size: 16px;
  line-height: 24px;
  color: var(--play-ink);
}
.play-show__row-value {
  font-weight: 600;
  font-size: 16px;
  line-height: 24px;
  font-variant-numeric: tabular-nums;
}
/* Tinted by whoever is counting, the score pill's rule exactly: the points on
   this list and the pill that announced them are the same points. */
.play-show__rows--you .play-show__row-value {
  color: var(--play-pill-text-you);
}
.play-show__rows--bot .play-show__row-value {
  color: var(--play-pill-text-bot);
}

/* The highlight. The row takes the accent fill and a radius; the hairline under
   it and the one under the row above turn TRANSPARENT rather than off, so
   nothing on the list moves as the pointer travels down it. */
.play-show__row.is-active {
  background: var(--play-accent-fill);
  border-radius: var(--play-radius-figure);
}
.play-show__row.is-nodiv {
  border-bottom-color: transparent;
}
/* Points that were never awarded: the count stopped at 121 and the rest of the
   hand is shown anyway, struck through. They existed; they were not paid. */
.play-show__row.is-grayed .play-show__row-label,
.play-show__row.is-zero .play-show__row-label {
  color: var(--play-lead);
}
.play-show__row.is-grayed .play-show__row-value {
  color: var(--play-lead);
  text-decoration: line-through;
}
.play-show__row.is-zero .play-show__row-value {
  color: var(--play-lead);
}

/* Every card that is not in the highlighted row greys where it lies, at the
   app's overlayPlayed 0.6 — the pile's own .is-spent wash, arrived at the other
   way round. A black pane at opacity a composites to c * (1 - a), which is what
   brightness(1 - a) does: 0.6 here draws the pixels brightness(0.4) drew, cream
   rim included, and both card shadows are pure black to begin with. What
   changes is the cost. This fires on every pointer move down the list, and
   opacity is a compositor property where filter re-rasterises five SVGs for the
   whole 300ms.

   The starter greys through its two cards rather than as a block: its peek
   stands 4px out from under the face, and one pane on the block would leave
   that sliver bright. The class stays on the block all the same — show.js dims
   the starter as one thing (its peek belongs to it), and only this rule knows
   that the thing is drawn twice. */
.play-show__card.is-dim::after,
.play-show__starter.is-dim .play-show__card::after {
  opacity: 0.6;
}

/* "Counted 8 of 12", under the list, when a win cut the count short. */
.play-show__note {
  flex: none;
  font-family: var(--play-font-mono);
  font-weight: 500;
  font-size: 12px;
  line-height: 16px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--play-lead);
}

/* The panel's one control, and the felt's ghost button unchanged. */
.play-show__next {
  flex: none;
}

/* ---------- Stages ----------
   `data-stage` on the surface says which moment of the hand is on screen, and
   these rules fade the furniture that moment does not use. OPACITY ONLY: the
   surface's height may not change with game state (W-0068), so nothing here
   may hide, move or resize a box. Both directions ease, entrances arriving and
   exits leaving.

   cut             the ceremony above owns the felt, so the table and the board
                   panel step out of the way entirely and fade back in as the
                   deal takes over. The dialogue strip goes with them: it is
                   still an aria-live region and still narrates every beat, and
                   on screen the ceremony says the same things larger.
   deal / discard  the deck fixture IS the deck, so the in-flow starter fan
                   steps aside; and there is no count to show yet.
   show            the count column's label and numeral go. The count itself
                   lives on the .play-show panel above the area (§The show);
                   these rules only quiet the furniture it covers.
   pegging, or no attribute at all (the opening table): everything. */
.play__starter,
.play__count-num,
.play__label--accent {
  transition: opacity var(--play-dur-chrome) var(--play-ease-arrive);
}
.play:is([data-stage='deal'], [data-stage='discard']) .play__starter,
.play:is([data-stage='deal'], [data-stage='discard'], [data-stage='show'])
  :is(.play__count-num, .play__label--accent) {
  opacity: 0;
  transition-timing-function: var(--play-ease-exit);
}

/* The furniture is named one row at a time rather than swept up as
   `.play__area > *`: a bare sweep would out-specify .play-chip's own
   transition list and turn the active seat's glow instant, so the chip
   carries its opacity in that list instead (§Seat chips). The child
   combinator matters as much — the ceremony's own plaques are .play-chip too,
   and they live in the overlay rather than in the area. */
.play__row,
.play__centre,
.play__strip,
.play__panel {
  transition: opacity var(--play-dur-furniture) var(--play-ease-arrive);
}
.play[data-stage='cut'] .play__area > :is(.play__row, .play__centre, .play__strip, .play-chip),
.play[data-stage='cut'] .play__panel {
  opacity: 0;
  transition-timing-function: var(--play-ease-exit);
}

/* ---------- Rail: the Nobs promo ----------
   One banner at three widths (W-0092). The reserved 300x600 ad box that used to
   sit under the old upsell is gone, superseding W-0067 §7 and §8.

   Nothing here is given a height. The grid aligns the rail to `start`, so the
   promo sizes to its own content and the copy can grow a line without a rule
   moving; it is still the taller of the two shell columns above 1560, and so
   still what sets the page height there.

   The QR and the button are one offer at two widths, and exactly one is ever
   shown — the pattern the homepage's download block already uses (W-0060).
   Above 1560 the reader is at a desktop, so they scan the code with a phone;
   below that they are holding the device that installs it, so they tap
   (W-0117). Neither width shows both, and no width shows neither. */
.play-rail {
  display: flex;
  flex-direction: column;
  /* The shell aligns its items to `start`; the rail opts out, so the grid row
     stretches it to the game's height and the promo can fill it (W-0093). */
  align-self: stretch;
}
.play-promo {
  flex-grow: 1;
  padding: 24px 16px;
  border-radius: var(--play-radius-card);
  /* The weave is the only texture on the surface, and it is deliberately at the
     threshold of visible: it separates the promo from the felt it sits beside
     without becoming a second material. */
  background:
    repeating-linear-gradient(
      135deg,
      rgba(243, 249, 255, 0.028) 0 1px,
      rgba(243, 249, 255, 0) 1px 6px
    ),
    linear-gradient(var(--play-felt-top), var(--play-felt-bottom));
  color: var(--play-ink);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.play-promo__shot {
  display: block;
  width: 268px;
  height: auto;
  /* The one valve. If the copy ever grows past the height the game gives us,
     the shot gives way first rather than the column overflowing. */
  min-height: 0;
  object-fit: contain;
}
.play-promo__copy {
  /* This fixed gap and the scan's auto margin share the remaining height. At
     24px, the copy sits evenly between the phone art and the QR card. */
  margin-top: 24px;
  min-width: 0;
}
.play-promo__title {
  margin: 0;
  font-weight: 600;
  font-size: 32px;
  line-height: 36px;
  letter-spacing: -0.01em;
  text-wrap: balance;
}
.play-promo__brand {
  color: var(--play-blue);
}
.play-promo__lead {
  margin: 16px 0 0;
  font-size: 16px;
  line-height: 24px;
  color: var(--play-lead);
  text-wrap: pretty;
}
/* `auto` rather than a length: this is the one gap that gives, so the column
   always lands exactly on the height the game handed it (W-0093). */
.play-promo__scan {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
}
/* 180 square holding a 172 code — the owner's baseline. The padding is only 4
   because the artwork carries most of its own quiet zone: the SVG reserves
   4.88% per side (56 of its 1148 units), ~8.4px at this size, so the code sits
   ~12.4px clear of the card edge. That is short of the 4-module ideal (~17px
   here, a module being ~4.2px), as it was under the old file too, so this one
   is settled by decoding the rendered page rather than by arithmetic (W-0117).
   `overflow: hidden` is load-bearing at this padding — the image's white corners
   reach past the 16px radius and would square the card off without it. */
.play-promo__qr-card {
  padding: 4px;
  border-radius: var(--play-radius-card);
  overflow: hidden;
  background: #ffffff;
  box-shadow: var(--play-shadow-onfelt);
}
.play-promo__qr {
  display: block;
  width: 172px;
  height: 172px;
}
/* The tag rides 8px over the card's bottom edge, so it has to paint after it:
   the positioning here buys stacking order, not placement. It is a label and
   never a control — the QR is the affordance, and there is nothing to click. */
.play-promo__tag {
  position: relative;
  z-index: 1;
  margin-top: -8px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: max-content;
  min-width: 168px;
  height: 40px;
  padding: 0 16px;
  /* The label stays on one line and grows with its copy. The old 168px width
     included its padding under border-box sizing, so longer text spilled past
     the red pill instead of widening it. */
  white-space: nowrap;
  border-radius: var(--play-radius-pill);
  background: var(--play-red);
  color: #ffffff;
  font-weight: 700;
  font-size: 14px;
  line-height: 20px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}
.play-promo__actions {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.play-promo__cta {
  display: none;
}
.play-promo__note {
  margin: 20px 0 0;
  width: 184px;
  font-size: 14px;
  line-height: 20px;
  color: var(--play-lead);
  text-wrap: pretty;
}

/* ---------- Phone banner ----------
   Ships hidden; main.js reveals it on small screens. A nudge, never a gate.
   It is a fixed bottom sheet rather than a block above the game because it
   appears after first paint and can be dismissed mid-game: in the flow, both
   moments would shove the whole surface up or down the screen. Out of the flow
   it costs the layout nothing. In-surface overlays suppress it below so it
   cannot cover their controls. */
.play-banner {
  position: fixed;
  left: 16px;
  right: 16px;
  bottom: 16px;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 0;
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
  border-radius: var(--play-radius-figure);
  box-shadow: var(--play-shadow-onfelt);
  background: linear-gradient(var(--play-felt-top), var(--play-felt-bottom));
  color: var(--play-ink);
}
.play-banner__text {
  flex: 1;
  min-width: 0;
  font-size: 16px;
  line-height: 24px;
}
.play-banner__cta {
  flex: none;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 8px 16px;
  border-radius: var(--play-radius-pill);
  background: var(--play-red);
  color: #ffffff;
  font-weight: 600;
  font-size: 16px;
  line-height: 20px;
  text-decoration: none;
}
.play-banner__close {
  flex: none;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--play-ink);
  font-size: 24px;
  line-height: 24px;
  cursor: pointer;
}

/* ---------- End of game ----------
   The Nobs app's own end-game screen, brought to the felt (W-0104). Three
   things about it are deliberate and easy to undo by accident:

   1. It lives INSIDE .play, absolutely, like the opponent select — not in the
      browser's top layer, which is where the <dialog> this replaced put it.
      The surface clips its overflow and rounds its corners, so the panel is
      the shape of the game and cannot paint over the page around it.
   2. Its blue is the app's, not the seat colours this site reversed
      (W-0067 §6). The crown, the wreath, the eyebrow and the CTA come from the
      app's EndGameScreen together and read as one borrowed fixture. The felt's
      own red/blue seating is untouched everywhere else, including in the two
      score columns below, which carry no seat tint at all.
   3. The columns' height is the same whoever won: the crown's 24px slot is
      reserved on both, and the wreath is out of the flow. Nothing in this
      panel moves when .is-winner changes sides.

   Type here is the site's three faces at the owner's sizes: the serif carries
   the headline, because the cut ceremony and the show already speak in it and
   the felt has one display voice; the grotesque carries the tracked cap line
   above it, mono being a 12-16px caption treatment on this surface and not a
   display one. */
.play-modal {
  position: absolute;
  inset: 0;
  z-index: 7;
  display: flex;
  /* Centred by `margin: auto` on the panel below rather than by `align-items`,
     which is the one difference from .play-select: a flex-centred child that
     outgrows a scrolling box has its TOP clipped and cannot be scrolled back
     to, and this panel grows a whole promo block on the phone. */
  padding: 32px;
  background: var(--play-scrim);
  /* The surface is a fixed height and clips its overflow, so the safety valve
     for a short window is a scroll rather than a taller surface — the same
     valve .play-select uses, for the same reason. */
  overflow-y: auto;
  /* The entrance is an animation and not a transition, and the resting state
     above is the VISIBLE one, deliberately. A transition needs a class flipped
     a frame after the panel is shown, and rAF is frozen in a hidden tab — the
     same trap the select's fade lands a timer on rather than animationend
     (§Opponent select). A keyframe that only ever runs downhill from opacity 0
     cannot leave an invisible panel standing over the felt swallowing clicks:
     the worst it can do is skip the fade. `fill-mode` stays at its default
     `none` for that reason; never give this one `both`. */
  animation: play-modal-in var(--play-dur-chrome) var(--play-ease-arrive);
}
@keyframes play-modal-in {
  from {
    opacity: 0;
    transform: translateY(16px);
  }
}
@supports (backdrop-filter: blur(4px)) {
  .play-modal {
    backdrop-filter: blur(4px);
  }
}
.play-modal__inner {
  margin: auto;
  width: min(480px, 100%);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.play-modal__eyebrow {
  font-family: var(--play-font-ui);
  font-weight: 700;
  font-size: 24px;
  line-height: 32px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--play-blue);
}
.play-modal__title {
  margin: 4px 0 0;
  font-family: var(--play-font-display);
  font-weight: 400;
  font-size: 44px;
  line-height: 56px;
  color: var(--play-ink);
}
/* The skunk callout, in the slot the app gives its result line. Quiet on
   purpose: the eyebrow above it is already blue, and two blues stacked would
   make the loser's margin compete with the result. */
.play-modal__skunk {
  margin-top: 8px;
  font-family: var(--play-font-mono);
  font-weight: 500;
  font-size: 12px;
  line-height: 16px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--play-lead);
}

/* ---------- The two score columns ---------- */
/* The width is explicit because the panel above centres its children, which
   makes this row shrink to fit — and a column whose width is a percentage OF
   this row would then be resolving against its own contents. */
.play-modal__columns {
  width: 100%;
  display: flex;
  justify-content: center;
  gap: 16px;
  margin-top: 36px;
}
/* 140 is the app's column, and the width the wreath was drawn against. The
   clamp is what lets two of them plus their gap sit inside a 320px phone
   without the wreath overhanging the panel: a fluid width, so grid-exempt. */
.play-modal__col {
  position: relative;
  width: min(140px, calc((100% - 16px) / 2));
  display: flex;
  flex-direction: column;
  align-items: center;
}
/* Behind the winner, and only the winner. Out of the flow so the two columns
   measure the same, and stretched 186x160 -> 140x120 the way the app stretches
   it (preserveAspectRatio="none" is baked into the file). Its own white
   gradient fades the lower branches out; there is no glow behind the avatar in
   the app, and there is none here — the wreath IS the winner treatment. */
.play-modal__wreath {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: auto;
  visibility: hidden;
  pointer-events: none;
}
/* Reserved on BOTH columns, empty on the loser, so the avatars share a
   baseline. Never give this a state-dependent height. */
.play-modal__crown {
  display: block;
  height: 24px;
  margin-bottom: 4px;
}
.play-modal__crown > img {
  display: block;
  visibility: hidden;
}
.play-modal__col.is-winner .play-modal__crown > img,
.play-modal__col.is-winner .play-modal__wreath {
  visibility: visible;
}
.play-modal__avatar {
  width: 72px;
  height: 72px;
  border-radius: var(--play-radius-pill);
  border: 2px solid var(--play-ink);
  background: var(--play-felt-top);
  object-fit: cover;
}
.play-modal__score {
  margin-top: 8px;
  font-family: var(--play-font-ui);
  font-weight: 700;
  font-size: 32px;
  line-height: 40px;
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
  color: var(--play-ink);
}
/* The app shrink-fits this to 0.8x before it ellipsises, which CSS cannot do.
   All three of the site's opponents are six letters or fewer, so the truncation
   is a guard and not a layout the reader will meet. */
.play-modal__name {
  max-width: 120px;
  font-family: var(--play-font-ui);
  font-weight: 500;
  font-size: 14px;
  line-height: 16px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--play-ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.play-modal__actions {
  display: flex;
  margin-top: 36px;
}
.play-modal__cta {
  min-width: 280px;
}

/* ---------- The phone's app promo ----------
   Off at every width but the phone rung, where it is turned on in §Responsive.
   The rail carries this promo everywhere else and sits beside the game the
   whole time someone is playing; on a phone the rail is a long way below the
   felt, so the promo comes to the modal instead. */
.play-modal__promo {
  display: none;
  width: 100%;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  margin-top: 24px;
  padding-top: 24px;
  border-top: 1px solid var(--play-blue-hairline);
}
.play-modal__promo-title {
  font-family: var(--play-font-ui);
  font-weight: 600;
  font-size: 20px;
  line-height: 24px;
  color: var(--play-ink);
}
.play-modal__brand {
  color: var(--play-blue);
}
/* 16/20 is the owner's spec and is tighter than the site's body idiom; it holds
   because this is two short lines in a 480-wide panel, never a paragraph. */
.play-modal__promo-lead {
  font-family: var(--play-font-ui);
  font-weight: 400;
  font-size: 16px;
  line-height: 20px;
  color: var(--play-lead);
  text-wrap: pretty;
}

/* The phone nudge is fixed to the viewport at z-index 20 and would otherwise
   stand on top of the modal's own promo, which says the same thing twice. It
   goes away while the panel is up. CSS and not script on purpose: main.js owns
   the banner, game.js owns the modal, and the entry point's header keeps the
   two apart so an iframe port of the game leaves the banner behind. */
.play-page:has(.play-modal:not([hidden]), .play__speed.is-open) .play-banner {
  display: none;
}

/* ---------- SEO band ----------
   Single measure column below the game. Type comes from site.css .prose, which
   already sets the centred 680px measure and the serif display headings. */
.play-seo {
  padding: 56px 0 0;
}

/* ---------- Responsive ladder ----------
   Full layout needs ~1560px. Below that the rail leaves the grid — the upsell
   moves under the game, the ad slot goes — and what is left is the table
   itself: play area, gutter, board panel. That table never changes shape again.
   It SCALES. --card-w, --play-gutter and the panel's width are capped clamps
   that hold today's exact numbers from 1300px up and shrink linearly below, so
   an iPad in portrait gets the same picture a desktop gets, smaller, instead of
   a different layout (W-0082). Nothing here hides the board.

   1080 is where the chrome around the table gives way: the surface's own
   padding, the chips, the score tiles, and the dialogue, which reserves two
   lines from there down.

   768 is the floor, and below it the page stops being a page: the felt fills
   the screen under the slimmed header, the board lies on its side as a strip
   across the top, and each seat plaque becomes a narrow tile beside its own
   hand (W-0084). The board is no longer taken away at any width — it turns
   instead, which is the whole of what changed. Heights there come from the
   viewport as well as the width, and every band inside them is fixed, so a
   phone is still exactly one surface height per screen. */

/* 1: the rail leaves the grid, and the promo lies on its side. The QR goes with
   it (W-0092): a reader below 1560 is on a device that can open the link, so
   the button takes over and nothing here has to be scanned. */
@media (max-width: 1559px) {
  .play-shell {
    grid-template-columns: minmax(0, 1fr);
  }
  .play-promo {
    flex-grow: 0;
    flex-direction: row;
    align-items: center;
    gap: 32px;
    /* Not symmetric: the shot carries its own transparent margin on the left,
       so an equal padding reads tighter on the right than it measures. */
    padding: 20px 48px 20px 20px;
    text-align: left;
  }
  .play-promo__shot {
    flex: none;
    width: auto;
    height: 176px;
  }
  .play-promo__copy {
    flex-grow: 1;
    margin-top: 0;
  }
  .play-promo__lead {
    margin-top: 8px;
    max-width: 560px;
  }
  .play-promo__scan {
    display: none;
  }
  .play-promo__shot {
    min-height: auto;
  }
  .play-promo__actions {
    flex: none;
    width: 240px;
  }
  .play-promo__cta {
    display: inline-flex;
    align-self: stretch;
  }
  .play-promo__note {
    margin-top: 12px;
    width: auto;
    text-align: center;
  }
}

/* 2: the table keeps its shape and the chrome around it gives way. Not one rule
   here moves a column — the play area, the gutter and the panel have been
   narrowing continuously since 1300 and carry on doing so. What changes is
   everything that was authored in pixels beside them: the page's side padding
   and the surface's own padding hand their width to the table, and the hands
   close up to the phone's 8px gap.

   The dialogue reserves two lines from here down. 20/28 needs about a 411px
   track for the longest thing the game says, and the track runs out around
   1080 — but the strip's height is part of the layout guarantee (W-0068), so
   the reservation has to be made BEFORE the wrap arrives rather than when it
   does. That is the phone's own recipe, 16/24 in a 48px box, moved up a rung
   (W-0079 type steps, geometry scales), and the pill drops with the buttons it
   floats over. The strip's bottom margin comes down with it: 44 was centring
   the dialogue in a band this rung no longer has the height to spare for.

   The score tiles stack into two full-width rows for a plainer reason: the
   panel is between 240 and 317px wide through this band, and OPPONENT beside
   its own numeral does not fit in half of that. One tile per line, label left
   and number right. The chips give up 12px of height and 4 of padding, which is
   also what buys the crib dock its clearance over the starter cluster at the
   bottom of the band. */
@media (max-width: 1079px) {
  .play-page {
    padding: 16px 16px 96px;
  }
  .play {
    --hand-gap: 8px;
    --pile-inset: 8px;
    gap: 16px;
    padding: 16px;
  }
  .play__strip {
    height: 100px;
    margin-bottom: 24px;
  }
  /* Two lines of room instead of one: the longest things the game says do not
     fit a single line in this track. The pill follows the box (W-0088) rather
     than carrying an offset of its own. */
  .play__voice {
    height: 48px;
  }
  .play__status {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 16px;
    line-height: 24px;
  }
  .play-chip {
    height: 80px;
    padding: 12px;
    gap: 12px;
  }
  .play__scores {
    flex-direction: column;
  }
  .play-score {
    flex-direction: row;
    justify-content: space-between;
    gap: 12px;
    padding: 8px 16px;
  }
  .play-score__num {
    font-size: 32px;
    line-height: 36px;
  }
}

/* 3: the phone, and the one rung that is an app rather than a page (W-0084,
   re-proportioned by W-0087).
   Every rung above this one scales the same table. This one re-cuts it: the
   felt fills the screen under the slimmed site header, the board lies on its
   side as a strip across the top, and each seat plaque becomes a narrow tile
   standing beside its own hand. Nothing is hidden that a wider width shows —
   the board turns and narrows rather than goes, and the score moves from the
   panel's tiles onto the seat tiles, which is the one thing this rung shows
   differently.

   Top to bottom: control row, board strip, opponent tile and hand, the centre
   band, the dialogue, the reader's tile and hand. Nothing renders below the
   hand, which is the order the markup has always had.

   W-0087 keeps that stack and re-proportions it around one idea: a card is as
   large as the width allows, and it is paid for in OVERLAP rather than by
   shrinking anything. Both seats deal the same size card, the played row runs
   the full width at that same size, and the board gives up two thirds of the
   felt it was never using so the cards can have the height. The two stages
   draw into bands of identical height, so the discard and the play are the
   same picture with different furniture in the middle of it.

   HEIGHT. The felt is now a function of the viewport as well as the width, and
   that is bought the same way the width was: every band inside it is fixed for
   a given screen, and no game state can change one. What is new is a valve. The
   surface asks for min-height and never height, so a screen that cannot spare
   the room for the bands grows the surface and scrolls the page instead of
   clipping the reader's hand off the bottom of it. Two bands are allowed to
   take a taller screen's surplus, in order: the board first, up to its own
   ceiling, then the centre. Both are ranges with a floor, not contents that
   grow.

   --card-w keeps a line of its own: 19vw is steeper again than the tablet's,
   because a phone hands the cards the width the gutter used to take AND lets
   them lie under each other to spend it. */
@media (max-width: 767px) {
  /* The page gets out of the way. No side padding, no width cap, no gap under
     the felt: this is the only rung where the game runs to both edges of the
     screen. The prose and the rail take their own margins back at the foot of
     this block. */
  .play-page {
    padding: 0;
    max-width: none;
  }
  .play-shell {
    gap: 0;
  }
  /* The app nudge at 14/20. main.js watches this same 767 to decide whether the
     nudge appears at all, which is the only breakpoint literal anywhere in the
     game's script. It is a fixed sheet over the felt and stays exactly that. */
  .play-banner__text {
    font-size: 14px;
    line-height: 20px;
  }

  /* The felt, fitted to the screen.

     `min-height` and never `height` — that is the valve above, and it is what
     replaces W-0068's no-explicit-height clause on this rung. Everything else
     about the guarantee stands: not one band below is sized by what a hand is
     holding, and the discard and the pegging draw into bands of exactly the
     same height, so the felt does not so much as twitch between the two.

     svh and not dvh, deliberately. dvh would re-lay the whole table out from
     under the reader's thumb the moment the URL bar collapsed. svh is the small
     viewport, so the felt is sized for the bar being there and simply has room
     to spare when it goes. --site-header-h is what site.css publishes for the
     slimmed phone bar; the literal fallback is the same 65 and keeps this file
     renderable without site.css, like every other token here.

     The gutter falls to 0, which is the switch game.js reads — off the hand
     row's resolved padding-left, not off this custom property — to hide the
     crib, to shorten the discard button's label and to lay the board on its
     side. Zero it; never delete it. */
  .play {
    /* THE PHONE'S CARD (W-0087). A card is paid for in overlap here, not in
       height: six of them fill the track by sliding under each other, and what
       a reader needs off the ones behind is the rank and the pip under it. So
       the line is steeper again than the tablet's — 71 at 375, 75 at 393, 82 at
       430 — and the ceiling comes down to 96, which is the width at which six
       cards stop needing to overlap at all before the rung ends at 767. */
    --card-w: clamp(64px, 19vw, 96px);
    --hand-gap: 8px;
    /* The starter is the one card on the felt deliberately smaller than a hand
       card: it shares a band with the count, and the band has 88 to give. At
       0.62 of a card the cluster measures fan 66 + gap 4 + caption 16 = 86. */
    --play-starter-w: calc(var(--card-w) * 0.62);
    /* The played row at hand-card size, running the whole width. The eight
       fixed slots and their landing-index rule are untouched; what changes is
       that a played card is now the size of the card that was played, which is
       what a reader is looking for when they scan the set (W-0087). */
    --pile-w: var(--card-w);
    --pile-inset: 0px;
    /* The seat tile and the band it stands in. The mockup's rule is that a card
       stands 8px taller than the tile beside it, and that holds wherever a card
       is tall enough to spare it. Below that the tile takes its 96px floor —
       which is exactly what its stack measures: 2 border + 32 avatar + 4 + 16
       name + 16 role + 20 score + 2 border = 92, in 96 — and the BAND grows
       rather than the tile shrinking under its own contents. */
    --play-seat-h: max(96px, calc(var(--card-h) - 8px));
    --play-seat-band: max(var(--card-h), calc(var(--play-seat-h) + 8px));
    --play-gutter: 0px;

    flex-direction: column;
    min-height: calc(100vh - var(--site-header-h, 65px));
    min-height: calc(100svh - var(--site-header-h, 65px));
    gap: 8px;
    padding: 8px 8px calc(8px + env(safe-area-inset-bottom, 0px));
    border-radius: 0;
    box-shadow: none;
  }

  /* The panel above the play area, carrying two things: the control row and the
     board. Its furniture goes — a fill, a hairline and 20px of padding are what
     make a panel read as a panel, and here it is not one, it is the top of the
     screen. The score tiles go with it: the seat tiles carry the score on this
     rung (.play-chip__score), so the panel's pair would be the same two numbers
     twice.

     `flex: 999` against the area's 1 is the absorber: whatever height the
     screen has spare goes to the board first, and what the board cannot take
     lands in the centre band. 188 is the ceiling — a 44px control row, the 8px
     gap, a 132px board, and two spare for the New game pill's mono line-height.
     Neither flex line may shrink: the valve, not compression, is what absorbs a
     screen too short to hold the bands. */
  .play__panel {
    order: -1;
    flex: 999 0 auto;
    max-height: 188px;
    width: auto;
    gap: 8px;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
  }
  .play__scores {
    display: none;
  }
  /* Quit game, the speed and mute on one 44px line above the board. Order
     rather than markup, the same move the ceremony's deck makes. The wrap that
     gives the three speed words their own line everywhere else is turned off
     with the sheet that replaces them — see §Speed sheet below, which is where
     that wrap is turned off. */
  .play__panel-foot {
    order: -1;
    align-items: stretch;
    gap: 8px;
    height: 44px;
  }
  /* Quit and the speed share what the mute circle leaves. Two flexible pills
     rather than two natural widths: "Relaxed" and "Normal" are different
     lengths, and a control row that reshuffled itself when the setting changed
     would be the one thing on the felt that moves for no reason. */
  .play__panel-foot > .play-btn--pill {
    flex: 1 1 0;
    padding: 0 12px;
  }
  /* Quit, the speed, then the sound circle — the mockup's order, and the right
     one: the two things with words on them read left to right and the round
     icon terminates the row. Order rather than markup, so the DOM keeps the
     reading sequence every other width lays out in. */
  .play__mute {
    order: 1;
  }
  /* The board, on its side: board.js draws the 620x196 viewBox wherever the
     layout has no gutter to stand the tall one beside. Narrow (W-0087) — the
     bare felt between the two turns was carrying nothing, and giving it up
     hands about 60px back to the cards. 88 is the floor a strip is still a
     board at, 132 the ceiling at which it is as large as it is any use, and
     between them it is the surface's shock absorber. The base rule holds the
     positioning context its SVG pins itself to. */
  .play__board {
    display: block;
    flex: 1 1 auto;
    min-height: 88px;
    max-height: 132px;
  }

  /* The play area as four bands over two columns: an 80px tile column beside
     the hands, and the centre and the dialogue spanning both.

     The bands are the budget. Two seat bands of a card's height, a centre that
     holds the taller of the two stages' contents, and 156 for the dialogue —
     12 of clearance, 48 of words, 12, the 48px button, and 36 more so the
     button clears the reader's resting hand by the 44 the mockup asks for once
     the grid's own 8px gap is counted. At 393x852 the four bands and their
     gaps come to 570, the panel to 188, and the surface settles flush inside
     the 787 the viewport has under the slimmed header. A 375x667 screen is
     about 80 short of that and opens the valve: the felt keeps its bands and
     the page scrolls, which is the same bargain this rung has always struck
     and the price of the mockup's larger cards (W-0087).

     The hand rows keep their padding-left, which resolves to 0 here: it is what
     game.js reads to know which rung it is on, so it may not be swapped for a
     margin or dropped as dead weight. */
  .play__area {
    flex: 1 0 auto;
    display: grid;
    gap: 8px;
    grid-template:
      'chip-bot bot-hand' var(--play-seat-band)
      'centre   centre' minmax(min-content, 1fr)
      'strip    strip' 156px
      'chip-you you-hand' var(--play-seat-band)
      / 80px minmax(0, 1fr);
  }
  /* The map, in the order the template above lists it. */
  .play-chip--bot {
    grid-area: chip-bot;
  }
  .play__row--opponent {
    grid-area: bot-hand;
  }
  .play__centre {
    grid-area: centre;
  }
  .play__strip {
    grid-area: strip;
  }
  .play-chip--you {
    grid-area: chip-you;
  }
  .play__row--player {
    grid-area: you-hand;
  }
  /* Both seats deal the same cards at the same size (W-0087, superseding
     W-0084's three-quarter backs). The two rows were never asymmetric on a
     table and the overlap rule below is what makes symmetry affordable: a hand
     that would not fit slides under itself instead of shrinking. */
  .play__row {
    align-self: center;
    align-items: center;
  }

  /* The plaques become identity tiles: avatar over name over role over score,
     filling the 80px column beside their own hand and standing 8px shorter than
     the cards, which is the mockup's own proportion.

     The DEALER / PONE line is back (W-0087, superseding W-0084's cut of it):
     who deals decides which crib the discards are going to, it is the one fact
     the reader carries through the whole hand, and a band tall enough for a
     card is tall enough to say it.

     Base pins these to the corners of the area and sizes them from the gutter,
     which is 0 here — calc(0px - 20px) is a negative width — so the position,
     the width and the height all have to be handed back. */
  .play-chip {
    position: static;
    width: auto;
    height: var(--play-seat-h);
    flex-direction: column;
    justify-content: center;
    align-self: center;
    gap: 4px;
    padding: 0 4px;
  }
  .play-chip__avatar {
    width: 32px;
    height: 32px;
  }
  .play-chip__text {
    align-items: center;
    gap: 0;
    text-align: center;
  }
  .play-chip__name {
    max-width: 100%;
    font-size: 14px;
    line-height: 16px;
  }
  .play-chip__role {
    display: block;
    font-size: 12px;
    line-height: 16px;
    letter-spacing: 0.12em;
  }
  .play-chip__score {
    display: block;
    font-size: 16px;
    line-height: 20px;
  }

  /* THE FAN. One step for every hand, not two rules for two counts.

     The step is whatever it takes for SIX cards to fill the track exactly —
     the base rule's own arithmetic — and every shorter hand then uses that same
     step and centres in what it occupies. That is the mockup's picture in both
     stages: six cards edge to edge while the reader is choosing two, and a
     compact stack of four during the play that stays put as it empties. The
     base rule engages at five cards and up because a wide table has room for
     four to lie flat; a phone never does, so the count test goes and the
     adjacent-sibling selector takes every card after the first.

     min(0px, ...) is what keeps it honest at the top of the rung: by 767 six
     96px cards already fit and the margin clamps away, leaving them flat and
     gapped rather than pushed apart. */
  .play__hand > .play-card + .play-card {
    margin-left: min(0px, calc((100% - 6 * var(--card-w) - 5 * var(--hand-gap)) / 5));
  }
  /* And a chosen card rises 12 rather than the base's 14: the app's own
     selectRaise, and the mockup asks for it by that number. A phone card is
     smaller than a desktop's and there is no hover state under it to out-rise,
     so the raise is doing less work here and needs less of it. */
  .play-card--slot.is-selected {
    transform: translateY(-12px);
  }

  /* THE CENTRE, as two rows both stages draw into.

     Row one is the starter beside the count; row two is the played set, at
     hand-card size across the whole screen. The pile cannot do that from inside
     the count column, so the column stops being a box: `display: contents`
     lifts its two children into this grid, the caption and numeral travel
     together in the wrapper the template gives them, and the pile becomes a row
     in its own right (W-0087).

     The discard draws its four crib slots into the same band, out of the flow
     and centred on it, so the band is the same height in both stages and the
     felt below it never moves. */
  .play__centre {
    height: auto;
    justify-items: center;
    align-items: center;
    /* The starter/count row takes the band's surplus, and the played row keeps
       exactly one card's height at the foot of it. `1fr` and not `align-content`
       (W-0090): centring the two rows as a GROUP put every spare pixel a taller
       phone had into one gap above the starter — 21 of them at 430 — and left
       the gap below it at 8. Inside the row the surplus splits evenly around the
       fan and the numeral instead, which is what makes the deck read centred
       between the seat tile above it and the played cards below at every height
       the rung runs at. */
    grid-template:
      'starter head' minmax(88px, 1fr)
      'pile    pile' var(--card-h)
      / auto minmax(0, 1fr);
    gap: 8px 16px;
  }
  .play__count {
    display: contents;
  }
  .play__count-head {
    grid-area: head;
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  /* The base rule pulls the caption 8px onto the numeral to close a 16px column
     gap this rung does not have. */
  .play__count .play__label {
    margin-top: 0;
    margin-bottom: 0;
  }
  .play__count-num {
    font-size: 56px;
    line-height: 56px;
  }
  .play__pile {
    grid-area: pile;
    justify-self: stretch;
    height: var(--card-h);
  }
  /* The plaques are back in the flow, so the starter comes back into its cell
     with them: there is no gap between two corners left to centre on.

     And the caption goes (W-0090). A deck of backs with one card turned face up
     on top of it, in the one place on the felt a cut card has ever appeared, is
     not a thing a reader has to be told the name of — and the word was the only
     reason the cluster sat high in its band. Without it the fan is the whole of
     this cell and the row's `align-items: center` centres it outright: an equal
     11px above and below inside the 88, and the 8px gaps either side of the band
     are equal too, so it reads centred between the seat tile over it and the
     played row under it, which is what the owner asked for. The figcaption stays
     in the DOM and keeps its caption at every width above this rung. */
  .play__starter {
    grid-area: starter;
    position: static;
    width: auto;
    transform: none;
    gap: 0;
    /* Centred on what the reader can SEE, not on the cell. The seat tile above
       is 8px shorter than a card and centred in its band, so it stops 4px above
       the band's edge and the felt over the starter is 4px taller than the felt
       under it. Handing that 4 back as a bottom margin closes it: the cluster is
       centred in its cell, and a margin on a centred box moves it by half, so
       the deck lands exactly between the tile's bottom edge and the played row's
       top one. Derived from the two tokens rather than authored, so it stays
       true at every card size the rung runs at. */
    margin-bottom: calc((var(--play-seat-band) - var(--play-seat-h)) / 2);
  }
  .play__starter .play__label {
    display: none;
  }

  /* The crib, in the band the discards are actually going into. The well is
     the fixture the base rule already draws — four card-shaped slots centred on
     the play axis — at the size of the cards being sent to it, which is the one
     change this rung makes to it (W-0087).

     And there is no dock behind it. Every band here is spent, so the fixture
     leaves the felt with the discards and comes back on the show's own panel;
     game.js drops `is-shown` the moment the anchor stops being the well. The
     caption goes with the dock: the prompt directly below names the crib's
     owner in bold, and two tellings in one band is one too many. */
  .play-crib--centre .play-crib__slot {
    width: var(--card-w);
    height: var(--card-h);
  }
  .play-crib--centre .play-crib__slots {
    gap: 8px;
  }
  .play-crib__label {
    display: none;
  }

  /* THE DIALOGUE, and the one button left on the felt.

     12 of clearance, two lines of status in 48, 12, and a 48px button — the
     mockup's own spacing, and the 36 under it plus the grid's 8 is the 44 of
     clearance it asks for above the reader's resting hand. The band is the
     grid's to set, so the strip pads rather than margins: a margin would push a
     156px box out of the 156px row it was given.

     Two lines of status and not one, because the longest things the game says
     do not fit on one line in a 289px track, and a third line would break the
     band. The prompt is the mockup's 20; the play's running commentary drops to
     16, which changes type inside a box whose height is authored either way and
     so touches no guarantee. */
  .play__strip {
    height: auto;
    margin-bottom: 0;
    justify-content: flex-start;
    gap: 12px;
    padding: 12px 0 36px;
  }
  .play__voice {
    height: 48px;
  }
  .play__status {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 20px;
    line-height: 24px;
    color: var(--play-ink);
  }
  .play[data-stage='pegging'] .play__status {
    font-size: 16px;
    line-height: 20px;
  }
  /* The crib's owner, picked out. This is the one word on the felt a reader has
     to READ rather than glance at — a discard sent to the wrong crib cannot be
     taken back — and at a phone's measure the sentence around it is two lines
     of small type (W-0087). game.js hands the fragment over separately. */
  .play__status-em {
    font-weight: 700;
  }
  .play__actions {
    height: 48px;
  }
  .play__actions .play-btn {
    min-height: 48px;
  }

  /* The ceremony keeps every part it has and gives up the room between them:
     the plaques are already compact here (they are .play-chip, so they are the
     same tile the seats are), and the deck draws the same fifty-two backs a
     desktop does, overlapped as tightly as the track makes it.

     The one thing that moves is the deck itself, to the bottom. A desktop reads
     top down and the outcome belongs under the thing that decides it; a phone
     is held, and the thing to touch belongs nearest the thumb. Order rather
     than markup, so the DOM keeps reading headline, deck, seats. */
  .play-stage {
    gap: 12px;
    padding: 16px;
  }
  .play-stage__headline {
    font-size: 32px;
    line-height: 40px;
  }
  .play-stage__fan {
    order: 1;
    margin-top: 12px;
  }
  .play-stage__seats {
    gap: 12px;
    margin-top: 0;
  }
  .play-stage__seat {
    gap: 8px;
  }
  /* The show keeps every part it has and gives up the room between them, the
     same bargain the ceremony above makes. The one thing that changes shape is
     the rows list: 480px was a reading measure for a desktop track, and a phone
     has no width to give away, so the rows take the panel.

     The rows list is also the one box on the panel that compresses, and the
     felt it sits on is viewport-fit now rather than as tall as it likes. So it
     gets a floor of exactly two rows — 2 x 48 — which is what it takes to read
     as a list that scrolls rather than as one line and a mystery. Below that
     the panel would sooner overflow its own bottom than swallow the count. */
  .play-show {
    gap: 8px;
    padding: 12px 16px;
  }
  .play-show__body {
    gap: 8px;
  }
  .play-show__name {
    font-size: 24px;
    line-height: 32px;
  }
  .play-show__total-num {
    font-size: 40px;
    line-height: 40px;
  }
  .play-show__rows {
    max-width: none;
    min-height: 96px;
  }
  /* The four cards of a count keep the authored 32px overlap rather than the
     base rule's two sevenths of a card. A phone card is small already, and an
     overlap that shrank with it would leave four cards standing apart where the
     panel means to show one hand. */
  .play-show__hand > .play-card + .play-card {
    margin-left: -32px;
  }
  .play-select {
    padding: 16px;
  }
  .play-select__bot {
    width: 152px;
    padding: 16px 8px;
  }
  .play-select__avatar {
    width: 72px;
    height: 72px;
  }

  /* §End of game. The felt fills the phone's screen on this rung, so the panel
     does too, and the padding comes in to buy the two 140px columns their
     width. Both bands above the CTA tighten by 12: what is added below it is a
     whole promo block, and the panel has to stand in one screen. */
  .play-modal {
    padding: 24px 16px;
  }
  .play-modal__eyebrow {
    font-size: 20px;
    line-height: 24px;
  }
  .play-modal__title {
    font-size: 36px;
    line-height: 44px;
  }
  .play-modal__columns,
  .play-modal__actions {
    margin-top: 24px;
  }
  .play-modal__actions {
    width: 100%;
  }
  .play-modal__cta {
    flex: 1;
    width: 100%;
    min-width: 0;
  }
  /* The rail's promo is a long way below the felt here, so it comes to the
     panel. This is the only rung it appears on, and CSS is the only thing that
     decides — main.js's 767 is still the game's one breakpoint literal. */
  .play-modal__promo {
    display: flex;
  }

  /* §Speed sheet. One group, two presentations: three words on a row wherever
     there is a row for them, and a sheet the trigger opens where there is not.
     A 44px control row has no width for three more targets, and the speed a
     game runs at is chosen once and then left alone — a menu is the right shape
     for a setting like that.

     The sheet used to be a `popover`, gated on `@supports selector(:popover-open)`
     with the inline three as its fallback. Both are gone (W-0104). A popover is
     promoted to the browser's top layer, so it could only ever be centred on the
     VIEWPORT, and every other overlay this game draws is centred on the FELT and
     clipped by it. It is an absolutely-placed box in the surface now — .play is
     its containing block, neither .play__panel nor .play__panel-foot being
     positioned — which needs no feature gate and no fallback, so the foot's wrap
     is simply off on this rung. game.js opens and closes it through .is-open.

     A real sibling backdrop darkens the felt and owns every covered pointer
     event. A painted box-shadow cannot do that. */
  .play__panel-foot {
    flex-wrap: nowrap;
  }
  /* The trigger, on two lines: what the setting is over what it is set to
     (W-0087). It says both because it is the only thing on the row that
     does — the sheet behind it is closed, and a lone word reading "Normal"
     on a control row names nothing. Flexible like the Quit pill beside it, so
     the row does not reshuffle when a longer word is chosen. The caret goes
     with the second line: a two-line control with a live value already reads
     as a setting, and 44px has no room for a third thing. */
  .play__speed-trigger {
    flex: 1 1 0;
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0;
    min-height: 44px;
    padding: 4px 8px;
    border: 1px solid var(--play-seat-off);
    border-radius: var(--play-radius-pill);
    background: var(--play-well);
    color: var(--play-ink);
    font-family: var(--play-font-ui);
    font-weight: 700;
    font-size: 14px;
    line-height: 16px;
    cursor: pointer;
  }
  .play__speed-cap {
    display: block;
    font-weight: 500;
    font-size: 12px;
    line-height: 16px;
    color: var(--play-muted);
  }
  .play__speed-caret {
    display: none;
  }
  .play__speed-trigger:disabled {
    opacity: 0.45;
    cursor: not-allowed;
  }
  .play__speed-trigger:focus-visible {
    outline: 2px solid var(--play-seat-on);
    outline-offset: 4px;
  }
  .play__speed:not(.is-open) {
    display: none;
  }
  .play__speed-backdrop:not([hidden]) {
    position: absolute;
    inset: 0;
    z-index: 5;
    display: block;
    background: var(--play-scrim);
  }
  @supports (backdrop-filter: blur(4px)) {
    .play__speed-backdrop:not([hidden]) {
      backdrop-filter: blur(4px);
    }
  }
  /* The open sheet, centred on the felt over a scrim: the same fixture the
     end-of-game panel is, drawn in the same felt, so the page has one idea of
     what a sheet is and not two. The three options stack and keep their own
     44px minimum, which is what gives the fit-content box its height. z-index 6
     stands it over the backdrop and under the end-of-game panel (7). */
  .play__speed.is-open {
    position: absolute;
    inset: 0;
    z-index: 6;
    margin: auto;
    width: min(320px, calc(100% - 48px));
    height: fit-content;
    flex-direction: column;
    gap: 8px;
    padding: 16px;
    border: 1px solid var(--play-seat-off);
    border-radius: var(--play-radius-card);
    background: linear-gradient(var(--play-felt-top), var(--play-felt-bottom));
    box-shadow: var(--play-shadow-onfelt);
  }

  /* Below the felt. The page gave up its padding at the top of this block so
     the game could run to both edges, and the two things under the game take
     it back for themselves: they are reading, and reading wants a margin. */
  .play-rail {
    margin: 24px 20px 0;
  }
  /* The promo stands back up, but keeps the button: a phone cannot scan its own
     screen, so the QR stays away below 1560 at every width under it. */
  .play-promo {
    flex-direction: column;
    padding: 24px 16px;
    text-align: center;
  }
  .play-promo__shot {
    width: 100%;
    max-width: 268px;
    height: auto;
  }
  .play-promo__copy {
    margin-top: 24px;
  }
  .play-promo__lead {
    max-width: none;
  }
  .play-promo__actions {
    margin-top: 24px;
    width: 100%;
    max-width: 280px;
  }
  .play-seo {
    padding: 32px 20px 64px;
  }
}

/* Preserve card suits while letting system colours carry every control and
   overlay in Windows high-contrast mode. */
@media (forced-colors: active) {
  .play,
  .play-select,
  .play-modal,
  .play__speed-backdrop:not([hidden]),
  .play__speed.is-open {
    background: Canvas;
    color: CanvasText;
  }
  .play-card,
  .play__board > svg {
    forced-color-adjust: none;
  }
  .play-card--slot.is-selected {
    outline: 4px solid Highlight;
    outline-offset: 4px;
  }
  .play-card--slot.is-illegal {
    opacity: 1;
    outline: 2px solid GrayText;
  }
  .play button,
  .play [role='button'] {
    border-color: ButtonText;
  }
}

/* ---------- Motion ----------
   The lift, the deal and the chip glow are all transitions, so one rule turns
   the whole surface still.

   This block cannot reach the flights: the Web Animations API ignores CSS, so
   motion.js and game.js's decorate() carry the same guard in script and are
   the half of the pair that actually stops a card moving. What is named here
   is the CSS half — the riffle, the deck's crossfade, the stage fades — and
   the new classes are named explicitly rather than left to `.play *`, so
   deleting the descendant rule can never quietly re-animate them. */
@media (prefers-reduced-motion: reduce) {
  /* One selector where there used to be two: the end-of-game panel is a child
     of the surface now (W-0104), so `.play *` reaches it and everything in it,
     including its own entrance fade. */
  .play * {
    transition: none !important;
    animation: none !important;
  }
  .play-flight,
  .play-flight .play-ghost,
  .play-deck,
  .play-deck.is-rifling,
  .play-deck__card,
  .play-crib,
  .play-crib__slot,
  .play-stage,
  .play-stage__seat,
  .play-stage__back,
  .play-stage__flier,
  .play-show,
  .play-show__card,
  /* Named as a pseudo-element because the rule above cannot reach one: the
     highlight's dim lives on ::after and would keep its 300ms fade. */
  .play-show__card::after,
  .play-show__starter,
  .play-pill,
  .play-pill__chip {
    transition: none !important;
    animation: none !important;
  }
  /* The hand's hover lift, cancelled. Left ungated on purpose: the base rule
     is now behind (hover: hover) and a :where() scope, both of which cost this
     selector nothing — equal specificity, later in the file, so it still wins,
     and on touch it is a no-op over a lift that can no longer happen (W-0098).
     Adding weight to either selector would break that balance. */
  .play-card--slot:hover:not(:disabled):not(.is-illegal):not(.is-selected) {
    transform: none;
  }
}
