/* 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   76px            status 24 + gap 8 + actions 44
 *
 * 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 strip the backs are dealt across
 *   .play-stage__back          one back in that fan, always a <button>
 *   .play-stage__flier         a cut card between the fan and its slot
 *   .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)
 *   .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
 *   .is-open                   the cut ceremony is on the felt
 *   .is-loser                  the seat that drew high, dimmed (on a stage seat)
 *   .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, 'Hanken Grotesk', '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);

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

/* ---------- 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: from 880px up --card-w is a constant
     112, and below it a function of the viewport alone. */
  --card-w: 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);
  /* 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 204px 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. */
  --play-gutter: 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. */
.play__area {
  position: relative;
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

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

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

/* ---------- Seat chips ----------
   Both seats render from this one block so they cannot drift: fixed 204×92,
   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). */
.play-chip {
  flex: none;
  display: flex;
  align-items: center;
  gap: 16px;
  width: 204px;
  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: 56px;
  height: 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);
}
.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,
    opacity 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. */
.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);
}
.play-card--slot.is-illegal {
  opacity: 0.45;
  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;
}
/* 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 ---------- */
/* 230px is the count column at its tallest: label 16, gap 16, numeral 72, gap
   16, pile 110. The starter column is 192 (fan 160 + gap 16 + label 16) and
   centres inside it. Neither column has a state that changes either number. */
.play__centre {
  flex: none;
  height: 230px;
  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;
}

.play__starter {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}
.play__starter-fan {
  position: relative;
  width: 152px;
  height: 160px;
}
/* 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: 136px;
  width: auto;
}

.play__count {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}
.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. 24 + 8 + 44 = 76,
   whichever of the two buttons is showing, or neither. */
.play__strip {
  flex: none;
  height: 76px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  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. One line at every width the desktop layout
   runs at; the phone reserves two (see §Responsive). */
.play__status {
  margin: 0;
  height: 24px;
  font-size: 16px;
  line-height: 24px;
  text-align: center;
  text-wrap: pretty;
  color: var(--play-lead);
}

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

/* ---------- 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);
}
.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 ---------- */
.play__panel {
  flex: none;
  width: 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);
}

.play__panel-foot {
  flex: none;
  display: flex;
  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: center;
  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 {
  width: min(640px, 100%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
  text-align: center;
}
.play-select__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-select__bots {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 16px;
}
.play-select__bot {
  width: 160px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 20px 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: 72px;
  height: 72px;
  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: 600;
  font-size: 18px;
  line-height: 24px;
}
.play-select__level {
  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 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: 52ch;
  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, in that seat's colour. Blue is the opponent's, so the
   default is theirs and the class names yours. */
.play-crib__label {
  font-family: var(--play-font-mono);
  font-weight: 500;
  font-size: 12px;
  line-height: 16px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--play-blue);
}
.play-crib.is-yours .play-crib__label {
  color: var(--play-red);
}
/* 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.

   The label sits BESIDE the fan rather than under it, which is the one place
   this fixture departs from its sketch: below the opponent's plaque there are
   97px of gutter before the starter fan begins, and a stacked label needs 100.
   Beside, the dock is 72 tall and clears it. */
.play-crib--dealer-bot,
.play-crib--dealer-you {
  flex-direction: row;
  gap: 8px;
}
.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: 32px;
  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: 36px;
  line-height: 44px;
  color: var(--play-ink);
}
.play-stage__sub {
  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);
}

