/* ---------- table layout ---------- */
.table-view {
  position: relative;
  display: flex;
  gap: var(--side-gap);
  padding: 1rem var(--page-pad) 2rem;
  max-width: var(--page-max);
  margin: 0 auto;
  align-items: flex-start;
  /* The deal animation swings fanned cards out sideways for half a second
     (measured: to x=424 on a 360px phone), which made the whole page wider
     than the screen during every deal — a sideways wobble, or a stray
     horizontal scroll, on a phone. `clip` (unlike `hidden`) makes no scroll
     box, so sticky / fixed children and vertical overflow behave as before. */
  overflow-x: clip;
}

/* ---------- wide screens: centre the TABLE, not just the page block --------
   .table-view was already centred, but the Scores / Action History column is
   absolutely positioned at its left edge and .felt is pushed right to clear it.
   So the block was centred while the felt — the thing the eye actually reads as
   "the table" — sat ~300px right of screen centre. That imbalance was most of
   why a wide desktop looked lopsided.

   Reserving an equal, empty gutter on the right makes the felt symmetric inside
   the container, so a centred container finally means a centred table. The felt
   also gets *wider* here, not narrower, because the cap rises with it.

   Gated at 1700px deliberately: below that, the reserved gutter would have to
   come out of the felt's own width, and the felt should never get narrower to
   buy symmetry. Both numbers are the variables in base.css if this wants
   tuning.

   NOTE: the reserved gutter itself is the padding-right in safe-area.css, not
   here. That file loads last and owns the final word on .table-view's
   horizontal padding (it adds the device insets), so a padding-right written
   here is silently overwritten — which is exactly what happened first try. */
@media (min-width: 1700px) {
  .table-view { max-width: var(--page-max-wide); }
}

/* The scoreboard is a fixed-height column (top:1rem → bottom:2rem) holding TWO
   independent cards: Scores and Action History. It is just a transparent
   flex container — the visible panels are its two children. */
.scoreboard {
  position: absolute;
  top: 1rem;
  bottom: 2rem;
  left: var(--page-pad);
  width: var(--side-col);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
  background: transparent;
  border: 0;
  overflow: hidden;
}

/* --- Scores card (top) ---------------------------------------------------
   Sized by its OWN content (player count), never squeezed by the chat below:
   flex-shrink 0 keeps it stable. It only grows up to 60% of the column, after
   which it scrolls internally. `calc(... - 0.4rem)` reserves half the flex gap
   so Scores(≤60%) + gap + Action History(≥40%) can never overflow the column. */
.sb-players {
  flex: 0 0 auto;
  max-height: calc(60% - 0.4rem);
  overflow-y: auto;
  padding: 0.85rem 1rem 0.7rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.sb-players::-webkit-scrollbar { width: 5px; }
.sb-players::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }

/* --- Action History card (bottom) ----------------------------------------
   Takes ALL the space Scores leaves, with a hard 40% floor. flex-basis 0 means
   its own message count never inflates it or pushes on Scores — overflow just
   scrolls inside the list. */
.action-log {
  flex: 1 1 0;
  min-height: calc(40% - 0.4rem);
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
/* Shared collapsible-panel header, used by both Action History and Scores. */
.action-log h3,
#scores-toggle {
  font-family: var(--display);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 0.6rem;
  padding: 0.7rem 1rem 0.45rem;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  cursor: pointer;
  user-select: none;
}
.action-log h3 { margin-bottom: 0; border-bottom: 1px solid var(--line); }
/* Icon, title, then the chevron pushed to the far edge. */
.action-log h3,
#scores-toggle { justify-content: flex-start; gap: 0.5rem; }
.action-log h3 > .ico,
#scores-toggle > .ico { width: 15px; height: 15px; }
.collapse-chevron { margin-left: auto; }
.collapse-chevron .ico { width: 14px; height: 14px; }

/* A small round badge (not just a bare glyph) so the collapse control reads
   clearly as an interactive toggle, not decoration. */
.collapse-chevron {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.08);
  color: var(--text);
  font-size: 0.7rem;
  transition: transform 0.2s ease, background 0.15s ease;
}
.action-log h3:hover .collapse-chevron,
#scores-toggle:hover .collapse-chevron {
  background: rgba(255, 255, 255, 0.18);
}

/* Collapsed: header stays visible, list + its border collapse away so the
   panel shrinks to header-height only. Toggled by action_log.js (Action
   History; defaults to collapsed on phones, since the log matters far less
   than the turn/score info above it) and chrome.js (Scores; stays open by
   default everywhere — it carries the turn indicator). */
.action-log.is-collapsed {
  min-height: 0;
  flex: 0 0 auto;
}
/* Both collapsed headers share one padding/margin value declared in one
   place — the previous version split padding differently between each
   panel's own container and its header, which was meant to match but
   actually rendered visibly taller for Action History and left its
   chevron sitting further from the text than Scores'. Zeroing both
   containers' padding and putting 100% of it on the toggle row itself
   removes that asymmetry by construction. */
.action-log.is-collapsed h3,
.sb-players.is-collapsed #scores-toggle {
  margin: 0;
  padding: 0.5rem 0.85rem;
  border-bottom: 0;
}
.action-log.is-collapsed #action-log-list { display: none; }
.action-log.is-collapsed .collapse-chevron,
.sb-players.is-collapsed .collapse-chevron { transform: rotate(-90deg); }

