/* ==========================================================================
   CardPorch — game.css
   The euchre table: felt, seats, cards, trick area, counter scoreboard,
   bid panel, hint line, table controls.
   Class contract per ENGINE_API.md §6.3 — .card, .card--back, .card--playable,
   .card--illegal, .card--played, .seat--active, .hand, .trick-area,
   .scoreboard, .bid-panel, .hint. Every selector below matches either the
   server-rendered DOM in templates/euchre/game.html or the nodes built by
   static/js/ui/table.js and static/js/main.js — the two must look the same,
   so hydration shifts nothing.
   Loads after site.css and reuses its tokens.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Table shell, card sizing tokens, felt
   --card-base scales with the viewport; --card-scale is the player's card-size
   setting (table.js sets data-card-size on #game-root).
   -------------------------------------------------------------------------- */
.table {
  --card-base: 72px; /* spec floor: >= 72px wide on mobile */
  --card-scale: 1;
  --card-w: calc(var(--card-base) * var(--card-scale));
  --card-h: calc(var(--card-w) * 1.4);
  --card-r: 6px;
  --card-shadow: 0 1px 2px rgba(0, 0, 0, 0.32), 0 4px 10px rgba(0, 0, 0, 0.2);
  --hand-step: -26px; /* negative = fanned overlap; positive = spread out */
  --felt-dark: #113526;
  --suit-red: #a03325;
  --gold: #ffd766;
  --slide: 200ms;

  position: relative;
  display: grid;
  grid-template-columns: minmax(58px, 15%) minmax(0, 1fr) minmax(58px, 15%);
  grid-template-areas:
    "score  score   score"
    "north  north   north"
    "west   center  east"
    "hint   hint    hint"
    "south  south   south";
  gap: var(--sp-2);
  align-content: space-between;
  /* The felt is capped rather than filling the column: a tighter table reads
     better, and it leaves a rail free on each side of the game for a banner
     (see .game-rail in templates/euchre/game.html). --card-base is capped to
     match, since a fixed-width table has no use for ever-larger cards. */
  max-width: var(--table-max, 720px);
  min-height: clamp(400px, 48vh, 540px);
  margin: 0 auto var(--sp-2); /* the button row sits right below the felt */
  padding: var(--sp-2);
  overflow: hidden;
  border: 8px solid var(--wood);
  border-radius: var(--r-lg);
  background: radial-gradient(120% 85% at 50% 36%, var(--porch-green-light) 0%, var(--porch-green) 56%, var(--felt-dark) 100%);
  box-shadow:
    inset 0 0 0 2px var(--wood-dark),
    inset 0 6px 24px rgba(0, 0, 0, 0.28),
    var(--shadow-2);
  color: var(--cream);
}

.table[data-card-size="small"] {
  --card-scale: 0.86;
}

.table[data-card-size="large"] {
  --card-scale: 1.16;
}

@media (min-width: 480px) {
  .table {
    --card-base: 80px;
    --hand-step: -8px;
  }
}

/* The table stops growing here — it is already at --table-max, so there is no
   wider viewport step. Cards stay at a size that fits the capped felt. */
@media (min-width: 700px) {
  .table {
    --card-base: 76px;
    --hand-step: 2px;
    gap: 6px var(--sp-3);
    padding: 10px;
    border-width: 6px;
    /* Fixed side columns: the felt is at --table-max here, so percentages only
       made the side seats spindly. The centre keeps ~350px, far more than the
       trick area needs. */
    --side-col: 208px;
    grid-template-columns: var(--side-col) minmax(0, 1fr) var(--side-col);
    /* The score panel shares the top row with North rather than overlaying the
       felt: an absolutely-placed panel could not be made collision-proof — a
       North packed with badges slid under it, and its own bottom edge met the
       East seat. In its own cell that is structurally impossible, and it costs
       nothing, because the compact panel is no taller than the North seat. */
    grid-template-areas:
      "north  north   score"
      "west   center  east"
      "hint   hint    hint"
      "south  south   south";
  }
}


/* --------------------------------------------------------------------------
   1b. Game stage — the felt with a reserved ad rail on each side
   The rails are display:none below 1120px so a narrow screen gives the whole
   width to the game; above it they hold their 160x600 footprint whether or not
   ads are enabled, so turning ads on causes no shift (spec §7: zero CLS).
   -------------------------------------------------------------------------- */
.game-stage {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: var(--sp-4);
}

/* The felt's column: the table and its button row, stacked. Sized here (not on
   the table) so the buttons share the felt's width and sit right under it,
   inside the stage — after the stage they would land below the 600px rails. */
.game-stage__centre {
  flex: 0 1 var(--table-max, 720px);
  min-width: 0;
  display: flex;
  flex-direction: column;
}

/* Explicit width, because the felt's own `margin: 0 auto` is a cross-axis auto
   margin inside this column flex — and auto cross margins cancel the default
   stretch, shrinking the table to fit its content instead of the column. */
.game-stage__centre > .table {
  width: 100%;
}

.game-rail {
  display: none;
  flex: none;
  width: 160px;
  position: sticky;
  top: var(--sp-4);
}

@media (min-width: 1120px) {
  .game-rail {
    display: block;
  }
}

/* --------------------------------------------------------------------------
   2. Seats
   -------------------------------------------------------------------------- */
