/*
 * Vellum — the web board. Spec §7 ("deluxe tabletop").
 *
 * Everything visual lives here as tokens (§7.1) and states (§7.2) so the light
 * "ivory table" variant of §7.5 is a palette swap rather than a rewrite.
 *
 * The guardrails from §7.5, because they are easy to violate one property at a
 * time: brass is LIT, never emissive (gradient fill, no outer glow on gold);
 * one gradient per element; never gold on gold; letterpress stays tiny.
 */

:root {
  /* Every colour below is written here again at runtime by js/theme.js, from
     core/src/theme.js — which is the authority, and is shared with the app
     (§8.7). These are the DEFAULT theme's values, kept in the stylesheet only
     so the page paints correctly in the moment before the module runs. Edit
     core/src/theme.js; if you edit these, the two disagree for one frame. */

  /* The table (§7.1) */
  --ground-core: #17293f;
  --ground-mid: #0d1a2b;
  --ground: #070f1a;

  /* The tile, and the letter cut into it */
  --tile-face: #263c60;
  --tile-edge: #16233a;
  --ink: #e9c27c;
  --ink-press: rgba(0, 0, 0, 0.5);
  --value: #b8934f;

  /* Brass is LIT, never emissive (§7.5) */
  --brass-lit: #f7e3af;
  --brass: #d4ad57;
  --brass-deep: #c2a061;
  --rule: rgba(214, 176, 96, 0.3);
  --text: #ece5d5;

  /* §7.2 — selection. Blue always means "held": a theme may retune it for its
     ground, never reassign it. */
  --select-lit: #7ba6f5;
  --select-mid: #3d6fd6;
  --select-dim: #24499c;
  --select-ink: #eef3ff;
  --select-ring: rgba(150, 186, 245, 0.95);
  --select-glow: rgba(90, 140, 230, 0.6);
  --trace-halo: rgba(4, 10, 26, 0.5);
  --trace-core: rgba(60, 104, 196, 0.97);
  --trace-edge: rgba(160, 196, 255, 0.7);
  --trace-node: #0b1a3c;

  /* Confirmed word. Jade — a material the name pool already uses (§8.2), lit
     the same way as every other tile state, never emissive. */
  --confirm-lit: #7fd8ab;
  --confirm-mid: #34a074;
  --confirm-dim: #1d6b50;
  --confirm-ink: #f0fff7;
  --confirm-ring: rgba(126, 214, 170, 0.95);
  --confirm-glow: rgba(52, 160, 116, 0.5);

  --invalid-lit: #d98a8a;
  --invalid-dim: #b25050;
  --invalid-ink: #ffffff;

  /* The emboss (§7.4). An ivory tile takes a bright top highlight; a dark one
     is blown out by it, so this is per-theme rather than a constant. */
  --tile-hi: rgba(255, 255, 255, 0.13);
  --tile-lo: rgba(0, 0, 0, 0.3);
  --gloss-hi: rgba(255, 255, 255, 0.19);
  --gloss-mid: rgba(255, 255, 255, 0.055);
  --glass-rim: rgba(255, 255, 255, 0.15);
  --premium-under: #0a1730;
  --premium-under-mid: #16253f;
  --premium-rim: rgba(5, 12, 27, 0.8);
  --premium-hi: rgba(255, 255, 255, 0.06);
  --premium-tag: #ad9264;

  /* The scoring language (§7.2), constant across themes. */
  --bonus-letter: rgba(42, 140, 140, 0.5);
  --bonus-letter-ink: #e9fdff;
  --bonus-word: rgba(126, 86, 180, 0.5);
  --bonus-word-ink: #f4edff;
  --bonus-length: rgba(90, 120, 200, 0.45);
  --bonus-length-ink: #eaf0ff;

  /* One gradient per element (§7.5) — composed here so a theme only ever has
     to supply flat colours, which is all React Native can take anyway. */
  --tile: linear-gradient(158deg, var(--tile-face), var(--tile-edge));
  /* The glass: a sheen swept off the top-left, dying out before the middle of
     the tile, over a soft linear fall. Layered ABOVE the face and BELOW the
     letter, which is the whole difference between a lit pane and a laminated
     one. Applied to every tile state so a held or scored tile is the same
     object in a different colour, not a different material. */
  --gloss:
    linear-gradient(177deg, var(--gloss-hi) 0%, var(--gloss-mid) 33%, transparent 54%),
    radial-gradient(135% 82% at 24% 2%, var(--gloss-hi) 0%, transparent 58%);
  --sapphire: linear-gradient(158deg, var(--select-lit), var(--select-mid) 46%, var(--select-dim));
  --jade: linear-gradient(158deg, var(--confirm-lit), var(--confirm-mid) 46%, var(--confirm-dim));
  --invalid: linear-gradient(158deg, var(--invalid-lit), var(--invalid-dim));

  --serif: 'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', Georgia, serif;
  --sans: -apple-system, system-ui, 'Segoe UI', Roboto, sans-serif;

  /* Geometry (§7.4): 62px cells, 10px gaps, 350px board — scaled down, never up. */
  --gap: 10px;
  --cell: clamp(44px, calc((min(100vw, 430px) - 34px - 4 * var(--gap)) / 5), 62px);
  --board: calc(5 * var(--cell) + 4 * var(--gap));
  --step: calc(var(--cell) + var(--gap));

  --drop: 220ms; /* §7.4: cascade 180-260ms */
  --stagger: 30ms;

  /* The confirmation beat (§7.4). Deliberately slower than the cascade: it is
     the only moment in a run that is pure reward, and the cascade is the part
     the player is waiting through. Kept in step with board-view.js, which
     sequences the classes — change both or the phases overlap.
     `--lift` is set from JS (it is a fraction of the MEASURED cell, and cell
     geometry is never parsed back out of CSS — trap 11). */
  --pop-ms: 300ms;
  --lift-ms: 400ms;
  --boom-ms: 320ms;
  --shake-ms: 300ms;

  color-scheme: dark;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--ground);
  color: var(--text);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
  overscroll-behavior: none;
}

