/* web/theme.css — U2 design system (tokens first, screens later).
 *
 * The structural fix for BP #16 and the root cause of BLK-1/BLK-2: layout must stop living in
 * inline `style.cssText` strings. This file is the SINGLE source of design tokens (two-tier
 * palette · 8pt spacing · one type scale · semantic colors), the component classes, and — the
 * U2 gate — the launcher/panel/modal positioning classes that let every `web/*.mjs`
 * drop its inline `position:fixed` string. (The gate: no `.mjs` in `web/` may contain
 * the string `position:fixed`; it lives here instead.)
 *
 * TWO TIERS (hybrid): the *atmosphere* tier carries the Barovian identity (blood/gold/parchment,
 * Cinzel) for shell chrome, branding, lore & handouts; the *clarity* tier is neutral dark grays
 * and off-white text with semantic HP/damage colors, for sheets, tracker, builder, VTT HUD and
 * libraries. Every asserted text/UI pair meets WCAG (≥4.5:1 body, ≥3:1 UI) — see
 * shared/theme.test.mjs, which parses these tokens and computes the ratios.
 *
 * NOTE: index.html still carries its legacy inline :root + component styles; theme.css defines the
 * canonical token set (same atmosphere values) plus the clarity tier, the scales, and the new
 * component classes. Later chunks (U3+) migrate the screens onto these `ds-*` classes and delete
 * the inline duplicates. The `cos-launcher`/`cos-panel`/`cos-modal-*` classes below are LIVE now.
 */

:root {
  /* ═══ AETHER (Phase F · R-001/005/006/007) — the Obsidian token layer. DESIGN_DIRECTION §3 is the
   * contract. One palette drives both legacy tiers: the old atmosphere/clarity split remains as
   * structure (wrappers below), but both now resolve to the same warm indigo-charcoal surfaces,
   * with ember (candlelight) as the primary accent and arcane (spell-light) as the secondary. ═══ */

  /* ── Surfaces — warm indigo-charcoal, never pure black ── */
  --bg:#121118; --surface:#1A1922; --surface-2:#232230; --surface-3:#2E2C3B;
  --inset:#100F16; --stage:#0E0D14;
  --edge-strong:#5B5677;
  --text:#ECEAF2; --text-dim:#B4B0C6; --text-faint:#8A85A0;

  /* ── Signature accents ── */
  --ember:#E8B15A; --ember-bright:#F5C877; --ember-deep:#B8863A; --on-ember:#241A08;
  --arcane:#9A86FF; --arcane-bright:#B4A5FF; --arcane-deep:#6C5BD8;

  /* ── Semantic ── */
  --hp:#57D08C; --hp-bright:#8FE3B1; --danger:#F0655E; --warning:#F0B84E; --info:#6AA8F0;

  /* ── Alpha layers (washes · glows · scrims) — the ONE place translucent color literals live ── */
  --ember-wash:rgba(232,177,90,.12); --ember-wash-strong:rgba(232,177,90,.18);
  --ember-glow:0 0 12px rgba(232,177,90,.35); --ember-glow-strong:0 0 18px rgba(232,177,90,.55);
  --arcane-wash:rgba(154,134,255,.14); --arcane-glow:0 0 12px rgba(154,134,255,.35);
  --danger-wash:rgba(240,101,94,.12); --danger-glow:0 0 12px rgba(240,101,94,.4);
  --hp-wash:rgba(87,208,140,.12);
  --scrim:rgba(10,9,15,.66);
  --shadow-text:0 1px 2px rgba(0,0,0,.8);
  --surface-veil:rgba(26,25,34,.85);
  /* the app backdrop: quiet arcane + ember ambience over --bg (the mockups' body gradient) */
  --backdrop:radial-gradient(1200px 600px at 80% -10%, rgba(154,134,255,.10), transparent 60%),
             radial-gradient(900px 500px at -10% 110%, rgba(232,177,90,.08), transparent 55%),
             var(--bg);

  /* ── Legacy atmosphere aliases — SAME values as above (literal hexes: theme.test.mjs parses
   * hex-valued tokens for the WCAG contrast gate). Old names, Aether values. ── */
  --ink:#121118; --panel:#1A1922; --panel2:#232230; --edge:#37344A; --edge2:#454158;
  --blood:#A03C36; --blood-bright:#F0655E; --gold:#E8B15A; --gold-bright:#F5C877;
  --parch:#ECEAF2; --dim:#B4B0C6; --faint:#8A85A0; --mist:#9DB4D8;

  /* ── Legacy clarity aliases — unified onto the same Aether surfaces ── */
  --c-bg:#121118; --c-surface:#1A1922; --c-surface-2:#232230; --c-edge:#37344A;
  --c-inset:#100F16; --c-edge-strong:#5B5677;
  --c-text:#ECEAF2; --c-text-dim:#B4B0C6; --c-text-faint:#8A85A0; --c-accent:#E8B15A;

  /* ── Semantic aliases (HP / damage / status) — tuned to hold ≥4.5:1 on --surface-2.
   * --blood-text is the TEXT-safe wound red (same as --danger on Aether). ── */
  --s-hp:#57D08C; --s-hp-hurt:#F0B84E; --s-hp-crit:#F0655E;
  --s-damage:#F0655E; --s-heal:#57D08C;
  --s-success:#57D08C; --s-warning:#F0B84E; --s-danger:#F0655E; --s-info:#6AA8F0;
  --blood-text:#F0655E;

  /* ── Damage-type accents (curated set kept — all pass on their darkest usage surfaces) ── */
  --good:#6d9a58; --bad:#c25a4a; --radiant:#e0c565; --fire:#d07030; --lightning:#6fa7d9;
  --poison:#7aa05a; --necrotic:#a48cbb; --cold:#8fc3d9; --psychic:#c77fb8; --acid:#a4b840;
  --thunder:#b0a090; --force:#d0b0ff;

  /* ── 8pt spacing scale (4pt half-steps) ── */
  --sp-0:0; --sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-5:24px; --sp-6:32px; --sp-7:48px; --sp-8:64px;

  /* ── Modular type scale, 1.25 ratio — 14px data floor preserved (no sub-12px microcopy) ── */
  --fs-caption:12px; --fs-body:14px; --fs-body-lg:16px; --fs-h3:20px; --fs-h2:25px; --fs-h1:31px; --fs-display:39px;

  /* ── The three Aether faces, one job each (self-hosted — fonts/fonts.css):
   * Cormorant Garamond = display & brand · Inter = every control, label & number ·
   * Spectral = rules text, stat blocks & lore. Legacy names remap onto them. ── */
  --font-display:'Cormorant Garamond',Georgia,serif;
  --font-serif:'Spectral',Georgia,serif;
  --font-atmos:'Inter',system-ui,'Segoe UI',sans-serif;
  --font-atmos-body:'Spectral',Georgia,serif;
  --font-clarity:'Inter',system-ui,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;

  /* ── Radii — softer than before ── */
  --radius-sm:6px; --radius:10px; --radius-lg:16px; --radius-full:999px;

  /* ── Elevation (R-005) — four soft levels; never a hard 1px drop ── */
  --elev-1:0 1px 2px rgba(0,0,0,.4), 0 4px 16px rgba(0,0,0,.30);
  --elev-2:0 2px 8px rgba(0,0,0,.45), 0 12px 32px rgba(0,0,0,.35);
  --elev-3:0 4px 12px rgba(0,0,0,.5), 0 24px 60px rgba(0,0,0,.5);
  --elev-4:0 6px 16px rgba(0,0,0,.55), 0 32px 80px rgba(0,0,0,.55);
  --shadow-panel:var(--elev-2); --shadow-modal:var(--elev-3);

  /* ── Motion (R-008) — durations + easings; all consumers respect prefers-reduced-motion ── */
  --dur-fast:120ms; --dur:200ms; --dur-slow:320ms;
  --ease-standard:cubic-bezier(.2,0,0,1); --ease-emphasized:cubic-bezier(.2,0,0,0);

  /* ── Focus — a 2px arcane ring with a soft outer glow, consistent everywhere ── */
  --focus-ring:0 0 0 2px var(--arcane), 0 0 10px rgba(154,134,255,.4);
  --hit-min:24px;
  --content-max:1200px;

  /* ── Named z-index scale — overlays never fight: rail < hud < tutorial < toast < launcher <
   * panel < modal < palette. Legacy names kept as the live consumers. ── */
  --z-rail:100; --z-hud:300; --z-tutorial:9000; --z-toast:9600;
  --z-launcher:9800; --z-panel:9900; --z-modal:100000; --z-palette:100100;
}

/* ── Tier wrappers: components read generic vars; the wrapper remaps them to a tier. Under Aether
 * both tiers share one palette — what remains of the split is TYPE: atmosphere (shell chrome,
 * branding, lore, handouts) heads in Cormorant and reads in Spectral; clarity (sheets, tracker,
 * builder, VTT HUD, libraries) is Inter throughout. ─────────────────────────────────────────── */
.tier-atmos {
  --edge-c:var(--edge2); --accent:var(--ember-bright);
  --heading-font:var(--font-display); --body-font:var(--font-serif);
}
.tier-clarity {
  --edge-c:var(--c-edge); --accent:var(--c-accent);
  --heading-font:var(--font-atmos); --body-font:var(--font-clarity);
}