/* Each seat hugs its own content, so the active ring outlines the player
   rather than the whole width of the felt. */
.seat {
  justify-self: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  max-width: 100%;
  padding: var(--sp-2);
  border-radius: var(--r-md);
}

.seat--north {
  grid-area: north;
}

.seat--west {
  grid-area: west;
}

.seat--east {
  grid-area: east;
}

.seat--south {
  grid-area: south;
}

.seat__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1) 6px;
}

.seat__name {
  font-family: var(--font-serif);
  font-size: var(--fs-sm);
  font-weight: 700;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}

.seat__count {
  font-size: var(--fs-xs);
  color: var(--cream);
  opacity: 0.88;
}

.seat__badge,
.seat__tricks,
.seat__bid {
  display: inline-flex;
  align-items: center;
  min-height: 26px;
  padding: 0 10px;
  border-radius: var(--r-pill);
  font-size: var(--fs-xs);
  font-weight: 700;
  white-space: nowrap;
}

.seat__badge {
  background: rgba(247, 243, 232, 0.18);
  box-shadow: inset 0 0 0 1px rgba(247, 243, 232, 0.65);
  color: var(--cream);
}

.seat__tricks {
  min-width: 26px;
  justify-content: center;
  background: var(--cream);
  color: var(--ink);
}

.seat__tricks:empty {
  display: none;
}

/* The bid a seat just made, flashed next to their name by table.js. */
.seat__bid {
  background: var(--gold);
  color: var(--ink);
}

/* Role badges, added and removed by table.js as the deal moves round. */
.seat__badge--dealer {
  background: var(--wood);
  box-shadow: inset 0 0 0 1px var(--wood-dark);
}

.seat__badge--maker {
  background: var(--gold);
  box-shadow: inset 0 0 0 1px rgba(38, 34, 27, 0.35);
  color: var(--ink);
}

.seat__badge--out {
  background: var(--felt-dark);
  box-shadow: inset 0 0 0 1px rgba(247, 243, 232, 0.4);
}

.seat--out .seat__cards {
  opacity: 0.6;
}

/* Whose turn it is — ring plus a highlighted name, never colour alone. */
/* The ring is a box-shadow, so it paints OUTSIDE the seat box and needs
   clearance rather than padding. Trimmed from 6px to 5px so it clears the
   neighbouring rows at the current row gap. */
/* The turn cue lives on the AVATAR's ring, because a box-shadow occupies no
   layout space — the earlier gold name pill added padding and a marker glyph,
   which re-centred the whole chip row and visibly nudged the avatar every time
   the turn moved. Nothing about this rule can shift a pixel. */
.seat--active {
  box-shadow: none;
}

.seat--active .seat__avatar {
  border-color: var(--gold);
  box-shadow: 0 0 0 3px var(--gold), 0 1px 4px rgba(0, 0, 0, 0.3);
}

/* South is boxed in: the hint bubble sits directly above it and the felt edge
   directly below, and `overflow: hidden` on the table was clipping the ring. */
.seat--south {
  margin-top: 8px;
  margin-bottom: 8px;
}

/* North's ring came within 1px of the card the North seat plays into the trick,
   which lands at the very top of the trick area. */
.seat--north {
  margin-bottom: 5px;
}

/* Hidden hands: half-size backs, overlapped. --card-w has to be restated
   rather than just rescaled — custom properties substitute where they are
   declared, so changing --card-scale here would not reach it. */
.seat--north,
.seat--west,
.seat--east {
  --card-w: calc(var(--card-base) * 0.5);
  --card-h: calc(var(--card-w) * 1.4);
}

/* A hidden hand shrinks from five backs to none over a hand. Its box is sized
   for the full five either way: without this the seat resizes on every card
   played, which resizes the grid row, which resizes the whole felt. Cards are
   flex: none so the reserved box cannot squash them instead. */
.seat__cards {
  display: flex;
  flex: none;
}

.seat__cards > .card {
  flex: none;
}

.seat--north .seat__cards {
  width: calc(var(--card-w) * 2.8); /* 1 card + 4 overlaps of 0.45 */
  height: var(--card-h); /* holds the row once the last card is played */
}

.seat--north .seat__cards > .card + .card {
  margin-left: calc(var(--card-w) * -0.55);
}

/* The side columns are narrow: stack each chip on its own line. */
.seat--west .seat__meta,
.seat--east .seat__meta {
  flex-direction: column;
  gap: var(--sp-1);
  /* Role chips come and go (the dealer moves each hand, Maker/Alone/Sitting out
     appear when trump is named). In a column each one is a whole line, so the
     block is reserved for its busiest state — name + dealer + maker + tricks —
     and chips fill it from the top. */
  min-height: 124px;
  justify-content: flex-start;
}

/* From 700px the side columns are a fixed 150px (§1), wide enough for the role
   chips to sit two-up — three lines instead of four. The side seats set the
   height of the middle row, so this is where the table's height is won.
   Must come after the rule above: same specificity, so source order decides. */
@media (min-width: 700px) {
  .seat--west .seat__meta,
  .seat--east .seat__meta {
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
    min-height: 58px; /* two lines of chips */
  }
}

.seat--west .seat__cards,
.seat--east .seat__cards {
  flex-direction: column;
  height: calc(var(--card-h) * 2.12); /* 1 card + 4 overlaps of 0.28 */
}

