/* Coup — client stylesheet. Mobile-first. NEVER 100vh: use 100dvh + safe-area insets. */

:root {
  --bg: #101018;
  --panel: #17171f;
  --panel-2: #1b1b26;
  --line: #26263a;
  --line-2: #32324a;
  --text: #e8e6f0;
  --text-dim: #9aa0b4;
  --gold: #ffd34d;
  --purple: #7c5cff;
  --danger: #e5405e;
  --ok: #3dbf7f;
  --radius: 14px;
  /* the table: one camera, one cloth. Every card on the cloth is sized off --felt, never the
     viewport, or a phone gets a card that is a different fraction of the table than a laptop's. */
  --cam-tilt: 38deg;
  --cam-persp: 1000px;
  /* the felt leaves room for its own perspective: the near edge of a tilted plane projects WIDER
     than the plane itself, so the cloth is sized off the box, then the camera gets its margin */
  --felt: min(86cqw, 82cqh, 500px);
  --mini-w: 15px;
  --log-lines: 4;              /* MUST match LOG_TAIL in game.js: the band holds one full tail.
                                  ONE deliberate exception: `#table.claiming .log` drops it to 1 while a
                                  claim is up, so the card gets the room back. */
  /* The table's typeface. ITC Bookman Bold Italic is a licensed face, so the game ships its free
     metric twin — URW Bookman Demi Italic, the base-35 Bookman: same design, near-identical
     advances — out of this same mount. No CDN, no network, no fallback lottery on a phone. */
  --font-coup: 'Coup Bookman', 'ITC Bookman', 'Bookman Old Style', 'URW Bookman', Georgia, 'Times New Roman', serif;
}

/*
 * ONE face on purpose: ITC Bookman Bold Italic is the whole client's look, so every weight in this
 * sheet maps onto the single shipped face and `font-synthesis: none` (body) stops the browser from
 * faking a bolder or a slanted one. `font-weight` spans the full range so no rule here can pull a
 * different font in by asking for 400 or 900.
 */
@font-face {
  font-family: 'Coup Bookman';
  src: url('fonts/bookman-bold-italic.woff2') format('woff2');
  font-weight: 100 900;
  font-style: italic;
  font-display: swap;
}

* { box-sizing: border-box; }

/* A form control does NOT inherit the page's font from the UA sheet (Chrome hands an input its own
   system face), so a button or a text field silently keeps the browser's font and the table ends up
   with two. Say it explicitly. */
button, input, select, textarea { font-family: inherit; font-style: inherit; }

/* The `hidden` attribute must win over the display rules below (flex/grid). */
[hidden] { display: none !important; }

html, body {
  height: 100%;
  margin: 0;
}

body {
  height: 100dvh;
  width: 100vw;
  overflow: hidden;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-coup);
  font-style: italic;          /* the shipped face IS the italic — never a synthesised slant */
  font-synthesis: none;
  -webkit-tap-highlight-color: transparent;
  -webkit-text-size-adjust: 100%;
  overscroll-behavior: none;
  user-select: none;
}

.screen {
  height: 100dvh;
  width: 100vw;
  overflow: hidden;
}

/* ---------------------------------------------------------------- Lobby --- */

#lobby {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 12px;
  padding: calc(env(safe-area-inset-top) + 24px) 20px calc(env(safe-area-inset-bottom) + 24px);
  overflow-y: auto;
}

.lobby-head { text-align: center; margin-bottom: 4px; }
.logo { font-size: 46px; letter-spacing: 8px; margin: 0; font-weight: 900; color: var(--purple); }
.tagline { margin: 4px 0 0; color: var(--text-dim); font-size: 14px; }

.panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.panel label {
  font-size: 12px;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.6px;
}

input[type="text"] {
  min-height: 48px;
  font-size: 18px;
  padding: 0 12px;
  border-radius: 10px;
  border: 1px solid var(--line-2);
  background: var(--panel-2);
  color: var(--text);
  text-transform: uppercase;
  letter-spacing: 2px;
  outline: none;
}
input[type="text"]:focus { border-color: var(--purple); }
#createName, #joinName { text-transform: none; letter-spacing: 0; }

