/*
  Egyptian War v3 — the table.
  Two ideas carry this stylesheet:
  1. Every size is a fluid TOKEN with the phone value as its floor, so there is one
     layout path for a 320px phone, an iPad and a desktop pane — not four.
  2. The table is a CONTAINER (container-type: size), so the seat ring, the pile card
     and the plates size themselves against the table box rather than the viewport.
     That is what lets one geometry work at 292px and at 734px.
  Viewport classes: phone (<640) · iPad portrait (640-1023) · two-pane (>=1024).
*/
*,
*::before,
*::after {
  box-sizing: border-box;
}

html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  max-width: 100%;
  overflow-x: hidden;
  background: #102018;
  color: #f4ead4;
  font-family: Georgia, "Palatino Linotype", Palatino, "Times New Roman", serif;
}

:root {
  --gold: #d4af37;
  --gold-dim: #c4a574;
  --cream: #e8d5a8;
  --felt-1: #1e3d2e;
  --felt-2: #102018;
  --felt-3: #0b1611;
  --ink: #0c1812;
  --card-ratio: 1.398;          /* 151 / 108 — the card art's aspect */
  --app-max: 430px;             /* phone */
}

body {
  min-height: 100dvh;
  min-height: 100vh;
}

.felt {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(ellipse at 50% 18%, var(--felt-1) 0%, var(--felt-2) 62%, var(--felt-3) 100%);
}