.seat--west .seat__cards > .card + .card,
.seat--east .seat__cards > .card + .card {
  margin-top: calc(var(--card-h) * -0.72);
}

/* The side columns are narrow and every chip there costs a whole line, so the
   count is left to the card backs — which show it directly — and kept for
   screen readers only. (The backs themselves are aria-hidden.) */
.seat--west .seat__count,
.seat--east .seat__count {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The bid flash is transient. Floating it out of flow means a seat cannot
   change height the moment someone bids. */
.seat--west .seat__bid,
.seat--east .seat__bid {
  position: absolute;
  left: 50%;
  top: 0;
  transform: translate(-50%, -100%);
  z-index: 5;
}

.seat--west,
.seat--east {
  position: relative;
}

/* --------------------------------------------------------------------------
   3. Cards
   .card is a <button> in the player's hand and a <span> everywhere else.
   -------------------------------------------------------------------------- */
.card {
  display: block;
  position: relative;
  flex: none;
  inline-size: var(--card-w);
  block-size: var(--card-h);
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: var(--card-r);
  background: var(--paper);
  box-shadow: var(--card-shadow);
  overflow: hidden;
  color: var(--ink);
  font: inherit;
  cursor: default;
  -webkit-appearance: none;
  appearance: none;
  -webkit-tap-highlight-color: transparent;
  transition: transform 140ms ease, filter 140ms ease, opacity 140ms ease;
}

.card__face {
  display: block;
  width: 100%;
  height: 100%;
}

/* Drawn in CSS so the backs are right before the sprite loads and behind it
   afterwards — the server-rendered backs carry no SVG at all. */
.card--back {
  background-color: var(--porch-green);
  background-image:
    radial-gradient(circle at 50% 50%, rgba(247, 243, 232, 0.3) 0 17%, transparent 18%),
    repeating-linear-gradient(45deg, var(--porch-green) 0 6px, var(--porch-green-dark) 6px 12px);
  box-shadow: inset 0 0 0 3px var(--cream), inset 0 0 0 4px var(--porch-green-dark), var(--card-shadow);
}

/* On the felt the focus ring inverts: cream core, dark halo (site.css §2). */
.card:focus {
  --focus-core: var(--paper);
  --focus-halo: var(--felt-dark);
  z-index: 6;
}

.card--playable {
  cursor: pointer;
}

/* Illegal cards stay focusable and clickable so the tap can explain itself
   in the hint line, never fail silently.
   Dimmed with an overlay, NOT opacity: a translucent card in an overlapped fan
   shows the card behind through its own face, which reads as a rendering bug.
   The card stays opaque and a dark veil sits on top of it instead. */
.card--illegal {
  position: relative;
  cursor: not-allowed;
  filter: grayscale(0.45);
}

.card--illegal::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--card-r);
  background: rgba(0, 0, 0, 0.42);
}

/* --------------------------------------------------------------------------
   4. The player's hand — fanned along the bottom
   -------------------------------------------------------------------------- */
.hand {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  margin: 0;
  padding-top: 20px; /* headroom for the hover/focus lift */
  /* The outer cards of the fan are arced down and rotated, which puts them
     ~11px below the flex line. Without this they spill past the seat box and
     straight through the active ring drawn on its edge. */
  padding-bottom: 12px;
  min-height: calc(var(--card-h) + 32px); /* reserved before JS: no CLS */
}

.hand > .card {
  --rot: 0deg;
  --arc: 0px;
  --lift: 0px;
  z-index: 1;
  transform: translateY(calc(var(--arc) + var(--lift))) rotate(var(--rot));
  transform-origin: 50% 130%;
}

.hand > .card + .card {
  margin-left: var(--hand-step);
}

/* Later children paint on top, so every card's left index stays visible. */
.hand > .card:nth-child(1) {
  --rot: -6deg;
  --arc: 7px;
}

.hand > .card:nth-child(2) {
  --rot: -3deg;
  --arc: 2px;
}

.hand > .card:nth-child(4) {
  --rot: 3deg;
  --arc: 2px;
}

.hand > .card:nth-child(5) {
  --rot: 6deg;
  --arc: 7px;
}

/* A sixth card exists only while the dealer holds the upcard before discarding.
   Its arc matches the outermost regular card: any deeper and the fan outgrows
   the hand's reserved height, so picking up the upcard bounced the whole felt
   by 16px until the discard. */
.hand > .card:nth-child(6) {
  --rot: 9deg;
  --arc: 7px;
}

.hand > .card--playable:hover,
.hand > .card:focus {
  --lift: -18px;
  z-index: 6;
}

.hand > .card--playable:active {
  --lift: -8px;
}

.hand__note {
  align-self: center;
  padding: var(--sp-2) var(--sp-4);
  border-radius: var(--r-pill);
  background: rgba(17, 53, 38, 0.8);
  color: var(--cream);
  font-size: var(--fs-sm);
  text-align: center;
}

/* --------------------------------------------------------------------------
   5. Trick area
   table.js sets data-seat="0..3" on each played card, so it lands in front of
   the seat that played it, and data-gather="<winner>" for 260 ms afterwards.
   -------------------------------------------------------------------------- */
.trick-area {
  grid-area: center;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: calc(var(--card-h) * 1.6); /* reserved before JS: no CLS */
}