.sb-players.is-collapsed { padding: 0; max-height: none; }
.sb-players.is-collapsed #score-list,
.sb-players.is-collapsed #spectators-heading,
.sb-players.is-collapsed #spectator-list { display: none; }
#action-log-list {
  list-style: none;
  margin: 0;
  padding: 0 0.55rem 0.5rem;
  overflow-y: auto;
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 0.18rem;
}
#action-log-list::-webkit-scrollbar { width: 4px; }
#action-log-list::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }

.al-entry {
  display: flex;
  align-items: baseline;
  gap: 0.35rem;
  /* Compact system font — thinner and more space-efficient than a handwritten
     face, so most messages fit on one line (two at most). */
  font-family: var(--body);
  font-style: normal;
  font-weight: 400;
  font-size: 0.78rem;
  letter-spacing: 0;
  line-height: 1.25;
  padding: 0.24rem 0.45rem;
  border-radius: 8px;
  background: rgba(255,255,255,0.04);
  animation: al-fadein 0.25s ease;
}
@keyframes al-fadein { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.al-icon { flex: 0 0 auto; font-size: 0.8rem; font-style: normal; line-height: 1.25; }
.al-text { flex: 1 1 auto; color: var(--text); word-break: break-word; }
.al-actor { font-weight: 600; font-style: normal; margin-right: 0.15rem; }
/* timestamps carry no meaning in the feed — hidden if ever emitted */
.al-time { display: none; }

/* type-specific accents */
.al-play    { border-left: 2px solid var(--gold); }
.al-pass    { border-left: 2px solid var(--muted); }
.al-challenge { border-left: 2px solid var(--accent); background: rgba(239,68,68,0.06); }
.al-win     { border-left: 2px solid var(--turn); background: rgba(87,217,138,0.06); }
.al-system  { border-left: 2px solid #4ea1ff; }
.scoreboard h3 {
  font-family: var(--display);
  font-size: 0.8rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 0.75rem;
}
#score-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
#score-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.45rem 0.55rem 0.55rem;
  border-radius: 8px;
  border: 1px solid transparent;
  font-size: 0.9rem;
  position: relative;
  overflow: hidden;
}
#score-list li.turn {
  border-color: var(--turn);
  background: rgba(87, 217, 138, 0.08);
}
#score-list li.out { opacity: 0.45; }
.sb-name { display: flex; align-items: center; gap: 0.4rem; min-width: 0; flex: 1 1 auto; overflow: hidden; }
.sb-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.sb-score { flex: 0 0 auto; margin-left: 0.4rem; font-family: var(--display); font-weight: 600; color: var(--gold); }
/* Bluff ranks on finishing place and cards held rather than a score. A banked
   place is the achievement, so it keeps the gold; a live card count is ordinary
   running state and steps back to muted, so the panel reads as "who is out"
   at a glance instead of a wall of identical gold numbers. */
.sb-cards { color: var(--muted); font-weight: 500; font-size: 0.82em; }
.sb-place { letter-spacing: 0.02em; }
.sb-you {
  flex: 0 0 auto; font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--muted); border: 1px solid var(--line); border-radius: 999px;
  padding: 0.02rem 0.32rem; line-height: 1.4;
}

/* ---- score progress strip ---- */
.sb-bar {
  position: absolute;
  bottom: 0; left: 0;
  height: 3px;
  width: 100%;
  background: var(--line);
  border-radius: 0 0 8px 8px;
}
.sb-bar-fill {
  height: 100%;
  width: var(--bar-pct, 0%);
  background: linear-gradient(
    to right,
    #4ade80 0%,
    #facc15 40%,
    #f97316 70%,
    #ef4444 100%
  );
  background-size: calc(100% / var(--bar-raw, 0.01)) 100%;
  border-radius: 0 0 8px 8px;
  transition: width 0.4s ease;
}