body {
  /* The spotlight IS the frame — no board border, no well (§7.4). */
  background:
    radial-gradient(120% 90% at 50% 32%, var(--ground-core) 0%, var(--ground-mid) 46%, var(--ground) 100%) var(--ground);
  background-attachment: fixed;
}

/* Grain, kept under the 3% ceiling in §7.5 — above that it stops being texture. */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  opacity: 0.025;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

.app {
  position: relative;
  z-index: 1;
  height: 100dvh;
  display: grid;
  /* header · stage · board · thumb zone. The last row takes the slack, which is
     what holds the board optically high (§7.4). */
  grid-template-rows: auto auto auto minmax(22dvh, 1fr);
  justify-items: center;
  max-width: 480px;
  margin: 0 auto;
  padding: max(10px, env(safe-area-inset-top)) 16px env(safe-area-inset-bottom);
}

/* ─── HUD: things you READ, all of it above the board (§7.4) ─────────────── */

.hud {
  width: var(--board);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: 6px 2px 0;
}

.score {
  font-family: var(--serif);
  font-size: 30px;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  background: linear-gradient(158deg, var(--brass-lit), var(--brass));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.progress {
  font-size: 12.5px;
  letter-spacing: 0.02em;
  color: color-mix(in srgb, var(--text) 62%, transparent);
  font-variant-numeric: tabular-nums;
}

/* Fixed height so nothing below it moves as the word grows (§7.4). */
.stage {
  width: var(--board);
  height: 108px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 7px;
  text-align: center;
}

.lure {
  margin: 0;
  font-size: 12.5px;
  letter-spacing: 0.01em;
  color: color-mix(in srgb, var(--text) 50%, transparent);
  transition: opacity 200ms;
}
.lure b {
  font-weight: 600;
  color: var(--brass);
  font-variant-numeric: tabular-nums;
}

.word {
  margin: 0;
  font-family: var(--serif);
  font-size: 30px;
  line-height: 1.05;
  letter-spacing: 0.12em;
  text-indent: 0.12em;
  min-height: 32px;
  color: var(--text);
}
.word.is-invalid { color: var(--invalid-lit); }

.chips {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  justify-content: center;
  min-height: 21px;
}

.chip {
  font-size: 11.5px;
  line-height: 1;
  padding: 5px 8px;
  border-radius: 999px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  background: rgba(255, 255, 255, 0.06);
  color: color-mix(in srgb, var(--text) 80%, transparent);
}
/* §7.2: teal = letter, violet = word, blue = length. Consistent everywhere. */
.chip--letter { background: var(--bonus-letter); color: var(--bonus-letter-ink); }
.chip--word   { background: var(--bonus-word); color: var(--bonus-word-ink); }
.chip--length { background: var(--bonus-length); color: var(--bonus-length-ink); }
.chip--total {
  background: linear-gradient(158deg, var(--brass-lit), var(--brass));
  color: #2b2010;
  font-weight: 600;
}

/* ─── The board ──────────────────────────────────────────────────────────── */

.board-wrap { padding-top: 4px; }

.board {
  position: relative;
  width: var(--board);
  height: var(--board);
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}

/* Stroke colours live here, not in board-view.js, so a theme can retune the
   trace without JS knowing a colour. The widths stay in JS — they are the
   stacking order of the four strokes, which is structure, not palette. */
.trace polyline:nth-of-type(1) { stroke: var(--trace-halo); }
.trace polyline:nth-of-type(2) { stroke: var(--trace-core); }
.trace polyline:nth-of-type(3) { stroke: var(--trace-edge); }
.trace circle { fill: var(--trace-node); }

.trace {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
  z-index: 3;
}

.tile {
  position: absolute;
  width: var(--cell);
  height: var(--cell);
  left: 0;
  top: 0;
  border-radius: 10px;
  display: grid;
  place-items: center;
  font-family: var(--serif);
  font-size: calc(var(--cell) * 0.5);
  line-height: 1;
  color: var(--ink);
  background: var(--gloss), var(--tile);
  /* Emboss, glass rim, drop shadow (§7.4). Still not 3D plastic (§7.5): the
     rim is a hairline and the sheen is a sweep, so what reads is a lit pane. */
  box-shadow:
    inset 0 1px 0 var(--tile-hi),
    inset 0 0 0 1px var(--glass-rim),
    inset 0 -7px 12px -7px var(--tile-lo),
    0 2px 5px rgba(0, 0, 0, 0.45),
    0 10px 22px rgba(0, 0, 0, 0.28);
  text-shadow: 0 1px 0 var(--ink-press); /* letterpress: tiny (§7.5) */
  transform: translate3d(var(--x), var(--y), 0);
  transition:
    transform var(--drop) cubic-bezier(0.2, 0.9, 0.28, 1.06),
    background 120ms linear,
    box-shadow 120ms linear,
    color 120ms linear,
    opacity 140ms linear;
  will-change: transform;
  z-index: 2;
}

.tile__value {
  position: absolute;
  right: 5px;
  bottom: 3px;
  font-size: calc(var(--cell) * 0.19);
  color: var(--value);
  font-variant-numeric: tabular-nums;
  text-shadow: none;
}

/* Premiums belong to POSITIONS, so they must stay legible as letters fall past
   them — the terrain is the constant (§7.4). Rendered under the tiles. */
.premium {
  position: absolute;
  width: var(--cell);
  height: var(--cell);
  border-radius: 10px;
  transform: translate3d(var(--x), var(--y), 0);
  /* The same well, with nothing standing in it. Engraved rather than outlined:
     shadow along the top lip, a hairline of light along the bottom. Seen only
     for the ~1s of the confirmation beat, while the played tiles are lifting
     and exploding away — but it has to be the same idea as the tile, or the
     square changes meaning the moment it is uncovered. */
  background: var(--premium-under);
  box-shadow:
    inset 0 2px 6px rgba(0, 0, 0, 0.55),
    inset 0 -1px 0 var(--glass-rim);
  opacity: 0.9;
  z-index: 0;
  pointer-events: none;
}

/* A premium square is a WELL the glass sits over, lit from its bottom edge —
   not a frame around the letter (§4.1: the multiplier belongs to the square).
   Same rim, same emboss, same shadow as any other tile: it is the same object,
   lit differently. `background` is in the tile's transition list, so a tile that
   cascades INTO a premium square warms into it over 120ms as it lands. */
.tile[data-premium] {
  background:
    var(--gloss),
    linear-gradient(0deg,
      var(--premium-under) 0%,
      var(--premium-under-mid) 42%,
      var(--tile-face) 78%,
      var(--tile-edge) 100%);
  /* The rim darkens WITH the face. Left at the shared `--glass-rim`, the same
     pale hairline gains contrast as the tile behind it darkens and reads as a
     bright outline — which is the thing the underlight exists to avoid. The top
     highlight comes down for the same reason. */
  box-shadow:
    inset 0 1px 0 var(--premium-hi),
    inset 0 0 0 1px var(--premium-rim),
    inset 0 -7px 12px -7px var(--tile-lo),
    0 2px 5px rgba(0, 0, 0, 0.45),
    0 10px 22px rgba(0, 0, 0, 0.28);
}

.tile__tag {
  position: absolute;
  left: 5px;
  top: 3px;
  font-family: var(--serif);
  font-size: calc(var(--cell) * 0.185);
  letter-spacing: 0.03em;
  /* Louder than the ring on purpose: the ring says "this square is special",
     the tag says which multiplier, and only one of those is routed around. */
  color: var(--premium-tag);
  text-shadow: none;
}

/* Selection (§7.2) */
.tile.is-held {
  background: var(--gloss), var(--sapphire);
  color: var(--select-ink);
  text-shadow: none;
  box-shadow:
    inset 0 0 0 1.5px var(--select-ring),
    inset 0 1px 0 var(--gloss-hi),
    0 0 14px var(--select-glow),
    0 4px 10px rgba(0, 0, 0, 0.45);
  z-index: 4;
}
.tile.is-held .tile__value,
.tile.is-held .tile__tag { color: rgba(238, 243, 255, 0.75); }

/* Confirmed — the word is real and has scored. */
.tile.is-scored {
  background: var(--gloss), var(--jade);
  color: var(--confirm-ink);
  text-shadow: none;
  box-shadow:
    inset 0 0 0 1.5px var(--confirm-ring),
    inset 0 1px 0 var(--gloss-hi),
    0 0 18px var(--confirm-glow),
    0 4px 10px rgba(0, 0, 0, 0.45);
  z-index: 4;
  animation: pop var(--pop-ms) cubic-bezier(0.2, 1.4, 0.4, 1) both;
}
.tile.is-scored .tile__value,
.tile.is-scored .tile__tag { color: rgba(240, 255, 247, 0.8); }

/* The glow lives on a halo, not on the tile, so the LIGHT and the RISE can be
   timed independently — the tile floats on one curve while the light quickens
   on another. Sharing one box-shadow would force them onto the same clock. */
.tile.is-scored::after {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: 12px;
  pointer-events: none;
  opacity: 0;
  box-shadow: 0 0 17px 2px var(--confirm-glow), inset 0 0 0 1px var(--confirm-ring);
}

/* ── The lift: the whole word leaves the felt as one body ─────────────────── */

/* Every scored tile gets the SAME offset on the SAME curve, with no per-column
   stagger. The stagger is what makes the cascade read as gravity acting on
   separate tiles; here the opposite is wanted — five tiles have to read as one
   word, so anything that separates them is wrong. */
.tile.is-lifting {
  animation: lift var(--lift-ms) cubic-bezier(0.24, 0.68, 0.26, 1) both;
  z-index: 5;
}
@keyframes lift {
  0%   { transform: translate3d(var(--x), var(--y), 0) scale(1.05); }
  100% { transform: translate3d(var(--x), calc(var(--y) + var(--lift)), 0) scale(1.11); }
}

/* The light quickens as the word rises: the gaps between peaks close from 30%
   of the lift to 6% of it. It is the tell that something is about to give —
   the pulse rate, not the brightness, is what reads as "about to". */
.tile.is-lifting::after {
  animation: charge var(--lift-ms) linear both;
}
@keyframes charge {
  0%   { opacity: 0.15; }
  24%  { opacity: 0.85; }
  40%  { opacity: 0.30; }
  58%  { opacity: 1;    }
  70%  { opacity: 0.40; }
  82%  { opacity: 1;    }
  89%  { opacity: 0.55; }
  95%  { opacity: 1;    }
  100% { opacity: 1;    }
}

/* ── The explosion: it lets go ────────────────────────────────────────────── */

/* Each tile keeps the lift as its ORIGIN and flies outward from the word's
   centre, so the debris leaves from where the word was hanging rather than
   from the grid it came off. `--bx`/`--by`/`--brot` are per-tile, set in JS.
   The punch at 20% is what separates an explosion from a fade. */
.tile.is-bursting {
  animation: boom var(--boom-ms) cubic-bezier(0.18, 0.72, 0.36, 1) both;
  z-index: 6;
}
@keyframes boom {
  0% {
    transform: translate3d(var(--x), calc(var(--y) + var(--lift)), 0) scale(1.11) rotate(0deg);
    opacity: 1;
  }
  20% {
    transform:
      translate3d(calc(var(--x) + var(--bx) * 0.16), calc(var(--y) + var(--lift) + var(--by) * 0.16), 0)
      scale(1.3) rotate(calc(var(--brot) * 0.18));
    opacity: 1;
  }
  100% {
    transform:
      translate3d(calc(var(--x) + var(--bx)), calc(var(--y) + var(--lift) + var(--by)), 0)
      scale(0.58) rotate(var(--brot));
    opacity: 0;
  }
}

.tile.is-clearing {
  opacity: 0;
  transform: translate3d(var(--x), var(--y), 0) scale(1.32);
  transition: transform 200ms ease-out, opacity 200ms ease-in;
  z-index: 4;
}

.tile.is-invalid {
  background: var(--gloss), var(--invalid);
  color: var(--invalid-ink);
  text-shadow: none;
  animation: shake var(--shake-ms) ease-in-out;
}
.tile.is-invalid .tile__value,
.tile.is-invalid .tile__tag { color: rgba(255, 255, 255, 0.8); }

@keyframes pop {
  0%   { transform: translate3d(var(--x), var(--y), 0) scale(1); }
  55%  { transform: translate3d(var(--x), var(--y), 0) scale(1.14); }
  100% { transform: translate3d(var(--x), var(--y), 0) scale(1.05); }
}

@keyframes shake {
  0%, 100% { transform: translate3d(var(--x), var(--y), 0); }
  25% { transform: translate3d(calc(var(--x) - 4px), var(--y), 0); }
  75% { transform: translate3d(calc(var(--x) + 4px), var(--y), 0); }
}

/* The burst — one ring per tile, thrown at the moment the word lets go, so it
   starts from the LIFTED height rather than from the grid cell. */
.burst {
  position: absolute;
  width: var(--cell);
  height: var(--cell);
  border-radius: 12px;
  transform: translate3d(var(--x), calc(var(--y) + var(--lift)), 0);
  box-shadow: 0 0 0 2px var(--confirm-ring);
  opacity: 0.85;
  pointer-events: none;
  z-index: 5;
  animation: burst 520ms cubic-bezier(0.15, 0.7, 0.25, 1) forwards;
}
@keyframes burst {
  0%   { transform: translate3d(var(--x), calc(var(--y) + var(--lift)), 0) scale(0.86); opacity: 0.9; }
  100% { transform: translate3d(var(--x), calc(var(--y) + var(--lift)), 0) scale(2.5); opacity: 0; }
}

/* One wave from the word's centre. Five rings alone read as five tiles going
   off at once; a single wave over them is what makes it one detonation. Its
   centre is set in JS, because it is the centroid of the path, not a cell. */
.shock {
  position: absolute;
  width: var(--cell);
  height: var(--cell);
  margin: calc(var(--cell) / -2) 0 0 calc(var(--cell) / -2);
  border-radius: 50%;
  box-shadow: 0 0 0 1.5px var(--confirm-ring), 0 0 24px 4px var(--confirm-glow);
  pointer-events: none;
  z-index: 5;
  animation: shock 560ms cubic-bezier(0.1, 0.72, 0.22, 1) forwards;
}
@keyframes shock {
  0%   { transform: scale(0.5); opacity: 0.65; }
  100% { transform: scale(4.2); opacity: 0; }
}

/* The played word rises off the board so it can be read after the tiles go. */
.floaters {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 6;
}

.floater {
  position: absolute;
  left: 50%;
  transform: translate(-50%, 0);
  text-align: center;
  white-space: nowrap;
  animation: rise 1300ms cubic-bezier(0.22, 0.8, 0.3, 1) forwards;
}
.floater__word {
  display: block;
  font-family: var(--serif);
  font-size: 27px;
  letter-spacing: 0.13em;
  text-indent: 0.13em;
  color: var(--confirm-ink);
  text-shadow: 0 2px 10px rgba(20, 70, 50, 0.7), 0 1px 2px rgba(0, 0, 0, 0.6);
}
.floater__score {
  display: block;
  margin-top: 3px;
  font-family: var(--serif);
  font-size: 19px;
  font-variant-numeric: tabular-nums;
  background: linear-gradient(158deg, var(--brass-lit), var(--brass));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
@keyframes rise {
  0%   { transform: translate(-50%, 0) scale(1.08); opacity: 0; }
  10%  { transform: translate(-50%, -10px) scale(1); opacity: 1; }
  66%  { opacity: 1; }
  100% { transform: translate(-50%, -168px) scale(1.02); opacity: 0; }
}

/* Tiles settle in on load (§7.4). */
.tile.is-settling { animation: settle 380ms cubic-bezier(0.2, 0.9, 0.28, 1.05) both; }
@keyframes settle {
  from { transform: translate3d(var(--x), calc(var(--y) - 14px), 0); opacity: 0; }
  to   { transform: translate3d(var(--x), var(--y), 0); opacity: 1; }
}

/* ─── Thumb zone: things you TOUCH. Small, quiet (§7.4) ──────────────────── */

/* The board stays optically high and the controls drop into the thumb arc —
   things you TOUCH at the bottom, things you READ at the top (§7.4). The slack
   between them is deliberate dead space for the hand. */
.tray {
  align-self: stretch;
  width: 100%;
  padding: 16px 0 max(10px, env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
}

.tray__row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  flex-wrap: wrap;
}
.tray__row--card { margin-top: 18px; }

.btn {
  appearance: none;
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 999px;
  color: color-mix(in srgb, var(--text) 72%, transparent);
  font-family: var(--sans);
  font-size: 12.5px;
  letter-spacing: 0.02em;
  padding: 9px 15px;
  cursor: pointer;
}
.btn:active { background: color-mix(in srgb, var(--brass) 9%, transparent); }
.btn--primary {
  background: linear-gradient(158deg, var(--brass-lit), var(--brass));
  border-color: transparent;
  color: #2b2010;
  font-weight: 600;
}

.tray__note {
  font-size: 11.5px;
  color: color-mix(in srgb, var(--text) 34%, transparent);
  text-align: center;
}

/* ─── Result ─────────────────────────────────────────────────────────────── */

.sheet {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: grid;
  place-items: center;
  padding: 20px;
  background: color-mix(in srgb, var(--ground) 84%, transparent);
  backdrop-filter: blur(7px);
  animation: fade 260ms ease-out both;
}
.sheet[hidden] { display: none; }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }

.card {
  width: min(100%, 400px);
  max-height: 92dvh;
  overflow-y: auto;
  border: 1px solid var(--rule);
  border-radius: 16px;
  padding: 22px 20px;
  background: linear-gradient(158deg, var(--ground-core), var(--ground));
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6);
}