/* 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. */
.play-stage__seats {
  display: flex;
  align-items: flex-start;
  gap: 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 fan. Every back is a real button on a 44px step, absolutely placed from
   one transform each (stages.js writes it), so a back leaving is a transform
   change on the ones that are left and the gap closes itself. The strip is one
   card tall; the tilt overhangs it and the stage's overflow catches it. */
.play-stage__fan {
  position: relative;
  width: 100%;
  height: var(--card-h);
}
.play-stage__back {
  position: absolute;
  top: 0;
  left: 50%;
  margin-left: calc(var(--card-w) / -2);
  cursor: pointer;
  transition: transform var(--play-dur-chrome) var(--play-ease-arrive);
}
.play-stage__back > svg {
  display: block;
  width: 100%;
  height: 100%;
}
/* Hover lights the back where a hand card would raise: the raise is the
   transform, and the transform is the fan's own position. */
.play-stage__back:hover:not(:disabled) {
  box-shadow:
    0 0 0 1px var(--play-seat-on),
    0 0 16px rgba(243, 249, 255, 0.35),
    var(--play-shadow-card);
}
.play-stage__back:focus-visible {
  z-index: 1;
  outline: 2px solid var(--play-seat-on);
  outline-offset: 4px;
}
.play-stage__back:disabled {
  cursor: default;
}

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

/* ---------- 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 pile below them
                   is the show's own five cards and stays.
   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: upsell + reserved ad slot ---------- */
.play-rail {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.play-upsell {
  padding: 16px;
  border-radius: var(--play-radius-card);
  background: linear-gradient(var(--play-felt-top), var(--play-felt-bottom));
  color: var(--play-ink);
}
.play-upsell__mark {
  display: block;
  height: 18px;
  width: auto;
  margin: 0 0 12px;
}
.play-upsell__body {
  display: flex;
  align-items: center;
  gap: 16px;
}
.play-upsell__shot {
  flex: none;
  width: 88px;
  height: 136px;
  border-radius: 8px;
  object-fit: cover;
  object-position: top center;
}
.play-upsell__copy {
  min-width: 0;
}
.play-upsell__claim {
  display: block;
  margin: 0 0 12px;
  font-weight: 600;
  font-size: 16px;
  line-height: 24px;
  text-wrap: pretty;
}

/* Deliberately empty, at exact ad-unit dimensions so a future network fills it
   with zero reflow (W-0067 §7, owner's call). No ad network exists today. */
.play-ad {
  width: 300px;
  height: 600px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: 1px dashed var(--play-edge);
  border-radius: var(--play-radius-figure);
  background: var(--play-linen);
}
.play-ad__label,
.play-ad__size {
  font-family: var(--play-font-mono);
  font-weight: 500;
  font-size: 12px;
  line-height: 16px;
  text-transform: uppercase;
  color: var(--play-muted);
}
.play-ad__label {
  letter-spacing: 0.22em;
}
.play-ad__size {
  letter-spacing: 0.12em;
}

/* ---------- 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. The win dialog is a <dialog> in the top layer, so
   it still paints over this. */
.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;
}

/* ---------- Win / loss modal ---------- */
.play-modal {
  width: min(480px, calc(100% - 32px));
  max-height: calc(100dvh - 32px);
  overflow-y: auto;
  padding: 0;
  border: 0;
  border-radius: 20px;
  background: linear-gradient(var(--play-felt-top), var(--play-felt-bottom));
  color: var(--play-ink);
  box-shadow: var(--play-shadow-onfelt);
}
.play-modal::backdrop {
  background: var(--play-scrim);
}
.play-modal__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  padding: 32px;
  text-align: center;
}
.play-modal__title {
  margin: 0;
  font-family: var(--play-font-display);
  font-weight: 400;
  font-size: 36px;
  line-height: 44px;
  color: var(--play-ink);
}
.play-modal__score {
  font-family: var(--play-font-display);
  font-weight: 400;
  font-size: 40px;
  line-height: 44px;
  font-variant-numeric: tabular-nums;
}
.play-modal__skunk {
  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-blue);
}
.play-modal__actions {
  display: flex;
  gap: 16px;
}
.play-modal__upsell {
  display: flex;
  align-items: center;
  gap: 16px;
  width: 100%;
  margin-top: 8px;
  padding-top: 24px;
  border-top: 1px solid var(--play-hairline);
  text-align: left;
}
.play-modal__shot {
  flex: none;
  width: 88px;
  height: 136px;
  border-radius: 8px;
  object-fit: cover;
  object-position: top center;
}

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

/* ---------- Responsive ladder ----------
   Full layout needs ~1560px. Below that: drop the rail out of the grid (the
   upsell moves under the game, the ad slot goes), then hide the visual board
   (the scores survive as a compact strip), then single column with the seat
   plaques back in the flow. A card's size is a function of the screen width and
   of nothing else, so each rung is one constant surface height. */

/* 1: the rail leaves the grid. */
@media (max-width: 1559px) {
  .play-shell {
    grid-template-columns: minmax(0, 1fr);
  }
  .play-rail {
    flex-direction: row;
    align-items: stretch;
    gap: 24px;
  }
  .play-upsell {
    flex: 1;
  }
  .play-upsell__body {
    justify-content: center;
  }
  .play-ad {
    display: none;
  }
}

/* 2: the board panel becomes a compact score strip above the play area. One DOM
   copy, two placements — the panel flips to a row and its board hides, so the
   scores and New game stay reachable without a second set of score elements. */
@media (max-width: 1299px) {
  .play {
    flex-direction: column;
  }
  .play__panel {
    order: -1;
    width: auto;
    flex-direction: row;
    align-items: center;
    gap: 16px;
    padding: 12px 16px;
  }
  .play__scores {
    flex: 1;
  }
  .play__board {
    display: none;
  }
  .play-score {
    flex-direction: row;
    justify-content: center;
    gap: 12px;
    padding: 8px 16px;
  }
  .play-score__num {
    font-size: 32px;
    line-height: 36px;
  }
}