/* felt */
.felt {
  margin-left: calc(var(--side-col) + var(--side-gap));
  width: calc(100% - var(--side-col) - var(--side-gap));
  flex: 1 1 auto;
  min-width: 0;
  background:
    radial-gradient(120% 120% at 50% 0%, #1b3a33 0%, var(--felt) 70%);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  box-shadow: inset 0 0 60px -20px rgba(0,0,0,0.6);
}

/* opponents — arc layout. Seats are absolutely positioned by JS
   (core/seats.js), curved so a single opponent centers at the very top and
   more opponents bow down toward the edges, like sitting around an oval
   table. The container needs an explicit height since absolutely positioned
   children don't contribute to it. */
.opponents {
  position: relative;
  width: 100%;
  height: 150px;
}
.opp-seat {
  position: absolute;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  width: 74px;
  transition: opacity 0.2s ease, filter 0.2s ease;
}
.opp-seat.offline { opacity: 0.5; }
.opp-seat.out { opacity: 0.4; filter: grayscale(0.6); }

/* Two concentric rings, always both present: the inner one is the
   elimination/score progress (conic-gradient "bar bent into a circle",
   same green -> yellow -> orange -> red meaning as .sb-bar-fill in the
   Scores panel) and NEVER goes away. The outer one only lights up with
   --timer-pct/--timer-color while this seat is the active turn, so the two
   meanings stay visible at once instead of the timer replacing the score
   ring. Games with no elimination metric (Bluff) skip the inner ring via
   .no-ring below, but still get the outer timer ring on their turn. */
.opp-ring-outer {
  position: relative;
  width: 62px;
  height: 62px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: conic-gradient(var(--timer-color, transparent) 0% var(--timer-pct, 0%), transparent var(--timer-pct, 0%) 100%);
  transition: background 0.3s ease;
}
/* Now a plain wrapper. It used to paint the elimination arc, 3px inside the
   timer arc and in the same green-to-red ramp, so the two were near
   indistinguishable — worst on phones where they were 50px and 44px. Elimination
   moved inside the avatar as a liquid level (.opp-avatar.gauge below). */
/* Now a bare wrapper with no size of its own. It used to be 56px, which is what
   made the timer read as a thick band: the arc showed from 62px all the way in
   to the avatar. Sizing the avatar directly against the 62px outer is what makes
   the ring a hairline (see .opp-avatar below). */
.opp-ring {
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
}
/* Sole turn indicator now that seats sit in clockwise-from-you order (no
   more TURN/NEXT text badges) — a gentle pulse so it reads at a glance.
   Applies to the player's own seat too (.my-seat), not just opponents'. */
.opp-seat.active .opp-ring-outer,
.my-seat.active .opp-ring-outer {
  box-shadow: 0 0 0 3px var(--turn), 0 0 16px 2px rgba(87, 217, 138, 0.45);
  animation: opp-active-pulse 1.4s ease-in-out infinite;
}
@keyframes opp-active-pulse {
  0%, 100% { box-shadow: 0 0 0 3px var(--turn), 0 0 16px 2px rgba(87, 217, 138, 0.45); }
  50% { box-shadow: 0 0 0 3px var(--turn), 0 0 22px 6px rgba(87, 217, 138, 0.7); }
}

/* 58px inside a 62px outer leaves the timer as a 2px hairline instead of the old
   5px band, and hands the reclaimed radius to the face — the ring is a countdown,
   not a frame, so it should be the thinnest thing here. */
.opp-avatar {
  width: 58px;
  height: 58px;
  border-radius: 50%;
  border: 2px solid var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--display);
  font-weight: 700;
  font-size: 1.3rem;
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
  flex: 0 0 auto;
}
.opp-seat.no-ring .opp-avatar,
.my-seat.no-ring .opp-avatar { border: 2px solid var(--line); }
/* A chosen avatar motif (game/core/avatars.py) in place of the initial. */
.opp-avatar .av-motif { width: 52%; height: 52%; stroke-width: 2.1; filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.35)); }
.opp-avatar.gauge .av-ini { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }

/* ---------- the elimination gauge ----------
   A liquid level inside the avatar that DRAINS as the score climbs: full and
   calm means healthy, near-empty means nearly out. That direction is the
   intuitive one — a battery discharging rather than a tank filling with poison —
   and it frees the ring to mean one thing only (whose turn, how long left).

   With the score arc gone there is spare radius, so the gauge avatar takes the
   same 52px the ring-less avatars already use: a level reads better at 52px than
   a 4px arc ever did at 56px.

   Identity moves to the rim (border-color, set per player in core/seats.js),
   because a player-coloured fill would collide with the level's own colour. */
.opp-avatar.gauge {
  width: 58px;
  height: 58px;
  position: relative;
  overflow: hidden;
  background: #0c1512;
  border-width: 2px;
  border-style: solid;
}
.opp-avatar.gauge .av-level {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  /* height + background come from core/seats.js. The transition is the one
     animation worth having: the level visibly settling when scores land at
     round end. */
  transition: height 700ms cubic-bezier(0.3, 0, 0.2, 1), background 700ms ease;
}
.opp-avatar.gauge .av-ini {
  position: relative;
  z-index: 2;
  font-weight: 700;
}

/* Wave surface — on every gauge, so an opponent's level looks alive rather than
   like a flat block of colour.
   Cost is controlled by layer count rather than by excluding opponents: they get
   ONE rotating layer, your own seat gets two (a slightly richer surface on the
   one you actually watch), and a table of more than 10 seats drops the animation
   entirely via .dense from core/seats.js. That keeps the worst case near 10
   composited layers instead of the 40 that animating everything twice would
   cost on a page already holding a wake lock (todos §9). */
.opp-avatar.gauge .av-level::before,
.my-seat .opp-avatar.gauge .av-level::after {
  content: "";
  position: absolute;
  left: 50%;
  top: -9px;
  width: 70px;
  height: 70px;
  margin-left: -35px;
  background: inherit;
  border-radius: 43%;
  animation: av-wave 5s linear infinite;
}
.my-seat .opp-avatar.gauge .av-level::after {
  animation-duration: 7.5s;
  animation-direction: reverse;
  opacity: 0.55;
}
@keyframes av-wave { to { transform: rotate(360deg); } }
.opponents.dense .opp-avatar.gauge .av-level::before { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .opp-avatar.gauge .av-level::before,
  .my-seat .opp-avatar.gauge .av-level::after { animation: none; }
}

/* ---------- name plate ----------
   One capsule instead of two text lines floating on the felt. The backing is
   load-bearing: --muted on Casino Felt's upper gradient measures about 1.8:1,
   and Red Casino and Sunset are no better, so plain text legibility depended on
   which of the fourteen themes the host picked. One plate fixes all of them.

   One horizontal row for every seat, not a two-line stack. Stacked, the plate
   grew almost as tall as the avatar and competed with it; on one line it reads as
   a name tag under a portrait. The name flexes and ellipsises, the score is
   pinned right, and --plate-max is the arc's real per-seat spacing (set in
   core/seats.js) so plates can be wider than the 74px seat without colliding. */