.card h2 {
  margin: 0 0 2px;
  font-family: var(--serif);
  font-size: 22px;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--text);
}
/* The result card puts a date immediately under its heading; settings goes
   straight into a control, and needs the room that date was taking. */
#settings .card h2 { margin-bottom: 18px; }
.card__date {
  margin: 0 0 16px;
  font-size: 12px;
  color: color-mix(in srgb, var(--text) 45%, transparent);
}

.final {
  font-family: var(--serif);
  font-size: 52px;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  background: linear-gradient(158deg, var(--brass-lit), var(--brass));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.rows { margin: 16px 0 4px; display: grid; gap: 9px; }
.row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  font-size: 13px;
  color: color-mix(in srgb, var(--text) 62%, transparent);
}
.row b {
  font-family: var(--serif);
  font-weight: 500;
  font-size: 15px;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.row--rule { border-top: 1px solid color-mix(in srgb, var(--brass) 16%, transparent); padding-top: 10px; }

/* The share card is a skyline: one bar per word, height = its score, colour =
   the premium it routed through (§5.4). */
.skyline {
  display: flex;
  align-items: flex-end;
  gap: 3px;
  height: 62px;
  margin: 16px 0 6px;
}
.bar {
  flex: 1;
  min-height: 3px;
  border-radius: 2px 2px 1px 1px;
  background: color-mix(in srgb, var(--text) 30%, transparent);
}
.bar--letter { background: color-mix(in srgb, var(--bonus-letter) 90%, var(--text)); }
.bar--word   { background: color-mix(in srgb, var(--bonus-word) 90%, var(--text)); }
.bar--both   { background: linear-gradient(180deg, var(--bonus-word), var(--bonus-letter)); }

.toast {
  position: fixed;
  left: 50%;
  bottom: calc(24px + env(safe-area-inset-bottom));
  transform: translateX(-50%);
  z-index: 30;
  padding: 9px 14px;
  border-radius: 999px;
  border: 1px solid var(--rule);
  background: color-mix(in srgb, var(--ground-mid) 94%, transparent);
  font-size: 12.5px;
  color: color-mix(in srgb, var(--text) 85%, transparent);
  animation: toast 2200ms ease-out forwards;
}
/* A card that asks something, rather than reporting it: narrower, and the
   question is the whole content. */
.card--ask { width: min(100%, 340px); }
.ask {
  margin: 10px 0 18px;
  font-size: 13.5px;
  line-height: 1.5;
  color: color-mix(in srgb, var(--text) 62%, transparent);
}

/* ─── The gear, and what is behind it ────────────────────────────────────── */

/* Bottom right, small and dim. It is the one persistent control on the screen,
   so it has to be reachable without being something the eye keeps landing on
   while reading the board. It sits outside `.board`, so a trace can never
   start on it, and `--gap` clear of the safe area on a notched phone. */
.gear {
  position: fixed;
  right: max(12px, env(safe-area-inset-right));
  bottom: max(12px, env(safe-area-inset-bottom));
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: color-mix(in srgb, var(--ground-core) 70%, transparent);
  color: color-mix(in srgb, var(--text) 50%, transparent);
  cursor: pointer;
  z-index: 8;
  transition: color 140ms linear, border-color 140ms linear, transform 140ms ease-out;
}
.gear svg { width: 21px; height: 21px; display: block; }
.gear:hover { color: var(--brass); border-color: color-mix(in srgb, var(--brass) 45%, transparent); }
.gear:active { transform: scale(0.94); }

.setting { margin: 0 0 20px; }
.setting__label {
  margin: 0;
  font-family: var(--serif);
  font-size: 15px;
  color: var(--text);
}
.setting__hint {
  margin: 2px 0 10px;
  font-size: 12.5px;
  line-height: 1.35;
  color: color-mix(in srgb, var(--text) 45%, transparent);
}

.swatches {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(74px, 1fr));
  gap: 8px;
}