/* ── M-138: the skip link — visually hidden until focused, then a real control above the header. */
.skip-link {
  position:fixed; left:var(--sp-3); top:var(--sp-2); z-index:calc(var(--z-modal) + 10);
  transform:translateY(-64px); opacity:0;
  background:var(--surface-3); color:var(--text); border:1px solid var(--edge-strong);
  border-radius:var(--radius-sm); padding:var(--sp-2) var(--sp-3); font-family:var(--font-clarity);
  font-size:var(--fs-body); font-weight:600; text-decoration:none;
  transition:transform var(--dur-fast) var(--ease-standard), opacity var(--dur-fast) var(--ease-standard);
}
.skip-link:focus-visible { transform:none; opacity:1; outline:2px solid var(--arcane); outline-offset:2px; }

/* ── Icons (F2 · R-002) — one line-icon language (icons.js). Icons inherit currentColor and scale
 * with the text they sit beside; never a color emoji. ─────────────────────────────────────────── */
svg.ico {
  width:1.15em; height:1.15em; display:inline-block; vertical-align:-0.2em; flex:none;
}
.ico--lg { width:1.5em; height:1.5em; vertical-align:-0.3em; }

/* ── Component classes (ds-*) — used by the styleguide now; screens migrate onto them in U3+ ───── */
.ds-panel {
  background:var(--surface); border:1px solid var(--edge-c); border-radius:var(--radius);
  padding:var(--sp-4); color:var(--text); font-family:var(--body-font); font-size:var(--fs-body);
}
.ds-panel > h2, .ds-panel > h3 { font-family:var(--heading-font); color:var(--accent); margin-bottom:var(--sp-2); }
.ds-card {
  background:var(--surface-2); border:1px solid var(--edge-c); border-radius:var(--radius-sm);
  padding:var(--sp-3); color:var(--text); font-family:var(--body-font); font-size:var(--fs-body);
}

/* V3 control-typography audit: controls read at --fs-body (14px) minimum — --fs-caption is for true
 * captions only, never controls — and letter-spacing is killed below 16px (V2 rule, now applied to
 * the ds-* controls the V2 sweep left for this chunk). */
.ds-btn {
  font-family:var(--font-atmos); font-size:var(--fs-body); font-weight:600;
  min-height:var(--hit-min); padding:var(--sp-2) var(--sp-3); border-radius:var(--radius-sm);
  background:var(--surface-3); color:var(--text); border:1px solid var(--edge-strong);
  cursor:pointer; transition:all var(--dur-fast) var(--ease-standard);
}
.ds-btn:hover:not(:disabled) { border-color:var(--ember); color:var(--ember-bright); }
.ds-btn:disabled { opacity:.32; cursor:not-allowed; }
.ds-btn--primary { background:linear-gradient(180deg,var(--ember-bright),var(--ember)); border-color:transparent; color:var(--on-ember); box-shadow:var(--elev-1); }
.ds-btn--primary:hover:not(:disabled) { filter:brightness(1.06); color:var(--on-ember); border-color:var(--ember-bright); box-shadow:var(--elev-2); }
.ds-btn--ghost { background:transparent; border-color:transparent; color:var(--text-dim); }
.ds-btn--ghost:hover:not(:disabled) { background:var(--surface-3); color:var(--text); border-color:transparent; box-shadow:none; }
.ds-btn--danger { background:transparent; border-color:var(--danger); color:var(--danger); }
.ds-btn--danger:hover:not(:disabled) { background:var(--danger-wash); color:var(--danger); border-color:var(--danger); box-shadow:var(--danger-glow); }

.ds-tab {
  font-family:var(--font-atmos); font-size:var(--fs-body); font-weight:600;
  min-height:var(--hit-min); padding:var(--sp-2) var(--sp-4); border:none; border-bottom:2px solid transparent;
  background:transparent; color:var(--text-dim); cursor:pointer;
}
.ds-tab--active { color:var(--accent); border-bottom-color:var(--ember); }

.ds-chip {
  display:inline-flex; align-items:center; gap:var(--sp-1); min-height:var(--hit-min);
  padding:var(--sp-1) var(--sp-2); border-radius:999px; font-size:var(--fs-body);
  background:var(--surface-2); border:1px solid var(--edge-c); color:var(--text);
}

.ds-input, .ds-select {
  min-height:var(--hit-min); padding:var(--sp-2); border-radius:var(--radius-sm);
  background:var(--inset); border:1px solid var(--edge-strong); color:var(--text);
  font-family:var(--font-clarity); font-size:var(--fs-body);
}
.tier-clarity .ds-input, .tier-clarity .ds-select { background:var(--c-inset); border-color:var(--c-edge-strong); color:var(--c-text); }
.ds-input:focus, .ds-select:focus { outline:none; border-color:var(--arcane); box-shadow:var(--focus-ring); }

.ds-table { width:100%; border-collapse:collapse; font-family:var(--body-font); font-size:var(--fs-body); color:var(--text); }
.ds-table-row > td, .ds-table-row > th { padding:var(--sp-2) var(--sp-3); border-bottom:1px solid var(--edge-c); text-align:left; }
.ds-table-row:hover { background:var(--ember-wash); }

.ds-tooltip {
  display:inline-block; padding:var(--sp-1) var(--sp-2); border-radius:var(--radius-sm);
  background:var(--surface-3); border:1px solid var(--edge-strong); color:var(--text); font-size:var(--fs-caption);
  box-shadow:var(--elev-2);
}

.ds-toast {
  display:flex; align-items:center; gap:var(--sp-2); padding:var(--sp-2) var(--sp-4);
  border-radius:var(--radius-sm); background:var(--surface-2); border:1px solid var(--edge-c);
  border-left:3px solid var(--accent); color:var(--text); font-size:var(--fs-body); box-shadow:var(--shadow-panel);
}
.ds-toast--success { border-left-color:var(--s-success); }
.ds-toast--warning { border-left-color:var(--s-warning); }
.ds-toast--danger  { border-left-color:var(--s-danger); }

.ds-rail-item {
  display:flex; align-items:center; gap:var(--sp-2); width:100%; min-height:var(--hit-min);
  padding:var(--sp-2) var(--sp-3); border:none; border-left:3px solid transparent; border-radius:0;
  background:transparent; color:var(--text-dim); font-family:var(--font-atmos); font-size:var(--fs-body);
  font-weight:500; cursor:pointer; text-align:left;
}
.ds-rail-item:hover { color:var(--accent); background:var(--ember-wash); }
.ds-rail-item--active { color:var(--accent); border-left-color:var(--ember); background:var(--ember-wash); }

/* ── F3 (R-003) COMPONENT LIBRARY ADDITIONS — the pieces the ds-* seed set was missing ─────────── */

/* The signature reading component: a stat block set in Spectral with an ember rule (mockup
 * aether-style-tile §stat block). Used by the Library monster detail and campaign NPC panels. */
.statblock {
  font-family:var(--font-serif); background:var(--surface-2);
  border:1px solid var(--edge-c, var(--edge)); border-top:3px solid var(--ember);
  border-radius:var(--radius); padding:var(--sp-4) var(--sp-4) var(--sp-3); max-width:56ch;
}
.statblock__name { font-family:var(--font-display); font-size:var(--fs-h2); font-weight:700; color:var(--ember-bright); margin:0; }
.statblock__sub { font-style:italic; color:var(--text-faint); font-size:var(--fs-body); margin:2px 0 var(--sp-3); }
.statblock__abils {
  display:grid; grid-template-columns:repeat(6,1fr); gap:var(--sp-1); text-align:center;
  border-top:1px solid var(--edge-c, var(--edge)); border-bottom:1px solid var(--edge-c, var(--edge));
  padding:var(--sp-2) 0; margin:var(--sp-3) 0;
}
.statblock__abils b { display:block; font-family:var(--font-clarity); font-size:11px; color:var(--text-faint); letter-spacing:.08em; }
.statblock__abils span { font-family:var(--font-clarity); font-weight:600; color:var(--text); }
.statblock__line { margin:0 0 var(--sp-1); color:var(--text-dim); font-size:14.5px; line-height:1.55; }
.statblock__line b { color:var(--ember-bright); font-weight:600; }

/* Designed empty state — every list/detail pane says what it is and what to do next, never a
 * blank black rectangle (kills the "did it work?" ambiguity — R-003/R-010). */
.ds-empty {
  display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center;
  gap:var(--sp-2); padding:var(--sp-6) var(--sp-4); color:var(--text-faint);
}
.ds-empty .ico { width:2.2em; height:2.2em; opacity:.6; }
.ds-empty__title { font-family:var(--font-display); font-size:var(--fs-h3); font-weight:600; color:var(--text-dim); }
.ds-empty__hint { font-size:var(--fs-body); max-width:44ch; line-height:1.5; }

/* Skeleton loader — a quiet shimmer while content arrives (respects reduced motion below). */
.ds-skeleton {
  border-radius:var(--radius-sm); min-height:1em;
  background:linear-gradient(90deg, var(--surface-2) 25%, var(--surface-3) 45%, var(--surface-2) 65%);
  background-size:220% 100%; animation:ds-shimmer 1.4s var(--ease-standard) infinite;
}
@keyframes ds-shimmer { from { background-position:180% 0; } to { background-position:-40% 0; } }

/* The toast stack — toasts queue VERTICALLY, newest at the bottom, never overprint (V-004's fix;
 * F5 wires the queue JS + enter/exit motion). position:fixed lives here per the U2 gate. */
.toast-stack {
  position:fixed; left:var(--sp-4); bottom:var(--sp-4); z-index:var(--z-toast);
  display:flex; flex-direction:column; gap:var(--sp-2); pointer-events:none; max-width:min(420px, 80vw);
}
.toast-stack .ds-toast { pointer-events:auto; box-shadow:var(--elev-2); }