.seat-plate {
  display: flex;
  flex-direction: row;
  align-items: center;
  /* Every value here is bought back as name letters: the padding and gap used to
     spend ~22px of a plate that is sometimes only 56px wide. */
  gap: 0.28rem;
  max-width: var(--plate-max, 100%);
  padding: 0.14rem 0.4rem;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.42);
}
.seat-plate .opp-name {
  flex: 1 1 auto;
  min-width: 0;              /* lets ellipsis actually engage inside a flex row */
  font-size: 0.68rem;
  font-weight: 500;          /* lighter than the old 600: a label, not a heading */
  letter-spacing: -0.01em;   /* a touch narrower, so more of the name survives */
}
.seat-plate .opp-score { flex: 0 0 auto; font-size: 0.64rem; }
/* Crowded arc: no room for both, so the name wins. Set by core/seats.js from the
   arc's real spacing. The live countdown is exempt — it is the one number that
   matters more than the name while a clock is running. */
.opponents.tight .opp-seat .opp-score:not(.timer) { display: none; }
.seat-you {
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--cream);
  background: var(--line);
  border-radius: 999px;
  padding: 0 0.3rem;
}
/* Your own seat is rendered under the hand, not into the arc, so it is not
   width-constrained by neighbouring seats and can lay everything out in one
   row: name, the you pill, score, and the countdown together. */
.my-seat .seat-plate {
  gap: 0.4rem;
  padding: 0.22rem 0.7rem;
  max-width: none;
}
.my-seat .seat-plate .opp-name { flex: 0 0 auto; max-width: none; font-size: 0.76rem; }
/* Thin separators so the single row reads as distinct facts rather than one
   run-on string: "xenone you · 0 pts · 38s". */
.my-seat .seat-plate .opp-score::before {
  content: "·";
  color: var(--line);
  margin-right: 0.4rem;
}

/* Live countdown. The colour is set inline from the ring's current colour, so
   the two halves of one signal escalate together — this used to be a static
   green while the ring reddened. */
.opp-score.timer { font-weight: 600; }
.opp-score.timer.urgent { font-weight: 800; }

/* Hand size, drawn as a little card rather than a pill — it is a count of cards,
   so the shape should say so. Card proportions, card corners, a cream face, and a
   few degrees of tilt so it reads as resting on the table rather than as a badge.
   Moved off gold too: after the graphite change gold is the chrome accent, and
   this is game state. */
.opp-count {
  position: absolute;
  bottom: -4px;
  right: -8px;
  min-width: 17px;
  height: 23px;
  padding: 0 3px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--cream);
  color: #1b1d1f;
  font-family: var(--display);
  font-weight: 700;
  font-size: 0.68rem;
  border-radius: 3px;
  border: 1px solid rgba(0, 0, 0, 0.5);
  transform: rotate(-7deg);
}
/* Emptied your hand: the card goes green and reads 0. */
.opp-count.safe { background: var(--turn); color: var(--ink); }

.opp-name {
  font-weight: 600;
  font-size: 0.78rem;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}
.opp-score { font-size: 0.7rem; color: var(--muted); }

@media (max-width: 720px) {
  .opponents { height: 118px; }
  .opp-seat { width: 60px; }
  .opp-ring-outer { width: 50px; height: 50px; }
  /* 46px inside the 50px outer keeps the same 2px hairline ring as desktop. */
  .opp-avatar,
  .opp-seat.no-ring .opp-avatar,
  .opp-avatar.gauge { width: 46px; height: 46px; font-size: 1.05rem; }
  .opp-count { min-width: 15px; height: 20px; font-size: 0.6rem; right: -7px; }
  .seat-plate .opp-name { font-size: 0.64rem; }
  .seat-plate .opp-score { font-size: 0.6rem; }
  .seat-plate { padding: 0.1rem 0.4rem; gap: 0.28rem; }
  .my-seat .seat-plate { padding: 0.16rem 0.55rem; gap: 0.3rem; }
  .my-seat .seat-plate .opp-name { font-size: 0.7rem; }
}

/* center row */
.center-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 2.5rem;
  flex: 1 1 auto;
  min-height: 130px;
}
.pile {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 98px;
}
.pile-label {
  position: absolute;
  bottom: -1.4rem;
  left: 50%;
  transform: translateX(-50%);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}
.deck { cursor: default; }
.deck img { width: 66px; height: 92px; border-radius: 6px; }
.pile-count {
  position: absolute;
  top: -8px; right: -8px;
  background: var(--gold);
  color: var(--ink);
  font-family: var(--display);
  font-weight: 700;
  font-size: 0.75rem;
  border-radius: 999px;
  padding: 0.1rem 0.45rem;
  min-width: 1.4rem;
  text-align: center;
}
.discard {
  border: 1px dashed var(--line);
  border-radius: 10px;
  padding: 0.4rem 0.6rem;
  min-width: 90px;
  justify-content: center;
}
.pile-empty { color: var(--muted); font-size: 0.8rem; }
.center-card { width: 62px; height: 86px; }

/* Own hand + seat + actions — no boxed panel around them (the fanned
   cards, ring, and pill buttons already read as their own things sitting
   directly on the felt, same as opponents do); this is just the layout
   column that stacks and centers them. */