.felt::after {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0.18;
  background:
    repeating-linear-gradient(-18deg, transparent 0 9px, #00000022 9px 10px),
    repeating-linear-gradient(18deg, transparent 0 11px, #d4af370c 11px 12px);
}

.app {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: var(--app-max);
  margin: 0 auto;
  padding: calc(10px + env(safe-area-inset-top, 0px))
           calc(14px + env(safe-area-inset-right, 0px))
           calc(16px + env(safe-area-inset-bottom, 0px))
           calc(14px + env(safe-area-inset-left, 0px));
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
  min-height: 100vh;
  min-width: 0;
}

/* ------------------------------------------------------------------ chrome */

.bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 0 10px;
  border-bottom: 1px solid #d4af3755;
  min-width: 0;
}

.ankh {
  color: var(--gold);
  font-size: 1.2rem;
  line-height: 1;
  flex-shrink: 0;
}

.bar h1 {
  margin: 0;
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  color: var(--cream);
  white-space: nowrap;
  min-width: 0;
}

.net-state {
  margin-left: auto;
  font-size: 0.72rem;
  color: var(--gold);
  letter-spacing: 0.04em;
}

[data-ew="screen"] {
  display: none;
  flex-direction: column;
  gap: 10px;
  width: 100%;
  min-width: 0;
  padding-top: 12px;
}

[data-ew="screen"].on {
  display: flex;
  flex: 1 1 auto;
}

/* ------------------------------------------------------------------- forms */

.lede {
  margin: 0 0 4px;
  color: #cbb88a;
  font-size: 0.95rem;
  line-height: 1.35;
}

.field {
  font-size: 0.8rem;
  letter-spacing: 0.06em;
  color: var(--gold);
}

input, button, summary {
  font-family: inherit;
  color: inherit;
}

input[type="text"] {
  width: 100%;
  min-height: 48px;
  border: 1px solid var(--gold);
  background: var(--ink);
  color: #f4ead4;
  border-radius: 6px;
  padding: 0 12px;
  font-size: 1.05rem;
}

[data-ew="code-input"] {
  text-transform: uppercase;
  letter-spacing: 0.28em;
  font-weight: 700;
  text-align: center;
  flex: 1 1 auto;
  min-width: 0;
  font-size: 1.2rem;
}

.join-row {
  display: flex;
  gap: 8px;
  width: 100%;
  min-width: 0;
}

.btn {
  min-height: 48px;
  border: 1px solid var(--gold);
  background: #163024;
  color: var(--cream);
  padding: 0 14px;
  border-radius: 6px;
  font-size: 0.95rem;
  cursor: pointer;
  touch-action: manipulation;
}

.btn.gold,
.flip-btn {
  background: var(--gold-dim);
  color: var(--felt-2);
  font-weight: 800;
  border-color: var(--gold);
}

.btn.ghost {
  background: transparent;
  border-color: #c4a57455;
  color: #cbb88a;
}

.btn:disabled,
.flip-btn:disabled {
  opacity: 0.42;
  cursor: not-allowed;
}

.btn:focus-visible,
.flip-btn:focus-visible,
.pot:focus-visible,
input:focus-visible,
[data-ew="pet-option"]:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

.or {
  margin: 6px 0 0;
  text-align: center;
  color: var(--gold-dim);
  font-size: 0.85rem;
}

.join-error {
  margin: 0;
  min-height: 1.2em;
  color: #e07070;
  font-size: 0.9rem;
}

.kicker {
  margin: 0;
  text-align: center;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  font-size: 0.72rem;
  color: var(--gold-dim);
}

.room-code {
  text-align: center;
  font-size: clamp(2rem, 11vw, 3.1rem);
  font-weight: 800;
  letter-spacing: 0.22em;
  color: var(--gold);
  line-height: 1.1;
  word-break: break-all;
}

/* ------------------------------------------------------------------ lobby */

.player-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

[data-ew="player"] {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 10px;
  background: #163024;
  border: 1px solid #c4a57466;
  border-radius: 6px;
  min-width: 0;
}

[data-ew="player"] .pname {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

[data-ew="player"] .ready-badge {
  flex-shrink: 0;
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  color: #6a7a70;
}

[data-ew="player"][data-ready="true"] .ready-badge {
  color: var(--gold);
}

.lobby-count {
  margin: 0;
  text-align: center;
  color: #cbb88a;
}

.lobby-actions {
  display: flex;
  gap: 8px;
}

.lobby-actions .btn {
  flex: 1;
}

/* the Delgotchi strip: pick the pet you play as ---------------------------- */

[data-ew="pet-picker"] {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

[data-ew="pet-list"] {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
  gap: 6px;
  max-height: 168px;
  overflow-y: auto;
  padding: 4px;
  background: #0c1812aa;
  border: 1px solid #c4a57444;
  border-radius: 6px;
}

[data-ew="pet-option"] {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 4px;
  border: 1px solid transparent;
  border-radius: 6px;
  background: transparent;
  cursor: pointer;
}

[data-ew="pet-option"] img {
  width: 44px;
  height: 44px;
  border-radius: 6px;
  display: block;
}

[data-ew="pet-option"] .pet-label {
  font-size: 0.6rem;
  color: #cbb88a;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

[data-ew="pet-option"][data-on="1"] {
  border-color: var(--gold);
  box-shadow: 0 0 0 2px #d4af3755;
  background: #16302488;
}

/* ========================================================== THE TABLE ==== */

/*
  v4 — the 3D POV table.

  One idea carries this layout: the FURNITURE lives on a felt PLANE that the
  camera looks down on (CSS `perspective` + `rotateX(--cam-tilt)`), while the
  LABELS stay in screen space. A card's thickness, its tilt and its distance all
  come from being an object ON that plane; a nameplate never foreshortens.

    .table            the box the seat PLATES are measured against (unchanged)
      .stage          the camera: perspective + the tilt
        .plane        the felt: rotateX(--cam-tilt), preserve-3d
          .felt-oval  the cloth itself (scaled to a real oval on the tilted plane)
          .decks      one .deck-rig per seat, positioned/rotated by src/seats.js
          .pile-zone  the middle of the table, where flipped cards land
      .seats          screen-space plates (HUD, never foreshortened)
      .pot            the slap target, over the pile

  Everything on the plane is sized in --cw/--ch (card units), and the felt oval
  is a real 3D object: a 78x62 square scaled to (--oval-w x --oval-w*0.8) and
  stood UP by --felt-rise so it lies flat on the tilted plane.
*/

.table-wrap {
  position: relative;          /* the FLIP button is positioned over the felt */
  width: 100%;
  min-width: 0;
}

.table {
  container-type: size;
  position: relative;
  width: 100%;
  /* The rail lost its two rows (your card count and the "last played" strip), so
     the box can breathe: the width still decides it on a phone (1.18 of the box),
     while a tall narrow pane — an iPad portrait — now gets 72% of the viewport
     instead of 60%. Mirrored by tableSize() in src/seats.js. */
  height: clamp(360px, min(118cqw, 72vh), 620px);
  margin: 0 auto;

  /* the camera */
  --cam-persp: clamp(520px, 175cqw, 1180px);
  --cam-tilt: 31deg;

  /* card units: the floor of every clamp is today's phone look. --cw/--ch are
     the card unit used by every 3D card on the felt (the felt's cards are the
     same physical size wherever they sit — distance, not size, is what makes a
     far deck look smaller).

     A card on the felt is a fraction of the CLOTH, not of the screen: the ratio
     that matters at a table is "how much of the felt does one card cover", and
     it has to hold at 320px and at 1440px alike. Sizing them off the viewport
     put a 72px card on a 234px felt on a narrow phone — a third of the table,
     enough to shove the hero deck off the cloth.
     --card-w/--card-h ARE --cw/--ch: the pile is just cards lying on the felt, so
     it cannot be sized in screen px beside them. Three sizes were showing at once
     on one phone — an opponent's card 57px (flat, far), the pile 72px (screen px),
     yours 87px (lifted) — and on desktop the old 100px cap bound while the felt
     kept growing, so the same card was 26% of the cloth on a phone and 13% on a
     desktop. That is the "the scaling is weird" report, in numbers. The flight
     also landed at FELT size and then popped when the pile repainted at HUD size.
     One unit, a fraction of the cloth, at every viewport. */
  --cw: clamp(38px, calc(var(--oval-w) * 0.185), 170px);
  --ch: calc(var(--cw) * var(--card-ratio));
  --card-w: var(--cw);
  --card-h: var(--ch);

  --plate-w: clamp(76px, min(26cqw, 24.6cqh), 132px);
  --plate-h: calc(var(--plate-w) * 1.22);
  /* How far each plate sits to the LEFT of its own pile. In felt widths (so it
     scales with the table, not the screen), matching PLATE_SHIFT_FELT in
     web/src/seats.js — the seat ring proves every plate still fits after it. */
  --plate-shift: calc(var(--oval-w) * 0.16);
  --avatar: clamp(48px, 17cqw, 88px);
  /* THE SLAP TARGET, and the play area the plates have to stay off. The floors are
     tap-target floors (120px is four thumbs); they used to be 140/170, which on a
     short pane made the pot half the play area and left the plates nowhere to stand
     — so they are lower now, and the pot grows with the table again on the phones it
     was sized for. web/src/seats.js mirrors these numbers (POT_MIN_W/POT_MIN_H/
     POT_W_CARDS/POT_H_CARDS) and web/tests/seats.test.js reads both back. */
  --pot-w: max(120px, calc(var(--cw) * 1.7));
  --pot-h: max(140px, calc(var(--ch) * 1.35));

  /* the felt: one square disc, so a ring of seats on it stays a circle.
     85% of the table's width, not 92%: the camera's perspective makes the plane's
     near corners project ~14% wider than the plane itself (p/(p - R·sin tilt) at
     the box's own corner), and a felt that pokes out of the table box is clipped
     at the sides on a phone. 85% leaves the box's projected corners inside the
     table at every size the verifier measures — and the cloth is the unit
     everything else is a fraction of, so growing it grows the cards with it.
     The HEIGHT term is now 88%, not 96%: with the desktop box at 88vh the felt
     started binding on height too, and the tilt + the 40% perspective origin push
     the plane's projected box DOWN — measured by verify_mp.py, the near rim came
     out 11px past the table's bottom edge at 1440x900. 88% leaves ~19px. It never
     binds on a phone (a phone's felt is bound by its width); it binds on a
     desktop, which is exactly where the room is. */
  --oval-w: min(85cqw, 88cqh);
}

/* ------------------------------------------------------------- the camera */

.stage {
  position: absolute;
  inset: 0;
  z-index: 1;
  perspective: var(--cam-persp);
  perspective-origin: 50% 40%;
  pointer-events: none;
}

.plane {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--oval-w);
  height: var(--oval-w);
  margin: calc(var(--oval-w) / -2) 0 0 calc(var(--oval-w) / -2);
  transform-style: preserve-3d;
  transform: rotateX(var(--cam-tilt));
}

/* the cloth: a true circle lying on the plane, foreshortened by the camera.
   The felt is the light source's subject — a lit table, not a dark hole — so the
   gradient is a wet-looking teal under a warm key light. */
.felt-oval {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: radial-gradient(ellipse at 46% 30%, #245f54 0%, #174b43 52%, #0b2b27 100%);
  box-shadow:
    inset 0 0 46px #00000088,
    inset 0 0 0 1px #c89e6226,
    0 0 0 2px #302820cc,
    0 26px 46px #00000088;
}

/* the glowing rail + the play-direction marks on the cloth */
.felt-ring {
  position: absolute;
  left: 6%;
  top: 6%;
  width: 88%;
  height: 88%;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1px #d4af3744, inset 0 0 34px #d4af3712;
  opacity: 0.9;
}

.felt-arrows {
  position: absolute;
  left: 13%;
  top: 13%;
  width: 74%;
  height: 74%;
  border-radius: 50%;
  border: 1px dashed #d4af3726;
  opacity: 0.75;
}

/* ---------------------------------------------------------- the seat decks */

.decks {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
}

/*
  One seat's deck of cards: a stack of backs on the felt, rotated so it points at
  the player who owns it. The hero deck (yours) additionally lifts toward the
  camera --lift and leans back --tip.
*/
.deck-rig {
  position: absolute;
  width: var(--cw);
  height: var(--ch);
  transform: translate(-50%, -50%);
  transform-style: preserve-3d;
}

.deck-pos {
  position: absolute;
  inset: 0;
  transform: rotate(var(--rot, 0deg));
  transform-style: preserve-3d;
}

.deck {
  position: absolute;
  inset: 0;
  transform-origin: 50% 100%;
  transform-style: preserve-3d;
  transform: translateZ(var(--lift, 0px)) rotateX(calc(var(--tip, 0deg) * -1)) scale(var(--scale, 1));
}

.deck-card {
  position: absolute;
  inset: 0;
  transform: translateZ(calc(var(--n) * 0.42px));
  border-radius: calc(var(--cw) * 0.075);
  box-shadow: 0 2px 3px #0006;
}

.deck-card svg {
  display: block;
  width: 100%;
  height: 100%;
}

/*
  A real pile is never a perfect block: the top few cards sit a degree or two off
  each other. These three are the SAME cards (never extra ones) — just laid as a
  human would have laid them.
*/
.deck-card.k1 { transform: translate3d(-3%, 4px, calc(var(--n) * 0.42px)) rotate(-3.2deg); }
.deck-card.k2 { transform: translate3d(2.6%, 8px, calc(var(--n) * 0.42px)) rotate(2.4deg); }

/* the stack's contact shadow, on the cloth under it */
.deck-rig::after {
  content: "";
  position: absolute;
  left: 6%;
  top: 12%;
  width: 88%;
  height: 84%;
  border-radius: 50%;
  background: #00000055;
  filter: blur(5px);
  transform: translate3d(0, 7%, 0.2px);
  pointer-events: none;
}

/*
  The stack's THICKNESS. A pile of card stock shows its cut edges on the side
  facing the camera, so this block is offset straight down the FELT (toward the
  viewer) rather than along the deck's own axis — which keeps it correct for every
  seat at the table instead of only the one at the bottom. Height comes from the
  card count, so a 26-card deck reads heavy and a 2-card deck reads flat.
*/
.deck-edge {
  position: absolute;
  left: 0;
  top: calc(var(--ch) * 0.06);
  width: var(--cw);
  height: calc(var(--ch) * 0.94 + var(--th, 1px));
  border-radius: 0 0 calc(var(--cw) * 0.075) calc(var(--cw) * 0.075);
  background:
    repeating-linear-gradient(to bottom, #efe6cf 0 0.7px, #b9ad93 0.7px 1.5px),
    #ded3b8;
  box-shadow: 0 3px 6px #0007;
}

.deck-count {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) rotateX(calc(var(--cam-tilt) * -1));
  font-size: clamp(0.52rem, 1.9cqw, 0.7rem);
  font-weight: 700;
  letter-spacing: 0.04em;
  color: #f6ecd4;
  text-shadow: 0 1px 3px #000c, 0 0 6px #0008;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

.deck-rig[data-you="1"] .deck-count {
  font-size: clamp(0.62rem, 2.2cqw, 0.86rem);
  color: var(--gold);
}

/* ------------------------------------------------------------- the centre */

/* The play area's centre is 52% down the plane, not 50%: the cascade is taller
   than the gap between the far decks and the hero's own, and 54% is where
   the pile sits with clearance above AND below (see PILE_AT in src/seats.js —
   the two numbers must agree, or every flight lands a few px off its own card). */
.pile-zone {
  position: absolute;
  left: 50%;
  top: 52%;
  width: var(--card-w);
  height: var(--card-h);
  margin: calc(var(--card-h) / -2) 0 0 calc(var(--card-w) / -2);
  transform-style: preserve-3d;
}

.pile-stack {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
}

/* a card on the pile: face up, flat on the cloth, with a hint of thickness.

   THE TRANSITION IS THE POINT. renderPile() in src/game.js reconciles rather than
   rebuilding, so a card that is already on the felt keeps its element and this is
   what carries it: when a card lands on top, every card under it SLIDES one notch
   up the cloth instead of teleporting. That slide is the history read — you watch
   the pile step back and see which card just arrived. Duration is PILE_SLIDE_MS
   in src/game.js; the verifier reads both and fails if they disagree. */
.ew-card {
  position: absolute;
  left: 0;
  top: 0;
  width: var(--card-w);
  height: var(--card-h);
  line-height: 0;
  transform-style: preserve-3d;
  transition: transform 300ms cubic-bezier(0.22, 0.85, 0.28, 1);
}

.ew-card svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* each card lies a touch differently, so a double or a sandwich is visible */
.ew-card.d0 { transform: translate(0, calc(var(--card-h) * -0.06)) rotate(-2deg) translateZ(calc(var(--n) * 0.5px)); z-index: 4; }
.ew-card.d1 { transform: translate(calc(var(--card-w) * -0.07), calc(var(--card-h) * 0.01)) rotate(-7deg) translateZ(calc(var(--n) * 0.5px)); z-index: 3; }
.ew-card.d2 { transform: translate(calc(var(--card-w) * 0.09), calc(var(--card-h) * 0.03)) rotate(5deg) translateZ(calc(var(--n) * 0.5px)); z-index: 2; }
.ew-card.d3 { transform: translate(calc(var(--card-w) * -0.09), calc(var(--card-h) * 0.07)) rotate(9deg) translateZ(calc(var(--n) * 0.5px)); z-index: 1; }

.ew-card svg {
  filter: drop-shadow(0 3px 2px #0006);
}

/* the flight layer: one card in the air, deck -> middle */
.flight-layer {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  pointer-events: none;
  z-index: 8;
}

.flight-layer.calibrating {
  visibility: hidden;
}

.flight {
  position: absolute;
  width: var(--cw);
  height: var(--ch);
  line-height: 0;
  transform-style: preserve-3d;
  will-change: transform, opacity;
}

/* the flying card is a card-sized box: the layer IS the card, and the .c3 fills
   it. (Without a SIZE on the layer, `.c3 { height: 100% }` resolves against an
   auto height, collapses to 0 and the flying card is invisible — the deck and
   pile cards never hit this because their parents are sized.) */
.flight .card-anim {
  width: var(--cw);
  height: var(--ch);
  /* THE 3D CHAIN HAS TO SURVIVE THE FLIGHT'S WRAPPER. `.c3` and `.c3-flip` are
     preserve-3d and so is `.fan-card .card-anim`, but this one was not — so the
     two faces of the card were laid out FLAT and painted in DOM order, back over
     face, for the entire flight. The card turned over in the transform all along;
     what you saw was always a back, and the reveal only appeared when the pile
     painted the card after landing — "it just appears in the middle." */
  transform-style: preserve-3d;
}

.flight .c3 {
  width: 100%;
  height: 100%;
}

.flight .c3-flip svg {
  width: 100%;
  height: 100%;
}

.flight.is-reveal {
  filter: drop-shadow(0 10px 8px #0008);
}

/* the 3D card: back + face, an extruded edge, and a real half-turn */
.c3 {
  position: relative;
  width: var(--cw);
  height: var(--ch);
  transform-style: preserve-3d;
}

.c3-flip {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
}

.c3-face,
.c3-back {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  line-height: 0;
  border-radius: calc(var(--cw) * 0.08);
  overflow: hidden;
}

.c3-back svg,
.c3-face svg {
  display: block;
  width: 100%;
  height: 100%;
}

.c3-back { transform: rotateY(0deg); }
.c3-face { transform: rotateY(180deg); }

.c3-down .c3-back { box-shadow: 0 2px 4px #0007; }

/* ------------------------------------------------------ the slap target --- */

/*
  The pile is a lit, thumb-sized zone over the middle of the felt. It is HUD: it
  never foreshortens (a 3D-transformed tap target is a bad tap target), and it is
  what a slap actually hits.
*/
.pot {
  position: absolute;
  left: 50%;
  top: 52%;                    /* the play area's centre — see .pile-zone above */
  transform: translate(-50%, -50%);
  width: var(--pot-w);
  height: var(--pot-h);
  min-height: 120px;             /* mirrors seats.js POT_MIN_H's intent: a tap target,
                                    not a slab (see --pot-h above) */
  border: 0;
  border-radius: 50%;
  background: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-user-select: none;
  user-select: none;
  z-index: 6;
  overscroll-behavior: contain;
}

.pot.hot {
  background: radial-gradient(closest-side, #d4af3722, #d4af3700 74%);
  box-shadow: 0 0 0 2px #d4af3766, 0 0 30px #d4af3744;
}

/* the flip and the fan-out — durations are asserted by the verifier --------- */

.card-anim {
  display: block;
  line-height: 0;
  will-change: transform, opacity;
}

/*
  THE FLIP: a card leaves the flipper's OWN deck and lands face up on the pile.
  The deltas are per-card custom properties (--fx/--fy in the felt's px, --frot0
  the seat's angle, --frot1 where it settles), so one keyframe track serves every
  seat and every card, and the card turns over about its OWN long axis — which is
  what makes the revealed face readable to the player who flipped it.
*/
/* the flip: one card, deck to pile. 640ms, with the turn already finished by 30%.
   Two measured reasons, both from the live table:
   - THE FLIP HAD TO CARRY ITS OWN READ. The felt is only ~88px deep between the
     flipper's deck and the pile, so a card that merely translates between them
     moves 88px in a second — measured on the live site the whole flight was a
     vertical drift, and the owner described it exactly: "it just appears in the
     middle". The toss is --farc (an apex in SCREEN px, set by the client from the
     felt's own height) plus a scale pop, so the card visibly arcs over the felt and
     grows as it comes at you.
   - 760ms of that was visibly sluggish ("laggy"), so the same arc is delivered
     faster. FLIGHT_MS in src/game.js is this number; the verifier reads both and
     fails if they disagree.

   WHY THE ARC IS 2D, NOT A translateZ: measured on this very table, with the
   animation cancelled and the transform set by hand, translate3d(0,0,0),
   translate3d(0,0,60px) and translate3d(0,0,120px) all produce the IDENTICAL box
   (71.8x93.5 at the same point) while translate3d(0,-61px,0) moves it 61px up. A
   translateZ on the flying card does not project here at all — so the old --flift
   "arc" (64px, then 118px) was a no-op that looked like a fix. Anything that has to
   be SEEN on the flight must be a 2D offset or a scale. */
.card-anim.anim-flight {
  animation: ew-card-flight 640ms cubic-bezier(0.34, 0.55, 0.2, 1) both;
  /* will-change comes from the base .card-anim rule (transform, opacity) — do not
     stack a second one here: a will-change on a preserve-3d ancestor is itself a
     way to lose the 3D chain. */
}

/* an illegal slap: the burned card slides UNDER the pile and stays there */
.card-anim.anim-burn {
  animation: ew-card-burn 420ms cubic-bezier(0.3, 0.7, 0.3, 1) both;
}

@keyframes ew-card-flight {
  0% {
    /* it lifts off the stack it came from: --fz0 is that deck's own height above
       the cloth (the viewer's deck floats), --fs0 its scale, so the card does not
       pop smaller and sideways the instant it leaves the deck. The 6px and the 4%
       on top of that are what make the launch VISIBLE: the flight is inserted at the
       deck's own centre, so without them the first fifth of a second (the hold, plus
       the run to the 16% stop) is a card hidden inside the stack it just left. 6px
       also stays inside the "the card starts on its own deck" contract the verifier
       asserts, which allows 14. */
    opacity: 0.98;
    transform: translate3d(0, -6px, calc(var(--fz0, 0px) + 12px)) rotateZ(var(--frot0))
               rotateY(0deg) scale(calc(var(--fs0, 1) * 1.04));
  }
  16% {
    opacity: 1;
    transform: translate3d(calc(var(--fx) * 0.16), calc(var(--fy) * 0.16 - var(--farc) * 0.55), 18px)
               rotateZ(calc(var(--frot0) + (var(--frot1) - var(--frot0)) * 0.16))
               rotateY(56deg) scale(1.06);
  }
  30% {
    /* THE TOSS. --farc is the apex of the arc, in SCREEN px, and it is what makes
       this read as a card being thrown rather than a card sliding — see the note on
       anim-flight above: a translateZ on this element does not project at all, so
       the arc has to be 2D. The scale is the depth cue the perspective cannot give.
       THE TURN IS ALSO DONE BY HERE, which is the other half: a card that turns over
       at the middle of its own flight is edge-on — measured on the live table at
       exactly that moment it was 110x21px, a sliver — and a flip you cannot see
       reads as "it just appeared in the middle". A real card does snap over fast. */
    transform: translate3d(calc(var(--fx) * 0.30), calc(var(--fy) * 0.30 - var(--farc)), 26px)
               rotateZ(calc(var(--frot0) + (var(--frot1) - var(--frot0)) * 0.30))
               rotateY(150deg) scale(1.13);
  }
  44% {
    opacity: 1;
    transform: translate3d(calc(var(--fx) * 0.46), calc(var(--fy) * 0.46 - var(--farc) * 0.92), 22px)
               rotateZ(calc(var(--frot0) + (var(--frot1) - var(--frot0)) * 0.46))
               rotateY(180deg) scale(1.13);
  }
  72% {
    transform: translate3d(calc(var(--fx) * 0.78), calc(var(--fy) * 0.78 - var(--farc) * 0.30), 10px)
               rotateZ(calc(var(--frot0) + (var(--frot1) - var(--frot0)) * 0.78))
               rotateY(180deg) scale(1.05);
  }
  100% {
    opacity: 1;
    transform: translate3d(var(--fx), var(--fy), 1.4px) rotateZ(var(--frot1)) rotateY(180deg) scale(1);
  }
}

@keyframes ew-card-burn {
  0% {
    opacity: 0.96;
    transform: translate3d(0, 0, var(--fz0, 0px)) rotateZ(var(--frot0)) rotateY(0deg) scale(var(--fs0, 1));
  }
  55% {
    opacity: 1;
    transform: translate3d(calc(var(--fx) * 0.55), calc(var(--fy) * 0.55), 26px)
               rotateZ(calc(var(--frot0) + 10deg)) rotateY(0deg) scale(1.05);
  }
  100% {
    /* BELOW the pile, but never below the cloth: a card animated to a negative
       Z disappears behind the opaque felt, which is the bug this avoids. */
    opacity: 1;
    transform: translate3d(var(--fx), var(--fy), 0.6px) rotateZ(var(--frot1)) rotateY(0deg) scale(1);
  }
}

@keyframes ew-fan-in {
  0%   { opacity: 0; transform: translate3d(0, 9px, 0) scale(0.85); }
  70%  { opacity: 1; }
  100% { opacity: 1; transform: translate3d(0, 0, 0); }
}

/* the fan is sized by the table, not by a hard-coded 320px ------------------ */

.card-anim.anim-fan {
  animation: ew-fan-in 1320ms cubic-bezier(0.2, 0.9, 0.25, 1) both;
}

/*
  A WON PILE: the last cards fan out on the felt, lifted off the cloth and leaned
  toward the camera so the double or sandwich that won it is unmistakable.

  THE FAN BOX IS THE CARD SPAN, AND NOTHING MAY CLAMP IT.

  The n cards are placed at ((i + 0.5) / n) of THIS box, so the gap between two
  neighbours is exactly --fan-w / n, and the only thing that decides the overlap is
  how big a card is next to that gap. So the box is sized FROM n — n steps of 0.62
  of a card, which is the ~38% overlap a fan wants — and capped by the cloth, never
  by whatever box happens to contain it.

  THE MEASURED BUG THIS REPLACES: `max-width: 100%` silently clamped the box to its
  parent (.pile-zone), which is exactly ONE CARD wide (83px on a 390px phone, while
  --fan-w implied 288px). The cards' `left` percentages then resolved against 83px
  while `margin-left` still used the UNCLAMPED --fan-w/2, so four ~50px cards landed
  22px apart — 65% overlap, "the cards are cutting into each other" — and the whole
  fan sat ~100px left of the cloth's centre with the oldest card at x = -20px, off
  the table. A won pile has to be readable; nothing here may be width-clamped.
*/
.fan {
  /* One step is --fan-step of a card, and --fw below is derived FROM the step, so
     the overlap is always the same ~38% however the numbers move. Two bounds,
     both learned the hard way:
       - the CORRIDOR (40% of the cloth): a won pile fans out over the MIDDLE, and
         at three or four seats the opponents' decks close the middle in. A fan
         wide enough to touch them reads as one mess of cards — the owner's report
         was exactly that, "the decks do not intersect". web/tests/seats.test.js
         proves the fan's cards clear every deck with SAT at 2..6 seats.
       - 92cqw: never wider than the stage.
     NOTHING may width-clamp this box: its containing block (.pile-zone) is one
     card wide, and a `max-width` there silently rewrote every percentage inside. */
  --fan-step: 0.44;
  --fan-w: min(calc(var(--cw) * var(--n, 4) * var(--fan-step)),
               calc(var(--oval-w) * 0.40),
               92cqw);
  --fan-h: calc(var(--ch) * 1.3 + 18px);
  /* the card inside is capped at a step / 0.62 — the same 38%, whatever n is, and
     it shrinks with the cloth when the felt, not n, is the binding constraint */
  --fw: max(22px, min(calc(var(--cw) * 0.86),
                    calc(var(--fan-w) / (var(--n, 4) * 0.62))));
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--fan-w);
  max-width: none;
  height: var(--fan-h);
  margin: calc(var(--fan-h) / -2) 0 0 calc(var(--fan-w) / -2);
  transform: translateZ(24px);
  transform-style: preserve-3d;
  pointer-events: none;
  z-index: 5;
}

.fan.sweeping {
  animation: ew-fan-sweep 520ms cubic-bezier(0.4, 0.05, 0.7, 0.4) both;
}

@keyframes ew-fan-sweep {
  0%   { opacity: 1; transform: translate3d(0, 0, 24px) scale(1); }
  100% { opacity: 0; transform: translate3d(var(--sx, 0px), var(--sy, 0px), 12px) scale(0.42); }
}

.fan-card {
  left: auto;
  top: 50%;
  line-height: 0;
  transform-origin: 50% 100%;
  transform-style: preserve-3d;
}

.fan-card .card-anim {
  transform-style: preserve-3d;
}

.fan-card svg {
  width: var(--fw);
  height: calc(var(--fw) * var(--card-ratio));
  filter: drop-shadow(0 6px 5px #0008);
}

.fan-card.is-hot svg {
  filter: drop-shadow(0 6px 5px #0008) drop-shadow(0 0 5px var(--gold));
}

/* the DECIDING card of a won pile wears a gold trim — but an `outline` on this
   wrapper lands on the WRAPPER's box, which is a full --card-w while the card the
   player sees is the --fw SVG inside it: measured on the felt, the trim sat ~11px
   proud of the card it was supposed to be marking, which is the owner's "the outline
   of the final card is not fitting properly". Drawn on the SVG's own box instead, so
   the trim hugs the card at every fan width. */
.fan-card.is-reason svg {
  box-shadow: 0 0 0 2px var(--gold);
  border-radius: calc(var(--fw) * 0.075);
}

body.revealing .pot {
  box-shadow: 0 0 0 2px #d4af3755, 0 0 34px #d4af3744;
  cursor: default;
}

body.revealing .pot .slap-hint,
body.revealing .pot .pile-empty {
  opacity: 0;
}

/*
  THE SLAP CALL. It has to be the loudest thing on the felt for the half-second a
  slap is legal — the pile is in the middle, the thumb is on the FLIP button, and a
  missed double is a lost pile. It used to be 0.72rem of gold text on a gold-tinted
  felt, i.e. camouflaged by the felt ring, the pile's own indices and the deck
  counts (the owner: "slap icon more noticeable a different color and a little
  bigger"). So: a filled pill in HOT CORAL — the one warm accent on the table that
  is not gold — with the hand glyph, ~45% bigger type, and a slow pulse while the
  slap is live. Nothing else on the cloth is this colour, which is the point.
*/
.slap-hint {
  margin-top: 8px;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 15px;
  border-radius: 999px;
  background: linear-gradient(#ff6a4d, #d93d21);
  color: #fff8f2;
  font-size: 1.05rem;
  font-weight: 800;
  letter-spacing: 0.16em;
  line-height: 1.2;
  opacity: 0;
  transform: scale(0.86);
  box-shadow: 0 8px 20px #00000077, 0 0 0 2px #ffd7c866 inset, 0 0 22px #ff5a3a66;
  text-shadow: 0 1px 2px #00000066;
  transition: opacity 130ms ease, transform 130ms ease;
  pointer-events: none;
}

.slap-hint .slap-hand {
  font-size: 1.25em;
  line-height: 1;
  filter: drop-shadow(0 1px 1px #0007);
}

.pot.hot .slap-hint {
  opacity: 1;
  transform: scale(1);
  animation: ew-slap-pulse 900ms ease-in-out infinite;
}

@keyframes ew-slap-pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.07); }
}

.pile-empty {
  position: absolute;
  bottom: 6px;
  left: 8px;
  right: 8px;
  font-size: 0.8rem;
  color: var(--gold-dim);
  text-align: center;
}

/* ----------------------------------------------------------- the seats --- */

.seats {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.seat {
  position: absolute;
  width: var(--plate-w);
  height: var(--plate-h);
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: opacity 220ms ease;
  z-index: 4;
}

/* dimmed, never hidden: everyone's name and count stay readable */
.seat[data-turn="0"] {
  opacity: 0.72;
}

.seat[data-out="1"] {
  opacity: 0.36;
}

.seat[data-you="1"] {
  opacity: 1;
}

.seat-ring {
  position: absolute;
  inset: -3px;            /* was -5: the trim's standoff is part of "too big" too */
  border-radius: calc(var(--plate-w) * 0.16);
  pointer-events: none;
}

.seat[data-turn="1"] .seat-ring {
  /* the gold trim that says "this seat is up". It was inset -5px with a 3px ring and
     a 28px glow — 8px of gold standing off the plate, which the owner read as too
     big ("the out lines for character turns are too big", clarified: "what I mean
     for outlines is the gold trim"). It is now 3px of everything instead of 8: a 2px
     ring at -3px with a tight 14px glow, so it reads as trim rather than as a frame. */
  box-shadow: 0 0 0 2px var(--gold), 0 0 14px #d4af3755;
  background: #d4af3712;
}

.seat-ring.turn-pulse {
  animation: ew-turn-pulse 900ms cubic-bezier(0.2, 0.9, 0.25, 1) both;
}

@keyframes ew-turn-pulse {
  0%   { opacity: 0; transform: scale(0.9); }
  40%  { opacity: 1; transform: scale(1.04); }
  100% { opacity: 1; transform: scale(1); }
}

.seat-plate {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  width: 100%;
  min-width: 0;
}

.seat-avatar {
  width: var(--avatar);
  height: var(--avatar);
  border-radius: calc(var(--avatar) * 0.18);
  overflow: hidden;
  background: var(--ink);
  border: 1px solid #c4a57466;
  box-shadow: 0 3px 8px #0007;
  flex-shrink: 0;
}

.seat-avatar img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}

.seat-avatar .seat-initials {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  font-weight: 800;
  font-size: calc(var(--avatar) * 0.36);
  color: var(--gold);
  letter-spacing: 0.04em;
}

.seat-name {
  font-size: clamp(0.6rem, 2.4cqw, 0.78rem);
  color: var(--cream);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: 1.15;
}

.seat-handle {
  font-size: clamp(0.5rem, 1.9cqw, 0.62rem);
  color: #9fb3a6;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: 1.1;
}

.seat-count {
  font-size: clamp(0.62rem, 2.6cqw, 0.86rem);
  font-weight: 700;
  color: var(--gold);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.seat[data-you="1"] .seat-name::after {
  content: " · you";
  color: var(--gold-dim);
  font-weight: 400;
}

/* --------------------------------------------------------------- the rail */

.rail {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
}

.turn-banner {
  margin: 0;
  text-align: center;
  color: var(--gold);
  font-size: 0.92rem;
  min-height: 1.2em;
}

.event {
  margin: 0;
  min-height: 2.3em;
  padding: 8px 10px;
  background: var(--cream);
  color: #1a1408;
  border-radius: 4px;
  font-size: 0.9rem;
  text-align: center;
}

.event.bump {
  animation: ew-event-bump 340ms ease-out;
}

@keyframes ew-event-bump {
  0%   { transform: translateY(-3px); opacity: 0.6; }
  100% { transform: none; opacity: 1; }
}

/*
  THE FLIP BUTTON SITS ON YOUR OWN STACK. It used to live in the rail, a
  thumb-length below the felt and off to the side — the one place nobody is
  looking while the pile is being played. placeFlip() in src/game.js measures the
  hero deck's box on the felt and puts the button on it, so it scales with the
  deck, moves with the ring, and stays under the thumb on a phone. It cannot eat
  the slap target: the pot is centred on the felt's middle, and the ring's
  clearance rule keeps every deck (and so this button) off it.
*/
.flip-btn {
  position: absolute;
  left: 50%;
  top: 0;                      /* both axes replaced in px by placeFlip() */
  transform: translate(-50%, -50%);
  z-index: 6;                  /* over the cloth, under the seat plates */
  min-width: 128px;
  max-width: 52%;
  min-height: 60px;
  padding: 0 20px;
  border: 2px solid var(--gold);
  border-radius: 14px;
  font-size: 1.3rem;
  letter-spacing: 0.14em;
  cursor: pointer;
  box-shadow: 0 10px 24px #000000a6, 0 0 0 3px #00000055 inset;
  touch-action: manipulation;
  -webkit-user-select: none;
  user-select: none;
}

.flip-btn:not(:disabled):hover {
  background: #d4af37;
}

.pot:not(.revealing):hover {
  box-shadow: 0 0 0 2px #d4af3755;
}

.rules {
  font-size: 0.88rem;
  color: var(--cream);
  border-top: 1px solid #c4a57444;
  padding-top: 8px;
  margin-top: auto;
}

.rules summary {
  min-height: 44px;
  display: flex;
  align-items: center;
  cursor: pointer;
  color: var(--gold);
}

.rules p {
  margin: 0 0 10px;
  color: #d9c9a4;
  line-height: 1.45;
}

/* --------------------------------------------------------------- overlay */

.overlay {
  position: fixed;
  inset: 0;
  z-index: 20;
  background: #102018e8;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

/* ---- leaving, the way back, and a held seat (v3.1) ------------------------- */

.leave-warn {
  margin: 10px 0 16px;
  font-size: 0.95rem;
  line-height: 1.35;
  color: #3b2f16;
}

.leave-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.leave-actions .btn {
  min-height: 52px;
}

/* the rail's Leave: small and quiet, and NEVER inside the slap zone */
.leave-rail {
  align-self: center;
  margin-top: 10px;
  min-height: 40px;
  padding: 8px 18px;
  font-size: 0.82rem;
  letter-spacing: 0.06em;
}

.rejoin-note {
  margin: 14px 0 0;
  font-size: 0.9rem;
  line-height: 1.35;
  color: #cbb88a;
}

[data-ew="rejoin"] {
  width: 100%;
  margin-top: 10px;
  min-height: 52px;
  border-color: var(--gold);
  color: var(--gold);
}

/* a seat whose person stepped away: dimmed, tagged, and obviously still theirs */
.seat-away {
  display: none;
  margin-top: 2px;
  font-size: 0.6rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #e0c37a;
}

.seat[data-away="1"] .seat-away {
  display: block;
}

.seat[data-away="1"] .seat-plate {
  opacity: 0.55;
  border-style: dashed;
}

.seat[data-away="1"] .seat-ring {
  opacity: 0.25;
}

.overlay-card {
  width: 100%;
  max-width: 340px;
  background: var(--cream);
  color: #1a1408;
  border: 2px solid var(--gold);
  border-radius: 8px;
  padding: 22px 18px;
  text-align: center;
}

.overlay-kicker {
  margin: 0;
  letter-spacing: 0.14em;
  font-size: 0.75rem;
  text-transform: uppercase;
  color: #5c4a28;
}

.winner-text {
  margin: 10px 0 18px;
  font-size: 1.7rem;
  font-weight: 700;
}

[data-ew="again"] {
  min-width: 160px;
  min-height: 48px;
}

.hidden,
.pile-empty.hidden {
  display: none !important;
}

/* ===================================================== viewport classes == */

/*
  ONE RIG, TWO FRAMINGS. The reference framing (a big table under a warm key
  light, the players' decks round the rim and your own pile standing nearest the
  camera) is what the desktop two-pane view shows; the phone shows the SAME felt,
  the SAME tilt and the SAME materials, just smaller and with the rail beneath
  it. Nothing about the table changes shape between them — only the room it gets.
*/

/* iPad portrait: a wider column, a bigger table, the rules open ----------- */
@media (min-width: 640px) {
  :root { --app-max: 720px; }

  .seats .seat-name { font-size: 0.8rem; }
  .flip-btn { font-size: 1.6rem; }
  .rules[open] summary { margin-bottom: 6px; }
}

/* Phones used to get a screen-space "last played" strip here. It is gone: the pile
   itself is the read now — four cards deep, each one's index corner showing, and
   sliding to its new notch as the stack grows. See reconcilePile() in src/game.js.
   The rail it lived in is two rows shorter, which is where the extra table height
   below comes from. -------------------------------------------------------------- */

/* five and six seats need the CARD smaller, not the table: the ring is tighter,
   so both the decks and the pile use --cw = 0.16 of the cloth (CARD_R_SMALL in
   src/seats.js carries the same number, and web/tests/seats.test.js reads this
   block to prove the two cannot drift). */
.table[data-seats="5"],
.table[data-seats="6"],
body[data-seats="5"],
body[data-seats="6"] {
  /* five and six seats: the ring is tighter, so the plate's leftward nudge is
     halved rather than dropped (matches the ring<=4 factor in seats.js boxFor,
     and 0.16-felt overlapped a neighbour at 5 seats on a 320px phone). */
  --plate-shift: calc(var(--oval-w) * 0.08);
  --cw: clamp(34px, calc(var(--oval-w) * 0.16), 150px);
  --ch: calc(var(--cw) * var(--card-ratio));
  --card-w: var(--cw);
  --card-h: var(--ch);
  /* and the won pile fans out a step tighter, so four cards still fit the
     corridor the six decks leave in the middle */
  --fan-step: 0.40;
}

/* iPad landscape / desktop: table left, rail right, whole thing centred ---- */
@media (min-width: 1024px) {
  /* THE DESKTOP IS ITS OWN LAYOUT, and the owner asked for it twice: "schedule
     change to optimize for desktop use" and "fix the desktop view to be bigger.
     mobile and desktop views have to be different to adjust to the different aspect
     ratios". A phone is a TALL box and the felt is bound by its width; a desktop is
     a WIDE box where the felt used to be bound by the box's height and the app's own
     1180px cap, so a 1920x1080 screen showed a 687px disc in a 1180px column with
     ~370px of empty screen either side — measured, not guessed. The column now takes
     the room a desktop actually has (up to 1560px or 92vw) and the box takes 88vh
     (up to 950px), which is what makes the cloth — and every card on it — 912px
     instead of 687 on that screen: +33% of felt and +33% of card, and the seat ring
     spreads with the wider box. The CLOTH stays 85% of the box's width (the plane's
     near corners project 1.14 of it, so a felt that filled a wide-but-short pane
     would be clipped at the sides — measured by verify_mp.py), and 96% of the
     height: at 1440x900 and 1920x1080 both terms are live, which is the design
     point of a desktop box that is ~1.25 as wide as it is tall rather than square. */
  :root { --app-max: 1560px; }

  .app {
    padding-left: calc(18px + env(safe-area-inset-left, 0px));
    padding-right: calc(18px + env(safe-area-inset-right, 0px));
  }

  [data-ew="screen"][data-screen="table"].on {
    display: grid;
    grid-template-columns: minmax(560px, 1fr) 320px;
    grid-template-rows: auto;
    align-items: start;
    gap: 16px;
  }

  [data-screen="table"] .table-wrap { grid-column: 1; grid-row: 1; }

  [data-screen="table"] .rail {
    grid-column: 2;
    grid-row: 1;
    position: sticky;
    top: 16px;
    max-height: calc(100dvh - 32px);
    overflow-y: auto;
  }

  /* the table gets the room a desktop has: 88% of the viewport's height (950px
     ceiling), so the cloth inside it is bound by BOTH its width and its height and
     grows with the screen. Mirrored by tableSize()/appWidth() in src/seats.js. */
  .table {
    height: clamp(480px, min(88vh, 950px), 950px);
    --pot-w: clamp(130px, 24cqw, 190px);
  }

  .rail .rules { margin-top: 0; }
  .seats .seat-name { font-size: 0.86rem; }
  .turn-banner { font-size: 1rem; }
  .event { font-size: 0.95rem; }
}

/* a phone in landscape has the height, not the width: keep the felt circular
   and let the rail sit under it rather than squeezing the table flat --------- */
@media (max-height: 560px) and (min-width: 640px) {
  .table { height: clamp(320px, min(96cqw, 82vh), 520px); }
  .flip-btn { min-height: 56px; height: auto; font-size: 1.15rem; }
}

/* narrow phones: the card floor shrinks so 6 decks still fit the felt ------ */
@media (max-width: 360px) {
  .app { padding-left: 10px; padding-right: 10px; }
  .flip-btn { font-size: 1.25rem; letter-spacing: 0.08em; }
  .table {
    --plate-w: 76px;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* A reduced-motion DESKTOP still has to show the flip, and it has to show WHICH
     CARD it was. This rule's first cut turned the flight off (the card simply
     arrived), and its second cut replaced it with a de-spun 240ms slide — measured
     on this table that is a card that appears on the pile already face up, which is
     the owner's "animations dont work on desktop" and, before that, "it just appears
     in the middle". The flight is information, not decoration: it keeps the turn and
     is shortened and flattened instead (ew-card-toss: 360ms, half the arc, no scale
     pop). Pure decoration (the turn pulse, the fan settling, the event nudge, the
     slap call's pulse) still goes. */
  .seat-ring.turn-pulse,
  .fan.sweeping,
  .event.bump,
  .pot.hot .slap-hint {
    animation: none !important;
  }
  .card-anim.anim-flight {
    animation: ew-card-toss 360ms cubic-bezier(0.34, 0.55, 0.2, 1) both !important;
  }
  /* the burn is information too — the card you threw away slides under the pile —
     so it keeps its movement at half the time instead of vanishing mid-air */
  .card-anim.anim-burn {
    animation: ew-card-burn 240ms linear both !important;
  }
  /* the pile's slide is information (which card just arrived, and which ones
     stepped back), so it is SHORTENED rather than deleted: the cards still end up
     where they belong. 300ms -> 120ms. */
  .ew-card { transition-duration: 120ms; }
  .seat { transition: none; }
}

/* the reduced-motion flight: the same deck-to-pile line, ~30% of the arc, no scale
   pop, and the TURN KEPT — the card still shows itself to the player who flipped it.
   The apex is expressed against --farc (the flight's own arc, set per launch by
   src/game.js) the same way the full keyframes express it, so it scales with the
   felt: measured at 1440x900 the full flight peaks 163px above its landing and this
   one ~96px, well clear of the 24px at which a toss stops reading as a toss. */
@keyframes ew-card-toss {
  0%   { opacity: 0.98;
         transform: translate3d(0, -6px, 20px) rotateZ(var(--frot0)) rotateY(0deg); }
  42%  { opacity: 1;
         transform: translate3d(calc(var(--fx) * 0.45), calc(var(--fy) - var(--farc) * 0.30), 20px)
                    rotateZ(calc(var(--frot0) + (var(--frot1) - var(--frot0)) * 0.45)) rotateY(155deg); }
  100% { opacity: 1;
         transform: translate3d(var(--fx), var(--fy), 1.4px) rotateZ(var(--frot1)) rotateY(180deg); }
}