.trick-area > .card--played {
  position: absolute;
  left: 50%;
  top: 50%;
  margin: calc(var(--card-h) / -2) 0 0 calc(var(--card-w) / -2);
  box-shadow: var(--card-shadow), 0 8px 16px rgba(0, 0, 0, 0.3);
  animation: cp-play-in var(--slide) ease-out;
}

.trick-area > [data-seat="0"] {
  --from: translateY(190%);
  --to: translate(0, 200%) scale(0.84);
  transform: translateY(42%) rotate(2deg);
}

.trick-area > [data-seat="1"] {
  --from: translateX(-230%);
  --to: translate(-250%, 0) scale(0.84);
  transform: translateX(-58%) rotate(-5deg);
}

.trick-area > [data-seat="2"] {
  --from: translateY(-190%);
  --to: translate(0, -200%) scale(0.84);
  transform: translateY(-42%) rotate(-2deg);
}

.trick-area > [data-seat="3"] {
  --from: translateX(230%);
  --to: translate(250%, 0) scale(0.84);
  transform: translateX(58%) rotate(5deg);
}

/* The winner sweeps the trick to their own seat. */
/* Cards that arrived via the play-flight skip the entry animation only. This
   rule must stay ABOVE the gather rules: same specificity, so source order
   lets the gather sweep take over when the trick is claimed. */
.trick-area > .card--played[data-no-entry] {
  animation: none;
}

.trick-area[data-gather] > .card--played {
  animation: cp-gather 380ms ease-in forwards;
}

.trick-area[data-gather="0"] > .card--played {
  --to: translate(0, 200%) scale(0.45);
}

.trick-area[data-gather="1"] > .card--played {
  --to: translate(-250%, 0) scale(0.45);
}

.trick-area[data-gather="2"] > .card--played {
  --to: translate(0, -200%) scale(0.45);
}

.trick-area[data-gather="3"] > .card--played {
  --to: translate(250%, 0) scale(0.45);
}

/* The upcard, shown in the middle while trump is still being decided. */
.trick-area > [data-upcard] {
  transform: rotate(-4deg);
  box-shadow: var(--card-shadow), 0 0 0 3px rgba(255, 215, 102, 0.6);
}

.trick-area > [data-upcard="down"] {
  filter: grayscale(0.5);
  opacity: 0.75;
  box-shadow: var(--card-shadow);
}

@keyframes cp-play-in {
  from {
    opacity: 0;
    transform: var(--from, translateY(60%)) scale(0.94);
  }
}

@keyframes cp-gather {
  to {
    opacity: 0;
    transform: var(--to, scale(0.84));
  }
}

/* --------------------------------------------------------------------------
   6. Scoreboard — the traditional euchre counters
   Two cards, one laid across the other, the way the score is really kept at a
   kitchen table. Drawn with a pseudo-element so the markup stays tiny.
   Styles both the server-rendered rows and the counters table.js renders.
   -------------------------------------------------------------------------- */
.scoreboard {
  grid-area: score;
  position: relative;
  z-index: 4; /* also the stacking context the counter's under-card sits in */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2) var(--sp-4);
  min-height: 72px; /* slim bar height is fixed: hydration shifts nothing */
  margin: 0;
  padding: var(--sp-2) var(--sp-3);
  background: var(--paper);
  border: 2px solid var(--wood);
  border-radius: var(--r-md);
  color: var(--ink);
  box-shadow: var(--shadow-1);
}

/* The gap leaves room for the counter's under-card, which leans out to the left. */
.scoreboard__row,
.counter {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}

.scoreboard__counters,
.scoreboard__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2) var(--sp-4);
}

.scoreboard__team,
.counter__label {
  max-width: 5.5em;
  font-size: var(--fs-xs);
  font-weight: 700;
  line-height: 1.25;
  color: var(--ink-soft);
}

/* The counter itself: the score card, with its partner card behind it. */
.scoreboard__score,
.counter__value {
  position: relative;
  display: grid;
  place-items: center;
  width: 38px;
  height: 52px;
  border-radius: var(--r-sm);
  background: var(--paper);
  box-shadow: inset 0 0 0 1px rgba(38, 34, 27, 0.55), 0 1px 3px rgba(0, 0, 0, 0.25);
  font-family: var(--font-serif);
  font-size: 1.4rem;
  font-weight: 700;
  line-height: 1;
  color: var(--ink);
}

.scoreboard__score::before,
.counter__value::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  transform: rotate(-13deg) translate(-7px, 2px);
  border-radius: var(--r-sm);
  background-color: var(--paper);
  background-image:
    radial-gradient(circle at 15% 22%, var(--suit-red) 0 2.6px, transparent 2.7px),
    radial-gradient(circle at 15% 78%, var(--suit-red) 0 2.6px, transparent 2.7px);
  box-shadow: inset 0 0 0 1px rgba(38, 34, 27, 0.55), 0 1px 3px rgba(0, 0, 0, 0.25);
}

.scoreboard__trump,
.trump-indicator {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  margin: 0;
  padding: 0 var(--sp-3);
  background: var(--cream-deep);
  border: 2px solid var(--wood);
  border-radius: var(--r-pill);
  font-size: var(--fs-xs);
  font-weight: 700;
  white-space: nowrap;
}