.myhand-wrap {
  display: flex; flex-direction: column; align-items: center; gap: 0.6rem;
  width: 100%; max-width: 640px; margin: 0.2rem auto 0;
}
/* Own seat badge — same ring/avatar/name/score parts as .opp-seat
   (core/seats.js buildSeatEl), just centered/static instead of arced, and a
   little larger since it's the one seat that's always front and center. */
.my-seat {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
}
.my-seat.offline { opacity: 0.5; }
.my-seat.out { opacity: 0.4; filter: grayscale(0.6); }
.my-seat .opp-ring-outer { width: 66px; height: 66px; }
.my-seat .opp-avatar,
.my-seat.no-ring .opp-avatar { width: 62px; height: 62px; font-size: 1.4rem; }
.my-seat .opp-name { font-size: 0.85rem; }
.my-seat .opp-score { font-size: 0.76rem; }

.hand {
  display: flex;
  /* Always one row — core/seats.js layoutHandFan() computes per-card
     overlap so any hand size fits, collapsing cards onto each other
     instead of wrapping to a second row. */
  flex-wrap: nowrap;
  justify-content: center;
  align-items: flex-start;
  gap: 0;
  width: 100%;
}
.card.hand-card {
  width: 74px;
  height: 104px;
  border-radius: 8px;
  box-shadow: 0 6px 14px -8px rgba(0,0,0,0.7);
}
/* Bluff-only: smaller cards since hands can run much larger than Seven's
   fixed 7 — layoutHandFan() (core/seats.js) keeps them in one row
   regardless. Placed here (before the mobile media queries below) on
   purpose: at equal specificity the later rule wins regardless of media
   query, so this unconditional rule must come before the ≤600px override
   or it would win at every width, including mobile, and shadow it. */
.bluff-hand .hand-card { width: 58px; height: 82px; }

/* Box view (core/view_mode.js): a flat, wrapped, boxed, scrollable grid —
   the alternative to the fan for hands too big to fit one overlapped row
   comfortably at any width (a 2-player Bluff hand can run to 50+ cards).
   A per-player preference, not gated to mobile — sized here for the
   larger desktop cards; the ≤600px override below re-sizes it down for
   the smaller phone cards.
   justify-content:center (not flex-start) does double duty on a wrapped
   flex container: it centers every row's items as a group, which both
   removes the empty space that piled up on the right with flex-start AND
   centers a short last row, instead of that row hugging the left edge
   while the earlier full rows look centered. Cards still fill row 1
   before wrapping to row 2 — justify-content only distributes the extra
   space within a row, it doesn't change which row an item packs into. */
.hand.hand-flat-scroll {
  flex-wrap: wrap;
  justify-content: center;
  align-content: flex-start;
  gap: 6px 4px;
  /* Capped at exactly 7 cards wide (Super Seven's full hand: 7*58px cards
     + 6*4px gaps + 8px padding each side) rather than stretching to fill
     .myhand-wrap's full width — otherwise a 7-card (or shorter) hand sits
     centered in a box far wider than it needs, reading as lopsided empty
     space even with justify-content:center. Bluff's bigger hands wrap
     within this same width; the ≤600px override below shrinks it to match
     the smaller phone cards. */
  max-width: 446px;
  margin: 0 auto;
  max-height: 232px;
  overflow-y: auto;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 10px;
  /* Extra top padding: a selected/hovered card in the first row grows
     slightly toward the top, and needs headroom so it doesn't get clipped
     by this container's own overflow-y. */
  padding: 14px 8px 8px;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  scrollbar-color: var(--gold) transparent;
}
.hand.hand-flat-scroll::-webkit-scrollbar { width: 6px; }
.hand.hand-flat-scroll::-webkit-scrollbar-track { background: transparent; }
.hand.hand-flat-scroll::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 999px; }
/* One consistent card size for box view across BOTH games at each
   breakpoint, rather than each game's own (larger) standard-view size —
   smaller cards fit two full rows more comfortably. Higher specificity
   (3 classes) than .card.hand-card / .bluff-hand .hand-card (2 classes
   each) so this wins regardless of source order. */
.hand.hand-flat-scroll .hand-card { width: 58px; height: 82px; }
@media (max-width: 600px) {
  .hand.hand-flat-scroll .hand-card { width: 46px; height: 66px; }
}
/* Box view's selected state: a much smaller bump (~2%) with no vertical
   lift, unlike the fan's — in a tightly packed grid a bigger lift would
   push a first-row card's growth past this container's own top edge and
   get clipped by overflow-y. Hover (unscoped rule below) stays as-is. */
.hand-flat-scroll .card-slot.selected .hand-card {
  transform: scale(1.02);
}
.hand-flat-scroll .card-slot.selected:hover .hand-card {
  transform: scale(1.02);
}

.badge.safe { background: var(--turn); color: var(--ink); }