/* Field wrapper — label · control · helper/error, one shape for every form (mockup §inputs). */
.ds-field { display:flex; flex-direction:column; gap:6px; }
.ds-field > label { font-size:var(--fs-caption); color:var(--text-faint); font-weight:600; letter-spacing:.04em; }
.ds-field__error { font-size:var(--fs-caption); color:var(--danger); }
.ds-field--invalid .ds-input, .ds-field--invalid input, .ds-field--invalid select { border-color:var(--danger); }

/* ── LIVE positioning classes — these absorb the `position:fixed` strings out of the .mjs modules ─
 *    (interim: U3 replaces the floating launchers with the left rail and removes these) ── */

/* Floating launchers: one shared, de-collided offset table (was web/launchslots.mjs). */
.cos-launcher { position:fixed; z-index:var(--z-launcher); }
.cos-launcher--map      { left:12px; bottom:12px; }
.cos-launcher--tokens   { left:12px; bottom:52px; }
.cos-launcher--fog      { left:12px; bottom:92px; }
.cos-launcher--sheets   { left:12px; bottom:132px; }
.cos-launcher--module   { left:12px; bottom:172px; }
.cos-launcher--campaign { left:12px; bottom:212px; }
.cos-launcher--cos      { left:12px; bottom:252px; }
.cos-launcher--engine   { left:12px; bottom:292px; }
.cos-launcher--dm       { right:12px; bottom:12px; }
.cos-launcher--myboard  { right:12px; bottom:52px; }
.cos-launcher--combat   { right:12px; bottom:92px; }
.cos-launcher--roll     { right:12px; top:12px; }
.cos-launcher--spell    { left:50%; transform:translateX(-50%); bottom:12px; }

/* Floating panels (toggled open by their launcher). Position anchors only — width/appearance stay
 * on the element. `display:none` remains inline so each module's show/hide toggle is unchanged. */
.cos-panel { position:fixed; z-index:var(--z-panel); }
.cos-panel--map      { left:12px; bottom:48px; }
.cos-panel--tokens   { left:12px; bottom:84px; }
.cos-panel--fog      { left:12px; bottom:120px; }
.cos-panel--sheets   { left:12px; bottom:156px; }
.cos-panel--module   { left:12px; bottom:48px; }
.cos-panel--campaign { left:12px; bottom:84px; }
.cos-panel--cos      { left:12px; bottom:80px; }
.cos-panel--engine   { left:12px; bottom:52px; }
.cos-panel--dm       { right:12px; bottom:52px; }
.cos-panel--myboard  { right:12px; bottom:48px; }
.cos-panel--combat   { left:12px; top:52px; }
.cos-panel--roll     { right:12px; top:52px; }
.cos-panel--spell    { left:50%; transform:translateX(-50%); bottom:52px; }

/* Themed modal (window.modal — U0.2). Overlay + centered dialog. */
.cos-modal-overlay {
  position:fixed; inset:0; z-index:var(--z-modal); display:flex; align-items:center; justify-content:center;
  background:var(--scrim); backdrop-filter:blur(6px);
}
.cos-modal-dialog {
  min-width:320px; max-width:min(92vw,480px); background:var(--surface); border:1px solid var(--edge2);
  border-radius:var(--radius-lg); padding:var(--sp-4) var(--sp-4) var(--sp-4); box-shadow:var(--elev-3); color:var(--text);
}

/* ── V5 ENTRY SCREEN — the dedicated Host / Join / Solo lobby. Opaque from first paint (static
 *    #entryScreen in index.html), so nothing renders behind it until a path is chosen. The
 *    position:fixed lives HERE (the U2 gate keeps it out of .mjs), like the modal overlay. ──── */
.entry-screen {
  position:fixed; inset:0; z-index:calc(var(--z-modal) - 10); overflow:auto;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  /* CW-11: `background-attachment: fixed` on an already-position:fixed element buys nothing and
   * costs a full-viewport repaint on every scroll in Chrome. Measured at 5 ms here — not the cause
   * of the live freeze (that was the health-check fetch) — but it is pure waste, so it goes. */
  gap:var(--sp-4); padding:var(--sp-6); background:var(--backdrop);
}
.entry-screen--closed { display:none; }
/* G-007 (F-030) LAYOUT ECONOMY — the entry band, the Table sidebar/tools and the Campaign tree were
 * all pinned to constant pixel widths, so a 2560×1440 window got the same 640px island a 1280px one
 * did: measured 576×227px, 3.5% of the viewport, the rest black. They now grow with the viewport
 * (clamp/min against vw) and stop at a sane reading width. The ≤1200 / ≤1040 media queries below
 * still pin SMALLER constants — cramped screens keep giving their space to the map. */
/* F8 (R-011): the wordmark — Cormorant, ember-to-arcane gradient (the aether-style-tile brand),
 * over the d20 mark. The entry hero doubles as the warm splash: opaque from first paint. */
.entry-mark { color:var(--ember); text-align:center; }
.entry-mark svg { width:64px; height:64px; filter:drop-shadow(0 0 14px rgba(232,177,90,.35)); }
.entry-brand {
  font-family:var(--font-display); font-size:var(--fs-display); font-weight:700; letter-spacing:.5px; text-align:center;
  background:linear-gradient(90deg, var(--ember-bright), var(--ember) 55%, var(--arcane-bright));
  -webkit-background-clip:text; background-clip:text; color:transparent;
}
.entry-brand small { font-size:var(--fs-h3); -webkit-text-fill-color:var(--text-faint); font-style:italic; }
.entry-tagline { font-family:var(--font-atmos-body); font-size:var(--fs-body-lg); color:var(--dim); margin-bottom:var(--sp-2); }
.entry-step { display:none; flex-direction:column; align-items:center; gap:var(--sp-4); width:100%; max-width:min(88vw, 1120px); }
.entry-step--active { display:flex; }
.entry-cards { display:flex; gap:var(--sp-4); flex-wrap:wrap; justify-content:center; width:100%; }
.entry-card {
  flex:1 1 clamp(280px, 24vw, 400px); max-width:520px; min-height:150px; text-align:left; padding:var(--sp-4); cursor:pointer;
  background:var(--panel); border:1px solid var(--edge2); border-radius:var(--radius-lg); color:var(--parch);
}
.entry-card:hover:not(:disabled) { border-color:var(--gold); box-shadow:var(--ember-glow); }
.entry-card:disabled { opacity:.45; cursor:not-allowed; }
.entry-card__title { font-family:var(--font-atmos); font-size:var(--fs-h3); color:var(--gold-bright); margin-bottom:var(--sp-2); }
.entry-card__sub { font-size:var(--fs-body); color:var(--dim); line-height:1.5; }
/* B-12 (F-018): the resume card sits above the path cards — the returning DM's one-click continue. */
.entry-resume { display:flex; justify-content:center; width:100%; margin-bottom:var(--sp-2); }
.entry-resume:empty { display:none; margin:0; }
.entry-card--resume {
  width:100%; max-width:min(88vw, 1120px); min-height:0; text-align:left;
  border-color:var(--gold); background:linear-gradient(180deg, var(--ember-wash), var(--panel));
}
.entry-card--resume:hover:not(:disabled) { box-shadow:var(--ember-glow-strong); }
.entry-card--resume .entry-card__title { color:var(--gold-bright); margin-bottom:2px; }
/* CW-12 — the Rejoin card sits above everything else on the entry screen, because a guest who
   reloaded mid-fight wants exactly one thing and it is not a list of recent games. */
.entry-rejoin { display:flex; flex-direction:column; align-items:center; gap:var(--sp-1); }
.entry-minor { display:flex; justify-content:center; }
.entry-side { width:100%; max-width:min(88vw, 900px); }
/* G-007, second half: a wider band on a big screen is still a postage stamp if the type stays at
 * 1280px size. The entry screen — and only the entry screen — scales its display type up past
 * 1800px, so the first thing a new player sees is sized for the screen they actually have. */
@media (min-width: 1800px) {
  .entry-brand { font-size:calc(var(--fs-display) * 1.7); }
  .entry-brand small { font-size:calc(var(--fs-h3) * 1.4); }
  .entry-tagline { font-size:calc(var(--fs-body-lg) * 1.4); }
  .entry-card { min-height:200px; padding:var(--sp-5); }
  .entry-card__title { font-size:calc(var(--fs-h3) * 1.25); }
  .entry-card__sub { font-size:calc(var(--fs-body) * 1.2); }
}
.entry-side__title { font-family:var(--font-atmos); font-size:var(--fs-h3); color:var(--gold-bright); margin-bottom:var(--sp-2); }
.entry-hint { font-size:var(--fs-body); color:var(--dim); line-height:1.5; }
.entry-status { font-size:var(--fs-body); color:var(--mist); min-height:var(--sp-5); line-height:1.5; }
.entry-list { display:flex; flex-direction:column; gap:var(--sp-1); align-items:stretch; }
.entry-list__item { text-align:left; }
.entry-row { display:flex; gap:var(--sp-2); align-items:center; margin:var(--sp-2) 0; }
.entry-addr { flex:1 1 auto; min-width:0; }
/* The relocated V4 invite panel (now inside the "Open the Table" dialog) + the header control. */
.invite-addr { font-size:var(--fs-h2); font-weight:700; color:var(--parch); user-select:text; overflow-wrap:anywhere; margin:var(--sp-2) 0; }
.table-open-btn { font-size:var(--fs-body); }

/* ── U3 app shell: left rail + mode panes. Flex / grid / sticky ONLY — no position:fixed, so the
 *    U2 gate stays green. The rail replaces the 13 floating launchers; docked module panels render
 *    inline in their mode pane (`.pane-embed`), never floating, never covering the rail. ───────── */