/* The swatch shows the table it selects, not a label with a colour beside it —
   the choice IS the colour, so the control should be the thing being chosen. */
.swatch {
  display: grid;
  justify-items: center;
  gap: 7px;
  padding: 9px 4px 8px;
  border: 1px solid var(--rule);
  border-radius: 11px;
  background: transparent;
  color: color-mix(in srgb, var(--text) 55%, transparent);
  font-family: var(--sans);
  font-size: 11.5px;
  letter-spacing: 0.02em;
  cursor: pointer;
  transition: border-color 140ms linear, color 140ms linear, background 140ms linear;
}
.swatch__dot {
  width: 36px;
  height: 36px;
  border-radius: 9px;
  display: grid;
  place-items: center;
  background: linear-gradient(158deg, var(--sw-ground), var(--sw-edge));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 16%, transparent),
              0 2px 5px rgba(0, 0, 0, 0.35);
}
.swatch__tile {
  width: 19px;
  height: 19px;
  border-radius: 4px;
  display: grid;
  place-items: center;
  background: var(--sw-tile);
  color: var(--sw-ink);
  font-family: var(--serif);
  font-size: 12px;
  line-height: 1;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
}
.swatch[aria-checked='true'] {
  border-color: var(--brass);
  color: var(--text);
  background: color-mix(in srgb, var(--brass) 8%, transparent);
}
/* Brass is LIT, never emissive (§7.5) — the selected swatch is marked with a
   ring in the gradient's own colours rather than a glow. */