@media (max-width: 720px) {
  .table-view { flex-direction: column; }
  /* Stack Scores over Action History, each FULL-WIDTH, so names never clip and
     log lines fit in one or two rows (side-by-side made both columns too narrow). */
  .scoreboard { position: static; width: 100%; top: auto; bottom: auto; left: auto;
                max-height: none; flex-direction: column; gap: 0.6rem; }
  .felt { margin-left: 0; width: 100%; }
  .sb-players { max-height: 170px; flex: 0 0 auto; }
  .action-log { min-height: 96px; max-height: 190px; flex: 1 1 auto; }
  .center-row { gap: 1.5rem; }

  /* Placed in this (later) 720px block on purpose: the base .my-seat rule
     sits further down this file than the earlier 720px block above, and a
     same-specificity tie resolves by source order, not by which media
     query "sounds" more specific — an override placed before the base rule
     it's meant to override silently loses. */
  .my-seat .opp-ring-outer { width: 54px; height: 54px; }
  .my-seat .opp-avatar,
  .my-seat.no-ring .opp-avatar { width: 50px; height: 50px; font-size: 1.15rem; }
  .my-seat .opp-name { font-size: 0.72rem; }
  .my-seat .opp-score { font-size: 0.66rem; }
}

/* Phones: shrink hand cards — layoutHandFan() (core/seats.js) recomputes
   overlap from the smaller measured card width, so they still always fit
   one row. */
@media (max-width: 600px) {
  .felt { padding: 1rem 0.75rem; }
  /* A little smaller than before (60x84): seven cards fanned across a 375px
     phone now fit inside the felt with their corner indexes still readable. */
  .card.hand-card { width: 54px; height: 76px; }
  .bluff-hand .hand-card { width: 46px; height: 66px; }
  .myhand-wrap { max-width: 100%; }

  /* Box view's cards are smaller here too — shrink the box to match,
     still fitting about two rows. Same 7-cards-wide cap, recomputed for
     the smaller mobile card size (7*46px + 6*4px + 16px padding); on a
     narrower phone it simply never reaches this width, same as any
     max-width. */
  .hand.hand-flat-scroll { max-height: 156px; max-width: 362px; }
}

/* ---------- card selection ---------- */
/* Fanned hand: each card gets a rotation + downward-arc offset via --rot /
   --fan-y (set per-card in JS — seven/table.js and bluff/table.js
   renderHand(), same shape as the opponent arc math: 0 at the center card,
   growing toward the edges). transform-origin sits at the bottom so cards
   pivot like they're held in a hand, not spun around their own center.
   Large Bluff hands leave --rot/--fan-y at their 0 defaults and keep the
   flat wrapped layout — a rotated multi-row hand wouldn't read as a fan. */
.card-slot {
  position: relative;
  display: inline-block;
  line-height: 0;
  transform-origin: bottom center;
  transform: rotate(var(--rot, 0deg)) translateY(var(--fan-y, 0px));
  transition: transform 0.25s cubic-bezier(0.25, 0.8, 0.25, 1);
}
/* The slot itself never resizes on hover — only z-index changes — so the
   hover trigger stays bound to the card's original footprint. If the slot
   grew too, leaving that original area (even while still visually "inside"
   the bigger card underneath) would keep hover active, which reads as
   sticky/unpredictable. Growth happens on the child (.hand-card) below. */
.card-slot:hover {
  z-index: 30;
}

.card-slot .tick {
  position: absolute;
  top: -8px; right: -8px;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  font-size: 0.8rem;
  line-height: 22px;
  text-align: center;
  font-weight: 700;
  opacity: 0;
  transform: scale(0.6);
  transition: opacity 0.12s ease, transform 0.12s ease;
  pointer-events: none;
}
.card-slot.selected .tick { opacity: 1; transform: scale(1); }
.card-slot .hand-card {
  cursor: pointer;
  /* Anchored at bottom-right so growth reaches toward the top-left — away
     from the next card in the fan, which overlaps this one from the
     right, and away from neighbors above/below in a wrapped row. */
  transform-origin: bottom right;
  transition: transform 0.25s cubic-bezier(0.25, 0.8, 0.25, 1), outline-color 0.15s ease;
}
.card-slot.locked .hand-card { cursor: default; }
.card-slot.selected {
  z-index: 15;
}
/* Selected: colored outline + a modest lift/scale — noticeably marked, not
   a full preview (that's what hover, below, is for). */
.card-slot.selected .hand-card {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
  transform: scale(1.05) translateY(-8px);
}
/* Comes after .selected so hovering a selected card still shows the full
   enlarge-preview (equal specificity — last rule wins). Kept small — just
   enough lift/scale to read the card clearly, not a dramatic zoom. */
.card-slot:hover .hand-card {
  transform: scale(1.08) translate(-6px, -10px);
}
/* Touch devices simulate :hover after a tap and it doesn't clear until a
   different element is tapped, so a selected card on mobile would otherwise
   get stuck at the full hover-preview size (matching the rule above at
   equal specificity) instead of settling to its own modest selected scale.
   Higher specificity here (two classes + :hover) wins regardless of order. */
.card-slot.selected:hover .hand-card {
  transform: scale(1.05) translateY(-8px);
}

/* ---------- action bar ---------- */
.action-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  margin-top: 0.4rem;
  min-height: 44px;
  flex-wrap: wrap;
}
.play-label { font-size: 0.88rem; min-width: 11rem; text-align: right; }
.play-label.muted { color: var(--muted); }
.play-label.ok { color: var(--turn); font-weight: 600; }
.play-label.bad { color: var(--accent); }
.play-label.draw { color: var(--gold); font-weight: 600; }
.action-btns { display: flex; gap: 0.6rem; }
/* Pill buttons with a leading icon glyph, not just text — same treatment
   for every variant's action row (Seven's Throw/Stop, Bluff's
   Throw/Pass/Show!). */