.trump-indicator[data-suit="H"],
.trump-indicator[data-suit="D"] {
  color: var(--suit-red);
}

.scoreboard__hand-tricks {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--ink-soft);
  white-space: nowrap;
}

/* Pre-hydration placeholder: a per-team trick count that is always zero and
   that table.js replaces with a combined one. Hiding it also keeps the board
   the same height before and after the first render — nothing jumps. */
.scoreboard__tricks {
  display: none;
}

.scoreboard__title,
.scoreboard__target {
  display: none; /* the slim bar keeps only the score and trump */
}

/* Small phones: tighten the counters so both still share one line. */
@media (max-width: 400px) {
  .scoreboard__team,
  .counter__label {
    max-width: 4em;
  }

  .scoreboard__row,
  .counter,
  .scoreboard__counters,
  .scoreboard__meta {
    gap: var(--sp-2);
  }
}

@media (min-width: 560px) {
  .scoreboard__team,
  .counter__label {
    max-width: 7.5em;
  }

  .scoreboard__score,
  .counter__value {
    width: 42px;
    height: 58px;
    font-size: 1.6rem;
  }
}

/* The felt is capped at --table-max, so it never gets wide enough for a
   three-column layout with a scoreboard panel in the corner: squeezed into a
   ~150px column the panel wrapped to over 300px tall and drove the whole table
   taller than it was before the cap. The slim bar in §6 is used at every width
   instead — shorter, and it can never land on a seat. */

/* --------------------------------------------------------------------------
   7. Bid panel — explicit buttons, overlaid so opening it shifts nothing
   -------------------------------------------------------------------------- */
.bid-panel {
  position: absolute;
  left: 50%;
  top: 46%;
  transform: translate(-50%, -50%);
  z-index: 20;
  width: min(94%, 460px);
  padding: var(--sp-4);
  background: var(--paper);
  border: 3px solid var(--wood);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-2), 0 0 0 100vmax rgba(17, 53, 38, 0.4);
  color: var(--ink);
}

.bid-panel__prompt {
  margin: 0 0 var(--sp-3);
  font-family: var(--font-serif);
  font-size: var(--fs-md);
  font-weight: 700;
  text-align: center;
  color: var(--porch-green-dark);
}

.bid-panel__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-2);
}

.bid-panel .btn {
  flex: 1 1 44%;
  padding-inline: var(--sp-3);
  font-size: var(--fs-sm);
}

.bid-panel .btn[data-move="pass"] {
  flex-basis: 100%;
}

/* Suit glyphs sit on a light chip so they read on a green button. The suit is
   on the button (table.js), so the colour is keyed off the button. */
.bid-panel .suit {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: 1.7em;
  height: 1.7em;
  border-radius: 50%;
  background: var(--paper);
  color: var(--ink);
  line-height: 1;
}

.bid-panel .btn[data-suit="H"] .suit,
.bid-panel .btn[data-suit="D"] .suit {
  color: var(--suit-red);
}

.bid-panel__alone {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: var(--touch);
  margin-top: var(--sp-3);
  font-size: var(--fs-sm);
  font-weight: 600;
  cursor: pointer;
}

.bid-panel__alone input {
  flex: none;
  width: 26px;
  height: 26px;
  accent-color: var(--porch-green);
}

/* --------------------------------------------------------------------------
   8. Hint line — holds its row whether or not it has text
   -------------------------------------------------------------------------- */
.hint {
  grid-area: hint;
  justify-self: center;
  display: flex;
  align-items: center;
  justify-content: center;
  max-width: 100%;
  /* Two lines are always reserved. The text changes on every event and the long
     ones ("Euchred! West & East took only 2 …") wrap, so a one-line box would
     grow and shrink under the table on almost every move. */
  min-height: calc(var(--sp-2) * 2 + 3em);
  margin: 0;
  padding: var(--sp-2) var(--sp-4);
  background: var(--paper);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-1);
  color: var(--ink);
  font-size: var(--fs-sm);
  font-weight: 600;
  line-height: 1.4;
  text-align: center;
}

.hint:empty {
  visibility: hidden;
}

.hint--warn {
  background: var(--alert-red);
  color: #fff;
}

/* --------------------------------------------------------------------------
   9. Controls under the table, settings, dialogs
   -------------------------------------------------------------------------- */
.game-controls,
.game-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--sp-3);
  margin: 0 0 var(--sp-5);
}

.game-settings > summary {
  list-style: none;
}

.game-settings > summary::-webkit-details-marker {
  display: none;
}

.game-settings__grid {
  display: grid;
  gap: var(--sp-2);
  margin-top: var(--sp-3);
  padding: var(--sp-4);
  background: var(--paper);
  border: 2px solid var(--rule);
  border-radius: var(--r-md);
}

.game-settings label {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: var(--touch);
  font-size: var(--fs-sm);
  font-weight: 600;
}

.game-settings input[type="checkbox"] {
  flex: none;
  width: 26px;
  height: 26px;
  accent-color: var(--porch-green);
}

.game-settings select,
.cp-dialog select {
  min-height: 44px;
  padding: 0 var(--sp-2);
  background: var(--paper);
  border: 2px solid var(--wood);
  border-radius: var(--r-sm);
}

