@font-face {
  font-family: "Bauvex";
  src: url("../assets/fonts/bauvex.woff2?v=247") format("woff2"),
       url("../assets/fonts/bauvex.otf?v=247") format("opentype");
  font-weight: normal; font-style: normal; font-display: swap;
}
/* The brand wordmark face (PACKLABS). The source is an OpenType-SVG colour
   font; this is an outline-only build of it so the letterforms render the same
   in every browser and take a CSS gold gradient (Chrome ignores the SVG table). */
@font-face {
  font-family: "GoldWhisper";
  src: url("../assets/fonts/goldwhisper.woff2?v=321") format("woff2");
  font-weight: normal; font-style: normal; font-display: swap;
}
:root {
  --bg: #0e1116;
  --bg-2: #161b24;
  --bg-3: #1f2733;
  --panel: #1a212c;
  --panel-2: #222c3a;
  --line: #2c3848;
  --line-2: #3a4a5f;
  --text: #e8edf4;
  --text-dim: #9fb0c3;
  --text-faint: #6c7c90;
  --accent: #d8a24a;
  --accent-2: #e6b765;
  --accent-ink: #2a1f08;
  --blue: #4a8fd8;
  --green: #4cae7a;
  --red: #d6604d;
  --purple: #9a6fd0;
  --radius: 12px;
  --radius-sm: 8px;
  --shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
  --shadow-sm: 0 2px 10px rgba(0, 0, 0, 0.35);
  --font: "Segoe UI", system-ui, -apple-system, Roboto, Helvetica, Arial, sans-serif;
  /* Faint monochrome fractal noise, baked at low alpha. Layered on top of the
     felt's radial gradients to dither 8-bit banding - the subtle green/dark
     ramps quantize into visible bands otherwise, most obvious behind the logo
     and text on a flat stretch of felt. Tiled at its natural size so it stays
     grain, not pattern; pointer-events aren't a concern since it's a paint. */
  --felt-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='0.035'/%3E%3C/svg%3E");
  /* The felt vignette, baked once as a pre-dithered bitmap. The compositor
     samples this texture instead of recomputing a live radial gradient every
     frame, so the tile seam that used to flash across the green during the
     menu's entrance animation is gone - a bitmap's tiles always line up, and
     the dither is baked in so it never bands. The live --felt-noise still
     rides on top at device resolution to keep the dither crisp on hi-dpi,
     where the bitmap itself gets upscaled. Regenerate via tools if the
     palette changes; source math mirrors the old three-radial stack. */
  --felt-image: url("../assets/textures/felt.webp?v=328");
  /* The cloth. A small greyscale tile of real fabric fibre (a photographed
     felt weave, high-passed down to detail only and centred on mid-grey), laid
     over the colour layer with background-blend-mode: overlay. Mid-grey is
     overlay's identity value, so the tile only modulates whatever colour is
     underneath and never tints it - one set of tiles textures all five felt
     themes. Tiled at --felt-fibre-size rather than stretched, so the fibre
     stays the same physical size on a full-screen table and on a 120px
     preview swatch. Baked by tools/feltfibre.py; the strength comes from
     Settings > Table look (body.fibre-*), and Off drops the layer entirely. */
  --felt-fibre: url("../assets/textures/felt-fibre-subtle.webp?v=2");
  --felt-fibre-size: 100px 100px;
}

/* FELT COLOUR themes (Settings > Table look). One baked texture per colour, each
   a hue-shifted copy of the default felt that keeps the exact same vignette and
   dither - so every felt surface (menus AND the play table, which all read
   var(--felt-image)) recolours together from a single body class. Emerald is the
   default and carries no class. See js/views/settings.js applyFeltTheme(). */
body.felt-sapphire { --felt-image: url("../assets/textures/felt-sapphire.webp?v=986"); }
body.felt-slate    { --felt-image: url("../assets/textures/felt-slate.webp?v=986"); }
body.felt-burgundy { --felt-image: url("../assets/textures/felt-burgundy.webp?v=986"); }
body.felt-amethyst { --felt-image: url("../assets/textures/felt-amethyst.webp?v=986"); }

/* FELT FIBRE strength (Settings > Table look). Same shape as the colour themes:
   one body class swaps the tile every felt surface reads. Subtle is the default
   and carries no class; Off swaps in `none`, which is a valid background layer,
   so the layer counts (and therefore the size/repeat/blend lists at each felt)
   stay the same either way. See js/views/settings.js applyFeltFibre(). */
body.fibre-off    { --felt-fibre: none; }
body.fibre-medium { --felt-fibre: url("../assets/textures/felt-fibre-medium.webp?v=2"); }
body.fibre-strong { --felt-fibre: url("../assets/textures/felt-fibre-strong.webp?v=2"); }

/* REDUCE MOTION (Settings > Motion). Near-zeros every CSS transition and
   animation so nothing slides, flips, tumbles or pulses. Paired with
   TCG.reducedMotion() in JS, which also short-circuits the staged/canvas
   animations that aren't pure CSS. Scoped to the explicit player pref
   (body.reduce-motion); the OS "prefers-reduced-motion" setting is honoured
   separately by the JS gates. */
body.reduce-motion *, body.reduce-motion *::before, body.reduce-motion *::after {
  animation-duration: 0.001ms !important;
  animation-delay: 0ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.001ms !important;
  transition-delay: 0ms !important;
}

/* INTERFACE SIZE (Settings > Comfort). Scales the whole app. 1 = default, so
   nothing changes unless the player picks another size. Set live on :root by
   TCG.applyUiScale(). */
body { zoom: var(--ui-zoom, 1); }

/* VISUAL MODE (Settings > Comfort). An accessibility mode for players who read
   with difficulty: hide the words on surfaces that already carry a meaning-
   bearing icon and let the icon (plus its hover tooltip) do the talking. Turned
   on via body.visual-mode by TCG.applyVisualMode(). Experimental first pass -
   scoped to the clear wins (main-menu cards, icon+label buttons), and it never
   touches the Settings screen itself, so the switch that turns it back off
   always stays in plain words. */
/* Main-menu (and My Games / Settings landing) cards: drop the title and
   description, grow the icon so the picture is the whole card. The Settings
   landing keeps its words (see the :not below) so the toggle stays findable. */
body.visual-mode .menu-hand:not(.settings-hand) .menu-card-title,
body.visual-mode .menu-hand:not(.settings-hand) .menu-card-sub {
  display: none;
}
body.visual-mode .menu-hand:not(.settings-hand) .menu-card-ic {
  width: 74px; height: 74px; border-radius: 18px;
}
body.visual-mode .menu-hand:not(.settings-hand) .menu-card-ic svg {
  width: 42px; height: 42px;
}
/* Buttons built as icon + label (TCG.btn with opts.icon): keep the icon, hide
   the caption, keep the tooltip. Left the Settings view out so its controls stay
   readable, and skip icon-only buttons (nothing to hide there anyway). */
body.visual-mode :not(.settings-view) .btn .ic + span {
  display: none;
}
/* Effect-sentence pills. A pill that carries a value glyph (or a colour swatch)
   is tagged .has-ic in the editor. Normally the glyph is hidden and the word
   shows; in Visual mode the word hides and the glyph shows, with the full wording
   still on hover (the pill keeps its data-tooltip). Pills WITHOUT a glyph - an
   amount, a category, a free label - keep their words in both modes, since a
   number or a game's own category name reads better as text. The colour swatch
   is a dot of the colour itself. Toggle/chrome pills (eye, reset) are untouched:
   they never get .has-ic, so their own always-on icon still shows. */
.free-fx-pill .fx-pill-swatch {
  display: inline-block; width: 3.4cqw; height: 3.4cqw; border-radius: 50%;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,0.35);
}
.free-fx-pill.has-ic .fx-pill-ic,
.free-fx-pill.has-ic .fx-pill-swatch { display: none; }
body.visual-mode .free-fx-pill.has-ic .fx-pill-ic { display: inline-flex; }
body.visual-mode .free-fx-pill.has-ic .fx-pill-swatch { display: inline-block; }
body.visual-mode .free-fx-pill.has-ic .fx-pill-label { display: none; }
/* Step / phase pills - the phase bar in the editor AND the in-play tracker. Full
   step pills now carry a hidden leading step-type icon (.tphase-ic); every config
   pill (goal, turns, tie, buy cap, currency, stage...) already leads with an icon.
   In Visual mode the icon shows and the pill's own words + text badges hide, so the
   whole bar reads as pictures. A pill keeps its hover title, so the words are one
   hover away. Any child that holds an svg (the icon, the remove x) is left alone;
   only text spans hide. The one-word view labels (Setup/Turn/End) sit outside the
   pills and stay, since they're the view switcher. */
.tphase-ic { display: none; }
.tphase-ic svg { width: 15px; height: 15px; }
body.visual-mode .tphase-ic { display: inline-flex; align-items: center; }
body.visual-mode .tphase > span:not(:has(svg)) { display: none; }

/* Zone icon (set from a zone's "Choose icon"). Sits exactly where the label sits
   (above the zone, or below when the zone hugs the felt top), hidden until Visual
   mode - then the typed name hides and the picture shows. Only zones that were
   given an icon (.has-zicon) swap; a zone with no icon keeps its name so it never
   goes blank. Kept the same muted colour and size as the label so nothing jumps. */
.tzone-ic {
  display: none; position: absolute; top: calc(-18px - var(--zlabel-clear, 0px)); left: 0; right: 0;
  justify-content: center; color: rgba(255,255,255,0.55);
}
.tzone-ic.tzone-ic-below { top: calc(100% + 3px + var(--zlabel-clear, 0px)); }
.tzone-ic svg { width: 15px; height: 15px; }
body.visual-mode .tzone.has-zicon .tzone-label { display: none; }
body.visual-mode .tzone.has-zicon .tzone-ic { display: flex; }

/* Context-menu rows whose caller gave a real icon (.ctx-has-ic): in Visual mode
   hide the words and let the icon carry the row. Rows without an explicit icon
   (generic fallback glyph) keep their text, and section heads stay, so a menu
   never collapses into a column of identical glyphs. */
body.visual-mode .ctx-item.ctx-has-ic .ctx-text { display: none; }
body.visual-mode .ctx-item.ctx-has-ic { justify-content: center; }

* { box-sizing: border-box; }

/* Never let click+drag highlight UI text blue. Text fields opt back in below. */
html { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; }
input, textarea, [contenteditable="true"], [contenteditable=""] {
  -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text; user-select: text;
}

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background:
    radial-gradient(900px 500px at 12% -8%, rgba(74,143,216,0.16), transparent 60%),
    radial-gradient(900px 500px at 88% -4%, rgba(216,162,74,0.16), transparent 60%),
    radial-gradient(1100px 700px at 50% 110%, rgba(154,111,208,0.12), transparent 60%),
    radial-gradient(1200px 600px at 50% -10%, #1a2230 0%, var(--bg) 60%) fixed;
  color: var(--text);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
}

button {
  font-family: inherit;
  cursor: pointer;
  /* the browser's default button text color ignores the page's light-on-dark
     theme (it's a system black in most browsers), so any button that skips
     the .btn styling - context menu rows, seat chips, zone controls - showed
     unreadable dark text on this dark background. Inherit instead; specific
     classes like .btn-primary still override this where they need to. */
  color: inherit;
}

a { color: var(--accent-2); }

/* ---------- Top bar ---------- */
.topbar {
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 10px 20px;
  background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(0,0,0,0.2)), var(--bg-2);
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  z-index: 50;
}
.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  user-select: none;
}
.brand-mark { display: inline-flex; color: var(--accent); }
.brand-name {
  font-weight: 700;
  letter-spacing: 0.4px;
  font-size: 16px;
}
.crumbs {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--text-dim);
  font-size: 13px;
  flex: 1;
  min-width: 0;
}
.crumbs .crumb { display: inline-flex; align-items: center; gap: 8px; }
.crumbs .crumb.clickable { cursor: pointer; color: var(--text-dim); }
.crumbs .crumb.clickable:hover { color: var(--text); }
.crumbs .sep { color: var(--text-faint); display: inline-flex; }
.crumbs .current { color: var(--text); font-weight: 600; }
.topbar-actions { display: flex; gap: 10px; align-items: center; }

/* ---------- View ---------- */
.view { padding: 28px 24px 60px; max-width: 1180px; margin: 0 auto; }

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  border: 1px solid var(--line-2);
  background: var(--panel-2);
  color: var(--text);
  padding: 9px 14px;
  border-radius: var(--radius-sm);
  font-size: 14px;
  font-weight: 600;
  transition: border-color 0.15s, background 0.15s, transform 0.05s;
}
.btn:hover { border-color: var(--accent); background: var(--bg-3); }
.btn:active { transform: translateY(1px); }
.btn svg { width: 18px; height: 18px; }
.btn-primary {
  background: linear-gradient(180deg, var(--accent-2), var(--accent));
  border-color: var(--accent);
  color: var(--accent-ink);
}
.btn-primary:hover { filter: brightness(1.06); background: linear-gradient(180deg, var(--accent-2), var(--accent)); }
.btn-ghost { background: transparent; border-color: transparent; color: var(--text-dim); }
.btn-ghost:hover { color: var(--text); background: var(--panel-2); border-color: var(--line); }
.btn-danger:hover { border-color: var(--red); color: var(--red); background: var(--bg-3); }
.btn-sm { padding: 6px 10px; font-size: 13px; }
/* An icon-only button holds one glyph and nothing else, so it centres it.
   .btn is a flex row with align-items but no justify-content, which is invisible
   while the button hugs its content - and puts the glyph flush against the left
   edge the moment anything gives the button a fixed width (a round provider
   button, a square tool button). Centring here fixes every one of those at once
   and is a no-op for a button still sized by its content. */
.btn-icon { padding: 8px; justify-content: center; }
.btn-icon svg { width: 18px; height: 18px; }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }

/* ---------- Headings ---------- */
.page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 22px;
}
.page-head h1 { margin: 0; font-size: 26px; letter-spacing: 0.2px; }
.page-head p { margin: 6px 0 0; color: var(--text-dim); }
.section-title {
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 1.2px;
  color: var(--text-faint);
  margin: 26px 0 12px;
  font-weight: 700;
}

/* ---------- Cards grid (home) ---------- */
/* Each saved game is a 5:7 playing card. The grid packs the cards a few across
   and lets them keep their card shape via aspect-ratio, so they read as a
   spread of cards rather than a wall of panels. */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: 20px;
  align-items: start;
}
.game-tile {
  --tile-accent: var(--accent);
  container-type: inline-size;
  aspect-ratio: 5 / 7;
  background:
    linear-gradient(180deg, rgba(255,255,255,0.06), rgba(0,0,0,0.28)),
    var(--tile-accent);
  border: 0.5px solid rgba(0,0,0,0.45);
  border-radius: 6.7% / 4.8%;
  /* Fixed, not a percentage: percentage padding resolves against the PARENT's
     width, and the hover "fling" layer reparents the card into a full-hand-wide
     layer, which would blow a 2.2% frame up to ~22px. A fixed value keeps the
     accent frame thin no matter what the card's parent is. */
  padding: 4px;
  cursor: pointer;
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  position: relative;
  box-shadow: var(--shadow);
}
.game-tile:hover { transform: translateY(-4px); box-shadow: 0 12px 28px rgba(0,0,0,0.45); }
.gcard-inner {
  position: relative;
  height: 100%;
  display: flex; flex-direction: column;
  background: #14171d;
  border-radius: 4cqw;
  border: 1px solid rgba(0,0,0,0.5);
  overflow: hidden;
  color: #f4f1e8;
}
.gcard-head {
  padding: 4cqw 4.6cqw 3cqw;
  background: linear-gradient(180deg, rgba(255,255,255,0.06), transparent);
  display: flex; align-items: flex-start; gap: 2cqw;
}
.gcard-head .gcard-name { flex: 1; min-width: 0; }
/* Published / unlisted badge. Only a game somebody else can reach wears one:
   private is the normal state and prints nothing. */
.gcard-pub {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: 1.2cqw;
  padding: 1cqw 2.2cqw; border-radius: 999px;
  font-size: 3.6cqw; font-weight: 700; line-height: 1.4;
  color: #ffe1a8; background: rgba(0,0,0,0.34);
  border: 1px solid rgba(255,225,168,0.34);
}
.gcard-pub .ic svg { width: 3.6cqw; height: 3.6cqw; display: block; }
.gcard-name {
  font-weight: 700; font-size: 6cqw; line-height: 1.15;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.gcard-art {
  flex: 1; min-height: 0; overflow: hidden;
  border-top: 1px solid rgba(0,0,0,0.5);
  border-bottom: 1px solid rgba(0,0,0,0.5);
}
.gcard-art svg { width: 100%; height: 100%; display: block; }
.gcard-desc {
  padding: 3.6cqw 4.6cqw 4.4cqw;
  color: var(--text-dim); font-size: 4.4cqw; line-height: 1.3;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* A colour-matched action bar flush across the foot of the card face. It clips
   to the inner's rounded corners and is tinted with the card's own accent, so
   the five icons read as built into the card rather than floating over it. */
.tile-actions {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; justify-content: space-around; align-items: center; gap: 2px;
  padding: 4px 5px;
  background: linear-gradient(0deg,
    color-mix(in srgb, var(--tile-accent) 90%, #06080a),
    color-mix(in srgb, var(--tile-accent) 68%, #06080a));
  border-top: 1px solid color-mix(in srgb, var(--tile-accent) 55%, #ffffff 22%);
  opacity: 0; transform: translateY(6px); transition: opacity 0.16s, transform 0.16s;
}
.game-tile:hover .tile-actions { opacity: 1; transform: none; }
/* Seven icons have to fit the narrowest card in the fan (150px), so the bar
   runs a touch tighter than the app's usual icon button. */
.tile-actions .btn.btn-icon { padding: 3px; border-radius: 6px; }
.tile-actions .btn-icon svg { width: 12px; height: 12px; }
.tile-actions .btn-ghost { color: rgba(255, 255, 255, 0.92); }
.tile-actions .btn-ghost:hover { color: #fff; background: rgba(255, 255, 255, 0.2); border-color: transparent; }
/* A lit action says the game is out in the world (the publish button). */
.tile-actions .btn-ghost.on { color: #ffe1a8; background: rgba(0, 0, 0, 0.28); }
.tile-new {
  background: transparent;
  border: 1.5px dashed var(--line-2);
  box-shadow: none;
  color: var(--text-dim);
}
.tile-new:hover { border-color: var(--accent); color: var(--accent-2); transform: translateY(-4px); box-shadow: none; }
.tile-new-face {
  height: 100%;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px;
}

/* ---------- My games as a fanned hand ----------
   Same construction as the main-menu hand: every card is a stationary .game-slot
   rotated around one shared pivot far below the hand, so bottoms converge and
   tops splay outward. The card inside lifts on hover; the slot holds the hover
   so the lift can't slide the card out from under the cursor. */
.game-hand { position: relative; width: 100%; max-width: 1040px; height: 360px; margin: 30px auto 0; }
.game-slot {
  position: absolute; left: 50%; top: 24px;
  width: 176px; height: 246px;
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  transform: translateX(-50%) rotate(var(--rot, 0deg));
  transform-origin: 50% 600px;
}
.game-slot:hover { z-index: 200 !important; }
/* Dealt in from the pivot, upright (the slot's fan rotation is cancelled at the
   start), then sliding out into the splayed spot - reuses the menu's keyframe. */
.game-riser { animation: menuDeal 0.46s cubic-bezier(0.22, 0.85, 0.28, 1.02) both; }
.game-slot .game-tile {
  width: 176px; margin: 0;
  transform-origin: center bottom; /* fan rotation lives on the slot; the card only lifts */
  transition: transform 0.34s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.34s cubic-bezier(0.4, 0, 0.2, 1), filter 0.34s;
}
.game-slot .game-tile:hover { transform: none; } /* the slot drives the lift, not the tile's own hover */
.game-slot:hover .game-tile {
  transform: translateY(-34px) scale(1.05);
  box-shadow: 0 34px 54px rgba(0,0,0,0.6);
  filter: brightness(1.04);
}
.game-slot:hover .tile-new { box-shadow: none; }
@media (max-width: 640px) {
  .game-hand { height: 320px; }
  .game-slot, .game-slot .game-tile { width: 150px; }
  .game-slot { height: 210px; }
}

/* Multi-select in My games */
.select-bar { display: flex; align-items: center; gap: 10px; }
.select-count { color: var(--text-dim); font-size: 14px; }
.grid.selecting { position: relative; user-select: none; }
.game-tile.selectable:hover { transform: none; }
.game-tile.selected { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.tile-check {
  position: absolute; top: 12px; right: 12px;
  width: 24px; height: 24px; border-radius: 6px;
  border: 1.5px solid var(--line-2); background: var(--bg-2);
  display: flex; align-items: center; justify-content: center;
  color: #fff;
}
.tile-check svg { width: 16px; height: 16px; opacity: 0; }
.game-tile.selected .tile-check { background: var(--accent); border-color: var(--accent); }
.game-tile.selected .tile-check svg { opacity: 1; }
.marquee-box {
  position: absolute; z-index: 5; pointer-events: none;
  border: 1px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border-radius: 3px;
}
.marquee-box.subtract {
  border-color: var(--danger, #d6604d);
  background: color-mix(in srgb, var(--danger, #d6604d) 18%, transparent);
}
.tile-new svg { width: 30px; height: 30px; }

.empty {
  text-align: center; padding: 60px 20px; color: var(--text-dim);
  border: 1px dashed var(--line); border-radius: var(--radius); background: rgba(255,255,255,0.01);
}
.empty svg { width: 40px; height: 40px; color: var(--text-faint); margin-bottom: 12px; }

/* ---------- Forms ---------- */
.panel {
  background: linear-gradient(180deg, var(--panel), var(--bg-2));
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 20px;
}
.form-row { margin-bottom: 16px; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 16px; }
label.field-label {
  display: block; font-size: 12.5px; color: var(--text-dim);
  margin-bottom: 6px; font-weight: 600; letter-spacing: 0.2px;
}
input[type="text"], input[type="number"], textarea, select {
  width: 100%;
  background: var(--bg);
  border: 1px solid var(--line-2);
  border-radius: var(--radius-sm);
  color: var(--text);
  padding: 9px 11px;
  font-family: inherit;
  font-size: 14px;
}
input:focus, textarea:focus, select:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(216, 162, 74, 0.15);
}
/* ---------- Search fields: one pattern for every search box ----------
   A search box is an input with a magnifier pinned inside its left edge, so the
   typed text needs a gutter wide enough to clear that icon. The gutter lives
   HERE, on the wrapper, and never on a field's own class. Reason: a bare class
   selector (.my-search, specificity 0-1-0) LOSES to the global input[type="text"]
   rule above (0-1-1), so its padding-left is silently dropped and the placeholder
   prints on top of the magnifier. That is the bug that shipped on the card
   gallery's "Search cards" field. Every selector below carries an attribute or a
   second class of its own, so it out-specifies the global rule whatever a field
   is called.

   Build one with TCG.searchField() (js/ui.js) rather than by hand. Two sizes:
   the default, and .search-sm for pill-scale surfaces (a palette, a flyout). */
.search-wrap { position: relative; display: flex; align-items: center; }
.search-wrap > .search-ic { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); display: inline-flex; color: var(--text-faint); pointer-events: none; }
.search-wrap > .search-ic svg { width: 15px; height: 15px; }
.search-wrap > input, .search-wrap > input[type="text"], .search-wrap > input[type="search"] { box-sizing: border-box; width: 100%; padding-left: 34px; }
.search-wrap.search-sm > .search-ic { left: 8px; }
.search-wrap.search-sm > .search-ic svg { width: 13px; height: 13px; }
.search-wrap.search-sm > input, .search-wrap.search-sm > input[type="text"], .search-wrap.search-sm > input[type="search"] { padding-left: 30px; }

textarea { resize: vertical; min-height: 72px; }
.hint { color: var(--text-faint); font-size: 12px; margin-top: 5px; }
.inline { display: flex; gap: 10px; align-items: center; }
.checkbox { display: inline-flex; align-items: center; gap: 9px; cursor: pointer; user-select: none; }
.checkbox input { width: 16px; height: 16px; accent-color: var(--accent); }

/* phases editor */
.chip-list { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--panel-2); border: 1px solid var(--line-2);
  border-radius: 999px; padding: 6px 8px 6px 12px; font-size: 13px;
}
.chip button { background: none; border: none; color: var(--text-faint); display: inline-flex; padding: 2px; border-radius: 50%; }
.chip button:hover { color: var(--red); }
.chip svg { width: 14px; height: 14px; }

/* ---------- Editor layout (card creator) ---------- */
.editor-layout { display: grid; grid-template-columns: 1fr 360px; gap: 26px; align-items: start; }
@media (max-width: 900px) { .editor-layout { grid-template-columns: 1fr; } }
.preview-rail { position: sticky; top: 78px; }

/* ---------- Card visual ---------- */
/* Cards are a query container: every inner size is written in cqw (1cqw = 1% of
   the card's width), so the whole face, the center number, icons, and badges
   scale together at any display width, from a tiny table card to the big
   right-click inspector. Reference design width is 240px, so a px value of N
   from the old design becomes (N / 2.4)cqw. */
/* Floating enlarged copy shown while hovering a small card. Sits above
   everything, never eats the pointer, and fades in so it doesn't flicker
   as the mouse crosses a row of cards. */
#card-hover-preview {
  position: fixed;
  z-index: 9999;
  pointer-events: none;
  opacity: 0;
  transform: scale(0.96);
  transition: opacity 0.09s ease, transform 0.09s ease;
  filter: drop-shadow(0 18px 40px rgba(0, 0, 0, 0.55));
}
#card-hover-preview.show { opacity: 1; transform: scale(1); }
#card-hover-preview .tcard { box-shadow: none; }

.tcard {
  --card-accent: var(--accent);
  /* Card shadows live in cqw (1% of the card's own width), not the app-wide px
     --shadow vars, so the drop shadow and every badge shadow grow with the card
     instead of staying a fixed pixel size that looks huge on a felt card and
     tight on the enlarged one. Reference width 240px: the old 8px/30px becomes
     3.33cqw/12.5cqw, the old 2px/10px becomes 0.83cqw/4.17cqw. */
  --card-shadow: 0 3.33cqw 12.5cqw rgba(0, 0, 0, 0.45);
  --card-shadow-sm: 0 0.83cqw 4.17cqw rgba(0, 0, 0, 0.35);
  container-type: inline-size;
  width: 240px;
  aspect-ratio: var(--card-aspect, 5 / 7);
  border-radius: 6.7% / 4.8%;
  background:
    linear-gradient(180deg, rgba(255,255,255,0.05), rgba(0,0,0,0.25)),
    var(--card-accent);
  padding: var(--card-pad, 1.7%);
  display: flex; flex-direction: column;
  box-shadow: var(--card-shadow);
  position: relative;
  color: #f4f1e8;
  /* No outer border: a fixed-px border can't scale with the card (a container
     query length is circular on its own container), so it left the frame
     proportionally thicker on a small card than on an enlarged one. The grey
     padding frame and the drop shadow define the edge, and both scale. */
  border: none;
  user-select: none;
}
.tcard-inner {
  flex: 1; display: flex; flex-direction: column;
  background: #14171d;
  border-radius: 4.2cqw;
  border: 0.42cqw solid rgba(0,0,0,0.5);
  overflow: hidden;
}
.tcard-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 3.3cqw; padding: 3.3cqw 4.2cqw;
  background: linear-gradient(180deg, rgba(255,255,255,0.06), transparent);
}
.tcard-name { font-weight: 700; font-size: 6.25cqw; line-height: 1.15; }
.tcard-cost {
  min-width: 10.8cqw; height: 10.8cqw; padding: 0 2.5cqw;
  border-radius: 50%;
  background: var(--blue);
  color: #fff; font-weight: 800; font-size: 5.83cqw;
  display: inline-flex; align-items: center; justify-content: center;
  box-shadow: var(--card-shadow-sm);
  flex-shrink: 0;
}
.tcard-art {
  flex: 1; min-height: 0;
  background: #0b0d11;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
  border-top: 0.42cqw solid rgba(255,255,255,0.05);
  border-bottom: 0.42cqw solid rgba(0,0,0,0.5);
}
.tcard-art img { width: 100%; height: 100%; object-fit: cover; }
.tcard-art .art-empty { color: var(--text-faint); display: flex; flex-direction: column; align-items: center; gap: 2.5cqw; font-size: 4.6cqw; }
.tcard-art .art-empty svg { width: 10.8cqw; height: 10.8cqw; }
.tcard-type {
  font-size: 4.6cqw; text-transform: uppercase; letter-spacing: 0.4cqw;
  color: #d7c9a6; text-align: center; padding: 2.1cqw 3.3cqw;
  background: rgba(0,0,0,0.35);
  border-bottom: 0.42cqw solid rgba(255,255,255,0.04);
}
.tcard-text {
  flex: 0 0 auto; min-height: 23.3cqw; padding: 3.75cqw 4.6cqw;
  font-size: 4.8cqw; line-height: 1.35; color: #e7e2d4;
  background: #181b22;
  overflow: auto;
}
.tcard-text .empty-text { color: var(--text-faint); font-style: italic; }
.tcard-stats { display: flex; justify-content: space-between; padding: 0 1.7cqw; margin-top: 3.3cqw; }
.stat-badge {
  min-width: 12.5cqw; height: 12.5cqw; padding: 0 2.9cqw;
  border-radius: 3.3cqw;
  display: inline-flex; align-items: center; justify-content: center;
  font-weight: 800; font-size: 6.67cqw;
  box-shadow: var(--card-shadow-sm);
  gap: 1.7cqw;
}
.stat-badge svg { width: var(--bw, 5.83cqw); height: var(--bw, 5.83cqw); }
.stat-atk { background: #c8923a; color: #221703; }
.stat-hp { background: var(--red); color: #2a0c07; }
.tcard.is-standard .tcard-inner { background: #f5f1e6; color: #1a1a1a; }
.tcard-suit {
  flex: 1; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 1.7cqw;
}
.tcard-suit .rank-lg { font-size: 23.3cqw; font-weight: 800; line-height: 1; }
.tcard-suit svg { width: 19.2cqw; height: 19.2cqw; }
.tcard-corner { position: absolute; font-weight: 800; font-size: 6.25cqw; display: flex; flex-direction: column; align-items: center; line-height: 1; }
.tcard-corner svg { width: 5.83cqw; height: 5.83cqw; margin-top: 0.8cqw; }
.tcard-corner.tl { top: 5.83cqw; left: 5.83cqw; }
.tcard-corner.br { bottom: 5.83cqw; right: 5.83cqw; transform: rotate(180deg); }
.suit-red { color: #c0392b; }
.suit-black { color: #1a1a1a; }

/* numbered (Uno-style) cards: solid color face, big centered number */
.tcard.is-numbered .tcard-inner {
  background: var(--num-color, #444);
  color: #fff;
  align-items: center; justify-content: center;
}
.tcard.is-numbered .num-face {
  flex: 1; display: flex; align-items: center; justify-content: center;
  width: 100%;
  background: radial-gradient(circle at 50% 40%, rgba(255,255,255,0.18), transparent 60%);
}
.tcard.is-numbered .num-lg {
  font-size: 26.7cqw; font-weight: 800; line-height: 1;
  text-shadow: 0 2px 6px rgba(0,0,0,0.3);
}
.tcard-corner.num { color: #fff; font-size: 7.08cqw; text-shadow: 0 1px 3px rgba(0,0,0,0.35); }
/* a numbered card with artwork: the art (position:absolute, z-index 0) sits
   behind the printed value the same way a freeform card's art does - see
   .tcard-free.has-art below for the same pattern. */
.tcard.is-numbered.has-art .num-face { position: relative; z-index: 2; background: none; }
.tcard.is-numbered.has-art .tcard-corner { z-index: 2; }

/* fully borderless card: no printed frame at all, just the artwork edge to
   edge - Punto's square card scans are already a complete piece of art and
   don't want the usual accent-colored frame ring around them. */
.tcard.no-frame { --card-pad: 0%; border: none; }
.tcard.no-frame .tcard-inner { border: none; }

/* freeform cards: every element is free-floating on a positioning canvas */
/* Freeform inner never clips its own children, in the editor OR at the table,
   so a value badge, name, or number placed at a corner overhangs the edge in
   full (a Hearthstone-style gem sitting on the frame) instead of being sliced
   off the way overflow:hidden would. Background layers that DO need to clip to
   the rounded face keep their own clip: art/bg self-clip (below), and shapes
   ride in the .free-shapes layer. */
.tcard-free-inner { position: relative; display: block; overflow: visible; }
/* No picture on a card is ever the browser's to drag. An <img> is draggable by
   default: press and move on one and the browser starts its own drag-and-drop,
   paints a translucent ghost of the picture, and STOPS delivering pointermove -
   which silently kills any pointer drag the app is running on top of it (it
   froze the card editor's drag-to-pan about six pixels in). renderCard also
   sets the draggable attribute on every card image, because this property is a
   WebKit/Blink one and Firefox only honours the attribute. Both halves, always. */
.tcard img, .ce-card img { -webkit-user-drag: none; user-drag: none; }
/* uploaded artwork fills the card face, behind every element */
.free-art { position: absolute; inset: 0; z-index: 0; overflow: hidden; border-radius: inherit; background: #0b0d11; }
/* When the card has its own colour, that colour is the art backdrop: transparent
   parts of the artwork (a drawing's unpainted areas, a PNG's transparent
   corners, contain-fit letterbox margins) show the card colour, not a black
   matte. Without a colour, the dark matte stays as the neutral default. */
.tcard-free.has-color .free-art { background: transparent; }
.free-art img { width: 100%; height: 100%; display: block; }
.free-art.fit-cover img { object-fit: cover; }
.free-art.fit-contain img { object-fit: contain; }
.tcard-free.has-art .free-el, .tcard-free.has-art .free-fx { z-index: 2; }
/* full-card background image: behind the artwork and everything else. z-index 0
   (same layer as art, appended before it so it paints under); a negative z-index
   would fall behind the inner panel's own opaque background and vanish. The inner
   background is cleared whenever a bg image exists so the texture isn't hidden. */
.free-bg { position: absolute; inset: 0; z-index: 0; overflow: hidden; border-radius: inherit; background: #0b0d11; }
.free-bg img { width: 100%; height: 100%; display: block; }
.free-bg.fit-cover img { object-fit: cover; }
.free-bg.fit-contain img { object-fit: contain; }
.tcard-free.has-bg .tcard-free-inner { background: transparent; }
.tcard-free.has-bg .free-art { background: transparent; }
.tcard-free.has-bg .free-el, .tcard-free.has-bg .free-fx { z-index: 2; }
/* a soft readability wash so text/badges stay legible over any art */
.tcard-free.has-art .tcard-free-inner::before {
  content: ""; position: absolute; inset: 0; z-index: 1; border-radius: inherit; pointer-events: none;
  background: linear-gradient(180deg, rgba(0,0,0,0.16), rgba(0,0,0,0) 30%, rgba(0,0,0,0) 62%, rgba(0,0,0,0.32));
}
/* ...unless the card turns it off (card.artWash false, the Artwork panel's
   Readability wash toggle): a poster-flat frame keeps its inks untouched. The
   wash darkened the colour block's sand band 22-30% and no card control could
   say otherwise - a hardcoded overlay was a knob that didn't exist. */
.tcard-free.no-art-wash .tcard-free-inner::before { content: none; }
/* card face fill: gradient overlay (default, richer) or flat solid color */
.tcard-free.has-color.fill-gradient .tcard-free-inner {
  background: linear-gradient(180deg, rgba(255,255,255,0.16), rgba(0,0,0,0.28)), var(--free-color, #444);
  color: #fff;
}
.tcard-free.has-color.fill-solid .tcard-free-inner {
  background: var(--free-color, #444);
  color: #fff;
}
/* legacy cards saved before fill choice existed: keep the gradient look */
.tcard-free.has-color:not(.fill-gradient):not(.fill-solid) .tcard-free-inner {
  background: linear-gradient(180deg, rgba(255,255,255,0.16), rgba(0,0,0,0.28)), var(--free-color, #444);
  color: #fff;
}
.free-el { position: absolute; transform: translate(-50%, -50%); }
/* decorative background shapes: above the art, behind every value/icon/text.
   pointer-events off in play so they never intercept a card click; the editor
   re-enables them (see .ce-card .free-shape). */
/* Shapes ride in their own inset layer, sized to the card face, that clips at
   the table so an oversized background panel trims to the rounded edge (the
   inner itself no longer clips - see .tcard-free-inner).
   The editor picks which of those two it shows, from the Shapes panel's
   "Trim to card edge" toggle (pref cardTrim, on by default):
   - .ce-trim on: clip exactly like the table, so a 102%-wide set band reads as
     the full-bleed stripe it prints as, without leaving the editor.
   - off: leave the layer unclipped, so a shape dragged partly off-card stays
     visible and its geometry is readable while you place it.
   With the trim on, a corner handle sitting past the card edge would be clipped
   too, so bindShapes pulls it back inside - see placeHandle in cardEditor.js. */
.free-shapes { position: absolute; inset: 0; z-index: 1; overflow: hidden; border-radius: inherit; pointer-events: none; }
.ce-card .free-shapes { overflow: visible; }
.ce-card.ce-trim .free-shapes { overflow: hidden; }
.free-shape { position: absolute; transform: translate(-50%, -50%); box-sizing: border-box; pointer-events: none; z-index: 1; }
.free-shape.shape-circle { border-radius: 50%; }
/* Polygon kinds (diamond/triangle/hexagon/banner) render an inline SVG so they
   can stroke their own edge; the old faint-fill .shape-outline-clip fallback is
   retired (ui.js freeShapeEl). The svg fills the positioned box. */
.free-shape > svg { width: 100%; height: 100%; display: block; }
/* procedural surface textures (ui.js textureUrl): a grayscale turbulence tile,
   overlay-blended over the face or a shape so the colour underneath keeps
   reading through and only picks up tooth. Sized in cqw so the grain scales
   with the card instead of shimmering at different render sizes. */
.free-texture {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  border-radius: inherit; overflow: hidden;
  background-size: 46cqw 46cqw; mix-blend-mode: overlay;
}
.shape-texture {
  position: absolute; inset: 0; pointer-events: none;
  border-radius: inherit; overflow: hidden;
  background-size: 46cqw 46cqw; mix-blend-mode: overlay;
}
/* art window (ui.js): an inset opening with a directional metal-gradient
   frame; the inner shadow rides an ::after overlay so it paints ABOVE the
   image (an inset box-shadow on the container would hide behind it) */
.free-artwin { position: absolute; transform: translate(-50%, -50%); z-index: 0; }
/* The frame's lift is written inline from the window's own Depth, so depth 0
   really is flat (see renderFreeform). */
.free-artwin-frame { position: relative; width: 100%; height: 100%; box-sizing: border-box; }
.free-artwin-art { position: relative; width: 100%; height: 100%; overflow: hidden; background: #0b0d11; }
.free-artwin-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.free-artwin-art.fit-contain img { object-fit: contain; }
.free-artwin-art::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  box-shadow: var(--artwin-shadow, inset 0 0 0 1px rgba(0,0,0,0.55), inset 0 1cqw 1.6cqw rgba(0,0,0,0.34));
}
/* cell row (ui.js): the footer-grid primitive - equal columns, micro labels,
   hairline dividers that follow the row's own ink colour */
.free-cellrow { display: grid; align-items: stretch; line-height: 1.2; color: #e8e2d2; }
.free-cellrow-cell { display: flex; align-items: center; justify-content: center; gap: 0.9cqw; padding: 0.7cqw 1.2cqw; min-height: 5cqw; }
.free-cellrow-cell.has-div { border-left: 0.2cqw solid color-mix(in srgb, currentColor 30%, transparent); }
.free-cellrow-label { letter-spacing: 0.05em; opacity: 0.75; }
.free-cellrow-val { font-weight: 700; font-variant-numeric: tabular-nums; }
.free-cellrow-ic { display: inline-flex; align-items: center; justify-content: center; width: 1.5em; height: 1.5em; flex: 0 0 auto; }
.free-cellrow-ic svg { width: 78%; height: 78%; }
.free-cellrow-ic.has-chip { border-radius: 50%; border: 0.09em solid; box-shadow: inset 0 0.1em 0.14em rgba(255,255,255,0.4), inset 0 -0.1em 0.14em rgba(0,0,0,0.35); }
/* layout bands: editor-only dashed guides at each band boundary; dragged
   elements snap to band centres and edges (cardEditor snapCardY) */
.ce-band-guides { position: absolute; inset: 0; pointer-events: none; z-index: 5; }
.ce-band-guide { position: absolute; left: 0; right: 0; height: 0; border-top: 1px dashed rgba(126, 200, 255, 0.45); }
/* alignment guides: the card-editor twin of the game editor's .snap-guide, drawn
   while an element is dragged or resized when one of its edges (or its centre)
   lands on another element's edge or centre, or on the card's own. Same gold
   hairline, and drawn ABOVE the dragged element (.ce-dragging is z-index 5) so
   the line the element just landed on stays visible under it. A band guide is
   dashed and blue because a band is a rule the card carries; an alignment guide
   is solid and gold because it is live feedback that disappears on release. */
.ce-align-guides { position: absolute; inset: 0; pointer-events: none; z-index: 8; }
.ce-align-guide {
  position: absolute;
  background: var(--accent);
  box-shadow: 0 0 5px var(--accent), 0 0 1px var(--accent);
}
.ce-align-guide.ce-align-v { top: -6%; bottom: -6%; width: 1px; }
.ce-align-guide.ce-align-h { left: -6%; right: -6%; height: 1px; }
/* vignette (ui.js): radial darkening from past the middle to the edge, over
   the face and panels but under every element; opacity = its strength */
.free-vignette {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  border-radius: inherit;
  background: radial-gradient(118% 92% at 50% 44%, rgba(0,0,0,0) 52%, rgba(0,0,0,0.5) 100%);
}
/* foil (ui.js foilLayerEl): printed holo shards + a rainbow wash, colour-
   dodged over whatever it covers - the whole face (card.foil) or one shape
   (sh.foil). The sweep only runs while the card is hovered, so a table of
   foils stays still until looked at. */
.free-foil {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  border-radius: inherit;
  background:
    repeating-linear-gradient(115deg,
      rgba(255,255,255,0) 0, rgba(255,255,255,0.42) 1.2cqw,
      rgba(255,255,255,0) 2.8cqw, rgba(255,255,255,0.16) 4.4cqw,
      rgba(255,255,255,0) 6cqw),
    linear-gradient(115deg,
      rgba(255,120,255,0.22), rgba(120,220,255,0.22) 30%,
      rgba(255,255,160,0.22) 55%, rgba(160,255,200,0.22) 80%, rgba(255,150,220,0.22));
  background-size: 200% 200%, 260% 260%;
  mix-blend-mode: color-dodge;
}
/* The sweep animates on hover. The wash never does - it's driven by the
   pointer, not by a clock - so it's excluded here rather than having to switch
   the animation off again below. */
.tcard:hover .free-foil:not(.foil-wash):not(.foil-glare) { animation: foil-sweep 5.5s linear infinite; }
@keyframes foil-sweep {
  0% { background-position: 0% 0%, 0% 50%; }
  100% { background-position: 200% 200%, 260% 50%; }
}

/* ---- foil style "wash" (ui.js foilLayerEls + js/foil.js) -------------------
   A spectrum wheel anchored to the pointer, and a soft highlight that slides
   the opposite way so the two separate as the cursor crosses. That parallax is
   what makes it read as light on a surface instead of a gradient sliding about.

   Every var here has a resting fallback, so a card renders correctly with no
   JS at all (the printed card, a screenshot, a test in jsdom): --live 0 means
   no highlight and a dim spectrum sitting at the card's centre. js/foil.js
   only ever raises it.

     --px / --py   pointer position inside THIS layer, as a percentage
     --mx / --my   the same, re-centred to -1..1
     --live        0 at rest, 1 while the card is under the pointer
     --foil-k      the card's foil strength, for the layer that can't use
                   inline opacity because it has to multiply by --live
   ------------------------------------------------------------------------- */
.free-foil.foil-wash {
  /* One alpha for all six stops, lifted while live so the colour comes up with
     the light instead of sitting at full strength on an untouched card. */
  --fa: calc(0.3 * (0.55 + var(--live, 0) * 0.45));
  background: conic-gradient(from calc(var(--mx, 0) * 60deg) at var(--px, 50%) var(--py, 50%),
    rgba(255,110,170,var(--fa)), rgba(255,215,120,var(--fa)) 18%,
    rgba(130,245,190,var(--fa)) 38%, rgba(120,190,255,var(--fa)) 58%,
    rgba(205,130,255,var(--fa)) 78%, rgba(255,110,170,var(--fa)));
  background-size: auto;
  mix-blend-mode: color-dodge;
}
/* The highlight: wide, low and feathered, with no hot core - the card glows
   rather than flashing. Offset against the spectrum by a fraction of the
   pointer's own travel, which is the parallax. */
.free-foil.foil-glare {
  background: radial-gradient(98% 98% at
    calc(var(--px, 50%) - var(--mx, 0) * 6%)
    calc(var(--py, 50%) - var(--my, 0) * 6%),
    rgba(255,255,255,0.30), rgba(255,255,255,0.13) 44%, transparent 74%);
  background-size: auto;
  mix-blend-mode: screen;
  /* A true zero at rest, so every card that isn't being pointed at costs the
     compositor nothing at all for this layer. */
  opacity: calc(var(--foil-k, 0) * var(--live, 0));
  transition: opacity 200ms ease;
}
/* While the pointer is actually on the card the fade is off, so the highlight
   tracks 1:1 instead of lagging a fifth of a second behind the cursor. */
.tcard.foil-live .free-foil.foil-glare { transition: none; }
/* keep values, icons and effect text above any background shape, on art or not */
.tcard-free .free-el, .tcard-free .free-fx { z-index: 2; }
/* width:max-content is what makes max-width mean anything. A free element is
   absolutely positioned at left:x% and pulled back by a -50% translate, so the
   shrink-to-fit box the browser offers is only (100-x)% of the face wide - half
   of it for a centred name, a sliver for one parked near an edge - and the name
   broke there rather than at its stated limit. max-content asks for the text's
   own width; max-width is then the real cap. It is in cqw (1% of the card's
   inner face) so js/ui.js can name the same number as NAME_BOX_CQW. */
.free-name { font-weight: 700; font-size: 5.83cqw; line-height: 1.15; width: max-content; max-width: 78cqw; text-align: center; text-shadow: 0 2px 6px rgba(0,0,0,0.35); }
.free-name.no-shadow { text-shadow: none; }
.free-name.grad-text { -webkit-background-clip: text; background-clip: text; color: transparent; text-shadow: none; }
.free-number { font-size: 27.5cqw; font-weight: 800; line-height: 1; text-shadow: 0 2px 6px rgba(0,0,0,0.35); }
.free-number.no-shadow { text-shadow: none; }
.free-number.grad-text { -webkit-background-clip: text; background-clip: text; color: transparent; text-shadow: none; }
.free-suit { display: flex; color: #efe7d4; }
.free-suit svg { width: var(--sw, 23.3cqw); height: var(--sw, 23.3cqw); display: block; }
.free-suit.has-shadow svg { filter: drop-shadow(0 2px 5px rgba(0,0,0,0.4)); }
.free-suit-pip { display: inline-flex; }
/* multiple pips (playing-card style): wrap into a centered block that stays
   roughly where a single glyph sat */
.free-suit.multi { flex-wrap: wrap; gap: 1.4cqw; max-width: 44cqw; justify-content: center; align-items: center; }
.free-suit.suit-red { color: #e2574a; }
/* flavor/description text - a draggable box, same as name but multi-line */
.free-text {
  font-weight: 500; font-size: 4.4cqw; line-height: 1.3; max-width: 76%;
  text-align: center; white-space: pre-wrap; color: #f3efe4;
  text-shadow: 0 1px 3px rgba(0,0,0,0.5);
}
.free-text.no-shadow { text-shadow: none; }
.free-text-flavor { font-style: italic; }
.free-cost { box-shadow: var(--card-shadow-sm); }
/* named category pill - category-count and categorical-majority win rules
   match cards by this name */
.free-category {
  display: inline-flex; align-items: center; gap: 1.4cqw;
  background: rgba(12, 16, 22, 0.55); color: #f3efe4;
  font-size: 4cqw; font-weight: 650; line-height: 1;
  padding: 1.4cqw 2.6cqw; border-radius: 999px; white-space: nowrap;
  box-shadow: var(--card-shadow-sm);
}
.free-category.no-shadow { box-shadow: none; }
/* Type-line mode: no plate, no padding, just the words. */
.free-category.cat-plain { background: none; padding: 0; box-shadow: none; }
.free-category-dot { width: 2.4cqw; height: 2.4cqw; min-width: 5px; min-height: 5px; border-radius: 50%; background: var(--cat-color, #8a949f); }
/* icon marker sizes off the pill's own font (em) so it scales with the
   category Size control, and takes its colour from --cat-color via currentColor */
.free-category-icon { display: inline-flex; color: var(--cat-color, #8a949f); }
.free-category-icon svg { width: 1.25em; height: 1.25em; display: block; }
/* purely decorative icon element: an outlined-SVG glyph placed on the face for
   looks, no rules attached. Sized off its own font-size (set inline from the
   element's size), tinted by its own colour (default light glyph). */
.free-icon { display: inline-flex; color: #f3efe4; filter: drop-shadow(0 2px 6px rgba(0,0,0,0.35)); }
.free-icon.no-shadow { filter: none; }
.free-icon svg { width: 1em; height: 1em; display: block; }

/* ---- card slots: a place on the face that a real piece rests in ----------
   Everything here is in em off the element's own font-size (set in cqw by the
   renderer), so one Size row scales the well, its ring, its ghost numeral and
   its label together, and all of it tracks the card at any print size.
   The WELL's shape says which kind of piece it takes - a rounded square for a
   die, a disc for tokens, a card-shaped rectangle for a card - so an empty slot
   already reads as "something goes here, and it is that shape". */
.free-slot { display: flex; flex-direction: column; align-items: center; gap: 0.18em; pointer-events: none; }
.free-slot-well {
  width: 2em; height: 2em; box-sizing: border-box;
  display: flex; align-items: center; justify-content: center;
  border: 0.13em dashed rgba(255,255,255,0.62);
  border-radius: 0.28em;
  background: rgba(0,0,0,0.22);
  box-shadow: inset 0 0.08em 0.2em rgba(0,0,0,0.45);
}
.free-slot.slot-coin .free-slot-well { border-radius: 50%; }
.free-slot.slot-card .free-slot-well { width: 1.72em; height: 2.4em; border-radius: 0.16em; }
.free-slot.slot-any .free-slot-well { border-style: dotted; }
.free-slot-mark {
  font-weight: 800; font-size: 0.92em; line-height: 1;
  color: rgba(255,255,255,0.8); letter-spacing: -0.02em;
}
.free-slot-label {
  font-size: 0.42em; line-height: 1; text-transform: uppercase; letter-spacing: 0.08em;
  color: rgba(255,255,255,0.72); max-width: 5.5em; text-align: center;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A slot holding a piece stops advertising: the felt puts the real die or token
   on top of it, and a dashed ring under a solid piece reads as a mistake. The
   table adds this class to the host card's rendered face. */
.free-slot.slot-filled .free-slot-well { border-color: rgba(255,255,255,0.14); background: none; box-shadow: none; }
.free-slot.slot-filled .free-slot-mark { opacity: 0; }
/* chip backing: the glyph on its own coloured disc with a darker ring and a
   bevelled face - a self-contained symbol token (energy / resource mark).
   Ring + bevel are in em so they track the icon's own size slider. */
.free-icon.has-chip {
  border-radius: 50%;
  border: 0.09em solid;
  padding: 0.17em;
  /* bevel only, no outer drop - a token shouldn't shade the face under it */
  box-shadow: inset 0 0.1em 0.16em rgba(255,255,255,0.4), inset 0 -0.1em 0.16em rgba(0,0,0,0.35);
}
/* gloss: a glass highlight across a badge's upper half - the polished-gem look.
   Clipped shapes clip the highlight with them, so it follows the gem's cut.
   The ::after needs a positioned host, but that must NEVER override the
   absolute positioning .free-el uses to centre-anchor an on-card badge -
   `position: relative` here once beat it on specificity and sent every
   glossed gem drifting half its size past the card edge. */
.free-badge.gloss { position: relative; }
.free-el.free-badge.gloss { position: absolute; }
.free-badge.gloss::after {
  content: ""; position: absolute; pointer-events: none;
  left: 9%; right: 9%; top: 5%; height: 44%;
  border-radius: 50%;
  background: linear-gradient(rgba(255,255,255,0.5), rgba(255,255,255,0.05));
}
.stat-def { background: #3f7cc2; color: #04122a; }

/* value badges in shapes. --bstretch (default 1) widens the box only, so width
   scales independently of the badge's height. */
.free-badge { --bsize: 12.5cqw; box-shadow: var(--card-shadow-sm); }
.free-badge.no-shadow { box-shadow: none; }
.free-badge.shape-circle, .free-badge.shape-square, .free-badge.shape-diamond,
.free-badge.shape-hexagon, .free-badge.shape-triangle, .free-badge.shape-pentagon,
.free-badge.shape-none { min-width: 0; }
.free-badge-content { display: inline-flex; align-items: center; justify-content: center; gap: 1.7cqw; line-height: 1; }
.free-badge-content svg { width: var(--bw, 5.83cqw); height: var(--bw, 5.83cqw); }
/* Renamed stat word (Names pill): a tiny caption wrapping below the icon+number
   inside the badge, so a player reads the game's own word ("Power") on the face.
   Only present when the game set an override, so a default card is unaffected. */
.stat-badge.has-term .free-badge-content, .stat-badge.has-term { flex-wrap: wrap; height: auto; }
.free-badge-term { flex: 0 0 100%; text-align: center; font-size: 0.44em; line-height: 1.05; margin-top: 0.4cqw; letter-spacing: 0.02em; text-transform: uppercase; opacity: 0.92; font-weight: 700; }
/* Icon-count mode: the badge grows to wrap a row/column of glyphs instead of a
   fixed number box, so a horizontal row widens it and a vertical one heightens
   it, in either shape. */
.free-badge.icons-mode { width: auto; height: auto; min-width: var(--bsize); min-height: var(--bsize); padding: 1.6cqw 2.4cqw; }
.free-badge-icons { display: inline-flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 1.2cqw; max-width: 40cqw; }
.free-badge-icons.dir-col { flex-direction: column; max-width: none; max-height: 46cqw; }
.free-badge-pip { display: inline-flex; }
.free-badge-pip svg { width: var(--pipw, 5.83cqw); height: var(--pipw, 5.83cqw); display: block; }
.free-badge.shape-rounded { min-width: calc(var(--bsize) * var(--bstretch, 1)); height: var(--bsize); padding: 0 2.9cqw; border-radius: 3.3cqw; }
.free-badge.shape-circle { width: calc(var(--bsize) * var(--bstretch, 1)); height: var(--bsize); padding: 0; border-radius: 50%; }
.free-badge.shape-square { width: calc(var(--bsize) * var(--bstretch, 1)); height: var(--bsize); padding: 0; border-radius: 2.1cqw; }
.free-badge.shape-diamond {
  width: calc(var(--bsize) * 1.3 * var(--bstretch, 1)); height: calc(var(--bsize) * 1.3); padding: 0; border-radius: 1.7cqw;
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}
.free-badge.shape-hexagon {
  width: calc(var(--bsize) * 1.26 * var(--bstretch, 1)); height: calc(var(--bsize) * 1.16); padding: 0;
  clip-path: polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%);
}
/* Triangle sits taller and wider than the old box so the slanted sides leave a
   clear margin around the content instead of pinching it against the edges;
   the content rides lower, where the triangle is widest. */
.free-badge.shape-triangle {
  width: calc(var(--bsize) * 2.1 * var(--bstretch, 1)); height: calc(var(--bsize) * 1.5); padding: 0;
  clip-path: polygon(50% 3%, 100% 100%, 0 100%);
}
.free-badge.shape-triangle .free-badge-content { transform: translateY(46%); }
/* A point-up pentagon. Its widest span is the upper third, but its centroid sits
   about 4.7% of the box height BELOW the box centre, so a numeral centred in the
   box reads high; the content rides down to sit in the visual middle instead.
   Slightly wider than tall for the same reason a diamond is: the sloped upper
   sides eat into the corners the text would otherwise use. */
.free-badge.shape-pentagon {
  width: calc(var(--bsize) * 1.2 * var(--bstretch, 1)); height: calc(var(--bsize) * 1.14); padding: 0;
  clip-path: polygon(50% 0, 100% 38.2%, 81% 100%, 19% 100%, 0 38.2%);
}
.free-badge.shape-pentagon .free-badge-content { transform: translateY(5%); }
.free-badge.shape-none { padding: 0; box-shadow: none; background: none; font-weight: 800; }
.free-badge.shape-none .free-badge-content { text-shadow: 0 2px 5px rgba(0,0,0,0.4); }
.free-badge.shape-none.no-shadow .free-badge-content { text-shadow: none; }

/* big centered effect glyph(s) on freeform cards - the same effect the
   banner below spells out in small text, readable at a glance instead */
.free-fx-glyphs {
  position: absolute; top: 44%; left: 50%; transform: translate(-50%, -50%);
  z-index: 2; display: flex; align-items: center; justify-content: center;
  gap: 3.3cqw; pointer-events: none;
}
.free-fx-glyph {
  display: flex; flex-direction: column; align-items: center; gap: 1.1cqw;
  background: rgba(10, 13, 18, 0.4); border-radius: 50%;
  width: 30cqw; height: 30cqw; justify-content: center;
  box-shadow: 0 2px 10px rgba(0,0,0,0.28);
}
.free-fx-glyphs.multi .free-fx-glyph { width: 21cqw; height: 21cqw; }
.free-fx-glyph-ic { display: inline-flex; color: #fff; filter: drop-shadow(0 1px 3px rgba(0,0,0,0.45)); }
.free-fx-glyph-ic svg { width: 11cqw; height: 11cqw; }
.free-fx-glyphs.multi .free-fx-glyph-ic svg { width: 8cqw; height: 8cqw; }
.free-fx-glyph-n {
  font-size: 8.3cqw; font-weight: 800; line-height: 1; color: #fff;
  text-shadow: 0 2px 5px rgba(0,0,0,0.45);
}
.free-fx-glyphs.multi .free-fx-glyph-n { font-size: 6cqw; }

/* effect banner on freeform cards - ONE RULES BOX.
   Every effect used to carry its own dark box: 1.8cqw of padding top and bottom,
   a 1px border on all four sides, and a 2.2cqw gap to the next one, which is
   about 6cqw of chrome per effect spent separating lines that are already
   separated by being sentences. Measured on a real card, five effects took 41%
   of the face and eight took 64%, sitting through the artwork and over the name.
   As one box the same eight take 43%.

   So the box is the banner's and the lines are paragraphs in it, which is what a
   printed card has always done. What the per-line boxes were genuinely carrying
   is that some lines are a different KIND of line - a keyword, or a rule the
   card's type granted - and that survives as a coloured rail, not a second box.

   --fx-bottom is where the box sits, set per card by renderCard (fxBottomPct in
   ui.js) so the box clears the stat badges instead of the badges dodging it. */
.free-fx {
  position: absolute; left: 6%; right: 6%; bottom: var(--fx-bottom, 5%);
  display: flex; flex-direction: column; gap: 0;
  padding: 2cqw 2.8cqw; border-radius: 2.4cqw;
  background: rgba(12, 16, 22, 0.62);
  border: 1px solid rgba(255,255,255,0.14);
}
/* Dragged off the default bottom band (card.fxPos): anchor by centre like every
   other free element, keeping the 88% band width so the lines don't reflow. */
.free-fx.free-fx-moved {
  left: 0; right: auto; bottom: auto; top: 0;
  width: 88%; transform: translate(-50%, -50%);
}
.free-fx-line {
  display: flex; align-items: flex-start; gap: 2.2cqw;
  font-size: 3.9cqw; font-weight: 650; line-height: 1.12; text-align: left;
  color: #f3efe4; padding: 0.7cqw 0;
}
/* A hairline only BETWEEN lines - the box supplies the outer edge. */
.free-fx-line + .free-fx-line { border-top: 1px solid rgba(255,255,255,0.09); padding-top: 0.9cqw; }
/* The leading icon is sized in em (relative to this line's own font) rather
   than a flat cqw, so it always matches the words beside it at any card scale
   instead of drifting larger than the text when the card is enlarged. Top of
   the line (with a hair of nudge) so on a wrapped effect it leads the phrase
   from the first line rather than floating beside the middle one. */
/* Leading action icons are off by default now - the words carry the effect,
   and a builder opts icons back in per card (card.showFxIcons -> the card root
   gains .show-fx-icons). */
.free-fx-ic { flex: 0 0 auto; display: none; margin-top: 0.06em; }
.tcard-free.show-fx-icons .free-fx-ic { display: inline-flex; }
.free-fx-ic svg { width: 1em; height: 1em; }
.free-fx-wild { font-weight: 750; }
/* A Note line: the builder's own explanatory text - it reads as prose, so it
   takes no rule above it either. Its own font/size/colour/rotation are applied
   inline in renderCard. In the editor the text is clickable to edit. */
.free-fx-line.free-fx-note { padding: 0.6cqw 0; font-weight: 600; }
.free-fx-line.free-fx-note + .free-fx-line, .free-fx-line + .free-fx-line.free-fx-note { border-top: none; }
.free-fx-line.free-fx-note .ce-note-text { cursor: pointer; }
.free-fx-line.free-fx-note .ce-note-empty { opacity: 0.55; font-style: italic; }

/* A KEYWORD line: one named reusable effect (game.keywords), printed as its bold
   name plus a small italic reminder of what it does. Marked with a gold edge so a
   keyword reads as a different KIND of line from an effect built on this card -
   the name is a pointer to a shared rule, not the rule itself. */
.free-fx-line.free-fx-kw { padding-left: 2.2cqw; box-shadow: inset 0.55cqw 0 0 rgba(216,162,74,0.75); }
/* A line the card's TYPE grants, not the card's own. Marked with a rail on the
   left the way a keyword line is, in a cooler colour, so it reads as coming from
   somewhere else at a glance - which matters because it is the one line on the
   face a builder cannot edit from this card. */
.free-fx-line.free-fx-cat { padding-left: 2.2cqw; box-shadow: inset 0.55cqw 0 0 rgba(122,162,214,0.62); }
/* A line something ATTACHED to this card gives it. A third rail colour, because
   it is a third source: the card's own, its type's, and its equipment's. It comes
   and goes with the attachment, which is the one of the three that can change
   between one look at the card and the next. */
.free-fx-line.free-fx-att { padding-left: 2.2cqw; box-shadow: inset 0.55cqw 0 0 rgba(140,198,164,0.66); }
/* An attached card on the felt. It sits under whatever it is on (the felt paints
   it first), so this only softens it slightly and marks the join - the offset
   itself is authored per card, not written here. */
.piece.is-attached .tpiece-card { filter: saturate(0.92) brightness(0.94); }
.piece.has-attached > .grab-ring { box-shadow: 0 0 0 1px rgba(140,198,164,0.5); }
.kw-print { flex: 1; min-width: 0; }
.kw-print .kw-name { font-weight: 850; letter-spacing: 0.02em; }
.kw-print .kw-rem { display: block; font-style: italic; font-weight: 500; font-size: 0.82em; line-height: 1.15; color: rgba(243,239,228,0.78); margin-top: 0.35cqw; }
.kw-print.no-rem .kw-rem { display: none; }
/* The open slot's number, editable right on the card face in the card editor:
   a compact stepper at pill scale, since the value is the one part of a keyword
   that belongs to THIS card rather than to the shared definition. */
.kw-step { display: inline-flex; align-items: center; gap: 1cqw; vertical-align: baseline; margin: 0 0.3cqw; padding: 0.1cqw 0.6cqw; border-radius: 999px; background: rgba(216,162,74,0.16); border: 1px solid var(--accent-2, #d8a24a); }
.kw-step button { width: 4.6cqw; height: 4.6cqw; display: inline-flex; align-items: center; justify-content: center; padding: 0; border: none; background: none; border-radius: 50%; color: var(--accent-2, #d8a24a); cursor: pointer; }
.kw-step button:hover { background: rgba(216,162,74,0.3); }
.kw-step button svg { width: 3cqw; height: 3cqw; }
.kw-step .kw-step-n { min-width: 3.4cqw; text-align: center; font-weight: 850; color: var(--accent-2, #d8a24a); }

/* The KEYWORD LIBRARY and BUILDER (the Global lane's Keywords pill). Both live in
   a TCG.flyout, so they inherit the glass skin and only need their own rows here.
   Compact scale throughout, matching the turn-step pills. */
.kw-lib { display: flex; flex-direction: column; gap: 6px; }
.kw-row { display: flex; align-items: flex-start; gap: 8px; padding: 7px 8px; border-radius: 9px; cursor: pointer; background: rgba(255,255,255,0.045); border: 1px solid rgba(255,255,255,0.1); }
.kw-row:hover { border-color: rgba(216,162,74,0.45); background: rgba(255,255,255,0.075); }
.kw-row-ic { width: 22px; height: 22px; flex: 0 0 auto; display: flex; align-items: center; justify-content: center; border-radius: 6px; background: rgba(216,162,74,0.14); color: var(--accent-2); }
.kw-row-ic svg { width: 13px; height: 13px; }
.kw-row-body { flex: 1; min-width: 0; }
.kw-row-name { font-size: 12px; font-weight: 700; letter-spacing: 0.2px; }
.kw-row-rem { font-size: 10.5px; line-height: 1.35; color: var(--text-dim); font-style: italic; margin-top: 1px; }
.kw-row-used { font-size: 9.5px; color: var(--text-faint); margin-top: 3px; letter-spacing: 0.3px; text-transform: uppercase; font-weight: 700; }
.kw-row-acts { display: flex; gap: 4px; flex: 0 0 auto; opacity: 0; transition: opacity 0.1s; }
.kw-row:hover .kw-row-acts { opacity: 1; }

.kw-build { display: flex; flex-direction: column; gap: 7px; }
.kw-build-head { display: flex; gap: 6px; align-items: center; }
.kw-build-head .gl-name-input { flex: 1; }
.kw-build-stage { display: flex; flex-direction: column; gap: 6px; padding: 8px 9px; border-radius: 9px; background: rgba(255,255,255,0.045); border: 1px solid rgba(255,255,255,0.1); }
.kw-build-line { display: flex; align-items: flex-start; gap: 6px; }
/* The pill sentence is authored in cqw against a card, so the builder gives it a
   card-sized container: the pills then come out at exactly the size they will be
   on the card face, rather than a second hand-tuned scale that drifts. */
.kw-build-sentence { flex: 1; min-width: 0; container-type: inline-size; font-size: 12px; font-weight: 650; line-height: 1.75; color: #f3efe4; }
.kw-slot-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 2px 0; }
.kw-slot-idle { font-size: 10.5px; color: var(--text-faint); font-style: italic; }
.kw-rem-row { display: flex; gap: 6px; align-items: flex-start; }
.kw-rem-input { flex: 1; resize: vertical; line-height: 1.35; font-style: italic; min-height: 34px; }
/* The builder's live "on a card it reads" preview: one real keyword line at real
   card scale, so what the builder approves is what a card prints. */
.kw-preview { container-type: inline-size; width: 100%; max-width: 240px; }
.free-fx.kw-preview-band { position: static; left: auto; right: auto; bottom: auto; }

/* On-card effect pills (card editor only): each editable slot of an effect
   sentence is a tappable pill that opens a dropdown. Sized in the card's own cqw
   units so it scales with the preview. Normal inline flow inside the line - never
   a flex container, so plain words and pills wrap together as one sentence. */
.free-fx-sentence { display: inline; }
.free-fx-pill {
  display: inline-flex; align-items: center; gap: 0.5cqw; vertical-align: baseline;
  padding: 0.2cqw 1.4cqw; margin: 0 0.2cqw; border-radius: 999px;
  background: var(--accent-2, #d8a24a); color: #14161c; cursor: pointer; font-weight: 650;
  white-space: nowrap;
}
.free-fx-pill.fx-pill-muted { background: rgba(255,255,255,0.2); color: #f3efe4; }
/* An idle pill: a slot this effect can't use in its current shape (a count on a
   place seen whole, a category on a flat score). It stays on the line - controls
   never vanish - but reads as unavailable: dashed outline, no fill, no caret, and
   its tooltip says what to change to wake it. Not clickable. */
.free-fx-pill.fx-pill-off {
  background: none; color: rgba(243,239,228,0.55); cursor: default; font-weight: 550;
  border: 1px dashed rgba(255,255,255,0.28);
}
.free-fx-pill.fx-pill-off:hover { filter: none; }
.free-fx-pill:hover { filter: brightness(1.1); }
.free-fx-pill .fx-pill-caret { display: inline-flex; opacity: 0.75; }
.free-fx-pill .fx-pill-caret svg { width: 3cqw; height: 3cqw; transform: rotate(90deg); }

/* The FOLD control (card editor only). Collapses the rules box so the artwork
   under it can be read, and opens it again. It rides the box's own top-right
   corner rather than sitting in a panel, because it is the box's disclosure
   control and not a setting about the card - the settings all moved to the right
   panel, which is what made room for this one thing to be here.

   Nothing about it is saved: it is a way of LOOKING at the card, not a property
   of it, so it never reaches a save, an export, the felt or a print. See
   fxFolded in cardEditor.js. */
.free-fx-fold {
  position: absolute; top: -1.5cqw; right: -1.5cqw; z-index: 3;
  width: 6.6cqw; height: 6.6cqw; display: flex; align-items: center; justify-content: center;
  border-radius: 999px; cursor: pointer;
  background: rgba(15,20,28,0.92); border: 1px solid rgba(255,255,255,0.2); color: #f3efe4;
}
.free-fx-fold:hover { border-color: var(--accent-2, #d8a24a); color: var(--accent-2, #d8a24a); }
.free-fx-fold svg { width: 3.6cqw; height: 3.6cqw; transition: transform 0.12s ease; }
.free-fx.fx-folded .free-fx-fold svg { transform: rotate(180deg); }
/* Folded, the box keeps its edge and its corner control and swaps its lines for
   one line saying what is in there - so it reads as something closed rather than
   as the effects having been deleted, and there is always something to click. */
.free-fx.fx-folded { padding: 1.3cqw 2.8cqw; }
/* Hide everything EXCEPT the two things that are the fold itself. Written as
   "everything but", not as "> .free-fx-line", because a line has not always been
   a child of the box: the editor used to wrap them in .free-fx-tiles >
   .free-fx-block > .free-fx-blockbody, so a child combinator reached none of them
   and a folded card printed its full rules with a chevron pointing the wrong way.
   Assertions did not catch it (the class was on, the lines were in the DOM);
   looking at it did. That lane is gone and the lines are children again, but the
   rule stays written this way: it is right whatever the box turns out to hold. */
.free-fx.fx-folded > *:not(.free-fx-fold):not(.free-fx-foldbar) { display: none; }
.free-fx-foldbar { display: none; font-size: 3.2cqw; font-weight: 700; letter-spacing: 0.02em;
  color: rgba(243,239,228,0.72); cursor: pointer; text-align: left; }
.free-fx-foldbar:hover { color: #f3efe4; }
.free-fx.fx-folded .free-fx-foldbar { display: block; }
.free-fx-pill .fx-pill-ic { display: inline-flex; }
.free-fx-pill .fx-pill-ic svg { width: 3.4cqw; height: 3.4cqw; }
.free-fx-rm { flex: 0 0 auto; display: inline-flex; margin-left: auto; cursor: pointer; opacity: 0.6; color: #f3efe4; }
.free-fx-rm:hover { opacity: 1; color: var(--red, #d6604d); }
.free-fx-rm svg { width: 4cqw; height: 4cqw; }
/* The trailing "+" adds an optional rule to an effect/income line. It grabs the
   free space so it and the remove "x" sit together at the line's right edge. */
.free-fx-more { flex: 0 0 auto; display: inline-flex; margin-left: auto; cursor: pointer; opacity: 0.6; color: #f3efe4; border-radius: 999px; padding: 0.4cqw; }
.free-fx-more:hover { opacity: 1; background: rgba(255,255,255,0.16); color: var(--accent-2, #d8a24a); }
.free-fx-more svg { width: 4cqw; height: 4cqw; }
.free-fx-more + .free-fx-rm { margin-left: 0.6cqw; }
/* THE LANE'S OWN DRESSING LIVED HERE, and it is gone with the lane:
   .free-fx-move (the reorder arrows), .free-fx-grip / .fx-dragging / .fx-drop-line
   (dragging one effect past another), .free-fx-block / .free-fx-blockhead /
   .free-fx-cost / .free-fx-bracket* (a card's effects drawn as their real block
   and bracket structure), .free-fx-chain, .free-fx-blank and .fx-pill-action (a
   half-built effect line), and the whole .fx-pal-* palette that dragged pieces
   onto them. The card face prints ONE thing now - the same flat sentence every
   other surface prints - and an effect is edited as pieces in the builder's rows.
   .free-fx-rm and .free-fx-more above stay: the note, trait, keyword and income
   lines still carry them. */
/* A pill a KEYWORD leaves open for each card to fill in - the "2" in "Rally 2".
   Dashed and gold like the add pill (it's a hole, not a value), but still live and
   clickable, since clicking it is how the builder closes the slot again. */
.free-fx-pill.fx-pill-slotx { background: rgba(216,162,74,0.16); color: var(--accent-2, #d8a24a); border: 1px dashed var(--accent-2, #d8a24a); font-weight: 800; }
.free-fx-pill.fx-pill-slotx:hover { filter: brightness(1.15); }

/* wild / colorless card: a dark face with a subtle rainbow rim */
.tcard-free.is-wild { --card-accent: #d7cdbb; }
.tcard-free.is-wild .tcard-free-inner {
  background:
    radial-gradient(120% 90% at 50% 8%, rgba(255,255,255,0.10), rgba(255,255,255,0) 60%),
    #14161c;
  color: #f3efe4;
}
.tcard-free.is-wild::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  padding: 2px;
  background: conic-gradient(from 0deg, #d6483b, #e6c531, #3d9a57, #2f7fd0, #6f47b0, #d6483b);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
  opacity: 0.85;
}

/* card creator: a centered, clickable card on the felt */
.ce-holder { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
/* Editable card name, centered just above the 290px card (aspect 5/7, so its
   top edge sits 203px above felt center). Keeps the card itself centered so the
   dock/panel math around it stays put. */
.ce-title {
  position: absolute; left: 50%; bottom: calc(50% + 232px); transform: translateX(-50%);
  z-index: 16; display: flex; align-items: center; justify-content: center;
  width: max-content; min-width: var(--ce-card-w, 290px); max-width: min(760px, 92vw);
  transition: transform 0.14s;
}
/* The card lifts and scales up on hover, raising its top edge ~10px - move the
   name up with it so the gap between them stays constant. */
/* Scoped to the card-editor felt on purpose: a bare `.felt:has(...)` makes EVERY
   .felt a :has() invalidation anchor, so the browser re-evaluates this selector on
   every DOM change inside the PLAY felt too - and a dice roll mutates ~200 nodes a
   frame, which turned style recalc into the roll's dominant cost. The editor felt
   lives under .ce-view; the play felt does not, so this keeps the anchor off it. */
.ce-view .felt:has(.ce-card:hover) .ce-title { transform: translateX(-50%) translateY(-10px); }
.ce-title input.ce-title-input {
  flex: 1 1 auto; min-width: 0; width: auto; text-align: center;
  -webkit-appearance: none; appearance: none;
  background: none; border: none; box-shadow: none; border-radius: 0;
  color: var(--text); font-size: 19px; font-weight: 700; letter-spacing: 0.2px;
  padding: 4px 6px; outline: none;
  text-shadow: 0 1px 3px rgba(0,0,0,0.55);
}
.ce-title input.ce-title-input:hover,
.ce-title input.ce-title-input:focus,
.ce-title input.ce-title-input:focus-visible { background: none; border: none; box-shadow: none; outline: none; }
.ce-title input.ce-title-input::placeholder { color: var(--text-faint); font-weight: 600; text-shadow: none; }
/* The editor draws the card at a fixed size, and WHICH fixed size depends on
   its shape: a portrait or square card is 290px wide, a landscape one is 406px
   wide (the portrait card turned, so a builder gets the same working area
   either way). renderCardFace publishes both --ce-card-w and --ce-card-h on
   the felt; everything that has to line up with the card edge reads them
   rather than repeating 290. */
.ce-holder .ce-card { width: var(--ce-card-w, 290px); cursor: pointer; transition: transform 0.14s, box-shadow 0.2s; position: relative; }
.ce-holder .ce-card:hover { transform: translateY(-6px) scale(1.02); box-shadow: 0 32px 54px rgba(0,0,0,0.55); }

/* card + its rotate control. The card is the only in-flow child, so .ce-holder
   centers the CARD (not card+controls) and its position never drifts with how
   many controls sit below - which keeps a constant gap under the name above it.
   The controls hang beneath the card, absolutely placed so they add no height. */
.ce-card-wrap { position: relative; display: flex; flex-direction: column; align-items: center; }
.ce-card-wrap .ce-flip-controls { position: absolute; top: 100%; left: 50%; transform: translateX(-50%); margin-top: 14px; }
/* rotate control below the card - turns the card over */
.ce-flip-btn {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 34px; padding: 0 13px 0 11px; border-radius: 999px; cursor: pointer;
  background: rgba(15,20,28,0.88); border: 1px solid rgba(255,255,255,0.18); color: var(--accent-2);
  backdrop-filter: blur(8px); box-shadow: 0 6px 18px rgba(0,0,0,0.4);
  transition: transform 0.12s, border-color 0.12s, background 0.12s;
}
.ce-flip-btn:hover { border-color: var(--accent); background: rgba(15,20,28,0.97); transform: scale(1.06); }
.ce-flip-btn:active { transform: scale(0.93); }
.ce-flip-btn.on-back { color: var(--accent); border-color: rgba(216,162,74,0.5); background: rgba(216,162,74,0.16); }
.ce-flip-ic { display: inline-flex; }
.ce-flip-ic svg { width: 17px; height: 17px; }
.ce-flip-side { font-size: 11px; font-weight: 800; letter-spacing: 0.4px; text-transform: uppercase; color: var(--text); }
/* flip button + the two copy-across controls, stacked under the card */
.ce-flip-controls { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.ce-copy-faces { display: flex; gap: 6px; }
.ce-copy-face { font-size: 10.5px; font-weight: 700; padding: 4px 9px; gap: 5px; border-radius: 8px; letter-spacing: 0.2px; }
.ce-copy-face svg { width: 13px; height: 13px; }
@keyframes ce-cardflip { from { transform: perspective(900px) rotateY(82deg); opacity: 0.25; } to { transform: perspective(900px) rotateY(0); opacity: 1; } }
.ce-holder .ce-card.ce-card-flip { animation: ce-cardflip 0.34s ease; }
/* the card back shown when a one-sided card is turned over (preview) */
.ce-card .ce-card-back {
  position: absolute; inset: 0; z-index: 4; border-radius: inherit; container-type: inline-size;
  display: flex; align-items: center; justify-content: center; cursor: pointer; color: rgba(255,255,255,0.82);
  background:
    linear-gradient(135deg, rgba(255,255,255,0.14), transparent 45%),
    repeating-linear-gradient(45deg, #3a4a63 0 14px, #2c3a50 14px 28px);
}
.ce-card .ce-card-back svg { width: 26cqw; height: 26cqw; }
.ce-card .ce-card-back.has-back-art { background: none; overflow: hidden; }
.ce-card .ce-card-back.has-back-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ce-card .ce-card-back.has-back-art.fit-contain img { object-fit: contain; }
/* the call-to-action floated over a one-sided card's back preview: design a real
   editable back for this card, or set the shared deck-back image. Glass panel at
   compact scale, matching the rest of the tool's floating surfaces. */
.ce-back-cta {
  position: absolute; inset: 12cqw; z-index: 5; cursor: default;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; text-align: center;
  padding: 14px; border-radius: 12px;
  background: rgba(15,20,28,0.72); backdrop-filter: blur(10px);
  border: 1px solid rgba(255,255,255,0.12); box-shadow: 0 10px 26px rgba(0,0,0,0.45);
}
.ce-back-cta-title { font-size: 13px; font-weight: 800; color: var(--text); }
.ce-back-cta-sub { font-size: 10.5px; line-height: 1.35; color: var(--text-faint); max-width: 22ch; }
.ce-back-cta-main { margin-top: 2px; }
.ce-back-cta-alt {
  display: inline-flex; align-items: center; gap: 5px; cursor: pointer;
  background: none; border: none; color: var(--text-faint); font-size: 10.5px; font-weight: 700; padding: 3px 4px;
}
.ce-back-cta-alt:hover { color: var(--accent-2); }
.ce-back-cta-alt svg { width: 13px; height: 13px; }

.ce-shelf-hint { color: var(--text-faint); font-size: 12px; padding: 22px 4px; }
.ce-shelf-label { display: flex; align-items: center; gap: 12px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.7px; color: var(--text-faint); font-weight: 700; }
.ce-mini-wrap { position: relative; flex: 0 0 auto; cursor: pointer; border-radius: 9px; padding: 2px; transition: transform 0.1s; }
.ce-mini-wrap:hover { transform: translateY(-3px); }
.ce-mini-wrap.editing { outline: 2px solid var(--accent-2); }
.ce-mini-wrap.sel { outline: 2px solid var(--accent); background: rgba(216,162,74,0.12); }
.ce-mini-chk {
  position: absolute; top: 3px; right: 3px; width: 20px; height: 20px; border-radius: 50%;
  border: 1px solid var(--line-2); background: rgba(6,9,14,0.8); color: transparent; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.ce-mini-chk svg { width: 13px; height: 13px; }
.ce-mini-wrap:hover .ce-mini-chk { color: var(--text-faint); }
.ce-mini-wrap.sel .ce-mini-chk { background: var(--accent); color: #221703; border-color: var(--accent); }
/* duplicate/delete on hover - shown wherever a mini card sits (the gallery
   grid and the Decks popover), so copying a card never requires digging */
.ce-mini-wrap:hover .ce-gal-acts { opacity: 1; }

/* deck stacks, used in the Decks popover */
.ce-deck-stack { flex: 0 0 auto; width: 58px; cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 4px; }
.ce-deck-stack-face {
  position: relative; width: 50px; height: 70px; border-radius: 8px;
  background: linear-gradient(180deg, rgba(255,255,255,0.12), rgba(0,0,0,0.35)), var(--deck-color, #3a4a63);
  box-shadow: 3px 3px 0 rgba(0,0,0,0.35), 6px 6px 0 rgba(0,0,0,0.2);
  display: flex; align-items: center; justify-content: center; color: rgba(255,255,255,0.9);
  transition: transform 0.12s;
}
.ce-deck-stack:hover .ce-deck-stack-face { transform: translateY(-3px); }
.ce-deck-stack-face svg { width: 20px; height: 20px; }
.ce-deck-count { position: absolute; bottom: 3px; right: 5px; font-size: 11px; font-weight: 800; }
.ce-deck-stack-dup {
  position: absolute; top: 3px; right: 3px; z-index: 2; width: 18px; height: 18px;
  border-radius: 50%; border: none; padding: 0; cursor: pointer;
  background: rgba(6,9,14,0.82); color: #fff;
  display: flex; align-items: center; justify-content: center;
  opacity: 0; transition: opacity 0.12s, background 0.12s;
}
.ce-deck-stack:hover .ce-deck-stack-dup { opacity: 1; }
.ce-deck-stack-dup:hover { background: var(--accent-2); }
.ce-deck-stack-dup svg { width: 11px; height: 11px; }
.ce-deck-stack-name { font-size: 10.5px; color: var(--text-dim); max-width: 58px; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* deck tray overlay */
.ce-deck-back { position: fixed; inset: 0; z-index: 280; background: rgba(6,9,14,0.72); backdrop-filter: blur(3px); display: flex; align-items: center; justify-content: center; padding: 24px; }
.ce-deck-tray { width: 100%; max-width: 900px; max-height: 82vh; display: flex; flex-direction: column; background: rgba(15,20,28,0.82); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.12); border-radius: 16px; box-shadow: var(--shadow); overflow: hidden; }
.ce-deck-head { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid var(--line); }
.ce-deck-dot { width: 18px; height: 18px; border-radius: 5px; flex-shrink: 0; }
.ce-deck-name { flex: 1; background: transparent; border: 1px solid transparent; border-radius: 8px; color: var(--text); font-size: 17px; font-weight: 700; padding: 5px 8px; }
.ce-deck-name:hover, .ce-deck-name:focus { border-color: var(--line-2); }
.ce-deck-body { padding: 16px; overflow: auto; }
.ce-deck-grid { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 18px; min-height: 40px; }
.ce-deck-card { position: relative; cursor: grab; border-radius: 11px; padding: 3px; }
.ce-deck-card.dragging { opacity: 0.4; }
.ce-deck-card.drop-target { outline: 2px dashed var(--accent-2); outline-offset: 1px; }
.ce-deck-card .tcard.mini { width: 92px; }
/* The copy count is the control that SETS the copy count, so it is a real
   button: border and font reset because it used to be a span, and the rest of
   the rule is unchanged so it still reads as the badge it has always been. */
.ce-deck-ord { position: absolute; top: 5px; left: 5px; z-index: 2; min-width: 18px; height: 18px; padding: 0 4px; border-radius: 9px; background: rgba(6,9,14,0.82); color: #fff; font-size: 11px; font-weight: 800; display: flex; align-items: center; justify-content: center; border: none; font-family: inherit; cursor: pointer; }
.ce-deck-ord:hover { background: var(--accent-2); }
.ce-deck-rm { position: absolute; top: 4px; right: 4px; z-index: 2; width: 20px; height: 20px; border-radius: 50%; border: none; background: rgba(6,9,14,0.82); color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; }
.ce-deck-rm:hover { background: var(--red); }
.ce-deck-rm svg { width: 12px; height: 12px; }
.ce-deck-dup { position: absolute; top: 4px; right: 28px; z-index: 2; width: 20px; height: 20px; border-radius: 50%; border: none; background: rgba(6,9,14,0.82); color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; }
.ce-deck-dup:hover { background: var(--accent-2); }
.ce-deck-dup svg { width: 12px; height: 12px; }
.ce-deck-pool { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }
.ce-deck-pool .tcard.mini { width: 72px; }
.ce-deck-pool .ce-mini-wrap:hover { transform: translateY(-3px); outline: 2px solid var(--accent); }

/* live element editor panel on the card-creator felt */
/* The scale here is the one dev/menu-design.html states for a sequencer menu
   (rule 10's table): 240 wide, 10px body padding, 13px/700 title, 13px title
   icon, 13px radius. An element panel is not a sequencer menu - it is docked
   rather than hanging off a pill - but there is no reason for the two glass
   surfaces to be measured differently, and a second scale is how the app stops
   reading as one tool. dev/_menu-continuity-check.js measures both and prints
   them side by side. */
.ce-panel {
  position: absolute; top: 64px; right: 14px; z-index: 20; width: 240px;
  max-height: calc(100vh - 84px); max-height: calc(100dvh - 84px); overflow-y: auto;
  background: rgba(15,20,28,0.82); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.12);
  border-radius: 13px; box-shadow: var(--shadow); padding: 10px;
  display: flex; flex-direction: column; gap: 6px;
}
.ce-panel-head { display: flex; align-items: center; gap: 7px; }
.ce-panel-head > span:first-child { display: inline-flex; color: var(--accent-2); }
.ce-panel-head > span:first-child svg { width: 13px; height: 13px; }
.ce-panel-title { font-weight: 700; font-size: 13px; flex: 1; letter-spacing: 0.2px; }
.ce-row { display: flex; flex-direction: column; gap: 6px; }
.ce-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--text-faint); font-weight: 700; }
.ce-step { display: flex; align-items: center; gap: 8px; }
.ce-step-btn {
  width: 30px; height: 30px; border-radius: 8px; border: 1px solid var(--line-2); background: var(--panel-2);
  color: var(--text); cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
}
.ce-step-btn:hover { border-color: var(--accent); color: var(--accent-2); }
.ce-step-btn svg { width: 15px; height: 15px; }
.ce-val { min-width: 36px; text-align: center; font-weight: 800; font-size: 18px; }
/* A stepper readout you can also TYPE into (TCG.stepField in js/ui.js). It
   carries .ce-val as well, so its width, centring and weight are the ones the
   printed number had, and it reads as that number until the cursor is in it.

   Only the properties the GLOBAL `input[type="text"]` block (0-1-1, near the
   top of this file) would otherwise steal are restated here - width, the
   field chrome, colour, padding and font-size - each on `input.ce-step-val`
   (0-2-0) so it wins. That block is why `.ce-val` alone is not enough: at
   0-1-0 it loses, and the readout would arrive full-row-wide at 14px in a
   bordered box. Same trap as the search fields and .ce-num-val; see the note
   above .search-wrap.

   The three context sizes are restated for the same reason: `.ce-panel
   .ce-val` and `.payout-step .ce-val` are 0-2-0 and would only TIE with the
   base rule below, leaving source order to decide the type size. */
input.ce-step-val {
  width: 3.1em; box-sizing: content-box; flex: 0 0 auto;
  background: none; border: 1px solid transparent; border-radius: 6px;
  color: inherit; font-family: inherit; font-size: 18px;
  font-variant-numeric: tabular-nums; padding: 0 2px; cursor: text; box-shadow: none;
}
.ce-panel input.ce-step-val { font-size: 13px; }
.payout-step input.ce-step-val { font-size: 14px; width: 2.4em; }
input.ce-step-val:hover { background: rgba(255,255,255,0.06); }
input.ce-step-val:focus { background: rgba(255,255,255,0.10); border-color: rgba(255,255,255,0.18); outline: none; box-shadow: none; }
input.ce-step-val:disabled { opacity: 0.45; cursor: default; }
input.ce-step-val::selection { background: rgba(216, 162, 74, 0.35); color: var(--text); }
/* roll-payout editor: a compact panel that extends from the step's pill */
.payout-panel.floating {
  position: absolute; z-index: 26; display: flex; flex-direction: column; gap: 8px;
  padding: 9px 11px; min-width: 248px;
  background: rgba(15,20,28,0.88); backdrop-filter: blur(10px);
  border: 1px solid rgba(255,255,255,0.12); border-radius: 12px; box-shadow: var(--shadow);
}
.payout-head-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.payout-title { font-weight: 800; font-size: 12px; color: var(--text); }
.payout-close { width: 20px; height: 20px; display: inline-flex; align-items: center; justify-content: center; border: none; background: transparent; color: var(--text-dim); cursor: pointer; border-radius: 6px; }
.payout-close:hover { color: var(--text); background: rgba(255,255,255,0.08); }
.payout-close svg { width: 13px; height: 13px; }
.payout-mode { display: flex; align-items: center; gap: 6px; }
.payout-mode-lbl { font-size: 11px; color: var(--text-dim); margin-right: 2px; }
.payout-seg { font-size: 11px; padding: 3px 11px; border-radius: 999px; }
.payout-seg.on { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.payout-grid { display: grid; grid-template-columns: auto auto auto auto; gap: 7px 10px; align-items: center; justify-content: start; }
.payout-gh { font-size: 10px; color: var(--text-faint); text-align: center; text-transform: uppercase; letter-spacing: 0.4px; }
.payout-step { gap: 4px; }
.payout-step .ce-val { min-width: 22px; font-size: 14px; }
.payout-step .ce-step-btn, .payout-rm { width: 22px; height: 22px; border-radius: 6px; }
.payout-step .ce-step-btn svg, .payout-rm svg { width: 12px; height: 12px; }
.ce-step-btn:disabled { opacity: 0.3; cursor: default; }
.ce-step-btn:disabled:hover { border-color: var(--line-2); color: var(--text); }
.payout-rm.danger { color: var(--accent-2); }
.payout-rm.danger:hover { border-color: #d6604d; color: #d6604d; }
.payout-foot { display: flex; justify-content: space-between; gap: 8px; margin-top: 2px; }
.payout-tail {
  position: absolute; bottom: -6px; width: 11px; height: 11px; background: rgba(15,20,28,0.88);
  border-right: 1px solid rgba(255,255,255,0.12); border-bottom: 1px solid rgba(255,255,255,0.12);
  transform: translateX(-50%) rotate(45deg);
}
.payout-panel.below .payout-tail { bottom: auto; top: -6px; border: none; border-left: 1px solid rgba(255,255,255,0.12); border-top: 1px solid rgba(255,255,255,0.12); }
/* buy-cap editor: reuses the payout panel skin. The attribute and values
   controls are compact dropdowns so the panel stays one narrow column no matter
   how many categories/values a game has - each collapses to a single line and
   opens a capped-height scrolling list on demand. */
.buycap-panel { min-width: 210px; }
.buycap-panel .bcap-unit { font-size: 11px; color: var(--text-dim); }
.buycap-panel .bcap-sum { font-size: 11px; color: var(--text-dim); line-height: 1.4; max-width: 260px; }
.bcap-dd { position: relative; display: inline-flex; }
/* the trigger carries a live selection, so it wears the gold accent like every
   other selected control (matches .payout-seg.on) rather than staying grey */
.buycap-panel .bcap-dd-trig {
  display: inline-flex; align-items: center; gap: 6px; font-size: 11px;
  padding: 3px 10px; border-radius: 999px; max-width: 170px;
  background: var(--accent); color: var(--accent-ink); border: 1px solid var(--accent);
}
.buycap-panel .bcap-dd-trig:hover { background: var(--accent-2); border-color: var(--accent-2); color: var(--accent-ink); filter: none; }
.buycap-panel .bcap-dd.open .bcap-dd-trig { background: var(--accent-2); border-color: var(--accent-2); }
.bcap-dd-lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bcap-dd-caret { display: inline-flex; transition: transform 0.12s ease; }
.bcap-dd-caret svg { width: 11px; height: 11px; }
.bcap-dd.open .bcap-dd-caret { transform: rotate(180deg); }
.bcap-dd-menu {
  display: none; position: absolute; top: calc(100% + 5px); left: 0; z-index: 40;
  min-width: 160px; max-height: 190px; overflow-y: auto; padding: 5px; gap: 2px;
  flex-direction: column;
  background: rgba(15,20,28,0.96); backdrop-filter: blur(12px);
  border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.4);
}
.bcap-dd.open .bcap-dd-menu { display: flex; }
/* opens upward when there isn't room below (positioned in JS) */
.bcap-dd.up .bcap-dd-menu { top: auto; bottom: calc(100% + 5px); }
.bcap-dd-row {
  display: flex; align-items: center; gap: 6px; width: 100%; text-align: left;
  font-size: 11px; padding: 4px 7px; border-radius: 7px; white-space: nowrap;
  background: transparent; border: none; color: var(--text); cursor: pointer;
}
.bcap-dd-row:hover { background: rgba(255,255,255,0.07); }
/* selected row wears the gold accent, like every other selected control */
.bcap-dd-row.on { color: var(--accent-2); background: color-mix(in srgb, var(--accent) 16%, transparent); }
.bcap-dd-row.on:hover { background: color-mix(in srgb, var(--accent) 24%, transparent); }
.bcap-dd-ck { width: 13px; height: 13px; display: inline-flex; align-items: center; color: var(--accent-2); flex: 0 0 auto; }
.bcap-dd-ck svg { width: 13px; height: 13px; }
.bcap-dd-note { font-size: 11px; color: var(--text-dim); padding: 4px 7px; }
.ce-panel input[type="range"] { width: 100%; accent-color: var(--accent); cursor: pointer; }
.ce-text-input { width: 100%; font-size: 13px; min-height: 76px; resize: vertical; }
/* single-line variant (font pickers and the like) - drop the textarea height */
.ce-input-sm { min-height: 0; height: 34px; padding: 0 8px; resize: none; }
/* rotation slider paired with its degree readout */
.ce-rot { display: flex; align-items: center; gap: 9px; }
.ce-rot input[type="range"] { flex: 1; }
.ce-rot-val { min-width: 38px; text-align: right; font-variant-numeric: tabular-nums; }
/* scale slider: a 100% tick under the thumb + a clickable percent readout that
   resets to 1:1. The tick paints over the track so it stays visible, and reads
   as "you're on the mark" when the thumb sits over it. */
.ce-scale { display: flex; align-items: center; gap: 9px; flex: 1; }
.ce-scale-slot { position: relative; flex: 1; display: flex; align-items: center; }
.ce-scale-slot input[type="range"] { flex: 1; }
.ce-scale-tick { position: absolute; left: 50%; top: 50%; width: 2px; height: 11px; margin-top: -6px; transform: translateX(-50%); background: var(--text-faint, #6b7280); border-radius: 1px; pointer-events: none; opacity: 0.7; }
.ce-scale-val { min-width: 42px; text-align: right; font-variant-numeric: tabular-nums; background: none; border: none; color: var(--text-faint, #98a1af); cursor: pointer; font: inherit; padding: 2px 4px; border-radius: 6px; }
.ce-scale-val:hover { color: var(--text); background: rgba(255,255,255,0.06); }
/* A slider readout you can also TYPE into (numVal in cardEditor.js). It reads
   as the quiet number it replaced until you put the cursor in it, then it is a
   real field - so nothing about the row looks different until you use it.
   Written as `input.ce-num-val` (0-2-0) on purpose: the global
   `input[type="text"]` block near the top of this file is 0-1-1, so a bare
   class loses to it and the readout would arrive wearing a full field's
   border, background, 14px type and width:100%. Same trap as the search
   fields; see the note above .search-wrap. */
input.ce-num-val {
  width: 4.2em; min-width: 0; flex: 0 0 auto; box-sizing: content-box;
  background: none; border: 1px solid transparent; border-radius: 6px;
  color: var(--text-faint, #98a1af); font: inherit; font-variant-numeric: tabular-nums;
  text-align: right; padding: 1px 3px; cursor: text; box-shadow: none;
}
input.ce-num-val:hover { color: var(--text); background: rgba(255,255,255,0.06); }
input.ce-num-val:focus { color: var(--text); background: rgba(255,255,255,0.10); border-color: rgba(255,255,255,0.18); outline: none; box-shadow: none; }
input.ce-num-val:disabled { opacity: 0.45; cursor: default; }
/* Focusing selects the number so it can be typed straight over, and the
   browser's default selection blue is the one loud colour on the panel. */
input.ce-num-val::selection { background: rgba(216, 162, 74, 0.35); color: var(--text); }
.ce-panel .swatches { gap: 6px; }
.ce-panel .swatch { width: 24px; height: 24px; border-radius: 6px; }
.ce-swatch-none { display: inline-flex; align-items: center; justify-content: center; background: var(--panel-2); color: var(--text-faint); }
.ce-swatch-none svg { width: 13px; height: 13px; }
.ce-icons { display: flex; flex-wrap: wrap; gap: 6px; }
.ce-icon {
  width: 34px; height: 34px; border-radius: 8px; border: 1px solid var(--line); background: var(--panel-2);
  color: var(--text); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; transition: border-color 0.1s;
}
.ce-icon:hover { border-color: var(--accent); }
.ce-icon.sel { border-color: var(--accent-2); box-shadow: inset 0 0 0 1px var(--accent-2); }
.ce-icon svg { width: 18px; height: 18px; }
.ce-icon.suit-red { color: #c0392b; }
.ce-icon.suit-black { color: #cfcabc; }

/* Compact single-swatch / single-icon triggers: a colour or icon control shown
   as one button that opens its palette/picker in a glass flyout, instead of a
   full grid stretched down the panel. They sit inline (label left, trigger right)
   on a .ce-row-inline row. */
.ce-color-trigger { width: 26px; height: 26px; border-radius: 7px; border: 1px solid var(--line-2);
  box-shadow: inset 0 0 0 1px rgba(0,0,0,0.35); cursor: pointer; flex: 0 0 auto; }
.ce-color-trigger.ce-swatch-none { box-shadow: none; }
.ce-color-trigger.ce-swatch-none svg { width: 14px; height: 14px; }
.ce-icon-trigger { width: 32px; height: 32px; flex: 0 0 auto; }
.ce-icon-trigger.ce-icon-empty { color: var(--text-faint); }
/* The colour flyout's swatch grid wraps a few per row on the glass. */
.ce-color-grid { max-width: 180px; }
/* The colour flyout is a swatch grid with the opacity slider under it. The row
   is the same .ce-row a panel uses, but the flyout isn't a .ce-panel, so it
   picks up none of that block's compact sizing - these few lines are that
   sizing, scoped to this one wrapper. No checkerboard behind a translucent
   swatch: every swatch sets its colour as the `background` SHORTHAND inline,
   which resets background-image, so a stylesheet pattern under it would be
   thrown away by the very element it was meant to sit behind. The swatches sit
   on dark glass, where a faded ink simply reads darker, which is honest. */
.ce-color-alpha { display: block; }
/* flex-direction is stated because base .ce-row is a COLUMN (label above
   control) and re-declaring `display: flex` does not put it back on a row.
   Without it, `align-items: center` centres the label inside that column and
   the row prints "Opacity" as a heading over its own slider - the third thing
   in this block that inherited from a rule it only half overrode. */
.ce-color-alpha .ce-row { display: flex; flex-direction: row; align-items: center; gap: 6px; margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(255, 255, 255, 0.08); }
/* The label needs the whole panel treatment, not just a size. Base .ce-label is
   a shouty uppercase 700 heading, and what quiets it down inside a panel is
   `.ce-panel .ce-label`, which resets text-transform and letter-spacing. This
   flyout is NOT a .ce-panel, so it never got that reset and printed a 700
   uppercase "OPACITY" over its own slider - measured, not guessed. Same shape
   as the search-field trap: overriding one property of a rule that sets five
   leaves the other four in place. */
.ce-color-alpha .ce-label { font-size: 11.5px; font-weight: 600; text-transform: none; letter-spacing: 0; color: var(--text-dim); opacity: 0.85; }
/* And the slider's accent is on `.ce-panel input[type="range"]` for the same
   reason, so this one rendered in the browser's own blue - the one colour in
   the app that belongs to nobody. */
.ce-color-alpha input[type="range"] { accent-color: var(--accent); cursor: pointer; }
.ce-color-alpha .ce-rot { display: flex; align-items: center; gap: 6px; flex: 1; }
.ce-color-alpha .ce-rot input[type="range"] { flex: 1; min-width: 0; }
.ce-color-alpha .ce-rot-val { font-size: 11px; min-width: 30px; text-align: right; }
.ce-color-alpha .ce-row.row-off { opacity: 0.45; }
/* ---- the colour spectrum (TCG.buildColorSpectrum) ----
   Our own custom-colour control, in place of the browser's input[type=color]
   and the OS dialog behind it. A saturation/value square, a hue strip, a #code
   field. Both surfaces carry `touch-action: none`, without which a mobile
   browser claims the drag as a page pan and fires pointercancel on the first
   move - the other half of the pointer-event rule, and easy to leave off since
   nothing errors on a laptop.
   The square's two washes are background-IMAGE, never the background shorthand:
   the hue underneath is written inline as background-color, and a shorthand
   here would throw the gradients away the moment a hue was set. */
.ce-colpick { display: flex; flex-direction: column; gap: 7px; margin-top: 8px; }
.ce-sv {
  position: relative; height: 84px; border-radius: 7px; cursor: crosshair;
  touch-action: none; overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
  background-color: #f00;
  background-image: linear-gradient(to top, #000, rgba(0, 0, 0, 0)),
                    linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
}
.ce-hue {
  position: relative; height: 12px; border-radius: 999px; cursor: pointer;
  touch-action: none;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
  background-image: linear-gradient(to right, #f00 0%, #ff0 16.66%, #0f0 33.33%,
    #0ff 50%, #00f 66.66%, #f0f 83.33%, #f00 100%);
}
/* One handle style for both surfaces: a white ring over the colour it sits on,
   so it reads against a black corner and a white one alike. */
.ce-sv-dot, .ce-hue-dot {
  position: absolute; pointer-events: none; border-radius: 50%;
  border: 2px solid #fff; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5), 0 1px 3px rgba(0, 0, 0, 0.4);
}
.ce-sv-dot { width: 12px; height: 12px; transform: translate(-50%, -50%); }
.ce-hue-dot { width: 12px; height: 12px; top: 50%; transform: translate(-50%, -50%); }
.ce-hexrow { display: flex; align-items: center; gap: 5px; }
.ce-hex-hash { font-size: 12px; color: var(--text-faint); flex: 0 0 auto; }
/* Written as `input.ce-hex` on purpose. The global input[type="text"] rule is
   0-1-1 and a bare class is 0-1-0, so a class alone LOSES and the padding,
   radius and border below are dropped in silence - the same cascade trap the
   search fields hit. Same reasoning as the .search-wrap block. */
.ce-colpick input.ce-hex {
  flex: 1; min-width: 0; width: auto; margin: 0;
  font-size: 11.5px; font-family: ui-monospace, Menlo, monospace; letter-spacing: 0.4px;
  padding: 4px 7px; border-radius: 7px;
  border: 1px solid var(--line-2); background: rgba(255, 255, 255, 0.05); color: var(--text);
}
.ce-colpick input.ce-hex:focus { outline: none; border-color: var(--accent-2); }
.ce-hex-prev { width: 22px; height: 22px; border-radius: 6px; flex: 0 0 auto;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.4); }
/* Grouped icon picker (flyout): category tabs over a wrapping glyph grid. */
.ce-iconpick { display: flex; flex-direction: column; gap: 8px; }
.ce-iconpick-tabs { display: flex; flex-wrap: wrap; gap: 4px; }
.ce-iconpick-tab { font-size: 11px; padding: 3px 8px; border-radius: 999px; border: 1px solid var(--line);
  background: rgba(255,255,255,0.04); color: var(--text-faint); cursor: pointer; transition: border-color 0.1s, color 0.1s; }
.ce-iconpick-tab:hover { border-color: var(--accent); color: var(--text); }
.ce-iconpick-tab.sel { border-color: var(--accent-2); color: var(--accent-2); background: rgba(255,255,255,0.07); }
.ce-iconpick-grid { max-width: 232px; }

/* Foldable element-panel sections: a header row that toggles its body of rows
   open or closed. Sections fold independently, so a tall badge/number panel
   collapses to just the sections a builder keeps open. */
.ce-fold { display: flex; flex-direction: column; }
.ce-fold-head { display: flex; align-items: center; gap: 7px; width: 100%; padding: 5px 2px;
  background: none; border: none; border-bottom: 1px solid var(--line); color: var(--text);
  cursor: pointer; text-align: left; }
.ce-fold-head:hover { color: var(--accent-2); }
.ce-fold-ic { display: inline-flex; color: var(--accent-2); }
.ce-fold-ic svg { width: 12px; height: 12px; }
/* A section head is `.ctx-head`'s scale, which is what dev/menu-design.html
   rule 6 names: 10px uppercase, 0.7px tracking, muted. Kept the same as the
   glass menus' group heads so the two surfaces head a group identically. */
.ce-fold-title { flex: 1; font-size: 10px; text-transform: uppercase; letter-spacing: 0.7px; font-weight: 700; }
.ce-fold-chev { display: inline-flex; color: var(--text-faint); transition: transform 0.14s; }
.ce-fold-chev svg { width: 12px; height: 12px; }
.ce-fold.open .ce-fold-chev { transform: rotate(90deg); }
.ce-fold-body { display: none; flex-direction: column; gap: 6px; padding: 7px 0 5px; }
.ce-fold.open .ce-fold-body { display: flex; }

/* Icon-only actions, right-aligned. A panel's footer carries controls and
   nothing else - the explanation a tip sentence used to print lives in the
   help bubble now (js/helpContent.js), reached with the ? tool. */
.ce-panel-foot { display: flex; align-items: center; justify-content: flex-end; gap: 6px; border-top: 1px solid var(--line); padding-top: 8px; }

/* The card-type ruleset panel (js/categoryRules.js). It is a .ce-panel, so every
   control inside already comes out at pill scale from the block below; these are
   only the two things it adds - the row a type is picked from, and the clash
   note. A type that carries rules is marked with the gold rail rather than a
   word, so the list reads at a glance and stays the same height either way. */
.catrules .ce-panel-name { font-size: 12px; font-weight: 700; color: var(--accent-2); margin: 0 0 6px; }
.ce-typerow { display: grid; grid-template-columns: 1fr auto auto; align-items: center; gap: 4px 6px;
  width: 100%; text-align: left; padding: 5px 7px; margin-bottom: 4px; border-radius: 7px;
  border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.03); color: inherit; cursor: pointer; }
.ce-typerow:hover { background: rgba(255,255,255,0.07); border-color: rgba(255,255,255,0.14); }
.ce-typerow.ruled { border-left: 2px solid var(--accent-2); border-radius: 0 7px 7px 0; }
.ce-type-name { font-size: 11.5px; font-weight: 600; }
.ce-type-sub { grid-column: 1 / -1; font-size: 10px; opacity: 0.62; }
.ce-type-warn, .ce-type-chev { display: inline-flex; opacity: 0.7; }
.ce-type-warn { color: var(--warn, #e0a33a); opacity: 1; }
.ce-type-warn svg, .ce-type-chev svg { width: 12px; height: 12px; }
.catrule-clash { display: flex; align-items: flex-start; gap: 6px; font-size: 10.5px; line-height: 1.45; }
.catrule-clash-ic { display: inline-flex; flex: 0 0 auto; margin-top: 1px; color: var(--warn, #e0a33a); }
.catrule-clash-ic svg { width: 12px; height: 12px; }
.catrule-clash-text b { color: var(--accent-2); font-weight: 600; }
.catrule-fxline { flex: 1 1 auto; min-width: 0; }
/* A value control that prints a whole phrase rather than a word, so it wraps
   instead of running off the panel. Still a .ce-seg-btn, so it takes its size
   from the one panel-scale block above. */
.ce-rules-btn { white-space: normal; text-align: left; line-height: 1.3; }
/* A lane pill whose rule is in trouble. Same weight as the red an overlapping
   zone gets: the felt already says "this needs looking at" this way. */
.tphase.tphase-warn { border-color: rgba(224,163,58,0.55); }
.tphase.tphase-warn .tphase-badge { color: var(--warn, #e0a33a); }

/* One compact scale for everything inside an element panel, at the same size
   as a turn-step pill. Scoped to .ce-panel on purpose: .ce-seg, .ce-step and
   friends are shared with the game editor and the rules windows, which keep
   their own sizing. Anything new dropped into a panel inherits this and needs
   no per-control tuning. */
.ce-panel .ce-row { gap: 6px; }
/* A ROW label, at the sequencer's `.gl-rule-lbl` scale: 11.5px, sentence case.
   It used to be uppercase micro-caps, which is the SECTION head's dress - with
   folds now heading every group, having both levels in caps left the panel with
   two things that looked like headings and nothing that looked like a row. */
.ce-panel .ce-label { font-size: 11.5px; font-weight: 600; text-transform: none; letter-spacing: 0; color: var(--text-dim); }
.ce-panel .ce-seg { border-radius: 7px; }
.ce-panel .ce-seg-btn { padding: 4px 9px; font-size: 11px; }
.ce-panel .ce-step-btn { width: 22px; height: 22px; border-radius: 7px; }
.ce-panel .ce-step-btn svg { width: 12px; height: 12px; }
.ce-panel .ce-val { min-width: 26px; font-size: 13px; }
.ce-panel .swatch { width: 20px; height: 20px; border-radius: 5px; }
.ce-panel .btn.btn-sm { padding: 4px 7px; font-size: 11px; }
/* The effect-text panel's one row: the four card-level settings as bare icons
   with tooltips, which is what every other button in a panel is. A toggle shows
   its state with the button's own lit state rather than a caption, and one that
   cannot apply right now goes idle (.row-off + disabled) with its tooltip saying
   what to change - it never leaves the row. */
.ce-fxtext-row { flex-direction: row; align-items: center; gap: 6px; }
.ce-fxtext-row .btn.btn-icon { width: 26px; height: 26px; padding: 0; }
.ce-fxtext-row .btn.btn-icon.on { background: var(--accent-2, #d8a24a); border-color: transparent; color: #14161c; }
.ce-fxtext-row .btn.btn-icon.row-off { opacity: 0.4; cursor: not-allowed; }
.ce-fxtext-row .btn.btn-icon.row-off:hover { border-color: var(--line-2); color: inherit; }
/* A row button: 20px, icon-only, ghost (rule 5). */
.ce-panel .btn.btn-icon { padding: 4px; width: 20px; height: 20px; }
.ce-panel .btn.btn-icon svg { width: 12px; height: 12px; }
.ce-panel .ce-text-input { font-size: 11.5px; padding: 5px 7px; }
/* One line, 10px, muted, when a list holds nothing - the one piece of prose a
   panel is allowed (rule 4). */
.ce-panel .ce-fx-empty { font-size: 10px; opacity: 0.7; padding: 2px 0; }
/* A row whose control is NARROW puts the label beside it instead of above it,
   which halves the height of a slider-heavy panel. Selected with :has() rather
   than by adding a class at every call site, so a row built anywhere - by a
   shared helper, by a panel, by something written next year - lands in the
   right shape without its author knowing this rule exists. The wide controls
   (a segmented wrap, a shape grid, a textarea, a picker list) are left stacked
   by simply not matching: they need the panel's full width to be legible. */
.ce-panel .ce-row:has(> .ce-step),
.ce-panel .ce-row:has(> .ce-scale),
.ce-panel .ce-row:has(> .ce-rot),
.ce-panel .ce-row:has(> select) {
  flex-direction: row; align-items: center; gap: 8px;
}
/* A fixed label column, wrapping rather than truncating. An ellipsis here is
   worse than a second line: "Box size" and "Width" both cut down to "..." at
   this width and the row stops saying which slider it is. */
.ce-panel .ce-row:has(> .ce-step) > .ce-label,
.ce-panel .ce-row:has(> .ce-scale) > .ce-label,
.ce-panel .ce-row:has(> .ce-rot) > .ce-label,
.ce-panel .ce-row:has(> select) > .ce-label {
  flex: 0 0 64px; min-width: 0; white-space: normal; line-height: 1.2;
}
.ce-panel .ce-row > .ce-scale,
.ce-panel .ce-row > .ce-rot,
.ce-panel .ce-row > select { flex: 1 1 auto; min-width: 0; }
.ce-panel .ce-row > .ce-step { margin-left: auto; }
/* The readout gives width back to the slider it belongs to. */
.ce-panel .ce-rot { gap: 6px; }
.ce-panel .ce-scale { gap: 6px; }
.ce-panel .ce-rot-val { min-width: 30px; font-size: 10px; }
.ce-panel .ce-scale-val { min-width: 32px; font-size: 10px; padding: 2px; }
/* The typable readout at panel scale. Its width is in em rather than px so it
   follows whatever font-size it lands on - which matters on a touch device,
   where every field is bumped to 16px to stop iOS zooming the page in. */
.ce-panel input.ce-num-val { font-size: 10px; padding: 1px 2px; width: 4em; }
/* The shape chips are the one grid that dominates a panel's height. At pill
   scale two rows of ten fit where one used to. */
.ce-panel .ce-shape { width: 27px; height: 27px; border-radius: 7px; }
.ce-panel .ce-shape-glyph { width: 14px; height: 14px; }
.ce-panel .ce-shapes { gap: 5px; }

/* solid / gradient segmented toggle */
.ce-seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; align-self: flex-start; }
.ce-seg-btn { padding: 6px 14px; background: var(--panel-2); color: var(--text); border: none; cursor: pointer; font-size: 12px; font-weight: 600; transition: background 0.1s; }
.ce-seg-btn + .ce-seg-btn { border-left: 1px solid var(--line); }
.ce-seg-btn:hover { background: var(--panel); }
.ce-seg-btn.sel { background: var(--accent-2); color: #221703; }
.ce-seg.ce-seg-wrap { flex-wrap: wrap; align-self: stretch; }
.ce-seg.ce-seg-wrap .ce-seg-btn { flex: 1 1 auto; text-align: center; }
/* bold / italic / underline toggles: each button previews its own effect and
   the three share equal width */
.ce-type-seg .ce-type-btn { flex: 1 1 0; min-width: 34px; text-align: center; font-size: 13px; }
.ce-type-btn.ce-type-bold { font-weight: 800; }
.ce-type-btn.ce-type-italic { font-style: italic; }
.ce-type-btn.ce-type-underline { text-decoration: underline; }

/* effect (action-card) editor */
.ce-row-inline { flex-direction: row; align-items: center; justify-content: space-between; gap: 10px; }
.ce-row-inline .ce-label { display: flex; flex-direction: column; gap: 2px; }
.ce-sub { font-size: 9.5px; text-transform: none; letter-spacing: 0; font-weight: 500; color: var(--text-faint); }
.ce-bulk-body { display: flex; flex-direction: column; gap: 12px; }
.ce-bulk-preview { flex: 1; max-height: 190px; overflow-y: auto; border: 1px solid var(--line); border-radius: 10px; background: var(--panel-2); padding: 4px 2px; }
.ce-bulk-prev-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 3px 10px; font-size: 12px; }
.ce-bulk-prev-row + .ce-bulk-prev-row { border-top: 1px solid var(--line); }
.ce-bulk-prev-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
.ce-bulk-prev-val { font-weight: 800; color: var(--text); font-variant-numeric: tabular-nums; }
.ce-bulk-prev-more { padding: 5px 10px; font-size: 11px; color: var(--text-faint); }
.ce-fx-list { display: flex; flex-direction: column; gap: 8px; }
.ce-fx-empty { font-size: 11.5px; color: var(--text-faint); padding: 4px 0; }
.ce-fx-item { border: 1px solid var(--line); border-radius: 10px; padding: 8px 9px; background: var(--panel-2); display: flex; flex-direction: column; gap: 8px; }
.ce-fx-item-head { display: flex; align-items: center; gap: 7px; }
.ce-fx-item-ic { display: inline-flex; color: var(--accent-2); }
.ce-fx-item-ic svg { width: 15px; height: 15px; }
.ce-fx-item-name { font-weight: 700; font-size: 13px; flex: 1; }
.ce-fx-rm { width: 22px; height: 22px; border-radius: 50%; border: none; background: rgba(6,9,14,0.5); color: var(--text); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
.ce-fx-rm:hover { background: var(--red); color: #fff; }
.ce-fx-rm svg { width: 12px; height: 12px; }
.ce-fx-prev { font-size: 11px; line-height: 1.35; color: var(--text-faint); border-top: 1px dashed var(--line); padding-top: 6px; }
.ce-fx-add { display: flex; flex-wrap: wrap; gap: 6px; }
.ce-fx-add-btn { display: inline-flex; align-items: center; gap: 5px; padding: 6px 9px; border-radius: 8px; border: 1px dashed var(--line-2); background: transparent; color: var(--text); cursor: pointer; font-size: 11.5px; font-weight: 600; }
.ce-fx-add-btn:hover { border-color: var(--accent); color: var(--accent-2); border-style: solid; }
.ce-fx-add-btn svg { width: 14px; height: 14px; }

/* collapsible sections in the effect panel: add-effect categories + the card
   properties group. Compact, at pill scale, matching the rest of the editor. */
.ce-acc-lbl { margin-top: 2px; }
.ce-acc-group { display: flex; flex-direction: column; gap: 6px; }
.ce-acc { border: 1px solid var(--line); border-radius: 9px; background: var(--panel-2); overflow: hidden; }
.ce-acc.open { border-color: var(--line-2); }
.ce-acc-head { display: flex; align-items: center; gap: 7px; width: 100%; padding: 7px 9px; background: none; border: none; color: var(--text); cursor: pointer; font: inherit; text-align: left; }
.ce-acc-head:hover { background: rgba(255,255,255,0.03); }
.ce-acc-ic { display: inline-flex; color: var(--accent-2); }
.ce-acc-ic svg { width: 14px; height: 14px; }
.ce-acc-title { flex: 1; font-size: 12px; font-weight: 700; }
.ce-acc-count { font-size: 10px; font-weight: 700; color: var(--text-faint); min-width: 15px; height: 15px; padding: 0 4px; border-radius: 999px; background: rgba(255,255,255,0.06); display: inline-flex; align-items: center; justify-content: center; }
.ce-acc-count:empty { display: none; }
.ce-acc-chev { display: inline-flex; color: var(--text-faint); transition: transform 0.12s; }
.ce-acc-chev svg { width: 13px; height: 13px; }
.ce-acc.open .ce-acc-chev { transform: rotate(90deg); }
.ce-acc-body { padding: 2px 9px 9px; }
.ce-acc-toggles { display: flex; flex-direction: column; gap: 8px; }

/* small on/off toggle */
.ce-toggle { flex: 0 0 auto; width: 38px; height: 22px; border-radius: 11px; border: 1px solid var(--line-2); background: var(--panel-2); cursor: pointer; position: relative; padding: 0; transition: background 0.12s; }
.ce-toggle .ce-toggle-dot { position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: var(--text-faint); transition: left 0.12s, background 0.12s; }
.ce-toggle.on { background: var(--accent-2); border-color: var(--accent-2); }
.ce-toggle.on .ce-toggle-dot { left: 18px; background: #221703; }

/* shape picker */
.ce-shapes { display: flex; flex-wrap: wrap; gap: 6px; }
.ce-shape {
  width: 34px; height: 34px; border-radius: 8px; border: 1px solid var(--line); background: var(--panel-2);
  cursor: pointer; display: inline-flex; align-items: center; justify-content: center; transition: border-color 0.1s;
}
.ce-shape:hover { border-color: var(--accent); }
.ce-shape.sel { border-color: var(--accent-2); box-shadow: inset 0 0 0 1px var(--accent-2); }
.ce-shape-glyph { width: 17px; height: 17px; background: var(--text); display: block; }
.ce-shape-circle .ce-shape-glyph { border-radius: 50%; }
.ce-shape-rounded .ce-shape-glyph { border-radius: 5px; }
.ce-shape-square .ce-shape-glyph { border-radius: 1px; }
.ce-shape-diamond .ce-shape-glyph { clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); }
.ce-shape-triangle .ce-shape-glyph { clip-path: polygon(50% 4%, 100% 100%, 0 100%); }
.ce-shape-hexagon .ce-shape-glyph { clip-path: polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%); }
.ce-shape-pentagon .ce-shape-glyph { clip-path: polygon(50% 0, 100% 38.2%, 81% 100%, 19% 100%, 0 38.2%); }
.ce-shape-banner .ce-shape-glyph { width: 21px; height: 11px; clip-path: polygon(0 0, 100% 0, 80% 50%, 100% 100%, 0 100%, 20% 50%); }
.ce-shape-rule .ce-shape-glyph { width: 21px; height: 5px; clip-path: polygon(0 50%, 12% 0, 88% 0, 100% 50%, 88% 100%, 12% 100%); }
/* a slider or row that can't apply to the current selection stays visible,
   greyed, with its reason in the tooltip (controls never disappear) */
.ce-row.row-off { opacity: 0.4; }
.ce-row.row-off input { pointer-events: none; }
/* theme-linked swatches (the type colour system): a dashed gold ring marks a
   swatch (or a field's trigger) that FOLLOWS the card's theme colour */
.ce-swatch-theme { outline: 1px dashed var(--accent, #c79a3a); outline-offset: 1px; }
.ce-theme-trio { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
.ce-theme-trio .ce-sub { margin-right: 5px; }
/* an option that can't apply to the current selection stays visible, greyed,
   with its reason in the tooltip (controls never disappear) */
.ce-seg-btn.disabled { opacity: 0.4; cursor: not-allowed; }
.ce-shape-none .ce-shape-glyph { background: none; border: none; display: flex; align-items: center; justify-content: center; }
.ce-shape-none .ce-shape-glyph svg { width: 17px; height: 17px; }

/* element being dragged on the card face */
.ce-card .free-el { transition: none; }
.ce-card .free-el.ce-dragging { z-index: 5; filter: brightness(1.12); cursor: grabbing; }
.ce-card .free-el:hover { outline: 1px dashed rgba(255,255,255,0.4); outline-offset: 3px; border-radius: 4px; }
/* background shapes are interactive in the editor: grab the body to move, the
   corner handle to resize. Selected/hovered shapes show a dashed outline. */
.ce-card .free-shape { pointer-events: auto; }
.ce-card .free-shape:hover, .ce-card .free-shape.ce-shape-sel { outline: 1px dashed rgba(255,255,255,0.55); outline-offset: 2px; }
.ce-card .free-shape.ce-dragging { filter: brightness(1.12); }
/* One grip, two homes, one paint - a resize grip is a resize grip whatever it
   is resizing. A shape's hangs off its own bottom-right corner. The ARTWORK's
   rides in its own editor-only layer (bindArt in cardEditor.js) and is placed
   in face percent, centred on the point it marks, because .free-art and
   .free-artwin both carry z-index 0: a grip parented to either would open
   inside that stacking context and paint under every element above it, and
   .free-art clips to the face on top of that. */
.ce-shape-handle, .ce-art-handle {
  position: absolute; width: 13px; height: 13px;
  background: var(--accent-2, #c79a3a); border: 1.5px solid #0b0d11; border-radius: 3px;
  cursor: nwse-resize; display: none; z-index: 6; box-shadow: 0 1px 3px rgba(0,0,0,0.5);
}
.ce-shape-handle { right: -6px; bottom: -6px; }
.ce-card .free-shape:hover .ce-shape-handle, .ce-card .free-shape.ce-shape-sel .ce-shape-handle { display: block; }
/* The artwork's grip layer sits above the elements (z-index 2), so the grip is
   never buried under a name or a badge that happens to sit in that corner. The
   layer itself is inert; only the grip takes the pointer. */
.ce-art-layer { position: absolute; inset: 0; pointer-events: none; z-index: 7; }
.ce-art-handle { transform: translate(-50%, -50%); pointer-events: auto; touch-action: none; }
/* Full-face art marks the card's own bottom-right corner, which is rounded, so
   the grip tucks just inside it rather than floating off the curve. */
.ce-art-handle.ce-art-handle-face { margin-left: -8px; margin-top: -8px; }
.ce-card .ce-art-handle.on { display: block; }
/* Dragging the picture: dim it a touch so it reads as the thing being moved,
   the same feedback a dragged shape gets. */
.ce-card .free-art.ce-dragging, .ce-card .free-artwin.ce-dragging { filter: brightness(1.12); }
/* The inner is overflow:visible everywhere now (see .tcard-free-inner), so a
   corner badge shows in full at the table too, not just here. Shape clipping is
   handled by the .free-shapes layer: always clipped at the table, and in the
   editor whichever the Trim toggle says (.ce-trim). The handle's own -6px
   overhang is overridden inline by placeHandle when the trim would cut it off. */
/* the shapes stack list + selected-shape controls in the panel */
.ce-shape-list { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.ce-shape-sep { height: 1px; background: var(--line, rgba(255,255,255,0.1)); margin: 8px 0 2px; }
.ce-shape-arrange { display: flex; gap: 6px; }
.ce-shape-arrange .btn { padding: 5px 8px; }
.ce-shape-arrange .ce-shape-del { margin-left: auto; }
/* Compact layout for the Shapes panel: it carries the most rows, so tighten the
   spacing enough that every control fits without the panel needing to scroll.
   overflow-y:auto stays as a graceful fallback on very short screens. */
.ce-panel-shapes { gap: 7px; padding-bottom: 12px; }
.ce-panel-shapes .ce-row { gap: 4px; }
.ce-panel-shapes .ce-shape-sep { margin: 3px 0 1px; }
.ce-panel-shapes .swatch { width: 20px; height: 20px; }
.ce-panel-shapes .swatches { gap: 5px; }
.ce-panel-shapes .ce-panel-foot { padding-top: 8px; }
.ce-card .free-text.ce-inline-edit { outline: 2px solid var(--accent, #c79a3a); outline-offset: 3px; border-radius: 4px; cursor: text; background: rgba(12,16,22,0.4); }
.ce-card .free-text.ce-inline-edit:hover { outline: 2px solid var(--accent, #c79a3a); }
/* Text panel: the stack of boxes on the card, each a chip showing its first
   words; the selected one and the box on the card share the accent outline. */
.ce-text-list { display: flex; flex-direction: column; gap: 5px; align-items: stretch; width: 100%; }
.ce-text-row { display: flex; gap: 6px; align-items: center; }
.ce-text-row .ce-text-chip { flex: 1; min-width: 0; }
.ce-text-chip-del { flex: 0 0 auto; padding: 5px 8px; }
.ce-text-chip { text-align: left; padding: 5px 8px; border: 1px solid var(--line, rgba(255,255,255,0.12)); border-radius: 7px; background: rgba(255,255,255,0.04); color: var(--text); font-size: 11.5px; cursor: pointer; }
.ce-text-chip:hover { border-color: var(--accent); }
.ce-text-chip.sel { border-color: var(--accent-2); box-shadow: inset 0 0 0 1px var(--accent-2); }
.ce-text-chip-t { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ce-card .free-text.ce-text-sel:not(.ce-inline-edit) { outline: 1px dashed rgba(255,255,255,0.55); outline-offset: 2px; border-radius: 4px; }
/* decorative-icon panel: the icon chips show the glyph itself, and the selected
   icon on the card shares the same dashed outline as text/category. */
.ce-icon-chip { display: inline-flex; align-items: center; justify-content: center; padding: 5px; }
.ce-icon-chip-g { display: inline-flex; }
.ce-icon-chip-g svg { width: 18px; height: 18px; display: block; }
.ce-card .free-icon.ce-cat-sel { outline: 1px dashed rgba(255,255,255,0.55); outline-offset: 3px; border-radius: 4px; }

/* category panel: the card's categories as removable tags, a type-to-filter
   search to add more, and the selected pill outlined on the card. Matches the
   text-list scale and the glass surface it sits on. */
.ce-cat-list { display: flex; flex-wrap: wrap; gap: 5px; width: 100%; }
.ce-cat-tag { display: inline-flex; align-items: center; gap: 5px; padding: 3px 5px 3px 7px; border: 1px solid var(--line, rgba(255,255,255,0.12)); border-radius: 999px; background: rgba(255,255,255,0.04); color: var(--text); font-size: 11.5px; cursor: pointer; max-width: 100%; }
.ce-cat-tag:hover { border-color: var(--accent); }
.ce-cat-tag.sel { border-color: var(--accent-2); box-shadow: inset 0 0 0 1px var(--accent-2); }
.ce-cat-tag-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; box-shadow: 0 0 0 1px rgba(0,0,0,0.25); }
.ce-cat-tag-t { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ce-cat-tag-x { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; padding: 0; border: 0; background: transparent; color: var(--muted, #9aa5b1); cursor: pointer; border-radius: 50%; }
.ce-cat-tag-x:hover { background: rgba(255,255,255,0.1); color: var(--text); }
.ce-cat-tag-x svg { width: 11px; height: 11px; }
.ce-catsearch { position: relative; width: 100%; }
.ce-catsearch-results { position: absolute; left: 0; right: 0; top: calc(100% + 3px); z-index: 20; max-height: 168px; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; padding: 3px; border: 1px solid var(--line, rgba(255,255,255,0.12)); border-radius: 8px; background: rgba(15,20,28,0.96); backdrop-filter: blur(12px); box-shadow: 0 8px 22px rgba(0,0,0,0.4); }
.ce-catsearch-row { display: flex; align-items: center; gap: 6px; text-align: left; padding: 5px 7px; border: 0; border-radius: 6px; background: transparent; color: var(--text); font-size: 11.5px; cursor: pointer; }
.ce-catsearch-row:hover { background: rgba(255,255,255,0.08); }
.ce-catsearch-add { color: var(--accent-2); }
.ce-catsearch-add svg { width: 12px; height: 12px; }
.ce-card .free-category.ce-cat-sel { outline: 1px dashed rgba(255,255,255,0.55); outline-offset: 2px; border-radius: 4px; }

/* suit picker */
.suit-picker { display: flex; gap: 12px; justify-content: center; }
.suit-pick { width: 64px; height: 64px; border-radius: 12px; border: 1px solid var(--line-2); background: var(--panel-2); color: var(--text); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: border-color 0.12s, transform 0.1s; }
.suit-pick:hover { border-color: var(--accent); transform: translateY(-2px); }
.suit-pick svg { width: 34px; height: 34px; }

/* ============ card-creator upgrades: browser, art, frame, juice ============ */

/* juicy card minis wherever they appear (gallery, Decks popover) */
.ce-mini-wrap { transition: transform 0.14s cubic-bezier(0.34,1.56,0.64,1); }
.ce-mini-wrap:hover { transform: translateY(-5px) scale(1.05) rotate(-1deg); }

/* left rail: the same "Add to the card" / "Style" options the on-card click
   menu shows, but always visible - reuses .table-tools.floating/.tool-btn
   (the tableEditor's own left tool rail) for a consistent look. */
.ce-rail .tool-group-label:first-child { margin-top: 2px; }
.tool-btn { justify-content: flex-start; }
.tool-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; }
.tool-btn.on { border-color: rgba(216,162,74,0.35); background: rgba(216,162,74,0.08); }
.table-tools.floating .tool-btn.on { background: rgba(216,162,74,0.12); border-color: rgba(216,162,74,0.4); }
.tool-btn.on:hover { border-color: var(--accent); }
.tool-on-ic { display: inline-flex; color: var(--accent-2); flex: 0 0 auto; }
.tool-on-ic svg { width: 14px; height: 14px; }

/* shortcut dock: a growing column of per-element jump buttons sitting just
   left of the card. Right edge is pinned half a card plus a 12px gap left of
   the felt centre, so it never touches the card at any state or any shape (a
   landscape card is wider, which is why the half-card comes from
   --ce-card-w rather than being written here as 145px).
   It hangs from the card's TOP edge rather than the card's middle: the
   effect-assembly strip (.pb-stage.ce-fx-oncard) is 150% of the card's width
   and bottom-anchored, so it reaches into this column's lane. Growing downward
   from the top keeps the buttons clear of it, and the group fold below caps the
   column at three buttons so it can never grow far enough down to reach it.
   --ce-card-h is the live card's rendered height, set by renderCardFace. */
.ce-dock {
  position: absolute; top: calc(50% - var(--ce-card-h, 406px) / 2 - 2px); right: calc(50% + var(--ce-card-w, 290px) / 2 + 12px); transform: none;
  z-index: 15; display: flex; flex-direction: column; gap: 8px; align-items: stretch;
  max-height: var(--ce-card-h, 406px); overflow-y: auto; padding: 2px;
}
/* The column stays at that same short distance from the card whether or not the
   effect builder is up - it reads as part of the card. The strip yields instead:
   it stops growing where the column ends (see --ce-dock-h below). */
.ce-dock-btn {
  display: flex; align-items: center; gap: 8px; width: 100%; min-height: 40px;
  padding: 0 10px 0 9px; border-radius: 10px; cursor: pointer;
  background: rgba(15,20,28,0.55); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,0.1); color: var(--text);
  transition: transform 0.1s cubic-bezier(0.34,1.56,0.64,1), border-color 0.12s, background 0.12s;
}
.ce-dock-btn:hover { border-color: var(--accent); background: rgba(15,20,28,0.72); }
.ce-dock-btn:active { transform: scale(0.94); }
.ce-dock-btn.on { border-color: var(--accent); background: rgba(216,162,74,0.14); }
.ce-dock-ic { display: inline-flex; flex: 0 0 auto; color: var(--accent-2); }
.ce-dock-ic svg { width: 17px; height: 17px; }
.ce-dock-val {
  font-size: 11px; font-weight: 700; color: var(--text-faint);
  max-width: 46px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A folded group button: the count rides as a small pill so it reads as "how
   many are in here", not as one element's value. */
.ce-dock-btn.ce-dock-group .ce-dock-val {
  min-width: 15px; height: 15px; padding: 0 4px; border-radius: 999px;
  background: rgba(255,255,255,0.08); color: var(--text-faint);
  display: inline-flex; align-items: center; justify-content: center; font-size: 10px;
}

/* Decks popover - a lightweight list of deck stacks, opened from the rail's
   "Decks" button now that decks no longer live in a permanent shelf */
.ce-decks-back { position: fixed; inset: 0; z-index: 265; }
.ce-decks-panel {
  position: absolute; left: 160px; top: 50%; transform: translateY(-50%);
  width: 240px; max-height: calc(100vh - 60px); max-height: calc(100dvh - 60px); overflow-y: auto;
  background: rgba(15,20,28,0.82); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.12);
  border-radius: 14px; box-shadow: var(--shadow); padding: 12px 14px 14px;
  animation: ce-rise 0.16s cubic-bezier(0.34,1.4,0.64,1);
}
.ce-decks-head { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.ce-decks-head h3 { margin: 0; font-size: 15px; font-weight: 800; flex: 1; }
.ce-decks-grid { display: flex; flex-wrap: wrap; gap: 12px; }

/* the card creator's Library rail: current-deck banner (arrived via "Edit
   these cards") and the cross-game picker share the same list styling */
/* A compact breadcrumb pill sitting in the top-left corner beside the game
   name, so it never covers the card or its title. Single row, pill-shaped to
   match the neighbouring name chip. */
.ce-scope-banner {
  margin: 0; padding: 5px 11px; border-radius: 999px;
  background: rgba(15,20,28,0.55); backdrop-filter: blur(8px);
  border: 1px solid rgba(216,162,74,0.3);
  display: flex; flex-direction: row; align-items: center; gap: 9px;
}
.ce-scope-label { font-size: 11.5px; color: var(--text-dim); line-height: 1.2; white-space: nowrap; }
.ce-scope-label b { color: var(--text); }
.ce-scope-clear {
  align-self: center; background: none; border: none; color: var(--accent-2);
  font-size: 11px; font-weight: 700; cursor: pointer; padding: 0; white-space: nowrap;
}
.ce-scope-clear:hover { text-decoration: underline; }
.ce-chip.ce-scope-chip { background: rgba(216,162,74,0.1); }

.ce-gamepick-modal {
  width: 100%; max-width: 380px; max-height: 82vh; overflow-y: auto;
  background: rgba(15,20,28,0.82); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.12);
  border-radius: 18px; box-shadow: var(--shadow); padding: 16px 18px 18px;
  animation: ce-rise 0.2s cubic-bezier(0.34,1.4,0.64,1);
}
.ce-gamepick-list { display: flex; flex-direction: column; gap: 3px; max-height: 60vh; overflow-y: auto; }
.ce-gamepick-game, .ce-gamepick-deck {
  display: flex; align-items: center; gap: 8px; padding: 8px 9px; border-radius: 9px;
  cursor: pointer; border: 1px solid transparent;
}
.ce-gamepick-game:hover, .ce-gamepick-deck:hover { background: var(--bg-3); border-color: var(--line-2); }
.ce-gamepick-game { font-weight: 700; margin-top: 6px; }
.ce-gamepick-game:first-child { margin-top: 0; }
.ce-gamepick-game.current { color: var(--accent-2); }
.ce-gamepick-deck { margin-left: 12px; font-size: 12.5px; color: var(--text-dim); }
.ce-gamepick-deckic { display: flex; color: var(--text-faint); }
.ce-gamepick-deckic svg { width: 13px; height: 13px; }
.ce-gamepick-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ce-gamepick-count { font-size: 10.5px; color: var(--text-faint); flex-shrink: 0; }

.ce-cardback-stage { display: flex; justify-content: center; }
.ce-cardback-preview { width: 150px; }
.ce-cardback-preview .tcard { width: 100%; }
.ce-cardback-preview > .ce-cardback-empty {
  width: 150px; aspect-ratio: 5 / 7; border-radius: 10px;
  border: 1px dashed var(--line-2); background: #0b0d11;
}
.ce-cardback-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 6px; color: var(--text-faint); font-size: 11px; text-align: center; padding: 6px;
}
.ce-cardback-empty svg { width: 22px; height: 22px; }

/* "New card" template picker - a visual preview per option, not a plain list */
.ce-tmpl-back {
  position: fixed; inset: 0; z-index: 300; background: rgba(6,9,14,0.78); backdrop-filter: blur(5px);
  display: flex; align-items: center; justify-content: center; padding: 24px;
  animation: ce-fade 0.16s ease;
}
.ce-tmpl-panel {
  width: 100%; max-width: 660px; max-height: 82vh; overflow-y: auto;
  background: rgba(15,20,28,0.82); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.12);
  border-radius: 18px; box-shadow: var(--shadow); padding: 16px 18px 20px;
  animation: ce-rise 0.2s cubic-bezier(0.34,1.4,0.64,1);
}
.ce-tmpl-head { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.ce-tmpl-head h3 { margin: 0; font-size: 18px; font-weight: 800; flex: 1; }
.ce-tmpl-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 14px; }
.ce-tmpl-cell {
  display: flex; flex-direction: column; align-items: center; gap: 7px; padding: 14px 10px 12px;
  border: 1px solid var(--line); border-radius: 12px; background: var(--panel-2); cursor: pointer;
  text-align: center; transition: border-color 0.12s, transform 0.1s, background 0.12s;
}
.ce-tmpl-cell:hover { border-color: var(--accent); transform: translateY(-3px); background: var(--bg-3); }
.ce-tmpl-cell .tcard { width: 108px; pointer-events: none; }
.ce-tmpl-name { font-size: 13px; font-weight: 700; margin-top: 2px; }
.ce-tmpl-desc { font-size: 11px; color: var(--text-dim); line-height: 1.3; }

/* artwork panel + frame controls reuse .ce-row; a couple of extras */
.ce-art-thumb { width: 100%; height: 96px; border-radius: 8px; overflow: hidden; border: 1px solid var(--line-2); background: #0b0d11; }
.ce-art-thumb img { width: 100%; height: 100%; object-fit: cover; }

/* ============ celebratory burst when a card is created ============ */
.ce-burst { position: absolute; inset: 0; pointer-events: none; overflow: visible; z-index: 30; }
.ce-confetti {
  position: absolute; top: 46%; left: 50%; width: 9px; height: 9px; border-radius: 2px;
  transform: translate(-50%, -50%); opacity: 0;
  animation: ce-pop 0.95s cubic-bezier(0.22,0.61,0.36,1) forwards;
}
@keyframes ce-pop {
  0% { opacity: 1; transform: translate(-50%,-50%) rotate(0) scale(0.4); }
  70% { opacity: 1; }
  100% { opacity: 0; transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) rotate(var(--rot)) scale(1); }
}
/* the card gives a happy bounce as it lands */
.ce-holder .ce-card.ce-just-made { animation: ce-landed 0.5s cubic-bezier(0.34,1.56,0.64,1); }
@keyframes ce-landed { 0% { transform: scale(0.9); } 55% { transform: translateY(-8px) scale(1.05); } 100% { transform: none; } }

/* ============ full-screen card gallery ============ */
.ce-gallery-back {
  position: fixed; inset: 0; z-index: 300; background: rgba(6,9,14,0.78); backdrop-filter: blur(5px);
  display: flex; align-items: center; justify-content: center; padding: 24px;
  animation: ce-fade 0.16s ease;
}
@keyframes ce-fade { from { opacity: 0; } to { opacity: 1; } }
.ce-gallery {
  width: 100%; max-width: 1120px; max-height: 88vh; display: flex; flex-direction: column;
  background: rgba(15,20,28,0.82); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.12);
  border-radius: 18px; box-shadow: var(--shadow); overflow: hidden;
  animation: ce-rise 0.2s cubic-bezier(0.34,1.4,0.64,1);
}
@keyframes ce-rise { from { opacity: 0; transform: translateY(18px) scale(0.98); } to { opacity: 1; transform: none; } }
.ce-gal-head { display: flex; align-items: center; gap: 12px; padding: 14px 16px; border-bottom: 1px solid var(--line); }
.ce-gal-title { display: flex; align-items: center; gap: 9px; }
.ce-gal-title-ic { display: inline-flex; color: var(--accent-2); }
.ce-gal-title-ic svg { width: 22px; height: 22px; }
.ce-gal-title h3 { margin: 0; font-size: 18px; font-weight: 800; }
/* Layout only - the icon and the text gutter come from the shared .search-wrap
   pattern near the top of this file. The element selector is deliberate: a bare
   .ce-gal-search loses to the global input[type="text"] rule and keeps none of
   the look below. */
.ce-gal-search-wrap { flex: 1; max-width: 340px; margin-left: auto; }
input.ce-gal-search { width: 100%; padding-top: 9px; padding-bottom: 9px; padding-right: 12px; border-radius: 10px; border: 1px solid var(--line-2); background: var(--panel-2); color: var(--text); font-size: 14px; }
input.ce-gal-search:focus { border-color: var(--accent); outline: none; }
.ce-gal-tools { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; padding: 12px 16px; border-bottom: 1px solid var(--line); }
.ce-gal-tool { display: flex; align-items: center; gap: 7px; }
.ce-gal-tool-ic { display: inline-flex; color: var(--text-faint); }
.ce-gal-tool-ic svg { width: 17px; height: 17px; }
.ce-gal-select { padding: 8px 10px; border-radius: 9px; border: 1px solid var(--line-2); background: var(--panel-2); color: var(--text); font-size: 13px; font-weight: 600; cursor: pointer; }
.ce-gal-chips { display: flex; flex-wrap: wrap; gap: 6px; flex: 1; }
.ce-chip {
  padding: 6px 12px; border-radius: 999px; border: 1px solid var(--line-2); background: var(--panel-2);
  color: var(--text-dim); font-size: 12px; font-weight: 700; cursor: pointer; transition: all 0.12s;
}
.ce-chip:hover { border-color: var(--accent); color: var(--text); }
.ce-chip.on { background: var(--accent-2); border-color: var(--accent-2); color: #221703; }
/* Income panel: group-tag chips + inline add input. */
.ce-chips-wrap { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; flex: 1; }
.ce-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.ce-chips-wrap .ce-chip { display: inline-flex; align-items: center; gap: 5px; cursor: default; }
.ce-chip-x { display: inline-flex; padding: 0; border: none; background: none; color: inherit; cursor: pointer; opacity: 0.7; }
.ce-chip-x:hover { opacity: 1; }
.ce-chip-x svg { width: 12px; height: 12px; }
.ce-chips-wrap .ce-text-input { flex: 1 1 120px; min-width: 120px; }
.ce-gal-view { display: flex; align-items: center; gap: 8px; }
.ce-gal-listbtn.sel { background: var(--accent-2); color: #221703; }
.ce-gal-grid { flex: 1; overflow: auto; padding: 18px; display: grid; gap: 16px; align-content: start; }
.ce-gal-grid.size-sm { grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); }
.ce-gal-grid.size-md { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
.ce-gal-grid.size-lg { grid-template-columns: repeat(auto-fill, minmax(196px, 1fr)); }
.ce-gal-grid.list { display: flex; flex-direction: column; gap: 3px; padding: 12px 14px; }
.ce-gal-cell { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 6px; }
.ce-gal-cell .tcard { width: 100%; cursor: pointer; }
.ce-gal-cellname { font-size: 12px; color: var(--text-dim); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: center; }
.ce-gal-cell .ce-mini-chk { top: 8px; right: 8px; }
.ce-gal-foot { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-top: 1px solid var(--line); }

/* ---- empty panels hold their shape (ghostSlots in ui.js) ----
   A card panel used to be content-sized, so with nothing in it the frame
   collapsed to a strip and jumped back out the moment the first card existed.
   The floor below is half the answer; the slots the grid draws are the other
   half, and they are what keeps a panel holding ONE card full as well. The
   grid clips and fades its last row when every cell is a slot, which is what
   says "this keeps going" without anything having to count rows. */
.ce-gallery { min-height: min(86vh, 720px); }
.ce-decks-panel { min-height: min(58vh, 380px); display: flex; flex-direction: column; }
/* flex-basis 0, so the slots fill the popover rather than pushing it taller:
   a grid that grows with its own content is counting rows against a box its
   own count just moved */
.ce-decks-grid { flex: 1 1 0; min-height: 0; overflow-y: auto; align-content: flex-start; }
.ce-decks-grid > .ce-shelf-hint { flex: 0 0 100%; padding: 2px 2px 6px; }
/* the grid is auto-fill columns, so a hint left to itself prints one word per
   line in a 140px column */
.ce-gal-grid > .ce-shelf-hint { grid-column: 1 / -1; }
.ce-gal-grid.has-ghosts, .ce-decks-grid.has-ghosts {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(180deg, #000 52%, rgba(0,0,0,0.2) 86%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 52%, rgba(0,0,0,0.2) 86%, transparent 100%);
}
.ce-ghost-cell { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 6px; }
.ce-ghost-card {
  width: 100%; aspect-ratio: 5 / 7; border-radius: 12px;
  border: 1px dashed rgba(255,255,255,0.15); background: rgba(255,255,255,0.022);
}
.ce-ghost-line { width: 58%; height: 7px; border-radius: 999px; background: rgba(255,255,255,0.055); }
.ce-ghost-word { font-size: 11px; font-weight: 700; color: var(--accent-2); }
/* the first slot is the one you can press - a grid of slots that were only
   decor would be a wall you have to look somewhere else to get past */
.ce-ghost-cell.add .ce-ghost-card, .ce-ghost-stack.add .ce-ghost-stack-face {
  border-color: rgba(216,162,74,0.55); background: rgba(216,162,74,0.07); cursor: pointer;
  display: flex; align-items: center; justify-content: center; color: var(--accent-2);
  transition: background 0.12s;
}
.ce-ghost-cell.add .ce-ghost-card:hover, .ce-ghost-stack.add .ce-ghost-stack-face:hover { background: rgba(216,162,74,0.14); }
.ce-ghost-cell.add .ce-ghost-card svg { width: 26px; height: 26px; }
.ce-ghost-stack.add .ce-ghost-stack-face svg { width: 20px; height: 20px; }
.ce-ghost-stack { flex: 0 0 auto; width: 58px; display: flex; flex-direction: column; align-items: center; gap: 4px; }
.ce-ghost-stack-face { width: 50px; height: 70px; border-radius: 8px; border: 1px dashed rgba(255,255,255,0.15); background: rgba(255,255,255,0.022); }
.ce-ghost-stack .ce-ghost-line { width: 34px; }
/* a wrapped word makes the first slot taller than the row beside it */
.ce-ghost-stack .ce-ghost-word { font-size: 10.5px; white-space: nowrap; }

.ce-gal-selinfo { font-size: 12.5px; color: var(--text-faint); margin-right: auto; }

/* deck builder: source tabs, sub-tabs, per-card copy count, save bar */
.dbx-tabs { display: flex; gap: 4px; padding: 10px 16px 0; }
.dbx-tab {
  padding: 8px 16px; border: 1px solid var(--line); border-bottom: none;
  border-radius: 9px 9px 0 0; background: var(--panel-2); color: var(--text-dim);
  font-size: 13px; font-weight: 700; cursor: pointer; transition: background 0.12s, color 0.12s;
}
.dbx-tab:hover { color: var(--text); }
.dbx-tab.on { background: var(--accent-2); color: #221703; border-color: var(--accent-2); }
.dbx-subs { display: flex; flex-wrap: wrap; gap: 6px; padding: 10px 16px 0; }
.dbx-sub {
  padding: 5px 12px; border-radius: 999px; border: 1px solid var(--line-2);
  background: var(--panel-2); color: var(--text-dim); font-size: 12px; font-weight: 700;
  cursor: pointer; transition: all 0.12s;
}
.dbx-sub:hover { border-color: var(--accent); color: var(--text); }
.dbx-sub.on { background: var(--accent); border-color: var(--accent); color: #221703; }
.dbx-count {
  position: absolute; bottom: 30px; left: 8px; padding: 1px 7px; border-radius: 999px;
  background: var(--accent); color: #221703; font-size: 11px; font-weight: 800; z-index: 3;
}
.dbx-savebar { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-top: 1px solid var(--line); background: var(--panel-2); }
.dbx-savelabel { font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-faint); }
.dbx-name { flex: 1; max-width: 320px; }

/* per-card actions (duplicate / delete), shown on hover wherever a mini
   card appears - the bottom shelf, the gallery grid, and the list rows below */
.ce-gal-acts { position: absolute; top: 8px; left: 8px; display: flex; gap: 5px; opacity: 0; transition: opacity 0.12s; z-index: 3; }
.ce-gal-cell:hover .ce-gal-acts { opacity: 1; }
.ce-gal-act { width: 26px; height: 26px; border-radius: 7px; border: none; background: rgba(6,9,14,0.82); color: #fff; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; transition: background 0.12s, transform 0.1s; }
.ce-gal-act:hover { background: var(--accent); color: #221703; transform: scale(1.08); }
.ce-gal-act.danger:hover { background: var(--red); color: #fff; }
.ce-gal-act svg { width: 14px; height: 14px; }

/* ultra-compact list rows - a swatch instead of a full card render, name,
   inline stat badges and tag icons, all on one thin line. Built so a big set
   (a 90-card deck, say) can be scanned 20-30 rows at a time with no scrolling
   gymnastics, which is the entire point of a "list view" over the grid. */
.ce-list-row {
  display: flex; align-items: center; gap: 9px; padding: 4px 8px 4px 10px; border-radius: 7px;
  border: 1px solid transparent; border-left: 4px solid transparent; background: var(--panel-2); cursor: pointer;
  transition: background 0.1s, border-color 0.1s;
}
.ce-list-row:hover { border-color: var(--line-2); background: var(--bg-3); }
.ce-list-row:hover.has-color { border-left-color: var(--rowc); }
.ce-list-row.sel { outline: 2px solid var(--accent); }
.ce-list-row.editing { border-color: var(--accent-2); }
.ce-list-row.editing.has-color { border-left-color: var(--rowc); }
/* the row itself carries the card's colour: a solid left bar plus a soft
   full-row tint, instead of a separate swatch chip - no images anywhere here */
.ce-list-row.has-color {
  border-left-color: var(--rowc);
  background: color-mix(in srgb, var(--rowc) 16%, var(--panel-2));
}
.ce-list-row.has-color:hover { background: color-mix(in srgb, var(--rowc) 26%, var(--panel-2)); }
.ce-list-name { flex: 1 1 auto; min-width: 60px; font-size: 13px; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ce-list-badges { display: flex; gap: 4px; flex: 0 0 auto; }
.ce-list-badge {
  display: inline-flex; align-items: center; gap: 3px; font-size: 11px; font-weight: 800;
  color: var(--text-dim); background: rgba(255,255,255,0.05); border-radius: 5px; padding: 1px 5px;
}
.ce-list-badge svg { width: 10px; height: 10px; }
.ce-list-badge.num { color: var(--accent-2); }
.ce-list-badge.atk { color: #dba55d; }
.ce-list-badge.def { color: #78aee0; }
.ce-list-badge.hp { color: #e2897b; }
.ce-list-tags { display: flex; gap: 4px; flex: 0 0 auto; }
.ce-list-tagicon {
  display: inline-flex; align-items: center; gap: 4px; color: var(--text-faint);
  font-size: 11px; font-weight: 600; white-space: nowrap;
}
.ce-list-tagicon svg { width: 12px; height: 12px; flex: 0 0 auto; }
.ce-list-row .ce-gal-acts { position: static; opacity: 0; flex: 0 0 auto; }
.ce-list-row:hover .ce-gal-acts { opacity: 1; }
.ce-list-row .ce-gal-act { width: 20px; height: 20px; border-radius: 5px; }
.ce-list-row .ce-gal-act svg { width: 11px; height: 11px; }
.ce-list-row .ce-mini-chk { position: static; flex: 0 0 auto; width: 18px; height: 18px; }
.ce-list-row .ce-mini-chk svg { width: 11px; height: 11px; }

/* deck-tray "same look for every card" panel */
.ce-deck-look { display: flex; flex-direction: column; gap: 11px; padding: 12px 13px; border: 1px solid var(--line); border-radius: 12px; background: var(--panel-2); margin-bottom: 16px; }
.ce-deck-look .ce-row { gap: 7px; }
.ce-deck-look .swatches { display: flex; flex-wrap: wrap; gap: 6px; }
.ce-deck-look .swatch { width: 24px; height: 24px; border-radius: 6px; cursor: pointer; border: 1px solid var(--line-2); }
.ce-deck-look .swatch.sel { box-shadow: 0 0 0 2px var(--accent-2); }
.ce-deck-look .ce-swatch-none { display: inline-flex; align-items: center; justify-content: center; background: var(--panel); color: var(--text-faint); }
.ce-deck-look .ce-swatch-none svg { width: 13px; height: 13px; }
.ce-deck-look input[type="range"] { width: 100%; accent-color: var(--accent); cursor: pointer; }

/* same felt as the play table (shared themeable texture), plus a gold spotlight
   behind the card. Colour layer is var(--felt-image) so it recolours with the
   chosen felt theme like every other felt. */
.ce-view .felt {
  background:
    radial-gradient(60% 46% at 50% 40%, rgba(216,162,74,0.14), transparent 70%),
    radial-gradient(120% 90% at 50% -10%, rgba(255,255,255,0.06), transparent 55%),
    radial-gradient(140% 120% at 50% 120%, rgba(0,0,0,0.6), transparent 60%),
    var(--felt-fibre),
    var(--felt-image);
  background-color: #123a2e;
  background-size: auto, auto, auto, var(--felt-fibre-size), cover;
  background-position: center;
  background-repeat: no-repeat, no-repeat, no-repeat, repeat, no-repeat;
  background-blend-mode: normal, normal, normal, overlay, normal;
}
.ce-view .felt::after { opacity: 0.4; }
/* buttons across the creator get a springy press */
.ce-view .btn, .ce-gallery .btn, .ce-panel .btn { transition: transform 0.1s cubic-bezier(0.34,1.56,0.64,1), background 0.14s, border-color 0.14s, color 0.14s; }
.ce-view .btn:active, .ce-gallery .btn:active, .ce-panel .btn:active { transform: scale(0.94); }
.ce-fx-add-btn:active, .ce-chip:active, .ce-seg-btn:active, .ce-icon:active, .ce-shape:active { transform: scale(0.94); }
.suit-pick.suit-red { color: #c0392b; }
.suit-pick.suit-black { color: #e7e2d4; }

/* custom stat rows in editor */
.stat-rows { display: flex; flex-direction: column; gap: 8px; }
.stat-rows .stat-row { display: grid; grid-template-columns: 1fr 110px auto; gap: 8px; align-items: center; }

/* color swatches */
.swatches { display: flex; gap: 8px; flex-wrap: wrap; }
/* Heads-up shown above a deck/counter recolour swatch row when the Player
   colours setting is overriding that piece's colour, so a pick won't visibly
   apply (ownerColorNote, tableEditor.js). Compact amber note that sits on the
   flyout glass. */
.owner-color-note {
  display: flex; gap: 6px; align-items: flex-start; margin-bottom: 9px; max-width: 216px;
  font-size: 11px; line-height: 1.4; color: var(--accent-2);
}
.owner-color-note-ic { flex: 0 0 13px; display: inline-flex; margin-top: 1px; }
.owner-color-note-ic svg { width: 13px; height: 13px; }
.swatch {
  width: 30px; height: 30px; border-radius: 8px; border: 2px solid transparent; cursor: pointer;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,0.4);
}
.swatch.sel { border-color: var(--text); }
/* palette swatches that line up with a number deck get a small corner dot */
.swatch.matches { position: relative; }
.swatch.matches::after { content: ""; position: absolute; top: 2px; right: 2px; width: 5px; height: 5px;
  border-radius: 50%; background: #fff; box-shadow: 0 0 0 1px rgba(0,0,0,0.5); }
/* custom swatch: opens the OS color picker for any color, not just the fixed palette */
.swatch-custom { position: relative; display: inline-flex; align-items: center; justify-content: center;
  background: var(--panel-2); color: var(--text-faint); overflow: hidden; }
.swatch-custom svg { width: 14px; height: 14px; }
.swatch-custom input[type="color"] { position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; cursor: pointer; border: none; padding: 0; }

/* card list in game editor */
.card-strip { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 16px; }
.mini-wrap { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.mini-wrap .mini-actions { display: flex; gap: 6px; opacity: 0; transition: opacity 0.15s; }
.mini-wrap:hover .mini-actions { opacity: 1; }
.tcard.mini { width: 150px; cursor: pointer; transition: transform 0.12s; }
.tcard.mini:hover { transform: translateY(-4px) scale(1.02); }

/* ---------- Modal ---------- */
.modal-backdrop {
  position: fixed; inset: 0; background: rgba(6, 9, 14, 0.55);
  backdrop-filter: blur(3px);
  display: flex; align-items: center; justify-content: center; z-index: 400; padding: 20px;
  overflow-y: auto;
}
.modal {
  background: var(--panel); border: 1px solid var(--line-2);
  border-radius: var(--radius); box-shadow: var(--shadow);
  width: 100%; max-width: 460px; padding: 22px;
  /* Long forms (like Game rules) can outgrow the viewport. Cap the modal's own
     height and let it scroll internally, rather than relying on the centered
     flex backdrop to scroll - a centered flex item clips instead of scrolling,
     which put the Save button out of reach on short windows. */
  max-height: calc(100vh - 40px); max-height: calc(100dvh - 40px);
  overflow-y: auto;
}
.modal h3 { margin: 0 0 6px; font-size: 19px; }
.modal p { margin: 0 0 18px; color: var(--text-dim); }
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; }
.modal.modal-wide { max-width: 620px; }

/* ---------- Glass modal skin ----------
   Every TCG.modal() shares .modal, so this one block sets the app's whole
   floating-surface look: dark translucent glass, a blur, a thin white hairline,
   and the gold accent - the same language as the game-editor toolbar
   (.table-tools.floating), the bet panel, and the rules dock. It does NOT remap
   the theme vars: the base palette is already dark, so every control inside
   (buttons, inputs, pills, rule lines) is correct for glass with no per-control
   rule. See the "Glass UI" design rules in CLAUDE.md. */
.modal {
  padding: 15px 16px 14px; border-radius: 13px;
  color: var(--text);
  background: rgba(15,20,28,0.82);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  border: 1px solid rgba(255,255,255,0.12);
  box-shadow: 0 18px 50px rgba(0,0,0,0.55);
}
/* Title reads as a compact panel header, at pill scale, under a hairline rule -
   not the old serif small-caps. */
.modal h3 {
  text-align: left; font-weight: 700; font-size: 13.5px; letter-spacing: 0.3px;
  text-transform: none; color: var(--text);
  margin: 0 0 11px; padding-bottom: 9px; border-bottom: 1px solid rgba(255,255,255,0.08);
}
/* Buttons, inputs, and focus rings ride the base dark theme - no overrides
   needed. The primary button keeps its gold gradient from .btn-primary. */

/* ---------- Anchored flyout (TCG.flyout) ----------
   The glass panel that extends out from the button that opened it, the preferred
   surface for menus and pickers over a centered modal. Same skin as the toolbar
   and rules dock; position is set in JS. Body scrolls when tall (capped height),
   so a long picker stays a tidy flyout instead of a full-screen sheet. */
.tcg-flyout {
  position: fixed; z-index: 340; max-width: min(92vw, 680px);
  max-height: calc(100vh - 24px); max-height: calc(100dvh - 24px); display: flex; flex-direction: column;
  background: rgba(15,20,28,0.82); -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  border: 1px solid rgba(255,255,255,0.12); border-radius: 13px;
  box-shadow: 0 18px 50px rgba(0,0,0,0.55); animation: ctxIn 0.12s ease;
}
.tcg-flyout-head { display: flex; align-items: center; gap: 7px; padding: 9px 11px; border-bottom: 1px solid rgba(255,255,255,0.08); }
.tcg-flyout-head .ic { display: inline-flex; color: var(--accent-2); }
.tcg-flyout-head .ic svg { width: 14px; height: 14px; }
.tcg-flyout-title { flex: 1; font-size: 13px; font-weight: 700; color: var(--text); letter-spacing: 0.2px; }
.tcg-flyout-x.btn-icon { width: 22px; height: 22px; padding: 0; flex: 0 0 auto; }
.tcg-flyout-x svg { width: 13px; height: 13px; }
.tcg-flyout-body { padding: 10px; overflow-y: auto; }
/* ---- the game log (js/matchLog.js) ----------------------------------------
   A flyout off the toolbar's list button, wearing the standard glass at the
   standard compact scale. Nothing here is new furniture: the panel is a
   .tcg-flyout, the filter chips are the same pill the rest of the tool uses,
   and the body scrolls inside the flyout's own height cap - which is also why
   the log needs no phone variant, since that cap is already viewport-relative.

   The one idea in this block is --mlog-tint: each row carries its seat's crest
   colour as a variable, and the stripe and the turn dot read it. So a log with
   six players in it is scannable by colour without a single extra element, and
   a table with one player renders identically to one with none. */
.tcg-flyout.mlog-fly { max-width: min(94vw, 340px); }
.mlog-body { display: flex; flex-direction: column; gap: 7px; min-height: 0; }
/* The two-chip filter. A seg, not a dropdown: there are exactly two answers and
   both fit on the line, so nothing has to be opened to see what the choice is. */
.mlog-filter { display: flex; gap: 4px; flex: 0 0 auto; }
.mlog-chip {
  flex: 1; padding: 4px 8px; border-radius: 999px;
  border: 1px solid rgba(255,255,255,0.10); background: rgba(255,255,255,0.04);
  color: var(--text-dim); font-size: 11px; font-weight: 600; cursor: pointer;
  white-space: nowrap; transition: border-color 0.12s, background 0.12s, color 0.12s;
}
.mlog-chip:hover { border-color: var(--accent); color: var(--text); background: rgba(255,255,255,0.09); }
.mlog-chip.on { border-color: var(--accent); color: var(--text); background: rgba(216,162,74,0.16); }
/* The list scrolls, the panel doesn't grow. 46vh keeps a long match off most of
   the felt while still showing a dozen lines; dvh is written after vh for the
   phone's retracting URL bar, per the responsive rule. */
.mlog-list {
  display: flex; flex-direction: column; gap: 2px;
  max-height: 46vh; max-height: 46dvh; overflow-y: auto; overscroll-behavior: contain;
  padding-right: 2px; min-height: 0;
}
.mlog-empty { font-size: 11px; color: var(--text-dim); opacity: 0.75; padding: 8px 2px; }
/* A turn heading. Its rule spans the panel and its dot carries the seat colour,
   so the spine of the log reads at a glance while scrolling fast. */
.mlog-turn {
  display: flex; align-items: center; gap: 6px;
  margin: 7px 0 2px; padding-top: 6px;
  border-top: 1px solid rgba(255,255,255,0.09);
  font-size: 11px; font-weight: 700; letter-spacing: 0.3px;
  color: var(--text); text-transform: uppercase;
}
.mlog-turn:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.mlog-turn-dot {
  width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto;
  background: var(--mlog-tint, var(--accent-2));
}
.mlog-turn-tx { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A step marker: quiet, indented, no icon. It is punctuation, not an event. */
.mlog-step {
  font-size: 10px; font-weight: 600; letter-spacing: 0.6px; text-transform: uppercase;
  color: var(--text-dim); opacity: 0.65; padding: 3px 0 1px 13px;
}
/* An event row. The seat stripe down the left is a border rather than a child
   element, so a row is still just two spans. */
.mlog-row {
  display: flex; align-items: flex-start; gap: 6px;
  padding: 3px 4px 3px 8px; border-radius: 6px;
  border-left: 2px solid var(--mlog-tint, rgba(255,255,255,0.12));
  font-size: 11.5px; line-height: 1.4; color: var(--text-dim);
}
.mlog-row:hover { background: rgba(255,255,255,0.05); }
.mlog-ic { display: inline-flex; flex: 0 0 auto; color: var(--accent-2); padding-top: 1px; }
.mlog-ic svg { width: 12px; height: 12px; }
.mlog-tx { min-width: 0; overflow-wrap: anywhere; }
/* Three weights for three kinds. An automatic step is dimmer than a choice
   somebody made, and a result is the thing you scroll back to find. */
.mlog-auto { opacity: 0.72; }
.mlog-result { color: var(--text); font-weight: 600; }
.mlog-result .mlog-ic { color: var(--accent); }
.mlog-foot { display: flex; justify-content: flex-end; flex: 0 0 auto; padding-top: 1px; }

/* ---- emotes: the message wheel and the bubble it pops (js/emotes.js) ----
   Both wear the standard glass skin at the standard compact scale, so the one
   thing players say to each other looks like part of the tool rather than a
   chat widget bolted on. The wheel is a flyout (.emote-fly) off whatever
   opened it; the bubble is a fixed element on <body> so a seat-rail repaint
   can't tear it out mid-message. */
/* Eight chips in two columns: four short rows, a panel small enough to sit in a
   corner of the felt. The grid's columns are equal and content-sized (max-content
   capped by the flyout), so both labels in a row line up and the panel is only as
   wide as its longest message. */
.tcg-flyout.emote-fly { max-width: min(94vw, 380px); }
.emote-wheel { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 5px 6px; }
/* The gate's reason, printed once above the chips instead of only in tooltips,
   so a blocked player is told what happened without having to hover a grey one.
   Spans both columns. */
.emote-wheel-why { grid-column: 1 / -1; font-size: 11px; line-height: 1.35; color: var(--accent-2); background: rgba(216,162,74,0.10); border: 1px solid rgba(216,162,74,0.28); border-radius: 8px; padding: 6px 8px; }
.emote-pick {
  display: flex; align-items: center; gap: 6px; padding: 5px 11px 5px 8px;
  border-radius: 999px; border: 1px solid rgba(255,255,255,0.10);
  background: rgba(255,255,255,0.04); color: var(--text-dim);
  font-size: 12px; font-weight: 600; cursor: pointer; text-align: left;
  white-space: nowrap;
  transition: border-color 0.12s, background 0.12s, color 0.12s, transform 0.12s;
}
.emote-pick-tx { flex: 1; }
.emote-pick:hover { border-color: var(--accent); color: var(--text); background: rgba(255,255,255,0.09); transform: translateY(-1px); }
.emote-pick-ic { display: inline-flex; flex: 0 0 auto; color: var(--accent-2); }
.emote-pick-ic svg { width: 14px; height: 14px; }
/* Blocked, not gone: a chip the gate refuses stays in its row, dashed and
   inert, carrying the reason in its tooltip (CLAUDE.md, "Controls never
   disappear"). */
.emote-pick.disabled { border-style: dashed; opacity: 0.45; cursor: not-allowed; pointer-events: auto; }
.emote-pick.disabled:hover { border-color: rgba(255,255,255,0.10); color: var(--text-dim); background: rgba(255,255,255,0.04); transform: none; }

.emote-bubble {
  position: fixed; z-index: 1100; pointer-events: none;
  display: inline-flex; align-items: center; gap: 7px; padding: 6px 12px 6px 9px;
  border-radius: 999px; white-space: nowrap;
  background: rgba(15,20,28,0.72); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  border: 1px solid rgba(255,255,255,0.12);
  box-shadow: 0 10px 26px rgba(0,0,0,0.45);
  font-size: 12px; font-weight: 700; color: var(--text);
  opacity: 0; transform: translateY(6px) scale(0.94);
  transition: opacity 0.18s ease, transform 0.22s cubic-bezier(0.2, 1.4, 0.4, 1);
}
.emote-bubble.show { opacity: 1; transform: translateY(0) scale(1); }
.emote-bubble-ic { display: inline-flex; color: var(--emote-tint, var(--accent-2)); }
.emote-bubble-ic svg { width: 15px; height: 15px; }
/* Your own message is marked so a busy table still reads as "this one is me":
   the gold hairline the rest of the UI reserves for the active thing. */
.emote-bubble.is-mine { border-color: rgba(216,162,74,0.42); }

/* A short text-entry form inside a flyout: the field, then a compact right-
   aligned confirm. Matches the pill-scale controls of the rest of the glass UI. */
.fly-form { display: flex; flex-direction: column; gap: 9px; min-width: 196px; }
.fly-form-actions { display: flex; justify-content: flex-end; gap: 6px; }
.fly-form-actions .btn { padding: 6px 12px; font-size: 12px; border-radius: 8px; }

/* Value flyout: the anchored stepper/field that replaced the old number/text
   modal, plus the shared footer the richer editor flyouts use for their confirm.
   Pill-scale, so it reads as an extension of the menu row that opened it. */
.val-fly { display: flex; flex-direction: column; gap: 10px; min-width: 168px; }
.val-fly-step { justify-content: center; }
/* `input.val-fly-num` for the reason stated on input.ctx-num above. */
input.val-fly-num {
  width: 66px; flex: 0 0 auto; text-align: center; font-weight: 800; font-size: 15px;
  color: var(--text); background: rgba(0,0,0,0.3); border: 1px solid var(--line-2);
  border-radius: 7px; padding: 5px 4px; outline: none; font-variant-numeric: tabular-nums;
  -moz-appearance: textfield;
}
input.val-fly-num:focus { border-color: var(--accent); box-shadow: none; }
input.val-fly-num::-webkit-outer-spin-button, input.val-fly-num::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.val-fly-txt {
  width: 100%; box-sizing: border-box; font-size: 13px; color: var(--text);
  background: rgba(0,0,0,0.3); border: 1px solid var(--line-2); border-radius: 7px; padding: 7px 9px; outline: none;
}
.val-fly-txt:focus { border-color: var(--accent); }
.val-fly-foot { display: flex; align-items: center; gap: 8px; margin-top: 2px; }
.val-fly-done, .val-fly-save { margin-left: auto; padding: 6px 12px; font-size: 12px; border-radius: 8px; }

/* ---------- Rules modal ---------- */
.rules-objective { font-size: 15px; color: var(--text); font-weight: 600; margin: 0 0 20px; }
/* The box-cover strip: players / age / duration, side by side like the back
   of a real board game box, sitting above the rulebook sections proper. */
.rules-box { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 18px; padding-bottom: 16px; margin-bottom: 18px; border-bottom: 1px solid var(--line); }
.rules-stat { display: flex; align-items: center; gap: 6px; }
.rules-stat-ic { display: inline-flex; align-items: center; line-height: 0; color: var(--text-faint); flex-shrink: 0; margin-top: -1px; }
.rules-stat-ic svg { width: 16px; height: 16px; display: block; }
.rules-box .rule-line { margin-bottom: 0; }
/* Targets any <p> directly in the box, not just ones wrapped in .rule-line -
   the player-count stat is a plain string (no click control), so it renders
   as a bare <p> with no wrapper, and was falling through to the generic
   ".modal p" rule (dimmer color, bigger font, 18px margin) instead of this. */
.rules-box p { margin: 0; font-size: 13px; font-weight: 700; line-height: 1; color: var(--text); }
.rules-section { margin-bottom: 18px; }
.rules-section:last-child { margin-bottom: 0; }
.rules-section h4 { margin: 0 0 8px; font-size: 12px; text-transform: uppercase; letter-spacing: 1px; color: var(--text-faint); font-weight: 700; }
.rules-section p { margin: 0 0 8px; color: var(--text-dim); line-height: 1.5; }
.rules-section p:last-child { margin-bottom: 0; }
.rules-list { margin: 0 0 8px; padding-left: 20px; color: var(--text-dim); line-height: 1.5; }
.rules-list li { margin-bottom: 4px; }
/* Optional deck overview: wraps into as many compact columns as the width
   allows so a 52-card deck stays a tidy reference rather than a long scroll. */
.rules-card-list { column-width: 150px; column-gap: 24px; }
.rules-card-list li { break-inside: avoid; }
/* A rule line with a live value in it: plain prose, except the one word or
   short clause that carries a setting, which reads in the accent colour so the
   eye lands on it. Nothing is boxed and nothing is clickable - the rulebook is
   a document now, and every setting in it is authored from the lanes and the
   piece menus instead (docs/RULEBOOK_DESIGNER.md). It deliberately carries no
   cursor and no underline: both used to promise a click, and a word that still
   looks like a link but does nothing is worse than a word that never did.
   `.rule-text-input` is NOT part of this and still has a caller - the inline
   game rename in library.js. */
.rule-line { margin-bottom: 10px; }
.rule-line:last-child { margin-bottom: 0; }
.rule-token { color: var(--accent-2); font-weight: 700; }
.rule-text-input { width: 100%; background: var(--panel); border: 1px solid var(--line-2); border-radius: 8px; color: var(--text); padding: 8px 10px; font: inherit; }
.rule-text-input:focus { outline: none; border-color: var(--accent); }
.rules-list li:last-child { margin-bottom: 0; }

/* ---------- Toast ---------- */
.toast-root { position: fixed; bottom: 20px; right: 20px; left: auto; z-index: 200; display: flex; flex-direction: column; gap: 8px; align-items: flex-end; }
.toast {
  background: var(--panel-2); border: 1px solid var(--line-2);
  border-radius: 999px; padding: 10px 18px; box-shadow: var(--shadow);
  display: flex; align-items: center; gap: 9px; font-size: 13.5px;
  animation: toastIn 0.2s ease;
}
.toast.ok { border-color: var(--green); }
.toast.err { border-color: var(--red); }
.toast svg { width: 16px; height: 16px; }
@keyframes toastIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* =================================================================
   Board / playtest
   ================================================================= */
.board-view { max-width: 1320px; }
.board {
  display: grid;
  grid-template-rows: auto 1fr auto 1fr auto;
  gap: 10px;
  min-height: calc(100vh - 150px); min-height: calc(100dvh - 150px);
}
.board-side { display: flex; flex-direction: column; gap: 8px; }
.zone {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: rgba(255,255,255,0.015);
  padding: 10px;
  min-height: 120px;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  position: relative;
}
.zone.battlefield.active-drop { border-color: var(--accent); background: rgba(216,162,74,0.06); }
.zone-tag {
  position: absolute; top: 6px; left: 10px;
  font-size: 10px; letter-spacing: 1.5px; text-transform: uppercase;
  color: var(--text-faint);
}
.hand-zone { min-height: 150px; perspective: 900px; justify-content: center; }

.player-bar {
  display: flex; align-items: center; gap: 16px;
  padding: 8px 14px;
  background: linear-gradient(180deg, var(--panel), var(--bg-2));
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.player-bar.is-active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.player-id { display: flex; align-items: center; gap: 10px; font-weight: 700; }
.avatar {
  width: 38px; height: 38px; border-radius: 50%;
  background: var(--bg-3); border: 1px solid var(--line-2);
  display: flex; align-items: center; justify-content: center; color: var(--accent-2);
}
.life {
  display: inline-flex; align-items: center; gap: 7px;
  font-weight: 800; font-size: 18px;
}
.life svg { width: 20px; height: 20px; color: var(--red); }
.resource { display: inline-flex; align-items: center; gap: 6px; color: var(--blue); font-weight: 700; }
.resource svg { width: 18px; height: 18px; }
.deck-count { display: inline-flex; align-items: center; gap: 6px; color: var(--text-dim); font-size: 13px; }
.deck-count svg { width: 16px; height: 16px; }
.player-bar.is-target { cursor: pointer; outline: 2px dashed var(--red); outline-offset: 2px; }
.player-bar.is-target:hover { background: rgba(214,96,77,0.1); }

/* board cards */
.bcard {
  width: 108px; aspect-ratio: 5/7; border-radius: 10px;
  background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(0,0,0,0.25)), var(--card-accent, var(--accent));
  padding: 5px; position: relative; box-shadow: var(--shadow-sm);
  --card-accent: var(--accent);
  cursor: default; transition: transform 0.1s, box-shadow 0.1s;
  color: #f4f1e8;
}
.bcard .b-inner { background: #14171d; border-radius: 7px; height: 100%; display: flex; flex-direction: column; overflow: hidden; }
.bcard .b-name { font-size: 10.5px; font-weight: 700; padding: 4px 5px; line-height: 1.1; }
.bcard .b-art { flex: 1; background: #0b0d11; overflow: hidden; }
.bcard .b-art img { width: 100%; height: 100%; object-fit: cover; }
.bcard .b-cost { position: absolute; top: -6px; left: -6px; width: 22px; height: 22px; border-radius: 50%; background: var(--blue); color: #fff; font-weight: 800; font-size: 12px; display: flex; align-items: center; justify-content: center; box-shadow: var(--shadow-sm); }
.bcard .b-stats { display: flex; justify-content: space-between; padding: 3px 4px; }
.bcard .b-stat { font-weight: 800; font-size: 13px; padding: 1px 5px; border-radius: 6px; }
.b-atk { background: #c8923a; color: #221703; }
.b-hp { background: var(--red); color: #2a0c07; }
.b-hp.hurt { background: #8a3328; color: #ffd9d2; }
.bcard.standard .b-inner { background: #f5f1e6; }
.bcard.standard .b-name { color: #1a1a1a; }
.bcard .b-face { flex:1; display:flex; align-items:center; justify-content:center; flex-direction: column; }
.bcard .b-face .rank { font-size: 26px; font-weight: 800; }
.bcard .b-face svg { width: 24px; height: 24px; }

.bcard.in-hand { cursor: grab; }
.bcard.in-hand:hover { transform: translateY(-14px) scale(1.06); box-shadow: var(--shadow); z-index: 5; }
.hand-zone .bcard { margin: 0 -10px; }
.bcard.playable { outline: 2px solid var(--green); outline-offset: 1px; }
.bcard.can-attack { outline: 2px solid var(--accent); outline-offset: 1px; cursor: pointer; }
.bcard.attacker-selected { outline: 3px solid var(--accent-2); transform: translateY(-6px); }
.bcard.valid-target { outline: 2px dashed var(--red); cursor: pointer; }
.bcard.exhausted { filter: grayscale(0.55) brightness(0.8); }
.bcard.dying { animation: die 0.35s ease forwards; }
@keyframes die { to { opacity: 0; transform: scale(0.7) rotate(8deg); } }

.controls-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 10px 16px; background: var(--bg-2);
  border: 1px solid var(--line); border-radius: var(--radius);
}
.phase-track { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.phase-pill {
  font-size: 12px; padding: 5px 11px; border-radius: 999px;
  border: 1px solid var(--line-2); color: var(--text-dim);
}
.phase-pill.current { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 700; }
.turn-info { font-size: 13px; color: var(--text-dim); display: flex; gap: 14px; align-items: center; }
.turn-info b { color: var(--text); }

.log-panel {
  position: fixed; right: 0; top: 0; bottom: 0; width: 300px;
  background: var(--bg-2); border-left: 1px solid var(--line);
  transform: translateX(100%); transition: transform 0.2s; z-index: 40;
  display: flex; flex-direction: column;
}
.log-panel.open { transform: none; }
.log-head { padding: 12px 14px; border-bottom: 1px solid var(--line); display: flex; justify-content: space-between; align-items: center; font-weight: 700; }
.log-body { flex: 1; overflow: auto; padding: 8px 14px; font-size: 12.5px; color: var(--text-dim); }
.log-entry { padding: 5px 0; border-bottom: 1px solid rgba(255,255,255,0.04); }
.log-entry .who { color: var(--accent-2); font-weight: 600; }

.banner {
  position: fixed; inset: 0; display: flex; align-items: center; justify-content: center;
  background: rgba(6,9,14,0.78); z-index: 120; backdrop-filter: blur(3px);
}
.banner-card { text-align: center; padding: 40px 50px; background: var(--panel); border: 1px solid var(--accent); border-radius: var(--radius); box-shadow: var(--shadow); }
.banner-card h2 { margin: 0 0 8px; font-size: 30px; }
.banner-card p { color: var(--text-dim); margin: 0 0 22px; }

.tag {
  font-size: 11px; padding: 2px 8px; border-radius: 999px;
  background: var(--panel-2); border: 1px solid var(--line-2); color: var(--text-dim);
}
/* ---------- ability text on cards ---------- */
.ability-line { color: #f0e6cf; margin-bottom: 4px; }
.tcard.is-standard .ability-line { color: #2a2a2a; }

/* ---------- effect builder ---------- */
.kw-row { display: flex; flex-wrap: wrap; gap: 8px; }
.kw-chip {
  display: inline-flex; align-items: center; gap: 7px;
  border: 1px solid var(--line-2); background: var(--panel-2);
  color: var(--text-dim); padding: 7px 13px; border-radius: 999px; font-size: 13px; font-weight: 600;
}
.kw-chip .kw-check { display: inline-flex; opacity: 0; width: 14px; }
.kw-chip .kw-check svg { width: 14px; height: 14px; }
.kw-chip.on { border-color: var(--accent); color: var(--text); background: rgba(216,162,74,0.12); }
.kw-chip.on .kw-check { opacity: 1; color: var(--accent-2); }
.kw-chip:hover { border-color: var(--accent); }

.effect-rows { display: flex; flex-direction: column; gap: 10px; }
.effect-row {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  background: var(--bg); border: 1px solid var(--line-2);
  border-radius: var(--radius-sm); padding: 9px 11px;
}
.fx-select {
  width: auto; min-width: 90px; padding: 6px 9px; font-size: 13px;
  background: var(--panel-2); border-color: var(--line-2);
}
.fx-num { width: 64px; padding: 6px 8px; text-align: center; }
.fx-word { color: var(--text-faint); font-size: 13px; }

/* =================================================================
   Board polish: drag, targeting, animation effects
   ================================================================= */
.fx-layer { position: fixed; inset: 0; pointer-events: none; z-index: 150; overflow: hidden; }
.fx-arrow { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.drag-clone { position: fixed; z-index: 160; pointer-events: none; transform: translate(-50%, -50%) rotate(-3deg) scale(1.05); filter: drop-shadow(0 16px 24px rgba(0,0,0,0.5)); transition: none; }
.float-num {
  position: fixed; z-index: 165; font-weight: 800; font-size: 22px; pointer-events: none;
  text-shadow: 0 2px 6px rgba(0,0,0,0.7); transform: translate(-50%, -50%);
  animation: floatUp 0.9s ease forwards;
}
.float-num.dmg { color: #ff6a55; }
.float-num.heal { color: #59d18b; }
@keyframes floatUp { 0% { opacity: 0; transform: translate(-50%, -30%) scale(0.6); } 25% { opacity: 1; } 100% { opacity: 0; transform: translate(-50%, -150%) scale(1.1); } }
.ghost-card { position: fixed; z-index: 158; pointer-events: none; animation: ghostDie 0.4s ease forwards; }
@keyframes ghostDie { to { opacity: 0; transform: scale(0.7) rotate(10deg) translateY(10px); } }

.zone.battlefield.drop-ready { border-color: var(--green); background: rgba(76,174,122,0.08); box-shadow: 0 0 0 1px var(--green) inset; }

.bcard.dragging-src { opacity: 0.25; }
.bcard.pop-in { animation: popIn 0.32s cubic-bezier(0.2, 1.3, 0.5, 1); }
@keyframes popIn { 0% { transform: scale(0.3); opacity: 0; } 100% { transform: scale(1); opacity: 1; } }
.bcard.lunge { transition: transform 0.14s ease; z-index: 20; }
.bcard.hit { animation: shake 0.28s; }
@keyframes shake { 0%,100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } }

.bcard.targetable { outline: 2px dashed var(--red); outline-offset: 2px; cursor: crosshair; animation: pulseT 1.1s infinite; }
.player-bar.targetable { outline: 2px dashed var(--red); outline-offset: 2px; cursor: crosshair; animation: pulseT 1.1s infinite; }
@keyframes pulseT { 0%,100% { outline-color: rgba(214,96,77,0.5); } 50% { outline-color: rgba(214,96,77,1); } }

.bcard .b-keywords { display: flex; gap: 3px; padding: 0 4px 3px; flex-wrap: wrap; }
.kw-tag { font-size: 8.5px; letter-spacing: 0.5px; text-transform: uppercase; padding: 1px 5px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.25); color: #f0e6cf; }
.bcard.standard .kw-tag { color: #2a2a2a; border-color: rgba(0,0,0,0.3); }
.bcard.taunt-unit { box-shadow: 0 0 0 2px var(--accent-2), var(--shadow-sm); }

.life.pulse { animation: lifePulse 0.5s; }
@keyframes lifePulse { 0% { transform: scale(1); } 40% { transform: scale(1.4); color: var(--red); } 100% { transform: scale(1); } }

/* A card announcing that one of its triggered effects is firing: a brief lift and
   glow so the player can attribute the effect to the right card. */
.piece.fx-trigger { z-index: 55; }
/* A card mid-swing paints above everything it is swinging at. Every .piece is
   z-index 2, so a tie is settled by DOM order and the attacker was sliding UNDER
   the card it was hitting - which reads as the strike passing behind its target.
   Above .lifted (40) and .fx-trigger (55) too, since an attack in progress is
   the thing being watched. Applied and re-applied by the live-motion record in
   00-open-state.js, not set once, so the re-render that lands the damage at
   contact cannot strip it half way through the swing. */
.piece.swinging { z-index: 60; }

/* ---- the attack lab (dev only, ?atklab=1) ----------------------------
   Same glass as every other floating surface, at the same compact scale, so
   judging the swing against it is judging it against the real thing. Ships
   inert: js/attackLab.js renders nothing without the URL flag, so these rules
   never match on a normal load. */
.atklab {
  position: fixed; left: 12px; bottom: 12px; z-index: 900; width: 268px;
  background: rgba(15,20,28,0.72); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(255,255,255,0.12); border-radius: 13px;
  box-shadow: 0 18px 40px rgba(0,0,0,0.5); color: var(--text, #e8e2d4);
  font-size: 11px; overflow: hidden;
}
.atklab-head { display: flex; align-items: center; justify-content: space-between; padding: 8px 10px; border-bottom: 1px solid rgba(255,255,255,0.09); font-size: 13px; font-weight: 700; }
.atklab-x { background: none; border: 0; color: inherit; font-size: 15px; cursor: pointer; line-height: 1; padding: 0 4px; }
.atklab.min .atklab-body, .atklab.min .atklab-foot, .atklab.min .atklab-note { display: none; }
.atklab-body { padding: 6px 10px 8px; max-height: 46vh; max-height: 46dvh; overflow-y: auto; }
.atklab-sec { font-size: 10px; letter-spacing: 0.7px; text-transform: uppercase; color: var(--accent-2, #d8a24a); margin: 9px 0 4px; }
.atklab-row { display: grid; grid-template-columns: 1fr 84px 40px; align-items: center; gap: 6px; margin-bottom: 4px; }
.atklab-lab { color: var(--text-faint, #9aa3ad); }
.atklab-val { text-align: right; font-variant-numeric: tabular-nums; color: var(--accent-2, #d8a24a); }
.atklab-row input[type="range"] { width: 100%; height: 14px; }
.atklab-foot { display: flex; flex-wrap: wrap; gap: 5px; padding: 8px 10px; border-top: 1px solid rgba(255,255,255,0.09); }
.atklab-btn { font: inherit; padding: 4px 9px; border-radius: 999px; cursor: pointer; color: inherit;
  background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.14); }
.atklab-btn:hover { background: rgba(255,255,255,0.13); }
.atklab-btn.pri { background: rgba(216,162,74,0.2); border-color: rgba(216,162,74,0.5); color: var(--accent-2, #d8a24a); }
.atklab-note { padding: 0 10px 9px; color: var(--text-faint, #9aa3ad); font-size: 10.5px; }

/* ---- the engine lab (dev only, ?lab=1) -------------------------------
   Same glass as the attack lab, on the opposite edge so both can be open at
   once, and taller because this one is a list rather than a row of sliders.
   Ships inert: js/engineLab.js renders nothing without the URL flag, so these
   rules never match on a normal load. The felt underneath is the point, so the
   panel stays a fixed 320 and never grows to fit its longest sentence. */
.elab {
  position: fixed; right: 12px; top: 64px; z-index: 900; width: 320px;
  background: rgba(15,20,28,0.74); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(255,255,255,0.12); border-radius: 13px;
  box-shadow: 0 18px 40px rgba(0,0,0,0.5); color: var(--text, #e8e2d4);
  font-size: 11.5px; overflow: hidden; display: flex; flex-direction: column;
}
.elab-head { display: flex; align-items: center; justify-content: space-between; padding: 8px 10px; border-bottom: 1px solid rgba(255,255,255,0.09); font-size: 13px; font-weight: 700; }
.elab-x { background: none; border: 0; color: inherit; font-size: 15px; cursor: pointer; line-height: 1; padding: 0 4px; }
.elab.min .elab-tabs, .elab.min .elab-body, .elab.min .elab-foot, .elab.min .elab-note { display: none; }
.elab-tabs { display: flex; gap: 2px; padding: 6px 8px 0; border-bottom: 1px solid rgba(255,255,255,0.09); }
.elab-tab { flex: 1; font: inherit; padding: 5px 4px; border: 0; border-bottom: 2px solid transparent; background: none; color: var(--text-faint, #9aa3ad); cursor: pointer; }
.elab-tab.on { color: var(--accent-2, #d8a24a); border-bottom-color: var(--accent-2, #d8a24a); }
.elab-body { padding: 4px 10px 8px; max-height: 58vh; max-height: 58dvh; overflow-y: auto; }
.elab-sec-h { font-size: 10px; letter-spacing: 0.7px; text-transform: uppercase; color: var(--accent-2, #d8a24a); margin: 10px 0 3px; }
.elab-sec-hint { color: var(--text-faint, #9aa3ad); font-size: 10.5px; line-height: 1.45; margin-bottom: 5px; }
.elab-lane, .elab-grid, .elab-row { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 4px; }
.elab-chip { font: inherit; font-size: 10.5px; padding: 3px 8px; border-radius: 999px; cursor: pointer; color: inherit;
  background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.14); }
.elab-chip:hover { background: rgba(255,255,255,0.13); }
.elab-chip.on { background: rgba(216,162,74,0.2); border-color: rgba(216,162,74,0.5); color: var(--accent-2, #d8a24a); }
/* An unavailable step is DIMMED and still clickable, because pressing it is how
   you get told what the board is missing. A disabled button says nothing. */
.elab-chip.off { opacity: 0.42; }
.elab-empty { color: var(--text-faint, #9aa3ad); font-style: italic; padding: 2px 0; }
.elab-search { width: 100%; font: inherit; padding: 4px 8px; margin: 2px 0 5px; border-radius: 7px;
  background: rgba(0,0,0,0.25); border: 1px solid rgba(255,255,255,0.14); color: inherit; }
.elab-shelf { max-height: 230px; overflow-y: auto; }
.elab-grp { font-size: 9.5px; letter-spacing: 0.6px; text-transform: uppercase; color: var(--text-faint, #9aa3ad); margin: 7px 0 2px; }
.elab-fx, .elab-rule { display: flex; align-items: center; gap: 6px; width: 100%; text-align: left; font: inherit;
  padding: 3px 6px; border-radius: 6px; border: 0; background: none; color: inherit; cursor: pointer; line-height: 1.35; }
.elab-fx:hover, .elab-rule:hover { background: rgba(255,255,255,0.08); }
.elab-rule { background: rgba(216,162,74,0.12); margin-bottom: 2px; }
.elab-fx-t { flex: 1; }
.elab-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; background: #6b7580; }
.elab-dot-green { background: #6fae62; }
.elab-dot-amber { background: #d8a24a; }
.elab-dot-grey { background: #6b7580; }
.elab-tally { display: flex; gap: 5px; margin-bottom: 5px; }
.elab-tal { flex: 1; text-align: center; padding: 5px 2px; border-radius: 8px; background: rgba(255,255,255,0.06); }
.elab-tal strong { display: block; font-size: 15px; }
.elab-tal span { font-size: 9.5px; color: var(--text-faint, #9aa3ad); }
.elab-tal.g strong { color: #6fae62; } .elab-tal.a strong { color: #d8a24a; }
.elab-tal.x strong { color: #8b949e; } .elab-tal.p strong { color: #5fa8d3; }
.elab-tbl { border-top: 1px solid rgba(255,255,255,0.07); }
.elab-tr { display: flex; align-items: center; gap: 6px; padding: 3px 2px; border-bottom: 1px solid rgba(255,255,255,0.05); line-height: 1.35; }
.elab-td-n { flex: 1; }
.elab-td-p { font-size: 9.5px; color: var(--text-faint, #9aa3ad); text-align: right; max-width: 46%; }
.elab-td-p.ok { color: #6fae62; } .elab-td-p.bad { color: #d8a24a; }
.elab-v { display: flex; align-items: baseline; gap: 6px; padding: 3px 2px; border-bottom: 1px solid rgba(255,255,255,0.05); line-height: 1.35; }
.elab-v-t { flex: 1; }
.elab-v-w { font-size: 9.5px; color: var(--text-faint, #9aa3ad); }
.elab-v-quiet { color: #d8a24a; }
.elab-v-never { color: #c9736a; }
.elab-ok { color: #6fae62; padding: 3px 0; }
.elab-foot { display: flex; flex-wrap: wrap; gap: 5px; padding: 8px 10px; border-top: 1px solid rgba(255,255,255,0.09); }
.elab-btn { font: inherit; font-size: 10.5px; padding: 4px 9px; border-radius: 999px; cursor: pointer; color: inherit;
  background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.14); }
.elab-btn:hover { background: rgba(255,255,255,0.13); }
.elab-btn.pri { background: rgba(216,162,74,0.2); border-color: rgba(216,162,74,0.5); color: var(--accent-2, #d8a24a); }
/* The felt is behind the recipe: the button says so rather than the click
   looking like it was ignored. */
.elab-btn.due { background: rgba(216,162,74,0.42); border-color: rgba(216,162,74,0.85); color: #f3e3c4; }
.elab-note { padding: 0 10px 9px; color: var(--text-faint, #9aa3ad); font-size: 10.5px; }
.piece.fx-trigger .piece-3d { animation: fxTriggerPulse 0.5s ease; }
@keyframes fxTriggerPulse {
  0% { transform: translateY(0) scale(1); filter: brightness(1); }
  35% { transform: translateY(-10px) scale(1.06); filter: brightness(1.5) drop-shadow(0 0 8px var(--accent-2)); }
  100% { transform: translateY(0) scale(1); filter: brightness(1); }
}

.targeting-hint {
  position: fixed; top: 64px; left: 50%; transform: translateX(-50%);
  background: var(--panel); border: 1px solid var(--accent); color: var(--text);
  padding: 8px 16px; border-radius: 999px; z-index: 155; font-size: 13.5px; box-shadow: var(--shadow);
  display: flex; align-items: center; gap: 8px;
}
.targeting-hint svg { width: 16px; height: 16px; color: var(--accent-2); }

.row-between { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.muted { color: var(--text-dim); }
.faint { color: var(--text-faint); }
.spacer { flex: 1; }
.divider { height: 1px; background: var(--line); margin: 18px 0; border: none; }

/* =================================================================
   Home main menu
   ================================================================= */
.menu-hero {
  text-align: center;
  padding: 18px 0 34px;
  position: relative;
}
.menu-logo {
  width: 64px; height: 64px; margin: 0 auto 10px;
  display: flex; align-items: center; justify-content: center;
  color: var(--accent-2);
  background: radial-gradient(circle at 50% 35%, rgba(216,162,74,0.25), transparent 70%);
}
.menu-logo svg { width: 52px; height: 52px; filter: drop-shadow(0 4px 14px rgba(216,162,74,0.4)); }
.menu-title {
  margin: 0; font-size: 46px; letter-spacing: 6px; font-weight: 800;
  background: linear-gradient(180deg, #fff, var(--accent-2) 60%, var(--accent));
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
  text-shadow: 0 2px 30px rgba(216,162,74,0.25);
}
.menu-tag { margin: 6px 0 0; color: var(--text-dim); font-size: 15px; font-family: var(--font); font-weight: 500; letter-spacing: -0.1px; }

.menu-buttons {
  display: flex; flex-direction: column; gap: 14px;
  max-width: 520px; margin: 30px auto 0;
}
.menu-btn {
  display: flex; align-items: center; gap: 16px; text-align: left;
  padding: 16px 20px; border-radius: 14px;
  border: 1px solid var(--btn-line, var(--line-2));
  background: var(--btn-bg, var(--panel-2));
  color: #fff; cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: transform 0.1s, box-shadow 0.15s, filter 0.15s, border-color 0.15s;
}
.menu-btn:active { transform: translateY(0); }
.menu-btn-ic { width: 46px; height: 46px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.22); border-radius: 11px; }
.menu-btn-ic svg { width: 26px; height: 26px; }
.menu-btn-text { display: flex; flex-direction: column; flex: 1; }
.menu-btn-title { font-size: 19px; font-weight: 800; letter-spacing: 0.4px; }
.menu-btn-sub { font-size: 13px; opacity: 0.82; }
.menu-btn-go { display: flex; opacity: 0.7; }
.menu-btn-go svg { width: 22px; height: 22px; }
.menu-gold { --btn-bg: linear-gradient(135deg, #e6b765, #c98a2f); --btn-line: #f0c879; color: #2a1d05; }
.menu-gold .menu-btn-sub { color: #3a2a08; opacity: 0.85; }
.menu-gold .menu-btn-ic { background: rgba(0,0,0,0.16); }
.menu-blue { --btn-bg: linear-gradient(135deg, #4f97df, #2f6cb0); --btn-line: #6aa9e8; }
.menu-green { --btn-bg: linear-gradient(135deg, #4cbf85, #2f9462); --btn-line: #69d49b; }
.menu-violet { --btn-bg: linear-gradient(135deg, #9a72d4, #6f47b0); --btn-line: #b08fe4; }

/* startup animation: logo first, then buttons top to bottom */
.anim-logo { opacity: 0; animation: fadeDown 0.6s ease forwards; }
.menu-title.anim-logo { animation-delay: 0.08s; }
.menu-tag.anim-logo { animation-delay: 0.16s; }
@keyframes fadeDown { from { opacity: 0; transform: translateY(-14px); } to { opacity: 1; transform: none; } }
.anim-btn { animation: rise 0.5s cubic-bezier(0.2, 0.9, 0.3, 1) both; }
@keyframes rise { from { opacity: 0; transform: translateY(22px) scale(0.97); } to { opacity: 1; transform: none; } }

/* hover: lift, brighten, and float. The intro animation lives on the
   .anim-btn wrapper, so this floaty animation on the button itself never
   re-triggers the intro on mouse-out. */
.menu-btn-wrap { display: flex; flex-direction: column; width: 100%; }
.menu-btn:hover {
  box-shadow: var(--shadow);
  border-color: #fff6;
  filter: brightness(1.08);
  animation: floaty 2.4s ease-in-out infinite;
}
.menu-btn:hover .menu-btn-go { transform: translateX(3px); transition: transform 0.15s; }
@keyframes floaty { 0%, 100% { transform: translateY(-4px); } 50% { transform: translateY(-9px); } }
.menu-hero { display: flex; flex-direction: column; align-items: center; padding-top: 0; }
.menu-buttons { width: 100%; }

/* =================================================================
   Card stat boost color
   ================================================================= */
.b-hp.boosted, .stat-hp.boosted { background: #3f9e63; color: #06210f; }

/* =================================================================
   Pass screen (hotseat)
   ================================================================= */
.pass-screen {
  position: fixed; inset: 0; z-index: 130;
  background: radial-gradient(800px 500px at 50% 30%, #1c2636, #0b0e13 80%);
  display: flex; align-items: center; justify-content: center;
}
.pass-card { text-align: center; animation: toastIn 0.25s ease; }
.pass-ic { width: 70px; height: 70px; margin: 0 auto 14px; color: var(--accent-2); background: rgba(216,162,74,0.12); border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.pass-ic svg { width: 38px; height: 38px; }
.pass-sub { color: var(--text-dim); letter-spacing: 1px; text-transform: uppercase; font-size: 12px; }
.pass-card h2 { margin: 6px 0 22px; font-size: 34px; }

/* =================================================================
   Bot turn banner
   ================================================================= */
.bot-banner {
  position: fixed; top: 64px; left: 50%; transform: translateX(-50%);
  z-index: 125; display: flex; align-items: center; gap: 11px;
  padding: 10px 18px; border-radius: 999px;
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
  border: 1px solid var(--blue); box-shadow: var(--shadow);
  font-size: 14px; font-weight: 600;
  animation: toastIn 0.2s ease;
}
.bot-ic { display: flex; color: var(--blue); }
.bot-ic svg { width: 20px; height: 20px; }
.bot-dots { display: inline-flex; gap: 4px; }
.bot-dots i { width: 5px; height: 5px; border-radius: 50%; background: var(--blue); display: inline-block; animation: botDot 1s infinite; }
.bot-dots i:nth-child(2) { animation-delay: 0.18s; }
.bot-dots i:nth-child(3) { animation-delay: 0.36s; }
@keyframes botDot { 0%, 100% { opacity: 0.3; transform: translateY(0); } 50% { opacity: 1; transform: translateY(-3px); } }

/* =================================================================
   Card inspector (right-click)
   ================================================================= */
.modal:has(.inspect) { max-width: 600px; }
.inspect { display: grid; grid-template-columns: 240px 1fr; gap: 22px; align-items: start; }
@media (max-width: 560px) { .inspect { grid-template-columns: 1fr; } }
.inspect-card { display: flex; justify-content: center; }
.inspect-facts { display: flex; flex-direction: column; gap: 7px; }
.inspect-name { font-size: 21px; font-weight: 800; margin-bottom: 4px; }
.inspect-row { display: flex; justify-content: space-between; gap: 14px; border-bottom: 1px solid var(--line); padding: 4px 0; }
.inspect-sub { margin-top: 12px; font-size: 12px; text-transform: uppercase; letter-spacing: 1px; color: var(--text-faint); font-weight: 700; }
.inspect-ability { color: #f0e6cf; padding: 2px 0; }

/* =================================================================
   In-page header bar (replaces the global top bar)
   ================================================================= */
.page-bar {
  display: flex; align-items: center; gap: 14px;
  margin-bottom: 22px; padding: 10px 14px;
  background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(0,0,0,0.18)), var(--bg-2);
  border: 1px solid var(--line); border-radius: 12px;
  box-shadow: var(--shadow-sm);
}
.board-view .page-bar { margin-bottom: 12px; }
.back-btn {
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--panel-2); border: 1px solid var(--line-2); color: var(--text);
  padding: 7px 13px 7px 10px; border-radius: 999px; font-size: 13.5px; font-weight: 700;
}
.back-btn:hover { border-color: var(--accent); color: var(--accent-2); }
.back-btn svg { width: 17px; height: 17px; }
.page-bar .crumbs { flex: 0 1 auto; }
.page-bar-actions { display: flex; gap: 8px; }

/* Floating home button (ui.js homeButton): the game editor's top-corner "back
   to menu" pill, reused on flat pages that dropped the breadcrumb bar. */
.home-row { margin-bottom: 18px; }
.home-btn {
  padding: 7px; border-radius: 999px;
  background: rgba(15,20,28,0.55); border: 1px solid rgba(255,255,255,0.1);
  color: var(--text-dim);
}
.home-btn svg { width: 16px; height: 16px; }
.home-btn:hover { border-color: var(--accent); color: var(--accent-2); background: rgba(15,20,28,0.7); }

/* =================================================================
   Editor pages: same game-like look as the rest
   ================================================================= */
.page-head h1 {
  background: linear-gradient(180deg, #fff, var(--accent-2));
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
}
.panel {
  background: linear-gradient(180deg, var(--panel), var(--bg-2));
  border: 1px solid var(--line-2);
  box-shadow: var(--shadow-sm);
  position: relative;
  overflow: hidden;
}
.panel::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
  background: linear-gradient(180deg, var(--accent-2), var(--accent));
  opacity: 0.85;
}
.section-title {
  color: var(--accent-2); position: relative; padding-left: 14px;
}
.section-title::before {
  content: ""; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  width: 5px; height: 5px; border-radius: 50%; background: var(--accent);
}
input[type="text"]:hover, input[type="number"]:hover, textarea:hover, select:hover { border-color: var(--line-2); }
.field-label { color: var(--text-dim); }

/* phase editor rows */
.phase-rows { display: flex; flex-direction: column; gap: 8px; }
.phase-row {
  display: grid; grid-template-columns: 24px 1fr 150px auto; gap: 10px; align-items: center;
  background: var(--bg); border: 1px solid var(--line-2); border-radius: var(--radius-sm); padding: 7px 9px;
}
.phase-row .phase-num { color: var(--accent-2); font-weight: 800; text-align: center; }
.phase-row select, .phase-row input { padding: 6px 8px; }

/* richer board accents */
.controls-bar { background: linear-gradient(180deg, var(--bg-3), var(--bg-2)); border-color: var(--line-2); }
.player-bar.is-active { box-shadow: 0 0 0 1px var(--accent) inset, 0 0 24px -6px rgba(216,162,74,0.5); }
.zone.battlefield.mine { background: linear-gradient(180deg, rgba(76,143,120,0.05), rgba(255,255,255,0.012)); }
.zone.battlefield.foe { background: linear-gradient(180deg, rgba(214,96,77,0.05), rgba(255,255,255,0.012)); }
.btn-primary { box-shadow: 0 2px 14px -4px rgba(216,162,74,0.6); }

/* =================================================================
   New Game wizard
   ================================================================= */
.wiz { max-width: 860px; margin: 10px auto 0; text-align: center; }
.wiz-dots { display: flex; gap: 8px; justify-content: center; margin-bottom: 26px; }
.wiz-dot { width: 26px; height: 5px; border-radius: 3px; background: var(--line); transition: background 0.2s; }
.wiz-dot.active { background: var(--accent); }
.wiz-dot.done { background: var(--accent-2); opacity: 0.55; }
.wiz-title { margin: 0; font-size: 30px; letter-spacing: 0.4px; }
.wiz-sub { margin: 10px auto 30px; color: var(--text-dim); font-size: 15px; max-width: 620px; line-height: 1.5; }

.wiz-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; }
.wiz-card {
  position: relative; text-align: left;
  display: flex; flex-direction: column; gap: 8px;
  padding: 22px 20px; border-radius: 16px;
  border: 1px solid var(--line-2); background: var(--panel-2);
  color: var(--text); cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: transform 0.12s, border-color 0.15s, filter 0.15s, box-shadow 0.15s;
}
.wiz-card:hover { transform: translateY(-4px); border-color: var(--accent); filter: brightness(1.06); box-shadow: var(--shadow); }
.wiz-card-ic {
  width: 52px; height: 52px; border-radius: 13px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(216,162,74,0.12); color: var(--accent-2); margin-bottom: 4px;
}
.wiz-card-ic svg { width: 28px; height: 28px; }
.wiz-num { font-size: 28px; font-weight: 800; color: var(--accent-2); }
.wiz-card-title { font-size: 19px; font-weight: 800; letter-spacing: 0.3px; }
.wiz-card-sub { font-size: 13px; color: var(--text-dim); line-height: 1.45; }
.wiz-tag {
  position: absolute; top: 14px; right: 14px;
  font-size: 10px; font-weight: 700; letter-spacing: 0.6px; text-transform: uppercase;
  color: var(--accent-2); background: rgba(216,162,74,0.14);
  border: 1px solid rgba(216,162,74,0.4); border-radius: 999px; padding: 3px 8px;
}

/* pop-in for option cards */
.pop { opacity: 0; animation: pop 0.32s cubic-bezier(0.2, 0.9, 0.3, 1.2) forwards; }
@keyframes pop { from { opacity: 0; transform: translateY(10px) scale(0.96); } to { opacity: 1; transform: none; } }

/* name step */
.wiz-name-step { max-width: 460px; margin: 0 auto; text-align: left; display: flex; flex-direction: column; gap: 20px; }
.wiz-name-field input { font-size: 18px; padding: 12px 14px; }
.wiz-summary { background: var(--panel-2); border: 1px solid var(--line); border-radius: 12px; padding: 6px 16px; }
.wiz-sum-row { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid var(--line); }
.wiz-sum-row:last-child { border-bottom: none; }
.wiz-sum-k { color: var(--text-dim); }
.wiz-sum-v { font-weight: 700; }
.wiz-actions { display: flex; justify-content: flex-end; }

/* =================================================================
   Flow editor (node system)
   ================================================================= */
.flow-banner { display: flex; align-items: center; justify-content: space-between; gap: 16px; border-color: var(--accent); }
.fe-block { margin-top: 22px; }
.fe-block-head { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; }
.fe-block-head .ic { color: var(--accent-2); display: flex; }
.fe-block-head .ic svg { width: 20px; height: 20px; }
.fe-block-head h2 { margin: 0; font-size: 20px; }
.fe-block-head p { margin: 0; color: var(--text-dim); font-size: 13px; flex-basis: 100%; }

.fe-event { background: rgba(255,255,255,0.015); border: 1px solid var(--line); border-radius: 14px; padding: 14px; margin-bottom: 14px; }
.fe-event-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.fe-event-title { font-weight: 700; color: var(--accent-2); }

.fe-steps { display: flex; flex-direction: column; gap: 10px; padding-left: 4px; }
.fe-node { background: var(--panel-2); border: 1px solid var(--line-2); border-radius: 12px; padding: 12px 14px; box-shadow: var(--shadow-sm); }
.fe-node-head { display: flex; align-items: center; gap: 10px; }
.fe-node-ic { width: 30px; height: 30px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; background: rgba(216,162,74,0.12); color: var(--accent-2); border-radius: 8px; }
.fe-node-ic svg { width: 17px; height: 17px; }
.fe-node-sum { flex: 1; font-size: 14px; font-weight: 600; }
.fe-node-body { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 12px; padding-left: 40px; }

.fe-field { display: flex; flex-direction: column; gap: 4px; }
.fe-field-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-faint); }
.fe-input { background: var(--bg-3); border: 1px solid var(--line-2); border-radius: 8px; color: var(--text); padding: 7px 9px; font: inherit; font-size: 13px; }
.fe-input:focus { outline: none; border-color: var(--accent); }
.fe-num { width: 80px; }

.fe-buttons { display: flex; flex-direction: column; gap: 8px; width: 100%; }
.fe-btn-row { display: flex; align-items: center; gap: 8px; }
.fe-btn-row .fe-input:first-child { width: 140px; }

.fe-slot { margin-top: 12px; margin-left: 24px; padding-left: 14px; border-left: 2px solid var(--line-2); }
.fe-slot-label { font-size: 12px; font-weight: 700; color: var(--text-dim); margin-bottom: 8px; }

.fe-add {
  display: inline-flex; align-items: center; gap: 8px; align-self: flex-start;
  background: transparent; border: 1px dashed var(--line-2); border-radius: 10px;
  color: var(--text-dim); padding: 9px 14px; cursor: pointer; font: inherit; font-size: 13px;
  transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.fe-add:hover { border-color: var(--accent); color: var(--accent-2); background: rgba(216,162,74,0.06); }
.fe-add-ic { display: flex; } .fe-add-ic svg { width: 15px; height: 15px; }
.fe-add-event { margin-top: 4px; }

/* min-width is what this menu WANTS, not a floor it may exceed: a bare
   min-width: 320px is wider than a 360px phone once the .ctx-menu padding and
   border are added, so the menu ran off the screen edge. min() lets it keep
   320px wherever there's room and give it back where there isn't. Same trick
   on .tok-creator below. Note max-height is 60vh, not dvh, on purpose: this is
   an inner scroll cap, not a layout height, so the stabler unit is the better
   one here. */
.fe-menu { display: flex; flex-direction: column; gap: 6px; min-width: min(320px, calc(100vw - 40px)); max-height: 60vh; overflow: auto; }
.fe-menu-item { display: flex; align-items: center; gap: 12px; text-align: left; background: var(--panel-2); border: 1px solid var(--line); border-radius: 10px; padding: 11px 13px; cursor: pointer; color: var(--text); transition: border-color 0.12s, transform 0.12s; }
.fe-menu-item:hover { border-color: var(--accent); transform: translateX(2px); }
.fe-menu-ic { width: 32px; height: 32px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; background: rgba(216,162,74,0.12); color: var(--accent-2); border-radius: 8px; }
.fe-menu-ic svg { width: 18px; height: 18px; }
.fe-menu-title { display: block; font-weight: 700; font-size: 14px; }
.fe-menu-sub { display: block; font-size: 12px; color: var(--text-dim); margin-top: 1px; }

/* =================================================================
   Flow play
   ================================================================= */
.flow-play { max-width: 720px; margin: 0 auto; }
.flow-strip { display: flex; gap: 22px; justify-content: center; margin: 8px 0 18px; }
.flow-stat { display: flex; align-items: center; gap: 8px; font-weight: 700; }
.flow-stat .ic { color: var(--accent-2); display: flex; } .flow-stat .ic svg { width: 18px; height: 18px; }
.flow-lives { display: flex; gap: 3px; }
.flow-lives .ic { color: var(--red); } .flow-lives .ic svg { width: 18px; height: 18px; }
.flow-faint { color: var(--text-faint); font-weight: 500; }

.flow-table { display: flex; gap: 40px; justify-content: center; align-items: flex-start; margin: 10px 0 18px; }
.flow-table.pos-left { justify-content: flex-start; padding-left: 40px; }
.flow-table.pos-right { justify-content: flex-end; padding-right: 40px; }
.flow-slot { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.flow-label { font-size: 12px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--text-faint); }

.flow-deck { width: 150px; height: 210px; border-radius: 12px; position: relative; cursor: default;
  background: linear-gradient(135deg, #2a3850, #1c2636); border: 2px solid var(--line-2);
  display: flex; align-items: center; justify-content: center; box-shadow: var(--shadow); transition: transform 0.12s, border-color 0.15s; }
.flow-deck.live { cursor: pointer; }
.flow-deck.live:hover { transform: translateY(-4px); border-color: var(--accent); }
.flow-deck-back { color: var(--accent-2); opacity: 0.85; } .flow-deck-back svg { width: 46px; height: 46px; }
.flow-deck-count { position: absolute; bottom: 8px; left: 0; right: 0; text-align: center; font-size: 12px; color: var(--text-dim); }
.flow-current .tcard { width: 150px; }
.flow-card-empty { width: 150px; height: 210px; border-radius: 12px; border: 2px dashed var(--line-2); display: flex; align-items: center; justify-content: center; color: var(--text-faint); font-size: 13px; }

.flow-msg { text-align: center; font-size: 15px; min-height: 22px; margin-bottom: 14px; }
.flow-choices { display: flex; gap: 14px; justify-content: center; }
.flow-choice { min-width: 120px; font-size: 16px; padding: 12px 22px; }
.flow-hint { text-align: center; color: var(--text-dim); }

.flow-end { position: fixed; inset: 0; z-index: 120; background: rgba(6,9,14,0.78); backdrop-filter: blur(3px); display: flex; align-items: center; justify-content: center; }
.flow-end-card { text-align: center; background: var(--panel); border: 1px solid var(--line-2); border-radius: 18px; padding: 34px 46px; box-shadow: var(--shadow); }
.flow-end-ic { width: 64px; height: 64px; margin: 0 auto 12px; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.flow-end-ic.good { background: rgba(76,174,122,0.16); color: var(--green); }
.flow-end-ic.bad { background: rgba(214,96,77,0.16); color: var(--red); }
.flow-end-ic svg { width: 34px; height: 34px; }
.flow-end-card h2 { margin: 6px 0; font-size: 28px; }
.flow-end-actions { display: flex; gap: 12px; justify-content: center; margin-top: 18px; }

/* turn-based board: player chips + a hand along the bottom */
.flow-pchip { display: flex; align-items: center; gap: 7px; padding: 6px 12px; border-radius: 999px;
  border: 1px solid var(--line-2); background: var(--bg-3); color: var(--text-dim); font-weight: 600; font-size: 13px; }
.flow-pchip .ic svg { width: 16px; height: 16px; }
.flow-pchip.up { border-color: var(--accent); color: var(--text); background: var(--panel); box-shadow: 0 0 0 1px var(--accent) inset; }
.flow-pchip-n { background: var(--bg-2); border-radius: 999px; min-width: 20px; text-align: center; padding: 1px 7px; font-size: 12px; }

.flow-hand-wrap { margin-top: 8px; }
.flow-hand-label { text-align: center; font-size: 12px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--text-faint); margin-bottom: 10px; }
.flow-hand { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.flow-hand-card { transition: transform 0.12s, filter 0.15s; border-radius: 12px; }
.flow-hand-card .tcard { width: 116px; }
.flow-hand-card.playable { cursor: pointer; box-shadow: 0 0 0 2px var(--green); }
.flow-hand-card.playable:hover { transform: translateY(-8px); }
.flow-hand-card.dim { filter: grayscale(0.55) brightness(0.72); }

.ge-example-choices { display: flex; flex-direction: column; gap: 10px; }
.ge-example-choices p { color: var(--text-dim); margin: 0 0 4px; }
.ge-field-row { flex-direction: row; align-items: center; gap: 8px; }
.ge-toggle { width: 15px; height: 15px; accent-color: var(--accent); }

/* =================================================================
   Graph editor (wire canvas)
   ================================================================= */
.view.graph-editor { max-width: none; padding: 18px 18px 0; }
.ge-wrap { display: flex; gap: 14px; height: calc(100vh - 120px); height: calc(100dvh - 120px); min-height: 460px; }

.ge-palette { width: 190px; flex-shrink: 0; overflow-y: auto; padding-right: 4px; }
.ge-palette-title { font-size: 12px; text-transform: uppercase; letter-spacing: 0.7px; color: var(--text-faint); margin-bottom: 10px; }
.ge-pal-group { font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--accent-2); margin: 14px 0 6px; }
.ge-pal-item { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; background: var(--panel-2); border: 1px solid var(--line); border-radius: 9px; padding: 8px 10px; margin-bottom: 6px; color: var(--text); cursor: pointer; font-size: 13px; transition: border-color 0.12s, transform 0.12s; }
.ge-pal-item:hover { border-color: var(--accent); transform: translateX(2px); }
.ge-pal-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
.ge-pal-ic { display: flex; color: var(--text-dim); } .ge-pal-ic svg { width: 15px; height: 15px; }

.ge-canvas {
  flex: 1; position: relative; overflow: hidden; cursor: grab;
  border: 1px solid var(--line); border-radius: 16px;
  background-color: #0c0f15;
  background-image: radial-gradient(circle, rgba(255,255,255,0.05) 1px, transparent 1px);
  background-size: 22px 22px;
}
.ge-canvas:active { cursor: grabbing; }
.ge-world { position: absolute; inset: 0; transform-origin: 0 0; }
.ge-wires { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; }
.ge-wire { pointer-events: none; }
.ge-wire-hit { pointer-events: stroke; cursor: pointer; }

.ge-zoom { position: absolute; right: 14px; bottom: 14px; z-index: 8; display: flex; align-items: center; gap: 4px;
  background: rgba(20,26,36,0.92); border: 1px solid var(--line-2); border-radius: 999px; padding: 4px; box-shadow: var(--shadow); }
.ge-zoom .btn { border: none; background: transparent; padding: 6px; }
.ge-zoom .btn:hover { background: var(--bg-3); }
.ge-zoom-pct { min-width: 48px; text-align: center; font-size: 12px; font-weight: 600; color: var(--text-dim);
  background: transparent; border: none; cursor: pointer; padding: 4px 2px; border-radius: 8px; }
.ge-zoom-pct:hover { color: var(--text); background: var(--bg-3); }

.ge-node {
  position: absolute; width: 212px;
  background: var(--panel); border: 1px solid var(--line-2); border-radius: 13px;
  box-shadow: 0 6px 20px -6px rgba(0,0,0,0.6); overflow: visible;
}
.ge-node-head {
  display: flex; align-items: center; gap: 8px; cursor: grab;
  padding: 9px 10px; border-radius: 12px 12px 0 0;
  background: var(--acc);
  background: linear-gradient(180deg, color-mix(in srgb, var(--acc) 88%, #000 0%), color-mix(in srgb, var(--acc) 62%, #000 20%));
  color: #fff;
}
.ge-node-head:active { cursor: grabbing; }
.ge-node-ic { display: flex; } .ge-node-ic svg { width: 17px; height: 17px; }
.ge-node-title { flex: 1; font-weight: 700; font-size: 13px; }
.ge-node-head .ge-del { color: rgba(255,255,255,0.85); }
.ge-node-head .ge-del:hover { color: #fff; }

.ge-node-body { padding: 10px; display: flex; flex-direction: column; gap: 9px; }
.ge-node-hint { font-size: 11px; color: var(--text-faint); line-height: 1.4; }
.ge-field { display: flex; flex-direction: column; gap: 3px; }
.ge-field-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-faint); }
.ge-input { background: var(--bg-3); border: 1px solid var(--line-2); border-radius: 7px; color: var(--text); padding: 6px 8px; font: inherit; font-size: 13px; }
.ge-input:focus { outline: none; border-color: var(--accent); }
.ge-num { width: 70px; }
.ge-btns { display: flex; flex-direction: column; gap: 6px; }
.ge-btn-row { display: flex; align-items: center; gap: 5px; }
.ge-btn-row .ge-input:first-child { width: 78px; }

.ge-outs { display: flex; flex-direction: column; gap: 2px; padding: 4px 10px 10px; }
.ge-out { position: relative; display: flex; justify-content: flex-end; align-items: center; min-height: 22px; padding-right: 6px; }
.ge-out-label { font-size: 11px; font-weight: 700; color: var(--text-dim); }
.ge-out-label.faint { color: var(--text-faint); font-weight: 500; }

.ge-port {
  position: absolute; width: 14px; height: 14px; border-radius: 50%;
  background: var(--bg); border: 3px solid var(--acc, var(--accent));
  cursor: crosshair; z-index: 2; transition: transform 0.1s;
}
.ge-port:hover { transform: scale(1.25); }
.ge-port.in { left: -8px; top: 14px; border-color: var(--line-2); }
.ge-port.out { right: -8px; top: 50%; transform: translateY(-50%); }
.ge-port.out:hover { transform: translateY(-50%) scale(1.25); }

.ge-empty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; }
.ge-empty-card { pointer-events: auto; max-width: 380px; text-align: center; background: rgba(20,26,36,0.92); border: 1px solid var(--line-2); border-radius: 16px; padding: 28px 30px; box-shadow: var(--shadow); }
.ge-empty-ic { width: 56px; height: 56px; margin: 0 auto 12px; border-radius: 14px; display: flex; align-items: center; justify-content: center; background: rgba(216,162,74,0.12); color: var(--accent-2); }
.ge-empty-ic svg { width: 30px; height: 30px; }
.ge-empty-card h3 { margin: 0 0 8px; font-size: 20px; }
.ge-empty-card p { margin: 0 0 18px; color: var(--text-dim); font-size: 14px; line-height: 1.5; }

/* =================================================================
   Table builder: a tangible 3D tabletop you drag real-feeling pieces on
   ================================================================= */
.table-view { max-width: 1640px; }
.table-bar-hint {
  display: flex; align-items: center; gap: 9px; color: var(--text-dim);
  font-size: 13px; margin: 0 2px 10px;
}
.table-bar-hint svg { width: 16px; height: 16px; color: var(--accent-2); }
.table-bar-hint b { color: var(--text); font-weight: 700; }

/* seat rail above the table */
.seat-rail { display: flex; align-items: center; gap: 10px; margin: 0 2px 10px; flex-wrap: wrap; }
.seat {
  display: flex; align-items: center; gap: 7px; padding: 4px 11px 4px 5px;
  border-radius: 999px; border: 1px solid var(--line-2);
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
  font-weight: 700; font-size: 12.5px; color: var(--text-dim);
  box-shadow: var(--shadow-sm); transition: border-color 0.15s, color 0.15s, transform 0.1s;
}
.seat.active { border-color: var(--accent); color: var(--text); box-shadow: 0 0 0 1px var(--accent) inset, 0 0 22px -8px rgba(216,162,74,0.6); }

/* ---- unlockable seat borders (js/borders.js) ----
   The frames themselves. All CSS - rings, masks and a couple of animated conic
   gradients - so a border costs nothing to ship, recolours itself from the
   wearer's crest, and stays sharp at any interface scale. To change one, edit
   its `.brd-<id>` rules and leave everything else alone: the catalog entry, the
   picker, the pill and the tests never look at how a border is painted.

   `beam`, `shimmer` and `aurora` move. Keep any motion slow: this sits beside a
   game in progress, and a frame that pulls the eye every few seconds is a frame
   nobody will wear. The reduce-motion block at the top of this file reaches
   ::before/::after, so each animated frame must still read as a frame when it is
   standing still.

   All of them ride on ::before / ::after of the pill, never on its own border
   or background, so the seat's active state, its AI tint and its hover keep
   working underneath whatever frame is on top. `.seat` gains position:relative
   for this; the pseudo-elements are click-through so none of them can eat a
   click meant for the crest or the remove X. (::before and ::after are both
   free on .seat itself - the ones already spoken for are on .seat-av.)

   --seat-tint is the wearer's own crest colour, written by renderSeats, so a
   coloured frame agrees with the mark inside it. */
.seat { position: relative; }
/* Keyed off an explicit `has-brd` class rather than a substring match on the
   class list, so nothing depends on where "brd-" happens to sit in the string. */
/* Every .brd-* rule below is written against its own class alone, so this one
   selector list is the whole cost of letting a second surface wear the frames:
   the seat run's pill (.sdial-tag) gets the entire catalog for free. */
.seat.has-brd::before, .seat.has-brd::after,
.sdial-tag.has-brd::before, .sdial-tag.has-brd::after { content: ""; position: absolute; border-radius: 999px; pointer-events: none; }
.seat.has-brd::before { inset: -3px; }
.seat.has-brd::after { inset: -6px; }
/* A run's pill wears the same frames on a tighter ring. It rings the WHOLE pill,
   the way it rings the whole seat pill above and for the same reason: a ring
   round the crest inside a pill draws two rings on one control, and the smaller
   of them is the one nobody can see at 16px. The insets are half the rail
   pill's, because this one lies IN 24px of wood - at -3/-6 the outer ring would
   be wider than the board it is set into. */
/* A FRAME'S REACH IS GUARANTEED BY CONSTRUCTION, NOT BY MEASUREMENT, and this
   is the one thing to know before touching either number. A frame is drawn on
   ::before/::after, and a pseudo-element contributes nothing to its parent's
   box - so fitSeatDialBadges, which decides what a run gives up by reading
   rendered boxes, cannot see a frame at all. The arithmetic has to hold on its
   own: the pill is 18px (a 16px crest + 1px of padding) and the outer ring sits
   2px beyond it, which is 22px inside 24px of wood. Widen either and a frame
   crosses the rail's edge with nothing to catch it - measured, -1.5/-3 put it
   1px over. */
.sdial-tag.has-brd::before { inset: -1px; }
.sdial-tag.has-brd::after { inset: -2px; }

/* earned by playing */
.brd-hairline::before { border: 1px solid rgba(255,255,255,0.22); }
.brd-double::before { border: 1px solid rgba(255,255,255,0.24); }
.brd-double::after { border: 1px solid rgba(255,255,255,0.12); }
/* A ring in the wearer's colour that falls away towards the bottom, so the pill
   reads as lit from above rather than outlined. The fade is a mask on a plain
   ring, which keeps the colour honest at the top instead of muddying it. */
.brd-fade::before {
  inset: -3px; border: 1.5px solid var(--seat-tint, var(--accent-2));
  -webkit-mask: linear-gradient(#000 15%, rgba(0,0,0,0.08) 95%);
  mask: linear-gradient(#000 15%, rgba(0,0,0,0.08) 95%);
}
.brd-glow::before { border: 1px solid var(--seat-tint, var(--accent-2)); box-shadow: 0 0 14px -2px var(--seat-tint, var(--accent-2)); opacity: 0.75; }
/* One bright arc travelling round a dim ring. The arc is a conic gradient whose
   start angle is animated, and the middle is punched out so only the band
   paints. Slow on purpose: it should be noticed once and then ignored, not
   pull the eye away from the table every four seconds. */
.brd-beam::before { border: 1px solid rgba(255,255,255,0.09); }
.brd-beam::after {
  inset: -5px;
  background: conic-gradient(from var(--brd-spin, 0deg),
    transparent 0 55%, var(--seat-tint, var(--accent-2)) 74%, transparent 88% 100%);
  -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
  mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
  animation: brdSpin 7s linear infinite;
}

/* earned by building */
.brd-drafted::before { border: 1px dashed rgba(255,255,255,0.30); }
.brd-stacked::before { border: 1px solid rgba(255,255,255,0.20); }
.brd-stacked::after { inset: -6px; border: 1px solid rgba(255,255,255,0.13); transform: translate(1.5px, 1.5px); }
.brd-inked::before { border: 2px solid var(--seat-tint, var(--accent-2)); opacity: 0.9; }
.brd-engraved::before { inset: -4px; border: 1px solid rgba(0,0,0,0.55); box-shadow: 0 1px 0 rgba(255,255,255,0.18), inset 0 1px 2px rgba(0,0,0,0.5); }

/* the paid tier */
.brd-gilded::before { inset: -4px; border: 2px solid transparent;
  background: linear-gradient(140deg, #f5dfa4, #b98c33 40%, #6d4f14 55%, #e8c977 80%) border-box;
  -webkit-mask: linear-gradient(#000 0 0) padding-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask: linear-gradient(#000 0 0) padding-box, linear-gradient(#000 0 0); mask-composite: exclude; }
.brd-shimmer::before { inset: -4px; border: 2px solid transparent;
  background: conic-gradient(from var(--brd-spin, 0deg), transparent 0 60%, rgba(255,255,255,0.85) 72%, transparent 84% 100%),
              linear-gradient(0deg, rgba(255,255,255,0.22), rgba(255,255,255,0.22)) border-box;
  background-origin: border-box;
  -webkit-mask: linear-gradient(#000 0 0) padding-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask: linear-gradient(#000 0 0) padding-box, linear-gradient(#000 0 0); mask-composite: exclude;
  animation: brdSpin 4.5s linear infinite; }
.brd-aurora::before { inset: -4px; border: 2px solid transparent;
  background: conic-gradient(from var(--brd-spin, 0deg), #7dd4a1, #79c8d8, #af81cf, #dc74a8, #e7d26a, #7dd4a1) border-box;
  background-origin: border-box; opacity: 0.85;
  -webkit-mask: linear-gradient(#000 0 0) padding-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask: linear-gradient(#000 0 0) padding-box, linear-gradient(#000 0 0); mask-composite: exclude;
  animation: brdSpin 9s linear infinite; }
/* given, never earned or bought (js/pro.js hands it out with a comped year)
   A plain ring in the wearer's colour with one solid mark at the top, like the
   pin on a proof copy. Deliberately quiet and deliberately still: it has to sit
   next to the paid frames without competing with them, and a frame that only a
   few dozen people will ever wear should not be the loudest thing in the rail.
   The mark is ::after pulled off the ring - it overrides the shared inset, so
   it is a pip rather than a second ring. */
.brd-playtester::before { inset: -3px; border: 1.5px solid var(--seat-tint, var(--accent-2)); opacity: 0.8; }
.brd-playtester::after {
  inset: auto; left: 50%; top: -7px; width: 5px; height: 5px; margin-left: -2.5px;
  background: var(--seat-tint, var(--accent-2));
  box-shadow: 0 0 8px -1px var(--seat-tint, var(--accent-2));
}
@property --brd-spin { syntax: "<angle>"; initial-value: 0deg; inherits: false; }
@keyframes brdSpin { to { --brd-spin: 360deg; } }
.seat.draggable { cursor: grab; }
/* the picked-up pill: a floating clone that tracks the cursor above the rail */
.seat-ghost {
  position: fixed; z-index: 1200; pointer-events: none; margin: 0;
  cursor: grabbing; opacity: 0.97;
  transform: rotate(-2deg) scale(1.05);
  box-shadow: 0 10px 24px -8px rgba(0,0,0,0.6), 0 0 0 1px var(--accent) inset;
}
/* the gap left behind while its clone is in the air */
.seat.seat-lift { opacity: 0.28; }
.seat .seat-av {
  width: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
  background: var(--bg-3); color: var(--accent-2); border: 1px solid var(--line-2);
}
.seat .seat-av svg { width: 15px; height: 15px; }
/* In build mode the avatar is the cue that a seat is clickable to make it the
   active player - give it a pointer and an accent lift on hover so it reads as
   the click target (the whole pill still selects; this just points the eye). */
.seat .seat-av.sel { cursor: pointer; transition: color 0.15s, border-color 0.15s, background 0.15s; }
.seat .seat-av.sel:hover { color: var(--accent); border-color: var(--accent); background: rgba(216,162,74,0.14); }
.seat.is-ai .seat-av.sel:hover { color: #9dd2ff; border-color: #5aa9e6; background: rgba(124,196,255,0.16); }
/* A seat wearing a crest keeps its own tint on hover (the inline colour wins),
   so only the ring and the wash react - the mark stays the player's colour. */
.seat .seat-av.has-crest svg { width: 17px; height: 17px; }
.seat .seat-av.has-crest.sel:hover { border-color: currentColor; background: rgba(255,255,255,0.10); }
/* The crest picker: a wrapping grid of the premade marks, on the flyout glass.
   Sized against the compact scale, one notch up from the .ce-icon glyph grid
   because a crest is picked for its look, not read as a label. */
.crest-fly .crest-grid { display: grid; grid-template-columns: repeat(4, 40px); gap: 7px; }
.crest-pick {
  width: 40px; height: 40px; border-radius: 10px; border: 1px solid var(--line);
  background: rgba(255,255,255,0.04); cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  transition: border-color 0.12s, background 0.12s, transform 0.12s;
}
.crest-pick svg { width: 22px; height: 22px; }
.crest-pick:hover { border-color: currentColor; background: rgba(255,255,255,0.09); transform: translateY(-1px); }
.crest-pick.sel { border-color: var(--accent-2); box-shadow: inset 0 0 0 1px var(--accent-2); background: rgba(255,255,255,0.09); }
.seat .seat-x { display: inline-flex; color: var(--text-faint); padding: 2px; border-radius: 50%; border: none; background: none; }
.seat { cursor: default; }
.seat .seat-x:hover { color: var(--red); }
.seat .seat-x svg { width: 13px; height: 13px; }
.seat-add {
  display: inline-flex; align-items: center; gap: 7px; padding: 8px 14px;
  border-radius: 999px; border: 1px dashed var(--line-2); background: transparent;
  color: var(--text-dim); font-weight: 700; font-size: 13px; transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.seat-add:hover { border-color: var(--accent); color: var(--accent-2); background: rgba(216,162,74,0.06); }
.seat-add svg { width: 15px; height: 15px; }

/* work area: a slim tool rail on the left, the table filling the rest */
.table-work { display: flex; gap: 14px; align-items: stretch; }
.table-tools {
  width: 138px; flex-shrink: 0; display: flex; flex-direction: column; gap: 6px;
  padding: 12px 10px; border-radius: 16px; align-self: flex-start;
  background: linear-gradient(180deg, var(--panel), var(--bg-2)); border: 1px solid var(--line);
  box-shadow: var(--shadow-sm); position: sticky; top: 16px;
}
.tool-group-label { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.7px; color: var(--text-faint); font-weight: 700; margin: 8px 4px 3px; }
.tool-group-label:first-child { margin-top: 2px; }
.tool-btn {
  display: flex; align-items: center; gap: 9px; width: 100%; text-align: left;
  padding: 9px 10px; border-radius: 10px; border: 1px solid var(--line-2);
  background: var(--panel-2); color: var(--text); font: inherit; font-size: 13px; font-weight: 600;
  cursor: pointer; transition: border-color 0.12s, transform 0.1s, background 0.12s;
  white-space: nowrap; min-height: 38px;
}
.tool-btn > span:last-child { overflow: hidden; text-overflow: ellipsis; }
.tool-btn.icon-only { justify-content: center; gap: 0; }
.tool-btn:hover { border-color: var(--accent); transform: translateX(2px); background: var(--bg-3); }
/* Nothing to undo / redo: dim the button and drop its hover lift. */
.tool-btn:disabled, .tool-btn.is-disabled { opacity: 0.35; cursor: default; }
.tool-btn:disabled:hover, .tool-btn.is-disabled:hover { border-color: var(--line-2); transform: none; background: var(--panel-2); }
.tool-btn .tool-ic { display: flex; align-items: center; justify-content: center; flex: 0 0 17px; width: 17px; height: 17px; color: var(--accent-2); } .tool-btn .tool-ic svg { width: 17px; height: 17px; }
.size-row { display: flex; gap: 6px; }
.size-row .tool-btn { justify-content: center; padding: 8px 0; }
.tool-icon-row { display: flex; flex-direction: column; gap: 6px; align-items: center; }
/* Match the game editor's compact rail buttons (.tool-rail), just a touch
   bigger since there are only three. Square, icon-only, not full-width. */
.tool-icon-row .tool-btn {
  width: 30px; min-width: 30px; height: 30px; min-height: 30px;
  padding: 0; border-radius: 8px; gap: 0; justify-content: center;
}
.tool-icon-row .tool-btn:hover { transform: none; }
.tool-icon-row .tool-btn .tool-ic,
.tool-icon-row .tool-btn .tool-ic svg { width: 18px; height: 18px; flex: 0 0 18px; }
/* Stacked vertically, so the hover label sits to the right, clear of the next
   button. Extra .icon-only keeps this ahead of the base below-the-button rule. */
.tool-icon-row .tool-btn.icon-only[data-tooltip]::after { top: 50%; bottom: auto; left: calc(100% + 8px); transform: translateY(-50%); }
/* The card rail is short, so let its label escape the rail box (its overflow:auto
   would otherwise clip a tooltip sitting to the right). */
.table-tools.floating.ce-rail { overflow: visible; width: auto; padding: 6px 8px; }
/* Hug the compact button column and center the group label over it, so the
   card rail reads like the game editor's tool rail rather than a wide panel. */
.ce-rail .tool-group-label { text-align: center; margin: 4px 6px 4px; }

/* Compact build rail (Photoshop-style): a single narrow column of icon buttons,
   no group labels or inline reads. Overrides the wider default .table-tools
   floating panel. Width follows the content (a 20px button + tiny padding),
   giving a ~26px column - about half the old width. This is the GAME editor's
   tool rail (.tool-rail); the card editor's own rail (.ce-rail) is left alone. */
.table-tools.floating.tool-rail { width: auto; overflow: visible; }
.tool-rail .rail-body { padding: 3px; gap: 5px; align-items: center; }
.tool-rail .rail-body .tool-btn {
  width: 28px; min-width: 28px; height: 28px; min-height: 28px;
  padding: 0; border-radius: 8px; gap: 0; justify-content: center;
}
.tool-rail .rail-body .tool-btn:hover { transform: none; } /* no sideways nudge in a tight column */
.tool-rail .rail-body .tool-btn .tool-ic,
.tool-rail .rail-body .tool-btn .tool-ic svg { width: 18px; height: 18px; flex: 0 0 18px; }
/* Section dividers between icon groups. */
.tool-rail-sep { width: 18px; height: 1px; background: rgba(255,255,255,0.12); margin: 1px 0; }
/* Compact head: the "TOOLS" title only shows when the rail is collapsed into
   the top bar; expanded, the grip and minimise button stack vertically so the
   head never forces the column wider than a single button. */
.tool-rail .rail-head { padding: 4px 6px; gap: 4px; justify-content: space-between; }
.tool-rail:not(.minimized) .rail-head { flex-direction: column; justify-content: center; gap: 3px; padding: 4px 3px; }
.tool-rail .rail-title { display: none; }
.tool-rail.minimized .rail-title { display: inline; }
.tool-rail .rail-min { width: 22px; height: 22px; }
.tool-rail .rail-min svg { width: 13px; height: 13px; }
.tool-rail .rail-grip svg { width: 13px; height: 13px; }

/* Custom tooltip - fast, theme-matched replacement for the native `title`
   popup. Kept at pill scale (12px text, snug padding) per the compact rule. */
.tt-tip {
  position: fixed;
  z-index: 9999;
  max-width: 240px;
  padding: 6px 9px;
  font-family: var(--font);
  font-size: 12px;
  line-height: 1.35;
  color: var(--text);
  background: var(--panel-2);
  border: 1px solid var(--line-2);
  border-radius: 8px;
  box-shadow: var(--shadow-sm);
  pointer-events: none;
  opacity: 0;
  transform: translateY(-2px);
  transition: opacity 90ms ease, transform 90ms ease;
}
.tt-tip.show { opacity: 1; transform: translateY(0); }

/* ---------- Contextual help mode (js/help.js) ----------
   A ? toggle in each toolbar puts the app in inspect mode: tagged controls
   ([data-help]) glow on hover and click open a glass bubble. Same glass skin
   as the rest of the floating surfaces, at pill scale. */
/* Lit look for every ? button while help mode is on, driven off the body class
   so re-rendered toolbars stay correct without a JS pass. Matches .tool-btn.on. */
body.help-mode [data-help-toggle] {
  background: rgba(216,162,74,0.14) !important;
  border-color: rgba(216,162,74,0.45) !important;
  color: var(--accent-2, var(--accent)) !important;
}
/* In help mode every tagged control reads as pokeable: help cursor + a soft
   ring on hover, and a steady gold ring on the one whose bubble is open. */
body.help-mode [data-help] { cursor: help; }
body.help-mode [data-help]:hover {
  outline: 2px solid rgba(216,162,74,0.5);
  outline-offset: 2px;
  border-radius: 8px;
}
body.help-mode [data-help].help-target {
  outline: 2px solid var(--accent, #d8a24a);
  outline-offset: 2px;
  border-radius: 8px;
}

/* The floating help banner: a compact glass pill, top-center, telling the user
   they're in help mode and how to leave. Transient overlay, not a nav bar. */
.help-banner {
  position: fixed; top: 12px; left: 50%; transform: translate(-50%, -8px);
  z-index: 10050;
  display: flex; align-items: center; gap: 8px;
  padding: 6px 8px 6px 10px; border-radius: 999px;
  font-family: var(--font); font-size: 12px; color: var(--text);
  background: rgba(15,20,28,0.72);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  border: 1px solid rgba(255,255,255,0.12);
  box-shadow: 0 10px 30px rgba(0,0,0,0.45);
  opacity: 0; pointer-events: none;
  transition: opacity 120ms ease, transform 120ms ease;
}
.help-banner.show { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }
.help-banner-ic { display: inline-flex; color: var(--accent-2, var(--accent)); }
.help-banner-ic svg { width: 15px; height: 15px; }
.help-banner-text { opacity: 0.92; }
.help-banner-done {
  margin-left: 2px; padding: 3px 10px; border-radius: 999px;
  font-size: 11px; font-weight: 700; color: #14100a;
  background: linear-gradient(180deg, var(--accent-2, #e6b45e), var(--accent, #d8a24a));
  border: none; cursor: pointer;
}
.help-banner-done:hover { filter: brightness(1.06); }

/* The info bubble: glass, pill-scale text, anchored to the clicked control. */
.help-bubble {
  position: fixed; z-index: 10060;
  width: max-content; max-width: 260px;
  padding: 10px 12px 11px; border-radius: 11px;
  font-family: var(--font); color: var(--text);
  background: rgba(15,20,28,0.86);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  border: 1px solid rgba(255,255,255,0.12);
  box-shadow: 0 14px 40px rgba(0,0,0,0.5);
  opacity: 0; transform: translateY(-3px);
  transition: opacity 110ms ease, transform 110ms ease;
  pointer-events: none;
}
.help-bubble.show { opacity: 1; transform: translateY(0); pointer-events: auto; }
.help-bubble-title {
  font-size: 12.5px; font-weight: 700; letter-spacing: 0.2px;
  padding-right: 16px; margin-bottom: 4px;
}
.help-bubble-body { font-size: 11.5px; line-height: 1.4; opacity: 0.9; }
.help-bubble.is-empty .help-bubble-body { font-style: italic; opacity: 0.6; }
.help-bubble-x {
  position: absolute; top: 6px; right: 6px;
  width: 20px; height: 20px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--text); background: transparent; border: none; cursor: pointer;
  opacity: 0.6; border-radius: 6px;
}
.help-bubble-x:hover { opacity: 1; background: rgba(255,255,255,0.08); }
.help-bubble-x svg { width: 13px; height: 13px; }

/* ---------------------------------------------------------------------------
   The guided first build (js/tutorial.js). Same glass skin as the help bubble
   above, deliberately - the two will drift in content but must never drift in
   look. What is new here is the scrim: four rectangles around the live control,
   never a huge box-shadow, so the hole is genuinely absent and needs no
   pointer-events work to stay clickable. A beat pointing at something the app
   draws at an ANGLE says `spot` instead and gets .tut-spot below, where one
   clipped scrim cuts the target's real corners - the four-rect hole stays the
   default for everything square.

   z-index sits BELOW help mode on purpose (banner 10050, bubble 10060), so a
   player who opens help mode mid-tutorial gets help mode on top.
---------------------------------------------------------------------------- */
/* AN INVISIBLE OVERLAY MUST NOT BE HIT-TESTABLE, and neither half of the first
   line here achieved that. `opacity: 0` hides a layer and leaves every press
   landing on it, and `pointer-events: none` does NOT reach a descendant that
   sets `pointer-events: auto` for itself - which .tut-bubble does, because the
   pill is the one part of the tutorial chrome you press. So a run that stopped
   left its last bubble standing, transparent, at z-index 10040, wherever the
   beat had parked it. Quitting on the felt beat parks it over the middle of the
   felt, so cards under it could not be picked up - silently, since the press
   really did land on something and no rule refused it (report
   tcg-report-20260917-181800). `visibility` is the property that answers this:
   it is inherited, it is not overridden by pointer-events, and it takes a
   descendant out of hit-testing altogether. It transitions as a step that holds
   `visible` for the whole fade, so the hide still fades out. */
.tut-root { position: fixed; inset: 0; z-index: 10040; pointer-events: none; opacity: 0; visibility: hidden; transition: opacity 140ms ease, visibility 140ms ease; }
.tut-root.show { opacity: 1; visibility: visible; }
.tut-scrim { position: fixed; background: rgba(6,9,13,0.52); pointer-events: none; }
.tut-hole {
  position: fixed; border-radius: 10px; pointer-events: none;
  outline: 2px solid var(--accent, #d8a24a); outline-offset: 2px;
  box-shadow: 0 0 0 1px rgba(216,162,74,0.25), 0 0 22px rgba(216,162,74,0.18);
  transition: left 140ms ease, top 140ms ease, width 140ms ease, height 140ms ease, opacity 130ms ease;
}
/* A beat CHANGE is a cut, not a flight (js/tutorial.js, cutStart/cutEnd). The
   transition above exists so the light can follow one control while a panel
   reflows under it; left alone it also drags the box from one beat's control to
   the next one's, across the whole screen and over everything in between. So a
   beat change suppresses it, moves the box dark, and fades it back up. */
.tut-hole.tut-jump, .tut-spot.tut-jump { transition: none; }

/* The shaped spotlight (a beat's `spot`, js/tutorial.js). A rectangle is the
   wrong hole for anything drawn at an angle: on the menu fan every card is
   rotated, so the hole above cut a box a third bigger than the card and the
   outline round it read as a box drawn NEAR the card rather than as the card.
   In spot mode one scrim covers the viewport and a slit polygon cuts the
   target's own four corners out of it, so the only thing not dimmed is the card
   itself, tilt and rounded corners included. No outline here on purpose - the
   glow bleeds outwards onto the dim, which is what makes it read as lit rather
   than as framed. */
.tut-spot {
  position: fixed; pointer-events: none; background: none; border: 0;
  animation: tutSpotGlow 2.4s ease-in-out infinite;
  transition: opacity 130ms ease;
  box-shadow: 0 0 26px 8px rgba(216,162,74,0.30), 0 0 0 1px rgba(230,180,94,0.32);
}
@keyframes tutSpotGlow {
  0%, 100% { box-shadow: 0 0 20px 5px rgba(216,162,74,0.22), 0 0 0 1px rgba(230,180,94,0.26); }
  50%      { box-shadow: 0 0 36px 13px rgba(216,162,74,0.44), 0 0 0 1px rgba(230,180,94,0.5); }
}

.tut-bubble {
  position: fixed; width: max-content;
  max-width: min(300px, calc(100vw - 32px));
  padding: 10px 12px 8px; border-radius: 11px;
  font-family: var(--font); color: var(--text);
  background: rgba(15,20,28,0.86);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  border: 1px solid rgba(255,255,255,0.12);
  box-shadow: 0 14px 40px rgba(0,0,0,0.5);
  pointer-events: auto;
}
.tut-bubble-title { font-size: 12.5px; font-weight: 700; letter-spacing: 0.2px; margin-bottom: 4px; }
.tut-bubble-body { font-size: 11.5px; line-height: 1.4; opacity: 0.9; }
/* The spoken line, printed. Voice arrives later; this is what makes the run
   completable with the sound off, and it comes from the same string that gets
   recorded, so caption and audio can't disagree. */
.tut-cc {
  margin-top: 7px; padding-top: 6px; border-top: 1px solid rgba(255,255,255,0.08);
  font-size: 10.5px; line-height: 1.35; font-style: italic; opacity: 0.55;
}

/* Choice sub-rows, at pill scale to match .tphase. */
.tut-choices { display: flex; flex-direction: column; gap: 4px; margin-top: 8px; }
.tut-choice {
  display: block; width: 100%; text-align: left; cursor: pointer;
  padding: 6px 10px; border-radius: 999px; font-size: 11px; line-height: 1.3;
  color: var(--text); background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.10);
}
.tut-choice:hover { background: rgba(255,255,255,0.09); }
.tut-choice.on { border-color: rgba(216,162,74,0.55); background: rgba(216,162,74,0.14); }
.tut-choice strong { font-weight: 700; }
.tut-choice span { opacity: 0.62; }

.tut-foot { display: flex; align-items: center; gap: 6px; margin-top: 9px; }
.tut-foot-wide .tut-fbtn { flex: 1; text-align: center; }
.tut-pos { font-size: 10px; opacity: 0.45; margin-right: auto; letter-spacing: 0.3px; }
.tut-fbtn {
  padding: 3px 10px; border-radius: 999px; font-size: 10.5px; font-weight: 600;
  cursor: pointer; color: var(--text);
  background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.10);
}
.tut-fbtn:hover { background: rgba(255,255,255,0.10); }
/* Back and Start over: bare icons at pill scale, per the icon-only rule. An
   idle one (Back on the first beat) stays exactly where it is and stops
   answering, rather than leaving a gap that moves everything else along. */
.tut-ibtn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; padding: 0; border-radius: 999px;
  cursor: pointer; color: var(--text); opacity: 0.7;
  background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.10);
}
.tut-ibtn svg { width: 12px; height: 12px; display: block; }
.tut-ibtn:hover { opacity: 1; background: rgba(255,255,255,0.10); }
.tut-ibtn.off { opacity: 0.25; cursor: default; }
.tut-ibtn.off:hover { opacity: 0.25; background: rgba(255,255,255,0.05); }
.tut-quit { opacity: 0.7; }
/* The one primary action on a beat that has no gesture to gate on. Gold, like
   every other primary in the app. */
.tut-go {
  color: #14100a; border-color: transparent; font-weight: 700;
  background: linear-gradient(180deg, var(--accent-2, #e6b45e), var(--accent, #d8a24a));
}
.tut-go:hover { filter: brightness(1.06); background: linear-gradient(180deg, var(--accent-2, #e6b45e), var(--accent, #d8a24a)); }
/* A confirm button with nothing to confirm yet. It STAYS where it is and stops
   answering rather than appearing once the beat is nearly done - the same
   treatment .tut-ibtn.off gives Back on the first beat. Idle by class and not
   by the `disabled` attribute, so the tooltip saying what it still wants is
   readable; the press is refused in js/tutorial.js and shakes the pill. */
.tut-fbtn.off { opacity: 0.38; cursor: default; }
.tut-fbtn.tut-go.off { filter: saturate(0.35); }
.tut-fbtn.off:hover { opacity: 0.38; background: rgba(255,255,255,0.05); }
.tut-fbtn.tut-go.off:hover { filter: saturate(0.35); background: linear-gradient(180deg, var(--accent-2, #e6b45e), var(--accent, #d8a24a)); }

/* A blocked click has to DO something, or the app reads as frozen. */
.tut-shake { animation: tutShake 220ms ease; }
@keyframes tutShake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-4px); }
  60% { transform: translateX(4px); }
}
.tut-bubble.tut-done { border-color: rgba(120,200,140,0.5); box-shadow: 0 14px 40px rgba(0,0,0,0.5), 0 0 0 1px rgba(120,200,140,0.25); }

/* The lit control, and its dimmed neighbours. A dimmed control STAYS on screen
   and keeps its words: the player is meant to see the shelf they are not using.
   Never remove an option to make a beat easier to gate. */
body.tut-mode .tut-lit {
  position: relative; z-index: 10042;
  outline: 2px solid var(--accent-2, #e6b45e); outline-offset: 2px;
  border-radius: 8px;
  animation: tutPulse 1.8s ease-in-out infinite;
}
@keyframes tutPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(216,162,74,0.0); }
  50% { box-shadow: 0 0 14px 2px rgba(216,162,74,0.35); }
}
/* A control the spotlight already traces drops its own rectangular outline. Two
   highlights on one control is the box back again, at a different angle.
   The !important on the z-index is not decoration: a fanned card carries an
   INLINE z-index (home.js stacks the hand), which beats a class rule, so without
   it the lit card stays half-covered by the dimmed neighbour lying over it. Same
   reason .menu-slot:hover states it. */
body.tut-mode .tut-lit.tut-lit-shaped {
  outline: none; box-shadow: none; animation: none;
  z-index: 10042 !important;
}
body.tut-mode .tut-dim { opacity: 0.34; filter: saturate(0.5); }

/* Everything above animates beside a game in progress, so it all stops here. */
@media (prefers-reduced-motion: reduce) {
  .tut-hole { transition: none; }
  body.tut-mode .tut-lit { animation: none; }
  .tut-spot { animation: none; }
  .tut-shake { animation: none; }
}
body.reduce-motion .tut-hole { transition: none; }
body.reduce-motion .tut-lit { animation: none; }
body.reduce-motion .tut-spot { animation: none; }

/* The tutorial lab (dev/tutorial-lab.html -> index.html?tutlab=1). Same glass,
   pinned bottom-left, above the scrim so it stays clickable while a beat is
   gating everything else. */
.tut-lab {
  position: fixed; left: 12px; bottom: 12px; z-index: 10070;
  width: 218px; overflow-y: auto;
  /* vh first, dvh after: on a phone 100vh is the LARGE viewport, so a vh-only
     value puts the bottom of this under the browser toolbar. The duplicate is
     deliberate; _responsive_test.js fails if a vh loses its twin. */
  max-height: calc(100vh - 24px); max-height: calc(100dvh - 24px);
  padding: 8px; border-radius: 11px; display: none;
  font-family: var(--font); color: var(--text);
  background: rgba(15,20,28,0.86);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  border: 1px solid rgba(255,255,255,0.12);
  box-shadow: 0 14px 40px rgba(0,0,0,0.5);
}
.tut-lab.show { display: block; }
.tut-lab-head { font-size: 12px; font-weight: 700; margin-bottom: 6px; opacity: 0.9; }
.tut-lab-row { display: flex; gap: 4px; margin-bottom: 6px; flex-wrap: wrap; }
.tut-lab-list { display: flex; flex-direction: column; gap: 2px; }
.tut-lab-btn, .tut-lab-step {
  cursor: pointer; font-size: 10.5px; color: var(--text);
  padding: 3px 8px; border-radius: 999px;
  background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.10);
}
.tut-lab-step { text-align: left; border-radius: 7px; }
.tut-lab-btn:hover, .tut-lab-step:hover { background: rgba(255,255,255,0.10); }
.tut-lab-step.on { border-color: rgba(216,162,74,0.55); background: rgba(216,162,74,0.14); }
.tut-lab-restart { margin-left: auto; }
.tut-lab-read { margin-top: 7px; padding-top: 6px; border-top: 1px solid rgba(255,255,255,0.08); }
.tut-lab-line { font-family: ui-monospace, Menlo, monospace; font-size: 10px; opacity: 0.55; line-height: 1.5; }
.tut-lab-line.good { opacity: 0.95; color: var(--accent-2, #e6b45e); }

/* Home has no toolbar, so its ? floats in the bottom-right corner as a glass
   circle, matching the app's floating-surface skin. */
.home-help-btn {
  position: fixed; right: 16px; bottom: 16px; z-index: 45;
  width: 38px; height: 38px; padding: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(15,20,28,0.72);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  border: 1px solid rgba(255,255,255,0.12);
  box-shadow: 0 10px 26px rgba(0,0,0,0.4);
}
.home-help-btn:hover { border-color: rgba(255,255,255,0.2); }
.home-help-btn .ic svg { width: 19px; height: 19px; }

/* Size stepper popover opened from the rail's piece/zone-size icons. Hugs its
   content rather than inheriting the context menu's row-list min-width. */
.size-flyout { display: flex; align-items: center; gap: 6px; padding: 6px; min-width: 0; }
.size-flyout-read {
  min-width: 42px; text-align: center; font-size: 12px; font-weight: 700;
  color: var(--text-dim); font-variant-numeric: tabular-nums;
}
.size-flyout-btn {
  width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;
  border-radius: 8px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.12);
  color: var(--text); cursor: pointer;
}
.size-flyout-btn:hover { border-color: var(--accent); }
.size-flyout-btn .tool-ic, .size-flyout-btn .tool-ic svg { width: 15px; height: 15px; display: flex; }
/* Hover label for icon-only buttons (top library bar + rail group icons). */
.tool-btn.icon-only[data-tooltip] { position: relative; }
.tool-btn.icon-only[data-tooltip]::after {
  content: attr(data-tooltip); position: absolute; top: calc(100% + 6px); left: 50%; transform: translateX(-50%);
  background: rgba(15,20,28,0.96); color: var(--text); border: 1px solid rgba(255,255,255,0.12);
  padding: 4px 8px; border-radius: 6px; font-size: 11.5px; font-weight: 600; white-space: nowrap;
  pointer-events: none; opacity: 0; transition: opacity 0.12s ease; transition-delay: 0s; z-index: 40;
}
.tool-btn.icon-only[data-tooltip]:hover::after { opacity: 1; transition-delay: 0.3s; }
.size-read { text-align: center; font-size: 11px; color: var(--text-dim); margin-top: 2px; }

.table-stage { flex: 1; min-width: 0; perspective: 1700px; perspective-origin: 50% 28%; }
/* a just-dealt card held up large for a beat before anything reacts to it */
.card-reveal {
  position: absolute; inset: 0; z-index: 40; pointer-events: none;
  display: flex; align-items: center; justify-content: center;
  opacity: 0; transition: opacity 0.24s ease;
}
.card-reveal.show { opacity: 1; }
.card-reveal.hide { opacity: 0; }
.card-reveal-card {
  width: min(30vw, 230px); aspect-ratio: 5 / 7;
  transform: scale(0.82) translateY(10px);
  transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
  filter: drop-shadow(0 24px 44px rgba(0,0,0,0.55));
}
.card-reveal.show .card-reveal-card { transform: scale(1) translateY(0); }
.card-reveal-card .tcard { width: 100%; height: 100%; }

.felt {
  position: relative; width: 100%; height: min(82vh, 1040px); min-height: 600px;
  border-radius: 22px; overflow: hidden; transform-style: preserve-3d;
  border: 1px solid #20303a;
  /* The colour layer is the shared themeable felt texture (var(--felt-image)),
     the same one the menus use, so the play table and every menu felt always
     match and recolour together. The two overlays above it are hue-neutral
     (a soft top spotlight, a bottom shadow) so they keep the table's depth in
     any felt colour. */
  background:
    radial-gradient(120% 90% at 50% -10%, rgba(255,255,255,0.05), transparent 55%),
    radial-gradient(140% 120% at 50% 120%, rgba(0,0,0,0.55), transparent 60%),
    var(--felt-fibre),
    var(--felt-image);
  background-color: #123a2e;
  background-size: auto, auto, var(--felt-fibre-size), cover;
  background-position: center, center, center, center;
  background-repeat: no-repeat, no-repeat, repeat, no-repeat;
  background-blend-mode: normal, normal, overlay, normal;
  box-shadow: inset 0 0 0 2px rgba(255,255,255,0.03), inset 0 14px 60px rgba(0,0,0,0.45), var(--shadow);
  cursor: default;
}
.felt::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0.5;
  background-image: radial-gradient(rgba(255,255,255,0.05) 1px, transparent 1.4px);
  background-size: 26px 26px;
}
.felt.dragging { cursor: grabbing; }
.felt.space-pan { cursor: grab; }
.felt.space-pan.panning { cursor: grabbing; }

/* marquee: the rubber-band box dragged across empty felt to select several
   pieces/zones at once (build mode). Sits above zones and pieces so it's
   always visible, and ignores the mouse so the drag underneath keeps going. */
.felt-marquee {
  position: absolute; z-index: 60; pointer-events: none; border-radius: 4px;
  border: 1.5px solid var(--accent-2);
  background: rgba(216,162,74,0.12);
  box-shadow: 0 0 0 1px rgba(216,162,74,0.25) inset;
}
/* Alignment guide: a thin gold line that flashes on the felt while a zone is
   dragged or resized and one of its edges/centres lines up with a neighbour's.
   Sits above zones and pieces (like the marquee) and ignores the mouse so the
   drag underneath keeps running. Gold glow matches the app's accent. */
.snap-guide {
  position: absolute; z-index: 58; pointer-events: none;
  background: var(--accent);
  box-shadow: 0 0 5px var(--accent), 0 0 1px var(--accent);
}
.snap-guide.snap-v { top: 0; bottom: 0; width: 1px; }
.snap-guide.snap-h { left: 0; right: 0; height: 1px; }

/* a piece or zone caught in the current selection. A selected piece isn't ringed
   with a hard border - it LIGHTS UP on its own silhouette, the same warm beckon
   the dice use for "ready to roll" (see .die-wrap.can-roll). A drop-shadow filter
   on the piece's visual rides the actual edge of the card, coin, deck, or die, so
   the glow hugs the shape with no halo box. Colour matches the dice glow. */
.piece.selected .piece-3d {
  outline: none;
  filter: drop-shadow(0 0 3px rgba(230,183,101,0.95)) drop-shadow(0 0 7px rgba(230,183,101,0.55));
}
/* The die drops all filters while it tumbles (per-frame rasterize is the roll's
   dominant cost); a still-selected die keeps that guarantee. */
.piece.selected.die-rolling .piece-3d { filter: none; }
/* No ring, no halo box: the grab-ring stays dark and the group hull renders as
   nothing (JS still positions it). Overlapping silhouette glows read as a lit
   group on their own. */
.piece.selected .grab-ring { opacity: 0; box-shadow: none; }
.sel-hull { display: none; }
/* While grabbing a dice selection to throw it, hide the whole selection outline
   (hull + per-piece rings) so the dice read as picked up, not still highlighted. */
.felt.sel-grabbing .sel-hull { display: none; }
.felt.sel-grabbing .piece.selected .grab-ring { opacity: 0; box-shadow: none; }
.tzone.selected {
  border: 1.5px solid var(--accent-2);
  background: rgba(216,162,74,0.10);
  box-shadow: 0 0 0 1px rgba(216,162,74,0.5) inset;
}

/* snap zones drawn on the felt */
.tzone {
  position: absolute; border: none;
  border-radius: 14px; box-sizing: border-box; pointer-events: none;
  background: transparent; transition: border-color 0.12s, background 0.12s, box-shadow 0.12s;
}
/* Zones read as rounded gold CORNER BRACKETS instead of a full outline - far
   less line on a busy felt while still bounding the area. One pseudo draws a
   rounded-rect gold border, then a four-corner mask keeps only the L at each
   corner. Length = mask block (8px), thickness = border (3px), elbow =
   border-radius (4px), colour = gold at 0.55 (the tuned look). This draws in
   both PLAY and BUILD, so the editor previews the player's chosen Zone outline
   (Settings); build mode keeps its own faint editable box UNDER these brackets
   for drag/resize/overlap. Grid cells, lit states, and overlap warnings opt out
   (they draw their own border). */
.tzone:not(.tzone-cell)::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  /* Default gold; a seat-owned zone overrides --zone-bracket with the player's
     own colour when the "Player colours" table-look setting is on (set inline
     in renderZones, tableEditor.js). Shared zones leave the var unset and stay
     gold. */
  border: 3px solid var(--zone-bracket, rgba(216,162,74,0.55)); border-radius: 4px;
  -webkit-mask:
    linear-gradient(#000,#000) 0 0/8px 8px no-repeat,
    linear-gradient(#000,#000) 100% 0/8px 8px no-repeat,
    linear-gradient(#000,#000) 0 100%/8px 8px no-repeat,
    linear-gradient(#000,#000) 100% 100%/8px 8px no-repeat;
  mask:
    linear-gradient(#000,#000) 0 0/8px 8px no-repeat,
    linear-gradient(#000,#000) 100% 0/8px 8px no-repeat,
    linear-gradient(#000,#000) 0 100%/8px 8px no-repeat,
    linear-gradient(#000,#000) 100% 100%/8px 8px no-repeat;
}
/* A lit play zone (drop target, selection, rejection, clickable dice tray)
   swaps to a full bright border + glow, brighter than the resting brackets so
   "active" reads clearly against the gold at rest. Hide the brackets so the
   two never stack. */
.tzone.hot::before, .tzone.selected::before, .tzone.reject::before,
.tzone.dice-tray:not(.build):hover::before,
.tzone.overlap-stuck::before, .tzone.overlap-blocker::before { display: none; }
/* HAIRLINE zone style - the other option a player can pick in Settings. Swaps
   the corner brackets for a single thin gold outline. It only restyles the
   ::before decoration (drops the corner mask, thins the stroke, rounds to the
   full box), never the element's own border, so the lit states above keep their
   full bright border + glow and still win. A .zones-hairline ancestor (the
   <body>, set from the saved preference, or a Settings preview box) turns it on.
   See js/views/settings.js. */
.zones-hairline .tzone:not(.tzone-cell)::before {
  /* Same --zone-bracket override as the brackets, so "Player colours" tints the
     hairline too; unset stays the fainter default gold. */
  border-width: 1px; border-color: var(--zone-bracket, rgba(216,162,74,0.40)); border-radius: 14px;
  -webkit-mask: none; mask: none;
}
/* Sits just ABOVE the zone's own box, not inside it, on purpose: a small
   zone sized close to a single card (Claim's Follower deck/Score pile, for
   instance) centers that card using the box's own full height, so a label
   living inside the box's top edge crowds right up against the card sitting
   there. Living in the gap above the box instead means the box's whole
   interior stays free for whatever's placed in it. */
/* Width-capped to the zone's own box (minus the left inset) and truncated
   with an ellipsis rather than left as unbounded nowrap text - an
   uncapped label overflows sideways past a narrow zone's edge and into
   whatever sits next to it, which reads as the same "touching" bug as an
   overlapping zone box even when the boxes themselves never overlap. See
   CLAUDE.md's Table layout rule: a zone's label must never touch another
   zone, same guarantee as the zone box itself. */
/* --zlabel-clear is how far the cards resting in this zone hang out past the
   edge the name is drawn against, measured per zone by zoneContentOverhang and
   written by placeZoneLabel (06-layout.js). Sitting OUTSIDE the box is not on
   its own enough: a card is centre-anchored at a fixed size, so a box shorter
   than the card standing in it pushes that card into the very strip the name
   lives in, which is why every hand used to print its cards over its own name.
   Zero for a zone whose contents fit, so a roomy zone is unchanged. */
.tzone-label {
  position: absolute; top: calc(-16px - var(--zlabel-clear, 0px)); left: 2px; right: 2px;
  font-size: 9.5px; letter-spacing: 1.2px;
  text-transform: uppercase; color: rgba(255,255,255,0.4); font-weight: 700;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* A zone flush against the top of the felt has no room above it for the
   label (the felt clips overflow) - it drops to just below the box instead,
   still outside it, so the name never sits inside the area. It clears the
   overhang on THAT side, which is the bottom one. */
.tzone-label.tzone-label-below { top: calc(100% + 3px + var(--zlabel-clear, 0px)); }
/* NO ROOM ON EITHER SIDE. Two rows of cards standing edge to edge leave no
   strip for a name at all (measured on Siem's own table: 0.3px between the
   face-down area's cards and the hand's), and the honest answers are all bad -
   sit on the neighbour's cards, sit on the neighbour's name, or walk off to a
   gap that belongs to some other area. So the name comes IN, as a chip in its
   own box's corner: over its own cards and nobody else's, unambiguous about
   which area it names, and readable because it carries the same glass backing
   every floating surface wears. Painted above the pieces (a .tzone is inserted
   under them, and the count badge at z-index 4 is the same trick). */
.tzone-label.tzone-label-in {
  top: 3px; left: 3px; right: auto; max-width: calc(100% - 6px);
  padding: 1px 6px; border-radius: 999px; z-index: 5;
  background: rgba(15,20,28,0.72); border: 1px solid rgba(255,255,255,0.10);
  color: rgba(255,255,255,0.62);
}
.tzone-ic.tzone-ic-in {
  top: 3px; left: 3px; right: auto; justify-content: flex-start;
  padding: 1px 5px; border-radius: 999px; z-index: 5;
  background: rgba(15,20,28,0.72); border: 1px solid rgba(255,255,255,0.10);
}
/* Split-area (z.partition) band overlay: one translucent coloured stripe per
   region, clipped to the zone's rounded box and sitting UNDER the cards (a card
   is a separate felt piece with its own higher stacking). Purely decorative -
   pointer-events off - so it shows where each kind of card lands without ever
   catching a drag. Region colours match the editor's swatches (partitionBandTint). */
.tzone-bands { position: absolute; inset: 0; border-radius: 14px; overflow: hidden; pointer-events: none; z-index: 0; }
.tzone-band { position: absolute; box-sizing: border-box; }
.tzone-band-lbl {
  position: absolute; top: 2px; left: 5px; font-size: 8px; letter-spacing: 0.5px;
  text-transform: uppercase; font-weight: 800; white-space: nowrap; opacity: 0.9;
  text-shadow: 0 1px 2px rgba(0,0,0,0.55);
}
/* Count badge for a card zone set to hide its size: a small pill in the
   corner reading the number, or "?" when it's masked for the viewer. Same
   accent pill as a deck's own count badge, sized to the compact UI scale. */
.tzone-count {
  position: absolute; top: -8px; right: -8px; min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: 999px; background: var(--accent); color: var(--accent-ink);
  font-weight: 800; font-size: 11px; line-height: 1; display: flex; align-items: center; justify-content: center;
  box-shadow: var(--shadow-sm); border: 1.5px solid rgba(0,0,0,0.3); z-index: 4; pointer-events: none;
}
.tzone.hot { border: 1.5px solid var(--accent-2); background: rgba(216,162,74,0.12); box-shadow: 0 0 0 1px rgba(216,162,74,0.5) inset, 0 0 40px -10px rgba(216,162,74,0.6); }
/* ---- placement slots: where cards go, and how many ------------------------
   A step waiting on the player to place cards lights its destination zone and
   draws one card-shaped outline per card still owed (refreshPlaceSlots,
   06-layout.js). The row sits UNDER the cards - .tzone is inserted below every
   piece in the felt - so an outline is covered as its zone fills rather than
   sitting on top of somebody's card. Never in the way: pointer-events stay off
   the whole thing, exactly like the rest of a zone's decoration.
   Motion is a slow breath on the border and the glow only; nothing moves and
   nothing scales, per the card-in-motion rule (a scale change re-rasters the
   layer, and this sits over a felt full of card art). */
.tzone-slots {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  pointer-events: none; z-index: 0;
}
.tzone-slot {
  box-sizing: border-box; border-radius: 6px; flex: 0 0 auto;
  border: 2px dashed rgba(216,162,74,0.75);
  background: rgba(216,162,74,0.07);
  box-shadow: 0 0 12px -2px rgba(216,162,74,0.45), 0 0 0 1px rgba(0,0,0,0.25) inset;
  animation: slotBreathe 2.1s ease-in-out infinite;
}
/* A slot past the run's floor: one the player MAY fill, not one they owe. Same
   outline, quieter, and it doesn't breathe - so "these are required, those are
   yours to take" reads without a word being printed. */
.tzone-slot.tzone-slot-opt {
  border-color: rgba(216,162,74,0.34); background: rgba(216,162,74,0.03);
  box-shadow: none; animation: none;
}
/* A slot whose card has landed. The row is drawn once for the whole wait and
   never shortens, so a filled slot stays to hold its space and paints NOTHING -
   the card resting on it is the only thing that should be visible there.
   visibility rather than display, since display:none would take its space with
   it and re-centre every outline still on the felt, which is the bug this
   whole row exists to not have. */
.tzone-slot.tzone-slot-done { visibility: hidden; }
/* When the destination lays its cards out as a run grid, each outline is placed
   on the very cell its card will fill (refreshPlaceSlots asks runGridGeom, the
   same geometry arrangeTrick places the cards with), so the row is positioned
   rather than flowed. */
.tzone-slots.tzone-slots-grid { display: block; }
.tzone-slots.tzone-slots-grid .tzone-slot { position: absolute; }
/* A SPLIT AREA sorts a landing card into one of its bands, so the row is
   narrowed to that band (placeSlotBand writes left/top/width/height as
   percentages of the zone, and releases right/bottom) - or the outline would
   sit on the divider between two bands, which is the one place no card rests.
   Nothing here to style: the inset:0 above is just overridden per band. */
@keyframes slotBreathe {
  0%, 100% { border-color: rgba(216,162,74,0.55); box-shadow: 0 0 10px -3px rgba(216,162,74,0.35), 0 0 0 1px rgba(0,0,0,0.25) inset; }
  50% { border-color: rgba(216,162,74,0.95); box-shadow: 0 0 18px -1px rgba(216,162,74,0.6), 0 0 0 1px rgba(0,0,0,0.25) inset; }
}
/* The zone itself carries a soft ring while it is waiting, so the answer to
   "where" is legible before the eye has counted the slots. */
.tzone.tzone-await { box-shadow: 0 0 0 1px rgba(216,162,74,0.35) inset, 0 0 34px -12px rgba(216,162,74,0.55); }
/* A dice tray in play takes clicks (to roll every die inside it), so it opts
   back into pointer events that a zone drops in play, and reads as clickable.
   Scoped away from build (.build) so the tray still drags and resizes like any
   other zone while building. A click on a die sitting on top still hits the die
   first (it stacks above the zone), so only a click on empty tray rolls all. */
.tzone.dice-tray:not(.build) { pointer-events: auto; cursor: pointer; }
.tzone.dice-tray:not(.build):hover { border: 1.5px solid var(--accent-2); background: rgba(216,162,74,0.10); }
.tzone.reject { animation: shake 0.4s; border: 1.5px solid var(--red); background: rgba(214,96,77,0.12); }
/* "Hide zones" play toggle (the zoneSize button, top-right): fade the zone
   boxes, labels and count badges out so only the cards and pieces read on the
   felt. Grid cells stay - they ARE the board for a grid game. A zone the
   player drags a card over still surfaces (.hot) so a drop stays aimable, then
   fades back once the drag ends. */
.felt.zones-hidden .tzone:not(.tzone-cell) { opacity: 0; transition: opacity 0.18s ease; }
.felt.zones-hidden .tzone:not(.tzone-cell).hot { opacity: 1; }
.turn-meter { font-size: 11px; letter-spacing: 0.4px; color: var(--accent-2); padding: 5px 11px; border: 1px solid rgba(255,255,255,0.14); border-radius: 999px; margin-left: 8px; white-space: nowrap; }
.limit-form { display: flex; flex-direction: column; gap: 16px; }
.limit-row { display: flex; align-items: center; justify-content: space-between; gap: 18px; cursor: default; }
.limit-row input { width: 80px; }
.limit-lbl { font-weight: 600; }
.limit-sub { color: var(--text-dim); }
.limit-sub-bad { color: #e28080; }
.limit-sub-good { color: #8fd6a4; }
.round-end-reason { max-width: 320px; font-size: 12.5px; color: var(--text-dim); margin-top: -4px; }
/* in build mode a zone can be moved, resized, renamed, and deleted. The gold
   chosen-outline (brackets/hairline) now draws in build too (::before above),
   so the editor previews the play look; this faint editable box sits UNDER it
   for drag, resize, delete and overlap flags to read against a real rectangle.
   No dashed box at rest - the gold corner brackets ARE the zone's look in the
   editor now; the border here is invisible (solid transparent), kept only at
   1.5px width so the dragging/locked/overlap states below (which set just a
   border colour/style) still render a visible border. Hover brightens the gold
   corners instead of drawing a box, plus a faint fill for a grab cue. */
.tzone.build { pointer-events: auto; cursor: grab; border: 1.5px solid transparent; background: transparent; }
.tzone.build:hover { background: rgba(255,255,255,0.02); }
.tzone.build:hover::before { border-color: rgba(216,162,74,0.85); }
.tzone.dragging { cursor: grabbing; border-style: solid; border-color: var(--accent-2); }
/* A zone placed via "Standard position" - no drag/resize handles, so no grab
   cursor either. Like every other build zone it shows just the gold corners;
   the default cursor is the "fixed in place" hint (no box). Right-click still
   opens zoneMenu (see tableEditor.js). */
.tzone.build.tzone-locked { cursor: default; border-color: transparent; }
.tzone.build.tzone-locked:hover::before { border-color: rgba(216,162,74,0.55); }
/* A zone the auto-layout couldn't place clear of everything else (the felt is
   too full to fit it): a warning outline, with a corner marker naming the
   culprit. The zone it collides with reads as a subtler "blocker" outline so
   the builder sees both halves of the problem. Build mode only. */
.tzone.overlap-stuck { border-style: solid; border-color: var(--danger, #d64545); background: rgba(214,96,77,0.10); box-shadow: 0 0 0 1px rgba(214,96,77,0.45) inset; }
.tzone.overlap-blocker { border-style: solid; border-color: rgba(214,96,77,0.5); }
.tzone-warn {
  position: absolute; top: -9px; left: -9px; width: 20px; height: 20px;
  display: flex; align-items: center; justify-content: center; z-index: 5; cursor: help;
}
.tzone-warn svg { width: 100%; height: 100%; color: var(--danger, #d64545); filter: drop-shadow(0 1px 2px rgba(0,0,0,0.45)); }
.tzone-warn::after {
  content: attr(data-tip);
  position: absolute; bottom: 118%; left: 0;
  width: max-content; max-width: 220px; padding: 6px 9px; border-radius: 6px;
  background: #1c1c1c; color: #fff; font-size: 11px; line-height: 1.35; font-weight: 500;
  box-shadow: var(--shadow-sm); border: 1px solid rgba(255,255,255,0.12);
  opacity: 0; pointer-events: none; z-index: 20;
}
.tzone-warn:hover::after { opacity: 1; }
.tzone-del {
  position: absolute; top: 5px; right: 5px; width: 22px; height: 22px; border-radius: 50%;
  display: none; align-items: center; justify-content: center; border: none; cursor: pointer;
  background: rgba(6,9,14,0.55); color: rgba(255,255,255,0.8);
  /* Above a piece placed on the zone (pieces are z-index 2, 40 when lifted) so
     the delete X on a small zone never hides beneath a deck sitting on it. */
  z-index: 50;
}
.tzone.build:hover .tzone-del { display: flex; }
.tzone-del:hover { background: var(--red); color: #fff; }
.tzone-del svg { width: 13px; height: 13px; }
.tzone-resize {
  position: absolute; width: 16px; height: 16px; border-radius: 4px;
  display: none; background: var(--accent-2); border: 2px solid var(--bg);
  /* Grips stick 3px outside the zone into a neighbour. Without a z-index the
     neighbour's body paints over the grip, so a click on the corner starts
     MOVING the neighbour instead of resizing. Lift the grip above sibling zone
     bodies (z-index 0) but keep it under the delete X (50) where the two meet
     at the top-right corner. */
  z-index: 49;
}
.tzone-resize-nw { left: -3px; top: -3px; cursor: nwse-resize; }
.tzone-resize-ne { right: -3px; top: -3px; cursor: nesw-resize; }
.tzone-resize-sw { left: -3px; bottom: -3px; cursor: nesw-resize; }
.tzone-resize-se { right: -3px; bottom: -3px; cursor: nwse-resize; }
.tzone.build:hover .tzone-resize { display: block; }

/* The one box drawn round a multi-ZONE selection, with its own four grips:
   dragging one scales the whole group about the opposite corner. A zone's own
   grips are hover-shown on that zone alone, which is why a selection of five
   areas needs a box of its own rather than five sets of grips.
   The frame ignores the mouse (a zone inside it stays clickable) while the
   grips take it back; it sits above the zone bodies and their grips, and under
   the marquee and the snap guides. */
.zgroup {
  position: absolute; z-index: 55; pointer-events: none; border-radius: 6px;
  border: 1px dashed color-mix(in srgb, var(--accent-2) 60%, transparent);
}
.zgroup-resize {
  position: absolute; width: 16px; height: 16px; border-radius: 4px;
  pointer-events: auto; background: var(--accent-2); border: 2px solid var(--bg);
  box-shadow: 0 0 0 1px rgba(216,162,74,0.35);
}
.zgroup-resize-nw { left: -8px; top: -8px; cursor: nwse-resize; }
.zgroup-resize-ne { right: -8px; top: -8px; cursor: nesw-resize; }
.zgroup-resize-sw { left: -8px; bottom: -8px; cursor: nesw-resize; }
.zgroup-resize-se { right: -8px; bottom: -8px; cursor: nwse-resize; }

/* a grid cell (Punto-style spatial games): a small board square, not a
   labelled drop area - no label text, a tighter corner radius, and a subtle
   marker on the one cell the first card of the game must open on. */
.tzone-cell { border-radius: 4px; border-style: solid; border-color: rgba(255,255,255,0.10); background: rgba(255,255,255,0.03); }
.tzone-cell .tzone-label { display: none; }
.tzone-cell.hot { border-radius: 4px; }
.tzone-cell-center { border-color: rgba(216,162,74,0.45); background: rgba(216,162,74,0.07); }

/* a piece: outer holds position, inner does the 3D lift + tilt */
.piece { position: absolute; transform: translate(-50%, -50%); transform-style: preserve-3d; z-index: 2; }
/* Depth badge on the top card of a squared "Free stacks" pile: how many cards sit
   stacked underneath it. Lives on the .piece so it overhangs the card corner
   without being clipped, and scales with the pieces via --pscale (same gold pill
   as a deck's count). */
.piece > .stack-count {
  position: absolute; bottom: calc(-6px * var(--pscale, 1)); right: calc(-6px * var(--pscale, 1));
  min-width: calc(18px * var(--pscale, 1)); height: calc(18px * var(--pscale, 1));
  padding: 0 calc(5px * var(--pscale, 1)); box-sizing: border-box;
  border-radius: 999px; background: var(--accent); color: var(--accent-ink);
  font-weight: 800; font-size: calc(10px * var(--pscale, 1)); line-height: 1;
  display: flex; align-items: center; justify-content: center;
  box-shadow: var(--shadow-sm); border: 1.5px solid rgba(0,0,0,0.3);
  z-index: 6; pointer-events: none;
}
/* A die sits one layer above the flat pieces (cards, coins, counters), so a die
   resting on a card reads as an object on top of it, not tucked under it. */
.piece.piece-die { z-index: 3; }
.piece.lifted { z-index: 40; }
/* Floating roll-number readout: the value a die lands on, popped beside it when
   a roll settles (flashRollNumbers). Lives inside the felt so its CSS zoom
   scales it with the dice; translateZ lifts it clear of the resting pieces in
   the felt's 3D space, and pointer-events are off so it never blocks a click.
   Sized against the pill scale (compact by default). */
.roll-num {
  position: absolute; z-index: 60; pointer-events: none;
  font-size: 19px; font-weight: 800; line-height: 1; white-space: nowrap;
  padding: 4px 9px; border-radius: 10px;
  color: var(--text); background: rgba(14, 17, 22, 0.88);
  border: 1.5px solid var(--line-2); box-shadow: var(--shadow-sm);
  transform-origin: 50% 50%;
  animation: rollNumPop 1.7s cubic-bezier(0.2, 1.2, 0.4, 1) forwards;
}
.roll-num.roll-total {
  font-size: 23px; color: var(--accent-ink);
  background: var(--accent); border-color: var(--accent-2);
}
@keyframes rollNumPop {
  0%   { opacity: 0; transform: translate(-50%, -50%) translateZ(62px) scale(0.4); }
  12%  { opacity: 1; transform: translate(-50%, -50%) translateZ(62px) scale(1.16); }
  24%  { opacity: 1; transform: translate(-50%, -50%) translateZ(62px) scale(1); }
  76%  { opacity: 1; transform: translate(-50%, -50%) translateZ(62px) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -50%) translateZ(62px) scale(0.92); }
}
@media (prefers-reduced-motion: reduce) {
  .roll-num { animation: rollNumFade 1.7s linear forwards; }
  @keyframes rollNumFade {
    0% { opacity: 0; transform: translate(-50%, -50%) translateZ(62px); }
    8% { opacity: 1; transform: translate(-50%, -50%) translateZ(62px); }
    80% { opacity: 1; transform: translate(-50%, -50%) translateZ(62px); }
    100% { opacity: 0; transform: translate(-50%, -50%) translateZ(62px); }
  }
}
/* No persistent will-change here: it would pin every piece to a compositing
   layer rasterized at 1x, so zooming the felt (transform: scale) just magnifies
   that stale bitmap and the pieces go blurry past 100%. Without it the browser
   re-rasterizes each piece at the current zoom scale, staying sharp. will-change
   is re-applied only while a piece is actually moving (below), where the paint
   hint matters and the piece isn't being read closely anyway. */
.piece-3d { transform-style: preserve-3d; transition: transform 0.18s cubic-bezier(0.2, 1.2, 0.4, 1); }
/* A rotated (exhausted) card reads as spent: it's turned 90 degrees by an inline
   transform (renderPiece) and dimmed a touch so a full board shows what's used. */
.piece.exhausted .piece-3d { filter: brightness(0.9) saturate(0.92); }
/* Cost payment picker (discard / destroy / copy-target): a compact grid of the
   player's own cards, tap to select, glass modal at the tool's scale. */
.cost-pick-grid { display: flex; flex-wrap: wrap; gap: 8px; max-height: 360px; overflow-y: auto; padding: 2px; justify-content: center; }
.cost-pick-card { width: 104px; cursor: pointer; border-radius: 10px; outline: 2px solid transparent; outline-offset: 2px; transition: outline-color 0.12s ease, transform 0.12s ease; }
.cost-pick-card > .tcard { width: 100%; height: auto; pointer-events: none; }
.cost-pick-card:hover { transform: translateY(-2px); }
.cost-pick-card.sel { outline-color: var(--accent); }
.piece.lifted .piece-3d, .piece.dragging .piece-3d, .piece.settle .piece-3d,
.piece.just-dealt .piece-3d, .piece.dealing .piece-3d, .piece.playing .piece-3d,
.piece.sliding .piece-3d { will-change: transform; }
.piece.dragging .piece-3d { transition: none; }
.piece.settle .piece-3d { animation: pieceSettle 0.4s cubic-bezier(0.2, 1.4, 0.45, 1); }
@keyframes pieceSettle {
  0% { transform: translateZ(54px) scale(1.05); }
  55% { transform: translateZ(0) scale(0.985); }
  100% { transform: translateZ(0) scale(1); }
}
.piece .grab-ring {
  position: absolute; inset: -10px; border-radius: 16px; pointer-events: none;
  box-shadow: 0 0 0 2px var(--accent-2); opacity: 0; transition: opacity 0.12s;
}
.piece.lifted .grab-ring { opacity: 0.0; }
/* Build-mode quick-delete x on a deck piece (see renderPiece). Glass button in
   the top-right corner, hidden until the piece is hovered, red on its own hover. */
.piece-del {
  position: absolute; top: -9px; right: -9px; z-index: 6; padding: 0;
  width: 20px; height: 20px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: rgba(15,20,28,0.82); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  border: 1px solid rgba(255,255,255,0.14); color: #cbd5e1; cursor: pointer;
  opacity: 0; transition: opacity 0.12s, background 0.12s, color 0.12s, border-color 0.12s;
  box-shadow: 0 3px 10px rgba(0,0,0,0.45);
}
.piece:hover .piece-del { opacity: 1; }
.piece-del:hover { background: var(--red); border-color: var(--red); color: #fff; }
.piece-del .ic { display: inline-flex; }
.piece-del svg { width: 12px; height: 12px; }

/* DECK piece: a real-looking stack with thickness via shadow layers.
   Base size is half of what it used to be. The deck is a query container so its
   emblem, name, count, and stack thickness all scale together with the piece
   size slider (--pscale). Reference width is the deck's own width, so inner
   sizes are written in cqw (1cqw = 1% of deck width). */
/* --card-wide is 1 for a portrait or square card and 7/5 for a landscape one,
   so a turned card comes out 84 x 60 beside a portrait 60 x 84 - the same card,
   turned. Without it a landscape card would be 60 wide and 43 tall, which reads
   as a card somebody shrank. Both vars are written together by
   TCG.applyCardShape; never set one here without the other. */
.deck { container-type: inline-size; position: relative; width: calc(60px * var(--pscale, 1) * var(--card-wide, 1)); aspect-ratio: var(--card-aspect, 5 / 7); cursor: grab; }
.deck.live { cursor: pointer; }
.deck-top {
  position: absolute; inset: 0; border-radius: 9.5cqw;
  background:
    linear-gradient(135deg, rgba(255,255,255,0.16), transparent 45%),
    repeating-linear-gradient(45deg, var(--deck-color, #3a4a63) 0 9px, var(--deck-color-2, #2c3a50) 9px 18px);
  border: 1.7cqw solid rgba(255,255,255,0.16);
  box-shadow:
    1.7cqw 2.6cqw 0 var(--deck-edge, #1d2738), 3.4cqw 5.2cqw 0 var(--deck-edge, #1d2738),
    5.2cqw 7.8cqw 0 var(--deck-edge, #1d2738), 6.9cqw 9.5cqw 0 var(--deck-edge, #1d2738),
    8.6cqw 12cqw 0 var(--deck-edge, #1d2738), 0 20.7cqw 29cqw rgba(0,0,0,0.55);
  display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 5.2cqw;
  color: rgba(255,255,255,0.85);
}
.piece.lifted .deck-top { box-shadow: 1.7cqw 2.6cqw 0 var(--deck-edge,#1d2738), 3.4cqw 5.2cqw 0 var(--deck-edge,#1d2738), 5.2cqw 7.8cqw 0 var(--deck-edge,#1d2738), 6.9cqw 9.5cqw 0 var(--deck-edge,#1d2738), 8.6cqw 12cqw 0 var(--deck-edge,#1d2738), 0 36cqw 43cqw rgba(0,0,0,0.6); }
/* A face-up deck: the real top card fills the box, sitting on the same stacked
   edge a face-down deck shows so it still reads as a pile of many cards. */
.deck-face { position: absolute; inset: 0; border-radius: 6.7% / 4.8%;
  box-shadow:
    1.7cqw 2.6cqw 0 var(--deck-edge, #1d2738), 3.4cqw 5.2cqw 0 var(--deck-edge, #1d2738),
    5.2cqw 7.8cqw 0 var(--deck-edge, #1d2738), 6.9cqw 9.5cqw 0 var(--deck-edge, #1d2738),
    8.6cqw 12cqw 0 var(--deck-edge, #1d2738), 0 20.7cqw 29cqw rgba(0,0,0,0.55);
}
.deck-face .tcard { width: 100%; height: 100%; box-shadow: none; }
.piece.lifted .deck-face { box-shadow:
    1.7cqw 2.6cqw 0 var(--deck-edge,#1d2738), 3.4cqw 5.2cqw 0 var(--deck-edge,#1d2738),
    5.2cqw 7.8cqw 0 var(--deck-edge,#1d2738), 6.9cqw 9.5cqw 0 var(--deck-edge,#1d2738),
    8.6cqw 12cqw 0 var(--deck-edge,#1d2738), 0 36cqw 43cqw rgba(0,0,0,0.6); }
/* Deck flip depth: injected only while a deck turns over (tableEditor
   buildDeckThickness), a short stack of edge slabs sits behind the face at
   increasing depth so the pile turns as a solid block, not a flat card. The
   flip switches .deck to preserve-3d so each slab's translateZ actually stacks;
   at rest the deck carries none of this. */
.deck-flipping { will-change: transform; }
.deck-flipping .deck { transform-style: preserve-3d; }
.deck-thickness { position: absolute; inset: 0; transform-style: preserve-3d; pointer-events: none; }
.deck-slab {
  position: absolute; inset: 0; border-radius: 9.5cqw;
  background:
    linear-gradient(135deg, rgba(255,255,255,0.10), transparent 45%),
    repeating-linear-gradient(45deg, var(--deck-color, #3a4a63) 0 9px, var(--deck-color-2, #2c3a50) 9px 18px);
  border: 1.7cqw solid rgba(0,0,0,0.28);
  box-shadow: inset 0 0 8cqw rgba(0,0,0,0.22);
}
/* A face-up pile's edge slabs are stacked cards, tinted by the top card's own
   frame colour (--deck-card, darkening toward --deck-edge) so the sides match
   the cards in the deck instead of a flat paper or the deck's back colour. */
.deck-thickness.up .deck-slab {
  background: linear-gradient(135deg, var(--deck-card, #d8d2c2), var(--deck-edge, #b8b2a2));
  border-color: rgba(0,0,0,0.22);
}
.deck-emblem { width: 34.5cqw; height: 34.5cqw; opacity: 0.9; }
.deck-emblem svg { width: 34.5cqw; height: 34.5cqw; }
.deck-name { font-size: 10.3cqw; font-weight: 700; letter-spacing: 0.3px; text-shadow: 0 1px 3px rgba(0,0,0,0.5); }
.deck-count {
  position: absolute; bottom: -6.9cqw; right: -6.9cqw; min-width: 20.7cqw; height: 20.7cqw; padding: 0 5.2cqw;
  border-radius: 999px; background: var(--accent); color: var(--accent-ink);
  font-weight: 800; font-size: 10.8cqw; display: flex; align-items: center; justify-content: center;
  box-shadow: var(--shadow-sm); border: 1.5px solid rgba(0,0,0,0.3); z-index: 3;
}
.deck-warn {
  position: absolute; top: -8cqw; left: -8cqw; width: 26cqw; height: 26cqw;
  display: flex; align-items: center; justify-content: center; z-index: 4;
  cursor: help;
}
.deck-warn svg {
  width: 100%; height: 100%; color: var(--danger, #d64545);
  filter: drop-shadow(0 1px 2px rgba(0,0,0,0.45));
}
.deck-warn::after {
  content: attr(data-tip);
  position: absolute; bottom: 108%; left: 50%; transform: translateX(-50%);
  width: max-content; max-width: 200px; padding: 6px 9px; border-radius: 6px;
  background: #1c1c1c; color: #fff; font-size: 11px; line-height: 1.35; font-weight: 500;
  box-shadow: var(--shadow-sm); border: 1px solid rgba(255,255,255,0.12);
  opacity: 0; pointer-events: none; transition: none; z-index: 20;
}
.deck-warn:hover::after { opacity: 1; }
/* Shuffle: the deck gives a small bounce while a riffle overlay (built in
   tableEditor playDeckShuffle) rides on top of the face. */
.deck.shuffling { animation: deckShuffle 0.8s ease; }
@keyframes deckShuffle {
  0%,100% { transform: translateY(0) scale(1); }
  28% { transform: translateY(-3%) scale(1.03); }
  60% { transform: translateY(0) scale(0.99); }
}
/* Riffle overlay: two card-back halves split apart and square back up while a
   cascade of slivers falls between them, over (not replacing) the real deck. */
.deck-shuffle { position: absolute; inset: 0; z-index: 2; pointer-events: none; }
.sh-half {
  position: absolute; inset: 0; border-radius: 9.5cqw;
  background:
    linear-gradient(135deg, rgba(255,255,255,0.16), transparent 45%),
    repeating-linear-gradient(45deg, var(--deck-color, #3a4a63) 0 9px, var(--deck-color-2, #2c3a50) 9px 18px);
  border: 1.7cqw solid rgba(255,255,255,0.18);
  box-shadow: 0 4cqw 9cqw rgba(0,0,0,0.4);
  transform-origin: 50% 100%; backface-visibility: hidden;
}
.sh-l { animation: shHalfL 0.8s ease-in-out both; }
.sh-r { animation: shHalfR 0.8s ease-in-out both; }
@keyframes shHalfL {
  0% { transform: translateX(0) translateY(0) rotate(0); }
  30% { transform: translateX(-58%) translateY(-6%) rotate(-9deg); }
  55% { transform: translateX(-30%) translateY(-3%) rotate(-5deg); }
  100% { transform: translateX(0) translateY(0) rotate(0); }
}
@keyframes shHalfR {
  0% { transform: translateX(0) translateY(0) rotate(0); }
  30% { transform: translateX(58%) translateY(-6%) rotate(9deg); }
  55% { transform: translateX(30%) translateY(-3%) rotate(5deg); }
  100% { transform: translateX(0) translateY(0) rotate(0); }
}
.sh-sliver {
  position: absolute; left: 9%; right: 9%; top: 12%; height: 12%;
  border-radius: 3cqw;
  background: repeating-linear-gradient(45deg, var(--deck-color, #3a4a63) 0 6px, var(--deck-color-2, #2c3a50) 6px 12px);
  border: 1px solid rgba(255,255,255,0.16); opacity: 0;
  animation: shSliver 0.8s ease-in-out both;
}
@keyframes shSliver {
  0%, 28% { opacity: 0; transform: translateY(0) translateX(var(--sx, 0)) scaleX(0.6); }
  42% { opacity: 1; transform: translateY(35%) translateX(calc(var(--sx, 0) / 2)) scaleX(0.9); }
  72% { opacity: 1; transform: translateY(150%) translateX(0) scaleX(1); }
  100% { opacity: 0; transform: translateY(210%) translateX(0) scaleX(1); }
}
@media (prefers-reduced-motion: reduce) {
  .deck.shuffling { animation: none; }
  .sh-half, .sh-sliver { animation-duration: 0.01s; }
}

/* DICE piece: a flat, outlined SVG die icon that spins and settles when rolled.
   Sized off the same --pscale the felt sets for every piece, so a die tracks the
   table's piece size like a card or deck does. The roll sim drives a 2D spin on
   .die-icon and lifts the piece via .piece-3d; the shadow tracks it on the felt. */
.die-wrap {
  --die-size: calc(48px * var(--pscale, 1));
  width: var(--die-size); height: var(--die-size);
  position: relative;
  cursor: default;
}
.die-wrap.live { cursor: pointer; }
/* The felt die renders on its <canvas> for BOTH rolling and idle - one renderer,
   so a number sits in the exact same spot on its face whether the die is tumbling
   or at rest (an SVG and a canvas place text with slightly different baselines,
   which made the numbers appear to shift the instant the die went idle). The svg
   stays in the DOM (for the roll trigger's hit-test) but is not shown. */
.die-icon {
  width: 100%; height: 100%;
  overflow: visible;
  transform-origin: 50% 50%;
  display: none;
}
.pf-poly, .pf-inner {
  fill: #f4f3ec;
  stroke: #23231c;
  stroke-width: 3;
  stroke-linejoin: round;
}
.pf-inner { fill: #eae9dd; stroke-width: 2; }
.pf-line { stroke: #23231c; stroke-width: 2; stroke-linecap: round; }
.pf-num {
  fill: #17170f;
  font-family: "Inter", system-ui, sans-serif;
  font-weight: 800;
  text-anchor: middle;
  dominant-baseline: central;
}
.pf-pip { fill: #17170f; }
/* the "your turn, roll me" cue: the die gently PULSES (grows and shrinks). The
   gold glow is reserved for selection now, so a ready die only breathes here, it
   doesn't shine - that keeps "glowing" meaning "selected" across every piece. */
.die-wrap.can-roll .die-canvas { animation: dieBeckon 1.5s ease-in-out infinite; }
/* A selected die lights up on its own silhouette like every other piece. The die
   is a <canvas> that fills the whole piece square and carries its own filter, so a
   drop-shadow on the parent .piece-3d doesn't read on it - the glow has to go on
   the canvas itself (the same element the roll cue used). Keeps the die's own
   depth shadow, adds the gold. Cleared while it tumbles by .die-rolling below. */
.piece.selected .die-canvas {
  filter: drop-shadow(0 1px 1px rgba(0,0,0,0.28)) drop-shadow(0 0 3px rgba(230,183,101,0.95)) drop-shadow(0 0 7px rgba(230,183,101,0.55));
}
@keyframes dieBeckon {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.07); }
}
/* Contact shadow a die drops on the felt. At rest a tight pool under it; the roll
   sim drives --sx/--sy to track it and --sh (0..1, higher = airborne) to spread
   and fade it, so the die reads as lifting off the felt and landing back on it. */
.die-shadow {
  position: absolute; left: 50%; top: 53%;
  width: calc(43px * var(--pscale, 1)); height: calc(42px * var(--pscale, 1));
  border-radius: 30%;
  background: radial-gradient(ellipse at center, rgba(0,0,0,0.5), rgba(0,0,0,0.26) 55%, rgba(0,0,0,0) 76%);
  transform: translate(-50%, -50%) translateZ(-1px) translate(var(--sx, 0px), var(--sy, 0px)) scale(calc(1 + var(--sh, 0)));
  opacity: calc(0.8 - 0.45 * var(--sh, 0));
  pointer-events: none;
  filter: blur(calc(1px + 3px * var(--sh, 0)));
}
/* While a die is actively rolling (class set for the duration of the throw), drop
   the two filters that would re-rasterize every single frame on a moving/redrawn
   die: the die's own drop-shadow and the contact shadow's animated blur radius.
   The contact shadow still tracks and fades through transform + opacity (both
   GPU-composited and cheap), just with a fixed blur. Promote the lifting layer so
   the per-frame transform stays off the main thread. This is what keeps a fistful
   of dice smooth; the rest-state look is restored the instant each die lands. */
.piece.die-rolling .piece-3d { will-change: transform; }
/* A die in flight rides above every flat piece it crosses (cards, coins,
   counters), so a thrown die passing over or landing on a card is always visible
   on top of it. Still below a hand-lifted piece (z 40). */
.piece.die-rolling { z-index: 30; }
/* The die is drawn to this <canvas> at all times - it re-rasterizes far cheaper
   than the SVG (whose per-face text dropped a 10-dice roll to ~8fps) and, being
   the only renderer, guarantees the numbers never shift between rolling and idle.
   A soft drop-shadow at rest; dropped while rolling so the moving/redrawn canvas
   doesn't re-rasterize the filter every frame. */
.die-canvas {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none;
  filter: drop-shadow(0 1px 1px rgba(0,0,0,0.28));
}
.piece.die-rolling .die-canvas { filter: none; }
/* The contact shadow is a sibling of .piece-3d, so it needs its OWN promotion.
   Unpromoted it repaints the felt beneath it through a blur filter every frame,
   and that cost is per-die: it's what made a roll scale from smooth at one die to
   ~8fps at ten. Drop the blur for the roll (the radial gradient is already soft,
   so it reads the same) and give the shadow its own layer, so moving it is a pure
   composite instead of a filtered repaint. The soft blur returns once it lands. */
.piece.die-rolling .die-shadow { filter: none; will-change: transform, opacity; }
/* The dominant cost of a multi-die roll: .felt is preserve-3d, so a tumbling
   die's 3D transform forces the browser to re-rasterize the felt's big
   gradient/shadow background every single frame (measured ~120ms/frame at 10
   dice, versus ~2ms of actual JS). For the duration of a roll, collapse the felt
   to a flat rendering context - the identical cheap path the zoomed-in view
   already uses - so each die composites on its own layer over a cached
   background. The die's tumble is drawn in SVG and its hop still reads from the
   contact shadow, so the only thing dropped is a subtle perspective shift nobody
   can catch mid-roll. This is the fix that takes the roll from single-digit FPS
   back to smooth. */
.piece.die-landed .die-wrap { animation: dieBounce 0.34s cubic-bezier(0.2, 1.5, 0.4, 1); }
@keyframes dieBounce {
  0% { transform: scale(1.14); }
  60% { transform: scale(0.96); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .die-wrap.can-roll .die-canvas { animation: none; }
  .piece.die-landed .die-wrap { animation: none; }
}
/* A temporary die rolled for an effect's amount fades out once its number has
   been read, so it clears the felt without a jarring pop. */
.piece.fx-die-out { transition: opacity 0.24s ease; opacity: 0; pointer-events: none; }
/* Die appearance panel: a compact floating panel over the felt (not a full-screen
   modal), so the die stays in view and updates live under the picker as its colour,
   numbers, finish and gloss are tuned. Matches the standings-rail scale and skin. */
.die-look-panel.floating {
  position: absolute; left: 50%; bottom: 18px; transform: translateX(-50%); z-index: 25;
  width: 232px; display: flex; flex-direction: column;
  background: rgba(15,20,28,0.78); backdrop-filter: blur(10px);
  border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; box-shadow: var(--shadow);
}
.dlp-head { display: flex; align-items: center; gap: 6px; padding: 7px 8px 7px 10px; border-bottom: 1px solid rgba(255,255,255,0.08); }
.dlp-drag { cursor: grab; user-select: none; }
.dlp-drag:active { cursor: grabbing; }
.dlp-title { flex: 1; font-weight: 800; font-size: 12px; color: var(--text); }
.dlp-close { display: flex; align-items: center; justify-content: center; width: 20px; height: 20px;
  padding: 0; border-radius: 6px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.12); color: var(--text); cursor: pointer; }
.dlp-close:hover { border-color: var(--accent); }
.dlp-close svg { width: 12px; height: 12px; }
.dlp-body { padding: 8px 10px 10px; }
/* Compact to match the panel: a small section label, tight rows, a full-width
   slider. Each label after the first gets a little breathing room above it. */
.die-look .fe-field-label { margin: 10px 0 5px; font-size: 11px; }
.die-look .fe-field-label:first-child { margin-top: 0; }
.die-look .swatches { gap: 6px; }
.die-look .swatch { width: 20px; height: 20px; border-radius: 6px; border-width: 2px; }
/* Grouped colour grid: a hue's three shades cluster tight (2px), clusters space out
   (10px) and wrap, so light/regular/dark of one colour read as a single group. */
.swatch-grid { display: flex; flex-wrap: wrap; gap: 7px 10px; }
.swatch-group { display: flex; gap: 2px; }
.swatch-grid .swatch { width: 20px; height: 20px; border-radius: 6px; }
.die-gloss-row { display: flex; margin-top: 2px; }
.die-gloss { width: 100%; accent-color: var(--accent-2, #e6b765); }

/* COIN piece: a poker-style chip stack. One or more columns of chips climb
   upward (each chip lifted by --i), and the higher tiers add columns so a big
   pile reads as a row of stacks. --chips is how many chips high the top of a
   column sits, which the count badge rides so the number always sits on the top
   chip. The whole stack keeps its base centered on the piece point. */
.coin {
  --coin-size: calc(30px * var(--pscale, 1));
  --chip-lift: calc(var(--coin-size) * 0.16);
  position: relative;
  display: flex; align-items: flex-end; justify-content: center;
  gap: calc(var(--coin-size) * 0.16);
  cursor: grab;
}
.coin .chip-col {
  position: relative;
  width: var(--coin-size); height: var(--coin-size);
}
/* Base geometry every token unit shares. The material look (its background and
   rim shadows) is layered on by the .tk-* class the piece carries, so a token's
   appearance is a pure dressing over one shared stack shape. */
.coin .chip {
  position: absolute; left: 0; bottom: calc(var(--i, 0) * var(--chip-lift));
  width: var(--coin-size); height: var(--coin-size); border-radius: 50%;
  background:
    radial-gradient(circle at 50% 34%, #ffe9a6 0%, #f4c542 42%, #c8901f 82%, #8a5f13 100%);
  box-shadow:
    inset 0 0 0 calc(var(--coin-size) * 0.09) rgba(255,255,255,0.28),
    inset 0 calc(var(--coin-size) * -0.09) calc(var(--coin-size) * 0.14) rgba(120,80,10,0.5),
    0 2px 3px rgba(0,0,0,0.4);
}
/* Cube-shaped tokens: square blocks instead of round chips. */
.coin.tk-shape-cube .chip { border-radius: 18%; }
/* POKER CHIP - a red casino chip with a pale inner face and edge dashes. */
.coin.tk-chip .chip {
  background:
    radial-gradient(circle at 50% 50%, #e9edf2 0 30%, rgba(233,237,242,0) 31%),
    repeating-conic-gradient(#c8323b 0deg 18deg, #f4f6f8 18deg 36deg);
  box-shadow:
    inset 0 0 0 calc(var(--coin-size) * 0.06) rgba(255,255,255,0.5),
    inset 0 calc(var(--coin-size) * -0.08) calc(var(--coin-size) * 0.14) rgba(90,10,14,0.4),
    0 2px 3px rgba(0,0,0,0.4);
}
/* CUBE - a plain slate resource cube. */
.coin.tk-cube .chip {
  background: linear-gradient(150deg, #6d8bb0 0%, #4f6b8c 55%, #3a516d 100%);
  box-shadow:
    inset 0 calc(var(--coin-size) * 0.1) 0 rgba(255,255,255,0.28),
    inset 0 calc(var(--coin-size) * -0.12) calc(var(--coin-size) * 0.1) rgba(20,30,45,0.5),
    0 2px 3px rgba(0,0,0,0.42);
}
/* WOOD - a warm wooden block. */
.coin.tk-wood .chip {
  background: linear-gradient(150deg, #b9834a 0%, #96612f 55%, #6f451f 100%);
  box-shadow:
    inset 0 calc(var(--coin-size) * 0.1) 0 rgba(255,236,205,0.3),
    inset 0 calc(var(--coin-size) * -0.12) calc(var(--coin-size) * 0.1) rgba(60,35,15,0.5),
    0 2px 3px rgba(0,0,0,0.42);
}
/* STONE - a cool grey stone block. */
.coin.tk-stone .chip {
  background: linear-gradient(150deg, #a7adb4 0%, #7f868e 55%, #5b6167 100%);
  box-shadow:
    inset 0 calc(var(--coin-size) * 0.1) 0 rgba(255,255,255,0.32),
    inset 0 calc(var(--coin-size) * -0.12) calc(var(--coin-size) * 0.1) rgba(30,34,40,0.5),
    0 2px 3px rgba(0,0,0,0.42);
}
/* CARDBOARD 1 - a punched kraft chit, the cheapest denomination. Matte tan with
   a soft ring bevel rather than a metal sheen, so it reads as pressed board next
   to the struck metal coins. */
.coin.tk-card1 .chip {
  background:
    radial-gradient(circle at 50% 36%, #e6cfa4 0%, #c9ab74 46%, #a9854f 84%, #86663a 100%);
  box-shadow:
    inset 0 0 0 calc(var(--coin-size) * 0.08) rgba(255,246,222,0.35),
    inset 0 calc(var(--coin-size) * -0.09) calc(var(--coin-size) * 0.12) rgba(90,64,30,0.45),
    0 2px 3px rgba(0,0,0,0.4);
}
/* SILVER 5 - an aged silver piece: cool white highlight down to a tarnished
   grey rim, brighter and cleaner than the stone cube. */
.coin.tk-silver5 .chip {
  background:
    radial-gradient(circle at 50% 32%, #ffffff 0%, #e3e8ee 40%, #a9b2bd 80%, #6f7883 100%);
  box-shadow:
    inset 0 0 0 calc(var(--coin-size) * 0.09) rgba(255,255,255,0.55),
    inset 0 calc(var(--coin-size) * -0.09) calc(var(--coin-size) * 0.14) rgba(60,68,78,0.5),
    0 2px 3px rgba(0,0,0,0.42);
}
/* GOLD 10 - the top denomination: a richer, more ornate gold than the plain
   gold chip, with a deeper amber core and a bright rim. */
.coin.tk-gold10 .chip {
  background:
    radial-gradient(circle at 50% 30%, #fff3c4 0%, #ffd558 38%, #e3a11d 78%, #a06a10 100%);
  box-shadow:
    inset 0 0 0 calc(var(--coin-size) * 0.1) rgba(255,246,200,0.5),
    inset 0 calc(var(--coin-size) * -0.1) calc(var(--coin-size) * 0.16) rgba(120,78,8,0.55),
    0 2px 4px rgba(0,0,0,0.45);
}
/* ICON SHAPE - the token body IS the chosen glyph: one big filled silhouette in
   the stack's tint (--tint) with a dark rim and drop shadow, so a star reads as a
   physical star piece. Its own element (not a .chip), so it carries no disc, no
   ring, and no per-unit tower - the floating count badge reports how many. Sized
   well past a chip so the shape is the piece, not a mark on one. */
.coin .coin-glyph {
  width: var(--coin-size);
  height: var(--coin-size);
  display: flex; align-items: center; justify-content: center;
}
.coin .coin-glyph svg {
  width: 100%; height: 100%;
  fill: var(--tint, #e6b765);
  stroke: rgba(20,14,4,0.5);
  stroke-width: 1.1;
  stroke-linejoin: round;
  filter: drop-shadow(0 2px 3px rgba(0,0,0,0.45));
}
/* The worth badge rides the top of the big glyph rather than a chip tower. */
.coin.tk-shape-glyph .coin-count {
  bottom: calc(var(--coin-size) * 0.95);
}
/* The struck denomination on a faced token: the value pressed into the top coin
   like a mint stamp. Centered on the top chip and lifted with the tower so it
   always rides the face, dark engraving with a faint highlight below so it reads
   as struck into metal (or board) on gold, silver, and kraft alike. */
.coin .coin-face {
  position: absolute; left: 50%; z-index: 5;
  bottom: calc((var(--chips, 1) - 1) * var(--chip-lift) + var(--coin-size) * 0.5);
  transform: translate(-50%, 50%);
  font-weight: 900;
  font-size: calc(var(--coin-size) * 0.4);
  line-height: 1;
  color: rgba(60,42,14,0.78);
  text-shadow: 0 1px 0 rgba(255,255,255,0.32);
  pointer-events: none;
  user-select: none;
}
.coin.tk-silver5 .coin-face { color: rgba(48,54,62,0.72); }
/* A glyph struck on the top coin in place of a number, so a stack reads as its
   own kind of token (a sword for Attack). Sits where the numeric face would,
   sized to the chip and inheriting the same engraved colour so it reads as
   pressed into the same metal or board. */
.coin .coin-face-icon {
  width: calc(var(--coin-size) * 0.5);
  height: calc(var(--coin-size) * 0.5);
  display: flex; align-items: center; justify-content: center;
}
.coin .coin-face-icon svg {
  width: 100%; height: 100%;
  stroke-width: 2.4;
  filter: drop-shadow(0 1px 0 rgba(255,255,255,0.32));
}
.coin .coin-count {
  position: absolute; left: 50%; z-index: 6;
  /* ride just above the top chip so the number never sits buried mid-tower */
  bottom: calc((var(--chips, 1) - 1) * var(--chip-lift) + var(--coin-size) * 0.62);
  transform: translateX(-50%);
  padding: 1px calc(var(--coin-size) * 0.22);
  border-radius: 999px;
  background: rgba(18, 24, 32, 0.85);
  font-weight: 800;
  font-size: calc(var(--coin-size) * 0.42);
  line-height: 1.25;
  color: #ffe6a3;
  white-space: nowrap;
  box-shadow: 0 2px 5px rgba(0,0,0,0.45);
  pointer-events: none;
}
.piece.lifted .coin { cursor: grabbing; }
.piece.lifted .coin .chip {
  box-shadow: inset 0 0 0 calc(var(--coin-size) * 0.09) rgba(255,255,255,0.3), 0 18px 22px rgba(0,0,0,0.5);
}

/* TOKEN CREATOR - a compact glass form (inside TCG.modal) with a live preview
   drawn by the real token renderer, so what a builder designs is what lands on
   the felt. Fields stack tight at editor scale; the panel scrolls itself if the
   glyph grid runs long. */
.tok-creator { display: flex; flex-direction: column; gap: 10px; max-height: 62vh; overflow-y: auto; min-width: min(300px, calc(100vw - 40px)); }
.tok-preview-wrap {
  display: flex; align-items: center; justify-content: center;
  min-height: 96px; padding: 10px;
  border-radius: 10px;
  background: radial-gradient(circle at 50% 40%, #274536 0%, #1c3327 100%);
  border: 1px solid rgba(255,255,255,0.08);
}
.tok-preview { position: relative; display: flex; align-items: flex-end; justify-content: center; }
.tok-creator .fe-field { display: flex; flex-direction: column; gap: 5px; }
.tok-creator .fe-field-label { font-size: 11px; color: var(--text-faint); }
/* `input.tok-*` for the reason stated on input.ctx-num above. */
input.tok-name, input.tok-val {
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.12);
  border-radius: 7px; padding: 6px 8px;
  color: var(--text); font-size: 12px;
}
input.tok-val { width: 84px; flex: 0 0 auto; text-align: center; font-variant-numeric: tabular-nums; }
input.tok-name.invalid { border-color: var(--red, #c0432f); }
.tok-looks { display: flex; flex-wrap: wrap; gap: 5px; }
.tok-glyphs { max-height: 118px; overflow-y: auto; }
.tok-tints { gap: 6px; }

/* CARD piece reuses the card visual, sized for the table */
.tpiece-card { cursor: grab; transition: filter 0.15s; }
.tpiece-card .tcard { width: calc(60px * var(--pscale, 1) * var(--card-wide, 1)); box-shadow: 0 1px 2px rgba(0,0,0,0.35); }
.piece.lifted .tpiece-card .tcard { box-shadow: 0 40px 50px rgba(0,0,0,0.6); }
/* Stacked cards: only the visible TOP of a pile casts the full drop shadow (it
   stands in for the whole stack). Cards beneath it cast a thin contact edge when
   they peek out of a sloping pile, or nothing when they hide directly behind the
   top of a squared pile - otherwise every card's own shadow compounds into one
   large dark blob. This holds while the pile is DRAGGED too: lifting a stack adds
   .lifted to every card in it, and without suppressing the under-cards their big
   raised shadows would compound into a huge dark halo under the pile. So these
   rules sit after (and beat) the .lifted rule above; only the top card, which
   carries no understack class, shows the raised drag shadow. See paintStackShadow
   in tableEditor.js. */
.piece.understack .tpiece-card .tcard,
.piece.understack .tpiece-card .card-back { box-shadow: 0 0.5px 1px rgba(0,0,0,0.22); }
.piece.understack-flat .tpiece-card .tcard,
.piece.understack-flat .tpiece-card .card-back { box-shadow: none; }
.tpiece-card.face-down > .tcard { display: none; }
/* ...but never the card RENDERED INSIDE a designed back (.back-face hosts a
   real .tcard via renderCard) - the descendant selector this replaced hid the
   night face the moment the piece turned over, so the back slot showed empty.
   FLEX, never block: a .tcard is a flex column whose .tcard-inner is flex: 1,
   so forcing block here collapsed the inner face to its content height and the
   whole card rendered as a bare accent-dark slab (proved by live-browser
   measurement: card 210x277, inner 185x18). */
.tpiece-card.face-down .card-back.back-face .tcard { display: flex; }
.tpiece-card .card-back {
  container-type: inline-size;
  width: calc(60px * var(--pscale, 1) * var(--card-wide, 1)); aspect-ratio: var(--card-aspect, 5 / 7); border-radius: 6.7% / 4.8%; border: 1px solid rgba(255,255,255,0.16);
  background:
    linear-gradient(135deg, rgba(255,255,255,0.14), transparent 45%),
    repeating-linear-gradient(45deg, #3a4a63 0 9px, #2c3a50 9px 18px);
  display: none; align-items: center; justify-content: center; color: rgba(255,255,255,0.8);
  box-shadow: 0 1px 2px rgba(0,0,0,0.35);
}
.tpiece-card .card-back > svg { width: 29cqw; height: 29cqw; }
.tpiece-card.face-down .card-back { display: flex; }
/* a game-supplied back image (set in the card creator) replaces the generic
   striped placeholder - fills the back edge to edge like a real card back. */
.card-back.has-back-art { padding: 0; overflow: hidden; }
.card-back.has-back-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.card-back.has-back-art.fit-contain img { object-fit: contain; }
/* a two-sided card's back is its OTHER designed face, rendered by the real
   renderCard (the faceCard rule) - the back slot just hosts that card face
   edge to edge instead of painting the striped cover. The hosted card is
   ABSOLUTELY positioned: a percentage height would have to resolve through a
   flex item inside an aspect-ratio box, which real browsers refuse, so the
   .tcard-inner (flex: 1 of that height) collapsed to nothing and the face
   rendered as a bare accent-dark slab with its elements bunched at the top.
   inset: 0 gives the card a definite box whatever the chain above thinks. */
.card-back.back-face { position: relative; padding: 0; overflow: visible; background: none; border: none; box-shadow: none; }
.card-back.back-face .tcard { position: absolute; inset: 0; width: 100%; height: 100%; }
/* A card-back-art deck keeps the same stacked-edge shadow as the generic striped
   deck-top and the face-up deck-face, so an art-backed pile still reads as many
   cards rather than one flat card. Without this it fell back to box-shadow:none
   and looked like a single card next to a plain deck. */
.deck-top-art { padding: 0; overflow: hidden; background: none; border-width: 0;
  box-shadow:
    1.7cqw 2.6cqw 0 var(--deck-edge, #1d2738), 3.4cqw 5.2cqw 0 var(--deck-edge, #1d2738),
    5.2cqw 7.8cqw 0 var(--deck-edge, #1d2738), 6.9cqw 9.5cqw 0 var(--deck-edge, #1d2738),
    8.6cqw 12cqw 0 var(--deck-edge, #1d2738), 0 20.7cqw 29cqw rgba(0,0,0,0.55);
}
.deck-top-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.deck-top-art.fit-contain img { object-fit: contain; }
.piece.lifted .deck-top-art { box-shadow:
    1.7cqw 2.6cqw 0 var(--deck-edge,#1d2738), 3.4cqw 5.2cqw 0 var(--deck-edge,#1d2738),
    5.2cqw 7.8cqw 0 var(--deck-edge,#1d2738), 6.9cqw 9.5cqw 0 var(--deck-edge,#1d2738),
    8.6cqw 12cqw 0 var(--deck-edge,#1d2738), 0 36cqw 43cqw rgba(0,0,0,0.6); }
.tpiece-card.tapped { transform: rotate(90deg); }
.piece.just-dealt .piece-3d { animation: dealIn 0.42s cubic-bezier(0.2, 1.1, 0.4, 1); }
@keyframes dealIn {
  0% { transform: translateY(-26px) translateZ(70px) rotateZ(-10deg) scale(0.8) rotate(var(--piece-rot, 0deg)); opacity: 0; }
  60% { opacity: 1; }
  100% { transform: rotate(var(--piece-rot, 0deg)); opacity: 1; }
}
/* drawing cards fly in from the deck, one at a time. --fly-x/--fly-y is the
   deck's offset from the card's final slot; --deal-delay staggers the batch.
   "backwards" fill keeps a waiting card parked at the deck until its turn.

   A FLIGHT MUST NOT CHANGE THE CARD'S SCALE. This keyframe used to grow the card
   from scale(0.84), and to fly it forward through translateZ(60px), which is a
   second scale change because .table-stage carries a perspective. A card face is
   an uploaded 512px picture drawn into a ~120px box, and Safari re-rasters a
   layer whenever its scale changes - so every intermediate scale of the flight
   re-resampled that picture, once per card, while sixteen of them were in the
   air. Measured on Siem's machine with ?jank=1 (js/jankProbe.js): the deal froze
   for 723ms up front and again at +730ms and +1093ms mid-flight, with the app's
   own card building at 11ms of it. Dealing at a flat scale removed every
   mid-flight stall and the deal reads as smooth. Same finding as the die, which
   drops its filters while it tumbles: in Safari the per-frame rasterize is the
   dominant cost of any card motion, so move and fade a card, never resize it. */
.piece.dealing .piece-3d {
  animation: cardDeal var(--deal-dur, 0.46s) cubic-bezier(0.22, 1, 0.36, 1) var(--deal-delay, 0ms) backwards;
}
@keyframes cardDeal {
  0% { transform: translate(var(--fly-x, 0), var(--fly-y, 0)) rotate(var(--piece-rot, 0deg)); opacity: 0; }
  30% { opacity: 1; }
  100% { transform: rotate(var(--piece-rot, 0deg)); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .piece.dealing .piece-3d { animation-duration: 0.01ms; animation-delay: 0ms; }
}
/* a card the AI plays glides from its hand to the pile (no fade, it was visible
   in hand already). --fly-x/--fly-y is the hand's offset from the pile slot. */
.piece.playing .piece-3d {
  /* --play-dur lets a code-routed glide (a bought/taken/awarded card, set by
     flyPieceFrom/flyToNewSpot) travel slower so it's easy to follow, while a
     direct play keeps the default snappy 0.52s. */
  animation: cardPlay var(--play-dur, 0.52s) cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
@keyframes cardPlay {
  /* --play-rot lets a code-routed glide tilt a bit more as it travels (set by
     startGlide), while a direct play keeps the subtle default -6deg. The tilt
     eases back to straight partway, so the card lands square. */
  0% { transform: translate(var(--fly-x, 0), var(--fly-y, 0)) translateZ(72px) rotateZ(var(--play-rot, -6deg)) scale(1.05) rotate(var(--piece-rot, 0deg)); }
  55% { transform: translateZ(46px) rotateZ(calc(var(--play-rot, -6deg) * 0.34)) scale(1.03) rotate(var(--piece-rot, 0deg)); }
  100% { transform: rotate(var(--piece-rot, 0deg)); }
}
@media (prefers-reduced-motion: reduce) {
  .piece.playing .piece-3d { animation-duration: 0.01ms; }
}
/* a card nudged sideways as its row recenters to fit a newly added card: a
   subtle slide to its new spot with no lift, tilt, or scale (unlike .playing),
   so the cards already down ease over to make room instead of snapping there.
   --fly-x/--fly-y is the old spot's offset from the new one, set by slidePieceFrom. */
.piece.sliding .piece-3d {
  animation: cardSlide 0.34s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
@keyframes cardSlide {
  0% { transform: translate(var(--fly-x, 0), var(--fly-y, 0)) rotate(var(--piece-rot, 0deg)); }
  100% { transform: rotate(var(--piece-rot, 0deg)); }
}
@media (prefers-reduced-motion: reduce) {
  .piece.sliding .piece-3d { animation-duration: 0.01ms; }
}
/* ghost cards shown when a hidden hand draws (e.g. an opponent). They fly from
   the deck to that player's seat marker, then fade, so the deal stays visible. */
.deal-ghost {
  position: absolute; width: 46px; height: 64px; margin: -32px 0 0 -23px;
  z-index: 120; pointer-events: none;
  animation: ghostDeal var(--deal-dur, 0.46s) cubic-bezier(0.4, 0, 0.5, 1) var(--deal-delay, 0ms) backwards;
}
.deal-ghost .card-back {
  width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
  border-radius: 6px; border: 1px solid var(--line-2);
  background: linear-gradient(150deg, var(--panel-2), var(--panel));
  box-shadow: 0 6px 14px rgba(0,0,0,0.35); color: var(--text-faint);
}
.deal-ghost .card-back svg { width: 22px; height: 22px; }
/* A GHOST IS A CARD IN MOTION, so it moves and fades and never changes size -
   the same rule cardDeal above was rewritten to obey. This keyframe used to run
   scale(0.9) -> scale(0.6), and a ghost carrying a game's own card back is an
   uploaded picture drawn into a 46px box, so Safari re-rasterised and
   re-resampled that picture at every intermediate scale, once per ghost, with a
   whole hidden hand's worth in the air at once. A hidden hand is the common case
   in hotseat (every opponent's opening deal goes through here), so this fired on
   exactly the deal the flat cardDeal fix was made for and was missed because the
   ghost path is a different keyframe. Move and fade only. */
@keyframes ghostDeal {
  0% { transform: translate(0, 0); opacity: 0; }
  20% { opacity: 1; }
  80% { opacity: 1; }
  100% { transform: translate(var(--gx, 0), var(--gy, 0)); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) { .deal-ghost { animation-duration: 0.01ms; } }

/* a coin flying between zones (a purchase, a bet stake, a payout) - same travel
   pattern as a deal ghost. It carries the same tk-* token classes a real stack
   wears, so the ghost's single .chip is dressed by the shared token styling and
   always matches the material of the coins it left (gold, poker chip, cube...). */
.coin-ghost {
  position: absolute; z-index: 120; pointer-events: none;
  --coin-size: calc(26px * var(--pscale, 1));
  width: var(--coin-size); height: var(--coin-size);
  margin: calc(var(--coin-size) / -2) 0 0 calc(var(--coin-size) / -2);
  animation: ghostCoin 0.52s cubic-bezier(0.35, 0, 0.4, 1) var(--deal-delay, 0ms) both;
}
@keyframes ghostCoin {
  0% { transform: translate(0, 0) scale(1); opacity: 0; }
  18% { opacity: 1; }
  80% { opacity: 1; }
  100% { transform: translate(var(--gx, 0), var(--gy, 0)) scale(0.55); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) { .coin-ghost { animation-duration: 0.01ms; } }

/* right-click context menu, shared across the builder */
.ctx-menu {
  position: fixed; z-index: 350; min-width: 168px; padding: 4px;
  /* Same glass as the floating toolbar / turn sequencer (.table-tools.floating):
     translucent dark fill, blur, thin white hairline. Every menu opened from a
     pill or the toolbar wears this, so the app reads as one floating surface. */
  background: rgba(15,20,28,0.5); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,0.08); border-radius: 10px; box-shadow: var(--shadow);
  animation: ctxIn 0.12s ease;
  max-height: min(70vh, 460px); overflow-y: auto;
}
@keyframes ctxIn { from { opacity: 0; transform: translateY(-4px) scale(0.98); } to { opacity: 1; transform: none; } }
/* Filter field at the top of a searchable menu (the add-effect picker). Compact,
   glass-consistent: a translucent inset field with a gold focus ring, sized to
   the pill scale so it reads as part of the menu, not a form control dropped in. */
.ctx-search {
  display: block; width: 100%; box-sizing: border-box; margin: 2px 0 4px;
  padding: 6px 9px; border-radius: 7px; font: inherit; font-size: 12px;
  color: var(--text); background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.1); outline: none;
}
.ctx-search::placeholder { color: var(--text-faint); }
.ctx-search:focus { border-color: var(--accent-2); background: rgba(255,255,255,0.09); }
/* A searchable menu built by showCtx's opts.search (the sequencer's add-a-step
   menus). The menu itself stops scrolling and becomes a column, so the head and
   the field stay put while only the results move under them - a filter box that
   scrolls away with its own results is worse than none. */
.ctx-menu.ctx-searchable { display: flex; flex-direction: column; overflow: hidden; min-width: 236px; }
.ctx-menu.ctx-searchable > .ctx-rows { overflow-y: auto; min-height: 0; flex: 1 1 auto; }
.ctx-search-wrap { margin: 2px 2px 4px; flex: 0 0 auto; }
/* Written as input.ctx-search-input, not .ctx-search-input: a bare class (0-1-0)
   loses the cascade to the global input[type="text"] rule (0-1-1) and its look is
   dropped without a word. The left gutter that clears the magnifier is NOT set
   here - it belongs to .search-wrap (see TCG.searchField). */
input.ctx-search-input {
  padding-top: 6px; padding-right: 9px; padding-bottom: 6px;
  border-radius: 7px; font-size: 12px;
  color: var(--text); background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.1); outline: none;
}
input.ctx-search-input::placeholder { color: var(--text-faint); }
input.ctx-search-input:focus { border-color: var(--accent-2); background: rgba(255,255,255,0.09); }
.ctx-head { font-size: 10px; text-transform: uppercase; letter-spacing: 0.7px; color: var(--text-faint); padding: 6px 9px 3px; font-weight: 700; }
.ctx-sep { height: 1px; background: var(--line); margin: 4px 4px; }
.ctx-note { display: flex; align-items: center; gap: 9px; padding: 6px 9px; color: var(--text-faint); font-size: 12px; }
.ctx-note .ctx-ic { background: none; color: var(--text-faint); }
.ctx-note small { display: block; font-size: 11px; opacity: 0.85; }
.ctx-item {
  display: flex; align-items: center; gap: 9px; width: 100%; text-align: left;
  padding: 6px 9px; border-radius: 7px; border: none; background: none; color: var(--text);
  font: inherit; font-size: 13px; cursor: pointer; transition: background 0.1s;
}
.ctx-item:hover { background: rgba(255,255,255,0.07); }
.ctx-item.disabled { opacity: 0.6; cursor: default; }
.ctx-item.disabled:hover { background: none; }
/* A greyed row's REASON has to stay readable - it is the only thing on the row
   that a builder can act on. Opacity applies to a whole subtree, so the faint
   sub colour underneath a 0.45 row left the one sentence that explains the row
   nearly invisible; the row is muted a little less and the sentence is given a
   brighter colour of its own, so "grey" still reads as inert while the words
   read as words. The width cap makes a full sentence WRAP rather than stretch
   the menu out to the length of its longest explanation. */
.ctx-item.disabled .ctx-text { max-width: 250px; }
.ctx-item.disabled .ctx-text small { color: var(--text-dim); line-height: 1.35; white-space: normal; }
.ctx-item.danger:hover { background: rgba(214,96,77,0.12); color: var(--red); }
.ctx-ic { width: 24px; height: 24px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; background: rgba(216,162,74,0.12); color: var(--accent-2); border-radius: 6px; }
.ctx-ic svg { width: 14px; height: 14px; }
.ctx-item.danger .ctx-ic { background: rgba(214,96,77,0.12); color: var(--red); }
.ctx-text { display: flex; flex-direction: column; }
.ctx-text small { color: var(--text-faint); font-size: 11px; font-weight: 400; }
/* WHAT A ROW IS CARRYING, on the right: how many things inside it are set, or
   on a door how many settings are behind it. Never a zero - an untouched
   section says so by saying nothing, which is what makes a set-first resting
   page readable at a glance. */
.ctx-mark {
  margin-left: auto; font-size: 10px; font-weight: 700; line-height: 1;
  padding: 3px 6px; border-radius: 999px; letter-spacing: 0.2px;
  background: rgba(216,162,74,0.16); color: var(--accent-2);
}
.ctx-item .ctx-mark + .ctx-on { margin-left: 4px; }
/* THE DOOR: the one row holding everything not on the resting page. Quieter
   than a setting, because it is a way in rather than a thing to change. */
.ctx-door { color: var(--text-dim); }
.ctx-door .ctx-ic { background: rgba(255,255,255,0.06); color: var(--text-dim); }
.ctx-door .ctx-mark { background: rgba(255,255,255,0.08); color: var(--text-dim); }

/* WHAT IS WRONG WITH THIS ONE THING, above its settings. Fed by js/lint.js
   filtered to this subject, so nothing here is authored. THREE LEVELS, and they
   are lint's own three rather than a new vocabulary - split by consequence, not
   by severity: stops (it cannot run), silent (it is set and does nothing),
   check (legal, and probably not what was meant).

   The quiet one is deliberate. "Legal, probably not meant" is the level most
   likely to be wrong about a correct game, and on a set-first page it is no
   longer a warning strip somebody can ignore - it is most of what a new area
   says. It must never shout as loudly as a stop. */
.ctx-lint { margin: 2px 2px 4px; display: flex; flex-direction: column; gap: 3px; }
.ctx-lintrow {
  display: flex; align-items: flex-start; gap: 8px; width: 100%; text-align: left;
  padding: 6px 8px; border-radius: 8px;
  border: 1px solid rgba(224,163,58,0.28); background: rgba(224,163,58,0.10);
  font: inherit; font-size: 12px; color: var(--text); cursor: pointer; transition: background 0.1s;
}
.ctx-lintrow:hover { background: rgba(224,163,58,0.2); }
.ctx-lintrow .ctx-ic { width: 20px; height: 20px; border-radius: 5px; background: rgba(224,163,58,0.16); color: var(--warn, #e0a33a); }
.ctx-lintrow .ctx-ic svg { width: 12px; height: 12px; }
.ctx-lintrow .ctx-text { gap: 1px; }
/* The fix line. It is the only part of a finding a builder can act on - the
   line above it is the diagnosis - so it is the one that carries the colour. */
.ctx-lintrow small { color: var(--accent-2); font-size: 10.5px; font-weight: 700; line-height: 1.35; white-space: normal; }
.ctx-lintrow.stops { background: rgba(214,96,77,0.12); border-color: rgba(214,96,77,0.34); }
.ctx-lintrow.stops:hover { background: rgba(214,96,77,0.22); }
.ctx-lintrow.stops .ctx-ic { background: rgba(214,96,77,0.16); color: var(--red); }
.ctx-lintrow.check { background: rgba(255,255,255,0.045); border-color: rgba(255,255,255,0.09); }
.ctx-lintrow.check:hover { background: rgba(255,255,255,0.09); }
.ctx-lintrow.check .ctx-ic { background: rgba(255,255,255,0.07); color: var(--text-dim); }
.ctx-lintrow.check small { color: var(--text-dim); }

.ctx-item .ctx-on { margin-left: auto; color: var(--accent-2); display: none; }
.ctx-item.on .ctx-on { display: inline-flex; }
.ctx-item .ctx-on svg { width: 14px; height: 14px; }

/* Inline number row inside a context menu: a typed field with -/+ steppers,
   edited in place instead of opening a full-screen number modal. Sized against
   the pill/menu scale, glass-consistent. */
.ctx-item.ctx-stepper { cursor: default; }
.ctx-item.ctx-stepper:hover { background: none; }
.ctx-step { margin-left: auto; gap: 4px; }
.ctx-step .ce-step-btn { width: 22px; height: 22px; border-radius: 6px; }
.ctx-step .ce-step-btn svg { width: 12px; height: 12px; }
/* Written as `input.ctx-num` on purpose, the same cascade trap the search fields
   hit twice: the global `input[type="number"]` block near the top of this file is
   0-1-1 and sets `width: 100%` plus a 9px/11px page-form padding, so a bare
   `.ctx-num` (0-1-0) LOSES and the compact stepper field renders as a full-width
   page input inside a pill-scale menu. Every number field on a glass surface
   states its class as `input.<class>`. */
input.ctx-num {
  width: 42px; flex: 0 0 auto; text-align: center; font-weight: 800; font-size: 13px;
  color: var(--text); background: rgba(0,0,0,0.3); border: 1px solid var(--line-2);
  border-radius: 6px; padding: 3px 2px; outline: none; font-variant-numeric: tabular-nums;
  -moz-appearance: textfield;
}
input.ctx-num:focus { border-color: var(--accent); box-shadow: none; }
input.ctx-num::-webkit-outer-spin-button, input.ctx-num::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* Dice-pool builder: a compact popup (inside the context-menu overlay) with a
   count stepper per die shape, so a builder drops any mix in one action. Sized
   against the ctx-menu / standings-rail scale, not a full modal. */
.ctx-menu.dice-pool { min-width: 190px; padding: 4px 6px 6px; }
.dice-pool-list { display: flex; flex-direction: column; gap: 3px; margin: 2px 0; }
.dice-pool-row { display: flex; align-items: center; gap: 8px; padding: 3px 3px; }
.dice-pool-name { font-size: 12px; font-weight: 700; color: var(--text); min-width: 34px; }
.dice-pool-sub { font-size: 11px; color: var(--text-faint); flex: 1; }
.dice-step {
  width: 22px; height: 22px; border-radius: 6px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.05); border: 1px solid var(--line-2); color: var(--text);
  cursor: pointer; transition: border-color 0.1s;
}
.dice-step:hover { border-color: var(--accent); }
.dice-step svg { width: 12px; height: 12px; }
.dice-pool-amt {
  min-width: 18px; text-align: center; font-size: 12px; font-weight: 800;
  color: var(--text); font-variant-numeric: tabular-nums;
}
.dice-pool-actions { margin-top: 4px; }
.dice-pool-add {
  width: 100%; padding: 6px 10px; border-radius: 8px; font: inherit; font-size: 12px; font-weight: 700;
  background: var(--accent); border: 1px solid var(--accent); color: #10151d; cursor: pointer;
}
.dice-pool-add:hover:not(:disabled) { filter: brightness(1.08); }
.dice-pool-add:disabled { background: rgba(255,255,255,0.05); border-color: var(--line-2); color: var(--text-faint); cursor: default; }

/* Double-click-to-rename: a name label (deck, turn step, phase) hints it's
   editable, and swaps to this inline input while being edited. */
.renamable { cursor: text; }
.inline-rename {
  display: block; color: var(--text);
  background: rgba(0,0,0,0.35); border: 1px solid var(--accent); border-radius: 4px;
  padding: 0 2px; margin: 0; outline: none; box-sizing: border-box;
  min-width: 0; max-width: 100%;
  /* font + width + height are set inline from the label being edited, so the
     field matches the original text's size and footprint. display:block avoids
     the inline-block baseline gap that would otherwise push it down a pixel. */
}

/* THE NAME'S BUDGET, shown while it is being typed. Fixed, not absolute: the
   block a step name lives in clips its overflow, so anything positioned inside
   the label would be cut off at the block's edge and never seen. Sits under the
   field, out of the way of the words themselves, and goes gold on the last
   letter so the ceiling announces itself rather than just refusing keystrokes. */
.rename-count {
  position: fixed; z-index: 1250; pointer-events: none;
  padding: 2px 7px; border-radius: 999px;
  background: rgba(0,0,0,0.78); border: 1px solid var(--line-2);
  color: var(--text-dim); font-size: 10.5px; font-weight: 700;
  letter-spacing: 0.2px; white-space: nowrap;
}
.rename-count.full { color: var(--accent); border-color: var(--accent); }

/* phase strip below the table */
.tphase-bar { display: flex; align-items: center; gap: 5px; margin: 14px 2px 0; flex-wrap: wrap; }
.tphase-bar > .lbl { font-size: 12px; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.8px; font-weight: 700; margin-right: 2px; }
.tphase-bar > .lbl.lbl-click { cursor: pointer; }
.tphase-bar > .lbl.lbl-click:hover { color: var(--accent-2); }
/* TWO LANES AT ONCE. The bar is a single wrapping row of pills in the normal
   case and is left exactly as it was; only with a second row open does it become
   a column of rows, each of which is the row the bar used to be. The felt makes
   space for the taller bar on its own, because syncRailInsets MEASURES the bar
   rather than assuming a height - the same reason a wrapped seat rail works. */
.tphase-bar.two-rows { flex-direction: column; align-items: flex-start; gap: 5px; }
.tphase-row { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
/* A hairline between the two lanes, so the rows read as two sequences rather
   than one long wrapped one. On the row itself rather than between them, so it
   moves with the row and needs no extra element. */
.tphase-bar.two-rows .tphase-row.row-b { border-top: 1px solid var(--line); padding-top: 5px; width: 100%; }
/* The second row's label says it is a second row rather than the view you are
   in - same size, one notch quieter, so the primary lane still leads. */
.tphase-bar.two-rows .tphase-row.row-b > .lbl { opacity: 0.75; }
/* The row a dragged step is about to CHANGE LANE into. Only ever on the row the
   pill did not come from, so it can never be mistaken for the within-row
   reorder - which is the whole point of it: the two gestures are one drag and
   have to be told apart before the release, not after. A wash and a ring rather
   than a fill, so the pills under it stay readable while it is lit. */
.tphase-row.tphase-row-drop {
  outline: 1px dashed var(--accent); outline-offset: 3px; border-radius: 10px;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.tphase {
  display: inline-flex; align-items: center; gap: 4px; padding: 4px 8px; border-radius: 999px;
  border: 1px solid var(--line-2); background: var(--panel-2); font-size: 12px; font-weight: 600;
  position: relative; overflow: hidden;
}
/* the auto-resolve fill bar: sweeps left-to-right across the current pill over
   the step's dwell, so a player can see how long until it moves on. Sits behind
   the pill's own text/badges (which are lifted above it below). */
.tphase-fill {
  position: absolute; left: 0; top: 0; bottom: 0; width: 0;
  background: rgba(216,162,74,0.32); pointer-events: none; z-index: 0;
}
.tphase > *:not(.tphase-fill) { position: relative; z-index: 1; }
.tphase .ph-x { display: inline-flex; color: var(--text-faint); border: none; background: none; padding: 1px; border-radius: 50%; }
.tphase .ph-x:hover { color: var(--red); }
.tphase .ph-x svg { width: 12px; height: 12px; }
.tphase-arrow { color: var(--text-faint); display: inline-flex; margin: 0 -1px; } .tphase-arrow svg { width: 12px; height: 12px; }
/* the Setup lane's separator is the WORD "and", not a chevron: those steps are a
   list of things that all happen before turn one, and a chevron between two of
   them reads as one step continued rather than two steps (see setupSep). Sized
   below the pill's own 12px so it reads as grammar between pills, not as a pill. */
.tphase-and {
  color: var(--text-faint); display: inline-flex; align-items: center;
  font-size: 10px; font-style: italic; letter-spacing: 0.2px; margin: 0 1px;
  user-select: none;
}
/* drag a build-mode pill left/right to reorder its lane (mirrors the seat rail) */
.tphase.draggable { cursor: grab; }
/* the picked-up pill: a floating clone that tracks the cursor above the bar */
.tphase-ghost {
  position: fixed; z-index: 1200; pointer-events: none; margin: 0;
  cursor: grabbing; opacity: 0.97;
  transform: rotate(-2deg) scale(1.05);
  box-shadow: 0 10px 24px -8px rgba(0,0,0,0.6), 0 0 0 1px var(--accent) inset;
}
/* the gap left behind while the clone is in the air */
.tphase.tphase-lift { opacity: 0.28; }
/* mute the between-pill arrows while reordering so pills slide apart cleanly
   (visibility, not display, so the row keeps its width and nothing jumps) */
.tphase-bar.reordering .tphase-arrow,
.tphase-bar.reordering .tphase-and { visibility: hidden; }
.tphase.add { border-style: dashed; color: var(--text-dim); cursor: pointer; }
.tphase.add:hover { border-color: var(--accent); color: var(--accent-2); }
.tphase.add svg { width: 14px; height: 14px; }
/* Triggers lane: a condition pill -> outcome pill pair. The condition reads the
   board, the outcome (a message) reacts; a hairline divider sits between rules. */
.tphase.trg-cond { cursor: pointer; }
.trg-empty { color: var(--text-faint); font-style: italic; }
/* THE OTHER HALF OF THE TRIGGER, over the felt rather than on the lane. A
   trigger's message is free text with no ceiling - 260px for one ordinary
   sentence - so it can never be on a pill that has to sit in a row. It prints
   here when a card is pointed at, and it costs the lane nothing.

   ABOVE the hand, not under it, and that is forced rather than chosen: the bar
   is anchored to the bottom of the felt, so there is no "under". It also lands
   where the card is already going, since a hovered card lifts. Absolutely
   positioned, which is what keeps it out of the flex row and out of the width
   the hand tightens itself against (fanRoom). */
.tphase-cap {
  position: absolute; left: 0; right: 0; bottom: calc(100% + 4px); z-index: 60; pointer-events: none;
  text-align: center; opacity: 0; transition: opacity 160ms linear;
}
.tphase-cap.on { opacity: 1; }
/* ON A PLATE, not as bare text on the felt. The lane is anchored to the bottom
   edge, and the seat pills sit on that same edge - a line of glowing text ran
   straight through "Player 1". A plate with its own z-index reads over whatever
   is behind it, which is what a message appearing on demand should do. */
.tphase-cap-in {
  display: inline-block; max-width: 100%; padding: 4px 10px; border-radius: 8px;
  background: rgba(12,17,22,0.94); border: 1px solid var(--line-2);
  box-shadow: 0 4px 14px rgba(0,0,0,0.5);
  font-size: 11.5px; font-weight: 700; color: var(--text); line-height: 1.3;
}
.tphase-cap-w { color: var(--text-dim); font-weight: 500; }
/* play-mode turn tracker: step through phases, end the turn */
.tphase.step { cursor: pointer; transition: border-color 0.12s, background 0.12s, color 0.12s; }
.tphase.step:hover { border-color: var(--line-2); color: var(--text); }
.tphase.cur { border-color: var(--accent); background: rgba(216,162,74,0.16); color: var(--text); }
.tphase.step.skip { border-color: var(--line-2); background: var(--panel-2); color: var(--text-dim); opacity: 0.55; }
.tphase.step.skip .tphase-badge { color: var(--text-dim); background: rgba(255,255,255,0.06); }
.tphase.step.skip .tphase-fill { background: rgba(255,255,255,0.10); }
.tphase.end-turn { cursor: pointer; margin-left: 4px; border-color: var(--accent-2); color: var(--accent-2); transition: background 0.12s; }
.tphase.end-turn:hover { background: rgba(216,162,74,0.12); }
.tphase.end-turn svg { width: 14px; height: 14px; }
/* the auto-resolve marker in the Turn view: a "Resolves to a winner" zone
   decides itself the moment it fills, so its pill is a derived, non-draggable
   marker rather than a reorderable step. A faint gold tint and click cursor set
   it apart from a plain step (grey) and the dashed "+ Step" adder. */
.tphase.resolve { cursor: pointer; border-color: rgba(216,162,74,0.55); background: rgba(216,162,74,0.10); color: var(--text); transition: background 0.12s, border-color 0.12s; }
.tphase.resolve:hover { background: rgba(216,162,74,0.18); border-color: var(--accent); }
.tphase.resolve svg { width: 13px; height: 13px; }
/* manual end-turn: the turn has run out its steps and is waiting on the click */
.tphase.end-turn.ready { border-color: var(--accent); color: #1a1206; background: linear-gradient(180deg, var(--accent-2), var(--accent)); font-weight: 700; animation: endTurnReady 1.3s ease-in-out infinite; }
.tphase.end-turn.ready:hover { filter: brightness(1.06); background: linear-gradient(180deg, var(--accent-2), var(--accent)); }
@keyframes endTurnReady { 0%, 100% { box-shadow: 0 0 0 0 rgba(216,162,74,0.55); } 50% { box-shadow: 0 0 0 5px rgba(216,162,74,0); } }
/* the "you owe a draw stack" obligation chip */
.tphase.fx-owe { cursor: pointer; margin-left: 4px; border-color: var(--red); color: #ffd9d2; background: rgba(192,57,43,0.28); font-weight: 700; animation: fxOwePulse 1.3s ease-in-out infinite; }
.tphase.fx-owe:hover { background: rgba(192,57,43,0.45); }
.tphase.fx-owe svg { width: 14px; height: 14px; }
@keyframes fxOwePulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(192,57,43,0.5); } 50% { box-shadow: 0 0 0 4px rgba(192,57,43,0); } }
/* A turn-bar pill that is on screen but has nothing to do yet - nothing staged
   to take back, no wager big enough to confirm. It stays put and goes quiet
   rather than appearing once the player has already done the thing it is for
   (the controls-never-disappear rule), and it is still clickable so a tap says
   why out loud - a tooltip alone is invisible to a finger. */
.tphase.pill-off { opacity: 0.45; }
.tphase.pill-off:hover { background: var(--panel-2); }
.tphase.stay-btn.pill-off:hover { background: rgba(61,154,87,0.18); }
/* the "bank your points and sit out" choice in a press-your-luck round */
.tphase.stay-btn { cursor: pointer; margin-left: 4px; border-color: #3d9a57; color: #a9e6b8; background: rgba(61,154,87,0.18); font-weight: 700; }
.tphase.stay-btn:hover { background: rgba(61,154,87,0.32); }
.tphase.stay-btn svg { width: 14px; height: 14px; }
/* the choice step's action pills - one per alternative the active player may
   take. Gold accent so they read as the live "do one of these" call to action,
   matching the glass toolbar's primary highlight. */
.tphase.choice-opt { cursor: pointer; margin-left: 4px; display: inline-flex; align-items: center; gap: 5px; border-color: var(--accent, #d9b25a); color: var(--accent, #d9b25a); background: rgba(217,178,90,0.14); font-weight: 700; }
.tphase.choice-opt:hover { background: rgba(217,178,90,0.28); }
.tphase.choice-opt svg { width: 14px; height: 14px; }
/* long-turn collapse: an inactive step shrinks to an icon-only pill (its name
   lives in the tooltip), and expands to full text when it's the firing step in
   play or the one clicked open in build. A fired step (.done) dims so the row
   still reads done / here / coming up. */
.tphase.mini { padding: 4px; gap: 0; }
.tphase.mini svg { width: 14px; height: 14px; display: block; }
.tphase.mini.tap { cursor: pointer; transition: border-color 0.12s, color 0.12s, opacity 0.12s; }
.tphase.mini.tap:hover { border-color: var(--accent); color: var(--accent-2); opacity: 1; }
.tphase.mini.done { opacity: 0.4; }
.tphase.mini.done:hover { opacity: 0.7; }
/* HOVER TO READ A COLLAPSED PILL: the name (and short badge) live in the DOM but
   are squeezed to nothing, then the pill widens smoothly to reveal them and its
   neighbours move aside - the icon-only row stays readable without a click, and
   the pills getting out of the way is the point of it rather than a side effect.

   IT IS OPENED BY A CLASS, NOT BY :hover, AND THAT IS LOAD-BEARING. Widening a
   pill in the flow re-centres the line it sits on (.tphase-bar.floating is
   justify-content:center inside max-width:calc(100% - 28px)) and, on a full row,
   rewraps it - so the pill can slide out from under a cursor that never moved.
   Tied to :hover that is a loop: hover drops, the pill collapses, the row springs
   back under the cursor, and it opens again several times a second, which is what
   Siem hit on "Players take their turns" in the Dealer row. Opened by a class
   that only a REAL pointer move can change (miniPill, 18-step-types.js), the pill
   simply stays open wherever the reflow put it, and there is nothing to oscillate.
   GENERALISE IT: a reveal that reflows the row may not be driven by the hover
   state of the thing it moves. */
.tphase.mini .tphase-mini-lbl {
  display: inline-flex; align-items: center; gap: 5px; margin-left: 0;
  max-width: 0; overflow: hidden; opacity: 0; white-space: nowrap;
  transition: max-width 0.18s ease, opacity 0.16s ease, margin-left 0.18s ease;
}
.tphase.mini.expandable.peek .tphase-mini-lbl { max-width: 240px; opacity: 1; margin-left: 5px; }
/* The gold ring came off :hover with it, for the same reason: a pill that has
   moved out from under the cursor must not look un-hovered either. */
.tphase.mini.expandable.peek { border-color: var(--accent); color: var(--accent-2); opacity: 1; }
.tphase.mini .tphase-mini-lbl .tphase-badge { margin-left: 0; }
/* Actor view: the round lane read as ONE participant's own sequence - the
   dealer, the house, or a named seat with its own script. The subject chip on
   the row head carries the same gold accent the other derived anchors wear
   (.tphase.resolve), because it names a standing fact sitting in a row of steps
   rather than a step itself. The muted chips are the round steps that are NOT
   the subject's, kept in place so the row shows the real sequence - they are
   drawn in the collapsed form every lane already uses, dimmed one notch further,
   and they come back to full opacity on hover so a row of them stays readable. */
/* THE ROW'S SUBJECT, as a chip. It used to wear .blk, so it was a 176x50 block
   standing beside a hand of 46x64 cards - the last of the old wide pills in any
   lane. It is not a step, so it does not become a card; it is the same object
   the AI lane draws as a plain name label beside its own hand, and it reads the
   same way now. The badge rides inline rather than on a line of its own. */
.tphase.actor-who {
  cursor: pointer; align-self: center; flex: none;
  display: inline-flex; align-items: center; gap: 5px; flex-wrap: nowrap;
  width: auto; height: auto; padding: 4px 9px; border-radius: 999px;
  font-size: 11px; font-weight: 700; line-height: 1.2;
  border: 1px solid rgba(216,162,74,0.55); background: rgba(216,162,74,0.10); color: var(--text);
  transition: background 0.12s, border-color 0.12s;
}
.tphase.actor-who > .tphase-badge { position: static; margin: 0; font-size: 9px; font-weight: 600; }
.tphase.actor-who:hover { background: rgba(216,162,74,0.18); border-color: var(--accent); }
.tphase.actor-who svg { width: 13px; height: 13px; }
/* The subject's own crest, so a row headed by a named seat reads as that
   character rather than as a word. Same size as the AI lane's chip crest. */
.tphase.actor-who .actor-crest { display: inline-flex; align-items: center; }
.tphase.actor-who .actor-crest svg { width: 14px; height: 14px; }
/* the seat that is the HOUSE - a chair that never takes a turn, whether it is
   dealing or being fought. The same cool slate the seat pill's House tag wears,
   so every surface that names one names it the same way. */
.tphase.actor-who.is-house { border-color: rgba(120,140,168,0.55); background: rgba(120,140,168,0.12); }
.tphase.actor-who.is-house:hover { background: rgba(120,140,168,0.2); border-color: rgba(168,188,214,0.8); }
.tphase.actor-who.is-house .tphase-badge { color: #a8bcd6; background: rgba(120,140,168,0.2); }
/* nobody chosen yet: the chip is still a control and still says what it is for,
   rather than going missing until the row already has a subject. */
.tphase.actor-who.is-none { border-style: dashed; border-color: rgba(255,255,255,0.22); background: transparent; }
.tphase.mini.actor-ctx { opacity: 0.5; border-style: dashed; }
.tphase.mini.actor-ctx:hover { opacity: 1; }
/* A ROUND STEP THIS ROW IS ONLY BORROWING. Same treatment as the AI lane's
   automatic step and for the same reason: it keeps its place and its size, and
   what says it is not the subject's is the face, not a smaller box. Dashed,
   because in this row it is a step standing in for a moment rather than a step
   the actor takes. It still opens on a click, so nothing here is a dead end. */
.tphase.stepcard.actor-ctx .tphase-stepcardface { opacity: 0.5; border-style: dashed; }
.tphase.stepcard.actor-ctx:hover .tphase-stepcardface { opacity: 1; }
/* the unique start-of-game node and the action badge on a phase */
.tphase.setup { border-color: var(--accent-2); }
.tphase.setup .ic svg, .tphase.setup svg { width: 13px; height: 13px; color: var(--accent-2); }
.tphase.setup > span:first-of-type, .tphase > span:first-of-type { cursor: pointer; }
.tphase-badge { font-size: 10px; font-weight: 700; letter-spacing: 0.3px; text-transform: uppercase; color: var(--accent-2); background: rgba(216,162,74,0.16); border-radius: 999px; padding: 1px 6px; }
.tphase-sep { display: inline-block; width: 1px; height: 20px; background: var(--line-2); margin: 0 3px; }
.tphase-hint { font-size: 12px; color: var(--text-faint); font-style: italic; }

/* ---------------------------------------------------------------------------
   THE STEP BLOCK - stage 0 of the lane overhaul (docs/Lanes en pills.docx).
   A lane pill stops being a capsule and becomes a BLOCK OF ONE FIXED SIZE:
   the mechanic's icon and the step's name on the first line, the badge on a
   line of its own under it, the remove control top-right. Siem picked this over
   a hybrid carrying the puzzle pieces' naad, having seen the two side by side:
   a naad glues the steps into one snake, and the LOOSENESS is what makes a lane
   readable.

   Three things about it that are not obvious from the declarations:

   - THE SIZE IS THE POINT, so the text yields to the block and never the other
     way round. Measured in a real browser at 164x50: about 15 characters of
     name and 23 of badge, with a wide spread (nine M's against twenty-eight
     i's), which is why the rename field stops on a MEASURED width rather than a
     counted number - the same rule fitSeatDialBadges follows on the wood.
   - NOTHING HERE TOUCHES THE PILL'S CHILDREN. Every lane builds its pill as the
     same flat run - icon, name, badge, remove - so the block is laid out by
     selecting on what each child IS (a span holding an svg is the icon, a span
     holding words is the name) rather than by restructuring fourteen call
     sites. :has() is already load-bearing here: Visual mode hides names with
     the same test one block down.
   - THE ICON IS ALWAYS DRAWN, where a capsule hid it until Visual mode. In a
     block it is part of the shape, not a stand-in for the words.

   `blk` is opt-in: it marks a BUILD-mode step or rule, never a play-time
   button (End turn, Stay, Reroll) and never a mini pill, which are pills in
   their own right and stay capsules. --------------------------------------- */
.tphase.blk {
  display: flex; flex-wrap: wrap; align-content: center; align-items: center;
  row-gap: 1px; column-gap: 6px;
  /* THE WIDTH IS DERIVED, NOT PICKED. Swept in a real browser against every
     name the step catalog itself produces (dev/step-block-harness.html): the

     widest common one is "Attacking rules" at 172px, so 176 clears the lot with
     two pixels to spare. The one name that overflowed it was the round lane's
     "Players take their turns" marker at 232px, and that is settled rather than
     excused: a name now has a ceiling of TCG.STEP_NAME_MAX letters, the marker's
     catalog pill was shortened to fit it, and its sentence moved to the badge.
     Re-run the sweep if a catalog name grows - and if one wants to, the ceiling
     is the thing to look at first. */
  width: var(--blk-w, 176px); height: var(--blk-h, 50px);
  padding: 6px 9px; border-radius: 10px; overflow: hidden;
}
/* A span holding an svg is the icon, whatever the lane called it. */
.tphase.blk > .tphase-ic,
.tphase.blk > span:has(svg) {
  display: inline-flex; align-items: center; justify-content: center;
  flex: none; order: 0; width: 18px; height: 18px; border-radius: 5px;
  background: rgba(255, 255, 255, 0.07); color: var(--accent-2);
}
.tphase.blk > .tphase-ic svg,
.tphase.blk > span:has(svg) svg { width: 12px; height: 12px; }
/* A span holding words is the name. */
.tphase.blk > span:not(:has(svg)):not(.tphase-badge) {
  /* basis ZERO, not auto: with `auto` a long name asks for its full content
     width first, which overflows the row and wraps the name onto a line of its
     own under the icon - the block quietly becomes three lines, and the harness
     was the only thing that noticed. With `0` it shrinks and ellipsises, which
     is what a block of one fixed size means. */
  flex: 1 1 0; min-width: 0; order: 1;
  font-size: 12.5px; font-weight: 700; color: var(--text); line-height: 1.15;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tphase.blk > .ph-x { flex: none; order: 2; }
/* flex-basis 100% is what puts the badge on its own line, and it is also what
   keeps a second badge off the first one's line rather than on top of it. */
.tphase.blk > .tphase-badge {
  flex: 1 0 100%; order: 3; background: none; padding: 0;
  color: var(--text-dim); font-size: 10.5px; font-weight: 600;
  text-transform: none; letter-spacing: 0.1px; line-height: 1.2;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tphase.blk:hover { border-color: var(--accent-2); }
/* THIS STEP WAITS ON A PERSON, which is the one thing the naad was going to say
   and the one thing a row of loose blocks cannot. Asked of the engine's own
   predicates (stepIsChoiceMoment / setupStepInteractive, through
   stepWaitsOnAPerson) rather than a list of step types kept here, so a step type
   added later answers for itself. A colour carries less than a shape - it has to
   be learned, and gold is already spent on "this is on" - so this is the weaker
   half of a deliberate trade, recorded as such. */
.tphase.blk.asks { border-color: rgba(92, 134, 180, 0.7); }
.tphase.blk.asks > .tphase-ic,
.tphase.blk.asks > span:has(svg) { color: #8fb6dd; background: rgba(92, 134, 180, 0.14); }
/* The adder is a button, not a step: it takes the block's height so the row has
   one baseline, and none of its width, icon tile or badge row. */
.tphase.add.blk { width: auto; flex-wrap: nowrap; justify-content: center; border-radius: 10px; }
.tphase.add.blk > span:has(svg) { background: none; width: auto; height: auto; border-radius: 0; color: inherit; }
.tphase.add.blk > span:has(svg) svg { width: 14px; height: 14px; }
.tphase.add.blk > span:not(:has(svg)) { flex: none; font-weight: 600; color: inherit; }

/* THE ADDER IS A BLANK CARD, the same one the home menu offers for a new game:
   a card-sized face with a plus in the middle and its word underneath. It used
   to be a wide rounded button standing beside a hand of 46x64 cards, which was
   the last thing in any lane still wearing the old shape.

   It STANDS UPRIGHT beside the hand rather than being dealt into it. A card in
   the arc reads as one of the game's own steps, and this one is a control - so
   it keeps the card's size and face and none of the fan's geometry, which is
   also why it has to override the absolute placement .tphase.stepcard sets up. */
.tphase.add.stepcard {
  position: relative; left: auto; bottom: auto; align-self: flex-end; flex: none;
  width: var(--stepcard-w, 46px); height: var(--stepcard-h, 64px);
  transform: none; padding: 0; overflow: visible;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0;
  border: 1px dashed var(--line-2); border-radius: 7px;
  background: linear-gradient(180deg, rgba(24,30,38,0.55), rgba(16,21,27,0.55));
  color: var(--text-faint);
  transition: border-color 160ms linear, color 160ms linear, transform 220ms cubic-bezier(0.4, 0, 0.2, 1);
}
.tphase.add.stepcard:hover { border-color: var(--accent); color: var(--accent-2); transform: translateY(-6px); }
/* The plus rides where every card's icon rides, and slides up by the same
   amount to make room for the word - the one move the whole hand shares. */
.tphase.add.stepcard > span:has(svg) {
  background: none; width: auto; height: auto; border-radius: 0; color: inherit;
  transform: translateY(0); transition: transform 220ms cubic-bezier(0.4, 0, 0.2, 1);
}
.tphase.add.stepcard:hover > span:has(svg) { transform: translateY(calc(-1 * var(--icup, 9px) / 2)); }
.tphase.add.stepcard > span:has(svg) svg { width: 18px; height: 18px; }
.tphase.add.stepcard > span:not(:has(svg)) {
  position: absolute; left: 2px; right: 2px; top: calc(50% + 6px); text-align: center;
  flex: none; font-size: 8.5px; font-weight: 700; line-height: 1.15; color: inherit;
  opacity: 0; transform: translateY(5px);
  transition: opacity 180ms linear, transform 220ms cubic-bezier(0.4, 0, 0.2, 1);
}
.tphase.add.stepcard:hover > span:not(:has(svg)) { opacity: 1; transform: translateY(0); }

/* ================= THE STEP CARD, AND THE LANE HAND =================
   Both words are deliberate and neither is the obvious one, because this
   codebase already spends both obvious ones elsewhere. A STEP CARD is a lane
   pill drawn as a playing card; a plain "card" is one of the GAME's cards. A
   LANE HAND is the row that holds them; a plain "hand" is what a player holds,
   and a "fan" is a zone layout on the felt. See CLAUDE.md, "A STEP CARD".
   A turn step stops being a block and becomes a small playing card; the lane
   holding them is a fanned hand. Picked by Siem over three other arrangements
   in dev/lane-card-sketch.html, with the face, the hover and the beat settled
   over dev/lane-card-face-sketch.html, -hover-, -live- and -beat-.

   Measured against the block it replaces, on a seven-step turn: the lane went
   from 1166px (which does not fit the felt and wrapped onto three rows) to
   about 260px on one row. That is the whole reason it exists - the bar had
   become a wall.

   FIVE THINGS HERE ARE LOAD-BEARING. Read them before changing any of it:

   1. THE PILL IS THE HIT AREA AND IT NEVER MOVES. Everything that moves -
      the hover lift, the beat, the parting - happens on the two wrappers
      INSIDE it (.tphase-stepcardin, .tphase-stepcardface), and both are
      pointer-events:none. A card that triggers its own hover lifts out from
      under the cursor, ends the hover, drops back and triggers again several
      times a second. The home menu states this where it does it
      (.menu-slot:hover .menu-card) and the mini pill's `peek` class is the
      same lesson a second time; this is the third.
   2. ONLY `transform` AND `opacity` MOVE. The lifted shadow and the gold ring
      are their own layer (::after) cross-fading on opacity, because a
      box-shadow that transitions repaints on every frame and Siem plays in
      Safari. No scale anywhere: a scale re-rasters the card, which is what
      the deal's 723ms freeze was traced to.
   3. THE SHIFT AND THE LIFT ARE ON DIFFERENT ELEMENTS. The beat is a keyframe
      animation that writes `transform` whole, so a parting written on the same
      element would be thrown away the moment a card beats. .tphase-stepcardin
      carries the sideways step, .tphase-stepcardface the vertical one.
   4. NOTHING IS HELD ABOVE THE HAND. The step you are on lies flush and wears
      the ring and its name; a raise is only ever something that HAPPENED. That
      keeps the lane exactly as tall as the arc, and it was Siem's own reversal
      of an earlier pick.
   5. THE NAME'S PLACE IS ARITHMETIC, NOT A MEASUREMENT. The icon sits half the
      words' height above centre and the name hangs under it, both from
      constants, so a lane of seven cards costs no forced layout reads per
      render. Pinning the name to the card's bottom edge - which an earlier
      draft did - puts it on the rim.

   The geometry (where each card sits on the arc) is written as custom
   properties by layoutFan() in 18-step-types.js; nothing here computes a
   position. ---------------------------------------------------------------- */
.tphase-lanehand { position: relative; flex: none; align-self: flex-end; }
.tphase.stepcard {
  position: absolute; bottom: 0; left: var(--fx, 0px);
  width: var(--stepcard-w, 46px); height: var(--stepcard-h, 64px);
  padding: 0; border: none; background: none; border-radius: 0; overflow: visible;
  transform: translateY(var(--fy, 0px)) rotate(var(--fr, 0deg));
  transform-origin: 50% 130%;
  cursor: pointer; display: block;
}
.tphase.stepcard:hover { z-index: 300 !important; }
/* `.tphase.stepcard >` AND NOT `.tphase-stepcardin` ALONE. `.tphase > *:not(.tphase-fill)`
   further up this file lifts every direct child of a pill above the dwell fill,
   and `:not()` carries its argument's weight - so that selector is 0-2-0 and a
   bare `.tphase-stepcardin` is 0-1-0 and loses. It was setting `position: relative`
   on the wrapper, `inset: 0` then means nothing, the height collapses to a
   content box holding only an absolutely-placed face, and every card in the
   lane rendered as a two-pixel line. Nothing errors and the declarations all
   read correctly; the only way to see it is to look at the page. Same cascade
   trap as .ce-val against the global input rule. */
.tphase.stepcard > .tphase-stepcardin {
  position: absolute; inset: 0; pointer-events: none;
  transform: translateX(var(--part, 0px));
  transition: transform 220ms cubic-bezier(0.4, 0, 0.2, 1);
}
.tphase-stepcardface {
  position: absolute; inset: 0; pointer-events: none; border-radius: 7px; overflow: hidden;
  background: linear-gradient(180deg, rgba(38,46,58,0.97), rgba(24,30,39,0.97));
  border: 1px solid rgba(255,255,255,0.13);
  box-shadow: -3px 2px 8px rgba(0,0,0,0.45);
  transform: translateY(0);
  transition: transform 220ms cubic-bezier(0.4, 0, 0.2, 1), border-color 160ms linear;
}
.tphase-stepcardface::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  box-shadow: 0 16px 30px rgba(0,0,0,0.55), 0 0 0 1px var(--accent-2);
  opacity: 0; transition: opacity 220ms cubic-bezier(0.4, 0, 0.2, 1);
}
.tphase.stepcard:hover .tphase-stepcardface { transform: translateY(-14px); }
.tphase.stepcard:hover .tphase-stepcardface::after { opacity: 1; }

/* The icon is the card's whole face at rest, so it is drawn at every moment -
   where a capsule hid it until Visual mode - and it is bare rather than sitting
   in a tile. A 12px glyph inside a 22px tile was most of why the icons read
   badly: the box competes with the stroke and leaves too little stroke to
   recognise. */
/* A SPAN HOLDING AN SVG IS THE ICON, whatever the lane called it - the same
   test the block uses one screen up, and the reason it has to be here too: the
   turn lanes build their icon as `.tphase-ic`, while the Global, End and Phase
   lanes use the shared spanIcon() helper, which carries no class at all. Asking
   only for `.tphase-ic` left every rule pill's icon sitting in the flow. */
.tphase.stepcard .tphase-ic,
.tphase.stepcard .tphase-stepcardface > span:has(svg) {
  display: flex; align-items: center; justify-content: center;
  position: absolute; left: 50%; top: 50%; width: 22px; height: 22px;
  color: var(--accent-2); background: none; border-radius: 0;
  transform: translate(-50%, -50%);
  transition: transform 220ms cubic-bezier(0.4, 0, 0.2, 1);
}
.tphase.stepcard .tphase-ic svg,
.tphase.stepcard .tphase-stepcardface > span:has(svg) svg { width: 100%; height: 100%; display: block; }
/* A span holding words is the name, the same test the block uses. It is absent
   until the card is hovered or live, and it rises the last few pixels in rather
   than switching on. */
.tphase.stepcard .tphase-stepcardface > span:not(:has(svg)):not(.tphase-badge) {
  position: absolute; left: 2px; right: 2px; top: calc(50% + 15px); text-align: center;
  font-size: 8.5px; font-weight: 700; line-height: 1.12; color: var(--text);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
  opacity: 0; transform: translateY(calc(-1 * var(--icup, 9px) + 5px));
  transition: opacity 220ms cubic-bezier(0.4, 0, 0.2, 1), transform 220ms cubic-bezier(0.4, 0, 0.2, 1);
}
.tphase.stepcard:hover .tphase-ic,
.tphase.stepcard:hover .tphase-stepcardface > span:has(svg),
.tphase.stepcard.cur .tphase-ic,
.tphase.stepcard.cur .tphase-stepcardface > span:has(svg) { transform: translate(-50%, -50%) translateY(calc(-1 * var(--icup, 9px))); }
.tphase.stepcard:hover .tphase-stepcardface > span:not(:has(svg)):not(.tphase-badge),
.tphase.stepcard.cur .tphase-stepcardface > span:not(:has(svg)):not(.tphase-badge) {
  opacity: 1; transform: translateY(calc(-1 * var(--icup, 9px)));
}
/* The badge is a tag floating clear of the card, not a line on it: measured,
   no badge the step catalog produces fits a 54px card while every name does. It
   is lifted OUT of the face by the fan pass so the face can go on clipping. */
.tphase.stepcard > .tphase-badge {
  position: absolute; bottom: 100%; left: 50%; margin: 0 0 7px;
  padding: 3px 7px; border-radius: 7px; white-space: nowrap; pointer-events: none;
  font-size: 9px; font-weight: 600; letter-spacing: 0; text-transform: none;
  color: var(--text); background: rgba(15,20,28,0.92);
  border: 1px solid rgba(255,255,255,0.12);
  opacity: 0; transform: translateX(-50%) translateY(5px);
  transition: opacity 220ms cubic-bezier(0.4, 0, 0.2, 1), transform 220ms cubic-bezier(0.4, 0, 0.2, 1);
}
.tphase.stepcard:hover > .tphase-badge { opacity: 1; transform: translateX(-50%) translateY(0); }
/* The remove control is the one thing inside the card that takes a press, so it
   is the one thing that turns pointer events back on. */
.tphase.stepcard .ph-x {
  position: absolute; top: 1px; right: 1px; pointer-events: auto;
  opacity: 0; transition: opacity 0.15s;
}
.tphase.stepcard:hover .ph-x { opacity: 1; }
.tphase.stepcard .ph-x svg { width: 10px; height: 10px; }
/* This step waits on a person - the same slate blue the block's border wears,
   here carried by the border AND the icon, which is more than a border alone
   could say. */
.tphase.stepcard.asks .tphase-stepcardface { border-color: rgba(92,134,180,0.75); }
.tphase.stepcard.asks .tphase-ic,
.tphase.stepcard.asks .tphase-stepcardface > span:has(svg) { color: #8fb6dd; }
/* The step you are on: flush in the hand, wearing the ring and its name. */
.tphase.stepcard.cur .tphase-stepcardface { border-color: var(--accent); background: linear-gradient(180deg, rgba(48,44,36,0.97), rgba(30,27,22,0.97)); }
.tphase.stepcard.cur .tphase-stepcardface::after { opacity: 1; box-shadow: 0 6px 14px rgba(0,0,0,0.4), 0 0 0 1px var(--accent); }
.tphase.stepcard.cur .tphase-ic,
.tphase.stepcard.cur .tphase-stepcardface > span:has(svg) { color: var(--accent); }
.tphase.stepcard.resolve .tphase-stepcardface { border-color: rgba(216,162,74,0.55); background: linear-gradient(180deg, rgba(48,44,36,0.95), rgba(28,26,22,0.97)); }
.tphase.stepcard.tphase-warn .tphase-stepcardface { border-color: rgba(224,163,58,0.7); }
.tphase.stepcard.skip .tphase-stepcardface { opacity: 0.5; }
.tphase.stepcard.done .tphase-stepcardface { opacity: 0.55; }

/* THE BEAT: a step starting in play rises once and settles back. An animation
   and not a transition, because it ends where it began - a transition between
   two values cannot describe up-and-back, and chaining two of them means a
   timer holding state a re-entry can corrupt. Re-armed in JS by removing the
   class, reading a layout property to flush it, and adding it again; without
   that read the browser coalesces both writes into one frame, sees no change,
   and the second beat never plays, which looks exactly like the walk having
   stopped. 26px and 900ms are Siem's own numbers, picked by playing a whole
   turn rather than one step. */
@keyframes tphaseBeat {
  0%   { transform: translateY(0); }
  50%  { transform: translateY(-26px); }
  100% { transform: translateY(0); }
}
.tphase.stepcard.beating .tphase-stepcardface { animation: tphaseBeat 900ms cubic-bezier(0.34, 0.9, 0.3, 1) both; }
/* The dwell fill still sweeps the card it is on; it rides the face so it moves
   with the beat rather than being left behind on the slot. */
.tphase.stepcard .tphase-fill { border-radius: 7px; }
@media (prefers-reduced-motion: reduce) {
  .tphase-stepcardin, .tphase-stepcardface, .tphase.stepcard .tphase-ic,
  .tphase.stepcard .tphase-stepcardface > span, .tphase.stepcard > .tphase-badge {
    transition-duration: 1ms !important;
  }
  .tphase.stepcard.beating .tphase-stepcardface { animation: none !important; }
}
/* Visual mode hides names everywhere else; on a card the name is already the
   thing that only appears on demand, and the icon is the card, so the mode's
   two rules must not reach in here and blank it. */
body.visual-mode .tphase.stepcard .tphase-ic,
body.visual-mode .tphase.stepcard .tphase-stepcardface > span:has(svg) { display: flex; }
body.visual-mode .tphase.stepcard .tphase-stepcardface > span:not(:has(svg)):not(.tphase-badge) { display: -webkit-box; }
/* THE DRAG GHOST IS A CLONE OF A CARD, and a card is absolutely placed on the
   arc - so the clone would inherit both its position and its slot's own
   rotation and fly at an offset from the cursor. It is put back on the ghost's
   own terms here, and it keeps the rest of the ghost's look. .tphase-ghost
   alone is one specificity point under .tphase.stepcard, which is the whole reason
   this block has to exist rather than the ghost's own rules winning. */
.tphase.stepcard.tphase-ghost {
  position: fixed; left: 0; top: 0; width: var(--stepcard-w, 54px); height: var(--stepcard-h, 76px);
  transform: rotate(-2deg); box-shadow: none;
}
.tphase.stepcard.tphase-ghost .tphase-stepcardface { transform: none; box-shadow: 0 10px 24px -8px rgba(0,0,0,0.6); border-color: var(--accent); }
.tphase.stepcard.tphase-ghost .tphase-stepcardin { transform: none; }
.tphase.stepcard.tphase-ghost .ph-x, .tphase.stepcard.tphase-ghost > .tphase-badge { display: none; }
/* the card's name is on while it is in the air, so you can see what you are moving */
.tphase.stepcard.tphase-ghost .tphase-stepcardface > span:not(:has(svg)):not(.tphase-badge) { opacity: 1; transform: translateY(calc(-1 * var(--icup, 9px))); }
.tphase.stepcard.tphase-ghost .tphase-ic,
.tphase.stepcard.tphase-ghost .tphase-stepcardface > span:has(svg) { transform: translate(-50%, -50%) translateY(calc(-1 * var(--icup, 9px))); }
/* A trigger is a PAIR of half-pills rather than one step, so its two halves take
   the block's shape and share its height without claiming its fixed width. */
.tphase.trg-cond.blk { width: auto; max-width: 260px; }
/* between-turns hand-off cover, so players don't see each other's hands */
/* The scrim is the one surface in the app that must NOT show what's behind it -
   it exists to conceal a hand - so it's near-opaque plus a blur, rather than the
   see-through glass every other floating surface wears. The card sitting on it
   is normal glass. */
.handoff { position: absolute; inset: 0; z-index: 130; display: flex; align-items: center; justify-content: center;
  background: rgba(6,9,14,0.985); backdrop-filter: blur(14px); animation: fadeIn 0.18s ease; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.handoff-card { display: flex; flex-direction: column; align-items: center; gap: 10px; text-align: center;
  padding: 24px 32px; border: 1px solid rgba(255,255,255,0.11); border-radius: 12px;
  background: rgba(15,20,28,0.72); backdrop-filter: blur(12px); box-shadow: var(--shadow); }
/* Type stays large here on purpose, against the compact default: this is read at
   arm's length across a table by someone who has just been handed the device,
   not a control panel inspected up close. */
.handoff-ic { width: 72px; height: 72px; display: flex; align-items: center; justify-content: center;
  border-radius: 50%; background: rgba(216,162,74,0.12); color: var(--accent-2); }
.handoff-ic svg { width: 44px; height: 44px; }
.handoff-sub { font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: var(--text-faint); font-weight: 700; }
.handoff-name { font-size: 26px; font-weight: 800; letter-spacing: 0.4px; }
.handoff .btn { margin-top: 6px; }

/* ---- AI seats ---- */
.seat.is-ai .seat-av { color: #7cc4ff; border-color: rgba(124,196,255,0.5); background: rgba(124,196,255,0.1); }
.seat-tag { font-size: 10px; font-weight: 800; letter-spacing: 0.6px; padding: 1px 6px; border-radius: 999px;
  background: rgba(124,196,255,0.16); color: #9fd2ff; border: 1px solid rgba(124,196,255,0.35); }
.seat.is-ai.active { box-shadow: 0 0 0 1px #5aa9e6 inset, 0 0 22px -8px rgba(124,196,255,0.7); border-color: #5aa9e6; }

/* ---- press-your-luck round status ---- */
.seat-score {
  font-size: 11px; font-weight: 800; font-variant-numeric: tabular-nums; padding: 1px 7px; border-radius: 999px;
  background: rgba(216,162,74,0.16); color: var(--accent-2); border: 1px solid rgba(216,162,74,0.35);
  white-space: nowrap;
}
/* The per-seat numbers collapse to nothing so the rail stays slim, and slide open
   only while that pill is hovered. max-width (not width) so it can animate, and a
   negative margin swallows the parent's 7px flex gap while closed so a collapsed
   stats block leaves no stray space. */
.seat-stats {
  display: flex; align-items: center; gap: 6px;
  max-width: 0; opacity: 0; overflow: hidden; margin-left: -7px; pointer-events: none;
  transition: max-width 0.3s ease, opacity 0.2s ease, margin-left 0.3s ease;
}
.seat:hover .seat-stats { max-width: 520px; opacity: 1; margin-left: 0; pointer-events: auto; }
/* Auto-unfold for a beat when this seat just earned (or spent) something, so the
   number is seen without hovering. Mirrors the hover-open, then the JS pulls the
   .revealed class and it folds back. A soft accent flash draws the eye to it. */
/* Opened by a TAP, for a finger that cannot hover. Same open state as the hover
   and the flash, so there is one look and one transition rather than three; what
   differs is only what puts the class on. Held by _seattapstats_test.js. */
.seat-stats.tapped-open { max-width: 520px; opacity: 1; margin-left: 0; pointer-events: auto; }
.seat-stats.revealed { max-width: 520px; opacity: 1; margin-left: 0; pointer-events: auto; }
.seat-stats.revealed .seat-score { animation: seatStatFlash 2.4s ease-out; }
@keyframes seatStatFlash {
  0% { box-shadow: 0 0 0 0 rgba(216,162,74,0.55); border-color: var(--accent); }
  35% { box-shadow: 0 0 0 3px rgba(216,162,74,0); border-color: var(--accent); }
  100% { box-shadow: 0 0 0 0 rgba(216,162,74,0); }
}
/* a small dot cue that a hidden readout is tucked in here, hidden once it opens */
.seat.has-stats > .seat-av::after {
  content: ""; position: absolute; right: -1px; bottom: -1px;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent-2); border: 1px solid var(--panel);
  transition: opacity 0.2s ease;
}
.seat.has-stats .seat-av { position: relative; }
.seat.has-stats:hover > .seat-av::after,
.seat.has-stats:has(> .seat-stats.tapped-open) > .seat-av::after { opacity: 0; }
/* :has() is the only way round here - the dot rides the crest, which is a
   SIBLING of the strip that opens, so no descendant selector can reach it. Safari
   has shipped it since 15.4 and it is the browser this is tested in; a browser
   without it simply keeps the dot showing next to an open strip, which is untidy
   rather than broken. */
/* still in the round and holding points that aren't banked yet - a slow pulse
   says "this number can still change" without needing extra text */
.seat-score.is-live { border-style: dashed; animation: seatScorePulse 1.6s ease-in-out infinite; }
/* The glyph a resource counter wears, struck inline in place of the resource's
   name so the badge matches the physical tokens that stand for it. Sized to the
   surrounding 11px text and nudged onto the baseline. */
.seat-res { display: inline-flex; align-items: center; }
/* One resource's count and glyph, boxed together so the value pulse can light
   up the single resource that moved instead of the whole readout. Purely a
   handle - it inherits the row it replaced, so the readout reads identically. */
.seat-res-one { display: inline-flex; align-items: center; }
.seat-res-ico { display: inline-flex; align-items: center; }
.seat-res-ico svg { width: 12px; height: 12px; }
@keyframes seatScorePulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(216,162,74,0.45); }
  50% { box-shadow: 0 0 0 4px rgba(216,162,74,0); }
}
.seat-tag-bad { background: rgba(198,74,74,0.16); color: #e28080; border: 1px solid rgba(198,74,74,0.4); }
.seat-tag-good { background: rgba(90,168,110,0.18); color: #8fd6a4; border: 1px solid rgba(90,168,110,0.4); }
/* THE HOUSE: a seat at the table that is not a player. It keeps its crest, its
   name and its numbers - it holds a bank and you play against it - so it is not
   dimmed like an eliminated seat. What sets it apart is a cool slate tag and rim
   against the warm gold every ACTIVE state uses, so it reads at a glance as the
   one pill that is never whose turn it is. */
.seat-tag-house { background: rgba(120,140,168,0.18); color: #a8bcd6; border: 1px solid rgba(120,140,168,0.45); }
.seat.is-house { border-color: rgba(120,140,168,0.45); }
.seat.is-house .seat-av { cursor: default; }
.sdial.is-house .sdial-tag, .sdial-tag.is-house { border-color: rgba(120,140,168,0.5); }
/* the seat that acts next: a soft accent outline on the pill plus a "next" tag */
.seat.is-next { border-color: rgba(216,162,74,0.55); color: var(--text); }
.seat-tag-next { background: rgba(216,162,74,0.16); color: var(--accent-2); border: 1px solid rgba(216,162,74,0.45); }
.seat.is-out { opacity: 0.55; }
.seat.is-stayed { opacity: 0.8; }

/* the "AI is playing" banner, floated over the felt during its turn */
.ai-turn { position: absolute; top: 62px; left: 50%; transform: translateX(-50%); z-index: 60;
  display: flex; align-items: center; gap: 12px; padding: 11px 18px 11px 12px;
  border-radius: 14px; border: 1px solid rgba(124,196,255,0.4);
  background: linear-gradient(180deg, rgba(18,28,40,0.97), rgba(11,17,26,0.97));
  box-shadow: 0 12px 34px -12px rgba(0,0,0,0.8), 0 0 26px -14px rgba(124,196,255,0.8);
  animation: aiPop 0.24s ease; }
@keyframes aiPop { from { opacity: 0; transform: translate(-50%, -8px); } to { opacity: 1; transform: translate(-50%, 0); } }
.ai-turn-ic { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;
  border-radius: 11px; background: rgba(124,196,255,0.14); color: #8fcaff; border: 1px solid rgba(124,196,255,0.3);
  animation: aiThink 1.6s ease-in-out infinite; }
.ai-turn-ic svg { width: 23px; height: 23px; }
@keyframes aiThink { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
.ai-turn-name { font-weight: 800; font-size: 15px; color: var(--text); }
.ai-turn-sub { font-size: 12.5px; color: #9fd2ff; margin-top: 1px; }

/* The first screen while a LAB builds its table (?lab=1 / ?atklab=1). The felt
   of the menu without the menu on it, so the page opens on the surface it is
   going to stay on instead of flashing a main menu nobody asked for. */
.view.lab-boot { display: flex; align-items: center; justify-content: center; }
.lab-boot-line { font-size: 14px; letter-spacing: 0.02em; color: rgba(255,255,255,0.5); }

/* THE IDLE-TURN BRAKE's banner: the table has gone around twice with nobody
   asked anything and has stopped to say so. Centred on the felt rather than
   perched at the top like the AI banner, because it is a stop and not a status,
   and it catches clicks (the AI banner does not) - it carries the control that
   ends it. Amber, because nothing is wrong: the table is waiting, not broken. */
.idle-brake { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 70;
  width: min(460px, 78%); padding: 18px 20px 16px; border-radius: 16px;
  border: 1px solid rgba(240,190,110,0.45);
  background: linear-gradient(180deg, rgba(26,24,18,0.98), rgba(14,13,10,0.98));
  box-shadow: 0 18px 46px -14px rgba(0,0,0,0.85), 0 0 30px -16px rgba(240,190,110,0.8);
  animation: aiPop 0.24s ease; }
.idle-brake-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.idle-brake-head strong { font-size: 16px; color: var(--text); }
.idle-brake-ic { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
  border-radius: 9px; background: rgba(240,190,110,0.15); color: #f0be6e; border: 1px solid rgba(240,190,110,0.32); }
.idle-brake-ic svg { width: 18px; height: 18px; }
.idle-brake-line { font-size: 13.5px; color: #e7dcc7; line-height: 1.45; }
.idle-brake-fix { font-size: 12.5px; color: #b9a98c; line-height: 1.45; margin-top: 8px; }
.idle-brake-acts { display: flex; gap: 10px; margin-top: 14px; }
.idle-brake-acts button { font: inherit; font-weight: 700; font-size: 13.5px; cursor: pointer;
  padding: 9px 16px; border-radius: 10px; border: 1px solid transparent; }
.idle-brake-go { background: #f0be6e; border-color: #f0be6e !important; color: #201a10; }
.idle-brake-go:hover { background: #f7cd88; }
.idle-brake-stop { background: rgba(255,255,255,0.06); border-color: rgba(255,255,255,0.18) !important; color: #e7dcc7; }
.idle-brake-stop:hover { background: rgba(255,255,255,0.12); }

/* the colour a Wild picked, shown as a dot on the played card so the pile reads
   clearly, and spelled out when the card is inspected */
.tpiece-card { position: relative; }
.chosen-color {
  position: absolute; top: 6%; right: 6%; width: 26%; aspect-ratio: 1; border-radius: 50%;
  border: 2px solid #fff; box-shadow: 0 2px 7px rgba(0,0,0,0.55), inset 0 0 0 1px rgba(0,0,0,0.3);
  z-index: 6; pointer-events: none; animation: chosenPop 0.28s ease backwards;
}
@keyframes chosenPop { 0% { transform: scale(0); } 70% { transform: scale(1.18); } 100% { transform: scale(1); } }
.tpiece-card.face-down .chosen-color { display: none; }
.chosen-note { display: flex; align-items: center; gap: 9px; justify-content: center; margin-top: 14px;
  font-size: 14px; font-weight: 700; color: var(--text); }
.chosen-note-sw { width: 18px; height: 18px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 1px 4px rgba(0,0,0,0.5); }

/* winner banner: shown over the felt when a win rule fires */
.win-screen { position: absolute; inset: 0; z-index: 140; display: flex; align-items: center; justify-content: center;
  background: rgba(6,9,14,0.4); backdrop-filter: blur(1.5px); animation: fadeIn 0.2s ease;
  /* let the felt behind stay inspectable - only the card itself catches clicks */
  pointer-events: none; }
.win-card { position: relative; display: flex; flex-direction: column; align-items: center; gap: 12px; text-align: center;
  padding: 34px 52px 44px; border: 1px solid var(--line-2); border-top: 3px solid var(--accent);
  border-radius: 20px;
  background: linear-gradient(180deg, var(--panel), var(--bg-2)); box-shadow: var(--shadow);
  animation: winSnap 0.42s cubic-bezier(0.18,0.9,0.24,1.15) both; pointer-events: auto; }
@keyframes winSnap {
  0% { transform: scale(0.86); opacity: 0; }
  60% { transform: scale(1.02); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
/* the celebratory headline: bold gold, snaps in a touch after the card lands */
.win-head { font-size: 15px; font-weight: 800; text-transform: uppercase; letter-spacing: 4px;
  background: linear-gradient(180deg, var(--accent-2), var(--accent)); -webkit-background-clip: text;
  background-clip: text; color: transparent; margin-bottom: -2px;
  animation: winHeadIn 0.4s 0.12s cubic-bezier(0.18,0.9,0.24,1.15) both; }
@keyframes winHeadIn { from { transform: translateY(6px) scale(0.9); opacity: 0; } to { transform: none; opacity: 1; } }
.win-bar { display: flex; align-items: center; justify-content: flex-end; gap: 4px; width: 100%; margin: -22px 0 -6px; }
.win-ctl { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; padding: 0;
  border: 1px solid var(--line-2); border-radius: 8px; background: var(--bg-2); color: var(--text-faint); cursor: pointer; }
.win-ctl:hover { color: var(--text); border-color: var(--line-1); }
.win-ctl svg { width: 15px; height: 15px; transition: transform 0.15s ease; }
.win-ctl.is-expand svg { transform: rotate(-90deg); }
.win-card.minimized { flex-direction: row; align-items: center; gap: 12px; padding: 12px 16px; }
.win-card.minimized .win-bar { width: auto; margin: 0; order: 2; }
.win-card.minimized .win-ic, .win-card.minimized .win-sub, .win-card.minimized .win-head,
.win-card.minimized .limit-form, .win-card.minimized .win-actions { display: none; }
.win-card.minimized .win-name { font-size: 18px; order: 1; }
/* Minimised: drop the veil entirely so the whole table is clear to read, and
   park the pill in one standard spot (bottom-centre, clear of the left rail). */
.win-screen.minimized { background: transparent; backdrop-filter: none; }
.win-screen.minimized .win-card { position: absolute; left: 50%; bottom: 18px; top: auto;
  transform: translateX(-50%); margin: 0; }
.win-ic { width: 72px; height: 72px; display: flex; align-items: center; justify-content: center;
  border-radius: 50%; background: rgba(216,162,74,0.16); color: var(--accent-2); }
.win-ic svg { width: 38px; height: 38px; }
.win-sub { font-size: 12px; text-transform: uppercase; letter-spacing: 2px; color: var(--text-faint); font-weight: 700; }
.win-name { font-size: 34px; font-weight: 800; letter-spacing: 0.4px;
  text-shadow: 0 0 22px rgba(216,162,74,0.4);
  animation: winNameIn 0.4s 0.2s cubic-bezier(0.18,0.9,0.24,1.15) both; }
@keyframes winNameIn { from { transform: scale(0.82); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.win-actions { display: flex; gap: 10px; margin-top: 10px; }

/* over-felt announcement (the "Game on!" start flourish). Never catches clicks,
   snaps in bold then fades itself out over ~1.9s. Punchy but restrained: one
   quick scale-in, a hold, a clean fade - no confetti.

   It is a child of the STAGE (so it never pans or zooms with the camera), but it
   is boxed to the FELT, using the same four insets .felt-viewport reads - the
   MEASURED --rail-top / --rail-bot from syncRailInsets, and the same side insets.
   inset: 0 centred it on the stage instead, which includes the seat rail above
   and the phase bar below; those two bands are rarely the same height (and wrap
   independently at six seats), so "centred" landed off the cloth's own middle by
   whatever the difference happened to be that frame. Every announcement lands in
   the middle of the TABLE, which is the one part of the felt that is reliably
   clear of a hand, a play area or a seat's own zones. */
.felt-flash { position: absolute; top: var(--rail-top, 84px); left: calc(4px + var(--rail-t, 24px)); right: calc(4px + var(--rail-t, 24px)); bottom: var(--rail-bot, 84px);
  z-index: 138; display: flex; align-items: center; justify-content: center;
  pointer-events: none; }
.felt-flash-text { font-size: 68px; font-weight: 900; letter-spacing: 1px; text-align: center;
  background: linear-gradient(180deg, var(--accent-2), var(--accent)); -webkit-background-clip: text;
  background-clip: text; color: transparent;
  filter: drop-shadow(0 6px 22px rgba(0,0,0,0.55)) drop-shadow(0 0 18px rgba(216,162,74,0.35));
  animation: feltFlashLife 1.9s cubic-bezier(0.18,0.9,0.24,1.15) both; }
@keyframes feltFlashLife {
  0% { transform: scale(0.5); opacity: 0; }
  12% { transform: scale(1.08); opacity: 1; }
  20% { transform: scale(1); opacity: 1; }
  72% { transform: scale(1); opacity: 1; }
  100% { transform: scale(1.14); opacity: 0; }
}
/* the lighter, quicker variant - a per-turn "whose turn" cue, smaller and
   shorter than the big game-start flourish so it never dominates the felt. It
   sits in the same place as the big one: SIZE and DURATION are what make it the
   lighter cue, not a second position. It used to sit 12% down from the top,
   which on a full table is the far seat's own row of zones. */
.felt-flash.small .felt-flash-text { font-size: 40px; animation-duration: 1.2s; }

/* the coin-flip setup step's flourish: the real d2 die (a chunky round coin)
   tossed in the middle of the felt, drawn by the tool's own die renderer into
   this canvas and tumbled per-frame. Never catches clicks. */
.setup-coin { position: absolute; inset: 0; z-index: 139; display: flex; align-items: center; justify-content: center; pointer-events: none; }
.setup-coin-canvas { width: 108px; height: 108px; transform-origin: center; will-change: transform, opacity; filter: drop-shadow(0 14px 32px rgba(0,0,0,0.55)); }

/* win-condition picker inside the Game rules modal */
.win-list { display: flex; flex-direction: column; gap: 8px; }
.win-opt { display: flex; align-items: flex-start; gap: 10px; text-align: left; width: 100%;
  padding: 11px 13px; border: 1px solid var(--line); border-radius: 12px; background: var(--bg-2);
  cursor: pointer; transition: border-color 0.12s, background 0.12s; }
.win-opt:hover { border-color: var(--line-2); }
.win-opt.on { border-color: var(--accent); background: rgba(216,162,74,0.08); }
.win-tick { width: 18px; height: 18px; flex: none; margin-top: 2px; display: flex; align-items: center; justify-content: center;
  border-radius: 50%; border: 1.5px solid var(--line-2); color: var(--accent-2); }
.win-opt.on .win-tick { border-color: var(--accent); }
.win-tick svg { width: 12px; height: 12px; }
/* Picker choices show only their bold title until hovered/selected, then reveal the sub. */
.win-opt .limit-sub { display: block; max-height: 0; opacity: 0; overflow: hidden; margin-top: 0;
  transition: max-height 0.16s ease, opacity 0.16s ease, margin-top 0.16s ease; }
.win-opt:hover .limit-sub, .win-opt:focus-visible .limit-sub, .win-opt.on .limit-sub {
  max-height: 80px; opacity: 1; margin-top: 3px; }

/* new-game template chooser rows: a bigger accent icon instead of the tick */
.tmpl-opt { align-items: center; }
.tmpl-ic { width: 34px; height: 34px; flex: none; display: flex; align-items: center; justify-content: center;
  border-radius: 9px; background: rgba(216,162,74,0.12); color: var(--accent-2); }
.tmpl-ic svg { width: 19px; height: 19px; }

/* card tray overlay: pick a card to drop on the table, or make a new one */
.tray-back { position: fixed; inset: 0; z-index: 280; background: rgba(6,9,14,0.72); backdrop-filter: blur(3px); display: flex; align-items: flex-end; justify-content: center; }
.tray {
  width: 100%; max-width: 1180px; max-height: 74vh; background: linear-gradient(180deg, var(--panel), var(--bg-2));
  border: 1px solid var(--line-2); border-bottom: none; border-radius: 18px 18px 0 0;
  box-shadow: var(--shadow); display: flex; flex-direction: column; animation: trayUp 0.25s cubic-bezier(0.2,0.9,0.3,1);
}
@keyframes trayUp { from { transform: translateY(40px); opacity: 0.6; } to { transform: none; opacity: 1; } }
.tray-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 16px 20px; border-bottom: 1px solid var(--line); }
.tray-head h3 { margin: 0; font-size: 18px; }
.tray-head p { margin: 3px 0 0; color: var(--text-dim); font-size: 13px; }
.tray-body { padding: 18px 20px; overflow: auto; }
.tray-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 16px; }
.tray-cardwrap { display: flex; flex-direction: column; align-items: center; gap: 7px; cursor: pointer; }
.tray-cardwrap .tcard { width: 132px; transition: transform 0.12s; }
.tray-cardwrap:hover .tcard { transform: translateY(-6px) scale(1.03); }
.tray-cardwrap .tray-add-lbl { font-size: 12px; color: var(--text-dim); display: inline-flex; align-items: center; gap: 5px; }
.tray-cardwrap:hover .tray-add-lbl { color: var(--accent-2); }
.tray-card-box { position: relative; display: inline-flex; }
.tray-cardwrap.sel .tcard { outline: 2px solid var(--accent-2); outline-offset: 2px; border-radius: 10px; }
.tray-card-del {
  position: absolute; top: 6px; right: 6px; width: 26px; height: 26px; border-radius: 8px;
  border: 1px solid var(--line-2); background: rgba(20, 24, 32, 0.82); color: var(--text-dim);
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  opacity: 0; transition: opacity 0.12s, color 0.12s, border-color 0.12s; z-index: 2;
}
.tray-card-del svg { width: 14px; height: 14px; }
.tray-cardwrap:hover .tray-card-del { opacity: 1; }
.tray-card-del:hover { color: var(--red); border-color: var(--red); }
.tray-card-chk {
  position: absolute; top: 6px; left: 6px; width: 24px; height: 24px; border-radius: 50%;
  border: 1px solid var(--line-2); background: rgba(20, 24, 32, 0.82); color: transparent; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: color 0.12s, background 0.12s, border-color 0.12s; z-index: 2;
}
.tray-card-chk svg { width: 14px; height: 14px; }
.tray-cardwrap:hover .tray-card-chk { color: var(--text-faint); }
.tray-cardwrap.sel .tray-card-chk { background: var(--accent-2); color: #221703; border-color: var(--accent-2); }
.tray-foot { display: flex; align-items: center; gap: 10px; padding: 12px 20px; border-top: 1px solid var(--line); }
.tray-selinfo { font-size: 12.5px; color: var(--text-faint); margin-right: auto; }
/* See the Future / Alter the Future: the next few cards off a deck, in draw
   order, each row optionally carrying reorder buttons (Alter the Future). */
.future-list { display: flex; flex-direction: column; gap: 10px; }
.future-row { display: flex; align-items: center; gap: 14px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 12px; background: var(--bg-2); }
.future-pos { width: 44px; flex: none; font-size: 12px; font-weight: 600; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.03em; }
.future-card-wrap .tcard { width: 96px; }
.future-ctrl { display: flex; flex-direction: column; gap: 6px; margin-left: auto; }
.future-ctrl .tool-btn:disabled { opacity: 0.3; cursor: default; }
/* Reveal: the one card being shown to the whole table, centred and a little
   larger than a tray thumbnail since everyone is reading it at once. Its picker
   marks the cards already chosen so a multi-card reveal shows its own progress. */
.reveal-shown { display: flex; justify-content: center; padding: 4px 0 2px; }
.reveal-shown .tcard { width: 168px; }
.tray-cardwrap.picked .tcard { outline: 2px solid var(--accent-2); outline-offset: 2px; border-radius: 10px; opacity: 0.75; }
/* Trick-result pop-up: the two played cards, winner outlined, above the
   plain-English result lines. Siem asked for the actual cards to show up
   here rather than just naming them in text. */
.trick-result-body { display: flex; flex-direction: column; gap: 18px; }
.trick-result-cards { display: flex; justify-content: center; gap: 26px; }
.trick-result-card { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.trick-result-card .tcard { width: 128px; border-radius: 10px; transition: transform 0.15s, box-shadow 0.15s; }
.trick-result-card.is-winner .tcard { outline: 3px solid var(--green); outline-offset: 3px; box-shadow: 0 0 18px rgba(76, 174, 122, 0.5); transform: translateY(-4px); }
.trick-result-seat { font-size: 13px; color: var(--text-dim); display: inline-flex; align-items: center; gap: 5px; }
.trick-result-seat .ic { width: 14px; height: 14px; }
.trick-result-card.is-winner .trick-result-seat { color: var(--green); font-weight: 600; }
.trick-result-text p { margin: 0 0 8px; }
.trick-result-text p:last-child { margin-bottom: 0; }
/* A RUN's result: one group per position, each holding only the cards that
   actually met. The comparison is what's being explained, so the pairs are the
   layout rather than a caption under one long row. The groups scroll sideways
   inside their own box on a narrow screen so the pop-up never widens the page. */
.trick-result-pairs { display: flex; justify-content: center; gap: 20px; overflow-x: auto; padding-bottom: 4px; }
.trick-result-pair { display: flex; flex-direction: column; align-items: center; gap: 8px; flex: 0 0 auto; padding: 10px 12px; border-radius: 12px; background: rgba(255, 255, 255, 0.04); border: 1px solid rgba(255, 255, 255, 0.08); }
.trick-result-pairhead { font-size: 11px; text-transform: uppercase; letter-spacing: 0.7px; color: var(--text-dim); }
.trick-result-pair .trick-result-cards { gap: 14px; }
.trick-result-pair .trick-result-card .tcard { width: 96px; }
.trick-result-pts { font-weight: 600; color: var(--accent); }

/* ---- the showdown stage (js/showdown.js) ----
   A comparison played out big: the felt dims, the cards come up at a readable
   size one at a time, then the verdict lands. The whole thing is the app's own
   glass, just larger, so it reads as the tool leaning in rather than a second
   surface bolted on.

   EVERY reveal here is opacity + translate and nothing else. A card face is an
   uploaded picture drawn into a box, and Safari re-rasters a layer whenever its
   scale changes, which is the whole story behind the deal jank (CLAUDE.md, "A
   card in motion moves and fades"). So no scale, no translateZ, no filter on
   anything that moves - and the winner's ring is its OWN element fading in over
   the card rather than a box-shadow transition on the card itself, which would
   repaint the picture underneath it. */
/* The scene sits ON the felt. There is no panel and no fill behind the cards -
   the felt only dims, so the game is still visibly there underneath and the
   comparison reads as a moment in the match rather than a window over it. Which
   means every word here is on an unknown background, so the type carries its own
   shadow instead of leaning on a box. */
.sdw-scrim {
  position: fixed; inset: 0; z-index: 190; display: flex; align-items: center; justify-content: center;
  background: rgba(6, 9, 14, 0.52); backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);
  animation: fadeIn 0.18s ease; padding: 14px;
}
.sdw-panel {
  --sdw-w: 150px;
  display: flex; flex-direction: column; align-items: center; gap: 13px;
  max-width: min(1100px, 100%); max-height: 100%; overflow-y: auto;
}
/* the chrome carries the shadow, NEVER `.sdw-panel *` - that would reach inside
   the card faces and restyle every card's own type on this one screen */
.sdw-title, .sdw-round-label, .sdw-vs, .sdw-who, .sdw-val, .sdw-mark, .sdw-pts, .sdw-verdict, .sdw-recap, .sdw-notes {
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.65);
}
.sdw-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.1px; color: var(--text-dim); }
.sdw-body { display: flex; flex-direction: column; gap: 16px; width: 100%; }
.sdw-round { display: flex; flex-direction: column; align-items: center; gap: 9px; }
/* A run is several comparisons and they are played ONE AT A TIME, so a pair that
   has had its moment leaves the layout rather than merely going transparent - a
   hidden round that still owns its own height is what pushed the pair being
   watched off the top of the screen. `display: none` is deliberate over
   `visibility`/`position: absolute`: the panel is centred on the scrim and has
   to be exactly as tall as the one pair showing. */
.sdw-round.sdw-off { display: none; }
.sdw-round-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.7px; color: var(--text-dim); }
/* nowrap plus its own sideways scroll: a six-card comparison stays one row a
   player can read across, rather than wrapping into a grid that stops looking
   like a line-up. */
/* The padding is what lets the winner's ring exist. `overflow-x: auto` with no
   overflow-y written computes overflow-y to `auto` as well, so this box CLIPS
   both axes - and a scroll container clips at its inline-START and block-START
   edges while letting content overflow at the end edges into scrollable area.
   The ring sits at `inset: -6px` with a 22px glow, so its top and left were cut
   off and only its right side survived: a winner marked on two sides out of
   four, which is exactly what a highlight that "doesn't quite work" looks like.
   Measured, not guessed - scrollWidth 458 against clientWidth 452 on a row whose
   box started at the first card's own left edge. The vertical margin claws the
   rhythm back; the horizontal one is deliberately left alone, since a negative
   inline margin here would push the row wider than the panel holding it. */
.sdw-cards {
  display: flex; align-items: flex-start; justify-content: center; gap: 12px; flex-wrap: nowrap;
  max-width: 100%; overflow-x: auto;
  padding: 48px 24px 30px; margin: -22px 0 -26px;
}
.sdw-card { position: relative; display: flex; flex-direction: column; align-items: center; gap: 5px; flex: 0 0 auto; }
/* The face is given the card's width EXPLICITLY, and the card fills it. It is
   the ring's containing block, so any difference between the two boxes shows up
   as a ring that hugs one edge and floats off the opposite one - which is what
   shipped first, when the face was left to shrink-wrap and came out wider than
   the card it held. `font-size: 0` kills the inline baseline gap under the card,
   the same difference read vertically. */
.sdw-face { position: relative; display: block; width: var(--sdw-w); font-size: 0; }
/* the card keeps its OWN corner shape (a percentage, so it holds at any size) -
   the ring just follows it outwards by its own offset.
   NEVER write a `display` here. `.tcard` is `display: flex; flex-direction:
   column` and `.tcard-inner` is `flex: 1`, so forcing the card to `block`
   leaves the inner canvas at ZERO height - and since every free-form element is
   positioned at `top: X%` of that canvas, all of them collapse onto the top
   edge and the artwork window loses its height. The card then draws as a blank
   frame with its name and badges piled in a strip at the top, which is what
   shipped and is most of why this screen read as unreadable. Measured: inner
   height 0 against a 294px card, 294 with the `display` gone. The inline
   baseline gap that line was there to kill is already handled by `font-size: 0`
   on `.sdw-face`. */
.sdw-face .tcard { width: 100%; }
.sdw-ring {
  position: absolute; inset: -6px; border-radius: calc(var(--sdw-w) * 0.067 + 6px);
  border: 2px solid var(--accent);
  box-shadow: 0 0 22px rgba(216, 162, 74, 0.4); opacity: 0; transition: opacity 0.22s ease; pointer-events: none;
}
/* level with the middle of the CARD, not the middle of the whole column - the
   column also holds the name, the mark and the points, so centring against it
   would drop the "v" well below the cards it sits between */
.sdw-vs { align-self: flex-start; margin-top: calc(var(--sdw-w) * 0.7 - 0.5em); font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: var(--text-dim); }
.sdw-who { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--text-dim); max-width: var(--sdw-w); }
.sdw-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sdw-crest { width: 14px; height: 14px; flex: 0 0 auto; display: inline-flex; }
.sdw-crest svg { width: 100%; height: 100%; }
.sdw-val { font-size: 21px; font-weight: 800; line-height: 1; letter-spacing: -0.4px; transition: color 0.26s ease; }
.sdw-card.sdw-judged[data-result="win"] .sdw-val { color: var(--accent-2); }
/* The halo behind the winner. Its own element, BEHIND the card - a glow painted
   over a card face washes the picture out, which reads as the artwork having
   gone missing rather than as the card being lit. */
.sdw-halo {
  position: absolute; inset: -34px; border-radius: 40px; opacity: 0; pointer-events: none;
  background: radial-gradient(closest-side, rgba(255,240,205,0.42), rgba(232,180,90,0.16) 55%, rgba(232,180,90,0) 76%);
  transition: opacity 0.5s ease;
}
.sdw-card.sdw-judged[data-result="win"] .sdw-halo { opacity: 1; }
/* the card sits ON the halo. NEVER write a `display` on it - see the note on
   `.sdw-face .tcard` above. */
.sdw-face .tcard { position: relative; z-index: 1; }
.sdw-ring, .sdw-mk, .sdw-flash { z-index: 2; }
/* The pill that concludes a comparison. It names the WINNER and it lands at the
   END: an earlier version marked whoever was ahead as each card arrived, which
   told the answer before the last card had been turned over.
   It hangs ABOVE the card rather than sitting in the column, because only the
   winner has one and a row in the flow would push that one card down against
   its neighbours. */
.sdw-crown {
  position: absolute; top: -30px; left: 50%; z-index: 3;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 11px 5px 8px; border-radius: 999px; white-space: nowrap;
  background: linear-gradient(180deg, var(--accent), var(--accent-2)); color: #201503;
  font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.9px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5), 0 0 22px rgba(232, 180, 90, 0.35);
  transform: translate(-50%, 0);
  transition: opacity 0.3s ease, transform 0.34s cubic-bezier(.2, .9, .3, 1);
}
.sdw-crown.sdw-pending { transform: translate(-50%, -18px); }
.sdw-crown-ic { width: 13px; height: 13px; display: inline-flex; }
.sdw-crown-ic svg { width: 100%; height: 100%; }
.sdw-mark { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 600; color: var(--text-dim); opacity: 0; transition: opacity 0.22s ease; }
.sdw-card[data-result="win"] .sdw-mark { color: var(--accent-2); }
.sdw-mark-ic { width: 12px; height: 12px; display: inline-flex; }
.sdw-mark-ic svg { width: 100%; height: 100%; }
.sdw-pts { font-size: 13px; font-weight: 700; color: var(--accent); opacity: 0; transform: translateY(7px); transition: opacity 0.3s ease, transform 0.3s ease; }
.sdw-pts.neg { color: var(--red); }
.sdw-verdict { font-size: 13px; font-weight: 600; text-align: center; max-width: 520px; }
/* The recap of the pairs that have already left the screen. A COLUMN, not a row
   of chips: a round count runs from two to a full table, and a name is as long
   as a player made it, so a row is one that wraps into a grid at the exact
   moment the scene is trying to be read. The label sits in a column of its own
   so the sentences line up under each other. Deliberately at note scale and in
   the dim colour - it is the account of what has been, and it must not compete
   with the pair still on screen above it. */
.sdw-recap { display: flex; flex-direction: column; gap: 3px; align-items: center; width: 100%; }
.sdw-recap-row { display: flex; align-items: center; gap: 7px; max-width: 100%; }
.sdw-recap-lab {
  flex: 0 0 auto; min-width: 52px; text-align: right;
  font-size: 10px; text-transform: uppercase; letter-spacing: 0.7px; color: var(--text-dim); opacity: 0.75;
}
.sdw-recap-crest { width: 13px; height: 13px; flex: 0 0 auto; display: inline-flex; }
.sdw-recap-crest svg { width: 100%; height: 100%; }
.sdw-recap-said { font-size: 11.5px; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sdw-notes { width: 100%; font-size: 11.5px; color: var(--text-dim); text-align: center; }
.sdw-notes p { margin: 0 0 4px; }
.sdw-notes p:last-child { margin-bottom: 0; }
/* centred, not right-aligned: with no panel there is no edge to align to */
.sdw-foot { display: flex; justify-content: center; width: 100%; }
.sdw-go {
  padding: 6px 14px; border-radius: 999px; border: 1px solid rgba(255, 255, 255, 0.12);
  background: linear-gradient(180deg, var(--accent), var(--accent-2)); color: #201503;
  font-size: 11.5px; font-weight: 700; cursor: pointer;
}
.sdw-go:hover { filter: brightness(1.06); }

/* Hidden until its beat. Built into the DOM up front and revealed by taking a
   class off, so "skip to the end" is the same code path as playing it out. */
.sdw-round, .sdw-round-label, .sdw-card, .sdw-vs, .sdw-verdict, .sdw-recap, .sdw-notes, .sdw-foot {
  transition: opacity 0.26s ease, transform 0.26s ease;
}
.sdw-pending { opacity: 0; }
.sdw-card.sdw-pending { transform: translateY(16px); }
.sdw-verdict.sdw-pending, .sdw-recap.sdw-pending, .sdw-notes.sdw-pending, .sdw-round-label.sdw-pending { transform: translateY(6px); }
/* the verdict: every card judged in one beat, so the losers dimming and the
   winner ringing read as one decision rather than a run of small ones */
.sdw-card.sdw-judged[data-result="lose"] { opacity: 0.42; }
.sdw-card.sdw-judged[data-result="win"] .sdw-ring { opacity: 1; }
.sdw-card.sdw-judged .sdw-mark { opacity: 1; }
.sdw-card.sdw-judged .sdw-pts { opacity: 1; transform: none; }
/* the points a card earned rise into place rather than appearing. No scale on
   the way, for the same reason nothing else here scales. */
.sdw-pts { font-size: 22px; }
.sdw-attack .sdw-pts {
  position: absolute; top: 30%; left: 50%; z-index: 6; pointer-events: none;
  font-size: 46px; letter-spacing: -1px; transform: translate(-50%, 14px);
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.55), 0 0 26px rgba(232, 180, 90, 0.5);
}
.sdw-attack .sdw-card.sdw-judged .sdw-pts { transform: translate(-50%, -26px); }

/* ---------------------------------------------------------------------- */
/* THE STRIKE. An attack has an aggressor and a victim, so everything about
   the impact belongs to the card being HIT: the mark is drawn inside its own
   box, the flare fills only it, the recoil and the tilt are its. An earlier
   version ripped a white line across the middle of the screen, which reads as
   the screen breaking rather than as one card being struck.

   The direction is one class (`sdw-fwd` / `sdw-back`), set on the attacker and
   the defender alike, so the lunge, the recoil and the tilt cannot disagree
   about which way the blow travelled. Nothing here scales: the lunge is a
   translate, the recoil is a translate and a rotate, and the mark is a stroked
   overlay that draws itself by dash offset. */
.sdw-card.sdw-windup { transition: transform 0.16s ease-out; }
.sdw-card.sdw-windup.sdw-fwd { transform: translateX(-14px); }
.sdw-card.sdw-windup.sdw-back { transform: translateX(14px); }
.sdw-card.sdw-lunge { transition: transform 0.11s cubic-bezier(.5, 0, .9, .6); }
.sdw-card.sdw-lunge.sdw-fwd { transform: translateX(34px); }
.sdw-card.sdw-lunge.sdw-back { transform: translateX(-34px); }
.sdw-card.sdw-recover { transition: transform 0.34s cubic-bezier(.2, .9, .3, 1); transform: none; }
.sdw-card.sdw-struck.sdw-fwd { animation: sdwHitFwd 0.34s cubic-bezier(.36, .07, .19, .97); }
.sdw-card.sdw-struck.sdw-back { animation: sdwHitBack 0.34s cubic-bezier(.36, .07, .19, .97); }
@keyframes sdwHitFwd {
  0% { transform: translate(0, 0) rotate(0); }
  16% { transform: translate(16px, 3px) rotate(2.5deg); }
  44% { transform: translate(-6px, -2px) rotate(0deg); }
  72% { transform: translate(3px, 1px); }
  100% { transform: translate(0, 0) rotate(0); }
}
@keyframes sdwHitBack {
  0% { transform: translate(0, 0) rotate(0); }
  16% { transform: translate(-16px, 3px) rotate(-2.5deg); }
  44% { transform: translate(6px, -2px) rotate(0deg); }
  72% { transform: translate(-3px, 1px); }
  100% { transform: translate(0, 0) rotate(0); }
}
/* a card that was destroyed tips over as it dims, the way it was knocked */
.sdw-attack .sdw-card.sdw-judged[data-result="lose"] .sdw-face { transition: transform 0.5s cubic-bezier(.2, .9, .3, 1); }
.sdw-attack .sdw-card.sdw-judged.sdw-fwd[data-result="lose"] .sdw-face { transform: rotate(7deg); }
.sdw-attack .sdw-card.sdw-judged.sdw-back[data-result="lose"] .sdw-face { transform: rotate(-7deg); }
/* the panel reacts to the hit; the scrim deliberately does not, or the dimmed
   felt would move with it and the whole screen would read as breaking */
.sdw-panel.sdw-shake { animation: sdwShake 0.24s cubic-bezier(.36, .07, .19, .97); }
@keyframes sdwShake {
  0%, 100% { transform: translate(0, 0); }
  18% { transform: translate(-5px, 2px); }
  38% { transform: translate(4px, -2px); }
  62% { transform: translate(-3px, -1px); }
  84% { transform: translate(2px, 1px); }
}
/* the white frame the hit actually lands on */
.sdw-flash {
  position: absolute; inset: 0; border-radius: 6.7% / 4.8%; pointer-events: none;
  background: var(--sdw-mk, var(--accent-2)); opacity: 0; mix-blend-mode: screen;
}
.sdw-card.sdw-struck .sdw-flash { animation: sdwFlash 0.3s ease-out forwards; }
@keyframes sdwFlash { 0% { opacity: 0; } 12% { opacity: 0.6; } 100% { opacity: 0; } }
/* the mark itself: one stroked overlay in the attacking card's element colour */
.sdw-mk { position: absolute; inset: 0; pointer-events: none; overflow: visible; }
.sdw-mk svg { width: 100%; height: 100%; display: block; overflow: visible; }
.sdw-mk-glow {
  stroke: var(--sdw-mk, var(--accent-2)); stroke-linecap: round; fill: none; opacity: 0.9;
  filter: drop-shadow(0 0 6px var(--sdw-mk, var(--accent-2)));
}
.sdw-mk-core { stroke: #fff; stroke-linecap: round; fill: none; }
/* The cut is undrawn until the mark is SHOWN, and the animation is attached to
   the shown state rather than gated by opacity. Attaching it to the base state
   would run it the moment the scene is built - which is well before the blow
   lands - and `forwards` would leave a finished cut lying on the card from the
   first frame. It also means a hurried scene, where every animation is off,
   skips the cut rather than freezing one half-drawn. */
.sdw-mk-draw { stroke-dasharray: 220; stroke-dashoffset: 220; }
.sdw-mk:not(.sdw-pending) .sdw-mk-draw {
  animation: sdwMkDraw 150ms cubic-bezier(.2, .8, .3, 1) var(--sdw-mk-d, 0ms) forwards,
             sdwMkFade 620ms ease var(--sdw-mk-fd, 260ms) forwards;
}
@keyframes sdwMkDraw { to { stroke-dashoffset: 0; } }
@keyframes sdwMkFade { to { opacity: 0; } }

/* a press said "get on with it": land every remaining beat at once */
.sdw-hurried, .sdw-hurried * { transition: none !important; animation: none !important; }

.tray-qty { display: flex; align-items: center; gap: 8px; }
.tray-qty-btn {
  width: 22px; height: 22px; border-radius: 7px; border: 1px solid var(--line-2); background: var(--panel-2);
  color: var(--text-dim); display: flex; align-items: center; justify-content: center; cursor: pointer;
}
.tray-qty-btn svg { width: 12px; height: 12px; }
.tray-qty-btn:hover { border-color: var(--accent); color: var(--accent-2); }
.tray-qty-n { min-width: 18px; text-align: center; font-size: 13px; font-variant-numeric: tabular-nums; color: var(--text); }
.tray-new {
  aspect-ratio: 5/7; width: 132px; border: 1.5px dashed var(--line-2); border-radius: 14px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: var(--text-dim);
}
.tray-new:hover { border-color: var(--accent); color: var(--accent-2); }
.tray-new svg { width: 28px; height: 28px; }

/* =================================================================
   Main menu as a fanned hand of cards
   ================================================================= */
.menu-hand { position: relative; width: 100%; max-width: 860px; height: 320px; margin: 8px auto 0; }
.menu-slot {
  position: absolute; left: 50%; top: 30px;
  width: 148px; height: 206px; /* stationary hit area, inset from the card's edges */
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  /* all cards share one pivot far below the hand, so they fan from a common
     point: bottoms converge, tops splay outward like a real hand. */
  transform: translateX(-50%) rotate(var(--rot, 0deg));
  transform-origin: 50% 540px;
}
.menu-slot:hover { z-index: 60 !important; } /* beats the inline per-card z-index so the hovered card sits in front */
/* the card and its riser ignore the pointer; only the stationary slot detects
   hover, so the lift can't pull the hit area off the cursor and loop. */
/* Dealt from a deck: each card starts stacked near the hand's pivot, square to
   the screen (the parent slot's fan rotation is cancelled), then slides out and
   spins into its splayed spot. The staggered per-card delay (set inline in
   home.js) makes them land one after another like a dealer flicking cards. */
.menu-riser { animation: menuDeal 0.44s cubic-bezier(0.22, 0.85, 0.28, 1.02) both; pointer-events: none; }
@keyframes menuDeal {
  0%   { opacity: 0; transform: translateY(200px) rotate(calc(-1 * var(--rot, 0deg))) scale(0.62); }
  55%  { opacity: 1; }
  100% { opacity: 1; transform: none; }
}

.menu-card {
  position: relative; display: flex; width: 162px; aspect-ratio: 5 / 7; padding: 7px;
  border: none; cursor: pointer; color: #f4f1e8; pointer-events: none;
  border-radius: 15px;
  background: linear-gradient(180deg, rgba(255,255,255,0.09), rgba(0,0,0,0.34)), var(--mc-accent, #c98a2f);
  box-shadow: 0 12px 28px rgba(0,0,0,0.5);
  transform-origin: center bottom; /* rotation lives on the slot; the card only lifts */
  transition: transform 0.34s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.34s cubic-bezier(0.4, 0, 0.2, 1), filter 0.34s;
}
/* hover is tracked on the stationary slot, not the card. the card lifts up and
   out from under the cursor, so triggering on the card itself would end the
   hover, drop it back, and loop. the slot's hit area never moves, so it holds. */
.menu-slot:hover .menu-card {
  transform: translateY(-50px) scale(1.05);
  box-shadow: 0 34px 54px rgba(0,0,0,0.6), 0 0 0 2px var(--mc-accent2, var(--accent-2));
  filter: brightness(1.05);
}
.menu-card-face {
  position: relative; /* anchors the .mc-foil sheen pseudo-elements */
  flex: 1; width: 100%; background: #14171d; border-radius: 10px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 13px;
  padding: 18px 12px; text-align: center; overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,0.55);
  transition: box-shadow 0.34s cubic-bezier(0.4, 0, 0.2, 1);
}
.menu-slot:hover .menu-card-face { box-shadow: inset 0 0 0 1px var(--mc-accent2, var(--accent-2)); }
.menu-card-ic {
  width: 52px; height: 52px; border-radius: 13px; display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.06); color: var(--mc-accent2, var(--accent-2));
}
.menu-card-ic svg { width: 28px; height: 28px; }
.menu-card-title { font-size: 16px; font-weight: 600; letter-spacing: -0.2px; font-family: var(--font); }
.menu-card-sub { font-size: 11px; line-height: 1.35; letter-spacing: -0.1px; color: var(--text-dim); font-family: var(--font); }
.mc-pip { position: absolute; display: flex; color: rgba(255,255,255,0.82); z-index: 2; }
.mc-pip svg { width: 15px; height: 15px; }
.mc-pip.tl { top: 12px; left: 12px; }
.mc-pip.br { bottom: 12px; right: 12px; transform: rotate(180deg); }

.mc-gold { --mc-accent: #c98a2f; --mc-accent2: #f0c879; }
.mc-blue { --mc-accent: #2f6cb0; --mc-accent2: #6aa9e8; }
.mc-green { --mc-accent: #2f9462; --mc-accent2: #69d49b; }
.mc-violet { --mc-accent: #6f47b0; --mc-accent2: #b08fe4; }
.mc-crimson { --mc-accent: #a8392b; --mc-accent2: #e08475; }
.mc-pink { --mc-accent: #c43a7e; --mc-accent2: #f090c2; }
.mc-teal { --mc-accent: #1f8a8f; --mc-accent2: #5fd0d4; }
.mc-slate { --mc-accent: #4a5a6a; --mc-accent2: #9db4c7; }

@media (max-width: 640px) {
  .menu-card { width: 132px; }
  .menu-hand { height: 340px; }
}

/* The Back card: a neutral slate card so it reads as chrome, not an action.
   Used by the Settings landing fan; the main menu is a single hand with no
   Back card, since Home is the root. */
.menu-card.is-back { --mc-accent: #2b323c; --mc-accent2: #93a6ba; }

/* Foil / shiny: gilt foil for any surface tagged .mc-foil (the Packlabs Pro
   card's face in the menu fan). The host must be positioned and clip its
   overflow; .menu-card-face is both. Content inside a shiny face is lifted
   above the sheen so text stays readable.
   Three parts, and they are deliberately split that way:
   - A MOTIONLESS resting layer (::after): a soft gold sheen plus a warm
     top-left highlight. Never animates, so the card reads as foil while it
     sits in the fan.
   - A TRAVELLING glint (::before): absent at rest, and only declared inside
     the :hover rule, so it plays exactly once per hover and restarts from
     the top every time the pointer comes back. It's oversized and translated
     rather than background-positioned, so the direction is literal: it fades
     in above the top-left corner, crosses the card, and fades out past the
     bottom-right.
   - The icon chip and the rim, below, run the same pass on a small delay, so
     one light source appears to travel over the whole card.
   Earlier versions looped a rainbow wash forever; that's gone on purpose.
   Don't reintroduce an infinite foil animation - the whole point is that the
   menu is still when nobody is touching it. --pass is the one timing knob;
   it drives the face, the icon and the rim together. */
.mc-foil {
  --glint: rgba(255,238,196,0.75);   /* the travelling band */
  --glint-2: rgba(255,196,96,0.4);   /* its trailing edge */
  --rest: rgba(255,226,160,0.2);     /* the motionless idle sheen */
  --spark: rgba(255,240,205,0.9);    /* rim sparkle */
  --pass: 0.8s;                      /* one crossing, top-left to bottom-right */
}
.mc-foil::before, .mc-foil::after { content: ""; position: absolute; border-radius: inherit; pointer-events: none; }
.mc-foil::after {
  inset: 0; mix-blend-mode: screen;
  background:
    linear-gradient(135deg, transparent 28%, var(--rest) 44%, transparent 60%),
    linear-gradient(160deg, rgba(255,214,140,0.16), transparent 45%, rgba(180,120,40,0.16));
}
.mc-foil::before {
  inset: -45%; opacity: 0; mix-blend-mode: screen;
  background: linear-gradient(135deg,
    transparent 44%, var(--glint) 49.5%, var(--glint-2) 53%, transparent 60%);
  transform: translate3d(-42%, -42%, 0);
}
/* The slot is the hover host everywhere the fan is used (the card itself lifts
   out from under the cursor, see .menu-slot:hover), so the trigger hangs off
   the slot, with a bare :hover fallback for any foil surface not inside one. */
.menu-slot:hover .mc-foil::before,
.mc-foil:hover::before { animation: foilPass var(--pass) cubic-bezier(0.3, 0.55, 0.35, 1) 1 both; }
@keyframes foilPass {
  0%   { opacity: 0; transform: translate3d(-42%, -42%, 0); }
  18%  { opacity: 1; }
  82%  { opacity: 1; }
  100% { opacity: 0; transform: translate3d(42%, 42%, 0); }
}

/* The icon on a shiny card is its own little foil chip: a gilt tile with a
   warm inner hairline, the mark glowing softly, and the same glint crossing it
   a beat (0.14s) after the face. */
.menu-card.shiny .menu-card-ic {
  position: relative; overflow: hidden;
  background:
    linear-gradient(160deg, rgba(255,236,190,0.26), rgba(255,255,255,0.04) 45%, rgba(0,0,0,0.18)),
    rgba(255,255,255,0.06);
  box-shadow: inset 0 0 0 1px rgba(255,231,178,0.35), 0 2px 8px rgba(0,0,0,0.35);
}
.menu-card.shiny .menu-card-ic svg {
  position: relative; z-index: 1;
  filter: drop-shadow(0 0 5px rgba(255,214,140,0.45));
  transition: filter 0.3s ease;
}
.menu-slot:hover .menu-card.shiny .menu-card-ic svg { filter: drop-shadow(0 0 9px rgba(255,226,160,0.85)); }
.menu-card.shiny .menu-card-ic::after {
  content: ""; position: absolute; inset: -60%; pointer-events: none; opacity: 0;
  background: linear-gradient(135deg, transparent 40%, rgba(255,255,255,0.75) 50%, transparent 62%);
  transform: translate3d(-45%, -45%, 0);
}
.menu-slot:hover .menu-card.shiny .menu-card-ic::after {
  animation: foilPass 0.8s cubic-bezier(0.3, 0.55, 0.35, 1) 0.14s 1 both;
}

/* The rim: a 2px ring of sparkle around the card edge, cut out of a solid
   block by masking the content box out of the border box, so only the border
   band paints. Low at rest, one flare as the glint goes past. */
.menu-card.shiny::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit; padding: 2px;
  pointer-events: none; z-index: 3; opacity: 0.55;
  background:
    radial-gradient(circle at 30% 40%, rgba(255,240,205,0.9) 0 0.8px, transparent 1.3px),
    radial-gradient(circle at 72% 68%, rgba(255,255,255,0.85) 0 0.7px, transparent 1.2px),
    linear-gradient(135deg, rgba(255,236,190,0.55), rgba(255,255,255,0.15) 35%, rgba(255,236,190,0.6) 70%, rgba(255,255,255,0.2));
  background-size: 13px 13px, 21px 21px, auto;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
}
.menu-slot:hover .menu-card.shiny::after { animation: rimFlare 1.08s ease-out 1 both; }
@keyframes rimFlare {
  0%   { opacity: 0.55; background-position: 0 0, 0 0, 0 0; }
  45%  { opacity: 1; }
  100% { opacity: 0.55; background-position: 13px 13px, -21px 21px, 0 0; }
}
/* Reduced motion keeps the foil, drops the travel: the resting sheen, the gilt
   icon chip and the rim all still paint, nothing crosses the card. */
@media (prefers-reduced-motion: reduce) {
  .menu-slot:hover .mc-foil::before, .mc-foil:hover::before,
  .menu-slot:hover .menu-card.shiny .menu-card-ic::after,
  .menu-slot:hover .menu-card.shiny::after { animation: none; }
}
.menu-card.shiny .menu-card-face { box-shadow: inset 0 0 0 1px rgba(255, 225, 150, 0.55); }
.menu-card.shiny .menu-card-face > * { position: relative; z-index: 1; }

/* Solid-card physics layer (js/cardFling.js). After the fan deals in, the menu
   and My Games cards become solid, collidable bodies. They're not draggable:
   hovering a card lifts it (as before), and because each card is solid it shoves
   its neighbours aside, which spring back into the fan when the hover leaves.
   The hand gets a perspective so each body's forward lean (rotateX) reads as a
   3D slab; the layer keeps 3D so the tilt isn't flattened. */
.menu-hand, .game-hand { perspective: 1200px; perspective-origin: 50% 40%; }
.fling-layer { position: absolute; inset: 0; transform-style: preserve-3d; }
.fling-body { pointer-events: auto; transition: none !important; cursor: pointer;
  box-shadow: 0 16px 32px rgba(0,0,0,0.55); }

/* =================================================================
   Full-bleed table: the page is the stage, but the felt itself is
   INSET from the floating controls (tool rail / seat rail / phase bar /
   corner buttons) rather than sitting edge-to-edge underneath them.
   Zones and pieces are laid out as fractions of the felt's own box, so
   giving the felt a smaller box automatically keeps every zone and
   piece clear of the floating chrome around it - nothing needs to
   "duck" a rail because the play surface simply doesn't extend there.
   The margins below are sized to comfortably clear the tallest control
   in each band (seat rail up top, phase bar along the bottom, the tool
   rail on the left) with room to spare.

   THE DOUBLED HEIGHT DECLARATION IS DELIBERATE - DO NOT "TIDY" IT AWAY.
   Every viewport-height value in this file is written twice, vh first and
   then dvh. On a phone, 100vh resolves to the LARGE viewport (the one with
   the browser's URL bar retracted), so a 100vh table puts its bottom band
   permanently underneath the toolbar - and because this rule also clips
   with overflow:hidden, that band is not merely covered, it is unreachable.
   The bottom band is where the phase bar lives, i.e. the primary play
   control. dvh tracks the toolbar and fixes it. The vh line stays in front
   as the fallback a browser too old for dvh will keep, since dropping the
   declaration entirely would leave the height at auto and collapse the
   stage. Same pattern for every min-height/max-height below.
   ================================================================= */
.view.table-view { max-width: none; padding: 0; height: 100vh; height: 100dvh; overflow: hidden; position: relative; }
/* HOW THICK THE WOODEN EDGE IS, stated once. The frame, the felt's own side
   insets, the flash overlay and the seat-dial layer all read it, and so does
   syncRailInsets in 04-rolling-buying.js (which keeps 14px of clearance above
   the wood whatever it is set to). It was three hardcoded 18s and a 22, which
   is why the rail could not be changed without four edits that had to agree.
   24px is what the flat seat run needs: a 16px crest and a 15px cell, with a
   few px of wood showing round them. */
.table-view .table-stage { position: relative; flex: none; width: 100%; height: 100%; perspective: 1700px; perspective-origin: 50% 30%; --rail-t: 24px; }

/* the SCALER wraps the wooden surround and the felt viewport as one unit, and is
   the thing the camera zooms/pans (applyCamera in tableEditor.js, CSS `zoom` + a
   translate). Zooming in therefore moves the whole table closer - wood edge and
   all - instead of magnifying the cloth inside a fixed frame. It mirrors the
   felt's old pin-then-zoom setup: left/top-anchored so a pinned pixel size grows
   down-right from the corner, then the pan re-centres it. The floating controls
   are NOT inside it (they stay direct children of the stage), so they never move. */
.table-view .table-scaler {
  position: absolute; left: 0; top: 0; width: 100%; height: 100%;
  transform-origin: 0 0;
}
/* Clip the grown table to the stage, but only while zoomed - at rest a 3D card
   lift can stand proud of the felt and must not be cut off. */
.table-view .table-stage.cam-zoomed { overflow: hidden; }

/* the VIEWPORT is the fixed window onto the felt - within the scaler it never
   moves or resizes on its own; it only rides the scaler's zoom/pan. The felt
   sits inside it at 100% size, and the wood frame rings it. */
.felt-viewport {
  /* Full-bleed left/right: the felt uses the whole width. The build-mode
     tool rail and the play-mode majority tracker no longer get their own
     reserved band - they float OVER the felt like the bet panel does, and
     both are draggable + minimisable so a builder can move them off any
     content they land on (or collapse them entirely). Left is kept level
     with the right (22px) so the felt is centred, not lopsided. */
  /* Top and bottom are MEASURED, not guessed: syncRailInsets() in
     04-rolling-buying.js reads the seat rail's and phase bar's real rendered
     heights every time either one changes and writes them here as --rail-top /
     --rail-bot. The old hardcoded 78px (bumped to 118px under a 900px query)
     was sized for the rails wrapping onto exactly ONE extra line; a six-seat
     game on a narrow screen wraps onto three or four, and the surplus rows
     then sat on top of the felt. The fallbacks below are the single-line
     values, so a browser that never runs the script still renders as before. */
  position: absolute; top: var(--rail-top, 84px); left: calc(4px + var(--rail-t, 24px)); right: calc(4px + var(--rail-t, 24px)); bottom: var(--rail-bot, 84px);
  /* Square corners so the felt meets the wooden surround's square opening flush -
     a rounded felt would leave a dark triangle in each miter corner. */
  border-radius: 0; overflow: hidden;
  border: 1px solid #20303a;
  box-shadow: inset 0 0 0 2px rgba(255,255,255,0.03), inset 0 14px 60px rgba(0,0,0,0.45), var(--shadow);
}

/* Wooden table edge: the felt is recessed into a hardwood surround, built like a
   real board-game table - four solid cherry rails whose grain runs along each
   board, mitred at 45deg where they meet. The frame rings the felt-viewport
   (by --rail-t on every side), so the wood shows all round the opaque viewport. It's the stage's first child, painting behind the felt.
   Each rail is one <div> clipped to a trapezoid; the two clip edges are the
   45deg miter cuts, so adjacent rails tile the ring with a clean joint. The
   grain is an feTurbulence texture (long fibres along the board) - horizontal
   for the top/bottom rails, the same turbulence turned 90deg for the sides. */
.table-view .table-frame {
  position: absolute; z-index: 0; pointer-events: none; --t: var(--rail-t, 24px);
  /* The wood sits exactly one rail thickness outside the felt, so it tracks the
     measured inset rather than carrying its own copy of the number. Same
     fallbacks as .felt-viewport, minus that thickness. */
  top: calc(var(--rail-top, 84px) - var(--rail-t, 24px)); left: 4px; right: 4px; bottom: calc(var(--rail-bot, 84px) - var(--rail-t, 24px));
  /* a tight contact shadow plus a broad soft one, so the table reads as lifted
     off the page and casting real depth beneath its edge */
  filter: drop-shadow(0 5px 6px rgba(0,0,0,0.45)) drop-shadow(0 26px 40px rgba(0,0,0,0.55));
}
.table-frame .wrail { position: absolute; inset: 0; background-position: center; background-repeat: repeat; }
/* Lighting to match the felt: the felt's glow sits in the MIDDLE and falls off
   outward, so the light reaches the wood along the vertical mid-line (the left and
   right rails, lit across their centres) while the top and bottom rails - farthest
   from that central glow - drop into shadow. A vertical gradient dark at both ends
   and light through the middle does exactly that, and blends smoothly through each
   corner; a soft warm bloom bleeds the central light onto the inner wood. */
.table-view .table-frame::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    radial-gradient(58% 52% at 50% 48%, rgba(255,240,214,0.1), transparent 72%),
    linear-gradient(180deg,
      rgba(0,0,0,0.52) 0%,
      rgba(0,0,0,0.1) 32%,
      rgba(255,255,255,0.05) 48%,
      rgba(0,0,0,0.1) 66%,
      rgba(0,0,0,0.52) 100%);
}
/* the four mitred trapezoids - each shares its angled edges with its neighbours */
.wrail-top    { clip-path: polygon(0 0, 100% 0, calc(100% - var(--t)) var(--t), var(--t) var(--t)); }
.wrail-bottom { clip-path: polygon(0 100%, var(--t) calc(100% - var(--t)), calc(100% - var(--t)) calc(100% - var(--t)), 100% 100%); }
.wrail-left   { clip-path: polygon(0 0, var(--t) var(--t), var(--t) calc(100% - var(--t)), 0 100%); }
.wrail-right  { clip-path: polygon(100% 0, 100% 100%, calc(100% - var(--t)) calc(100% - var(--t)), calc(100% - var(--t)) var(--t)); }
/* The grain is a real wood-plank photo (assets/textures/wood-h.jpg). The photo's
   grain runs vertically, so wood-v.jpg is that image and wood-h.jpg is the same
   board turned 90deg - the grain then runs the length of each rail. A soft sheen
   gradient over the top gives the polished finish its highlight and shaded foot. */
/* Size each rail's texture to the axis the rail actually shows, so it only ever
   downscales the photo (crisp) instead of enlarging it (blurry): the long
   top/bottom rails fit their width, the tall side rails fit their height. */
.wrail-top, .wrail-bottom {
  background-size: 100% auto;
  background-image:
    linear-gradient(180deg, rgba(255,255,255,0.18), transparent 30%, rgba(0,0,0,0.28)),
    url("../assets/textures/wood-h.jpg?v=4");
}
.wrail-left, .wrail-right {
  background-size: auto 100%;
  background-image:
    linear-gradient(90deg, rgba(255,255,255,0.14), transparent 30%, rgba(0,0,0,0.3)),
    url("../assets/textures/wood-v.jpg?v=4");
}
/* The wooden edge stands proud of the recessed felt, so it casts a shadow inward
   onto the cloth - a soft band all round the felt that pools darkest in the four
   corners (where two edges' shadows overlap). Drawn on a viewport overlay so it
   stays pinned to the felt's edge and never pans/zooms with the felt itself. */
.table-view .felt-viewport::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  box-shadow: inset 0 0 26px 3px rgba(0,0,0,0.42), inset 0 0 60px 14px rgba(0,0,0,0.22);
  background:
    radial-gradient(90px 90px at 0 0, rgba(0,0,0,0.4), transparent 100%),
    radial-gradient(90px 90px at 100% 0, rgba(0,0,0,0.4), transparent 100%),
    radial-gradient(90px 90px at 0 100%, rgba(0,0,0,0.4), transparent 100%),
    radial-gradient(90px 90px at 100% 100%, rgba(0,0,0,0.4), transparent 100%);
}
/* ================= seat runs =================
   A player's readout, lying IN the wooden edge at their side of the table while
   a game is being played: their crest flush in the wood inside its earned
   frame, and beside it one cell per value the game tracks - the number, and
   under it a tick filled to its share. Built by renderSeatDials in
   js/views/tableEditor/16-cardtray-seats-phases.js, whose header carries the
   full reasoning; the design pages are dev/player-dial-rail.html (the dial this
   replaced) and dev/dial-blend-sketch.html (why it lies flat).

   The short version, because it is the one rule every value below follows:
   NOTHING HERE MAY BE THICKER THAN THE RAIL. A raised dial was 96px across on a
   24px rail, so it spilled onto the cloth at one end and under the turn
   sequencer's pills at the other. Everything is sized against --rail-t, and a
   new element that needs more depth than that is not a thing this surface can
   hold - it belongs in one of the two bars instead.

   The layer takes the SAME box as .table-frame and sits inside .table-scaler,
   so a run pans and zooms with the wood it is set into. The frame stays scenery
   (pointer-events: none); a crest is a control, so the pointer comes back on
   for the crest alone and the felt underneath is untouched. */
.sdial-layer {
  position: absolute; z-index: 3; display: none; pointer-events: none;
  top: calc(var(--rail-top, 84px) - var(--rail-t, 24px)); left: 4px; right: 4px; bottom: calc(var(--rail-bot, 84px) - var(--rail-t, 24px));
}
.sdial-layer.on { display: block; }
/* In play the runs are where a seat appears, so the rail stands down rather
   than drawing every player a second time. */
.seat-rail.stood-down { display: none; }

/* one seat's run, centred on the rail's centre line. It is laid out by flexbox
   rather than by trig, which is the whole saving of this treatment: which way
   it runs is a class, not an angle. */
.sdial {
  position: absolute; left: 0; top: 0; transform: translate(-50%, -50%);
  display: flex; align-items: center; gap: 5px;
  pointer-events: none; z-index: 2;
}
.sdial-vert { flex-direction: column; gap: 4px; }

/* WHO THIS RUN BELONGS TO: the crest lying flush in the wood, and the player's
   name beside it, the pair of them in a pill. The PILL is the control rather
   than the crest inside it - a 16px disc is a small thing to ask anybody to hit
   and the rail has length going spare, so the target costs nothing.

   The name comes off for the whole table when it will not fit (fitSeatDialBadges
   adds .no-name), and the pill then closes up round the crest and is a circle
   again. Which is why the name is a child that can be hidden rather than a
   different pill built two ways: one element, two states, and nothing to keep
   in step. */
.sdial-tag {
  position: relative; flex: 0 0 auto; display: inline-flex; align-items: center; gap: 5px;
  padding: 1px; border-radius: 999px; cursor: pointer; pointer-events: auto;
  background: rgba(10,14,19,0.5);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.06);
}
/* Closed up round the crest, the pill is the crest: no ring of its own, or a
   circle in the wood would wear two. */
.sdial-layer.no-name .sdial-tag { padding: 0; background: none; box-shadow: none; gap: 0; }
.sdial-layer.no-name .sdial-nm { display: none; }
.sdial-nm {
  font-size: 10.5px; font-weight: 800; letter-spacing: 0.2px; white-space: nowrap;
  color: rgba(255,255,255,0.88); text-shadow: 0 1px 2px rgba(0,0,0,0.9);
  padding-right: 5px; max-width: 96px; overflow: hidden; text-overflow: ellipsis;
}
.sdial-sm .sdial-nm { font-size: 9.5px; color: rgba(255,255,255,0.74); max-width: 78px; }
/* A run is deaf to the pointer except here, so the crest keeps its cursor and
   the felt underneath keeps every drag. */
.sdial-crest {
  position: relative; flex: 0 0 auto; pointer-events: none;
  width: var(--sdial-c, 16px); height: var(--sdial-c, 16px); border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: radial-gradient(120% 120% at 32% 24%, #26303d, #131922);
  border: 1px solid rgba(0,0,0,0.6);
  box-shadow: 0 0 0 1px rgba(255,255,255,0.09), inset 0 1px 0 rgba(255,255,255,0.10);
  color: var(--accent-2);
}
.sdial-crest svg { width: 68%; height: 68%; }
/* whose turn it is, said by the crest's own ring: the ticks already own every
   colour on the run, so a gold pill beside them would read as one more value */
.sdial-active .sdial-crest { box-shadow: 0 0 0 1.5px var(--accent), 0 0 14px -3px var(--accent), inset 0 1px 0 rgba(255,255,255,0.12); }
/* whose turn it is reaches the pill as well, since the pill is what the eye
   lands on once there is a name in it */
.sdial-active .sdial-tag { background: rgba(216,162,74,0.14); box-shadow: inset 0 0 0 1px rgba(216,162,74,0.4); }
.sdial-layer.no-name .sdial-active .sdial-tag { background: none; box-shadow: none; }
.sdial-active .sdial-nm { color: var(--accent-2); }
.sdial-tag:hover { background: rgba(255,255,255,0.10); }
.sdial.is-out .sdial-tag:hover { background: rgba(10,14,19,0.5); }
.sdial.is-ai .sdial-crest { border-color: rgba(124,196,255,0.5); }
.sdial.is-out { opacity: 0.45; }
.sdial.is-out .sdial-tag { cursor: default; }

/* THE RUN IS THE CONTROL, IN BUILD AS WELL AS IN PLAY. It used to be a muted,
   pointer-deaf FOOTPRINT in build - drawn only so a builder could see the strip
   of rail a seat would claim - while a rail floating above the felt held the
   pill you actually pressed. Two surfaces for one thing, and the rail was the
   one costing a band of screen at the top of every build, so the footprint took
   the job and the rail stands down. The .sdial-ghost rules that dimmed it are
   gone; .sdial-ghostdrag below is a different thing entirely (the clone that
   follows the pointer while two players trade places).

   Nothing here needs the pointer turned back on beyond the pill: the layer takes
   the same box as .table-frame, which is the WOOD, outside .felt-viewport - so a
   run has no felt gesture underneath it to steal. */
.sdial-draggable { cursor: grab; }
.sdial-swapping .sdial-draggable { cursor: grabbing; }
/* the run you picked up stays as a dimmed placeholder while its clone flies */
.sdial-lift { opacity: 0.3; }
/* the clone, on <body> so no ancestor's overflow or transform can clip it */
.sdial-ghostdrag {
  position: fixed; z-index: 200; pointer-events: none; opacity: 0.92;
  transform: translate(-50%, -50%) scale(1.08);
  filter: drop-shadow(0 6px 14px rgba(0,0,0,0.55));
}
/* who you are about to trade places with */
.sdial-drop .sdial-tag {
  background: rgba(216,162,74,0.22);
  box-shadow: inset 0 0 0 1px var(--accent), 0 0 12px -2px var(--accent);
}
.sdial-drop .sdial-crest { box-shadow: 0 0 0 1.5px var(--accent), 0 0 14px -3px var(--accent); }

/* ADDING A PLAYER, ON THE WOOD. It is placed at the point the next seat's run
   would land on (layoutSeatAddNode walks the ring for one more seat than there
   are), so pressing it is pressing the strip of rail that player will occupy.
   Kept under the rail's own thickness like everything else on this surface:
   18px inside 24px of wood, which is the same arithmetic the earned frames have
   to satisfy, since neither can be seen by fitSeatDialBadges. */
.sdial-add {
  position: absolute; left: 0; top: 0; transform: translate(-50%, -50%);
  z-index: 4; pointer-events: auto;
  width: 18px; height: 18px; padding: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(10,14,19,0.6); color: var(--accent-2); cursor: pointer;
  border: 1px dashed rgba(255,255,255,0.28);
}
.sdial-add svg { width: 10px; height: 10px; }
.sdial-add:hover { background: rgba(216,162,74,0.2); border-color: var(--accent); color: var(--accent); }
/* At the seat cap it goes idle and says why, rather than leaving a gap where a
   control was. */
.sdial-add.row-off { opacity: 0.4; cursor: default; }
.sdial-add.row-off:hover { background: rgba(10,14,19,0.6); border-color: rgba(255,255,255,0.28); color: var(--accent-2); }
/* The wood is a control surface now, so it has to be reachable with a finger.
   The + gets the usual invisible expander - the hit area grows, the control
   does not - and it is already an absolutely positioned box, so the pseudo needs
   no position rule of its own.

   .sdial-tag DELIBERATELY GETS NONE, and the reason is worth stating so nobody
   adds one. An earned frame rides the pill's OWN ::before and ::after (every
   .brd-* rule is written against its own class alone, which is what let the
   frames land on this pill for the cost of one selector), so on this host both
   pseudos are already spoken for and an expander would blank somebody's frame.
   The child .sdial-crest is free but pointer-deaf, so an expander there reaches
   nothing. And the tag cannot grow instead: it lies in 24px of wood and
   fitSeatDialBadges drops the whole table's names the moment a run is thicker
   than the rail. What a finger gets on this surface is the long press, which
   js/longpress.js turns into the seat menu with nothing here to add. */
@media (pointer: coarse) {
  .sdial-add::before {
    content: ""; position: absolute; inset: -11px; border-radius: 999px;
  }
}

/* THE COMPUTER OPPONENT'S LANE, in the band the seat rail used to hold. A peer
   of the turn sequencer along the bottom, so it wears the same glass and the
   same pill scale: that bar says what a TURN does, this one says how the
   computer decides it, one line apart on the same screen. Measured into
   --rail-top by syncRailInsets like every other bar, so the felt makes room for
   it instead of being covered. */
/* Both selectors carry .table-top-center as well, because that class is written
   LATER in this file and sets display:flex - a bare .ai-lane is the same
   specificity and would simply lose, which is the same one-point cascade trap
   .gl-name-input and every search field hit. */
.table-top-center.ai-lane { display: none; }
.table-top-center.ai-lane.on { display: flex; }
/* The corner clusters share this band and are pinned to the corners, so the lane
   is kept out of their way rather than centred over them. The floor is what
   stops a narrow window resolving that to nothing; below it the lane wraps, and
   syncRailInsets grows the band to whatever it wrapped to. */
/* A FIXED WIDTH RATHER THAN A CEILING, because the hand has to measure the room
   it has. A shrink-to-fit box reports the width its content happens to take, so
   fanRoom would have been asking the cards how much room the cards have - and
   the hand would tighten its own overlap a little on every draw for space that
   was never missing. Given a real width there is a real answer.
   The cost of a full-width box across the top of the felt is that it would
   swallow clicks meant for the table, so the frame takes no pointer at all and
   only the controls in it do. */
.ai-lane { width: max(260px, calc(100% - 420px)); pointer-events: none; }
.ai-lane .tphase-bar { margin: 0; max-width: 100%; flex-wrap: wrap; justify-content: center; pointer-events: none; }
.ai-lane .ai-lane-who, .ai-lane .ai-lane-bar, .ai-lane .tphase-lanehand { pointer-events: none; }
.ai-lane .tphase, .ai-lane .ai-lane-chip, .ai-lane .ai-knob, .ai-lane .lbl { pointer-events: auto; }

/* THE TWO KINDS OF STEP, told apart by the face rather than by the size. A step
   the computer decides is an ordinary card; a step it simply takes is quiet and
   inert, and its tooltip says why. Neither is smaller than the other, which is
   the point: the lane reads as one sequence. */
.ai-lane .tphase.stepcard.ai-auto { cursor: default; }
.ai-lane .tphase.stepcard.ai-auto .tphase-stepcardface { opacity: 0.55; background: linear-gradient(180deg, rgba(26,32,40,0.9), rgba(18,23,29,0.9)); }
.ai-lane .tphase.stepcard.ai-auto:hover .tphase-stepcardface { opacity: 0.8; }
/* A step tuned off its derived default. A quiet mark on the face, because the
   VALUE is already on the tag under the card - this only has to say "somebody
   has been here", which is not worth the gold ring `cur` wears. */
.ai-lane .tphase.stepcard.ai-set .tphase-stepcardface { border-color: rgba(216,162,74,0.55); }

/* THE TAG HANGS THE OTHER WAY UP HERE, and it has to. A card's tag prints above
   the card, which is right for the turn lane because that hand sits along the
   bottom of the felt and there is a whole table above it. This lane sits in the
   top band: measured, the tags landed at -6px and -3px, off the top of the
   window, so the value would have been unreadable whenever it was shown. Below
   the card it hangs into the felt, where there is room. */
.ai-lane .tphase.stepcard > .tphase-badge {
  bottom: auto; top: 100%; margin: 7px 0 0;
  transform: translateX(-50%) translateY(-5px);
}
.ai-lane .tphase.stepcard:hover > .tphase-badge { transform: translateX(-50%) translateY(0); }
/* which opponent the lane is about, and only when there is more than one - a
   chooser between one thing answers nothing */
.ai-lane-who { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; justify-content: center; }
.ai-lane-chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 8px 3px 4px; border-radius: 999px; cursor: pointer;
  font-size: 11px; font-weight: 700; letter-spacing: 0.2px;
  color: var(--text-dim); background: rgba(15,20,28,0.5); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,0.08);
}
.ai-lane-chip:hover { background: rgba(255,255,255,0.07); color: var(--text); }
.ai-lane-chip.on { color: var(--accent-2); border-color: rgba(216,162,74,0.4); background: rgba(216,162,74,0.14); }
.ai-lane-crest {
  width: 16px; height: 16px; border-radius: 50%; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  background: radial-gradient(120% 120% at 32% 24%, #26303d, #131922);
  border: 1px solid rgba(0,0,0,0.6);
}
.ai-lane-crest svg { width: 68%; height: 68%; }
/* Pointing the lane at a different opponent is a quiet change in a corner of the
   screen, so it says so once. */
.ai-lane-lit .tphase-bar { box-shadow: 0 0 0 1px var(--accent), 0 0 18px -4px var(--accent); }
@media (prefers-reduced-motion: reduce) { .ai-lane-lit .tphase-bar { box-shadow: 0 0 0 1px var(--accent); } }

/* one value: the number, and under it a tick filled to its share. The number is
   what you read; the tick's COLOUR is what says which value it is, and its fill
   is how far along it is. A value with no ceiling fills its tick whole. */
/* THE CELL TAKES THE POINTER BACK, and it is the only other thing on a run that
   does. .sdial is deaf so the layer - which is a rectangle over the whole table,
   felt included - never steals a drag; the pill turns it back on for itself, and
   a cell needs the same for the same reason the pill does: a title on a
   pointer-events:none element fires no mouseover, so the tip naming the value
   can never appear. A cell's own box lies in the WOOD, where there is no felt
   gesture underneath to take, which is the argument the run's right-click
   already rests on. cursor:help is the affordance - the number is not a control,
   it is a thing that explains itself if you rest on it. */
.sdial-cell {
  display: inline-flex; align-items: center; gap: 2px; pointer-events: auto; cursor: help;
  flex: 0 0 auto; color: var(--sdial-tint, var(--accent-2));
  font-size: 10.5px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1;
  text-shadow: 0 1px 2px rgba(0,0,0,0.9);
}
/* A CELL IS THE SAME SHAPE ON EVERY RAIL, and only the RUN changes direction.
   Laying the cell along the rail is what a "the rail is vertical, so the cell
   must be too" instinct produces, and measured it is wrong: it spends the rail's
   ONE scarce axis on every part of the cell at once, which came to 23px inside
   24px of wood and tripped the thickness rule on every table with a side seat.
   A cell's length runs along the wood, where there is room to spare. */

.sdial-b-ico { display: inline-flex; }
.sdial-b-ico svg { width: 9px; height: 9px; }
.sdial-sm .sdial-cell { font-size: 9.5px; }
.sdial-sm .sdial-b-ico svg { width: 8px; height: 8px; }
/* dropped by MEASUREMENT (fitSeatDialBadges), for the whole table at once,
   never at a value count written down here */
.sdial-layer.no-ico .sdial-b-ico { display: none; }
.sdial-layer.no-ico .sdial-cell { gap: 0; }


/* the manual mounts the real table; keep its demo felt frameless */
.table-stage.demo-mount .table-frame { display: none; }
.table-stage.demo-mount .sdial-layer { display: none; }
.table-stage.demo-mount .felt-viewport::after { display: none; }
/* The 900px query that used to live here (frame to top/bottom 100px, to track
   the viewport's 118px) is gone: both now derive from the MEASURED
   --rail-top / --rail-bot, which is correct at every width instead of at one
   breakpoint. Don't reintroduce a width breakpoint for this - the thing that
   actually varies is how many lines the rails wrap onto, and that depends on
   the seat count and step count as much as on the viewport. */
.table-view .felt {
  /* left/top only (not inset:0) so that when the camera pins an explicit pixel
     width/height for zoom, the box can grow down-right from the corner instead of
     being clamped by right:0/bottom:0. At 100% the width/height:100% fills the
     frame exactly as before. */
  position: absolute; left: 0; top: 0; width: 100%; height: 100%; min-height: 0;
  border-radius: 0; border: none; box-shadow: none;
  transform-origin: 0 0;
  /* No transform transition. Zoom (the `zoom` property) applies instantly, but a
     transitioned pan would slide the re-centering translate in over 0.18s - so
     every zoom step the content snaps to the top-left corner first (magnified but
     not yet recentred) and then glides back, which looks like the board edge gets
     cut off on zoom. Applying zoom and the recentre pan in the same frame keeps
     the view centred throughout. Pan-drag was already instant (transition:none). */
}
.felt-viewport.zoomed-in .felt { cursor: grab; }
.felt-viewport.zoomed-in .felt.panning { cursor: grabbing; }
/* Sharp zoom. The felt is a 3D stage (.table-stage sets perspective; the felt
   and every piece use preserve-3d for the die tumble and card lift). A 3D
   subtree is rasterized once as a GPU texture, so the felt's own transform:
   scale() zoom just magnifies that 1x bitmap - pieces go pixelated past 100%.
   While zoomed in we flatten the felt and its pieces to plain 2D, so Chromium
   re-rasterizes the vector art, SVG dice and text crisply at the zoom scale.
   The full 3D (lift/tumble depth) returns the moment zoom drops back to 100%,
   which is also the only time it's really on show. */
.felt-viewport.zoomed-in .felt,
.felt-viewport.zoomed-in .piece,
.felt-viewport.zoomed-in .piece-3d { transform-style: flat; }
/* The companion 900px query (felt-viewport to 118px top/bottom) is gone for the
   same reason as the frame's: syncRailInsets() measures the wrapped rails and
   writes the real number, so two lines, three or four all clear the felt. */

/* zoom controls: a small floating rail, bottom-right, mirroring the hint
   caption's treatment on the opposite corner */
.zoom-rail.floating {
  position: absolute; bottom: 16px; right: 16px; z-index: 20;
  display: flex; align-items: center; gap: 4px; padding: 5px;
  background: rgba(15,20,28,0.5); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,0.08); border-radius: 999px; box-shadow: var(--shadow);
}
.zoom-btn { padding: 7px; background: rgba(255,255,255,0.04); border-color: rgba(255,255,255,0.1); border-radius: 999px; }
.zoom-btn:hover { background: rgba(255,255,255,0.09); border-color: var(--accent); }
.zoom-btn:disabled { opacity: 0.35; cursor: default; pointer-events: none; }
.zoom-btn .tool-ic { display: flex; }
.zoom-btn svg { width: 14px; height: 14px; }
.zoom-read { font-size: 12px; font-weight: 700; color: var(--text-dim); min-width: 34px; text-align: center; font-variant-numeric: tabular-nums; }

/* A wager has no panel of its own: it is staked by moving coins on the felt and
   confirmed from the turn bar, so the readout and the Confirm live on .tphase
   pills with every other in-progress step. */

/* live "who's ahead on what" panel for majority-rule games - same left-center
   slot the build-mode tool rail uses below, since that rail is hidden the
   whole time this one is relevant (play mode only). */
.majority-rail.floating {
  position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
  z-index: 19; width: 172px; max-height: calc(100vh - 28px); max-height: calc(100dvh - 28px); overflow: hidden;
  background: rgba(15,20,28,0.55); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,0.08); border-radius: 14px; padding: 0;
  box-shadow: var(--shadow); display: none;
}
.majority-rail.floating.show { display: block; }
.majority-rail .rail-body { gap: 2px; }
.majority-rail .tool-group-label:first-child { margin-top: 2px; }
.faction-summary { display: flex; flex-direction: column; gap: 5px; margin-bottom: 6px; }
.faction-summary-row { display: flex; align-items: center; gap: 6px; font-size: 12px; }
.faction-summary-ic { display: flex; color: var(--text-faint); }
.faction-summary-ic svg { width: 13px; height: 13px; }
.faction-summary-name { flex: 1; font-weight: 700; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.faction-summary-score { font-variant-numeric: tabular-nums; font-weight: 800; color: var(--text-dim); }
.faction-summary-score.secured { color: #8fd6a4; }
.faction-row { display: flex; align-items: center; gap: 7px; padding: 5px 1px; border-bottom: 1px solid rgba(255,255,255,0.06); }
.faction-row:last-of-type { border-bottom: none; }
.faction-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; box-shadow: 0 0 0 1px rgba(255,255,255,0.18); }
.faction-name { font-size: 11.5px; font-weight: 700; color: var(--text-dim); flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.faction-chips { display: flex; align-items: center; gap: 4px; }
.faction-chip {
  display: flex; align-items: center; gap: 3px; padding: 2px 5px; border-radius: 999px;
  background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.08);
  font-size: 10.5px; font-weight: 700; color: var(--text-faint);
}
.faction-chip-ic { display: flex; }
.faction-chip-ic svg { width: 10px; height: 10px; }
.faction-chip.leads { background: rgba(90,168,110,0.2); border-color: rgba(90,168,110,0.55); color: #8fd6a4; }
.faction-check { display: flex; color: #8fd6a4; }
.faction-check svg { width: 11px; height: 11px; stroke-width: 3; }
.faction-target { font-size: 10px; color: var(--text-faint); text-align: center; padding-top: 7px; margin-top: 3px; border-top: 1px solid rgba(255,255,255,0.08); }

/* left tool rail floats, semi-transparent, over the felt */
.table-tools.floating {
  position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
  z-index: 20; width: 132px; max-height: calc(100vh - 28px); max-height: calc(100dvh - 28px); overflow: hidden;
  padding: 0;
  background: rgba(15,20,28,0.5); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,0.08); box-shadow: var(--shadow);
}
.table-tools.floating .tool-btn { background: rgba(255,255,255,0.04); border-color: rgba(255,255,255,0.1); }
.table-tools.floating .tool-btn:hover { background: rgba(255,255,255,0.09); border-color: var(--accent); }

/* Drag/minimise header shared by the build tool rail and the play-mode
   majority tracker. Both float over the felt (no reserved band anymore), so
   both carry a grip to move them and a button to collapse them out of the way.
   The head is the drag handle; the min button toggles the body. */
.rail-head {
  display: flex; align-items: center; gap: 6px;
  padding: 8px 8px 7px 10px; cursor: grab; user-select: none;
  border-bottom: 1px solid rgba(255,255,255,0.08);
}
.rail-head:active { cursor: grabbing; }
.floating.dragging { cursor: grabbing; }
.rail-grip { display: flex; align-items: center; color: var(--text-faint); flex: 0 0 auto; }
.rail-grip svg { width: 14px; height: 14px; }
.rail-title {
  flex: 1 1 auto; min-width: 0; font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.7px; color: var(--text-faint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rail-min {
  flex: 0 0 auto; display: flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; border-radius: 7px; cursor: pointer;
  background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.1); color: var(--text-dim);
}
.rail-min:hover { border-color: var(--accent); color: var(--text); }
.rail-min svg { width: 13px; height: 13px; }
.rail-body {
  display: flex; flex-direction: column; gap: 6px;
  padding: 10px; max-height: calc(100vh - 96px); max-height: calc(100dvh - 96px); overflow: auto;
}
/* Collapsed: head only, body gone, panel shrinks to the handle. */
.floating.minimized .rail-body { display: none; }
.floating.minimized .rail-head { border-bottom: none; }
.floating.minimized { width: auto; }
/* A minimised rail parks inline in the top-left header bar, right after the
   game name, so the collapsed handle always sits in the same known place on the
   dark strip instead of floating out on the felt. It's reparented into the
   .table-top-left flex row, so here it just drops absolute positioning and flows
   as a normal flex child. Three classes keeps it ahead of the base
   .table-tools.floating / .majority-rail.floating placement rules. */
.floating.minimized.docked {
  position: static; left: auto; top: auto; right: auto; bottom: auto; transform: none;
  z-index: auto; max-height: none;
}

/* Side-docked ("clicked in") state: dragging a floating rail hard against the
   left or right edge snaps it flush there as a full-height side bar. Shared by
   the build tool rail and the play-mode majority tracker (both use attachRailChrome).
   The docked inner corner is squared so it reads as attached to the edge. */
/* Snap flush to the FELT's edge, not the stage's - the felt-viewport is inset
   22px left/right (see .felt-viewport), so docking at 22px keeps the black
   border empty. Still vertically centered on the felt (equal top/bottom insets
   mean the felt's center is the stage's center). */
.floating.railed { top: 50%; bottom: auto; transform: translateY(-50%); }
.floating.railed-left { left: 22px; right: auto; border-top-left-radius: 0; border-bottom-left-radius: 0; }
.floating.railed-right { right: 22px; left: auto; border-top-right-radius: 0; border-bottom-right-radius: 0; }
/* Preview while a drag hovers a snap edge: a bright inner border on the side it
   will click into, so the target is obvious before releasing. */
.floating.will-rail-left { box-shadow: inset 3px 0 0 var(--accent), var(--shadow); }
.floating.will-rail-right { box-shadow: inset -3px 0 0 var(--accent), var(--shadow); }

/* seats top-center, phases bottom-center, all floating.
   Both are centred with a translate and both wrap, so without a cap they grow
   sideways out of the stage - and .view.table-view clips with overflow:hidden,
   so what runs off the edge is gone rather than scrollable. The cap keeps them
   inside the felt's own 22px gutters (14px of stage margin each side plus a
   hair), which forces the wrap to happen instead of the overflow. justify-content
   keeps a wrapped row centred rather than left-ragged. */
.seat-rail.floating {
  position: absolute; top: 14px; left: 50%; transform: translateX(-50%); z-index: 20; margin: 0;
  max-width: calc(100% - 28px); justify-content: center;
}
.tphase-bar.floating {
  position: absolute; bottom: 14px; left: 50%; transform: translateX(-50%); z-index: 20; margin: 0;
  max-width: calc(100% - 28px); justify-content: center;
  /* NO CAPSULE AT REST. The glass pill existed to gather a row of small pills
     into one surface; a hand of cards is already one object and gathers itself,
     so the ring around it was a second frame saying the same thing. It comes
     back the moment the workbench unfolds, because THEN the bar really is the
     bottom edge of a sheet (see .bench-open below) - which is the one job the
     glass was doing that the cards cannot do for themselves. */
  background: none; backdrop-filter: none; border: none; border-radius: 0;
  padding: 0 8px; gap: 8px;
}
/* With no surface behind them, the bar's own words sit straight on the felt, so
   they carry their own contrast rather than borrowing the glass's. */
.tphase-bar.floating > .lbl { text-shadow: 0 1px 3px rgba(0,0,0,0.7); }

/* ================= THE WORKBENCH (etappe 1) =================
   A step's settings used to live in a right-click menu. They live here now: a
   panel that the lane bar UNFOLDS into, flush against the bar's top edge and
   growing upward over the felt.

   THE PANEL IS A CHILD OF THE BAR, and that is the whole trick. Flush was the
   thing that had to be exact - the sketch's finding was that a panel merely
   NEAR the bar reads as a window that appeared, where one growing out of its
   top edge reads as the bar opening - and `bottom: 100%` inside the bar is
   flush by construction. No measuring of the bar's height (which changes with
   a second lane open), no second absolute box to keep in step with the first,
   and the panel follows the bar wherever the bar goes.

   The felt does NOT shrink for it. A shrinking felt would have to drag every
   card, coin and die on it, and a card's size is its own setting in pixels, so
   a shorter felt crowds rather than scales - measured in
   dev/lane-workbench-placements.html, where a 168px bench left the cards lying
   over the zones. The panel floats instead, and the other blocks dim so it is
   clear which step is open. */
.tphase-bench {
  /* -1px, not 0. The bar's own 1px border sits OUTSIDE the box this offset is
     measured from, so left:0 lands the panel two pixels narrower than the bar
     and the seam reads as a step on each side - the one thing that stops it
     looking like a single sheet. Measured, not guessed. */
  position: absolute; left: -1px; right: -1px; bottom: 100%;
  /* 200px is the resting height Siem picked, and it is a FLOOR rather than a
     fixed size. A step with ten settings in a 200px box shows four of them and
     scrolls the rest, which is the disappearing-control bug the whole panel
     exists to undo - it would be a menu's door with a scrollbar for a handle.
     So the panel grows to its content and stops well short of the table: the
     ceiling is what keeps a long step from becoming a wall over the felt. */
  /* 200px is the resting height Siem picked, and it is a FLOOR: a step with a
     long rule in a fixed box shows a fraction of it and scrolls the rest, which
     is a door with a scrollbar for a handle. The CEILING is the real number to
     tune by eye - the panel floats over the felt rather than shrinking it, so
     every pixel here is a pixel of table covered. At 50vh a 620px-tall window
     gives the builder 310px and leaves the felt's top half clear. */
  min-height: var(--bench-h, 200px); max-height: min(400px, 50vh);
  display: flex; flex-direction: column;
  background: rgba(15, 20, 28, 0.5); backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 255, 255, 0.08); border-bottom: none;
  border-radius: 16px 16px 0 0;
  overflow: hidden; text-align: left;
}
/* ONE SHEET OF GLASS. The bar takes the panel's width, the seam loses its
   border, and the corners that meet go square - otherwise two rounded edges
   touch and it reads as a panel resting ON the bar rather than the same
   surface opened up. The bar's width is the only layout this costs. */
.tphase-bar.floating.bench-open {
  width: min(920px, calc(100% - 28px));
  background: rgba(15,20,28,0.5); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,0.08); border-top: none;
  border-radius: 0 0 16px 16px; padding: 7px 14px;
}
/* Which step is open, said on the blocks themselves. The others go quiet
   rather than away: a lane that empties while you work in it loses the thing
   the panel is for, which is seeing the step in its sequence. */
.tphase-bar.floating.bench-open .tphase.blk { opacity: 0.4; }
.tphase-bar.floating.bench-open .tphase.blk.bench-on { opacity: 1; border-color: var(--accent); }
/* the same for a hand: the card being worked on stays lit and the rest go quiet.
   A card's colour lives on its face, so the ring is set there and not on the
   slot, which paints nothing. */
.tphase-bar.floating.bench-open .tphase.stepcard { opacity: 0.4; }
.tphase-bar.floating.bench-open .tphase.stepcard.bench-on { opacity: 1; }
.tphase-bar.floating.bench-open .tphase.stepcard.bench-on .tphase-stepcardface { border-color: var(--accent); }

.tphase-bench-head {
  display: flex; align-items: center; gap: 10px; flex: none;
  padding: 10px 12px; border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}
/* The head is THE BLOCK ITSELF - the same icon, name and badge that is sitting
   in the lane - so there is never a moment of wondering which step this is. It
   is a still copy, not a control: nothing here is clickable except the name. */
.tphase-bench-head .tphase.blk { cursor: default; opacity: 1; border-color: var(--accent); }
.tphase-bench-head .tphase.blk .ph-x { display: none; }
/* the same, for the head's copy of a step CARD: it stands upright in a hand of
   one, reads as the step this panel is about, and takes no gestures of its own. */
.tphase-bench-head .tphase-lanehand.bench-lanehand { flex: none; align-self: center; }
.tphase-bench-head .tphase.stepcard { cursor: default; opacity: 1; }
.tphase-bench-head .tphase.stepcard .tphase-stepcardface { border-color: var(--accent); }
.tphase-bench-head .tphase.stepcard .ph-x { display: none; }
/* its name is on, since the head is a label rather than something to hover */
.tphase-bench-head .tphase.stepcard .tphase-stepcardface > span:not(:has(svg)):not(.tphase-badge) { opacity: 1; transform: translateY(calc(-1 * var(--icup, 9px))); }
.tphase-bench-head .tphase.stepcard .tphase-ic,
.tphase-bench-head .tphase.stepcard .tphase-stepcardface > span:has(svg) { transform: translate(-50%, -50%) translateY(calc(-1 * var(--icup, 9px))); }
.tphase-bench-head .tphase.stepcard > .tphase-badge { opacity: 1; transform: translateX(-50%) translateY(0); }
.tphase-bench-head .bench-spacer { flex: 1 1 auto; }
.tphase-bench-back, .tphase-bench-x {
  flex: none; display: inline-flex; align-items: center; gap: 6px;
  background: rgba(255, 255, 255, 0.05); color: var(--text-dim);
  border: 1px solid var(--line-2); border-radius: 8px;
  padding: 6px 10px; font-size: 12px; font-weight: 700; cursor: pointer;
}
.tphase-bench-back:hover, .tphase-bench-x:hover { color: var(--text); border-color: var(--accent-2); }
.tphase-bench-back svg, .tphase-bench-x svg { width: 13px; height: 13px; }

/* THE BODY, and the one thing it has that a menu never did: ROOM. Every row a
   step menu can build carries a `sub` saying what that setting is currently on,
   and the menu deliberately throws it away to stay one line per row. Here it is
   printed, which is most of why the panel is worth having. */
/* THE HANDLE STRIP - the rows that are not what the step does. Deliberately
   quieter and smaller than a tile: what the step DOES is the pieces below, and
   these are who takes it, how often, which lane, and removing it. The strip is
   temporary by design and empties itself as each of these gets its own piece. */
.tphase-bench-strip {
  flex: none; display: flex; flex-wrap: wrap; gap: 6px;
  padding: 8px 12px; border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}
.bench-chip {
  display: inline-flex; align-items: center; gap: 6px; text-align: left;
  padding: 5px 9px; border-radius: 999px; cursor: pointer;
  background: rgba(255, 255, 255, 0.04); color: var(--text-dim);
  border: 1px solid var(--line-2); font-size: 11.5px; font-weight: 700;
}
.bench-chip:hover { color: var(--text); border-color: var(--accent-2); }
.bench-chip > .bench-ic { width: 16px; height: 16px; background: none; border-radius: 0; }
.bench-chip > .bench-ic svg { width: 12px; height: 12px; }
.bench-chip > .bench-chip-txt > span { display: block; line-height: 1.15; }
.bench-chip > .bench-chip-txt > small { display: block; color: var(--text-faint); font-size: 10px; font-weight: 600; line-height: 1.15; }
.bench-chip.on { color: var(--text); border-color: rgba(201, 162, 77, 0.5); }
.bench-chip.on > .bench-ic { color: var(--accent); }
.bench-chip.danger { color: var(--red); border-color: rgba(200, 80, 80, 0.3); }
.bench-chip.disabled { cursor: default; opacity: 0.45; }
.bench-chip.disabled:hover { color: var(--text-dim); border-color: var(--line-2); }

/* THE STEP'S OWN SENTENCE, said in the same pieces the rules under it are made
   of. Siem: "ik wil die settings als puzzelstukjes die aan elkaar vastzitten in
   hun eigen rij erboven", and TWO short runs rather than one long one is his
   pick off dev/bench-toprow-sketch.html - measured over the real panel, one run
   holding both sentences ran to 767px of the 896 a panel has and wrapped the
   moment the window narrowed, while the two apart are 396px and never wrap.
   They are statements ABOUT the step (where it happens, what it does when it is
   done), so they sit above the rules rather than in them. */
.tphase-bench-says {
  flex: none; display: flex; flex-direction: column; gap: 2px;
  padding: 6px 12px 7px; border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}
.tphase-bench-says .pb-run { flex: 0 1 auto; min-height: 0; }
/* A word nothing can change is still part of the sentence - it just offers no
   menu, so it must not offer a pointer either. */
.pb-piece.pb-fixed { cursor: default; }
/* The two gestures that are not settings: what kind of step this is, and taking
   it off the lane. They ride the head, beside Done, because they act on the
   BLOCK rather than on anything the step says. */
/* Its OWN class, not the Done button's. Sharing .tphase-bench-x made "the
   panel's close control" ambiguous the moment a second control wore it - the
   same split the .brd-* / .bpick-* namespaces exist for. */
.bench-act {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255, 255, 255, 0.05); color: var(--text-dim);
  border: 1px solid var(--line-2); border-radius: 8px;
  padding: 6px 8px; cursor: pointer;
}
.bench-act:hover { color: var(--text); border-color: var(--accent-2); }
.bench-act svg { width: 13px; height: 13px; }
.bench-act.danger { color: var(--red); border-color: rgba(200, 80, 80, 0.3); }
.bench-act.danger:hover { color: var(--red); border-color: rgba(200, 80, 80, 0.55); }
.bench-act.disabled { opacity: 0.45; cursor: default; }
.bench-act.disabled:hover { color: var(--text-dim); border-color: var(--line-2); }
.tphase-bench-body { flex: 1 1 auto; overflow: auto; padding: 10px 12px; }
/* The piece builder lives IN the panel rather than in a window over it - Siem's
   complaint about the old one was that it was "gewoon een nieuwe window of
   modal". Its embedded form already exists; this only takes the box it sits in
   down to the panel's own width and lets the panel do the scrolling. */
.tphase-bench-body > .pb-panel.pb-embed {
  width: auto; max-width: none; max-height: none; border: none; background: none;
}
.tphase-bench-body > .pb-panel.pb-embed > .pb-head { display: none; }
.tphase-bench-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(232px, 1fr)); gap: 8px; }
.bench-tile {
  display: flex; align-items: flex-start; gap: 9px; width: 100%;
  padding: 9px 10px; text-align: left; cursor: pointer;
  background: rgba(255, 255, 255, 0.035); color: var(--text);
  border: 1px solid var(--line-2); border-radius: 10px;
}
.bench-tile:hover { border-color: var(--accent-2); background: rgba(255, 255, 255, 0.06); }
.bench-tile > .bench-ic {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 6px;
  background: rgba(255, 255, 255, 0.07); color: var(--accent-2);
}
.bench-tile > .bench-ic svg { width: 13px; height: 13px; }
.bench-tile > .bench-txt { flex: 1 1 auto; min-width: 0; }
.bench-tile > .bench-txt > span { display: block; font-size: 12.5px; font-weight: 700; line-height: 1.2; }
.bench-tile > .bench-txt > small { display: block; margin-top: 2px; color: var(--text-dim); font-size: 11px; font-weight: 600; line-height: 1.25; }
.bench-tile > .bench-mark {
  flex: none; align-self: center; padding: 2px 7px; border-radius: 999px;
  background: rgba(255, 255, 255, 0.07); color: var(--text-dim);
  font-size: 10.5px; font-weight: 700; white-space: nowrap;
}
.bench-tile.on { border-color: rgba(201, 162, 77, 0.55); }
.bench-tile.on > .bench-ic { color: var(--accent); background: rgba(201, 162, 77, 0.14); }
.bench-tile.danger { color: var(--red); }
.bench-tile.danger > .bench-ic { color: var(--red); background: rgba(200, 80, 80, 0.12); }
.bench-tile.disabled { cursor: default; opacity: 0.5; }
.bench-tile.disabled:hover { border-color: var(--line-2); background: rgba(255, 255, 255, 0.035); }
.bench-note { grid-column: 1 / -1; color: var(--text-dim); font-size: 11.5px; font-weight: 600; padding: 2px 2px 4px; }
.bench-head { grid-column: 1 / -1; color: var(--text-faint); font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.8px; padding: 6px 2px 2px; }
/* A menu opened from a bench tile is DRAWN IN THE BENCH, not floated over it -
   same rows, same glass, one surface. It gets the full width here rather than a
   menu's own narrow column. */
.tphase-bench-body > .ctx-menu {
  position: static; transform: none; width: auto; max-width: none; max-height: none;
  background: none; border: none; box-shadow: none; backdrop-filter: none; padding: 0;
}

/* corner controls: home + name on the left, play/edit on the right */
.table-top-left { position: absolute; top: 14px; left: 14px; z-index: 25; display: flex; align-items: center; gap: 8px; }
.table-top-right { position: absolute; top: 14px; right: 14px; z-index: 25; display: flex; align-items: center; gap: 8px; }
/* A toolbar button whose rule this game doesn't carry goes idle rather than
   disappearing - "Keep cards apart" on a game with no such rule is the case.
   A CLASS and not the `disabled` attribute, deliberately: a disabled button
   fires no pointer events, so the tooltip saying why it is idle and where to
   turn the rule on would never appear. It stays hoverable and refuses the
   press in its own handler. */
.table-top-right .btn.row-off { opacity: 0.4; cursor: default; }
.table-top-right .btn.row-off:hover { color: var(--text-dim); background: transparent; border-color: transparent; }
.table-top-center { position: absolute; top: 14px; left: 50%; transform: translateX(-50%); z-index: 25; display: flex; flex-direction: column; align-items: center; gap: 8px; }
.ce-lib-row { display: flex; gap: 6px; }
.ce-lib-row .tool-btn { width: 42px; min-width: 42px; height: 40px; min-height: 40px; padding: 0;
  background: rgba(15,20,28,0.55); backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.1); }
.ce-lib-row .tool-btn:hover { background: rgba(15,20,28,0.7); border-color: var(--accent); transform: none; }
.ce-lib-row .tool-btn:disabled { opacity: 0.32; cursor: default; }
.ce-lib-row .tool-btn:disabled:hover { background: rgba(15,20,28,0.55); border-color: rgba(255,255,255,0.1); }
.ce-lib-div { width: 1px; align-self: stretch; margin: 4px 2px; background: rgba(255,255,255,0.14); }
.ce-lib-row .tool-btn.ce-lib-save .tool-ic { color: var(--accent); }
.ce-lib-row .tool-btn.ce-lib-save:hover { border-color: var(--accent); background: rgba(216,162,74,0.16); }
.ce-lib-row .tool-btn.ce-lib-discard .tool-ic { color: #d6604d; }
.ce-lib-row .tool-btn.ce-lib-discard:hover { border-color: #d6604d; background: rgba(214,96,77,0.16); }
.table-top-left .btn, .table-top-right .btn-ghost {
  background: rgba(15,20,28,0.55); backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.1);
}
/* Slim the top-bar controls so they read as light as the phase pills below the
   felt - smaller icon buttons, rounded like the pills, and a smaller name chip. */
.table-top-left .btn, .table-top-right .btn { padding: 7px; border-radius: 999px; }
.table-top-left .btn svg, .table-top-right .btn svg { width: 16px; height: 16px; }
.table-name {
  background: rgba(15,20,28,0.55); backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.1);
  color: var(--text); border-radius: 999px; padding: 6px 13px; font-weight: 700; font-size: 13px; cursor: pointer;
}
.table-name:hover { border-color: var(--accent); color: var(--accent-2); }

/* tiny floating caption, bottom-left */
.table-hint {
  position: absolute; bottom: 24px; left: 16px; z-index: 18;
  display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--text-dim);
  background: rgba(15,20,28,0.5); backdrop-filter: blur(6px); padding: 5px 10px; border-radius: 8px;
}
.table-hint svg { width: 13px; height: 13px; color: var(--accent-2); }

/* a card tapped into a Cat-combo selection: lifted and rimmed so it reads as
   "picked" without needing a drag, distinct from the drag-lift's .lifted */
.piece.combo-selected .piece-3d { transform: translateZ(30px) translateY(-10px); }
.piece.combo-selected .tpiece-card .tcard { box-shadow: 0 0 0 3px var(--accent), 0 14px 26px rgba(0,0,0,0.55); }

/* a market card picked for this turn's draft - same "lifted and rimmed" read
   as a combo selection, so both selection styles feel like one language */
.piece.market-selected .piece-3d { transform: translateZ(30px) translateY(-10px); }
.piece.market-selected .tpiece-card .tcard { box-shadow: 0 0 0 3px var(--accent-2), 0 14px 26px rgba(0,0,0,0.55); }

/* floating bar above the phase pill, showing what the current tap-selection
   resolves to (or that it doesn't yet) with a Play/Clear action */
.combo-bar {
  position: absolute; bottom: 78px; left: 50%; transform: translateX(-50%); z-index: 21;
  display: flex; align-items: center; gap: 12px; padding: 9px 10px 9px 16px;
  background: rgba(15,20,28,0.85); backdrop-filter: blur(8px);
  border: 1px solid rgba(216,162,74,0.4); border-radius: 999px; box-shadow: var(--shadow);
  animation: aiPop 0.2s ease;
}
.combo-bar-label { font-size: 12.5px; font-weight: 700; color: var(--text-dim); white-space: nowrap; }
.combo-bar-actions { display: flex; gap: 6px; }

/* big readable card view (right-click a card) */
.inspect-wrap { display: flex; justify-content: center; padding: 6px 0 2px; }
.inspect-wrap .tcard { width: 300px; }

/* a chosen wild-color note, sits between the card and (if present) the
   effect section below - no rule existed for this before, so it rendered
   flush against whatever came next with no breathing room at all. */
.chosen-note { display: flex; align-items: center; gap: 8px; margin-top: 18px; font-size: 13.5px; color: var(--text-dim); }
.chosen-note-sw { width: 15px; height: 15px; border-radius: 50%; flex-shrink: 0; border: 1px solid rgba(255,255,255,0.25); }

/* "What this card does" - plain-English rules text under the enlarged card,
   for effects and category powers that aren't written on the card face */
.inspect-effect { margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--line); }
.inspect-effect-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 12px; text-transform: uppercase; letter-spacing: 1px; color: var(--text-faint); font-weight: 700; }
.inspect-effect-head svg { width: 15px; height: 15px; color: var(--accent-2); }
.inspect-effect-list { margin: 0; padding-left: 20px; color: var(--text-dim); line-height: 1.55; font-size: 14px; }
.inspect-effect-list li { margin-bottom: 6px; }
.inspect-effect-list li:last-child { margin-bottom: 0; }

/* ---- in-place paint surface (card art painting, drawn on the card itself) ----
   The canvas is overlaid on the real .tcard-free-inner and stretched to it, so
   painting happens on the card at its own size; the tools live in a compact
   panel (the editor's side panel, or the card-back modal). Sized at the
   editor's own small scale, matching the phase pills and build rail. */
.paint-overlay {
  position: absolute; inset: 0; z-index: 0; /* match .free-art: the drawing sits UNDER text, badges, and effects, exactly where the committed art lands */
  width: 100%; height: 100%;
  border-radius: inherit; overflow: hidden; /* match .free-art: strokes stay inside the face */
  cursor: crosshair; touch-action: none;
}
/* While painting, the canvas is the only interactive layer: every printed
   element (name, values, effects, shapes) is lifted above it by z-index but
   drops its pointer events, so a stroke started over text still reaches the
   canvas below and paints beneath that text, not on top of it. */
.tcard-free-inner.painting > *:not(.paint-overlay) { pointer-events: none; }

/* Brush-size cursor ring: a circle the size of the brush's footprint that
   follows the pointer over the canvas. A white edge over a dark outline so it
   reads on any artwork; body-level and pointer-events off so it never blocks. */
.paint-cursor {
  position: fixed; z-index: 99999; pointer-events: none;
  border-radius: 50%; transform: translate(-50%, -50%);
  border: 1.5px solid rgba(255,255,255,0.92);
  box-shadow: 0 0 0 1.5px rgba(0,0,0,0.5), inset 0 0 0 1px rgba(0,0,0,0.28);
}

.paint-panel { display: flex; flex-direction: column; gap: 10px; }
.paint-head { display: flex; align-items: center; gap: 8px; }
.paint-head-ic { display: inline-flex; color: var(--accent-2); }
.paint-head-ic svg { width: 16px; height: 16px; }
.paint-title { font-weight: 800; font-size: 13px; }

.paint-group { display: flex; flex-direction: column; gap: 6px; }
.paint-glabel { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--text-faint); font-weight: 700; }

.paint-tools { display: flex; gap: 6px; flex-wrap: wrap; }
.paint-tool {
  width: 30px; height: 30px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--bg-3); border: 1px solid var(--line);
  border-radius: 8px; color: var(--text-dim); cursor: pointer;
  transition: border-color .12s, color .12s, background .12s;
}
.paint-tool:hover { color: var(--text); border-color: var(--accent); background: var(--panel-2); }
.paint-tool.sel { color: var(--accent-ink); background: linear-gradient(180deg, var(--accent-2), var(--accent)); border-color: var(--accent); }
.paint-tool svg { width: 16px; height: 16px; }

.paint-size-row { display: flex; align-items: center; gap: 10px; min-height: 30px; }
.paint-size { flex: 1; accent-color: var(--accent); cursor: pointer; }
.paint-size-dot { border-radius: 50%; flex-shrink: 0; border: 1px solid rgba(255,255,255,0.35); transition: width .1s, height .1s; }

/* Paintbrush-only settings (edge hardness + flow), shown when it's selected. */
.paint-brushonly { display: flex; flex-direction: column; gap: 10px; }
.paint-cap { font-size: 9.5px; letter-spacing: 0.3px; color: var(--text-faint); flex-shrink: 0; min-width: 26px; }
.paint-cap:last-child { text-align: right; }

.paint-color-row { display: flex; align-items: center; gap: 8px; }
.paint-swatches { display: grid; grid-template-columns: repeat(8, 1fr); gap: 5px; flex: 1; }
.paint-swatch {
  width: 100%; aspect-ratio: 1; border-radius: 5px;
  border: 1px solid rgba(255,255,255,0.14); cursor: pointer; padding: 0;
  transition: transform .1s, box-shadow .1s;
}
.paint-swatch:hover { transform: scale(1.1); }
.paint-swatch.sel { box-shadow: 0 0 0 2px var(--panel), 0 0 0 3px var(--accent); }
.paint-custom {
  width: 30px; height: 30px; padding: 0; flex-shrink: 0;
  background: var(--bg-3); border: 1px solid var(--line); border-radius: 8px; cursor: pointer;
}

.paint-actions { display: flex; gap: 6px; margin-top: 2px; }
.paint-actions .btn { flex: 1; justify-content: center; }

/* Card-back modal hosts the same paint panel; keep it centred and roomy. */
.cardback-draw-tools { margin-top: 10px; max-width: 240px; margin-left: auto; margin-right: auto; }

/* ===== combat step: attacker / target highlights + targeting arrow =====
   Applied to live .piece nodes while a combat step is aiming. Attackers glow
   gold (the tool accent), targets glow red; a crosshair cursor says "aim". */
.piece.can-attack { cursor: crosshair; }
.piece.can-target { cursor: crosshair; }
/* Glow lives on the real card (.tcard) so it follows the card's own rounded
   corners; even (non-negative) spread keeps the halo the same thickness at the
   corners as along the flat edges. Trailing base shadow keeps the lift. */
.piece.can-attack .tpiece-card .tcard { box-shadow: 0 0 0 2px var(--accent), 0 0 18px 1px rgba(216,162,74,0.7), 0 1px 2px rgba(0,0,0,0.35); }
.piece.attacker-aimed .tpiece-card .tcard { box-shadow: 0 0 0 2px var(--accent), 0 0 26px 3px rgba(216,162,74,0.95), 0 1px 2px rgba(0,0,0,0.35); }
.piece.can-target .tpiece-card .tcard { box-shadow: 0 0 0 2px #d6604d, 0 0 16px 1px rgba(214,96,77,0.7), 0 1px 2px rgba(0,0,0,0.35); }
/* Resting: one of your own armed cards that only arrived this turn, so it can't
   attack yet. It reads as cooled and asleep against the gold of the ready
   attackers beside it - dimmed, desaturated, and ringed in a soft dashed line
   rather than a solid glow, since a dash says "not yet" where a ring says "go". */
.piece.atk-resting { cursor: not-allowed; }
.piece.atk-resting .tpiece-card { position: relative; } /* anchor for the dashed rim below */
.piece.atk-resting .tpiece-card .tcard { filter: saturate(0.45) brightness(0.72); box-shadow: 0 0 0 2px rgba(150,160,175,0.55), 0 1px 2px rgba(0,0,0,0.35); }
.piece.atk-resting .tpiece-card::after {
  content: ""; position: absolute; inset: -4px; border-radius: 12px; pointer-events: none;
  border: 1.5px dashed rgba(178,190,205,0.7); animation: atk-rest-breathe 2.4s ease-in-out infinite;
}
@keyframes atk-rest-breathe { 0%, 100% { opacity: 0.35; } 50% { opacity: 0.8; } }
@media (prefers-reduced-motion: reduce) { .piece.atk-resting .tpiece-card::after { animation: none; opacity: 0.6; } }
.piece.combat-target-hot .tpiece-card .tcard { box-shadow: 0 0 0 3px #ff5a44, 0 0 22px 3px rgba(255,90,68,0.95), 0 1px 2px rgba(0,0,0,0.35); }
.piece.combat-hit .tpiece-card { animation: combat-shake .34s ease; }
@keyframes combat-shake {
  0%, 100% { transform: translate(0, 0) rotate(0); }
  20% { transform: translate(-3px, 1px) rotate(-2deg); }
  45% { transform: translate(3px, -1px) rotate(2deg); }
  70% { transform: translate(-2px, 1px) rotate(-1deg); }
}
/* the targeting arrow, an SVG layer over the felt. pointer-events:none so the
   card under the cursor is still the target of the click/drop. */
.combat-arrow-layer { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 400; overflow: visible; }
.combat-arrow-line { stroke: var(--accent); stroke-width: 4; stroke-linecap: round; opacity: 0.95; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.5)); }
.combat-arrow-head { fill: var(--accent); filter: drop-shadow(0 1px 2px rgba(0,0,0,0.5)); }
.combat-arrow-layer.strike .combat-arrow-line { stroke: #ff5a44; stroke-width: 5; }
.combat-arrow-layer.strike .combat-arrow-head { fill: #ff5a44; }

/* ---- life counters ----------------------------------------------------
   A kraft cardboard life counter with two combination-lock wheels. Each wheel
   carries 0-9 printed upright around its rim; the digit under the pointer is the
   live one, so the pair reads 00-99. Spin a wheel to change its digit, and an
   attack spins them down to the new total. Sized off the same --pscale every
   other piece reads. The seat colour tints the two pointers for identity. */
.life-counter {
  --lc-h: calc(46px * var(--pscale, 1));
  position: relative;
  width: calc(var(--lc-h) * 2.8); height: calc(var(--lc-h) * 1.45);
  border-radius: calc(var(--lc-h) * 0.22);
  cursor: grab; user-select: none;
  color: #2b1d0e;
}
/* The cardboard sheet the wheels are mounted on - a real kraft photo with a
   thick laser-cut ply edge. */
.life-frame {
  position: absolute; inset: 0;
  box-sizing: border-box;
  display: flex; align-items: center; justify-content: center;
  gap: calc(var(--lc-h) * 0.24);
  padding-bottom: calc(var(--lc-h) * 0.2);
  border-radius: calc(var(--lc-h) * 0.22);
  background-image: url('../assets/textures/kraft-ribbed.jpg');
  background-size: cover; background-position: center;
  border: calc(var(--lc-h) * 0.05) solid #8a6533;
  box-shadow:
    0 6px 10px rgba(0,0,0,0.45),
    0 0 0 calc(var(--lc-h) * 0.03) #6e4f27,
    inset 0 -4px 8px rgba(90,60,25,0.3), inset 0 2px 3px rgba(255,255,255,0.22);
}
/* The player's name, printed across the bottom of the sheet. */
.life-name {
  position: absolute; bottom: calc(var(--lc-h) * 0.045); left: 50%; transform: translateX(-50%);
  max-width: 82%;
  font-size: calc(var(--lc-h) * 0.13); line-height: 1;
  letter-spacing: 0.12em; text-transform: uppercase; font-weight: 700;
  color: rgba(43,29,14,0.78);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  pointer-events: none;
}
/* Each wheel sits in a fixed slot; the pointer above the slot marks the live
   digit and the slot never rotates, only the wheel inside it does. */
.life-slot {
  position: relative; flex: 0 0 auto;
  width: calc(var(--lc-h) * 1.25); height: calc(var(--lc-h) * 1.25);
  display: flex; align-items: center; justify-content: center;
}
/* A seat-colour pointer tab dropping onto the live digit. */
.life-ptr {
  position: absolute; z-index: 3; top: calc(var(--lc-h) * -0.04); left: 50%;
  width: 0; height: 0; transform: translateX(-50%);
  border-left: calc(var(--lc-h) * 0.1) solid transparent;
  border-right: calc(var(--lc-h) * 0.1) solid transparent;
  border-top: calc(var(--lc-h) * 0.14) solid var(--life-color, #c0432f);
  filter: drop-shadow(0 1px 1px rgba(0,0,0,0.45));
  pointer-events: none;
}
/* A faint highlight behind the digit sitting under the pointer. */
.life-slot::before {
  content: ""; position: absolute; z-index: 2; pointer-events: none;
  top: calc(var(--lc-h) * 0.08); left: 50%; transform: translateX(-50%);
  width: calc(var(--lc-h) * 0.36); height: calc(var(--lc-h) * 0.34);
  border-radius: calc(var(--lc-h) * 0.08);
  background: rgba(255,250,235,0.22);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.22);
}
/* The wheel: a cardboard disc with a knurled rim. `--rot` (set inline) is its
   rotation; the numbers read it to place themselves and stay upright. */
.life-wheel {
  position: absolute; inset: 0; border-radius: 50%;
  --r: calc(var(--lc-h) * 0.46);
  background:
    repeating-conic-gradient(from 0deg, rgba(120,86,44,0.14) 0deg 3deg, rgba(120,86,44,0) 3deg 6deg),
    radial-gradient(circle at 40% 34%, #f0e3c7 0%, #d3bf95 62%, #b89b68 100%);
  box-shadow: 0 2px 4px rgba(0,0,0,0.45), inset 0 1px 2px rgba(255,255,255,0.55), inset 0 -4px 7px rgba(90,60,25,0.4);
  border: 1px solid #8a6f45;
  cursor: grab;
}
.life-wheel.spinning { cursor: grabbing; }
/* One number on the wheel: orbited to its angle (rot + its own base) at radius
   --r, then counter-rotated the same amount so it always reads upright. */
.life-num {
  position: absolute; left: 50%; top: 50%;
  font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
  font-weight: 800; font-size: calc(var(--lc-h) * 0.19);
  color: #3a2a18;
  transform:
    translate(-50%, -50%)
    rotate(calc(var(--rot) + var(--base)))
    translateY(calc(-1 * var(--r)))
    rotate(calc(-1 * (var(--rot) + var(--base))));
}
/* The hub cap pinning the wheel to the sheet. */
.life-hub {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: calc(var(--lc-h) * 0.4); height: calc(var(--lc-h) * 0.4);
  border-radius: 50%;
  background: radial-gradient(circle at 40% 34%, #fdfbf6, #d3ccbb 60%, #a49a84 100%);
  box-shadow: inset 0 1px 1px rgba(255,255,255,0.85), 0 1px 2px rgba(0,0,0,0.4);
  border: 1px solid #8f846b;
}
/* A knocked-out player's dial greys out where it stands. */
.life-counter.is-out { filter: grayscale(0.85) brightness(0.6); }
/* Combat: a life counter an attacker can strike glows red like a targetable
   card; the hot (hovered) target burns brighter; a resolved hit shakes it.
   The glow rings the oval (box-shadow follows its rounded radius). */
.piece.can-target .life-counter { box-shadow: 0 0 0 2px #d6604d, 0 0 16px 1px rgba(214,96,77,0.7), 0 8px 8px rgba(0,0,0,0.5); }
.piece.combat-target-hot .life-counter { box-shadow: 0 0 0 3px #ff5a44, 0 0 22px 3px rgba(255,90,68,0.95); }
.piece.life-hit .life-counter { animation: combat-shake 0.34s ease; }

/* ---- the value pulse --------------------------------------------------
   A number that just moved: it swells, colours green or red, and settles back
   to the size and colour it was. Queued and painted by the value pulse in
   00-open-state.js, which strips the class off again on a timer; nothing here
   decides WHEN it runs, and nothing here decides WHICH colour.

   The classes are good/bad rather than up/down on purpose, and that is not a
   naming preference. For every number a player holds - health, attack, points,
   coins - rising and good news are the same thing, but a PRICE runs the other
   way: a discount is the good news, and it makes the digit smaller. So the
   colour follows what the change was worth to whoever the number belongs to,
   and the class names have to say that or the one number that runs backwards
   makes liars of them. Which numbers those are is LOWER_IS_BETTER, in the
   value pulse; this file only ever paints what it is handed.

   Three surfaces, one vocabulary, and only one of them grows. .stat-pulse is
   a card's stat badge - a bare numeral on a card face with room around it, so
   it takes the swell. .life-pulse is a counter whose value lives on two
   rotating wheels, and every digit already carries a transform of its own
   that a scale would fight, so it takes the colour and leaves the motion to
   the spin the wheels are already doing. .seat-pulse is a readout inside
   .seat-stats, which collapses with overflow:hidden - a numeral growing past
   its pill there would be cut off rather than seen, so colour only again.

   Two things about the keyframes. There is no fill mode on any of them: when
   the animation stops applying, the number's own colour comes back by itself,
   which is what lets a badge with a hand-picked textColor pulse and land back
   on that colour with nothing written down here. And 0% / 100% are absent on
   purpose - the browser fills them in from the element's current computed
   style, which is the only way to start and end on a colour this stylesheet
   cannot name. (An animated declaration also outranks an inline style, which
   is what lets the colour win over that same hand-picked textColor.) */
/* --pulse-ms is the same 620ms PULSE_MS holds in 00-open-state.js, which is
   what strips the class again; the two have to agree, or the number either
   snaps back mid-swell or sits coloured after it has finished. */
:root { --pulse-good: #5fd48d; --pulse-bad: #ff6a55; --pulse-ms: 620ms; }
@keyframes valuePulseGood {
  18% { transform: scale(var(--pulse-scale, 1.5)); color: var(--pulse-good); }
  55% { color: var(--pulse-good); }
  100% { transform: scale(1); }
}
@keyframes valuePulseBad {
  18% { transform: scale(var(--pulse-scale, 1.5)); color: var(--pulse-bad); }
  55% { color: var(--pulse-bad); }
  100% { transform: scale(1); }
}
@keyframes valueTintGood { 18%, 55% { color: var(--pulse-good); } }
@keyframes valueTintBad { 18%, 55% { color: var(--pulse-bad); } }

.stat-pulse:not(.free-el) { display: inline-block; }
.stat-pulse.pulse-good { animation: valuePulseGood var(--pulse-ms, 620ms) cubic-bezier(0.22, 1, 0.36, 1); }
.stat-pulse.pulse-bad { animation: valuePulseBad var(--pulse-ms, 620ms) cubic-bezier(0.22, 1, 0.36, 1); }
/* A pulse that landed on the badge BOX instead of the numeral inside it (the
   defensive fallback in pulseNumEl, for a readout shaped in some way it does
   not recognise) must NOT scale. A .free-el carries the translate that centres
   it on the card face, and a keyframe writing transform drops that translate,
   so the badge would jump half its own width and come back. Colour only. */
.stat-pulse.free-el.pulse-good { animation-name: valueTintGood; }
.stat-pulse.free-el.pulse-bad { animation-name: valueTintBad; }

.life-pulse.pulse-good .life-num { animation: valueTintGood var(--pulse-ms, 620ms) ease; }
.life-pulse.pulse-bad .life-num { animation: valueTintBad var(--pulse-ms, 620ms) ease; }
.seat-pulse.pulse-good { animation: valueTintGood var(--pulse-ms, 620ms) ease; }
.seat-pulse.pulse-bad { animation: valueTintBad var(--pulse-ms, 620ms) ease; }

/* Reduce motion: the SWELL is motion, the COLOUR is information, so only the
   swell goes. The global reduce-motion block near the top of this file
   near-zeros every animation on the page, which would take the colour with it
   - so the colour is restated here as a plain declaration, held for exactly as
   long as the class sits on the element. Nothing moves at all. !important
   because a badge may carry a hand-picked textColor inline, and a normal class
   declaration loses to an inline style (only the animated one outranks it). */
body.reduce-motion .stat-pulse.pulse-good, body.reduce-motion .seat-pulse.pulse-good,
body.reduce-motion .life-pulse.pulse-good .life-num { color: var(--pulse-good) !important; }
body.reduce-motion .stat-pulse.pulse-bad, body.reduce-motion .seat-pulse.pulse-bad,
body.reduce-motion .life-pulse.pulse-bad .life-num { color: var(--pulse-bad) !important; }
@media (prefers-reduced-motion: reduce) {
  .stat-pulse.pulse-good, .seat-pulse.pulse-good, .life-pulse.pulse-good .life-num { animation: none; color: var(--pulse-good) !important; }
  .stat-pulse.pulse-bad, .seat-pulse.pulse-bad, .life-pulse.pulse-bad .life-num { animation: none; color: var(--pulse-bad) !important; }
}

/* ---- a value standing away from the one printed on the card ----
   The pulse above is an EVENT ("that just moved") and lasts 700ms. This is a
   STATE ("this is not what the card says any more") and lasts as long as it is
   true. Written by markModifiedValues in 00-open-state.js, derived at render
   from the live pool against the printed number, so nothing here has to be
   cleared - a heal back to full simply stops adding the class.

   It is a BAR under the numeral, not a colour on it, and that is the whole
   point rather than a stylistic preference. A builder who printed their health
   in red already has a red number: recolour it and a damaged card looks exactly
   like an untouched one, which is the single case this feature exists to
   answer. A builder who printed it green has their design overwritten on the
   cards combat touches most. The bar carries the colour itself and leaves the
   type alone, so it reads the same on a white numeral, a red one, and a red
   numeral on a red badge - and no card gives up its design to say it.

   ::after, so it costs no layout and can never resize the badge. Which is the
   same promise the swell makes: the box stays exactly where the builder put it,
   and only what is written inside it changes. */
.val-mod { position: relative; }
/* A numeral is usually a flex item already (blockified), but .tcard-cost prints
   its value with no span at all and falls back to the host itself, and a bare
   inline box cannot anchor an ::after at its own width. Never applied to a
   .free-el, whose translate centres it on the card face - see the pulse note
   above; position:relative is safe there, display is not. */
.val-mod:not(.free-el) { display: inline-block; }
.val-mod::after {
  content: "";
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  /* Sits just under the digits rather than on the badge's own bottom edge, so a
     tall badge does not park the bar half a numeral away from the number it is
     about. em, so it tracks the type size at every card scale - the numeral is
     sized in cqw and a px offset would drift from it. */
  top: 100%;
  margin-top: 0.08em;
  width: 100%;
  min-width: 0.6em;
  height: 0.1em;
  min-height: 1px;
  border-radius: 1px;
  background: currentColor;
  pointer-events: none;
}
.val-mod-good::after { background: var(--pulse-good); }
.val-mod-bad::after { background: var(--pulse-bad); }
/* The bar is information, not decoration, so reduce-motion leaves it alone -
   there is nothing here that moves. */

/* ---- the numeral swell (swellPieceValues, 00-open-state.js) ----
   For the length of an exchange the numbers on both cards grow to a size that
   can be read, hold while they change, and settle back. A felt numeral is FOUR
   PIXELS, so without this an attack moves everything except the thing the
   player needs to see.

   Hosted on .tcard rather than inside the badge, and the reason is clipping:
   four of the badge shapes (diamond, hexagon, triangle, pentagon) are clip-path
   boxes, and a clip-path clips its descendants - so a numeral scaled where it
   sits is sliced back to the badge silhouette, correct on a rounded badge and
   cut in half on a diamond. .tcard clips nothing (container-type is layout and
   style containment, never paint), so a ghost here is safe on every shape and
   on a templated card, whose badges sit inside .tcard-inner's overflow:hidden. */
/* A layer on the way out drops the .val-swell-layer class entirely (see
   endSwell), so the two states are separate selectors rather than one with a
   modifier - which is what stops a second attack landing inside the fade from
   finding the last one's ghosts. Both need the same box. */
.val-swell-layer, .val-swell-gone {
  position: absolute; inset: 0;
  pointer-events: none;
  z-index: 8;
  opacity: 1;
  transition: opacity var(--swell-out, 260ms) ease;
}
.val-swell-gone { opacity: 0; }
/* The real numeral, standing in place under its ghost. visibility, never
   display: the badge's box must not move by a pixel while its number is being
   read somewhere else, which is the whole of "only the numbers swell, not the
   badges". */
.val-swelled { visibility: hidden; }
/* TWO elements, two transforms, on purpose. The outer carries the swell; the
   inner carries whatever the pulse writes. One element could not hold both -
   the second transform would simply replace the first, and the number would
   snap back to its own size on the beat it changed. Same reasoning as the
   combat shake riding .tpiece-card, a child of the node the lunge is on. */
.val-swell {
  position: absolute;
  transform-origin: 50% 50%;
  transform: translate(-50%, -50%) scale(var(--swell-from, 0.5));
  animation: valSwellIn var(--swell-in, 190ms) cubic-bezier(0.2, 0.9, 0.3, 1) forwards;
  white-space: nowrap;
  line-height: 1;
  /* NO will-change, and this is not an oversight. The ghost is laid out at its
     final size and scales UP TO 1 so the glyph is rasterised big and sharp; a
     layer hint would have the compositor raster it once at the small starting
     scale and stretch every frame after that, which is exactly the pixelation
     this arrangement exists to remove. */
}
/* Grows INTO its size rather than past it. Laying the ghost out at the numeral's
   own four pixels and scaling up magnifies a four-pixel bitmap, which is what
   "very blurry (pixelated)" was; laying it out at reading size and scaling up to
   1 rasterises the glyph at that size instead. Every frame on the way in is a
   downscale of a sharp raster, and the number is native-crisp at the hold, which
   is where the eye rests. */
@keyframes valSwellIn { to { transform: translate(-50%, -50%) scale(1); } }
/* A number read at four times its size is being read over artwork, so it
   carries its own contrast rather than depending on what is behind it. In em,
   so the halo grows with the numeral instead of thinning out as it swells. */
.val-swell-num {
  display: inline-block;
  text-shadow:
    0 0 0.12em rgba(0, 0, 0, 0.95),
    0 0.04em 0.1em rgba(0, 0, 0, 0.8),
    0 0 0.3em rgba(0, 0, 0, 0.55);
}
/* Reduce motion: the number still arrives at reading size, it just does not
   travel to get there. Dropping the swell here would hand the player who most
   needs a legible number the four-pixel one, which is the opposite of the
   setting's intent - the growing is the motion, the size is the information.
   The same split the pulse block above makes between its swell and its colour. */
body.reduce-motion .val-swell { animation-duration: 1ms; }
body.reduce-motion .val-swell-layer { transition-duration: 1ms; }
@media (prefers-reduced-motion: reduce) {
  .val-swell { animation-duration: 1ms; }
  .val-swell-layer { transition-duration: 1ms; }
}

/* =================================================================
   Packlabs branding
   Brand art (emblem + wordmark) added to the home hero and used as
   the default card back. These are brand images, not UI icons, so
   they're intentionally filled/gradient SVGs rather than outlined
   glyphs. Files live in assets/brand/.
   ================================================================= */
.menu-brand {
  display: block; width: min(620px, 92vw); max-height: 56vh; height: auto; margin: 0 auto 2px;
  aspect-ratio: 640 / 277;
  filter: drop-shadow(0 12px 34px rgba(0,0,0,0.55));
  -webkit-user-drag: none; user-select: none;
}
/* The logo is an <object> so its inner SVG can animate + react to hover. */
object.menu-brand { border: 0; background: transparent; }

/* Default face-down card back (a game with no custom back art): the
   Packlabs howling wolf, gold-foil, centred on a dark felt card. The mark is
   the crest's wolf ALONE (assets/brand/wolf-gold.svg) - no crest ring, no
   fanned cards - since a card back is already a card and wants one clean
   silhouette rather than a picture of three cards inside a coin.
   Games that set their own back keep .has-back-art and are untouched. */
.tpiece-card .card-back:not(.has-back-art) {
  background:
    url("../assets/brand/wolf-gold.svg?v=1") center / 72% no-repeat,
    #16181d;
  border-color: rgba(228,191,98,0.5);
  box-shadow: 0 1px 2px rgba(0,0,0,0.35), inset 0 0 0 1.5px rgba(228,191,98,0.28);
}
.tpiece-card .card-back:not(.has-back-art) svg { display: none; }

/* Same default in the card-editor's card-back preview, for consistency. */
.ce-card .ce-card-back:not(.has-back-art) {
  background:
    url("../assets/brand/wolf-gold.svg?v=1") center / 67% no-repeat,
    #16181d;
  box-shadow: inset 0 0 0 1.5px rgba(228,191,98,0.26);
}
.ce-card .ce-card-back:not(.has-back-art) svg { display: none; }

/* ---- Brand card-back on decks & the deal animation (v231) ----
   Extends the default face-down back so DECK piles and the dealing ghost
   also carry the Packlabs crest when a game sets no custom back.
   Games with their own game.cardBack keep .has-back-art / .deck-top-art. */

/* Deck pile top face: swap the generic cards icon for the gold wolf,
   keeping the deck's own colour striping and its name label. */
.deck-top:not(.deck-top-art) .deck-emblem {
  opacity: 1;
  width: 44cqw; height: 44cqw; background: url("../assets/brand/wolf-gold.svg?v=1") center / contain no-repeat;
}
.deck-top:not(.deck-top-art) .deck-emblem svg { display: none; }

/* Cards flying from the deck while dealing: same gold-foil felt back. */
.deal-ghost .card-back:not(.has-back-art) {
  background:
    url("../assets/brand/wolf-gold.svg?v=1") center / 70% no-repeat,
    #16181d;
  border-color: rgba(228,191,98,0.5);
  color: transparent;
}
.deal-ghost .card-back:not(.has-back-art) svg { display: none; }

/* ---------- Artwork library modal ---------- */
/* Widen the gallery beyond the default wide modal (620px). Falls back to 620 in
   browsers without :has(); the grid just shows fewer columns. */
.modal.modal-wide:has(.art-lib) { max-width: 860px; }
.art-lib { display: flex; flex-direction: column; gap: 12px; min-height: 300px; }
.art-lib-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; }
.art-lib-toolbar-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.art-lib-seg { font-size: 11px; }
.art-lib-search {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px;
  color: var(--text); font-size: 12px; padding: 6px 10px; width: 150px;
}
.art-lib-search:focus { outline: none; border-color: var(--accent-2); }
.art-lib-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 10px; overflow-y: auto; max-height: 62vh; padding: 2px;
}
.art-lib-tile {
  position: relative; padding: 0; border: 1px solid var(--line);
  background: var(--panel-2); border-radius: 10px; overflow: hidden; cursor: pointer;
  aspect-ratio: 5 / 7; display: block; transition: border-color 0.1s, transform 0.1s;
}
.art-lib-tile:hover { border-color: var(--accent-2); }
.art-lib-tile:active { transform: scale(0.97); }
.art-lib-tile.sel { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }
.art-lib-thumb { width: 100%; height: 100%; object-fit: cover; display: block; }
.art-lib-tag {
  position: absolute; top: 6px; right: 6px; font-size: 9px; font-weight: 700;
  letter-spacing: 0.3px; padding: 2px 6px; border-radius: 999px;
  background: rgba(6, 9, 14, 0.7); color: #fff; text-transform: uppercase;
}
.art-lib-tag-sample { left: 6px; right: auto; background: rgba(108, 75, 214, 0.85); }
.art-lib-cap {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 14px 8px 6px;
  font-size: 11px; font-weight: 600; color: #fff; text-align: left;
  background: linear-gradient(to top, rgba(6, 9, 14, 0.9), transparent);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.art-lib-empty { grid-column: 1 / -1; text-align: center; color: var(--text-dim); padding: 36px 20px; line-height: 1.5; }
.art-lib-empty-title { font-weight: 700; color: var(--text); font-size: 14px; margin-bottom: 8px; }
.art-lib-empty-code {
  display: inline-block; background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 6px; padding: 4px 8px; font-family: ui-monospace, monospace;
  font-size: 12px; margin: 6px 0; color: var(--accent-2);
}
.ce-btnrow { display: flex; gap: 6px; flex-wrap: wrap; }
.art-lib-note {
  display: flex; align-items: center; gap: 8px; font-size: 11px; line-height: 1.35;
  color: var(--text-dim); background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 8px; padding: 7px 10px;
}
.art-lib-note .ic { flex-shrink: 0; color: var(--accent-2); display: inline-flex; }
.art-lib-note .ic svg { width: 15px; height: 15px; }

/* =================================================================
   Music library (js/musicLibrary.js) - the audio sibling of Browse
   artwork: a list of tracks with a preview button and (multi mode)
   tick-to-select. Same glass modal skin, reuses .art-lib-* toolbar.
   ================================================================= */
.modal.modal-wide:has(.music-lib) { max-width: 620px; }
.music-lib { display: flex; flex-direction: column; gap: 12px; min-height: 300px; }
.music-lib-head { display: flex; justify-content: flex-end; }
.music-lib-count { font-size: 11.5px; color: var(--text-dim); }
.music-lib-list { display: flex; flex-direction: column; gap: 6px; max-height: 52vh; overflow-y: auto; padding-right: 2px; }
.music-row {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  padding: 8px 10px; border-radius: 10px; cursor: pointer;
  background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08);
  color: var(--text); font: inherit;
}
.music-row:hover { border-color: rgba(255,255,255,0.18); background: rgba(255,255,255,0.06); }
.music-row.on { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.music-row-play {
  flex: 0 0 auto; width: 30px; height: 30px; border-radius: 999px; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(0,0,0,0.25); border: 1px solid rgba(255,255,255,0.12); color: var(--accent-2);
}
.music-row-play:hover { border-color: var(--accent); color: var(--accent); }
.music-row-play-ic { display: inline-flex; }
.music-row-play-ic svg { width: 15px; height: 15px; }
.music-row-meta { flex: 1; min-width: 0; }
.music-row-title { display: block; font-size: 12.5px; font-weight: 650; color: #e8ecf0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.music-row-sub { display: block; font-size: 11px; color: rgba(255,255,255,0.5); margin-top: 1px; }
.music-row-tick { flex: 0 0 auto; display: inline-flex; opacity: 0; color: var(--accent); }
.music-row.on .music-row-tick { opacity: 1; }
.music-row-tick svg { width: 16px; height: 16px; }

/* Settings: the "Choose tracks" button and the volume slider row. */
.set-choose-btn {
  flex: 0 0 auto; padding: 7px 14px; border-radius: 999px; cursor: pointer;
  font-size: 12px; font-weight: 650; color: var(--accent-2);
  background: rgba(0,0,0,0.22); border: 1px solid rgba(255,255,255,0.12);
}
.set-choose-btn:hover { border-color: var(--accent); color: var(--accent); }
.set-range { flex: 0 0 180px; max-width: 46%; accent-color: var(--accent); cursor: pointer; }

/* =================================================================
   Music controls (js/music.js) - the audio panel opened from the cog
   button in the play toolbar's top-right cluster. The glass chrome comes
   from the flyout it sits in (.tcg-flyout); this is just the compact
   vertical layout of its transport row, title, and volume row.
   ================================================================= */
.music-panel { display: flex; flex-direction: column; gap: 8px; min-width: 190px; }
.music-panel .music-title { padding: 0; }
.music-panel-row { display: flex; align-items: center; gap: 6px; }
.music-panel-row .music-vol { flex: 1 1 auto; }
.music-btn {
  flex: 0 0 auto; width: 26px; height: 26px; border-radius: 999px; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: none; color: var(--text);
}
.music-btn:hover { background: rgba(255,255,255,0.10); color: var(--accent-2); }
.music-btn.off { color: rgba(255,255,255,0.4); }
.music-btn svg { width: 16px; height: 16px; }
.music-title {
  flex: 1 1 auto; min-width: 0; font-size: 11.5px; color: rgba(255,255,255,0.8);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 0 4px;
}
.music-vol { flex: 0 0 74px; accent-color: var(--accent); cursor: pointer; }

/* =================================================================
   Home as a felt table: the whole view becomes the board, with the
   wordmark and fanned menu cards sitting on the felt. Reuses the exact
   gradient + dotted grain from the table felt (.felt) - applied via the
   .home-view class that js/views/home.js adds to the view element.
   ================================================================= */
.view.home-view {
  max-width: none;
  min-height: 100vh; min-height: 100dvh;
  padding: 18px 24px 24px;
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background:
    var(--felt-noise),
    var(--felt-fibre),
    var(--felt-image);
  background-color: #123a2e;
  background-size: auto, var(--felt-fibre-size), cover;
  background-position: center, center, center;
  background-repeat: repeat, repeat, no-repeat;
  background-blend-mode: normal, overlay, normal;
}
.view.home-view::before {
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0.5;
  background-image: radial-gradient(rgba(255,255,255,0.05) 1px, transparent 1.4px);
  background-size: 26px 26px;
}
.view.home-view .menu-hero { position: relative; z-index: 1; }
.menu-footer {
  position: absolute; bottom: 10px; left: 0; right: 0; z-index: 2;
  text-align: center;
  color: var(--text-dim);
  font-family: var(--font); font-size: 11px; letter-spacing: 0.2px;
}
.menu-legal {
  display: flex; justify-content: center; align-items: center;
  flex-wrap: wrap; gap: 4px 8px; margin-bottom: 5px;
}
.menu-legal-link {
  background: none; border: none; padding: 0; cursor: pointer;
  font-family: var(--font); font-size: 11px; letter-spacing: 0.2px;
  color: var(--text-dim); opacity: 0.75;
}
.menu-legal-link:hover { color: var(--accent-2); opacity: 1; text-decoration: underline; }
.menu-legal-sep { color: var(--text-faint); opacity: 0.6; }
.menu-copy { opacity: 0.6; pointer-events: none; }

/* ---------- My games (and Browse) on the felt ----------
   Same felt table as the main menu (identical gradient + dotted grain), applied
   via the .lib-view class js/views/library.js adds and the .browse-view class
   js/views/browse.js adds. The page header, actions, and the cards themselves
   all sit above the felt. Both pages share the one block so the felt, the
   grain and the footer can't drift apart between them. */
.view.lib-view, .view.browse-view {
  max-width: none;
  min-height: 100vh; min-height: 100dvh;
  position: relative;
  display: flex; flex-direction: column;
  background:
    var(--felt-noise),
    var(--felt-fibre),
    var(--felt-image);
  background-color: #123a2e;
  background-size: auto, var(--felt-fibre-size), cover;
  background-position: center, center, center;
  background-repeat: repeat, repeat, no-repeat;
  background-blend-mode: normal, overlay, normal;
}
.view.lib-view::before, .view.browse-view::before {
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0.5;
  background-image: radial-gradient(rgba(255,255,255,0.05) 1px, transparent 1.4px);
  background-size: 26px 26px;
}
.view.lib-view > *, .view.browse-view > * { position: relative; z-index: 1; }
/* The footer rides the bottom of the felt: margin-top:auto pushes it below the
   hand no matter how tall the page runs, in flow rather than pinned. */
.view.lib-view .lib-footer, .view.browse-view .lib-footer {
  position: relative; bottom: auto; margin-top: auto; padding-top: 28px;
}
/* "My games" title in the brand display font (Bauvex), gold like the wordmark. */
.lib-title {
  font-family: "GoldWhisper", "Bauvex", Georgia, serif;
  text-align: center; margin: 2px 0 0; font-weight: 400;
  font-size: 58px; line-height: 1.05; letter-spacing: 0.5px;
  background: linear-gradient(135deg, #ffbf66 0%, #ffd580 50%, #ffbf66 100%);
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
}

/* ---------- Browse: the shelf of published games ----------
   The felt and the gold display title come from the block above; everything
   below is Browse's own. One glass control strip, one spotlight panel, one
   grid of shelf tiles. Same glass skin, same pill scale as the table's
   floating surfaces - a chip here matches a turn-step pill there. */
.browse-tag {
  text-align: center; margin: 6px 0 0;
  font-size: 12px; color: var(--text-dim);
}
.browse-bar {
  display: flex; align-items: center; justify-content: center;
  flex-wrap: wrap; gap: 6px;
  margin: 16px auto 0; padding: 6px;
  width: fit-content; max-width: min(940px, 94vw);
  background: rgba(15,20,28,0.5);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: 12px;
  box-shadow: 0 10px 28px rgba(0,0,0,0.35);
}
.browse-bar .search-wrap { margin: 0; }
.browse-rails { display: flex; align-items: center; gap: 4px; }
.browse-chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 10px; border-radius: 999px;
  font-size: 12px; font-weight: 600; line-height: 1;
  color: var(--text-dim); cursor: pointer;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.08);
  transition: background 0.14s, color 0.14s, border-color 0.14s;
}
.browse-chip:hover { background: rgba(255,255,255,0.09); color: var(--text); }
.browse-chip.on {
  color: #10141a; border-color: transparent;
  background: linear-gradient(180deg, var(--accent-2), var(--accent));
}
.browse-chip .ic svg { width: 13px; height: 13px; }
.browse-chip .ic:last-child svg { width: 11px; height: 11px; opacity: 0.7; }
/* The filter menus: the same glass a pill menu wears on the table, so a menu
   reads as an extension of the chip it sprang from. */
.browse-menu { display: flex; flex-direction: column; gap: 1px; }
.browse-menu-item {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 6px 8px; border-radius: 7px;
  font-size: 12px; color: var(--text); text-align: left; cursor: pointer;
  background: transparent; border: 0;
}
.browse-menu-item:hover { background: rgba(255,255,255,0.07); }
.browse-menu-item.on { color: var(--accent-2); }
.browse-menu-item .ic svg { width: 12px; height: 12px; }

.browse-results { width: min(1040px, 94vw); margin: 0 auto; }
.browse-count {
  margin: 26px 0 10px; font-size: 11px; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-faint);
}

/* The spotlight: one game shown big, its cover card standing on the felt. */
.browse-spot {
  display: flex; align-items: center; gap: 22px;
  margin: 22px auto 0; padding: 18px 22px;
  background: rgba(15,20,28,0.62);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  border: 1px solid rgba(255,255,255,0.10);
  border-radius: 16px;
  box-shadow: 0 18px 46px rgba(0,0,0,0.42);
}
.spot-art { flex: 0 0 auto; width: 168px; }
.spot-art .tcard, .spot-art .pub-cover-blank {
  width: 100%; transform: rotate(-3deg);
  box-shadow: 0 16px 34px rgba(0,0,0,0.5);
  transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1);
}
.browse-spot:hover .spot-art .tcard, .browse-spot:hover .spot-art .pub-cover-blank { transform: rotate(-1deg) translateY(-4px); }
.spot-main { flex: 1; min-width: 0; }
.spot-rail-tag {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 8px; border-radius: 999px;
  font-size: 10px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--accent-2); background: rgba(201,138,47,0.14);
  border: 1px solid rgba(201,138,47,0.34);
}
.spot-rail-tag .ic svg { width: 11px; height: 11px; }
.spot-name {
  font-family: "GoldWhisper", "Bauvex", Georgia, serif;
  font-size: 34px; line-height: 1.1; margin: 8px 0 2px; color: var(--text);
}
.spot-by { font-size: 12px; color: var(--text-dim); }
.spot-desc {
  margin: 8px 0 0; font-size: 13px; line-height: 1.45; color: var(--text);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.spot-main .pub-facts { margin-top: 10px; }
.spot-acts { display: flex; align-items: center; gap: 6px; margin-top: 14px; }

/* The grid of shelf tiles. */
.browse-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
  gap: 16px; align-items: start;
}
.pub-tile {
  display: flex; flex-direction: column;
  padding: 8px; border-radius: 12px; cursor: pointer;
  background: rgba(15,20,28,0.52);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  border: 1px solid rgba(255,255,255,0.09);
  box-shadow: 0 8px 22px rgba(0,0,0,0.32);
  transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s, border-color 0.2s;
}
.pub-tile:hover {
  transform: translateY(-4px);
  border-color: rgba(255,255,255,0.18);
  box-shadow: 0 16px 34px rgba(0,0,0,0.46);
}
.pub-cover { position: relative; border-radius: 8px; overflow: hidden; }
/* A cover card keeps its OWN display. `.tcard` is a flex column and
   `.tcard-inner` takes its height from `flex: 1`, so overriding the card to
   `display: block` collapses the face to zero height - every element on it then
   resolves its percentage position against nothing and stacks above the card,
   which renders as a flat coloured rectangle with the name spilling out of the
   top. Same bug the showdown stage hit. Only the blank stand-in, which is a
   plain box with an svg in it, wants `display: block`. */
.pub-cover .tcard { width: 100%; }
.pub-cover .pub-cover-blank { width: 100%; display: block; }
.pub-cover-blank { aspect-ratio: 5 / 7; border-radius: 8px; overflow: hidden; background: #14171d; }
.pub-cover-blank svg { width: 100%; height: 100%; display: block; }
/* The play badge sits over the cover and fades in on hover - the tile itself
   is the button, this just says so. */
.pub-play {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(6,10,14,0.42);
  opacity: 0; transition: opacity 0.16s;
}
.pub-play .ic {
  display: flex; padding: 10px; border-radius: 999px;
  color: #10141a; background: linear-gradient(180deg, var(--accent-2), var(--accent));
  box-shadow: 0 6px 18px rgba(0,0,0,0.45);
}
.pub-play .ic svg { width: 20px; height: 20px; }
.pub-tile:hover .pub-play { opacity: 1; }
.pub-body { padding: 8px 2px 0; }
.pub-name {
  font-size: 13px; font-weight: 700; color: var(--text); line-height: 1.2;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pub-by { font-size: 11px; color: var(--text-dim); margin-top: 2px; }
.pub-facts { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 7px; }
.pub-fact {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 7px; border-radius: 999px;
  font-size: 11px; color: var(--text-dim);
  background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.07);
}
.pub-fact .ic svg { width: 11px; height: 11px; }
.pub-foot {
  display: flex; align-items: center; justify-content: space-between;
  margin-top: 8px; padding: 6px 2px 0;
  border-top: 1px solid rgba(255,255,255,0.07);
}
.pub-stat, .pub-like {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 11px; color: var(--text-faint);
}
.pub-stat .ic svg, .pub-like .ic svg { width: 12px; height: 12px; }
.pub-like {
  padding: 3px 7px; border-radius: 999px; cursor: pointer;
  background: transparent; border: 1px solid transparent;
}
.pub-like:hover { color: var(--text); background: rgba(255,255,255,0.07); }
.pub-like.on { color: #e08475; border-color: rgba(224,132,117,0.34); }
.spot-acts .pub-like { padding: 6px 10px; border: 1px solid rgba(255,255,255,0.10); }
.browse-empty { margin-top: 40px; }
.browse-empty-sub { margin-top: 6px; font-size: 12px; color: var(--text-faint); }

@media (max-width: 700px) {
  .browse-spot { flex-direction: column; align-items: flex-start; gap: 14px; }
  .spot-art { width: 132px; align-self: center; }
  .spot-name { font-size: 26px; }
}

/* ---------- The publish flyout (My games) ---------- */
.pub-panel-lbl {
  font-size: 10px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--text-faint); margin: 2px 0 6px;
}
.pub-panel-lbl + * + .pub-panel-lbl, .pub-vis + .pub-panel-lbl { margin-top: 12px; }
/* A section head that carries its own reset button. The label keeps its own
   margins so a head with a control sits exactly where a bare one would. */
.pub-lbl-row { display: flex; align-items: center; gap: 6px; margin-top: 12px; }
.pub-lbl-row .pub-panel-lbl { flex: 1; min-width: 0; margin: 0; }
.pub-lbl-row .btn.btn-icon { flex: 0 0 auto; }
.pub-lbl-row .btn.btn-icon.row-off { opacity: 0.4; cursor: default; }
/* The box-cover sentence. Its pills are the shared .gl-sent grammar, so only
   the space around the line belongs here. */
.pub-box { margin-bottom: 2px; }
.pub-vis { display: flex; flex-direction: column; gap: 3px; }
.pub-vis-opt {
  display: flex; align-items: flex-start; gap: 7px;
  padding: 6px 8px; border-radius: 8px; cursor: pointer; text-align: left;
  color: var(--text); background: transparent; border: 1px solid transparent;
}
.pub-vis-opt:hover { background: rgba(255,255,255,0.07); }
.pub-vis-opt.on { border-color: rgba(216,162,74,0.34); background: rgba(216,162,74,0.10); }
.pub-vis-opt .ic svg { width: 13px; height: 13px; }
.pub-vis-opt.on > .ic:first-child { color: var(--accent-2); }
.pub-vis-txt { flex: 1; min-width: 0; }
.pub-vis-lbl { font-size: 12px; font-weight: 700; line-height: 1.2; }
.pub-vis-sub { display: block; font-size: 11px; line-height: 1.35; color: var(--text-dim); margin-top: 2px; }
.pub-cover-row { display: flex; align-items: flex-start; gap: 10px; }
.pub-cover-thumb { flex: 0 0 auto; width: 58px; }
.pub-cover-thumb .tcard { width: 58px; }
.pub-cover-thumb svg { width: 58px; height: 81px; border-radius: 4px; display: block; }
.pub-cover-side { flex: 1; min-width: 0; }
.pub-cover-name {
  font-size: 12px; font-weight: 700; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pub-cover-hint { display: block; font-size: 11px; color: var(--text-dim); margin-top: 2px; }
.pub-cover-acts { display: flex; align-items: center; gap: 4px; margin-top: 7px; }
.pub-panel-foot {
  display: block; margin-top: 12px; padding-top: 8px;
  border-top: 1px solid rgba(255,255,255,0.08);
  font-size: 11px; line-height: 1.4; color: var(--text-faint);
}

/* ---------- Legal pages (privacy, terms, cookies, refund) ---------- */
.legal-page { max-width: 760px; margin: 0 auto; }
.view.legal-view > .home-row { max-width: 760px; margin-left: auto; margin-right: auto; margin-bottom: 18px; }
.legal-title { font-size: 26px; font-weight: 800; margin: 4px 0 6px; color: var(--text); }
.legal-tag { font-size: 14px; color: var(--text-dim); margin: 0 0 4px; }
.legal-updated { font-size: 12px; color: var(--text-faint); margin: 0 0 22px; }
.legal-h { font-size: 16px; font-weight: 700; color: var(--text); margin: 24px 0 8px; }
.legal-p { font-size: 14px; line-height: 1.65; color: var(--text); margin: 0 0 12px; }
.legal-list { margin: 0 0 12px; padding-left: 20px; }
.legal-list li { font-size: 14px; line-height: 1.6; color: var(--text); margin-bottom: 7px; }
.legal-more {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px;
  margin-top: 34px; padding-top: 16px; border-top: 1px solid var(--line);
}
.legal-more-label { font-size: 12px; color: var(--text-faint); }
.legal-more-link {
  background: none; border: none; padding: 0; cursor: pointer;
  font-family: var(--font); font-size: 13px; color: var(--text-dim);
}
.legal-more-link:hover { color: var(--accent-2); text-decoration: underline; }

/* ---------- Pricing page (preview) ---------- */
.pricing-page { max-width: 860px; margin: 0 auto; }
.pricing-head { text-align: center; margin-bottom: 16px; }
.pricing-head h1 { font-size: 24px; font-weight: 800; color: var(--text); margin: 2px 0 6px; }
.pricing-sub { font-size: 14px; color: var(--text-dim); margin: 0; }
.pricing-preview {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  max-width: 580px; margin: 0 auto 22px; padding: 8px 14px;
  background: rgba(216,162,74,0.08); border: 1px solid var(--line-2); border-radius: 10px;
  color: var(--text-dim); font-size: 12.5px;
}
.pricing-preview-ic { display: inline-flex; color: var(--accent-2); }
.pricing-preview-ic svg { width: 15px; height: 15px; }
.plan-grid { display: flex; flex-wrap: wrap; justify-content: center; align-items: stretch; gap: 26px; padding-top: 16px; }
/* Each plan is dressed as a collectible card: a coloured frame with a dark
   inner face, a cost token, a type line, and a rules box. Reuses the frame
   language of the fanned menu cards (.menu-card). On load it flips in from a
   logo card back, Free first then Plus. */
.plan-blue { --pc-accent: #2f6cb0; --pc-accent2: #6aa9e8; }
.plan-gold { --pc-accent: #c98a2f; --pc-accent2: #f0c879; }
/* 3D flip-in: a perspective wrapper holds a two-faced card (logo back + plan
   front) that rotates from back to front on mount, staggered per card. */
.plan-flip { position: relative; width: 320px; max-width: 100%; aspect-ratio: 5 / 7; perspective: 1200px; }
.plan-3d { position: absolute; inset: 0; transform-style: preserve-3d; animation: planReveal 0.95s cubic-bezier(0.2, 0.7, 0.2, 1) both; }
.plan-flip.p1 .plan-3d { animation-delay: 0.15s; }
.plan-flip.p2 .plan-3d { animation-delay: 0.62s; }
@keyframes planReveal {
  0% { transform: rotateY(180deg); opacity: 0; }
  22% { opacity: 1; }
  100% { transform: rotateY(0deg); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) { .plan-3d { animation: none; } }
.plan-card, .plan-back {
  position: absolute; inset: 0; padding: 8px; border-radius: 16px;
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
  background: linear-gradient(180deg, rgba(255,255,255,0.09), rgba(0,0,0,0.34)), var(--pc-accent, #c98a2f);
  box-shadow: 0 12px 28px rgba(0,0,0,0.5);
}
.plan-card.featured { box-shadow: 0 16px 36px rgba(0,0,0,0.55), 0 0 0 1px var(--pc-accent2); }
/* Card back: the site emblem centered on the dark face. */
.plan-back { transform: rotateY(180deg); }
.plan-back-face {
  height: 100%; border-radius: 11px; background: #14171d;
  display: flex; align-items: center; justify-content: center; overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.04), inset 0 0 60px rgba(0,0,0,0.5);
}
.plan-back-logo { width: 66%; max-width: 190px; opacity: 0.92; }
/* Foil sheen: a rotated bright band swept across the face. Clipped to the
   face's rounded rect (overflow hidden) and started/ended fully off the card,
   so the band never stops mid-corner and no hard edge shows at the loop. */
.plan-card.featured .plan-face { overflow: hidden; }
.plan-card.featured .plan-face::after {
  content: ""; position: absolute; top: -50%; left: 0; width: 55%; height: 200%; pointer-events: none; z-index: 2;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.16), transparent);
  animation: planFoil 6.5s ease-in-out infinite;
}
@keyframes planFoil {
  0% { transform: translateX(-220%) rotate(18deg); }
  55%, 100% { transform: translateX(340%) rotate(18deg); }
}
@media (prefers-reduced-motion: reduce) {
  .plan-card.featured .plan-face::after { animation: none; }
}
.plan-face {
  position: relative; height: 100%; background: #14171d; border-radius: 11px;
  padding: 16px 16px 20px; display: flex; flex-direction: column;
}
.plan-badge {
  position: absolute; top: -11px; left: 50%; transform: translateX(-50%); z-index: 5;
  font-size: 11px; font-weight: 700; letter-spacing: 0.2px; color: #12161d;
  background: var(--pc-accent2, var(--accent-2)); padding: 3px 12px; border-radius: 999px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.45); white-space: nowrap;
}
.plan-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.plan-name { font-size: 19px; font-weight: 800; color: var(--text); line-height: 1.1; }
.plan-rarity { font-size: 11px; font-weight: 600; letter-spacing: 0.3px; color: var(--pc-accent2); margin-top: 3px; }
.plan-cost {
  flex: none; display: flex; align-items: center; justify-content: flex-end;
  font-size: 30px; font-weight: 800; line-height: 1; letter-spacing: -0.5px;
  color: var(--pc-accent2, var(--accent-2));
}
.plan-tag { font-size: 12px; color: var(--text-dim); margin-top: 8px; line-height: 1.4; }
.plan-divider { height: 1px; margin: 12px 0; background: linear-gradient(90deg, transparent, var(--pc-accent2), transparent); opacity: 0.5; }
.plan-rules { flex: 1; display: flex; flex-direction: column; gap: 7px; margin-bottom: 14px; }
.plan-feat { display: flex; align-items: flex-start; gap: 8px; font-size: 12.5px; color: var(--text); line-height: 1.35; }
.plan-feat-ic { display: inline-flex; flex: none; margin-top: 1px; color: var(--pc-accent2); }
.plan-feat-ic svg { width: 15px; height: 15px; }
.plan-feat.excl { color: var(--text-faint); }
.plan-feat.excl .plan-feat-ic { color: var(--text-faint); }
.plan-cta { width: 100%; justify-content: center; }
.pricing-section-label { text-align: center; font-size: 12.5px; color: var(--text-dim); margin: 30px 0 2px; }
/* Art packs as a little fanned hand, the same fan mechanic as the main menu:
   a stationary slot pivots from a shared point below; the card inside lifts on
   hover. Packs deal in after the plan cards flip. */
.pack-hand { position: relative; width: 100%; max-width: 500px; height: 232px; margin: 6px auto 0; }
.pack-slot {
  position: absolute; left: 50%; top: 20px; width: 118px; height: 166px;
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  transform: translateX(-50%) rotate(var(--rot, 0deg)); transform-origin: 50% 430px;
  animation: packFan 0.52s cubic-bezier(0.22, 0.85, 0.28, 1.02) both;
}
.pack-slot:hover { z-index: 60 !important; }
@keyframes packFan {
  0% { opacity: 0; transform: translateX(-50%) translateY(28px) rotate(0deg); }
  55% { opacity: 1; }
  100% { opacity: 1; transform: translateX(-50%) rotate(var(--rot, 0deg)); }
}
@media (prefers-reduced-motion: reduce) { .pack-slot { animation: none; } }
.pack-card {
  position: relative; width: 132px; aspect-ratio: 5 / 7; padding: 6px; pointer-events: none;
  border-radius: 13px;
  background: linear-gradient(180deg, rgba(255,255,255,0.09), rgba(0,0,0,0.34)), var(--pc-accent, #c98a2f);
  box-shadow: 0 12px 26px rgba(0,0,0,0.5);
  transform-origin: center bottom;
  transition: transform 0.22s cubic-bezier(0.2, 1, 0.4, 1), box-shadow 0.2s, filter 0.2s;
}
.pack-slot:hover .pack-card {
  transform: translateY(-42px) scale(1.06);
  box-shadow: 0 30px 48px rgba(0,0,0,0.6), 0 0 0 2px var(--pc-accent2, var(--accent-2));
  filter: brightness(1.05);
}
.pack-face {
  height: 100%; border-radius: 9px; background: #14171d;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; padding: 8px;
}
.pack-art {
  width: 50px; height: 50px; border-radius: 12px; display: flex; align-items: center; justify-content: center;
  color: var(--pc-accent2, var(--accent-2)); background: rgba(255,255,255,0.06);
}
.pack-art svg { width: 27px; height: 27px; }
.pack-meta { text-align: center; }
.pack-name { font-size: 12px; font-weight: 700; color: var(--text); }
.pack-price { font-size: 12px; font-weight: 700; color: var(--pc-accent2, var(--accent-2)); margin-top: 2px; }
.pk-violet { --pc-accent: #6f47b0; --pc-accent2: #b08fe4; }
.pk-blue { --pc-accent: #2f6cb0; --pc-accent2: #6aa9e8; }
.pk-gold { --pc-accent: #c98a2f; --pc-accent2: #f0c879; }
.pricing-fine { text-align: center; font-size: 12px; color: var(--text-dim); margin-top: 14px; }

/* Felt table backdrop for the pricing page, reusing the home felt so the
   fanned cards read as cards on a table. */
.view.pricing-view {
  max-width: none; min-height: 100vh; min-height: 100dvh; position: relative; padding: 22px 24px 48px;
  background:
    var(--felt-noise),
    var(--felt-fibre),
    var(--felt-image);
  background-color: #123a2e;
  background-size: auto, var(--felt-fibre-size), cover;
  background-position: center, center, center;
  background-repeat: repeat, repeat, no-repeat;
  background-blend-mode: normal, overlay, normal;
}
.view.pricing-view::before {
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0.5;
  background-image: radial-gradient(rgba(255,255,255,0.05) 1px, transparent 1.4px);
  background-size: 26px 26px;
}
.view.pricing-view > * { position: relative; z-index: 1; }
.view.pricing-view > .page-bar, .view.pricing-view > .home-row, .view.pricing-view > .pricing-page { max-width: 860px; margin-left: auto; margin-right: auto; }
.view.pricing-view > .home-row { margin-bottom: 18px; }

/* ===== Die creator (js/views/dieEditor.js) ===================================
   Glass, compact, icon-first, same language as the rest of the tool. The die is
   the real renderer on a felt stage; the panel is one capped-height glass column
   that scrolls inside itself. */
/* The whole page is the felt table, same treatment as the card editor / menu. */
.view.die-editor {
  max-width: none; min-height: 100vh; min-height: 100dvh; padding: 14px 24px 24px; position: relative;
  background: var(--felt-noise), var(--felt-fibre), var(--felt-image);
  background-color: #123a2e; background-size: auto, var(--felt-fibre-size), cover;
  background-position: center, center, center; background-repeat: repeat, repeat, no-repeat;
  background-blend-mode: normal, overlay, normal;
}
/* The die sits dead-centre of the felt; the glass panel floats over the right
   (like the card editor's .ce-panel) so it doesn't push the die off-centre. */
.die-editor .de-wrap { display: block; padding: 4px 8px 28px; }
.die-editor .de-stage { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; min-height: calc(100vh - 96px); min-height: calc(100dvh - 96px); }
/* The die name is plain centred text you type over, exactly like the card
   editor's title (.ce-title-input): no box, no border, just text on the felt. */
.die-editor .de-name {
  width: auto; min-width: 200px; max-width: 90%; text-align: center;
  -webkit-appearance: none; appearance: none;
  background: none; border: none; box-shadow: none; border-radius: 0;
  color: var(--text); font-size: 19px; font-weight: 700; letter-spacing: 0.2px;
  padding: 4px 6px; outline: none; text-shadow: 0 1px 3px rgba(0,0,0,0.55);
}
.die-editor .de-name:hover,
.die-editor .de-name:focus,
.die-editor .de-name:focus-visible { background: none; border: none; box-shadow: none; outline: none; }
.die-editor .de-name::placeholder { color: var(--text-faint); font-weight: 600; text-shadow: none; }
.die-editor .de-felt {
  border-radius: 50%; padding: 22px;
  /* No hard box now the whole page is felt - just a soft spotlight so the die
     reads as sitting on the table. */
  background: radial-gradient(closest-side at 50% 42%, rgba(255,255,255,0.08), rgba(255,255,255,0.02) 55%, transparent 75%);
}
.die-editor .de-die { width: 300px; height: 300px; display: block; cursor: grab; touch-action: none; }
.die-editor .de-die:active { cursor: grabbing; }
.die-editor .de-hint { font-size: 11px; color: rgba(233,230,221,0.55); }
.die-editor .de-strip { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; max-width: 360px; }
.die-editor .de-facechip {
  display: inline-flex; align-items: center; gap: 4px; min-width: 26px; height: 26px;
  padding: 0 8px; border-radius: 999px; cursor: pointer; font-size: 12px; font-weight: 600;
  color: #e9e6dd; background: rgba(15,20,28,0.72); border: 1px solid rgba(255,255,255,0.12);
  backdrop-filter: blur(10px);
}
.die-editor .de-facechip.sel { border-color: var(--accent); color: var(--accent-2); box-shadow: 0 0 0 1px var(--accent) inset; }
.die-editor .de-facechip-ic svg, .die-editor .de-facechip-fx svg { width: 12px; height: 12px; }
.die-editor .de-facechip-fx { color: var(--accent-2); }

/* The die creator now works like the card creator: a left rail of group
   buttons (Design / Values / Play), a left dock of pills for whatever's been
   customised, and an on-demand glass panel (reusing the card editor's .ce-panel
   skin) that opens the picked setting. */
/* Left dock: a growing column of jump-back pills, sitting left of the centred
   die (radius ~172px + a gap), so it never touches the die at any type. Reuses
   the card editor's .ce-dock-btn button skin. */
.die-editor .de-dock {
  position: absolute; top: 50%; right: calc(50% + 192px); transform: translateY(-50%);
  z-index: 15; display: flex; flex-direction: column; gap: 8px; align-items: stretch;
  max-height: calc(100vh - 120px); max-height: calc(100dvh - 120px); overflow-y: auto; padding: 2px;
}
.die-editor .de-panel-body { display: flex; flex-direction: column; gap: 10px; }
.die-editor .de-panel-tip { font-size: 10.5px; line-height: 1.35; color: var(--text-faint); }
/* Kept for older references; the panel is now the shared .ce-panel glass. */
.die-editor .de-panel.glass {
  position: absolute; top: 64px; right: 14px; z-index: 20; width: 300px;
  max-height: calc(100vh - 84px); max-height: calc(100dvh - 84px); overflow-y: auto;
  padding: 10px 10px 14px; border-radius: 12px;
  background: rgba(15,20,28,0.72); border: 1px solid rgba(255,255,255,0.12);
  backdrop-filter: blur(12px); box-shadow: 0 10px 30px rgba(0,0,0,0.4);
}
.die-editor .de-sec { padding: 8px 0; border-top: 1px solid rgba(255,255,255,0.08); }
.die-editor .de-sec:first-child { border-top: 0; }
.die-editor .de-sec-h { font-size: 12px; font-weight: 700; letter-spacing: 0.02em; color: var(--accent-2); margin-bottom: 7px; }
.die-editor .de-sec-b { display: flex; flex-direction: column; gap: 7px; }
.die-editor .de-sub { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.04em; color: rgba(233,230,221,0.5); margin-top: 2px; }
.die-editor .de-chips { display: flex; flex-wrap: wrap; gap: 5px; }
.die-editor .de-chip {
  padding: 4px 9px; border-radius: 7px; font-size: 11.5px; cursor: pointer; color: #e9e6dd;
  background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.12);
}
.die-editor .de-chip.sel { border-color: var(--accent); color: var(--accent-2); background: color-mix(in srgb, var(--accent) 16%, transparent); }
.die-editor .de-chip-add { width: 26px; height: 26px; }
.die-editor .de-saved { align-items: center; }
.die-editor .de-swatch {
  width: 22px; height: 22px; border-radius: 6px; cursor: pointer; padding: 0;
  border: 1px solid rgba(255,255,255,0.2); background: #888;
}
.die-editor .de-swatch.sel { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }
.die-editor .de-swatch-auto {
  background:
    linear-gradient(135deg, #f6f6f2 0 50%, #17170f 50% 100%);
}
.die-editor .de-iconchip { display: inline-flex; align-items: center; justify-content: center; color: #e9e6dd; }
.die-editor .de-iconchip svg { width: 14px; height: 14px; }
.die-editor .de-range { width: 100%; accent-color: var(--accent); }
.die-editor .de-row { display: flex; align-items: center; gap: 6px; }
.die-editor .de-row-l { flex: 0 0 auto; min-width: 52px; font-size: 11.5px; color: rgba(233,230,221,0.75); }
.die-editor .de-num { min-width: 26px; text-align: center; font-size: 13px; font-weight: 700; color: #e9e6dd; }
.die-editor .de-inp, .die-editor .de-select {
  flex: 1 1 auto; min-width: 0; padding: 5px 8px; font-size: 11.5px; border-radius: 7px; color: #e9e6dd;
  background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.14); outline: none;
}
.die-editor .de-inp:focus, .die-editor .de-select:focus { border-color: var(--accent); }
.die-editor .de-fx { display: flex; flex-direction: column; gap: 6px; }
.die-editor .de-fxpill {
  display: flex; align-items: center; gap: 6px; padding: 5px 6px 5px 9px; border-radius: 8px; cursor: pointer;
  background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.12);
}
.die-editor .de-fxpill-ic svg { width: 14px; height: 14px; color: var(--accent-2); }
.die-editor .de-fxpill-t { flex: 1 1 auto; font-size: 11.5px; color: #e9e6dd; }
.die-editor .de-artthumb {
  width: 26px; height: 26px; border-radius: 6px; background-size: cover; background-position: center;
  border: 1px solid rgba(255,255,255,0.2);
}
.de-menu { position: fixed; z-index: 9000; }

/* face paint modal: 5:7 canvas stage beside the real paint tool panel */
/* 220px canvas + 12px gap + a 180px tools column needs 412px, so this row
   overflowed any phone. It wraps now, putting the tools under the canvas
   instead of off the edge. */
.de-paint { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; }
.de-paint-stage { flex: 0 0 auto; }
.de-paint-canvas { width: 220px; height: 308px; border-radius: 8px; background: #1a1f27; border: 1px solid rgba(255,255,255,0.14); touch-action: none; cursor: crosshair; }
.de-paint-tools { flex: 1 1 auto; min-width: 180px; }
@media (max-width: 720px) {
  .die-editor .de-stage { min-height: auto; }
  .die-editor .de-panel.glass { position: static; width: min(360px, 92%); max-height: none; margin: 12px auto 0; }
}

/* saved-die pills carry a delete x (die creator "Your dice") */
.die-editor .de-savedpill {
  display: inline-flex; align-items: stretch; border-radius: 7px; overflow: hidden;
  background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.12);
}
.die-editor .de-savedpill.sel { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.die-editor .de-savedpill-name {
  padding: 4px 8px; font-size: 11.5px; cursor: pointer; color: #e9e6dd; background: transparent; border: 0; max-width: 120px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.die-editor .de-savedpill.sel .de-savedpill-name { color: var(--accent-2); }
.die-editor .de-savedpill-x {
  display: inline-flex; align-items: center; justify-content: center; width: 22px; cursor: pointer;
  color: rgba(233,230,221,0.5); background: transparent; border: 0; border-left: 1px solid rgba(255,255,255,0.1);
}
.die-editor .de-savedpill-x:hover { color: #ff6b6b; background: rgba(255,107,107,0.12); }
.die-editor .de-savedpill-x svg { width: 12px; height: 12px; }

/* draw a face in place on the stage (no modal), the card-editor way */
.die-editor .de-paint-inline {
  width: 236px; height: 330px; border-radius: 12px; display: block;
  background: #1a1f27; border: 1px solid rgba(255,255,255,0.16);
  box-shadow: 0 10px 30px rgba(0,0,0,0.45); touch-action: none; cursor: crosshair;
}
.die-editor .de-paint-cap { font-size: 11px; color: rgba(233,230,221,0.7); margin-bottom: 2px; }

/* ============================================================
   SETTINGS page - player preferences, reached from the menu's
   Settings card. Sits on the same green felt as the other menus
   (home, pricing), with glass panels floating on top.
   ============================================================ */
.view.settings-view {
  max-width: none; min-height: 100vh; min-height: 100dvh; position: relative; padding: 40px 24px;
  /* Centre the title + cards in the middle of the screen. min-height (not
     height) means a tall category's options grow the page instead of clipping. */
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  background: var(--felt-noise), var(--felt-fibre), var(--felt-image);
  background-color: #123a2e;
  background-size: auto, var(--felt-fibre-size), cover;
  background-position: center, center, center;
  background-repeat: repeat, repeat, no-repeat;
  background-blend-mode: normal, overlay, normal;
}
.view.settings-view::before {
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0.5;
  background-image: radial-gradient(rgba(255,255,255,0.05) 1px, transparent 1.4px);
  background-size: 26px 26px;
}
.view.settings-view > * { position: relative; z-index: 1; }
.view.settings-view > .home-row, .view.settings-view > .settings-page { max-width: 720px; margin-left: auto; margin-right: auto; }
.view.settings-view > .home-row { margin-bottom: 10px; }
.settings-page { width: 100%; max-width: 720px; margin: 0 auto; padding: 0 24px; }
.settings-head { margin-bottom: 26px; }
.settings-head h1 { font-size: 26px; font-weight: 700; margin: 0 0 6px; color: var(--ink, #e8ecf0); }
.settings-sub { font-size: 14px; color: rgba(255,255,255,0.55); margin: 0; }
.settings-section { margin-top: 30px; }
.settings-section-label { font-size: 12px; letter-spacing: 1px; text-transform: uppercase; font-weight: 700;
  color: rgba(255,255,255,0.5); margin-bottom: 12px; }
.set-choices { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; }
.set-opt {
  display: flex; flex-direction: column; gap: 10px; padding: 12px; cursor: pointer; text-align: left;
  border-radius: 14px; background: rgba(15,20,28,0.55); backdrop-filter: blur(10px);
  border: 1px solid rgba(255,255,255,0.10); color: inherit; transition: border-color 0.12s, box-shadow 0.12s;
}
.set-opt:hover { border-color: rgba(255,255,255,0.24); }
.set-opt.on { border-color: var(--accent-2); box-shadow: 0 0 0 1px var(--accent-2) inset, 0 8px 26px -12px rgba(216,162,74,0.5); }
.set-opt-title { font-size: 14px; font-weight: 700; color: #e8ecf0; display: flex; align-items: center; gap: 7px; }
.set-opt-check { width: 16px; height: 16px; color: var(--accent-2); opacity: 0; }
.set-opt.on .set-opt-check { opacity: 1; }
.set-opt-desc { font-size: 12px; line-height: 1.45; color: rgba(255,255,255,0.55); }

/* live preview felt: uses the REAL .tzone classes so the swatch always matches
   how the table actually draws a zone (brackets vs hairline). */
.zone-demo-felt {
  position: relative; height: 120px; border-radius: 14px; overflow: hidden;
  border: 1px solid #20303a;
  /* Colour layer is the shared themeable felt (var(--felt-image)), so these
     preview swatches show the CURRENTLY chosen felt colour, not a fixed green -
     pick a felt above and the zone-outline and player-colour previews recolour
     live with it. Neutral top spotlight kept for depth, same as the real felt. */
  background:
    radial-gradient(120% 90% at 50% -10%, rgba(255,255,255,0.05), transparent 55%),
    var(--felt-fibre),
    var(--felt-image);
  background-color: #123a2e;
  background-size: auto, var(--felt-fibre-size), cover;
  background-position: center, center, center;
  background-repeat: no-repeat, repeat, no-repeat;
  background-blend-mode: normal, overlay, normal;
}
.zone-demo-felt::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0.5;
  background-image: radial-gradient(rgba(255,255,255,0.05) 1px, transparent 1.4px); background-size: 22px 22px;
}
.zone-demo-felt .tzone { position: absolute; left: 16px; top: 24px; right: 16px; bottom: 16px; }
.zone-demo-felt .demo-card {
  position: absolute; top: 34%; height: 46%; width: 15%; border-radius: 6px;
  background: linear-gradient(160deg, #eae2cf, #cdbf9c); border: 1px solid rgba(0,0,0,0.35);
  box-shadow: 0 3px 8px rgba(0,0,0,0.4);
}

/* "Settings" in the brand title font, matching "My games" and the wordmark. */
.settings-title { margin: 2px 0 6px; }

/* Settings landing = a fanned hand of playing cards (reuses .menu-hand/.menu-card
   from the main menu), sized a notch bigger than the main-menu hand. */
/* Settings deals its category cards as the same hand as the main-menu decks,
   at the same size - no per-card overrides, so it inherits .menu-hand exactly
   (Back card at the far right, standard card dimensions). */
.settings-hand { margin-top: 6px; }

/* category cards (legacy glass-row landing - kept for reference, no longer used
   now that the landing fans playing cards) */
.set-cats { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
.set-cat {
  display: flex; align-items: center; gap: 12px; padding: 14px; text-align: left; cursor: pointer;
  border-radius: 14px; background: rgba(15,20,28,0.55); backdrop-filter: blur(10px);
  border: 1px solid rgba(255,255,255,0.10); color: inherit;
  transition: border-color 0.12s, box-shadow 0.12s, transform 0.12s;
}
.set-cat:hover { border-color: rgba(255,255,255,0.24); transform: translateY(-1px); }
.set-cat-ic {
  width: 34px; height: 34px; flex: 0 0 34px; display: flex; align-items: center; justify-content: center;
  border-radius: 9px; background: rgba(216,162,74,0.14); color: var(--accent-2);
}
.set-cat-ic svg { width: 19px; height: 19px; }
.set-cat-tx { flex: 1; min-width: 0; }
.set-cat-title { font-size: 14px; font-weight: 700; color: #e8ecf0; }
.set-cat-desc { font-size: 12px; color: rgba(255,255,255,0.55); margin-top: 2px; }
.set-cat-chev { color: rgba(255,255,255,0.38); display: flex; }
.set-cat-chev svg { width: 16px; height: 16px; }

/* back chip inside a category */
.set-back {
  display: inline-flex; align-items: center; gap: 5px; padding: 6px 12px 6px 8px; margin-bottom: 16px;
  border-radius: 999px; cursor: pointer; font-size: 12px; font-weight: 600; color: #e8ecf0;
  background: rgba(15,20,28,0.55); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.10);
  transition: border-color 0.12s;
}
.set-back:hover { border-color: rgba(255,255,255,0.24); }
.set-back-ic { display: flex; }
.set-back-ic svg { width: 15px; height: 15px; }

/* option rows with a segmented pill control */
.set-rows { display: flex; flex-direction: column; gap: 10px; }
.set-row {
  display: flex; align-items: center; gap: 16px; padding: 12px 14px; border-radius: 12px;
  background: rgba(15,20,28,0.55); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.10);
}
.set-row-tx { flex: 1; min-width: 0; }
.set-row-title { font-size: 13px; font-weight: 700; color: #e8ecf0; }
.set-row-desc { font-size: 11.5px; color: rgba(255,255,255,0.5); margin-top: 2px; line-height: 1.4; }
.seg { display: flex; gap: 3px; padding: 3px; border-radius: 999px; background: rgba(0,0,0,0.25); border: 1px solid rgba(255,255,255,0.08); flex: 0 0 auto; }
.seg-pill {
  font-weight: 600; font-size: 11.5px; line-height: 1; color: rgba(255,255,255,0.7); padding: 6px 12px; border-radius: 999px;
  cursor: pointer; border: none; background: transparent; white-space: nowrap; transition: background 0.12s, color 0.12s;
}
.seg-pill:hover { color: #fff; }
.seg-pill.on { background: var(--accent-2); color: #2a1c04; }
.set-note { font-size: 11.5px; color: rgba(255,255,255,0.45); margin: 12px 2px 0; line-height: 1.5; }
/* The same note printed UNDER a section heading rather than after a group, so
   it leads the rows instead of trailing them. Only the margins differ. */
.set-note-lead { margin: -4px 2px 10px; }

/* ---- Keyboard section (js/views/settings.js + js/keys.js) ----
   One row per shortcut, in the same glass card as every other setting row; the
   right-hand control is a run of key caps rather than a segmented pill.

   A cap is a real <kbd> so it reads as a key to a screen reader as well as to
   the eye, and the printed characters come from TCG.keys.caps(), never from
   this file - the stylesheet draws a cap, it never decides what is on one. */
.set-keys { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; flex-wrap: wrap; justify-content: flex-end; max-width: 58%; }
.key-combo { display: inline-flex; align-items: center; }
.key-caps {
  display: inline-flex; align-items: center; gap: 3px; padding: 4px 6px; border-radius: 7px;
  background: rgba(0,0,0,0.25); border: 1px solid rgba(255,255,255,0.10);
  cursor: pointer; transition: border-color 0.12s, background 0.12s;
}
button.key-caps:hover { border-color: var(--accent); background: rgba(0,0,0,0.34); }
/* An inert cap (a locked action) keeps the shape and loses the affordance:
   dashed edge, no pointer, the reason in its tooltip. Same three-shape rule the
   effect pills follow - a control that can't be used goes idle, never away. */
.key-combo.is-locked .key-caps { cursor: default; border-style: dashed; opacity: 0.62; }
/* Longhands, not a `font:` shorthand. `font: 600 11px/1 inherit` is invalid and
   the browser drops the whole declaration in silence - the bug that had seven
   sequencer controls rendering at the UA default (see dev/menu-design.html). */
.key-cap {
  font-size: 11px; font-weight: 600; line-height: 1;
  min-width: 20px; padding: 4px 5px; border-radius: 5px; text-align: center;
  color: #e8ecf0; background: rgba(255,255,255,0.08);
  box-shadow: inset 0 -1px 0 rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.10);
}
.key-cap-none { font-size: 11px; font-weight: 600; color: rgba(255,255,255,0.42); padding: 4px 3px; }
/* Unbound is a state worth seeing from across the screen: this is what a row
   looks like after another action took its key. */
.key-combo.is-unset .key-caps { border-color: rgba(224,132,117,0.55); }
.key-combo.is-unset .key-cap-none { color: #e08475; }
.set-row.is-unbound { border-color: rgba(224,132,117,0.34); }
/* Listening for the press. The gold border is the only thing on screen that
   pulses, so it's obvious which row is waiting. */
.key-combo.is-listening .key-caps { border-color: var(--accent-2); animation: keyListen 1.1s ease-in-out infinite; }
.key-combo.is-listening .key-cap-none { color: var(--accent-2); }
@keyframes keyListen { 0%, 100% { box-shadow: 0 0 0 0 rgba(240,200,121,0.34); } 50% { box-shadow: 0 0 0 4px rgba(240,200,121,0); } }
.key-x {
  display: flex; align-items: center; justify-content: center; width: 18px; height: 18px; margin-left: 2px;
  border: none; background: transparent; border-radius: 5px; cursor: pointer; color: rgba(255,255,255,0.34);
  opacity: 0; transition: opacity 0.12s, color 0.12s;
}
.key-combo:hover .key-x, .key-x:focus-visible { opacity: 1; }
.key-x:hover { color: #e08475; background: rgba(255,255,255,0.07); }
.key-x svg { width: 11px; height: 11px; }
.key-mini {
  display: flex; align-items: center; justify-content: center; width: 24px; height: 24px;
  border-radius: 7px; cursor: pointer; color: rgba(255,255,255,0.55);
  background: rgba(0,0,0,0.22); border: 1px solid rgba(255,255,255,0.10);
  transition: border-color 0.12s, color 0.12s;
}
.key-mini:hover { border-color: var(--accent); color: var(--accent); }
.key-mini svg { width: 13px; height: 13px; }
/* Idle rather than gone, for the mini buttons and the reset-everything button
   alike: still on screen, still explains itself in its tooltip, does nothing. */
.key-mini.row-off, .set-choose-btn.row-off { opacity: 0.4; cursor: default; }
.key-mini.row-off:hover { border-color: rgba(255,255,255,0.10); color: rgba(255,255,255,0.55); }
.set-choose-btn.row-off:hover { border-color: rgba(255,255,255,0.12); color: var(--accent-2); }
/* The caps are small on purpose (the compact scale), so on a touch device they
   get the invisible expander every other small control gets, not a bigger box. */
@media (pointer: coarse) {
  .key-x, .key-mini { position: relative; }
  .key-x::before, .key-mini::before { content: ""; position: absolute; inset: -10px; }
  .set-keys { gap: 10px; }
}

/* ---- Profile section (js/views/settings.js) ----
   A row whose control is too big to sit beside its label stacks under it
   instead, keeping the same glass card. */
.set-row-stack { flex-direction: column; align-items: stretch; gap: 10px; }
.set-row-title { display: flex; align-items: baseline; gap: 8px; }
.set-row-count { font-size: 11px; font-weight: 600; color: var(--text-faint); }
/* The name field. Written `input.set-text`, not `.set-text`, on purpose: the
   global input[type="text"] rule sets `width: 100%` at 0-1-1, so a bare class
   (0-1-0) loses and the field silently stretches the whole row. Same cascade
   trap as the search boxes, and it caught this field on the way in. */
input.set-text { flex: 0 0 auto; width: 190px; font-size: 12px; }

.set-crest-grid { display: flex; flex-wrap: wrap; gap: 7px; }

/* The border picker. Each option shows a REAL .seat pill wearing the frame, so
   the preview and the table can never disagree. Generous padding around the
   stage because the frames sit OUTSIDE the pill and would otherwise be clipped
   by their own tile. */
/* The unlock moment: a small glass card that shows the FRAME ITSELF on a real
   seat pill, because the thing worth showing is the thing you got. Bottom
   right, out of the way of the phase bar and the seat rail, and above the
   toasts so it is not buried by one. Click it to go and wear it. */
.bunlock {
  position: fixed; right: 18px; bottom: 20px; z-index: 1250; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 12px 18px 12px; border-radius: 14px; text-align: center; min-width: 168px;
  background: rgba(15,20,28,0.82); -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  border: 1px solid rgba(216,162,74,0.34);
  box-shadow: 0 18px 44px rgba(0,0,0,0.55), 0 0 26px -12px var(--accent-2);
  opacity: 0; transform: translateY(10px) scale(0.96);
  transition: opacity 0.2s ease, transform 0.28s cubic-bezier(0.2, 1.3, 0.4, 1);
}
.bunlock.show { opacity: 1; transform: translateY(0) scale(1); }
.bunlock-tag { font-size: 9.5px; text-transform: uppercase; letter-spacing: 1.1px; font-weight: 700; color: var(--accent-2); }
/* Room for the frame, which sits OUTSIDE the pill and would otherwise be
   clipped by the card's own padding. */
.bunlock-stage { display: flex; align-items: center; justify-content: center; padding: 8px 10px 6px; }
.bunlock-name { font-size: 14px; font-weight: 700; color: var(--text); }
.bunlock-sub { font-size: 10.5px; color: var(--text-faint); }

.bpick-head { display: flex; align-items: center; gap: 16px; }
.bpick-note { font-size: 11px; line-height: 1.4; color: var(--accent-2); background: rgba(216,162,74,0.10); border: 1px solid rgba(216,162,74,0.28); border-radius: 8px; padding: 6px 9px; }
.bpick-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
.bpick-opt {
  position: relative; display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 14px 10px 10px; border-radius: 11px; cursor: pointer; text-align: center;
  border: 1px solid rgba(255,255,255,0.09); background: rgba(255,255,255,0.03);
  transition: border-color 0.12s, background 0.12s, transform 0.12s;
}
.bpick-opt:hover { border-color: var(--accent); background: rgba(255,255,255,0.07); transform: translateY(-1px); }
.bpick-opt.sel { border-color: var(--accent-2); box-shadow: inset 0 0 0 1px var(--accent-2); background: rgba(216,162,74,0.10); }
.bpick-opt-stage { display: flex; align-items: center; justify-content: center; min-height: 42px; padding: 4px 8px; }
.bpick-opt-name { font-size: 12px; font-weight: 700; color: var(--text); }
.bpick-opt-sub { font-size: 10.5px; line-height: 1.35; color: var(--text-faint); }
.bpick-opt-lock { position: absolute; top: 7px; right: 8px; display: inline-flex; color: var(--text-faint); }
.bpick-opt-lock svg { width: 12px; height: 12px; }
/* Locked is dimmed, never hidden: the frame still shows, so the set reads as a
   path rather than a short list that mysteriously grows. With the preview
   switch on, an unearned frame comes back to full strength (you can wear it)
   but keeps its lock and its requirement, so trying one on never reads as
   having got it. */
.bpick-opt.locked .bpick-opt-stage { opacity: 0.4; }
.bpick-opt.locked .bpick-opt-name { color: var(--text-dim); }
.bpick-opt.previewing .bpick-opt-stage { opacity: 1; }
.bpick-opt.previewing .bpick-opt-name { color: var(--text); }
.bpick-opt.previewing .bpick-opt-lock { color: var(--accent-2); }
/* The demo pill is the real .seat, shrunk a little and stripped of the table's
   own interactions. */
.bpick-demo { cursor: default; pointer-events: none; font-size: 11.5px; padding: 3px 9px 3px 4px; }
.bpick-demo .seat-av { width: 22px; height: 22px; }
.bpick-demo .seat-av svg { width: 14px; height: 14px; }
.set-crest-auto { font-size: 10.5px; font-weight: 700; color: var(--text-dim); letter-spacing: 0.3px; }

/* Two columns you drag between: your wheel on the left, the whole pool on the
   right. They stack on a narrow screen rather than squeezing, since a drop
   target thinner than a message is no use to anybody. */
.emote-picker { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); gap: 12px; align-items: start; }
@media (max-width: 720px) { .emote-picker { grid-template-columns: minmax(0, 1fr); } }
.emote-picker-col { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.emote-picker-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 0 2px; }
.emote-picker-head > span:first-child { font-size: 12px; font-weight: 700; color: var(--text-dim); }
/* Both columns are drop targets, so both read as a defined area rather than
   loose content, and both light up when something is over them. */
.emote-zone {
  border-radius: 10px; border: 1px dashed rgba(255,255,255,0.10);
  background: rgba(0,0,0,0.16); padding: 6px; min-height: 76px;
  transition: border-color 0.12s, background 0.12s;
}
.emote-zone.drop-on { border-color: var(--accent-2); background: rgba(216,162,74,0.08); border-style: solid; }
.emote-zone-hint { font-size: 11px; color: var(--text-faint); text-align: center; padding: 8px 4px 6px; }
/* The pool can run long; it scrolls inside itself rather than pushing the page,
   the same rule every tall glass panel follows. */
.emote-pool { max-height: 340px; overflow-y: auto; }

/* Your wheel, in slot order. A list, because order is what this column edits
   and "third from the top" only reads in a list. */
.emote-loadout { display: flex; flex-direction: column; gap: 4px; }
.emote-slot {
  display: flex; align-items: center; gap: 8px; padding: 5px 6px 5px 6px;
  border-radius: 9px; border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.03);
  cursor: grab; touch-action: none;
}
.emote-slot:active { cursor: grabbing; }
.emote-slot-grip { display: inline-flex; flex: 0 0 auto; color: var(--text-faint); }
.emote-slot-grip svg { width: 13px; height: 13px; }
.emote-slot:hover .emote-slot-grip { color: var(--accent-2); }
/* The row being carried: left in place, faded, so the list keeps its shape
   while the ghost moves. */
.emote-slot.dragging, .emote-pick.dragging { opacity: 0.3; }
/* The clone under the cursor. Fixed and click-through, so it never becomes the
   drop target it is being dragged onto. */
.emote-drag-ghost {
  position: fixed; z-index: 1300; pointer-events: none; margin: 0;
  opacity: 0.96; transform: rotate(-1.5deg);
  box-shadow: 0 12px 28px rgba(0,0,0,0.55); border-color: var(--accent-2);
}
/* Where it would land. A gold rule between rows, not a gap that opens - a gap
   makes every row below it jump while the pointer is still moving. */
.emote-drop-line { height: 2px; border-radius: 2px; background: var(--accent-2); box-shadow: 0 0 8px rgba(216,162,74,0.6); margin: 1px 0; }
.emote-slot-n { flex: 0 0 auto; width: 16px; font-size: 10.5px; font-weight: 700; color: var(--text-faint); text-align: center; }
.emote-slot-ic { display: inline-flex; flex: 0 0 auto; color: var(--accent-2); }
.emote-slot-ic svg { width: 14px; height: 14px; }
.emote-slot-tx { flex: 1; min-width: 0; font-size: 12px; font-weight: 600; color: var(--text); }
.emote-slot-btn {
  flex: 0 0 auto; width: 22px; height: 22px; padding: 0; display: inline-flex; align-items: center; justify-content: center;
  border-radius: 6px; border: 1px solid rgba(255,255,255,0.10); background: rgba(255,255,255,0.04);
  color: var(--text-dim); cursor: pointer; transition: border-color 0.12s, color 0.12s, background 0.12s;
}
.emote-slot-btn svg { width: 13px; height: 13px; }
.emote-slot-btn:hover { border-color: var(--accent); color: var(--accent); background: rgba(216,162,74,0.10); }
.emote-slot-x:hover { border-color: var(--red); color: var(--red); background: rgba(255,90,90,0.10); }
/* A move that has nowhere to go, or the last message on the wheel: greyed in
   place with the reason on it, never removed from the row. */
.emote-slot-btn.disabled { opacity: 0.3; cursor: not-allowed; }
.emote-slot-btn.disabled:hover { border-color: rgba(255,255,255,0.10); color: var(--text-dim); background: rgba(255,255,255,0.04); }

/* The pool. Chips grouped by when you'd say them, since this column is about
   finding a message rather than ordering one. */
.emote-pool { display: flex; flex-direction: column; gap: 3px; }
.emote-pool .emote-pick { cursor: grab; touch-action: none; }
.emote-pool .emote-pick:active { cursor: grabbing; }
.emote-pool .emote-pick.disabled { cursor: not-allowed; }
.emote-pool-head { font-size: 10px; text-transform: uppercase; letter-spacing: 0.7px; color: var(--text-faint); font-weight: 700; margin: 6px 0 1px 3px; }
.emote-pool-head:first-child { margin-top: 0; }
.emote-pool-row { display: flex; flex-wrap: wrap; gap: 5px; }
/* A carried message wears the gold the rest of the tool reserves for "this one
   is on", so the two halves of the picker agree at a glance. */
.emote-pool .emote-pick.on { border-color: var(--accent-2); color: var(--text); background: rgba(216,162,74,0.14); }

/* Global-lane rule editors (Attacking rules / Card rules): compact rows, a gold
   pill dropdown, and an iOS-style switch - the app's glass/gold language, no
   native checkbox or select. */
.gl-rule-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 6px 0; font-size: 11.5px; }
.gl-rule-lbl { flex: 1 1 auto; min-width: 0; line-height: 1.25; }
/* A rule that can't apply to THIS game goes idle rather than away (the app-wide
   "controls never disappear" rule) - dimmed, unclickable, its own title saying
   what to build first. The row keeps its pointer events so that title can still
   be read; only the control inside it stops answering. */
.gl-rule-row.row-off { opacity: 0.4; }
.gl-rule-row.row-off .tgl, .gl-rule-row.row-off .pill-dd, .gl-rule-row.row-off input { pointer-events: none; }

/* THE SENTENCE BODY. The second thing a sequencer menu's body may be, beside a
   stack of .gl-rule-row: one line of English with a pill on every settable word.
   Rules 12-15 in dev/menu-design.html; built by glSentence in 18-step-types.js.
   Use it when the pill sets ONE rule made of several slots (a choice step's
   actions, a matchup's axis and order, a decide-winner's what-and-where); use
   rows when it sets several independent rules (Card rules, Attacking rules).
   The test: write out what the pill sets as one line of plain English - one
   sentence means a sentence, three means rows.
   The panel is 340 wide rather than the rows' 240, which is not a per-panel
   decision but the sentence's one width: it buys two wrapped lines instead of
   four, and over a felt a wide short panel covers a band of table where a
   narrow tall one sits on the board.
   Same gold pill language as the card-effect builder (.free-fx-pill), on
   purpose - somebody who has written a card effect can read a step's rule
   without being taught a second vocabulary. Note the longhand font properties,
   for the same reason the .pill-dd block above spells them out. */
.gl-sent { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; font-size: 11.5px; line-height: 1.85; }
/* grammar words: not controls, so no box, no pointer, and never gold */
.gl-sent-word { color: var(--text-dim); }
.gl-sent-pill {
  display: inline-flex; align-items: center; gap: 4px; padding: 2px 7px; border-radius: 7px;
  font-family: inherit; font-size: 11.5px; font-weight: 700; line-height: 1.35; cursor: pointer;
  border: 1px solid rgba(216,162,74,0.5); background: rgba(216,162,74,0.13); color: var(--accent-2);
  transition: background 0.12s, border-color 0.12s;
}
.gl-sent-pill:hover { background: rgba(216,162,74,0.26); border-color: var(--accent); }
/* the pill whose menu is open, so the sentence says where you are */
.gl-sent-pill.live { background: rgba(216,162,74,0.3); border-color: var(--accent); }
.gl-sent-pill svg { width: 12px; height: 12px; }
.gl-sent-ic { display: inline-flex; }
.gl-sent-caret { display: inline-flex; opacity: 0.55; }
.gl-sent-caret svg { width: 11px; height: 11px; }
/* a value that is a number rather than a choice: same box, quieter fill */
.gl-sent-pill.num { border-color: var(--line-2); background: rgba(255,255,255,0.05); color: var(--text); }
.gl-sent-pill.num:hover { border-color: var(--accent); background: rgba(255,255,255,0.09); }
/* the adder that extends the list. Dashed like .tphase.add, ALWAYS present -
   an add control that appears only once there is something to add to is one
   nobody can find (the controls-never-disappear rule, read for a sentence). */
.gl-sent-pill.add { border-style: dashed; border-color: var(--line-2); background: none; color: var(--text-dim); font-weight: 600; }
.gl-sent-pill.add:hover { border-style: solid; border-color: var(--accent); color: var(--accent-2); background: rgba(216,162,74,0.08); }
/* a slot this shape cannot use: idle in place with the reason on it, never
   dropped from the line (same contract as .fx-pill-off on a card) */
.gl-sent-pill.off { border-style: dashed; border-color: var(--line-2); background: none; color: var(--text-faint); cursor: default; font-weight: 600; }
.gl-sent-pill.off:hover { border-style: dashed; border-color: var(--line-2); background: none; color: var(--text-faint); }
/* an empty list says so, once, at the empty-state scale rule 4 allows */
.gl-sent-empty { font-size: 10px; opacity: 0.7; color: var(--text-dim); font-style: italic; }

/* gold pill dropdown: shows the current choice, opens a glass menu on click.
   Note the longhand type here and in the six controls around it. They were all
   written `font: 600 11px/1 inherit`, which is an INVALID shorthand - `inherit`
   is not a family token - so the browser dropped the whole declaration and took
   the size and the weight with it. On a <span> that hid itself (it inherited the
   row's 11.5px and looked about right); on a <button> like this one it fell all
   the way back to the UA default, so every gold dropdown in every sequencer menu
   rendered at 13.33px/400 instead of 11px/600. Found by measuring the panel
   against dev/menu-design.html's scale table, not by reading the stylesheet. */
.pill-dd {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 5px; max-width: 56%;
  font-weight: 600; font-size: 11px; line-height: 1; color: #2a1c04; background: var(--accent-2); border: none;
  padding: 5px 9px; border-radius: 999px; cursor: pointer; white-space: nowrap;
  box-shadow: 0 1px 3px rgba(0,0,0,0.3); transition: filter 0.12s;
}
.pill-dd:hover { filter: brightness(1.07); }
.pill-dd-tx { overflow: hidden; text-overflow: ellipsis; }
.pill-dd-caret { display: inline-flex; flex: 0 0 auto; }
.pill-dd-caret svg { width: 12px; height: 12px; }

/* Glass text field for renaming (the Names pill): matches the compact glass UI.
   Written `input.gl-name-input` (0-1-1), not `.gl-name-input` (0-1-0), because
   the global `input[type="text"], ... { }` block at the top of this file is
   0-1-1 and was winning every property they share - so this field was rendering
   full-width at 14px with 9px 11px padding and the plain background, inside a
   250px glass menu. Third time this trap has bitten (see the search-field block
   above and .ce-gal-search); the rule is in CLAUDE.md. */
input.gl-name-input {
  flex: 0 0 auto; width: 50%; max-width: 56%; font-weight: 600; font-size: 11px; line-height: 1; color: #f2ede2;
  background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.14);
  padding: 5px 8px; border-radius: 7px; outline: none; transition: border-color 0.12s, background 0.12s;
}
.gl-name-input::placeholder { color: rgba(255,255,255,0.4); font-weight: 500; }
.gl-name-input:focus { border-color: var(--accent-2); background: rgba(255,255,255,0.11); }

/* compact -/+ stepper for a Global-lane number rule (Card rules play caps) */
.gl-step { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px; }
.gl-step-btn {
  width: 22px; height: 22px; display: inline-flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 14px; line-height: 1; color: #f2ede2; background: rgba(255,255,255,0.09);
  border: 1px solid rgba(255,255,255,0.16); border-radius: 7px; cursor: pointer; transition: background 0.12s;
}
.gl-step-btn:hover { background: rgba(255,255,255,0.16); }
.gl-step-val { min-width: 32px; text-align: center; font-weight: 600; font-size: 11px; line-height: 1; color: #f2ede2; }

/* An idle Global-lane rule row: a control whose setting can't bite right now.
   It stays on screen and stays clickable (changing it is how a builder discovers
   what it needs) - it just says so, the same "controls never disappear" rule the
   effect sentence's .fx-pill-off follows. */
.gl-rule-idle { opacity: 0.5; }
.gl-rule-idle .pill-dd { box-shadow: none; filter: saturate(0.5); }

/* A section head inside a Global-lane panel: the 10px uppercase head from
   dev/menu-design.html, with room for the one bare icon button that adds to the
   group it names. This is how a panel says what a group of rows IS - it replaced
   a run of <p class="hint"> paragraphs, since a panel never prints an
   explanation (that is what the ? tool and data-help are for). */
.gl-sec {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  margin: 10px 0 4px; font-size: 10px; font-weight: 700; letter-spacing: 0.7px;
  text-transform: uppercase; color: var(--text-faint);
}
.gl-sec:first-child { margin-top: 2px; }
.gl-sec-t { flex: 1 1 auto; min-width: 0; }
.gl-sec-add {
  flex: 0 0 auto; width: 20px; height: 20px; display: inline-flex; align-items: center; justify-content: center;
  padding: 0; color: var(--accent-2); background: rgba(216,162,74,0.14);
  border: 1px solid rgba(216,162,74,0.28); border-radius: 6px; cursor: pointer; transition: background 0.12s;
}
.gl-sec-add:hover { background: rgba(216,162,74,0.26); }
.gl-sec-empty { font-size: 10.5px; color: var(--text-faint); font-style: italic; margin: 2px 0 4px; }

/* Matchup editor (the Global-lane Matchups pill). The ordered list of values,
   and the preview grid that proves what the rule actually does - a property list
   is not evidence that a comparison compares what its author meant.

   THE ICON RULE, and the trap it closes: `.btn svg { width: 18px }` needs the
   `.btn` class, and these panels build their controls as a bare `btn-ghost
   btn-sm` with an unclassed <span> around the glyph. So nothing sized those
   svgs at all, and an svg with a viewBox and no width renders at the browser's
   300x150 default for a replaced element - which is why every + and every
   delete X in here came out enormous and the exception row's delete was pushed
   out of reach. Any new control in either panel is covered by this one rule. */
.mu-edit button svg, .kw-lib button svg { width: 12px; height: 12px; display: block; }
.mu-edit { display: flex; flex-direction: column; }
.mu-ord-row { display: flex; align-items: center; gap: 4px; margin: 4px 0; font-size: 11.5px; }
.mu-ord-btn {
  flex: 0 0 auto; width: 20px; height: 20px; display: inline-flex; align-items: center; justify-content: center;
  padding: 0; color: var(--text-dim); background: transparent; border: 1px solid transparent;
  border-radius: 6px; cursor: pointer; transition: background 0.12s, color 0.12s;
}
.mu-ord-btn:hover { color: var(--text); background: rgba(255,255,255,0.08); }
.mu-rm:hover { color: var(--red); background: rgba(210,95,90,0.16); }

/* One exception, on one line: [value] [beats] [value] [what it scores] [bin].
   The amount pill is the reason this row exists - a ring gives every pair the
   same arithmetic, and a hand-set number is the builder overruling it. */
.mu-ex-row { display: flex; align-items: center; gap: 4px; margin: 4px 0; }
.mu-ex-v, .mu-ex-op, .mu-ex-amt {
  display: inline-flex; align-items: center; justify-content: center; gap: 4px;
  padding: 3px 6px; font-size: 11px; font-weight: 600; line-height: 1; color: #f2ede2;
  background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.14);
  border-radius: 7px; cursor: pointer; transition: background 0.12s, border-color 0.12s;
}
.mu-ex-v:hover, .mu-ex-op:hover, .mu-ex-amt:hover { background: rgba(255,255,255,0.14); border-color: rgba(216,162,74,0.5); }
/* A value is a chip AND its name everywhere it is offered, so both pickers on an
   exception row are flex boxes with a cap: two long names either side of "beats"
   would otherwise push the amount and the bin off the end of the row. */
.mu-ex-v { flex: 0 1 auto; min-width: 0; max-width: 84px; display: inline-flex; align-items: center; gap: 4px; }
.mu-ex-op { flex: 0 0 auto; color: var(--text-dim); font-weight: 500; }
.mu-ex-amt { flex: 1 1 auto; color: var(--accent-2); font-variant-numeric: tabular-nums; }
.mu-ex-amt.auto { color: var(--text-faint); font-weight: 500; font-style: italic; }
/* Idle, not gone: a control never disappears because of what another one says. */
.mu-ex-amt.off { opacity: 0.42; cursor: default; border-style: dashed; }
.mu-ex-amt.off:hover { background: rgba(255,255,255,0.07); border-color: rgba(255,255,255,0.14); }
.mu-ord-n {
  flex: 0 0 auto; width: 16px; text-align: center; font-weight: 700; font-size: 9.5px; line-height: 1;
  color: var(--accent-2); opacity: 0.8;
}
.mu-ord-v { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mu-ord-row .mu-ord-btn[disabled] { opacity: 0.28; pointer-events: none; }
.mu-grid-wrap { margin: 2px 0 0; }
.mu-grid { display: grid; gap: 2px; --mu-lead: 62px; }
.mu-cell {
  display: flex; align-items: center; justify-content: center; min-width: 0;
  padding: 4px 2px; border-radius: 5px; font-weight: 600; font-size: 9.5px; line-height: 1;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  border: 0; font-family: inherit;
}
/* A cell is a control: a number in this grid is edited where it sits, which is
   the same rule the sequencer's pills follow one level up. */
button.mu-cell { cursor: pointer; }
button.mu-cell:hover { outline: 1px solid var(--accent-2); outline-offset: -1px; filter: brightness(1.25); }
.mu-head { background: none; color: rgba(255,255,255,0.5); font-weight: 700; font-size: 9px; }
/* The two headers wear a band so they read as headers rather than as more
   cells, and each carries the hairline on the side that faces the numbers. The
   2px grid gap breaks that line into segments, which is the price of keeping
   one set of cell metrics for the whole grid. */
.mu-head-col, .mu-head-row, .mu-corner { background: rgba(255,255,255,0.05); }
.mu-head-col {
  flex-direction: column; gap: 2px; padding: 3px 2px;
  border-bottom: 1px solid rgba(255,255,255,0.16); border-radius: 5px 5px 0 0;
}
.mu-head-row {
  justify-content: flex-start; gap: 4px; padding-left: 4px;
  border-right: 1px solid rgba(255,255,255,0.16); border-radius: 5px 0 0 5px;
}
.mu-corner {
  flex-direction: column; align-items: flex-start; justify-content: center; gap: 1px;
  padding-left: 4px; border-radius: 5px 0 0 0;
  border-right: 1px solid rgba(255,255,255,0.16); border-bottom: 1px solid rgba(255,255,255,0.16);
}
.mu-corner-l { font-size: 8px; font-weight: 600; letter-spacing: 0.2px; color: rgba(255,255,255,0.42); }
/* text-overflow does not reach the children of a flex box, so the ellipsis
   lives on the text's own span rather than on the cell it sits in. */
.mu-head-txt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.mu-head-col .mu-head-txt { max-width: 100%; text-align: center; }
.mu-head-row .mu-head-txt { flex: 1 1 auto; }
.mu-head-name { font-weight: 600; font-size: 9.5px; }
/* Pointing at a cell answers "which row and which column is this" by taking
   every other pair off the grid for a moment. Opacity only, so nothing moves
   and nothing re-lays-out under the pointer. */
.mu-cell { transition: opacity 90ms linear; }
.mu-grid.mu-hot .mu-cell { opacity: 0.28; }
.mu-grid.mu-hot .mu-cross, .mu-grid.mu-hot .mu-lit, .mu-grid.mu-hot .mu-corner { opacity: 1; }
.mu-head.mu-lit { color: var(--accent-2); background: rgba(216,162,74,0.14); }
/* The pair as a sentence, kept after the pointer leaves rather than blanked. */
.mu-read {
  min-height: 12px; margin: 5px 2px 0; font-size: 10px; line-height: 1.2;
  color: var(--text-dim); font-variant-numeric: tabular-nums;
}
/* Win and lose read as one diverging pair rather than two unrelated hues, so the
   gap between two cards is legible as a distance and not just a colour. */
.mu-win { background: rgba(110,190,140,0.20); color: #b8e6c8; }
.mu-lose { background: rgba(210,95,90,0.20); color: #f0b6b2; }
.mu-draw { background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.45); }
.mu-none { background: none; color: rgba(255,255,255,0.25); }
.mu-over { outline: 1px solid var(--accent-2); outline-offset: -1px; }
/* A value that names a colour is shown as that colour instead of being spelled
   out. The inset hairline is what keeps a dark chip off a dark panel and a pale
   one off the glass, so one rule serves every colour without a per-colour
   border. In the grid head the chip fills the cell, since four characters of a
   hex ("#8a2") is the one place there was never room for the words anyway. */
.mu-sw {
  display: inline-block; width: 26px; height: 12px; border-radius: 4px;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.12);
}
.mu-ord-col { display: flex; align-items: center; gap: 5px; }
.mu-head-sw { padding: 2px; }
.mu-sw-cell { width: 100%; height: 7px; border-radius: 3px; flex: 0 0 auto; }
.mu-sw-row { width: 10px; height: 10px; border-radius: 3px; flex: 0 0 auto; }
/* The same chip in a menu row, round because it sits in the icon's slot. */
.ctx-ic-sw { display: inline-flex; align-items: center; justify-content: center; }
.ctx-sw {
  display: inline-block; width: 12px; height: 12px; border-radius: 50%;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.14);
}

/* Split-area editor (editZonePartition): compact region rows at pill scale,
   matching the Global-lane rule pills. A colour dot ties each region to its felt
   band, a pill-dd names what the band holds, a -/+ stepper sets its share, and a
   kebab holds move/remove. The add button is small, not a full-width slab. */
.part-head { font-size: 10px; letter-spacing: 0.6px; text-transform: uppercase; opacity: 0.55; font-weight: 700; margin: 8px 0 4px; }
.part-reg { display: flex; align-items: center; gap: 6px; margin: 5px 0; }
.part-dot { flex: 0 0 auto; width: 10px; height: 10px; border-radius: 3px; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.28); }
.part-match { flex: 1 1 auto; min-width: 0; }
.part-kebab {
  flex: 0 0 auto; width: 22px; height: 22px; display: inline-flex; align-items: center; justify-content: center;
  border-radius: 6px; background: transparent; border: none; color: rgba(255,255,255,0.7); cursor: pointer; transition: background 0.12s;
}
.part-kebab:hover { background: rgba(255,255,255,0.12); color: #fff; }
.part-kebab svg { width: 15px; height: 15px; }
.part-add {
  display: inline-flex; align-items: center; gap: 5px; margin-top: 8px; padding: 4px 9px; font-size: 11px;
  border-radius: 7px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.12); color: #f2ede2; cursor: pointer; transition: background 0.12s;
}
.part-add:hover { background: rgba(255,255,255,0.12); }
.part-add-ic { display: inline-flex; }
.part-add-ic svg { width: 13px; height: 13px; }

/* iOS-style switch: glass track, gold when on, sliding knob */
.tgl {
  flex: 0 0 auto; position: relative; width: 34px; height: 19px; padding: 0; border-radius: 999px;
  background: rgba(255,255,255,0.14); border: 1px solid rgba(255,255,255,0.16); cursor: pointer;
  transition: background 0.14s, border-color 0.14s;
}
.tgl.on { background: var(--accent-2); border-color: var(--accent-2); }
.tgl-knob {
  position: absolute; top: 1px; left: 1px; width: 15px; height: 15px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.45); transition: transform 0.14s;
}
.tgl.on .tgl-knob { transform: translateX(15px); }

/* felt colour swatches (Table look > Felt colour) */
.set-swatches { display: flex; gap: 10px; flex-wrap: wrap; }
.set-sw {
  width: 44px; height: 44px; border-radius: 11px; cursor: pointer; padding: 0; position: relative;
  border: 2px solid rgba(255,255,255,0.14); box-shadow: inset 0 2px 8px rgba(0,0,0,0.4);
  transition: border-color 0.12s, transform 0.12s;
}
.set-sw:hover { transform: translateY(-1px); border-color: rgba(255,255,255,0.3); }
.set-sw.on { border-color: var(--accent-2); box-shadow: inset 0 2px 8px rgba(0,0,0,0.4), 0 0 0 2px rgba(216,162,74,0.35); }
.set-sw-check { position: absolute; inset: 0; display: none; align-items: center; justify-content: center; color: #fff; }
.set-sw.on .set-sw-check { display: flex; }
.set-sw-check svg { width: 18px; height: 18px; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.7)); }

/* =================================================================
   Storage panel (js/views/library.js openStoragePanel) - a glass modal
   showing the localStorage budget and every saved game heaviest first,
   with Compact / Export / Delete per game. Rides the base .modal glass;
   these rules only lay out the meter and the rows, at pill scale.
   ================================================================= */
.modal.modal-wide:has(.stg-panel) { max-width: 540px; }
.stg-panel { display: flex; flex-direction: column; gap: 11px; }
.stg-panel.is-busy { opacity: 0.6; pointer-events: none; }
.stg-meter { display: flex; flex-direction: column; gap: 5px; }
.stg-meter-track { height: 9px; border-radius: 999px; overflow: hidden; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.10); }
.stg-meter-fill { height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--accent-2), var(--accent)); transition: width 0.25s ease; }
.stg-meter.full .stg-meter-fill { background: linear-gradient(90deg, #c96b4a, #d8574a); }
.stg-meter-cap { display: flex; justify-content: space-between; font-size: 11px; color: var(--text-dim); }
.stg-meter-pct { color: var(--text); font-weight: 650; }
.stg-warn { font-size: 11px; line-height: 1.4; color: #e6b98f; background: rgba(201,107,74,0.12); border: 1px solid rgba(216,87,74,0.28); border-radius: 8px; padding: 7px 9px; }
.stg-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.stg-head-t { font-size: 11px; color: var(--text-dim); }
.stg-empty { font-size: 12px; color: var(--text-dim); padding: 10px 2px; }
.stg-list { display: flex; flex-direction: column; gap: 6px; max-height: 52vh; overflow-y: auto; padding-right: 2px; }
.stg-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border-radius: 10px;
  background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08);
}
.stg-row:hover { border-color: rgba(255,255,255,0.16); }
.stg-row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.stg-row-name { font-size: 12.5px; font-weight: 650; color: #e8ecf0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.stg-row-meta { font-size: 10.5px; color: rgba(255,255,255,0.5); }
.stg-row-bar { height: 5px; border-radius: 999px; overflow: hidden; background: rgba(255,255,255,0.07); margin-top: 2px; }
.stg-row-fill { height: 100%; border-radius: inherit; background: rgba(255,255,255,0.22); }
.stg-row-fill-img { height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--accent-2), var(--accent)); }
.stg-row-acts { flex: 0 0 auto; display: flex; align-items: center; gap: 2px; }

/* ---- Piece builder (drag-and-snap effect authoring) ---- */
.pb-overlay { position: fixed; inset: 0; z-index: 300; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.5); backdrop-filter: blur(3px); }
.pb-panel { width: min(680px, 94vw); max-height: 88vh; display: flex; flex-direction: column;
  background: rgba(15,20,28,0.72); backdrop-filter: blur(14px); border: 1px solid rgba(255,255,255,0.12);
  border-radius: 14px; box-shadow: var(--shadow); color: var(--text); }
/* Embedded (in the effect panel, no popup): fill the panel width, no overlay chrome. */
.pb-embed { width: 100%; max-height: none; box-shadow: none; border-color: rgba(255,255,255,0.10); background: rgba(15,20,28,0.5); }
.pb-embed .pb-body { overflow: visible; padding: 8px 10px; }
.pb-embed .pb-head { padding: 8px 10px; }
.ce-fx-dock { margin-bottom: 6px; }
.ce-fx-dock-sep { border-top: 1px solid rgba(255,255,255,0.08); margin: 8px 0 6px; }
/* The piece builder docked at the card (bottom-centre of the editor stage). */
/* The effect piece builder docks as a right-side vertical menu, matching the
   element-editor panel (.ce-panel): same top/right offset, same glass, so it
   never overlaps the card. It carries the assembly rows above a searchable,
   collapsible palette, and scrolls inside itself when tall. */
/* top + bottom give the dock a definite height, so the flex:1 body below can
   resolve its own height and scroll rather than run off the bottom of the felt. */
.ce-fx-carddock { position: absolute; top: 64px; bottom: 16px; right: 14px; left: auto; transform: none; width: 300px; z-index: 20; display: flex; flex-direction: column; }
/* The .ce-fx-dock wrapper must itself be a bounded flex column, or .pb-embed's
   flex:1 has no flex parent to resolve against and the panel grows to content
   height - which leaves .pb-body with no fixed height to scroll inside, so the
   palette runs off the bottom of the dock. This is the middle link the scroll
   chain needs. */
.ce-fx-carddock .ce-fx-dock { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.ce-fx-carddock .pb-embed { flex: 1; min-height: 0; max-height: none; display: flex; flex-direction: column; background: rgba(15,20,28,0.82); box-shadow: var(--shadow); border-color: rgba(255,255,255,0.12); }
.ce-fx-carddock .pb-embed .pb-body { flex: 1; min-height: 0; overflow-y: auto; }
.ce-fx-carddock .ce-fx-loadrow { margin-top: 6px; display: flex; justify-content: center; }
.ce-fx-carddock .ce-fx-loadrow .ce-load-pieces { font-size: 11px; }
/* The card-level effect settings, under the palette. They used to live in the
   right-hand panel, which is this same slot - so they could only be on screen
   while the builder was tucked out of it, and tucking is gone. Here they are
   simply always present while effects are being edited. Glass to match the
   palette above, and at panel scale like every other row of icon buttons. */
.ce-fx-textslot:empty { display: none; }
.ce-fx-textslot { margin-top: 6px; padding: 6px; border-radius: 10px;
  background: rgba(15,20,28,0.82); border: 1px solid rgba(255,255,255,0.12); box-shadow: var(--shadow); }
.ce-fx-textslot .ce-fxtext-row { margin: 0; justify-content: center; flex-wrap: wrap; }
/* The effect-assembly stage lives ON the card face while editing effects (the
   palette + footer stay in the dock above): drag a palette piece onto the card
   and it snaps and shows its green/amber/red chip right here. A compact glass
   strip over the card's lower band, scrolling inside itself as the effect grows. */
.pb-stage.ce-fx-oncard {
  /* Wider than the 290px card: a centred strip that reaches into the free felt
     around the card (the right panel is hidden while editing effects), so a row
     fits more pieces before it wraps. Capped to the viewport on narrow windows. */
  position: absolute; left: 50%; right: auto; bottom: 5%; transform: translateX(-50%);
  width: 150%; max-width: 96vw; z-index: 8;
  /* Being 150% of the card's width, the strip reaches into the lane the shortcut
     column occupies to the card's left. The column keeps its place (it belongs
     with the card), so the strip is the one that stops: it grows up to 84% of the
     card as before, but never past the bottom of the column. --ce-dock-h is the
     column's measured height, published by renderDock; with no column (or no
     layout to measure) it falls back to the plain 84%. Past that the strip
     scrolls inside itself, which it already does. */
  min-height: 28%; max-height: min(84%, calc(95% - var(--ce-dock-h, 0px) - 12px)); overflow-y: auto;
  padding: 6px; gap: 6px;
  background: rgba(15,20,28,0.74); backdrop-filter: blur(10px);
  border: 1px solid rgba(255,255,255,0.12); border-radius: 10px;
  box-shadow: 0 6px 20px rgba(0,0,0,0.5);
}
.pb-stage.ce-fx-oncard .pb-hint { font-size: 10px; padding: 4px; }
.pb-stage.ce-fx-oncard .pb-rowline { padding: 3px 4px; gap: 6px; }
.pb-stage.ce-fx-oncard .pb-rowline-pieces { min-height: 24px; }
.pb-stage.ce-fx-oncard .pb-piece { margin: 2px 0; }
.pb-stage.ce-fx-oncard .pb-in { height: 22px; font-size: 11px; padding: 0 calc(5px + var(--pbd, 9px)); }
.pb-stage.ce-fx-oncard .pb-lflat > .pb-in { padding-left: 7px; }
.pb-stage.ce-fx-oncard .pb-rflat > .pb-in { padding-right: 7px; }
.pb-stage.ce-fx-oncard .pb-newrow { font-size: 10px; padding: 3px 8px; }
.pb-stage.ce-fx-oncard .pb-chip { font-size: 9px; padding: 1px 6px; }

/* Palette search box: a magnifier pinned inside the field on the left, so the
   typed text is padded clear of it and never overlaps. A clear (x) button
   appears on the right only once something is typed. */
.pb-search { position: relative; display: flex; align-items: center; margin-bottom: 4px; }
.pb-search-ic { position: absolute; left: 8px; top: 50%; transform: translateY(-50%); display: inline-flex; color: var(--text-faint); pointer-events: none; }
.pb-search-ic svg { width: 13px; height: 13px; }
/* The left gutter is NOT set here: the wrapper wears .search-wrap.search-sm and
   the shared pattern near the top of this file supplies it, so this field can't
   drift from every other search box. Only the right padding (for the clear
   button) is this field's own business. */
.pb-search input.pb-search-input { width: 100%; box-sizing: border-box; font: inherit; font-size: 11.5px; color: var(--text); background: rgba(255,255,255,0.05); border: 1px solid var(--line-2); border-radius: 8px; padding-top: 6px; padding-bottom: 6px; padding-right: 26px; }
.pb-search input.pb-search-input::placeholder { color: var(--text-faint); }
.pb-search input.pb-search-input:focus { outline: none; border-color: var(--accent); }
.pb-search-x { position: absolute; right: 5px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; display: none; align-items: center; justify-content: center; border: none; background: none; color: var(--text-faint); cursor: pointer; padding: 0; }
.pb-search-x svg { width: 12px; height: 12px; }
.pb-search-x:hover { color: var(--accent-2); }
.pb-search.has-q .pb-search-x { display: inline-flex; }

/* Collapsible category header: the whole row is a toggle, with a caret that
   points down when open and right when the section is folded shut. */
.pb-sechead { display: flex; align-items: center; gap: 5px; width: 100%; text-align: left; background: none; border: none; cursor: pointer; padding: 4px 2px; margin-bottom: 2px; font: inherit; font-size: 10px; letter-spacing: .08em; text-transform: uppercase; color: var(--text-faint); }
.pb-sechead:hover { color: var(--accent-2); }
.pb-sec-caret { display: inline-flex; flex: none; }
.pb-sec-caret svg { width: 11px; height: 11px; transition: transform 0.12s ease; }
.pb-sec.collapsed .pb-sec-caret svg { transform: rotate(-90deg); }
.pb-sec.collapsed .pb-palrow { display: none; }
.pb-sec-txt { flex: 1; }
/* While a search is active the caret is meaningless (folds are overridden), so hide it. */
.pb-sec.searching .pb-sec-caret { visibility: hidden; }
.pb-head { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid rgba(255,255,255,0.10); }
.pb-title { font-size: 13px; font-weight: 650; flex: 1; }
.pb-x { width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--line-2); background: rgba(255,255,255,0.05); color: var(--text-dim); border-radius: 7px; cursor: pointer; }
.pb-x:hover { border-color: var(--accent); color: var(--accent-2); }
.pb-x svg { width: 14px; height: 14px; }
.pb-body { padding: 12px 14px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; }
/* ---- THE MODAL BUILDER IS TWO COLUMNS: stage left, palette right ----
   A piece is dragged RIGHT TO LEFT onto the sentence, which is the direction the
   card editor has always run (the palette docks at the right of the felt, the
   assembly strip sits on the card). Stacked - palette under the rows - every drag
   in the step builder ran bottom-to-top, so the same gesture meant two different
   things in the two halves of one tool. The column layout is kept for the embedded
   builder (the card dock, which is already a narrow right-hand column) and comes
   back on a narrow window, where two columns would leave neither wide enough.

   The scroll chain matters: the body is the bounded flex row (flex:1 + min-height:0
   against the panel's max-height), and each column scrolls inside itself, so a long
   palette can never push the footer off the bottom. */
.pb-panel:not(.pb-embed) { width: min(1000px, 96vw); }
.pb-panel:not(.pb-embed) .pb-body { flex: 1; min-height: 0; flex-direction: row; align-items: stretch; gap: 12px; overflow: hidden; }
.pb-panel:not(.pb-embed) .pb-body > .pb-stage { flex: 1 1 auto; min-width: 0; overflow-y: auto; }
.pb-panel:not(.pb-embed) .pb-body > .pb-palette {
  flex: 0 0 286px; min-width: 0; overflow-y: auto; overflow-x: hidden;
  border-top: none; padding-top: 0;
  border-left: 1px solid rgba(255,255,255,0.08); padding-left: 12px;
}
/* The search box is the way INTO a long palette, so it may not scroll away from
   the pieces it filters - the same reasoning as the zone menu's search door. */
.pb-panel:not(.pb-embed) .pb-body > .pb-palette > .pb-search { position: sticky; top: 0; z-index: 2; background: rgba(15,20,28,0.94); margin-bottom: 0; padding: 4px 0; }
@media (max-width: 780px) {
  .pb-panel:not(.pb-embed) .pb-body { flex-direction: column; overflow-y: auto; }
  .pb-panel:not(.pb-embed) .pb-body > .pb-stage { overflow-y: visible; }
  .pb-panel:not(.pb-embed) .pb-body > .pb-palette { flex: 0 0 auto; overflow-y: visible; border-left: none; padding-left: 0; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 10px; }
}
.pb-stage { position: relative; min-height: 96px; border-radius: 10px; border: 1px dashed var(--line-2);
  background: linear-gradient(180deg, rgba(30,38,50,0.6), rgba(16,20,28,0.7)); padding: 10px; display: flex; flex-direction: column; align-items: stretch; gap: 8px; }
/* The opening hint sits IN THE FLOW at the top of the stage, above the rows -
   it used to be position:absolute;inset:0 and merely faded to opacity 0, so on
   an empty builder it printed straight through the standing empty row's own
   "Drop or click pieces here…" placeholder and the two read as one smudged
   paragraph. Nothing can overlap what is laid out in the same column, so the
   fix is the layout rather than a z-index or a shorter sentence, and it goes
   away entirely (display:none) the moment a piece is placed. */
.pb-hint { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; font-size: 11px; line-height: 1.5; color: var(--text-faint); pointer-events: none; padding: 2px 6px 0; }
.pb-stage.pb-has .pb-hint { display: none; }
.pb-rowsWrap { display: flex; flex-direction: column; gap: 8px; }
/* WRAPS RATHER THAN PUSHES THE VERDICT OFF THE PANEL. The row is a pieces run
   plus a status chip, and the run's automatic minimum is the widest single
   piece in it - "has one number for attack and life" is 267px as one pill. In
   the card editor the panel is wide enough that the chip still fits beside it;
   in a 360px flyout (the Global lane's Attacking rules) it did not, and a
   nowrap row put the chip 13px past the panel's own edge, where it was clipped.
   Shrinking the run instead would only move the clipping onto the pill. So the
   chip is allowed onto a line of its own, and margin-left:auto keeps it right
   whichever line it lands on - inert at every width where it already fit. */
.pb-rowline { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 5px 6px; border-radius: 8px; border: 1px solid transparent; cursor: pointer; }
.pb-rowline > .pb-chip { margin-left: auto; }
.pb-rowline.active { border-color: rgba(230,183,101,0.35); background: rgba(230,183,101,0.06); }
.pb-rowline-pieces { display: inline-flex; align-items: center; flex-wrap: wrap; flex: 1; min-height: 30px; }
.pb-rowline-empty { color: var(--text-faint); font-size: 11px; font-style: italic; padding: 4px 4px; }
.pb-newrow { align-self: flex-start; font-size: 11px; color: var(--text-dim); background: rgba(255,255,255,0.04); border: 1px dashed var(--line-2); border-radius: 7px; padding: 4px 10px; cursor: pointer; }
/* Empty stage: the centered explainer already invites the first click, so the
   "+ New effect" button would just sit under it. Hide it until a row exists. */
.pb-stage:not(.pb-has) .pb-newrow { display: none; }
.pb-newrow:hover, .pb-newrow.pb-drop-hot { border-color: var(--accent-2); color: var(--accent-2); background: rgba(230,183,101,0.08); }
.pb-summary { font-size: 11.5px; color: var(--text-dim); }
.pb-row { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 0; }
/* A PIECE IS TWO ELEMENTS, and it has to be. The silhouette is a clip-path and
   the 1px outline is a stack of drop-shadows, and clip-path is applied AFTER
   filter - so with both on one element the shape renders and the outline is
   clipped clean away, which reads as the border having been forgotten. The
   wrapper takes the filter, the child (.pb-in) takes the shape.
   --pbd is how far a naad stands out; fxPiecesUI.js writes it onto the panel
   from the same number it builds the polygons with, so the padding that keeps
   the words off the naad and the naad itself can never disagree. */
.pb-piece { position: relative; display: inline-flex; flex: none; margin: 3px 0; cursor: pointer;
  --edge: var(--line-2); --top: rgba(40,50,64,0.95); --bot: rgba(28,35,46,0.95);
  transition: filter 0.18s ease;
  filter: drop-shadow(1px 0 0 var(--edge)) drop-shadow(-1px 0 0 var(--edge))
          drop-shadow(0 1px 0 var(--edge)) drop-shadow(0 -1px 0 var(--edge)); }
.pb-in { display: inline-flex; align-items: center; height: 26px; padding: 0 calc(7px + var(--pbd, 9px));
  font-size: 12px; font-weight: 600; white-space: nowrap; color: var(--text);
  background: linear-gradient(180deg, var(--top), var(--bot)); }
/* A flat side has no notch to clear, so it keeps the plain padding. */
.pb-lflat > .pb-in { padding-left: 9px; }
.pb-rflat > .pb-in { padding-right: 9px; }
/* A tab stands 9px past the piece's nominal edge, and the next piece's notch has
   to receive it - so every piece after the first sits that far INTO its
   neighbour. A piece with no tab gives the space back on its right instead. */
.pb-rowline-pieces .pb-piece + .pb-piece { margin-left: calc(-1 * var(--pbd, 9px)); }
/* A piece with no naad on its right reserves no room for one (clipFor ends its
   silhouette at 100%), so there is nothing to give back here - only the padding
   changes, since there is no naad for the words to clear. */
/* The click: the piece that just landed flashes its outline gold and settles. */
.pb-piece.pb-pop { --edge: var(--accent-2); }
.pb-pal-piece:hover { --edge: var(--accent-2); }
.pb-pal-piece:hover > .pb-in { color: var(--accent-2); }
/* A palette piece with nowhere legal to land in the row being built. It stays in
   the palette (every piece is always visible) but reads as not-yet: dimmed, with
   its tooltip naming what the row needs first. Still draggable, so a builder can
   try it against another row and see for themselves. */
.pb-pal-piece.pb-blocked { opacity: 0.38; }
.pb-pal-piece.pb-blocked:hover { --edge: var(--line-2); }
.pb-pal-piece.pb-blocked:hover > .pb-in { color: var(--text); }
/* KEYWORD chips: this game's named reusable effects, in their own palette section.
   Marked apart from the grammar pieces beside them (gold-tinted, with the
   keyword's own icon) because a chip isn't a word that snaps into a sentence - it
   puts a whole finished effect on the card. Lit gold while the card carries it, so
   the section doubles as the list of what this card has. */
.pb-sec-kw .pb-palrow { gap: 6px; }
/* A keyword chip is a piece with FLAT EDGES, which is the honest shape for it:
   it is not a word that snaps into a sentence, it puts a whole finished effect
   on the card, so nothing fits it and it fits nothing. Gold rather than a
   category colour, for the same reason it always was. */
.pb-kw-piece { --edge: rgba(216,162,74,0.5); --top: rgba(216,162,74,0.16); --bot: rgba(216,162,74,0.07); }
.pb-kw-piece > .pb-in { gap: 5px; }
.pb-kw-piece.on { --edge: var(--accent-2); --top: var(--accent-2); --bot: var(--accent); }
.pb-kw-piece.on > .pb-in { color: var(--accent-ink); }
/* The brightness goes on the BODY. On the wrapper it would replace the stack of
   drop-shadows that IS the outline, so hovering a keyword would rub its border
   out - the one hazard of spending `filter` on a border. */
.pb-kw-piece.on:hover > .pb-in { filter: brightness(1.07); }
.pb-kw-ic { display: inline-flex; }
.pb-kw-ic svg { width: 12px; height: 12px; }
.pb-kw-label { font-weight: 650; }
/* On a keyword the card carries, the chip also carries the two things that belong
   to THIS card: the number it fills into the keyword's open slot, and its
   card-level options. Same controls as the keyword's line on the card face, for
   when the builder is covering it. */
.pb-kw-step { display: inline-flex; align-items: center; gap: 1px; margin-left: 2px; }
.pb-kw-piece.on .pb-kw-stepbtn, .pb-kw-piece.on .pb-kw-opt { background: rgba(0,0,0,0.22); }
.pb-kw-stepbtn, .pb-kw-opt { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px;
  padding: 0; border: none; border-radius: 50%; background: rgba(0,0,0,0.18); color: inherit; cursor: pointer; }
.pb-kw-stepbtn:hover, .pb-kw-opt:hover { background: rgba(0,0,0,0.32); }
.pb-kw-stepbtn svg, .pb-kw-opt svg { width: 9px; height: 9px; }
.pb-kw-opt { margin-left: 3px; }
.pb-row-piece { cursor: grab; }
.pb-row-piece > .pb-in { cursor: inherit; }
.pb-row-piece:active { cursor: grabbing; }
/* A piece that wrapped onto a new line has no neighbour to sit in, so it gives
   its overlap back - otherwise the negative margin pulls it off the left of the
   line. The naad and the notch are drawn on every piece at all times now, so a
   wrapped line already reads as the sentence running on, and the gold arrows the
   old flat pills needed have gone with them. */
.pb-rowline-pieces .pb-piece.pb-wrap-start { margin-left: 0; }
.pb-snapmark { position: absolute; top: 8px; bottom: 8px; width: 3px; border-radius: 3px; background: var(--accent-2); box-shadow: 0 0 8px var(--accent-2); pointer-events: none; z-index: 3; }
.pb-stage.pb-reject { animation: pb-reject 0.32s ease; }
@keyframes pb-reject { 0%, 100% { border-color: var(--line-2); } 45% { border-color: var(--red); box-shadow: 0 0 0 1px var(--red) inset; } }
.pb-piece.trigger { --edge: #5c86b4; --top: rgba(38,58,84,0.95); --bot: rgba(26,40,58,0.95); }
.pb-piece.subject { --edge: #4d9b9b; --top: rgba(30,66,66,0.95); --bot: rgba(20,42,42,0.95); }
.pb-piece.state { --edge: #b4685c; --top: rgba(78,44,40,0.95); --bot: rgba(48,28,26,0.95); }
.pb-piece.cost, .pb-piece.action { --edge: #b08a44; --top: rgba(70,56,28,0.9); --bot: rgba(46,38,22,0.92); }
.pb-piece.number { --edge: #5aa37e; --top: rgba(34,64,50,0.95); --bot: rgba(22,42,34,0.95); }
.pb-piece.number > .pb-in { font-weight: 700; }
.pb-piece.target, .pb-piece.value { --edge: #8a72b8; --top: rgba(52,42,80,0.95); --bot: rgba(34,28,52,0.95); }
.pb-piece.glue { --edge: #556273; --top: rgba(38,46,58,0.85); --bot: rgba(26,32,42,0.9); }
.pb-piece.glue > .pb-in { color: var(--text-dim); font-weight: 500; }
.pb-piece.join { --edge: #556273; --top: rgba(46,40,58,0.85); --bot: rgba(30,26,40,0.9); }
.pb-piece.join > .pb-in { color: var(--text-dim); font-weight: 600; font-style: italic; }
.pb-piece.bracket { --edge: #a8913f; --top: rgba(64,54,30,0.92); --bot: rgba(44,36,22,0.92); }
.pb-piece.bracket > .pb-in { font-style: italic; }
.pb-piece.cond { --edge: #5a8fa0; --top: rgba(34,54,62,0.92); --bot: rgba(22,36,42,0.92); }
.pb-piece.source, .pb-piece.rule { --edge: #7aa07a; --top: rgba(40,54,44,0.9); --bot: rgba(26,36,30,0.92); }
/* Place pieces (where a look reads): their own family, next to the target purple. */
.pb-piece.place { --edge: #7089c4; --top: rgba(40,50,80,0.95); --bot: rgba(26,34,54,0.95); }
/* Stat pieces (which stat a raise/lower moves): their own family, reading warm
   against the purple target it sits beside so "a card's | attack" splits clearly. */
.pb-piece.stat { --edge: #c07a70; --top: rgba(80,48,42,0.95); --bot: rgba(52,32,28,0.95); }
/* A piece at the compiler's default ("when played"): still on the row, still
   draggable, just dimmed - it adds no meaning, but dropping it would leave the
   builder wondering when the effect fires. */
.pb-piece.pb-dflt { opacity: 0.62; }
/* The piece under the cursor. No box-shadow: a shadow follows the BORDER BOX
   and the piece is a clipped silhouette, so it would draw a rectangle behind a
   puzzle piece. The outline the wrapper already carries is what reads. */
.pb-ghost { position: fixed; z-index: 320; pointer-events: none; transform: translate(-50%, -50%) rotate(-2deg) scale(1.04); }
.pb-readout { display: flex; gap: 8px; align-items: baseline; font-size: 12px; }
.pb-lab { font-size: 10px; letter-spacing: .08em; text-transform: uppercase; color: var(--text-faint); flex: none; }
.pb-read-txt { color: var(--text); }
.pb-muted { color: var(--text-faint); font-style: italic; }
.pb-status { display: flex; align-items: center; gap: 8px; min-height: 20px; font-size: 11.5px; }
.pb-status-msg { color: var(--text-dim); }
.pb-chip { flex: none; font-size: 10px; font-weight: 700; letter-spacing: .03em; padding: 2px 8px; border-radius: 999px; border: 1px solid; }
.pb-chip-green { color: #7fe0a8; border-color: rgba(76,174,122,0.55); background: rgba(76,174,122,0.12); }
.pb-chip-amber { color: #e6c268; border-color: rgba(230,183,101,0.55); background: rgba(230,183,101,0.12); }
.pb-chip-red { color: #f0917f; border-color: rgba(214,96,77,0.55); background: rgba(214,96,77,0.12); }
.pb-palette { display: flex; flex-direction: column; gap: 4px; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 10px; }
/* .pb-sechead / .pb-sec collapsible styling lives in the .ce-fx-carddock block above. */
.pb-palrow { display: flex; flex-wrap: wrap; gap: 6px; padding-bottom: 4px; }
.pb-foot { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-top: 1px solid rgba(255,255,255,0.10); }
.pb-foot-sp { flex: 1; }
.pb-btn { font: inherit; font-size: 12px; color: var(--text); background: rgba(255,255,255,0.05); border: 1px solid var(--line-2); border-radius: 8px; padding: 6px 12px; cursor: pointer; }
.pb-btn:hover { border-color: var(--accent); color: var(--accent-2); }
.pb-primary { background: linear-gradient(180deg, var(--accent-2), var(--accent)); border-color: var(--accent); color: var(--accent-ink); font-weight: 650; }
.pb-primary:hover { filter: brightness(1.06); color: var(--accent-ink); }
.pb-btn:disabled { opacity: 0.4; cursor: not-allowed; filter: grayscale(0.4); }
/* Nothing ready to add: the button greys but stays clickable, so pressing it can
   say what the row still needs (a disabled button gets no hover, so no tooltip). */
.pb-btn.pb-idle { opacity: 0.45; filter: grayscale(0.5); }
.pb-btn.pb-idle:hover { filter: grayscale(0.5) brightness(1.02); }

/* ------------------------------------------------------------------
   Print and play (js/views/printPlay.js)
   The sheets on screen ARE the printout: each .pnp-sheet is a real
   paper-sized box holding real cards positioned in mm. On screen it is
   scaled down by --z inside a wrapper that reserves the scaled size (a
   bare transform would leave full-size gaps); printing sets --z to 1.
   ------------------------------------------------------------------ */
.pnp-view { padding-bottom: 40px; }

/* The options bar. Same glass as .table-tools.floating, at pill scale. */
.pnp-bar {
  position: sticky; top: 8px; z-index: 40;
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  margin: 0 auto 18px; padding: 7px 10px; width: max-content; max-width: calc(100% - 24px);
  background: rgba(15,20,28,0.72); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  border: 1px solid rgba(255,255,255,0.10); border-radius: 12px; box-shadow: var(--shadow);
}
.pnp-group { display: flex; align-items: center; gap: 4px; }
.pnp-sep { width: 1px; align-self: stretch; background: rgba(255,255,255,0.10); margin: 0 2px; }
.pnp-count { font-size: 11px; color: var(--text-faint); padding: 0 6px 0 2px; white-space: nowrap; }

/* A control that has nothing to act on stays on the bar, dashed and inert,
   and says why in its tooltip. It never disappears. */
.pnp-off { opacity: 0.4; cursor: default; border-style: dashed !important; }
.pnp-off:hover { background: none !important; transform: none !important; }

.pnp-pill {
  padding: 5px 10px; border-radius: 999px; font-size: 12px; line-height: 1;
  background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.10); color: var(--text);
}
.pnp-pill:hover { background: rgba(255,255,255,0.11); }
.pnp-step { display: flex; align-items: center; gap: 2px; padding: 2px 4px; border-radius: 999px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.10); }
.pnp-step-n { font-size: 12px; min-width: 22px; text-align: center; color: var(--text); }
.pnp-menu { display: flex; flex-direction: column; gap: 1px; }

/* ---- the sheets ----
   A SHEET is a real piece of paper: sized in mm, scaled to the screen by --z,
   and printed at --z:1 so what is on screen IS the printout. Nothing about it
   is specific to cards, so the rulebook designer wears the same three classes
   under the `paper-` names and the rules are stated once as a selector list.
   The `@media print` block below is scoped to `.paper-view`, which both views
   carry, so there is ONE print pipeline rather than two that drift. */
.pnp-preview, .paper-stack { display: flex; flex-direction: column; align-items: center; gap: 22px; --z: 0.62; }
.pnp-sheet-wrap, .paper-sheet-wrap { width: calc(var(--pw) * var(--z)); height: calc(var(--ph) * var(--z)); flex: none; }
.pnp-sheet, .paper-sheet {
  position: relative; width: var(--pw); height: var(--ph); overflow: hidden;
  transform: scale(var(--z)); transform-origin: 0 0;
  background: #fff; box-shadow: var(--shadow);
}
.pnp-marks { position: absolute; inset: 0; pointer-events: none; }
.pnp-marks svg { width: 100%; height: 100%; display: block; }
/* A card with no back yet still takes its slot, so the grid and the cut lines
   line up whether or not every card in the batch has a second side. */
.pnp-blank { position: absolute; border: 0.2mm dashed #c9c9c9; border-radius: 3mm; box-sizing: border-box; }
.pnp-net, .pnp-token { position: absolute; display: flex; flex-direction: column; align-items: center; }
.pnp-net svg, .pnp-token svg { display: block; }
.pnp-net-cap { font-size: 3mm; line-height: 5mm; color: #444; text-align: center; }
.pnp-empty { color: var(--text-faint); font-size: 13px; text-align: center; max-width: 380px; }

/* A printed card is a real .tcard at true playing-card width. Everything inside
   it is sized in cqw off that width, so this one property scales the whole face.
   The live-only layers come off: the foil needs a pointer to look like anything,
   and a drop shadow round a card is a grey smudge across the cut line. */
.pnp-card { box-shadow: none !important; }
.pnp-card .free-foil { display: none !important; }
.pnp-card .free-texture { mix-blend-mode: normal; opacity: 0.5; }

@media print {
  /* The page box is written by the view just before the dialog opens (it has to
     match the paper the user picked) and removed again afterwards. */
  html, body { background: #fff !important; margin: 0 !important; padding: 0 !important; }
  #topbar, #modalRoot, #toastRoot, #card-hover-preview, .help-banner, .help-bubble, .tip-bubble { display: none !important; }
  /* Every bit of app chrome a paper view puts around its sheets. A new paper
     view adds its own bar class here and gets the rest for free. */
  #view.paper-view .home-row, #view.paper-view .pnp-bar, #view.paper-view .pnp-empty,
  #view.paper-view .rb-bar, #view.paper-view .rb-rail, #view.paper-view .rb-panel,
  #view.paper-view .ce-align-guides, #view.paper-view .rb-grip { display: none !important; }
  #view.paper-view { padding: 0 !important; }
  /* True size, one sheet per page, no gaps. */
  #view.paper-view .pnp-preview, #view.paper-view .paper-stack { display: block; gap: 0; --z: 1; }
  #view.paper-view .pnp-sheet-wrap, #view.paper-view .paper-sheet-wrap { width: var(--pw); height: var(--ph); page-break-after: always; break-after: page; }
  #view.paper-view .pnp-sheet-wrap:last-child, #view.paper-view .paper-sheet-wrap:last-child { page-break-after: auto; break-after: auto; }
  #view.paper-view .pnp-sheet, #view.paper-view .paper-sheet { transform: none; box-shadow: none; }
  /* A selected block is an editing state, never something that prints. */
  #view.paper-view .rb-block { outline: none !important; }
  /* Cards are dark by design. Without this every browser strips the fills and
     prints white rectangles with pale text on them. */
  #view.paper-view, #view.paper-view * { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
  /* Nothing on paper is hoverable, so no hover transform can be allowed to fire
     mid-print and shift a card off its cut line. */
  #view.paper-view .tcard { transform: none !important; transition: none !important; animation: none !important; }
}

/* A badge whose icon labels its value: the glyph stacks ABOVE the number rather
   than sitting beside it (iconDir "col" in normal mode). The icon is the label,
   doing the job POWER/LIFE does in a word on other frames. */
.free-badge.badge-stack .free-badge-content { flex-direction: column; gap: 0.6cqw; line-height: 1; }
/* The stacked glyph follows --bw like every other badge glyph, so the badge's
   Size and Icon size controls actually reach it. It used to be pinned at
   4.6cqw, which meant the icon above a stacked numeral was the one glyph in the
   app that ignored its own element's size control. 0.79 is the ratio that
   number held against the 5.83cqw base, so a card built before this renders
   the same. */
.free-badge.badge-stack .free-badge-content svg,
.free-badge.badge-stack .free-badge-content .icon-img {
  width: calc(var(--bw, 5.83cqw) * 0.79); height: calc(var(--bw, 5.83cqw) * 0.79);
}

/* =====================================================================
   LOG IN / CREATE AN ACCOUNT  (js/views/login.js)

   The form is printed on a real .tcard, so every size inside it is in cqw
   (1cqw = 1% of the card's width) and the whole face scales as one piece
   when the card resizes. This card's design width is 340px, so a px value
   of N converts as (N / 3.4)cqw - not the 240px reference the table's
   cards use, because this card is deliberately bigger than a table card.
   ===================================================================== */
.view.login-view {
  max-width: none;
  min-height: 100vh; min-height: 100dvh;
  padding: 18px 24px 24px;
  position: relative;
  /* Nothing else is on this screen, so the card sits in the middle of the
     felt. Its way out and its ? ride on the card itself. */
  display: flex; flex-direction: column; justify-content: center; align-items: center;
  background: var(--felt-noise), var(--felt-fibre), var(--felt-image);
  background-color: #123a2e;
  background-size: auto, var(--felt-fibre-size), cover;
  background-position: center, center, center;
  background-repeat: repeat, repeat, no-repeat;
  background-blend-mode: normal, overlay, normal;
}
.view.login-view::before {
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0.5;
  background-image: radial-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1.4px);
  background-size: 26px 26px;
}
.view.login-view > * { position: relative; z-index: 1; }

.login-stage { display: flex; justify-content: center; padding: 10px 0; }

/* The card's corner pips. Absolutely placed over the face so they take no part
   in the form's flex flow, and pointer-events are handed back to the buttons
   alone so the bar between them never eats a click meant for the card. */
.login-corners {
  position: absolute; top: 0; left: 0; right: 0; z-index: 4;
  display: flex; justify-content: space-between;
  padding: 3cqw 3.2cqw 0;
  pointer-events: none;
}
.login-corners > * { pointer-events: auto; }
.login-corner-btn.btn.btn-icon {
  width: 7.4cqw; height: 7.4cqw; padding: 0; border-radius: 999px;
  /* One of the pair is an <a> (it leaves the app), so the link furniture has
     to be stripped for the two to read as the same control. */
  display: inline-flex; align-items: center; justify-content: center;
  box-sizing: border-box; text-decoration: none;
  background: rgba(0, 0, 0, 0.26);
  border: 0.3cqw solid rgba(255, 255, 255, 0.14);
  /* The face's own ink, like every other mark on the card. A global text var
     here would go the same way the body copy did on a coloured face. */
  color: var(--login-ink-dim);
  transition: color 0.12s ease, border-color 0.12s ease, background 0.12s ease;
}
.login-corner-btn.btn.btn-icon svg { width: 3.9cqw; height: 3.9cqw; }
.login-corner-btn.btn.btn-icon:hover {
  color: var(--login-ink); border-color: var(--login-ink); background: rgba(0, 0, 0, 0.36);
}
/* Help mode lights every ? in the app; on the card it lights in the card's ink. */
body.help-mode .login-corner-btn.help-toggle { color: var(--login-ink); border-color: var(--login-ink); }

/* The turn. Perspective and the deal-in live on the outer box; the rotation
   lives on .login-3d alone, so an entry animation can never eat the flip's
   own transform (the trap the pricing cards' planReveal sets). */
.login-flip {
  position: relative;
  width: min(340px, 90vw);
  aspect-ratio: 5 / 7;
  perspective: 1500px;
  animation: loginDeal 0.66s cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
@keyframes loginDeal {
  0% { transform: translateY(30px) rotate(-5deg) scale(0.94); opacity: 0; }
  30% { opacity: 1; }
  100% { transform: none; opacity: 1; }
}
.login-3d {
  position: absolute; inset: 0;
  transform-style: preserve-3d;
  transition: transform 0.62s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.login-3d.flipped { transform: rotateY(180deg); }
.login-side {
  position: absolute; inset: 0;
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
}
/* ---- each face carries its own ink ----
   The app's --text-dim / --text-faint are mixed for the near-black panels
   everywhere else, and they are cool blue-greys. Dropped onto a warm face they
   went muddy: --text-faint sat at about 3:1 on both cards, which is why the
   standing note could be on screen and still unreadable. So each face declares
   its own ink ladder, warmed or cooled to its own background, and every rule
   below reads these instead of the global vars. Each tier is checked against
   BOTH ends of that face's gradient; the weakest lands near 7:1, and the tiers
   carrying real information sit near 10:1. */
.login-side.front {
  --login-ink: #fdf6e9;
  --login-ink-dim: #e7d8bd;
  --login-ink-faint: #c9b48d;
  --login-rule: rgba(253, 246, 233, 0.24);
  --login-hair: rgba(253, 246, 233, 0.3);
  --login-wash: rgba(253, 246, 233, 0.07);
  --login-wash-hi: rgba(253, 246, 233, 0.15);
  --login-well: rgba(0, 0, 0, 0.32);
  --login-well-hint: rgba(253, 246, 233, 0.58);
  --login-title: linear-gradient(135deg, #ffbf66 0%, #ffd580 50%, #ffbf66 100%);
}
.login-side.back {
  transform: rotateY(180deg);
  --login-ink: #eef5ff;
  --login-ink-dim: #cfe0f4;
  --login-ink-faint: #a8c2de;
  --login-rule: rgba(238, 245, 255, 0.24);
  --login-hair: rgba(238, 245, 255, 0.3);
  --login-wash: rgba(238, 245, 255, 0.07);
  --login-wash-hi: rgba(238, 245, 255, 0.15);
  --login-well: rgba(0, 0, 0, 0.32);
  --login-well-hint: rgba(238, 245, 255, 0.58);
  --login-title: linear-gradient(135deg, #9ec9ff 0%, #d6e9ff 50%, #9ec9ff 100%);
}
.login-side .tcard { width: 100%; height: 100%; }
/* A freeform face lets its elements hang outside the card on purpose; a
   form must not. Clip this one back to the card's own edge. */
.login-side .tcard-free-inner { overflow: hidden; }
/* A card refuses text selection; a card you type on cannot. */
.login-side .tcard, .login-form, .login-input { user-select: text; -webkit-user-select: text; }
@media (prefers-reduced-motion: reduce) {
  .login-flip { animation: none; }
  .login-3d { transition: none; }
}

/* ---- the printed face ---- */
.login-form {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center;
  padding: 6.5cqw 6cqw 5.5cqw;
  text-align: center;
  gap: 2.4cqw;
}
.login-emblem { width: 10.6cqw; height: 10.6cqw; opacity: 0.95; }
.login-title {
  margin: 0; font-family: "GoldWhisper", "Bauvex", Georgia, serif;
  font-weight: 400; font-size: 7.6cqw; line-height: 1.05; letter-spacing: 0.02em;
  background: var(--login-title);
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
}
.login-lede { margin: -0.6cqw 0 0; font-size: 3.4cqw; line-height: 1.35; color: var(--login-ink-dim); max-width: 88%; }

.login-providers { display: flex; gap: 3.2cqw; margin-top: 1.2cqw; }
.login-prov.btn.btn-icon {
  width: 13cqw; height: 13cqw; padding: 0; border-radius: 999px;
  background: var(--login-wash);
  border: 0.3cqw solid var(--login-hair);
  color: var(--login-ink);
  transition: transform 0.12s ease, border-color 0.12s ease, background 0.12s ease;
}
/* A brand mark is the whole point of the button, so it fills the disc the way a
   sign-in button does elsewhere, rather than sitting in it like a hint. */
.login-prov.btn.btn-icon svg { width: 7.2cqw; height: 7.2cqw; }
.login-prov.btn.btn-icon:hover {
  transform: translateY(-0.5cqw);
  background: var(--login-wash-hi);
  border-color: var(--login-ink);
}
.login-prov:focus-visible { outline: 0.6cqw solid var(--login-hair); outline-offset: 0.3cqw; }

.login-or {
  display: flex; align-items: center; gap: 2.4cqw;
  width: 100%; margin: 0.4cqw 0 0;
  font-size: 3cqw; letter-spacing: 0.08em; text-transform: uppercase; color: var(--login-ink-faint);
}
.login-or::before, .login-or::after {
  content: ""; flex: 1; height: 0.3cqw; border-radius: 999px;
  background: linear-gradient(90deg, transparent, var(--login-rule), transparent);
}

.login-field { display: block; width: 100%; text-align: left; }
.login-label {
  display: block; margin-bottom: 1.2cqw;
  font-size: 3cqw; font-weight: 700; letter-spacing: 0.02em; color: var(--login-ink-dim);
}
.login-input {
  width: 100%; box-sizing: border-box;
  padding: 2.6cqw 3.2cqw; font-family: inherit; font-size: 3.9cqw;
  color: var(--login-ink);
  background: var(--login-well);
  border: 0.3cqw solid var(--login-hair);
  border-radius: 2.4cqw;
}
.login-input::placeholder { color: var(--login-well-hint); }
.login-input:focus {
  outline: none; border-color: var(--login-ink);
  box-shadow: 0 0 0 0.9cqw var(--login-wash-hi);
}
.login-input.bad { border-color: var(--red); box-shadow: 0 0 0 0.9cqw rgba(200, 70, 60, 0.14); }
/* A phone zooms the page in on any field under 16px. Worth breaking the cqw
   scale for, since the zoom is far more disruptive than the size jump. */
@media (pointer: coarse) {
  .login-input { font-size: 16px; }
}

.login-go.btn { width: 100%; justify-content: center; padding: 2.8cqw 3cqw; font-size: 3.7cqw; border-radius: 2.4cqw; }
.login-go.btn svg { width: 4.4cqw; height: 4.4cqw; }

/* Always on the card, never appearing or vanishing: it carries the standing
   truth until an action has something more specific to report. */
/* It reports the state of the world, so it is information, not decoration -
   it takes the dim tier, not the faint one. This is the line the old
   --text-faint made unreadable. */
.login-note {
  margin: 0; min-height: 7.4cqw;
  font-size: 3cqw; line-height: 1.35; color: var(--login-ink-dim); max-width: 94%;
}
.login-note.is-err { color: #f0998c; }

.login-swap { margin-top: auto; display: flex; align-items: baseline; justify-content: center; gap: 1.4cqw; font-size: 3.1cqw; color: var(--login-ink-dim); flex-wrap: wrap; }
.login-swap-link, .login-fine-link {
  background: none; border: none; padding: 0; cursor: pointer;
  font: inherit; color: var(--login-ink); text-decoration: underline;
  text-underline-offset: 0.24em; text-decoration-thickness: 0.06em;
}
.login-swap-link:hover, .login-fine-link:hover { color: var(--accent-2); }
/* The one line on the card that really is small print. */
.login-fine { margin: 0; font-size: 2.8cqw; line-height: 1.3; color: var(--login-ink-faint); }

/* A radiating pattern (fan, rings) fills its element once rather than tiling -
   a sunburst has one centre, and repeating it would print a grid of little
   suns. The tiled kinds keep the fixed 46cqw tile above. */
.free-texture.texture-stretch, .shape-texture.texture-stretch { background-repeat: no-repeat; background-size: 100% 100%; }

/* An outline fill on a CLIPPED badge shape: the ring is an SVG polygon behind
   the content, because a CSS border on a clipped box loses its stroke wherever
   the silhouette slopes away from the box edge. The svg is clipped to the same
   silhouette by the badge itself, so doubling the stroke width and letting the
   outer half clip leaves a full-width line sitting exactly on the shape. */
/* A badge that draws its ring as a polygon is still a free element, so it stays
   ABSOLUTE. `position: relative` here used to beat `.free-el`'s absolute on
   specificity, which dropped the badge back into the block flow and slid it a
   full box-width off its own coordinate - visible only on the second such badge
   on a card, which is why it survived. The absolute position is its own
   containing block for the ring svg anyway. */
.free-badge.badge-svg-ring { position: absolute; box-shadow: none; }
/* A FILLED clipped badge that draws its rim as a polygon still wants its own
   drop shadow - only the outline fill (no fill at all) goes shadowless. */
.free-badge.badge-ring-over { box-shadow: var(--card-shadow-sm); }
.free-badge.badge-ring-over.no-shadow { box-shadow: none; }
/* An empty value prints a dash, muted, so the cell holds its place. */
.free-badge-blank { opacity: 0.6; font-weight: 400; }
.free-badge .badge-ring-svg { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }
.free-badge.badge-svg-ring .free-badge-content { position: relative; z-index: 1; }

/* An uploaded picture standing in for a built-in glyph. Sized by the same rules
   as the svg it replaces (the surrounding svg selectors set width/height), and
   contained rather than cropped so a square-ish upload isn't cut off. */
.icon-img { width: 100%; height: 100%; object-fit: contain; display: block; }
.free-badge-content .icon-img { width: var(--bw, 5.83cqw); height: var(--bw, 5.83cqw); }
.free-badge-pip .icon-img { width: var(--pipw, 5.83cqw); height: var(--pipw, 5.83cqw); }

/* The icon picker's upload tab: the add button reads as an empty slot rather
   than a glyph, and the note under it explains the one behaviour that differs
   from a built-in glyph. */
.ce-icon-upload { border-style: dashed !important; opacity: 0.85; }
.ce-iconpick-note { flex: 0 0 100%; margin-top: 5px; line-height: 1.35; font-size: 10.5px; }

/* A segmented row that has outgrown one line. The texture picker now offers both
   noise grains and drawn patterns, so it wraps instead of running off the panel;
   the inner hairlines become a grid rather than a single row of dividers. */
.ce-seg.ce-seg-wrap { flex-wrap: wrap; }
.ce-seg.ce-seg-wrap .ce-seg-btn { padding: 5px 10px; border-top: 1px solid var(--line); border-left: 1px solid var(--line); }

/* =====================================================================
   TOUCH HIT EXPANDERS  (coarse pointers only)
   =====================================================================
   The house style is deliberately compact - 11-12px text, ~24px steppers
   (see CLAUDE.md "Compact by default") - and that stays. iOS wants a 44pt
   target and Android 48dp, so every control in the tool is roughly half
   the recommended minimum. Rather than enlarge anything, this layer grows
   the HIT AREA and leaves the pixels alone: an invisible ::before pinned
   outside the button's box. A pseudo-element is part of its host for hit
   testing, so a finger landing on it activates the button exactly as if it
   had landed on the icon. Nothing moves, nothing repaints, and the whole
   block is behind @media (pointer: coarse), so a mouse never sees it.

   ::before, not ::after, on purpose: .seat-av::after is a real decoration
   (the stats badge) and several other controls may want ::after later.
   Expanders own ::before across the board so the two never collide.

   The 10px inset turns a 24px control into 44px and a 30px one into 50px.

   THE ONE THING TO WATCH: two expanders that overlap fight, and the one
   later in DOM order wins the shared strip. The tight rows (a -/+ stepper
   pair, the zoom rail, a row of pill actions) sit on 4-6px gaps, so their
   expanders would overlap by ~14px and the left button of each pair would
   lose its right side. The gap bumps at the bottom of this block are the
   fix, and they are the only thing here that changes layout - spacing
   between controls on a touch device, never the size of a control. If a
   surface ever looks too airy on a tablet, tune the gap there, don't
   delete the expander.

   Deliberately NOT expanded: .tcard and .piece (a card's hit area IS its
   art, and expanding it would make neighbouring cards in a fanned hand
   unpickable), and anything inside .felt, where pieces are packed tighter
   than any expander could survive.
   ===================================================================== */
@media (pointer: coarse) {
  .tool-btn, .zoom-btn, .ce-step-btn, .rail-min, .tphase,
  .tphase .ph-x, .seat .seat-x, .seat .seat-av, .ctx-item, .ce-cat-tag-x,
  .piece-del, .tzone-del, .pb-search-x, .ce-gal-act, .tcg-flyout-x,
  .ce-deck-rm, .ce-deck-dup, .payout-rm, .menu-legal-link, .lp-bar-link,
  .checkbox input, .search-wrap .search-x {
    position: relative;
  }
  .tool-btn::before, .zoom-btn::before, .ce-step-btn::before,
  .rail-min::before, .tphase::before,
  .tphase .ph-x::before, .seat .seat-x::before, .seat .seat-av::before,
  .ctx-item::before, .ce-cat-tag-x::before, .piece-del::before,
  .tzone-del::before, .pb-search-x::before, .ce-gal-act::before,
  .tcg-flyout-x::before, .ce-deck-rm::before, .ce-deck-dup::before,
  .payout-rm::before, .menu-legal-link::before, .lp-bar-link::before,
  .checkbox input::before, .search-wrap .search-x::before {
    content: "";
    position: absolute;
    inset: -10px;
    /* No paint of any kind: this exists purely to be hit. */
    background: none;
    border: 0;
    border-radius: inherit;
    /* Behind the host's own content so it can never cover an icon or a
       label, but still in front of the page, so it still takes the tap. */
    z-index: 0;
  }
  /* NESTED EXPANDERS. Two places put an expanded control INSIDE another
     expanded control: the delete X inside a turn-step pill, and the -/+
     steppers inside a context-menu stepper row. Left alone, the outer
     expander paints over the inner control and swallows every tap meant
     for it - the pill would eat its own X. The ladder that fixes it:
     outer expander at 0, inner expander at 1, and the inner control ITSELF
     at 2, so the child's own box always wins over both. */
  .tphase .ph-x, .ctx-item.ctx-stepper .ce-step-btn,
  .ctx-item.ctx-stepper .ctx-num { z-index: 2; }
  .tphase .ph-x::before, .ctx-item.ctx-stepper .ce-step-btn::before { z-index: 1; }
  /* The hue strip is 12px tall, which is a fine target for a mouse and a
     miss for a finger. Expanded on its long axis only: growing it sideways
     would put the ends of the strip past the panel's own padding, and the
     strip is already full width. The saturation square needs nothing - it
     is 84px of target. */
  .ce-hue { position: relative; }
  .ce-hue::before { content: ""; position: absolute; inset: -12px 0; background: none; border: 0; z-index: 0; }
  /* These sit inside hosts that are NOT themselves expanded (a seat pill, a
     category tag, a deck row), so they only need to clear their host's own
     in-flow content. */
  .seat .seat-x::before, .seat .seat-av::before, .ce-cat-tag-x::before,
  .ce-deck-rm::before, .ce-deck-dup::before, .search-wrap .search-x::before { z-index: 1; }

  /* Open the tight rows so neighbouring expanders stop overlapping. This is
     spacing only - every control keeps its exact size. 22px is 2 x the 10px
     inset plus a hair, which is the smallest gap at which two expanders are
     guaranteed not to touch. */
  .ce-step, .zoom-rail, .ce-lib-row,
  .table-top-left, .table-top-right { gap: 22px; }
  .tphase-bar { gap: 22px; }
  .seat-rail { gap: 22px; }
  .tool-icon-row, .tool-rail .rail-body { gap: 22px; }

  /* iOS zooms the page in on any focused field under 16px. The login field
     already had this; every other typed field in the tool needs it too, or
     tapping a search box throws the whole layout out of position. Only the
     rendered size changes, and only on a touch device. */
  .login-input, .ctx-search, .pb-search-input,
  .de-savedpill-name, .ctx-num, .search-wrap input[type="text"],
  input[type="text"], input[type="number"], textarea, select { font-size: 16px; }
  /* The card editor's typable slider readouts sit inside .ce-panel, which sets
     them to 10px at a specificity the line above can't reach - so they need
     naming here or tapping one zooms the page. Their width is in em, so the
     field grows with the type and the slider gives up the room. */
  .ce-panel input.ce-num-val, input.ce-num-val { font-size: 16px; }
  /* Same for the typable stepper readouts, which are sized per context
     (.ce-panel at 13px, .payout-step at 14px) at a specificity the blanket
     line above can't reach. */
  .ce-panel input.ce-step-val, .payout-step input.ce-step-val, input.ce-step-val { font-size: 16px; }
}

/* =====================================================================
   TOUCH-ACTION: the CSS half of the pointer-event conversion
   =====================================================================
   Pointer handlers alone are not enough. By default a browser treats a
   finger drag on any element as a page pan or pinch, claims the gesture,
   and then fires pointercancel - so the drag dies on the first move even
   though every listener is correctly wired. touch-action: none tells it
   this surface handles its own gestures.

   Scoped to the things that ARE draggable, never to a whole view: an
   element with touch-action: none can't be scrolled past on a phone, so
   putting it on a container would trap the page.

   Nothing here changes anything for a mouse. Pair each entry with a real
   pointerdown handler; a touch-action with no handler behind it just
   disables scrolling for no reason. See TCG.trackDrag in js/ui.js.
   ===================================================================== */
/* The felt itself: empty-felt drag is pan (zoomed in) or marquee select
   (05-camera-multiselect.js). Without this, panning a zoomed table scrolls
   the page instead, and there is no other way to reach the rest of the felt
   on a tablet, since the zoom buttons work but the pan gesture didn't. */
.felt { touch-action: none; }
/* Every piece: cards, decks, dice, coins, life counters. This is the drag
   the whole tool trains a player to reach for. */
.piece { touch-action: none; }
/* Zones and their four corner grips: moving and resizing a zone is what
   building a table consists of. */
.tzone,
.tzone-resize,
.zgroup-resize { touch-action: none; }
/* The life counter's spin wheel, which has no other input path at all -
   a plain click on a life counter is deliberately a no-op
   (11-limits-market-setplay.js), so without the spin it does nothing. */
.life-wheel { touch-action: none; }
/* Floating panel handles: any rail head (tool rail, standings, a dragged
   context window). */
.rail-head { touch-action: none; }
/* The My Games grid, for marquee multi-select. Note this one is a
   deliberate trade: the grid can be long enough to scroll, so it takes
   pan-y rather than none - a vertical drag still scrolls the page, and a
   horizontal one starts the marquee. */
.game-hand { touch-action: pan-y; }

/* Long-press: stop the browser hijacking the hold.
   iOS raises its own "Save Image / Copy" sheet when a finger rests on an
   <img>, and card art IS a real img (.tcard-art img). That sheet lands on
   top of the menu the hold is meant to open, so the gesture reads as
   broken on the one element it matters most for. -webkit-touch-callout
   suppresses it. Paired with user-select: none so a hold on a card's name
   doesn't start a text selection instead. Scoped to the things a long
   press acts on - a text field must keep its native callout, which is why
   js/longpress.js excludes inputs rather than the CSS blanket-disabling
   them. See js/longpress.js. */
.tcard,
.tcard img,
.piece,
.tzone,
.seat,
.tphase {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

/* =====================================================================
   PHONE PLAY  (body.phone-play, set by 24-phone-play.js)
   =====================================================================
   A phone is not a small tablet. At 390px the felt is about 346px wide,
   roughly six cards across, and a card's name renders at 3.75px - so the
   desktop table reflowed onto a phone is a thing you pinch around rather
   than a game you play. Decided with Siem: the phone gets its own play
   shape, landscape only.

   The vertical budget is the whole design problem. A phone in landscape
   is about 390px TALL, and that has to hold a seat rail, the felt, the
   phase pills and a hand you can read. So:

     seat rail   ~26px   compact, one line
     felt map   ~200px   the table as an overview, not a work surface
     phase bar   ~26px
     hand tray    46px   peeked (card tops only)
                 ~150px  open, lifting OVER the felt rather than pushing it

   The tray overlays rather than reflows on purpose: reflowing would
   resize the felt every time a player looked at their hand, which moves
   every zone and piece underneath them mid-decision.

   Nothing here is a width breakpoint. body.phone-play is set from
   TCG.viewport, which asks whether the pointer is coarse AND the screen's
   SHORT side is small - so a narrow desktop window is not a phone, and an
   iPad is not either. See js/viewport.js.
   ===================================================================== */

/* ---- the rotate gate ---- */
/* Scoped to the table. Home, My Games and Browse stay usable in portrait,
   or a player following a link would hit a wall before reaching anything. */
.phone-rotate {
  position: absolute; inset: 0; z-index: 400;
  display: flex; align-items: center; justify-content: center;
  background: rgba(10,14,20,0.92);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
}
.phone-rotate-card {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  padding: 22px 26px; text-align: center;
  max-width: min(340px, calc(100vw - 40px));
  background: rgba(15,20,28,0.72);
  border: 1px solid rgba(255,255,255,0.12); border-radius: 14px;
  box-shadow: var(--shadow);
}
.phone-rotate-ic { color: var(--accent); }
.phone-rotate-ic svg { width: 34px; height: 34px; }
/* The icon turns a quarter, slowly, and stops. It is showing the player
   what to do, so it must not read as a spinner: a continuous rotation
   would say "loading" instead of "turn this". */
@media (prefers-reduced-motion: no-preference) {
  .phone-rotate-ic { animation: phone-rotate-hint 2.6s ease-in-out infinite; }
}
@keyframes phone-rotate-hint {
  0%, 30% { transform: rotate(0deg); }
  55%, 85% { transform: rotate(90deg); }
  100% { transform: rotate(0deg); }
}
.phone-rotate-title { font-size: 15px; font-weight: 700; color: var(--text); }
.phone-rotate-sub { font-size: 12px; color: var(--text-dim); line-height: 1.45; }

/* ---- the table, squeezed ---- */
/* Every floating rail loses its padding and its wood, because at 390px tall
   the frame alone would cost a tenth of the height. */
/* THE WOOD GOES ON A PHONE IN BOTH MODES, not only in play - which is what
   body.phone-mode buys over body.phone-play here, and it is the one pair of
   rules in this block that widens.
   A phone is never a wooden seat surface: useSeatDials() is false there, so the
   rail holds the seats and the frame carries nothing. Scoped to phone-play, the
   frame was still drawn in BUILD, which left a phone builder looking at an empty
   wooden band eating a tenth of the height of the one screen with none to spare.
   Everything else in this block is the play shape proper and stays there. */
body.phone-mode .table-frame { display: none; }
body.phone-mode .felt-viewport { left: 0; right: 0; border-width: 0; }
body.phone-play .seat-rail.floating { top: 6px; gap: 6px; }
body.phone-play .tphase-bar.floating { bottom: 52px; padding: 4px 8px; gap: 6px; }
body.phone-play .table-top-left,
body.phone-play .table-top-right { top: 6px; gap: 4px; }
/* The zoom rail and the standings rail are desktop affordances: the map is
   already the overview, and there is no room for a 172px panel on a 390px
   felt. Both remain reachable from the table menu. */
body.phone-play .zoom-rail.floating,
body.phone-play .majority-rail.floating { display: none; }
body.phone-play .table-hint { bottom: 52px; left: 8px; font-size: 10.5px; max-width: 46vw; }
/* A zone on the map is a tap target for its sheet, so it reads as pressable. */
body.phone-play .tzone { cursor: pointer; }
body.phone-play .tzone-label { font-size: 7px; }

/* ---- the hand tray ---- */
.phone-play-wrap { position: absolute; inset: 0; pointer-events: none; z-index: 30; }
.phone-play-wrap > * { pointer-events: auto; }

.phone-tray {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; flex-direction: column;
  background: rgba(15,20,28,0.72);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  border-top: 1px solid rgba(255,255,255,0.12);
  box-shadow: 0 -10px 30px rgba(0,0,0,0.4);
  /* Peeked by default: the handle plus a sliver of card. Opening grows the
     tray upward OVER the felt, so the table underneath never resizes. */
  max-height: 46px;
  transition: max-height 0.18s ease;
}
.phone-tray.open { max-height: 62%; }
@media (prefers-reduced-motion: reduce) { .phone-tray { transition: none; } }

/* The handle is the whole affordance. A tray that only opened by swiping
   would be invisible, and the tool teaches no swipe anywhere else. */
.phone-tray-handle {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; padding: 5px 10px;
  background: none; border: 0; cursor: pointer;
  color: var(--text-dim); font-size: 11px; font-weight: 700;
}
.phone-tray-handle:hover { color: var(--text); }
.phone-tray-handle.on { color: var(--accent); }
.phone-tray-grip { width: 30px; height: 3px; border-radius: 999px; background: rgba(255,255,255,0.28); }
.phone-tray-count { letter-spacing: 0.2px; }

.phone-tray-row {
  display: flex; gap: 6px; align-items: flex-start;
  padding: 0 10px 10px;
  overflow-x: auto; overflow-y: hidden;
  /* pan-x, not none: the row scrolls sideways when a hand outgrows the
     screen, so the finger has to keep that axis. The cards inside are tap
     targets rather than drag handles here, which is what makes that safe. */
  touch-action: pan-x;
  -webkit-overflow-scrolling: touch;
}
.phone-tray-card {
  flex: 0 0 auto; width: 84px; cursor: pointer;
  border-radius: 8px; padding: 2px;
  border: 2px solid transparent;
  transition: transform 0.12s ease, border-color 0.12s ease;
}
.phone-tray-card .tcard { width: 100%; height: auto; }
/* Picked up and waiting for a destination. The same lifted-and-rimmed
   language the felt already uses for a selected piece, so the state reads
   the same wherever a player meets it. */
.phone-tray-card.picked { border-color: var(--accent); transform: translateY(-6px); }
@media (prefers-reduced-motion: reduce) { .phone-tray-card { transition: none; } }
.phone-tray-empty { padding: 10px 4px; font-size: 11px; color: var(--text-faint); }

/* ---- the zone sheet ---- */
/* Tapping a zone opens it full-surface: the felt is a map, this is where a
   player actually reads and acts. Same glass as every other floating
   surface in the tool. */
.phone-sheet {
  position: absolute; inset: 0; z-index: 40;
  display: flex; flex-direction: column;
  background: rgba(12,17,24,0.88);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
}
.phone-sheet-head {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 10px;
  border-bottom: 1px solid rgba(255,255,255,0.10);
}
.phone-sheet-title { font-size: 13px; font-weight: 700; color: var(--text); flex: 1 1 auto;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.phone-sheet-count {
  font-size: 11px; font-weight: 700; color: var(--text-dim);
  padding: 2px 7px; border-radius: 999px; background: rgba(255,255,255,0.08);
}
.phone-sheet-x {
  display: flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; padding: 0;
  background: none; border: 0; border-radius: 7px; cursor: pointer; color: var(--text-dim);
}
.phone-sheet-x:hover { color: var(--text); background: rgba(255,255,255,0.07); }
.phone-sheet-x svg { width: 14px; height: 14px; }
.phone-sheet-grid {
  flex: 1 1 auto; overflow-y: auto;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(86px, 1fr));
  gap: 8px; padding: 10px;
  /* pan-y so the sheet scrolls; it holds no drag of its own. */
  touch-action: pan-y;
}
.phone-sheet-card { cursor: pointer; border-radius: 8px; }
.phone-sheet-card .tcard { width: 100%; height: auto; }
.phone-sheet-other {
  display: flex; align-items: center; justify-content: center;
  aspect-ratio: 5 / 7; border-radius: 8px;
  border: 1px dashed rgba(255,255,255,0.18);
  font-size: 10.5px; color: var(--text-faint); text-transform: capitalize;
}

/* ================================================================
   THE RULEBOOK DESIGNER (js/views/rulebook.js)
   A page is a real sheet of paper: the .paper-sheet rules above give it its
   mm size and the --z zoom, and the shared @media print block prints it. What
   is here is only the editing chrome around it, at the app's own compact scale.
   Every rule is scoped to .rb-*, so nothing here can reach a printed page: the
   print block switches the chrome off and leaves the paper alone.
   ================================================================ */
.rb-view { padding: 10px 14px 18px; }
.rb-bar {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  margin: 6px 0 10px; padding: 5px 8px; border-radius: 12px;
  background: rgba(15,20,28,0.5); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,0.08);
}
.rb-sep { width: 1px; height: 16px; background: rgba(255,255,255,0.12); margin: 0 2px; }
.rb-zoom-val { font-size: 11px; color: var(--text-dim); min-width: 34px; text-align: center; }
.rb-body { display: flex; align-items: flex-start; gap: 12px; }
.rb-rail { flex: none; display: flex; flex-direction: column; gap: 4px; padding: 4px; }
.rb-stack { flex: 1; min-width: 0; align-items: center; overflow: auto; }
/* The page is a container-query root, exactly as .tcard is, so everything on it
   can be sized in cqw and hold that size at any zoom and on paper. */
.rb-page { container-type: inline-size; }
.rb-block {
  position: absolute; transform: translate(-50%,-50%); transform-origin: 50% 50%;
  box-sizing: border-box; touch-action: none; cursor: grab;
}
.rb-block.sel { outline: 1px solid var(--accent-2); outline-offset: 1px; }
.rb-block.ce-dragging { cursor: grabbing; z-index: 5; }
.rb-text-in { width: 100%; height: 100%; color: #16181d; line-height: 1.35; overflow-wrap: anywhere; }
.rb-text-in[contenteditable="true"] { outline: 1px dashed var(--accent-2); cursor: text; }
.rb-empty { opacity: 0.35; font-style: italic; }
.rb-image { overflow: hidden; }
.rb-image img { width: 100%; height: 100%; display: block; }
.rb-ph {
  width: 100%; height: 100%; display: grid; place-items: center;
  border: 0.3mm dashed #b9bec7; color: #b9bec7; background: #f4f5f7;
}
.rb-ph svg { width: 28%; height: 28%; }
/* One corner grip, on the selected block only. It sits OUTSIDE the block's
   bottom-right corner so it stays grabbable on a block scaled down to a few
   millimetres, the same reasoning as the card editor's shape handle. */
.rb-grip {
  position: absolute; right: -5px; bottom: -5px; width: 10px; height: 10px;
  border-radius: 3px; background: var(--accent-2); border: 1px solid rgba(0,0,0,0.35);
  cursor: nwse-resize; display: none; touch-action: none;
}
.rb-block.sel .rb-grip, .rb-block:hover .rb-grip { display: block; }
.rb-panel {
  flex: none; width: 240px; padding: 10px; border-radius: 13px;
  background: rgba(15,20,28,0.72); backdrop-filter: blur(12px);
  border: 1px solid rgba(255,255,255,0.10); box-shadow: var(--shadow);
}
.rb-panel-head { font-size: 13px; font-weight: 700; margin-bottom: 8px; }
.rb-panel-empty { font-size: 11px; color: var(--text-faint); }
.rb-rows { display: flex; flex-direction: column; gap: 6px; }
.rb-acts { display: flex; gap: 4px; }
.rb-strip { display: flex; justify-content: center; gap: 4px; margin-top: 10px; }
.rb-page-chip {
  min-width: 22px; height: 22px; border-radius: 7px; font: inherit; font-size: 11px;
  background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.10);
  color: var(--text-dim); cursor: pointer;
}
.rb-page-chip.on { border-color: var(--accent); color: var(--accent-2); }
/* The rulebook shown to READ, inside the rules modal: pages at a legible
   reading size, scrolling inside the modal like any other long glass panel. */
.rules-book { display: block; }
.rb-read { gap: 14px; }

/* ===================================================================
   TOURNAMENTS (js/views/tournaments.js)

   One glass language with the rest of the app: the same dark translucent
   fill, the same hairline, the gold accent for the one live thing, and
   everything at pill scale (11-12px body, 12-13px titles). An entrant is
   their crest inside their border - the same two files a seat pill reads,
   so a bracket needs no art of its own.
   =================================================================== */
.trn-view { padding-bottom: 60px; }

/* -- the notices at the top: not live, an update offered, an expiry -- */
.trn-note { display: flex; align-items: center; gap: 8px; max-width: 760px; margin: 0 auto 12px;
  padding: 8px 10px; border-radius: 10px; font-size: 11.5px; line-height: 1.45;
  background: rgba(15,20,28,0.5); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,0.08); color: var(--muted); }
.trn-note svg { width: 14px; height: 14px; flex: none; }
.trn-note-quiet { color: var(--text); }
.trn-note-warn { border-color: rgba(230,170,60,0.35); color: var(--accent-2); }

/* -- the list -- */
.trn-bar { display: flex; align-items: center; gap: 10px; justify-content: center; margin: 14px 0 18px; }
.trn-cap { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; color: var(--muted); }
.trn-cap svg { width: 12px; height: 12px; }
.trn-list { display: grid; gap: 12px; max-width: 900px; margin: 0 auto;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
.trn-tile { padding: 10px; border-radius: 12px; cursor: pointer;
  background: rgba(15,20,28,0.55); backdrop-filter: blur(10px);
  border: 1px solid rgba(255,255,255,0.10); transition: border-color .15s, transform .15s; }
.trn-tile:hover { border-color: var(--accent); transform: translateY(-2px); }
.trn-tile-head { display: flex; align-items: center; gap: 7px; }
.trn-tile-head svg { width: 13px; height: 13px; flex: none; color: var(--accent-2); }
.trn-tile-name { font-size: 13px; font-weight: 700; flex: 1;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.trn-tile-game { font-size: 11px; color: var(--muted); margin: 3px 0 8px; }
.trn-status { font-size: 10px; text-transform: uppercase; letter-spacing: .6px;
  padding: 2px 6px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.12); color: var(--muted); }
.trn-status.s-live { border-color: var(--accent); color: var(--accent-2); }
.trn-status.s-done { border-color: rgba(120,200,140,0.4); color: rgb(150,215,165); }
.trn-status.s-off { opacity: .6; }
.trn-warn { margin-top: 8px; font-size: 10.5px; color: var(--accent-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* -- faces -- */
.trn-crests { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.trn-more { font-size: 10.5px; color: var(--muted); }
.trn-face { position: relative; display: inline-flex; align-items: center; gap: 6px;
  border-radius: 999px; }
.trn-face.dim { opacity: .45; }
.trn-crest { display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 999px; flex: none;
  background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.12); }
.trn-crest.sm { width: 20px; height: 20px; }
.trn-crest svg { width: 68%; height: 68%; }
.trn-face-name { font-size: 11.5px; }

/* -- one tournament -- */
.trn-head { max-width: 900px; margin: 0 auto 10px; text-align: center; }
.trn-back { display: inline-flex; align-items: center; gap: 5px; font-size: 11px;
  color: var(--muted); background: none; border: 0; cursor: pointer; padding: 4px 2px; }
.trn-back:hover { color: var(--accent-2); }
.trn-back svg { width: 12px; height: 12px; }
.trn-title { margin: 2px 0 4px; }
.trn-sub { display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: 7px; font-size: 11.5px; color: var(--muted); }
.trn-sub svg { width: 12px; height: 12px; }
.trn-dot { opacity: .4; }
.trn-code { display: inline-flex; align-items: center; gap: 4px; letter-spacing: 1.5px;
  font-weight: 700; color: var(--accent-2); }

.trn-lobby { display: grid; gap: 12px; max-width: 900px; margin: 0 auto;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.trn-panel { position: static; width: auto; max-width: none; }
.trn-row { display: flex; align-items: center; gap: 8px; padding: 3px 0; flex-wrap: wrap; }
.trn-row-label { font-size: 11.5px; color: var(--muted); min-width: 78px; }
.trn-seg { flex: 1; flex-wrap: wrap; }
.trn-toggle { min-width: 30px; }
.trn-hint-row { display: flex; align-items: center; gap: 6px; font-size: 11px;
  color: var(--muted); padding: 5px 0 2px; }
.trn-hint-row svg { width: 12px; height: 12px; }

.trn-entrant-list { display: flex; flex-direction: column; gap: 4px; }
.trn-entrant { display: flex; align-items: center; gap: 7px; padding: 3px 4px; border-radius: 8px; }
.trn-entrant:hover { background: rgba(255,255,255,0.05); }
.trn-entrant.is-org { background: rgba(230,170,60,0.07); }
.trn-seed { font-size: 10.5px; color: var(--muted); min-width: 24px; }
.trn-entrant-name { font-size: 11.5px; flex: 1;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.trn-tag { font-size: 9.5px; text-transform: uppercase; letter-spacing: .5px;
  color: var(--accent-2); border: 1px solid rgba(230,170,60,0.3); border-radius: 999px; padding: 1px 5px; }
.trn-mini { width: 20px; height: 20px; }
.trn-mini svg { width: 12px; height: 12px; }
.trn-mini.on { color: var(--accent-2); }
.trn-add { display: flex; gap: 6px; align-items: center; margin-top: 7px; }
input.trn-add-field { flex: 1; font-size: 11.5px; padding: 5px 8px; border-radius: 7px; }

.trn-plan { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center;
  max-width: 900px; margin: 12px auto 0; }
.trn-start-row { display: flex; gap: 8px; align-items: center; justify-content: center; margin: 16px 0; }

/* -- the bracket -- */
.trn-bracket { display: flex; gap: 18px; align-items: flex-start; justify-content: flex-start;
  max-width: 100%; overflow-x: auto; padding: 8px 4px 14px; margin: 0 auto; width: fit-content; }
.trn-round { display: flex; flex-direction: column; gap: 12px; justify-content: space-around;
  min-width: 168px; }
.trn-round-head { font-size: 10px; text-transform: uppercase; letter-spacing: .7px;
  color: var(--muted); text-align: center; }
.trn-round.last .trn-fix { border-color: rgba(230,170,60,0.35); }

.trn-fix { position: relative; padding: 6px; border-radius: 10px; min-width: 160px;
  background: rgba(15,20,28,0.55); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,0.10); }
.trn-fix.bye { opacity: .6; }
.trn-fix.disputed { border-color: rgba(210,90,90,0.5); }
.trn-fix.cons { border-style: dashed; }
.trn-fix-tag { font-size: 9.5px; text-transform: uppercase; letter-spacing: .5px;
  color: var(--muted); margin-bottom: 3px; }
.trn-fix-note { font-size: 10.5px; color: var(--muted); margin-top: 4px; }
.trn-slot { display: flex; align-items: center; gap: 6px; padding: 3px 4px; border-radius: 7px; }
.trn-slot.through { background: rgba(230,170,60,0.12); }
.trn-slot.through .trn-slot-name { color: var(--accent-2); font-weight: 700; }
.trn-slot.out { opacity: .5; }
.trn-slot-name { font-size: 11.5px; flex: 1;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.trn-slot-score { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
.trn-fix-acts { display: flex; gap: 4px; justify-content: flex-end; margin-top: 4px;
  padding-top: 4px; border-top: 1px solid rgba(255,255,255,0.07); }

/* -- a league's rounds -- */
.trn-sched { max-width: 900px; margin: 0 auto; display: flex; flex-direction: column; gap: 12px; }
.trn-sched-row { display: flex; flex-wrap: wrap; gap: 8px; }

/* -- standings -- */
.trn-table { display: flex; flex-direction: column; gap: 2px; }
.trn-tr { display: flex; align-items: center; gap: 8px; padding: 4px 5px; border-radius: 7px; font-size: 11.5px; }
.trn-tr.top { background: rgba(230,170,60,0.10); }
.trn-th { font-size: 10px; text-transform: uppercase; letter-spacing: .6px; color: var(--muted); }
.trn-td-place { min-width: 22px; color: var(--muted); }
.trn-td-name { flex: 1; display: flex; align-items: center; gap: 6px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.trn-td-num { min-width: 46px; text-align: right; font-variant-numeric: tabular-nums; }

.trn-champ { display: flex; align-items: center; justify-content: center; gap: 10px;
  max-width: 900px; margin: 0 auto 14px; padding: 10px 14px; border-radius: 12px;
  background: rgba(230,170,60,0.10); border: 1px solid rgba(230,170,60,0.35); }
.trn-champ svg { width: 18px; height: 18px; color: var(--accent-2); }
.trn-champ-name { font-size: 14px; font-weight: 700; color: var(--accent-2); }

/* -- the audit line -- */
.trn-audit-row { display: flex; gap: 8px; font-size: 11px; padding: 3px 0;
  border-bottom: 1px solid rgba(255,255,255,0.05); }
.trn-audit-when { color: var(--muted); min-width: 86px; flex: none; }
.trn-audit-what { flex: 1; }

/* -- picking a game, and reporting a result -- */
.trn-pick-list, .trn-report-list { display: flex; flex-direction: column; gap: 4px;
  max-height: 320px; overflow-y: auto; }
.trn-pick-row { display: flex; justify-content: space-between; align-items: center; gap: 10px;
  padding: 7px 9px; border-radius: 8px; cursor: pointer; text-align: left;
  background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); }
.trn-pick-row:hover { border-color: var(--accent); }
.trn-pick-name { font-size: 12px; }
.trn-pick-sub { font-size: 10.5px; color: var(--muted); }
.trn-report-picked { display: flex; flex-direction: column; gap: 3px; margin-bottom: 8px; }
.trn-report-row { display: flex; align-items: center; gap: 7px; padding: 3px 4px;
  border-radius: 7px; background: rgba(230,170,60,0.08); }
.trn-report-pick { display: flex; align-items: center; gap: 7px; padding: 5px 7px;
  border-radius: 7px; cursor: pointer; font-size: 11.5px; text-align: left;
  background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); }
.trn-report-pick:hover { border-color: var(--accent); }

@media (max-width: 640px) {
  .trn-bracket { gap: 10px; }
  .trn-round { min-width: 144px; }
}
/* Rulebook blocks that size their own height (a generated window, a card): the
   box is as tall as its content, which is what makes overflow impossible. */
.rb-block.rb-auto-h { height: auto; }
/* The generated rules on a page. The rules-* rules are written for a dark glass
   modal, so the page re-states the colours it needs on white paper and nothing
   else - one place, rather than a second copy of the rulebook stylesheet. */
.rb-gen-in { width: 100%; color: #16181d; line-height: 1.4; }
.rb-gen-in .rules-section { margin: 0 0 0.6em; }
.rb-gen-in h3, .rb-gen-in h4 { color: #16181d; margin: 0 0 0.25em; }
.rb-gen-in p, .rb-gen-in li { color: #2a2e36; margin: 0 0 0.3em; }
.rb-gen-in .rule-token { color: #7a5a12; font-weight: 700; }
.rb-gen-in .rules-box { display: flex; gap: 1.2em; opacity: 0.85; }
.rb-gen-in .rules-list { margin: 0 0 0.4em 1.1em; }
.rb-shape-in { width: 100%; height: 100%; }
.rb-card-face { display: block; }
.rb-color { width: 34px; height: 22px; padding: 0; border: 1px solid rgba(255,255,255,0.12); border-radius: 6px; background: none; cursor: pointer; }

/* The bug-report flyout (js/sessionRecorder.js, opened from the flag in the play
   toolbar). One field and one button: every other fact in the file was gathered
   without asking, and the note is the only thing the recording cannot know. Pill
   scale like every other glass surface. */
.rep-form { display: flex; flex-direction: column; gap: 8px; }
.rep-note { width: 100%; box-sizing: border-box; }
.rep-count { font-size: 10px; letter-spacing: .3px; text-transform: uppercase; opacity: .55; }
.rep-foot { display: flex; justify-content: flex-end; }

/* CHECK THIS GAME (js/lint.js, the flag's build-mode neighbour in the toolbar).
   Rows at pill scale on the same glass as every other flyout: a title naming the
   thing, the fix under it, and where to go in muted type. The left rail carries
   the level, so the three kinds are told apart by colour without a badge per
   row. */
.lint-body { display: flex; flex-direction: column; gap: 2px; max-height: 60vh; overflow-y: auto; }
.lint-head {
  font-size: 10px; letter-spacing: .7px; text-transform: uppercase;
  opacity: .6; margin: 8px 0 2px; font-weight: 700;
}
.lint-head:first-child { margin-top: 0; }
.lint-row { padding: 6px 8px; border-left: 2px solid transparent; border-radius: 0 7px 7px 0; background: rgba(255, 255, 255, .03); }
.lint-title { font-size: 11.5px; font-weight: 600; line-height: 1.35; }
.lint-fix { font-size: 11px; opacity: .72; line-height: 1.4; margin-top: 2px; }
.lint-where { font-size: 10px; opacity: .5; margin-top: 3px; }
.lint-row.lint-stops { border-left-color: var(--bad, #e2564a); }
.lint-row.lint-silent { border-left-color: var(--accent, #d8b25a); }
.lint-row.lint-check { border-left-color: rgba(255, 255, 255, .22); }
.lint-head.lint-stops { color: var(--bad, #e2564a); opacity: .85; }
.lint-clear { display: flex; gap: 7px; align-items: flex-start; font-size: 11.5px; line-height: 1.4; opacity: .8; }
.lint-clear svg { width: 14px; height: 14px; flex: none; margin-top: 1px; }
/* The dot on the toolbar button: something here would stop the game playing. On
   ::after because a ghost button's ::before is the coarse-pointer expander. */
.btn.has-dot { position: relative; }
.btn.has-dot::after {
  content: ""; position: absolute; top: 3px; right: 3px;
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--bad, #e2564a); pointer-events: none;
}

/* FIND A SETTING (js/settingsIndex.js, the crosshair in the build toolbar). A
   reading surface: a search field, a count, then results grouped by kind with
   the WHERE on the group heading rather than repeated on every row. Same glass
   and the same pill scale as every other flyout. */
.find-body { display: flex; flex-direction: column; gap: 8px; }
.find-count { font-size: 10px; letter-spacing: .3px; text-transform: uppercase; opacity: .5; }
.find-results { display: flex; flex-direction: column; gap: 1px; max-height: 52vh; overflow-y: auto; }
.find-head {
  display: flex; flex-direction: column; gap: 1px;
  margin: 8px 0 3px; font-size: 10px; letter-spacing: .7px; text-transform: uppercase;
  font-weight: 700; opacity: .65;
}
.find-head:first-child { margin-top: 0; }
.find-where { font-size: 9.5px; letter-spacing: .2px; text-transform: none; font-weight: 500; opacity: .7; }
.find-row { padding: 5px 8px; border-radius: 7px; background: rgba(255, 255, 255, .03); }
.find-label { display: flex; gap: 6px; align-items: center; font-size: 11.5px; font-weight: 600; }
.find-label svg { width: 12px; height: 12px; flex: none; opacity: .7; }
.find-sub { font-size: 11px; opacity: .68; line-height: 1.4; margin-top: 2px; }
.find-key { font-size: 10px; opacity: .55; margin-top: 3px; }
.find-empty { font-size: 11px; opacity: .7; line-height: 1.45; padding: 4px 2px; }

/* ---- a route still in flight -------------------------------------------
   Most route files arrive after the menu is painted (js/lateLoad.js), so a
   button pressed in the first fraction of a second has a screen to wait for.
   The router only appends this after 150ms, so a file that lands in 20ms
   never flashes it - which is why there is no fade-in here to fight with.
   Three dots rather than a word: there is nothing to say that the pressed
   button has not already said, and a sentence at this size reads as an error. */
.route-wait {
  display: flex; align-items: center; justify-content: center;
  gap: 6px; padding: 48px 0; min-height: 120px;
}
.route-wait-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: rgba(255, 255, 255, 0.28);
  animation: route-wait-pulse 1s ease-in-out infinite;
}
.route-wait-dot:nth-child(2) { animation-delay: 0.14s; }
.route-wait-dot:nth-child(3) { animation-delay: 0.28s; }
@keyframes route-wait-pulse {
  0%, 100% { opacity: 0.25; transform: translateY(0); }
  50%      { opacity: 0.85; transform: translateY(-3px); }
}
@media (prefers-reduced-motion: reduce) {
  /* Still visible, just not moving - the dots are the only thing on screen,
     so removing them entirely would leave a blank panel. */
  .route-wait-dot { animation: none; opacity: 0.5; }
}