.btn-throw, .btn-stop {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  border-radius: 999px;
  min-width: 108px;
}
.btn-ico { font-size: 0.95em; line-height: 1; }
.btn-throw {
  background: var(--gold);
  color: var(--ink);
}
.btn-throw:hover:not(:disabled) { filter: brightness(1.06); }
.btn-stop {
  background: transparent;
  color: var(--accent);
  border: 1px solid var(--accent);
}
.btn-stop:hover:not(:disabled) { background: var(--accent); color: #fff; }
.btn-stop.solid {
  background: var(--accent);
  color: #fff;
}
.btn-stop.solid:hover:not(:disabled) { filter: brightness(1.1); }

/* deck becomes interactive only when a draw is owed by the viewer */
.deck.clickable img {
  cursor: pointer;
  outline: 3px solid var(--turn);
  outline-offset: 2px;
  animation: deckpulse 1.1s ease-in-out infinite;
}
@keyframes deckpulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(87, 217, 138, 0.0); }
  50% { box-shadow: 0 0 0 6px rgba(87, 217, 138, 0.25); }
}

/* ---------- round-end reveal ---------- */
/* ---------- round summary ----------
   A gold edge on top, one card per player: avatar, name and badges, the
   change this round as a pill, then the total; their cards underneath. */
.modal.roundend {
  max-width: 580px;
  border-top: 3px solid var(--gold);
}
.modal.roundend > h2 { margin-bottom: 0.25rem; }
.re-sub { color: var(--muted); margin: 0 0 var(--sp-5); }
#roundend-body { display: flex; flex-direction: column; gap: var(--sp-2); }
.re-row {
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-md);
  background: var(--bg-2);
  border: 1px solid var(--line);
}
.re-row.caller {
  border-color: color-mix(in srgb, var(--gold) 60%, var(--line));
  background: color-mix(in srgb, var(--gold) 7%, var(--bg-2));
}
.re-head { display: flex; align-items: center; gap: var(--sp-3); }
.re-head + * { margin-top: var(--sp-3); }
.re-av {
  width: 32px; height: 32px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: 50%;
  color: #fff;
  font-family: var(--display);
  font-size: 0.82rem;
  font-weight: 700;
  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
}
.re-av .av-motif { width: 17px; height: 17px; stroke-width: 2.2; }
.re-name {
  flex: 1;
  min-width: 0;
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 0.35rem;
  overflow: hidden;
  white-space: nowrap;
}
.re-score {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--display);
  font-weight: 600;
  white-space: nowrap;
  color: var(--text);
}
.re-delta {
  padding: 0.05rem 0.55rem;
  border-radius: var(--r-pill);
  font-size: var(--fs-sm);
  background: rgba(239, 100, 80, 0.14);
  color: #ff8a73;
}
.re-delta.zero { background: rgba(87, 217, 138, 0.14); color: var(--success); }
.re-total-pts { min-width: 4.2ch; text-align: right; }
.re-cards { display: flex; align-items: center; flex-wrap: wrap; gap: 0.3rem; }
.re-card { width: 40px; height: 56px; border-radius: 5px; box-shadow: 0 4px 10px -6px rgba(0, 0, 0, 0.8); }
.re-total { color: var(--muted); font-size: 0.82rem; margin-left: 0.4rem; }
.re-empty { color: var(--turn); font-size: 0.85rem; font-weight: 600; }

/* game-over rows reuse .re-row; winner uses .caller highlight, out uses .out */
.re-row.out { opacity: 0.5; }

/* ---------- enhancements ---------- */
/* player colour swatch */
.swatch {
  display: inline-block;
  width: 10px; height: 10px;
  border-radius: 3px;
  margin-right: 0.1rem;
  flex: 0 0 auto;
}
/* The Scores rows' mini avatar: the player's colour with their motif or
   initial, matching their seat. */
.swatch.av-mini {
  width: 22px; height: 22px;
  border-radius: 50%;
  display: inline-grid;
  place-items: center;
  color: #fff;
  font-family: var(--display);
  font-size: 0.66rem;
  font-weight: 700;
  line-height: 1;
  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
}
.swatch.av-mini .av-motif { width: 12px; height: 12px; stroke-width: 2.4; }

/* just-drawn card highlight (clears after ~5s) */
.card-slot.just-drawn .hand-card {
  outline: 3px solid var(--gold);
  outline-offset: 1px;
  animation: drawnpulse 1s ease-in-out 0s 3;
}
@keyframes drawnpulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(230,178,60,0.0); }
  50% { box-shadow: 0 0 0 6px rgba(230,178,60,0.35); }
}