.app-shell { display:flex; align-items:stretch; gap:0; min-height:calc(100vh - 120px); }
.app-rail {
  display:flex; flex-direction:column; gap:var(--sp-1); padding:var(--sp-2);
  min-width:172px; align-self:flex-start; position:sticky; top:var(--sp-2);
  border-right:1px solid var(--edge2); background:var(--surface-veil);
}
.app-rail .brandmark { font-family:var(--font-display); color:var(--ember); letter-spacing:1px;
  font-size:var(--fs-caption); padding:var(--sp-2) var(--sp-3) var(--sp-3); }
.app-content { flex:1 1 auto; min-width:0; padding:var(--sp-4); }
/* F4 (R-004): ≤1280 the rail is a first-class ICON-ONLY mode — labels fold away, the tooltip
 * (title) still names each item, and the content keeps the width (V-008's band, fixed early). */
@media (max-width: 1280px) {
  .app-rail { min-width:0; }
  .app-rail .rail-label { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0,0,0,0); }
  .app-rail .brandmark { display:none; }   /* the header wordmark is right above — no duplicate in the narrow rail */
  .ds-rail-item { justify-content:center; padding:var(--sp-2); }
  .ds-rail-item svg.ico { width:1.4em; height:1.4em; }
}
.mode-pane { display:none; }
.mode-pane.active { display:block; }
/* F6 (R-009): Settings/About is a READING surface — a centered column at content width, not a
 * strip pinned to the top-left of a wide viewport (V-005). */
.mode-pane[data-mode="settings"].active { max-width:var(--content-max); margin:0 auto; }
.mode-pane__title { font-family:var(--font-atmos); color:var(--gold-bright); letter-spacing:1px;
  font-size:var(--fs-h3); margin-bottom:var(--sp-3); }
/* docked panels flow in a wrap layout; each is a static, in-flow block (not fixed) */
.pane-flow { display:flex; flex-wrap:wrap; gap:var(--sp-4); align-items:flex-start; }
.pane-embed { position:static; display:block; margin:0; max-width:100%; box-shadow:none; }
.mode-empty { color:var(--dim); font-family:var(--font-atmos-body); font-size:var(--fs-body); max-width:60ch; line-height:1.5; }

/* ── V3: clarity-tier adoption for the legacy screen classes ─────────────────────────────────────
 * The play/data surfaces (sheet cards, skills table, inventory tables, combat panels, forms, logs)
 * are built from index.html's legacy inline classes. Rather than rewrite every JS template, the
 * screens now sit inside a `.tier-clarity` wrapper (#view on the Combat/Character/Inventory tabs,
 * per-panel wrappers on the Roster, the Table/Library/DM/Campaign/Settings shell panes) and these
 * scoped rules remap the legacy classes onto the clarity tokens. Descendant selectors out-rank the
 * single-class legacy rules in index.html's inline <style>, so load order doesn't matter.
 * Atmosphere stays untouched on: header/nav/rail/footer chrome, screen titles, lore & flavor text
 * (.mode-empty, the empty-roster welcome), handouts, and the memorial ("The Fallen").
 * Every pair asserted here is documented with its computed ratio in docs/ui-overhaul/V3_CONTRAST.md. */
.tier-clarity .panel { background:var(--c-surface); border-color:var(--c-edge); color:var(--c-text);
  box-shadow:0 6px 22px rgba(0,0,0,.45); }
.tier-clarity .panel h3 { color:var(--c-text-dim); }
.tier-clarity .muted { color:var(--c-text-dim); }
/* POL-4: on clarity surfaces helper/microcopy text reads at ≥14px in --c-text-faint (5.09:1 on
 * --c-surface) instead of 12px italic --dim. True captions (.stat .l, .abil .an…) stay 12px but
 * take --c-text-dim (8.27:1). */
.tier-clarity .tiny { color:var(--c-text-faint); font-size:var(--fs-body); }
.tier-clarity .stat, .tier-clarity .abil { background:var(--c-surface-2); border-color:var(--c-edge); }
.tier-clarity .stat .l, .tier-clarity .abil .an, .tier-clarity .abil .sv { color:var(--c-text-dim); }
.tier-clarity .abil .md, .tier-clarity .wcard .nm, .tier-clarity .featrow .fn,
.tier-clarity .calc td.v { color:var(--c-text); }
.tier-clarity .effect { background:var(--c-surface-2); border-color:var(--c-edge); }
.tier-clarity .effect .dur { color:var(--c-text-dim); }
.tier-clarity .wcard, .tier-clarity .ecard, .tier-clarity .featrow, .tier-clarity .modrow,
.tier-clarity .rcard, .tier-clarity .bopt, .tier-clarity .recrow { background:var(--c-surface-2); border-color:var(--c-edge); }
.tier-clarity .wcard .dmg, .tier-clarity .ecard .taken, .tier-clarity .featrow .ft,
.tier-clarity .itemtable th, .tier-clarity .slotrow .sl, .tier-clarity .rcard .rm,
.tier-clarity .calc .dimc, .tier-clarity .breakdown, .tier-clarity .log .note,
.tier-clarity .xplabel { color:var(--c-text-dim); }
.tier-clarity .wcard .tags { color:var(--c-text-faint); font-size:var(--fs-body); }
.tier-clarity .ecard.dead .enm { color:var(--c-text-faint); } /* on the card's own dark bg — 6.19:1 */
.tier-clarity .chip, .tier-clarity .tog, .tier-clarity .dchip {
  background:var(--c-inset); border-color:var(--c-edge-strong); color:var(--c-text-dim); }
.tier-clarity .dchip .delx { color:var(--c-text-faint); }
.tier-clarity .dchip.res { color:var(--s-info); border-color:var(--s-info); }
/* the active roster card's border is its only "selected" signal — needs ≥3:1 against the card fill
 * on clarity (--blood-bright manages only 2.52:1 there; --s-danger holds 4.73:1) */
.tier-clarity .rcard.on { border-color:var(--ember); }
.tier-clarity .itemtable td, .tier-clarity .skrow, .tier-clarity .log p,
.tier-clarity .calc td, .tier-clarity .dmgtable td { border-bottom-color:var(--c-edge); }
.tier-clarity .log { border-top-color:var(--c-edge); }
.tier-clarity input[type=number], .tier-clarity input[type=text],
.tier-clarity select, .tier-clarity textarea {
  background:var(--c-inset); border-color:var(--c-edge-strong); color:var(--c-text); }
.tier-clarity button.ghost { color:var(--c-text-dim); }

/* ── V8a TABLE LAYOUT — the canvas-first Table composition (table.mjs). Flex/grid ONLY — no
 *    position:fixed, so the U2 gate stays green. Structure: a main band (thin layer rail · that
 *    layer's tool drawer · the canvas stage · a collapsible tabbed sidebar) over the bottom
 *    active-combat bar. The sidebar collapses to an icon strip (.table-side--collapsed). ──────── */
/* V11 (UIR2 NEW-4): the shell is BOUND to the viewport (header + content padding ≈ 130px), so an
 * overflowing sidebar pane / tool drawer scrolls INTERNALLY and the combat bar stays on-screen —
 * the page never scrolls the rail along with a tall panel. */
/* B-5 (F-017): the shell fills the full window width — previously it sized to content, so once the
 * canvas became an absolute fill (0 in-flow width) the stage starved. width:100% keeps the stage
 * growing to the real viewport at every width. */
.table-shell { width:100%; flex:1 1 auto; align-self:stretch; height:calc(100vh - 140px); min-height:480px; }
.table-shell__stack { display:flex; flex-direction:column; gap:var(--sp-2); width:100%; height:100%; min-height:0; }
.table-main { display:flex; gap:var(--sp-2); align-items:stretch; min-height:0; flex:1 1 0; width:100%; }
.table-rail {
  display:flex; flex-direction:column; gap:var(--sp-1); padding:var(--sp-1);
  flex:0 0 auto; align-self:flex-start;
  background:var(--surface-veil); backdrop-filter:blur(10px);
  border:1px solid var(--edge-c); border-radius:var(--radius-lg); box-shadow:var(--elev-2);
}
.table-rail__btn {
  width:38px; height:38px; min-height:var(--hit-min); display:flex; align-items:center; justify-content:center;
  background:transparent; border:1px solid transparent; border-radius:var(--radius-sm);
  color:var(--text-dim); font-size:var(--fs-body-lg); cursor:pointer;
}
.table-rail__btn:hover { border-color:var(--gold); color:var(--accent); }
.table-rail__btn--active { border-color:var(--gold); background:var(--ember-wash); color:var(--accent); }
.table-tools {
  flex:0 0 clamp(236px, 12vw, 340px); max-width:340px; overflow:auto; padding:var(--sp-2);
  background:var(--surface-veil); backdrop-filter:blur(10px);
  border:1px solid var(--edge-c); border-radius:var(--radius-lg); box-shadow:var(--elev-2);
}
.table-tools__group { display:block; }
.table-tools__title { font-family:var(--font-atmos); color:var(--accent); font-size:var(--fs-body); margin:var(--sp-2) 0 var(--sp-1); }
/* B-5 (F-006/F-017): the stage is a definite-size box; its layer canvas fills it edge-to-edge
 * (no more 600×400 postage stamp centered in dead space). A ResizeObserver in each layer keeps the
 * backing store matched. The token HUD still anchors to the (relative) stage. */