.btn {
  min-height: 48px;
  border: 0;
  border-radius: 12px;
  font-size: 16px;
  font-weight: 700;
  color: #0d0d14;
  cursor: pointer;
  background: var(--purple);
}
.btn:active { transform: scale(0.98); }
.btn-primary { background: var(--purple); }
.btn-secondary { background: var(--panel-2); color: var(--text); border: 1px solid var(--line-2); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

.room-code { text-align: center; padding: 6px 0 2px; }
.room-code-label { display: block; font-size: 12px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; }
.room-code-value { display: block; font-size: 40px; font-weight: 900; letter-spacing: 10px; color: var(--gold); }

.lobby-players { list-style: none; margin: 4px 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.lobby-players li { padding: 8px 12px; background: var(--panel-2); border-radius: 8px; font-size: 15px; }
.lobby-players .you-tag { color: var(--purple); font-weight: 700; margin-left: 8px; }

/* A BOT seat is marked from the projected `seat.bot` flag alone. The chip never takes width on
   a 390px phone and never causes a horizontal scroll. */
.bot-chip {
  display: inline-block; margin-left: 4px; padding: 0 4px;
  font-size: 9px; font-weight: 800; letter-spacing: 0.6px; line-height: 1.5;
  color: #0d0d14; background: var(--gold); border-radius: 5px;
  vertical-align: middle;
}

.hint { font-size: 13px; color: var(--text-dim); text-align: center; margin: 2px 0; }

/* ----------------------------------------------------------------- Game --- */

#game.game {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  height: 100dvh;
  width: 100vw;
  overflow: hidden;
  gap: 6px;
  padding: calc(env(safe-area-inset-top) + 6px) calc(env(safe-area-inset-right) + 8px) calc(env(safe-area-inset-bottom) + 8px) calc(env(safe-area-inset-left) + 8px);
}

/* ============================ the table (3D) ============================== */

#table {
  position: relative;
  min-height: 0;
  overflow: hidden;
  border-radius: 18px;
  border: 1px solid var(--line);
  background:
    radial-gradient(70% 46% at 50% 46%, rgba(40, 150, 140, 0.16), rgba(0, 0, 0, 0) 70%),
    radial-gradient(120% 80% at 50% 0%, #1a1e2a 0%, #10131a 62%, #080a0f 100%);
  container-type: size;
}

.table-wrap {
  position: absolute;
  inset: 0;
  perspective: var(--cam-persp);
  perspective-origin: 50% 46%;
}

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

/*
 * The cloth, as a projection deck: a dark glass panel with a cyan-green lattice burning on it, a
 * hard inked rim with the neon line just inside it (the retro-anime read — flat colour, hard edge,
 * no soft gradient pretending to be metal). The lattice is two repeating gradients, so it costs
 * almost nothing and stays crisp at any size, and the plane is STILL a real circle under
 * `rotateX`, so the camera flattens it into the oval the player actually sits at.
 */
.plane {
  position: absolute;
  left: 50%;
  top: 48%;
  width: var(--felt);
  height: var(--felt);
  transform: translate(-50%, -50%) rotateX(var(--cam-tilt));
  transform-style: preserve-3d;
  border-radius: 50%;
  background:
    /* the lattice, plus a fainter sub-grid, burning in the glass */
    repeating-linear-gradient(to right, rgba(140, 255, 226, 0.62) 0 1px, rgba(140, 255, 226, 0) 1px 10%),
    repeating-linear-gradient(to bottom, rgba(140, 255, 226, 0.62) 0 1px, rgba(140, 255, 226, 0) 1px 10%),
    repeating-linear-gradient(to right, rgba(140, 255, 226, 0.20) 0 1px, rgba(140, 255, 226, 0) 1px 2.5%),
    repeating-linear-gradient(to bottom, rgba(140, 255, 226, 0.20) 0 1px, rgba(140, 255, 226, 0) 1px 2.5%),
    /* the light pooling where the projector lands */
    radial-gradient(closest-side at 50% 50%, rgba(100, 255, 212, 0.32), rgba(34, 160, 148, 0.12) 52%, rgba(0, 0, 0, 0) 80%),
    /* the glass itself */
    radial-gradient(closest-side at 50% 46%, #114042 0%, #0a2126 55%, #061014 100%);
  box-shadow:
    0 0 0 1px rgba(150, 255, 230, 0.45),
    0 0 0 3px #0a1519,
    0 0 0 5px #1b2b33,
    0 0 0 7px rgba(90, 255, 205, 0.30),
    0 0 0 12px #05090c,
    0 20px 44px rgba(0, 0, 0, 0.72),
    0 0 40px rgba(60, 255, 205, 0.16),
    inset 0 0 60px rgba(0, 0, 0, 0.60),
    inset 0 0 22px rgba(80, 255, 205, 0.14);
}
/* one flat highlight band across the far rim — a cel band, not a glow */
.plane::after {
  content: '';
  position: absolute;
  inset: 1.5%;
  border-radius: 50%;
  background: conic-gradient(from 196deg,
    rgba(200, 255, 245, 0.26), rgba(200, 255, 245, 0.02) 20%,
    rgba(0, 0, 0, 0) 74%, rgba(200, 255, 245, 0.12) 100%);
  -webkit-mask: radial-gradient(closest-side, rgba(0, 0, 0, 0) 0 92.5%, #000 93.5% 100%);
  mask: radial-gradient(closest-side, rgba(0, 0, 0, 0) 0 92.5%, #000 93.5% 100%);
  pointer-events: none;
}

/*
 * EVERY SEAT'S PURSE, ON THE CLOTH. Not decoration any more: one cluster per seat, sitting where
 * that seat sits, holding that seat's own coins and the exact number beside them — the tokens are a
 * picture of `seat.coins` and the numeral IS `seat.coins`, printed from the one projection the whole
 * client renders from, so the two cannot disagree. Positions come from renderCoins()/coinClusters()
 * in game.js as `--x/--y` in the PLANE's own percentages, which is why they tilt with the table.
 *
 * Still `pointer-events: none` on the layer, the cluster and every token: the money is scenery and
 * must never swallow a tap meant for a plate or a card (the layout probe hit-tests exactly that).
 */
.holo-coins { position: absolute; inset: 0; pointer-events: none; }
/* the camera's measuring sticks: zero-size, invisible, inert — they exist only to be measured */
.purse-probe { position: absolute; left: 50%; top: 50%; width: 0; height: 0; pointer-events: none; }
.purse-probe i { position: absolute; left: 0; top: 0; width: 0; height: 0; pointer-events: none; }
.coin-cluster {
  position: absolute;
  left: calc(50% + var(--x));
  top: calc(50% + var(--y));
  display: flex;
  align-items: center;
  transform: translate(-50%, -50%);
  pointer-events: none;
}
/* THE PILE. The coins are laid in a heap by pileLayout() in game.js, which hands each one its place as
   `--px/--py` in TOKEN WIDTHS off the pile's own centre, and the heap's footprint as `--pw/--ph` on the
   cluster — so this is a box measured in felt units with its children placed inside it, and a phone and
   a laptop get the same heap. Rows are stacked, never queued: the box is a little wider than it is
   tall, every coin still shows the crescent the ones above it do not cover, and each is laid a hair off
   its slot so a pile reads as money put down by hand. */
.coin-toks {
  position: relative;
  display: block;
  width: calc(var(--felt) * 0.048 * var(--pscale, 1) * var(--pw, 1));
  height: calc(var(--felt) * 0.048 * var(--pscale, 1) * var(--ph, 1));
  pointer-events: none;
}
.coin-tok {
  position: absolute;
  left: calc(50% + var(--felt) * 0.048 * var(--pscale, 1) * var(--px, 0));
  top: calc(50% + var(--felt) * 0.048 * var(--pscale, 1) * var(--py, 0));
  display: block;
  width: calc(var(--felt) * 0.048 * var(--pscale, 1));
  height: calc(var(--felt) * 0.048 * var(--pscale, 1));
  pointer-events: none;
  /* a purse is laid down by hand: every coin a little off its slot in angle and size */
  transform: translate(-50%, -50%) rotate(var(--tilt, 0deg)) scale(var(--sc, 1));
  transform-origin: 50% 50%;
}
/* the purse's own contact shadow on the cloth: absolute, so it is not a flex item and cannot move the
   heap — with the transform on the cluster this keeps its own stacking context, hence z-index: -1 */
.coin-cluster::before {
  content: '';
  position: absolute;
  left: -14%;
  right: -14%;
  top: 4%;
  bottom: -22%;
  z-index: -1;
  background: radial-gradient(56% 54% at 50% 60%, rgba(2, 8, 10, 0.5), rgba(2, 8, 10, 0) 74%);
  pointer-events: none;
}
.coin-tok svg { display: block; width: 100%; height: auto; }
.coin-fallback { display: block; width: 100%; aspect-ratio: 1; border-radius: 50%; background: #8d95a1; }
/* the count, in the client's own face, on a lit tab of the same light as the deck's rim */
.coin-num {
  font-size: calc(var(--felt) * 0.055 * var(--pscale, 1));
  font-weight: 800;
  font-style: italic;
  line-height: 1;
  padding: 0.06em 0.3em 0.12em;
  margin-left: calc(var(--felt) * 0.012);
  border-radius: 0.34em;
  color: #dcfff6;
  background: rgba(6, 20, 22, 0.74);
  border: 1px solid rgba(120, 255, 222, 0.55);
  box-shadow: 0 0 9px rgba(60, 255, 205, 0.35), inset 0 0 6px rgba(60, 255, 205, 0.16);
  text-shadow: 0 0 6px rgba(120, 255, 222, 0.5);
}
/* an eliminated seat keeps its money on the table, greyed: it is still the count that person had */
.coin-cluster.out { opacity: 0.4; }
.coin-cluster.out .coin-num { color: #b9c2cc; border-color: rgba(190, 200, 210, 0.4); text-shadow: none; }

/* what the deck throws into the air: a screen-space glow over the surface, under the seats */
.holo-haze {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background:
    radial-gradient(46% 30% at 50% 44%, rgba(80, 255, 210, 0.16), rgba(80, 255, 210, 0) 72%),
    radial-gradient(30% 16% at 50% 50%, rgba(150, 255, 235, 0.10), rgba(150, 255, 235, 0) 80%);
  mix-blend-mode: screen;
}

/* the deck lies on the cloth; the stack thins as the deck is drawn down */
.deck-pile {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--felt) * 0.19);
  transform: translate(-50%, -50%);
  transform-style: preserve-3d;
}
.deck-pile .deck-back {
  width: 100%;
  aspect-ratio: 100 / 140;
  border-radius: 5px;
  overflow: hidden;
  box-shadow: 0 3px 7px rgba(0, 0, 0, 0.6);
}
.deck-pile .deck-back + .deck-back {
  position: absolute;
  left: 0;
  top: 0;
  transform: translate(3px, -3px);
  filter: brightness(0.82);
}
.deck-pile .deck-back + .deck-back + .deck-back { transform: translate(6px, -6px); filter: brightness(0.68); }
.deck-pile img, .deck-pile svg { width: 100%; height: 100%; display: block; }

/* the count reads normally while sitting on the cloth (counter-rotated against the camera) */
.center-info {
  position: absolute;
  left: 50%;
  top: 72%;
  transform: translate(-50%, -50%) rotateX(calc(-1 * var(--cam-tilt)));
  display: flex;
  flex-direction: column;
  align-items: center;
  line-height: 1.05;
  text-shadow: 0 2px 6px rgba(0, 0, 0, 0.8);
}
.deck-count { font-size: 17px; font-weight: 900; color: #f0ead6; text-shadow: 0 2px 6px rgba(0,0,0,0.7); }
.deck-label { font-size: 9.5px; color: rgba(240, 234, 214, 0.9); text-transform: uppercase; letter-spacing: 1px; }

/* the seats. Opponents ring the far rim; JS sets left/top per seat index. */
.seat-strip {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}

.seat {
  position: absolute;
  transform: translate(-50%, -50%);
  width: 76px;
  padding: 4px 5px 5px;
  border-radius: 11px;
  border: 1px solid var(--line-2);
  background: linear-gradient(#22222f, #16161e);
  box-shadow: 0 9px 18px rgba(0, 0, 0, 0.55);
  text-align: center;
  pointer-events: auto;
  transition: box-shadow 0.15s ease, border-color 0.15s ease;
}
.seat.active {
  border-color: var(--gold);
  box-shadow: 0 0 0 2px rgba(255, 211, 77, 0.55), 0 10px 22px rgba(0, 0, 0, 0.6);
}
.seat.you { border-color: var(--purple); }
.seat.dead { opacity: 0.45; }
.seat .s-name {
  font-size: 11px;
  font-weight: 800;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.seat .s-coins { font-size: 10px; color: var(--gold); font-weight: 800; }
/* GLOBAL on purpose: the HUD's own-seat line and every plate show the same mini cards, and an
   inline span here would ignore width and blow a 100x140 face through the layout. */
.s-cards { display: flex; gap: 3px; justify-content: center; align-items: center; }
.seat .s-cards { margin-top: 2px; }
.you-status .s-cards { gap: 2px; margin-left: 4px; }

/* one influence, one tiny card: a back while it lives, a named face once it is revealed */
.mini {
  width: var(--mini-w);
  aspect-ratio: 100 / 140;
  border-radius: 2px;
  overflow: hidden;
  flex: 0 0 auto;
}
.mini img, .mini svg { width: 100%; height: 100%; display: block; }
.mini.dead {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 7px;
  font-weight: 800;
  letter-spacing: 0.2px;
  color: #0d0d14;
  background: var(--c, #6a6f82);
  filter: grayscale(0.35) brightness(0.85);
}
/* A revealed card is public knowledge, so its mini is a READ shortcut — tap it and the whole face
   opens. The pseudo-element widens the hit area: the tile is 15px and a 15px target is not a tap. */
.mini[data-examine] { position: relative; cursor: pointer; }
.mini[data-examine]::after { content: ''; position: absolute; inset: -6px; }
.mini[data-examine]:active { transform: scale(0.92); }

.seat.you, .seat.hero {
  width: 88px;
  border-color: var(--purple);
  background: linear-gradient(#241f38, #16141f);
}
.seat.hero .s-name { color: #cfc6ff; }
.seat .you-tag {
  position: absolute; top: -7px; right: -4px;
  font-size: 8px; font-weight: 800; letter-spacing: 0.5px;
  color: #0d0d14; background: var(--purple);
  padding: 1px 4px; border-radius: 6px;
}
.seat .dc-marker {
  position: absolute; top: -5px; left: -5px;
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--danger);
  border: 2px solid var(--bg);
}
.seat.targetable {
  cursor: pointer;
  border-color: var(--gold);
  animation: targetPulse 1s ease-in-out infinite;
}
/* five opponents share the rim: tighter plates so neighbours never overlap on a 390px phone */
.seat-strip.dense .seat { width: 60px; padding: 3px 3px 4px; }
.seat-strip.dense .seat .s-name { font-size: 10px; }
.seat-strip.dense .seat .s-coins { font-size: 9px; }
.seat-strip.dense .mini { --mini-w: 12px; }
.seat-strip.dense .bot-chip { font-size: 8px; padding: 0 3px; margin-left: 2px; }
@keyframes targetPulse {
  0%, 100% { box-shadow: 0 0 0 2px var(--gold), 0 0 8px rgba(255, 211, 77, 0.4); }
  50% { box-shadow: 0 0 0 3px var(--gold), 0 0 18px rgba(255, 211, 77, 0.7); }
}

/* ---------------------------------------------------- holographic chrome --- */
/*
 * The plates and the panels float ON the projection, so they wear the same finish as the deck: a
 * hard inked rim, one flat cyan line and a soft cast — flat colour and hard edges, which is the
 * retro-anime read, and never a gradient pretending to be brushed metal.
 */
.seat, .pending-banner, .reaction-prompt, .log, .your-area {
  border-color: rgba(120, 255, 222, 0.34);
  box-shadow:
    0 0 0 1px rgba(4, 10, 12, 0.90),
    0 0 16px rgba(30, 180, 165, 0.16),
    0 10px 20px rgba(0, 0, 0, 0.55);
}
/* the lit seat keeps its gold; the holo cast rides along with it */
.seat.active {
  border-color: var(--gold);
  box-shadow: 0 0 0 2px rgba(255, 211, 77, 0.55), 0 0 22px rgba(60, 255, 205, 0.22), 0 10px 22px rgba(0, 0, 0, 0.60);
}
/* a card is a physical object on a lit table, not a tile on a page */
.card {
  box-shadow:
    0 0 0 1px rgba(4, 10, 12, 0.90),
    0 0 0 2px rgba(120, 255, 222, 0.30),
    0 0 18px rgba(60, 255, 205, 0.14),
    0 6px 14px rgba(0, 0, 0, 0.55);
}
/* your own seat sits in the projection's light too: the panel comes up off the floor */
.your-area { background: linear-gradient(#20232f, #191a24); }
/* one flat highlight along the top edge: cel shading, not a gradient */
.btn { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16); }

/* ------- the claim, front and centre on the cloth ------- */
/*
 * The claim is the biggest object on the table and it sits in the middle of it: whoever named a card
 * shows the FACE, who claimed it, and one line saying what that move pays for. The WINDOW — the
 * question the table is asked and the countdown it is asked against — is NOT painted here: it is the
 * answer prompt's own line at the HUD, printed once, right where the buttons are. It is drawn in screen
 * space over the cloth (above the seats, under the HUD) and sized off `--felt` like every other piece on
 * this table, capped by the table's own height so a short phone cannot push it off the cloth.
 *
 * It takes NO tap: `pointer-events: none` on the layer and on everything in it, so a tap "on the claim"
 * falls through to whatever is under the pointer — the table. On this table a claim is something you
 * read; the reaction window is where you answer it, and buttons you cannot reach are a broken game.
 */
.claim {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: calc(var(--felt) * 0.022);
  /*
   * TRULY CENTRED, and only ever moved UP by the least amount that clears the HUD. The callout is
   * centred on the CLOTH — the centre the eye reads the table from — and placeClaim() measures the HUD
   * the table answers with and writes the minimum lift needed (`--claim-lift`); padding-bottom is that
   * lift doubled, which moves the centred content up by exactly the lift. The band-above-the-HUD
   * centring this replaced put the claim a hundred pixels high on a phone while every gate passed.
   */
  padding: 0 8px calc(2 * var(--claim-lift, 0px));
  text-align: center;
  pointer-events: none;
}
.claim, .claim * { pointer-events: none; }
.claim[hidden] { display: none; }
/*
 * Everything BEHIND the claim fades to black: the card and its window are the only lit things on the
 * cloth. Inside the claim's own layer, so the dim covers the lattice, the coins and the seat plates
 * (z-index 3 and below) and never the HUD (z-index 6) — the buttons that ANSWER the claim stay lit.
 * It is a veil, not a control: it takes no tap, and the claim over it takes none either.
 */
/*
 * The banner stands down while a claim is up (`#table.claiming` is put there by renderClaim). It says
 * the same thing the claim's own line does — "Bot 1 claims Duke" over "Bot 1 CLAIMS DUKE / Blocks the
 * Foreign Aid" — and that duplicated row of HUD is precisely what pushed a truly-centred card 35px off
 * the cloth's centre on a phone and 80px off it on a desktop. Hiding it is what buys the card its
 * centre.
 */
#table.claiming #pendingBanner { display: none; }
/*
 * …and the HISTORY stands down too, for the same reason: while a claim is up its newest line IS the
 * claim ("Bot 3 tax (claims Duke)" under a callout already saying Bot 3 claims the Duke), so the band
 * would print the claim a third time — and on a phone it is a third of the room the card needs. The
 * band is back the moment the claim resolves.
 */
#table.claiming .log { display: none; }
.claim-veil {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: rgba(2, 4, 9, 0.5);
  animation: claim-veil-in 0.18s ease-out;
}
@keyframes claim-veil-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
/*
 * NEVER let a flex column shrink the face: with no room left, `flex-shrink: 1` collapsed the card to
 * 0x0 and the claimed card simply was not drawn on the smallest phone (the classic squashed-child
 * trap). The card is sized by the two `min()` caps below instead, and always keeps its own box.
 */
.claim-face, .claim-copy { flex: 0 0 auto; }
.claim {
  /*
   * How big the card may be, in HEIGHT (the aspect ratio owns the width):
   *   want  — what the cloth asks for (`--felt`);
   *   above — the room above the HUD, so a centred callout pushed up still fits;
   *   sym   — the room a TRULY CENTRED callout may use: the cloth's centre to the HUD, doubled;
   *   floor — the smallest card worth showing, sized to stay wider than a hand card.
   * It takes `sym` when that still leaves a readable card, and `above` when it does not: a phone whose
   * reaction prompt eats the middle of the table gets a bigger card, lifted the least it can be, rather
   * than an unreadable stamp dead centre.
   */
  --claim-copy-h: 58px;
  --claim-want: calc(var(--felt) * 0.728);
  --claim-above: max(40px, calc(100cqh - var(--claim-reserve, 0px) - 8px - var(--claim-copy-h)));
  --claim-sym: max(0px, calc(100cqh - 2 * var(--claim-reserve, 0px) - var(--claim-copy-h)));
  --claim-floor: calc(var(--felt) * 0.44);
  --claim-h: min(var(--claim-want), min(var(--claim-above), max(var(--claim-sym), var(--claim-floor))));
}
.claim-face {
  height: var(--claim-h, 40vh);
  width: auto;
  aspect-ratio: 100 / 140;
  border-radius: 9px;
  overflow: hidden;
  background: var(--panel);
  box-shadow:
    0 0 0 1px rgba(4, 10, 12, 0.90),
    0 0 0 3px var(--c, #7c5cff),
    0 0 24px rgba(60, 255, 205, 0.24),
    0 12px 26px rgba(0, 0, 0, 0.66);
  animation: claim-in 0.16s ease-out;
}
.claim-face img, .claim-face svg { width: 100%; height: 100%; display: block; }
/* the copy is a panel of the same finish as the plates: hard inked edge, flat fill, readable over
   the lattice of the cloth */
.claim-copy {
  max-width: min(88cqw, 340px);
  padding: 5px 10px 6px;
  border-radius: 10px;
  background: rgba(12, 14, 20, 0.88);
  border: 1px solid var(--line-2);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.6);
}
.claim-line {
  margin: 0;
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: var(--text);
}
.claim-act { margin: 1px 0 0; font-size: 12.5px; font-weight: 700; color: var(--gold); }
/*
 * A SHORT TABLE — a 360x640 phone, an old 320x568, a landscape tablet — has no room for a column
 * above the HUD, so the card and its copy go SIDE BY SIDE and the card is sized off the room that is
 * left. The card is small there; it is never absent, and it never covers the buttons.
 */
@container (max-height: 470px) {
  .claim { flex-direction: row; gap: 8px; }
  .claim-face {
    width: auto;
    height: max(52px, min(calc(100cqh - var(--claim-reserve, 0px) - 18px), calc(34cqw * 7 / 5)));
  }
  /* the copy has to leave room for the CARD beside it: a wide panel plus a card is wider than a
     304px cloth (seen as in_table=False), and a narrow one wraps taller than the band allows. 58%
     plus the card fits both ways on the smallest table. */
  .claim-copy { max-width: 58%; padding: 3px 7px 4px; text-align: left; }
  .claim-line { font-size: 10.5px; }
  .claim-act { font-size: 11px; }
}
@keyframes claim-in {
  from { opacity: 0; transform: scale(0.94); }
  to   { opacity: 1; transform: none; }
}

/* ------- floating over the near edge: the decision, the history ------- */

.table-hud {
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: 8px;
  z-index: 6;
  display: flex;
  flex-direction: column;
  gap: 5px;
  pointer-events: none;
}
.table-hud > * { pointer-events: auto; }

.pending-banner {
  padding: 6px 10px;
  border-radius: 10px;
  font-size: 12px;
  font-weight: 600;
  background: rgba(23, 23, 31, 0.94);
  border: 1px solid var(--line-2);
  color: var(--text);
}
.pending-banner b { color: var(--gold); }

.reaction-prompt {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  padding: 8px 10px;
  border-radius: 12px;
  background: rgba(26, 21, 46, 0.95);
  border: 1px solid var(--purple);
  box-shadow: 0 12px 26px rgba(0, 0, 0, 0.6);
}
.reaction-prompt .rp-label {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
}
/*
 * THE COUNTDOWN, beside the question it times — one figure, in one place. It is the window's own
 * deadline read off the server's clock (renderWindowClock, a 250 ms local tick), printed big enough to
 * read at a glance and with tabular figures and a fixed floor so it does not jitter as it counts down.
 * `aria-hidden` and removed when empty: a live region that counted out loud for ten seconds is not a
 * countdown, it is an alarm.
 */
.reaction-prompt .rp-clock {
  flex: 0 0 auto;
  min-width: 3.2ch;
  text-align: center;
  font-size: 16px;
  font-weight: 900;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: #1a1405;
  background: var(--gold);
  border-radius: 999px;
  padding: 4px 7px;
}
.reaction-prompt .rp-clock:empty { display: none; }
.btn-challenge { background: var(--danger); color: #fff; }
.btn-pass { background: var(--panel-2); color: var(--text); border: 1px solid var(--line-2); }
.btn-block { background: var(--panel-2); color: var(--text); border: 1px solid var(--line-2); }
/*
 * PROVE_WINDOW: the challenge landing on you. Two answers, ONE of them live — the engine takes
 * `prove` only while the claimed role is really in hand and `concede` only while it is not, so the
 * other button is rendered present-but-dead: dim, desaturated, no glow, and disabled (it takes no
 * tap and never reaches the server).
 */
.btn-prove { background: var(--ok); color: #08161a; }
.btn-bluff { background: var(--danger); color: #fff; }
.reaction-prompt .btn-dead,
.reaction-prompt .btn-dead:disabled {
  background: var(--panel);
  color: var(--text-dim);
  border: 1px solid var(--line);
  opacity: 0.4;
  filter: grayscale(0.8);
  box-shadow: none;
  cursor: not-allowed;
}
.reaction-prompt .btn { min-height: 44px; padding: 0 14px; font-size: 14px; flex: 1 1 auto; }

/* a small history, not a log file: the last few things that happened, one line each */
/*
 * The history band. It is exactly as tall as the lines it holds — NEVER a fixed height: a flex
 * column with a fixed height shrinks its children, and a 9px-tall line box slices 11px text into
 * unreadable slivers (the "shrunken box" that is impossible to read). Capped at LOG_TAIL lines of
 * game.js so it can never grow past the small history we promised.
 */
.log {
  --log-line: 1.35em;
  max-height: calc(var(--log-lines) * var(--log-line) + 14px);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 1px;
  padding: 5px 9px;
  border-radius: 10px;
  background: rgba(11, 11, 16, 0.68);
  border: 1px solid var(--line);
  font-size: 11.5px;
  line-height: 1.35;
  scrollbar-width: none;
}
.log::-webkit-scrollbar { display: none; }
/* an empty history is not dead space: it says so, once, in the quietest possible voice */
.log:empty::before {
  content: 'no moves yet';
  color: var(--text-dim);
  opacity: 0.5;
  font-style: italic;
}
.log .entry {
  flex: 0 0 auto;                 /* a history line is never squashed to fit the band */
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.log .entry.public { color: #bdb8cd; }
.log .entry.highlight { color: var(--gold); font-weight: 700; }

/* ============================= your seat ================================= */

.your-area {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 7px 8px 8px;
  background: linear-gradient(#1a1a24, var(--panel));
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.you-status {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
}
.you-status .ys-name { font-weight: 800; }
.you-status .ys-coins { color: var(--gold); font-weight: 800; }
.you-status .s-cards { margin: 0; }
.you-status .ys-phase { margin-left: auto; color: var(--text-dim); font-size: 12px; text-align: right; }

.hand {
  display: flex;
  gap: 10px;
  justify-content: center;
  align-items: flex-start;
  flex-wrap: wrap;
  perspective: 900px;
}

.card {
  --w: clamp(72px, 22vw, 116px);
  width: var(--w);
  aspect-ratio: 100 / 140;
  border-radius: 8px;
  position: relative;
  overflow: hidden;
  box-shadow: 0 6px 14px rgba(0, 0, 0, 0.55);
  flex: 0 0 auto;
  /* a card held in front of you at the table, not a flat tile */
  transform: rotateX(7deg);
  transform-origin: 50% 100%;
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.card img, .card svg { width: 100%; height: 100%; display: block; }
.card.selectable { cursor: pointer; }
.card.selectable:active { transform: rotateX(7deg) translateY(2px); }
.card.selected {
  outline: 3px solid var(--gold);
  outline-offset: 1px;
  transform: rotateX(4deg) translateY(-9px) scale(1.04);
  box-shadow: 0 14px 22px rgba(0, 0, 0, 0.6);
}
.card.revealed-dead { filter: grayscale(0.85) brightness(0.6); }
/* a card you can READ: the same "pick it up" press as a selectable one, to a different end */
.card.readable { cursor: pointer; }
.card.readable:active { transform: rotateX(5deg) translateY(-4px) scale(1.03); }
.card .card-cover {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(10, 10, 16, 0.55);
  color: var(--text-dim); font-size: 11px; font-weight: 700; letter-spacing: 1px;
}

/* actions: what it is, and one short line saying what it does */
.action-bar {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 5px;
}
.action-bar .btn {
  min-height: 46px;
  padding: 4px 3px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  font-weight: 800;
  border-radius: 10px;
}
.action-bar .btn .a-label { font-size: 12px; line-height: 1.1; }
.action-bar .btn .a-desc {
  font-size: 9px;
  font-weight: 600;
  line-height: 1.15;
  opacity: 0.82;
  text-align: center;
  overflow-wrap: anywhere;
}
.action-bar .btn:disabled {
  background: #262b39;
  color: #9099ad;
  border: 1px solid var(--line);
}
.action-bar .btn:disabled .a-desc { opacity: 0.9; }
.action-bar .btn.awaiting { opacity: 0.6; }
.action-bar .btn.targeting-active {
  border: 2px solid var(--gold);
  box-shadow: 0 0 10px rgba(255, 211, 77, 0.5);
}

/* Reconnect banner + toast + closed */
/*
 * The "i" — a small information button that is reachable from the lobby AND mid-game, plus the
 * how-to sheet it opens. The sheet has a FIXED header (so the close button is never scrolled away)
 * and a scrolling body; the body carries `min-height: 0` because a flex child without it refuses
 * to shrink and the sheet would grow past the viewport instead of scrolling inside it.
 */
.howto-btn {
  position: fixed;
  top: calc(env(safe-area-inset-top) + 8px);
  right: calc(env(safe-area-inset-right) + 10px);
  z-index: 40;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid rgba(212, 175, 55, 0.55);
  background: rgba(28, 28, 38, 0.96);
  color: var(--gold);
  font-weight: 800;
  font-size: 20px;
  line-height: 1;
  font-family: var(--font-coup);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.55), inset 0 0 0 3px rgba(212, 175, 55, 0.10);
}
.howto-btn:active { transform: scale(0.94); }
.howto-btn[aria-expanded="true"] { opacity: 0.35; }

.howto {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: calc(env(safe-area-inset-top) + 12px) 12px calc(env(safe-area-inset-bottom) + 12px);
  background: rgba(6, 6, 10, 0.82);
}
.howto[hidden] { display: none; }
.howto-panel {
  width: min(100%, 560px);
  max-height: 100%;
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.7);
  overflow: hidden;
}
.howto-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 12px 10px 14px;
  border-bottom: 1px solid var(--line);
  background: var(--panel-2);
}
.howto-head h2 {
  margin: 0;
  font-size: 14px;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--gold);
}
.howto-close {
  width: 38px;
  height: 38px;
  flex: 0 0 auto;
  border-radius: 50%;
  border: 1px solid var(--line-2);
  background: #1d1d28;
  color: var(--text);
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
}
.howto-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 12px 14px 16px;
}
.howto-lede { margin: 0 0 10px; font-size: 13px; color: var(--text-dim); }

/*
 * The CARD GUIDE: five pictures, each with what it does and what it excels at. Flat plates with the
 * role's own accent as a left rail — the same read as the cards on the cloth — and a face at 68px
 * on the sheet's full width. `.guide-body` carries `min-width: 0` because a flex child without it
 * refuses to shrink: the sentences wrap on a 390px phone only if the text column is allowed to.
 * The face is a tap target (`data-examine`), so it enlarges the card like every other card tap.
 */
.howto-h {
  margin: 15px 0 8px;
  font-size: 11px;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--text);
}
.guide { display: grid; gap: 8px; }
.guide-card {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 8px 10px 9px 7px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent, var(--line-2));
  border-radius: 8px;
  background: var(--panel-2);
}
.guide-face {
  flex: 0 0 auto;
  width: 68px;
  line-height: 0;
  border-radius: 6px;
  overflow: hidden;
  cursor: pointer;
}
.guide-face img, .guide-face svg { display: block; width: 100%; height: auto; }
.guide-body { min-width: 0; }
.guide-name {
  margin: 0 0 4px;
  font-size: 13px;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--accent, var(--text));
}
.guide-line {
  margin: 0 0 4px;
  font-size: 12.5px;
  line-height: 1.42;
  color: var(--text-dim);
}
.guide-line:last-child { margin-bottom: 0; }
.guide-k {
  display: inline-block;
  min-width: 47px;
  margin-right: 3px;
  font-size: 10px;
  letter-spacing: 1.1px;
  text-transform: uppercase;
  color: var(--text);
}
.howto-free {
  margin: 9px 0 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--text-dim);
  opacity: 0.9;
}

.howto-list { margin: 0; padding-left: 19px; }
.howto-list li {
  flex: 0 0 auto;
  font-size: 13px;
  line-height: 1.45;
  color: var(--text-dim);
}
.howto-list li + li { margin-top: 9px; }
.howto-list li b { color: var(--text); }
.howto-note {
  margin: 13px 0 0;
  font-size: 12px;
  color: var(--text-dim);
  opacity: 0.85;
}

/* ---------------------------------------------------------------- Examine --- */
/*
 * The card you tapped, held closer: a dim scrim and the face at full size, over the table but under
 * nothing else. The card is sized off the WIDTH and the HEIGHT (a phone is tall and narrow, a laptop
 * short and wide) so it can never grow past the screen it is being read on — and `dvh` is declared
 * after a `vh` fallback, because an unsupported unit voids the whole declaration.
 */
.examine {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: calc(env(safe-area-inset-top) + 16px) 16px calc(env(safe-area-inset-bottom) + 16px);
  background: rgba(6, 6, 10, 0.86);
  cursor: pointer;
}
.examine[hidden] { display: none; }
.examine-card {
  width: min(72vw, calc((100vh - 200px) * 5 / 7), 300px);
  width: min(72vw, calc((100dvh - 200px) * 5 / 7), 300px);
  aspect-ratio: 100 / 140;
  border-radius: 12px;
  overflow: hidden;
  background: var(--panel);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.75), 0 0 0 1px var(--line-2);
  animation: examine-in 0.14s ease-out;
}
.examine-card img, .examine-card svg { width: 100%; height: 100%; display: block; }
.examine-name {
  font-size: 15px;
  font-weight: 800;
  letter-spacing: 2.6px;
  text-transform: uppercase;
  color: var(--gold);
}
.examine-kw { font-size: 12px; font-weight: 700; color: var(--text-dim); min-height: 14px; }
.examine-kw:empty { min-height: 0; }
.examine-hint { margin: 0; font-size: 11px; color: var(--text-dim); opacity: 0.75; }

@keyframes examine-in {
  from { opacity: 0; transform: scale(0.92); }
  to   { opacity: 1; transform: scale(1); }
}

/* Reconnect banner + toast + closed */
.reconnect-banner {
  position: fixed;
  top: calc(env(safe-area-inset-top) + 6px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 50;
  padding: 8px 16px;
  border-radius: 20px;
  background: rgba(229, 64, 94, 0.95);
  color: #fff;
  font-size: 13px;
  font-weight: 700;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}
.reconnect-banner.spin::after { content: " ⏳"; }

.toast {
  position: fixed;
  bottom: calc(env(safe-area-inset-bottom) + 16px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 60;
  max-width: 88vw;
  padding: 10px 16px;
  border-radius: 12px;
  background: var(--danger);
  color: #fff;
  font-size: 14px;
  font-weight: 600;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}

#closed {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.closed-card {
  text-align: center;
  max-width: 340px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.closed-card h1 { margin: 0; font-size: 24px; }
.closed-card p { color: var(--text-dim); margin: 0; }

/* ----------------------------------------------------- Landscape / wide --- */

/* A short, wide viewport: the table keeps the room it needs and your seat moves to one row. */
@media (orientation: landscape) and (min-width: 700px) {
  #table { --felt: min(84cqw, 84cqh, 560px); }
  .your-area {
    flex-direction: row;
    align-items: center;
    justify-content: center;
    gap: 14px;
  }
  .you-status { flex: 0 0 auto; max-width: 190px; flex-wrap: wrap; }
  .you-status .ys-phase { width: 100%; margin-left: 0; text-align: left; }
  .hand { flex: 0 0 auto; flex-wrap: nowrap; gap: 8px; }
  .card { --w: clamp(56px, 9vw, 92px); }
  .action-bar { flex: 1 1 auto; max-width: 620px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .action-bar .btn { min-height: 42px; }
  .log { font-size: 11.5px; }
}

/* portrait but wide (tablet): bigger everything, more room on the cloth */
@media (min-width: 760px) and (orientation: portrait) {
  .seat { width: 96px; padding: 6px 7px; }
  .seat .s-name { font-size: 13px; }
  .seat .s-coins { font-size: 12px; }
  .mini { --mini-w: 18px; }
  .log { font-size: 12.5px; }
}

/* short viewports (a 568px-tall phone): a slightly tighter history so the cloth keeps its room */
@media (max-height: 630px) {
  .log { font-size: 10.5px; }
  .seat { width: 68px; }
  .seat.you, .seat.hero { width: 76px; }
  .mini { --mini-w: 13px; }
  .action-bar .btn { min-height: 42px; }
  .action-bar .btn .a-label { font-size: 11px; }
  .action-bar .btn .a-desc { font-size: 8.5px; }
}

/* a desktop window: keep the table and your seat one centred column instead of a wide empty room */
@media (min-width: 1100px) {
  #table, .your-area { width: 100%; max-width: 1040px; justify-self: center; }
}


/* ========================== THE KILL ===================================== */
/*
 * A Coup that takes a seat's LAST influence gets the screen (Darren's ask): the card comes up in the
 * middle, EVERYTHING else on the screen goes dark — the cloth, the plates, the HUD, the hand, the
 * action bar, every dialog — the card is HIGHLIGHTED for one second, a slash cuts it in half on the
 * diagonal, an anime impact frame lands, and the two halves phase out while the game UI comes back.
 *
 * One number owns the timeline: `ELIM` in src/game.js. It reaches this sheet as --elim-* custom
 * properties (written by playKill), so "highlighted for a second, then cut" is one constant, not a
 * second copy of the timing hiding in a keyframe. The phases are `data-phase` on #elim —
 * rise → highlight → cut → out — which is also what tools/elimination_ui_probe.py measures the
 * one-second highlight off.
 *
 * INERT, like the claim and the CRT pass: `pointer-events: none` here and on everything inside.
 * The engine does not pause for the animation, so every button under it must still take a tap; the
 * phases where the engine is waiting on YOUR decision end the show outright (eliminationUrgent).
 *
 * z-index 65: above every dialog (the how-to is 50, the examine 60) and below the scanline pass
 * (70), so the CRT texture still runs over the kill the way it runs over everything else.
 */
.elim {
  position: fixed;
  inset: 0;
  z-index: 65;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  /*
   * The card's HEIGHT (its aspect ratio 100/140 owns the width). Both caps are needed:
   *   56dvh — never taller than half the screen, so the caption and the dark edge stay visible;
   *   95vw  — 95vw / 1.4 = 68vw of card WIDTH, the widest that still leaves the veil readable
   *           either side of it on a 320px phone.
   */
  --elim-h: min(56dvh, 95vw, 420px);
}
.elim, .elim * { pointer-events: none; }
.elim[hidden] { display: none; }

/*
 * The veil: the whole screen, darkest at the edges so the middle — where the card is — is the lit
 * part. It sits INSIDE this layer, so unlike the claim's dim it covers the HUD and the dialogs too:
 * this is the one moment the table is not meant to be read.
 */
.elim-veil {
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 50% 50%, rgba(3, 5, 10, 0.72) 0%, rgba(2, 3, 7, 0.94) 68%, rgba(1, 2, 5, 0.98) 100%);
  /*
   * 1, NOT 0: this is the steady state of every phase between the rise and the fade-out, and a base
   * of 0 only holds while an animation is attached — the veil is bound to [data-phase="rise"], so at
   * 0 it vanished the instant the phase moved on to 'highlight' and the table came back up under the
   * card. tools/elimination_ui_probe.py measures the darkening off a real screenshot and caught it
   * (26.3 -> 26.3 luminance, 0% darker); the rise animation still fades it in from 0.
   */
  opacity: 1;
}

.elim-stage {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* the card box: the two halves are stacked in it and clipped to their own triangle */
.elim-card {
  position: relative;
  height: var(--elim-h);
  width: calc(var(--elim-h) * 100 / 140);
}

.elim-half {
  position: absolute;
  inset: 0;
  background: var(--panel);
  border-radius: 10px;
  overflow: hidden;
}
/* the SAME face twice, each half clipped along the card's own diagonal (top-right → bottom-left) */
.elim-half-a { clip-path: polygon(0% 0%, 100% 0%, 0% 100%); }
.elim-half-b { clip-path: polygon(100% 0%, 100% 100%, 0% 100%); }
.elim-face { position: absolute; inset: 0; }
.elim-face img, .elim-face svg { width: 100%; height: 100%; display: block; }

/* the highlight: a hard cyan-ish ring in the card's own accent, plus the glow it throws */
.elim-glow {
  position: absolute;
  inset: 0;
  border-radius: 10px;
  /* the same trap as the veil, one layer in: a glow whose base is 0 is only lit while its own
     animation is attached, and this one belongs to the rise/ highlight / cut phases in turn. 0.85 is
     where the highlight leaves it, so every phase hands over to the next without a blink. */
  opacity: 0.85;
  box-shadow:
    0 0 0 2px var(--c, #7c5cff),
    0 0 0 4px rgba(4, 10, 12, 0.85),
    0 0 34px 6px var(--c, #7c5cff);
}
.elim[data-phase="highlight"] .elim-glow { animation: elim-glow-hi var(--elim-hi, 1000ms) ease-in-out both; }

/* the foil flash that crosses the card while it is highlighted */
.elim-shine {
  position: absolute;
  inset: 0;
  border-radius: 10px;
  overflow: hidden;
  opacity: 0;
  background: linear-gradient(103deg,
    rgba(255, 255, 255, 0) 37%, rgba(255, 255, 255, 0.88) 50%, rgba(255, 255, 255, 0) 63%);
  background-size: 250% 100%;
  background-position: 140% 0;
}
.elim[data-phase="highlight"] .elim-shine { animation: elim-shine-sweep var(--elim-hi, 1000ms) ease-in-out both; }

/*
 * The blade. Its length is the card's own diagonal (122% of the width) and its angle is that
 * diagonal's (atan(140/100) = 54.46deg), so the cut it draws and the cut the two halves part along
 * are the same line — a slash at any other angle reads as a decoration laid over the card.
 */
.elim-slash {
  position: absolute;
  left: -11%;
  top: 50%;
  width: 122%;
  height: 4px;
  margin-top: -2px;
  opacity: 0;
  background: linear-gradient(90deg,
    rgba(255, 255, 255, 0) 0%, #eafff8 45%, #ffffff 55%, rgba(255, 255, 255, 0) 100%);
  box-shadow: 0 0 16px rgba(234, 255, 248, 0.85);
  transform: rotate(-54.46deg) scaleX(0);
}
.elim[data-phase="cut"] .elim-slash { animation: elim-slash-sweep calc(var(--elim-cut, 200ms) * 0.8) ease-out both; }

/* the caption: the history's own line for this loss, in the table's one face */
.elim-line {
  /*
   * OUT of the flex flow, deliberately: a caption stacked under the card in the flow lifts the card
   * by half the caption's height, and the card's centre is the SCREEN's centre in this animation —
   * measured 24 px high before this (tools/elimination_ui_probe.py, "brought to the MIDDLE").
   */
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin: calc(var(--elim-h) * 0.055) 0 0;
  max-width: min(86vw, 420px);
  padding: 5px 11px 6px;
  border-radius: 10px;
  background: rgba(12, 14, 20, 0.9);
  border: 1px solid var(--line-2);
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0.9px;
  text-transform: uppercase;
  color: var(--text);
  text-align: center;
}
.elim-line:empty { display: none; }

/* the impact frames: a full-screen flash and the speed lines thrown by the blow */
.elim-impact { position: absolute; inset: 0; }
.elim-flash { position: absolute; inset: 0; background: #eafff8; opacity: 0; }
.elim-rays {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 190vmax;
  height: 190vmax;
  margin: -95vmax 0 0 -95vmax;
  opacity: 0;
  background: repeating-conic-gradient(from 0deg at 50% 50%,
    rgba(255, 255, 255, 0.9) 0deg 1.5deg, rgba(255, 255, 255, 0) 1.5deg 6.5deg);
  -webkit-mask-image: radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0) 11%, #000 33%, rgba(0, 0, 0, 0) 71%);
  mask-image: radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0) 11%, #000 33%, rgba(0, 0, 0, 0) 71%);
  transform: scale(0.18);
}
/* steps(1, end) on both: these are FRAMES, not fades — a fade reads as a light, a frame reads as a blow */
.elim[data-phase="cut"] .elim-flash { animation: elim-flash-frames var(--elim-cut, 200ms) steps(1, end) both; }
.elim[data-phase="cut"] .elim-rays { animation: elim-rays-burst var(--elim-cut, 200ms) linear both; }

/* ---- the phases ---- */
.elim[data-phase="rise"] .elim-veil { animation: elim-veil-in var(--elim-rise, 320ms) ease-out both; }
.elim[data-phase="rise"] .elim-stage { animation: elim-stage-in var(--elim-rise, 320ms) cubic-bezier(0.18, 0.9, 0.22, 1.12) both; }
.elim[data-phase="rise"] .elim-glow { animation: elim-glow-in var(--elim-rise, 320ms) ease-out both; }

.elim[data-phase="highlight"] .elim-card { animation: elim-card-hi var(--elim-hi, 1000ms) ease-in-out both; }

.elim[data-phase="cut"] .elim-stage { animation: elim-shake var(--elim-cut, 200ms) steps(1, end) both; }
.elim[data-phase="cut"] .elim-glow { animation: elim-glow-cut var(--elim-cut, 200ms) ease-out both; }
.elim[data-phase="cut"] .elim-half-a { animation: elim-half-a-cut var(--elim-cut, 200ms) ease-out both; }
.elim[data-phase="cut"] .elim-half-b { animation: elim-half-b-cut var(--elim-cut, 200ms) ease-out both; }

.elim[data-phase="out"] .elim-half-a { animation: elim-half-a-out var(--elim-out, 900ms) cubic-bezier(0.2, 0.6, 0.3, 1) both; }
.elim[data-phase="out"] .elim-half-b { animation: elim-half-b-out var(--elim-out, 900ms) cubic-bezier(0.2, 0.6, 0.3, 1) both; }
/* the glow died with the cut: it must not come back on its steady state while the halves leave */
.elim[data-phase="out"] .elim-glow { opacity: 0; }
/* the card phases out first, then the table comes back — the UI is never brighter than the card */
.elim[data-phase="out"] .elim-stage { animation: elim-stage-out 520ms ease-in both; animation-delay: calc(var(--elim-out, 900ms) - 520ms); }
.elim[data-phase="out"] .elim-veil { animation: elim-veil-out 380ms ease-in both; animation-delay: calc(var(--elim-out, 900ms) - 380ms); }

@keyframes elim-veil-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes elim-veil-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes elim-stage-in {
  from { opacity: 0; transform: scale(0.72); }
  60%  { opacity: 1; }
  to   { opacity: 1; transform: none; }
}
@keyframes elim-stage-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes elim-glow-in { from { opacity: 0; } to { opacity: 0.5; } }
@keyframes elim-glow-hi {
  0%   { opacity: 0.5; }
  22%  { opacity: 1; }
  45%  { opacity: 0.45; }
  70%  { opacity: 1; }
  100% { opacity: 0.85; }
}
@keyframes elim-glow-cut { from { opacity: 0.85; } to { opacity: 0; } }
/* the card breathes: it is what is being HEARD, so it never sits perfectly still for the second */
@keyframes elim-card-hi {
  0%   { transform: none; }
  22%  { transform: scale(1.035); }
  45%  { transform: scale(1); }
  70%  { transform: scale(1.045); }
  100% { transform: scale(1.02); }
}
@keyframes elim-shine-sweep {
  from { opacity: 0; background-position: 140% 0; }
  12%  { opacity: 0.9; }
  88%  { opacity: 0.9; }
  to   { opacity: 0; background-position: -40% 0; }
}
@keyframes elim-slash-sweep {
  from { opacity: 0; transform: rotate(-54.46deg) scaleX(0); }
  30%  { opacity: 1; }
  to   { opacity: 0; transform: rotate(-54.46deg) scaleX(1); }
}
@keyframes elim-half-a-cut {
  from { transform: none; }
  to   { transform: translate(-1.6%, -2.2%) rotate(-1.1deg); }
}
@keyframes elim-half-b-cut {
  from { transform: none; }
  to   { transform: translate(1.6%, 2.2%) rotate(1.1deg); }
}
@keyframes elim-half-a-out {
  from { transform: translate(-1.6%, -2.2%) rotate(-1.1deg); opacity: 1; }
  to   { transform: translate(-52%, -72%) rotate(-15deg) scale(0.9); opacity: 0; }
}
@keyframes elim-half-b-out {
  from { transform: translate(1.6%, 2.2%) rotate(1.1deg); opacity: 1; }
  to   { transform: translate(54%, 76%) rotate(16deg) scale(0.9); opacity: 0; }
}
@keyframes elim-flash-frames {
  0%   { opacity: 0.95; }
  33%  { opacity: 0.10; }
  50%  { opacity: 0.82; }
  66%  { opacity: 0.10; }
  84%  { opacity: 0.55; }
  100% { opacity: 0; }
}
@keyframes elim-rays-burst {
  from { opacity: 0; transform: scale(0.18) rotate(0deg); }
  15%  { opacity: 0.95; }
  55%  { opacity: 0.5; }
  to   { opacity: 0; transform: scale(1.3) rotate(6deg); }
}
@keyframes elim-shake {
  0%   { transform: translate(0, 0); }
  25%  { transform: translate(-7px, 4px); }
  50%  { transform: translate(6px, -5px); }
  75%  { transform: translate(-3px, -2px); }
  100% { transform: translate(0, 0); }
}

/* ========================== the scanline pass ============================= */
/*
 * ONE static layer: the line structure of a CRT, over everything, dialogs included.
 *
 * The MOTION is deliberately gone. The tape grain (a 1.1s steps(4) crawl) and the tracking band
 * (a bar running the height of the picture every 11s) read as a shaking screen rather than a
 * recording, so both were REMOVED, not slowed — along with the chroma/vignette fringe that came
 * with them. Nothing in this stack animates any more, which is why there is no
 * `prefers-reduced-motion` variant left to write: a still layer needs no motion opt-out.
 *
 * The layer must be decorative and may NOT take a tap: `elementFromPoint` skips a layer with
 * `pointer-events: none`, which the examine probe relies on (`on_top` — the card under the pointer
 * must be the card). A filter layer that accepted a tap would break the game for a player exactly as
 * it breaks that probe, so keep `pointer-events: none` on `#vhs` and on everything inside it.
 */
.vhs { position: fixed; inset: 0; z-index: 70; pointer-events: none; }
.vhs > * { position: absolute; inset: 0; pointer-events: none; }

/* the line structure of a CRT: dark line, faint line, gap */
.vhs-scan {
  /* light by design: a tape's line structure is a texture, not a dark veil — at 0.26 the whole
     client read as muddy and fought the glossy projection under it. */
  background: repeating-linear-gradient(to bottom,
    rgba(2, 6, 8, 0.15) 0 1px,
    rgba(255, 255, 255, 0.030) 1px 2px,
    rgba(2, 6, 8, 0) 2px 3px);
  mix-blend-mode: multiply;
}