.settings-readout {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 0.5rem;
}
.sr-item {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.5rem 0.7rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.sr-item span { font-size: 0.72rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; }
.sr-item strong { font-family: var(--display); color: var(--text); }


/* ---------- footer (home page only) ----------
   Room pages have no footer at all. On the home page it is one slim row: the
   name, Install, and About — the maker's links live in the About dialog. */
.site-footer {
  border-top: 1px solid var(--line);
  padding: 0.85rem var(--page-pad) calc(0.85rem + env(safe-area-inset-bottom));
  background: var(--bg);
}
.footer-main {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  max-width: 1100px;
  margin: 0 auto;
}
.footer-brand-name { font-family: var(--display); font-weight: 600; font-size: 0.85rem; color: var(--muted); }
.footer-links { display: flex; align-items: center; gap: 0.25rem; }
.footer-link {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.65rem;
  background: transparent;
  border-radius: 8px;
  color: var(--muted);
  font-size: 0.82rem;
  font-weight: 500;
}
.footer-link:hover { color: var(--text); background: rgba(255, 255, 255, 0.05); }
.footer-link[hidden] { display: none; }

.about-modal .about-games { color: var(--muted); font-size: 0.85rem; }
.about-links { display: flex; flex-direction: column; gap: 0.25rem; margin-top: 1rem; }
.about-links a {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.6rem 0.5rem;
  border-radius: 10px;
  color: var(--text);
  text-decoration: none;
  font-size: 0.9rem;
}
.about-links a .ico { color: var(--muted); }
.about-links a:hover { background: rgba(255, 255, 255, 0.05); }

/* ---------- Batch 1 enhancements ---------- */
/* 3-second pick countdown on the deck */
.deck { position: relative; }
.pick-timer {
  position: absolute;
  top: -10px; right: -10px;
  min-width: 26px; height: 26px;
  padding: 0 6px;
  border-radius: 13px;
  background: var(--accent);
  color: #fff;
  font-family: var(--display);
  font-weight: 700;
  font-size: 0.9rem;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 2px 8px rgba(0,0,0,0.4);
  animation: pickpulse 1s ease-in-out infinite;
}
@keyframes pickpulse { 0%,100% { transform: scale(1);} 50% { transform: scale(1.15);} }

/* lobby roster: tags + host kick button */
.roster-tags { display: flex; align-items: center; gap: 0.35rem; }
.kick-btn {
  border: none;
  background: transparent;
  color: var(--muted);
  width: 26px; height: 26px;
  border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
  font-size: 0.8rem;
  line-height: 1;
  opacity: 0.6;
  transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease;
}
.kick-btn:hover { background: rgba(239, 68, 68, 0.18); color: #ff6b6b; opacity: 1; }
.kick-btn .ico { width: 15px; height: 15px; }

/* host-editable lobby settings */
.se-hint { font-weight: 400; font-size: 0.7rem; color: var(--muted); text-transform: none; letter-spacing: 0; }
.settings-edit {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: 0.5rem;
}
.se-item { display: flex; flex-direction: column; gap: 0.2rem; }
.se-item label { font-size: 0.72rem; color: var(--muted); }
.se-item input {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.4rem 0.5rem;
  color: var(--text);
  font-size: 0.95rem;
}
.se-save { margin-top: 0.7rem; width: auto; padding: 0.5rem 1rem; }

/* ══════════════════════════════════════════════════════════════════════
   Table motion (core/motion.js sets the body classes)
   ══════════════════════════════════════════════════════════════════════
   Always on the inner card image, never the slot: the hand fan positions each
   slot with its own transform, and an animation there would fight it. Fill mode
   is `backwards`, so once a card has landed its normal styles (raised when
   selected, the fan tilt) apply untouched. */

/* The deal: each card flies in from the deck, one after another. */
@keyframes card-deal {
  from { opacity: 0; transform: translateY(-46vh) rotate(-14deg) scale(0.6); }
  60%  { opacity: 1; }
  to   { opacity: 1; transform: none; }
}
body.dealing .hand .card-slot .hand-card {
  animation: card-deal 520ms var(--ease-out) backwards;
}
body.dealing .hand .card-slot:nth-child(2)  .hand-card { animation-delay: 60ms; }
body.dealing .hand .card-slot:nth-child(3)  .hand-card { animation-delay: 120ms; }
body.dealing .hand .card-slot:nth-child(4)  .hand-card { animation-delay: 180ms; }
body.dealing .hand .card-slot:nth-child(5)  .hand-card { animation-delay: 240ms; }
body.dealing .hand .card-slot:nth-child(6)  .hand-card { animation-delay: 300ms; }
body.dealing .hand .card-slot:nth-child(7)  .hand-card { animation-delay: 360ms; }
body.dealing .hand .card-slot:nth-child(8)  .hand-card { animation-delay: 410ms; }
body.dealing .hand .card-slot:nth-child(9)  .hand-card { animation-delay: 460ms; }
body.dealing .hand .card-slot:nth-child(10) .hand-card { animation-delay: 500ms; }
body.dealing .hand .card-slot:nth-child(n+11) .hand-card { animation-delay: 540ms; }

/* A throw lands in the centre. Both games rebuild the pile only when it
   actually changes, so this plays once per throw — and only on the newest
   cards, so a long Bluff pile does not all drop in again. */
@keyframes card-land {
  from { opacity: 0; transform: translateY(-28px) rotate(-6deg) scale(1.14); }
  to   { opacity: 1; transform: none; }
}
.discard img.center-card:nth-last-of-type(-n+4) {
  animation: card-land 320ms var(--ease-out) backwards;
}

/* Your turn: a soft ring of light rolls out from your seat, twice. */
@keyframes turn-halo {
  0%   { box-shadow: 0 0 0 0 rgba(87, 217, 138, 0.55); }
  100% { box-shadow: 0 0 0 18px rgba(87, 217, 138, 0); }
}
body.turn-arrived .my-seat .opp-avatar {
  animation: turn-halo 650ms ease-out 2;
}

@media (prefers-reduced-motion: reduce) {
  body.dealing .hand .card-slot .hand-card,
  .discard img.center-card,
  body.turn-arrived .my-seat .opp-avatar { animation: none !important; }
}