/* 3: single column, fluid cards, compact chips. --card-w drives every card on
   the surface, so nothing here has to know a card's pixel size.

   The seat chips leave the corners and rejoin the flow here as compact bars,
   each one directly above its own hand. That is the order they are written in,
   so nothing has to be re-ordered — and the rows give up their gutter with them.
   A corner pin would spend 224px of a ~300px track on a plaque, and the hand
   needs that width more than the corner does. The heights are still all fixed,
   so the surface is still a function of the screen width alone. */
@media (max-width: 879px) {
  .play {
    --card-w: clamp(64px, 14vw, 112px);
    --hand-gap: 8px;
    --pile-inset: 8px;
    --play-gutter: 0px;
    gap: 16px;
    padding: 16px;
  }
  /* Starter beside the count rather than above it: stacking them spends about
     150px of a screen that has none to give. 60 is the count column around the
     pile — label 16, gap 16, numeral 56, gap 16 — and 160 is the starter column,
     whichever is taller. */
  .play__centre {
    grid-template-columns: 120px minmax(0, 1fr);
    gap: 16px;
    height: max(160px, calc(var(--card-h) + 60px));
    justify-items: center;
  }
  /* justify-items: center would shrink the count column to fit-content, and in
     slots mode the pile's absolutely-placed cards have no intrinsic width — the
     8 slots would collapse to the numeral's width. Stretch the column; its own
     align-items keeps the label and numeral centred. */
  .play__count {
    justify-self: stretch;
  }
  /* Two lines of status reserved. The longest things the game says do not fit
     on one line in a ~300px track, and the strip may not change height when one
     of them arrives. */
  .play__strip {
    height: 100px;
  }
  .play__status {
    height: 48px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .play__starter-fan {
    width: 120px;
    height: 128px;
  }
  .play-card--starter {
    height: 112px;
  }
  .play__count-num {
    font-size: 56px;
    line-height: 56px;
  }
  .play-chip {
    position: static;
    width: auto;
    height: 64px;
    padding: 8px 12px;
    gap: 12px;
  }
  .play-chip__avatar {
    width: 40px;
    height: 40px;
  }
  /* No gutter column left to dock the crib in, so the fan nests inside the
     dealer's own chip bar instead — right end, vertically centred, at half
     size. game.js reads the gutter to decide, so this rung and that decision
     are the same rung. The label goes: at 32px a back is a marker, and the bar
     it is sitting in already says whose it is. */
  .play-crib--dealer-bot .play-crib__slots,
  .play-crib--dealer-you .play-crib__slots {
    width: 56px;
    height: 48px;
  }
  .play-crib--dealer-bot .play-crib__slot,
  .play-crib--dealer-you .play-crib__slot {
    left: calc(var(--i) * 8px);
    width: 32px;
    height: 44px;
  }
  .play-crib--dealer-bot .play-crib__label,
  .play-crib--dealer-you .play-crib__label {
    display: none;
  }
  /* The ceremony keeps every part it has and gives up the room between them:
     the plaques are already compact here (they are .play-chip), and the fan
     derives its own count from the track, so a phone draws six or seven backs
     where a desktop draws twenty. */
  .play-stage {
    gap: 20px;
    padding: 16px;
  }
  .play-stage__headline {
    font-size: 28px;
    line-height: 36px;
  }
  .play-stage__seats {
    gap: 12px;
  }
  .play-stage__seat {
    gap: 8px;
  }
  .play-select {
    padding: 16px;
  }
  .play-select__bot {
    width: 128px;
    padding: 16px 8px;
  }
  .play-select__avatar {
    width: 56px;
    height: 56px;
  }
  .play-rail {
    flex-direction: column;
  }
  .play-seo {
    padding: 32px 0 0;
  }
}

@media (max-width: 767px) {
  .play-page {
    padding: 16px 20px 64px;
  }
  .play-banner__text {
    font-size: 14px;
    line-height: 20px;
  }
}

/* 4: the score strip stacks. Two score tiles, New game and mute need ~458px of
   panel; a phone leaves ~300, and the surface clips its overflow, so on a
   375px screen New game and mute were off the right edge and unreachable. The
   panel goes back to a column here — scores on one line, the two controls on
   the next — and the tiles go back to label-over-number, which is what lets
   OPPONENT and its score fit side by side down to 320px. */
@media (max-width: 599px) {
  .play__panel {
    flex-direction: column;
    align-items: stretch;
  }
  .play-score {
    flex-direction: column;
    gap: 4px;
    padding: 8px 12px;
  }
}

/* ---------- 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) {
  .play *,
  .play-modal * {
    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 {
    transition: none !important;
    animation: none !important;
  }
  .play-card--slot:hover:not(:disabled):not(.is-illegal):not(.is-selected) {
    transform: none;
  }
}