.swatch[aria-checked='true'] .swatch__dot {
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 16%, transparent),
              0 0 0 2px var(--ground), 0 0 0 3.5px var(--brass);
}

@keyframes toast {
  0% { opacity: 0; transform: translate(-50%, 8px); }
  12%, 78% { opacity: 1; transform: translate(-50%, 0); }
  100% { opacity: 0; transform: translate(-50%, -4px); }
}

.loading {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: var(--serif);
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  font-size: 14px;
  color: color-mix(in srgb, var(--text) 40%, transparent);
}
.loading[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
  }
}

/* ── The leaderboard on the result card (§8.6, §14.3) ─────────────────────── */

.ladder {
  margin-top: 18px;
  border-top: 1px solid var(--rule);
  padding-top: 14px;
}
.ladder__head {
  margin: 0 0 8px;
  font-size: 11px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--brass);
  opacity: 0.8;
}
.ladder__rows {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 6px;
}
.ladder__row {
  display: grid;
  grid-template-columns: 1.6em 22px 1fr auto;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text);
}
/* Your own row is brighter, not differently coloured: the leaderboard's colour
   language is already spent on the scoring chips (§7.2), and a second meaning
   for teal or violet here would collide with it. */
.ladder__row--you {
  color: var(--brass-lit);
  font-weight: 600;
}
.ladder__rank {
  font-variant-numeric: tabular-nums;
  opacity: 0.6;
  text-align: right;
}
/* The tile avatar (§8.2): two tones from the id. It gives the identity a face
   without a photo — which the zero-PII model could never allow — and keeps two
   rows apart when their generated names collide. */
.ladder__tile {
  width: 22px;
  height: 22px;
  border-radius: 5px;
  display: grid;
  place-items: center;
  font-family: var(--serif);
  font-size: 12px;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
}
.ladder__name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ladder__score {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}
.sent {
  margin: 14px 0 0;
  font-size: 12px;
  text-align: center;
  color: var(--text);
  opacity: 0.55;
}

/* Your own name, in the settings panel (§8.2). */
.whoami {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-top: 6px;
}
.whoami__name {
  font-family: var(--serif);
  font-size: 17px;
  color: var(--ink);
  letter-spacing: 0.2px;
}