.table-stage {
  flex:1 1 auto; min-width:min(60vw, 480px); min-height:0; overflow:hidden;
  position:relative;   /* V8b: the token HUD anchors to the stage */
  background:var(--stage); border:1px solid var(--edge-c); border-radius:var(--radius-lg);
}
.table-stage__canvas { position:absolute; inset:0; width:100%; height:100%; max-width:none; }
.table-side { flex:0 0 clamp(340px, 22vw, 560px); display:flex; gap:var(--sp-1); align-items:stretch; min-width:0; min-height:0; }
.table-side__tabs { display:flex; flex-direction:column; gap:var(--sp-1); flex:0 0 auto; }
.table-side__tabbtn {
  display:flex; align-items:center; min-height:var(--hit-min); padding:var(--sp-1) var(--sp-2);
  background:var(--surface); border:1px solid var(--edge-c); border-radius:var(--radius-sm);
  color:var(--text-dim); font-size:var(--fs-body); cursor:pointer; text-align:left; white-space:nowrap;
}
.table-side__tabbtn:hover { border-color:var(--gold); color:var(--accent); }
.table-side__tabbtn--active { border-color:var(--gold); background:var(--ember-wash); color:var(--accent); }
.table-side__body {
  flex:1 1 auto; min-width:0; overflow:auto; padding:var(--sp-2);
  background:var(--surface-veil); backdrop-filter:blur(10px);
  border:1px solid var(--edge-c); border-radius:var(--radius-lg); box-shadow:var(--elev-2);
}
.table-side--collapsed { flex-basis:auto; }
.table-side--collapsed .table-side__body { display:none; }
.table-side--collapsed .table-side__tablabel { display:none; }
.table-side__pane > .pane-embed, .table-embed { width:auto; max-width:100%; }
.table-bottom {
  display:flex; flex-direction:column; gap:var(--sp-1); padding:var(--sp-2);
  flex:0 0 auto;   /* V11: the combat bar never yields its height to an overflowing panel */
  position:relative;   /* V8b: the encounter picker anchors above the bar */
  background:var(--surface-veil); backdrop-filter:blur(10px);
  border:1px solid var(--edge-c); border-radius:var(--radius-lg); box-shadow:var(--elev-2);
}
/* D-013: the fold's body was hidden unless the bar was ACTIVE, so out of combat the "⋯ Advanced"
 * button flipped its own label and revealed nothing — a control that acknowledges a click and does
 * nothing. The disclosure now owns its own visibility (the JS toggle sets the inline display); the
 * active class only styles it. */
.table-bottom__combat { flex-direction:column; gap:var(--sp-1); }
/* D-005: the typed fallbacks for combatant/condition/effect — one disclosure below the pickers, so
 * one parameter never has two visible controls holding different values. */
.table-typed > summary { cursor:pointer; color:var(--text-dim); font-size:var(--fs-body); min-height:var(--hit-min); }
.table-typed > summary:hover { color:var(--accent); }
.table-bottom--active { border-color:var(--gold); }
.table-bottom--active .table-bottom__combat { border-left:2px solid var(--gold); padding-left:var(--sp-1); }
.table-row { display:flex; flex-wrap:wrap; gap:var(--sp-1); align-items:center; }
.table-label { color:var(--text-dim); font-size:var(--fs-body); }
/* B-5 (F-017): at narrow widths the tools drawer + sidebar yield space so the map stays the
 * majority of the stage, and the sidebar/tools scroll internally (the page never scrolls the rail
 * off-screen — the combat bar stays visible). */
@media (max-width: 1200px) {
  .table-tools { flex-basis:170px; max-width:170px; }
  .table-side { flex-basis:290px; }
  .table-stage { min-width:min(58vw, 420px); }
}
@media (max-width: 1040px) {
  .table-tools { flex-basis:132px; max-width:132px; }
  /* the sidebar tabs collapse to icons so the panel body (initiative/ledger/…) keeps real width
   * instead of being crushed to a sliver by full-text tab labels. */
  .table-side { flex-basis:220px; }
  .table-stage { min-width:min(56vw, 340px); }
  .table-side__tablabel { display:none; }
  .table-side__tabbtn { justify-content:center; padding:var(--sp-1); }
  /* the top chrome (header + board title) is taller when it wraps at narrow widths — give the shell
   * more headroom so the combat bar stays on-screen (no page scroll). */
  .table-shell { height:calc(100vh - 180px); min-height:420px; }
}
.table-hint { color:var(--text-faint); font-size:var(--fs-body); line-height:1.5; margin:var(--sp-1) 0; }

/* ── V8b CONTEXTUAL TOOLS — popover cards (token HUD · map picker · encounter picker). Absolute
 *    only, inside a position:relative region — never fixed, so the U2 gate stays green. ───────── */
.table-pop {
  background:var(--surface); border:1px solid var(--edge-c); border-radius:var(--radius-sm);
  box-shadow:var(--elev-2); padding:var(--sp-2); margin:var(--sp-1) 0;
}
.table-hud { position:absolute; left:12px; top:12px; z-index:30; min-width:250px; max-width:330px; margin:0; }
.table-hud__head { justify-content:space-between; }
.table-hud__title { margin:0; }
.table-pop--enc { position:absolute; bottom:calc(100% + var(--sp-1)); left:var(--sp-2); z-index:30; min-width:300px; max-width:420px; margin:0; }

/* ── FP-06: the Vision segmented mode control + the stage mode chip. Absolute inside the
 *    position:relative stage (the V8b popover pattern) — never fixed, the U2 gate stays green. ── */
.table-vision__modes { gap:0; }
.table-vision__mode { border-radius:0; }
.table-vision__mode:first-child { border-radius:var(--radius-sm) 0 0 var(--radius-sm); }
.table-vision__mode:last-child { border-radius:0 var(--radius-sm) var(--radius-sm) 0; }
.table-vision__mode--active { background:var(--ember-dim, var(--edge-c)); color:var(--ember-bright); border-color:var(--ember); }
.table-stage__modechip {
  position:absolute; left:var(--sp-2); bottom:var(--sp-2); z-index:20;
  background:var(--surface-veil, rgba(20,18,26,.82)); border:1px solid var(--edge-c);
  border-radius:var(--radius-sm); padding:2px var(--sp-1); font-size:var(--fs-small, 12px);
  color:var(--text-faint); pointer-events:none;
}

/* ── FP-26a: the turn-order strip — the stage wears the fight. Absolute inside the
 *    position:relative stage; never fixed (the U2 gate stays green). ─────────────────────────── */
.table-stage__turnstrip {
  position:absolute; top:var(--sp-1); left:50%; transform:translateX(-50%); z-index:20;
  display:flex; gap:4px; flex-wrap:wrap; justify-content:center; max-width:min(92%, 60vw);
  background:var(--surface-veil, rgba(20,18,26,.82)); border:1px solid var(--edge-c);
  border-radius:var(--radius-sm); padding:2px var(--sp-1); pointer-events:none;
}
.table-turnchip {
  font-size:var(--fs-small, 12px); color:var(--text-faint);
  padding:0 6px; border-radius:var(--radius-sm); border:1px solid transparent; white-space:nowrap;
}
.table-turnchip--round { color:var(--ember-bright); font-family:var(--font-atmos); }
.table-turnchip--active { color:var(--ember-bright); border-color:var(--ember); background:rgba(240,169,59,.12); }
.table-turnchip--down { text-decoration:line-through; opacity:.55; }

/* CW-06 — the DM approval card. A player waiting at the door is not a toast: it stays on the stage,
   top-right (clear of the centred turn strip), until the DM answers it one way or the other. */