/* Stats and settings dialogs, built by main.js. */
.cp-dialog {
  width: min(92vw, 460px);
  padding: var(--sp-5);
  background: var(--paper);
  border: 3px solid var(--wood);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-2);
  color: var(--ink);
}

.cp-dialog::backdrop {
  background: rgba(38, 34, 27, 0.55);
}

.cp-dialog h2 {
  margin-top: 0;
  padding-right: var(--touch);
}

.cp-dialog label {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: var(--touch);
}

.dialog__close {
  position: absolute;
  top: var(--sp-2);
  right: var(--sp-2);
  min-width: var(--touch);
  min-height: var(--touch);
  border: 0;
  border-radius: var(--r-pill);
  background: transparent;
  font-size: 1.5rem;
  line-height: 1;
}

.dialog__close:hover {
  background: var(--cream-deep);
}

.stats-list {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--sp-2) var(--sp-4);
  margin: 0;
  padding: 0;
  border: 0;
}

.stats-list dt {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
  font-weight: 400;
  color: var(--ink-soft);
}

.stats-list dd {
  margin: 0;
  font-family: var(--font-serif);
  font-weight: 700;
  text-align: right;
}

/* --------------------------------------------------------------------------
   10. Motion — the OS preference, and the player's animation setting
   (table.js sets data-animations="off" for either one).
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .table *,
  .table *::before,
  .table *::after {
    animation: none !important;
    transition: none !important;
  }

  .hand > .card--playable:hover,
  .hand > .card:focus {
    --lift: 0px;
  }
}

.table[data-animations="off"] *,
.table[data-animations="off"] *::before,
.table[data-animations="off"] *::after {
  animation: none !important;
  transition: none !important;
}


/* --------------------------------------------------------------------------
   10. Wide-screen overrides that must outrank their own base rules
   These selectors are re-declared at the same specificity as the rules above,
   so they only win by SOURCE ORDER — that is why they live at the end of the
   file rather than beside the table sizing in §1. Moving them up silently
   reverts every value here.
   -------------------------------------------------------------------------- */
@media (min-width: 700px) {
  /* Compact enough to sit entirely inside the north row's band: any taller and
     it would deepen the top row. Verified against the worst case (every role
     badge showing at once). */
  .scoreboard {
    align-self: start;
    flex-direction: column;
    align-items: stretch;
    width: 100%;
    min-height: 0;
    gap: 4px;
    padding: 6px 8px;
    border-radius: var(--r-md);
  }

  .scoreboard__counters {
    flex-direction: column;
    gap: 2px;
    width: 100%;
  }

  .counter {
    justify-content: space-between;
    gap: var(--sp-2);
    width: 100%;
  }

  .counter__label {
    max-width: none;
    font-size: var(--fs-xs);
  }

  .counter__value {
    width: 26px;
    height: 32px;
    font-size: 1.05rem;
  }

  .scoreboard__meta {
    width: 100%;
  }

  /* In a 172px panel the longest trump line ("Diamonds - North alone") needs
     167px but only 148px is free, so nowrap pushed the text through the pill
     and out of the panel. It wraps here instead, with two lines always
     reserved: otherwise the panel — and the whole table — would change height
     the moment trump was named. A pill radius reads wrong on two lines. */
  /* One line at 13px. The longest label ("Diamonds - North alone") needs 156px
     and the 208px column leaves 184px, so it fits with room to spare — the suit
     glyph does not scale with the text, which is why shrinking the font alone
     could not rescue it in the old 172px column. Truncation rather than wrap is
     the fallback, so an unexpectedly long label can never resize the panel. */
  .trump-indicator {
    width: 100%;
    justify-content: center;
    min-height: 28px;
    padding: 3px 10px;
    font-size: 13px;
    line-height: 1.3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* The counter's under-card is rotated, so it hangs ~4px below the counter box
     and was touching the trump line. This is its clearance. */
  .scoreboard__counters {
    margin-bottom: 6px;
  }

  /* Trimmed to pay for the two-line trump line above, so the panel stays no
     taller than the North seat beside it and the table keeps its height. */
  .counter__value {
    width: 26px;
    height: 32px;
    font-size: 1.05rem;
  }

  /* Every seat already shows its own trick count, so the aggregate is duplicate
     information — and in a corner panel it costs a whole line. */
  .scoreboard__hand-tricks {
    display: none;
  }

  /* The side columns are 208px wide. Trimming their chips keeps even the worst
     case — Dealer + Sitting out + a trick count on one seat — inside the two
     reserved lines, so a lone hand cannot make the table grow mid-game. */
  .seat--west .seat__badge,
  .seat--west .seat__tricks,
  .seat--west .seat__bid,
  .seat--east .seat__badge,
  .seat--east .seat__tricks,
  .seat--east .seat__bid {
    padding: 0 7px;
    font-size: 11px;
  }

  /* Two lines of hint are still reserved, just set tighter. */
  .hint {
    min-height: calc(var(--sp-1) * 2 + 2.7em);
    padding: var(--sp-1) var(--sp-4);
    line-height: 1.35;
  }

  /* Less headroom over the fan; the hover lift is trimmed to match so a raised
     card still has somewhere to go. */
  .hand {
    padding-top: 12px;
    padding-bottom: 12px;
    min-height: calc(var(--card-h) + 24px);
  }

  .hand > .card--playable:hover,
  .hand > .card:focus {
    --lift: -12px;
  }
}

/* --------------------------------------------------------------------------
   11. Phone height budget
   On a phone the felt plus the button row must fit the visible viewport
   (~700px on a large iPhone once Safari's chrome is up). Measured at 430px
   wide, the stock layout ran 918px, spent mostly on desktop-sized chrome:
   a wrapped two-row scoreboard (112px), the side seats' 124px chip-column
   reservation, a wrapped button row, and desktop gaps. Everything here trims
   chrome — card size never drops below the spec's 72px floor.
   Source order matters: this section must stay at the end of the file.
   -------------------------------------------------------------------------- */
@media (max-width: 699px) {
  .table {
    gap: 4px var(--sp-2);
    padding: 6px;
    border-width: 6px;
    min-height: 0; /* hug the content; the clamp is a desktop nicety */
  }

  .seat {
    gap: 4px;
    padding: 4px;
  }

  /* One-row scoreboard: the aggregate trick count duplicates the per-seat
     chips, and dropping it is what stops the bar wrapping to two rows. */
  .scoreboard__hand-tricks {
    display: none;
  }

  .scoreboard {
    gap: 4px var(--sp-3);
    min-height: 56px;
    padding: 4px var(--sp-2);
  }

  .scoreboard__score,
  .counter__value {
    width: 34px;
    height: 46px;
    font-size: 1.2rem;
  }

  /* Chips tighten a notch; the side seats' reserved column shrinks to their
     phone worst case (name + Dealer + Maker/Sitting out + tricks at 22px). */
  .seat__badge,
  .seat__tricks,
  .seat__bid {
    min-height: 22px;
    padding: 0 8px;
    font-size: 11px;
  }

  .seat--west .seat__meta,
  .seat--east .seat__meta {
    /* Worst case measured: name + Dealer + Sitting out + tricks = 105px.
       Reserved above it so a lone hand cannot grow the felt mid-game. */
    min-height: 106px;
  }

  /* Hidden hands overlap harder — five backs read fine at 80% overlap. */
  .seat--west .seat__cards,
  .seat--east .seat__cards {
    height: calc(var(--card-h) * 1.8);
  }

  .seat--west .seat__cards > .card + .card,
  .seat--east .seat__cards > .card + .card {
    margin-top: calc(var(--card-h) * -0.8);
  }

  .trick-area {
    min-height: calc(var(--card-h) * 1.5);
  }

  .hint {
    min-height: calc(var(--sp-1) * 2 + 2.7em);
    padding: var(--sp-1) var(--sp-3);
    font-size: var(--fs-xs);
    line-height: 1.35;
  }

  /* No hover on touch: the fan needs headroom only for the focus lift. */
  .hand {
    padding-top: 10px;
    padding-bottom: 8px;
    min-height: calc(var(--card-h) + 18px);
  }

  .hand > .card--playable:hover,
  .hand > .card:focus {
    --lift: -8px;
  }

  /* All three buttons on one row: tighter padding, same 48px touch height. */
  .game-controls {
    gap: var(--sp-2);
  }

  .game-controls .btn {
    padding-inline: 12px;
    font-size: 15px;
  }
}

/* --------------------------------------------------------------------------
   12. The deal in flight (table.js dealFlights)
   An overlay of half-size card backs travelling dealer -> seat. Pure
   presentation: by the time it runs the engine has already dealt, and the
   reserved seat/hand footprints mean the emptied containers do not resize
   the felt underneath it.
   -------------------------------------------------------------------------- */
.deal-layer {
  position: absolute;
  inset: 0;
  z-index: 15;
  pointer-events: none;
}

.deal-card {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--card-w) * 0.5);
  height: calc(var(--card-h) * 0.5);
  margin: 0;
  /* No box-shadow of its own: .card--back's shadow IS the cream border ring
     (an inset shadow), and overriding it here stripped the border off every
     card mid-deal — they then "grew" a border when the real backs rendered. */
  transition-property: transform;
  transition-timing-function: cubic-bezier(0.3, 0.7, 0.4, 1);
  will-change: transform;
}

/* Cards dealt to the human fly at full size — that is the size they will be
   when they turn face up in the fan, so nothing resizes at the flip. */
.deal-card--own {
  width: var(--card-w);
  height: var(--card-h);
}

/* The deck the deal comes from — same backs, no transition (it never moves). */
.deal-card--pile {
  transition: none;
}

/* --------------------------------------------------------------------------
   13. Floating action announcement (table.js announce())
   Names what an opponent just did, floating above the trick where the eyes
   already are. Absolutely positioned and pointer-inert, so it can never
   shift layout or intercept a tap; aria-hidden because the hint line is the
   screen-reader channel for the same text.
   -------------------------------------------------------------------------- */
.announce {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 18;
  max-width: 82%;
  margin: 0;
  padding: 7px 18px;
  border-radius: var(--r-pill);
  background: rgba(17, 53, 38, 0.92);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35), inset 0 0 0 1px rgba(247, 243, 232, 0.4);
  color: var(--cream);
  font-size: var(--fs-sm);
  font-weight: 700;
  text-align: center;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 170ms ease;
}

.announce.is-shown {
  opacity: 1;
}