.table-stage__joins {
  position:absolute; top:var(--sp-1); right:var(--sp-1); z-index:24;
  max-width:min(320px, 46%);
  background:var(--surface); border:1px solid var(--edge-c); border-left:3px solid var(--ember, #E8B15A);
  border-radius:var(--radius-sm); padding:var(--sp-1); box-shadow:0 8px 30px rgba(0,0,0,.55);
}
.table-stage__joins .table-row { gap:6px; flex-wrap:wrap; }
/* CW-05 — the token HUD's identity line: who this token IS, where a bind dropdown used to be. */
.table-hud__identity { margin-bottom:var(--sp-1); color:var(--text); }

/* CW-16 — THE PREVIEW. It sits on the stage, bottom-right, because it is the last thing between a
   roll and somebody's hit points and the table is looking at the board when it happens. It is
   scrollable: a fireball can catch eight creatures and every row of it stays reachable. */
.table-stage__preview {
  position:absolute; right:var(--sp-1); bottom:var(--sp-1); z-index:26;
  max-width:min(460px, 60%); max-height:56vh; overflow-y:auto;
  background:var(--surface); border:1px solid var(--edge-c); border-left:3px solid var(--ember, #E8B15A);
  border-radius:var(--radius-sm); padding:var(--sp-1); box-shadow:0 8px 30px rgba(0,0,0,.6);
}
.table-stage__preview .table-row { gap:4px; flex-wrap:wrap; align-items:center; }

/* CW-17 — the REACTION WINDOW. It sits directly above the preview, because that is the order it
   happens in: the window opens, somebody Shields, and only then does the preview say "miss". It is
   the one card on the stage that is WAITING ON A PERSON, so it wears the blood edge rather than the
   ember one — a table glancing up should be able to tell "review this" from "answer this". */
.table-stage__reaction {
  bottom:calc(56vh + var(--sp-2)); border-left-color:var(--blood, #8E2B2B);
}

/* CW-08 — the Table-side scene picker. Floor chips are a segmented control, not a dropdown: which
   floor you are on has to be READABLE at a glance, and switching has to be one click. */
.table-floorchip { border-color:var(--edge-c); }
.table-floorchip--on { color:var(--ember-bright); border-color:var(--ember); background:rgba(240,169,59,.12); }
.table-floors { gap:4px; flex-wrap:wrap; align-items:center; margin-top:var(--sp-1); }
.table-loclist { max-height:46vh; overflow-y:auto; margin:var(--sp-1) 0; }
.table-locfilter { width:100%; }
.table-row--indent { padding-left:var(--sp-2); }
.table-pop--loc { min-width:min(320px, 80vw); }

/* CW-10 — "＋ Add" on the board itself. Bottom-left of the stage, clear of the centred turn strip
   and the top-right approval card, so the one thing a DM does most is never three clicks deep. */
.table-stage__add { position:absolute; left:var(--sp-1); bottom:var(--sp-1); z-index:22; }
.table-pop--add { position:absolute; left:var(--sp-1); bottom:calc(var(--sp-1) * 3 + 18px); z-index:23; min-width:min(340px, 84vw); }

/* CW-09 — PLAYER BOARD PARITY. The guest's Table was a 660px card with a 600×400 map in it while
   the DM had the whole stage; on a phone that is a postage stamp. It fills its pane now, and wears
   the same two cues the DM's board does: whose turn it is, and who is down. */
.pb-board { display:flex; flex-direction:column; gap:var(--sp-1); min-height:0; }
.pb-board canvas { flex:1 1 auto; min-height:300px; }
.pb-turnstrip {
  display:flex; gap:4px; flex-wrap:wrap; align-items:center;
  background:var(--surface-veil, rgba(20,18,26,.82)); border:1px solid var(--edge-c);
  border-radius:var(--radius-sm); padding:2px var(--sp-1);
}
.pb-turnchip {
  font-size:var(--fs-small, 12px); color:var(--text-faint);
  padding:0 6px; border-radius:var(--radius-sm); border:1px solid transparent; white-space:nowrap;
}
.pb-turnchip--round { color:var(--ember-bright); font-family:var(--font-atmos); }
.pb-turnchip--active { color:var(--ember-bright); border-color:var(--ember); background:rgba(240,169,59,.12); }
.pb-turnchip--me { color:var(--s-hp, #8FE3B1); }
.pb-turnchip--down { text-decoration:line-through; opacity:.55; }
/* The roll call docks directly under the strip — an ask a player misses is an ask that never happened. */
.pb-prompts { border-left:3px solid var(--ember, #E8B15A); padding-left:var(--sp-1); }

/* ── V9 CAMPAIGN MASTER-DETAIL — the tree + detail composition (campaignview.mjs). Flex/grid
 *    ONLY — no position:fixed, so the U2 gate stays green. Structure: a toolbar (module picker ·
 *    entry/area actions) over a main band (tree · detail) over the status strip. The module
 *    picker is an absolute popover inside the position:relative toolbar (the V8b pattern). ────── */
.camp-shell { display:flex; flex-direction:column; gap:var(--sp-2); }
.camp-toolbar {
  display:flex; flex-wrap:wrap; gap:var(--sp-1); align-items:center; padding:var(--sp-2);
  position:relative;   /* the module picker anchors below the bar */
  background:var(--surface); border:1px solid var(--edge-c); border-radius:var(--radius-sm);
}
.camp-toolbar__mod { color:var(--text-dim); font-size:var(--fs-body); }
.camp-toolbar__spacer { flex:1 1 auto; }
.camp-val { display:flex; flex-direction:column; gap:var(--sp-1); }
.camp-val:empty { display:none; }
/* B-10 (F-014): the Campaign page used to be ONE long scroll — the tree grew past the viewport and
 * the detail pane stayed pinned at the top, so picking a low tree item rendered its detail off-screen.
 * Bounding .camp-main makes the tree and the detail each own an internal scroll region instead. */
/* The allowance covers the app header + pane title + toolbar + validation strip above, and the
 * status strip below — measured against the real app at 2560×1440 and 1024×700. */
.camp-main { display:flex; gap:var(--sp-2); align-items:stretch; min-height:16rem; max-height:calc(100vh - 23rem); }
.camp-tree {
  flex:0 0 clamp(280px, 15vw, 420px); max-width:420px; overflow:auto; padding:var(--sp-2);
  display:flex; flex-direction:column; gap:2px;
  background:var(--surface); border:1px solid var(--edge-c); border-radius:var(--radius-sm);
}
.camp-tree__group { font-family:var(--font-atmos); color:var(--accent); font-size:var(--fs-body); margin:var(--sp-2) 0 var(--sp-1); }
.camp-tree__kind { color:var(--text-dim); font-size:var(--fs-body); margin:var(--sp-1) 0 2px; }
.camp-node {
  display:flex; align-items:center; gap:var(--sp-1); width:100%; min-height:var(--hit-min);
  padding:2px var(--sp-1); background:transparent; border:1px solid transparent;
  border-radius:var(--radius-sm); color:var(--text); font-size:var(--fs-body); cursor:pointer; text-align:left;
}
.camp-node:hover { border-color:var(--gold); color:var(--accent); }
.camp-node--active { border-color:var(--gold); background:var(--ember-wash); color:var(--accent); }
.camp-node__label { flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.camp-node__badge { flex:0 0 auto; color:var(--text-dim); font-size:var(--fs-caption); }
.camp-detail {
  flex:1 1 auto; min-width:0; overflow:auto; padding:var(--sp-2);
  background:var(--surface); border:1px solid var(--edge-c); border-radius:var(--radius-sm);
}
.camp-detail__title { font-family:var(--font-atmos); color:var(--accent); font-size:var(--fs-h3); margin:0 0 var(--sp-1); }
.camp-meta { color:var(--text-dim); font-size:var(--fs-body); margin-bottom:var(--sp-1); }
.camp-body { font-size:var(--fs-body); line-height:1.5; white-space:pre-wrap; margin:var(--sp-1) 0; }
.camp-read { margin-bottom:var(--sp-2); }
.camp-chips { display:flex; flex-wrap:wrap; gap:var(--sp-1); align-items:center; margin:var(--sp-1) 0; }
.camp-chip {
  display:inline-flex; align-items:center; gap:4px; min-height:var(--hit-min);
  padding:2px var(--sp-2); background:var(--inset); border:1px solid var(--edge-c);
  border-radius:999px; color:var(--text); font-size:var(--fs-body); cursor:pointer;
}
.camp-chip:hover { border-color:var(--gold); color:var(--accent); }
/* D-006: an entry's CURRENT links, shown by name with an unlink action. The raw id string they
 * write moved behind the ⋯ Advanced disclosure — still there, no longer the primary surface. */
.camp-chip--linked { border-color:var(--gold); cursor:default; }
.camp-chip__x {
  min-height:var(--hit-min); min-width:var(--hit-min); margin-left:2px; padding:0 4px;
  background:transparent; border:none; border-radius:999px;
  color:var(--text-dim); font-size:var(--fs-body); cursor:pointer;
}
.camp-chip__x:hover { color:var(--s-danger); }
.camp-adv > summary { cursor:pointer; color:var(--text-dim); font-size:var(--fs-body); min-height:var(--hit-min); }
.camp-adv > summary:hover { color:var(--accent); }
.camp-pop {
  position:absolute; top:calc(100% + var(--sp-1)); left:var(--sp-2); z-index:30;
  min-width:320px; max-width:520px;
  background:var(--surface); border:1px solid var(--edge-c); border-radius:var(--radius-sm);
  box-shadow:var(--elev-2); padding:var(--sp-2);
}
.camp-status {
  display:flex; flex-wrap:wrap; gap:var(--sp-2); align-items:center; padding:var(--sp-1) var(--sp-2);
  background:var(--surface); border:1px solid var(--edge-c); border-radius:var(--radius-sm);
}

/* ── C-4 (ENH-4) THE TUTORIAL / COACH MARKS ──────────────────────────────────────────────────────
 *  The overlay is FOUR dim panels drawn AROUND the anchor, never one sheet over it, and every one
 *  is pointer-events:none. That is the ENH-4 "never blocks input" invariant expressed in CSS: the
 *  control being explained stays fully clickable, so the tour can say "try it" and mean it. ────── */
/*  All fixed positioning lives HERE, like .palette-overlay and the modal — the U2 gate keeps it out
 *  of the .mjs modules. tutorial.mjs computes only geometry (left/top/width/height) and toggles the
 *  --full / --center modifiers below. */
/* OL-U — THE ROOT IS A BAG, NOT A LAYER.
 *
 * It used to be `position:fixed; z-index:var(--z-tutorial)`, and BOTH of those create a stacking
 * context — which meant every child of the tour was flattened to z-index 9000 against the page no
 * matter what it declared for itself. Raising the card to sit above the modal layer did nothing at
 * all until this line changed, because the card's 100120 was being resolved INSIDE a box that the
 * modal overlay was already on top of.
 *
 * That one line is most of "the instruction disappears when I do what it says": open the level-up
 * wizard, the spell picker, or any confirmation, and the tour went behind it. Every child here is
 * `position:fixed` in its own right, so the container needs no positioning and no layer of its own —
 * it exists to be shown and hidden in one place. */
.tut-root { position:static; pointer-events:none; }
.tut-shade { position:fixed; background:var(--scrim); pointer-events:none; z-index:var(--z-tutorial); }
.tut-shade--full { inset:0; }
/* OL-U — THE RING AND THE CARD LIVE ABOVE THE MODAL LAYER, and the shades stay below it.
 *
 * Found by walking the tours: three of them say "press this" about a control that opens a modal or
 * a wizard — the level-up wizard, the spell picker, a delete confirmation — and .cos-modal-overlay
 * is a full-viewport panel at --z-modal (100000) against the tour's 9002. So the instruction card
 * did exactly what Nate described: it disappeared the moment you did what it asked, and there was
 * no way to get it back, because the thing hiding it was the thing the card had told you to open.
 *
 * "Above the modal" is right for the CARD (it is the instruction for what you are looking at) and
 * for the RING (it points at a control that may now be inside that modal). It is wrong for the
 * SHADES, which dim the app behind the lesson — a modal is already its own dimmer, and stacking a
 * second one over it just makes the dialog unreadable. So they stay where they were. */
.tut-ring {
  position:fixed; border:2px solid var(--gold); border-radius:var(--radius-sm); pointer-events:none;
  z-index:calc(var(--z-palette) + 10);
  box-shadow:0 0 0 3px var(--ember-wash-strong), var(--ember-glow-strong);
}
.tut-card, .tut-offer {
  position:fixed; pointer-events:auto; z-index:calc(var(--z-palette) + 20); width:min(360px, calc(100vw - 24px));
  background:var(--surface); border:1px solid var(--gold); border-radius:var(--radius-sm);
  padding:var(--sp-2); box-shadow:var(--elev-3);
}
.tut-card--center { left:50%; top:50%; transform:translate(-50%,-50%); }
.tut-offer { right:var(--sp-3); bottom:var(--sp-3); }
/* D-010: on a short or narrow screen the offer's 360×~140 card landed in the middle of the working
 * area — at 1024×700 it overprinted the Campaign Write/Edit fields and covered the combat bar's
 * "⋯ Advanced" button. It is an invitation, not a task, so on those viewports it shrinks to a single
 * line: the same choices, a fraction of the footprint, nothing readable hidden behind it. */
@media (max-height: 820px), (max-width: 1100px) {
  .tut-offer { width:auto; max-width:calc(100vw - 24px); padding:var(--sp-1) var(--sp-2); }
  .tut-offer .tut-card__body { display:none; }
  .tut-offer .tut-card__title { margin:0 var(--sp-2) 0 0; font-size:var(--fs-body); display:inline-block; }
  .tut-offer .tut-card__foot { display:inline-flex; gap:var(--sp-1); align-items:center; }
}
.tut-card__title { font-family:var(--font-atmos); color:var(--accent); font-size:var(--fs-body-lg); margin-bottom:var(--sp-1); }
.tut-card__body { font-size:var(--fs-body); line-height:1.5; color:var(--text); }
.tut-card__foot { display:flex; flex-wrap:wrap; align-items:center; gap:var(--sp-1); margin-top:var(--sp-2); }
.tut-card__count { color:var(--text-dim); font-size:var(--fs-caption); margin-left:auto; }

/* The ❓ Learn index — every tour, its state, and a one-click replay. */
/* OL-M: a tour running on a worked example says so on every card — quiet, always there, never in
   the way of the thing being explained. */
/* OL-N: the INSTRUCTION. "Here is what this is" and "now press this" are different kinds of
   sentence, so they get different weight — the imperative reads first and does not blend into the
   explanation above it. */
/* OL-P: the instruction card is DOCKED in a corner and stays there. It used to be positioned beside
   its anchor, which is fine for a tour you watch and wrong for one you DO: every click re-renders
   the surface underneath, the anchor's box collapses for a frame, and a card measured against it
   lands under the header or off the edge — the instruction vanishing at the exact moment it is
   needed. It moves only to get out of the ring's way, never to chase it. */
.tut-card--docked { position:fixed; width:400px; max-width:calc(100vw - 32px);
  max-height:60vh; overflow-y:auto; box-shadow:0 10px 40px rgba(0,0,0,.55); }

.tut-card__do { margin-top:var(--sp-2); padding:var(--sp-2); border-radius:5px;
  background:var(--surface-2, rgba(255,255,255,.04)); border-left:3px solid var(--gold);
  color:var(--text); font-size:var(--fs-body); }

.tut-card__demo { margin-top:var(--sp-1); padding-top:var(--sp-1); border-top:1px solid var(--edge2);
  color:var(--gold); font-size:var(--fs-caption); font-style:italic; }

.learn-list { display:flex; flex-direction:column; gap:var(--sp-1); }
/* OL-L: the Learn index groups its tours — one heading per part of the game. The heading is a row
   in the same flex column, so a group with no tours simply is not rendered and leaves no gap. */
.learn-group { display:flex; flex-wrap:wrap; align-items:baseline; gap:var(--sp-1);
  margin:var(--sp-2) 0 calc(var(--sp-1) * -1); padding-top:var(--sp-1);
  border-top:1px solid var(--edge2); }
.learn-group:first-child { margin-top:0; padding-top:0; border-top:0; }
.learn-group__label { font-family:var(--font-atmos); color:var(--gold); font-size:var(--fs-h3); }
.learn-group__blurb { color:var(--text-faint); font-size:var(--fs-caption); }
.learn-row {
  display:flex; align-items:center; gap:var(--sp-2); flex-wrap:wrap;
  padding:var(--sp-2); background:var(--surface); border:1px solid var(--edge-c); border-radius:var(--radius-sm);
}
.learn-row__icon { font-size:var(--fs-h3); }
.learn-row__label { font-family:var(--font-atmos); color:var(--accent); min-width:9rem; }
.learn-row__blurb { flex:1 1 16rem; color:var(--text-dim); font-size:var(--fs-body); }
.learn-row__state { color:var(--text-dim); font-size:var(--fs-caption); }
.learn-row__state[data-learn-state="done"] { color:var(--s-hp); }
/* Shown when a tour has nothing to point at yet ("make a character first"), so a Start that cannot
 * run explains itself rather than looking broken. */
.learn-row__note { color:var(--gold); font-size:var(--fs-caption); }
/* OL-V: the switch that takes the tour offer off the home page. A label wrapping its own checkbox,
 * so the words are part of the hit area rather than a target the size of a tick box. */
.learn-home { display:flex; align-items:center; gap:var(--sp-1); margin-top:var(--sp-2);
  color:var(--text-dim); font-size:var(--fs-caption); cursor:pointer; }

/* ── ENH-6 THE TAROKKA READING — five results, each revealed to the party on its own. ─────────── */
.camp-tarokka { display:flex; flex-direction:column; gap:2px; margin-top:var(--sp-2); }
.camp-tarokka__row {
  display:flex; flex-wrap:wrap; align-items:center; gap:var(--sp-1);
  padding:var(--sp-1); border:1px solid var(--edge-c); border-radius:var(--radius-sm); background:var(--inset);
  font-size:var(--fs-body);
}
.camp-tarokka__row > * { min-width:0; }            /* nothing may push the 280px tree column wider */
.camp-tarokka__slot { font-family:var(--font-atmos); color:var(--accent); flex:1 1 100%; }
.camp-tarokka__card { color:var(--gold); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.camp-tarokka__target { color:var(--text); flex:1 1 100%; overflow:hidden; text-overflow:ellipsis; }

/* ── C-2 (ENH-2) THE LOCATION MAP CARD — what this place looks like, and how to change it. ────── */
.camp-mapcard { margin-top:var(--sp-2); }
.camp-mapthumb {
  display:block; max-width:100%; max-height:180px; width:auto;
  border:1px solid var(--edge-c); border-radius:var(--radius-sm); background:var(--inset);
}

/* ── C-3 (ENH-3) THE DM STAT-BLOCK EDITOR + the edited/custom badges. The badge appears wherever a
 *    monster appears (Library rows, the detail head, the campaign NPC panel), so a DM can always
 *    tell at a glance whether they are looking at the published stats or their own. ───────────── */
.sb-editor {
  margin-top:var(--sp-2); padding:var(--sp-2);
  background:var(--surface); border:1px solid var(--gold); border-radius:var(--radius-sm);
  max-height:60vh; overflow:auto;
}
.sb-editor__head { display:flex; align-items:center; gap:var(--sp-1); margin-bottom:var(--sp-1); }
.sb-editor__title { font-family:var(--font-atmos); color:var(--accent); font-size:var(--fs-body-lg); }
.sb-editor__body { display:flex; flex-direction:column; gap:2px; }
.sb-editor__foot { display:flex; flex-wrap:wrap; gap:var(--sp-1); margin-top:var(--sp-2); padding-top:var(--sp-1); border-top:1px solid var(--edge-c); }
.sb-blocklist { display:flex; flex-direction:column; gap:2px; }
.sb-blockrow { align-items:flex-start; }
.sb-badge {
  display:inline-block; margin-left:6px; padding:0 6px; border-radius:999px;
  font-size:var(--fs-caption); font-family:var(--font-clarity); border:1px solid var(--edge-c); color:var(--text-dim);
}
.sb-badge--edited { border-color:var(--gold); color:var(--accent); }
.sb-badge--custom { border-color:var(--danger); color:var(--danger); }

/* ── B-14 (F-028) THEMED FORM CONTROLS — native checkboxes/radios rendered in the OS's default
 *    blue (Inventory "halve it", the fog brush toggle…) against a candle-lit theme. `accent-color`
 *    themes the real control, so its keyboard and assistive-tech behaviour is kept intact. ────── */
input[type=checkbox], input[type=radio] {
  accent-color:var(--blood-bright); width:16px; height:16px; cursor:pointer; vertical-align:middle;
}
input[type=checkbox]:focus-visible, input[type=radio]:focus-visible { outline:2px solid var(--arcane); outline-offset:2px; }

/* ── C-5 · ADVANCED DISCLOSURES — one obvious surface per action, depth folded (never deleted).
 *    Used by the sheet's "Advanced combat options" <details> and, in spirit, by the Initiative and
 *    Dice tabs' ⋯ Advanced buttons. The summary must read as a control, not as body text. ─────── */
details.adv-combat > summary {
  cursor:pointer; list-style:none; padding:var(--sp-1) 0;
  color:var(--accent); font-family:var(--font-atmos);
}
details.adv-combat > summary::-webkit-details-marker { display:none; }
details.adv-combat > summary::before { content:'▸ '; color:var(--gold); }
details.adv-combat[open] > summary::before { content:'▾ '; }
details.adv-combat > summary:hover { color:var(--gold-bright,#dcbc7e); }
details.adv-combat > summary:focus-visible { outline:2px solid var(--arcane); outline-offset:2px; }
details.adv-combat > summary h2 { margin:0; }

/* ── V10 COMMAND PALETTE — Ctrl+K over every named action (palette.mjs). The overlay's
 *    position:fixed lives HERE like the modal overlay (the U2 gate keeps it out of .mjs). A
 *    top-sheet card: filter input over a scrolling row list, each row carrying its kbd chip. ──── */
.palette-overlay {
  position:fixed; inset:0; z-index:var(--z-palette); display:flex;
  align-items:flex-start; justify-content:center; padding-top:12vh;
  background:var(--scrim); backdrop-filter:blur(6px);
}
.palette-card {
  width:min(560px,92vw); max-height:64vh; display:flex; flex-direction:column; gap:var(--sp-1);
  background:var(--surface); border:1px solid var(--edge-c); border-radius:var(--radius);
  box-shadow:var(--elev-4); padding:var(--sp-2);
}
.palette-input {
  width:100%; box-sizing:border-box; min-height:var(--hit-min);
  padding:var(--sp-1) var(--sp-2); font-family:var(--font-clarity); font-size:var(--fs-body-lg);
  color:var(--text); background:var(--inset); border:1px solid var(--edge-c); border-radius:var(--radius-sm);
}
.palette-list { overflow:auto; display:flex; flex-direction:column; gap:2px; }
.palette-row {
  display:flex; align-items:center; gap:var(--sp-2); width:100%; min-height:var(--hit-min);
  padding:var(--sp-1) var(--sp-2); background:transparent; border:1px solid transparent;
  border-radius:var(--radius-sm); color:var(--text); font-family:var(--font-clarity);
  font-size:var(--fs-body); cursor:pointer; text-align:left;
}
.palette-row:hover { border-color:var(--gold); color:var(--accent); }
.palette-row--active { border-color:var(--gold); background:var(--ember-wash); color:var(--accent); }
.palette-row__label { flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.palette-keys {
  flex:0 0 auto; font-family:var(--font-clarity); font-size:var(--fs-caption); color:var(--text-dim);
  background:var(--inset); border:1px solid var(--edge-c); border-radius:var(--radius-sm); padding:1px 6px;
}
.palette-empty { color:var(--text-dim); font-size:var(--fs-body); padding:var(--sp-2); }
.palette-input:focus, .list-filter:focus { outline:none; border-color:var(--gold); box-shadow:var(--focus-ring); }

/* ── V10 TYPE-TO-FILTER — the shared filter-input look (campaign tree · roster · encounter
 *    builder; the Library's own search keeps its ds-input). ───────────────────────────────────── */
.list-filter {
  width:100%; box-sizing:border-box; min-height:var(--hit-min);
  padding:var(--sp-1) var(--sp-2); font-family:var(--font-clarity); font-size:var(--fs-body);
  color:var(--text); background:var(--inset); border:1px solid var(--edge-c); border-radius:var(--radius-sm);
}
/* The campaign tree column: filter row over the V9 tree (same 280px slot in .camp-main). */
.camp-treecol { flex:0 0 280px; max-width:280px; display:flex; flex-direction:column; gap:var(--sp-1); min-height:0; }
/* `max-width:none` (V10) let the tree grow to its content and spill past the 280px column once the
 * Tarokka rows arrived. It fills the column and no more; long labels ellipsis inside it. */
.camp-treecol .camp-tree { flex:1 1 auto; max-width:100%; min-height:0; }

/* ── F5 (R-008) MOTION KIT — calm by default, expressive on meaningful events ─────────────────── */

/* Structural: panes and sheet views cross-fade/slide in with the standard easing. */
@keyframes pane-in { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:none; } }
.mode-pane.active { animation:pane-in var(--dur) var(--ease-standard); }

/* Toast enter/exit — the queue stacks; each card slides in from the left edge and fades away. */
@keyframes toast-in { from { opacity:0; transform:translateX(-12px); } to { opacity:1; transform:none; } }
.ds-toast--enter { animation:toast-in var(--dur) var(--ease-standard); }
.ds-toast--exit { opacity:0; transform:translateX(-8px); transition:opacity var(--dur-slow) var(--ease-standard), transform var(--dur-slow) var(--ease-standard); }

/* Event flourishes — a damage number pops red and rises, a heal pops green, a landed hit pulses
 * the target card, a level-up blooms ember. JS adds these classes at the moment of the event. */
@keyframes fx-float { 0% { opacity:0; transform:translateY(4px) scale(.9); } 15% { opacity:1; transform:translateY(0) scale(1.08); } 30% { transform:scale(1); } 100% { opacity:0; transform:translateY(-26px); } }
.fx-number {
  position:fixed;   /* viewport-anchored float; lives HERE per the U2 gate */ z-index:var(--z-hud); pointer-events:none; font-family:var(--font-clarity);
  font-weight:700; font-size:var(--fs-h3); text-shadow:var(--shadow-text);
  animation:fx-float 1.1s var(--ease-standard) forwards;
}
.fx-number--dmg { color:var(--danger); }
.fx-number--heal { color:var(--hp); }
@keyframes fx-pulse { 0% { box-shadow:0 0 0 0 rgba(240,101,94,.5); } 100% { box-shadow:0 0 0 14px rgba(240,101,94,0); } }
.fx-hit-pulse { animation:fx-pulse .5s var(--ease-standard); }
@keyframes fx-bloom { 0% { box-shadow:0 0 0 0 rgba(232,177,90,.55); } 100% { box-shadow:0 0 0 26px rgba(232,177,90,0); } }
.fx-level-bloom { animation:fx-bloom .9s var(--ease-standard); }
@keyframes fx-settle { 0% { transform:rotate(-8deg) scale(.92); } 55% { transform:rotate(5deg) scale(1.05); } 100% { transform:none; } }
.fx-die-settle { animation:fx-settle .45s var(--ease-standard); }

/* M-136 (R-008): reduced motion is a hard off-switch for EVERY non-essential animation and
 * transition — the app stays fully usable, just still. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration:.01ms !important; animation-iteration-count:1 !important;
    transition-duration:.01ms !important; scroll-behavior:auto !important;
  }
  .ds-skeleton { animation:none; background:var(--surface-2); }
}

/* D-017: the only control in the app under the design system's own --hit-min (24px). A checkbox
 * that small is a real miss target, especially mid-fight. */
input[type=checkbox], input[type=radio] { width:var(--hit-min); height:var(--hit-min); accent-color:var(--ember); }

/* D-012: a sheet tab that cannot open yet (empty roster) says so instead of looking live and
 * silently bouncing back to Roster. */
.tabbtn--off { opacity:.45; }

/* ── R-C/R-D — the player's own half ───────────────────────────────────────────────────────────
   Two panels that belong to the PERSON, not the table: their characters and what they do between
   sessions. They read like a list rather than a control panel, because that is what they are. */
.ph-panel {
  width:100%; max-width:100%; box-sizing:border-box;
  background:var(--surface); border:1px solid var(--edge-c, var(--edge2)); border-radius:var(--radius-sm);
  padding:var(--sp-2); margin-top:var(--sp-1);
}
.ph-title { font-family:var(--font-atmos); color:var(--ember-bright); margin-bottom:var(--sp-1); }
.ph-title--sub { font-size:var(--fs-body); margin-top:var(--sp-2); }
.ph-row { border-top:1px solid var(--edge-c, var(--edge2)); padding:var(--sp-1) 0; }
.ph-row:first-child { border-top:none; }
.ph-row__name { color:var(--text); margin-bottom:2px; }
.ph-camp { display:flex; gap:6px; flex-wrap:wrap; align-items:center; font-size:var(--fs-body); padding:2px 0; }
.ph-camp__title { color:var(--ember); }
.ph-stats { color:var(--text-faint); font-size:var(--fs-body); margin-bottom:var(--sp-1); }
.ph-actions { display:flex; gap:6px; flex-wrap:wrap; align-items:center; margin-top:6px; }
/* The DM's-to-give row is deliberately set apart — it is offered, and it is not the same thing. */
.ph-actions--override { border-top:1px dashed var(--edge-c, var(--edge2)); margin-top:var(--sp-2); padding-top:var(--sp-1); }
.ph-input { min-width:140px; }
.ph-input--num { width:72px; min-width:0; }
.ph-msg { margin-top:var(--sp-1); font-size:var(--fs-body); color:var(--ember-bright); }
.ph-entry { font-size:var(--fs-body); color:var(--text-faint); padding:1px 0; }
/* A flagged line has to look different at a glance, on both screens, or the promise is hollow. */
.ph-entry--flagged { color:var(--ember-bright); }
.table-stage__prep { border-left:3px solid var(--ember, #E8B15A); }
.table-row--flagged { color:var(--ember-bright); }