/* --------------------------------------------------------------------------
   14. Upcard caption — names the card in the middle during bidding
   "Up card — Hearts for trump?" under the turned-up card, so a newcomer knows
   what the lone card on the felt is deciding. Sized within the trick area's
   reserved height, so showing or turning it down never moves the table.
   -------------------------------------------------------------------------- */
.upcard-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}

.upcard-label {
  max-width: 96%;
  padding: 3px 12px;
  border-radius: var(--r-pill);
  background: rgba(247, 243, 232, 0.94);
  box-shadow: inset 0 0 0 1px var(--wood), 0 1px 3px rgba(0, 0, 0, 0.25);
  color: var(--ink);
  font-size: var(--fs-xs);
  font-weight: 700;
  text-align: center;
  white-space: nowrap;
}

.upcard-label--down {
  background: rgba(247, 243, 232, 0.72);
  font-weight: 600;
}

/* --------------------------------------------------------------------------
   15. Avatars and speech bubbles
   Each seat is a person: a small round robot portrait beside the name, and a
   speech bubble that pops up beside whoever just acted, tail pointing at them.
   The bubble is one absolutely positioned element (never affects layout);
   data-from = seat index picks the tail's side.
   -------------------------------------------------------------------------- */
/* One sprite holds all four robots (2x2 grid); background-position picks the
   quadrant. Sliced in CSS rather than into four files because the quadrants
   then can never drift apart in style or compression. The URL is templated at
   render time via a custom property set in game.html's head. */
.seat__avatar {
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 2px solid var(--cream);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
  background-color: var(--cream);
  background-image: var(--avatar-sprite);
  background-size: 200% 200%;
}

.seat__avatar--west { background-position: 0 0; }     /* top-left: blue */
.seat__avatar--north { background-position: 100% 0; } /* top-right: green */
.seat__avatar--east { background-position: 0 100%; }  /* bottom-left: red */
/* The human seat is a person, not a robot — its own image, not a sprite
   quadrant. Same warm gold family as the robot it replaced. */
.seat__avatar--you {
  background-image: var(--avatar-you);
  background-size: cover;
  background-position: center;
}

.speech {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 18;
  max-width: 240px;
  margin: 0;
  padding: 8px 14px;
  border-radius: 12px;
  background: var(--paper);
  border: 2px solid var(--wood);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
  color: var(--ink);
  font-size: var(--fs-sm);
  font-weight: 600;
  line-height: 1.3;
  text-align: center;
  pointer-events: none;
  opacity: 0;
  transition: opacity 380ms ease;
}

.speech.is-shown {
  opacity: 1;
}

/* The tail: a rotated square poking out of the side facing the speaker. */
.speech::after {
  content: "";
  position: absolute;
  width: 12px;
  height: 12px;
  background: var(--paper);
  border: 2px solid var(--wood);
  border-top: 0;
  border-left: 0;
}

.speech[data-from="2"]::after { /* speaker above: tail on top edge */
  top: -8px;
  left: 50%;
  transform: translateX(-50%) rotate(-135deg);
}

.speech[data-from="0"]::after { /* speaker below */
  bottom: -8px;
  left: 50%;
  transform: translateX(-50%) rotate(45deg);
}

.speech[data-from="1"]::after { /* speaker to the left */
  left: -8px;
  top: 50%;
  transform: translateY(-50%) rotate(135deg);
}

.speech[data-from="3"]::after { /* speaker to the right */
  right: -8px;
  top: 50%;
  transform: translateY(-50%) rotate(-45deg);
}

@media (max-width: 699px) {
  .seat__avatar {
    width: 38px;
    height: 38px;
  }

  .speech {
    max-width: 190px;
    font-size: var(--fs-xs);
    padding: 6px 10px;
  }
}

/* --------------------------------------------------------------------------
   16. The played card in flight (table.js flyCardToTrick)
   A back leaves the player's stack, grows to full size and flips over on the
   way to its trick slot. Two faces on a 3D-flipping inner; the wrapper does
   the travel, the inner does the turn.
   -------------------------------------------------------------------------- */
.play-flight {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 16;
  margin: 0;
  pointer-events: none;
  perspective: 700px;
  border-radius: var(--card-r);
  /* EXACTLY the landed trick card's shadow, carried by the non-rotating
     wrapper: the sides must not paint their own outer shadows, or the swap to
     the real card visibly deepens/changes the shadow at the last frame. */
  box-shadow: var(--card-shadow), 0 8px 16px rgba(0, 0, 0, 0.3);
  transition-property: transform, opacity;
  /* travel duration is set inline per flight; the opacity fade is fixed */
  transition-duration: 400ms, 150ms;
  transition-timing-function: cubic-bezier(0.3, 0.7, 0.4, 1), ease-out;
  will-change: transform, opacity;
  opacity: 1;
}

.play-flight__inner {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition-property: transform;
  transition-timing-function: ease-in-out;
}

.play-flight__side {
  position: absolute;
  inset: 0;
  width: 100% !important;
  height: 100% !important;
  margin: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}

.play-flight__side {
  box-shadow: none;
}

/* The back keeps its cream ring — that is part of the card's face design, an
   inset that rotates with the plane — but no outer drop shadow. */
.play-flight__side.card--back {
  box-shadow: inset 0 0 0 3px var(--cream), inset 0 0 0 4px var(--porch-green-dark);
}

.play-flight__face {
  transform: rotateY(180deg);
}
