/* ── Reset & base ─────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* ── Custom scrollbars ───────────────────────────────────────────────────── */
::-webkit-scrollbar              { width: 6px; height: 6px; }
::-webkit-scrollbar-track        { background: transparent; }
::-webkit-scrollbar-thumb        { background: var(--color-text-tertiary); border-radius: var(--radius-full); }
::-webkit-scrollbar-thumb:hover, ::-webkit-scrollbar-thumb:focus-visible  { background: var(--color-text-secondary); }
/* Firefox */
*                                { scrollbar-width: thin; scrollbar-color: var(--color-text-tertiary) transparent; }

/* ── Threadfall Design Tokens ────────────────────────────────────────────── */
:root {
  /* ── Light palette ──
     v1140: aligned to spec brand doc tokens verbatim (lines 14-22 of the
     spec source). The previous Tailwind-style cool slate palette
     (#F8FAFC / #F1F5F9 / #0F172A) was the app's pre-spec defaults; spec
     calls for warm parchment + ink. This swap cascades through every
     surface that references --bg / --bg-2 / --text-* / --border via the
     legacy aliases below.
     v800's "tertiary collapses with secondary" decision is reverted —
     spec specifies a real 4-step ink ladder; the hierarchy returns. */
  --color-bg:               #F5F2EA;   /* spec --paper        — warm parchment */
  --color-surface:          #EAE5D9;   /* spec --paper-2                       */
  --color-surface-elevated: #D9D2BF;   /* spec --paper-edge                    */
  --color-border:           #DCD5C2;   /* spec --rule         — warm parchment-edge */
  --color-text-primary:     #15191F;   /* spec --ink                           */
  --color-text-secondary:   #3A4049;   /* spec --ink-2                         */
  --color-text-tertiary:    #6A7079;   /* spec --ink-3                         */

  /* ── Spec extra tokens (light) — v1140 ──
     Added so spec recipes that reference --ink-4 / --rule-2 / --paper-edge
     resolve without falling back to currentColor or defaults. */
  --paper-edge:    #D9D2BF;   /* spec line 16 — heavier parchment-edge        */
  --ink-4:         #A0A4AB;   /* spec line 20 — quaternary ink (faintest)     */
  --rule-2:        #2A2F37;   /* spec line 22 — heavier rule for emphasis     */
  --ember-dim:     #8B3320;   /* spec line 50 — destructive text on tint bg   */

  /* ── Brand & semantic ── */
  --color-brand:   #2F7C85;
  /* v1072 (brand Phase 2): status palette pulled to the brand-doc values.
     Each is softened a half-step from a bright-SaaS default toward the
     paper-aesthetic ladder the spec calls for. The app's --color-ember +
     --ember-base ladder already matched spec's amber (#C26A2E) — those
     stay; what changes is danger / success / gold. */
  --color-success: #5A8345;       /* moss (was #16A34A bright green)        */
  --color-danger:  #B8442A;       /* ember-red destructive (was #DC2626)    */
  --color-warning: #C26A2E;       /* amber, unchanged — same as ember-base  */
  --color-info:    #2563EB;
  --color-purple:  #7C3AED;
  --color-gold:    #A88A47;       /* muted gold (was #CA8A04 saturated)     */
  --color-ember:   #C26A2E;       /* EMBER_BASE — dim firelight, weathered copper */
  --color-copper:  #C2763D;       /* Artifact rarity — distinct from ember (Item) */
  --color-muted-violet: #7C6FA0;  /* NPC / faction accent — softer than purple  */
  --color-white:        #FFFFFF;
  --color-discord:      #5865F2;
  --color-discord-hover:#4752C4;

  /* ── v1071 brand scale (from "Threadfall Logo & Brand" spec) ─────────────
     Additive layer. Existing --color-brand / --color-danger / etc. stay as
     the cascade source-of-truth across the app — this scale is for the new
     lockup, the SVG mark's --shear-fill, and any future surface that wants
     a fuller step ladder than the one-shot --color-* tokens give you. */
  --teal-1: #0F2E33;    /* shadow                          */
  --teal-2: #1C4D54;
  --teal-3: #2F7C85;    /* === --color-brand (light)        */
  --teal-4: #4A98A1;
  --teal-5: #7BBAC1;
  --teal-6: #C2DEE1;
  --teal-7: #E8F2F3;
  --slate-1: #15191F;
  --slate-2: #2D3744;
  --slate-3: #4D5868;
  --slate-4: #6B7888;
  --slate-5: #95A0AE;
  --slate-6: #C5CCD4;
  --slate-7: #E7EAEE;
  --snow-teal:  #D9EAEC;
  --snow-slate: #DCE0E5;
  --seam:       #F2EFE6;
  /* Light mode flips the mark's shear polygon to teal-2 ("dark thread"
     against pale paper). Dark mode overrides this to the inline snowy
     white #E8EEEC further down — keeps the "thread" metaphor reading
     in both directions. */
  --shear-fill: var(--teal-2);

  /* Display type (Source Serif 4) — used by .tf-wordmark / .tf-lockup for
     the Thread|fall brand lockup AND .modal-hdr h2 / .tf-card-title for
     spec'd display moments. v1086: added --type-ui (UI sans, current Inter
     stack) + --type-mono (monospace for meta/numeric chips) so the spec's
     .tf-card / .cc-card recipes resolve without per-rule font fallbacks. */
  --type-display: "Source Serif 4", Georgia, serif;
  --type-ui: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --type-mono: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;

  /* ── Typography scale ── */
  --font-size-xs:   0.68rem;   /* badges, chips, tiny labels       */
  --font-size-sm:   0.75rem;   /* secondary body, small buttons    */
  --font-size-base: 0.875rem;  /* default body / inputs            */
  --font-size-md:   0.9rem;    /* card body, log entries           */
  --font-size-lg:   1rem;      /* prominent labels, titles         */
  --font-size-xl:   1.1rem;    /* modal headers                    */
  --font-size-2xl:  1.5rem;    /* page-level headings (setup/login)*/
  --font-size-pc-name: 20px;   /* live character-sheet PC name only */
  --font-size-stat:    16px;   /* stat / ability-score numerals     */
  --font-size-2xs:     9.5px;  /* micro-labels: badges, ability/stat labels, tags, heat scores */

  --font-weight-normal:    400;
  --font-weight-medium:    500;
  --font-weight-semibold:  600;
  --font-weight-bold:      700;
  --font-weight-extrabold: 800;

  /* ── Spacing scale (base 4px / 0.25rem) ── */
  --header-height: 52px;

  --space-1:  0.25rem;   /*  4px — icon gaps, micro nudges      */
  --space-2:  0.5rem;    /*  8px — tight inline spacing         */
  --space-3:  0.75rem;   /* 12px — list items, form rows        */
  --space-4:  1rem;      /* 16px — card padding, section gaps   */
  --space-5:  1.25rem;   /* 20px — entity form padding          */
  --space-6:  1.5rem;    /* 24px — large card padding           */
  --space-8:  2rem;      /* 32px — modal / setup padding        */
  --space-10: 2.5rem;    /* 40px — setup card max padding       */

  /* ── Layout: chrome heights ──
     The two top bars stack on top of every .tab-panel. Hard-coding
     52/41 in `calc(100vh - var(--chrome-height))` was fragile — bumping either
     bar's padding meant grepping every panel rule. Centralised here so
     every panel can reference the same source of truth. */
  --nav-height:           52px;   /* main nav (#tab-nav) */
  --tab-strip-height:     41px;   /* per-tab title + pill row */
  --chrome-height:        calc(var(--nav-height) + var(--tab-strip-height));

  /* ── Border radius scale ── */
  --radius-sm:   4px;    /* tight controls (close btn, session num) */
  --radius-md:   6px;    /* inputs, small buttons                   */
  --radius-lg:   8px;    /* standard — cards, buttons, dropdowns    */
  --radius-xl:   10px;   /* session cards, panels                   */
  --radius-2xl:  12px;   /* modals, chat bubbles                    */
  --radius-3xl:  16px;   /* setup card, profile modal               */
  --radius-full: 999px;  /* pills, badges, avatars                  */

  /* ── Shadow / elevation scale ── */
  --shadow-sm: 0 1px 3px rgba(0,0,0,0.08);   /* subtle (status dots, toasts)     */
  --shadow-md: 0 4px 12px rgba(0,0,0,0.15);  /* hover states, floating buttons   */
  --shadow-lg: 0 8px 32px rgba(0,0,0,0.20);  /* modals, popovers                 */
  --shadow-xl: 0 20px 60px rgba(0,0,0,0.25); /* large modals (profile)           */

  /* ── Legacy aliases (keep existing code working) ── */
  --bg:     var(--color-bg);
  --bg-2:   var(--color-surface);
  --bg-3:   var(--color-surface-elevated);
  --text:   var(--color-text-primary);
  --text-1: var(--color-text-primary);
  --text-2: var(--color-text-secondary);
  --text-3: var(--color-text-tertiary);
  --border: var(--color-border);
  --surface: var(--color-surface);
  --bg-1:    var(--color-bg);
  --accent:  var(--color-brand);
  --shadow:  var(--shadow-sm);

  /* ── Accent aliases ── */
  --accent-teal:   var(--color-brand);
  --accent-amber:  var(--color-warning);
  --accent-red:    var(--color-danger);
  --accent-purple: var(--color-purple);
  --accent-green:  var(--color-success);
  --accent-blue:   var(--color-info);
  --accent-gold:   var(--color-gold);
  --accent-ember:  var(--color-ember);

  /* ── Teal system (TEAL_BASE = #2F7C85 — mineral teal, Threadfall mountain tones) ── */
  --teal-base:      #2F7C85;
  --teal-strong:    #25656C;
  --teal-soft:      #4F9AA3;
  --teal-muted:     #6FAFB6;
  --teal-text:      #25656C;  /* darker for light mode text */
  --teal-bg:        rgba(47, 124, 133, 0.14);
  --teal-border:    rgba(47, 124, 133, 0.35);
  --teal-glow:      rgba(47, 124, 133, 0.22);
  --teal-muted-bg:  rgba(47, 124, 133, 0.08);

  /* ── Ember system (EMBER_BASE = #C26A2E — dim firelight / weathered copper) ── */
  --ember-base:    #C26A2E;
  --ember-soft:    #A55724;
  --ember-muted:   #7A3F1A;
  --ember-bg:      rgba(194, 106, 46, 0.12);
  --ember-border:  rgba(194, 106, 46, 0.35);
  --ember-glow:    rgba(194, 106, 46, 0.18);
  --ember-text-on: #F3D6C2;

  /* ── Split motif ── */
  --split-color:  color-mix(in srgb, #2F7C85 40%, transparent);
  --split-strong: color-mix(in srgb, #2F7C85 60%, transparent);

  /* ── Additional border ── */
  --color-border-strong: #94A3B8;

  /* ── Motion system ── */
  --ease:          cubic-bezier(0.4, 0, 0.2, 1);
  --duration-fast: 80ms;
  --duration-base: 140ms;
  --duration-slow: 200ms;
  /* legacy aliases */
  --t-hover: var(--duration-base);
  --t-press: var(--duration-fast);
  --t-trans: var(--duration-slow);

  color-scheme: light;
}

[data-theme="dark"] {
  /* ── Dark palette ──
     v1105: --color-bg #080C12 → #0B0F14 to match spec --paper (dark).
     v1140: full alignment to spec dark tokens (spec source lines 703-712).
     The previous dark tokens were a cobalt-blue interpolation (#0F1924
     /#1E3050) that drifted from spec's slate (#141A22 / #1F2630). Cards,
     panels, hover tints, and text hierarchy all cascade off these. */
  --color-bg:               #0B0F14;   /* spec --paper (dark)        */
  --color-surface:          #141A22;   /* spec --paper-2 (dark)      */
  --color-surface-elevated: #1E2530;   /* spec --paper-edge (dark)   */
  --color-border:           #1F2630;   /* spec --rule (dark)         */
  --color-border-strong:    #3A4049;   /* spec --rule-2 (dark)       */
  --color-text-primary:     #E8EBF0;   /* spec --ink (dark)          */
  --color-text-secondary:   #B5BBC5;   /* spec --ink-2 (dark)        */
  --color-text-tertiary:    #818993;   /* spec --ink-3 (dark)        */

  /* ── Spec extra tokens (dark) — v1140 ── */
  --paper-edge:    #1E2530;   /* spec line 706 */
  --ink-4:         #5A6068;   /* spec line 710 */
  --rule-2:        #3A4049;   /* spec line 712 */
  --ember-dim:     #C04A2D;   /* spec line 716 */

  /* ── Brand & semantic (brighter for dark backgrounds) ── */
  --color-brand:   #4F9AA3;
  /* v1071: brand scale dark overrides. Most teal/slate steps stay (the
     scale is a fixed ladder), but the BRAND step (teal-3) shifts brighter
     to carry on a night ground — matches --color-brand's dark shift.
     --shear-fill flips to snowy white so the mark's "thread" reads light
     against the dark mountain (mirror of light mode's teal-2 on paper). */
  --teal-3:     #4F9AA3;
  --shear-fill: #E8EEEC;
  /* v1072 (brand Phase 2): dark-mode status equivalents pulled to the spec's
     night ladder — each value is a luminance bump on its light counterpart
     so the softer paper-aesthetic colors stay readable on a #0B0F14 ground. */
  --color-success: #7AAE5F;       /* moss bumped for night (was #22C55E)    */
  --color-danger:  #E26242;       /* ember-red bumped (was #EF4444)         */
  --color-warning: #D88240;       /* amber bumped (was #D4753A)             */
  --color-info:    #3B82F6;
  --color-purple:  #8B5CF6;
  --color-gold:    #C9A867;       /* muted gold bumped (was #FBBF24)        */
  --color-ember:   #D4753A;       /* EMBER_BASE brightened for dark background legibility */
  --color-copper:  #E8A77A;       /* Artifact rarity (lighter on dark to read clearly) */
  --color-muted-violet: #9B8EC4;  /* NPC / faction accent — softer on dark     */

  /* ── Teal system — mineral teal scaled for dark backgrounds ── */
  --teal-base:      #2F7C85;
  --teal-strong:    #2F7C85;
  --teal-soft:      #4F9AA3;
  --teal-muted:     #6FAFB6;
  --teal-text:      #6FAFB6;  /* lighter for dark mode text */
  --teal-bg:        rgba(47, 124, 133, 0.14);
  --teal-border:    rgba(47, 124, 133, 0.35);
  --teal-glow:      rgba(47, 124, 133, 0.22);
  --teal-muted-bg:  rgba(47, 124, 133, 0.08);

  /* ── Ember system (dark mode — brightened for legibility on dark bg) ── */
  --ember-base:    #D4753A;
  --ember-soft:    #B8612E;
  --ember-muted:   #8A4522;
  --ember-bg:      rgba(212, 117, 58, 0.12);
  --ember-border:  rgba(212, 117, 58, 0.35);
  --ember-glow:    rgba(212, 117, 58, 0.18);
  --ember-text-on: #F3D6C2;

  /* ── Split motif ── */
  --split-color:  color-mix(in srgb, #4F9AA3 35%, transparent);
  --split-strong: color-mix(in srgb, #4F9AA3 55%, transparent);

  /* ── Shadows are stronger on dark backgrounds ── */
  --shadow-sm: 0 1px 4px rgba(0,0,0,0.50);
  --shadow-md: 0 4px 12px rgba(0,0,0,0.35);
  --shadow-lg: 0 8px 32px rgba(0,0,0,0.45);
  --shadow-xl: 0 20px 60px rgba(0,0,0,0.55);

  /* ── Legacy aliases ── */
  --bg:     var(--color-bg);
  --bg-2:   var(--color-surface);
  --bg-3:   var(--color-surface-elevated);
  --text:   var(--color-text-primary);
  --text-1: var(--color-text-primary);
  --text-2: var(--color-text-secondary);
  --text-3: var(--color-text-tertiary);
  --border: var(--color-border);
  --surface: var(--color-surface);
  --bg-1:    var(--color-bg);
  --accent:  var(--color-brand);
  --shadow:  var(--shadow-sm);

  /* ── Accent aliases ── */
  --accent-teal:   var(--color-brand);
  --accent-amber:  var(--color-warning);
  --accent-red:    var(--color-danger);
  --accent-purple: var(--color-purple);
  --accent-green:  var(--color-success);
  --accent-blue:   var(--color-info);
  --accent-gold:   var(--color-gold);
  --accent-ember:  var(--color-ember);

  color-scheme: dark;
}

/* ── Brand motif: vertical seam / horizontal section divider ─────────────── */
.tf-split-divider {
  border: none;
  height: 1px;
  background: linear-gradient(
    to right,
    transparent 0%,
    var(--split-color) 20%,
    var(--split-strong) 50%,
    var(--split-color) 80%,
    transparent 100%
  );
  margin: var(--space-4) 0;
}

/* ── Site background image ───────────────────────────────────────────────────
   bg.png is fixed behind all content. Light mode uses a pale overlay so the
   texture shows through softly without clashing with light-surface panels.
   Dark mode overrides this further down with its own layers.
   Raise the rgba alpha toward 1.0 to mute more; lower toward 0.80 for more texture. */
body {
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
  /* v1105: wash recipe matches brand spec verbatim — wash opacity 0.78
     → 0.82 per spec value. Lets the parchment image read through more,
     gives the bg the textured depth the spec mockup shows. */
  background-image:
    radial-gradient(1200px 500px at 50% -10%,
      color-mix(in srgb, var(--teal-3) 9%, transparent) 0%,
      transparent 70%),
    radial-gradient(700px 400px at 100% 110%,
      color-mix(in srgb, var(--color-warning) 7%, transparent) 0%,
      transparent 60%),
    linear-gradient(rgba(245, 242, 234, 0.82), rgba(245, 242, 234, 0.82)),
    image-set(url('/static/bg.webp') type('image/webp'), url('/static/bg.png') type('image/png'));
  background-size: auto, auto, auto, cover;
  background-position: center, center, center, center;
  background-attachment: fixed, fixed, fixed, fixed;
  background-repeat: no-repeat;
  background-color: var(--bg); /* fallback if image fails to load */
  color: var(--text);
  min-height: 100vh;
  transition: background-color var(--t-trans) ease, color var(--t-trans) ease;
}

/* ── Header ──────────────────────────────────────────────────────────────── */
#app-header {
  /* Retired as a global ribbon (2026-05-13). Tier indication moved to a
     colored ring on the profile button so the top of the app isn't
     dedicated to a single-pill banner. Markup retained in case we revive
     it as a slim hidden default; display: none keeps it out of the layout
     entirely so nothing reserves space for it. */
  display: none;
}

#campaign-name {
  font-family: Georgia, 'Times New Roman', serif;
  font-size: var(--font-size-xl);
  font-weight: 600;
  color: var(--text);
}

#header-actions { display: flex; gap: 0.5rem; align-items: center; }

#theme-toggle {
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 4px 8px;
  cursor: pointer;
  color: var(--text-2);
  font-size: var(--font-size-lg);
  transition: background var(--t-hover);
}
#theme-toggle:hover, #theme-toggle:focus-visible { background: var(--bg-3); }

/* ── Screens ─────────────────────────────────────────────────────────────── */
.screen { min-height: calc(100vh - var(--nav-height)); }
.hidden { display: none !important; } /* must beat component display:flex/block rules */

/* ── Setup ───────────────────────────────────────────────────────────────── */
#setup-screen {
  display: flex;
  align-items: center;
  justify-content: center;
}

.setup-card {
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-2xl);
  padding: 2.5rem;
  width: 100%;
  max-width: 420px;
  box-shadow: var(--shadow);
}

.setup-card h1 {
  font-family: Georgia, serif;
  font-size: var(--font-size-2xl);
  margin-bottom: 0.5rem;
}

.setup-card p {
  color: var(--text-2);
  margin-bottom: 1.5rem;
  font-size: var(--font-size-md);
}

.setup-card form {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

/* ── Inputs & buttons ──────────────────────────────────────────────────────
   v1082: aligned to brand spec .tf-input — squared corners (radius 0),
   1.5px border, 14px / 11px 14px padding, italic placeholder, teal focus
   ring (border + 3px outer glow). The off-white background nudges inputs
   forward against the parchment page wash (spec uses #FCFAF4 in light;
   we map to a color-mix so it tracks the active theme). */
input[type="text"], input[type="date"], input[type="password"], input[type="email"], input[type="number"], input[type="search"], input[type="tel"], input[type="url"], textarea, select {
  width: 100%;
  padding: 11px 14px;
  background: color-mix(in srgb, var(--bg) 60%, white);
  border: 1.5px solid var(--border);
  border-radius: 0;
  color: var(--text-1);
  font-family: inherit;
  font-size: 14px;
  outline: none;
  transition:
    border-color var(--t-hover) var(--ease),
    background   var(--t-hover) var(--ease),
    box-shadow   var(--t-hover) var(--ease);
}
input[type="text"]::placeholder,
input[type="email"]::placeholder,
input[type="password"]::placeholder,
input[type="search"]::placeholder,
textarea::placeholder {
  color: var(--text-3);
  font-style: italic;
}
input[type="text"]:focus, input[type="date"]:focus, input[type="password"]:focus,
input[type="email"]:focus, input[type="number"]:focus, input[type="search"]:focus,
input[type="tel"]:focus, input[type="url"]:focus, textarea:focus, select:focus {
  border-color: var(--accent-teal);
  background: var(--bg);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-teal) 12%, transparent);
}
[data-theme="dark"] input[type="text"], [data-theme="dark"] input[type="date"],
[data-theme="dark"] input[type="password"], [data-theme="dark"] input[type="email"],
[data-theme="dark"] input[type="number"], [data-theme="dark"] input[type="search"],
[data-theme="dark"] input[type="tel"], [data-theme="dark"] input[type="url"],
[data-theme="dark"] textarea, [data-theme="dark"] select {
  background: color-mix(in srgb, var(--text-1) 4%, var(--bg));
}
[data-theme="dark"] input[type="text"]:focus, [data-theme="dark"] textarea:focus,
[data-theme="dark"] select:focus {
  background: var(--bg-2);
}

/* Legacy aliases — both `button[type="submit"]` (unclassed) and `.btn-primary`
   now compose the same ghost-pill recipe as `.btn-new`. Pre-v813 these rendered
   in legacy teal; user retired teal across the app. The dual-selector keeps
   submit-button specificity (button[type="submit"] = 0,0,1,1) above .btn-new.
   v1080 tightened to brand spec: 13px / 600 / 0.02em / 0.55rem 1rem / gap 0.45rem
   (was 12px / 700 / 0.04em / 0.5rem 1rem / gap 0.35rem). */
button[type="submit"]:not(.btn-new):not(.btn-danger), .btn-primary {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.55rem 1rem;
  font-family: inherit;
  font-size: 13px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.02em;
  color: var(--text-1);
  background: color-mix(in srgb, var(--text-1) 4%, transparent);
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  cursor: pointer;
  white-space: nowrap;
  transition:
    background   var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease),
    box-shadow   var(--t-hover) var(--ease),
    transform    var(--t-hover) var(--ease),
    color        var(--t-hover) var(--ease);
}
button[type="submit"]:not(.btn-new):not(.btn-danger):hover,
button[type="submit"]:not(.btn-new):not(.btn-danger):focus-visible,
.btn-primary:hover, .btn-primary:focus-visible {
  color: var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 8%, transparent);
  border-color: color-mix(in srgb, var(--accent-teal) 50%, transparent);
  box-shadow: 0 0 14px 2px color-mix(in srgb, var(--accent-teal) 18%, transparent);
  transform: translateY(-1px);
}
button[type="submit"]:not(.btn-new):not(.btn-danger):active,
.btn-primary:active {
  transform: translateY(0);
  transition-duration: var(--t-press);
}
/* v1082: dark-mode primary aligned to spec — hover is TEAL-tinted (not
   generic white rgba). Pre-v1082 dark hover used white box-shadow + bg,
   which broke the design language's "teal forward-motion verb" identity. */
[data-theme="dark"] button[type="submit"]:not(.btn-new):not(.btn-danger),
[data-theme="dark"] .btn-primary {
  color: var(--text-1);
  background: color-mix(in srgb, var(--text-1) 8%, transparent);
  border-color: rgba(255,255,255,0.18);
}
[data-theme="dark"] button[type="submit"]:not(.btn-new):not(.btn-danger):hover,
[data-theme="dark"] button[type="submit"]:not(.btn-new):not(.btn-danger):focus-visible,
[data-theme="dark"] .btn-primary:hover,
[data-theme="dark"] .btn-primary:focus-visible {
  color: #fff;
  background: color-mix(in srgb, var(--accent-teal) 20%, transparent);
  border-color: var(--accent-teal);
  box-shadow: 0 0 14px 2px color-mix(in srgb, var(--accent-teal) 30%, transparent);
}

/* Canonical "secondary action" recipe — used for every Cancel / Close / Skip /
   nav-arrow / passive button in the app. v813: tightened to a single style
   the user signed off on — transparent + squared + teal-tinted border + grey
   text at rest, text brightens to full white and border to full teal on hover.
   Single source of truth: do NOT add per-surface .btn-ghost overrides that
   re-tint background or border, or the design language drifts. */
.btn-ghost {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.55rem 1rem;
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--accent-teal) 30%, transparent);
  border-radius: 0;
  color: var(--text-3);
  font-family: inherit;
  font-size: 13px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.02em;
  white-space: nowrap;
  cursor: pointer;
  transition:
    color        var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease),
    background   var(--t-hover) var(--ease),
    transform    var(--t-hover) var(--ease);
}
.btn-ghost:hover, .btn-ghost:focus-visible {
  color: var(--text-1);
  border-color: var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 7%, transparent);
  transform: translateY(-1px);
}
.btn-ghost:active {
  transform: translateY(0);
  transition-duration: var(--t-press);
}
.btn-ghost:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: none;
}
/* v1082: dark-mode .btn-ghost aligned to spec [data-theme="dark"] .tf-btn--ghost —
   text in --text-2 at rest, full white on hover, full teal-3 border. */
[data-theme="dark"] .btn-ghost {
  color: var(--text-2);
  border-color: color-mix(in srgb, var(--accent-teal) 35%, transparent);
}
[data-theme="dark"] .btn-ghost:hover, [data-theme="dark"] .btn-ghost:focus-visible {
  color: #fff;
  background: color-mix(in srgb, var(--accent-teal) 10%, transparent);
  border-color: var(--accent-teal);
}
/* `.btn-ghost--danger` recipe lives in the canonical destructive-action
   block (~line 487) — composed with .btn-ghost so a Cancel/Delete pair in
   a modal share silhouette and only differ by color. */

/* Small inline action button — replaces style="font-size:0.78rem;padding:2px 8px"
   v1080: tightened to spec .tf-btn--sm (5px 12px / 12px). */
.btn-sm {
  padding: 5px 12px;
  font-size: 12px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.02em;
  gap: 0.4rem;
  border-radius: 0;
}
/* Extra-small — tight inline micro-actions (party card, roster, admin table).
   v1080: tightened to spec .tf-btn--xs (3px 9px / 11px / gap 0.3rem). */
.btn-xs {
  padding: 3px 9px;
  gap: 0.3rem;
  font-size: 11px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.02em;
  border-radius: 0;
}
/* v1080 cascade fix — when .btn-sm / .btn-xs compose with a base chassis
   (.btn-new / .btn-ghost / .btn-danger), source-order would otherwise let
   the chassis override the size modifier's padding / font-size / gap (e.g.
   "Restore" .btn-new.btn-xs rendered as a full .btn-new pill, not xs).
   Compound selectors below give the size modifier higher specificity (0,0,2,0
   vs the chassis's 0,0,1,0). border-radius is intentionally NOT in this rule
   so the pill chassis (.btn-new) keeps its pill silhouette in xs/sm form. */
.btn-new.btn-sm,
.btn-ghost.btn-sm,
.btn-danger.btn-sm,
.btn-new--danger.btn-sm,
.btn-ghost--danger.btn-sm {
  padding: 5px 12px;
  font-size: 12px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.02em;
  gap: 0.4rem;
}
.btn-new.btn-xs,
.btn-ghost.btn-xs,
.btn-danger.btn-xs,
.btn-new--danger.btn-xs,
.btn-ghost--danger.btn-xs {
  padding: 3px 9px;
  gap: 0.3rem;
  font-size: 11px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.02em;
}
/* Compact — modal-row inline actions (Copy / Save / Choose-image button clusters
   inside settings dialogs). Bigger than .btn-sm, smaller than default .btn-primary. */
.btn-compact {
  padding: 0.55rem 0.9rem;
  font-size: var(--font-size-sm);
  white-space: nowrap;
}
/* Canonical "destructive action" recipe — used for every Delete / Remove /
   warning button across the app. v813: legacy `.btn-danger` (filled red on
   hover), `.btn-ghost--danger`, and pill `.btn-new--danger` collapsed onto
   one squared red-ghost silhouette so deletes look identical everywhere.
   Mirrors `.btn-ghost`'s shape so a Cancel / Delete pair in the same modal
   reads as a unit, distinguished only by color. */
.btn-danger,
.btn-new.btn-new--danger,
.btn-ghost.btn-ghost--danger {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.55rem 1rem;
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--accent-red) 30%, transparent);
  border-radius: 0;
  color: color-mix(in srgb, var(--accent-red) 80%, var(--text-3));
  font-family: inherit;
  font-size: 13px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.02em;
  cursor: pointer;
  white-space: nowrap;
  transition:
    color        var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease),
    background   var(--t-hover) var(--ease),
    transform    var(--t-hover) var(--ease);
}
.btn-danger:hover, .btn-danger:focus-visible,
.btn-new.btn-new--danger:hover, .btn-new.btn-new--danger:focus-visible,
.btn-ghost.btn-ghost--danger:hover, .btn-ghost.btn-ghost--danger:focus-visible {
  /* v1083: text dims to --ember-soft (= spec's --ember-dim), border goes
     full --accent-red (= spec's --ember). Two-tone hover gives the
     destructive verb its visual hierarchy. */
  color: var(--ember-soft);
  border-color: var(--accent-red);
  background: color-mix(in srgb, var(--accent-red) 7%, transparent);
  box-shadow: none;
  transform: translateY(-1px);
}
.btn-danger:active,
.btn-new.btn-new--danger:active,
.btn-ghost.btn-ghost--danger:active {
  transform: translateY(0);
  transition-duration: var(--t-press);
}
.btn-danger:disabled,
.btn-new.btn-new--danger:disabled,
.btn-ghost.btn-ghost--danger:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: none;
}
/* v1082: dark-mode danger aligned to spec [data-theme="dark"] .tf-btn--danger —
   color color-mix(ember 80%, text-2), border ember 40%; hover bumps color to
   ember-tint, bg ember 14%, border full ember. */
[data-theme="dark"] .btn-danger,
[data-theme="dark"] .btn-new.btn-new--danger,
[data-theme="dark"] .btn-ghost.btn-ghost--danger {
  color: color-mix(in srgb, var(--accent-red) 80%, var(--text-2));
  border-color: color-mix(in srgb, var(--accent-red) 40%, transparent);
}
[data-theme="dark"] .btn-danger:hover, [data-theme="dark"] .btn-danger:focus-visible,
[data-theme="dark"] .btn-new.btn-new--danger:hover, [data-theme="dark"] .btn-new.btn-new--danger:focus-visible,
[data-theme="dark"] .btn-ghost.btn-ghost--danger:hover, [data-theme="dark"] .btn-ghost.btn-ghost--danger:focus-visible {
  color: #FBC3B0;
  background: color-mix(in srgb, var(--accent-red) 14%, transparent);
  border-color: var(--accent-red);
}

/* Campaign danger zone */
.camp-danger-zone {
  margin-top: var(--space-6);
  padding-top: var(--space-5);
  border-top: 1px solid var(--border);
  display: flex;
  justify-content: flex-end;
}
.camp-delete-warning {
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
  margin-bottom: var(--space-4);
  line-height: 1.5;
}

/* ── Canonical .tf-badge family (v1084) ──────────────────────────────────
   Brand-spec badge recipe. Compose `.tf-badge` + one variant (--teal, --moss,
   --amber, --ember, --gold, --ink, --ghost) for any pill-shaped status chip.
   Status-pill variants (--live, --broadcast, --ended) layer on top. Existing
   app badge classes (.camp-role-*, .entity-badge, .profile-role-badge, etc)
   are being retrofitted to match this recipe; new surfaces should compose
   .tf-badge directly. */
.tf-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  font-family: inherit;
  font-size: 10.5px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border-radius: var(--radius-full);
  border: 1px solid transparent;
  line-height: 1.5;
  white-space: nowrap;
}
.tf-badge--teal  { color: var(--accent-teal);   background: color-mix(in srgb, var(--accent-teal) 12%, var(--bg));  border-color: color-mix(in srgb, var(--accent-teal) 30%, transparent); }
.tf-badge--moss  { color: var(--moss-text-on, #355024);  background: color-mix(in srgb, var(--accent-green) 12%, var(--bg)); border-color: color-mix(in srgb, var(--accent-green) 30%, transparent); }
.tf-badge--amber { color: var(--amber-text-on, #7C4117); background: color-mix(in srgb, var(--accent-amber) 12%, var(--bg)); border-color: color-mix(in srgb, var(--accent-amber) 30%, transparent); }
.tf-badge--ember { color: var(--ember-soft);    background: var(--ember-bg);   border-color: var(--ember-border); }
.tf-badge--gold  { color: var(--gold-text-on, #5C4716);  background: color-mix(in srgb, var(--gold, #B7912E) 16%, var(--bg)); border-color: color-mix(in srgb, var(--gold, #B7912E) 35%, transparent); }
.tf-badge--ink   { color: var(--bg);            background: var(--text-1);     border-color: var(--text-1); }
.tf-badge--ghost { color: var(--text-3);        background: transparent;       border-color: var(--border); }
.tf-badge .tf-dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}

/* Dark-theme — brighter text against the dark surface (spec lines 889-895) */
[data-theme="dark"] .tf-badge--teal  { color: #BFE7EA; background: color-mix(in srgb, var(--accent-teal) 22%, var(--bg-2)); border-color: color-mix(in srgb, var(--accent-teal) 45%, transparent); }
[data-theme="dark"] .tf-badge--moss  { color: #B5DAA1; background: color-mix(in srgb, var(--accent-green) 18%, var(--bg-2));  border-color: color-mix(in srgb, var(--accent-green) 45%, transparent); }
[data-theme="dark"] .tf-badge--amber { color: #F1B97A; background: color-mix(in srgb, var(--accent-amber) 18%, var(--bg-2));  border-color: color-mix(in srgb, var(--accent-amber) 45%, transparent); }
[data-theme="dark"] .tf-badge--ember { color: #F5C0AE; background: color-mix(in srgb, var(--accent-red) 18%, var(--bg-2));    border-color: color-mix(in srgb, var(--accent-red) 45%, transparent); }
[data-theme="dark"] .tf-badge--gold  { color: #E5C97D; background: color-mix(in srgb, var(--gold, #D4B260) 18%, var(--bg-2)); border-color: color-mix(in srgb, var(--gold, #D4B260) 45%, transparent); }
[data-theme="dark"] .tf-badge--ink   { color: var(--bg); background: var(--text-1); border-color: var(--text-1); }
[data-theme="dark"] .tf-badge--ghost { color: var(--text-3); background: transparent; border-color: var(--border); }

/* Status-pill variants — filled saturated variants for unmistakable
   live/broadcast/ended states (spec lines 1261-1304). */
.tf-badge--live {
  color: #FFF6F1;
  background: var(--accent-red);
  border-color: var(--accent-red);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent-red) 50%, transparent);
  animation: tf-live-halo 2s ease-out infinite;
}
.tf-badge--live .tf-dot {
  width: 7px; height: 7px;
  background: #FFF6F1;
  box-shadow: 0 0 0 2px color-mix(in srgb, #fff 35%, transparent);
  animation: tf-dot-blink 1.2s ease-in-out infinite;
}
@keyframes tf-live-halo {
  0%   { box-shadow: 0 0 0 0   color-mix(in srgb, var(--accent-red) 55%, transparent); }
  70%  { box-shadow: 0 0 0 8px color-mix(in srgb, var(--accent-red) 0%,  transparent); }
  100% { box-shadow: 0 0 0 0   color-mix(in srgb, var(--accent-red) 0%,  transparent); }
}
@keyframes tf-dot-blink {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.55; }
}
.tf-badge--broadcast {
  color: #F4FAEF;
  background: var(--accent-green);
  border-color: var(--accent-green);
}
.tf-badge--broadcast .tf-dot {
  background: #F4FAEF;
  box-shadow: 0 0 0 2px color-mix(in srgb, #fff 30%, transparent);
  animation: tf-dot-blink 1.6s ease-in-out infinite;
}
.tf-badge--ended {
  color: var(--text-3);
  background: color-mix(in srgb, var(--text-1) 5%, var(--bg));
  border-color: var(--border);
}
.tf-live-timer {
  display: inline-flex;
  align-items: center;
  padding: 3px 9px;
  font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
  font-size: 11px;
  font-weight: 500;
  color: var(--ember-soft);
  background: color-mix(in srgb, var(--accent-red) 8%, var(--bg));
  border: 1px solid color-mix(in srgb, var(--accent-red) 25%, transparent);
  border-radius: var(--radius-full);
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}
[data-theme="dark"] .tf-live-timer {
  color: #F5C0AE;
  background: color-mix(in srgb, var(--accent-red) 18%, var(--bg-2));
  border-color: color-mix(in srgb, var(--accent-red) 40%, transparent);
}

/* ── UNIFIED CARD CHASSIS — Section 11 (v1086) ───────────────────────────
   Brand-spec recipe for every list-row card across the app: session cards,
   party PC cards, codex entity rows, quest nav rows, fates cards, atlas
   manage rows. Size varies by context via --lg/md/sm/xs modifier; the
   chassis (border, hover, selected, watermark, sub-components) is shared.
   Campaign cards have their own .cc-card family (Section 10 below) because
   the signature 16:9 aspect + cover-image variant is not list-row-shaped. */
.tf-card {
  --watermark-size: 70px;
  --watermark-offset: 6px;
  position: relative;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: var(--card-pad, 14px 16px);
  color: var(--text-1);
  cursor: pointer;
  overflow: hidden;
  transition:
    border-color 180ms ease,
    background 180ms ease,
    box-shadow 180ms ease,
    transform 180ms ease;
  box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
.tf-card--watermarked {
  /* v1101: bumped opacity 0.16 → 0.22 (spec base) to make the sigil
     readable against the new --bg-2 card surface. */
  background-image: url("data:image/svg+xml,%3Csvg opacity='0.22' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='46' fill='none' stroke='%232F7C85' stroke-width='2.2'/%3E%3Ccircle cx='50' cy='50' r='34' fill='none' stroke='%232F7C85' stroke-width='1.4'/%3E%3Ccircle cx='50' cy='50' r='19' fill='none' stroke='%232F7C85' stroke-width='1.4'/%3E%3Cline x1='50' y1='4' x2='50' y2='96' stroke='%232F7C85' stroke-width='1.2'/%3E%3Cline x1='4' y1='50' x2='96' y2='50' stroke='%232F7C85' stroke-width='1.2'/%3E%3Cline x1='18' y1='18' x2='82' y2='82' stroke='%232F7C85' stroke-width='0.9'/%3E%3Cline x1='82' y1='18' x2='18' y2='82' stroke='%232F7C85' stroke-width='0.9'/%3E%3Cpath d='M50 16 L84 50 L50 84 L16 50 Z' fill='none' stroke='%232F7C85' stroke-width='1.2'/%3E%3C/svg%3E");
  background-size: var(--watermark-size) var(--watermark-size);
  background-position: right var(--watermark-offset) bottom var(--watermark-offset);
  background-repeat: no-repeat;
}
[data-theme="dark"] .tf-card--watermarked {
  /* v1101: bumped opacity 0.22 → 0.30 in dark so the teal sigil reads
     against the darkened parchment-overlay body. */
  background-image: url("data:image/svg+xml,%3Csvg opacity='0.30' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='46' fill='none' stroke='%232F7C85' stroke-width='2.2'/%3E%3Ccircle cx='50' cy='50' r='34' fill='none' stroke='%232F7C85' stroke-width='1.4'/%3E%3Ccircle cx='50' cy='50' r='19' fill='none' stroke='%232F7C85' stroke-width='1.4'/%3E%3Cline x1='50' y1='4' x2='50' y2='96' stroke='%232F7C85' stroke-width='1.2'/%3E%3Cline x1='4' y1='50' x2='96' y2='50' stroke='%232F7C85' stroke-width='1.2'/%3E%3Cline x1='18' y1='18' x2='82' y2='82' stroke='%232F7C85' stroke-width='0.9'/%3E%3Cline x1='82' y1='18' x2='18' y2='82' stroke='%232F7C85' stroke-width='0.9'/%3E%3Cpath d='M50 16 L84 50 L50 84 L16 50 Z' fill='none' stroke='%232F7C85' stroke-width='1.2'/%3E%3C/svg%3E");
}
.tf-card:hover, .tf-card.is-hover {
  border-color: color-mix(in srgb, var(--accent-teal) 45%, var(--border));
  box-shadow: 0 4px 12px rgba(0,0,0,0.10);
  transform: translateY(-1px);
}
.tf-card.is-selected {
  border-color: color-mix(in srgb, var(--accent-teal) 65%, var(--border));
  background-color: color-mix(in srgb, var(--accent-teal) 7%, var(--bg-2));
  background-image: none;
}
.tf-card--lg  { --card-pad: 16px 18px; --watermark-size: 80px; --watermark-offset: 8px; }
.tf-card--md  { --card-pad: 12px 14px; --watermark-size: 60px; }
.tf-card--sm  { --card-pad: 10px 12px; --watermark-size: 44px; --watermark-offset: 4px; border-radius: 8px; }
.tf-card--xs  { --card-pad: 8px 12px;  --watermark-size: 0;    border-radius: 6px; }
.tf-card.is-dead     { filter: grayscale(100%); opacity: 0.5; border-color: var(--text-3); }
.tf-card.is-departed { filter: grayscale(55%);  opacity: 0.7; border-color: var(--text-3); }
.tf-card.is-zero     { background: color-mix(in srgb, var(--text-1) 4%, var(--bg-2)); }

.tf-card-head {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-width: 0;
}
.tf-card-eyebrow {
  font-family: var(--type-ui);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--teal-2);
  background: color-mix(in srgb, var(--accent-teal) 13%, transparent);
  padding: 2px 7px;
  border-radius: 3px;
  white-space: nowrap;
  flex-shrink: 0;
  text-transform: uppercase;
}
[data-theme="dark"] .tf-card-eyebrow { color: #BFE7EA; background: color-mix(in srgb, var(--accent-teal) 22%, transparent); }
.tf-card-eyebrow--neutral { color: var(--text-3); background: color-mix(in srgb, var(--text-1) 6%, transparent); }
[data-theme="dark"] .tf-card-eyebrow--neutral { color: var(--text-3); background: rgba(255,255,255,0.08); }
/* v1141: card title preserves natural width — no flex grow, no shrink.
   The previous `flex: 1; ellipsis` allowed the title to compress all the
   way down to a single character + "…" while the snippet stayed at full
   width (e.g. "a… — nine rods unlock or lock Companion" on the
   Adamantium Rods Codex row). That's the wrong priority — the name is
   the primary identifier and should never truncate before the snippet.
   With flex:0 0 auto + white-space:normal + overflow-wrap:break-word
   the name now takes its natural width and word-wraps to multiple
   lines only for extremely long names; the snippet shrinks first and
   ellipsises when the row is tight. */
.tf-card-title {
  font-family: var(--type-display);
  font-weight: var(--font-weight-semibold);
  font-size: 16px;
  color: var(--text-1);
  flex: 0 0 auto;
  min-width: 0;
  max-width: 100%;
  margin: 0;
  white-space: normal;
  overflow-wrap: break-word;
  letter-spacing: -0.005em;
}
/* v1141: snippet fills the remaining space after the name, ellipsises
   first when the row is forced to truncate. Was previously default-flex
   (0 1 auto) — bumped to (1 1 auto) so the snippet actively expands to
   fill the available row width even when the name is short. */
.tf-card-sub {
  font-family: var(--type-display);
  font-style: italic;
  font-weight: 400;
  color: var(--text-3);
  margin-left: 6px;
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tf-card-meta {
  font-family: var(--type-mono);
  font-size: 10.5px;
  color: var(--text-3);
  white-space: nowrap;
  flex-shrink: 0;
}
.tf-card-rail {
  width: 4px;
  align-self: stretch;
  background: var(--rail-color, var(--accent-teal));
  border-radius: 2px;
  flex-shrink: 0;
}
.tf-card-thumb {
  --thumb-size: 36px;
  width: var(--thumb-size);
  height: var(--thumb-size);
  flex-shrink: 0;
  border-radius: 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--thumb-color, var(--accent-teal)) 14%, transparent);
  color: var(--thumb-color, var(--accent-teal));
  border: 1px solid color-mix(in srgb, var(--thumb-color, var(--accent-teal)) 28%, transparent);
  font-size: 17px;
  overflow: hidden;
  position: relative;
}
.tf-card-thumb > img,
.tf-card-thumb > svg {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
  display: block;
}
.tf-card-thumb--sm { --thumb-size: 28px; font-size: 14px; border-radius: 6px; }
.tf-card-thumb--lg { --thumb-size: 48px; font-size: 22px; border-radius: 10px; }
.tf-card-thumb-glyph {
  width: 60%;
  height: 60%;
  stroke: currentColor;
  stroke-width: 1.6;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}
[data-theme="dark"] .tf-card-thumb {
  background: color-mix(in srgb, var(--thumb-color, var(--accent-teal)) 22%, transparent);
  border-color: color-mix(in srgb, var(--thumb-color, var(--accent-teal)) 40%, transparent);
}

/* ── CAMPAIGN CARD — Section 10 (v1086) ────────────────────────────────
   The brand's signature surface. 16:9 aspect, watermark sigil, gold
   chevron in bottom-right, role badge corner, settings gear, live pill.
   Cover-image variant adds three background layers (watermark + gradient
   wash + cover) so the card text stays legible over any cover photo. */
.cc-card {
  --watermark: url("data:image/svg+xml,%3Csvg opacity='0.18' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='46' fill='none' stroke='%232F7C85' stroke-width='2.2'/%3E%3Ccircle cx='50' cy='50' r='34' fill='none' stroke='%232F7C85' stroke-width='1.4'/%3E%3Ccircle cx='50' cy='50' r='19' fill='none' stroke='%232F7C85' stroke-width='1.4'/%3E%3Cline x1='50' y1='4' x2='50' y2='96' stroke='%232F7C85' stroke-width='1.2'/%3E%3Cline x1='4' y1='50' x2='96' y2='50' stroke='%232F7C85' stroke-width='1.2'/%3E%3Cline x1='18' y1='18' x2='82' y2='82' stroke='%232F7C85' stroke-width='0.9'/%3E%3Cline x1='82' y1='18' x2='18' y2='82' stroke='%232F7C85' stroke-width='0.9'/%3E%3Cpath d='M50 16 L84 50 L50 84 L16 50 Z' fill='none' stroke='%232F7C85' stroke-width='1.2'/%3E%3C/svg%3E");
  position: relative;
  aspect-ratio: 16 / 9;
  background-color: var(--bg-2);
  background-image: var(--watermark);
  background-size: 70px 70px;
  background-position: right 6px bottom 6px;
  background-repeat: no-repeat;
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 11px 14px;
  color: var(--text-1);
  cursor: pointer;
  transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease;
  box-shadow: 0 2px 6px rgba(0,0,0,0.06);
  overflow: hidden;
}
.cc-card:hover {
  border-color: color-mix(in srgb, var(--accent-teal) 50%, var(--border));
  box-shadow: 0 6px 18px rgba(0,0,0,0.10);
  transform: translateY(-1px);
}
[data-theme="dark"] .cc-card {
  --watermark: url("data:image/svg+xml,%3Csvg opacity='0.22' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='46' fill='none' stroke='%232F7C85' stroke-width='2.2'/%3E%3Ccircle cx='50' cy='50' r='34' fill='none' stroke='%232F7C85' stroke-width='1.4'/%3E%3Ccircle cx='50' cy='50' r='19' fill='none' stroke='%232F7C85' stroke-width='1.4'/%3E%3Cline x1='50' y1='4' x2='50' y2='96' stroke='%232F7C85' stroke-width='1.2'/%3E%3Cline x1='4' y1='50' x2='96' y2='50' stroke='%232F7C85' stroke-width='1.2'/%3E%3Cline x1='18' y1='18' x2='82' y2='82' stroke='%232F7C85' stroke-width='0.9'/%3E%3Cline x1='82' y1='18' x2='18' y2='82' stroke='%232F7C85' stroke-width='0.9'/%3E%3Cpath d='M50 16 L84 50 L50 84 L16 50 Z' fill='none' stroke='%232F7C85' stroke-width='1.2'/%3E%3C/svg%3E");
}
[data-theme="dark"] .cc-card:hover {
  border-color: color-mix(in srgb, var(--accent-teal) 60%, var(--border));
  box-shadow: 0 6px 18px rgba(0,0,0,0.30);
}
.cc-card--cover {
  color: #F4F1E8;
  border-color: rgba(0,0,0,0.25);
  /* v1168: top stop deepened 0.55 → 0.78 with a longer hold (25%) so the
     card title reads against bright cover art (e.g. Krellanon's green
     meteor scene, Tamar Grove's orange flames). Mid-card dip lifted to
     0.20 so the cover image still breathes through; bottom band kept
     at 0.78 for chrome (meta + chevron + watermark) contrast. */
  background-image:
    var(--watermark),
    linear-gradient(180deg, rgba(11,15,20,0.78) 0%, rgba(11,15,20,0.55) 25%, rgba(11,15,20,0.20) 55%, rgba(11,15,20,0.78) 100%),
    var(--cover-img);
  background-size: 70px 70px, auto, cover;
  background-position: right 6px bottom 6px, center, center;
  background-repeat: no-repeat, no-repeat, no-repeat;
}
/* v1242: light-theme cover cards. The base overlay above is a cold blue-black
   (rgba(11,15,20,…)) authored for the dark theme — on the warm parchment light
   theme it reads as a heavy dark block that fights the page. Swap to a warm
   espresso tone and lift the top/bottom darkness so the cover art shows
   brighter and the card sits in-theme. The title + GM line + meta all carry a
   text-shadow halo (v1241), so the cream text stays legible on the lighter
   overlay. Only background-image is redefined — size / position / repeat are
   inherited from the base .cc-card--cover rule. */
html:not([data-theme="dark"]) .cc-card--cover {
  background-image:
    var(--watermark),
    linear-gradient(180deg, rgba(30,23,15,0.50) 0%, rgba(30,23,15,0.35) 25%, rgba(30,23,15,0.13) 55%, rgba(30,23,15,0.51) 100%),
    var(--cover-img);
}
.cc-role {
  position: absolute;
  top: 10px;
  right: 12px;
  font-family: var(--type-ui);
  font-size: 10px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 3px 8px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text-3);
  z-index: 1;
}
.cc-role--gm    { color: var(--accent-teal); background: color-mix(in srgb, var(--accent-teal) 12%, var(--bg)); border-color: color-mix(in srgb, var(--accent-teal) 35%, transparent); }
.cc-role--cogm  { color: var(--accent-teal); background: color-mix(in srgb, var(--accent-teal) 7%, var(--bg));  border-color: color-mix(in srgb, var(--accent-teal) 25%, transparent); }
.cc-role--player{ color: var(--text-3); }
.cc-role--admin { color: var(--bg); background: var(--text-1); border-color: var(--text-1); }
[data-theme="dark"] .cc-role { background: rgba(255,255,255,0.06); color: var(--text-2); border-color: rgba(255,255,255,0.12); }
[data-theme="dark"] .cc-role--gm   { color: #BFE7EA; background: color-mix(in srgb, var(--accent-teal) 22%, var(--bg-2)); border-color: color-mix(in srgb, var(--accent-teal) 45%, transparent); }
[data-theme="dark"] .cc-role--cogm { color: #BFE7EA; background: color-mix(in srgb, var(--accent-teal) 14%, var(--bg-2)); border-color: color-mix(in srgb, var(--accent-teal) 35%, transparent); }
.cc-card--cover .cc-role { background: rgba(11,15,20,0.55); border-color: rgba(255,255,255,0.20); color: #fff; }
.cc-gear {
  position: absolute;
  top: 40px;
  right: 12px;
  width: 24px;
  height: 24px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-3);
  border-radius: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  cursor: pointer;
  z-index: 1;
  transition: color 160ms ease, border-color 160ms ease, background 160ms ease;
}
.cc-gear:hover { color: var(--text-1); border-color: var(--accent-teal); background: color-mix(in srgb, var(--accent-teal) 6%, transparent); }
[data-theme="dark"] .cc-gear { color: var(--text-3); border-color: var(--border); }
[data-theme="dark"] .cc-gear:hover { color: #fff; border-color: var(--accent-teal); }
.cc-card--cover .cc-gear { background: rgba(11,15,20,0.55); color: #fff; border-color: rgba(255,255,255,0.20); }
.cc-live {
  position: absolute;
  bottom: 12px;
  left: 16px;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: 999px;
  background: rgba(11,15,20,0.78);
  border: 1px solid color-mix(in srgb, var(--accent-gold) 60%, transparent);
  color: #F0DDA8;
  font-family: var(--type-ui);
  font-size: 10px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.cc-live-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent-gold);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-gold) 24%, transparent);
  animation: tf-dot-blink 1.8s ease-in-out infinite;
}
.cc-info {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-right: 56px;
  padding-bottom: 32px;
  height: 100%;
  box-sizing: border-box;
}
.cc-title {
  font-family: var(--type-display);
  font-weight: var(--font-weight-semibold);
  font-size: 17px;
  letter-spacing: -0.005em;
  line-height: 1.2;
  margin: 0;
  color: inherit;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.cc-gm {
  font-family: var(--type-ui);
  font-size: 12px;
  color: var(--text-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cc-card--cover .cc-gm { color: rgba(244,241,232,0.78); }
/* v1170: tight text-shadow halo on cover-card titles only. Surgical fix
   for cards where the cover image carries its own embossed text or busy
   highlights (e.g. Krellanon's background "KRELLANON" lettering) — the
   gradient alone can't separate the title from competing image elements.
   Two stacked shadows: sharp 1px below for edge crispness + soft 6px
   glow for general lift on bright backgrounds. Title-only, so cards
   that already read fine don't get further darkened. */
.cc-card--cover .cc-title {
  text-shadow:
    0 1px 2px rgba(0,0,0,0.65),
    0 0 8px rgba(0,0,0,0.45);
}
.cc-card--cover .cc-gm {
  text-shadow: 0 1px 2px rgba(0,0,0,0.5);
}
/* v1208: "Next session" line under the GM name. Quiet teal by default
   (a calm forward-looking reminder, deliberately NOT the loud amber
   boxed strip the Ready-for-Recap badge uses — GM agent wanted them
   visually distinct). ≤48h out flips to bold amber text so an imminent
   game jumps out of the grid. The empty nudge is dim italic and GM-only. */
.cc-next {
  font-family: var(--type-ui);
  font-size: 12px;
  font-weight: var(--font-weight-semibold);
  color: var(--accent-teal);
  margin-top: 3px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cc-next--soon {
  color: var(--accent-amber);
  font-weight: var(--font-weight-bold);
}
.cc-next--empty {
  color: var(--text-3);
  font-weight: 400;
  font-style: italic;
}
.cc-card--cover .cc-next { color: #BFE7EA; text-shadow: 0 1px 2px rgba(0,0,0,0.5); }
.cc-card--cover .cc-next--soon { color: #FFD9A8; }
.cc-card--cover .cc-next--empty { color: rgba(244,241,232,0.6); }
.cc-pcs {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 4px;
  margin-top: 8px;
  width: 66%;
}
.cc-pc {
  font-family: var(--type-ui);
  font-size: 10.5px;
  color: var(--text-2);
  background: color-mix(in srgb, var(--text-1) 5%, transparent);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 1px 8px;
  line-height: 1.4;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-align: center;
}
.cc-pc--more { color: var(--text-3); background: transparent; font-weight: var(--font-weight-semibold); }
.cc-card--cover .cc-pc { color: #F4F1E8; background: rgba(255,255,255,0.10); border-color: rgba(255,255,255,0.18); }
[data-theme="dark"] .cc-pc { background: rgba(255,255,255,0.06); border-color: rgba(255,255,255,0.10); color: var(--text-2); }
[data-theme="dark"] .cc-pc--more { border-color: rgba(255,255,255,0.18); }
.cc-meta {
  font-family: var(--type-mono);
  font-size: 10.5px;
  color: var(--text-3);
  display: flex;
  flex-direction: column;
  gap: 1px;
  margin-top: 10px;
}
.cc-meta strong { color: var(--text-2); font-weight: var(--font-weight-semibold); }
.cc-card--cover .cc-meta {
  color: rgba(244,241,232,0.72);
  /* v1241: same halo the title carries (line ~1172) — lifts the sessions /
     last-session / last-active lines off bright cover art so they stay
     legible. Inherits down to the <strong> sessions count too. */
  text-shadow:
    0 1px 2px rgba(0,0,0,0.65),
    0 0 8px rgba(0,0,0,0.45);
}
.cc-card--cover .cc-meta strong { color: rgba(244,241,232,0.92); }
.cc-pending {
  margin-top: 8px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 9px;
  background: color-mix(in srgb, var(--accent-amber) 14%, var(--bg));
  border: 1px solid color-mix(in srgb, var(--accent-amber) 35%, transparent);
  border-radius: 4px;
  font-family: var(--type-ui);
  font-size: 11px;
  font-weight: 500;
  color: #7C4117;
  width: fit-content;
  max-width: 100%;
}
.cc-pending-dot { color: var(--accent-amber); font-size: 10px; line-height: 1; }
[data-theme="dark"] .cc-pending { color: #F1B97A; background: color-mix(in srgb, var(--accent-amber) 22%, var(--bg-2)); border-color: color-mix(in srgb, var(--accent-amber) 45%, transparent); }
.cc-card--cover .cc-pending { background: rgba(217,118,40,0.88); color: #fff; border-color: rgba(255,255,255,0.18); }
.cc-chevron {
  position: absolute;
  right: 6px;
  bottom: 6px;
  width: 70px;
  height: 70px;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 1;
  font-family: ui-sans-serif, system-ui, sans-serif;
  font-size: 3.6rem;
  line-height: 1;
  color: var(--accent-gold);
  opacity: 0.7;
  transform: translate(14px, -3px);
  transition: opacity 180ms ease, transform 180ms ease, text-shadow 180ms ease, filter 180ms ease;
  user-select: none;
}
[data-theme="dark"] .cc-chevron { opacity: 0.55; }
.cc-card:hover .cc-chevron {
  opacity: 1;
  transform: translate(19px, -3px) scale(1.1);
  text-shadow:
    0 0 12px color-mix(in srgb, var(--accent-gold) 60%, transparent),
    0 0 24px color-mix(in srgb, var(--accent-gold) 30%, transparent);
  filter: drop-shadow(0 1px 2px rgba(0,0,0,0.25));
}

/* ════════════════════════════════════════════════════════════════════════
   SECTION 12-16 — Brand spec Wave 3+ (v1091)
   Token translation: --ink→--text-1, --paper→--bg, --rule→--border,
   --teal-2/3 kept, --gold→--accent-gold, --ember→--accent-red,
   --moss→--accent-green, --amber→--accent-amber, --type-display/ui/mono
   already added in v1086.
═════════════════════════════════════════════════════════════════════════ */

/* ── SECTION 12: NAV, TABS, DETAIL PANELS ───────────────────────────── */

/* Top nav bar */
.tf-topnav {
  display: flex;
  align-items: flex-end;
  gap: 0;
  padding-top: 4px;
  padding-right: 16px;
  background: color-mix(in srgb, var(--text-1) 3%, var(--bg));
  border-bottom: 1px solid var(--border);
  overflow: hidden;
  border-radius: 4px 4px 0 0;
}
[data-theme="dark"] .tf-topnav { background: var(--bg-2); }
.tf-topnav-left {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  padding: 6px 6px 6px 10px;
  gap: 4px;
}
.tf-topnav-back {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  background: none;
  border: 0;
  cursor: pointer;
  padding: 4px 4px 4px 2px;
  border-radius: 4px;
  color: var(--text-3);
  transition: background 160ms ease, color 160ms ease;
}
.tf-topnav-back:hover { background: color-mix(in srgb, var(--text-1) 5%, transparent); color: var(--text-1); }
.tf-topnav-back svg { width: 22px; height: auto; display: block; opacity: 0.85; }
.tf-topnav-back:hover svg { opacity: 1; }
.tf-topnav-tabs {
  display: flex;
  align-items: flex-end;
  gap: 0;
  flex: 1;
  overflow-x: auto;
  margin-bottom: -1px;
}
.tf-topnav-utility {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
  padding-left: 8px;
  margin-bottom: 6px;
}
.tf-topnav-campaign {
  font-family: var(--type-display);
  font-weight: 500;
  font-size: 14px;
  color: var(--text-3);
  padding: 0 8px;
  border-left: 1px solid var(--border);
}

/* Tab buttons */
.tf-tab {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  padding: 9px 14px 8px;
  font-family: var(--type-ui);
  font-size: 13px;
  font-weight: 500;
  color: var(--text-3);
  cursor: pointer;
  transition: color 160ms ease, border-color 160ms ease;
  margin-bottom: -1px;
  white-space: nowrap;
}
.tf-tab:hover { color: var(--text-1); }
.tf-tab.is-active { color: var(--teal-3); border-bottom-color: var(--teal-3); }
.tf-tab-mark {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
  opacity: 0.5;
  transition: opacity 160ms ease;
}
.tf-tab.is-active .tf-tab-mark,
.tf-tab:hover .tf-tab-mark { opacity: 1; }

/* Live tab — gold flicker pill */
.tf-tab-live {
  flex-shrink: 0;
  color: var(--accent-gold);
  font-weight: 700;
  border: 1px solid color-mix(in srgb, var(--accent-gold) 55%, transparent);
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent-gold) 16%, transparent);
  padding: 6px 14px;
  margin: 0 0 6px 8px;
  border-bottom-width: 1px;
  animation: tf-flicker 3.6s ease-in-out infinite;
  transition: background 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
}
.tf-tab-live:hover {
  background: color-mix(in srgb, var(--accent-gold) 24%, transparent);
  border-color: color-mix(in srgb, var(--accent-gold) 75%, transparent);
  box-shadow: 0 0 14px 3px color-mix(in srgb, var(--accent-gold) 25%, transparent);
}
.tf-tab-live .tf-tab-mark { opacity: 0.85; }
@keyframes tf-flicker {
  0%, 100% { box-shadow: 0 0 0 0 transparent; }
  35%      { box-shadow: 0 0 10px 1px color-mix(in srgb, var(--accent-gold) 22%, transparent); }
  50%      { box-shadow: 0 0 6px 0 color-mix(in srgb, var(--accent-gold) 12%, transparent); }
  62%      { box-shadow: 0 0 12px 2px color-mix(in srgb, var(--accent-gold) 28%, transparent); }
}

/* Profile / gear utility */
.tf-nav-icon-btn {
  width: 28px;
  height: 28px;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--bg);
  color: var(--text-2);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  transition: border-color 160ms ease, transform 160ms ease, box-shadow 160ms ease;
}
.tf-nav-icon-btn:hover {
  border-color: color-mix(in srgb, var(--accent-teal) 45%, var(--border));
  box-shadow: 0 4px 10px rgba(0,0,0,0.10);
  transform: translateY(-1px);
}
[data-theme="dark"] .tf-nav-icon-btn { background: rgba(255,255,255,0.06); color: var(--text-2); }

/* Tab toolbar — eyebrow LEFT + actions RIGHT, no bg/border */
.tf-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 18px 12px;
  flex-wrap: wrap;
}
.tf-toolbar-eyebrow { display: inline-flex; align-items: center; gap: 8px; }
.tf-toolbar-eyebrow svg { width: 14px; height: 14px; opacity: 0.7; }
.tf-toolbar-actions {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

/* Tab panel — two canonical patterns */
.tf-panel { background: transparent; border-radius: 4px; }
.tf-panel--single { padding: 20px 16px; }
.tf-panel--split  { padding: 0; }

/* Detail panel (right-column popout: Quest / Fates detail) */
.tf-detail {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 20px 24px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-height: 100%;
  box-shadow: 0 4px 16px rgba(0,0,0,0.06);
}
[data-theme="dark"] .tf-detail { background: var(--bg-2); border-color: var(--border); box-shadow: 0 4px 16px rgba(0,0,0,0.32); }
.tf-detail-hdr {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}
.tf-detail-icon {
  width: 24px; height: 24px;
  flex-shrink: 0;
  border-radius: 6px;
  background: color-mix(in srgb, var(--accent-teal) 14%, transparent);
  color: var(--teal-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
[data-theme="dark"] .tf-detail-icon { background: color-mix(in srgb, var(--accent-teal) 22%, transparent); color: #BFE7EA; }
.tf-detail-title {
  font-family: var(--type-display);
  font-weight: 700;
  font-size: 18px;
  letter-spacing: 0.01em;
  color: var(--text-1);
  flex: 1;
  min-width: 0;
  margin: 0;
}
.tf-detail-actions {
  display: inline-flex;
  gap: 8px;
  margin-left: auto;
  flex-shrink: 0;
}
.tf-detail-body {
  flex: 1;
  overflow-y: auto;
  font-family: var(--type-display);
  font-size: 14.5px;
  line-height: 1.7;
  color: var(--text-2);
}
.tf-detail-body p { margin: 0 0 14px; }
.tf-detail-body strong, .tf-detail-body b { color: var(--text-1); font-weight: 600; }
.tf-detail-body em { color: var(--text-1); }
.tf-detail-body a {
  color: var(--teal-2);
  text-decoration: none;
  border-bottom: 1px dashed color-mix(in srgb, var(--accent-teal) 50%, transparent);
  transition: color 160ms ease, border-color 160ms ease;
}
.tf-detail-body a:hover { color: var(--teal-3); border-bottom-color: var(--teal-3); }
[data-theme="dark"] .tf-detail-body a { color: #BFE7EA; }
.tf-detail-meta {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 12px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

/* Expanded entity body */
.tf-entity-body {
  padding: 14px 18px;
  border-top: 1px solid var(--border);
  background: color-mix(in srgb, var(--text-1) 2%, var(--bg-2));
  font-family: var(--type-display);
  font-size: 14px;
  line-height: 1.65;
  color: var(--text-2);
}
[data-theme="dark"] .tf-entity-body { background: color-mix(in srgb, var(--text-1) 8%, var(--bg-2)); }
.tf-entity-body p { margin: 0 0 10px; }
.tf-entity-keyterms {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 12px;
}
.tf-keyterm {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border-radius: 999px;
  font-family: var(--type-ui);
  font-size: 11px;
  font-weight: 500;
  background: color-mix(in srgb, var(--keyterm-color, var(--accent-teal)) 9%, transparent);
  color: var(--keyterm-color, var(--teal-2));
  border: 1px solid color-mix(in srgb, var(--keyterm-color, var(--accent-teal)) 28%, transparent);
  cursor: pointer;
  transition: background 160ms ease, color 160ms ease, border-color 160ms ease;
}
.tf-keyterm:hover {
  background: color-mix(in srgb, var(--keyterm-color, var(--accent-teal)) 16%, transparent);
  color: var(--keyterm-color, var(--accent-teal));
  border-color: var(--keyterm-color, var(--accent-teal));
}
.tf-keyterm::before {
  content: '';
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--keyterm-color, var(--accent-teal));
  flex-shrink: 0;
  opacity: 0.85;
}
/* Per-type defaults — v1160: aligned to ENTITY_CFG canonical map. The spec
   maps NPC→teal but that collides with Companion=teal in our entity types,
   so NPC takes muted-violet (Threadfall divergence). Item=gold (not ember)
   so it doesn't collide with Faction=amber, which share hex #C26A2E. */
.tf-keyterm--pc        { --keyterm-color: var(--accent-blue); }
.tf-keyterm--companion { --keyterm-color: var(--accent-teal); }
.tf-keyterm--npc       { --keyterm-color: var(--color-muted-violet); }
.tf-keyterm--location  { --keyterm-color: var(--text-3); }
.tf-keyterm--faction   { --keyterm-color: var(--accent-amber); }
.tf-keyterm--item      { --keyterm-color: var(--accent-gold); }
.tf-keyterm--deity     { --keyterm-color: #7E3FA0; }
.tf-keyterm--spell     { --keyterm-color: #2D6DA0; }
.tf-keyterm--lore      { --keyterm-color: var(--accent-green); }
.tf-keyterm--session   { --keyterm-color: var(--accent-red); }
[data-theme="dark"] .tf-keyterm {
  background: color-mix(in srgb, var(--keyterm-color, var(--accent-teal)) 22%, transparent);
  color: color-mix(in srgb, var(--keyterm-color, var(--accent-teal)) 35%, #fff);
  border-color: color-mix(in srgb, var(--keyterm-color, var(--accent-teal)) 45%, transparent);
}

/* Inline narrative links (dashed underline, color-coded) */
.tf-link {
  color: var(--keyterm-color, var(--teal-2));
  border-bottom: 1px dashed color-mix(in srgb, var(--keyterm-color, var(--accent-teal)) 55%, transparent);
  text-decoration: none;
  transition: color 160ms ease, border-color 160ms ease;
}
.tf-link:hover {
  color: var(--keyterm-color, var(--accent-teal));
  border-bottom-color: var(--keyterm-color, var(--accent-teal));
}
/* v1160: matches .tf-keyterm--* and ENTITY_CFG. Same NPC/Item/Item caveats. */
.tf-link--pc        { --keyterm-color: var(--accent-blue); }
.tf-link--companion { --keyterm-color: var(--accent-teal); }
.tf-link--npc       { --keyterm-color: var(--color-muted-violet); }
.tf-link--location  { --keyterm-color: var(--text-3); }
.tf-link--faction   { --keyterm-color: var(--accent-amber); }
.tf-link--item      { --keyterm-color: var(--accent-gold); }
.tf-link--deity     { --keyterm-color: #7E3FA0; }
.tf-link--spell     { --keyterm-color: #2D6DA0; }
.tf-link--lore      { --keyterm-color: var(--accent-green); }
.tf-link--session   { --keyterm-color: var(--accent-red); }
[data-theme="dark"] .tf-link { color: color-mix(in srgb, var(--keyterm-color, var(--accent-teal)) 35%, #fff); }
[data-theme="dark"] .tf-link:hover { color: #fff; }
.tf-entity-ai-row {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px dashed var(--border);
}

/* ── SECTION 13: FORMS, STATUS CHIPS, TABLES, TOOLTIPS, LOADERS ────── */

/* Checkbox / Radio */
.tf-check, .tf-radio {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--type-ui);
  font-size: 13px;
  color: var(--text-1);
  cursor: pointer;
  user-select: none;
}
.tf-check input[type="checkbox"], .tf-radio input[type="radio"] {
  position: absolute;
  width: 1px; height: 1px;
  opacity: 0;
  pointer-events: none;
}
.tf-check-box, .tf-radio-dot {
  width: 16px; height: 16px;
  flex-shrink: 0;
  border: 1.5px solid var(--text-3);
  background: #FCFAF4;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: border-color 160ms ease, background 160ms ease;
}
.tf-check-box { border-radius: 3px; }
.tf-radio-dot { border-radius: 50%; }
.tf-check:hover .tf-check-box,
.tf-radio:hover .tf-radio-dot { border-color: var(--accent-teal); }
.tf-check input:checked ~ .tf-check-box {
  background: var(--accent-teal);
  border-color: var(--accent-teal);
}
.tf-check input:checked ~ .tf-check-box::after {
  content: '';
  width: 4px; height: 8px;
  border: solid #fff;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg) translate(-1px, -1px);
}
.tf-radio input:checked ~ .tf-radio-dot { border-color: var(--accent-teal); }
.tf-radio input:checked ~ .tf-radio-dot::after {
  content: '';
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--accent-teal);
}
.tf-check input:focus-visible ~ .tf-check-box,
.tf-radio input:focus-visible ~ .tf-radio-dot {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-teal) 25%, transparent);
}
[data-theme="dark"] .tf-check-box,
[data-theme="dark"] .tf-radio-dot {
  background: color-mix(in srgb, var(--text-1) 4%, var(--bg));
  border-color: var(--text-3);
}

/* Toggle / switch */
.tf-toggle {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  font-family: var(--type-ui);
  font-size: 13px;
  color: var(--text-1);
}
.tf-toggle input { position: absolute; opacity: 0; pointer-events: none; }
.tf-toggle-track {
  width: 36px; height: 20px;
  border-radius: 999px;
  background: var(--text-3);
  position: relative;
  transition: background 180ms ease;
  flex-shrink: 0;
}
.tf-toggle-track::after {
  content: '';
  position: absolute;
  top: 2px; left: 2px;
  width: 16px; height: 16px;
  border-radius: 50%;
  background: var(--bg);
  transition: transform 180ms ease;
  box-shadow: 0 1px 3px rgba(0,0,0,0.20);
}
.tf-toggle input:checked ~ .tf-toggle-track { background: var(--accent-teal); }
.tf-toggle input:checked ~ .tf-toggle-track::after { transform: translateX(16px); }

/* Range slider */
.tf-range {
  width: 100%;
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
  cursor: pointer;
  padding: 8px 0;
}
.tf-range::-webkit-slider-runnable-track {
  height: 4px;
  background: linear-gradient(to right,
    var(--accent-teal) 0%,
    var(--accent-teal) var(--range-fill, 50%),
    var(--border) var(--range-fill, 50%),
    var(--border) 100%);
  border-radius: 999px;
}
.tf-range::-moz-range-track { height: 4px; background: var(--border); border-radius: 999px; }
.tf-range::-moz-range-progress { height: 4px; background: var(--accent-teal); border-radius: 999px; }
.tf-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 18px; height: 18px;
  border-radius: 50%;
  background: var(--bg);
  border: 2px solid var(--accent-teal);
  margin-top: -7px;
  cursor: grab;
  box-shadow: 0 1px 3px rgba(0,0,0,0.2);
  transition: transform 160ms ease, box-shadow 160ms ease;
}
.tf-range::-moz-range-thumb {
  width: 18px; height: 18px;
  border-radius: 50%;
  background: var(--bg);
  border: 2px solid var(--accent-teal);
  cursor: grab;
  box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.tf-range:hover::-webkit-slider-thumb {
  transform: scale(1.1);
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent-teal) 18%, transparent);
}

/* Color picker swatch row */
.tf-color-row {
  display: inline-flex;
  gap: 6px;
  padding: 4px;
  background: color-mix(in srgb, var(--text-1) 4%, var(--bg));
  border: 1px solid var(--border);
  border-radius: 999px;
}
.tf-color-swatch {
  width: 22px; height: 22px;
  border-radius: 50%;
  cursor: pointer;
  border: 2px solid transparent;
  background-color: var(--swatch);
  transition: border-color 160ms ease, transform 160ms ease;
  flex-shrink: 0;
}
.tf-color-swatch:hover { transform: scale(1.1); }
.tf-color-swatch.is-selected { border-color: var(--text-1); transform: scale(1.05); }
[data-theme="dark"] .tf-color-swatch.is-selected { border-color: var(--bg); }

/* File input / drop zone */
.tf-drop {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 24px 18px;
  border: 1.5px dashed var(--text-3);
  border-radius: 8px;
  background: color-mix(in srgb, var(--text-1) 2%, var(--bg));
  color: var(--text-3);
  cursor: pointer;
  transition: border-color 160ms ease, background 160ms ease, color 160ms ease;
  font-family: var(--type-ui);
  font-size: 13px;
  text-align: center;
}
.tf-drop:hover, .tf-drop.is-hover {
  border-color: var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 5%, var(--bg));
  color: var(--text-1);
}
.tf-drop-icon { width: 28px; height: 28px; opacity: 0.6; }
.tf-drop strong { color: var(--text-1); font-weight: 600; font-family: var(--type-display); font-size: 14px; }

/* Select (custom) */
.tf-select {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 9px 32px 9px 12px;
  font-family: var(--type-ui);
  font-size: 13px;
  color: var(--text-1);
  background: #FCFAF4;
  border: 1.5px solid var(--border);
  border-radius: 0;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8' fill='none' stroke='%236A7079' stroke-width='1.5' stroke-linecap='round'%3E%3Cpath d='M2 2 L6 6 L10 2'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 10px;
  transition: border-color 160ms ease;
}
.tf-select:focus { outline: none; border-color: var(--accent-teal); }
[data-theme="dark"] .tf-select { background-color: color-mix(in srgb, var(--text-1) 4%, var(--bg)); color: var(--text-1); }

/* Tinted chip base — coin/rarity/faction/non-combat all compose this */
.tf-chip-tinted {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 9px;
  border-radius: 999px;
  font-family: var(--type-ui);
  font-size: 11px;
  font-weight: 500;
  color: var(--chip-color, var(--text-3));
  background: color-mix(in srgb, var(--chip-color, var(--text-3)) 12%, var(--bg));
  border: 1px solid color-mix(in srgb, var(--chip-color, var(--text-3)) 30%, transparent);
  line-height: 1.45;
}
[data-theme="dark"] .tf-chip-tinted {
  color: color-mix(in srgb, var(--chip-color, var(--text-3)) 35%, #fff);
  background: color-mix(in srgb, var(--chip-color, var(--text-3)) 28%, var(--bg-2));
  border-color: color-mix(in srgb, var(--chip-color, var(--text-3)) 42%, transparent);
}

/* Currency chips — composes .tf-chip-tinted */
.tf-coin {
  font-family: var(--type-mono);
  font-size: 11px;
  font-weight: 600;
  padding: 2px 8px;
  font-variant-numeric: tabular-nums;
}
.tf-coin::before {
  content: '';
  width: 8px; height: 8px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
  opacity: 0.85;
}
.tf-coin--pp { --chip-color: #4A6E8C; }
.tf-coin--gp { --chip-color: #B89348; }
.tf-coin--ep { --chip-color: #6F8A88; }
.tf-coin--sp { --chip-color: #7A8492; }
.tf-coin--cp { --chip-color: #B8773C; }

/* Item rarity — squared */
.tf-rarity {
  border-radius: 3px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 2px 8px;
}
.tf-rarity--common    { --chip-color: #4D5868; }
.tf-rarity--uncommon  { --chip-color: #5A8345; }
.tf-rarity--rare      { --chip-color: #2D6DA0; }
.tf-rarity--veryrare  { --chip-color: #7E3FA0; }
.tf-rarity--legendary { --chip-color: #C68A1A; }
.tf-rarity--artifact  { --chip-color: #A04A3A; }

/* Faction tag — accepts --faction-color via inline style */
.tf-faction {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 9px;
  font-family: var(--type-ui);
  font-size: 11px;
  font-weight: 500;
  color: var(--faction-color, var(--teal-2));
  background: color-mix(in srgb, var(--faction-color, var(--accent-teal)) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--faction-color, var(--accent-teal)) 30%, transparent);
  border-radius: 999px;
}
.tf-faction::before {
  content: '';
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--faction-color, var(--accent-teal));
  flex-shrink: 0;
}

/* Non-combat / lore tag */
.tf-noncombat {
  --chip-color: #7E3FA0;
  border-radius: 999px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 3px 9px;
}

/* Tables */
.tf-table-wrap {
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg-2);
}
.tf-table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--type-ui);
  font-size: 13px;
}
.tf-table th {
  text-align: left;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--text-3);
  padding: 12px 14px 10px;
  background: color-mix(in srgb, var(--text-1) 3%, var(--bg-2));
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.tf-table td {
  padding: 11px 14px;
  color: var(--text-1);
  border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
  vertical-align: middle;
}
.tf-table tr:last-child td { border-bottom: 0; }
.tf-table tr:hover td { background: color-mix(in srgb, var(--accent-teal) 5%, transparent); }
.tf-table td.tf-num {
  font-family: var(--type-mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
  color: var(--text-2);
}
.tf-table td.tf-actions { text-align: right; white-space: nowrap; }

/* Tooltip — attribute-driven */
.tf-tooltip { position: relative; display: inline-block; }
.tf-tooltip::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  background: var(--text-1);
  color: var(--bg);
  padding: 5px 9px;
  border-radius: 4px;
  font-family: var(--type-ui);
  font-size: 11px;
  font-weight: 500;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 160ms ease, transform 160ms ease;
  z-index: 100;
  box-shadow: 0 4px 12px rgba(0,0,0,0.18);
}
.tf-tooltip::before {
  content: '';
  position: absolute;
  bottom: calc(100% + 1px);
  left: 50%;
  transform: translateX(-50%);
  border: 5px solid transparent;
  border-top-color: var(--text-1);
  opacity: 0;
  transition: opacity 160ms ease;
  z-index: 100;
}
.tf-tooltip:hover::after, .tf-tooltip:focus-within::after {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}
.tf-tooltip:hover::before, .tf-tooltip:focus-within::before { opacity: 1; }
[data-theme="dark"] .tf-tooltip::after { background: var(--bg); color: var(--text-1); }
[data-theme="dark"] .tf-tooltip::before { border-top-color: var(--bg); }

/* Loaders — falling thread spinner */
.tf-spinner {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--spin-size, 80px);
  height: calc(var(--spin-size, 80px) * 0.95);
  position: relative;
}
.tf-spinner svg { width: 100%; height: 100%; display: block; overflow: visible; }
.tf-thread-g {
  transform-origin: 322px 210px;
  transform-box: fill-box;
  opacity: 0;
  animation: tf-thread-stretch 1.6s cubic-bezier(0.55, 0.06, 0.45, 0.95) infinite;
}
.tf-thread-g line {
  stroke: #FFFFFF;
  stroke-width: 2.8;
  stroke-linecap: round;
  filter:
    drop-shadow(0 0 3px rgba(255,255,255,0.95))
    drop-shadow(0 0 10px rgba(255,255,255,0.55))
    drop-shadow(0 0 22px color-mix(in srgb, var(--accent-teal) 60%, transparent));
}
.tf-thread--a { animation-delay: 0s;     }
.tf-thread--b { animation-delay: 0.53s;  }
.tf-thread--c { animation-delay: 1.06s;  }
@keyframes tf-thread-stretch {
  0%   { opacity: 0;    transform: scaleY(0)    translateY(0); }
  8%   { opacity: 1;    transform: scaleY(0.15) translateY(0); }
  32%  { opacity: 1;    transform: scaleY(1)    translateY(0); }
  55%  { opacity: 1;    transform: scaleY(1)    translateY(60px); }
  78%  { opacity: 0.75; transform: scaleY(1.35) translateY(140px); }
  100% { opacity: 0;    transform: scaleY(1.8)  translateY(240px); }
}
.tf-spinner--inline { --spin-size: 22px; }
.tf-spinner--inline .tf-thread-g line { stroke-width: 4; }
.tf-loading-row {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--text-3);
  font-family: var(--type-display);
  font-style: italic;
  font-size: 14px;
}

/* Skeleton placeholder lines */
.tf-skeleton {
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--text-1) 5%, var(--bg)) 0%,
    color-mix(in srgb, var(--text-1) 10%, var(--bg)) 50%,
    color-mix(in srgb, var(--text-1) 5%, var(--bg)) 100%);
  background-size: 200% 100%;
  animation: tf-shimmer 1.6s ease-in-out infinite;
  border-radius: 3px;
  display: block;
}
@keyframes tf-shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
.tf-skel-card {
  aspect-ratio: 16 / 9;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* ── SECTION 14: LIVE SESSION UI ───────────────────────────────────── */

.tf-live-shell {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
  display: grid;
  grid-template-rows: auto 1fr auto;
  min-height: 420px;
}
[data-theme="dark"] .tf-live-shell { background: var(--bg); }
.tf-live-hdr {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 20px;
  background: color-mix(in srgb, var(--accent-red) 5%, var(--bg-2));
  border-bottom: 1px solid color-mix(in srgb, var(--accent-red) 22%, var(--border));
  flex-wrap: wrap;
}
[data-theme="dark"] .tf-live-hdr {
  background: color-mix(in srgb, var(--accent-red) 10%, var(--bg-2));
  border-bottom-color: color-mix(in srgb, var(--accent-red) 30%, var(--border));
}
.tf-live-hdr-left, .tf-live-hdr-right {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.tf-live-session-name {
  font-family: var(--type-display);
  font-weight: 600;
  font-size: 17px;
  letter-spacing: -0.005em;
  color: var(--text-1);
}
.tf-live-session-meta {
  font-family: var(--type-ui);
  font-size: 12px;
  color: var(--text-3);
}

/* Headline timer (the BIG always-visible clock; pill is .tf-live-timer in v1084) */
.tf-live-timer-hero {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 8px 14px;
  border-radius: 999px;
  background: var(--accent-red);
  color: #FFF6F1;
  font-family: var(--type-mono);
  font-weight: 600;
  font-size: 18px;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent-red) 55%, transparent);
  animation: tf-live-halo 2s ease-out infinite;
}
.tf-live-timer-dot {
  width: 9px; height: 9px;
  border-radius: 50%;
  background: #FFF6F1;
  box-shadow: 0 0 0 2px color-mix(in srgb, #fff 35%, transparent);
  animation: tf-dot-blink 1.2s ease-in-out infinite;
}
.tf-live-timer-label {
  font-family: var(--type-ui);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  opacity: 0.92;
}

/* Viewers strip */
.tf-live-viewers { display: inline-flex; align-items: center; gap: 6px; }
.tf-live-viewer {
  width: 28px; height: 28px;
  border-radius: 50%;
  background: var(--bg);
  border: 1px solid var(--border);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--type-ui);
  font-size: 11px;
  font-weight: 700;
  color: var(--text-2);
  position: relative;
}
.tf-live-viewer.is-online::after {
  content: '';
  position: absolute;
  bottom: -1px; right: -1px;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--accent-green);
  border: 1.5px solid var(--bg);
}
.tf-live-viewer-rec.is-online::after { background: var(--accent-red); }
[data-theme="dark"] .tf-live-viewer {
  background: rgba(255,255,255,0.06);
  color: var(--text-2);
  border-color: rgba(255,255,255,0.10);
}
[data-theme="dark"] .tf-live-viewer.is-online::after { border-color: var(--bg-2); }
.tf-live-viewer-count {
  font-family: var(--type-ui);
  font-size: 11px;
  color: var(--text-3);
  margin-left: 4px;
}

/* Live body — log LEFT, notebook RIGHT */
.tf-live-body {
  display: grid;
  grid-template-columns: 1fr 320px;
  min-height: 280px;
}
@media (max-width: 900px) {
  .tf-live-body { grid-template-columns: 1fr; }
}

/* Event log */
.tf-live-log {
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  overflow-y: auto;
  background: var(--bg);
}
.tf-log-entry {
  display: grid;
  grid-template-columns: 56px auto 1fr auto;
  align-items: baseline;
  gap: 10px;
  padding: 8px 10px;
  border-left: 3px solid var(--border);
  background: color-mix(in srgb, var(--text-1) 2%, var(--bg-2));
  border-radius: 4px;
  font-family: var(--type-ui);
  font-size: 13px;
  color: var(--text-1);
  line-height: 1.5;
  transition: background 160ms ease;
}
.tf-log-entry:hover { background: color-mix(in srgb, var(--accent-teal) 5%, var(--bg-2)); }
.tf-log-entry--story    { border-left-color: var(--accent-teal); }
.tf-log-entry--combat   { border-left-color: var(--accent-red); }
.tf-log-entry--travel   { border-left-color: var(--text-3); }
.tf-log-entry--key      { border-left-color: var(--accent-amber); }
.tf-log-entry--system   { border-left-color: var(--text-3); font-style: italic; color: var(--text-3); }
.tf-log-entry--new      { animation: tf-log-ping 0.7s ease-out forwards; }
@keyframes tf-log-ping {
  0%   { background: color-mix(in srgb, var(--accent-red) 18%, var(--bg-2)); transform: translateX(-4px); }
  100% { background: color-mix(in srgb, var(--text-1) 2%, var(--bg-2));      transform: translateX(0); }
}
.tf-log-time {
  font-family: var(--type-mono);
  font-size: 11px;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.tf-log-tag {
  font-family: var(--type-ui);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--tag-color, var(--text-3));
}
.tf-log-entry--story  .tf-log-tag { color: var(--teal-2); }
.tf-log-entry--combat .tf-log-tag { color: var(--ember-soft); }
.tf-log-entry--travel .tf-log-tag { color: var(--text-2); }
.tf-log-entry--key    .tf-log-tag { color: #7C4117; }
[data-theme="dark"] .tf-log-entry--story  .tf-log-tag { color: #BFE7EA; }
[data-theme="dark"] .tf-log-entry--combat .tf-log-tag { color: #F5C0AE; }
[data-theme="dark"] .tf-log-entry--travel .tf-log-tag { color: #C5CCD4; }
[data-theme="dark"] .tf-log-entry--key    .tf-log-tag { color: #F1B97A; }

/* GM Notebook side panel */
.tf-notebook {
  border-left: 1px solid var(--border);
  background: color-mix(in srgb, var(--text-1) 2%, var(--bg-2));
  padding: 14px 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  overflow-y: auto;
}
[data-theme="dark"] .tf-notebook { background: color-mix(in srgb, var(--text-1) 5%, var(--bg-2)); }
.tf-notebook-section { display: flex; flex-direction: column; gap: 8px; }
.tf-notebook-card {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 9px 11px;
  font-family: var(--type-ui);
  font-size: 12.5px;
  color: var(--text-2);
  line-height: 1.4;
  border-left: 3px solid var(--note-rail, var(--accent-teal));
}
.tf-notebook-card strong { color: var(--text-1); display: block; font-weight: 600; margin-bottom: 2px; }

/* Transport bar — bottom controls */
.tf-live-transport {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 18px;
  border-top: 1px solid var(--border);
  background: color-mix(in srgb, var(--text-1) 3%, var(--bg-2));
  flex-wrap: wrap;
}
.tf-live-transport-cluster {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

/* Mic level meter */
.tf-mic-meter {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 6px 9px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  height: 28px;
  box-sizing: border-box;
}
.tf-mic-bar {
  width: 2px;
  background: var(--accent-green);
  border-radius: 999px;
  animation: tf-mic-pulse 0.9s ease-in-out infinite;
}
.tf-mic-bar:nth-child(1) { height: 8px;  animation-delay: 0s;    }
.tf-mic-bar:nth-child(2) { height: 12px; animation-delay: 0.1s; }
.tf-mic-bar:nth-child(3) { height: 16px; animation-delay: 0.2s; }
.tf-mic-bar:nth-child(4) { height: 14px; animation-delay: 0.3s; }
.tf-mic-bar:nth-child(5) { height: 10px; animation-delay: 0.4s; }
.tf-mic-bar:nth-child(6) { height: 13px; animation-delay: 0.5s; }
.tf-mic-bar:nth-child(7) { height: 7px;  animation-delay: 0.6s; }
@keyframes tf-mic-pulse {
  0%, 100% { transform: scaleY(0.55); opacity: 0.65; }
  50%      { transform: scaleY(1);    opacity: 1; }
}

/* Minimized record bar */
.tf-live-bar {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  padding: 8px 12px 8px 14px;
  background: var(--bg-2);
  border: 1px solid color-mix(in srgb, var(--accent-red) 35%, var(--border));
  border-radius: 999px;
  font-family: var(--type-ui);
  font-size: 12px;
}
.tf-live-bar-pulse {
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--accent-red);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-red) 25%, transparent);
  animation: tf-live-halo 1.5s ease-in-out infinite;
}
.tf-live-bar-label {
  color: var(--ember-soft);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-size: 10px;
}
[data-theme="dark"] .tf-live-bar-label { color: #F5C0AE; }
.tf-live-bar-time {
  color: var(--ember-soft);
  font-family: var(--type-mono);
  font-weight: 600;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}
[data-theme="dark"] .tf-live-bar-time { color: #F5C0AE; }

/* ── SECTION 15: MARKETING & ONBOARDING ────────────────────────────── */

/* Auth / login */
.tf-auth-stage {
  display: grid;
  grid-template-columns: 1.1fr 1fr;
  gap: 0;
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
  background: var(--bg);
}
@media (max-width: 800px) { .tf-auth-stage { grid-template-columns: 1fr; } }
.tf-auth-hero {
  padding: 40px 36px;
  background: color-mix(in srgb, var(--accent-teal) 10%, var(--bg-2));
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 18px;
  justify-content: center;
}
[data-theme="dark"] .tf-auth-hero {
  background-color: color-mix(in srgb, var(--accent-teal) 16%, var(--bg-2));
  border-right-color: var(--border);
}
.tf-auth-headline {
  font-family: var(--type-display);
  font-weight: 500;
  font-size: 28px;
  line-height: 1.15;
  letter-spacing: -0.015em;
  margin: 0;
  color: var(--text-1);
  text-wrap: balance;
}
.tf-auth-sub {
  font-family: var(--type-display);
  font-size: 16px;
  color: var(--text-2);
  line-height: 1.5;
  margin: 0;
  max-width: 36ch;
}
.tf-auth-form {
  padding: 40px 36px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  justify-content: center;
}
.tf-auth-logo {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 18px;
}
.tf-auth-logo svg { width: 36px; }
.tf-auth-logo-name {
  font-family: var(--type-display);
  font-weight: 500;
  font-size: 24px;
  letter-spacing: -0.015em;
  color: var(--text-1);
}
.tf-auth-links {
  display: flex;
  justify-content: space-between;
  margin-top: 4px;
}
.tf-auth-link {
  background: none;
  border: 0;
  color: var(--teal-2);
  font-family: var(--type-ui);
  font-size: 12px;
  cursor: pointer;
  padding: 4px 0;
  text-decoration: none;
  border-bottom: 1px dashed transparent;
  transition: border-color 160ms ease;
}
.tf-auth-link:hover { border-bottom-color: var(--teal-3); }
[data-theme="dark"] .tf-auth-link { color: #BFE7EA; }
.tf-auth-trial {
  font-family: var(--type-display);
  font-style: italic;
  font-size: 13px;
  color: var(--text-3);
  margin-top: 14px;
}

/* Setup card / step flow */
.tf-setup-card {
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 36px 42px;
  max-width: 540px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.tf-step-indicator {
  font-family: var(--type-mono);
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-3);
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.tf-step-indicator-bar { display: inline-flex; gap: 4px; }
.tf-step-indicator-bar i {
  display: inline-block;
  width: 28px; height: 4px;
  border-radius: 2px;
  background: var(--border);
}
.tf-step-indicator-bar i.is-active { background: var(--accent-teal); }
.tf-step-indicator-bar i.is-done   { background: color-mix(in srgb, var(--accent-teal) 55%, var(--border)); }
.tf-setup-title {
  font-family: var(--type-display);
  font-weight: 500;
  font-size: 26px;
  letter-spacing: -0.012em;
  margin: 0;
  color: var(--text-1);
}
.tf-setup-sub {
  font-family: var(--type-display);
  font-size: 15px;
  color: var(--text-2);
  margin: 0;
  line-height: 1.55;
}

/* Pricing tier cards */
.tf-tiers {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
}
@media (max-width: 1000px) { .tf-tiers { grid-template-columns: 1fr 1fr; } }
@media (max-width: 600px)  { .tf-tiers { grid-template-columns: 1fr; } }
.tf-tier {
  position: relative;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 24px 22px 22px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease;
}
.tf-tier:hover {
  border-color: color-mix(in srgb, var(--accent-teal) 45%, var(--border));
  box-shadow: 0 6px 18px rgba(0,0,0,0.10);
  transform: translateY(-2px);
}
.tf-tier--recommended {
  border-color: var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 5%, var(--bg-2));
}
.tf-tier--founding {
  border-color: var(--accent-gold);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--accent-gold) 10%, var(--bg-2)) 0%,
    color-mix(in srgb, var(--accent-teal) 6%, var(--bg-2)) 100%);
}
.tf-tier-ribbon {
  position: absolute;
  top: -12px;
  left: 50%;
  transform: translateX(-50%);
  padding: 4px 12px;
  border-radius: 999px;
  background: var(--accent-teal);
  color: #fff;
  font-family: var(--type-ui);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  white-space: nowrap;
}
.tf-tier--founding .tf-tier-ribbon { background: var(--accent-gold); color: #2A1F08; }
.tf-tier-name {
  font-family: var(--type-display);
  font-weight: 600;
  font-size: 18px;
  color: var(--text-1);
  letter-spacing: -0.005em;
  margin: 0;
}
.tf-tier-price { font-family: var(--type-display); color: var(--text-1); line-height: 1; }
.tf-tier-price-amt {
  font-size: 36px;
  font-weight: 500;
  letter-spacing: -0.025em;
}
.tf-tier-price-cycle {
  font-size: 13px;
  color: var(--text-3);
  margin-left: 4px;
}
.tf-tier-savings {
  font-family: var(--type-ui);
  font-size: 11px;
  color: var(--accent-green);
  font-weight: 600;
  margin-top: 4px;
}
.tf-tier-features {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 4px 0 14px;
  font-family: var(--type-ui);
  font-size: 13px;
  color: var(--text-2);
  line-height: 1.5;
}
.tf-tier-features li {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 0;
  list-style: none;
}
.tf-tier-feature-icon {
  width: 14px; height: 14px;
  flex-shrink: 0;
  margin-top: 2px;
  color: var(--accent-teal);
}
.tf-tier-features li.is-disabled { color: var(--text-3); text-decoration: line-through; }
.tf-tier-features li.is-disabled .tf-tier-feature-icon { color: var(--text-3); }
.tf-tier-cta { margin-top: auto; }

/* Showcase carousel */
.tf-showcase {
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 32px 36px 24px;
  position: relative;
  overflow: hidden;
}
.tf-showcase-slide {
  display: grid;
  grid-template-columns: 1fr 1.2fr;
  gap: 32px;
  align-items: center;
}
@media (max-width: 800px) { .tf-showcase-slide { grid-template-columns: 1fr; } }
.tf-showcase-icon {
  width: 56px; height: 56px;
  border-radius: 12px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--accent-teal) 14%, transparent);
  color: var(--teal-2);
  font-size: 22px;
  margin-bottom: 14px;
}
.tf-showcase-icon--violet { background: color-mix(in srgb, #7E3FA0 14%, transparent); color: #5E2A78; }
.tf-showcase-icon--ember  { background: color-mix(in srgb, var(--accent-red) 14%, transparent); color: var(--ember-soft); }
.tf-showcase-head {
  font-family: var(--type-display);
  font-weight: 500;
  font-size: 24px;
  letter-spacing: -0.012em;
  margin: 6px 0 8px;
  color: var(--text-1);
  text-wrap: balance;
}
.tf-showcase-desc {
  font-family: var(--type-display);
  font-size: 14.5px;
  color: var(--text-2);
  line-height: 1.6;
  margin: 0 0 12px;
  max-width: 44ch;
}
.tf-showcase-mock {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 14px 16px;
  font-family: var(--type-ui);
  font-size: 12.5px;
  color: var(--text-2);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.tf-showcase-mock-beat {
  font-family: var(--type-ui);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--teal-2);
}
.tf-showcase-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 24px;
  gap: 12px;
}
.tf-showcase-dots { display: inline-flex; gap: 6px; }
.tf-showcase-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--border);
  border: 0;
  cursor: pointer;
  transition: background 160ms ease, transform 160ms ease;
}
.tf-showcase-dot:hover { background: var(--text-3); }
.tf-showcase-dot.is-active { background: var(--accent-teal); transform: scale(1.25); }

/* Banners */
.tf-banner {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 18px;
  border-radius: 4px;
  border: 1px solid var(--border);
  background: color-mix(in srgb, var(--text-1) 3%, var(--bg));
  font-family: var(--type-ui);
  font-size: 13px;
  color: var(--text-1);
}
.tf-banner-text { flex: 1; min-width: 0; }
.tf-banner-text a {
  color: var(--teal-2);
  border-bottom: 1px dashed currentColor;
  text-decoration: none;
}
[data-theme="dark"] .tf-banner-text a { color: #BFE7EA; }
.tf-banner-actions { display: inline-flex; gap: 8px; align-items: center; flex-shrink: 0; }
.tf-banner-dismiss {
  background: none;
  border: 0;
  color: var(--text-3);
  cursor: pointer;
  font-size: 14px;
  padding: 4px 8px;
  border-radius: 4px;
  transition: color 160ms ease, background 160ms ease;
}
.tf-banner-dismiss:hover { color: var(--text-1); background: color-mix(in srgb, var(--text-1) 5%, transparent); }
.tf-banner--update {
  border-color: color-mix(in srgb, var(--accent-teal) 40%, transparent);
  background: color-mix(in srgb, var(--accent-teal) 8%, var(--bg));
}
.tf-banner--discord {
  border-color: color-mix(in srgb, #5865F2 35%, transparent);
  background: color-mix(in srgb, #5865F2 6%, var(--bg));
}
.tf-banner--discord .tf-banner-text a { color: #4A52C4; }
[data-theme="dark"] .tf-banner--discord { background: color-mix(in srgb, #5865F2 14%, var(--bg-2)); }
[data-theme="dark"] .tf-banner--discord .tf-banner-text a { color: #A5ADF8; }

/* Inline alerts */
.tf-alert {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 12px 14px;
  border-radius: 6px;
  border: 1px solid;
  font-family: var(--type-ui);
  font-size: 13px;
  line-height: 1.5;
}
.tf-alert-icon { width: 18px; height: 18px; flex-shrink: 0; margin-top: 1px; }
.tf-alert-title { font-weight: 600; }
.tf-alert--info    { border-color: color-mix(in srgb, var(--accent-teal)  30%, transparent); background: color-mix(in srgb, var(--accent-teal)  10%, var(--bg)); color: var(--teal-2); }
.tf-alert--success { border-color: color-mix(in srgb, var(--accent-green) 30%, transparent); background: color-mix(in srgb, var(--accent-green) 10%, var(--bg)); color: #355024; }
.tf-alert--warn    { border-color: color-mix(in srgb, var(--accent-amber) 35%, transparent); background: color-mix(in srgb, var(--accent-amber) 12%, var(--bg)); color: #7C4117; }
.tf-alert--error   { border-color: color-mix(in srgb, var(--accent-red)   35%, transparent); background: color-mix(in srgb, var(--accent-red)   10%, var(--bg)); color: var(--ember-soft); }
[data-theme="dark"] .tf-alert--info    { color: #BFE7EA; background: color-mix(in srgb, var(--accent-teal)  20%, var(--bg-2)); border-color: color-mix(in srgb, var(--accent-teal)  45%, transparent); }
[data-theme="dark"] .tf-alert--success { color: #B5DAA1; background: color-mix(in srgb, var(--accent-green) 20%, var(--bg-2)); border-color: color-mix(in srgb, var(--accent-green) 45%, transparent); }
[data-theme="dark"] .tf-alert--warn    { color: #F1B97A; background: color-mix(in srgb, var(--accent-amber) 22%, var(--bg-2)); border-color: color-mix(in srgb, var(--accent-amber) 45%, transparent); }
[data-theme="dark"] .tf-alert--error   { color: #F5C0AE; background: color-mix(in srgb, var(--accent-red)   20%, var(--bg-2)); border-color: color-mix(in srgb, var(--accent-red)   45%, transparent); }
.tf-alert strong { color: inherit; }

/* Toast / snackbar */
.tf-toast {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  padding: 12px 18px 12px 14px;
  border-radius: 8px;
  background: var(--text-1);
  color: var(--bg);
  font-family: var(--type-ui);
  font-size: 13px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.30);
  border-left: 3px solid var(--accent-teal);
  max-width: 420px;
}
.tf-toast-icon { width: 18px; height: 18px; flex-shrink: 0; color: var(--accent-teal); }
.tf-toast--success { border-left-color: var(--accent-green); }
.tf-toast--success .tf-toast-icon { color: var(--accent-green); }
.tf-toast--error { border-left-color: var(--accent-red); }
.tf-toast--error .tf-toast-icon { color: var(--accent-red); }
.tf-toast-action {
  margin-left: auto;
  background: none;
  border: 0;
  color: color-mix(in srgb, var(--accent-teal) 70%, #fff);
  cursor: pointer;
  font-family: var(--type-ui);
  font-size: 12px;
  font-weight: 600;
  padding: 2px 6px;
  border-radius: 3px;
}
.tf-toast--success .tf-toast-action { color: color-mix(in srgb, var(--accent-green) 75%, #fff); }
.tf-toast--error .tf-toast-action { color: color-mix(in srgb, var(--accent-red) 75%, #fff); }
.tf-toast-stack {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 10px;
  position: fixed;
  bottom: 18px;
  right: 18px;
  z-index: 10500;
  pointer-events: none;  /* let clicks fall through stack gaps; toasts re-enable */
}
.tf-toast-stack .tf-toast { pointer-events: auto; }

/* v1103: enter/exit transitions for toast helper (window.tfToast). */
.tf-toast {
  opacity: 0;
  transform: translateX(20px);
  transition: opacity 220ms ease, transform 220ms ease;
}
.tf-toast.is-in {
  opacity: 1;
  transform: translateX(0);
}
.tf-toast.is-out {
  opacity: 0;
  transform: translateX(20px);
}
.tf-toast-text { flex: 1; min-width: 0; }
.tf-toast-dismiss {
  margin-left: 4px;
  color: rgba(255, 255, 255, 0.55);
  font-size: 16px;
  line-height: 1;
  padding: 0 4px;
}
.tf-toast-dismiss:hover { color: #fff; }

/* ── SECTION 16: RECAP, ATLAS, GALLERY ─────────────────────────────── */

/* Recap layout */
.tf-recap {
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 32px 36px;
  max-width: 760px;
}
.tf-recap-title {
  font-family: var(--type-display);
  font-weight: 500;
  font-size: 32px;
  letter-spacing: -0.018em;
  margin: 6px 0 6px;
  color: var(--text-1);
  text-wrap: balance;
}
/* v1124: spec recap header eyebrow — small uppercase "Session 33 · The
   Whispering Vale" label that sits above the meta line. Matches the
   .tf-recap-eyebrow recipe in the brand spec (font-weight 700,
   letter-spacing 0.14em, 11px UI font, tertiary text). */
.tf-recap-eyebrow {
  display: block;
  font-family: var(--type-ui);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-3);
  line-height: 1.4;
  margin-bottom: 6px;
}
.tf-recap-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 24px;
  font-family: var(--type-mono);
  font-size: 11px;
  color: var(--text-3);
}
.tf-recap-tldr {
  font-family: var(--type-display);
  font-style: italic;
  font-size: 16px;
  line-height: 1.6;
  color: var(--text-2);
  border-left: 3px solid var(--accent-teal);
  padding: 8px 16px;
  margin: 0 0 28px;
}
.tf-beat {
  font-family: var(--type-display);
  font-weight: 600;
  font-size: 13px;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--teal-2);
  margin: 28px 0 10px;
  padding-bottom: 6px;
  border-bottom: 1px solid color-mix(in srgb, var(--accent-teal) 35%, var(--border));
  display: flex;
  align-items: center;
  gap: 8px;
}
[data-theme="dark"] .tf-beat { color: #BFE7EA; }
.tf-beat::before {
  content: '';
  width: 6px; height: 6px;
  background: var(--accent-teal);
  border-radius: 50%;
  flex-shrink: 0;
}
.tf-beat:first-of-type { margin-top: 0; }
.tf-recap-body {
  font-family: var(--type-display);
  font-size: 15.5px;
  line-height: 1.75;
  color: var(--text-2);
}
.tf-recap-body p { margin: 0 0 14px; }
.tf-recap-body strong, .tf-recap-body b { color: var(--text-1); font-weight: 600; }
.tf-recap-body em { color: var(--text-1); font-style: italic; }

/* Citation chip */
.tf-cite {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 4px;
  margin-left: 3px;
  font-family: var(--type-mono);
  font-size: 10px;
  font-weight: 600;
  color: var(--teal-2);
  background: color-mix(in srgb, var(--accent-teal) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent-teal) 28%, transparent);
  vertical-align: 2px;
  cursor: pointer;
  transition: background 160ms ease, color 160ms ease;
}
.tf-cite:hover {
  background: color-mix(in srgb, var(--accent-teal) 22%, transparent);
  color: var(--teal-3);
}
[data-theme="dark"] .tf-cite { color: #BFE7EA; background: color-mix(in srgb, var(--accent-teal) 22%, transparent); }

/* Pull-quote */
.tf-pullquote {
  font-family: var(--type-display);
  font-style: italic;
  font-size: 16.5px;
  color: var(--text-1);
  line-height: 1.55;
  padding: 14px 18px;
  border-left: 3px solid var(--accent-gold);
  background: color-mix(in srgb, var(--accent-gold) 6%, var(--bg));
  margin: 14px 0;
  border-radius: 0 4px 4px 0;
}
.tf-pullquote-attr {
  display: block;
  font-style: normal;
  font-family: var(--type-ui);
  font-size: 11.5px;
  color: var(--text-3);
  margin-top: 6px;
  letter-spacing: 0.04em;
}

/* Atlas pin */
.tf-pin {
  position: absolute;
  transform: translate(-50%, -100%);
  width: 24px;
  height: 30px;
  cursor: pointer;
  z-index: 1;
}
.tf-pin-shape {
  width: 22px;
  height: 22px;
  border-radius: 50% 50% 50% 0;
  background: var(--pin-color, var(--accent-teal));
  border: 2px solid #fff;
  box-shadow: 0 2px 4px rgba(0,0,0,0.40);
  transform: rotate(-45deg) translate(0px, 0px);
  margin: 0 auto;
  transition: transform 180ms ease;
}
.tf-pin:hover .tf-pin-shape { transform: rotate(-45deg) scale(1.1); }
.tf-pin-label {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(11,15,20,0.85);
  color: #fff;
  font-family: var(--type-ui);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  padding: 3px 7px;
  border-radius: 3px;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 180ms ease, transform 180ms ease;
  margin-bottom: 4px;
}
.tf-pin:hover .tf-pin-label {
  opacity: 1;
  transform: translateX(-50%) translateY(-2px);
}

/* Map popup */
.tf-map-popup {
  position: absolute;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px 14px;
  min-width: 200px;
  max-width: 260px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.30);
  font-family: var(--type-ui);
  font-size: 12.5px;
  color: var(--text-1);
  z-index: 2;
}
[data-theme="dark"] .tf-map-popup { background: var(--bg-2); border-color: var(--border); }
.tf-map-popup-title {
  font-family: var(--type-display);
  font-weight: 600;
  font-size: 14px;
  color: var(--text-1);
  margin: 0 0 2px;
}
.tf-map-popup-meta { color: var(--text-3); font-size: 11px; }
.tf-map-popup-desc { color: var(--text-2); line-height: 1.5; margin-top: 6px; }
.tf-map-popup-close {
  position: absolute;
  top: 6px; right: 8px;
  background: none;
  border: 0;
  color: var(--text-3);
  font-size: 14px;
  cursor: pointer;
  padding: 2px 6px;
  border-radius: 3px;
}
.tf-map-popup-close:hover { color: var(--text-1); background: color-mix(in srgb, var(--text-1) 6%, transparent); }

/* Map zoom controls */
.tf-map-zoom {
  position: absolute;
  top: 10px;
  right: 10px;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
  background: var(--bg);
  box-shadow: 0 2px 6px rgba(0,0,0,0.20);
  z-index: 2;
}
[data-theme="dark"] .tf-map-zoom { background: var(--bg-2); border-color: var(--border); }
.tf-map-zoom button {
  width: 28px; height: 28px;
  background: transparent;
  border: 0;
  color: var(--text-2);
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  transition: background 160ms ease;
}
.tf-map-zoom button:hover {
  background: color-mix(in srgb, var(--accent-teal) 14%, transparent);
  color: var(--text-1);
}
.tf-map-zoom button + button { border-top: 1px solid var(--border); }

/* Map attribution + scale bar */
.tf-map-attribution {
  position: absolute;
  bottom: 4px; right: 6px;
  font-family: var(--type-mono);
  font-size: 9.5px;
  color: rgba(255,255,255,0.75);
  background: rgba(11,15,20,0.45);
  padding: 2px 6px;
  border-radius: 3px;
  z-index: 1;
}
.tf-map-scale {
  position: absolute;
  bottom: 12px; left: 12px;
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  z-index: 1;
}
.tf-map-scale-bar {
  width: 80px;
  height: 6px;
  background: linear-gradient(to right, #fff 0%, #fff 50%, transparent 50%, transparent 100%);
  background-size: 40px 6px;
  border: 1px solid rgba(11,15,20,0.55);
  border-top: 0;
}
.tf-map-scale-label {
  font-family: var(--type-mono);
  font-size: 9.5px;
  color: #fff;
  text-shadow: 0 1px 2px rgba(0,0,0,0.6);
  margin-bottom: 2px;
}

/* Image gallery */
.tf-gallery {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
}
@media (max-width: 700px) { .tf-gallery { grid-template-columns: 1fr 1fr; } }
.tf-gallery-thumb {
  aspect-ratio: 1 / 1;
  border-radius: 6px;
  overflow: hidden;
  cursor: pointer;
  position: relative;
  background: color-mix(in srgb, var(--text-1) 6%, var(--bg));
  border: 1px solid var(--border);
  transition: border-color 160ms ease, transform 160ms ease, box-shadow 160ms ease;
}
.tf-gallery-thumb:hover {
  border-color: var(--accent-teal);
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(0,0,0,0.12);
}
.tf-gallery-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.tf-gallery-thumb-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  color: var(--text-3);
  font-family: var(--type-mono);
  font-size: 18px;
}
.tf-gallery-thumb-cap {
  position: absolute;
  bottom: 0; left: 0; right: 0;
  padding: 14px 8px 6px;
  background: linear-gradient(transparent, rgba(11,15,20,0.85));
  color: #fff;
  font-family: var(--type-ui);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-shadow: 0 1px 2px rgba(0,0,0,0.4);
}

/* Lightbox preview */
.tf-lightbox {
  background: rgba(11,15,20,0.92);
  border-radius: 8px;
  padding: 36px 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 16px;
  min-height: 280px;
  position: relative;
}
.tf-lightbox-frame {
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  width: 280px;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  box-shadow: 0 12px 36px rgba(0,0,0,0.4);
}
.tf-lightbox-frame img { width: 100%; height: 100%; object-fit: cover; display: block; }
.tf-lightbox-caption {
  font-family: var(--type-display);
  font-style: italic;
  font-size: 14px;
  color: rgba(255,255,255,0.85);
  text-align: center;
  max-width: 36ch;
}
.tf-lightbox-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(255,255,255,0.10);
  border: 1px solid rgba(255,255,255,0.2);
  color: #fff;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  transition: background 160ms ease;
}
.tf-lightbox-nav:hover { background: rgba(255,255,255,0.2); }
.tf-lightbox-nav--prev { left: 14px; }
.tf-lightbox-nav--next { right: 14px; }
.tf-lightbox-close {
  position: absolute;
  top: 12px; right: 12px;
  background: rgba(255,255,255,0.10);
  border: 1px solid rgba(255,255,255,0.2);
  color: #fff;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  cursor: pointer;
  font-size: 14px;
}
.tf-lightbox-close:hover { background: rgba(255,255,255,0.2); }
.tf-lightbox-count {
  font-family: var(--type-mono);
  font-size: 11px;
  color: rgba(255,255,255,0.6);
  letter-spacing: 0.08em;
}

/* ════════════════════════════════════════════════════════════════════════
   v1117 — Spec v4 additions: Party PC kitchen-sink card + Quest detail
   chassis + Party Assessment + Kills-To-Date cards. Token-translated
   verbatim from spec (--ink → --text-1, --paper → --bg, --rule → --border,
   --moss/--amber/--ember/--gold → --accent-green/-amber/-red/-gold).
═════════════════════════════════════════════════════════════════════════ */

/* ── PARTY PC CARD (Section 11 — kitchen-sink) ─────────────────────────── */
.tf-pc-card {
  display: flex;
  flex-direction: row;
  align-items: stretch;
  overflow: hidden;
  padding: 0;
}
.tf-pc-portrait {
  position: relative;
  flex-shrink: 0;
  width: 200px;
  align-self: stretch;
  background: color-mix(in srgb, var(--text-1) 6%, var(--bg));
  border-right: 1px solid var(--border);
  overflow: hidden;
}
[data-theme="dark"] .tf-pc-portrait {
  background: color-mix(in srgb, var(--text-1) 12%, var(--bg));
  border-right-color: var(--border);
}
.tf-pc-portrait img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--portrait-x, 50%) var(--portrait-y, 50%);
  display: block;
}
.tf-pc-portrait-placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--entity-color, var(--accent-teal));
  background: color-mix(in srgb, var(--entity-color, var(--accent-teal)) 10%, transparent);
  line-height: 1;
}
.tf-pc-portrait-placeholder svg {
  width: 56px;
  height: 56px;
  max-width: 40%;
  max-height: 40%;
  stroke: currentColor;
  stroke-width: 1.4;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  aspect-ratio: 1 / 1;
}
.tf-pc-portrait-crop {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: rgba(11,15,20,0.65);
  border: 1px solid rgba(255,255,255,0.2);
  color: rgba(255,255,255,0.85);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  transition: background 160ms ease;
}
.tf-pc-portrait-crop:hover { background: rgba(11,15,20,0.85); color: #fff; }

.tf-pc-stats {
  flex: 1;
  min-width: 0;
  padding: 14px 16px 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.tf-pc-name-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}
.tf-pc-name-block { min-width: 0; }
.tf-pc-name {
  font-family: var(--type-display);
  font-weight: 600;
  font-size: var(--font-size-pc-name);
  letter-spacing: -0.012em;
  color: var(--text-1);
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  cursor: pointer;
  text-align: left;
  line-height: 1.1;
  transition: color 160ms ease;
}
.tf-pc-name:hover { color: var(--teal-2); }
.tf-pc-player {
  font-family: var(--type-ui);
  font-size: var(--font-size-xs);
  color: var(--text-3);
  margin-top: 2px;
  display: inline-flex;
  gap: 6px;
  flex-wrap: wrap;
}
.tf-pc-player em { font-style: italic; }
.tf-pc-pronouns {
  color: var(--text-3);
  font-size: var(--font-size-xs);
  letter-spacing: 0.02em;
}
.tf-pc-bg {
  font-family: var(--type-display);
  font-style: italic;
  font-size: var(--font-size-sm);
  color: var(--text-3);
  display: block;
  margin-top: 2px;
}
.tf-pc-actions {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  align-items: flex-start;
  flex-shrink: 0;
  justify-content: flex-end;
}
.tf-pc-class-line {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  font-family: var(--type-ui);
  font-size: var(--font-size-sm);
  color: var(--text-2);
}
.tf-pc-class-text { font-weight: 600; color: var(--text-1); }
.tf-pc-role-badge,
.tf-pc-origin-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px 7px;
  border-radius: 999px;
  font-family: var(--type-ui);
  font-size: var(--font-size-2xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  border: 1px solid;
}
.tf-pc-role-badge {
  color: var(--teal-2);
  background: color-mix(in srgb, var(--accent-teal) 10%, transparent);
  border-color: color-mix(in srgb, var(--accent-teal) 28%, transparent);
}
[data-theme="dark"] .tf-pc-role-badge {
  color: #BFE7EA;
  background: color-mix(in srgb, var(--accent-teal) 20%, transparent);
  border-color: color-mix(in srgb, var(--accent-teal) 40%, transparent);
}
.tf-pc-role-badge--noncombat {
  color: #5E2A78;
  background: color-mix(in srgb, #7E3FA0 11%, transparent);
  border-color: color-mix(in srgb, #7E3FA0 30%, transparent);
}
[data-theme="dark"] .tf-pc-role-badge--noncombat {
  color: #C8A5DC;
  background: color-mix(in srgb, #7E3FA0 24%, transparent);
  border-color: color-mix(in srgb, #7E3FA0 42%, transparent);
}
.tf-pc-origin-badge {
  color: var(--text-3);
  background: color-mix(in srgb, var(--text-1) 5%, transparent);
  border-color: var(--border);
  font-style: italic;
}
.tf-pc-ai-banner {
  font-family: var(--type-ui);
  font-size: var(--font-size-xs);
  color: #7C4117;
  background: color-mix(in srgb, var(--accent-amber) 12%, var(--bg));
  border: 1px solid color-mix(in srgb, var(--accent-amber) 30%, transparent);
  border-radius: 4px;
  padding: 6px 10px;
  display: flex;
  align-items: center;
  gap: 6px;
}
[data-theme="dark"] .tf-pc-ai-banner {
  color: #F1B97A;
  background: color-mix(in srgb, var(--accent-amber) 22%, var(--bg-2));
  border-color: color-mix(in srgb, var(--accent-amber) 42%, transparent);
}

/* Combat strip — HP / AC / Init / Speed */
.tf-pc-combat {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 6px;
}
.tf-pc-stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  background: color-mix(in srgb, var(--text-1) 3%, var(--bg));
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 8px;
  text-align: center;
  font-family: var(--type-mono);
  font-size: var(--font-size-stat);
  font-weight: 700;
  color: var(--text-1);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
.tf-pc-stat-label {
  display: block;
  font-family: var(--type-ui);
  font-size: var(--font-size-2xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
  line-height: 1;
}
.tf-pc-stat-val {
  display: inline-flex;
  align-items: baseline;
  gap: 3px;
  line-height: 1;
}
[data-theme="dark"] .tf-pc-stat {
  background: color-mix(in srgb, var(--text-1) 12%, var(--bg));
  border-color: var(--border);
}
.tf-pc-adv {
  display: inline-block;
  background: var(--accent-teal);
  color: #fff;
  font-size: var(--font-size-2xs);
  font-weight: 700;
  border-radius: 3px;
  padding: 0 3px;
  margin-left: 3px;
  vertical-align: 2px;
  letter-spacing: 0.06em;
}

/* Ability grid — STR/DEX/CON/INT/WIS/CHA + 3 passive scores */
.tf-pc-abilities {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 5px;
}
.tf-pc-abilities--passive { grid-template-columns: repeat(3, 1fr); }
.tf-pc-ab {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  background: color-mix(in srgb, var(--text-1) 3%, var(--bg));
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 5px 4px;
  text-align: center;
  font-family: var(--type-mono);
  line-height: 1.05;
}
[data-theme="dark"] .tf-pc-ab { background: color-mix(in srgb, var(--text-1) 12%, var(--bg)); }
.tf-pc-ab-label {
  display: block;
  font-family: var(--type-ui);
  font-size: var(--font-size-2xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
  line-height: 1;
}
.tf-pc-ab-mod {
  display: block;
  font-size: var(--font-size-stat);
  font-weight: 700;
  color: var(--text-1);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.tf-pc-ab-score {
  display: block;
  font-size: var(--font-size-2xs);
  color: var(--text-3);
  line-height: 1;
}
.tf-pc-ab.is-best,
.tf-pc-ab.is-tied-best {
  background: color-mix(in srgb, var(--accent-green) 16%, var(--bg));
  border-color: color-mix(in srgb, var(--accent-green) 45%, transparent);
}
.tf-pc-ab.is-best .tf-pc-ab-mod,
.tf-pc-ab.is-tied-best .tf-pc-ab-mod { color: #355024; }
.tf-pc-ab.is-near-best {
  background: color-mix(in srgb, var(--accent-green) 7%, var(--bg));
  border-color: color-mix(in srgb, var(--accent-green) 30%, transparent);
}
[data-theme="dark"] .tf-pc-ab.is-best,
[data-theme="dark"] .tf-pc-ab.is-tied-best {
  background: color-mix(in srgb, var(--accent-green) 22%, var(--bg-2));
  border-color: color-mix(in srgb, var(--accent-green) 50%, transparent);
}
[data-theme="dark"] .tf-pc-ab.is-best .tf-pc-ab-mod,
[data-theme="dark"] .tf-pc-ab.is-tied-best .tf-pc-ab-mod { color: #B5DAA1; }

/* Skill pill grid — 3 cols × 6 rows of 18 skills */
.tf-pc-skills {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px;
}
.tf-pc-skill {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 3px 8px;
  background: color-mix(in srgb, var(--text-1) 3%, var(--bg));
  border: 1px solid var(--border);
  border-radius: 4px;
  font-family: var(--type-ui);
  font-size: var(--font-size-xs);
  color: var(--text-2);
}
[data-theme="dark"] .tf-pc-skill { background: color-mix(in srgb, var(--text-1) 10%, var(--bg)); }
.tf-pc-skill-name {
  font-weight: 500;
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tf-pc-skill-val {
  flex-shrink: 0;
  font-family: var(--type-mono);
  font-variant-numeric: tabular-nums;
  color: var(--text-1);
  font-weight: 600;
  display: inline-flex;
  align-items: baseline;
  gap: 3px;
}
.tf-pc-skill.is-best,
.tf-pc-skill.is-tied {
  background: color-mix(in srgb, var(--accent-green) 12%, var(--bg));
  border-color: color-mix(in srgb, var(--accent-green) 40%, transparent);
  color: #355024;
}
.tf-pc-skill.is-best .tf-pc-skill-val,
.tf-pc-skill.is-tied .tf-pc-skill-val { color: #355024; }
.tf-pc-skill.is-near {
  background: color-mix(in srgb, var(--accent-green) 6%, var(--bg));
  border-color: color-mix(in srgb, var(--accent-green) 25%, transparent);
}
[data-theme="dark"] .tf-pc-skill.is-best,
[data-theme="dark"] .tf-pc-skill.is-tied {
  background: color-mix(in srgb, var(--accent-green) 22%, var(--bg-2));
  color: #B5DAA1;
  border-color: color-mix(in srgb, var(--accent-green) 45%, transparent);
}
[data-theme="dark"] .tf-pc-skill.is-best .tf-pc-skill-val,
[data-theme="dark"] .tf-pc-skill.is-tied .tf-pc-skill-val { color: #B5DAA1; }
.tf-pc-skill-dash { color: var(--text-3); }

/* Tag rows — Defenses, Senses, Languages, Profs, Tools, Items */
.tf-pc-tags-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
  font-family: var(--type-ui);
  font-size: var(--font-size-xs);
}
.tf-pc-tags-label {
  font-family: var(--type-ui);
  font-size: var(--font-size-2xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
  flex-shrink: 0;
  min-width: 64px;
}
.tf-pc-tag {
  display: inline-flex;
  align-items: center;
  padding: 1px 7px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: color-mix(in srgb, var(--text-1) 4%, var(--bg));
  color: var(--text-2);
  font-size: var(--font-size-2xs);
}
.tf-pc-tag--unique {
  background: color-mix(in srgb, var(--accent-gold) 11%, var(--bg));
  border-color: color-mix(in srgb, var(--accent-gold) 32%, transparent);
  color: #5C4716;
  font-weight: 600;
}
[data-theme="dark"] .tf-pc-tag {
  background: color-mix(in srgb, var(--text-1) 14%, var(--bg));
  color: var(--text-2);
}
[data-theme="dark"] .tf-pc-tag--unique {
  background: color-mix(in srgb, var(--accent-gold) 22%, var(--bg-2));
  color: #E5C97D;
  border-color: color-mix(in srgb, var(--accent-gold) 45%, transparent);
}

.tf-pc-gap {
  height: 1px;
  background: var(--border);
  margin: 4px 0;
}

.tf-pc-companions {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: 6px;
  padding-top: 10px;
  border-top: 1px dashed var(--border);
}
.tf-pc-companion-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px 2px 4px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg);
  color: var(--text-2);
  font-family: var(--type-ui);
  font-size: 11px;
  cursor: pointer;
}
.tf-pc-companion-chip:hover { border-color: var(--accent-teal); color: var(--text-1); }
.tf-pc-companion-emoji { font-size: 12px; }
[data-theme="dark"] .tf-pc-companion-chip { background: rgba(255,255,255,0.06); }

/* ── QUEST DETAIL CARD (Section 11 supplement) ─────────────────────────── */
.tf-quest-detail { }
.tf-quest-status-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding-bottom: 10px;
  margin-bottom: 10px;
  border-bottom: 1px dashed var(--border);
}
.tf-quest-progress {
  flex: 1;
  min-width: 120px;
  height: 6px;
  background: color-mix(in srgb, var(--text-1) 6%, var(--bg));
  border: 1px solid var(--border);
  border-radius: 999px;
  overflow: hidden;
  position: relative;
}
.tf-quest-progress-fill {
  height: 100%;
  background: linear-gradient(to right, var(--accent-teal), var(--accent-green));
  border-radius: 999px;
  transition: width 320ms ease;
}
.tf-quest-progress-label {
  font-family: var(--type-mono);
  font-size: 10.5px;
  font-weight: 600;
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}

.tf-quest-obj {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.tf-quest-obj li {
  display: grid;
  grid-template-columns: 20px 1fr auto;
  align-items: baseline;
  gap: 10px;
  padding: 6px 4px;
  border-bottom: 1px dashed color-mix(in srgb, var(--border) 60%, transparent);
  font-family: var(--type-display);
  font-size: 14px;
  line-height: 1.5;
  color: var(--text-1);
}
.tf-quest-obj li:last-child { border-bottom: 0; }
.tf-quest-obj label { display: contents; cursor: pointer; }
.tf-quest-obj input[type="checkbox"] {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}
.tf-quest-obj-box {
  width: 16px;
  height: 16px;
  border-radius: 4px;
  border: 1.5px solid var(--text-3);
  background: #FCFAF4;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition: border-color 160ms ease, background 160ms ease;
  margin-top: 3px;
}
[data-theme="dark"] .tf-quest-obj-box {
  background: color-mix(in srgb, var(--text-1) 4%, var(--bg));
  border-color: var(--text-3);
}
.tf-quest-obj label:hover .tf-quest-obj-box { border-color: var(--accent-teal); }
.tf-quest-obj input:checked + .tf-quest-obj-box {
  background: var(--accent-green);
  border-color: var(--accent-green);
}
.tf-quest-obj input:checked + .tf-quest-obj-box::after {
  content: '';
  width: 4px;
  height: 8px;
  border: solid #fff;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg) translate(-1px, -1px);
}
.tf-quest-obj-text { color: var(--text-1); }
.tf-quest-obj-meta {
  font-family: var(--type-mono);
  font-size: 10.5px;
  color: var(--text-3);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.tf-quest-obj li.is-done .tf-quest-obj-text,
.tf-quest-obj input:checked ~ .tf-quest-obj-text {
  color: var(--text-3);
  text-decoration: line-through;
  text-decoration-color: color-mix(in srgb, var(--accent-green) 60%, transparent);
}
.tf-quest-obj li.is-done { opacity: 0.7; }
.tf-quest-obj li.is-optional .tf-quest-obj-text { font-style: italic; }
.tf-quest-obj li.is-optional::before {
  content: '☆';
  color: var(--accent-amber);
  margin-right: 2px;
}
.tf-quest-obj-hdr {
  display: block;
  padding: 8px 4px 4px;
  font-family: var(--type-ui);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--text-3);
  border-bottom: 0;
}
.tf-quest-obj-hdr:first-child { padding-top: 0; }

/* ── PARTY ASSESSMENT (.tf-pa-card) ──────────────────────────────────── */
.tf-pa-card {
  padding: 14px 18px 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.tf-pa-hdr {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.tf-pa-eyebrow {
  font-family: var(--type-ui);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-3);
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.tf-pa-date {
  font-family: var(--type-mono);
  font-size: 10px;
  color: var(--text-3);
  letter-spacing: 0.04em;
}
.tf-pa-tldr {
  font-family: var(--type-display);
  font-style: italic;
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--text-2);
  border-left: 3px solid var(--accent-teal);
  padding: 6px 14px;
  margin: 0;
  background: color-mix(in srgb, var(--accent-teal) 5%, transparent);
}
.tf-pa-summary {
  font-family: var(--type-display);
  font-size: 14px;
  line-height: 1.65;
  color: var(--text-2);
  margin: 0;
}
.tf-pa-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}
@media (max-width: 700px) { .tf-pa-grid { grid-template-columns: 1fr; } }
.tf-pa-section {
  background: color-mix(in srgb, var(--text-1) 3%, var(--bg));
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 9px 12px 10px;
  border-left: 3px solid var(--section-color, var(--border));
}
[data-theme="dark"] .tf-pa-section { background: color-mix(in srgb, var(--text-1) 10%, var(--bg)); }
.tf-pa-section--strengths { --section-color: var(--accent-green); }
.tf-pa-section--gaps      { --section-color: var(--accent-red); }
.tf-pa-section--synergies { --section-color: var(--accent-teal); }
.tf-pa-section--watchouts { --section-color: var(--accent-amber); }
.tf-pa-section-label {
  font-family: var(--type-ui);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--section-color);
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 6px;
}
.tf-pa-count {
  font-family: var(--type-mono);
  font-size: 9.5px;
  font-weight: 500;
  color: var(--text-3);
  background: color-mix(in srgb, var(--text-1) 6%, transparent);
  padding: 1px 5px;
  border-radius: 999px;
  letter-spacing: 0;
}
.tf-pa-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
.tf-pa-chip {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 999px;
  font-family: var(--type-ui);
  font-size: 11px;
  color: var(--section-color);
  background: color-mix(in srgb, var(--section-color) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--section-color) 26%, transparent);
}
[data-theme="dark"] .tf-pa-chip {
  background: color-mix(in srgb, var(--section-color) 22%, transparent);
  color: color-mix(in srgb, var(--section-color) 35%, #fff);
  border-color: color-mix(in srgb, var(--section-color) 42%, transparent);
}
.tf-pa-tip {
  background: color-mix(in srgb, var(--accent-gold) 10%, var(--bg));
  border: 1px solid color-mix(in srgb, var(--accent-gold) 30%, transparent);
  border-radius: 4px;
  padding: 8px 12px;
  font-family: var(--type-ui);
  font-size: 12.5px;
  color: #5C4716;
  line-height: 1.5;
}
[data-theme="dark"] .tf-pa-tip {
  color: #E5C97D;
  background: color-mix(in srgb, var(--accent-gold) 20%, var(--bg-2));
  border-color: color-mix(in srgb, var(--accent-gold) 40%, transparent);
}
.tf-pa-tip strong { color: inherit; }
.tf-pa-stage-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  border-top: 1px dashed var(--border);
  padding-top: 8px;
}
.tf-pa-stage-label {
  font-family: var(--type-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
}

/* ── KILLS TO DATE / ACCOMPLISHMENTS (.tf-ktd-card) ──────────────────── */
.tf-ktd-card {
  padding: 0;
  overflow: hidden;
}
.tf-ktd-summary {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  background: var(--border);
  border-bottom: 1px solid var(--border);
}
.tf-ktd-stat {
  background: color-mix(in srgb, var(--text-1) 3%, var(--bg));
  padding: 14px 12px 12px;
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
[data-theme="dark"] .tf-ktd-stat { background: color-mix(in srgb, var(--text-1) 10%, var(--bg)); }
.tf-ktd-stat-num {
  font-family: var(--type-display);
  font-weight: 500;
  font-size: 28px;
  letter-spacing: -0.012em;
  color: var(--text-1);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.tf-ktd-stat-lbl {
  font-family: var(--type-ui);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--text-3);
}
.tf-ktd-sections {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1px;
  background: var(--border);
}
@media (max-width: 700px) { .tf-ktd-sections { grid-template-columns: 1fr; } }
.tf-ktd-section {
  background: var(--bg);
  padding: 12px 14px 14px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 180px;
}
.tf-ktd-section-hdr {
  font-family: var(--type-ui);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--text-2);
  display: flex;
  align-items: center;
  gap: 6px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--border);
}
.tf-ktd-section-hdr--kills      { color: var(--ember-soft); }
.tf-ktd-section-hdr--collateral { color: var(--accent-amber); }
.tf-ktd-section-hdr--conquests  { color: var(--accent-green); }
.tf-ktd-section-hdr--nearwipes  { color: var(--accent-red); }
[data-theme="dark"] .tf-ktd-section-hdr--kills      { color: #F5C0AE; }
[data-theme="dark"] .tf-ktd-section-hdr--collateral { color: #F1B97A; }
[data-theme="dark"] .tf-ktd-section-hdr--conquests  { color: #B5DAA1; }
[data-theme="dark"] .tf-ktd-section-hdr--nearwipes  { color: #F5C0AE; }
.tf-ktd-scroll {
  overflow-y: auto;
  max-height: 180px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-right: 4px;
}
.tf-ktd-row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
  gap: 8px;
  padding: 6px 8px;
  background: color-mix(in srgb, var(--text-1) 3%, var(--bg));
  border: 1px solid var(--border);
  border-radius: 4px;
  font-family: var(--type-ui);
  font-size: 12px;
  color: var(--text-1);
  line-height: 1.4;
}
[data-theme="dark"] .tf-ktd-row { background: color-mix(in srgb, var(--text-1) 12%, var(--bg)); }
.tf-ktd-row--bbeg {
  border-left: 3px solid var(--accent-gold);
  background: color-mix(in srgb, var(--accent-gold) 8%, var(--bg));
}
[data-theme="dark"] .tf-ktd-row--bbeg {
  background: color-mix(in srgb, var(--accent-gold) 18%, var(--bg));
  border-color: color-mix(in srgb, var(--accent-gold) 35%, transparent);
}
.tf-ktd-row-name { font-weight: 600; color: var(--text-1); }
.tf-ktd-row-detail {
  color: var(--text-3);
  font-style: italic;
  font-size: 11.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tf-ktd-row-meta {
  font-family: var(--type-mono);
  font-size: 10px;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.tf-ktd-subhdr {
  font-family: var(--type-ui);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-top: 4px;
  margin-bottom: 2px;
}
.tf-ktd-subhdr:first-child { margin-top: 0; }
.tf-ktd-empty {
  font-family: var(--type-display);
  font-style: italic;
  font-size: 12px;
  color: var(--text-3);
  padding: 8px 4px;
}

/* Uniform checkbox + radio styling */
input[type="checkbox"],
input[type="radio"] {
  width: 1rem; height: 1rem;
  accent-color: var(--accent-teal);
  cursor: pointer; flex-shrink: 0;
}

.error { color: var(--accent-red); font-size: var(--font-size-base); }

/* ── Tab nav ─────────────────────────────────────────────────────────────── */
/* v1122: aligned to spec .tf-topnav — paper bg with subtle ink wash, 16px
   right padding so utility cluster has breathing room, 1px border-bottom
   rule, and 4px top corner radius so the nav reads as a chrome strip
   framing the app body. `--nav-bg` is a local custom property the scroll-
   fade gradient inherits so it always matches the nav surface even in
   dark mode.
   v1125: switched align-items flex-end -> center so tab labels vertically
   align with the back chevron + Threadfall mark in .nav-left instead of
   anchoring to the bottom border. Active-tab underline now sits a few
   pixels below the text within the nav (no longer merging with the nav
   border) — cleaner reading at the cost of a small visual seam. */
#tab-nav {
  --nav-bg: color-mix(in srgb, var(--text-1) 3%, var(--bg));
  display: flex;
  gap: 0;
  align-items: center;
  padding-top: 4px;
  padding-right: 16px;
  background: var(--nav-bg);
  border-bottom: 1px solid var(--border);
  border-radius: 4px 4px 0 0;
  overflow: hidden;
}
[data-theme="dark"] #tab-nav { --nav-bg: var(--bg-2); }

/* Left cluster: profile avatar + back button (chevron + logo) */
/* v1122: aligned to spec .tf-topnav-left — 6px vertical / 6px right / 10px left
   padding, 4px gap between back button and any sibling avatar. */
.nav-left {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
  padding: 6px 6px 6px 10px;
}

/* Profile button — sits inside tab-scroll-area, immediately after last tab.
   Mirrors the campaign-select #cs-profile-btn treatment (card border, lift,
   teal hover) at the smaller 28px tab-bar scale. */
#tab-nav .nav-profile-btn,
#profile-nav-btn {
  margin: 0 0.25rem 0 0.5rem;
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--bg-2);
  box-shadow: 0 2px 6px rgba(0,0,0,0.08);
  overflow: hidden;
  cursor: pointer;
  transition: border-color var(--t-hover) ease,
              box-shadow   var(--t-hover) ease,
              transform    var(--t-hover) ease;
}
#profile-nav-btn:hover, #profile-nav-btn:focus-visible {
  border-color: rgba(47,154,163,0.45);
  box-shadow: 0 4px 10px rgba(0,0,0,0.12);
  transform: translateY(-1px);
}
/* Inner avatar fills the button — the button now owns the border. */
#profile-nav-btn .nav-avatar-img {
  width: 100%; height: 100%;
  border: none;
  box-shadow: none;
}
#profile-nav-btn:hover .nav-avatar-img,
#profile-nav-btn:focus-visible .nav-avatar-img { border: none; box-shadow: none; }

/* Back button wraps chevron + logo as one click target */
/* v798: bumped resting from --text-3 to --text-2 — primary nav affordance
   needs to read clearly on light theme (--text-3 was 2.4:1 against --bg). */
.nav-back-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.15rem;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0.4rem 0.2rem 0.4rem 0.25rem;
  border-radius: var(--radius-md);
  transition: background var(--t-hover) var(--ease);
  color: var(--text-2);
}
.nav-back-btn:hover, .nav-back-btn:focus-visible { background: var(--bg-3); color: var(--text-1); }
.nav-back-btn:hover .nav-logo, .nav-back-btn:focus-visible .nav-logo { opacity: 1; }

.nav-back-chevron {
  font-size: 1.3rem;
  line-height: 1;
  font-weight: 300;
  color: var(--text-2);
  transition: color var(--t-hover) var(--ease), transform var(--t-hover) var(--ease);
  display: inline-block;
  margin-right: 0.1rem;
}
.nav-back-btn:hover .nav-back-chevron, .nav-back-btn:focus-visible .nav-back-chevron {
  color: var(--text-1);
  transform: translateX(-2px);
}

/* Scrollable wrapper for tabs + profile — takes all remaining space, Live stays pinned right */
#tab-scroll-wrap {
  flex: 1;
  min-width: 0;            /* allow shrinking inside the flex nav */
  position: relative;      /* anchor for the fade pseudo-elements */
  overflow: hidden;
  display: flex;
  align-items: stretch;
}
/* v1125: tab strip centers vertically (was flex-end + -1px overlap) so the
   tab labels line up with the back chevron + Threadfall mark in .nav-left.
   The active-tab underline now sits below the text within the nav rather
   than on the nav border. */
#tab-scroll-area {
  display: flex;
  align-items: center;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;   /* Firefox */
}
#tab-scroll-area::-webkit-scrollbar { display: none; }  /* Chrome/Safari */

/* Fade-out right edge only — hints tabs are scrollable, left has no overflow.
   v1122: inherit --nav-bg from #tab-nav so the gradient stops at the actual
   nav surface (which now varies by theme via color-mix). */
#tab-scroll-wrap::after {
  content: '';
  position: absolute;
  top: 0; bottom: 0; right: 0;
  width: 1.5rem;
  pointer-events: none;
  z-index: 1;
  background: linear-gradient(to left, var(--nav-bg, var(--bg-2)), transparent);
}
.nav-logo {
  height: 28px;
  width: auto;
  opacity: 0.9;
  flex-shrink: 0;
  /* v1092: drop-shadow PNG outline removed — SVG lockup carries its own color. */
  transition: opacity var(--t-hover) var(--ease);
}

/* ── Right utility cluster (gear + profile) ──────────────────────────────
   Sits between #tab-scroll-wrap and .nav-right. Right-justified, always
   visible, doesn't scroll with the tab strip. v777 / v1125: nav now
   centers everything vertically (parent #tab-nav uses align-items: center),
   so the cluster floats at vertical center without the legacy margin-
   bottom: 6px anchor. 8px gap between icons, 8px left padding separator
   from the tab strip. */
.nav-utility-cluster {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
  padding-left: 8px;
}
/* v798: theme-correct base + dark override (mirrors .btn-new refactor).
   Round 28px utility button — sits in the right utility cluster of the
   in-campaign nav. Pre-v798 used rgba(255,255,255,*) which made the gear
   button invisible on light theme. */
.nav-utility-btn {
  width: 28px;
  height: 28px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: color-mix(in srgb, var(--text-1) 4%, transparent);
  color: var(--text-2);
  font-size: var(--font-size-md);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
  transition: transform    var(--t-hover) ease,
              background   var(--t-hover) ease,
              border-color var(--t-hover) ease,
              color        var(--t-hover) ease,
              box-shadow   var(--t-hover) ease;
}
.nav-utility-btn:hover, .nav-utility-btn:focus-visible {
  color: var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 10%, transparent);
  border-color: color-mix(in srgb, var(--accent-teal) 50%, transparent);
  box-shadow: 0 0 14px 2px color-mix(in srgb, var(--accent-teal) 18%, transparent);
  transform: translateY(-1px);
}
.nav-utility-btn.active {
  color: var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 14%, transparent);
  border-color: color-mix(in srgb, var(--accent-teal) 60%, transparent);
}
.nav-utility-btn:active {
  transform: translateY(0);
  transition-duration: var(--t-press);
}
[data-theme="dark"] .nav-utility-btn {
  border-color: rgba(255,255,255,0.32);
  background: rgba(255,255,255,0.05);
  color: rgba(255,255,255,0.92);
}
[data-theme="dark"] .nav-utility-btn:hover,
[data-theme="dark"] .nav-utility-btn:focus-visible {
  color: #fff;
  background: rgba(255,255,255,0.12);
  border-color: rgba(255,255,255,0.55);
  box-shadow: 0 0 14px 2px rgba(255,255,255,0.18);
}
[data-theme="dark"] .nav-utility-btn.active {
  color: #fff;
  background: rgba(255,255,255,0.14);
  border-color: rgba(255,255,255,0.6);
}

/* v1101: aligned to spec .tf-tab — 13px / 500 / 0.06em gap / teal-3
   underline on .active (was previously transparent border-bottom).
   Tab markers (.tf-tab-mark) inherit from spec recipe (opacity 0.5 → 1
   on hover/active). */
.tab-btn {
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  padding: 9px 14px 8px;
  font-family: var(--type-ui);
  font-size: 13px;
  font-weight: 500;
  color: var(--text-3);
  cursor: pointer;
  transition: color 160ms ease, border-color 160ms ease;
  /* v1125: dropped margin-bottom: -1px — nav now centers tabs vertically,
     so the active-tab underline lives inside the nav rather than overlapping
     the nav border. */
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}
.tab-btn:hover, .tab-btn:focus-visible { color: var(--text-1); }
.tab-btn.active {
  color: var(--accent-teal);
  border-bottom-color: var(--accent-teal);
}

/* Threadfall mark in tabs */
.tf-tab-mark {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
  opacity: 0.5;
  transition: opacity var(--t-hover);
}
.tab-btn.active .tf-tab-mark,
.tab-btn:hover .tf-tab-mark, .tab-btn:focus-visible .tf-tab-mark { opacity: 1; }
.live-nav-btn { flex-shrink: 0; color: var(--color-gold); font-weight: 700; }

/* ── Live nav button — premium action pill ───────────────────────────── */
#live-nav-btn {
  border: 1px solid color-mix(in srgb, var(--color-gold) 55%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--color-gold) 55%, transparent);
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--color-gold) 18%, transparent);
  /* v1122: nav now owns the right-edge gap (16px padding-right on #tab-nav),
     so drop the legacy 1.25rem right margin to avoid a double-gap. */
  margin: 0 0 0 0.5rem;
  padding: 0.38rem 1rem;
  animation: live-btn-flicker 3.6s ease-in-out infinite;
  transition: background var(--t-hover) var(--ease),
              border-color var(--t-hover) var(--ease),
              box-shadow var(--t-hover) var(--ease);
}
#live-nav-btn .tf-tab-mark { opacity: 0.85; }
#live-nav-btn:hover, #live-nav-btn:focus-visible {
  color: var(--color-gold);
  background: color-mix(in srgb, var(--color-gold) 26%, transparent);
  border-color: color-mix(in srgb, var(--color-gold) 75%, transparent);
  box-shadow: 0 0 14px 3px color-mix(in srgb, var(--color-gold) 25%, transparent);
}
#live-nav-btn:hover .tf-tab-mark, #live-nav-btn:focus-visible .tf-tab-mark { opacity: 1; }
/* Candle-flicker: irregular brief dips + a steady mid-cycle glow. The
   asymmetric keyframe times prevent it from reading as a smooth pulse. */
@keyframes live-btn-flicker {
  0%, 100% {
    box-shadow: 0 0 0 0 transparent;
    background: color-mix(in srgb, var(--color-gold) 18%, transparent);
  }
  17% { background: color-mix(in srgb, var(--color-gold) 9%, transparent); }
  20% { background: color-mix(in srgb, var(--color-gold) 24%, transparent); }
  50% {
    box-shadow: 0 0 12px 3px color-mix(in srgb, var(--color-gold) 24%, transparent);
    background: color-mix(in srgb, var(--color-gold) 20%, transparent);
  }
  73% { background: color-mix(in srgb, var(--color-gold) 11%, transparent); }
  77% { background: color-mix(in srgb, var(--color-gold) 22%, transparent); }
}
/* .nav-auth-btns removed — profile moved to nav-left, switch-campaign removed */
.nav-gm-btn {
  padding: 0.2rem 0.55rem;
  font-size: var(--font-size-sm); font-weight: 600; cursor: pointer;
  border-radius: var(--radius-md); border: 1px solid;
  border-color: var(--accent-teal); color: var(--accent-teal);
  background: none;
  transition: background var(--t-hover), color var(--t-hover);
}
.nav-gm-btn:hover, .nav-gm-btn:focus-visible { background: var(--accent-teal); color: #fff; }

/* ── Nav profile button ──────────────────────────────────────────────────── */
.nav-profile-btn {
  background: none; border: none; cursor: pointer; padding: 0;
  display: flex; align-items: center; justify-content: center;
}
.nav-avatar-img {
  width: 28px; height: 28px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: var(--font-size-xs); font-weight: 700; letter-spacing: 0.02em;
  overflow: hidden; flex-shrink: 0;
  border: 2px solid var(--accent-teal);
  background: var(--bg-3); color: var(--text-2);
  transition: border-color var(--t-hover), box-shadow var(--t-hover);
}
.nav-avatar-img img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.nav-profile-btn:hover .nav-avatar-img, .nav-profile-btn:focus-visible .nav-avatar-img { border-color: var(--accent-teal); box-shadow: 0 0 0 2px rgba(47,124,133,0.25); }

/* ── Campaign selector profile button — fixed top-right, mirrors admin pulse layout ── */
/* Outer button gets the campaign-card border & hover treatment so it reads as the
   visual sibling of the cards (instead of a tiny floating avatar). */
#cs-profile-btn {
  position: fixed;
  top: 1.5rem;
  right: 1.5rem;
  z-index: 10003;
  width: 40px;
  height: 40px;
  padding: 0;
  margin: 0;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--bg-2);
  box-shadow: 0 2px 6px rgba(0,0,0,0.08);
  overflow: hidden;
  cursor: pointer;
  transition: border-color var(--t-hover) ease,
              box-shadow var(--t-hover) ease,
              transform var(--t-hover) ease;
}
#cs-profile-btn:hover, #cs-profile-btn:focus-visible {
  border-color: rgba(47,154,163,0.45);
  box-shadow: 0 4px 12px rgba(0,0,0,0.12);
  transform: translateY(-1px);
}
/* Inner avatar fills the button — the button now owns the border. */
#cs-profile-btn .nav-avatar-img {
  width: 100%; height: 100%;
  border: none;
  box-shadow: none;
}
#cs-profile-btn:hover .nav-avatar-img,
#cs-profile-btn:focus-visible .nav-avatar-img { border: none; box-shadow: none; }

/* ── Profile modal ────────────────────────────────────────────────────────── */
.profile-modal-box {
  /* v1196: composes canonical .modal-box (background + 3-layer shadow +
     dark-mode lift to var(--bg-2)). This class only contributes the
     profile-specific shape: wider chassis (640px > .modal-box--lg's
     560px), rounded corners, flex column for sticky-footer layout,
     scrollable body inside. The background + box-shadow that were
     duplicated here pre-v1196 now come from .modal-box so dark mode
     lifts the surface consistently with every other modal. */
  border-radius: var(--radius-3xl);
  width: 640px; max-width: calc(100vw - 2rem);
  max-height: calc(100vh - 2rem);
  position: relative; display: flex; flex-direction: column;
  animation: modal-in 0.18s ease;
  overflow: hidden;  /* override .modal-box overflow-y:auto — body scrolls instead */
  padding: 0;
}
.profile-modal-close {
  position: absolute; top: 1rem; right: 1rem;
  z-index: 2;
}
/* Scrollable body — contains avatar, fields, founding/sub/usage cards,
   change-password. Padding here, not on the box, so the scroll edge is
   clean. Overflow-y: auto kicks in when content exceeds the cap. */
.profile-modal-body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 2rem 2rem 1rem;
  display: flex; flex-direction: column; align-items: center; gap: 1.5rem;
  /* Subtle inner-scroll affordance — the bottom edge of the body fades
     out slightly to hint there's more below the fold without needing
     a dedicated scrollbar indicator. */
  mask-image: linear-gradient(to bottom, black calc(100% - 1.5rem), transparent);
  -webkit-mask-image: linear-gradient(to bottom, black calc(100% - 1.5rem), transparent);
}
/* Sticky footer — Save/Logout + bottom utility links. Stays visible
   regardless of scroll. Top border separates it from the scrolling
   body so the visual seam is clear. */
.profile-modal-ftr {
  flex: 0 0 auto;
  padding: 1rem 2rem 1.5rem;
  border-top: 1px solid var(--border);
  background: var(--bg);
  border-radius: 0 0 var(--radius-3xl) var(--radius-3xl);
  display: flex; flex-direction: column; gap: 0.75rem; align-items: center;
}
@media (max-width: 480px) {
  .profile-modal-box {
    width: 100%; max-width: 100vw;
    max-height: 100vh;
    border-radius: var(--radius-xl) var(--radius-xl) 0 0;
    margin-top: auto;
  }
  .profile-modal-body { padding: 1.25rem 1.25rem 0.75rem; }
  .profile-modal-ftr  { padding: 0.75rem 1.25rem 1.25rem; }
}
.profile-avatar-section {
  display: flex; flex-direction: column; align-items: center; gap: 0.6rem;
}
/* v791: drop the teal accent + outer glow so the profile avatar matches the
   neutral image-frame treatment used elsewhere (e.g., .ef-img-preview). */
.profile-avatar-lg {
  width: 88px; height: 88px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 2rem; font-weight: 700; letter-spacing: 0.02em;
  overflow: hidden; border: 1px solid var(--border);
  background: var(--bg-3); color: var(--text-2);
}
.profile-avatar-lg img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.profile-avatar-actions {
  display: flex; gap: 0.4rem; align-items: center;
}
/* v1196: .profile-avatar-edit-btn retired — markup now composes
   .btn-ghost.btn-xs (the canonical squared-ghost recipe used by every
   other tight-cluster action). The custom rounded-rect at radius-md
   created a 3rd button family inside one modal; collapsing to two
   (ghost-xs + branded Discord pill) makes the affordance cluster read
   as a single family. */
.profile-discord-btn {
  display: inline-flex; align-items: center; gap: 0.3rem;
  background: var(--color-discord); border: none; border-radius: var(--radius-md);
  padding: 0.25rem 0.65rem; font-size: var(--font-size-sm); font-weight: 600;
  cursor: pointer; color: #fff;
  transition: background var(--t-hover), opacity var(--t-hover);
}
.profile-discord-btn:hover, .profile-discord-btn:focus-visible { background: var(--color-discord-hover); }
.profile-discord-btn:disabled { opacity: 0.5; cursor: default; }
/* Linked state — the button stops being a CTA and becomes a status display
   (still clickable for re-linking to a different Discord account, just
   visually muted to read as "this is the current state" not "do this"). */
.profile-discord-btn.is-linked {
  background: color-mix(in srgb, var(--color-discord) 55%, var(--bg-2));
}
.profile-discord-btn.is-linked:hover,
.profile-discord-btn.is-linked:focus-visible {
  background: color-mix(in srgb, var(--color-discord) 70%, var(--bg-2));
}
.profile-fields {
  width: 100%; display: flex; flex-direction: column; gap: 0.75rem;
}
.profile-field-row {
  display: flex; flex-direction: column; gap: 0.25rem;
}
.profile-label {
  font-size: var(--font-size-xs); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--text-3);
}
.profile-input {
  width: 100%; padding: 0.5rem 0.65rem; border-radius: var(--radius-lg);
  border: 1.5px solid var(--border); background: var(--bg-2);
  color: var(--text); font-size: var(--font-size-md); font-family: inherit;
  transition: border-color var(--t-hover);
}
.profile-input:focus { outline: none; border-color: var(--accent-teal); }
.profile-email-ro {
  font-size: var(--font-size-base); color: var(--text-3); padding: 0.35rem 0;
}
/* v1084: aligned to spec .tf-badge--teal — pill 3px 9px / 10.5px / 600 /
   0.06em uppercase / 1px teal-tinted border (previously borderless + capitalize). */
.profile-role-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: var(--radius-full);
  border: 1px solid color-mix(in srgb, var(--accent-teal) 30%, transparent);
  font-size: 10.5px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 12%, var(--bg));
}
[data-theme="dark"] .profile-role-badge {
  color: #BFE7EA;
  background: color-mix(in srgb, var(--accent-teal) 22%, var(--bg-2));
  border-color: color-mix(in srgb, var(--accent-teal) 45%, transparent);
}
/* v1196: parent .profile-modal-body uses gap: 1.5rem which already
   separates sections — divider's own 0.75rem top+bottom margin was
   compounding with parent gap + per-card margins to ~5rem of dead
   space. Margin zeroed so the divider only contributes its 1px line
   plus the inherited gap. */
.profile-section-divider {
  width: 100%; height: 1px; background: var(--border); margin: 0;
}
/* v1196: typography now inherits from .form-section-label (composed via
   markup) — was duplicating the eyebrow recipe with diverged values
   (700/0.06em vs canonical 600/0.12em). This rule keeps only the
   modal-specific margin so all profile section headers maintain their
   spacing rhythm. */
.profile-section-label {
  margin-bottom: 0.5rem;
}

/* ── Profile modal: Founding banner + Subscription + Usage cards ──────
   New post-launch sections. The Founding banner sits at the top
   (eligibility-gated), Subscription + Usage live as flat cards under
   the identity fields. */
.profile-founding-banner {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.65rem 0.8rem;
  /* v1196: margin removed — parent .profile-modal-body's gap: 1.5rem
     already separates this from neighbors. The pre-v1196 0.75rem extra
     on each side compounded with parent gap to ~3rem of dead space. */
  background: linear-gradient(
    135deg,
    color-mix(in srgb, var(--accent-gold) 18%, var(--bg-2)),
    color-mix(in srgb, var(--accent-teal) 8%, var(--bg-2))
  );
  border: 1px solid color-mix(in srgb, var(--accent-gold) 50%, transparent);
  border-radius: var(--radius-md);
}
.profile-founding-icon { font-size: 1.4rem; flex-shrink: 0; }
.profile-founding-body {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  flex: 1;
  font-size: var(--font-size-sm);
  line-height: 1.4;
}
.profile-founding-body strong { color: var(--accent-gold); }
.profile-founding-desc { color: var(--text); }
.profile-founding-countdown { font-size: var(--font-size-xs); color: var(--color-text-tertiary); }
.profile-founding-claim-btn { flex-shrink: 0; }

/* v1199: referral card — composes the same chassis silhouette as the
   Founding banner above (rounded callout, mixed gradient, accent
   border) but teal-on-teal so it reads as primary-brand instead of
   gold. Two-row layout because we need to fit the share link + Copy
   button below the title/desc — single horizontal row of "title +
   button" would force the link onto a third line and break the
   compact-callout feel. */
.profile-referral-card {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  padding: 0.7rem 0.85rem;
  background: linear-gradient(
    135deg,
    color-mix(in srgb, var(--accent-teal) 18%, var(--bg-2)),
    color-mix(in srgb, var(--accent-teal) 6%, var(--bg-2))
  );
  border: 1px solid color-mix(in srgb, var(--accent-teal) 50%, transparent);
  border-radius: var(--radius-md);
}
.profile-referral-hdr {
  display: flex;
  align-items: flex-start;
  gap: 0.65rem;
}
.profile-referral-icon { font-size: 1.4rem; flex-shrink: 0; line-height: 1.1; }
.profile-referral-body {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  flex: 1;
  font-size: var(--font-size-sm);
  line-height: 1.4;
}
.profile-referral-body strong { color: var(--accent-teal); }
.profile-referral-desc { color: var(--text); }
.profile-referral-stats {
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
}
.profile-referral-link-row {
  display: flex;
  gap: 0.4rem;
  align-items: stretch;
}
.profile-referral-link {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.4rem 0.55rem;
  font-size: var(--font-size-xs);
  font-family: var(--type-mono, ui-monospace, SFMono-Regular, monospace);
  color: var(--text);
  background: color-mix(in srgb, var(--bg-1) 75%, var(--bg-2));
  border: 1px solid color-mix(in srgb, var(--accent-teal) 25%, var(--border));
  border-radius: var(--radius-sm);
  cursor: text;
}
.profile-referral-link:focus {
  outline: none;
  border-color: var(--accent-teal);
}
.profile-referral-copy-btn { flex-shrink: 0; }

/* v1200: inline post-recap referral card — composes the same teal-on-
   teal gradient as .profile-referral-card but a more compact body
   (single-line copy + small CTA + close affordance). Sits at the
   bottom of the recap content via the .referral-promo-slot wrapper. */
.referral-promo-slot {
  margin-top: var(--space-3);
}
.referral-promo-card {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 0.65rem 0.8rem;
  background: linear-gradient(
    135deg,
    color-mix(in srgb, var(--accent-teal) 18%, var(--bg-2)),
    color-mix(in srgb, var(--accent-teal) 6%, var(--bg-2))
  );
  border: 1px solid color-mix(in srgb, var(--accent-teal) 50%, transparent);
  border-radius: var(--radius-md);
  /* IntersectionObserver flips this to opacity:1 + translateY(0) via
     the .referral-promo-card--visible class when the card enters view.
     Resting state is faintly visible so layout doesn't reflow on the
     reveal; only opacity + translate animate. */
  opacity: 1;
  transition: opacity var(--t-hover) var(--ease);
}
.referral-promo-card--dismissed {
  opacity: 0;
  pointer-events: none;
  transition: opacity 180ms ease;
}
.referral-promo-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  font-size: var(--font-size-sm);
  line-height: 1.4;
}
.referral-promo-body strong { color: var(--accent-teal); }
.referral-promo-sub { color: var(--text); }
.referral-promo-cta { flex-shrink: 0; }
.referral-promo-dismiss {
  position: absolute;
  top: 4px;
  right: 6px;
  width: 22px;
  height: 22px;
  padding: 0;
  background: transparent;
  border: 0;
  color: var(--color-text-tertiary);
  font-size: var(--font-size-md);
  line-height: 1;
  cursor: pointer;
  border-radius: var(--radius-sm);
  transition: color var(--t-hover) var(--ease), background var(--t-hover) var(--ease);
}
.referral-promo-dismiss:hover,
.referral-promo-dismiss:focus-visible {
  color: var(--text);
  background: color-mix(in srgb, var(--accent-teal) 10%, transparent);
  outline: none;
}

/* v1202: pending-reveal toggle in the entity modal. Locked state
   greys the label so the disabled checkbox reads as intentionally
   unavailable (Discord not configured) rather than broken. The
   amber --warn helper line below it carries the "set up in settings"
   route. */
.ef-pending-reveal-label--locked {
  opacity: 0.5;
  cursor: not-allowed;
}
#ef-pending-reveal-row code {
  font-family: var(--type-mono, ui-monospace, SFMono-Regular, monospace);
  font-size: 0.92em;
  padding: 0.05rem 0.3rem;
  background: color-mix(in srgb, var(--accent-teal) 10%, transparent);
  border-radius: var(--radius-sm);
}
/* v1210: nested "only me" sub-option — indented under the parent
   "hide from players" box with a teal left-rule so it reads as a
   refinement of the hide choice, not a separate setting. */
.ef-pending-reveal-sub {
  margin-left: 1.5rem;
  margin-top: 0.35rem;
  padding-left: 0.6rem;
  border-left: 2px solid color-mix(in srgb, var(--accent-teal) 30%, transparent);
  font-size: var(--font-size-sm);
}

/* v1200: register-form friend-shared landing banner. Same gradient
   family as the in-app referral card so the cross-context visual
   thread holds. Sits inside the .login-card column above the email
   field — first thing the visitor sees. */
.auth-reg-referral-banner {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0.55rem 0.7rem;
  margin-bottom: var(--space-2);
  background: linear-gradient(
    135deg,
    color-mix(in srgb, var(--accent-teal) 18%, var(--bg-2)),
    color-mix(in srgb, var(--accent-teal) 6%, var(--bg-2))
  );
  border: 1px solid color-mix(in srgb, var(--accent-teal) 50%, transparent);
  border-radius: var(--radius-md);
  font-size: var(--font-size-sm);
}
.auth-reg-referral-icon { font-size: 1.2rem; flex-shrink: 0; line-height: 1.1; }
.auth-reg-referral-body {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  line-height: 1.35;
}
.auth-reg-referral-body strong { color: var(--accent-teal); }
.auth-reg-referral-sub { color: var(--text); }

/* v1200: collapsible referral-code field on signup. Default closed
   so the form stays tight; opens when the user explicitly clicks
   "Have a referral code?" or when initAuth auto-fills a stash
   without showing the URL banner (rare). */
.login-referral-details {
  align-self: stretch;
  margin: 0;
}
.login-referral-summary {
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
  cursor: pointer;
  padding: 0.2rem 0;
  user-select: none;
}
.login-referral-summary:hover { color: var(--text); }
.login-input--code {
  margin-top: var(--space-2);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-family: var(--type-mono, ui-monospace, SFMono-Regular, monospace);
}

.profile-subscription-card,
.profile-usage-card {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  /* v1196: margin-top removed — parent gap: 1.5rem handles separation.
     Pre-v1196 the extra 0.75rem on top of each card compounded with
     parent gap + adjacent banner margin to several rem of dead space. */
  padding: 0.7rem 0.85rem;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
/* v1196: small reset-date caption under the usage card body. Only
   rendered when the user is on a tier with a finite cap (free /
   hobby / standard / master / founding_gm). Hidden when the .hidden
   class is set by JS for Pro / unlimited. */
.profile-usage-reset {
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
  margin: 0;
}
.profile-subscription-body,
.profile-usage-body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.profile-subscription-tier-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.profile-subscription-tier {
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-bold);
  color: var(--text);
}
.profile-subscription-period {
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
}
.profile-subscription-delta {
  margin: 0;
  font-size: var(--font-size-sm);
  line-height: 1.45;
  color: var(--color-text-secondary);
}
.profile-subscription-actions {
  display: flex;
  justify-content: flex-end;
}
.profile-usage-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: var(--font-size-sm);
}
.profile-usage-label { color: var(--color-text-tertiary); }
.profile-usage-value { color: var(--text); font-weight: var(--font-weight-semibold); }
.profile-usage-nudge {
  margin: 0;
  padding: 0.5rem 0.65rem;
  font-size: var(--font-size-xs);
  line-height: 1.4;
  color: var(--text);
  background: color-mix(in srgb, var(--accent-teal) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent-teal) 35%, transparent);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.profile-usage-nudge:hover {
  background: color-mix(in srgb, var(--accent-teal) 14%, transparent);
}

/* ── Upgrade modal ─────────────────────────────────────────────────────
   Fires when a user clicks an AI feature gated above their tier. Locked
   anatomy per UX agent: max-width 420px, centered, no icon header,
   title + body + full-width primary CTA + cancel-anytime micro-line +
   centered ghost-text secondary. Mobile = bottom sheet. */
.upgrade-modal-box {
  width: min(420px, calc(100vw - 2rem));
  max-width: 420px;
  padding: var(--space-5) var(--space-5) var(--space-4);
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
  position: relative;
}
.upgrade-modal-close {
  position: absolute;
  top: 0.4rem;
  right: 0.4rem;
}
.upgrade-modal-title {
  margin: 0 0 var(--space-3);
  padding-right: 2rem;
  font-size: 1.25rem;
  font-weight: var(--font-weight-bold);
  line-height: 1.3;
  color: var(--text);
}
.upgrade-modal-body {
  margin: 0 0 var(--space-4);
  font-size: var(--font-size-sm);
  line-height: 1.55;
  color: var(--color-text-secondary);
}
.upgrade-modal-body p {
  margin: 0 0 0.6rem;
}
.upgrade-modal-body p:last-child { margin-bottom: 0; }
.upgrade-modal-ack {
  padding: 0.4rem 0.55rem;
  margin-bottom: 0.65rem !important;
  background: color-mix(in srgb, var(--accent-teal) 8%, transparent);
  border-left: 2px solid color-mix(in srgb, var(--accent-teal) 50%, transparent);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-xs);
  color: var(--text);
}
.upgrade-modal-bullets {
  margin: 0.3rem 0 0;
  padding: 0 0 0 1rem;
  list-style: none;
}
.upgrade-modal-bullets li {
  position: relative;
  padding-left: 1rem;
  margin-bottom: 0.3rem;
  font-size: var(--font-size-sm);
  color: var(--text);
}
.upgrade-modal-bullets li::before {
  content: '✓';
  position: absolute;
  left: 0;
  color: var(--accent-teal);
  font-weight: var(--font-weight-bold);
}
.upgrade-modal-founding-note {
  padding: 0.45rem 0.6rem;
  font-size: var(--font-size-xs);
  color: var(--accent-gold);
  background: color-mix(in srgb, var(--accent-gold) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent-gold) 35%, transparent);
  border-radius: var(--radius-sm);
}
.upgrade-modal-actions {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.upgrade-modal-primary-btn {
  width: 100%;
  padding: 0.7rem;
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-bold);
}
.upgrade-modal-cancel-anytime {
  margin: 0;
  font-size: 0.7rem;
  color: var(--color-text-tertiary);
  text-align: center;
}
/* v1028: bell + dropdown CSS removed alongside the markup. The
   session-card ".session-new-recap-badge" below is the sole per-user
   recap-unread signal now. The user_notifications data layer + the
   per-user supersession logic remain intact. */
/* v1015 (Task 5 follow-up): per-user "● New" recap badge. Shows when
   the current user has an unread recap_ready notification matching this
   session. Brighter than the Completed badge — this is an active call to
   action ("there's something new for you here"), so it earns the
   saturated teal pill treatment rather than the subdued tint. Clears
   on click-to-expand. */
/* v1084: aligned to spec .tf-badge--teal (filled saturation for "new" call-out). */
.session-new-recap-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: var(--radius-full);
  border: 1px solid color-mix(in srgb, var(--accent-teal) 30%, transparent);
  font-size: 10.5px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 12%, var(--bg));
}
[data-theme="dark"] .session-new-recap-badge {
  color: #BFE7EA;
  background: color-mix(in srgb, var(--accent-teal) 22%, var(--bg-2));
  border-color: color-mix(in srgb, var(--accent-teal) 45%, transparent);
}

/* Session-card "Completed" badge — sits next to or replaces the live
   indicator when last_recap_at IS NOT NULL. Reads as quiet (subdued
   teal) — not a primary signal, just a state acknowledgment. */
/* v1084: aligned to spec .tf-badge--ghost — subdued neutral pill marking
   completed-but-not-active state. Check icon stays teal. */
.session-completed-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: var(--radius-full);
  border: 1px solid var(--border);
  font-size: 10.5px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--text-3);
  background: transparent;
}
.session-completed-badge::before {
  content: '✓';
  color: var(--accent-teal);
  font-weight: var(--font-weight-bold);
}

.upgrade-modal-pro-footnote {
  font-size: var(--font-size-xs);
  text-align: center;
  color: var(--accent-teal);
  text-decoration: none;
  padding: 0.3rem 0;
}
.upgrade-modal-pro-footnote:hover { text-decoration: underline; }
/* v1012: per-session $5.99 top-up. Reads as a secondary CTA — same teal
   accent + footnote weight as the Pro footnote, but as a button rather
   than an inline link because the action commits the user to a Stripe
   redirect. */
.upgrade-modal-buy-session-btn {
  background: transparent;
  border: 1px solid var(--accent-teal);
  border-radius: var(--radius-sm);
  color: var(--accent-teal);
  font: inherit;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-bold);
  cursor: pointer;
  padding: 0.5rem 0.7rem;
  margin-top: 0.2rem;
}
.upgrade-modal-buy-session-btn:hover {
  background: color-mix(in srgb, var(--accent-teal) 10%, transparent);
}
.upgrade-modal-buy-session-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
.upgrade-modal-secondary-btn {
  margin-top: 0.2rem;
  background: transparent;
  border: none;
  font: inherit;
  font-size: var(--font-size-sm);
  color: var(--color-text-tertiary);
  cursor: pointer;
  padding: 0.3rem;
}
.upgrade-modal-secondary-btn:hover { color: var(--text); }

@media (max-width: 480px) {
  .upgrade-modal-box {
    width: 100%;
    max-width: none;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    margin-top: auto;
  }
  #upgrade-modal { align-items: flex-end; }
}

/* ── Discard-transcript modal — soft-discard confirmation ──────────── */
.discard-transcript-body {
  margin: 0 0 var(--space-3);
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-secondary);
}

/* ── Campaign-card pending-recap badge ─────────────────────────────────
   Full-width amber strip between the card info and footer when a Live-
   recorded session is awaiting recap. Visible to GM/co-GM only (gated
   in the backend response). Calm color, no urgency language. */
.campaign-card-pending-recap {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.65rem;
  margin: 0.4rem 0 0.3rem;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  color: var(--accent-gold);
  background: color-mix(in srgb, var(--accent-gold) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent-gold) 35%, transparent);
  border-radius: var(--radius-sm);
}
.campaign-card-pending-dot {
  color: var(--accent-gold);
  font-size: 0.7rem;
  line-height: 1;
}

/* ── Sessions tab pending-recap banner ─────────────────────────────────
   Pinned strip at the top of the session list. Persistent until the
   recap is generated or the transcript discarded. Inline-fire Generate
   button (no nav per UX agent). */
.pending-recap-banner {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.65rem 0.9rem;
  margin-bottom: 0.75rem;
  background: color-mix(in srgb, var(--accent-gold) 10%, var(--bg-2));
  border: 1px solid color-mix(in srgb, var(--accent-gold) 40%, transparent);
  border-radius: var(--radius-md);
}
.pending-recap-banner-icon { font-size: 1.1rem; flex-shrink: 0; }
.pending-recap-banner-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  font-size: var(--font-size-sm);
  line-height: 1.4;
}
.pending-recap-banner-body strong { color: var(--text); }
.pending-recap-banner-quota {
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
}
.pending-recap-banner-actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-shrink: 0;
}
.pending-recap-discard-link {
  background: transparent;
  border: none;
  font: inherit;
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
  text-decoration: underline;
  cursor: pointer;
  padding: 0;
}
.pending-recap-discard-link:hover { color: var(--text); }
@media (max-width: 600px) {
  .pending-recap-banner {
    flex-direction: column;
    align-items: flex-start;
  }
  .pending-recap-banner-actions {
    width: 100%;
    justify-content: flex-end;
  }
}

/* v1197: helpers + appearance + change-pw all share the same column-
   with-gap recipe so the section eyebrow, helper text, and action
   button have breathing room. Pre-v1197 only change-pw had this rule —
   the other two sections inherited bare `display: block` and the button
   pressed up against the helper text. */
.profile-change-pw-section,
.profile-helpers-section,
.profile-appearance-section {
  width: 100%; display: flex; flex-direction: column; gap: 0.5rem;
}
.profile-footer {
  width: 100%; display: flex; gap: 0.5rem; align-items: center;
}
/* v1196: --flush modifier replaces the inline style="padding-top:0" that
   was being repeated on the helper + change-password footers. The
   surrounding section already supplies vertical rhythm; the footer
   shouldn't add a second gap. */
.profile-footer--flush { padding-top: 0; }
.profile-footer .btn-primary { flex: 1; padding: 0.6rem; font-size: var(--font-size-md); }
.btn-danger-outline {
  padding: 0.55rem 1rem; border-radius: 0;
  border: 1px solid var(--color-danger); color: var(--color-danger);
  background: none; font-size: var(--font-size-sm); font-weight: 500;
  cursor: pointer; transition: background var(--t-hover);
  white-space: nowrap;
}
.btn-danger-outline:hover, .btn-danger-outline:focus-visible {
  background: color-mix(in srgb, var(--color-danger) 10%, transparent);
}
.profile-delete-account-row {
  width: 100%; text-align: center; padding-top: 0.25rem;
}
.profile-delete-account-btn {
  background: none; border: none; cursor: pointer;
  font-size: var(--font-size-xs); color: var(--color-text-tertiary);
  text-decoration: underline; padding: 0.25rem;
  transition: color var(--t-hover);
}
.profile-delete-account-btn:hover, .profile-delete-account-btn:focus-visible { color: var(--color-danger); }

.theme-pill-group {
  display: flex; gap: 0.35rem;
}
.theme-pill {
  padding: 0.25rem 0.65rem; border-radius: var(--radius-full);
  border: 1px solid var(--border); background: none;
  color: var(--text-2); font-size: var(--font-size-xs); font-weight: 600;
  cursor: pointer; transition: background var(--t-hover), color var(--t-hover), border-color var(--t-hover);
}
.theme-pill:hover, .theme-pill:focus-visible { background: var(--bg-3); }
.theme-pill.active {
  background: var(--accent-teal); border-color: var(--accent-teal);
  color: #fff;
}

/* ── Preference toggle (profile modal) ───────────────────────────────────── */
.pref-toggle {
  display: flex; align-items: center; gap: 0.5rem; cursor: pointer; user-select: none;
}
.pref-toggle input[type="checkbox"] { position: absolute; opacity: 0; width: 0; height: 0; }
.pref-toggle-track {
  position: relative; width: 34px; height: 18px;
  background: var(--bg-3); border: 1px solid var(--border);
  border-radius: var(--radius-full); transition: background var(--t-hover), border-color var(--t-hover); flex-shrink: 0;
}
.pref-toggle input:checked + .pref-toggle-track {
  background: var(--accent-teal); border-color: var(--accent-teal);
}
.pref-toggle-thumb {
  position: absolute; top: 2px; left: 2px;
  width: 12px; height: 12px; border-radius: 50%;
  background: #fff; transition: transform var(--t-hover);
}
.pref-toggle input:checked + .pref-toggle-track .pref-toggle-thumb { transform: translateX(16px); }
.pref-toggle-label { font-size: var(--font-size-xs); color: var(--text-2); font-weight: 500; min-width: 2rem; }

/* ── Log entry author display ──────────────────────────────────────────────── */
.le-author-row {
  display: flex; align-items: center; gap: 0.4rem; margin-bottom: 0.25rem;
}
.le-author-avatar {
  width: 20px; height: 20px; border-radius: 50%; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 0.58rem; font-weight: 700; overflow: hidden;
  background: var(--bg-3); color: var(--text-3);
  border: 1.5px solid var(--border);
}
.le-author-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.le-author-name {
  font-size: var(--font-size-xs); font-weight: 600; color: var(--text-2);
}

/* ── Tab panels ──────────────────────────────────────────────────────────── */
.tab-panel {
  padding: 1.5rem;
  overflow-y: auto;
  height: calc(100vh - var(--chrome-height)); /* viewport minus nav + tab strip (--chrome-height) */
}
/* Journal tab fills edge-to-edge; sessions column scrolls, chronicle sticks */
#tab-sessions.tab-panel {
  padding: 0;
  overflow-y: auto;
}
/* v787: align Codex eyebrow to canonical top-left spacing. The default
   .tab-panel padding (1.5rem all around) was making the Codex eyebrow
   sit further from the corner than every other tab's. */
#tab-compendium.tab-panel {
  padding: var(--space-5) var(--space-4);
}
/* v788: zero the OUTER #tab-party padding so the inner .party-tab-panel's
   var(--space-5)/var(--space-4) is the only spacing from the tab edge.
   Without this override the base .tab-panel { padding: 1.5rem } stacked
   on top of the inner padding, producing 2.5rem left/top — no other tab
   was double-padded. */
#tab-party.tab-panel {
  padding: 0;
}
/* v906 RCCA: align Fates eyebrows (My Fates / Weave / Threads) to the
   canonical top-left spacing. The tab was inheriting the base
   `.tab-panel { padding: 1.5rem }` (24px), which sat the eyebrows
   4-8px right and below where every other single-column tab places
   them. Match Codex's canonical pattern so 🎭 / 🧶 / 🧵 align with
   📜 / 📖 / ⚔ / 🗺 across the app. */
/* v1156: same viewport-aware padding-inline pattern as Party and Quest
   tabs so the Fates tab caps content at 1280px centered on wide
   viewports. Switching between Party / Quests / Fates no longer
   creates a width jump. */
#tab-threads.tab-panel {
  padding-top: var(--space-5);
  padding-bottom: var(--space-5);
  padding-left: max(var(--space-4), calc((100% - 1280px) / 2));
  padding-right: max(var(--space-4), calc((100% - 1280px) / 2));
}
/* .map-tab-panel layout lives in the "Map shared" block below — single rule. */

/* v965: canonical empty/hint placeholder — small italic dim, left-aligned.
   Pattern reference: the Weave Fates "No threads selected yet…" line.
   Used for ALL inline helpers, empty-list bodies, and "select something"
   detail-panel placeholders. Bespoke per-tab classes (.chat-empty,
   .admin-empty, .feed-empty, .map-placeholder, .quest-detail-placeholder,
   .fates-saved-detail-placeholder, etc.) compose this and override only
   layout (flex containers); they MUST NOT re-set font-size, text-align,
   or font-style. Highlight action words inside placeholders with
   <strong> — the rule below tints + un-italicises them automatically. */
.placeholder {
  color: var(--text-3);
  font-size: var(--font-size-sm);
  font-style: italic;
}
.placeholder strong {
  color: var(--color-text-secondary);
  font-style: normal;
  font-weight: var(--font-weight-bold);
}
/* Padded variant — used as the empty / loading state inside list bodies
   (codex, party, quotes). Replaces inline style="padding:1.5rem" that
   recurred 6× across app.js render templates. */
.placeholder--padded {
  padding: var(--space-6);
}
/* v967: inline variant — sits at the canonical "in-tab inline hint"
   position (small horizontal nudge off the absolute tab edge for
   breathing room, no vertical padding). Used for any short hint that
   sits directly under a section eyebrow rather than inside a card body
   — Weave Fates / Saved Fates / Loose Threads empty states all compose
   this so they line up at exactly the same x. */
.placeholder--inline {
  padding: var(--space-2);
}

/* ── Compendium ──────────────────────────────────────────────────────────── */
/* Section header — "CODEX" eyebrow on the left, "+ Add" CTA group on the
   right. Mirrors .sessions-toolbar rhythm so all three primary tabs lead
   with the same eyebrow + paired-CTA grammar. */
.compendium-section-hdr {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-2);
}
.compendium-section-actions {
  display: flex;
  gap: var(--space-2);
}
.compendium-toolbar {
  display: flex;
  gap: 0.75rem;
  align-items: center;
  margin-bottom: 1rem;
  flex-wrap: wrap;
}
.compendium-toolbar input[type="text"] { flex: 1; min-width: 180px; }

#type-filters, #hooks-type-filters { display: flex; gap: 0.45rem; flex-wrap: wrap; }
/* Codex type-filter pills mirror the .btn-new pill metrics. The resting pill
   is the same white-outlined ghost; the .active pill is the teal counterpart
   (same kinetics, teal-tinted fill + glow). */
/* v798: theme-correct base + dark override (mirrors .btn-new refactor). */
/* v1082: aligned to brand spec .tf-filter-pill — neutral grey at rest,
   shifts to teal on .active, no halo. Spec wraps these in a .tf-filter-row
   "segmented control" container (rounded pill with 4px padding); the app
   currently renders them as standalone toggles, which is the same recipe
   minus the container shell. Tracking: filter-row container is in the
   spec-gap list pending a markup pass. */
.type-filter-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 14px;
  font-family: inherit;
  font-size: 12px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.02em;
  color: var(--text-3);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-full);
  cursor: pointer;
  white-space: nowrap;
  transition:
    color        var(--t-hover) var(--ease),
    background   var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease);
}
.type-filter-btn:hover, .type-filter-btn:focus-visible {
  color: var(--text-1);
}
.type-filter-btn.active {
  color: var(--accent-teal);
  background: var(--bg);
  border-color: color-mix(in srgb, var(--accent-teal) 35%, var(--border));
  box-shadow: 0 1px 2px rgba(0,0,0,0.04);
}
.type-filter-btn.active:hover, .type-filter-btn.active:focus-visible {
  color: var(--accent-teal);
  border-color: color-mix(in srgb, var(--accent-teal) 50%, var(--border));
}
/* v1082: retired Threads-specific #hooks-type-filters override. Every
   filter pill across the app (Codex, Threads, Quests-future) now shares
   the single canonical .type-filter-btn.active treatment above. */
/* v1083: dark-mode aligned to spec [data-theme="dark"] .tf-filter-pill —
   neutral text-3 at rest, full white on hover, #BFE7EA highlight + teal
   wash + teal border on .active. */
[data-theme="dark"] .type-filter-btn {
  color: var(--text-3);
}
[data-theme="dark"] .type-filter-btn:hover, [data-theme="dark"] .type-filter-btn:focus-visible {
  color: #fff;
}
[data-theme="dark"] .type-filter-btn.active {
  color: #BFE7EA;
  background: color-mix(in srgb, var(--accent-teal) 25%, var(--bg-2));
  border-color: color-mix(in srgb, var(--accent-teal) 60%, var(--border));
  box-shadow: none;
}
[data-theme="dark"] .type-filter-btn.active:hover, [data-theme="dark"] .type-filter-btn.active:focus-visible {
  color: #DFF3F5;
  background: color-mix(in srgb, var(--accent-teal) 35%, var(--bg-2));
  border-color: var(--accent-teal);
}

.entity-list {
  overflow: hidden;
}

/* ── Encyclopaedia A–Z layout ───────────────────────────────────────────────── */
/* Typography (uppercase, weight, letter-spacing, color, font-size) inherits
   from .form-section-label via composition; .alpha-letter-hdr only contributes
   the per-letter spacing. */
.alpha-letter-hdr {
  padding: 0.6rem 0.25rem 0.25rem;
  margin-top: 0.75rem;
}
.alpha-letter-hdr:first-child { margin-top: 0; }

/* Gap-separated cards (parity with .session-list) — was a stitched table
   grid with shared 1px borders. Each entity row now stands as its own
   bordered card with rounded corners and the canonical card hover. */
/* v1101: widened codex columns from 260px → 420px so the .tf-card-sub
   snippet has room to breathe (was being aggressively ellipsized at 6
   cols). Now lands at 2-4 cols depending on viewport width; spec mockup
   shows roughly 3-col density. */
/* v1137: bumped min from 420 -> 480 so cards have the spec mockup's
   breathing room. The eyebrow row (thumb / title / em-dash sub / badge)
   needs ~480px before the sub starts ellipsing. */
.alpha-entries-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(480px, 1fr));
  gap: var(--space-3);
}

/* v1087: chassis (bg, border, radius, hover, watermark) now from composing
   .tf-card --sm. .entity-row keeps the min-width/overflow layout bits and
   zeros outer padding (inner .entity-row-hdr owns its own padding). */
.entity-row {
  min-width: 0;
  overflow: hidden;
}
.tf-card.entity-row { padding: 0; }

/* Expanded row spans all grid columns */
.entity-row.expanded {
  grid-column: 1 / -1;
}

.entity-row-hdr {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.65rem 1rem;
  cursor: pointer;
  transition: background var(--t-hover);
}
.entity-row-hdr:hover, .entity-row-hdr:focus-visible { background: var(--bg-3); }

.entity-icon {
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--radius-lg);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--font-size-lg);
  flex-shrink: 0;
  overflow: hidden;
}
.entity-icon img { width: 2.25rem; height: 2.25rem; object-fit: cover; object-position: top center; }
/* Unicolor entity glyph (SVG #tf-glyph-<type>) inside the tinted icon square —
   paints in currentColor from .entity-icon-tinted's --entity-color. */
.entity-icon > svg { width: 1.3rem; height: 1.3rem; }

/* ── Entity icon tint — accepts --entity-color CSS var set inline ─────── */
/* Replaces inline style="background:${color}22;color:${color}" pattern    */
.entity-icon-tinted {
  background: color-mix(in srgb, var(--entity-color, var(--color-brand)) 13%, transparent);
  color: var(--entity-color, var(--color-brand));
}
/* Sibling utility — same --entity-color injection pattern, but for tinted
   text-only badges (entity type label, merge-search type). Replaces
   inline style="color:${cfg.color}" / style="color:${cfg.color || '#888'}". */
.entity-badge-tinted {
  color: var(--entity-color, var(--text-3));
}

/* v1098: .entity-meta switched from column to row-with-wrap so the spec's
   inline "title — snippet · badges" layout works. Title and snippet share
   the primary baseline; secondary badges (Dead, New, Updated, etc.) wrap
   to a second visual line only if they overflow. */
.entity-meta {
  flex: 1;
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 8px;
  min-width: 0;
}
.entity-name {
  font-size: var(--font-size-md);
  font-weight: 500;
  color: var(--text);
}
.entity-badge { font-size: var(--font-size-xs); font-weight: 500; }
.entity-chevron { color: var(--text-3); font-size: var(--font-size-xs); }

/* v1098: type-eyebrow variant — uses the per-entity-color CSS var so each
   Codex row shows a tinted pill (NPC=teal, Location=brown, Faction=violet,
   Item=gold, etc.) instead of the generic teal --neutral fallback. */
.tf-card-eyebrow--type {
  color: var(--entity-color, var(--accent-teal));
  background: color-mix(in srgb, var(--entity-color, var(--accent-teal)) 13%, transparent);
  flex-shrink: 0;
}
[data-theme="dark"] .tf-card-eyebrow--type {
  background: color-mix(in srgb, var(--entity-color, var(--accent-teal)) 22%, transparent);
}

/* v1098: status-eyebrow variants for Quest nav rows + future surfaces.
   Mirrors spec quest-status palette from screenshot mockup:
     active   → teal pill (ACTIVE)
     warn     → amber pill (LOOSE THREAD)
     success  → moss pill (RESOLVED)
     error    → ember pill (FAILED)
     neutral  → ink-3 pill (DORMANT / BACKGROUND — already defined in v1091) */
.tf-card-eyebrow--active {
  color: var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 14%, transparent);
}
[data-theme="dark"] .tf-card-eyebrow--active {
  color: #BFE7EA;
  background: color-mix(in srgb, var(--accent-teal) 22%, transparent);
}
.tf-card-eyebrow--warn {
  color: var(--accent-amber);
  background: color-mix(in srgb, var(--accent-amber) 14%, transparent);
}
[data-theme="dark"] .tf-card-eyebrow--warn {
  color: #F1B97A;
  background: color-mix(in srgb, var(--accent-amber) 22%, transparent);
}
.tf-card-eyebrow--success {
  color: var(--accent-green);
  background: color-mix(in srgb, var(--accent-green) 14%, transparent);
}
[data-theme="dark"] .tf-card-eyebrow--success {
  color: #B5DAA1;
  background: color-mix(in srgb, var(--accent-green) 22%, transparent);
}
.tf-card-eyebrow--error {
  color: var(--ember-soft);
  background: color-mix(in srgb, var(--accent-red) 14%, transparent);
}
[data-theme="dark"] .tf-card-eyebrow--error {
  color: #F5C0AE;
  background: color-mix(in srgb, var(--accent-red) 22%, transparent);
}

/* v1098: quest-nav-row inner padding tweak so the rail + content sit
   tighter together (was set by legacy .quest-nav-hdr padding 0.85/1rem). */
.tf-card.quest-nav-row .quest-nav-hdr.tf-card-head {
  padding: 0.6rem 0.85rem;
  gap: 10px;
}
.tf-card.session-card .session-card-hdr.tf-card-head {
  padding: 0.8rem 1rem 0.7rem;
  gap: 10px;
}
.tf-card.entity-row .entity-row-hdr.tf-card-head {
  padding: 0.55rem 0.85rem;
  gap: 10px;
}

/* One visual split at the expanded-body boundary — background change only.
   The previous border-top + background combo doubled up the affordance;
   .session-card uses border-top alone, .entity-row uses background alone.
   Either reads cleanly; doubling does not. */
/* v1130: aligned to spec .tf-entity-body recipe. The expanded inline body
   under a Codex entity card — 14/18 padding over a 2% ink wash that
   visually separates the body from the card row above, with a 1px top
   rule divider. Body type is display serif 14px / 1.65 / ink-2 prose so
   it reads as the same prose family as the spec recap body and the
   crosslink popout drawer.

   v1127 styled the Edit modal + crosslink drawer; this deploy brings the
   inline Codex expansion into the same family so the three Codex surfaces
   (inline row body, Edit modal, popout drawer) share one visual language. */
.entity-detail {
  padding: 14px 18px;
  border-top: 1px solid var(--border);
  background: color-mix(in srgb, var(--text-1) 2%, var(--bg-2));
  font-family: var(--type-display);
  font-size: 14px;
  line-height: 1.65;
  color: var(--text-2);
}
[data-theme="dark"] .entity-detail {
  background: color-mix(in srgb, var(--text-1) 8%, var(--bg-2));
}
.entity-detail p { margin: 0 0 10px; }
.entity-detail strong, .entity-detail b { color: var(--text-1); font-weight: 600; }
.entity-detail em { color: var(--text-1); }
/* Floated portrait — aligned to image-tile family (.ef-img-preview /
   .epd-portrait): 6px radius, 1px soft border, paper-tinted fallback bg,
   light drop shadow. Shrunk from 12.5rem to a more spec-aligned 10rem so
   the prose has more breathing room. */
.entity-full-img {
  float: right;
  width: 10rem;
  height: 10rem;
  object-fit: cover;
  object-position: top center;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: color-mix(in srgb, var(--text-1) 6%, var(--bg));
  box-shadow: 0 2px 6px rgba(0,0,0,0.08);
  margin: 0 0 12px 14px;
}
.entity-detail-body {
  overflow: visible;
}
.entity-detail-body::after {
  content: '';
  display: table;
  clear: both;
}
.entity-desc {
  font-family: var(--type-display);
  font-size: 14px;
  color: var(--text-2);
  line-height: 1.65;
  margin-bottom: 12px;
}
.entity-desc p { margin: 0 0 10px; }
/* Empty-state italic shown inside .entity-desc when an entity has no
   description yet. Replaces inline style="color:var(--text-3)" on <em>. */
.entity-desc-empty { color: var(--text-3); font-style: italic; }
/* v1130: action row spec-aligned — 8px gap, top margin separator, ghost
   buttons in a wrappable row so long action lists (Edit / Merge / Add
   Companion / Mark Fallen / Left Party / Rebuild Chronicle / Delete) wrap
   gracefully instead of overflowing the card on narrow screens. */
.entity-detail-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

/* ── Codex mode toggle ───────────────────────────────────────────────────── */
/* The codex-mode-btn now uses the global .btn-filter pattern (defined in
   the Sort/Filter icon-button block below). We keep the id selector solely
   to ensure white-space stays nowrap when the label flips between
   "All Entries" and "In Play". */
#codex-mode-btn { white-space: nowrap; }

/* ── Reason badges ───────────────────────────────────────────────────────── */
/* Used in the expanded-entity context header's "Why Now" row. (The Active
   Context Layer that previously wrapped these on the codex front page was
   removed — In-Play mode now filters the normal entity grid directly.) */
/* v1084: aligned to spec .tf-badge--ghost — neutral grey pill. */
.codex-reason-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: var(--radius-full);
  border: 1px solid var(--border);
  font-size: 10.5px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--text-3);
  background: transparent;
  white-space: nowrap;
}
.codex-reason-quest       { background: color-mix(in srgb, var(--color-purple) 12%, transparent); color: var(--accent-purple); border-color: var(--accent-purple); }
.codex-reason-thissession { background: color-mix(in srgb, var(--color-brand)  12%, transparent); color: var(--accent-teal);   border-color: var(--accent-teal); }
.codex-reason-recent      { background: color-mix(in srgb, var(--color-info)   10%, transparent); color: var(--accent-blue);   border-color: var(--accent-blue); }
.codex-reason-prevsession { background: var(--bg-3); color: var(--text-3); border-color: var(--border); }

/* ── Entity Context Header (inside expanded entity) ─────────────────────── */
.entity-ctx-header {
  margin-bottom: 0.75rem;
  padding: 0.5rem 0.65rem;
  background: color-mix(in srgb, var(--color-brand) 6%, var(--bg));
  border: 1px solid color-mix(in srgb, var(--color-brand) 20%, var(--border));
  border-radius: var(--radius-md);
  font-size: var(--font-size-sm);
}
/* Typography inherits from .form-section-label (composed via markup); the
   teal color is the only divergence — context-header eyebrow is teal so it
   reads as an "active" callout vs the neutral-tertiary canonical eyebrow. */
.entity-ctx-label {
  color: var(--accent-teal);
  margin-bottom: 0.4rem;
}
.ech-row {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  margin-bottom: 0.2rem;
  line-height: 1.45;
}
.ech-row:last-child { margin-bottom: 0; }
.ech-label {
  font-size: var(--font-size-xs);
  font-weight: 700;
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
  min-width: 70px;
}
.ech-value { color: var(--text-2); font-size: var(--font-size-sm); }
.ech-quest { color: var(--accent-purple); }
.ech-also  { display: flex; flex-wrap: wrap; gap: 4px; }
.ech-entity-btn {
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 1px 6px;
  font-size: var(--font-size-xs);
  color: var(--accent-teal);
  cursor: pointer;
  transition: background var(--t-hover);
}
.ech-entity-btn:hover, .ech-entity-btn:focus-visible { background: var(--bg-3); }

/* ── Inline Entity Preview Drawer ───────────────────────────────────────── */
/* v1127: rebuilt to spec .tf-detail recipe (Quest detail / Fates detail kin).
   The crosslink popout that opens when a user clicks an .entity-link in
   narrative prose. Outer container is the side-panel drawer; inner header
   + body + footer compose the .tf-detail recipe values (display serif
   title, teal-tinted icon badge, eyebrow type badge, 14.5px / 1.7 prose
   body, 2% ink wash footer). */
.entity-preview-drawer {
  position: fixed;
  top: var(--header-height);
  right: 0;
  width: 360px;
  max-width: 92vw;
  height: calc(100vh - var(--nav-height));
  background: var(--bg);
  border-left: 1px solid var(--border);
  box-shadow: -4px 0 24px rgba(0,0,0,0.20);
  z-index: 10005;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
[data-theme="dark"] .entity-preview-drawer {
  background: var(--bg-2);
  box-shadow: -4px 0 24px rgba(0,0,0,0.40);
}
.entity-preview-drawer.hidden { display: none; }
.epd-inner { display: flex; flex-direction: column; height: 100%; }
/* Spec .tf-detail-hdr — 10px gap, border-bottom rule, 12px padding-bottom. */
.epd-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 16px 20px 12px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
/* Container for the JS-injected portrait <img> OR the .epd-glyph-badge
   (spec .tf-detail-icon recipe — 36px square teal-tinted box with SVG
   glyph). The badge wins when there's no portrait image; the badge's own
   background covers the container's area. */
.epd-icon {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* Portrait variant — used when the entity has an uploaded image / DDB
   avatar. Same 36px frame as the glyph badge for consistency. */
.epd-icon-img {
  width: 36px;
  height: 36px;
  border-radius: 6px;
  object-fit: cover;
  display: block;
}
/* v1146: per-entity-type tinted badge. The --entity-color CSS var is
   set inline by _showEntityPreview from ENTITY_CFG[type].color, so a
   deity shows gold, NPC shows muted violet, item shows ember, spell
   shows purple, etc. Falls back to accent-teal if the var isn't set
   (defensive — keeps the v1128 behavior for any caller that forgets
   to pass the color). Glyph fill uses currentColor → inherits the
   badge's color directly. */
.epd-glyph-badge {
  width: 36px;
  height: 36px;
  border-radius: 6px;
  background: color-mix(in srgb, var(--entity-color, var(--accent-teal)) 14%, transparent);
  color: var(--entity-color, var(--teal-2));
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
[data-theme="dark"] .epd-glyph-badge {
  background: color-mix(in srgb, var(--entity-color, var(--accent-teal)) 22%, transparent);
  color: var(--entity-color, #BFE7EA);
}
.epd-glyph {
  width: 20px;
  height: 20px;
  fill: currentColor;
}
/* Lore (and any future type missing from the SVG sprite) falls back to
   the entity-type emoji centered inside the same tinted square. */
.epd-glyph-badge--emoji {
  font-size: 18px;
  line-height: 1;
}
.epd-titlebox { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
/* Spec .tf-detail-title — display serif 18px / 700 / 0.01em. */
.epd-name {
  font-family: var(--type-display);
  font-size: 18px;
  font-weight: 700;
  letter-spacing: 0.01em;
  color: var(--text-1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Spec eyebrow — 11px / 700 / 0.12em uppercase. JS still sets badge color
   inline from cfg.color; preserved by overriding only the typography. */
.epd-badge {
  font-family: var(--type-ui);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  line-height: 1.4;
}
/* Spec .tf-modal__close style — bare button, 18px, ink-3, no padding. */
.epd-close-btn {
  background: none;
  border: 0;
  font-size: 18px;
  color: var(--text-3);
  cursor: pointer;
  padding: 0;
  line-height: 1;
  flex-shrink: 0;
  transition: color var(--t-hover) var(--ease);
}
.epd-close-btn:hover, .epd-close-btn:focus-visible {
  background: none;
  color: var(--text-1);
}
/* Spec .tf-detail-body — display serif 14.5px / 1.7 / ink-2 prose with
   teal dashed-underline links to match recap body link recipe. */
.epd-content {
  flex: 1;
  overflow-y: auto;
  padding: 16px 20px;
  font-family: var(--type-display);
  font-size: 14.5px;
  line-height: 1.7;
  color: var(--text-2);
}
.epd-content p { margin: 0 0 14px; }
.epd-content strong, .epd-content b { color: var(--text-1); font-weight: 600; }
.epd-content em { color: var(--text-1); }
/* v1127: aligned to spec .tf-gallery-thumb / Edit modal preview recipe —
   6px radius, paper-tinted bg, 1px soft border, soft drop shadow. Reads
   as the same image-tile family as the modal previews. */
.epd-portrait {
  width: 100%;
  max-height: 180px;
  object-fit: cover;
  object-position: top center;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: color-mix(in srgb, var(--text-1) 6%, var(--bg));
  box-shadow: 0 2px 6px rgba(0,0,0,0.08);
  margin-bottom: 12px;
  display: block;
}
/* Spec .modal-foot — right-aligned actions over 2% ink wash with border-top. */
.epd-footer {
  padding: 14px 20px;
  border-top: 1px solid var(--border);
  background: color-mix(in srgb, var(--text-1) 2%, var(--bg));
  flex-shrink: 0;
  display: flex;
  justify-content: flex-end;
}
[data-theme="dark"] .epd-footer {
  background: color-mix(in srgb, var(--text-1) 4%, var(--bg-2));
}

/* ── Map shared ──────────────────────────────────────────────────────────── */
/* Single canonical layout rule — the duplicate `.map-tab-panel { height: …;
   overflow: hidden }` declaration that used to live up near the
   #tab-sessions block was retired in v774; layout + overflow live here. */
.map-tab-panel {
  padding: 0;
  display: flex;
  flex-direction: column;
  height: calc(100vh - var(--chrome-height));
  overflow: hidden;
}

/* Leaflet container inherits app theme */
.leaflet-container {
  background: var(--bg) !important;
  font-family: inherit;
}

/* Zoom controls — v1149 aligned to spec .tf-map-zoom: 28x28 cells,
   1px rule border, paper bg, 16px / 600 glyph, teal-tinted hover that
   tints the bg instead of replacing it with solid teal. The previous
   recipe used radius-lg + solid-teal-on-hover which felt heavier than
   the spec marker chrome. */
.leaflet-control-zoom {
  border: 1px solid var(--border) !important;
  background: var(--bg) !important;
  box-shadow: 0 2px 6px rgba(0,0,0,0.20) !important;
  border-radius: 6px !important;
  overflow: hidden;
}
[data-theme="dark"] .leaflet-control-zoom {
  background: var(--bg-2) !important;
}
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
  background: transparent !important;
  color: var(--text-2) !important;
  border: none !important;
  width: 28px !important;
  height: 28px !important;
  line-height: 28px !important;
  font-size: 16px !important;
  font-weight: 600 !important;
  transition: background 160ms ease, color 160ms ease !important;
}
.leaflet-control-zoom-in + .leaflet-control-zoom-out,
.leaflet-control-zoom-out + .leaflet-control-zoom-in {
  border-top: 1px solid var(--border) !important;
}
.leaflet-control-zoom-in:hover, .leaflet-control-zoom-in:focus-visible,
.leaflet-control-zoom-out:hover, .leaflet-control-zoom-out:focus-visible {
  background: color-mix(in srgb, var(--accent-teal) 14%, transparent) !important;
  color: var(--text-1) !important;
}
.leaflet-attribution-flag { display: none !important; }
.leaflet-control-attribution { display: none !important; }

/* Popup chrome — v1149 aligned to spec .tf-map-popup: paper bg, 1px rule
   border, 8px radius (was --radius-xl = 10), 12/14 padding, lighter
   8/24/0.30 shadow (was heavier 0/8/32/0.55). The popup tip arrow
   inherits the paper bg so it matches the wrapper. */
.leaflet-popup-content-wrapper {
  background: var(--bg) !important;
  border: 1px solid var(--border) !important;
  border-radius: 8px !important;
  box-shadow: 0 8px 24px rgba(0,0,0,0.30) !important;
  color: var(--text-1) !important;
  padding: 12px 14px !important;
}
[data-theme="dark"] .leaflet-popup-content-wrapper {
  background: var(--bg-2) !important;
}
.leaflet-popup-content { margin: 0 !important; font-family: var(--type-ui); font-size: 12.5px; }
.leaflet-popup-tip { background: var(--bg) !important; border: 1px solid var(--border) !important; }
[data-theme="dark"] .leaflet-popup-tip { background: var(--bg-2) !important; }
.leaflet-popup-close-button {
  color: var(--text-3) !important;
  font-size: 14px !important;
  padding: 2px 6px !important;
  top: 6px !important;
  right: 8px !important;
  line-height: 1 !important;
  border-radius: 3px !important;
  transition: color 160ms ease, background 160ms ease !important;
}
.leaflet-popup-close-button:hover, .leaflet-popup-close-button:focus-visible {
  color: var(--text-1) !important;
  background: color-mix(in srgb, var(--text-1) 6%, transparent) !important;
}

/* ── Map pin (Atlas + Live, unified) ─────────────────────────────────────── */
/* v1152: pin is a teardrop SHAPE per spec .tf-pin — NOT a "pin inside a
   pin". User correction on the v1151 circle: "it's supposed to be a pin
   SHAPE, just not with an actual pin in it. The shape should be filled
   with an image if there is one."
   Recipe:
     • 24×24 element with border-radius: 50% 50% 50% 0 (teardrop with
       point at bottom-left of the unrotated square)
     • transform: rotate(-45°) rotates the teardrop so its point sits
       at the bottom-center (over the lat/lng)
     • Fill = --pin-color (entity-type color injected inline) when no
       image is present
     • Border = 2px white
     • When the entity has a portrait: an inner <img class="map-pin-img">
       fills the shape, counter-rotated +45° + scaled 1.4× so the
       portrait stays upright and covers the rotated teardrop's interior.
     • Hover scales the pin 1.15× while preserving the base -45° rotation. */
.map-pin {
  width: 24px;
  height: 24px;
  border-radius: 50% 50% 50% 0;
  background: var(--pin-color, var(--accent-teal));
  border: 2px solid #fff;
  box-shadow: 0 2px 4px rgba(0,0,0,0.40);
  transform: rotate(-45deg);
  overflow: hidden;
  transition: transform 180ms ease, box-shadow var(--t-hover);
  cursor: pointer;
}
.map-pin:hover, .map-pin:focus-visible {
  transform: rotate(-45deg) scale(1.15);
  box-shadow: 0 4px 8px rgba(0,0,0,0.50);
}
/* Portrait image fills the teardrop's interior. The counter-rotation
   keeps the face upright; scale(1.4 ≈ √2) ensures the rotated rect
   covers the rotated teardrop's bounding box (no triangular gaps at
   the corners). */
.map-pin-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top;
  transform: rotate(45deg) scale(1.4);
  display: block;
}
.map-pin.locked {
  opacity: 0.4;
  filter: grayscale(0.85);
  cursor: default;
}

/* ── Party beacon (Live map) ─────────────────────────────────────────────── */
.party-beacon {
  position: relative;
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.party-beacon-dot {
  width: 13px;
  height: 13px;
  background: var(--accent-teal);
  border: 2.5px solid #fff;
  border-radius: 50%;
  position: relative;
  z-index: 2;
  box-shadow: 0 0 8px rgba(47,124,133,0.7);
}
.party-beacon-ring {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: rgba(47,124,133,0.3);
  animation: beacon-pulse 2.2s ease-out infinite;
}
@keyframes beacon-pulse {
  0%   { transform: scale(0.6); opacity: 0.9; }
  100% { transform: scale(2.4); opacity: 0; }
}

/* Pinned log entry markers on the live map */
.live-entry-pin {
  width: 14px; height: 14px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
}
.live-entry-pin.note {
  background: rgba(140,140,140,0.75);
  border: 1.5px solid rgba(255,255,255,0.5);
}
.live-entry-pin.key-event {
  background: none; border: none;
  font-size: 13px; line-height: 1;
}

/* ── Popup content (Atlas + Live, unified) ───────────────────────────────── */
.map-popup-inner {
  padding: var(--space-3);
  min-width: 175px;
}
.map-popup-name {
  font-size: var(--font-size-base);
  font-weight: 700;
  color: var(--text);
  margin-bottom: 0;
}
/* Thin divider between the location name and the GM rename controls.
   Composes the canonical 1px border-top against --border; only the local
   margin (asymmetric to keep the rename label tight under it) is bespoke. */
.map-popup-divider {
  border: none;
  border-top: 1px solid var(--border);
  margin: var(--space-2) 0 var(--space-1);
}
.map-popup-label {
  font-size: var(--font-size-xs);
  font-weight: 700;
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  margin-bottom: var(--space-1);
}
.map-popup-input {
  display: block;
  width: 100%;
  box-sizing: border-box;
  margin-bottom: var(--space-2);
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-3);
  color: var(--text);
  font-size: var(--font-size-sm);
  font-family: inherit;
}
.map-popup-input:focus { outline: none; border-color: var(--accent-teal); }
/* Save / Remove buttons inside the popup — composes .btn-primary +
   .btn-ghost--danger; this class only adds the full-width block layout
   and the tight inter-button spacing the popup needs. v773 retired the
   bespoke .map-popup-save-btn / .map-popup-remove-btn rules. */
.map-popup-btn {
  display: block;
  width: 100%;
  margin-bottom: var(--space-1);
  font-size: var(--font-size-sm);
}
.map-popup-btn:last-child { margin-bottom: 0; }

/* ── Map hover card tooltip ──────────────────────────────────────────────── */
.map-hover-tooltip {
  background: transparent !important;
  border: none !important;
  box-shadow: none !important;
  padding: 0 !important;
}
.map-hover-tooltip::before { display: none !important; }
.map-hover-card {
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent-teal);
  border-radius: var(--radius-lg);
  padding: 0.5rem 0.7rem;
  max-width: 240px;
  box-shadow: 0 8px 28px rgba(0,0,0,0.55);
  pointer-events: none;
}
.map-hover-card-name {
  font-size: var(--font-size-base);
  font-weight: 700;
  color: var(--text);
  margin-bottom: 0.2rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.map-hover-card-desc {
  font-size: var(--font-size-xs);
  color: var(--text-2);
  line-height: 1.45;
  white-space: normal;
}

/* ── Atlas toolbar ───────────────────────────────────────────────────────── */
/* Eyebrow (.form-section-label "Atlas") + optional map switcher on the LEFT,
   GM controls cluster on the RIGHT. Mirrors .quest-nav-toolbar /
   .sessions-toolbar / .compendium-section-hdr rhythm — no border-bottom,
   just an indent + space-between. The divider is implicit via #map-container
   below, which sits flush against the toolbar. */
.map-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  /* v787: canonical eyebrow top-left spacing — matches all other tabs. */
  padding: var(--space-5) var(--space-4) var(--space-2);
  flex-shrink: 0;
}
.map-toolbar-left,
.map-toolbar-right {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.map-toolbar .btn-ghost { font-size: var(--font-size-sm); }
.map-place-select {
  padding: 0.28rem 0.6rem;
  font-size: var(--font-size-base);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg);
  color: var(--text);
  width: auto;
  max-width: 220px;
}
.map-hint { color: var(--text-3); font-size: var(--font-size-sm); font-style: italic; }

#map-container { flex: 1; background: var(--bg-3); }

/* v968: stripped flex-center recipe — empty map state now sits at the
   canonical inline-hint x (matches every other empty state app-wide).
   Hint above, upload button below, both left-aligned, small gap. */
.map-placeholder {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);
  font-style: italic;
}
/* Inline-button-shaped <label> wrapping the hidden file input in the empty
   state. Composes .btn-primary; this class only sets cursor + sized type. */
.map-upload-label {
  cursor: pointer;
  font-size: var(--font-size-sm);
}
/* Re-extract modal location list — taller than the default checklist
   so users can scan ~20 candidates without paging. */
.reextract-checklist { max-height: 300px; }
/* Warn variant of .form-helper-text — amber-tinted, used by the re-extract
   "this will replace N existing locations" line. */
.form-helper-text--warn {
  margin-top: 3px;
  margin-bottom: 0;
  color: var(--accent-amber);
  font-size: var(--font-size-sm);
}

/* ── Entity modal ────────────────────────────────────────────────────────── */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: 1rem;
}
/* Heavier scrim for blocking moments (legal consent, irreversible flows) */
.modal-overlay--blocking { background: rgba(0,0,0,0.82); z-index: 9000; }
/* Confirmation dialogs always stack ABOVE the surface that triggered them.
   v918 fix: previously #confirm-modal got the same z-index: 1000 as every
   other overlay; since #fates-weave-modal sits later in the DOM, the
   workspace covered the confirm dialog when "Delete this Fate?" fired
   from inside the workspace — invisible confirm, double-click → double-
   delete → 404 on the second request.
   v1038 fix: bumped 1100 → 10100 so confirm clears #admin-screen (z-index
   10000 !important). Without this, "Delete user" / "Delete campaign"
   triggered from the admin Users / Campaigns tabs rendered behind the
   admin overlay — same invisible-confirm / double-click failure mode. */
#confirm-modal,
#pulse-drill-modal { z-index: 10100; }
/* v1085: modal chassis aligned to spec .tf-modal — near-square corners
   (4px), paper bg (--bg), paper-edge border, 3-layer drop shadow with a
   1px inset highlight for refinement. */
.modal-box {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  width: 100%;
  max-width: 460px;          /* default — spec md */
  max-height: 90vh;
  overflow-y: auto;
  box-shadow:
    0 1px 0 rgba(255,255,255,0.7) inset,
    0 24px 60px -20px rgba(0,0,0,0.4),
    0 4px 14px rgba(0,0,0,0.18);
}
[data-theme="dark"] .modal-box {
  background: var(--bg-2);
  box-shadow:
    0 1px 0 rgba(255,255,255,0.06) inset,
    0 24px 60px -20px rgba(0,0,0,0.55),
    0 4px 14px rgba(0,0,0,0.30);
}
.modal-box--sm { max-width: 420px; }
/* v987: stacked choice buttons for the first-session-choice modal.
   Primary recommended CTA on top, escape-hatch ghost button below.
   Both stretch full width of the modal body for thumb-target sizing. */
.first-session-choice-actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.first-session-choice-actions > button { width: 100%; justify-content: center; }
.modal-box--lg { max-width: 560px; }
.modal-box--xl { max-width: 720px; }
/* xxl — for the Brainstorm Workspace which needs three-column horizontal
   room. Sprint A only uses the center column; left rail (pinning, Sprint C)
   and right rail (saves, Sprint D) reuse this size. */
.modal-box--xxl { max-width: 1080px; }
/* v1085: aligned to spec .tf-modal__body — 22px 28px / 14px / 1.6 line-height
   / --ink-2 (--text-2) color. Reads as the spec's "paper page" body copy. */
.modal-body {
  padding: 22px 28px;
  color: var(--text-2);
  font-family: inherit;
  font-size: 14px;
  line-height: 1.6;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.modal-body strong { color: var(--text-1); }
/* v1085: canonical modal foot — 16px 28px 22px / 12px gap / right-aligned
   / border-top in --rule / 2% ink wash bg. Cancel/Save pair sits here. */
.modal-foot {
  padding: 16px 28px 22px;
  display: flex;
  gap: 12px;
  justify-content: flex-end;
  border-top: 1px solid var(--border);
  background: color-mix(in srgb, var(--text-1) 2%, var(--bg));
}
/* v1085: aligned to spec .tf-modal__hdr — 22px 28px 18px padding,
   border-bottom in --rule, baseline-aligned flex row. */
.modal-hdr {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: 22px 28px 18px;
  border-bottom: 1px solid var(--border);
}
/* v1085: modal title aligned to spec .tf-modal__title — Source Serif 4
   display weight 600, 22px, tight tracking, mixed case (was uppercase
   eyebrow recipe pre-v1085). The serif title is the spec's signature
   typographic moment for modal sections. */
.modal-hdr h2 {
  font-family: var(--type-display);
  font-size: 22px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: -0.012em;
  text-transform: none;
  color: var(--text-1);
  margin: 0;
}
/* v1085: aligned to spec .tf-modal__close — bare button, 18px, ink-3,
   no padding/radius. Hover deepens to full ink color. */
.modal-close-btn {
  background: none;
  border: 0;
  font-size: 18px;
  color: var(--text-3);
  cursor: pointer;
  padding: 0;
  line-height: 1;
  transition: color var(--t-hover) var(--ease);
}
.modal-close-btn:hover, .modal-close-btn:focus-visible {
  background: none;
  color: var(--text-1);
}

/* v1131: generalized modal-form chassis — every <form> inside a .modal-box
   gets the spec .modal-body padding (22/28), flex-column layout, and
   var(--space-4) gap between rows. Covers entity-form, session-form,
   quest-form, create-campaign-modal-form. The form-actions rule below
   then handles each form's footer with the spec .modal-foot recipe.
   v1127's #entity-form scope is retired in favor of this. */
.modal-box form {
  padding: 22px 28px;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.form-row { display: flex; flex-direction: column; gap: 0.3rem; }
.form-row label { font-size: var(--font-size-sm); font-weight: 500; color: var(--text-2); }
/* v1127: form labels inside modals adopt the spec .tf-form-label eyebrow
   recipe — 11px UI / 600 / 0.12em uppercase / text-3. The bare-text label
   reads as "what is this field" rather than competing with the input value.
   Scoped to .modal-box so non-modal forms (settings, etc) keep the legacy
   sans-serif label until they're individually aligned. */
.modal-box .form-row { gap: 8px; }
.modal-box .form-row label {
  font-family: var(--type-ui);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-3);
  line-height: 1.4;
  display: block;
  margin: 0;
}
.date-parts { display: flex; align-items: center; gap: 0.2rem; }
.date-part { width: 3ch; text-align: center; padding: 0.4rem 0.3rem; }
.date-sep { color: var(--text-3); font-weight: 600; }
.form-actions { display: flex; gap: 0.75rem; margin-top: 0.25rem; }
/* v1131: action row inside every modal form composes the .modal-foot
   recipe (16/28/22 padding, right-aligned, 1px border-top in --rule, 2%
   ink wash bg). Negative side+bottom margins extend it across the form's
   own 22/28 padding so the bar bleeds to the modal-box edges per spec.
   Generalized from v1127's #entity-form scope so every modal form gets
   the same footer treatment. */
.modal-box form .form-actions {
  display: flex;
  gap: 12px;
  justify-content: flex-end;
  padding: 16px 28px 22px;
  margin: var(--space-4) -28px -22px -28px;
  border-top: 1px solid var(--border);
  background: color-mix(in srgb, var(--text-1) 2%, var(--bg));
}
[data-theme="dark"] .modal-box form .form-actions {
  background: color-mix(in srgb, var(--text-1) 4%, var(--bg-2));
}

/* v1127: aligned to spec .tf-gallery-thumb recipe — 6px radius, paper-tinted
   bg, soft border, optional hover lift. Bumped to 96px for a more useful
   "currently set" preview at the top of the Edit Entity image row.
   Matches the gallery tiles below for a consistent thumbnail family. */
.ef-img-preview {
  width: 96px;
  height: 96px;
  object-fit: cover;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: color-mix(in srgb, var(--text-1) 6%, var(--bg));
  box-shadow: 0 2px 6px rgba(0,0,0,0.08);
  margin-bottom: 8px;
  display: block;
}
/* Multi-image gallery strip in the entity modal — horizontal scroller of
   thumbnail tiles with per-tile actions (★ make primary, × remove).
   v1127: 6px radius + spec gallery hover lift + teal hover border. */
.ef-image-gallery {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 8px 0;
}
.ef-gallery-tile {
  position: relative;
  width: 64px;
  height: 64px;
  border-radius: 6px;
  border: 1px solid var(--border);
  overflow: hidden;
  background: color-mix(in srgb, var(--text-1) 6%, var(--bg));
  flex-shrink: 0;
  transition: border-color 160ms ease, transform 160ms ease, box-shadow 160ms ease;
}
.ef-gallery-tile:hover {
  border-color: var(--accent-teal);
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(0,0,0,0.12);
}
.ef-gallery-tile--primary {
  border-color: var(--accent-gold, #d4a574);
  box-shadow: 0 0 0 1px var(--accent-gold, #d4a574);
}
.ef-gallery-tile--primary:hover {
  border-color: var(--accent-gold, #d4a574);
}
.ef-gallery-tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.ef-gallery-primary-badge {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.85rem;
  color: var(--accent-gold, #d4a574);
  background: rgba(0,0,0,0.55);
  border-radius: 50%;
  pointer-events: none;
}
.ef-gallery-btn {
  position: absolute;
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.85rem;
  background: rgba(0,0,0,0.55);
  color: var(--text);
  border: none;
  border-radius: 50%;
  cursor: pointer;
  padding: 0;
  line-height: 1;
  transition: background var(--t-hover) ease, color var(--t-hover) ease;
}
.ef-gallery-btn:hover,
.ef-gallery-btn:focus-visible {
  background: rgba(0,0,0,0.85);
  outline: none;
}
.ef-gallery-btn--star { top: 2px; left: 2px; color: var(--accent-gold, #d4a574); }
.ef-gallery-btn--remove { top: 2px; right: 2px; color: var(--accent-red, #d4724f); }

/* Image-cycler host + pagination dots. The host wraps each multi-image
   <img data-img-cycle="..."> in a positioned span so the dots can absolutely-
   position at the bottom-inside of the image. fit-content sizing means the
   wrapper hugs the image dimensions without disturbing the surrounding flow. */
.img-cycle-host {
  position: relative;
  display: block;
  width: fit-content;
  max-width: 100%;
  line-height: 0;  /* removes inline-image gap below */
}
/* When the wrapped image was sized via width:100% (full-width grid card etc.),
   the host needs to span the same width so the absolute-positioned dots align
   with the image's actual rendered bounds. */
.img-cycle-host > img { max-width: 100%; }

/* Context overrides: when the wrapped image previously carried layout
   responsibilities (float-right with text-wrap, 100%-fill of a sized parent),
   the host has to adopt those properties — otherwise wrapping in a
   `display: block; width: fit-content` span breaks the surrounding layout.
   :has() hoists float/dims to the host and resets the img to fill it. */
.img-cycle-host:has(> .entity-full-img) {
  float: right;
  width: 12.5rem;
  height: 12.5rem;
  margin: 0 0 0.75rem 1rem;
}
.img-cycle-host > .entity-full-img {
  float: none;
  margin: 0;
  width: 100%;
  height: 100%;
}
/* Party / live PC portrait: image fills the parent's sized box (160px square
   on Party tab, 140px tall × 100% wide in the live PC sidebar). */
.party-pc-portrait > .img-cycle-host {
  display: block;
  width: 100%;
  height: 100%;
}
/* Codex sidebar pane (#epd-content): image is full-width with max-height. */
#epd-content > .img-cycle-host {
  display: block;
  width: 100%;
}
.img-cycle-dots {
  position: absolute;
  bottom: 6px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 0.35rem;
  align-items: center;
  padding: 3px 6px;
  border-radius: var(--radius-full);
  background: rgba(0,0,0,0.45);
  backdrop-filter: blur(2px);
  pointer-events: auto;
}
.img-cycle-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  border: none;
  background: rgba(255,255,255,0.55);
  cursor: pointer;
  padding: 0;
  transition: background var(--t-trans), transform var(--t-trans);
}
.img-cycle-dot:hover,
.img-cycle-dot:focus-visible {
  background: rgba(255,255,255,0.8);
  outline: none;
}
.img-cycle-dot--active {
  background: #ffffff;
  transform: scale(1.3);
}

/* Search-field clear button. Wraps each <input data-clearable> with a
   positioning span and overlays a × button on the right. Hidden when the
   field is empty — an empty search field renders identical to before. The
   wrapper sizes to its content by default so it doesn't disturb existing
   toolbar layouts; specific contexts (e.g. .compendium-toolbar) re-apply
   the original input's flex sizing to the wrapper below. */
.search-clear-host {
  position: relative;
  display: inline-flex;
  align-items: stretch;
}
.search-clear-host > input {
  flex: 1;
  min-width: 0;
  /* Reserve room on the right for the × button so typed text doesn't run
     under it. The number matches .search-clear-btn's right offset + width. */
  padding-right: 2rem;
}
/* Codex toolbar: input had `flex: 1; min-width: 180px` — now that the input
   is wrapped, redirect that flex sizing to the wrapper so the toolbar layout
   matches the pre-wrap behaviour. */
.compendium-toolbar .search-clear-host { flex: 1; min-width: 180px; }
/* Fates entity picker: input was width:100% in a vertical flex column —
   the inline-flex host would otherwise collapse to content width and
   break the picker's full-width search field. */
.fates-weave-entity-picker .search-clear-host { display: flex; width: 100%; }
.search-clear-btn {
  position: absolute;
  right: 0.4rem;
  top: 50%;
  transform: translateY(-50%);
  width: 1.4rem;
  height: 1.4rem;
  display: none;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  border-radius: 50%;
  color: var(--text-3);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  padding: 0;
  transition: background var(--t-hover) ease, color var(--t-hover) ease;
}
.search-clear-btn--visible { display: inline-flex; }
.search-clear-btn:hover,
.search-clear-btn:focus-visible {
  background: color-mix(in srgb, var(--text-1) 10%, transparent);
  color: var(--text);
  outline: none;
}

/* ── Setup wizard ────────────────────────────────────────────────────────── */
.setup-step-indicator {
  font-size: var(--font-size-sm);
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 0.75rem;
}

.drop-zone {
  border: 2px dashed var(--border);
  border-radius: var(--radius-xl);
  padding: 2rem 1rem;
  text-align: center;
  cursor: pointer;
  color: var(--text-3);
  font-size: var(--font-size-base);
  transition: border-color var(--t-hover), background var(--t-hover);
  margin-bottom: 0.75rem;
}
.drop-zone:hover, .drop-zone:focus-visible, .drop-zone.drag-over {
  border-color: var(--accent-teal);
  background: var(--bg-3);
}

.map-preview {
  width: 100%;
  max-height: 180px;
  object-fit: cover;
  border-radius: var(--radius-lg);
  border: 1px solid var(--border);
  margin-bottom: 0.75rem;
}

.status-msg {
  font-size: var(--font-size-base);
  color: var(--text-2);
  margin-bottom: 0.5rem;
}
/* Tertiary helper line that sits *under* a form field — smaller + dimmer
   than .status-msg, slight margin-top so it tucks beneath the input.
   Replaces inline style="margin-top:3px;color:var(--text-3);font-size:0.78rem". */
.form-helper-text {
  margin-top: 3px;
  margin-bottom: 0;
  color: var(--text-3);
  font-size: var(--font-size-xs);
}
/* Inline-with-label variant — sits next to the label text inside the same
   <label> element, normal weight + tertiary colour. Replaces inline
   style="color:var(--text-3);font-weight:400" hints used after labels. */
.form-helper-inline {
  color: var(--text-3);
  font-weight: 400;
}

/* Inline "Skip next → / Undo skip" action on the recurrence schedule line
   (SCHEMA_VERSION 18). Teal text-link inside .form-helper-text, set off from
   the schedule text by a small gap. */
#camp-next-session-skip-link {
  margin-left: var(--space-2);
  color: var(--color-brand);
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}
#camp-next-session-skip-link:hover {
  text-decoration: underline;
}

.setup-actions {
  display: flex;
  gap: 0.75rem;
  align-items: center;
  margin-top: 0.75rem;
}

/* Location checklist */
.location-checklist {
  max-height: 260px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  margin-bottom: 1rem;
}
.location-check-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.75rem;
  cursor: pointer;
  border-bottom: 1px solid var(--border);
  font-size: var(--font-size-base);
}
.location-check-row:last-child { border-bottom: none; }
.location-check-row:hover, .location-check-row:focus-visible { background: var(--bg-3); }
.loc-name { flex: 1; color: var(--text); }
.loc-coords { color: var(--text-3); font-size: var(--font-size-sm); font-variant-numeric: tabular-nums; }

/* Loading spinner */
.loading-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  color: var(--text-2);
  font-size: var(--font-size-base);
  padding: 1rem 0;
}
/* Centered variant — replaces inline style="justify-content:center" on the
   roster loading spinner. */
.loading-row--center {
  justify-content: center;
}
.spinner {
  display: inline-block;
  width: 16px;
  height: 16px;
  border: 2px solid var(--border);
  border-top-color: var(--accent-teal);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ── DB status ───────────────────────────────────────────────────────────── */
#db-status {
  position: fixed;
  bottom: 1rem;
  right: 1rem;
  font-size: var(--font-size-sm);
  padding: 4px 10px;
  border-radius: var(--radius-full);
  background: var(--bg-3);
  color: var(--text-2);
  border: 1px solid var(--border);
  opacity: 0;
  transition: opacity var(--t-trans);
}
#db-status.visible { opacity: 1; }
#db-status.ok { color: var(--accent-green); }
#db-status.err { color: var(--accent-red); }

/* ── Journey tab layout ──────────────────────────────────────────────────── */
/* v1144: padding-top so the Campaign Chronicle box on the right and the
   session list on the left have breathing room below the tab nav. The
   col's sticky top:0 still pins it during scroll, but the initial paint
   sits ~16px below the tab strip instead of flush against it. */
.journey-layout {
  display: flex;
  align-items: flex-start; /* columns don't stretch to match each other */
  min-height: 100%;
  padding-top: var(--space-4);
}
.journey-sessions-col {
  flex: 0 0 42%;
  min-width: 320px;
  border-right: none;
  position: relative;
}
.journey-sessions-col::after {
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: 1px;
  background: linear-gradient(to bottom, transparent 0%, color-mix(in srgb, var(--border) 50%, transparent) 15%, var(--border) 50%, color-mix(in srgb, var(--border) 50%, transparent) 85%, transparent 100%);
}
.journey-summary-col {
  flex: 1;
  min-width: 0;
  padding: var(--space-5) var(--space-6);
  position: sticky;
  top: 0;
  max-height: 100vh;
  overflow-y: auto;
  box-sizing: border-box;
  /* v1125: card chassis — mirrors .journey-summary-card recipe so the
     Campaign Chronicle rail reads as a bordered panel (12px radius, paper
     bg, soft drop) instead of a bare sticky scroll area. */
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.06);
}
[data-theme="dark"] .journey-summary-col {
  background: var(--bg-2);
  box-shadow: 0 4px 16px rgba(0,0,0,0.32);
}
/* v1093: aligned to spec .tf-detail family. .journey-summary-card is the
   detail-panel chassis for Quest detail, Fates detail, and entity-link
   popout. Recipe matches spec verbatim — paper bg, 12px radius, 20/24
   padding, 12px gap, capped at 100% height, dropped shadow. Per-theme
   dark override matches spec's deeper drop. */
.journey-summary-card {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 20px 24px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-height: calc(100vh - 3.5rem);
  box-shadow: 0 4px 16px rgba(0,0,0,0.06);
}
[data-theme="dark"] .journey-summary-card {
  background: var(--bg-2);
  border-color: var(--border);
  box-shadow: 0 4px 16px rgba(0,0,0,0.32);
}
.journey-summary-hdr {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
  border-bottom: 1px solid var(--border);
  padding-bottom: 12px;
}
.journey-summary-title {
  font-family: var(--type-display);
  font-weight: 700;
  font-size: 18px;
  color: var(--text-1);
  letter-spacing: 0.01em;
  /* Take the available space between the leading badge icon and the
     trailing meta-actions cluster so meta-actions stay right-justified. */
  flex: 1;
  min-width: 0;
  margin: 0;
}
.journey-summary-body {
  flex: 1;
  overflow-y: auto;
  font-family: var(--type-display);
  font-size: 14.5px;
  line-height: 1.7;
  color: var(--text-2);
}
.journey-summary-body p { margin: 0 0 14px; }
.journey-summary-body em { color: var(--text-1); }
.journey-summary-body a {
  color: var(--teal-2);
  text-decoration: none;
  border-bottom: 1px dashed color-mix(in srgb, var(--accent-teal) 50%, transparent);
  transition: color 160ms ease, border-color 160ms ease;
}
.journey-summary-body a:hover { color: var(--teal-3); border-bottom-color: var(--teal-3); }
[data-theme="dark"] .journey-summary-body a { color: #BFE7EA; }
.chronicle-entry { padding: var(--space-3) 0; border-bottom: 1px solid var(--border); }
.chronicle-entry:last-child { border-bottom: none; }
/* Typography is now inherited from .form-section-label (composed via the
   markup); .chronicle-entry-hdr just provides the bullet-list spacing. */
.chronicle-entry-hdr { margin: 0 0 0.35rem; }
.chronicle-bullets { margin: 0; padding-left: 1.1rem; list-style: disc; }
.chronicle-bullets li { margin-bottom: 0.2rem; line-height: 1.5; }
.chronicle-entry--zero { background: var(--bg-2); border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); margin-bottom: var(--space-2); }
.chronicle-entry--zero .chronicle-bullets { list-style: none; padding-left: 0; }
.chronicle-entry--zero .chronicle-bullets li { font-style: italic; color: var(--text-2); line-height: 1.7; margin-bottom: 0; }
.chronicle-entry--zero .chronicle-entry-hdr { color: var(--accent-teal); }

/* GM-only pencil to inline-edit the session_blurb that feeds this chronicle
   entry. Same visual recipe as .session-title-edit-btn so the affordance
   reads identically across surfaces. */
.chronicle-tldr-edit-btn {
  background: none;
  border: none;
  color: var(--text-3);
  cursor: pointer;
  padding: 0 var(--space-1);
  margin-left: var(--space-2);
  font-size: var(--font-size-sm);
  line-height: 1;
  border-radius: var(--radius-sm);
  opacity: 0.45;
  transition: opacity var(--t-hover) ease, color var(--t-hover) ease;
}
.chronicle-entry:hover .chronicle-tldr-edit-btn,
.chronicle-tldr-edit-btn:hover,
.chronicle-tldr-edit-btn:focus-visible {
  opacity: 1;
  color: var(--accent-teal);
}
.chronicle-tldr-edit-box {
  width: 100%;
  font: inherit;
  color: var(--text-1);
  background: var(--bg-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-2);
  resize: vertical;
}
.chronicle-tldr-edit-box:focus {
  outline: none;
  border-color: var(--accent-teal);
}
.journey-summary-body strong, .journey-summary-body b {
  color: var(--text);
  font-weight: 600;
}
.journey-summary-empty {
  color: var(--text-3);
  font-style: italic;
}
.chronicle-backstory {
  font-style: italic;
  color: var(--text-2);
  border-left: 3px solid var(--accent-teal);
  padding: 0.4rem 0.75rem;
  margin-bottom: 1rem;
  font-size: var(--font-size-md);
}
@media (max-width: 900px) {
  /* On mobile we collapse to a single vertical flow: session list on top,
     chronicle on bottom, ONE scroll context (the page). The desktop layout
     created three nested scroll regions (col, card, body) which felt jarring
     on phones — scrolling inside one didn't move the others. */
  .journey-layout { flex-direction: column; }
  .journey-sessions-col {
    flex: none;
    border-right: none;
    max-height: none;
  }
  /* Divider rotates from a vertical line between columns to a horizontal
     gradient under the session list, matching other section dividers. */
  .journey-sessions-col::after {
    top: auto; right: 0; bottom: 0; left: 0;
    width: auto; height: 1px;
    background: linear-gradient(to right, transparent 0%, color-mix(in srgb, var(--border) 50%, transparent) 15%, var(--border) 50%, color-mix(in srgb, var(--border) 50%, transparent) 85%, transparent 100%);
  }
  .journey-summary-col {
    padding: var(--space-4);
    position: static;       /* override desktop sticky */
    max-height: none;       /* let content flow */
    overflow-y: visible;
    margin-top: var(--space-4);
  }
  .journey-summary-card {
    max-height: none;       /* override desktop max-height */
  }
  .journey-summary-body {
    overflow-y: visible;    /* parent (page) handles scroll */
    max-height: none;
  }
}

/* ── Sessions tab ────────────────────────────────────────────────────────── */
/* "SESSIONS" eyebrow on the left, "+ New Session" CTA on the right —
   matches the campaign-page section header rhythm. The horizontal divider
   was removed so the toolbar reads as a section title rather than a
   chrome strip. Vertical padding tuned so the gap from label → first
   session card mirrors the Chronicle column's eyebrow → first-entry
   rhythm (≈0.5rem, matching .form-section-label's inline margin-bottom). */
/* v787: canonical eyebrow padding for ALL primary tabs is
   var(--space-5) top, var(--space-4) horizontal. Chronicle is the
   reference; Codex / Party / Atlas / Quests / Manage all align here. */
.sessions-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: var(--space-5) var(--space-4) var(--space-2);
  flex-shrink: 0;
}

.session-list {
  padding: 0 1rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

/* v1087: chassis (bg, border, radius, hover, watermark) now comes from
   composing .tf-card --md. The legacy .session-card class is kept for
   inner-layout namespacing only. Outer padding is zeroed so the inner
   .session-card-hdr / .session-card-body keep owning their own padding. */
.session-card {
  overflow: hidden;
}
.tf-card.session-card { padding: 0; }

/* Header is the click target for expand/collapse. Position relative so the
   session-num and session-date can corner-anchor (mirrors .campaign-card-role
   / .campaign-live-badge), leaving the title to claim the full main row. */
/* v1098: padding tightened (was 1.85rem top for absolute-positioned
   .session-num chip in the corner; v1098 made .session-num inline so the
   reserved space is no longer needed). Position-relative kept in case
   downstream descendant positioning relies on it. */
.session-card-hdr {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.8rem 1rem 0.7rem;
  cursor: pointer;
  user-select: none;
  position: relative;
}
.session-card-hdr:hover, .session-card-hdr:focus-visible { background: var(--bg-3); }

/* Top-left corner badge — the canonical "where am I in the campaign"
   marker, parallel to .campaign-card-role's role chip. The previous
   `var(--accent-teal)22` was invalid CSS (you can't suffix-concatenate
   alpha hex onto a CSS variable); switched to color-mix so the chip
   actually renders the intended ~13%-teal fill. */
/* v1098: .session-num retired its absolute-positioned corner-badge layout
   when it began composing .tf-card-eyebrow. The eyebrow recipe (in v1091)
   handles font/padding/color/radius; this block is now a no-op stub kept
   for any remaining JS hooks that target the class. */
.session-num {
  white-space: nowrap;
}

.session-title {
  font-weight: 600;
  color: var(--text-1);
}
/* v783: wraps title + pencil + input so the inline-edit affordance shares
   the title's flex slot. flex:1 moved here from .session-title so the
   wrap (and the input inside it on rename) takes the same space. */
.session-title-wrap {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  flex: 1;
  min-width: 0;
}
/* Pencil affordance — subtle at rest, brightens on card hover or focus.
   stopPropagation in the click handler keeps it from toggling the card. */
.session-title-edit-btn {
  background: none;
  border: none;
  color: var(--text-3);
  cursor: pointer;
  padding: 0 var(--space-1);
  font-size: var(--font-size-sm);
  line-height: 1;
  border-radius: var(--radius-sm);
  opacity: 0.45;
  transition: opacity var(--t-hover) ease, color var(--t-hover) ease;
}
.session-card:hover .session-title-edit-btn,
.session-title-edit-btn:hover,
.session-title-edit-btn:focus-visible {
  opacity: 1;
  color: var(--accent-teal);
}
/* Inline rename input — inherits typography from .session-title so the
   swap is visually seamless. Only the box treatment + minimal padding
   are local. */
.session-title-input {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-weight: 600;
  color: var(--text-1);
  background: var(--bg-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 2px var(--space-2);
}
.session-title-input:focus {
  outline: none;
  border-color: var(--accent-teal);
}

/* Top-right corner — plain meta text (not chipped) so it reads as a
   secondary timestamp rather than competing with the session-num chip. */
/* v1098: .session-date retired its absolute-positioned corner layout
   when it began composing .tf-card-meta. The meta recipe (mono font,
   tabular numerals, ink-3 color) is inherited from .tf-card-meta;
   this block just enforces no-wrap. */
.session-date {
  white-space: nowrap;
}

.session-chevron {
  color: var(--text-3);
  font-size: var(--font-size-sm);
  margin-left: 0.25rem;
}

.session-body {
  border-top: 1px solid var(--border);
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}

.session-summary {
  color: var(--text-2);
  font-size: var(--font-size-md);
  line-height: 1.65;
  white-space: pre-wrap;
}
/* Inline recap edit — pencil button overlays the top-right of the wrapper,
   textarea swap mirrors the session-title rename pattern. */
.session-summary-wrap { position: relative; }
.session-summary-edit-btn {
  position: absolute;
  top: 0;
  right: 0;
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-2);
  border-radius: var(--radius-md);
  padding: 0.15rem 0.5rem;
  cursor: pointer;
  font-size: var(--font-size-sm);
  line-height: 1.2;
  transition: color var(--t-hover) ease, border-color var(--t-hover) ease;
}
.session-summary-edit-btn:hover,
.session-summary-edit-btn:focus-visible {
  color: var(--text);
  border-color: rgba(47,154,163,0.45);
}
.session-summary-textarea {
  width: 100%;
  min-height: 20rem;
  font-family: inherit;
  font-size: var(--font-size-md);
  line-height: 1.55;
  background: var(--bg-1);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 0.75rem 1rem;
  resize: vertical;
}
.session-summary-edit-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.5rem;
}
/* v1107: reverted v1105. User confirmed spec mockup DOES render beat
   headers as small uppercase teal-2 with dot prefix (the canonical
   .tf-beat recipe). The AI emits "## Title Case" in markdown source
   for parseability; CSS recasts to uppercase per the design language.
   Restored: 13px / 600 / 0.10em / uppercase / teal-2 / dot prefix /
   teal-tinted bottom rule. */
.session-beat-header {
  font-family: var(--type-display);
  font-weight: 600;
  font-size: 13px;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--teal-2);
  margin: 28px 0 10px;
  padding-bottom: 6px;
  border-bottom: 1px solid color-mix(in srgb, var(--accent-teal) 35%, var(--border));
  display: flex;
  align-items: center;
  gap: 8px;
  white-space: normal;
}
[data-theme="dark"] .session-beat-header { color: #BFE7EA; }
.session-beat-header::before {
  content: '';
  width: 6px;
  height: 6px;
  background: var(--accent-teal);
  border-radius: 50%;
  flex-shrink: 0;
}
.session-beat-header:first-of-type { margin-top: 0; }

.session-no-summary {
  color: var(--text-3);
  font-style: italic;
  font-size: var(--font-size-base);
}

.session-transcript-toggle {
  font-size: var(--font-size-sm);
  color: var(--accent-teal);
  cursor: pointer;
  background: none;
  border: none;
  padding: 0;
  font-family: inherit;
  text-align: left;
}
.session-transcript-toggle:hover, .session-transcript-toggle:focus-visible { text-decoration: underline; }

.session-transcript-box {
  width: 100%;
  min-height: 120px;
  max-height: 260px;
  resize: vertical;
  font-size: var(--font-size-sm);
  font-family: monospace;
  background: var(--bg-3);
  color: var(--text-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 0.6rem;
  box-sizing: border-box;
}

.session-actions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  align-items: center;
}
/* Inline AI status text next to "Generate Recap" — small + tertiary. */
.recap-status {
  font-size: var(--font-size-xs);
  color: var(--text-3);
}
/* Edit pushes itself + Delete to the right edge of the action bar; the
   recap-button + status sit at the left. Replaces inline margin-left:auto. */
.session-actions .edit-session-btn { margin-left: auto; }
/* (v784) Bespoke .delete-session-btn red-tint rules retired — destructive
   colouring now flows through .btn-new--danger composed in markup. */

#toast {
  position: fixed;
  bottom: 1.5rem;
  left: 50%;
  transform: translateX(-50%) translateY(1rem);
  background: var(--bg-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 0.6rem 1.1rem;
  font-size: var(--font-size-base);
  color: var(--text);
  box-shadow: var(--shadow);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--t-trans), transform var(--t-trans);
  z-index: 10001;
}
#toast.visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
  pointer-events: auto;
}
.toast-copy-btn {
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text-2);
  cursor: pointer;
  font-size: var(--font-size-xs);
  line-height: 1;
  margin-left: 0.6rem;
  padding: 0.15rem 0.4rem;
  vertical-align: middle;
  transition: background var(--t-hover) var(--ease), color var(--t-hover) var(--ease);
}
.toast-copy-btn:hover, .toast-copy-btn:focus-visible { background: var(--bg-3); color: var(--text); }

/* v1053: "new version available" banner — persistent (not a transient toast),
   sits bottom-center at 5rem so it clears the toast zone (#toast at 1.5rem).
   Reload button reuses the canonical .btn-new recipe. */
/* v1093: aligned to spec .tf-banner--update (teal-tinted info banner).
   Positioning + z-index kept since this is a fixed-position floater. */
.update-banner {
  position: fixed;
  bottom: 5rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 9500;
  display: flex;
  align-items: center;
  gap: 14px;
  background: color-mix(in srgb, var(--accent-teal) 8%, var(--bg));
  border: 1px solid color-mix(in srgb, var(--accent-teal) 40%, transparent);
  border-radius: 4px;
  padding: 12px 18px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.25);
  font-family: var(--type-ui);
  font-size: 13px;
  color: var(--text);
  max-width: calc(100vw - 2rem);
}
.update-banner-text { line-height: 1.3; }
.update-banner-dismiss {
  background: none;
  border: none;
  color: var(--text-3);
  cursor: pointer;
  font-size: var(--font-size-base);
  line-height: 1;
  padding: 0 0.25rem;
}
.update-banner-dismiss:hover, .update-banner-dismiss:focus-visible { color: var(--text); }
/* v1084: aligned to spec .tf-badge--moss (success) and --amber (warning). */
.entity-new-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: var(--radius-full);
  border: 1px solid color-mix(in srgb, var(--accent-green) 30%, transparent);
  font-size: 10.5px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--accent-green);
  background: color-mix(in srgb, var(--accent-green) 12%, var(--bg));
  margin-left: 0.4rem;
}
.entity-updated-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: var(--radius-full);
  border: 1px solid color-mix(in srgb, var(--accent-amber) 30%, transparent);
  font-size: 10.5px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--accent-amber);
  background: color-mix(in srgb, var(--accent-amber) 12%, var(--bg));
  margin-left: 0.4rem;
}
[data-theme="dark"] .entity-new-badge {
  color: #B5DAA1;
  background: color-mix(in srgb, var(--accent-green) 18%, var(--bg-2));
  border-color: color-mix(in srgb, var(--accent-green) 45%, transparent);
}
[data-theme="dark"] .entity-updated-badge {
  color: #F1B97A;
  background: color-mix(in srgb, var(--accent-amber) 18%, var(--bg-2));
  border-color: color-mix(in srgb, var(--accent-amber) 45%, transparent);
}

/* v1108: inline narrative entity-links aligned to spec .tf-link recipe.
   Dashed bottom border (not text-decoration dotted) in a 55%-tinted
   variant of the type color, so the underline reads quieter than the
   text itself — matches the spec recap mockup where Allen / Gibs /
   Iron Spire etc. show a subtle dashed underline. Hover deepens to
   the full type color. */
.entity-link {
  text-decoration: none;
  cursor: pointer;
  border-bottom: 1px dashed color-mix(in srgb, currentColor 55%, transparent);
  transition: color 160ms ease, border-bottom-color 160ms ease;
}
/* v1160: Aligned to ENTITY_CFG canonical map (app.js line ~1677) so a
   crosslink reads the same color as that entity's lead bar / thumb /
   eyebrow badge across Codex, Party, Quests, Atlas, Fates and Chronicle.
   Prior to v1160 this block had its own divergent allocation (npc=amber,
   item=red, faction=purple) which made the same NPC read as two different
   types depending on surface. */
.entity-link[data-type="pc"]        { color: var(--accent-blue); }
.entity-link[data-type="companion"] { color: var(--accent-teal); }
.entity-link[data-type="location"]  { color: var(--text-3); }
.entity-link[data-type="npc"]       { color: var(--color-muted-violet); }
.entity-link[data-type="item"]      { color: var(--accent-gold); }
.entity-link[data-type="faction"]   { color: var(--accent-amber); }
.entity-link[data-type="deity"]     { color: #7E3FA0; }
.entity-link[data-type="lore"]      { color: var(--accent-green); }
.entity-link[data-type="spell"]     { color: #2D6DA0; }
.entity-link:hover, .entity-link:focus-visible {
  border-bottom-color: currentColor;
  text-decoration: none;
}
.entity-link:focus { outline: 2px solid currentColor; outline-offset: 1px; }

.session-recap-result {
  /* var(--accent-teal)11 / 44 was invalid CSS (you can't suffix-concat alpha
     hex onto a CSS variable). Resolved to undefined → bg/border weren't
     rendering. color-mix gives the intended ~7%/27% teal tint. */
  background: color-mix(in srgb, var(--accent-teal) 7%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent-teal) 27%, transparent);
  border-radius: var(--radius-md);
  padding: 0.6rem 0.85rem;
  font-size: var(--font-size-base);
  color: var(--accent-teal);
}

.session-section {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.session-section-toggle {
  background: none;
  border: none;
  color: var(--text-2);
  font-size: var(--font-size-base);
  padding: 0.25rem 0;
  cursor: pointer;
  text-align: left;
  width: fit-content;
}
.session-section-toggle:hover, .session-section-toggle:focus-visible { color: var(--text); }

.section-empty { color: var(--text-3); }

.session-section-box {
  width: 100%;
  min-height: 120px;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 0.6rem;
  font-family: monospace;
  font-size: var(--font-size-base);
  color: var(--text);
  resize: vertical;
}
.session-section-pre {
  width: 100%;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent-teal);
  border-radius: var(--radius-md);
  padding: 0.6rem 0.75rem;
  font-family: monospace;
  font-size: var(--font-size-sm);
  color: var(--text-2);
  white-space: pre-wrap;
  word-break: break-word;
  margin: 0;
  max-height: 280px;
  overflow-y: auto;
}

/* Live transcript variant — greyed out, larger max-height for timestamped lines */
.session-live-transcript {
  color: var(--text-3);
  border-left-color: var(--accent-purple);
  max-height: 400px;
}

/* ── Speaker re-labeling bar ──────────────────────────────────────────────── */
.spk-bar {
  display: flex; flex-direction: column; align-items: stretch; gap: 0.45rem;
  background: var(--bg-2); border: 1px solid var(--border);
  border-bottom: none; border-radius: var(--radius-md) 6px 0 0;
  padding: 0.55rem 0.75rem;
  font-size: var(--font-size-sm);
}
.spk-bar-hdr {
  font-weight: 600; color: var(--text-2); white-space: nowrap; flex-shrink: 0;
  margin-bottom: 0.15rem;
}
.spk-row {
  display: flex; flex-direction: column; gap: 0.2rem;
  padding: 0.35rem 0.5rem;
  border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--bg) 40%, transparent);
}
.spk-row-top {
  display: flex; align-items: center; gap: 0.35rem;
}
.spk-row-sample {
  display: flex; align-items: flex-start; gap: 0.35rem;
  padding-left: 0.15rem;
  font-size: var(--font-size-sm);
  color: var(--text-2);
  font-style: italic;
  line-height: 1.45;
}
.spk-quote-text { flex: 1; min-width: 0; }
.spk-quote-more {
  margin-left: 0.35rem;
  font-size: 0.65rem; font-weight: 600; font-style: normal;
  color: var(--color-purple);
  background: color-mix(in srgb, var(--color-purple) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-purple) 28%, transparent);
  border-radius: var(--radius-full);
  padding: 0.05rem 0.35rem;
  white-space: nowrap;
}
.spk-quote-dismiss {
  background: none; border: none;
  color: var(--text-3); cursor: pointer;
  font-size: var(--font-size-base); line-height: 1;
  padding: 0 0.3rem;
  opacity: 0.65;
  transition: opacity var(--t-hover), color var(--t-hover);
  flex-shrink: 0;
}
.spk-quote-dismiss:hover, .spk-quote-dismiss:focus-visible { opacity: 1; color: var(--color-danger); }
.spk-token-chip {
  font-family: monospace; font-size: var(--font-size-sm); font-weight: 700;
  color: var(--accent-purple);
  background: color-mix(in srgb, var(--accent-purple) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent-purple) 30%, transparent);
  border-radius: var(--radius-sm); padding: 1px 5px;
  white-space: nowrap; max-width: 80px; overflow: hidden; text-overflow: ellipsis;
}
.spk-arrow { color: var(--text-3); font-size: var(--font-size-sm); }
.spk-avatar-preview {
  width: 24px; height: 24px; border-radius: 50%; overflow: hidden; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
}
.spk-avatar-preview img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.spk-select {
  font-size: var(--font-size-sm); padding: 2px 4px;
  background: var(--bg-3); border: 1px solid var(--border);
  border-radius: var(--radius-sm); color: var(--text); cursor: pointer;
  max-width: 150px;
}
/* Round off the transcript pre when speaker bar is above it */
.spk-bar + .session-live-transcript {
  border-radius: 0 0 6px 6px;
  border-top: none;
}

/* ── Live Session Entries section ─────────────────────────────────────────── */
.le-panel {
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent-teal);
  border-radius: var(--radius-md);
  background: var(--bg-2);
  overflow: hidden;
}
.le-list { display: flex; flex-direction: column; }
.le-row {
  display: grid;
  grid-template-columns: 4.5rem 1.4rem 1fr auto;
  gap: 0.4rem;
  align-items: baseline;
  padding: 0.35rem 0.65rem;
  border-bottom: 1px solid var(--border);
  font-size: var(--font-size-base);
}
.le-row:last-child { border-bottom: none; }
.le-ts {
  font-family: monospace;
  font-size: var(--font-size-sm);
  color: var(--text-3);
  white-space: nowrap;
}
.le-tag { font-size: var(--font-size-md); text-align: center; }
.le-content { color: var(--text-1); word-break: break-word; line-height: 1.45; }
.le-key .le-content { font-weight: 600; }
.le-count { font-size: var(--font-size-sm); color: var(--text-3); font-weight: 400; }
/* Auto-generated live beats — tint the tag column for visual grouping */
.le-story   .le-tag { color: var(--color-brand); }
.le-combat  .le-tag { color: var(--color-danger); }
.le-context .le-tag { color: var(--color-muted-violet, #7C6FA0); }
/* Row actions — always visible so the affordance is obvious on touch + desktop */
.le-actions {
  display: inline-flex;
  gap: 0.15rem;
  align-self: start;
  opacity: 0.55;
  transition: opacity var(--t-hover) ease;
}
.le-row:hover .le-actions,
.le-row:focus-within .le-actions { opacity: 1; }
.le-actions .btn-xs { padding: 0 0.35rem; line-height: 1.3; }
.le-del-btn { color: var(--color-danger); }
.le-edit-box {
  width: 100%;
  min-height: 3rem;
  font-family: inherit;
  font-size: var(--font-size-sm);
  line-height: 1.45;
  padding: 0.35rem 0.5rem;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--color-brand);
  border-radius: var(--radius-sm);
  resize: vertical;
}
.le-empty {
  padding: 0.6rem 0.75rem;
  font-size: var(--font-size-sm);
  color: var(--text-3);
  margin: 0;
}

.session-zero .session-card-hdr { border-left: 3px solid var(--color-text-secondary); }

.session-zero-badge {
  color: var(--color-text-secondary);
}

.session-entries-list {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.entry-item {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.6rem;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.entry-item.entry-new {
  border-style: dashed;
}

.entry-pc-row { margin-bottom: 0.3rem; }
.entry-pc-row select {
  background: var(--bg-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: var(--font-size-base);
  padding: 0.25rem 0.4rem;
  cursor: pointer;
}

.entry-box, .entry-new-box {
  width: 100%;
  min-height: 80px;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--border);
  padding: 0.3rem 0.2rem;
  font-family: monospace;
  font-size: var(--font-size-base);
  color: var(--text);
  resize: vertical;
}
.entry-box:focus, .entry-new-box:focus { outline: none; border-bottom-color: var(--accent-teal); }

.entry-item-actions {
  display: flex;
  gap: 0.4rem;
  justify-content: flex-end;
}

.checkbox-label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: 500;
  cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
  width: 1rem;
  height: 1rem;
  cursor: pointer;
}

/* ── Chat floating widget ────────────────────────────────────────────────── */
#chat-widget {
  position: fixed;
  bottom: 1.5rem;
  right: 1.5rem;
  z-index: 200;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.75rem;
}
#chat-widget.hidden { display: none; }

/* Floating action button — glass-translucent treatment that mirrors the
   admin-pulse panel (bottom-left mirror of this bottom-right button). The
   solid full-saturation teal background was visually loud; a teal-stroked
   icon on a frosted-surface background reads as "AI assistant available"
   without dominating the corner. Size + position unchanged. */
#chat-fab {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--color-surface) 18%, transparent);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: var(--accent-teal);
  border: 1px solid color-mix(in srgb, var(--color-border) 30%, transparent);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 20px rgba(0,0,0,0.10);
  transition:
    background       var(--t-hover),
    border-color     var(--t-hover),
    backdrop-filter  var(--t-hover),
    box-shadow       var(--t-hover),
    transform        var(--t-hover),
    color            var(--t-hover);
  flex-shrink: 0;
}
#chat-fab:hover, #chat-fab:focus-visible {
  background: color-mix(in srgb, var(--color-surface) 75%, transparent);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-color: color-mix(in srgb, var(--color-border) 60%, transparent);
  box-shadow: 0 4px 20px rgba(0,0,0,0.18);
  transform: translateY(-1px);
}
/* Open state — chat panel is visible; tone the icon down so it doesn't
   compete with the popout's content. */
#chat-fab.chat-fab-open { color: var(--text-3); }

/* Popout panel */
.chat-popout {
  width: 340px;
  height: 480px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-2xl);
  box-shadow: 0 8px 32px rgba(0,0,0,0.22);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  animation: chat-pop-in 0.18s ease;
}
.chat-popout.hidden { display: none; }
@keyframes chat-pop-in {
  from { opacity: 0; transform: translateY(12px) scale(0.97); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

/* v822: teal backdrop + bold title retired in favor of the canonical
   eyebrow header recipe — uppercase / 700 / --font-size-xs / --text-3 /
   0.06em — so the chat popout reads as a panel header consistent with
   modal-hdr eyebrows app-wide. */
.chat-popout-hdr {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.7rem 0.85rem;
  background: transparent;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.chat-popout-title {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-family: inherit;
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-3);
}
.chat-popout-hdr-actions { display: flex; gap: 0.25rem; }
.chat-hdr-btn {
  background: transparent;
  border: none;
  color: var(--text-3);
  cursor: pointer;
  padding: 0.2rem 0.4rem;
  border-radius: var(--radius-sm);
  font-size: var(--font-size-base);
  line-height: 1;
  transition: background var(--t-hover), color var(--t-hover);
}
.chat-hdr-btn:hover, .chat-hdr-btn:focus-visible { background: var(--bg-3); color: var(--text-1); }

.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
/* v965: .chat-empty inherits canonical .placeholder typography (sm,
   italic, dim, left-aligned). Only padding remains — chat empty needs
   a little vertical breathing room inside its column. */
.chat-empty {
  padding: var(--space-5) var(--space-4);
  line-height: 1.6;
}
.chat-msg { display: flex; }
.chat-msg-user      { justify-content: flex-end; }
.chat-msg-assistant { justify-content: flex-start; }

.chat-bubble {
  max-width: 82%;
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius-2xl);
  font-size: var(--font-size-base);
  line-height: 1.5;
  word-break: break-word;
}
/* v823: user bubble retired the filled-teal recipe in favor of a teal
   outline + default text color — reads as a quoted message rather than a
   loud chat-app bubble, and matches the rest of the app's ghost idiom. */
.chat-msg-user .chat-bubble {
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--accent-teal) 50%, transparent);
  color: var(--text-1);
  border-bottom-right-radius: 3px;
}
.chat-msg-assistant .chat-bubble {
  background: var(--bg-2);
  border: 1px solid var(--border);
  color: var(--text);
  border-bottom-left-radius: 3px;
}

/* Typing indicator */
.chat-typing {
  display: flex;
  gap: 4px;
  align-items: center;
  padding: 0.6rem 0.75rem;
}
.chat-typing span {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--text-3);
  animation: chat-bounce 1.2s infinite;
}
.chat-typing span:nth-child(2) { animation-delay: var(--t-trans); }
.chat-typing span:nth-child(3) { animation-delay: var(--t-trans); }
@keyframes chat-bounce {
  0%, 80%, 100% { transform: translateY(0); }
  40% { transform: translateY(-5px); }
}

.chat-input-bar {
  display: flex;
  gap: 0.4rem;
  padding: 0.6rem 0.7rem;
  border-top: 1px solid var(--border);
  background: var(--bg);
  flex-shrink: 0;
}
.chat-input-bar textarea {
  flex: 1;
  resize: none;
  border-radius: var(--radius-lg);
  padding: 0.45rem 0.6rem;
  font-size: var(--font-size-base);
  line-height: 1.4;
  background: var(--bg-2);
  border: 1px solid var(--border);
  color: var(--text);
  font-family: inherit;
}
.chat-input-bar textarea:focus { outline: none; border-color: var(--accent-teal); }
/* v822: drop the font-size + padding override so the Send button inherits the
   canonical .btn-new pill recipe (0.5rem 1rem, --font-size-sm) — matches every
   other primary CTA across the app. Layout-only properties remain. */
.chat-input-bar .btn-new { align-self: flex-end; white-space: nowrap; }

/* v827: tools-used audit footer inside each assistant bubble. A dim,
   monospaced line under a dashed separator showing which tools the
   Bardic Companion called to compose the answer — lets the user verify
   the model actually consulted the records and gives a spot-check
   surface when an answer feels off. */
.chat-tools-used {
  display: block;
  margin-top: 0.5rem;
  padding-top: 0.4rem;
  border-top: 1px dashed color-mix(in srgb, var(--text-3) 35%, transparent);
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 0.68rem;
  line-height: 1.4;
  color: var(--text-3);
  opacity: 0.75;
  word-break: break-all;
}
.chat-tools-used-icon {
  margin-right: 0.25rem;
  opacity: 0.85;
}

/* ── Quest Log — two-panel layout ───────────────────────────────────────── */
/* v1157: outer #tab-quests is just a zero-padded shell now — the
   scrolling + width-cap padding lives on .quest-tab-panel (mirroring
   Party tab's #tab-party + .party-tab-panel two-layer structure). */
#tab-quests.tab-panel {
  padding: 0;
}
/* v1157: inner scroll/width-cap wrapper — same recipe as
   .party-tab-panel so both tabs render at IDENTICAL content widths.
   The percentage 100% resolves against #tab-quests (the outer shell,
   no scrollbar) instead of the scrolling element itself, matching how
   Party computes its padding-inline. */
.quest-tab-panel {
  padding-left: max(var(--space-4), calc((100% - 1280px) / 2));
  padding-right: max(var(--space-4), calc((100% - 1280px) / 2));
  overflow-y: auto;
  height: calc(100vh - var(--chrome-height));
}

/* ── Manage tab (combined GM panel: settings + roster + feed) ──────────────
   Same split-scroll grammar as Chronicle (.journey-layout) and Quests
   (.quest-layout): wrapper scrolls, left col flows, right col sticks
   so the feed stays visible while the GM works through settings. v777. */
#tab-manage.tab-panel {
  padding: 0;
  overflow-y: auto;
}
.manage-layout {
  display: flex;
  align-items: flex-start;
  min-height: 100%;
}
.manage-nav-col {
  flex: 0 0 56%;
  min-width: 360px;
  position: relative;
  /* v787: canonical eyebrow top-left spacing — matches every other tab. */
  padding: var(--space-5) var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  box-sizing: border-box;
}
.manage-nav-col::after {
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: 1px;
  background: linear-gradient(to bottom,
    transparent 0%,
    color-mix(in srgb, var(--border) 50%, transparent) 15%,
    var(--border) 50%,
    color-mix(in srgb, var(--border) 50%, transparent) 85%,
    transparent 100%);
}
.manage-feed-col {
  flex: 1;
  min-width: 0;
  position: sticky;
  top: 0;
  max-height: 100vh;
  overflow: hidden;
  box-sizing: border-box;
}
.manage-feed-col .feed-layout { height: 100%; }
/* Each section in the left col — campaign settings, roster, etc. Stack
   gap is provided by .manage-nav-col's `gap`, not bottom-margins. */
.manage-section {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  /* Each section reads as its own card so the Manage tab feels
     structured rather than a wall of fields. Mirrors the design-token
     surface treatment used elsewhere: bg-2 + 1px border + radius-lg
     + (vertical 16, horizontal 20) padding. */
  background-color: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5);
}

/* ── Input with inline X-clear affordance ────────────────────────────────
   Canonical "clear is never a button" pattern: the X sits right-justified
   inside the input. Clicking it empties the field AND fires the input's
   row-level Save handler so the cleared state persists. Hidden when the
   input is empty (nothing to clear). Wired in JS via .input-clear-x. */
.input-with-clear {
  position: relative;
  display: inline-flex;
  flex: 1;
  min-width: 0;
}
.input-with-clear > .login-input {
  width: 100%;
  /* Reserve enough right-padding for the X so typed text doesn't underlap it. */
  padding-right: 2rem;
}
.input-clear-x {
  position: absolute;
  right: 0.4rem;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  color: var(--text-3);
  cursor: pointer;
  padding: 0.2rem 0.35rem;
  font-size: 0.85rem;
  line-height: 1;
  border-radius: var(--radius-sm);
  transition: color var(--t-hover) ease, background-color var(--t-hover) ease;
}
.input-clear-x:hover, .input-clear-x:focus-visible {
  color: var(--color-danger);
  background-color: color-mix(in srgb, var(--color-danger) 12%, transparent);
}
.input-clear-x.hidden { display: none; }

/* Indented sub-rows: Recap / Announce / Reveal under Server ID. Subtle
   vertical line on the left communicates "these are children of the
   server context above" without inventing a tree-widget. */
.manage-form-row--indented {
  padding-left: var(--space-4);
  border-left: 2px solid var(--border);
  margin-left: var(--space-2);
}

/* Datetime input with an "Auto-populated" overlay badge inside. When
   event-sync is on the badge floats at the right edge of the input so
   the state lives on the field, not in a separate line below. */
.input-with-overlay {
  position: relative;
  display: block;
}
.input-with-overlay > input { width: 100%; }
.input-overlay-badge {
  position: absolute;
  right: 0.5rem;
  top: 50%;
  transform: translateY(-50%);
  font-size: var(--font-size-xs);
  color: var(--text-3);
  background-color: var(--bg-2);
  padding: 0.15rem 0.45rem;
  border-radius: var(--radius-sm);
  pointer-events: none;
  letter-spacing: 0.02em;
}
.input-overlay-badge.hidden { display: none; }
/* Section header — eyebrow LEFT, optional CTA RIGHT. Mirrors the canonical
   toolbar rhythm without re-rolling typography (.form-section-label is
   composed in the markup). */
.manage-section-hdr {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}
/* Form rows inside the Manage tab — gap-driven layout instead of the
   per-row margin-bottom the modal used. Replaces inline
   style="margin-top:1.25rem;margin-bottom:0" on the cover-image row. */
.manage-form-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.manage-form-row > label { font-size: var(--font-size-sm); font-weight: 500; color: var(--text-2); }
/* Capability-gate disabled state for toggle labels — used by the Discord
   event-sync and live-invite checkboxes (and any future "this toggle
   needs prereqs" pattern). Replaces inline style.opacity / style.cursor
   sprinkles that didn't survive theme switches and were invisible under
   light mode. v1191 audit fix. The selectors are scoped under the
   label class the existing toggles use (.rec-sys-audio-label) so the
   recipe doesn't accidentally leak to other surfaces. */
.rec-sys-audio-label.cap-gate-disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
.rec-sys-audio-label.cap-gate-disabled input { cursor: not-allowed; }
/* Inline rows for invite-code controls (input + Copy + Save) and cover
   controls (Choose / Remove / status). Replace inline
   style="display:flex;gap:0.5rem;align-items:center" plus the wrap variant. */
.camp-invite-row,
.camp-cover-row {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  flex-wrap: wrap;
}
.camp-invite-inp {
  margin-bottom: 0;
  flex: 1;
  min-width: 200px;
}
/* Tighter danger zone in the manage layout — the modal needed bigger
   margins because it was a tall modal; here the section gap already
   provides breathing room. */
.manage-danger-zone {
  margin-top: var(--space-2);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
  display: flex;
  justify-content: flex-end;
}
/* Success-tinted helper line — green text, used by the "Saved!" message
   after invite-code save. Replaces inline
   style="font-size:0.8rem;color:var(--color-success);margin-top:0.3rem". */
.form-helper-text--success {
  margin-top: 3px;
  margin-bottom: 0;
  color: var(--color-success);
  font-size: var(--font-size-sm);
}


/* v1157: layout is now just a flex container — padding-inline + scroll
   live on the parent .quest-tab-panel wrapper (mirrors Party's
   .party-tab-panel inside #tab-party). Earlier attempts to put the
   padding-inline on .quest-layout itself produced a subtly narrower
   content area than Party because the 100% reference was the
   scrolling element (which lost width to its own scrollbar).
   v1160: padding-top so the Track Quest list + Quest Detail panel have
   breathing room below the tab strip (matches .journey-layout's
   Session ↔ Chronicle spacing). */
.quest-layout {
  display: flex;
  align-items: flex-start;
  min-height: 100%;
  padding-top: var(--space-4);
}

/* ── Left nav column — mirrors .journey-sessions-col (free-floating, no box).
   v1142: 42% → 40% so the list reads as ~2/3 the size of the detail panel
   per user preference. Spec mockup uses 1fr / 1.4fr (41.7% / 58.3%); 40/60
   leans slightly more detail-heavy which fits the user's "details are the
   primary surface" framing. */
.quest-nav-col {
  flex: 0 0 40%;
  min-width: 320px;
  position: relative;
}

/* Subtle fade divider between columns (same as session panel) */
.quest-nav-col::after {
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: 1px;
  background: linear-gradient(to bottom,
    transparent 0%,
    color-mix(in srgb, var(--border) 50%, transparent) 15%,
    var(--border) 50%,
    color-mix(in srgb, var(--border) 50%, transparent) 85%,
    transparent 100%);
}

/* Eyebrow LEFT (with inline hide-completed toggle), "+ New Quest" CTA
   RIGHT. The horizontal divider was removed in v761 — canonical pattern
   is no border-bottom under section headers. */
.quest-nav-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  padding: var(--space-5) var(--space-4) var(--space-2);
  flex-shrink: 0;
  gap: var(--space-2);
}
.quest-nav-toolbar-left {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

/* (Retired in v764) .quest-hide-label / #quest-hide-completed went away
   when the explicit Hide-Completed toggle was removed. Completed and
   failed quests now stay in the nav permanently, visually greyed via
   .quest-nav-row.is-completed (defined alongside .quest-nav-row below). */

.quest-nav-list {
  padding: 0 var(--space-4) var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* v968: .quest-nav-empty retired — Quests empty state now composes the
   canonical .placeholder.placeholder--inline like every other empty
   placeholder app-wide. */

/* (Retired in v762) The .quest-group-hdr / -chevron / -label / -sublabel /
   -count block re-implemented the .form-section-label eyebrow recipe and
   was never referenced by the JS — renderQuestNav() emits a flat sorted
   list. Two related dark-mode rules at lines ~6052 and ~6215 were also
   removed. The Leads count chip retained as .quest-leads-count below. */

/* ── Quest nav rows — share the canonical card recipe with .session-card,
   .campaign-card, .entity-row, .party-pc-card, .quotes-pc-section.
   Shadowless at rest, hover lifts with translateY + teal-tinted border
   + drop shadow. Selected state keeps the teal border tint without
   extra background fill (the hover lift already carries the affordance). */
/* v1087: chassis (bg, border, radius, hover, selected, completed/departed)
   now comes from composing .tf-card --sm + .is-selected / .is-departed.
   Outer padding zeroed so .quest-nav-hdr keeps owning its 0.85rem 1rem
   internal padding. */
.quest-nav-row { overflow: hidden; }
.tf-card.quest-nav-row { padding: 0; }
/* Restore-on-hover behavior for closed quests so they're easy to inspect
   (the spec's .is-departed opacity 0.7 + grayscale 55% is sticky). */
.tf-card.quest-nav-row.is-departed:hover,
.tf-card.quest-nav-row.is-departed:focus-within,
.tf-card.quest-nav-row.is-departed.is-selected {
  opacity: 1;
  filter: none;
}

.quest-nav-hdr {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.85rem 1rem;
  cursor: pointer;
  user-select: none;
}
.quest-nav-hdr:hover, .quest-nav-hdr:focus-visible { background: var(--bg-3); }
/* v1087: selected state composes spec .is-selected; the chassis-level
   bg-color override there suffices, no header-bg override needed. */

/* Status badge: same font as .session-num — no pill, just colored text */
.quest-nav-badge {
  font-size: var(--font-size-xs);
  font-weight: 700;
  color: var(--qbadge-color, var(--accent-teal));
  white-space: nowrap;
  flex-shrink: 0;
}
/* Icon variant — single-character status indicator (□ Active, □ purple
   Background, ✓ Complete, ✗ Failed). Sized up so the glyph reads at a
   glance next to the title text; .quest-nav-row.is-completed handles
   the dimming/desaturation. */
.quest-nav-badge--icon {
  font-size: var(--font-size-md);
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.1em;
}
/* Purple PC-name suffix for Background quests — sits inline after the
   quest title in the nav row; same accent-purple as the badge square. */
.quest-nav-pc {
  color: var(--accent-purple);
  font-size: var(--font-size-sm);
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex-shrink: 1;
  min-width: 0;
}
/* (Retired in v772) .quest-pc-link was the inline detail-header crosslink
   to the PC. The detail header is now title-only; PC linkage shows as a
   purple suffix in the nav row via .quest-nav-pc above. */

/* Quest title: identical to .session-title */
.quest-nav-title {
  font-weight: 600;
  flex: 1;
  color: var(--text-1);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Right detail column — sticky like .journey-summary-col ──
   v1162: zero outer padding. The inner .journey-summary-card chassis
   provides padding: 20px 24px + border + bg + radius. Chronicle's col
   IS its own chassis (single layer, ~42px gap from list-content to
   Chronicle-content). Quest detail uses the standard inner-card pattern
   (two layers); leaving the outer col padded too made the gap ~66px —
   24px wider than Chronicle. Zeroing the col padding lets the inner
   card sit at the col edge, matching Chronicle's effective spacing. */
.quest-detail-col {
  flex: 1;
  min-width: 0;
  padding: 0;
  position: sticky;
  top: 0;
  max-height: 100vh;
  overflow-y: auto;
  box-sizing: border-box;
}

.quest-detail-panel {
  display: flex;
  flex-direction: column;
  min-height: 100%;
}

/* v976: .quest-detail-placeholder retired. Detail-pane placeholders now
   compose the canonical .placeholder.placeholder--inline directly so they
   sit at the same x as every other in-tab hint and don't carry a bespoke
   vertical padding override. */

/* Quest detail header — title-block (title + Introduced label stacked) on
   the left, actions cluster (Edit / Delete) on the right.
   v761: parallel .quest-detail-inner / -hdr / -title / -meta / -badge /
   -divider block retired (renderQuestDetail uses .journey-summary-card).
   v1144: pulled .quest-detail-session OUT of the meta-actions cluster and
   placed it under the title via a new .quest-detail-title-block flex
   column. Long quest titles no longer have to wrap around "Introduced:". */
.quest-detail-title-block {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.quest-detail-meta-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-left: auto;
  flex-shrink: 0;
}
.quest-detail-session {
  font-size: var(--font-size-sm);
  color: var(--text-3);
}

/* Objectives section */
.quest-detail-objectives { margin-bottom: 0; }

/* Typography is now inherited from .form-section-label (composed via the
   markup); .quest-obj-header just contributes the bottom spacing before
   the objective/lead row list. */
.quest-obj-header { margin-bottom: 0.6rem; }

.quest-obj-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

/* v1119: quest objective rows aligned to spec .tf-quest-obj li recipe.
   Display-serif 14px / 1.5 / --text-1 with a dashed bottom divider
   between rows. Diamond marker replaced by a small empty checkbox-like
   square in --text-3 — visually consistent with the spec's checklist
   look without making objectives actually checkable (that's the
   side/optional feature the user said to skip). */
.quest-obj-row {
  display: grid;
  grid-template-columns: 16px 1fr;
  align-items: baseline;
  gap: 10px;
  padding: 6px 4px;
  border-bottom: 1px dashed color-mix(in srgb, var(--border) 60%, transparent);
  font-family: var(--type-display);
  font-size: 14px;
  line-height: 1.5;
  color: var(--text-1);
}
.quest-obj-row:last-child { border-bottom: 0; }

.quest-obj-diamond {
  width: 16px;
  height: 16px;
  border-radius: 4px;
  border: 1.5px solid var(--text-3);
  background: transparent;
  flex-shrink: 0;
  margin-top: 3px;
  font-size: 0;
  line-height: 0;
}

.quest-obj-text {
  font-family: var(--type-display);
  font-size: 14px;
  color: var(--text-1);
  line-height: 1.5;
}

.quest-obj-empty {
  font-size: var(--font-size-base);
  color: var(--text-3);
  font-style: italic;
}

/* ── Investigation leads (next-step threads on a quest) ──────────────────── */
/* Dashed border-top retired in v763 — only dashed divider in the app, and
   the section-label rhythm ("Leads N open") + extra margin-top already
   reads as a separate sub-section. */
.quest-detail-leads {
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}
/* Lead count chip + lead toggles inherit colour from the parent quest's
   identity via --lead-color (set inline on .quest-detail-leads):
   green for active/party-shared, purple for background/PC-personal.
   Visual hierarchy: green = "in progress / actionable",
   purple = "background / personal", muted/red = "settled / closed." */
.quest-leads-count {
  margin-left: 0.4rem;
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--lead-color, var(--color-success));
  background: color-mix(in srgb, var(--lead-color, var(--color-success)) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--lead-color, var(--color-success)) 28%, transparent);
  border-radius: var(--radius-full);
  padding: 0.08rem 0.45rem;
  letter-spacing: 0.03em;
  text-transform: none;
}
.quest-lead-row {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.35rem 0.1rem;
  border-radius: var(--radius-md);
  transition: background var(--t-hover);
}
.quest-lead-row:hover, .quest-lead-row:focus-visible { background: color-mix(in srgb, var(--color-border) 25%, transparent); }
.quest-lead-toggle {
  background: none;
  border: none;
  color: var(--lead-color, var(--color-success));
  font-size: var(--font-size-lg);
  line-height: 1;
  cursor: pointer;
  padding: 0.1rem 0.15rem;
  flex-shrink: 0;
}
.quest-lead-toggle:hover, .quest-lead-toggle:focus-visible { color: var(--color-brand); }
.quest-lead-text {
  flex: 1;
  font-size: var(--font-size-md);
  color: var(--text-2);
  line-height: 1.55;
}
.quest-lead-resolved .quest-lead-text {
  color: var(--text-3);
  text-decoration: line-through;
  text-decoration-color: color-mix(in srgb, var(--text-3) 60%, transparent);
}
.quest-lead-edit,
.quest-lead-delete {
  background: none;
  border: none;
  color: var(--text-3);
  cursor: pointer;
  padding: 0.1rem 0.3rem;
  font-size: var(--font-size-sm);
  opacity: 0;
  transition: opacity var(--t-hover), color var(--t-hover);
}
.quest-lead-row:hover .quest-lead-edit, .quest-lead-row:focus-within .quest-lead-edit,
.quest-lead-row:hover .quest-lead-delete, .quest-lead-row:focus-within .quest-lead-delete { opacity: 1; }
.quest-lead-edit:hover, .quest-lead-edit:focus-visible { color: var(--color-brand); }
.quest-lead-delete:hover, .quest-lead-delete:focus-visible { color: var(--color-danger); }
.quest-leads-resolved-wrap {
  margin-top: 0.5rem;
  font-size: var(--font-size-sm);
}
/* Typography inherits from .form-section-label (composed in markup);
   summary only adds the cursor + click padding. */
.quest-leads-resolved-wrap summary {
  cursor: pointer;
  padding: 0.25rem 0;
}
.quest-lead-edit-form {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: 1;
}
.quest-lead-edit-form .quest-lead-input { flex: 1; }
.quest-lead-add-row {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-3);
}
/* bg / border / radius / colour / focus inherit from the global
   input[type="text"] rule (line ~342). Local overrides only adjust
   size for the inline lead-add affordance. */
.quest-lead-add-row .quest-lead-input {
  flex: 1;
  padding: 0.4rem 0.6rem;
  font-size: var(--font-size-sm);
}
.quest-lead-add-btn {
  font-size: var(--font-size-xs);
  padding: 0.35rem 0.75rem;
  white-space: nowrap;
}

/* Detail action row — match session-actions. The previous margin-top:auto
   pushed actions to the bottom of the panel, orphaning them from the
   leads section on tall viewports. Now they sit naturally below the leads
   block with the standard top-padded divider. */
.quest-detail-actions {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
  align-items: center;
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
  margin-top: var(--space-3);
}

/* (Retired in v763) .quest-pc-tag was orphan CSS — Tier 2 routed PC
   linkage on background quests through .quest-nav-badge + .quest-pc-link
   crosslinks instead. The dark-mode override at line ~7719 was also
   removed. */

/* Responsive */
@media (max-width: 700px) {
  .quest-layout { flex-direction: column; }
  .quest-nav-col { flex: none; min-width: 0; width: 100%; }
  .quest-nav-col::after { top: auto; right: 0; bottom: 0; left: 0; width: auto; height: 1px; background: linear-gradient(to right, transparent 0%, color-mix(in srgb, var(--border) 50%, transparent) 15%, var(--border) 50%, color-mix(in srgb, var(--border) 50%, transparent) 85%, transparent 100%); }
  .quest-detail-col { position: static; max-height: none; padding: 1rem; }
  .quest-detail-inner { padding: 1rem 1.25rem; }
}

/* ── Alias chips ─────────────────────────────────────────────────────────── */
.alias-input-row {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}
.alias-input-row input { flex: 1; }

.alias-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-top: 0.4rem;
}

.alias-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  background: var(--accent-teal);
  color: #fff;
  font-size: var(--font-size-sm);
  padding: 0.2rem 0.5rem;
  border-radius: var(--radius-full);
}
.alias-chip button {
  background: none;
  border: none;
  color: rgba(255,255,255,0.8);
  cursor: pointer;
  font-size: var(--font-size-base);
  line-height: 1;
  padding: 0;
}
.alias-chip button:hover, .alias-chip button:focus-visible { color: #fff; }

/* Read-only alias chips in compendium card */
.alias-chip-ro {
  display: inline-flex;
  align-items: center;
  background: var(--bg-3);
  color: var(--text-2);
  font-size: var(--font-size-xs);
  padding: 0.15rem 0.45rem;
  border-radius: var(--radius-full);
  border: 1px solid var(--border);
}
/* Read-only chip-row spacing — separates the alias row from the description
   below. Replaces inline style="margin-bottom:0.5rem". */
.alias-chips--ro { margin-bottom: var(--space-2); }

/* ── D&D Beyond PC header ────────────────────────────────────────────────── */
.ddb-url-row {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}
.ddb-url-row input { flex: 1; }

.ddb-header {
  display: flex;
  flex-direction: row;
  align-items: stretch;
  background: var(--bg-2);
  border: 1px solid var(--accent-blue);
  border-radius: var(--radius-md);
  margin-bottom: 0.75rem;
  font-size: var(--font-size-base);
  overflow: hidden;
}
.ddb-portrait {
  flex-shrink: 0;
  overflow: hidden;
  /* width and height are set by JS to header.offsetHeight for a true 1:1 square */
}
.ddb-portrait img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--portrait-x, 50%) center;
  display: block;
}
.ddb-content {
  flex: 1;
  padding: 0.75rem 1rem;
  min-width: 0;
}
.ddb-header-empty {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem 1rem;
}
.ddb-header-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  margin-bottom: 0.15rem;
}
.ddb-title {
  font-weight: 600;
  /* Secondary text colour — was --accent-blue, which made "Level X Species"
     compete with the PC name. Now reads as supporting metadata. */
  color: var(--text-2);
  font-size: var(--font-size-md);
}
.ddb-background {
  margin-left: 0.5rem;
  font-size: var(--font-size-sm);
  color: var(--text-3);
  font-style: italic;
}
.ddb-class-line {
  color: var(--text-2);
  font-size: var(--font-size-base);
  margin-bottom: 0.5rem;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem;
}
/* v1084: aligned to spec .tf-badge--teal. */
.pc-role-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 10.5px;
  font-weight: var(--font-weight-semibold);
  padding: 3px 9px;
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--accent-teal) 12%, var(--bg));
  border: 1px solid color-mix(in srgb, var(--accent-teal) 30%, transparent);
  color: var(--accent-teal);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.5;
  white-space: nowrap;
}
/* v1093: aligned to spec .tf-noncombat — violet pill, uppercase, 0.06em. */
.pc-noncombat-badge {
  --chip-color: #7E3FA0;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 10.5px;
  font-weight: 600;
  padding: 3px 9px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--chip-color) 12%, var(--bg));
  border: 1px solid color-mix(in srgb, var(--chip-color) 30%, transparent);
  color: var(--chip-color);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.5;
  white-space: nowrap;
}
[data-theme="dark"] .pc-noncombat-badge {
  color: color-mix(in srgb, #7E3FA0 35%, #fff);
  background: color-mix(in srgb, #7E3FA0 28%, var(--bg-2));
  border-color: color-mix(in srgb, #7E3FA0 42%, transparent);
}
/* v1084: aligned to spec .tf-badge--gold. */
.pc-ai-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 10.5px;
  font-weight: var(--font-weight-semibold);
  padding: 3px 9px;
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--accent-gold) 16%, var(--bg));
  border: 1px solid color-mix(in srgb, var(--accent-gold) 35%, transparent);
  color: var(--accent-gold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.5;
  white-space: nowrap;
  opacity: 0.85;
}
.pc-ai-banner {
  width: 100%;
  padding: 0.3rem 0.6rem;
  margin: 0.25rem 0 0.1rem;
  background: color-mix(in srgb, var(--accent-amber) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent-amber) 45%, transparent);
  border-radius: var(--radius-md);
  color: var(--accent-amber);
  font-size: var(--font-size-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-align: center;
}
.pc-player-name {
  font-size: var(--font-size-xs);
  color: var(--text-3);
  margin-left: 0.35rem;
  font-style: italic;
}
.pc-pronouns {
  font-size: var(--font-size-xs);
  color: var(--text-3);
  background: var(--bg-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  padding: 0 0.45rem;
  margin-left: 0.35rem;
  font-style: normal;
  vertical-align: middle;
}

/* Combat quick-reference row — v1148 aligned to spec .tf-pc-combat:
   strict 4-col grid (HP / AC / INIT / SPEED) instead of flex+wrap so the
   4 stats always line up identically across PC cards.
   v1151: switched numeral typography from mono to UI sans with
   tabular-nums per user direction — mono fonts render 0 with a center
   dot that reads as 8 at small sizes. Sans + tabular-nums keeps the
   column alignment without the ambiguity. Same change applied to
   ability cells and skill values below. */
.ddb-combat-row {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 6px;
  margin-bottom: 0.6rem;
}
.ddb-stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  background: color-mix(in srgb, var(--text-1) 3%, var(--bg));
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 8px;
  text-align: center;
  font-family: var(--type-ui);
  font-size: var(--font-size-stat);
  font-weight: 700;
  color: var(--text-1);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
[data-theme="dark"] .ddb-stat { background: color-mix(in srgb, var(--text-1) 12%, var(--bg)); }
.ddb-stat-label {
  display: block;
  font-family: var(--type-ui);
  font-size: var(--font-size-2xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
  line-height: 1;
  white-space: nowrap;
}

/* Ability scores grid — v1148 aligned to spec .tf-pc-abilities: strict
   6-col grid (STR/DEX/CON/INT/WIS/CHA) so the layout is locked across
   all PCs. Each cell uses ink-3% wash + 1px rule border, mono 16px
   modifier with a smaller score below in tertiary text. */
.ddb-ability-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 5px;
  margin-bottom: 0.5rem;
}
.ddb-ab-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  background: color-mix(in srgb, var(--text-1) 3%, var(--bg));
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 5px 4px;
  text-align: center;
  /* v1151: mono → sans for the same dotted-zero reason as .ddb-stat. */
  font-family: var(--type-ui);
  font-variant-numeric: tabular-nums;
  line-height: 1.05;
  transition: background var(--t-hover), border-color var(--t-hover);
}
[data-theme="dark"] .ddb-ab-cell { background: color-mix(in srgb, var(--text-1) 12%, var(--bg)); }
.ddb-ab-label {
  display: block;
  font-family: var(--type-ui);
  font-size: var(--font-size-2xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
  line-height: 1;
}
.ddb-ab-mod {
  display: block;
  font-size: var(--font-size-stat);
  font-weight: 700;
  color: var(--text-1);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.ddb-ab-score {
  display: block;
  font-size: var(--font-size-2xs);
  color: var(--text-3);
  line-height: 1;
}

/* Best or tied for best — dark green. Token-driven palette mirrors the
   .pleg-swatch--best / --near legend swatches so the highlights and the
   legend stay locked together. */
.ddb-ab-best, .ddb-ab-tied-best {
  background: color-mix(in srgb, var(--accent-green) 18%, transparent);
  border-color: var(--accent-green);
}
.ddb-ab-best .ddb-ab-mod,      .ddb-ab-tied-best .ddb-ab-mod  { color: var(--accent-green); }
.ddb-ab-best .ddb-ab-label,    .ddb-ab-tied-best .ddb-ab-label { color: color-mix(in srgb, var(--accent-green) 80%, white); }

/* Within 1 of best — yellow-green. */
.ddb-ab-near-best {
  background: color-mix(in srgb, var(--accent-green) 30%, var(--accent-amber) 12%);
  border-color: color-mix(in srgb, var(--accent-green) 60%, var(--accent-amber));
}
.ddb-ab-near-best .ddb-ab-mod  { color: color-mix(in srgb, var(--accent-green) 50%, var(--accent-amber)); }
.ddb-ab-near-best .ddb-ab-label { color: color-mix(in srgb, var(--accent-green) 60%, var(--accent-amber)); }

.ddb-row {
  color: var(--text-2);
  font-size: var(--font-size-sm);
  margin-top: 0.18rem;
  line-height: 1.4;
}
.ddb-row-label {
  font-weight: 600;
  color: var(--text);
  margin-right: 0.35rem;
}
.ddb-strength-label { color: var(--accent-green); }
.ddb-gap-label      { color: var(--accent-red); }

/* ── Party Assessment Box ────────────────────────────────────────────────── */
.party-box {
  background: var(--bg-2);
  border: 1px solid var(--border);
  /* radius bumped from --lg to --xl in v748 to match every other card
     type in the app (.campaign-card / .session-card / .entity-row /
     .party-pc-card / .quotes-pc-section). */
  border-radius: var(--radius-xl);
  padding: 1rem 1.1rem;
  /* v932: dropped the var(--space-4) margin-top that previously
     simulated grid-gap. The party-section-label above it already
     supplies var(--space-2) margin-bottom; stacking another 16px
     on top gave Party Assessment a 24px gap below its eyebrow vs
     the ~8px every other tab uses (Quests / Codex / Sessions).
     Total now matches the rest of the app. */
  margin-bottom: 1rem;
  font-size: var(--font-size-base);
}
/* Empty-state variant — solid border (matches the rest of the app's
   "empty card" treatment); subtler text. The previous dashed-border was
   the only place in the app using a dashed-card empty state. */
.party-box-empty {
  color: var(--text-2);
}
.party-box-loading {
  display: flex;
  align-items: center;
  gap: 1rem;
}
.party-box-hdr {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.5rem;
}
.party-box-title {
  font-weight: 700;
  font-size: var(--font-size-md);
  color: var(--text);
}
.party-box-spinner {
  color: var(--text-3);
  font-style: italic;
  font-size: var(--font-size-base);
  animation: pulse 1.4s ease-in-out infinite;
}

.pa-summary {
  color: var(--text-2);
  margin-bottom: 0.75rem;
  line-height: 1.5;
  font-size: var(--font-size-base);
}
/* v1120: party-assessment classes aligned to spec .tf-pa-* recipe values
   in-place. Display-serif italic blockquote, teal-tinted bg + left border. */
.pa-tldr {
  font-family: var(--type-display);
  font-style: italic;
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--text-2);
  border-left: 3px solid var(--accent-teal);
  padding: 6px 14px;
  margin: 0 0 12px 0;
  background: color-mix(in srgb, var(--accent-teal) 5%, transparent);
}

/* Three-stage visibility: 1=TLDR · 2=Summary · 3=Full */
.party-box[data-stage="1"] .pa-summary,
.party-box[data-stage="1"] .pa-grid,
.party-box[data-stage="1"] .pa-tip { display: none; }

.party-box[data-stage="2"] .pa-tldr,
.party-box[data-stage="2"] .pa-detail { display: none; }

.party-box[data-stage="3"] .pa-tldr,
.party-box[data-stage="3"] .pa-chips { display: none; }
.pa-date {
  font-size: var(--font-size-xs);
  color: var(--text-3);
}
.pa-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.6rem;
  margin-bottom: 0.7rem;
}
@media (max-width: 700px) { .pa-grid { grid-template-columns: 1fr; } }

/* v1120: section card aligned to spec .tf-pa-section — subtle ink wash bg
   + 1px border + 3px left rail in per-section color. Spec uses teal for
   synergies (was accent-blue). */
.pa-section {
  background: color-mix(in srgb, var(--text-1) 3%, var(--bg));
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 9px 12px 10px;
  border-left: 3px solid transparent;
}
[data-theme="dark"] .pa-section { background: color-mix(in srgb, var(--text-1) 10%, var(--bg)); }
.pa-strengths  { border-color: var(--accent-green); }
.pa-gaps       { border-color: var(--accent-red); }
.pa-synergies  { border-color: var(--accent-teal); }
.pa-watchouts  { border-color: var(--accent-amber); }

.pa-section-label {
  font-family: var(--type-ui);
  font-weight: 700;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.10em;
  margin-bottom: 6px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.pa-strengths  .pa-section-label { color: var(--accent-green); }
.pa-gaps       .pa-section-label { color: var(--accent-red); }
.pa-synergies  .pa-section-label { color: var(--accent-teal); }
.pa-watchouts  .pa-section-label { color: var(--accent-amber); }

.pa-count {
  font-family: var(--type-mono);
  font-size: 9.5px;
  font-weight: 500;
  color: var(--text-3);
  background: color-mix(in srgb, var(--text-1) 6%, transparent);
  padding: 1px 5px;
  border-radius: 999px;
  letter-spacing: 0;
  margin-left: 4px;
}

/* Collapsed chip row */
.pa-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 0.25rem;
}
/* v1120: chip aligned to spec .tf-pa-chip — pill with per-section
   color (8% bg + 26% border + section text). */
.pa-chip {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 999px;
  font-family: var(--type-ui);
  font-size: 11px;
  border: 1px solid transparent;
  white-space: nowrap;
  color: var(--text-2);
  background: var(--bg);
}
.pa-chip-strengths { color: var(--accent-green); background: color-mix(in srgb, var(--accent-green) 8%, transparent); border-color: color-mix(in srgb, var(--accent-green) 26%, transparent); }
.pa-chip-gaps      { color: var(--accent-red);   background: color-mix(in srgb, var(--accent-red)   8%, transparent); border-color: color-mix(in srgb, var(--accent-red)   26%, transparent); }
.pa-chip-synergies { color: var(--accent-teal);  background: color-mix(in srgb, var(--accent-teal)  8%, transparent); border-color: color-mix(in srgb, var(--accent-teal)  26%, transparent); }
.pa-chip-watchouts { color: var(--accent-amber); background: color-mix(in srgb, var(--accent-amber) 8%, transparent); border-color: color-mix(in srgb, var(--accent-amber) 26%, transparent); }
[data-theme="dark"] .pa-chip-strengths { background: color-mix(in srgb, var(--accent-green) 22%, transparent); color: #B5DAA1; border-color: color-mix(in srgb, var(--accent-green) 42%, transparent); }
[data-theme="dark"] .pa-chip-gaps      { background: color-mix(in srgb, var(--accent-red)   22%, transparent); color: #F5C0AE; border-color: color-mix(in srgb, var(--accent-red)   42%, transparent); }
[data-theme="dark"] .pa-chip-synergies { background: color-mix(in srgb, var(--accent-teal)  22%, transparent); color: #BFE7EA; border-color: color-mix(in srgb, var(--accent-teal)  42%, transparent); }
[data-theme="dark"] .pa-chip-watchouts { background: color-mix(in srgb, var(--accent-amber) 22%, transparent); color: #F1B97A; border-color: color-mix(in srgb, var(--accent-amber) 42%, transparent); }

/* Full detail list */
.pa-list {
  margin: 0.3rem 0 0 0;
  padding-left: 1.1rem;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  color: var(--text-2);
  line-height: 1.45;
}

/* Stage-nav row (Less · Label · More) */
.pa-expand-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  margin-top: 0.6rem;
}
.pa-stage-label {
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  min-width: 80px;
  text-align: center;
}
.pa-stage-btn[disabled] {
  opacity: 0.35;
  cursor: not-allowed;
}

.pa-tip {
  background: linear-gradient(90deg, #ca8a0415, transparent);
  border-left: 3px solid var(--accent-gold);
  border-radius: var(--radius-sm);
  padding: 0.45rem 0.7rem;
  color: var(--text);
  line-height: 1.5;
}

/* ── Live Session Layout ────────────────────────────────────────────────────── */
.screen { position: fixed; inset: 0; z-index: 200; display: flex; flex-direction: column; }
.screen.hidden { display: none; }

/* v1134: aligned to spec .tf-live-hdr — subtle ember wash over bg-2 with
   an ember-tinted border-bottom rule. The ember tint signals "the table is
   watching" (live recording surface) without being so loud it competes
   with the recording timer pill. 14/20 padding per spec; legacy
   0.4/0.75rem was way too tight for the new visual weight. */
.live-header {
  display: flex; align-items: center; gap: 0.75rem;
  padding: 14px 20px;
  background: color-mix(in srgb, var(--ember-soft) 5%, var(--bg-2));
  border-bottom: 1px solid color-mix(in srgb, var(--ember-soft) 22%, var(--border));
  flex-shrink: 0;
}
[data-theme="dark"] .live-header {
  background: color-mix(in srgb, var(--ember-soft) 10%, var(--bg-2));
  border-bottom-color: color-mix(in srgb, var(--ember-soft) 30%, var(--border));
}
/* (v795) .live-header-back and .live-title retired — both were CSS-only
   orphans (no markup references). The Live header uses .nav-back-btn
   instead, and the live-session-controls dropdown replaced any title
   element this class targeted. */

/* Session switcher in live header */
.live-session-controls {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex: 1;
  min-width: 0;
}
.live-session-select {
  flex: 1;
  min-width: 0;
  max-width: 360px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text);
  font-family: inherit;
  font-size: var(--font-size-md);
  font-weight: 600;
  padding: 0.3rem 0.6rem;
  cursor: pointer;
  transition: border-color var(--t-hover) var(--ease);
}
.live-session-select:focus {
  outline: none;
  border-color: var(--accent-teal);
  box-shadow: 0 0 0 3px rgba(47,124,133,0.12);
}
.live-new-session-btn {
  white-space: nowrap;
  font-size: var(--font-size-base);
  padding: 0.3rem 0.75rem;
}


.live-body {
  display: flex; flex: 1; overflow: hidden;
}

/* Left column: map on top, quests below */
.live-left {
  flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0;
}

.live-map-panel {
  display: flex; flex-direction: column;
  flex: 1; min-height: 0; background: var(--bg);
  border-bottom: 1px solid var(--border);
}
.live-map-panel.collapsed { flex: none; }
.live-map-panel-hdr {
  display: flex; align-items: center; gap: 0.4rem;
  padding: 0.3rem 0.6rem;
  border-bottom: 1px solid var(--border);
  background: var(--bg); flex-shrink: 0;
}
/* v795: typography moved to .form-section-label composed in markup;
   this rule keeps only the layout (flex grow). */
.live-map-panel-title { flex: 1; }
.live-map-panel.collapsed .live-map-collapse-btn { transform: rotate(-90deg); }
.live-map-body {
  flex: 1; position: relative; min-height: 0;
}
.live-map-panel.collapsed .live-map-body { display: none; }
#live-map-container { width: 100%; height: 100%; }
#live-map-hint {
  position: absolute; top: 0.75rem; left: 50%; transform: translateX(-50%);
  background: var(--bg-2); color: var(--text);
  border: 1px solid var(--accent-teal);
  font-size: var(--font-size-sm); font-weight: 500;
  padding: 0.3rem 0.9rem; border-radius: var(--radius-full);
  pointer-events: none; z-index: 500; white-space: nowrap;
  box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
/* Layout-only — typography flows through .placeholder + .placeholder--padded
   composed in markup. v780 retired the bespoke font-size/colour/italic recipe. */
.live-map-empty {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; min-height: 80px;
}

/* ── Atlas map switcher ───────────────────────────────────────────────────── */
.atlas-map-switcher {
  display: flex; align-items: center; gap: var(--space-1); flex-shrink: 0;
}
.atlas-map-nav-btn {
  font-size: var(--font-size-xs); padding: 2px 7px; color: var(--text-2);
}
.atlas-map-nav-btn:hover, .atlas-map-nav-btn:focus-visible { color: var(--accent-teal); }
.atlas-map-title {
  font-size: var(--font-size-sm); font-weight: 600;
  color: var(--text); white-space: nowrap;
  max-width: 160px; overflow: hidden; text-overflow: ellipsis;
  padding: 0 var(--space-1);
}

/* Live map nav buttons in live-map-panel-hdr */
.live-map-nav-btn {
  font-size: var(--font-size-xs); padding: 1px 5px; flex-shrink: 0;
  color: var(--text-2);
}
.live-map-nav-btn:hover, .live-map-nav-btn:focus-visible { color: var(--accent-teal); }
#live-map-title-label {
  flex: 1; text-align: center;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Live map manager button + dropdown */
.live-map-manage-btn {
  font-size: var(--font-size-xs); padding: 1px 5px; flex-shrink: 0;
  color: var(--text-3);
}
.live-map-manage-btn:hover, .live-map-manage-btn:focus-visible { color: var(--accent-teal); }
.live-map-manager-dropdown {
  position: absolute; top: 100%; right: 0; z-index: 200;
  background: var(--bg-2); border: 1px solid var(--border);
  border-radius: var(--radius-lg); box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  padding: 0.35rem 0; min-width: 180px; max-width: 240px;
}
.live-map-mgr-item {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.35rem 0.75rem; cursor: pointer;
  font-size: var(--font-size-sm); color: var(--text);
}
.live-map-mgr-item:hover, .live-map-mgr-item:focus-visible { background: var(--bg-3); }
.live-map-mgr-item input[type="checkbox"] { flex-shrink: 0; accent-color: var(--accent-teal); }
.live-map-mgr-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* (v780) .live-map-mgr-empty retired — replaced by .placeholder.placeholder--padded
   composed in markup in renderLiveMapManagerDropdown. */

/* ── Map management modal ─────────────────────────────────────────────────── */
/* Sizing now flows through .modal-box--lg (width/max-width). This class
   only carries the column layout + max-height the manager needs to keep
   the long map list scrollable. v773 retired the literal width: 560px. */
.map-manage-box {
  max-height: 80vh;
  display: flex; flex-direction: column;
}
.map-manage-body {
  flex: 1; overflow-y: auto; padding: var(--space-4) var(--space-5);
  display: flex; flex-direction: column; gap: var(--space-4);
}
.map-manage-upload-section {
  background: var(--bg-2); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: var(--space-4);
}
.map-manage-upload-row {
  display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap;
}
.map-manage-title-input {
  flex: 1; min-width: 160px;
  padding: 0.45rem 0.65rem; border-radius: var(--radius-md);
  border: 1px solid var(--border); background: var(--bg);
  color: var(--text); font-size: var(--font-size-base);
  font-family: inherit;
}
.map-manage-title-input:focus { outline: none; border-color: var(--accent-teal); }
.map-manage-upload-btn { cursor: pointer; font-size: var(--font-size-sm); }
.map-manage-status {
  margin-top: var(--space-2); font-size: var(--font-size-sm);
  color: var(--text-2);
}
.map-manage-list {
  display: flex; flex-direction: column; gap: var(--space-3);
}
/* Map row card — composes the canonical card hover (matches .entity-row,
   .session-card, .party-pc-card, .quest-nav-row) so all repeated-row
   surfaces lift identically on hover. */
.map-manage-item {
  background: var(--bg-2); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: var(--space-3) var(--space-4);
  display: flex; gap: var(--space-3); align-items: flex-start;
  transition: border-color var(--t-hover) ease,
              box-shadow var(--t-hover) ease,
              transform var(--t-hover) ease;
}
.map-manage-item:hover, .map-manage-item:focus-visible {
  border-color: rgba(47,154,163,0.45);
  box-shadow: 0 4px 12px rgba(0,0,0,0.12);
  transform: translateY(-1px);
}
.map-manage-item-thumb {
  width: 64px; height: 48px; flex-shrink: 0;
  border-radius: var(--radius-sm); object-fit: cover;
  border: 1px solid var(--border);
}
.map-manage-item-info {
  flex: 1; min-width: 0;
}
.map-manage-item-title-row {
  display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2);
}
.map-manage-item-title-inp {
  flex: 1; font-size: var(--font-size-sm); font-weight: 600;
  padding: 0.2rem 0.45rem; border-radius: var(--radius-sm);
  border: 1px solid var(--border); background: var(--bg);
  color: var(--text); font-family: inherit;
}
.map-manage-item-title-inp:focus { outline: none; border-color: var(--accent-teal); }
.map-manage-vis-row {
  display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap;
}
.map-manage-vis-label {
  display: flex; align-items: center; gap: var(--space-1);
  font-size: var(--font-size-xs); font-weight: 600; color: var(--text-2);
  cursor: pointer; user-select: none;
}
.map-manage-vis-label input { accent-color: var(--accent-teal); }
.map-manage-item-actions {
  display: flex; flex-direction: column; gap: var(--space-2); flex-shrink: 0;
}
/* Inline "Save" button in the title-edit row — composes .btn-ghost; this
   class only resizes the button to fit alongside the title input. Replaces
   inline style="font-size:0.72rem;padding:2px 8px". */
.map-manage-save-btn {
  font-size: var(--font-size-xs);
  padding: 2px 8px;
}
/* Delete button — composes .btn-ghost.btn-ghost--danger; this class only
   resizes for the actions column. v773 retired the bespoke colour recipe. */
.map-manage-delete-btn {
  font-size: var(--font-size-xs);
  padding: 2px 8px;
}

.live-sidebar {
  width: 340px; flex-shrink: 0;
  display: flex; flex-direction: column;
  border-left: 1px solid var(--border);
  background: var(--bg);
  overflow: hidden;
}
/* v911 GM Notebook commit 3: chat textarea + log relocated INTO the
   sidebar. Without explicit flex shares, transcript-panel (flex:1)
   and live-log (flex:1) would each demand all remaining space and
   the layout would collapse one or the other to nothing. Engineering
   audit specifically flagged this as the chat-relocation gotcha.

   Transcript gets ~60% of remaining space (it's the focus during
   live), log gets ~30% (enough to scroll the last few entries).
   Both have min-height: 0 so they actually shrink within the
   sidebar's overflow:hidden context. Entry form is intrinsic-sized
   via its existing flex-shrink: 0. */
.live-sidebar > .live-transcript-panel { flex: 3 1 0; min-height: 0; }
.live-sidebar > .live-log { flex: 1 1 0; min-height: 0; }

/* Focus mode (#live-transcript-focus-btn): hide the PC sidebar, map/quests, and
   notebook so the record/transcript column fills the screen for heads-down live
   reading. Opt-in toggle — the default 4-column layout is untouched. */
.live-body.live-focus #live-pc-sidebar,
.live-body.live-focus #live-pc-sidebar-expander,
.live-body.live-focus .live-left,
.live-body.live-focus #live-left-rail,
.live-body.live-focus #live-ai-drawer,
.live-body.live-focus #live-ai-expander { display: none !important; }
.live-body.live-focus .live-sidebar { flex: 1 1 auto; width: auto; }
.live-collapse-btn--active { color: var(--color-brand); }

/* Both map AND quest panels collapsed (_syncLiveLeftMinimized): the .live-left
   column has nothing but two stranded thin headers. Fold the whole column into
   a single 28px vertical rail (mirrors the PC + Notebook expander rails) and
   let the transcript sidebar reclaim the freed width. Clicking the rail
   re-expands both panels. JS toggles .hidden on the rail; this block hides the
   now-empty .live-left so the two cannot both occupy the slot. */
.live-body.live-left-min .live-left { display: none; }
.live-body.live-left-min .live-sidebar { flex: 1 1 auto; width: auto; }
/* Left-column collapsed rail — same recipe as .live-pc-sidebar-expander so the
   three vertical rails (PC, Map/Quests, Notebook) read as one affordance family. */
.live-left-rail {
  width: 28px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border-right: 1px solid var(--border);
  background: var(--bg-2);
  cursor: pointer; user-select: none;
  transition: background var(--t-hover), color var(--t-hover);
}
.live-left-rail:hover, .live-left-rail:focus-visible { background: var(--bg-3); color: var(--accent-teal); outline: none; }
.live-left-rail span {
  writing-mode: vertical-rl; transform: rotate(180deg);
  font-size: var(--font-size-xs); font-weight: 600;
  color: var(--text-2); letter-spacing: 0.04em;
  white-space: nowrap; overflow: hidden; max-height: 320px; text-overflow: ellipsis;
}
.live-left-rail:hover span, .live-left-rail:focus-visible span { color: var(--accent-teal); }

.live-entry-form {
  padding: 0.45rem 0.6rem; border-bottom: 1px solid var(--border); flex-shrink: 0;
}
/* Textarea wrapper with GIF icon overlay */
.live-text-wrap {
  position: relative; margin-bottom: 0.4rem;
}
.live-text-wrap #live-entry-text {
  margin-bottom: 0; padding-right: 2rem;
}
.live-gif-icon {
  position: absolute; top: 6px; right: 6px;
  background: none; border: none; cursor: pointer;
  font-size: var(--font-size-lg); line-height: 1; opacity: 0.55;
  transition: opacity var(--t-hover);
  padding: 0; z-index: 1;
}
.live-gif-icon:hover, .live-gif-icon:focus-visible { opacity: 1; }

/* Key event checkbox row */

#live-entry-text {
  width: 100%; min-height: 48px; resize: vertical;
  padding: 0.5rem; border-radius: var(--radius-md);
  border: 1px solid var(--border); background: var(--bg-2); color: var(--text);
  font-size: var(--font-size-base); font-family: inherit; margin-bottom: 0.3rem;
}
#live-entry-text:focus { outline: none; border-color: var(--accent-teal); }

#live-set-pos-btn {
  padding: 0.4rem 0.7rem; background: none;
  border: 1.5px solid var(--border); border-radius: 0;
  font-size: var(--font-size-base); cursor: pointer; color: var(--text-2);
  transition: border-color var(--t-hover), background var(--t-hover);
}
#live-set-pos-btn.active { border-color: var(--accent-teal); background: var(--accent-teal); color: #fff; }
#live-set-pos-btn:hover:not(.active), #live-set-pos-btn:focus-visible:not(.active) { background: var(--bg-3); }

/* ── Session Preamble panel ──────────────────────────────────────────────── */
.live-preamble {
  flex-shrink: 0;
  margin: 0 0.5rem 0.5rem;
  border: 1px solid var(--border);
  border-top: 2px solid var(--split-color);
  border-radius: var(--radius-lg);
  background-color: color-mix(in srgb, var(--color-gold) 5%, var(--bg-2));
  overflow: hidden;
  max-height: 280px;
  display: flex;
  flex-direction: column;
}
.live-preamble-hdr {
  display: flex; align-items: center; gap: 0.4rem;
  padding: 0.28rem 0.6rem;
  background: color-mix(in srgb, var(--color-gold) 10%, var(--bg-3));
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.live-preamble-title {
  /* Composes .form-section-label for typography (uppercase, weight 700,
     letter-spacing) — only override the color so the recap eyebrow
     reads gold instead of the canonical tertiary-text. v920: dropped
     the ::before ◆ glyph because the 📍 emoji in the text now serves
     as the marker. */
  color: var(--color-gold);
  flex: 1;
}
.live-preamble-session-label {
  font-size: 0.66rem; color: var(--text-3); font-style: italic; white-space: nowrap;
}
.live-preamble-body {
  overflow-y: auto; padding: 0.45rem 0.6rem;
  flex: 1; display: flex; flex-direction: column; gap: 0.5rem;
}
.live-preamble-section-hdr {
  font-size: 0.66rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--text-3); margin-bottom: 0.28rem;
}
/* "What Comes Next" section header — ember tint to signal forward pressure */
.live-preamble-section-hdr:last-of-type,
.live-preamble-next .live-preamble-section-hdr {
  color: var(--ember-text);
  border-left: 2px solid var(--color-ember);
  padding-left: var(--space-2);
}
.live-preamble-bullets {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 0.22rem;
}
.live-preamble-bullet {
  font-size: var(--font-size-sm); color: var(--text-2); line-height: 1.45;
  padding-left: 0.85rem; position: relative;
}
.live-preamble-bullet::before {
  content: '◆'; position: absolute; left: 0;
  color: var(--split-color); font-size: 0.5rem; top: 0.25em;
}
.live-preamble-next {
  display: flex; flex-direction: column; gap: 0.2rem;
}
.live-preamble-next-line {
  font-size: var(--font-size-sm); color: color-mix(in srgb, var(--color-ember) 55%, var(--color-text-secondary)); line-height: 1.45; margin: 0;
}
.live-preamble-next-coda {
  color: var(--text-3); font-style: italic;
}

/* Live transcript preview panel — flex:1 so it splits 50/50 with the log */
.live-transcript-panel {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-1);
  margin: 0 0.5rem 0.5rem;
  overflow: hidden;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.live-transcript-box {
  flex: 1; overflow-y: auto;
  padding: 0.4rem 0.6rem;
  min-height: 60px;
  font-size: var(--font-size-sm); line-height: 1.55;
  color: var(--text-2);
  word-break: break-word;
}
.live-transcript-panel.hidden { display: none; }
/* ── Unified collapse/expand arrow button — used on all Live section headers ── */
.live-collapse-btn {
  background: none; border: none; cursor: pointer;
  color: var(--text-3); font-size: var(--font-size-sm); line-height: 1;
  padding: 2px 5px; flex-shrink: 0;
  transition: transform var(--t-hover) var(--ease), color var(--t-hover) var(--ease);
  font-family: inherit;
}
.live-collapse-btn:hover, .live-collapse-btn:focus-visible { color: var(--text); }
/* ✕ dismiss buttons in live panels (preamble close, etc.) keep their own style */
.live-panel-toggle {
  background: none; border: none; cursor: pointer;
  color: var(--text-3); font-size: var(--font-size-md); line-height: 1;
  padding: 0 0.15rem; flex-shrink: 0;
  transition: color var(--t-hover);
}
.live-panel-toggle:hover, .live-panel-toggle:focus-visible { color: var(--text-1); }
/* Transcript body wrapper — fills remaining height, enables scroll chain */
.live-transcript-body { display: flex; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; }
.live-transcript-panel.transcript-collapsed .live-transcript-body { display: none; }
/* Shrink panel to header-only when collapsed — frees space for entry form + log */
.live-transcript-panel.transcript-collapsed { flex: none; }

/* Transcript / Speakers tab bar */
.live-trans-tabs { display: flex; gap: 2px; flex: 1; align-items: center; }
.live-trans-tab {
  background: none; border: none; cursor: pointer;
  font-size: var(--font-size-xs); font-weight: 600; padding: 0.18rem 0.55rem;
  border-radius: 0; color: var(--text-3);
  transition: color var(--t-hover), background var(--t-hover);
}
.live-trans-tab:hover, .live-trans-tab:focus-visible { background: var(--bg-3); color: var(--text-1); }
.live-trans-tab.active {
  background: color-mix(in srgb, var(--accent-teal) 12%, transparent);
  color: var(--accent-teal);
}
.live-transcript-hdr {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0.3rem 0.6rem;
  background: var(--bg-2);
  border-bottom: 1px solid var(--border);
  font-size: var(--font-size-xs);
}
.live-transcript-label {
  display: flex; align-items: center; gap: 0.35rem;
  font-weight: 600; color: var(--accent-red);
}
.live-transcript-label .rec-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent-red);
  animation: rec-blink 0.9s ease-in-out infinite;
  flex-shrink: 0;
}
.live-transcript-wc { color: var(--text-3); font-size: var(--font-size-xs); }
/* .live-transcript-box defined above with max-height */
.transcript-interim { color: var(--text-3); font-style: italic; }
/* Live speaker indicator badge in transcript header */
.live-spk-indicator {
  font-size: var(--font-size-xs); font-weight: 700;
  padding: 0.1rem 0.45rem; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--accent-teal) 20%, transparent);
  color: var(--accent-teal);
  animation: spk-pulse 1.2s ease-in-out infinite;
}
@keyframes spk-pulse { 0%,100%{opacity:1} 50%{opacity:0.55} }


/* Speaker labeling panel — now a full tab, not an embedded strip */
.live-spk-bar-wrap {
  flex: 1; min-height: 0; display: flex; flex-direction: column;
  overflow: hidden; background: var(--bg);
  padding: 0.5rem 0.6rem;
}
/* Body rows — fills available height, scrollable */
.live-spk-body {
  flex: 1; overflow-y: auto;
  scroll-behavior: smooth;
  padding-bottom: 0.3rem;
  display: flex; flex-direction: column; gap: 0.4rem;
}
/* (v780) .live-spk-empty retired — replaced by .placeholder.placeholder--padded
   composed in markup in _refreshLiveSpkBar. */
.live-spk-bar-wrap .spk-bar { border: none; padding: 0; background: none; }

/* Grouped speaker layout */
.live-spk-grouped {
  display: flex; flex-direction: column; gap: 0.25rem;
}
/* v795: typography moved to .form-section-label composed in markup. */
.live-spk-grouped .spk-bar-hdr { margin-bottom: var(--space-1); }
.live-spk-group {
  display: flex; align-items: center; gap: 0.45rem;
}
.live-spk-group-name {
  font-size: var(--font-size-sm); font-weight: 600; color: var(--text);
  min-width: 4rem;
}
.live-spk-group-name--unlabeled {
  color: var(--text-3); font-style: italic; font-weight: 400;
}
.live-spk-chip-row { display: flex; flex-wrap: wrap; gap: 3px; }
.live-spk-chip {
  font-size: var(--font-size-xs); font-weight: 700;
  padding: 0.1rem 0.35rem; border-radius: var(--radius-sm); cursor: pointer;
  background: color-mix(in srgb, var(--accent-teal) 18%, transparent);
  color: var(--accent-teal);
  border: 1px solid color-mix(in srgb, var(--accent-teal) 35%, transparent);
  transition: background var(--t-hover);
}
.live-spk-chip:hover, .live-spk-chip:focus-visible { background: color-mix(in srgb, var(--accent-teal) 32%, transparent); }
.live-spk-chip--unlabeled {
  background: color-mix(in srgb, var(--text-3) 12%, transparent);
  color: var(--text-3);
  border-color: color-mix(in srgb, var(--text-3) 25%, transparent);
}
.live-spk-quote-row {
  display: flex; align-items: center; gap: 0.4rem;
  padding: 0.15rem 0;
}
.live-spk-quote-text {
  flex: 1; min-width: 0;
  font-size: var(--font-size-sm); color: var(--text-2);
  font-style: italic;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.4;
}
.live-spk-quote-more {
  display: inline-block;
  margin-left: 0.3rem;
  font-size: 0.6rem; font-weight: 600; font-style: normal;
  color: var(--color-purple);
  background: color-mix(in srgb, var(--color-purple) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-purple) 28%, transparent);
  border-radius: var(--radius-full);
  padding: 0.05rem 0.3rem;
  vertical-align: middle;
}
.live-spk-dismiss-btn {
  flex-shrink: 0;
  background: none; border: none; padding: 0 0.2rem;
  font-size: 0.65rem; color: var(--text-3); cursor: pointer;
  line-height: 1; opacity: 0.6; transition: opacity var(--t-hover), color var(--t-hover);
}
.live-spk-dismiss-btn:hover, .live-spk-dismiss-btn:focus-visible { opacity: 1; color: var(--accent-red); }
.live-spk-inline-select {
  font-size: var(--font-size-sm); padding: 0.1rem 0.2rem;
  border-radius: var(--radius-sm); border: 1px solid var(--accent-teal);
  background: var(--bg-2); color: var(--text);
  max-width: 120px;
}

.transcript-turn {
  display: flex; gap: 0.5rem; align-items: flex-start;
  padding: 0.25rem 0; border-bottom: 1px solid var(--border);
}
.transcript-turn--active { opacity: 0.85; }
.transcript-speaker {
  flex-shrink: 0;
  min-width: 1.9rem;                  /* fits 2-char labels like "Be", "GM" */
  max-width: 3.75rem;                 /* fits 4-char extended labels like "Grub" */
  text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-size: var(--font-size-xs); font-weight: 700; letter-spacing: 0.02em;
  color: var(--accent-teal); background: color-mix(in srgb, var(--accent-teal) 15%, transparent);
  border-radius: var(--radius-sm); padding: 0.1rem 0.35rem; line-height: 1.4;
  cursor: default;
}
/* When the chip is a button (committed segments), make it interactive. */
button.transcript-speaker--clickable {
  border: 1px solid transparent;
  cursor: pointer;
  font-family: inherit;
  transition: background var(--t-hover) ease, border-color var(--t-hover) ease;
}
button.transcript-speaker--clickable:hover,
button.transcript-speaker--clickable:focus-visible {
  background: color-mix(in srgb, var(--accent-teal) 28%, transparent);
  border-color: rgba(47,154,163,0.45);
  outline: none;
}
/* Manually-overridden chip — subtle ember tint so the GM can see at a glance
   which segments they corrected vs. which are still on AAI's original cluster. */
.transcript-speaker--override {
  color: var(--color-ember, #d4753a);
  background: color-mix(in srgb, var(--color-ember, #d4753a) 18%, transparent);
}
button.transcript-speaker--override:hover,
button.transcript-speaker--override:focus-visible {
  background: color-mix(in srgb, var(--color-ember, #d4753a) 30%, transparent);
}
/* Speaker column — fixed-width left rail so the transcript text always starts at
   the same x whether a turn has a thumbnail + pill or just a short pill (avoids a
   ragged left edge). Holds an optional Codex-style square thumbnail + a name
   pill, right-aligned against the text. */
.transcript-speaker--col {
  flex: 0 0 auto;          /* size to content — no fixed-width dead air */
  /* RESET the legacy .transcript-speaker min/max-width column clamp (1.9rem /
     3.75rem). The element carries BOTH classes; --col overrides bg/padding/
     radius/overflow but NOT width, so the 60px max-width leaked through and
     squeezed the pill to "G…" no matter what max-width the pill itself had.
     v1264 RCCA — this is why the v1263 pill-width bump did nothing. */
  min-width: 0; max-width: none;
  align-self: flex-start;
  display: flex; align-items: center; justify-content: flex-start; gap: 4px;
  padding: 0; background: none; border-radius: 0; overflow: visible;
}
button.transcript-speaker--col { border: none; cursor: pointer; }
button.transcript-speaker--col:hover .tspk-pill,
button.transcript-speaker--col:focus-visible .tspk-pill {
  box-shadow: 0 0 0 2px rgba(47,154,163,0.45); outline: none;
}
/* Square rounded thumbnail (Codex .tf-card-thumb idiom) — only when a mapped PC
   has a portrait. */
.tspk-thumb {
  width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0;
  overflow: hidden; border: 1px solid var(--border);
}
.tspk-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Name pill — first name (mapped PC), cluster letter (unmapped), or "GM".
   Transparent with a colored outline + text for players (stable per-speaker
   --spk-color); neutral border for unmapped; solid gold for the GM. */
.tspk-pill {
  max-width: 11ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding: 2px 7px; border-radius: var(--radius-sm);
  font-size: var(--font-size-xs); font-weight: 700; letter-spacing: 0.04em; line-height: 1.4;
  background: transparent; border: 1px solid;
  border-color: color-mix(in srgb, var(--spk-color, var(--border)) 50%, transparent);
  color: var(--spk-color, var(--text-2));
}
.tspk-pill--with-thumb { max-width: 10ch; }  /* fits "Gibs"/"Tannin"; longer first names ellipsize, full name in title= */
.tspk-pill--neutral { border-color: var(--border); color: var(--text-3); }
.tspk-pill--gm { background: var(--accent-gold); border-color: var(--accent-gold); color: #20170a; max-width: none; }
/* Manual-reassignment indicator — subtle ember ring on the pill. */
.transcript-speaker--override .tspk-pill {
  box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--color-ember, #d4753a) 60%, transparent);
}
/* Reassign popover anchored beside a clicked speaker chip. */
.spk-reassign-pop {
  position: absolute;
  z-index: 10000;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: 0 8px 24px rgba(0,0,0,0.25);
  padding: 0.6rem 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  min-width: 16rem;
  font-size: var(--font-size-sm);
}
.spk-reassign-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.spk-reassign-row label {
  color: var(--text-2);
  font-weight: var(--font-weight-semibold);
}
.spk-reassign-select {
  flex: 1;
  background: var(--bg-1);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.25rem 0.4rem;
  font: inherit;
}
.spk-reassign-checkbox {
  font-size: var(--font-size-xs);
  color: var(--text-2);
  font-weight: var(--font-weight-regular);
  align-items: flex-start;
  gap: 0.4rem;
}
.spk-reassign-checkbox input { margin-top: 2px; }
.spk-reassign-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 0.25rem;
}
.spk-reassign-actions .btn-new { padding: 0.25rem 0.75rem; font-size: var(--font-size-sm); }
.transcript-text { flex: 1; font-size: var(--font-size-base); line-height: 1.55; color: var(--text-1); }
.transcript-empty { color: var(--text-3); font-style: italic; font-size: var(--font-size-base); }
.transcript-divider {
  text-align: center; font-size: var(--font-size-xs); color: var(--text-3);
  padding: 0.4rem 0; letter-spacing: 0.05em;
  border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
  margin: 0.25rem 0;
}

.live-log {
  flex: 1; overflow-y: auto; padding: 0.5rem 0.6rem;
}
@keyframes live-log-ping {
  0%   { box-shadow: inset 3px 0 0 0 var(--accent-teal), inset 0 0 18px 0 color-mix(in srgb, var(--color-brand) 22%, transparent); }
  60%  { box-shadow: inset 3px 0 0 0 color-mix(in srgb, var(--accent-teal) 40%, transparent), inset 0 0 8px 0 color-mix(in srgb, var(--color-brand) 10%, transparent); }
  100% { box-shadow: none; }
}
.live-log--ping { animation: live-log-ping 0.7s ease-out forwards; }
/* v1134: aligned to spec .tf-log-entry recipe. Subtle ink wash bg over
   paper-2 so each entry reads as a small card, 8/10 padding, 4px radius,
   13px UI / 1.5 line-height for the chassis typography. The left-rail
   colour via --tag-color stays load-bearing for at-a-glance tag
   categorisation; hover behaviour preserved (lift + tinted border +
   drop shadow per canonical card-hover recipe). */
.live-log-entry {
  border-left: 3px solid var(--tag-color, var(--border));
  border: 1px solid transparent;
  border-left-width: 3px;
  border-radius: 4px;
  margin-bottom: 6px;
  padding: 8px 10px;
  background: color-mix(in srgb, var(--text-1) 2%, var(--bg-2));
  font-family: var(--type-ui);
  font-size: var(--font-size-base);
  color: var(--text-1);
  line-height: 1.5;
  transition: border-color var(--t-hover) ease,
              box-shadow   var(--t-hover) ease,
              transform    var(--t-hover) ease,
              background   var(--t-hover) ease;
}
[data-theme="dark"] .live-log-entry {
  background: color-mix(in srgb, var(--text-1) 5%, var(--bg-2));
}
.live-log-entry[data-tag="note"]      { border-left-color: var(--text-3); }
.live-log-entry[data-tag="key_event"] { border-left-color: var(--accent-amber); }
.live-log-entry[data-tag="travel"]    { border-left-color: var(--accent-teal); }
/* Canonical card hover — translateY + tinted border + drop shadow.
   Left-border-color (data-tag) stays untouched; only the other three
   border edges and the shadow respond. */
.live-log-entry:hover, .live-log-entry:focus-visible {
  background: var(--bg-2);
  border-top-color: rgba(47,154,163,0.45);
  border-right-color: rgba(47,154,163,0.45);
  border-bottom-color: rgba(47,154,163,0.45);
  box-shadow: 0 4px 12px rgba(0,0,0,0.12);
  transform: translateY(-1px);
}
.live-log-entry-hdr {
  display: flex; align-items: center; gap: var(--space-1); margin-bottom: 2px;
}
/* v1134: spec .tf-log-time recipe — mono 11px / ink-3 / tabular nums. */
.live-log-time {
  font-family: var(--type-mono);
  font-size: var(--font-size-xs);
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
/* v1134: spec .tf-log-tag recipe — small uppercase eyebrow per-tag tinted.
   --tag-color flows from the inline style set by renderLiveLog (matches
   the --entity-color / --pin-color / --lead-color injection pattern). */
.live-log-tag {
  font-family: var(--type-ui);
  font-size: var(--font-size-2xs);
  font-weight: 700;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  flex: 1;
  color: var(--tag-color, var(--text-3));
}
.live-log-delete {
  background: none; border: none; cursor: pointer;
  color: var(--text-3); font-size: var(--font-size-lg); line-height: 1; padding: 0 0.2rem;
  transition: color var(--t-hover);
}
.live-log-delete:hover, .live-log-delete:focus-visible { color: var(--accent-red); }
/* v1134: aligned with spec log-entry body — 13px UI / 1.5 line-height,
   ink-2 prose so the content reads at the same weight as the chassis. */
.live-log-content {
  font-family: var(--type-ui);
  font-size: var(--font-size-base);   /* matches .transcript-text so speech + beats read at one size (GM ask) */
  color: var(--text-2);
  line-height: 1.5;
  white-space: pre-wrap;
  margin: 0;
}
/* GIF picker */
.live-gif-picker {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 0.5rem;
  margin-bottom: 0.5rem;
}
.live-gif-search {
  width: 100%;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 0.35rem 0.5rem;
  font-size: var(--font-size-base);
  color: var(--text);
  outline: none;
  box-sizing: border-box;
}
.live-gif-search:focus { border-color: var(--accent-teal); }
.live-gif-results {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 4px;
  max-height: 180px;
  overflow-y: auto;
  margin-top: 0.4rem;
}
.live-gif-thumb {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: opacity var(--t-hover), transform var(--t-hover);
}
.live-gif-thumb:hover, .live-gif-thumb:focus-visible { opacity: 0.85; transform: scale(1.03); }
.live-gif-attribution {
  font-size: 0.65rem;
  color: var(--text-3);
  text-align: right;
  margin: 0.25rem 0 0;
}

.live-log-gif {
  max-width: 100%;
  max-height: 200px;
  border-radius: var(--radius-md);
  display: block;
  margin-top: 0.25rem;
}
.le-reactions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin-top: 0.35rem;
}
.le-reaction-btn {
  background: var(--surface-2, var(--bg-2));
  border: 1px solid var(--border);
  border-radius: var(--radius-2xl);
  padding: 0.1rem 0.4rem;
  font-size: var(--font-size-sm);
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.2rem;
  color: var(--text-2);
  transition: background var(--t-hover);
}
.le-reaction-btn:hover, .le-reaction-btn:focus-visible, .le-reaction-btn.mine {
  background: var(--surface-3, rgba(255,255,255,0.08));
  border-color: var(--accent-teal);
}
.le-react-add {
  background: transparent;
  border: 1px dashed var(--border);
  border-radius: var(--radius-2xl);
  padding: 0.1rem 0.35rem;
  font-size: var(--font-size-sm);
  cursor: pointer;
  color: var(--text-3);
  opacity: 0.7;
}
.le-react-add:hover, .le-react-add:focus-visible { opacity: 1; }
.le-emoji-picker {
  position: absolute;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 0.5rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  max-width: 200px;
  z-index: 100;
  box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.le-emoji-picker button {
  background: none;
  border: none;
  font-size: 1.2rem;
  cursor: pointer;
  padding: 0.2rem;
  border-radius: 0;
  transition: background var(--t-hover);
}
.le-emoji-picker button:hover, .le-emoji-picker button:focus-visible { background: var(--surface-2); }

/* (v795) .live-launch-btn / .live-mic-btn retired — CSS-only orphans (no
   markup references). Recording controls use .rec-start-btn instead. */

/* Recording status bar */
.live-rec-status {
  font-size: var(--font-size-sm); font-weight: 500;
  padding: 0.35rem 0.6rem; border-radius: var(--radius-md);
  margin-bottom: 0.4rem; display: flex; align-items: center; gap: 0.4rem;
}
.live-rec-status.hidden { display: none; }
.live-rec-listening {
  background: rgba(220,38,38,0.12); color: var(--accent-red);
  border: 1px solid rgba(220,38,38,0.25);
}
.live-rec-saved {
  background: rgba(22,163,74,0.12); color: var(--accent-green);
  border: 1px solid rgba(22,163,74,0.25);
}
.live-rec-error {
  background: var(--ember-bg); color: var(--accent-ember);
  border: 1px solid var(--ember-border);
}
.rec-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: currentColor; flex-shrink: 0;
  animation: rec-blink 0.9s ease-in-out infinite;
}
.rec-dot.capture-lost    { background: var(--accent-amber); animation: none; }
.rec-dot.recovering      { background: var(--accent-blue);  animation: rec-blink 0.5s ease-in-out infinite; }
.rec-dot.recovery-failed { background: var(--accent-red);   animation: none; }
.rec-word-count { opacity: 0.7; font-weight: 400; }

/* ── Capture health alert banner ──────────────────────────────────────────── */
.capture-health-banner {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 500;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  font-size: var(--font-size-sm);
  font-weight: 600;
  border-bottom: 1px solid transparent;
  animation: tf-slide-up var(--t-trans) ease reverse;
}
.capture-health-banner.hidden { display: none; }

/* Severity variants */
.capture-health-banner.error {
  background: color-mix(in srgb, var(--color-danger) 15%, var(--color-bg));
  border-color: color-mix(in srgb, var(--color-danger) 35%, transparent);
  color: var(--color-danger);
}
.capture-health-banner.warning {
  background: color-mix(in srgb, var(--color-warning) 15%, var(--color-bg));
  border-color: color-mix(in srgb, var(--color-warning) 35%, transparent);
  color: var(--color-warning);
}
.capture-health-banner.info {
  background: color-mix(in srgb, var(--color-info) 15%, var(--color-bg));
  border-color: color-mix(in srgb, var(--color-info) 35%, transparent);
  color: var(--color-info);
}
.capture-health-icon { font-size: var(--font-size-lg); flex-shrink: 0; }
.capture-health-msg  { flex: 1; }
.capture-health-detail {
  font-size: var(--font-size-xs);
  font-weight: 400;
  opacity: 0.75;
  white-space: nowrap;
}
.capture-health-dismiss {
  background: none; border: none; cursor: pointer;
  color: inherit; opacity: 0.6; font-size: var(--font-size-lg); padding: 0 0.25rem;
  flex-shrink: 0;
}
.capture-health-dismiss:hover, .capture-health-dismiss:focus-visible { opacity: 1; }

/* ── Login screen ────────────────────────────────────────────────────────── */
.login-screen {
  position: fixed; inset: 0; z-index: 1000;
  background: var(--bg);
  display: grid;
  grid-template-columns: 1fr 420px;
  align-items: center;
  overflow: hidden;
}
.login-screen.hidden { display: none; }
.login-card {
  /* v1216: aligned to the app's current card recipe — surface + 1px border +
     --radius-xl. Dropped the legacy accent-stripe border-top and the oversized
     --radius-3xl that made the card read as an older style. */
  background: var(--bg-2); border: 1px solid var(--border);
  border-radius: var(--radius-xl); padding: 2.5rem 2rem;
  width: 100%; max-width: 360px;
  box-shadow: 0 8px 32px rgba(0,0,0,0.12);
  text-align: center;
}
.login-title {
  font-family: var(--type-display);
  font-size: var(--font-size-2xl); font-weight: 600; margin-bottom: 0.25rem;
  color: var(--text);
}
.login-logo {
  max-width: 260px;
  width: 80%;
  margin: 0 auto 1.25rem;
  display: block;
}
.login-subtitle {
  font-size: var(--font-size-base); color: var(--text-2); margin-bottom: 1.25rem;
}
.login-input {
  /* v1216: aligned to the canonical .form-input recipe — squared corners +
     the shared teal focus ring. Was rounded (--radius-lg) with a ringless
     focus, which read as an older form style next to the rest of the app. */
  width: 100%; padding: 0.6rem 0.8rem;
  border: 1.5px solid var(--border); border-radius: 0;
  background: var(--bg); color: var(--text);
  font-size: var(--font-size-md); font-family: inherit;
  margin-bottom: 0.75rem;
  box-sizing: border-box;
  transition: border-color var(--t-hover) var(--ease), box-shadow var(--t-hover) var(--ease), background var(--t-hover) var(--ease);
}
.login-input:focus {
  outline: none;
  border-color: var(--accent-teal);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-teal) 12%, transparent);
}
/* v1216: the login CTA now COMPOSES .btn-primary (the app's canonical
   ghost-pill primary) in markup — .login-btn only adds full-width layout.
   Was a bespoke filled-teal button with an opacity-only hover, the last
   surface still using the retired solid-teal button style. */
.login-btn {
  width: 100%;
  justify-content: center;
  padding: 0.6rem 1rem;
  margin-bottom: 0.75rem;
}
.login-error {
  font-size: var(--font-size-sm); color: var(--accent-red);
  margin-bottom: 0.5rem;
}
.login-gm-link {
  /* v1216: modern text link — underline on hover only (was always-underlined
     with an opacity fade, an older link treatment). */
  background: none; border: none; cursor: pointer;
  font-size: var(--font-size-base); color: var(--accent);
  text-decoration: none; padding: 0.4rem 0;
  display: block; margin-top: 0.5rem;
  transition: color var(--t-hover) var(--ease);
}
.login-gm-link:hover, .login-gm-link:focus-visible {
  color: var(--accent-teal);
  text-decoration: underline;
}
.auth-links {
  display: flex; justify-content: space-between; gap: 0.5rem;
  margin-top: 0.25rem;
}
.auth-links .login-gm-link { margin-top: 0; }
.login-tos-note {
  font-size: var(--font-size-xs); color: var(--color-text-tertiary);
  text-align: center; margin: 0.25rem 0 0;
}
/* v1226: reassurance/urgency caption directly under the Start-free CTA —
   risk-reversal belongs adjacent to the click, not in a footnote. */
.login-offer-note {
  margin: 0.5rem 0 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
  text-align: center;
  line-height: 1.45;
}
/* v1240: muted, genuinely-visible link row beneath the auth card. Centers
   with the card group (form-col is flex/center); a real anchor to the GEO
   page so crawlers discover /what-is-threadfall. */
.login-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  margin-top: 1.5rem;
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
}
.login-footer a {
  color: var(--color-text-tertiary);
  text-decoration: none;
  transition: color var(--t-hover) var(--ease);
}
.login-footer a:hover { color: var(--accent); }
/* v980: marketing opt-in row on the signup card. Layout mirrors the
   Extended-content checkbox grammar (checkbox left, label flow right)
   so signup feels like the rest of the app. Font-size dimmer than the
   inputs — the checkbox is a secondary choice, not a primary one. */
.login-opt-in {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  margin: 0.25rem 0;
  padding: 0.5rem 0.1rem;
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
  line-height: 1.45;
  cursor: pointer;
}
.login-opt-in input[type="checkbox"] {
  width: 1rem;
  height: 1rem;
  accent-color: var(--accent-teal);
  flex-shrink: 0;
  margin-top: 0.15rem;
}
.login-opt-in strong { color: var(--text); font-weight: var(--font-weight-semibold); }
.pc-quote-divider {
  border: none; border-top: 1px solid var(--color-border);
  margin: 0.75rem 0 0.5rem;
}
.pc-best-quote {
  font-size: var(--font-size-md); color: var(--color-text-secondary);
  line-height: 1.5; padding: 0 0.5rem 0.25rem;
  text-align: center;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
}
.pc-best-quote-ctx {
  display: block; font-size: var(--font-size-xs); color: var(--color-text-tertiary);
  text-align: center; margin-top: 0.2rem; font-style: normal;
}

/* ── Codex Quotes panel ─────────────────────────────────────────────────── */
/* Each entity (or the synthetic "Party") renders as the canonical .entity-row
   shape — collapse-by-default, click to expand. The Quotes panel itself just
   provides the column layout + group-header rhythm; cards inherit border /
   radius / bg / hover from the shared .entity-row rules. */
.quotes-panel {
  padding: var(--space-4);
  display: flex; flex-direction: column; gap: var(--space-3);
}
/* Group headers (Active Party / NPCs / Fallen and Departed Party) — composed
   with .form-section-label; .quotes-group-hdr only adds the spacing above. */
.quotes-group-hdr {
  display: block;
  margin-top: var(--space-3);
  margin-bottom: 0;
}
.quotes-group-hdr:first-child { margin-top: 0; }
/* Fallen and Departed group — desaturated and slightly faded so it reads
   as historical, sits at the bottom. */
.quotes-fallen-group {
  display: flex; flex-direction: column; gap: var(--space-3);
  filter: grayscale(70%);
  opacity: 0.7;
}
.quotes-fallen-group .entity-row {
  border-color: var(--text-3);
}
/* Count chip — sits between entity-meta and chevron in the .entity-row-hdr.
   Matches the .session-num corner-badge recipe (teal-tinted color-mix,
   small radius, xs font). Single source of truth for "count chip". */
.quotes-count-badge {
  font-size: var(--font-size-xs);
  font-weight: 700;
  color: var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 13%, transparent);
  border-radius: var(--radius-sm);
  padding: 0.15rem 0.5rem;
  white-space: nowrap;
  flex-shrink: 0;
}
/* Quote list inside an expanded entity-row.entity-detail — subtle internal
   dividers (the outer .entity-row border handles overall enclosure). */
.quotes-list { display: flex; flex-direction: column; gap: 0; }
.quotes-item {
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--border);
  position: relative;
}
.quotes-item:first-child {
  border-top: none;
  padding-top: 0;
}
.quotes-item-text {
  font-size: var(--font-size-md);
  color: var(--text);
  line-height: 1.5;
}
.quotes-item-text em { font-style: italic; }
.quotes-item-ctx {
  font-size: var(--font-size-xs);
  color: var(--text-2);
  margin-top: 0.2rem;
}
.quotes-item-session {
  font-size: var(--font-size-xs);
  color: var(--text-3);
  margin-top: 0.15rem;
  font-style: italic;
}

.archived-campaign-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0.5rem 0.75rem; border-radius: var(--radius-lg);
  border: 1px solid var(--color-border); background: var(--color-surface);
  margin-bottom: 0.35rem; gap: 0.75rem;
}
.archived-campaign-name { font-size: var(--font-size-base); color: var(--color-text-secondary); flex: 1; }
.archived-campaign-meta { font-size: var(--font-size-xs); color: var(--color-text-tertiary); }

/* Campaign role badges & member rows.
   v1084: aligned to spec .tf-badge — pill silhouette (3px 9px / radius-full),
   10.5px / 600 / 0.06em uppercase, 1px transparent border default. Variants
   compose the .tf-badge--ember/ghost/gold semantic palette. */
.camp-role-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: var(--radius-full);
  border: 1px solid transparent;
  font-size: 10.5px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.5;
  white-space: nowrap;
  margin-left: 0.4rem;
}
.camp-role-gm     { color: var(--ember-soft); background: var(--ember-bg); border-color: var(--ember-border); }
.camp-role-co-gm  { color: var(--ember-soft); background: var(--ember-bg); border-color: var(--ember-border); }
.camp-role-member { color: var(--text-3);     background: transparent;     border-color: var(--border); }
.camp-role-admin  { color: var(--gold-text-on, #5C4716); background: color-mix(in srgb, var(--gold, #B7912E) 16%, var(--bg)); border-color: color-mix(in srgb, var(--gold, #B7912E) 35%, transparent); }
[data-theme="dark"] .camp-role-gm,
[data-theme="dark"] .camp-role-co-gm  { color: #F5C0AE; background: color-mix(in srgb, var(--accent-red) 18%, var(--bg-2)); border-color: color-mix(in srgb, var(--accent-red) 45%, transparent); }
[data-theme="dark"] .camp-role-member { color: var(--text-3); background: transparent; border-color: var(--border); }
[data-theme="dark"] .camp-role-admin  { color: #E5C97D; background: color-mix(in srgb, var(--gold, #D4B260) 18%, var(--bg-2)); border-color: color-mix(in srgb, var(--gold, #D4B260) 45%, transparent); }
.camp-member-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0.5rem 0.75rem; border-radius: var(--radius-lg);
  background: var(--bg-2); border: 1px solid var(--border);
  gap: 0.5rem;
}
.camp-member-info { display: flex; align-items: center; flex: 1; min-width: 0; }
.camp-member-email { font-size: var(--font-size-base); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.camp-member-actions { display: flex; gap: 0.3rem; flex-shrink: 0; }
.gm-escalate-btn {
  padding: 0.25rem 0.6rem;
  background: none; border: 1px solid var(--accent);
  border-radius: var(--radius-md); color: var(--accent);
  font-size: var(--font-size-sm); cursor: pointer;
  transition: background var(--t-hover), color var(--t-hover);
}
.gm-escalate-btn:hover, .gm-escalate-btn:focus-visible { background: var(--accent); color: #fff; }
.gm-escalate-overlay {
  position: fixed; inset: 0; z-index: 900;
  background: rgba(0,0,0,0.55);
  display: flex; align-items: center; justify-content: center;
}
.gm-escalate-overlay.hidden { display: none; }
.gm-escalate-card {
  background: var(--bg-1); border: 1px solid var(--border);
  border-radius: var(--radius-2xl); padding: 1.6rem 2rem;
  width: min(340px, 90vw); display: flex; flex-direction: column; gap: 0.8rem;
  box-shadow: 0 8px 32px rgba(0,0,0,0.4);
}
.gm-escalate-title {
  font-size: var(--font-size-xl); font-weight: 700; color: var(--text-1);
  margin: 0; text-align: center;
}
.gm-escalate-btns { display: flex; gap: 0.6rem; }
.logout-btn {
  padding: 0.25rem 0.6rem;
  background: none; border: 1px solid var(--border);
  border-radius: var(--radius-md); color: var(--text-3);
  font-size: var(--font-size-sm); cursor: pointer;
  transition: color var(--t-hover), border-color var(--t-hover);
}
.logout-btn:hover, .logout-btn:focus-visible { color: var(--accent-red); border-color: var(--accent-red); }

/* ── Recorder Widget ─────────────────────────────────────────────────────────── */
.live-recorder {
  padding: 0.3rem 0.6rem;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.live-recorder.hidden { display: none; }
/* v796: eyebrow row above the recorder panel. Tight padding so it doesn't
   eat the limited Live-sidebar real estate. */
.live-recorder-hdr {
  display: flex;
  align-items: center;
  padding: var(--space-1) 0 var(--space-2);
}
.rec-panel { display: flex; flex-direction: column; gap: 0.22rem; }
.rec-panel.hidden { display: none; }

/* v1022: post-stop "session saved" banner. Sits above the Start Recording
   button after a successful stop so the GM has a clear next-step CTA
   (generate recap on Chronicle) instead of staring at the Live screen
   with no obvious action. Subdued teal-tinted card to avoid competing
   with the red record button below. */
.rec-post-stop-cta {
  background: color-mix(in srgb, var(--accent-teal) 8%, var(--bg-2));
  border: 1px solid color-mix(in srgb, var(--accent-teal) 35%, transparent);
  border-radius: var(--radius-md);
  padding: 0.7rem 0.85rem;
  margin-bottom: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.rec-post-stop-cta.hidden { display: none; }
.rec-post-stop-title {
  margin: 0;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-bold);
  color: var(--text);
}
.rec-post-stop-body {
  margin: 0;
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
  line-height: 1.4;
}
.rec-post-stop-actions {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
}

/* System audio toggle */
.rec-sys-audio-label {
  display: flex; align-items: center; gap: 0.35rem;
  font-size: var(--font-size-sm); color: var(--text-2); cursor: pointer;
  user-select: none;
}
.rec-sys-audio-label input[type="checkbox"] { accent-color: var(--accent-teal); cursor: pointer; }

/* Ready state */
.rec-start-btn {
  width: 100%; padding: 0.3rem;
  font-size: var(--font-size-sm); font-weight: 700; letter-spacing: 0.03em;
  border: 2px solid var(--accent-red); border-radius: var(--radius-md);
  background: none; color: var(--accent-red); cursor: pointer;
  transition: background var(--t-hover), color var(--t-hover);
}
.rec-start-btn:hover, .rec-start-btn:focus-visible { background: var(--accent-red); color: #fff; }

/* Active / paused state — v1134 aligned to spec .tf-live-timer recipe.
   The recording row is now a unified ember pill (radius 999px / ember bg /
   mono numerals / cream text) with a halo pulse on the recording state,
   and a smaller cream dot that blinks in sync. The whole cluster reads
   as ONE chip ("● REC 0:00:00") instead of three loose pieces.
   Paused state drops the halo + dot blink to amber.
   The spec puts this in the page header — Threadfall keeps it inside the
   recorder panel, but the chip itself adopts the same recipe so it reads
   as the canonical "table is live" affordance wherever it appears. */
.rec-top-row {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 8px 14px;
  border-radius: 999px;
  background: var(--ember-soft);
  color: #FFF6F1;
  font-family: var(--type-mono);
  font-weight: 600;
  font-size: var(--font-size-xl);
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--ember-soft) 55%, transparent);
  animation: tf-live-halo 2s ease-out infinite;
}
.rec-top-row:has(.rec-indicator.paused) {
  background: var(--accent-amber);
  animation: none;
}
@keyframes tf-live-halo {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--ember-soft) 55%, transparent); }
  70%  { box-shadow: 0 0 0 10px color-mix(in srgb, var(--ember-soft) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--ember-soft) 0%, transparent); }
}
.rec-indicator {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: #FFF6F1;
  box-shadow: 0 0 0 2px color-mix(in srgb, #fff 35%, transparent);
  flex-shrink: 0;
}
.rec-indicator.recording {
  animation: tf-dot-blink 1.2s ease-in-out infinite;
}
.rec-indicator.paused { animation: none; opacity: 0.7; }
@keyframes tf-dot-blink {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.3; }
}
/* The state label and clock inherit the pill's typography. State label
   keeps its tighter UI eyebrow recipe (uppercase, tracked) for the
   "REC" / "PAUSED" word; the clock keeps the bigger mono numerals. */
.rec-state-lbl {
  font-family: var(--type-ui);
  font-size: var(--font-size-2xs);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  opacity: 0.92;
  color: inherit;
}
.rec-state-lbl.recording, .rec-state-lbl.paused { color: inherit; }
.rec-clock {
  font-family: var(--type-mono);
  font-size: var(--font-size-xl);
  font-weight: 600;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
  color: inherit;
  margin-left: auto;
}

/* ── Mic input level meter (v780) ─────────────────────────────────────────
   Segmented LED bar driven by liveAnalyser. 14 segments split into three
   zones — green (1-9), amber (10-12), red (13-14). Lit segments glow with
   their zone colour; unlit segments stay tinted-but-dim so the bar's
   silhouette is always readable. Premium signal: visible feedback that
   the mic is actually capturing audio, especially during in-room sessions. */
.rec-level-meter {
  display: flex;
  gap: 2px;
  align-items: stretch;
  width: 100%;
  height: 8px;
  margin-top: var(--space-1);
  border-radius: var(--radius-sm);
  background: var(--bg-3);
  border: 1px solid var(--border);
  padding: 1px;
  box-sizing: border-box;
  overflow: hidden;
}
.rec-level-seg {
  flex: 1;
  border-radius: 1px;
  background: color-mix(in srgb, var(--text-3) 25%, transparent);
  transition: background 60ms linear, box-shadow 60ms linear, opacity 60ms linear;
  opacity: 0.55;
}
.rec-level-seg.is-on { opacity: 1; }
.rec-level-seg.zone-green.is-on {
  background: var(--color-success);
  box-shadow: 0 0 4px color-mix(in srgb, var(--color-success) 60%, transparent);
}
.rec-level-seg.zone-amber.is-on {
  background: var(--accent-amber);
  box-shadow: 0 0 4px color-mix(in srgb, var(--accent-amber) 60%, transparent);
}
.rec-level-seg.zone-red.is-on {
  background: var(--accent-red);
  box-shadow: 0 0 5px color-mix(in srgb, var(--accent-red) 70%, transparent);
}
@media (prefers-reduced-motion: reduce) {
  .rec-level-seg { transition: none; }
}
.rec-btn-row { display: flex; gap: 0.4rem; }
.rec-btn {
  flex: 1; padding: 0.22rem 0.4rem;
  font-size: var(--font-size-xs); font-weight: 600; white-space: nowrap;
  border: 1.5px solid var(--border); border-radius: var(--radius-md);
  background: var(--bg-2); color: var(--text-1); cursor: pointer;
  transition: background var(--t-hover), border-color var(--t-hover), color var(--t-hover);
}
.rec-btn:hover, .rec-btn:focus-visible { background: var(--bg-3); }
.rec-btn-finish { border-color: var(--accent); color: var(--accent); }
.rec-btn-finish:hover, .rec-btn-finish:focus-visible { background: var(--accent); color: #fff; }
.rec-discard-row {
  display: flex; justify-content: center; gap: 1rem;
}
.rec-discard-link {
  background: none; border: none; padding: 0.1rem 0;
  font-size: var(--font-size-xs); color: var(--text-3); cursor: pointer;
  transition: color var(--t-hover);
}
.rec-discard-link:hover, .rec-discard-link:focus-visible { color: var(--accent-red); }
.rec-member-notice {
  font-size: var(--font-size-sm); color: var(--text-3); text-align: center;
  padding: 0.4rem 0; font-style: italic;
}
.rec-chime-notice {
  font-size: var(--font-size-xs); color: var(--text-3); text-align: center;
  margin-top: 0; opacity: 0.7;
}
.rec-bt-note {
  font-size: var(--font-size-xs); color: var(--color-warning);
  margin-top: 0.35rem; line-height: 1.35; cursor: help;
  padding: 0.3rem 0.5rem;
  background: color-mix(in srgb, var(--color-warning) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-warning) 25%, transparent);
  border-radius: var(--radius-md);
  display: flex;
  align-items: flex-start;
  gap: 0.4rem;
}
.rec-bt-note-text {
  flex: 1 1 auto;
  text-align: center;
}
.rec-bt-note-close {
  flex-shrink: 0;
  background: transparent;
  border: none;
  color: inherit;
  font-size: var(--font-size-sm);
  line-height: 1;
  padding: 0 0.2rem;
  cursor: pointer;
  opacity: 0.7;
  transition: opacity var(--t-hover) var(--ease);
}
.rec-bt-note-close:hover,
.rec-bt-note-close:focus-visible {
  opacity: 1;
  outline: none;
}

/* Quest panel — below map on the left */
/* v795: removed border-top — the project rule explicitly forbids
   border-top/border-bottom dividers under section headers. */
.live-quest-panel {
  padding: var(--space-2) var(--space-3);
  flex-shrink: 0; max-height: 220px; overflow-y: auto;
  background: var(--surface-1);
}
.live-quest-panel-hdr {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 0.4rem;
}
/* v795: typography moved to .form-section-label composed in markup. */
.live-quest-panel-body { display: flex; flex-direction: column; gap: 0.3rem; }
/* (v780) .lqp-empty retired — replaced by .placeholder.placeholder--padded
   composed in markup in renderLiveQuestPanel. */
.lqo-item {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  padding: 0.3rem 0.4rem; border-radius: var(--radius-md); background: var(--bg-2);
}
.lqo-main { flex: 1; min-width: 0; }
.lqo-title {
  font-size: var(--font-size-sm); color: var(--text-1); display: block;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lqo-desc {
  font-size: var(--font-size-xs); color: var(--text-3); margin-top: 2px;
  line-height: 1.35; white-space: pre-wrap; word-break: break-word;
}
.lqo-actions { display: flex; gap: 0.25rem; flex-shrink: 0; align-items: center; }
.lqo-btn {
  padding: 1px 6px; border-radius: 0; font-size: var(--font-size-sm); font-weight: 700;
  border: 1.5px solid var(--border); background: none; cursor: pointer; line-height: 1.5;
  transition: background var(--t-hover), color var(--t-hover), border-color var(--t-hover);
}
.lqo-complete { color: var(--accent-teal); border-color: var(--accent-teal); }
.lqo-complete:hover, .lqo-complete:focus-visible { background: var(--accent-teal); color: #fff; }
.lqo-fail { color: var(--accent-red); border-color: var(--accent-red); }
.lqo-fail:hover, .lqo-fail:focus-visible { background: var(--accent-red); color: #fff; }

/* no-map: left column always visible; map panel shows empty state placeholder */
/* (kept as hook for JS in case future layout overrides are needed) */

/* Quest log collapsed state */
.live-quest-panel.collapsed { max-height: none; overflow: visible; }
.live-quest-panel.collapsed .live-quest-panel-body { display: none; }
.live-quest-panel.collapsed .live-quest-panel-hdr  { margin-bottom: 0; }
.live-quest-panel.collapsed .live-quest-collapse-btn { transform: rotate(-90deg); }

/* ── Live screen PC character sidebar ───────────────────────────── */
.live-pc-sidebar-expander {
  width: 28px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border-right: 1px solid var(--border);
  background: var(--bg-2);
  cursor: pointer;
  user-select: none;
  transition: background var(--t-hover), color var(--t-hover);
}
.live-pc-sidebar-expander:hover, .live-pc-sidebar-expander:focus-visible { background: var(--bg-3); color: var(--accent-blue); }
.live-pc-sidebar-expander span {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-size: var(--font-size-xs); font-weight: 600;
  color: var(--text-2);
  letter-spacing: 0.04em;
  white-space: nowrap;
  overflow: hidden;
  max-height: 200px;
  text-overflow: ellipsis;
}
.live-pc-sidebar-expander:hover span, .live-pc-sidebar-expander:focus-visible span { color: var(--accent-blue); }
.live-pc-sidebar {
  width: 300px; flex-shrink: 0;
  display: flex; flex-direction: column;
  border-right: 1px solid var(--border);
  background: var(--bg);
  overflow: hidden;
}
.live-pc-sidebar-hdr {
  display: flex; align-items: center; gap: 0.25rem;
  padding: 0.35rem 0.5rem;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
  font-size: var(--font-size-base); font-weight: 600; color: var(--text);
}
/* v795: typography moved to .form-section-label composed in markup. */
#live-pc-sidebar-hdr-title {
  flex: 1; text-align: center;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.live-pc-nav-btn {
  font-size: var(--font-size-xs); padding: 2px 7px; flex-shrink: 0;
  color: var(--text-2);
}
.live-pc-nav-btn:hover, .live-pc-nav-btn:focus-visible { color: var(--accent-teal); }
/* v795: shared --subtle modifier for the small ▾/◁ collapse buttons.
   Used by .live-pc-collapse-btn and .live-ai-collapse-btn — same recipe,
   one source of truth. */
.live-collapse-btn--subtle,
.live-pc-collapse-btn,
.live-ai-collapse-btn { margin-left: auto; font-size: var(--font-size-xs); opacity: 0.6; }
.live-collapse-btn--subtle:hover, .live-collapse-btn--subtle:focus-visible,
.live-pc-collapse-btn:hover, .live-pc-collapse-btn:focus-visible,
.live-ai-collapse-btn:hover, .live-ai-collapse-btn:focus-visible { opacity: 1; }
.live-pc-sidebar-body {
  flex: 1; overflow-y: auto; padding: 0.6rem;
}

/* ── Live Chat Drawer (right side, mirrors PC sidebar) ─────────────────── */
/* v1135: aligned to spec .tf-notebook recipe — subtle ink wash bg over
   bg-2 (was flat bg) so the GM Notebook drawer reads as a distinct
   sidebar surface from the main log feed (which sits on bg / paper).
   Dark theme bumps the ink mix to 5% per spec. */
.live-ai-drawer {
  width: 420px; flex-shrink: 0;
  display: flex; flex-direction: column;
  border-left: 1px solid var(--border);
  background: color-mix(in srgb, var(--text-1) 2%, var(--bg-2));
  overflow: hidden;
}
[data-theme="dark"] .live-ai-drawer {
  background: color-mix(in srgb, var(--text-1) 5%, var(--bg-2));
}
.live-ai-drawer-hdr {
  display: flex; align-items: center; gap: 0.4rem;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
/* v795: typography moved to .form-section-label composed in markup. */
.live-ai-drawer-title { flex: 1; }
/* (v795) .live-ai-collapse-btn rules consolidated into the
   .live-collapse-btn--subtle group above. */
.live-ai-drawer-body {
  flex: 1;
  display: flex; flex-direction: column;
  overflow-y: auto;       /* drawer scrolls as a whole — sections stack vertically */
  min-height: 0;
}

/* GM Notebook section pattern (commit 4 of 7).
   Each section: header (eyebrow + count badge + chevron) + body.
   Click anywhere on the header to toggle .ln-collapsed.
   Compose .form-section-label / .quotes-count-badge / .live-collapse-btn
   from existing rules — no new chrome typography. */
.live-notebook-section {
  display: flex;
  flex-direction: column;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
}
.live-notebook-section:last-child { border-bottom: none; }
.live-notebook-section-hdr {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: var(--space-3) var(--space-3);
  cursor: pointer;
  user-select: none;
  background: transparent;
  transition: background var(--t-hover) var(--ease);
}
.live-notebook-section-hdr:hover {
  background: color-mix(in srgb, var(--accent-teal) 5%, transparent);
}
.live-notebook-section-hdr .form-section-label {
  flex: 1 1 auto;
  /* let the label truncate before pushing the chevron off-canvas */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Count badge hides when zero so an empty section reads cleanly. */
.live-notebook-count[data-count="0"],
.live-notebook-count:empty { display: none; }
.live-notebook-collapse {
  transition: transform var(--t-hover) var(--ease);
}
.live-notebook-section.ln-collapsed .live-notebook-collapse {
  transform: rotate(-90deg);
}
.live-notebook-body {
  padding: 0 var(--space-3) var(--space-3);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.live-notebook-section.ln-collapsed .live-notebook-body {
  display: none;
}
/* Empty-state body text inside a notebook section — used by the
   v912 placeholders that render until commits 5-7 wire real data. */
.live-notebook-empty {
  margin: 0;
  font-style: italic;
  color: var(--text-3);
  font-size: var(--font-size-sm);
}

/* Fates section card (Notebook column) — collapsed header is a compact
   row of: type-chip + title + pending-count + ↗ pop-out. Click anywhere
   on the header (except the pop-out) toggles expand. Expand reveals the
   full scene rendered inline (compact density tuned to ~360px width).
   v919: switched from "click row → open modal" to "click row → expand
   inline" per user direction; modal is now opt-in via the ↗ icon. */
.ln-fate-card {
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text);
  font-size: var(--font-size-sm);
  overflow: hidden;
  transition: border-color var(--t-hover) var(--ease);
}
.ln-fate-card--expanded {
  border-color: rgba(47, 154, 163, 0.45);
}
.ln-fate-card-hdr {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.55rem;
  cursor: pointer;
  user-select: none;
  transition: background var(--t-hover) var(--ease);
}
.ln-fate-card-hdr:hover {
  background: color-mix(in srgb, var(--accent-teal) 5%, transparent);
}
.ln-fate-card--expanded .ln-fate-card-hdr {
  background: color-mix(in srgb, var(--accent-teal) 6%, transparent);
}
.ln-fate-row-title {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-weight: var(--font-weight-bold);
}
.ln-fate-row-count {
  flex-shrink: 0;
  font-size: var(--font-size-xs);
  color: var(--text-3);
  font-weight: 500;
}
.ln-fate-popout {
  flex-shrink: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--text-3);
  font-size: var(--font-size-base);
  line-height: 1;
  padding: 0.15rem 0.35rem;
  cursor: pointer;
  opacity: 0;
  transition:
    opacity      var(--t-hover) var(--ease),
    color        var(--t-hover) var(--ease),
    background   var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease);
}
.ln-fate-card-hdr:hover .ln-fate-popout,
.ln-fate-card-hdr:focus-within .ln-fate-popout,
.ln-fate-card--expanded .ln-fate-popout {
  opacity: 0.7;
}
.ln-fate-popout:hover,
.ln-fate-popout:focus-visible {
  opacity: 1;
  color: var(--accent-teal);
  border-color: color-mix(in srgb, var(--accent-teal) 40%, var(--border));
  background: color-mix(in srgb, var(--accent-teal) 8%, transparent);
  outline: none;
}
/* Inline-expanded scene body — uses the same .fates-scene-section /
   .form-section-label / .fates-scene-premise rules as the modal render
   so the typography is consistent. Just wrapped with tighter padding
   for the narrow drawer column. */
.ln-fate-expanded {
  padding: var(--space-3) var(--space-3) var(--space-3);
  border-top: 1px dashed color-mix(in srgb, var(--accent-teal) 25%, var(--border));
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
.ln-fate-expanded .fates-scene-premise {
  margin: 0;
}
.ln-fate-expanded .fates-scene-section p {
  /* Slightly tighter line-height in the narrow column for scannability. */
  line-height: 1.45;
}

/* Foretelling row inside the Notebook — checkbox + placement chip on
   left, seed text + cover + parent-Fate footnote middle, hover-revealed
   ✕ delete on the right (v942 — GM-only seed delete). */
.ln-foretelling-row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 0.6rem;
  align-items: start;
  padding: 0.5rem 0.55rem;
  background: color-mix(in srgb, var(--accent-teal) 4%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent-teal) 18%, var(--border));
  border-radius: var(--radius-md);
  transition: opacity var(--t-hover) var(--ease);
}
/* Planted state — strikethrough + dim, mirrors the modal's inline view. */
.ln-foretelling-row--planted { opacity: 0.55; }
.ln-foretelling-row--planted .ln-foretelling-text {
  text-decoration: line-through;
  text-decoration-color: var(--text-3);
}
/* v942: hover-revealed ✕ on each foreshadow row. Same pattern as
   .fates-row-action-btn--snip / .fates-edit-npc-remove (rest opacity 0,
   row-hover reveals to 0.7, button hover to 1 + danger tint). */
.fates-foreshadow-row-delete {
  appearance: none;
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-3);
  font-size: var(--font-size-sm);
  line-height: 1;
  padding: 0.15rem 0.35rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  opacity: 0;
  transition:
    opacity      var(--t-hover) var(--ease),
    color        var(--t-hover) var(--ease),
    background   var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease);
  align-self: start;
}
.ln-foretelling-row:hover .fates-foreshadow-row-delete,
.ln-foretelling-row:focus-within .fates-foreshadow-row-delete { opacity: 0.7; }
.fates-foreshadow-row-delete:hover,
.fates-foreshadow-row-delete:focus-visible {
  opacity: 1;
  color: var(--accent-red);
  border-color: color-mix(in srgb, var(--accent-red) 35%, var(--border));
  background: color-mix(in srgb, var(--accent-red) 8%, transparent);
  outline: none;
}
/* v942: empty-state CTA when a Fate has no foreshadow yet. Stack a
   helper line + a single Generate button. .fates-foreshadow-regen is
   the same idiom for the populated state — small ghost button under
   the seed list that re-runs generation. */
.fates-foreshadow-empty {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.4rem;
  padding: 0.4rem 0;
}
.fates-foreshadow-empty .form-helper-text { margin: 0; }
.fates-foreshadow-regen {
  display: flex;
  justify-content: flex-end;
  margin-top: 0.4rem;
}

/* Foreshadow section embedded inside an expanded Fate (v921) — list
   of seed rows stacked vertically with tight spacing. */
.ln-fate-foreshadow-section .ln-fate-foreshadow-list {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin-top: 0.25rem;
}
.ln-foretelling-check {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  cursor: pointer;
  user-select: none;
}
.ln-foretelling-check input {
  cursor: pointer;
  accent-color: var(--accent-teal);
}
.ln-foretelling-body {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.ln-foretelling-text {
  margin: 0;
  font-size: var(--font-size-sm);
  line-height: 1.4;
  color: var(--text);
}
.ln-foretelling-cover {
  margin: 0;
  font-size: var(--font-size-xs);
  font-style: italic;
  color: var(--text-3);
  line-height: 1.35;
}
.ln-foretelling-cover-label {
  font-style: normal;
  font-weight: 500;
  color: var(--text-2);
  margin-right: 0.25rem;
}
.ln-foretelling-fate {
  margin: 0;
  font-size: var(--font-size-xs);
  color: color-mix(in srgb, var(--accent-teal) 70%, var(--text-3));
}

/* Thread row inside the Notebook — status glyph + title + heat score.
   Compact list style, no card chrome (the Threads section can have
   up to 7 visible — keeping rows tight prevents the drawer from
   becoming a wall). */
.ln-thread-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.3rem 0.4rem;
  border-radius: var(--radius-sm);
  font-size: var(--font-size-sm);
  color: var(--text);
  cursor: default;
  transition: background var(--t-hover) var(--ease);
}
.ln-thread-row:hover {
  background: color-mix(in srgb, var(--accent-teal) 5%, transparent);
}
.ln-thread-status {
  flex-shrink: 0;
  font-size: var(--font-size-sm);
  line-height: 1;
}
.ln-thread-status--hot  { color: var(--accent-ember); }
.ln-thread-status--warm { color: var(--accent-gold); }
.ln-thread-status--cold { color: var(--text-3); }
.ln-thread-title {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ln-thread-heat {
  flex-shrink: 0;
  font-size: var(--font-size-xs);
  font-variant-numeric: tabular-nums;
  color: var(--text-3);
}
.ln-thread-overflow {
  margin-top: 0.3rem;
  text-align: center;
}

/* v922: in-section expand/fewer button for the Threads cap.
   Mirrors the Fates-tab entity-rail expander recipe (dashed teal
   border, ▾ glyph, hover lifts subtle teal wash). The "Show fewer"
   variant flips the chevron via .ln-thread-expand-glyph--up. */
.ln-thread-expand-btn {
  display: block;
  width: 100%;
  margin-top: var(--space-2);
  padding: 0.4rem 0.5rem;
  background: transparent;
  border: 1px dashed color-mix(in srgb, var(--accent-teal) 28%, var(--border));
  border-radius: var(--radius-md);
  color: var(--text-3);
  font-family: inherit;
  font-size: var(--font-size-xs);
  text-align: left;
  cursor: pointer;
  transition:
    background   var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease),
    color        var(--t-hover) var(--ease);
}
.ln-thread-expand-btn:hover,
.ln-thread-expand-btn:focus-visible {
  background: color-mix(in srgb, var(--accent-teal) 6%, transparent);
  border-color: color-mix(in srgb, var(--accent-teal) 45%, var(--border));
  color: var(--text-2);
  outline: none;
}
.ln-thread-expand-glyph {
  display: inline-block;
  margin-right: 0.3rem;
  color: color-mix(in srgb, var(--accent-teal) 70%, var(--text-3));
  transition: transform var(--t-hover) var(--ease);
}
.ln-thread-expand-glyph--up { transform: rotate(180deg); }

/* Loose Threads — collapsible card pattern (v919). Header row is the
   compact summary; expand reveals the thread context (linkified to
   Codex entities) right in the column. */
.ln-thread-card {
  border-radius: var(--radius-sm);
  transition: background var(--t-hover) var(--ease);
}
.ln-thread-card .ln-thread-row {
  cursor: pointer;
}
.ln-thread-card--expanded {
  background: color-mix(in srgb, var(--accent-teal) 4%, transparent);
}
.ln-thread-card--expanded .ln-thread-row {
  background: color-mix(in srgb, var(--accent-teal) 5%, transparent);
}
.ln-thread-expanded {
  padding: 0.4rem 0.5rem 0.5rem;
}
.ln-thread-context {
  margin: 0;
  font-size: var(--font-size-xs);
  line-height: 1.45;
  color: var(--text-2);
}
.ln-thread-context--empty {
  font-style: italic;
  color: var(--text-3);
}

/* Live Context cards — NPCs / Factions / Locations mentioned ≥2× in
   this session's transcript (PCs / spells / items filtered out per
   v954). Horizontal layout: portrait LEFT, body RIGHT. Portrait is
   the click target → opens the canonical entity-preview side drawer.
   Codex snippet is always visible inline; the pre-v954 expand-on-
   card-click behavior + per-mention timestamp quotes are retired
   (the table is at the live session — they all heard it). */
.ln-context-card {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  gap: 0.55rem;
  padding: 0.5rem 0.6rem;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  transition:
    background   var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease);
}
.ln-context-card:hover {
  background: color-mix(in srgb, var(--accent-teal) 5%, var(--bg-2));
  border-color: color-mix(in srgb, var(--accent-teal) 35%, var(--border));
}
.ln-context-portrait {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-sm);
  object-fit: cover;
  cursor: pointer;
  border: 1px solid color-mix(in srgb, var(--text-1) 8%, transparent);
  background: var(--bg-3);
  display: flex;
  align-items: center;
  justify-content: center;
  transition:
    border-color var(--t-hover) var(--ease),
    transform    var(--t-hover) var(--ease),
    box-shadow   var(--t-hover) var(--ease);
}
.ln-context-portrait:hover,
.ln-context-portrait:focus-visible {
  outline: none;
  border-color: color-mix(in srgb, var(--accent-teal) 55%, var(--border));
  transform: translateY(-1px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
.ln-context-portrait--icon {
  font-size: var(--font-size-lg);
  appearance: none;
  padding: 0;
}
.ln-context-card-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.ln-context-card-hdr {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.ln-context-card-hdr .fates-warp-head {
  /* compact — name pill takes available width with ellipsis */
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ln-context-mentions {
  flex-shrink: 0;
  font-size: var(--font-size-xs);
  font-variant-numeric: tabular-nums;
  color: var(--text-3);
  font-weight: 500;
}
.ln-context-dismiss {
  flex-shrink: 0;
  background: transparent;
  border: none;
  color: var(--text-3);
  font-size: var(--font-size-sm);
  line-height: 1;
  padding: 0 0.25rem;
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--t-hover) var(--ease), color var(--t-hover) var(--ease);
}
.ln-context-card:hover .ln-context-dismiss,
.ln-context-card:focus-within .ln-context-dismiss {
  opacity: 0.7;
}
.ln-context-dismiss:hover {
  opacity: 1;
  color: var(--color-danger);
}
.ln-context-snippet {
  margin: 0;
  font-size: var(--font-size-xs);
  color: var(--text-2);
  line-height: 1.4;
  /* v954: codex blurb wraps to 2 lines max; ellipsis after that.
     Italic dropped — this is descriptive prose now, not a quote. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.ln-context-expanded {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin-top: 0.3rem;
  padding-top: 0.4rem;
  border-top: 1px dashed color-mix(in srgb, var(--accent-teal) 25%, var(--border));
}
.ln-context-quote {
  margin: 0;
  font-size: var(--font-size-xs);
  line-height: 1.4;
  color: var(--text-2);
}
.ln-context-quote-time {
  font-weight: 600;
  color: var(--accent-teal);
  font-variant-numeric: tabular-nums;
}
.ln-context-quote-text {
  font-style: italic;
}
.ln-context-codex {
  margin: 0;
  font-size: var(--font-size-xs);
  color: var(--text-3);
  line-height: 1.35;
  padding-top: 0.3rem;
  border-top: 1px dashed color-mix(in srgb, var(--border) 60%, transparent);
}
/* v910: hoisted off .live-ai-drawer-body parent so the rules survive
   when chat moves under the transcript (commit 3 of GM Notebook).
   Behavior unchanged in the drawer; same rules now also apply when
   .live-log + .live-entry-form mount under .live-sidebar. */
/* Log fills remaining space in its parent flex container */
.live-log { flex: 1; overflow-y: auto; }
/* Entry form doesn't scroll */
.live-entry-form { flex-shrink: 0; }
/* Preamble (Where We Left Off) — stays in the drawer as the Notebook's
   first section, so this rule keeps its drawer-context selector. */
.live-ai-drawer-body .live-preamble {
  max-height: none;
  border-bottom: none;
  flex-shrink: 0;
}

.live-ai-expander {
  width: 28px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border-left: 1px solid var(--border);
  background: var(--bg-2);
  cursor: pointer;
  user-select: none;
  transition: background var(--t-hover), color var(--t-hover);
}
.live-ai-expander:hover, .live-ai-expander:focus-visible { background: var(--bg-3); color: var(--accent-teal); }
.live-ai-expander span {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-size: var(--font-size-xs); font-weight: 600;
  color: var(--text-2);
  overflow: hidden;
  max-height: 160px;
  text-overflow: ellipsis;
}
.live-ai-expander:hover span, .live-ai-expander:focus-visible span { color: var(--accent-teal); }

/* ── Live viewers bar (GM-only — shows who's in the session) ─────────── */
/* v1135: aligned to spec .tf-live-viewers + .tf-live-viewer recipe — 28px
   circular avatar dots with online halos. Each chip is just the user's
   initial; full display name surfaces on hover via title="". The online
   halo is a moss-green dot in the bottom-right corner; the recording
   variant (.is-recording) swaps the halo to ember to signal "this user is
   actively recording". The legacy text pill ("👁 Display Name") was
   visually loud — the spec circles read as a much calmer presence
   indicator that scales gracefully when many players are in the session. */
.live-viewers-bar {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  flex-shrink: 0;
}
.live-viewers-bar.hidden { display: none; }
.live-viewer-chip {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--bg);
  border: 1px solid var(--border);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--type-ui);
  font-size: 11px;
  font-weight: 700;
  color: var(--text-2);
  position: relative;
}
[data-theme="dark"] .live-viewer-chip {
  background: rgba(255,255,255,0.06);
  color: var(--text-2);
  border-color: rgba(255,255,255,0.10);
}
.live-viewer-chip.is-online::after {
  content: '';
  position: absolute;
  bottom: -1px;
  right: -1px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent-green);
  border: 1.5px solid var(--bg);
}
[data-theme="dark"] .live-viewer-chip.is-online::after {
  border-color: var(--bg-2);
}
/* Recording viewer — the green online dot swaps to ember so the GM can
   see at a glance which player(s) are actually capturing audio. */
.live-viewer-chip.is-recording::after {
  background: var(--ember-soft);
}

/* "played by X" is redundant when viewing your own card */
.live-pc-sidebar .pc-player-name { display: none; }
/* Stack portrait above stats inside the narrow sidebar */
.live-pc-sidebar .party-pc-card { flex-direction: column; }
.live-pc-sidebar .party-pc-portrait { width: 100%; aspect-ratio: unset; height: 140px; }
/* Live sidebar uses a tall slim 140px crop where the head/face usually sits
   near the top of the source art. Default `--portrait-y: 0%` anchors the top
   edge to the panel's top edge; the crop tool on this surface is 2D so GMs
   can pull the face into frame and save a per-PC offset. Other surfaces
   keep `center` for vertical and ignore `--portrait-y`. */
.live-pc-sidebar .party-pc-portrait img { object-position: var(--portrait-x, 50%) var(--portrait-y, 0%); }
.live-pc-sidebar .party-pc-stats { padding: 0.55rem 0.65rem; }
/* Name row: stack name/metadata above buttons so both get full width (no squeeze) */
.live-pc-sidebar .party-pc-name-row {
  flex-direction: column;
  align-items: flex-start;
  gap: 0.25rem;
}
.live-pc-sidebar .party-pc-name-row > div:last-child {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: center;
}
/* Ability grid: 3 columns per row so 6 stats wrap as 3+3 */
.live-pc-sidebar .ddb-ability-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.35rem;
}
/* Toggle buttons for multiple assigned PCs */
.live-pc-toggle-row {
  display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 0.6rem;
}
.live-pc-toggle-btn.active {
  background: var(--accent-blue); color: #fff; border-color: var(--accent-blue);
}

/* ── Party vitals strip ───────────────────────────────────────────── */
/* Compact all-PC-at-a-glance rows rendered at the top of the Live PC sidebar.
   Each row shows name + HP / AC / INIT / PP. Clicking drills into the full sheet. */
.lv-vitals-strip {
  display: flex; flex-direction: column;
  gap: var(--space-1, 4px);
  padding: var(--space-2, 8px) 0;
}
.lv-vitals-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2, 8px);
  padding: var(--space-2, 8px) var(--space-3, 12px);
  border: 1px solid var(--border);
  border-radius: var(--radius-md, 6px);
  background: var(--bg-2);
  cursor: pointer;
  transition: transform var(--t-hover, 120ms), border-color var(--t-hover, 120ms), box-shadow var(--t-hover, 120ms);
  /* Canonical card-hover recipe — matches .party-pc-card / .entity-row */
}
.lv-vitals-row:hover, .lv-vitals-row:focus-visible {
  transform: translateY(-1px);
  border-color: rgba(47,154,163,0.45);
  box-shadow: 0 4px 12px rgba(0,0,0,0.12);
  outline: none;
}
.lv-vitals-name {
  flex: 1; min-width: 0;
  font-size: var(--font-size-sm, 0.85rem);
  font-weight: 600;
  color: var(--text-1);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lv-vitals-stats {
  display: flex; align-items: center;
  gap: var(--space-3, 12px);
  flex-shrink: 0;
}
.lv-vitals-stat {
  display: flex; flex-direction: column; align-items: center;
  gap: 1px;
  min-width: 2.2rem; text-align: center;
  font-size: var(--font-size-sm, 0.85rem);
  font-weight: 600;
  color: var(--text-1);
  line-height: 1.2;
}
.lv-vitals-label {
  font-size: var(--font-size-xs, 0.72rem);
  font-weight: 700;
  color: var(--color-text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  line-height: 1;
}
/* Back-to-vitals affordance shown at the top of the full-sheet drill-down */
.lv-vitals-back-btn {
  display: block;
  width: 100%;
  margin-bottom: var(--space-2, 8px);
  text-align: left;
}

/* Subtle recording dot in the live header */
.live-header-rec-dot {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--accent-red);
  animation: rec-pulse 1.2s ease-in-out infinite;
  flex-shrink: 0;
}
.live-header-rec-dot.hidden { display: none; }

/* ── Party tab ───────────────────────────────────────────────────────────── */
/* v787: canonical eyebrow spacing — matches .sessions-toolbar / Codex /
   .quest-nav-toolbar / .map-toolbar / .manage-nav-col.
   v1147: replaced max-width + auto margins with a viewport-aware
   padding-inline calc so the panel stays full-width (scrollbar pins to
   the viewport edge) but the CONTENT centers within a 1280px column on
   wide screens. The previous max-width approach centered the panel
   itself, which dragged the scrollbar inward with the centered box —
   user pointed out it should sit at the right edge of the screen. */
.party-tab-panel {
  padding-top: var(--space-5);
  padding-bottom: var(--space-5);
  padding-left: max(var(--space-4), calc((100% - 1280px) / 2));
  padding-right: max(var(--space-4), calc((100% - 1280px) / 2));
  overflow-y: auto;
  height: calc(100vh - var(--chrome-height));
}
/* Action bar above the legend (New PC button, etc.) */
.party-tab-actions {
  display: flex; align-items: center; justify-content: flex-end;
  padding-bottom: 0.5rem;
}

/* Portrait upload affordance (GM only) */
.party-pc-portrait--editable {
  cursor: pointer;
  position: relative;
}
.party-pc-portrait--editable::after {
  content: '📷';
  position: absolute; inset: 0;
  background: rgba(0, 0, 0, 0.5);
  color: var(--color-white);
  font-size: 1.4rem;
  display: flex; align-items: center; justify-content: center;
  opacity: 0;
  transition: opacity var(--t-hover);
  pointer-events: none;
}
.party-pc-portrait--editable:hover::after, .party-pc-portrait--editable:focus-within::after { opacity: 1; }
.party-legend {
  display: flex; gap: 1.25rem; align-items: center;
  padding: 0.35rem 0.75rem;
  background: var(--bg-2); border: 1px solid var(--border); border-radius: var(--radius-md);
  font-size: var(--font-size-xs); color: var(--text-2);
  margin-bottom: 0; /* gap comes from party-cards-grid margin-top */
}
/* Inline variant — sits in the section header row between the label and
   the action buttons. Drops the chip-like card chrome (bg/border/radius)
   so the swatches read as inline metadata, not a separate panel. Fills
   the middle space and wraps gracefully on narrow widths. */
.party-legend-inline {
  flex: 1 1 auto;
  flex-wrap: wrap;
  background: none;
  border: none;
  padding: 0;
  gap: var(--space-3);
  margin: 0;
}
.pleg-actions { display: flex; gap: 0.4rem; margin-left: auto; align-items: center; }
.pleg-actions .btn-xs { white-space: nowrap; }
.pleg-item { display: flex; align-items: center; gap: 0.35rem; white-space: nowrap; }
.pleg-swatch {
  width: 11px; height: 11px; border-radius: var(--radius-sm);
  border: 2px solid transparent; flex-shrink: 0;
}
/* Swatches share a single semantic palette with the ability cells below
   (.ddb-ab-best / .ddb-ab-near-best) so the legend is a 1:1 visual key.
   Token-driven via color-mix; no raw hex. */
.pleg-swatch--best   { border-color: var(--accent-green);  background: color-mix(in srgb, var(--accent-green)  18%, transparent); }
.pleg-swatch--near   { border-color: color-mix(in srgb, var(--accent-green) 60%, var(--accent-amber)); background: color-mix(in srgb, var(--accent-green) 30%, var(--accent-amber) 12%); }
.pleg-swatch--unique { border-color: var(--color-gold);    background: color-mix(in srgb, var(--color-gold)    14%, transparent); }
.pleg-swatch--fav    { border-color: var(--accent-purple); background: color-mix(in srgb, var(--accent-purple) 14%, transparent); }
.pleg-swatch--adv {
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--accent-blue); color: var(--color-white);
  border-radius: var(--radius-sm); font-size: 0.55rem; font-weight: 700;
  width: 11px; height: 11px; border: none; flex-shrink: 0;
}
.party-cards-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(520px, 1fr));
  gap: 1rem;
  margin-top: 1rem;
}
/* v1150: Party TL;DR mode — shrink the card grid min-width so more cards
   fit per row, and hide the .party-pc-extras wrapper inside each PC
   card (skills, items, spells, feats, quotes, etc.) — collapsing the
   card to portrait + name + class + HP/AC/INIT/SPEED + passives + main
   abilities. */
.party-cards-grid--tldr {
  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
}
.party-cards-grid--tldr .party-pc-extras { display: none; }
/* v1154: in TL;DR mode the portrait shrinks 200→140 wide so the stats
   column has horizontal room for the ability grid without overflow.
   No fixed height — portrait keeps stretching to match the card's
   stats-driven height. */
.party-cards-grid--tldr .party-pc-portrait {
  width: 140px;
}
/* v1154: the 6-cell main ability grid (STR/DEX/CON/INT/WIS/CHA) was
   rolling off the right edge in narrow TL;DR cards. Switching to a
   3-col grid (renders 6 abilities as 3×2) gives each cell ~60px —
   plenty for "STR +5" with the score below. The 3-cell passives grid
   (Pass. Perc / Inv / Ins) already lives in 3 columns; this rule
   matches it. */
.party-cards-grid--tldr .ddb-ability-grid {
  grid-template-columns: repeat(3, 1fr);
}
/* v1152: additional TL;DR-mode hides per user direction — collapse the
   card to the essentials by hiding visual noise that doesn't drive
   gameplay decisions at a glance:
     • Import / Edit button TEXT (keep the buttons + icons)
     • Species / Background line (.ddb-background)
     • Subclass parenthetical inside class line
     • Combat / Non-combat role badges
     • DDB sync date (other AI / Sheet provenance badges stay so user
       still knows stats are estimates)
     • Pill section spacers (the 6px vertical gaps between sections)
   The class hooks stay on the markup unconditionally; CSS toggles them. */
.party-cards-grid--tldr .import-sheet-btn .btn-label,
.party-cards-grid--tldr .edit-stats-btn .btn-label { display: none; }
.party-cards-grid--tldr .ddb-background { display: none; }
.party-cards-grid--tldr .ddb-class-subclass { display: none; }
.party-cards-grid--tldr .pc-role-badge,
.party-cards-grid--tldr .pc-noncombat-badge { display: none; }
.party-cards-grid--tldr .pc-ai-badge--ddb { display: none; }
.party-cards-grid--tldr .pill-section-gap { display: none; }
/* The Accomplishments card and inventory/treasury card span the full
   width regardless of TL;DR mode (grid-column: 1 / -1 from .ktd-card),
   so they're unaffected by the narrower column tracks. */
/* v1150: Party mode toggle — small ghost-button pair, active state
   teal-tinted. Lives in the .party-section-hdr-actions cluster alongside
   "+ New PC" / "Import Character". Same recipe as .ktd-mode-toggle. */
.party-mode-toggle {
  display: inline-flex;
  gap: 4px;
  margin-right: var(--space-2);
}
.party-mode-toggle .party-mode-btn.active {
  background: color-mix(in srgb, var(--accent-teal) 14%, transparent);
  color: var(--accent-teal);
  border-color: color-mix(in srgb, var(--accent-teal) 40%, transparent);
}
/* v1087: chassis (bg, border, radius, hover, lifecycle) now from composing
   .tf-card --md + .is-dead / .is-departed. .party-pc-card keeps the
   flex-row layout for portrait | stats split + zeros outer padding so
   the inner .party-pc-stats owns its padding. */
.party-pc-card {
  display: flex;
  flex-direction: row;
  align-items: stretch;
  overflow: hidden;
}
.tf-card.party-pc-card { padding: 0; }
/* v1150: the Accomplishments card composes .party-pc-card (for the grid
   span + watermark) but it's NOT a PC card — it wants its own internal
   padding so the content doesn't sit edge-to-edge against the box
   border. Higher specificity than the .tf-card.party-pc-card rule
   above so it wins. */
.tf-card.party-pc-card.ktd-card { padding: var(--space-6); }
/* v1087: .party-pc-card--dead / --departed visual treatment now from spec
   .tf-card.is-dead / .is-departed composition. Legacy variant classes
   remain in JS markup for backward compat but have no CSS effect. */
/* Party-tab section header rows. Two flavours, both compose with
   .form-section-label for typography (canonical eyebrow):
     • .party-section-label — standalone label, full grid width
     • .party-section-hdr   — eyebrow LEFT + paired CTA RIGHT
   The previous .fallen-section-hdr re-implemented the eyebrow recipe and
   added a forbidden border-top divider; both retired in v743. */
/* v789: flex + min-height matches the effective vertical extent of canonical
   button-row eyebrow toolbars (.sessions-toolbar / .compendium-section-hdr /
   .quest-nav-toolbar / .map-toolbar). Without this, a bare standalone
   .party-section-label sits ~0.5rem higher than every other tab's eyebrow,
   because peers center their eyebrow text in a flex row sized by the button
   companions — bare spans don't get that lift. */
/* v1147: dropped min-height: 2rem and trimmed margin-bottom. The 2rem
   was reserving CTA-button space — irrelevant when the label has no
   button next to it. The eyebrow now hugs its card without a 32px dead
   strip below it. The CTA variant (.party-section-hdr-row, used for
   labels WITH buttons) keeps its sizing via its own rule below. */
.party-section-label {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  margin-top: var(--space-3);
  margin-bottom: var(--space-1);
}
.party-section-label:first-child { margin-top: 0; }
.party-section-hdr {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
  margin-bottom: var(--space-2);
}
.party-section-hdr:first-child { margin-top: 0; }
/* Inline-edit affordance for the party name (v925) — same recipe
   as .session-title-edit-btn / .session-title-input. ✎ swaps to
   input; blur or Enter saves; Esc cancels. */
/* v945: rename-party affordance lives on the Party Assessment eyebrow.
   .party-section-hdr-row is the flex row holding the eyebrow LEFT + the
   rename trigger RIGHT. Two visual states for the trigger: when the
   campaign uses the default party name, render a full text-labeled
   ghost-pill ("✎ Rename Party") for discoverability; once the GM has
   set a custom name, collapse to a tiny pencil-icon circle button via
   .party-rename-btn--icon. */
.party-section-hdr-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  /* margin-bottom matches the previous standalone .party-section-label
     so the row → content gap is unchanged. */
  margin-bottom: var(--space-2);
}
.party-section-hdr-row .party-section-label { margin: 0; }
.party-rename-btn--icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.6rem;
  height: 1.6rem;
  padding: 0;
  border-radius: 50%;
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-3);
  font-size: var(--font-size-sm);
  line-height: 1;
  cursor: pointer;
  transition:
    color        var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease),
    background   var(--t-hover) var(--ease),
    transform    var(--t-hover) var(--ease);
}
.party-rename-btn--icon:hover,
.party-rename-btn--icon:focus-visible {
  color: var(--accent-teal);
  border-color: color-mix(in srgb, var(--accent-teal) 50%, var(--border));
  background: color-mix(in srgb, var(--accent-teal) 8%, transparent);
  outline: none;
  transform: translateY(-1px);
}
.party-rename-btn--icon:active {
  transform: translateY(0);
  transition-duration: var(--t-press);
}
/* Edit state — input + Save + Cancel inline. The "{Name} Assessment"
   eyebrow text is replaced with just "⚔️" + input so the input has
   room to breathe. */
.party-section-hdr-row--editing {
  flex-wrap: wrap;
}
.party-name-input {
  font: inherit;
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-1);
  background: var(--bg-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 2px var(--space-2);
  min-width: 8rem;
  flex: 1 1 auto;
}
.party-name-input:focus {
  outline: none;
  border-color: var(--accent-teal);
}
.party-section-hdr-actions {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  flex-wrap: wrap;
}
.party-section-hdr-actions .btn-xs { white-space: nowrap; }
/* PC card top-right action button cluster — Refresh / AI / Import / Edit /
   skull/leave/rejoin / Treasury controls. Replaces a verbatim inline
   flex-stack that used to repeat 4× across renderPartyCard,
   renderPartyInventoryCard, and the no-DDB-data branch. Children get
   white-space: nowrap so multi-word labels never break mid-button. */
.party-pc-actions {
  display: flex;
  gap: 4px;
  align-items: flex-start;
  flex-shrink: 0;
  flex-wrap: wrap;
  justify-content: flex-end;
}
.party-pc-actions > * { white-space: nowrap; }
/* Right-side cluster of the main nav: campaign name + Live pill share one
   flex row so they're vertically centered relative to each other (and to the
   nav as a whole), regardless of how many lines the campaign name takes.
   v1125: nav uses align-items: center, so this cluster naturally floats at
   vertical center — dropped the legacy margin-bottom: 6px anchor. The
   .has-cover override still restores stretch + edge-bleed for the campaign
   cover banner. */
.nav-right {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  padding: 0;
}
/* With a cover image, the cluster takes a fixed card-sized width and paints the
   cover via background-image so it never affects nav height. Same dark gradient
   tint as campaign cards keeps the name and Live pill readable. v1122:
   margin-right: -16px negates the nav's new padding-right so the cover
   image bleeds to the screen edge as designed. */
.nav-right.has-cover {
  align-self: stretch;
  margin-bottom: -1px;
  margin-right: -16px;
  width: clamp(240px, 28vw, 360px);
  justify-content: flex-end;
  background-image:
    linear-gradient(180deg, rgba(11,15,20,0.78) 0%, rgba(11,15,20,0.55) 50%, rgba(11,15,20,0.82) 100%),
    var(--cover-img, linear-gradient(transparent, transparent));
  background-size: auto, cover;
  background-repeat: no-repeat, no-repeat;
  background-position: center, center;
  padding: 0 0.45rem;
}
/* Brighter campaign-name on the cover so it pops against the image */
.nav-right.has-cover .nav-campaign-name { color: rgba(255,255,255,0.92); }
/* v801: light-theme inverse — same parchment wash as .campaign-card--has-cover
   so the nav banner reads consistently with the campaign cards. White text
   is invisible against the bright wash, so flip nav-campaign-name to ink. */
html:not([data-theme="dark"]) .nav-right.has-cover {
  background-image:
    linear-gradient(180deg, rgba(252,247,237,0.70) 0%, rgba(252,247,237,0.48) 50%, rgba(252,247,237,0.74) 100%),
    var(--cover-img, linear-gradient(transparent, transparent));
}
html:not([data-theme="dark"]) .nav-right.has-cover .nav-campaign-name {
  color: var(--text);
}
/* Campaign name to the left of the Live pill.
   v1122: aligned to spec .tf-topnav-campaign — display serif at 14px / 500
   weight / --text-3, with an 8px-padded left rule that separates it from the
   utility cluster. The eyebrow uppercase recipe is preserved only for the
   .has-cover overlay (white-on-image needs the heavier letter-spacing to
   stay legible). Wraps to a second line if needed; only truncates past two. */
.nav-campaign-name {
  font-family: var(--type-display);
  font-size: 14px;
  font-weight: 500;
  color: var(--text-3);
  line-height: 1.3;
  text-align: right;
  padding: 0 8px;
  margin: 0;
  border-left: 1px solid var(--border);
}
/* Cover-image override — eyebrow uppercase reads better on the busy backdrop. */
.nav-right.has-cover .nav-campaign-name {
  font-family: inherit;
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  line-height: 1.2;
  padding: 0;
  margin: 0 0.6rem 0 0.4rem;
  border-left: 0;
}
.nav-campaign-name .ncn-text {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  max-width: 22ch;
  word-break: break-word;
}
.nav-campaign-name:has(.ncn-text:empty) { display: none; }
/* (Retired in v743) .party-section-hdr-standalone existed to undo the
   forbidden border-top + top margin that the old .fallen-section-hdr
   class applied. Now that headers compose .form-section-label directly,
   no override is needed — first-child margin reset lives on
   .party-section-hdr / .party-section-label. */
/* ── Party Treasury Card ────────────────────────────────────────────────── */
.party-treasury-card { border-color: var(--border); }
.party-treasury-portrait { cursor: default; }
.party-treasury-portrait::after { display: none; }

.party-currency-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem 0.6rem;
  margin: 0.25rem 0;
}
.coin-group { display: inline-flex; align-items: baseline; gap: 0.2rem; }
/* v1093: entity descriptor snippet — composes spec .tf-card-sub recipe
   (display-serif italic, dim, ellipsis-on-overflow). Renders inline after
   .entity-name on Codex rows. Reads e.snippet (backend TBD) with fallback
   to truncated description. */
.entity-snippet {
  font-family: var(--type-display);
  font-style: italic;
  font-weight: 400;
  color: var(--text-3);
  margin-left: 6px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  flex: 1 1 auto;
}

/* v1093: .coin-badge family aligned to spec .tf-coin (composes .tf-chip-tinted
   chassis). Per-currency --chip-color sets fill/border/text. Mono digits +
   tabular numerals + dot prefix per spec. Pill silhouette replaces the
   previous radius-sm squared chip. */
.coin-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 8px;
  border-radius: 999px;
  font-family: var(--type-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0;
  color: var(--chip-color, var(--text-3));
  background: color-mix(in srgb, var(--chip-color, var(--text-3)) 12%, var(--bg));
  border: 1px solid color-mix(in srgb, var(--chip-color, var(--text-3)) 30%, transparent);
  font-variant-numeric: tabular-nums;
  line-height: 1.45;
}
.coin-badge::before {
  content: '';
  width: 8px; height: 8px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
  opacity: 0.85;
}
[data-theme="dark"] .coin-badge {
  color: color-mix(in srgb, var(--chip-color, var(--text-3)) 35%, #fff);
  background: color-mix(in srgb, var(--chip-color, var(--text-3)) 28%, var(--bg-2));
  border-color: color-mix(in srgb, var(--chip-color, var(--text-3)) 42%, transparent);
}
.coin-label {
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--text-3);
}
.coin-pp { --chip-color: #4A6E8C; }
.coin-gp { --chip-color: #B89348; }
.coin-ep { --chip-color: #6F8A88; }
.coin-sp { --chip-color: #7A8492; }
.coin-cp { --chip-color: #B8773C; }
/* v798: coin chip colors are tuned for dark bg; on light theme darker
   variants preserve readability without breaking the dark treatment. */
html:not([data-theme="dark"]) .coin-pp { color: #475569; border-color: #94a3b8; }
html:not([data-theme="dark"]) .coin-gp { color: #6B5C3D; border-color: #6B5C3D; }
html:not([data-theme="dark"]) .coin-ep { color: var(--teal-text); border-color: var(--teal-text); }
html:not([data-theme="dark"]) .coin-sp { color: #475569; border-color: #64748b; }
html:not([data-theme="dark"]) .coin-cp { color: #8B5226; border-color: #8B5226; }
.coin-total {
  display: inline-flex; align-items: baseline; gap: 0.2rem;
  margin-left: 0.4rem; padding-left: 0.6rem;
  border-left: 1px solid var(--color-border);
}

/* Dead PC — compendium row */
.entity-row--dead .entity-name {
  color: var(--text-3);
  text-decoration: line-through;
  text-decoration-color: var(--text-3);
}
.entity-icon--dead {
  filter: grayscale(100%) brightness(0.45);
}
.entity-full-img--dead {
  filter: grayscale(100%) brightness(0.45);
}
/* v1084: aligned to spec .tf-badge--ghost. */
.pc-dead-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: var(--radius-full);
  border: 1px solid var(--border);
  font-size: 10.5px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--text-3);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 1px 5px;
  margin-left: 4px;
  vertical-align: middle;
}
.entity-row--inactive .entity-name {
  color: var(--text-3);
  font-style: italic;
}
.pc-left-badge {
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--accent-amber);
  background: color-mix(in srgb, var(--accent-amber) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent-amber) 40%, transparent);
  border-radius: var(--radius-sm);
  padding: 1px 5px;
  margin-left: 4px;
  vertical-align: middle;
}
/* v1118: party PC portrait aligned to spec .tf-pc-portrait — 200px wide
   column that fills full card height (was 160px square). Spec border-right
   + subtle ink-tinted fill so the placeholder reads cleanly. */
/* v1154 (corrected): stats column drives card height. Portrait fills the
   card vertically via align-self:stretch, but the inner <img> is now
   absolutely positioned so its intrinsic dimensions can't push the
   card taller. Net effect:
     • Card height = stats column's natural height (TL;DR shorter, Full
       taller).
     • Portrait div stretches to match (always full-card-height column).
     • Image fills the portrait box via object-fit: cover, cropped per
       the user's --portrait-x / --portrait-y crop anchors.
   My v1154-first-pass set an explicit height: 240px which made the
   PORTRAIT stretch the card; reverted. */
.party-pc-portrait {
  position: relative;
  flex-shrink: 0;
  width: 200px;
  align-self: stretch;
  background: color-mix(in srgb, var(--text-1) 6%, var(--bg));
  border-right: 1px solid var(--border);
  overflow: hidden;
}
[data-theme="dark"] .party-pc-portrait {
  background: color-mix(in srgb, var(--text-1) 12%, var(--bg));
  border-right-color: var(--border);
}
.party-pc-portrait img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--portrait-x, 50%) var(--portrait-y, 50%);
  display: block;
}

/* ✂ Crop button — appears on GM hover, bottom-right */
.portrait-crop-btn {
  position: absolute;
  bottom: 6px;
  right: 6px;
  width: 26px;
  height: 26px;
  border-radius: var(--radius-md);
  background: rgba(0,0,0,0.65);
  color: #fff;
  border: 1px solid rgba(255,255,255,0.2);
  font-size: var(--font-size-sm);
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--t-hover), background var(--t-hover);
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: center;
}
.party-pc-portrait--editable:hover .portrait-crop-btn, .party-pc-portrait--editable:focus-within .portrait-crop-btn { opacity: 1; }
.portrait-crop-btn:hover, .portrait-crop-btn:focus-visible { background: rgba(0,0,0,0.85); }

/* Crop mode — suppress camera overlay, change cursor */
.party-pc-portrait.portrait-cropping::after { opacity: 0 !important; pointer-events: none; }
.party-pc-portrait.portrait-cropping .portrait-crop-btn { display: none; }
.party-pc-portrait.portrait-cropping img {
  cursor: ew-resize;
  will-change: object-position;
  user-select: none;
  -webkit-user-drag: none;
}
.party-pc-portrait.portrait-cropping {
  outline: 2px solid var(--color-brand);
  outline-offset: -2px;
  touch-action: none;
}

/* Confirm / cancel bar shown during crop mode */
.portrait-crop-bar {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px 6px;
  background: rgba(0,0,0,0.72);
  z-index: 6;
}
.portrait-crop-bar.hidden { display: none; }
.portrait-crop-bar-hint {
  flex: 1;
  font-size: 0.62rem;
  color: rgba(255,255,255,0.65);
  white-space: nowrap;
}
.portrait-crop-confirm-btn,
.portrait-crop-cancel-btn {
  background: none;
  border: none;
  color: #fff;
  font-size: var(--font-size-md);
  line-height: 1;
  cursor: pointer;
  padding: 2px 5px;
  border-radius: var(--radius-sm);
  transition: background var(--t-hover);
}
.portrait-crop-confirm-btn:hover, .portrait-crop-confirm-btn:focus-visible { background: rgba(34,197,94,0.4); }
.portrait-crop-cancel-btn:hover, .portrait-crop-cancel-btn:focus-visible  { background: rgba(239,68,68,0.4); }
.party-pc-portrait-placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 3rem;
  background: var(--bg-3);
  /* Class-icon color injected via inline `style="--entity-color:..."`
     (canonical CSS-var injection pattern, replaces `style="color:..."`
     direct colour). Falls back to secondary text colour. */
  color: var(--entity-color, var(--text-2));
}
/* Unicolor entity glyph in the portrait placeholder (replaced the class emoji). */
.party-pc-portrait-glyph { width: 44%; height: 44%; }
/* v966: empty-grid CTA — left-aligned to match canonical inline-hint
   position (matches the Weave Fates "No threads selected yet…" line
   and every other empty/hint placeholder app-wide). Padding nudges
   the hint off the absolute tab edge for breathing room; buttons sit
   below the hint with the same left alignment. No centering. */
.party-empty-cta {
  padding: var(--space-2);
}
.party-empty-cta-actions {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-top: var(--space-3);
}
/* v1118: stats column aligned to spec .tf-pc-stats — 14px/16px/16px
   padding (was 0.75rem 1rem), 10px gap between sections. */
.party-pc-stats {
  flex: 1;
  padding: 14px 16px 16px;
  min-width: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* v1121: actions row pinned ABOVE the name/meta block so the name line
   never wraps around overflow buttons. .party-pc-actions--top is a flex
   row that wraps action buttons; .party-pc-name-row--stacked is the
   inline-baseline row below it carrying name + player + pronouns + bg. */
.party-pc-actions--top {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: flex-start;
  justify-content: flex-end;
}
.party-pc-name-row--stacked {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px 10px;
  justify-content: flex-start;
}
.party-pc-name-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}
/* v1118: PC name aligned to spec .tf-pc-name — 20px display-serif title
   (was 16px in v1113), tighter -0.012em tracking. Hover teal-2 tint stays.
   Button retains its click handler (opens PC's codex). */
.party-pc-name-btn {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  cursor: pointer;
  font-family: var(--type-display);
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.012em;
  color: var(--text-1);
  text-decoration: none;
  text-align: left;
  line-height: 1.1;
  transition: color 160ms ease;
}
.party-pc-name-btn:hover, .party-pc-name-btn:focus-visible {
  color: var(--teal-2);
}
.party-pc-no-data {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: 0.5rem;
  color: var(--text-3);
  font-size: var(--font-size-base);
}

/* ── Party tag pills (senses, languages, proficiencies, tools) ── */
/* Two-column table layout: fixed label col | flex pill col */
.party-tags-row {
  display: grid;
  grid-template-columns: 4.5rem 1fr;
  column-gap: 0.45rem;
  align-items: start;
}
/* Label cell — right-aligned, section-label style, | seam as right border */
.party-tags-row > .ddb-row-label {
  text-align: right;
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-3);
  white-space: nowrap;
  margin-right: 0;
  padding-top: 3px;         /* nudge to align with pill cap-height */
  padding-right: 0.3rem;
  border-right: 1px solid var(--split-color);
}
/* Pill cell */
.party-tags-wrap { display: flex; flex-wrap: wrap; gap: 3px; }
.pill-section-gap {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0.45rem 0;
  pointer-events: none;
  user-select: none;
}
.pill-section-gap::after {
  content: '';
  display: block;
  width: 45%;
  height: 1px;
  background: linear-gradient(
    to right,
    transparent,
    var(--border, rgba(255,255,255,0.12)) 20%,
    var(--border, rgba(255,255,255,0.12)) 80%,
    transparent
  );
}
.party-trait-tag,
.party-unique-tag {
  display: inline-block;
  font-size: var(--font-size-xs);
  padding: 1px 6px;
  border-radius: var(--radius-sm);
  border: 1px solid;
  line-height: 1.5;
  white-space: nowrap;
}
/* Normal tag — subtle */
.party-trait-tag {
  background: var(--bg-3);
  border-color: var(--border);
  color: var(--text-2);
}
/* Unique tag — amber/gold highlight */
.party-unique-tag {
  background: #ca8a0420;
  border-color: #ca8a04;
  color: var(--accent-gold);
  font-weight: 600;
}
/* Skill pills grid — v1148 aligned to spec .tf-pc-skills: 3-col grid (was
   6) so each pill has room for the full abbreviated skill name + value
   inline. Pill is now a row-flex layout (name LEFT, value RIGHT,
   space-between) instead of stacked column, reading like a properly-
   labelled stat instead of a square chip. */
.skill-pills-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px;
  margin-bottom: 0.25rem;
}
.skill-pills-grid .skill-pill {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 3px 8px;
  background: color-mix(in srgb, var(--text-1) 3%, var(--bg));
  border: 1px solid var(--border);
  border-radius: 4px;
  font-family: var(--type-ui);
  font-size: 11px;
  color: var(--text-2);
  transition: background var(--t-hover), border-color var(--t-hover);
  white-space: nowrap;
}
[data-theme="dark"] .skill-pills-grid .skill-pill {
  background: color-mix(in srgb, var(--text-1) 10%, var(--bg));
}
.skill-pill .sp-name {
  font-weight: 500;
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-transform: none;
  letter-spacing: 0;
  color: var(--text-2);
  font-size: 11px;
}
.skill-pill .sp-val {
  flex-shrink: 0;
  /* v1151: mono → sans for the same dotted-zero reason as .ddb-stat. */
  font-family: var(--type-ui);
  font-variant-numeric: tabular-nums;
  color: var(--text-1);
  font-weight: 600;
  font-size: 11px;
  display: inline-flex;
  align-items: baseline;
  gap: 3px;
}
/* Advantage badge — tiny teal-tinted chip on skills + initiative cells. */
.sp-adv {
  display: inline-block;
  background: var(--accent-teal);
  color: #fff;
  font-size: 8.5px;
  font-weight: 700;
  border-radius: 3px;
  padding: 0 3px;
  margin-left: 3px;
  vertical-align: 2px;
  letter-spacing: 0.06em;
  flex-shrink: 0;
}
/* Best or tied for best — dark green. Same token-driven palette as the
   .ddb-ab-best ability cells and the .pleg-swatch--best legend chip. */
.skill-pill.sp-best, .skill-pill.sp-tied {
  background: color-mix(in srgb, var(--accent-green) 18%, transparent);
  border-color: var(--accent-green);
}
.skill-pill.sp-best .sp-name, .skill-pill.sp-tied .sp-name { color: color-mix(in srgb, var(--accent-green) 80%, white); }
.skill-pill.sp-best .sp-val,  .skill-pill.sp-tied .sp-val  { color: var(--accent-green); }
/* Within 1 of best — yellow-green. */
.skill-pill.sp-near {
  background: color-mix(in srgb, var(--accent-green) 30%, var(--accent-amber) 12%);
  border-color: color-mix(in srgb, var(--accent-green) 60%, var(--accent-amber));
}
.skill-pill.sp-near .sp-name { color: color-mix(in srgb, var(--accent-green) 60%, var(--accent-amber)); }
.skill-pill.sp-near .sp-val  { color: color-mix(in srgb, var(--accent-green) 50%, var(--accent-amber)); }

/* ── Key Spells ──────────────────────────────────────────────────────────── */
.key-spells-section {
  margin-top: 0.4rem;
}
.key-spells-header {
  margin-bottom: 0.25rem;
}
.key-spells-body {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
.key-spell-pill {
  display: inline-flex; align-items: center; gap: 3px;
  background: var(--bg-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 1px 6px;
  font-size: var(--font-size-xs);
  color: var(--text-2);
  white-space: nowrap;
}
.key-spell-fav {
  border-color: #7c3aed;
  background: #4c1d9518;
  color: #7c3aed;
  font-weight: 600;
}
.spell-use-count {
  display: inline-flex; align-items: center; justify-content: center;
  background: #7c3aed; color: #fff;
  border-radius: var(--radius-lg); font-size: 0.58rem; font-weight: 700;
  padding: 0 4px; min-width: 14px; height: 13px;
}

/* ── Spell metadata pills ──────────────────────────────────────────────── */
.spell-pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
  margin-bottom: var(--space-2);
}
.spell-pill {
  display: inline-flex;
  align-items: center;
  padding: 0.2rem 0.55rem;
  border-radius: var(--radius-full);
  font-size: var(--font-size-xs);
  font-weight: 600;
  border: 1px solid transparent;
}
.spell-pill--level {
  background: color-mix(in srgb, var(--color-text-tertiary) 12%, transparent);
  border-color: color-mix(in srgb, var(--color-text-tertiary) 25%, transparent);
  color: var(--color-text-secondary);
}
.spell-pill--school {
  background: color-mix(in srgb, var(--spell-school-color) 12%, transparent);
  border-color: color-mix(in srgb, var(--spell-school-color) 30%, transparent);
  color: var(--spell-school-color);
}
.spell-pill--conc {
  background: color-mix(in srgb, var(--color-warning) 12%, transparent);
  border-color: color-mix(in srgb, var(--color-warning) 30%, transparent);
  color: var(--color-warning);
}
.spell-pill--ritual {
  background: color-mix(in srgb, var(--color-success) 12%, transparent);
  border-color: color-mix(in srgb, var(--color-success) 30%, transparent);
  color: var(--color-success);
}

/* ── Item rarity pills ─────────────────────────────────────────────────── */
.item-pills { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: var(--space-2); }
.item-pill {
  display: inline-flex; align-items: center;
  font-size: var(--font-size-xs); font-weight: 600;
  border-radius: var(--radius-full); padding: 0.12rem 0.5rem;
  border: 1px solid transparent;
}
.item-pill--common     { background: color-mix(in srgb, var(--color-text-tertiary) 12%, transparent); border-color: color-mix(in srgb, var(--color-text-tertiary) 25%, transparent); color: var(--color-text-secondary); }
.item-pill--uncommon   { background: color-mix(in srgb, var(--color-success) 12%, transparent);       border-color: color-mix(in srgb, var(--color-success) 30%, transparent);       color: var(--color-success); }
.item-pill--rare       { background: color-mix(in srgb, var(--color-info) 12%, transparent);          border-color: color-mix(in srgb, var(--color-info) 30%, transparent);          color: var(--color-info); }
.item-pill--very-rare  { background: color-mix(in srgb, var(--color-purple) 12%, transparent);        border-color: color-mix(in srgb, var(--color-purple) 30%, transparent);        color: var(--color-purple); }
.item-pill--legendary  { background: color-mix(in srgb, var(--color-gold) 12%, transparent);          border-color: color-mix(in srgb, var(--color-gold) 30%, transparent);          color: var(--color-gold); }
.item-pill--artifact   { background: color-mix(in srgb, var(--color-copper) 12%, transparent);        border-color: color-mix(in srgb, var(--color-copper) 30%, transparent);        color: var(--color-copper); }

/* ── Item rarity colours (text-only) ──────────────────────────────────────
   Applied alongside any element to tint just its text by the standard D&D
   rarity palette. Used by:
     • Codex item rows — adds a "Rare" / "Very Rare" / etc. badge next to
       the "Item" type tag (item type stays its canonical ember colour).
     • Party tab inventory tags — colours the item NAME inside the trait
       tag without changing the pill's background or border.
   Common / Mundane intentionally omitted (no tint applied). */
.item-rarity-uncommon  { color: var(--color-success); }
.item-rarity-rare      { color: var(--color-info); }
.item-rarity-very-rare { color: var(--color-purple); }
.item-rarity-legendary { color: var(--color-gold); }
.item-rarity-artifact  { color: var(--color-copper); }

/* PC Chronicle (.pc-chronicle-*) classes removed 2026-05-06: the
   collapsible per-session-blurbs UI was retired in favor of the always-
   comprehensive PC description (regenerated each recap from the same
   blurbs source). The .pc-quote-* family below stays — quotes still
   render in the entity-quotes panel via a separate component. */

/* Section headers inside PC entity descriptions ("## Header" lines from
   the chronicle prompt). Composes .session-beat-header for the typography
   (uppercase, weight 700, teal); this rule tightens the margin to fit
   inside the entity-card context. The session-beat version is sized for
   full-page session summaries with more breathing room. */
.entity-section-header {
  margin: 1rem 0 0.35rem;
}
.entity-desc .entity-section-header:first-child {
  margin-top: 0.25rem;
}

/* Quotes inside PC entity rows (kept) */
.pc-quote-row {
  margin-top: 0.35rem; padding-left: 0.6rem;
  border-left: 2px solid var(--border);
}
.pc-quote-text {
  font-size: var(--font-size-sm); font-style: italic; color: var(--text-2);
  margin: 0; quotes: none;
}
.pc-quote-ctx {
  display: block; font-size: var(--font-size-xs); color: var(--text-3);
  margin-top: 0.1rem;
}
/* Reassign quote button + inline picker */
.pc-quote-reassign-btn {
  font-size: var(--font-size-xs); padding: 1px 6px;
  margin-top: 3px; color: var(--text-3);
}
.pc-quote-reassign-btn:hover, .pc-quote-reassign-btn:focus-visible { color: var(--accent-teal); }
.pc-quote-reassign-picker {
  display: inline-flex; align-items: center; gap: 4px; margin-top: 3px;
}
.pc-quote-reassign-sel {
  font-size: var(--font-size-xs);
  background: var(--bg-2); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: 1px 4px;
}
.pc-quote-reassign-ok, .pc-quote-reassign-cancel {
  font-size: var(--font-size-xs); padding: 1px 6px;
}
.pc-quote-reassign-ok:not(:disabled) { color: var(--accent-teal); }
/* Quote action button group (edit / reassign / delete) */
.pc-quote-actions {
  display: inline-flex; align-items: center; gap: 2px; margin-top: 3px;
}
.pc-quote-edit-btn, .pc-quote-delete-btn {
  font-size: var(--font-size-xs); padding: 1px 5px; color: var(--text-3);
}
.pc-quote-edit-btn:hover, .pc-quote-edit-btn:focus-visible  { color: var(--accent-teal); }
.pc-quote-delete-btn:hover, .pc-quote-delete-btn:focus-visible { color: var(--accent-red); }
/* Inline quote edit form */
.pc-quote-edit-form {
  display: flex; flex-direction: column; gap: 4px; margin-top: 4px;
}
.pc-quote-edit-text {
  font-size: var(--font-size-sm); font-style: italic;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius-md); padding: 4px 6px;
  color: var(--text); resize: vertical; width: 100%; box-sizing: border-box;
}
.pc-quote-edit-ctx {
  font-size: var(--font-size-xs); background: var(--bg);
  border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: 3px 6px; color: var(--text-2); width: 100%; box-sizing: border-box;
}
.pc-quote-edit-actions {
  display: flex; gap: 4px;
}
.pc-quote-edit-save  { font-size: var(--font-size-xs); padding: 1px 6px; color: var(--accent-teal); }
.pc-quote-edit-cancel { font-size: var(--font-size-xs); padding: 1px 6px; }


/* ── Companion section (party cards & compendium) ────────────────────────── */
.pc-companions-section {
  margin-top: 0.55rem;
  border-top: 1px solid var(--border);
  padding-top: 0.45rem;
}
.pc-companions-hdr {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: 0.3rem;
}
.companion-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}
.companion-chip-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: var(--font-size-xs);
  padding: 2px 8px;
  background: var(--bg-3);
  border: 1px solid var(--accent-teal);
  border-radius: var(--radius-2xl);
  color: var(--accent-teal);
  cursor: pointer;
  white-space: nowrap;
}
.companion-chip-btn:hover, .companion-chip-btn:focus-visible {
  background: color-mix(in srgb, var(--accent-teal) 13%, transparent);
}
/* Unicolor companion glyph in the chip (replaced the paw emoji); teal = the
   companion type color. */
.companion-chip-glyph { width: 18px; height: 18px; color: var(--accent-teal); flex-shrink: 0; }
.companion-chip-img {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  object-fit: cover;
}
.companion-none {
  font-size: var(--font-size-xs);
  color: var(--text-3);
  font-style: italic;
}
.compendium-companions-section {
  margin: 0.4rem 0 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.pc-companion-of-badge {
  font-size: 0.65rem;
  font-weight: 600;
  background: color-mix(in srgb, var(--accent-teal) 13%, transparent);
  color: var(--accent-teal);
  border-radius: var(--radius-sm);
  padding: 1px 5px;
  margin-left: 4px;
  white-space: nowrap;
}

/* ── Merge modal ─────────────────────────────────────────────────────────── */
.merge-pair {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 0.25rem;
}
.merge-card {
  flex: 1;
  background: var(--bg-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 0.6rem 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.merge-card-a { border-color: var(--accent-teal); }
.merge-card-b { border-color: var(--border); }
.merge-card-label {
  font-size: var(--font-size-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
}
.merge-card-name {
  font-size: var(--font-size-md);
  font-weight: 500;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.merge-card-type { font-size: var(--font-size-xs); color: var(--text-3); }
.merge-card-placeholder { font-size: var(--font-size-base); color: var(--text-3); font-style: italic; }
.merge-arrow { font-size: 1.2rem; color: var(--text-3); flex-shrink: 0; user-select: none; }
.merge-search-results {
  /* Inline flow (not absolute) so the modal's overflow-y: auto doesn't clip it */
  position: relative;
  z-index: 10;
  background: var(--bg-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  margin-top: 0.25rem;
  max-height: 13.75rem;
  overflow-y: auto;
  box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.merge-search-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.45rem 0.75rem;
  cursor: pointer;
  gap: 0.5rem;
  transition: background var(--t-hover);
}
.merge-search-item:hover, .merge-search-item:focus-visible { background: var(--bg-2); }
.merge-search-name { font-size: var(--font-size-base); color: var(--text); }
.merge-search-type { font-size: var(--font-size-xs); font-weight: 500; flex-shrink: 0; }
/* v965: .merge-search-empty inherits canonical .placeholder typography. */
.merge-search-empty { padding: 0.5rem 0.75rem; }
.merge-survivor-opts {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin-top: 0.25rem;
}
.merge-survivor-opt {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  background: var(--bg-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 0.4rem 0.75rem;
  cursor: pointer;
  font-size: var(--font-size-base);
  color: var(--text);
  transition: border-color var(--t-hover), background var(--t-hover);
  flex: 1;
}
.merge-survivor-opt:has(input:checked) {
  border-color: var(--accent-teal);
  background: rgba(47,124,133,0.08);
}
.merge-survivor-opt input { accent-color: var(--accent-teal); }
.merge-survivor-hint { font-size: var(--font-size-sm); color: var(--text-3); margin-top: 0.35rem; }
/* Merge modal layout — replaces inline style="margin-top:..." patches that
   sprinkled raw rems through the markup. All distances snap to --space-*. */
.merge-modal-intro {
  color: var(--text-2);
  font-size: var(--font-size-sm);
  margin-bottom: var(--space-4);
}
.merge-search-row    { margin-top: var(--space-4); position: relative; }
.merge-survivor-row  { margin-top: var(--space-3); }
.merge-error         { margin-top: var(--space-2); }
.merge-actions       { margin-top: var(--space-4); }
.merge-loading       { justify-content: center; padding: var(--space-4) 0; }

/* ── Campaign selector ──────────────────────────────────────────────────────── */
#campaign-selector { overflow-y: auto; }

.campaign-selector-wrap {
  max-width: 960px;
  margin: 0 auto;
  padding: 0.35rem 1.25rem 2rem;
}
/* Bump container width at the 3-col breakpoint so each card has a sensible
   16:9 footprint instead of being squashed into 1/3 of 960px. */
@media (min-width: 1200px) {
  .campaign-selector-wrap { max-width: 1400px; }
}
.cs-actions-row {
  display: flex;
  align-items: center;
  gap: 0.55rem;
}
/* "+ New" pill — used on every tab toolbar's primary "create new thing" CTA
   (campaign, session, quest, PC, codex entity). White-outlined, calm at rest,
   lifts and glows on hover with the same kinetics as the campaign cards.
   Reserved for create-NEW actions; modal Save/Confirm buttons keep .btn-primary. */
/* v791: full-width modifier for the join-campaign Join button. Replaces
   inline style="width:100%;margin-top:0.25rem". */
.join-camp-submit-btn {
  display: flex;
  width: 100%;
  margin-top: 0.25rem;
  justify-content: center;
}

/* v798: .btn-new now follows the project rule — base rule is light-correct,
   [data-theme="dark"] adds the white-rgba ghost recipe. Pre-v798 the base
   used rgba(255,255,255,*) which made every Save / Add / Send button across
   the app invisible in light mode (~25 surfaces). Dual-selector list keeps
   submit-button specificity (button[type="submit"] = 0,0,1,1). */
.btn-new,
button.btn-new {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.55rem 1rem;
  font-family: inherit;
  font-size: 13px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.02em;
  color: var(--text-1);
  background: color-mix(in srgb, var(--text-1) 4%, transparent);
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  cursor: pointer;
  white-space: nowrap;
  transition:
    background   var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease),
    box-shadow   var(--t-hover) var(--ease),
    transform    var(--t-hover) var(--ease),
    color        var(--t-hover) var(--ease);
}
.btn-new:hover, .btn-new:focus-visible,
button.btn-new:hover, button.btn-new:focus-visible {
  color: var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 8%, transparent);
  border-color: color-mix(in srgb, var(--accent-teal) 50%, transparent);
  box-shadow: 0 0 14px 2px color-mix(in srgb, var(--accent-teal) 18%, transparent);
  transform: translateY(-1px);
}
.btn-new:active,
button.btn-new:active {
  transform: translateY(0);
  transition-duration: var(--t-press);
}
/* v1082: dark-mode .btn-new aligned to spec [data-theme="dark"] .tf-btn--primary —
   teal-tinted hover (not generic white). Spec is the design language. */
[data-theme="dark"] .btn-new,
[data-theme="dark"] button.btn-new {
  color: var(--text-1);
  background: color-mix(in srgb, var(--text-1) 8%, transparent);
  border-color: rgba(255,255,255,0.18);
}
[data-theme="dark"] .btn-new:hover, [data-theme="dark"] .btn-new:focus-visible,
[data-theme="dark"] button.btn-new:hover, [data-theme="dark"] button.btn-new:focus-visible {
  color: #fff;
  background: color-mix(in srgb, var(--accent-teal) 20%, transparent);
  border-color: var(--accent-teal);
  box-shadow: 0 0 14px 2px color-mix(in srgb, var(--accent-teal) 30%, transparent);
}
/* v813: legacy `.btn-new--danger` (pill, fills red on hover) retired —
   canonical squared red-ghost recipe lives in the early btn block (~line 487).
   `.btn-new.btn-new--danger` now renders identical to `.btn-ghost.btn-ghost--danger`
   so every Delete / Remove / warning button across card actions, modals,
   roster, and map popups looks the same. */

/* ── Global Sort / Filter icon-buttons ────────────────────────────────────
   One canonical look for "sort" and "filter" triggers across the whole app.
   Same shape (rectangular ghost, mirroring the campaign page's "Sort:" pill
   in its original .btn-ghost.btn-sm form) and same kinetics — only the
   leading symbol differs.
     • .btn-sort   → Campaign sort, Codex sort
     • .btn-filter → Codex In-Play / All-Entries toggle
   The icon is rendered via a mask-image ::before so it inherits currentColor
   for free (active states, hover states, role variants all just work). */
.btn-sort,
.btn-filter {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 5px 12px;
  font-family: inherit;
  font-size: 12px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.02em;
  color: var(--text-2);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 0;
  cursor: pointer;
  white-space: nowrap;
  transition:
    background   var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease),
    color        var(--t-hover) var(--ease);
}
.btn-sort:hover, .btn-sort:focus-visible,
.btn-filter:hover, .btn-filter:focus-visible {
  color: var(--text-1);
  background: color-mix(in srgb, var(--text-1) 3%, transparent);
  border-color: color-mix(in srgb, var(--text-1) 35%, var(--border));
}
.btn-sort::before,
.btn-filter::before {
  content: '';
  display: inline-block;
  width: 14px;
  height: 14px;
  background-color: currentColor;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: contain;
          mask-size: contain;
  flex-shrink: 0;
}
/* Sort icon — three descending horizontal lines (sort hierarchy) */
.btn-sort::before {
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round'><line x1='3' y1='6' x2='21' y2='6'/><line x1='6' y1='12' x2='18' y2='12'/><line x1='9' y1='18' x2='15' y2='18'/></svg>");
          mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round'><line x1='3' y1='6' x2='21' y2='6'/><line x1='6' y1='12' x2='18' y2='12'/><line x1='9' y1='18' x2='15' y2='18'/></svg>");
}
/* Filter icon — funnel/hopper */
.btn-filter::before {
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linejoin='round' stroke-linecap='round'><polygon points='22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3'/></svg>");
          mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linejoin='round' stroke-linecap='round'><polygon points='22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3'/></svg>");
}
/* Active variants — teal counterpart of the rectangular ghost. Subtle so
   it reads as "this filter is on" without competing with the page content.
   v1082: aligned to spec .tf-icon-btn.is-active (color teal-2 / bg teal-3 12%
   / border teal-3 60% over rule). */
.btn-sort.active,
.btn-filter.active {
  color: var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 12%, transparent);
  border-color: color-mix(in srgb, var(--accent-teal) 60%, var(--border));
}
.btn-sort.active:hover, .btn-sort.active:focus-visible,
.btn-filter.active:hover, .btn-filter.active:focus-visible {
  color: var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 18%, transparent);
  border-color: color-mix(in srgb, var(--accent-teal) 75%, var(--border));
}
/* v1083: dark-mode .btn-sort / .btn-filter aligned to spec
   [data-theme="dark"] .tf-icon-btn — hover deepens ink wash + brightens
   text; active uses #BFE7EA cooler highlight + full teal border. */
[data-theme="dark"] .btn-sort,
[data-theme="dark"] .btn-filter {
  color: var(--text-2);
  border-color: var(--border);
}
[data-theme="dark"] .btn-sort:hover, [data-theme="dark"] .btn-sort:focus-visible,
[data-theme="dark"] .btn-filter:hover, [data-theme="dark"] .btn-filter:focus-visible {
  color: #fff;
  background: color-mix(in srgb, var(--text-1) 30%, transparent);
  border-color: var(--text-3);
}
[data-theme="dark"] .btn-sort.active,
[data-theme="dark"] .btn-filter.active {
  color: #BFE7EA;
  background: color-mix(in srgb, var(--accent-teal) 20%, transparent);
  border-color: var(--accent-teal);
}
[data-theme="dark"] .btn-sort.active:hover, [data-theme="dark"] .btn-sort.active:focus-visible,
[data-theme="dark"] .btn-filter.active:hover, [data-theme="dark"] .btn-filter.active:focus-visible {
  color: #DFF3F5;
  background: color-mix(in srgb, var(--accent-teal) 30%, transparent);
  border-color: var(--accent-teal);
}

.campaign-selector-hdr {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.6rem;
  gap: 1rem;
  flex-wrap: wrap;
}
/* v1092: PNG-era .campaign-selector-logo styles stripped — the SVG
   .tf-lockup (added v1071) handles sizing, centering, and color via
   currentColor in both themes. Filter drop-shadow and the legacy
   translateX(-27px) wordmark alignment hack were workarounds for the
   bitmap, no longer needed. */
.campaign-selector-logo {
  display: block;
  width: 100%;
  max-width: 300px;
  height: auto;
  margin: 0 auto;
}
/* (v787) .campaign-selector-title retired — eyebrow now composes
   .form-section-label like every in-tab eyebrow. Text changed from
   "Your Campaigns" to "Select Campaign" to match the verb-led pattern. */


/* Wrapper for the 2-col divider line — ::before on a grid container
   would be treated as a grid item, so this wrapper owns the pseudo-element */
.campaign-grid-wrap {
  position: relative;
}
/* The | dividers between columns. 2-col: one at 50% (matches Thread|fall brand mark).
   3-col (≥1200px): two at 33.33% and 66.66%. Both lines use the same gradient
   for consistency across breakpoints. The gradient fades top/bottom so the lines
   feel atmospheric rather than ruled. */
.campaign-grid-wrap::before,
.campaign-grid-wrap::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: linear-gradient(
    to bottom,
    transparent        0%,
    rgba(210,225,248,0.10)  6%,
    rgba(210,225,248,0.32) 25%,
    rgba(210,225,248,0.42) 50%,
    rgba(210,225,248,0.32) 75%,
    rgba(210,225,248,0.10) 94%,
    transparent       100%
  );
  pointer-events: none;
  z-index: 0;
}
.campaign-grid-wrap::before { left: calc(50% - 0.5px); }
/* ::after only used at the 3-col breakpoint */
.campaign-grid-wrap::after { display: none; }
@media (min-width: 1200px) {
  /* Gap-aware positioning: with 3 equal columns and a 1.5rem (24px) gap, the
     true gap centers are NOT at 33.333% / 66.666% — they're offset 4px toward
     each other (gap-width / 6 ≈ 0.25rem). Without this nudge the dividers
     sit slightly inside the middle column instead of cleanly between columns. */
  .campaign-grid-wrap::before { left: calc(33.333% - 4.5px); }
  .campaign-grid-wrap::after  { display: block; left: calc(66.666% + 3.5px); }
}
.campaign-list-screen {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.25rem 1.5rem;
}
/* Wide-screen density: at ≥1200px viewport the selector container has room for
   3 cards per row without making them too small to read. */
@media (min-width: 1200px) {
  .campaign-list-screen { grid-template-columns: repeat(3, 1fr); }
}
/* Group eyebrow: spans the full grid row so cards below it flow underneath the
   header naturally. Only emitted when sorting by Last Session AND both buckets
   have cards — see _renderCampaignCards. */
.cs-group-eyebrow {
  grid-column: 1 / -1;
  margin-top: 0.5rem;
}
.cs-group-eyebrow:first-child { margin-top: 0; }
/* Skeleton placeholder card — same dimensions as real cards, shimmer animation. */
.campaign-card--skeleton {
  cursor: default;
  pointer-events: none;
  background: linear-gradient(90deg, var(--bg-2) 0%, var(--bg-3) 50%, var(--bg-2) 100%);
  background-size: 200% 100%;
  animation: skel-shimmer 1.4s ease-in-out infinite;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.9rem;
}
.campaign-card--skeleton::before, .campaign-card--skeleton::after { content: none; }
.campaign-card--skeleton .skel-line {
  display: block;
  height: 0.7rem;
  border-radius: var(--radius-sm);
  background: rgba(255,255,255,0.08);
}
.campaign-card--skeleton .skel-line--name { width: 60%; height: 0.95rem; }
.campaign-card--skeleton .skel-line--gm   { width: 45%; height: 0.7rem; }
.campaign-card--skeleton .skel-line--meta { width: 70%; height: 0.6rem; }
@keyframes skel-shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
.campaign-card {
  display: flex;
  flex-direction: column;
  background-color: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  padding: 0.7rem 0.9rem;
  gap: 0.3rem;
  transition: border-color var(--t-hover) ease, box-shadow var(--t-hover) ease, transform var(--t-hover) ease;
  box-shadow: 0 2px 6px rgba(0,0,0,0.08);
  /* Match the 16:9 aspect ratio of cover images so the art fills without cropping. */
  aspect-ratio: 16 / 9;
  /* Whole card is clickable — wired in JS. Without this affordance users only
     discover the small Enter button. */
  cursor: pointer;
  /* Footer + role-badge are absolute-positioned children — establish containing block. */
  position: relative;
}
.campaign-card:hover, .campaign-card:focus-within {
  border-color: rgba(47,154,163,0.45);
  box-shadow: 0 4px 12px rgba(0,0,0,0.12);
  transform: translateY(-1px);
}
.campaign-card-info {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  /* Reserve space at the bottom for the absolute-positioned footer (chevron + gear). */
  padding-right: 60px;
  padding-bottom: 28px;
}
.campaign-card-name {
  font-size: var(--font-size-md);
  font-weight: 600;
  color: var(--text);
  /* Allow up to 2 lines so long campaign names don't truncate awkwardly. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.25;
  word-break: break-word;
}
.campaign-card-gm {
  font-size: var(--font-size-sm);
  color: var(--text-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.campaign-card-meta {
  font-size: var(--font-size-xs);
  color: var(--text-3);
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  margin-top: 0.5rem;
  width: fit-content;
}
.campaign-card-sessions {
  color: var(--text-2);
  font-weight: var(--font-weight-semibold);
}
/* PC name chips — 3-col grid, 2 rows for ≤6 PCs, 3 rows when more. Names that
   overflow a cell truncate with ellipsis. Constrained width keeps the right
   center of the cover image visible. */
.campaign-card-pcs {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 4px;
  margin-top: 2px;
  /* 2/3 of the card-info content area — leaves the right side of the cover open. */
  width: 66%;
}
/* v798: theme-correct base + dark override. */
.campaign-card-pc {
  font-size: var(--font-size-xs);
  color: var(--text-2);
  background: color-mix(in srgb, var(--text-1) 5%, transparent);
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  padding: 1px 0.5rem;
  line-height: 1.4;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-align: center;
}
.campaign-card-pc--more {
  color: var(--text-3);
  background: transparent;
  border-color: var(--border);
  font-weight: var(--font-weight-semibold);
}
[data-theme="dark"] .campaign-card-pc {
  background: rgba(255,255,255,0.06);
  border-color: rgba(255,255,255,0.10);
}
[data-theme="dark"] .campaign-card-pc--more { border-color: rgba(255,255,255,0.18); }
/* v799: footer container overlays the 70×70 watermark sigil at the
   bottom-right of the card. The chevron sits flex-centered inside, which
   places it on top of the diamond (the sigil's central path).
   Watermark is at `right 6px bottom 6px` size 70×70 (line ~8014); we
   match those coordinates exactly so the chevron tracks the diamond.
   pointer-events: none lets card-level clicks fall through to the
   card's own handler — the chevron is purely a visual cue. */
.campaign-card-footer {
  position: absolute;
  bottom: 6px;
  right: 6px;
  width: 70px;
  height: 70px;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
/* Role badge anchored to the top-right corner — an at-a-glance signal of "what am I in this campaign?" */
.campaign-card-role {
  position: absolute;
  top: 0.6rem;
  right: 0.7rem;
  margin-left: 0;
  z-index: 1;
}
/* Live Now badge — promoted to top-left when present so it reads as a status
   chip rather than another line of meta text inside the info stack. */
/* v786: live badge moved to the LOWER-left so it never overlaps the title
   (the top-left placement collided with .campaign-card-name's first line
   on most cards). The bottom-left slot sits in the same horizontal band
   as the chevron (bottom-right) inside the .campaign-card-info's reserved
   `padding-bottom: 28px`, so it doesn't fight any other content. */
.campaign-card .campaign-live-badge {
  position: absolute;
  bottom: 0.55rem;
  left: 0.8rem;
  margin-top: 0;
  background: rgba(11,15,20,0.55);
  border: 1px solid color-mix(in srgb, var(--color-gold) 38%, transparent);
  border-radius: var(--radius-full);
  padding: 2px 0.55rem;
  z-index: 1;
}
/* v799: chevron is now sized to fill the watermark's diamond path. At rest
   it's muted gold so it reads as part of the sigil; on hover it lights up
   (full saturation + halo glow), scales up, and slides right — the same
   "telegraph the click target" intent, dramatized to match the new size. */
.campaign-enter-chevron {
  flex-shrink: 0;
  font-size: 3.6rem;
  line-height: 1;
  color: var(--color-gold);
  opacity: 0.55;
  /* The `›` glyph (U+203A) carries asymmetric side-bearings in the system
     sans stack — the visible apex sits in the right half of its advance
     box, so flex-centering alone parks the visual mass right of the
     diamond's true center. Pull LEFT a hair to land the apex on (50,50)
     of the watermark sigil. The translateY(-1px) compensates for the
     glyph's baseline-relative vertical bias (sits a touch low at line-height: 1). */
  transform: translate(14px, -3px);
  transition:
    color var(--t-hover) var(--ease),
    opacity var(--t-hover) var(--ease),
    transform var(--t-hover) var(--ease),
    text-shadow var(--t-hover) var(--ease),
    filter var(--t-hover) var(--ease);
  user-select: none;
}
.campaign-card:hover .campaign-enter-chevron,
.campaign-card:focus-within .campaign-enter-chevron {
  opacity: 1;
  color: var(--color-gold);
  /* Hover shift kept inside the diamond's right vertex — a 5px slide from
     the rest position telegraphs the click without the chevron escaping
     the sigil. Scale stays gentle for the same reason. */
  transform: translate(19px, -3px) scale(1.1);
  text-shadow:
    0 0 12px color-mix(in srgb, var(--color-gold) 60%, transparent),
    0 0 24px color-mix(in srgb, var(--color-gold) 30%, transparent);
  filter: drop-shadow(0 1px 2px rgba(0,0,0,0.25));
}
/* Light theme — slightly less translucent at rest so the chevron reads
   against the bright card surface without losing its "part of sigil" feel. */
html:not([data-theme="dark"]) .campaign-enter-chevron { opacity: 0.7; }
/* Settings gear — pinned to the top-right corner directly underneath the
   role badge, so it sits in the corner-stack with role badge above and is
   never crowded by long PC chip rows along the bottom. Explicit 28×28
   touch target keeps it reliably hittable on mobile. */
.camp-settings-btn {
  position: absolute;
  top: 2.1rem;
  right: 0.7rem;
  z-index: 1;
  width: 28px;
  height: 28px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

/* ── Compendium AI Sync button ───────────────────────────────────────────────── */
.compendium-progress {
  font-size: var(--font-size-xs);
  color: var(--text-3);
  margin-top: 3px;
}
.compendium-progress.hidden { display: none; }

.entry-compendium-btn {
  color: var(--color-warning);
  border-color: var(--color-warning);
  background: linear-gradient(
    to right,
    color-mix(in srgb, var(--color-warning) 22%, transparent) var(--fill-pct, 0%),
    transparent var(--fill-pct, 0%)
  );
  transition: background var(--t-trans) ease, color var(--t-trans), border-color var(--t-trans);
}
.compendium-btn-done {
  color: var(--color-success);
  border-color: var(--color-success);
  background: color-mix(in srgb, var(--color-success) 12%, transparent);
}

/* ═══════════════════════════════════════════════════════════════════════════
   THREADFALL MOTION SYSTEM
   Consistent timing, hover lift, press feedback, focus states, entry anim.
   All rules here override earlier specifics — do not edit inline transitions.
═══════════════════════════════════════════════════════════════════════════ */

/* ── Entry keyframes ──────────────────────────────────────────────────── */
@keyframes tf-fade-up {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0);    }
}
@keyframes tf-modal-enter {
  from { opacity: 0; transform: translateY(12px) scale(0.97); }
  to   { opacity: 1; transform: translateY(0)    scale(1);    }
}
@keyframes tf-slide-up {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0);   }
}

/* ── Refined live-mode pulses (gentler than originals) ────────────────── */
@keyframes rec-blink  { 0%,100%{ opacity:1 } 50%{ opacity:0.35 } }
@keyframes pulse       { 0%,100%{ opacity:1 } 50%{ opacity:0.55 } }
@keyframes rec-pulse   {
  0%,100%{ opacity:1; transform:scale(1)    }
  50%    { opacity:.7; transform:scale(1.04)}
}
/* (v795) @keyframes mic-pulse retired — only consumer was .live-mic-btn.recording, now orphan. */

/* ── Tab panel & screen entry ─────────────────────────────────────────── */
.tf-entering {
  animation: tf-fade-up var(--t-trans) var(--ease) both;
}
/* Fade-in whenever a tab panel becomes visible */
.tab-panel:not(.hidden) {
  animation: tf-fade-up var(--t-trans) var(--ease) both;
}
#live-session-screen:not(.hidden) {
  animation: tf-fade-up var(--t-trans) var(--ease) both;
}

/* ── Collapse chevron — universal rotation system ─────────────────────── */
/* Transcript panel */
.live-transcript-panel.transcript-collapsed #live-transcript-toggle { transform: rotate(-90deg); }
/* Speaker bar */
.live-spk-bar-wrap.spk-collapsed .live-spk-toggle { transform: rotate(-90deg); }
/* (Quest group header chevron rotation retired in v762 along with the
   .quest-group-* block — the JS no longer renders quest sub-groups.) */
/* (PC chronicle chevron rules retired 2026-05-06 — see PC chronicle CSS
   block above for context.) */

/* ── Modal entry ──────────────────────────────────────────────────────── */
.modal-overlay:not(.hidden) .modal-box {
  animation: tf-modal-enter var(--t-trans) var(--ease) both;
}

/* ── Toast slide ──────────────────────────────────────────────────────── */
#toast {
  transition:
    opacity  var(--t-trans) var(--ease),
    transform var(--t-trans) var(--ease);
}

/* ── Unified button transitions ───────────────────────────────────────── */
.btn-primary,
button[type="submit"],
.btn-ghost,
.btn-danger {
  transition:
    transform    var(--t-hover) var(--ease),
    opacity      var(--t-hover) var(--ease),
    background   var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease),
    color        var(--t-hover) var(--ease),
    box-shadow   var(--t-hover) var(--ease);
}

/* v813: legacy teal recipes for `.btn-primary` / unclassed `button[type="submit"]`
   retired — ghost-pill recipe lives in the early btn block (~line 363).
   Canonical `.btn-ghost` hover + active lives next to its base rule and is no
   longer overridden here. */

/* ── Focus states (keyboard navigation) ──────────────────────────────── */
:focus-visible {
  outline: 2px solid var(--accent-teal);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px rgba(47,124,133,0.12);
  border-radius: var(--radius-sm);
}
/* Inputs get a tighter inline focus ring */
input[type="text"]:focus,
input[type="date"]:focus,
input[type="password"]:focus,
textarea:focus,
select:focus {
  outline: none;
  border-color: var(--accent-teal);
  box-shadow: 0 0 0 3px rgba(47,124,133,0.12);
  transition:
    border-color var(--t-hover) var(--ease),
    box-shadow   var(--t-hover) var(--ease);
}

/* ── Tab buttons ──────────────────────────────────────────────────────── */
.tab-btn {
  transition:
    color        var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease);
}

/* ── Filter pills ─────────────────────────────────────────────────────── */
/* .type-filter-btn defines its own richer transition (matches .btn-new)
   in the Codex toolbar block above; only .quest-filter-btn lives here. */
.quest-filter-btn {
  transition:
    background   var(--t-hover) var(--ease),
    color        var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease);
}

/* ── Session cards ────────────────────────────────────────────────────── */
/* v1087: hover for .session-card now comes from .tf-card:hover. The
   transitions block below preserves header-bg-on-hover only. */
.session-card-hdr {
  transition: background var(--t-hover) var(--ease);
}

/* ── Campaign cards ───────────────────────────────────────────────────── */
.campaign-card {
  transition:
    border-color var(--t-hover) ease,
    transform    var(--t-hover) ease,
    box-shadow   var(--t-hover) ease;
}
.campaign-card:hover, .campaign-card:focus-within {
  border-color: rgba(47,154,163,0.45);
  box-shadow: 0 4px 12px rgba(0,0,0,0.12);
  transform: translateY(-1px);
}

/* ── Entity / compendium rows ─────────────────────────────────────────── */
/* Canonical card hover — same recipe as .campaign-card and .session-card:
   shadowless at rest, lifts with translateY + teal-tinted border + drop
   shadow on hover/focus. */
/* v1087: .entity-row hover/transitions now inherited from .tf-card chassis. */

/* ── Quest nav ────────────────────────────────────────────────────────── */
/* v1087: .quest-nav-row transitions now inherited from .tf-card chassis. */
.quest-nav-hdr {
  transition: background var(--t-hover) var(--ease);
}
/* ── Login button ─────────────────────────────────────────────────────── */
.login-btn {
  transition:
    transform    var(--t-hover) var(--ease),
    opacity      var(--t-hover) var(--ease),
    box-shadow   var(--t-hover) var(--ease);
}
.login-btn:hover, .login-btn:focus-visible  { transform: translateY(-1px); box-shadow: 0 3px 10px rgba(0,0,0,0.28); }
.login-btn:active { transform: scale(0.98); transition-duration: var(--t-press); }

/* ── Merge survivor options ───────────────────────────────────────────── */
.merge-survivor-opt {
  transition:
    border-color var(--t-hover) var(--ease),
    background   var(--t-hover) var(--ease);
}

/* ── DDB party card cells ─────────────────────────────────────────────── */
.ddb-ab-cell,
.skill-pills-grid .skill-pill {
  transition:
    background   var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease);
}

/* ── Compendium AI sync button ────────────────────────────────────────── */
.entry-compendium-btn {
  transition:
    background   var(--t-trans) var(--ease),
    color        var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease) !important;
}

/* ── Live mode: subtle status pulse ──────────────────────────────────── */
/* Recording dot — already uses rec-blink, now gentler via keyframe above */
/* Active session indicator in live tab */
.live-active-indicator {
  animation: pulse 2s var(--ease) infinite;
}

/* ── Nav auth buttons ─────────────────────────────────────────────────── */
.nav-gm-btn,
.nav-logout-btn {
  transition:
    background   var(--t-hover) var(--ease),
    color        var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease);
}

/* ── Theme toggle ─────────────────────────────────────────────────────── */
#theme-toggle {
  transition:
    background   var(--t-hover) var(--ease),
    transform    var(--t-hover) var(--ease);
}
#theme-toggle:hover, #theme-toggle:focus-visible { transform: rotate(15deg); }

/* ── Admin Panel ──────────────────────────────────────────────────────── */
/* Admin screen is controlled via style.display (not .hidden class) for reliability */
#admin-screen {
  position: fixed !important;
  inset: 0 !important;
  z-index: 10000 !important;
  background: var(--bg) !important;
  display: none;
  flex-direction: column;
  overflow-y: auto;
}
.admin-wrap {
  min-height: 100vh;
  background: var(--bg);
  display: flex;
  flex-direction: column;
}
.admin-sticky-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--bg-2);
}
.admin-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem 1.5rem;
  border-bottom: 1px solid var(--border);
}
.admin-badge {
  font-size: var(--font-size-base);
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent-teal) 30%, transparent);
  border-radius: var(--radius-md);
  padding: 0.2rem 0.55rem;
}
.admin-tabs {
  display: flex;
  gap: 0;
  border-bottom: 1px solid var(--border);
  background: var(--bg-2);
  padding: 0 1.5rem;
  overflow-x: auto;
}
.admin-tab {
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  color: var(--text-2);
  cursor: pointer;
  font-size: var(--font-size-base);
  font-weight: 500;
  padding: 0.65rem 1rem;
  white-space: nowrap;
  transition: color var(--t-hover), border-color var(--t-hover);
}
.admin-tab:hover, .admin-tab:focus-visible { color: var(--text); }
.admin-tab.active { color: var(--accent-teal); border-bottom-color: var(--accent-teal); }
.admin-tab-panel { padding: 1.5rem; flex: 1; }
.admin-actions-bar {
  display: flex; gap: 0.5rem; flex-wrap: wrap;
  margin-bottom: 1rem;
}
.admin-stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 1rem;
  margin-bottom: 1.5rem;
}
.admin-stat-card {
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  padding: 1rem 1.25rem;
}
.admin-stat-card--accent {
  border-color: color-mix(in srgb, var(--accent-teal) 35%, transparent);
  background: color-mix(in srgb, var(--accent-teal) 5%, var(--bg-2));
}
.admin-stat-label { font-size: var(--font-size-xs); color: var(--text-3); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 0.35rem; }
.admin-stat-value { font-size: 1.6rem; font-weight: 700; color: var(--text); line-height: 1; }
.admin-stat-sub   { font-size: var(--font-size-sm); color: var(--text-3); margin-top: 0.3rem; }

/* ── Pulse dashboard (v1042) ──────────────────────────────────────────────
   The daily business-ops view. Composes admin-stat-card chassis for visual
   continuity; layout is rows-of-cards rather than the old equal-grid because
   different rows hold different cardinality of content (4 hero numbers,
   3 gauges, 1 full-width funnel, etc.). */
.pulse-row {
  display: grid;
  gap: 1rem;
  margin-bottom: 1.25rem;
}
.pulse-row--hero { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.pulse-row:not(.pulse-row--hero) { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }

.pulse-card {
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  padding: 1rem 1.25rem;
}
.pulse-card--hero {
  /* Slightly tighter on hero numerals to keep the row readable. */
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.pulse-card--full  { grid-column: 1 / -1; }
.pulse-card--wide  { grid-column: span 2; }
.pulse-card--health { /* op-health checklist, same chassis */ }

.pulse-card-label {
  font-size: var(--font-size-xs);
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 0.5rem;
}
.pulse-card-value {
  font-size: 2rem;
  font-weight: 700;
  color: var(--text);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.pulse-card-sub {
  font-size: var(--font-size-sm);
  color: var(--text-3);
  margin-top: 0.25rem;
}
.pulse-delta--up   { color: var(--accent-teal); font-weight: 600; }
.pulse-delta--down { color: var(--color-danger); font-weight: 600; }

/* Tier mix pills */
.pulse-tier-pills { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.pulse-tier-pill {
  display: inline-flex;
  align-items: center;
  padding: 0.25rem 0.6rem;
  border-radius: 999px;
  font-size: var(--font-size-sm);
  font-weight: 600;
  border: 1px solid var(--border);
  background: var(--bg-1);
  color: var(--text);
}
.pulse-tier-pill--founding_gm { border-color: rgba(47,154,163,0.55); color: var(--accent-teal); }
.pulse-tier-pill--hobby       { border-color: var(--border); }
.pulse-tier-pill--standard    { border-color: rgba(47,154,163,0.40); }
.pulse-tier-pill--master      { border-color: rgba(47,154,163,0.60); background: color-mix(in srgb, var(--accent-teal) 7%, var(--bg-1)); }
.pulse-tier-pill--pro         { border-color: rgba(47,154,163,0.85); background: color-mix(in srgb, var(--accent-teal) 14%, var(--bg-1)); }
.pulse-tier-pill--empty       { color: var(--text-3); font-weight: 500; font-style: italic; }
/* v1045: comped tier pill — muted, dashed border so it visually reads as
   "not real revenue." Same shape + click affordance as paying pills. */
.pulse-tier-pill--comp        { border-style: dashed; border-color: var(--border); color: var(--text-2); background: transparent; }
.pulse-tier-pill-suffix       { color: var(--text-3); font-weight: 400; font-size: 0.85em; margin-left: 0.15em; }
.pulse-comped-label           { color: var(--text-3); font-style: italic; }

/* Founding / webhook gauge two-column mini grid */
.pulse-clock-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  gap: 0.75rem;
  margin-top: 0.25rem;
}
.pulse-clock-grid > div {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.pulse-stat-n {
  font-size: 1.15rem;
  font-weight: 700;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.pulse-stat-l {
  font-size: var(--font-size-xs);
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* Operational health checklist */
.pulse-health-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.pulse-health-list li {
  display: grid;
  grid-template-columns: 14px 1fr auto;
  align-items: center;
  gap: 0.5rem;
  font-size: var(--font-size-sm);
}
.pulse-health-label { color: var(--text); }
.pulse-health-value { color: var(--text-3); font-variant-numeric: tabular-nums; font-weight: 600; }
.pulse-health-dot {
  width: 10px; height: 10px; border-radius: 50%;
  display: inline-block;
}
.pulse-health-dot--green  { background: #2f9aa3; box-shadow: 0 0 0 2px color-mix(in srgb, #2f9aa3 25%, transparent); }
.pulse-health-dot--yellow { background: #d97706; box-shadow: 0 0 0 2px color-mix(in srgb, #d97706 25%, transparent); }
.pulse-health-dot--red    { background: #dc2626; box-shadow: 0 0 0 2px color-mix(in srgb, #dc2626 25%, transparent); }

/* Funnel horizontal bar */
.pulse-funnel {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  margin-top: 0.25rem;
}
.pulse-funnel-stage {
  display: grid;
  grid-template-columns: 130px 1fr 60px;
  align-items: center;
  gap: 0.75rem;
}
.pulse-funnel-label {
  font-size: var(--font-size-sm);
  color: var(--text);
  font-weight: 500;
}
.pulse-funnel-bar-wrap {
  position: relative;
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  height: 28px;
  overflow: hidden;
}
.pulse-funnel-bar-fill {
  position: absolute;
  inset: 0 auto 0 0;
  background: color-mix(in srgb, var(--accent-teal) 25%, transparent);
  border-right: 1px solid color-mix(in srgb, var(--accent-teal) 55%, transparent);
  transition: width 0.3s ease;
}
.pulse-funnel-bar-count {
  position: relative;
  z-index: 1;
  height: 100%;
  display: flex;
  align-items: center;
  padding-left: 0.6rem;
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.pulse-funnel-conv {
  font-size: var(--font-size-sm);
  color: var(--text-3);
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* v1059: role-split funnel — GM-signups acquisition headline (primary number
   + muted total-signups context) sits above two stacked funnel groups (GM,
   Player), each under a canonical section-label eyebrow. */
.pulse-funnel-acq {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.4rem 0.85rem;
  margin: 0.25rem 0 1.25rem;
}
.pulse-funnel-acq-primary {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
}
.pulse-funnel-acq-label {
  font-size: var(--font-size-xs);
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.pulse-funnel-acq-secondary {
  font-size: var(--font-size-sm);
  color: var(--text-3);
}
.pulse-funnel-group + .pulse-funnel-group {
  margin-top: 1.25rem;
}
.pulse-funnel-group .form-section-label {
  display: block;
  margin-bottom: 0.5rem;
}
/* v1225: top-of-funnel "Visitors → Signup" line above the GM/player funnels. */
.pulse-funnel-landing {
  margin-bottom: 1.25rem;
}
.pulse-funnel-landing .form-section-label {
  display: block;
  margin-bottom: 0.5rem;
}
.pulse-funnel-landing-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.25rem;
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}
.pulse-funnel-landing-stat strong {
  color: var(--color-text-primary);
  font-weight: 700;
}
.pulse-funnel-landing-gap strong {
  color: var(--color-ember);
}

/* Mini tables (today's signal + top campaigns) */
.pulse-mini-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--font-size-sm);
  margin-top: 0.25rem;
}
.pulse-mini-table td, .pulse-mini-table th {
  padding: 0.4rem 0.5rem;
  text-align: left;
}
.pulse-mini-table th {
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-3);
  font-weight: 600;
}
.pulse-mini-table td:last-child, .pulse-mini-table th:last-child,
.pulse-mini-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.pulse-mini-table--lined tr { border-bottom: 1px solid var(--border); }
.pulse-mini-table--lined tr:last-child { border-bottom: none; }

/* ── Pulse drill-down affordances (v1043) ──────────────────────────────────
   Anything with a data-drill attribute gets the click recipe: pointer
   cursor, subtle hover-lift matching the canonical card-hover recipe. */
.pulse-card--drill,
.pulse-tier-pill--drill,
.pulse-stat-drill,
.pulse-funnel-stage--drill {
  cursor: pointer;
  transition: transform 0.12s ease, border-color 0.12s ease, background-color 0.12s ease, box-shadow 0.12s ease;
}
.pulse-card--drill:hover {
  transform: translateY(-1px);
  border-color: rgba(47,154,163,0.45);
  box-shadow: 0 4px 12px rgba(0,0,0,0.12);
}
.pulse-tier-pill--drill:hover {
  border-color: rgba(47,154,163,0.85);
  background: color-mix(in srgb, var(--accent-teal) 18%, var(--bg-1));
}
.pulse-stat-drill {
  /* Stat sub-cells inside a card — use background-tint on hover instead
     of transform so the card's own hover state doesn't conflict. */
  border-radius: var(--radius-md);
  padding: 0.25rem 0.4rem;
  margin: -0.25rem -0.4rem;
}
.pulse-stat-drill:hover {
  background: color-mix(in srgb, var(--accent-teal) 8%, var(--bg-1));
}
.pulse-funnel-stage--drill:hover .pulse-funnel-bar-wrap {
  border-color: rgba(47,154,163,0.65);
}
.pulse-funnel-stage--drill:hover .pulse-funnel-bar-fill {
  background: color-mix(in srgb, var(--accent-teal) 40%, transparent);
}
.pulse-mini-table--drill tr.pulse-stat-drill td {
  border-radius: var(--radius-md);
}
.pulse-mini-table--drill tr.pulse-stat-drill:hover td {
  background: color-mix(in srgb, var(--accent-teal) 8%, var(--bg-1));
}
.pulse-card-hint {
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  color: var(--text-3);
  font-size: var(--font-size-xs);
  margin-left: 0.5rem;
}

/* Drill modal table */
.pulse-drill-body {
  padding: var(--space-4) var(--space-5) var(--space-5);
  max-height: 70vh;
  overflow-y: auto;
}
.pulse-drill-meta {
  font-size: var(--font-size-xs);
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 0.5rem;
}
.pulse-drill-table-wrap {
  overflow-x: auto;
}
.pulse-drill-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--font-size-sm);
}
.pulse-drill-table th {
  text-align: left;
  padding: 0.5rem 0.75rem;
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-3);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
  position: sticky;
  top: 0;
  background: var(--bg-2);
}
.pulse-drill-table td {
  padding: 0.55rem 0.75rem;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
  font-variant-numeric: tabular-nums;
}
.pulse-drill-table tr:last-child td { border-bottom: none; }
.pulse-drill-table tr:hover td { background: color-mix(in srgb, var(--accent-teal) 5%, transparent); }

.admin-table-wrap { overflow-x: auto; }
.admin-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--font-size-base);
}
.admin-table th {
  text-align: left;
  padding: 0.5rem 0.75rem;
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-3);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.admin-table td {
  padding: 0.55rem 0.75rem;
  border-bottom: 1px solid var(--border);
  color: var(--text);
  vertical-align: middle;
}
.admin-table tr:last-child td { border-bottom: none; }
.admin-table tr:hover td, .admin-table tr:focus-within td { background: var(--bg-2); }
/* ── Admin view-as bar ───────────────────────────────────────────────────── */
/* Admin panel version — compact row inside the pulse footer */
.admin-view-as {
  display: flex; align-items: center; gap: 0.35rem; flex-wrap: wrap;
  padding: 0.4rem 0.65rem 0.35rem;
  border-bottom: 1px solid color-mix(in srgb, var(--color-border) 50%, transparent);
  background: color-mix(in srgb, #5b21b6 18%, transparent);
  font-size: var(--font-size-xs);
}
.admin-va-label { font-weight: 600; color: #c4b5fd; letter-spacing: 0.02em; white-space: nowrap; }
#view-as-label { font-weight: 600; letter-spacing: 0.02em; }
.view-as-toggle {
  display: flex; border-radius: var(--radius-sm); overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--color-white) 30%, transparent);
}
.view-as-btn {
  background: transparent; border: none;
  border-right: 1px solid color-mix(in srgb, var(--color-white) 20%, transparent);
  color: color-mix(in srgb, var(--color-white) 65%, transparent);
  padding: 0.15rem 0.65rem;
  cursor: pointer; font-size: var(--font-size-xs); transition: background var(--t-hover);
}
.view-as-btn:last-child { border-right: none; }
.view-as-btn:hover, .view-as-btn:focus-visible { background: color-mix(in srgb, var(--color-white) 10%, transparent); color: var(--color-white); }
.view-as-btn.active { background: color-mix(in srgb, var(--color-white) 22%, transparent); color: var(--color-white); font-weight: 700; }

.admin-pill {
  display: inline-block;
  font-size: var(--font-size-xs);
  font-weight: 600;
  padding: 0.1rem 0.45rem;
  border-radius: var(--radius-sm);
  line-height: 1.4;
}
.admin-pill--admin  { background: color-mix(in srgb, var(--accent-teal) 18%, transparent); color: var(--accent-teal); }
.admin-pill--ok     { background: color-mix(in srgb, #4ade80 15%, transparent); color: #4ade80; }
.admin-pill--warn    { background: color-mix(in srgb, #facc15 15%, transparent); color: #ca8a04; }
.admin-pill--blocked { background: color-mix(in srgb, var(--accent-red) 15%, transparent); color: var(--accent-red); }
.admin-pill--danger { background: color-mix(in srgb, var(--accent-red) 15%, transparent); color: var(--accent-red); }
/* v1030: tier-pill variants for the admin Users table. Muted = free/no
   subscription. Gold = Founding GM (honor tier). Teal saturation =
   Master/Pro. The base .admin-pill--ok (green) is reused for Hobby +
   Standard since those tiers are routine paid states. */
.admin-pill--muted  { background: color-mix(in srgb, var(--text-3) 12%, transparent); color: var(--text-3); }
.admin-pill--gold   { background: color-mix(in srgb, var(--accent-gold) 18%, transparent); color: var(--accent-gold); }
.admin-pill--teal   { background: color-mix(in srgb, var(--accent-teal) 22%, transparent); color: var(--accent-teal); }
.admin-section-hdr  { font-size: var(--font-size-sm); font-weight: 600; color: var(--text-2); text-transform: uppercase; letter-spacing: 0.06em; margin: 1.5rem 0 0.5rem; }
/* Affiliates tab (Phase 1) — form rows reuse .admin-actions-bar (flex+gap);
   these just size the inputs so the row reads as a coherent form. */
.affiliate-form-row { align-items: center; margin: 0.4rem 0 0.25rem; }
.affiliate-input {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 0.4rem 0.6rem;
  font-size: var(--font-size-sm);
  color: var(--text);
  min-width: 160px;
}
.affiliate-input--num { min-width: 110px; max-width: 200px; }
.affiliate-input:focus-visible { outline: none; border-color: rgba(47,154,163,0.55); }

/* Communications tab — scheduled email broadcasts. Inputs compose the
   canonical .form-input; these lay out the cap meters, compose-form rows,
   and the queue cards. */
.comms-cap-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin: 0.5rem 0 0.25rem; }
.comms-cap {
  display: inline-flex; align-items: center; gap: 0.35rem;
  background: var(--bg-2); border: 1px solid var(--border);
  border-radius: var(--radius-full); padding: 0.2rem 0.65rem;
  font-size: var(--font-size-xs); color: var(--text-2);
}
.comms-cap--full {
  border-color: color-mix(in srgb, var(--color-danger) 55%, transparent);
  color: var(--color-danger);
}
.comms-form { display: flex; flex-direction: column; gap: var(--space-2); margin: 0.4rem 0 0.25rem; }
.comms-form-row { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.comms-form-row > * { flex: 1 1 180px; }
.comms-body-input {
  width: 100%; min-height: 140px; resize: vertical;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--font-size-xs); line-height: 1.5;
}
.comms-list { display: flex; flex-direction: column; gap: var(--space-2); margin-top: 0.5rem; }
.comms-card {
  display: flex; align-items: center; gap: var(--space-3);
  background: var(--bg-2); border: 1px solid var(--border);
  border-radius: var(--radius-md); padding: var(--space-3) var(--space-4);
}
.comms-card--sent, .comms-card--cancelled, .comms-card--failed { opacity: 0.7; }
.comms-card-main { flex: 1; min-width: 0; }
.comms-card-subject { font-weight: 600; color: var(--text); }
.comms-card-meta { font-size: var(--font-size-xs); color: var(--text-3); margin-top: 0.2rem; }
.comms-card-actions { display: flex; gap: var(--space-2); flex-shrink: 0; }
.comms-status {
  display: inline-block; font-size: var(--font-size-xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.04em;
  padding: 0.05rem 0.45rem; border-radius: var(--radius-md); margin-right: 0.35rem;
}
.comms-status--pending   { background: color-mix(in srgb, var(--color-brand) 16%, transparent); color: var(--color-brand); }
.comms-status--sent      { background: color-mix(in srgb, var(--color-brand) 10%, transparent); color: var(--text-2); }
.comms-status--cancelled { background: var(--bg-2); border: 1px solid var(--border); color: var(--text-3); }
.comms-status--failed    { background: color-mix(in srgb, var(--color-danger) 16%, transparent); color: var(--color-danger); }

/* AI Usage tab two-column layout */
.admin-ai-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 0.5rem; }
.admin-ai-grid .admin-section-hdr:first-child { margin-top: 0; }
@media (max-width: 900px) { .admin-ai-grid { grid-template-columns: 1fr; } }
.admin-alert-card {
  background: var(--bg-2);
  border: 1px solid color-mix(in srgb, var(--accent-red) 30%, transparent);
  border-radius: var(--radius-lg);
  padding: 0.75rem 1rem;
  margin-bottom: 0.5rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}
.admin-alert-card--info { border-color: var(--border); }
/* v965: .admin-empty inherits canonical .placeholder typography. Only
   padding survives — admin tables need vertical breathing room. */
.admin-empty { padding: var(--space-5) 0; }

/* ── Recording consent modal ─────────────────────────────────────────────── */
/* Uses the standard .modal-overlay + .modal-box hierarchy now. The only
   bespoke piece that remains is .modal-overlay--blocking for the heavier
   "you can't dismiss this casually" scrim — defined alongside .modal-overlay.
   Below: only the inner-content rules unique to consent UX (body, actions, badge). */

.consent-body {
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
}
.consent-body p {
  margin: 0;
  font-size: var(--font-size-base);
  line-height: 1.55;
  color: var(--text-2);
}
.consent-actions {
  display: flex;
  gap: var(--space-3);
}
.consent-accept-btn {
  flex: 1;
  padding: 0.6rem 1rem;
  font-size: var(--font-size-md);
}
.consent-leave-btn {
  padding: 0.6rem 1rem;
  font-size: var(--font-size-md);
  color: var(--text-3);
}
.consent-leave-btn:hover, .consent-leave-btn:focus-visible { color: var(--text-1); }

/* Persistent badge shown after consent is given */
.consent-accepted-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: var(--font-size-xs);
  color: var(--text-3);
  background: color-mix(in srgb, var(--text-1) 5%, transparent);
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  padding: 0.18rem 0.65rem;
  white-space: nowrap;
  user-select: none;
}
.consent-accepted-badge.hidden { display: none; }

/* ── Live presence indicators ────────────────────────────────────────────── */
@keyframes presence-pulse {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0.35; }
}

/* Live tab — active state (other users are in Live) */
#live-nav-btn.live-active {
  color: var(--accent-green);
}
#live-nav-btn.live-active svg.tf-tab-mark use {
  /* inherit the green so the diamond marker also turns green */
  color: inherit;
}

/* Badge inside the Live tab button.
   v1136: stripped the inner pill chrome (border + bg + radius + padding) —
   the outer #live-nav-btn already IS a pill, so the badge was rendering
   as a pill-in-pill when a session was active. Now it reads as inline
   text (green dot + count) inside the Live pill, with just the pulse
   animation surviving as the "session active" affordance. */
.live-presence-badge {
  display: inline-flex;
  align-items: center;
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  color: var(--accent-green);
  margin-left: 0.3rem;
  gap: 0.2rem;
  white-space: nowrap;
  animation: presence-pulse 2.2s ease-in-out infinite;
}
.live-presence-badge::before {
  content: '';
  display: inline-block;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}
.live-presence-badge.hidden { display: none; }

/* Campaign selector — live badge on card */
/* v1084: aligned to spec .tf-badge--broadcast (filled moss with blinking
   dot) — GM live with active watchers. Recording state is .camp-badge--recording
   (filled ember/red). Browsing-only is .pres-badge--browsing (ended/neutral). */
.campaign-live-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: var(--radius-full);
  border: 1px solid var(--accent-green);
  font-size: 10.5px;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.5;
  color: #F4FAEF;
  background: var(--accent-green);
  margin-top: 0.2rem;
}
.campaign-live-badge.hidden { display: none; }
/* Admin "browsing only" state — neutral chip per spec .tf-badge--ended */
.campaign-live-badge.pres-badge--browsing {
  color: var(--text-3);
  background: color-mix(in srgb, var(--text-1) 5%, var(--bg));
  border-color: var(--border);
}
.campaign-live-badge.pres-badge--browsing .camp-live-dot {
  background: var(--text-3);
  box-shadow: none;
  animation: none;
}
.camp-live-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #F4FAEF;
  box-shadow: 0 0 0 2px color-mix(in srgb, #fff 30%, transparent);
  flex-shrink: 0;
  animation: tf-dot-blink 1.6s ease-in-out infinite;
}


/* ── Live log real-time sync ─────────────────────────────────────────────── */
@keyframes le-arrive {
  from { opacity: 0; transform: translateY(-5px); }
  to   { opacity: 1; transform: translateY(0); }
}
.live-log-entry--remote {
  animation: le-arrive 0.28s ease-out;
  border-left: 2px solid var(--accent-teal);
  padding-left: calc(0.65rem - 2px); /* compensate for border so content doesn't shift */
}
.live-log-entry[data-tag="system_event"] {
  border-left-color: var(--accent-teal);
  background: color-mix(in srgb, var(--color-brand) 7%, transparent);
  font-style: italic;
}
.live-log-entry--scan {
  animation: le-arrive 0.28s ease-out, le-scan-pulse 2.2s ease-out 0.28s forwards;
  border-left: 2px solid var(--accent-teal);
  padding-left: calc(0.65rem - 2px);
}
@keyframes le-scan-pulse {
  0%   { box-shadow: 0 0 0 0   color-mix(in srgb, var(--color-brand) 50%, transparent); }
  40%  { box-shadow: 0 0 0 5px color-mix(in srgb, var(--color-brand) 20%, transparent); }
  100% { box-shadow: 0 0 0 0   transparent; }
}

/* ── Roster panel ────────────────────────────────────────────────────────── */
.roster-wrap {
  max-width: 860px;
  margin: 0 auto;
  padding: var(--space-6) var(--space-5) var(--space-10);
}
.roster-hdr {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-5);
  gap: var(--space-4);
}
/* .roster-title rule retired in v775 — the eyebrow recipe now flows through
   .form-section-label composed in markup, no bespoke heading typography. */
.roster-new-btn { flex-shrink: 0; }

.roster-new-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-3);
}
.roster-link-select {
  font-size: var(--font-size-base);
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg);
  color: var(--text-1);
  max-width: 220px;
}
.roster-inline-input {
  flex: 1;
  background: var(--bg-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text-1);
  padding: var(--space-2) var(--space-2);
  font-size: var(--font-size-md);
  min-width: 0;
}
.roster-inline-input:focus { outline: none; border-color: var(--accent-teal); }
/* Compact override on the inline ✓/✕ buttons in the new-PC row. Composes
   .btn-primary / .btn-ghost; this class only tightens the padding so the
   buttons sit flush at the same height as the input. */
.roster-inline-btn {
  padding: var(--space-1) var(--space-2);
  font-size: var(--font-size-base);
  flex-shrink: 0;
}

.roster-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--font-size-base);
}
.roster-table th {
  text-align: left;
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-3);
  padding: 0 var(--space-3) var(--space-2);
  border-bottom: 1px solid var(--border);
}
.roster-table td {
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
  vertical-align: middle;
}
.roster-table tr:last-child td { border-bottom: none; }
.roster-actions-col { width: 60px; text-align: center; }
/* Member-row actions cell — replaces inline style="white-space:nowrap" on
   the per-member <td> in renderRoster(). */
.roster-actions-td { white-space: nowrap; }
/* Member email tag inside the "Played By" label — replaces inline
   style="color:var(--text-3);font-size:0.8rem". */
.roster-member-email {
  color: var(--text-3);
  font-size: var(--font-size-sm);
}

.roster-name-display {
  cursor: pointer;
  color: var(--text-1);
  border-bottom: 1px dashed transparent;
  transition: border-color var(--t-hover);
}
.roster-name-display:hover, .roster-name-display:focus-visible { border-bottom-color: var(--accent-teal); }

.roster-assign-select {
  background: var(--bg-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text-1);
  padding: var(--space-1) var(--space-2);
  font-size: var(--font-size-base);
  max-width: 240px;
}
.roster-assign-select:focus { outline: none; border-color: var(--accent-teal); }

/* Compact delete-icon button in the actions column. Composes .btn-ghost;
   this class only resizes the icon and tints it tertiary by default,
   accent-red on hover (which would otherwise need .btn-ghost--danger). */
.roster-del-btn {
  font-size: var(--font-size-lg);
  padding: var(--space-1) var(--space-1);
  color: var(--text-3);
}
.roster-del-btn:hover, .roster-del-btn:focus-visible { color: var(--accent-red); }
.roster-del-locked { color: var(--text-3); font-size: var(--font-size-md); cursor: default; }

/* PC name in live log author row */
.le-author-pc {
  font-size: var(--font-size-sm);
  color: var(--accent-teal);
  font-weight: 400;
  margin-left: 0.15rem;
}

/* PC badge in profile modal */
.profile-pc-badge {
  display: inline-flex;
  align-items: center;
  background: color-mix(in srgb, var(--color-success) 12%, transparent);
  color: var(--color-success);
  border: 1px solid color-mix(in srgb, var(--color-success) 30%, transparent);
  border-radius: var(--radius-full);
  font-size: var(--font-size-sm);
  font-weight: 600;
  padding: 0.25rem 0.75rem;
  margin-bottom: 0.75rem;
}

/* Read-only player name display in entity edit form */
.ef-player-name-display {
  font-size: var(--font-size-base);
  color: var(--text-2);
  padding: 0.35rem 0;
}
/* (v779) .roster-members-hdr retired — the separate "Manage Content
   Editors" sub-table folded into the unified member-row table, so a
   second eyebrow is no longer needed. */

/* ── Combined member-row table cells (v779) ──────────────────────────────
   One row per campaign member; columns: Member | PC chips | Role. */
.roster-pcs-cell { vertical-align: middle; }
.roster-role-cell { vertical-align: middle; white-space: nowrap; }
.roster-role-actions {
  display: inline-flex;
  gap: var(--space-1);
  margin-left: var(--space-2);
}
.roster-member-cell { vertical-align: middle; }

/* PC chip — compact pill showing one PC assigned to a member. The × button
   inside unassigns the PC (PC stays in the campaign as orphan). Neutral
   tinting (bg-3 + border) keeps chips quiet alongside the role badges
   and white-pill action buttons in the same row. */
.pc-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  align-items: center;
}
.pc-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  background: var(--bg-3);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 0.15rem 0.35rem 0.15rem 0.55rem;
  border-radius: var(--radius-full);
  font-size: var(--font-size-xs);
  font-weight: 600;
  white-space: nowrap;
}
.pc-chip-name { line-height: 1.2; }
.pc-chip-remove {
  background: none;
  border: none;
  color: var(--text-3);
  font-size: var(--font-size-md);
  line-height: 1;
  padding: 0 var(--space-1);
  margin-right: -0.15rem;
  cursor: pointer;
  border-radius: 50%;
  transition: color var(--t-hover), background var(--t-hover);
}
.pc-chip-remove:hover, .pc-chip-remove:focus-visible {
  color: var(--accent-red);
  background: color-mix(in srgb, var(--accent-red) 12%, transparent);
}

/* "+ Add" inline picker — composes .btn-new + .btn-xs for the canonical
   white-pill aesthetic at compact size. Click → reveals .pc-add-select. */
.pc-add-btn { font-size: var(--font-size-xs); padding: 2px 8px; }
.pc-add-select {
  font-size: var(--font-size-xs);
  padding: 2px 6px;
  max-width: 160px;
}

/* ── Party Treasury Modal ─────────────────────────────────────────────────── */
.treasury-currency-grid {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.treasury-coin-label {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-3);
}
.treasury-coin-input {
  width: 72px;
  text-align: center;
}
.treasury-item-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.treasury-qty-input {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text);
  padding: 0.2rem 0.4rem;
  font-size: var(--font-size-sm);
}

/* ── Treasury Transaction Log ─────────────────────────────────────────────── */
/* Loot splitter layout */
.ls-coin-grid {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-3);
  margin-bottom: var(--space-4);
}
.ls-coin-label {
  display: flex; flex-direction: column; align-items: center; gap: var(--space-2);
}
.ls-coin-badge { font-size: var(--font-size-sm); padding: 0.2rem 0.55rem; }
.ls-coin-input { text-align: center; padding: 0.45rem 0.3rem; width: 100%; }
.ls-players-row {
  display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-4);
}
.ls-players-label { font-size: var(--font-size-sm); font-weight: 600; color: var(--color-text-secondary); white-space: nowrap; }
.ls-players-input { width: 72px; text-align: center; }
.ls-calc-btn { padding: 0.45rem 1rem; white-space: nowrap; margin-left: auto; }

/* Loot splitter result */
.loot-split-result {
  background: color-mix(in srgb, var(--color-gold) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-gold) 30%, transparent);
  border-radius: var(--radius-lg);
  padding: var(--space-3) var(--space-4);
}
.ls-section-label {
  font-size: var(--font-size-xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--color-text-tertiary); margin-bottom: 0.35rem;
}
.ls-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.5rem; padding: 0.25rem 0;
  border-bottom: 1px solid color-mix(in srgb, var(--color-border) 50%, transparent);
}
.ls-row:last-child { border-bottom: none; }
.ls-row--treasury .ls-coins { color: var(--color-gold); }
.ls-count { font-size: var(--font-size-sm); color: var(--color-text-secondary); white-space: nowrap; }
.ls-coins { font-size: var(--font-size-sm); font-weight: 700; color: var(--color-text-primary); text-align: right; }

.treasury-log-entry {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  font-size: var(--font-size-xs);
  background: var(--bg-3);
  border-radius: var(--radius-md);
  padding: 0.3rem 0.5rem;
  flex-wrap: wrap;
}
.treasury-log-actor {
  font-weight: 700;
  color: var(--color-brand);
  white-space: nowrap;
}
.treasury-log-summary {
  flex: 1;
  color: var(--text-2);
  min-width: 0;
  word-break: break-word;
}
.treasury-log-time {
  color: var(--text-3);
  white-space: nowrap;
  font-size: 0.65rem;
}

/* ── Admin Service Health Cards ──────────────────────────────────────────── */
/* ── Session Feed tab ────────────────────────────────────────────────────────  */
@keyframes feed-flash {
  0%   { background: color-mix(in srgb, var(--color-brand) 18%, transparent); }
  60%  { background: color-mix(in srgb, var(--color-brand) 18%, transparent); }
  100% { background: transparent; }
}
.feed-item--new { animation: feed-flash 2.4s ease-out forwards; }

/* ── GM-only indicator pip ─────────────────────────────────────────────── */
.gm-pip {
  display: inline-flex;
  align-items: center;
  font-size: 0.52rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-ember);
  background: color-mix(in srgb, var(--color-ember) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-ember) 32%, transparent);
  border-radius: var(--radius-full);
  padding: 0.08rem 0.3rem;
  margin-right: 0.28rem;
  margin-left: 0;
  line-height: 1.2;
  vertical-align: middle;
  flex-shrink: 0;
}
/* On solid-color buttons (teal primary), invert pip to white so it's visible */
.btn-primary .gm-pip {
  color: rgba(255,255,255,0.92);
  background: rgba(255,255,255,0.18);
  border-color: rgba(255,255,255,0.38);
}

/* Admin pip — same shape as .gm-pip but purple, for admin-only features */
.admin-pip {
  display: inline-flex;
  align-items: center;
  font-size: 0.52rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-purple);
  background: color-mix(in srgb, var(--color-purple) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-purple) 32%, transparent);
  border-radius: var(--radius-full);
  padding: 0.08rem 0.3rem;
  margin-right: 0.28rem;
  margin-left: 0;
  line-height: 1.2;
  vertical-align: middle;
  flex-shrink: 0;
}
.btn-primary .admin-pip {
  color: rgba(255,255,255,0.92);
  background: rgba(255,255,255,0.18);
  border-color: rgba(255,255,255,0.38);
}

/* ── GM tabs — same color as normal tabs, distinguished by the GM pip only ── */
.tab-btn.gm-tab { color: var(--text-2); }
.tab-btn.gm-tab:hover, .tab-btn.gm-tab:focus-visible { color: var(--text); }
.tab-btn.gm-tab.active {
  color: var(--accent-teal);
  border-bottom-color: transparent;
}


.feed-layout {
  display: flex; flex-direction: column;
  height: 100%; overflow: hidden;
}
/* Eyebrow LEFT (composed via .form-section-label in markup), live-indicator
   pill RIGHT — mirrors .sessions-toolbar / .compendium-section-hdr /
   .quest-nav-toolbar / .map-toolbar. v775 retired the bg + border-bottom
   that made Feed the visual outlier across all primary tabs. */
.feed-toolbar {
  display: flex; align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4) var(--space-2);
  flex-shrink: 0;
}
/* .feed-toolbar-title rule retired in v775 — eyebrow recipe now flows
   through .form-section-label composed in markup. */
.feed-live-indicator {
  display: inline-flex; align-items: center; gap: var(--space-1);
  font-size: var(--font-size-xs); font-weight: 600; color: var(--color-success);
}
/* .feed-live-indicator.hidden { display:none } retired in v775 — the
   global .hidden { display:none !important } rule beats this anyway. */

.feed-list {
  flex: 1; overflow-y: auto;
  display: flex; flex-direction: column;
  padding: var(--space-1) 0;
}
/* Empty + error markers — typography flows through .placeholder +
   .placeholder--padded composed in markup; this class only contributes
   the centred alignment specific to the feed. .error class (red tint)
   added to the markup for the error variant. */
/* v965: .feed-empty inherits canonical .placeholder typography — no
   text-align override (was centered, now left-aligned to match the
   Weave Fates / Sessions / Codex empty-state grammar). */
.feed-item {
  display: flex; align-items: baseline; gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
  transition: background var(--t-hover) ease,
              border-color var(--t-hover) ease,
              transform var(--t-hover) ease,
              box-shadow var(--t-hover) ease;
  font-size: var(--font-size-sm);
}
.feed-item--link { cursor: pointer; }
/* Canonical card-hover recipe — matches .campaign-card / .session-card /
   .entity-row / .party-pc-card / .quest-nav-row / .map-manage-item. */
.feed-item--link:hover, .feed-item--link:focus-visible {
  background: var(--bg-2);
  border-color: rgba(47,154,163,0.45);
  box-shadow: 0 4px 12px rgba(0,0,0,0.12);
  transform: translateY(-1px);
}
.feed-item-icon { font-size: var(--font-size-lg); flex-shrink: 0; line-height: 1.3; }
.feed-item-body { flex: 1; color: var(--text); line-height: 1.45; }
.feed-item-body strong { font-weight: 600; }
.feed-item-time {
  color: var(--text-3); font-size: var(--font-size-xs);
  white-space: nowrap; flex-shrink: 0;
}
.feed-type-pill {
  display: inline-block;
  font-size: var(--font-size-xs); font-weight: 600;
  padding: 0.05rem var(--space-1); border-radius: var(--radius-full);
  background: var(--bg-3); color: var(--text-2);
  text-transform: capitalize; margin-left: var(--space-1);
}

/* Feed item type colouring */
.feed-item--death  { border-left: 3px solid var(--color-danger); }
.feed-item--departure { border-left: 3px solid var(--color-warning); }
.feed-item--complete { border-left: 3px solid var(--color-success); }
.feed-item--failed { border-left: 3px solid var(--color-danger); }
.feed-item--quest  { border-left: 3px solid var(--color-purple); }
.feed-item--session { border-left: 3px solid var(--color-brand); font-weight: 600; }
.feed-item--key    { border-left: 3px solid var(--color-gold); }
.feed-item--entity { border-left: 3px solid var(--color-info); }


/* ── Live session background bar (minimized live session) ───────────────── */
.live-record-bar {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.35rem 1rem;
  background: color-mix(in srgb, var(--color-danger) 10%, var(--bg-2));
  border-bottom: 1px solid color-mix(in srgb, var(--color-danger) 25%, var(--border));
  font-size: var(--font-size-sm);
  font-weight: 500;
}
.live-record-bar.hidden { display: none; }

.lrb-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
  background: var(--color-text-tertiary);
}
.lrb-dot--rec {
  background: var(--color-danger);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-danger) 30%, transparent);
  animation: rec-blink 1.4s ease-in-out infinite;
}
.lrb-dot--idle { background: var(--color-success); }

.lrb-label { color: var(--color-text-secondary); }
.lrb-timer { font-variant-numeric: tabular-nums; color: var(--color-danger); font-weight: 600; min-width: 4.5rem; }

.lrb-actions { display: flex; gap: 0.4rem; margin-left: auto; }
.lrb-btn {
  font-size: var(--font-size-xs);
  font-weight: 600;
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: none;
  color: var(--color-text-secondary);
  cursor: pointer;
  transition: background var(--t-hover), color var(--t-hover);
}
.lrb-btn:hover, .lrb-btn:focus-visible { background: var(--color-surface-elevated); color: var(--color-text-primary); }
.lrb-end-btn { color: var(--color-danger); border-color: color-mix(in srgb, var(--color-danger) 40%, var(--border)); }
.lrb-end-btn:hover, .lrb-end-btn:focus-visible { background: color-mix(in srgb, var(--color-danger) 10%, transparent); }
.lrb-end-btn.hidden { display: none; }

/* ── Live broadcast banner (shown to members when GM broadcasts) ──────────── */
.live-broadcast-banner {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.75rem;
  background: color-mix(in srgb, var(--color-success) 10%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--color-success) 30%, transparent);
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-success);
}
.live-broadcast-banner.hidden { display: none; }
.broadcast-live-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--color-success);
  animation: presence-pulse 1.4s ease-in-out infinite;
  flex-shrink: 0;
}
.broadcast-live-dot.small { width: 6px; height: 6px; }
.broadcast-banner-label { flex: 1; }
.broadcast-watch-btn {
  background: none;
  border: 1px solid var(--color-success);
  border-radius: var(--radius-full);
  color: var(--color-success);
  font-size: var(--font-size-xs);
  font-weight: 600;
  padding: 0.1rem 0.55rem;
  cursor: pointer;
  transition: background var(--t-hover);
}
.broadcast-watch-btn:hover, .broadcast-watch-btn:focus-visible { background: color-mix(in srgb, var(--color-success) 15%, transparent); }

/* ── Watcher panel (collapsible transcript drawer for members) ────────────── */
.live-watcher-panel {
  border-bottom: 1px solid var(--color-border);
  background: var(--bg-1);
  max-height: 280px;
  display: flex;
  flex-direction: column;
}
.live-watcher-panel.hidden { display: none; }
.watcher-hdr {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.3rem 0.75rem;
  border-bottom: 1px solid var(--color-border);
  background: var(--bg-2);
  font-size: var(--font-size-xs);
}
.watcher-hdr-title { font-weight: 600; color: var(--color-success); flex: 1; display: flex; align-items: center; gap: 0.35rem; }
.watcher-wordcount { color: var(--color-text-tertiary); font-size: var(--font-size-xs); }
.watcher-box {
  flex: 1;
  overflow-y: auto;
  padding: 0.4rem 0.75rem;
  max-height: 220px;
  font-size: var(--font-size-sm);
  line-height: 1.55;
}
.watcher-turn {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 0.2rem;
}
.watcher-speaker {
  font-weight: 700;
  font-size: var(--font-size-xs);
  color: var(--color-brand);
  min-width: 1.4rem;
  text-align: center;
  padding-top: 0.15rem;
  flex-shrink: 0;
}
.watcher-text { color: var(--color-text-primary); }
.watcher-empty { color: var(--color-text-tertiary); font-style: italic; }

.admin-services-list {
  display: flex; flex-direction: column; gap: 0.75rem; margin-top: 0.5rem;
}

/* ── Admin Pulse Panel (floating, bottom-left) ────────────────── */
#admin-pulse-panel {
  position: fixed;
  bottom: 1.5rem;
  left: 1.5rem;
  z-index: 10003; /* above admin-screen (10000) so expand buttons receive clicks */
  width: 210px;
  background: color-mix(in srgb, var(--color-surface) 18%, transparent);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  border: 1px solid color-mix(in srgb, var(--color-border) 30%, transparent);
  border-radius: var(--radius-xl);
  box-shadow: 0 4px 20px rgba(0,0,0,0.10);
  font-size: var(--font-size-xs);
  overflow: hidden;
  transition: background var(--t-hover), border-color var(--t-hover), backdrop-filter var(--t-hover), box-shadow var(--t-hover);
}
#admin-pulse-panel:hover, #admin-pulse-panel:focus-visible {
  background: color-mix(in srgb, var(--color-surface) 75%, transparent);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-color: color-mix(in srgb, var(--color-border) 60%, transparent);
  box-shadow: 0 4px 20px rgba(0,0,0,0.18);
}
.admin-pulse-hdr {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0.45rem 0.65rem 0.45rem 0.75rem;
  background: color-mix(in srgb, var(--color-brand) 10%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--color-border) 60%, transparent);
}
.admin-pulse-title {
  font-weight: 700; font-size: var(--font-size-xs);
  color: var(--color-brand); letter-spacing: 0.04em; text-transform: uppercase;
}
.admin-pulse-hdr-actions { display: flex; align-items: center; gap: 0.4rem; }
.admin-pulse-ts { color: var(--color-text-tertiary); font-size: 0.65rem; }
.admin-pulse-collapse-btn {
  background: none; border: none; cursor: pointer; color: var(--color-text-tertiary);
  font-size: var(--font-size-md); line-height: 1; padding: 0 2px;
}
.admin-pulse-collapse-btn:hover, .admin-pulse-collapse-btn:focus-visible { color: var(--color-text-primary); }

/* ── Admin pulse three-state collapse ─────────────────────────────────── */
/* State 1: header only */
#admin-pulse-panel[data-state="1"] #admin-pulse-body,
#admin-pulse-panel[data-state="1"] .admin-pulse-footer { display: none; }
#admin-pulse-panel[data-state="1"] .admin-pulse-collapse-btn::after { content: '●'; }
#admin-pulse-panel[data-state="1"] .admin-pulse-collapse-btn { font-size: 0.65rem; }

/* State 2: minimized — just the shield icon */
#admin-pulse-panel[data-state="2"] { width: auto; min-width: 0; border-radius: var(--radius-full); padding: 2px; }
#admin-pulse-panel[data-state="2"] .admin-pulse-hdr,
#admin-pulse-panel[data-state="2"] #admin-pulse-body,
#admin-pulse-panel[data-state="2"] .admin-pulse-footer { display: none; }
.admin-pulse-mini-btn {
  display: none;
  align-items: center; justify-content: center;
  width: 34px; height: 34px;
  background: none; border: none; cursor: pointer;
  font-size: var(--font-size-lg); line-height: 1;
  color: var(--color-brand);
}
.admin-pulse-mini-btn:hover, .admin-pulse-mini-btn:focus-visible { color: var(--color-text-primary); }
#admin-pulse-panel[data-state="2"] .admin-pulse-mini-btn { display: flex; }
.admin-pulse-body { padding: 0.25rem 0 0.25rem; display: flex; flex-direction: column; gap: 0; }
.admin-pulse-row { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.admin-pulse-label { color: var(--color-text-secondary); flex: 1; }
.admin-pulse-val {
  font-weight: 600; color: var(--color-text-primary);
  font-variant-numeric: tabular-nums; min-width: 2rem; text-align: right;
}
.admin-pulse-val--hot { color: var(--color-success); }
.admin-pulse-divider {
  height: 1px; background: var(--color-border);
  margin: 0.2rem 0;
}
.ap-expand-btn {
  background: none; border: none; cursor: pointer;
  color: var(--color-text-tertiary); font-size: var(--font-size-xs);
  line-height: 1; padding: 0 1px; margin-left: 2px;
  flex-shrink: 0; transition: color var(--t-hover);
}
.ap-expand-btn:hover, .ap-expand-btn:focus-visible, .ap-expand-btn--open { color: var(--color-brand); }
.ap-detail {
  margin: 0.15rem 0 0.35rem 0;
  border-left: 2px solid var(--color-border);
  padding-left: 0.5rem;
  display: flex; flex-direction: column; gap: 0.25rem;
}
.ap-detail-item { display: flex; flex-direction: column; gap: 0.05rem; }
.ap-di-name {
  font-size: 0.65rem; font-weight: 600;
  color: var(--color-text-primary);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ap-di-meta {
  font-size: 0.6rem; color: var(--color-text-tertiary);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ap-detail-empty {
  font-size: 0.62rem; color: var(--color-text-tertiary); font-style: italic;
}

/* Admin presence detail — campaign → tabs breakdown */
.ap-pres-campaign {
  display: flex; flex-direction: column; gap: 0.1rem;
  margin-bottom: 0.3rem;
}
.ap-pres-camp-hdr {
  font-size: 0.65rem; font-weight: 700;
  color: var(--color-text-primary);
  display: flex; align-items: center; gap: 0.3rem;
}
.ap-pres-count {
  font-size: 0.58rem; font-weight: 700;
  background: color-mix(in srgb, var(--color-brand) 14%, transparent);
  color: var(--color-brand);
  border-radius: var(--radius-full);
  padding: 0.05rem 0.32rem;
}
.ap-pres-tab-row {
  display: flex; gap: 0.35rem; align-items: baseline;
  padding-left: 0.5rem;
}
.ap-pres-tab-name {
  font-size: 0.6rem; font-weight: 600; text-transform: capitalize;
  color: var(--color-gold); flex-shrink: 0;
  min-width: 3.5rem;
}
.ap-pres-names {
  font-size: 0.6rem; color: var(--color-text-secondary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ── Admin "Who's Online" panel — who's-where list ────────────── */
.ap-campaign-section { padding: 0.35rem 0.65rem; }
.ap-campaign-section + .ap-campaign-section { border-top: 1px solid var(--color-border); }
.ap-campaign-name {
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-tertiary);
  margin-bottom: 0.25rem;
}
.ap-user-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.15rem 0;
  font-size: var(--font-size-xs);
}
.ap-user-dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--color-success);
  flex-shrink: 0;
}
.ap-user-dot--rec { background: var(--color-danger); }
.ap-user-name {
  color: var(--color-text-primary); font-weight: 500;
  flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ap-user-tab { color: var(--color-text-tertiary); font-size: 0.65rem; }
.ap-rec-tag {
  font-size: 0.6rem; font-weight: 700;
  color: var(--color-danger);
  border: 1px solid color-mix(in srgb, var(--color-danger) 40%, transparent);
  border-radius: var(--radius-sm);
  padding: 0 0.25rem;
  letter-spacing: 0.04em;
}
.ap-empty {
  padding: 0.6rem 0.65rem;
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
  font-style: italic;
}
.admin-pulse-footer { padding: 0.35rem 0.65rem 0.5rem; }

/* Campaign presence pill (in main app header) */
.camp-presence-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-success);
  background: color-mix(in srgb, var(--color-success) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-success) 25%, transparent);
  border-radius: var(--radius-full);
  padding: 0.1rem 0.5rem;
  cursor: default;
}
.camp-presence-pill::before {
  content: '';
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--color-success);
  flex-shrink: 0;
}

/* Campaign selector badge — recording state */
/* v1086: campaign-card live badge now on .cc-live (spec recipe; gold-bordered
   dark pill, .cc-live-dot gold-pulse). The recording-state override (filled
   ember + halo) here targets the spec class. The legacy .campaign-live-badge
   selector retired with the markup retrofit. */
.cc-live.camp-badge--recording {
  color: #FFF6F1;
  background: var(--accent-red);
  border-color: var(--accent-red);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent-red) 50%, transparent);
  animation: tf-live-halo 2s ease-out infinite;
}
.cc-live.camp-badge--recording .cc-live-dot {
  width: 7px; height: 7px;
  background: #FFF6F1;
  box-shadow: 0 0 0 2px color-mix(in srgb, #fff 35%, transparent);
  animation: tf-dot-blink 1.2s ease-in-out infinite;
}
/* Browsing-only override — admin presence, no recording */
.cc-live.pres-badge--browsing {
  color: var(--text-3);
  background: color-mix(in srgb, var(--text-1) 5%, var(--bg));
  border-color: var(--border);
}
.cc-live.pres-badge--browsing .cc-live-dot {
  background: var(--text-3);
  box-shadow: none;
  animation: none;
}

/* Live viewers bar — recording tag */
.live-viewer-rec {
  font-size: 0.6rem; font-weight: 700;
  color: var(--color-danger);
  vertical-align: middle;
}

/* ── Admin: section wrapper (shared by Services + Costs tabs) ─── */
.admin-section { margin-bottom: 2rem; }
.admin-section-label {
  font-size: var(--font-size-xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--color-text-tertiary); margin-bottom: 0.75rem;
}

/* ── Cost / Pricing Calculator ───────────────────────────────────── */
.cost-calc-summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 0.75rem;
  margin-bottom: 1rem;
}
.cost-calc-card {
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 0.6rem 0.75rem;
}
.cost-calc-card-lbl { font-size: var(--font-size-xs); color: var(--text-3); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 0.25rem; }
.cost-calc-card-val { font-family: var(--type-mono); font-size: var(--font-size-lg); font-weight: 700; color: var(--text); line-height: 1.1; }
.cost-calc-card-sub { font-size: var(--font-size-xs); color: var(--text-3); margin-top: 0.25rem; }
/* v1164: per-session split rows surfaced below the blended value. Mono
   font + right-aligned values so the numbers line up vertically across
   live / manual rows. */
.cost-calc-card--split { padding-bottom: 0.4rem; }
.cost-calc-split-rows {
  margin-top: 0.45rem;
  padding-top: 0.4rem;
  border-top: 1px dashed color-mix(in srgb, var(--border) 70%, transparent);
  display: flex;
  flex-direction: column;
  gap: 0.18rem;
}
.cost-calc-split-row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 0.5rem;
  align-items: baseline;
  font-size: var(--font-size-xs);
}
.cost-calc-split-lbl  { color: var(--text-3); text-transform: uppercase; letter-spacing: 0.04em; }
.cost-calc-split-val  { font-family: var(--type-mono); color: var(--text-1); font-weight: 600; text-align: right; }
.cost-calc-split-meta { color: var(--text-3); font-family: var(--type-mono); text-align: right; }

/* ── Trends tab (v1164) ─────────────────────────────────────────────────────
   MTD/QTD/YTD spend cards + SVG charts + outlier table. Composes spec
   .tf-card chassis throughout; numerics in mono so columns line up. */
.trends-period-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}
.trends-period-card { padding: var(--space-4); cursor: default; }
.trends-period-card:hover { transform: none; box-shadow: none; border-color: var(--border); }
.trends-period-lbl {
  font-size: var(--font-size-xs);
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 0.4rem;
}
.trends-period-val {
  font-family: var(--type-mono);
  font-size: 1.6rem;
  font-weight: 700;
  color: var(--text-1);
  line-height: 1.05;
}
.trends-period-sub {
  font-family: var(--type-mono);
  font-size: var(--font-size-xs);
  color: var(--text-3);
  margin-top: 0.35rem;
}
.trends-period-card--proj { border-color: color-mix(in srgb, var(--accent-teal) 35%, var(--border)); }
.trends-period-card--proj .trends-period-val { color: var(--accent-teal); }

.trends-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
}
@media (max-width: 920px) { .trends-grid { grid-template-columns: 1fr; } }
.trends-chart-card { padding: var(--space-4); cursor: default; }
.trends-chart-card:hover { transform: none; box-shadow: none; border-color: var(--border); }
.trends-chart-card--wide { grid-column: 1 / -1; }
.trends-chart-hdr {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border);
}
.trends-chart-sub {
  font-family: var(--type-mono);
  font-size: var(--font-size-xs);
  color: var(--text-3);
  margin-left: auto;
}

.trends-chart {
  width: 100%;
  height: auto;
  display: block;
  font-family: var(--type-mono);
}
.trends-grid-line {
  stroke: color-mix(in srgb, var(--border) 65%, transparent);
  stroke-width: 1;
  stroke-dasharray: 2 3;
}
.trends-grid-label {
  fill: var(--text-3);
  font-size: 10px;
}
.trends-line {
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.trends-line--teal  { stroke: var(--accent-teal); }
.trends-line--amber { stroke: var(--accent-amber); }
.trends-dot {
  fill: var(--accent-teal);
  stroke: var(--bg);
  stroke-width: 1.5;
}
.trends-dot--over { fill: var(--accent-red); }
.trends-target-line {
  stroke: var(--accent-red);
  stroke-width: 1.25;
  stroke-dasharray: 4 4;
  opacity: 0.7;
}
.trends-target-label {
  fill: var(--accent-red);
  font-size: 10px;
  opacity: 0.85;
}

.trends-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-2);
  font-size: var(--font-size-xs);
  color: var(--text-3);
}
.trends-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--type-mono);
}
.trends-legend-swatch {
  width: 10px;
  height: 10px;
  border-radius: 2px;
  display: inline-block;
}

.trends-top-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--font-size-sm);
}
.trends-top-table th,
.trends-top-table td {
  padding: 0.45rem 0.5rem;
  border-bottom: 1px solid var(--border);
  text-align: left;
}
.trends-top-table th {
  font-size: var(--font-size-xs);
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-weight: 700;
}
.trends-top-table tr:last-child td { border-bottom: 0; }
.trends-num { text-align: right; font-variant-numeric: tabular-nums; font-family: var(--type-mono); }
.trends-over-badge {
  display: inline-block;
  margin-left: 0.35rem;
  padding: 1px 0.4rem;
  font-size: 0.65rem;
  font-weight: 700;
  font-family: var(--type-mono);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--accent-red) 18%, transparent);
  color: var(--accent-red);
}
.trends-retry-badge {
  display: inline-block;
  padding: 1px 0.4rem;
  font-size: 0.65rem;
  font-weight: 700;
  font-family: var(--type-mono);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--accent-amber) 18%, transparent);
  color: var(--accent-amber);
}

/* v1166: per-segment target ticks on the Cost Composition chart.
   A short colored dash drawn across each bar at the cumulative-target
   y-coordinate for that segment. Segment bars whose actual top exceeds
   the corresponding tick get a thin amber outline so the "over target"
   read carries without relying on a tooltip. */
.trends-bar-seg--over {
  stroke: var(--accent-amber);
  stroke-width: 1.5;
}
.trends-legend-tick {
  display: inline-block;
  width: 16px;
  height: 0;
  border-top: 1.5px dashed var(--text-2);
  margin: 0 2px;
}
.trends-legend-target-mark {
  display: inline-block;
  width: 0;
  height: 14px;
  border-left: 1.5px dashed var(--accent-red);
  margin: 0 6px;
  vertical-align: middle;
}
.trends-legend-item--meta { color: var(--text-3); }

/* v1166: all-sessions horizontal-bar list. Each row is one session
   rendered as a stacked horizontal bar (fixed | live tx | AI) sorted
   most-expensive first. Per-session target shows as a red dashed
   vertical tick at the position corresponding to the target total. */
.trends-allsess-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-height: 360px;
  overflow-y: auto;
  padding-right: 4px;
}
.trends-allsess-row {
  display: grid;
  grid-template-columns: minmax(160px, 32%) 1fr;
  gap: var(--space-3);
  align-items: center;
  font-size: var(--font-size-sm);
}
.trends-allsess-label {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  overflow: hidden;
}
.trends-allsess-camp {
  font-family: var(--type-mono);
  font-size: var(--font-size-xs);
  color: var(--text-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 8rem;
  flex-shrink: 0;
}
.trends-allsess-title {
  font-weight: 600;
  color: var(--text-1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1;
  min-width: 0;
}
.trends-allsess-bar-wrap {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--space-3);
  align-items: center;
}
.trends-allsess-bar {
  position: relative;
  display: flex;
  height: 16px;
  background: color-mix(in srgb, var(--border) 50%, transparent);
  border-radius: 3px;
  overflow: visible; /* let the target tick poke a touch above/below */
}
.trends-allsess-seg {
  display: block;
  height: 100%;
  min-width: 1px;
}
.trends-allsess-seg:first-child { border-radius: 3px 0 0 3px; }
.trends-allsess-seg:last-child  { border-radius: 0 3px 3px 0; }
.trends-allsess-seg--fixed { background: var(--text-3); opacity: 0.7; }
.trends-allsess-seg--aai   { background: var(--accent-blue); }
.trends-allsess-seg--ai    { background: var(--accent-teal); }
.trends-allsess-target {
  position: absolute;
  top: -3px;
  bottom: -3px;
  width: 0;
  border-left: 1.5px dashed var(--accent-red);
  pointer-events: none;
  opacity: 0.85;
}
/* v1168: 3 per-segment target ticks, one for each component. Each tick is
   a thin vertical line color-matched to its segment, drawn at the
   cumulative target boundary (the right edge of where that segment SHOULD
   end). A segment whose bar pushes past its colored tick is over budget
   for that component. */
.trends-allsess-tick {
  position: absolute;
  top: -3px;
  bottom: -3px;
  width: 0;
  border-left: 1.5px dashed currentColor;
  pointer-events: none;
  opacity: 0.85;
}
.trends-allsess-tick--fixed { color: var(--text-2); }
.trends-allsess-tick--aai   { color: var(--accent-blue); }
.trends-allsess-tick--ai    { color: var(--accent-teal); }
/* Over-budget segment: amber outline so the offending component reads
   without needing the tooltip. Matches the composition chart's recipe. */
.trends-allsess-seg.trends-allsess-seg--over {
  outline: 1.5px solid var(--accent-amber);
  outline-offset: -0.5px;
}
.trends-allsess-total {
  font-family: var(--type-mono);
  font-weight: 700;
  color: var(--text-1);
  white-space: nowrap;
  font-size: var(--font-size-sm);
}
.trends-live-badge {
  display: inline-block;
  padding: 1px 0.4rem;
  font-size: 0.6rem;
  font-weight: 700;
  font-family: var(--type-mono);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--accent-teal) 18%, transparent);
  color: var(--accent-teal);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.trends-manual-badge {
  display: inline-block;
  padding: 1px 0.4rem;
  font-size: 0.6rem;
  font-weight: 700;
  font-family: var(--type-mono);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--text-3) 18%, transparent);
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.cost-calc-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem; }
@media (max-width: 720px) { .cost-calc-grid { grid-template-columns: 1fr; } }
.cost-calc-block {
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 0.75rem 0.85rem;
  margin-bottom: 1rem;
}
.cost-calc-block-hdr { font-size: var(--font-size-sm); font-weight: 600; color: var(--text-2); margin-bottom: 0.5rem; }
.cost-calc-block-sub { font-size: var(--font-size-xs); color: var(--text-3); font-weight: 400; }
.cost-calc-fixed-grid { display: grid; gap: 0.4rem; }
.cost-calc-fixed-row { display: grid; grid-template-columns: 1fr auto 7rem auto; gap: 0.4rem; align-items: center; }
.cost-calc-fixed-label { font-size: var(--font-size-sm); color: var(--text-2); }
.cost-calc-fixed-prefix, .cost-calc-fixed-suffix { color: var(--text-3); font-size: var(--font-size-xs); }
.cost-calc-fixed-input, .cost-calc-margin-input, .cost-calc-addon-input, .cost-calc-target-input {
  background: var(--bg-1); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 0.2rem 0.4rem; font: inherit;
}
.cost-calc-fixed-input  { width: 7rem; text-align: right; }
.cost-calc-margin-input { width: 4rem; text-align: right; }
.cost-calc-addon-input  { width: 5rem; text-align: right; }
.cost-calc-target-input { width: 6rem; text-align: right; }

/* Primary editable controls in the summary cards — make the value itself
   the input so the affordance is unmissable. The `--input` modifier swaps
   the static number for an inline editor sized to the same big-number scale. */
.cost-calc-card--editable { background: color-mix(in srgb, var(--accent-teal) 4%, var(--bg-2)); }
.cost-calc-card-val--input {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: var(--font-size-lg);
}
.cost-calc-bignum {
  display: inline-flex;
  align-items: baseline;
  font-size: var(--font-size-lg);
  font-weight: 700;
  color: var(--text);
  line-height: 1.1;
}
.cost-calc-bignum-input {
  background: transparent;
  color: var(--text);
  border: none;
  border-bottom: 2px dashed color-mix(in srgb, var(--accent-teal) 60%, transparent);
  padding: 0 0.15rem;
  font: inherit;
  font-weight: 700;
  width: 4rem;
  text-align: right;
  cursor: text;
}
.cost-calc-bignum-input:focus {
  outline: none;
  border-bottom-color: var(--accent-teal);
  border-bottom-style: solid;
}
.cost-calc-margin-range {
  width: 100%;
  accent-color: var(--accent-teal);
  height: 1.25rem;
  cursor: pointer;
}
.cost-calc-bucket-table { width: 100%; border-collapse: collapse; }
.cost-calc-bucket-table td { padding: 0.25rem 0.4rem; font-size: var(--font-size-sm); border-bottom: 1px dashed var(--border); }
.cost-calc-bucket-table tr:last-child td { border-bottom: none; }
.cost-calc-bucket-name { color: var(--text-2); text-transform: capitalize; }
.cost-calc-tier-table { width: 100%; border-collapse: collapse; font-size: var(--font-size-sm); }
.cost-calc-tier-table th, .cost-calc-tier-table td { padding: 0.4rem 0.5rem; border-bottom: 1px solid var(--border); }
.cost-calc-tier-table th { font-weight: 600; color: var(--text-3); text-align: left; font-size: var(--font-size-xs); text-transform: uppercase; letter-spacing: 0.04em; }
.cost-calc-tier-table td { color: var(--text); }
.cost-calc-tier-name { color: var(--text); }
.cost-calc-num { text-align: right; font-variant-numeric: tabular-nums; }
.cost-calc-price { color: var(--accent-teal, #2f9aa3); }
.cost-calc-arch-col { color: var(--text-2); font-size: var(--font-size-xs); }
.cost-calc-arch em { color: var(--text-3); font-style: normal; }
.cost-calc-capped {
  display: inline-block;
  margin-left: 0.3rem;
  padding: 1px 0.4rem;
  font-size: 0.65rem;
  font-weight: 700;
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--color-ember, #d4753a) 18%, transparent);
  color: var(--color-ember, #d4753a);
  text-transform: uppercase;
}
.cost-calc-capped--mono {
  background: color-mix(in srgb, var(--accent-teal, #2f9aa3) 18%, transparent);
  color: var(--accent-teal, #2f9aa3);
}
.cost-calc-addon-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding-top: 0.6rem;
  margin-top: 0.6rem;
  border-top: 1px dashed var(--border);
  flex-wrap: wrap;
  font-size: var(--font-size-sm);
}
.cost-calc-sanity {
  padding: 0.6rem 0.85rem;
  border-radius: var(--radius-md);
  font-size: var(--font-size-sm);
  border: 1px solid var(--border);
}
.cost-calc-sanity--ok   { background: color-mix(in srgb, var(--color-success, #2da84a) 12%, transparent); border-color: color-mix(in srgb, var(--color-success, #2da84a) 35%, transparent); }
.cost-calc-sanity--warn { background: color-mix(in srgb, var(--color-ember, #d4753a) 12%, transparent); border-color: color-mix(in srgb, var(--color-ember, #d4753a) 35%, transparent); }
.cost-calc-sanity--bad  { background: color-mix(in srgb, var(--accent-red, #d4724f) 14%, transparent); border-color: color-mix(in srgb, var(--accent-red, #d4724f) 40%, transparent); }

/* ── Admin: Costs tab ─────────────────────────────────────────── */
.costs-table {
  width: 100%; border-collapse: collapse;
  font-size: var(--font-size-sm);
}
.costs-th {
  text-align: left; font-size: var(--font-size-xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--color-text-tertiary);
  padding: 0.4rem 0.75rem; border-bottom: 1px solid var(--color-border);
}
.costs-th--num { text-align: right; }
.costs-td {
  padding: 0.55rem 0.75rem; border-bottom: 1px solid color-mix(in srgb, var(--color-border) 50%, transparent);
  color: var(--color-text-primary);
}
.costs-td--service { font-weight: 600; }
.costs-td--num { text-align: right; font-variant-numeric: tabular-nums; }
.costs-td--note { color: var(--color-text-tertiary); font-size: var(--font-size-xs); }
.costs-free { color: var(--color-success); font-weight: 600; }
.costs-total-row td {
  border-top: 2px solid var(--color-border); border-bottom: none;
  padding-top: 0.65rem;
}

/* Anthropic consumption bar */
.costs-ai-block { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: var(--space-4); }
.costs-ai-row { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.6rem; flex-wrap: wrap; }
.costs-ai-label { font-size: var(--font-size-sm); color: var(--color-text-secondary); }
.costs-ai-val { font-size: 1.15rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.costs-ai-val--spent { color: var(--color-brand); }
.costs-progress-track { height: 8px; background: var(--color-surface-elevated); border-radius: var(--radius-full); overflow: hidden; margin-bottom: 0.4rem; }
.costs-progress-bar { height: 100%; background: var(--color-brand); border-radius: var(--radius-full); transition: width var(--t-trans) ease; }
.costs-progress-bar--warn { background: var(--color-warning); }
.costs-ai-pct { font-size: var(--font-size-xs); color: var(--color-text-tertiary); }

/* Burn total cards */
.costs-burn-row { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 0.75rem; }
.costs-burn-card {
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius-lg); padding: 0.75rem 1.25rem; text-align: center; min-width: 110px;
}
.costs-burn-card--total { border-color: var(--color-brand); background: color-mix(in srgb, var(--color-brand) 8%, var(--color-surface)); }
.costs-burn-label { font-size: var(--font-size-xs); color: var(--color-text-tertiary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.3rem; }
.costs-burn-val { font-size: 1.2rem; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--color-text-primary); }
.costs-burn-sep { font-size: 1.25rem; color: var(--color-text-tertiary); font-weight: 300; }
.costs-note { font-size: var(--font-size-xs); color: var(--color-text-tertiary); margin: 0; }
.svc-card {
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius-lg); padding: var(--space-4); position: relative;
}
.svc-card::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0;
  width: 3px; border-radius: var(--radius-lg) 0 0 var(--radius-lg);
}
.svc-status-ok::before    { background: var(--color-success); }
.svc-status-warn::before  { background: var(--color-warning); }
.svc-status-danger::before{ background: var(--color-danger); }
.svc-card-header {
  display: flex; justify-content: space-between; align-items: center;
  margin-bottom: 0.75rem;
}
.svc-card-title { font-size: var(--font-size-base); font-weight: 600; color: var(--color-text-primary); }
.svc-stats-row {
  display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.5rem;
}
.svc-stat-val {
  font-size: var(--font-size-lg); font-weight: 600; color: var(--color-text-primary);
}
.svc-stat-lbl {
  font-size: var(--font-size-xs); color: var(--color-text-tertiary); margin-top: 0.1rem;
}
.text-danger { color: var(--color-danger) !important; }
.svc-quota-group { margin-bottom: 0.75rem; display: flex; flex-direction: column; gap: 0.35rem; }
.svc-quota-row { display: flex; align-items: center; gap: 0.5rem; }
.svc-quota-label { font-size: var(--font-size-xs); color: var(--color-text-secondary); min-width: 140px; }
.svc-quota-bar {
  flex: 1; height: 6px; background: var(--color-surface-elevated);
  border-radius: var(--radius-full); overflow: hidden;
}
.svc-quota-fill {
  height: 100%; background: var(--color-brand);
  border-radius: var(--radius-full); transition: width var(--t-trans);
}
.svc-quota-fill.svc-quota-warn { background: var(--color-warning); }
.svc-quota-pct { font-size: var(--font-size-xs); color: var(--color-text-tertiary); min-width: 32px; text-align: right; }
.svc-history {
  display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.5rem;
}
.svc-hist-item {
  display: flex; flex-direction: column; align-items: center;
  background: var(--color-surface-elevated); border-radius: var(--radius-md);
  padding: 0.2rem 0.5rem;
}
.svc-hist-month { font-size: var(--font-size-xs); color: var(--color-text-tertiary); }
.svc-hist-cost  { font-size: var(--font-size-sm); font-weight: 600; color: var(--color-text-primary); }
.svc-update-row {
  display: flex; gap: 0.5rem; align-items: center; margin-top: 0.75rem;
  padding-top: 0.75rem; border-top: 1px solid var(--color-border);
}
.svc-balance-input, .svc-threshold-input {
  width: 110px; padding: 0.3rem 0.5rem; font-size: var(--font-size-sm);
  background: var(--color-bg); border: 1px solid var(--color-border);
  border-radius: var(--radius-md); color: var(--color-text-primary);
}
.svc-error-note { font-size: var(--font-size-xs); color: var(--color-danger); margin: 0.25rem 0; }


/* ═══════════════════════════════════════════════════════════════════════════
   ARCANE IDENTITY — Modern Campaign Command Center
   Layered over the design system base. All values use design tokens.
   Goal: premium dark fantasy companion, not a medieval prop.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Page atmosphere (dark mode only — subtle teal bloom at top) ─── */
[data-theme="dark"] body {
  /* Must use background-image (not shorthand) to preserve the bg image from the base body rule.
     v1105: aligned to spec verbatim. Wash opacity 0.88 → 0.82 (spec
     value) lets the parchment texture read through. The previous higher
     floor (0.88) was a defensive choice for dense text legibility;
     user has now explicitly asked for design-language fidelity over
     the legibility-floor override.
     v1138: reverted v1137's well-meaning-but-wrong wash shift. The
     page-level bg was already perfect; the cobalt-card request was for
     CARD bg specifically — applied as a .tf-card dark-mode override
     below. */
  background-image:
    radial-gradient(ellipse 160% 40% at 50% -5%,
      color-mix(in srgb, var(--color-brand) 14%, transparent) 0%,
      transparent 100%),
    linear-gradient(rgba(4, 8, 14, 0.82), rgba(4, 8, 14, 0.82)),
    image-set(url('/static/bg.webp') type('image/webp'), url('/static/bg.png') type('image/png'));
  background-size: auto, auto, cover;
  background-position: center, center, center;
  background-attachment: fixed, fixed, fixed;
}

/* v1092: cancel-on-dark rule retired with the light drop-shadow
   workaround. Both themes now render the SVG lockup cleanly. */

/* ── Active tab — arcane glow on the underline accent ──────────────────── */
[data-theme="dark"] .tab-btn.active {
  text-shadow: 0 0 18px color-mix(in srgb, var(--color-brand) 75%, transparent);
}

/* ── Campaign cards — arcane presence ─────────────────────────────────── */
/* Watermark via background-image with opacity baked into the SVG.
   Background-image renders behind all content automatically — no z-index needed. */
.campaign-card {
  background-image: url("data:image/svg+xml,%3Csvg opacity='0.18' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='46' fill='none' stroke='%232F7C85' stroke-width='2.2'/%3E%3Ccircle cx='50' cy='50' r='34' fill='none' stroke='%232F7C85' stroke-width='1.4'/%3E%3Ccircle cx='50' cy='50' r='19' fill='none' stroke='%232F7C85' stroke-width='1.4'/%3E%3Cline x1='50' y1='4' x2='50' y2='96' stroke='%232F7C85' stroke-width='1.2'/%3E%3Cline x1='4' y1='50' x2='96' y2='50' stroke='%232F7C85' stroke-width='1.2'/%3E%3Cline x1='18' y1='18' x2='82' y2='82' stroke='%232F7C85' stroke-width='0.9'/%3E%3Cline x1='82' y1='18' x2='18' y2='82' stroke='%232F7C85' stroke-width='0.9'/%3E%3Cpath d='M50 16 L84 50 L50 84 L16 50 Z' fill='none' stroke='%232F7C85' stroke-width='1.2'/%3E%3C/svg%3E");
  background-size: 70px 70px;
  background-repeat: no-repeat;
  background-position: right 6px bottom 6px;
}

/* Cover image variant: layer (front → back) watermark over a dark gradient over the image.
   Watermark uses the same teal stroke as cards without a cover (#2F7C85) for visual consistency.
   Doubled selector (.campaign-card.campaign-card--has-cover) to win the cascade against the
   later .campaign-card rule that also sets background-image. */
.campaign-card.campaign-card--has-cover {
  background-image:
    url("data:image/svg+xml,%3Csvg opacity='0.18' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='46' fill='none' stroke='%232F7C85' stroke-width='2.2'/%3E%3Ccircle cx='50' cy='50' r='34' fill='none' stroke='%232F7C85' stroke-width='1.4'/%3E%3Ccircle cx='50' cy='50' r='19' fill='none' stroke='%232F7C85' stroke-width='1.4'/%3E%3Cline x1='50' y1='4' x2='50' y2='96' stroke='%232F7C85' stroke-width='1.2'/%3E%3Cline x1='4' y1='50' x2='96' y2='50' stroke='%232F7C85' stroke-width='1.2'/%3E%3Cline x1='18' y1='18' x2='82' y2='82' stroke='%232F7C85' stroke-width='0.9'/%3E%3Cline x1='82' y1='18' x2='18' y2='82' stroke='%232F7C85' stroke-width='0.9'/%3E%3Cpath d='M50 16 L84 50 L50 84 L16 50 Z' fill='none' stroke='%232F7C85' stroke-width='1.2'/%3E%3C/svg%3E"),
    var(--cover-tint, linear-gradient(transparent, transparent)),
    linear-gradient(180deg, rgba(11,15,20,0.68) 0%, rgba(11,15,20,0.45) 50%, rgba(11,15,20,0.72) 100%),
    var(--cover-img, linear-gradient(transparent, transparent));
  background-size: 70px 70px, auto, auto, cover;
  background-repeat: no-repeat, no-repeat, no-repeat, no-repeat;
  background-position: right 6px bottom 6px, center, center, center top;
}
.campaign-card.campaign-card--has-cover:hover, .campaign-card.campaign-card--has-cover:focus-visible { border-color: rgba(47,154,163,0.6); }
/* Slightly brighter meta text on covered cards — the default --text-3 reads too dim against the image */
.campaign-card.campaign-card--has-cover .campaign-card-meta { color: color-mix(in srgb, var(--text-3) 55%, #ffffff 45%); }

/* Settings-modal preview: same layering as the card so the GM sees what players will see */
.camp-cover-preview {
  /* Mirrors the .campaign-card sizing on the campaign selector exactly:
     16:9 + max-width 480px (campaign-selector grid uses
     minmax(480px, 1fr) so 480px is the floor card width). No surprise
     between "preview on Manage" and "what shows on the selector." */
  aspect-ratio: 16 / 9;
  width: 100%;
  max-width: 480px;
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  margin-bottom: 0.55rem;
  background-image:
    linear-gradient(180deg, rgba(11,15,20,0.45) 0%, rgba(11,15,20,0.25) 50%, rgba(11,15,20,0.55) 100%),
    var(--cover-img, none);
  background-size: auto, cover;
  background-repeat: no-repeat;
  background-position: center, center top;
}
.camp-cover-preview.hidden { display: none; }

[data-theme="dark"] .campaign-card {
  transition: border-color var(--t-hover) ease, box-shadow var(--t-hover) ease, transform var(--t-hover) ease;
}

/* Hover — grounded lift, no glow (dark mode) */
[data-theme="dark"] .campaign-card:hover, [data-theme="dark"] .campaign-card:focus-visible {
  border-color: rgba(79,154,163,0.35);
  box-shadow: 0 6px 16px rgba(0,0,0,0.45);
  transform: translateY(-1px);
}

/* ── GM badge — ember insignia (canonical rule is in .camp-role-gm base above) ── */

/* GM tabs dark-mode glow removed — they now match normal tab active style */

/* ── Live indicators — gold ──────────────────────────────────────────── */
.campaign-live-badge             { color: var(--color-gold); }
.camp-live-dot                   { background: var(--color-gold);
                                   animation: live-dot-pulse 2.6s ease-in-out infinite; }
#live-nav-btn.live-active {
  color: var(--color-gold);
  background: color-mix(in srgb, var(--color-gold) 13%, transparent);
  border-color: color-mix(in srgb, var(--color-gold) 60%, transparent);
  animation: live-btn-breathe-active 2.4s ease-in-out infinite;
}
@keyframes live-btn-breathe-active {
  0%, 100% { box-shadow: 0 0 4px 0   color-mix(in srgb, var(--color-gold) 25%, transparent); }
  50%       { box-shadow: 0 0 18px 4px color-mix(in srgb, var(--color-gold) 32%, transparent); }
}
.live-presence-badge             {
  background:  color-mix(in srgb, var(--color-gold) 13%, transparent);
  color:       var(--color-gold);
  border-color: color-mix(in srgb, var(--color-gold) 28%, transparent);
}

/* v808: Light-theme Live nav pill — INVERTED. Solid gold fill, white
   letters, gentle opacity pulse so it breathes on the page without
   shifting the background colour. Replaces the dark-theme `live-btn-flicker`
   animation (which mutates background through transparent color-mix steps —
   that would re-render our solid fill as semi-transparent gold mid-cycle).
   The pulse-light keyframes only touch opacity, so the gold pill stays
   gold and the white text rides along. Same treatment carries to hover,
   .live-active, and the .live-presence-badge count chip beside it. */
html:not([data-theme="dark"]) #live-nav-btn {
  background: var(--color-gold);
  border-color: var(--color-gold);
  color: #ffffff;
  animation: live-btn-pulse-light 2.8s ease-in-out infinite;
  box-shadow: 0 1px 2px rgba(202,138,4,0.25);
}
html:not([data-theme="dark"]) #live-nav-btn .tf-tab-mark { opacity: 1; }
html:not([data-theme="dark"]) #live-nav-btn:hover,
html:not([data-theme="dark"]) #live-nav-btn:focus-visible {
  background: var(--color-gold);
  color: #ffffff;
  border-color: var(--color-gold);
  filter: brightness(1.08);
  box-shadow: 0 0 14px 3px color-mix(in srgb, var(--color-gold) 40%, transparent);
}
html:not([data-theme="dark"]) #live-nav-btn.live-active {
  background: var(--color-gold);
  border-color: var(--color-gold);
  color: #ffffff;
}
/* v1136: light-theme presence-badge — same de-pilled treatment as the
   dark default. Only the text color flips to white so the count + dot
   read against the solid-gold #live-nav-btn.live-active bg. */
html:not([data-theme="dark"]) .live-presence-badge {
  color: #ffffff;
}
@keyframes live-btn-pulse-light {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.78; }
}

/* v810: Profile button — the rest state (border + drop) is fine, but
   .nav-avatar-img DEFAULT carries `border: 2px solid var(--accent-teal)`
   (= #2F7C85 on light = mineral teal). Inside #cs-profile-btn /
   #profile-nav-btn we already null that with `border: none`, but on hover
   the canonical .nav-profile-btn:hover rule adds an outer
   `box-shadow: 0 0 0 2px rgba(47,124,133,0.25)` halo — that's the "ultra
   bright thick circle" on the parchment wash. The dark-theme reading is
   "elegant teal accent ring"; on light it's an angry blue-green halo.
   Quiet it to a soft ink ring that matches the rest of the light-theme
   premium parity treatment. */
html:not([data-theme="dark"]) .nav-profile-btn:hover .nav-avatar-img,
html:not([data-theme="dark"]) .nav-profile-btn:focus-visible .nav-avatar-img {
  border-color: var(--color-border-strong);
  box-shadow: 0 0 0 1px rgba(15,23,42,0.10);
}
/* And drop the .nav-avatar-img default 2px teal ring on light — at the
   sizes it's used (28-40px round) the saturated teal reads as a halo.
   Use a light slate ring so the avatar reads as a framed portrait. */
html:not([data-theme="dark"]) .nav-avatar-img {
  border-color: var(--color-border-strong);
}

/* Campaign card live badge — base uses `rgba(11,15,20,0.55)` dark fill so
   gold text reads against the dark card. On the parchment wash that's a
   dark hole pill on a bright card. Flip the FILL to warm cream and let
   the gold border + gold dot/label do the signaling — same recipe shape
   as the dark version, just inverted for light surfaces. Recording
   variant follows: cream fill, red border + red dot/label preserved. */
html:not([data-theme="dark"]) .campaign-card .campaign-live-badge {
  background: rgba(252,247,237,0.92);
  border-color: var(--color-gold);
  box-shadow: 0 1px 2px rgba(202,138,4,0.18);
}
html:not([data-theme="dark"]) .campaign-card .campaign-live-badge.camp-badge--recording {
  border-color: var(--color-danger);
  box-shadow: 0 1px 2px rgba(220,38,38,0.18);
}
/* Camp-presence-pill (main app header — "N online") — base 10% green fill
   on transparent gives a faint green stain on parchment. Bump to a real
   light-mint pill with an ink-leaning green text so the count reads. */
html:not([data-theme="dark"]) .camp-presence-pill {
  background: color-mix(in srgb, var(--color-success) 14%, #ffffff);
  border-color: var(--color-success);
  color: #14532D;
}
html:not([data-theme="dark"]) .camp-presence-pill::before {
  background: var(--color-success);
}

/* Header live dot (recording active) stays red — recording = red convention */
/* Session-active dot in nav uses gold */

@keyframes live-dot-pulse {
  0%, 100% { opacity: 1;    box-shadow: 0 0 0 0   color-mix(in srgb, var(--color-gold) 55%, transparent); }
  50%       { opacity: 0.75; box-shadow: 0 0 0 5px color-mix(in srgb, var(--color-gold)  0%, transparent); }
}

/* ── Live session screen — arcane atmosphere (both modes) ─────────────── */
#live-session-screen:not(.hidden) {
  border-top: 2px solid color-mix(in srgb, var(--color-gold) 35%, transparent);
  background:
    radial-gradient(ellipse 70% 18% at 50% 0%,
      color-mix(in srgb, var(--color-gold) 5%, transparent) 0%,
      transparent 100%);
}
[data-theme="dark"] #live-session-screen:not(.hidden) {
  background:
    radial-gradient(ellipse 80% 22% at 50% 0%,
      color-mix(in srgb, var(--color-gold) 7%, transparent) 0%,
      transparent 100%);
}

/* Live header — very faint gold warmth when session is running */
[data-theme="dark"] .live-header {
  background: linear-gradient(
    to right,
    var(--color-bg),
    color-mix(in srgb, var(--color-gold) 3%, var(--color-bg)) 50%,
    var(--color-bg)
  );
}

/* ── AI story beats (system_event) — gold ────────────────────────────── */
.live-log-entry[data-tag="system_event"] {
  border-left-color: var(--color-gold);
  background: color-mix(in srgb, var(--color-gold) 5%, transparent);
}

/* ── AI combat beats (combat_event) — amber ──────────────────────────── */
.live-log-entry[data-tag="combat_event"] {
  border-left-color: var(--color-warning);
  background: color-mix(in srgb, var(--color-warning) 5%, transparent);
}

/* ── AI context beats (context_event) — muted violet, italic ─────────── */
.live-log-entry[data-tag="context_event"] {
  border-left-color: var(--color-muted-violet);
  background: color-mix(in srgb, var(--color-muted-violet) 6%, transparent);
  font-style: italic;
  color: var(--color-text-secondary);
}

/* Scan-pulse ripple uses gold */
.live-log-entry--scan {
  border-left-color: var(--color-gold);
  animation: le-arrive 0.28s ease-out, le-scan-pulse-gold 2.2s ease-out 0.28s forwards;
}
@keyframes le-scan-pulse-gold {
  0%   { box-shadow: 0 0 0 0   color-mix(in srgb, var(--color-gold) 55%, transparent); }
  40%  { box-shadow: 0 0 0 5px color-mix(in srgb, var(--color-gold) 18%, transparent); }
  100% { box-shadow: 0 0 0 0   transparent; }
}

/* ── Live sidebar + chat drawer — faint teal depth gradient ────────────── */
[data-theme="dark"] .live-sidebar,
[data-theme="dark"] .live-ai-drawer {
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--color-brand) 3%, var(--color-bg)) 0%,
    var(--color-bg) 14%);
}

/* ── Session cards — grounded lift hover (dark mode) ──────────────────── */
[data-theme="dark"] .session-card {
  transition: border-color var(--t-hover) ease, box-shadow var(--t-hover) ease, transform var(--t-hover) ease;
}
[data-theme="dark"] .session-card:hover, [data-theme="dark"] .session-card:focus-visible {
  border-color: rgba(79,154,163,0.35);
  box-shadow: 0 6px 16px rgba(0,0,0,0.45);
  transform: translateY(-1px);
}

/* ── Live preamble header — gold title, split bullet diamonds ─────────── */
.live-preamble-title   { color: var(--color-gold); }
.live-preamble-bullet::before { color: var(--split-color); }


/* ── Arcane watermark (v1087) ─────────────────────────────────────────────
   The card-type watermarks (session / party / entity / quest / fates) now
   come from the brand-spec .tf-card--watermarked rule on the card chassis
   itself. The legacy per-inner-element watermark recipes (the long
   selector list at this point in styles.css) were retired so each card
   doesn't double-paint the sigil. Only .live-preamble keeps the legacy
   recipe since it's not a .tf-card. */
.live-preamble {
  background-image: url("data:image/svg+xml,%3Csvg opacity='0.18' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='46' fill='none' stroke='%232F7C85' stroke-width='2.2'/%3E%3Ccircle cx='50' cy='50' r='34' fill='none' stroke='%232F7C85' stroke-width='1.4'/%3E%3Ccircle cx='50' cy='50' r='19' fill='none' stroke='%232F7C85' stroke-width='1.4'/%3E%3Cline x1='50' y1='4' x2='50' y2='96' stroke='%232F7C85' stroke-width='1.2'/%3E%3Cline x1='4' y1='50' x2='96' y2='50' stroke='%232F7C85' stroke-width='1.2'/%3E%3Cline x1='18' y1='18' x2='82' y2='82' stroke='%232F7C85' stroke-width='0.9'/%3E%3Cline x1='82' y1='18' x2='18' y2='82' stroke='%232F7C85' stroke-width='0.9'/%3E%3Cpath d='M50 16 L84 50 L50 84 L16 50 Z' fill='none' stroke='%232F7C85' stroke-width='1.2'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
}

/* Fates saved-card header — same recipe as .quest-nav-hdr verbatim
   (v929). User direction: "DUPLICATE the way the quest card is
   formatted." */
/* v1087: .session-card-hdr / .quest-nav-hdr / .entity-row-hdr / .party-pc-card /
   .fates-saved-card-hdr per-card watermark size + position rules retired —
   the canonical .tf-card--watermarked recipe (which scales with --md/--sm/--xs)
   provides the sigil on the chassis. */

/* Live preamble body — sigil bleeds from bottom-right */
.live-preamble {
  background-size: 110px 110px;
  background-position: right -6px bottom -6px;
}

/* ═══════════════════════════════════════════════════════════════════════════
   VISUAL REFINEMENT PASS — teal + ember gold hierarchy, arcane atmosphere
   ═══════════════════════════════════════════════════════════════════════════ */

/* v1161: This block used to redefine .entity-link[data-type] colors AFTER
   the canonical block at ~line 7221, which won the cascade and left
   crosslinks out of sync with entity lead bars / thumbs / pins. It also
   set faction=muted-violet, identical to NPC (RCCA for v1160 "Since when
   did NPCs and Factions have the same color?"). Removed — the canonical
   block (mirrors ENTITY_CFG in app.js) is now the single source of truth
   for entity crosslink color. Don't reintroduce a second selector list
   here. */

/* ── View-as bar — replace purple with dark indigo/teal + ember top edge */
/* v798: light-correct default (parchment-with-ember-edge), dark gets the
   indigo gradient. Pre-v798 the dark gradient was hardcoded for both
   themes — black bar across the top of light theme. */
#view-as-bar {
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--color-ember) 4%, var(--bg)) 0%,
    color-mix(in srgb, var(--color-ember) 8%, var(--bg)) 55%,
    color-mix(in srgb, var(--color-ember) 4%, var(--bg)) 100%);
  border-top: 2px solid color-mix(in srgb, var(--color-ember) 50%, transparent);
  border-bottom: 1px solid var(--border);
}
[data-theme="dark"] #view-as-bar {
  background: linear-gradient(90deg, #181430 0%, #0c2233 55%, #0a1e2e 100%);
  border-top: 2px solid color-mix(in srgb, var(--color-ember) 38%, transparent);
  border-bottom: 1px solid rgba(0,0,0,0.45);
}

/* v1094: legacy Chronicle border-left accent retired — the canonical
   .tf-beat recipe (defined earlier in this file) uses a dot prefix +
   bottom rule pattern instead. Keeping the block stub to avoid syntax
   error from the orphan close brace below; properties zeroed. */
.session-beat-header {
  font-size: inherit;
  padding-left: 0;
  border-left: 0;
  margin-top: 28px;
}

/* ── Campaign cards — radial teal glow layered behind sigil ──────────── */
.campaign-card {
  background-image:
    url("data:image/svg+xml,%3Csvg opacity='0.18' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='46' fill='none' stroke='%232F7C85' stroke-width='2.2'/%3E%3Ccircle cx='50' cy='50' r='34' fill='none' stroke='%232F7C85' stroke-width='1.4'/%3E%3Ccircle cx='50' cy='50' r='19' fill='none' stroke='%232F7C85' stroke-width='1.4'/%3E%3Cline x1='50' y1='4' x2='50' y2='96' stroke='%232F7C85' stroke-width='1.2'/%3E%3Cline x1='4' y1='50' x2='96' y2='50' stroke='%232F7C85' stroke-width='1.2'/%3E%3Cline x1='18' y1='18' x2='82' y2='82' stroke='%232F7C85' stroke-width='0.9'/%3E%3Cline x1='82' y1='18' x2='18' y2='82' stroke='%232F7C85' stroke-width='0.9'/%3E%3Cpath d='M50 16 L84 50 L50 84 L16 50 Z' fill='none' stroke='%232F7C85' stroke-width='1.2'/%3E%3C/svg%3E"),
    radial-gradient(circle at 88% 88%, color-mix(in srgb, var(--color-brand) 7%, transparent) 0%, transparent 52%);
  background-repeat: no-repeat;
  background-size: 70px 70px, 100%;
  background-position: right 6px bottom 6px, center;
}

/* Chevron — ember-gold tint on hover so it picks up the brand "Live" feel. */
.campaign-card:hover .campaign-enter-chevron,
.campaign-card:focus-within .campaign-enter-chevron {
  color: color-mix(in srgb, var(--color-ember) 60%, var(--color-gold));
}

/* ── Codex rows — softer grid, taller ─────────────────────────────────
   Hover treatment moved to the canonical .entity-row block (gap-separated
   cards, translateY + teal border + drop shadow). What remains here is
   resting border tint + the expanded row's elevated surface. The old
   2px left-border affordance + inset shadow on row-hdr were dropped — they
   re-implemented the very pattern we removed from .session-card last round. */
.entity-row {
  border-color: color-mix(in srgb, var(--color-border) 55%, transparent);
}
.entity-row.expanded {
  background: var(--color-surface-elevated);
}
.entity-row-hdr {
  min-height: 54px;
}

/* Decorative halo ring removed — campaign / session / party card icons
   don't carry one, and the standalone-card refactor of entity rows made
   the halo read as visual noise next to the new card border. */

/* ── Quest sidebar — selected row handled in main quest block above */

/* (Retired in v763) Intra-row hairlines were competing chrome — the
   .quest-obj-list `gap: 0.5rem` already separates rows cleanly. Other
   list patterns in the app (.session-list, .alpha-entries-grid,
   .party-cards-grid) rely on gap alone for the same reason. */

/* (v784) Quest action button hover glow retired — Mark Completed / Mark
   Failed / Reopen now use .btn-new which carries its own white-glow lift. */

/* ── Party PC cards — portrait vignette + frame glow on hover ─────────── */
.party-pc-portrait {
  position: relative;
}
[data-theme="dark"] .party-pc-portrait::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to right,
    transparent 60%,
    color-mix(in srgb, var(--color-surface) 55%, transparent) 100%
  );
  pointer-events: none;
}
/* Dark-mode hover override removed in v743 — canonical hover now lives on
   .party-pc-card itself (mirrors .campaign-card / .session-card / .entity-row).
   The previous full-saturation teal border + outer ring + inset glow was
   visually louder than the rest of the app's card hovers. */

/* ── Recording start button — idle gold glow pulse ──────────────────── */
.rec-start-btn {
  animation: rec-idle-gold 3.5s ease-in-out infinite;
}
.rec-start-btn:hover, .rec-start-btn:focus-visible { animation: none; }
@keyframes rec-idle-gold {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-gold) 0%, transparent); }
  50%       { box-shadow: 0 0 10px 2px color-mix(in srgb, var(--color-gold) 28%, transparent); }
}

/* ── Live indicator — breathing glow ─────────────────────────────────── */
.camp-live-dot {
  animation: live-breath 3s ease-in-out infinite;
}
@keyframes live-breath {
  0%, 100% { box-shadow: 0 0 0 0   color-mix(in srgb, var(--color-gold) 55%, transparent); opacity: 1; }
  50%       { box-shadow: 0 0 7px 2px color-mix(in srgb, var(--color-gold) 20%, transparent); opacity: 0.8; }
}

/* ── Live preamble "Where We Left Off" — split underline ─────────────── */
.live-preamble-section-hdr {
  border-bottom: 1px solid var(--split-color);
  padding-bottom: 0.3rem;
  margin-bottom: 0.4rem;
}

/* ── Active tab — restore correct token (uses ember for glow) ─────────── */
[data-theme="dark"] .tab-btn.active {
  text-shadow: 0 0 16px color-mix(in srgb, var(--color-brand) 65%, transparent);
}


/* ═══════════════════════════════════════════════════════════════════════════
   ORNATE FANTASY LAYER — 20% intensity
   Target: "forged steel + ember" — etched edges, metallic depth, no noise.
   Rule: if it draws attention → halve it. If it hurts readability → remove.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Dark mode card grounding — slate surface, flat border, clean shadow ──
   v1087: hoisted to [data-theme="dark"] .tf-card so EVERY list-row card
   (session / entity / party / quest / fates) gets the same elevated dark
   treatment. The previous per-card overrides for .campaign-card (markup
   now uses .cc-card so the rule was dead), .session-card, and
   .party-pc-card are retired. Body bg uses the parchment wash + dark
   tint; a card at plain --bg-2 sits too close to that wash, so we
   color-mix --color-bg into --color-surface for a darker card surface
   with a faint white border highlight + stronger drop shadow. */
[data-theme="dark"] .tf-card {
  /* v1101+: match spec mockup. Cards sit at the spec --paper-2 value
     directly. Body parchment is darker; cards rely on border + drop
     shadow for separation. Border 0.10 white + 0.50 drop shadow + faint
     inset highlight for the "lifted vellum" feel.
     v1104 CRITICAL: must use background-COLOR (longhand) here — the
     `background:` SHORTHAND wipes background-image: none, which kills
     the .tf-card--watermarked SVG sigil set by the rule above.
     v1140: reverted v1139's inline #141A22 hex to var(--bg-2) now that
     the dark token resolves to that spec value via :root. Single source
     of truth — every .tf-card-composing surface shifts in lockstep. */
  background-color: var(--bg-2);
  border: 1px solid rgba(255,255,255,0.10);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.05),
    0 4px 14px rgba(0,0,0,0.50);
}
[data-theme="dark"] .tf-card.is-selected {
  border-color: color-mix(in srgb, var(--accent-teal) 65%, rgba(255,255,255,0.18));
}


/* Active TF mark — faint teal sigil glow */
[data-theme="dark"] .tab-btn.active .tf-tab-mark {
  filter: drop-shadow(0 0 3px color-mix(in srgb, var(--color-brand) 65%, transparent));
}

/* ── Live header — etched divider feel ────────────────────────────────── */
[data-theme="dark"] .live-header {
  box-shadow:
    inset 0 -1px 0 rgba(0,0,0,0.45),
    inset 0 1px 0 rgba(255,255,255,0.025);
}

/* ── Live sidebar + chat drawer — inner teal glow (very diffuse) ────────── */
[data-theme="dark"] .live-sidebar,
[data-theme="dark"] .live-ai-drawer {
  box-shadow: inset 0 0 70px color-mix(in srgb, var(--color-brand) 4%, transparent);
}

/* ── Live preamble — inner engraved feel ─────────────────────────────── */
[data-theme="dark"] .live-preamble {
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.04),
    inset 0 -1px 0 rgba(0,0,0,0.28);
}

/* ── Live recorder border — gold-tinged at rest ──────────────────────── */
[data-theme="dark"] .live-recorder {
  border-bottom: 1px solid color-mix(in srgb, var(--color-gold) 14%, var(--color-border));
}

/* ── Arcane compass rose — map panel corner (pointer-events off) ───────── */
/* Tiny ember gold geometry, barely visible, adds depth behind the map */
[data-theme="dark"] .live-map-panel::after {
  content: '';
  position: absolute;
  bottom: 10px; right: 10px;
  width: 72px; height: 72px;
  background-image: url("data:image/svg+xml,%3Csvg opacity='0.09' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='46' fill='none' stroke='%23D4A843' stroke-width='1.8'/%3E%3Cpath d='M50 4 L96 50 L50 96 L4 50 Z' fill='none' stroke='%23D4A843' stroke-width='1.8'/%3E%3Cpath d='M50 26 L74 50 L50 74 L26 50 Z' fill='none' stroke='%23D4A843' stroke-width='1.2'/%3E%3Cline x1='50' y1='4' x2='50' y2='96' stroke='%23D4A843' stroke-width='0.9'/%3E%3Cline x1='4' y1='50' x2='96' y2='50' stroke='%23D4A843' stroke-width='0.9'/%3E%3C/svg%3E");
  background-size: contain;
  background-repeat: no-repeat;
  pointer-events: none;
  z-index: 5;
}



/* ── Entity preview drawer — inner engraved edge ─────────────────────── */
[data-theme="dark"] .entity-preview-drawer {
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.04),
    inset 1px 0 0 rgba(255,255,255,0.02),
    0 0 50px rgba(0,0,0,0.55);
}

/* Session card border is uniform — no etched-edge effect */

/* ── Chronicle panel — very faint teal inner glow ────────────────────── */
[data-theme="dark"] #sessions-panel {
  background: radial-gradient(ellipse 80% 30% at 50% 0%,
    color-mix(in srgb, var(--color-brand) 3%, transparent) 0%,
    transparent 100%);
}

/* ── Mobile second tab bar ─────────────────────────────────────────────
   On narrow screens the 7 content tabs break out of the main nav row
   into a full-width swipeable strip directly below it.
   Desktop / tablet layouts are completely unaffected.
──────────────────────────────────────────────────────────────────────── */
/* Campaign grid collapses to 1 column on narrow viewports; divider hidden */
@media (max-width: 540px) {
  .campaign-list-screen {
    grid-template-columns: 1fr;
    gap: 0.75rem;
  }
  .campaign-grid-wrap::before { display: none; }
}

@media (max-width: 600px) {
  /* Allow the nav to wrap — first row: logo + Live + auth; second row: tabs */
  #tab-nav {
    flex-wrap: wrap;
    overflow: visible;   /* let the second row extend below without clipping */
    padding: 0 0.5rem 0 0.75rem;
  }

  /* Push the tab strip to a second line, full width */
  #tab-scroll-wrap {
    order: 10;
    width: 100%;
    flex: none;
    min-width: unset;
    border-top: 1px solid var(--border);
    /* Fades still work — position:relative is inherited from the base rule */
  }

  /* Slightly more compact tabs on the mobile strip */
  #tab-scroll-area {
    padding: 0 0.25rem;
  }
  #tab-scroll-area .tab-btn {
    padding: 0.45rem 0.8rem;
    font-size: var(--font-size-sm);
    white-space: nowrap;
  }
}

/* ── Section eyebrow — composes to spec .ui-4 (11px / 600 / 0.12em uppercase).
   v1084: tightened weight 700→600 and tracking 0.06em→0.12em per spec —
   matches the spec's secondary-section-label rhythm. */
.form-section-label {
  font-size: 11px;
  font-weight: var(--font-weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--color-text-tertiary);
}
.pc-stats-field-lbl {
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-tertiary);
}
/* v1082: aligned to brand spec .tf-input — squared, 1.5px border, 14px / 11px 14px,
   teal focus glow. Matches the global input recipe at line 404. */
.form-input {
  background: color-mix(in srgb, var(--color-bg) 60%, white);
  border: 1.5px solid var(--color-border);
  border-radius: 0;
  color: var(--color-text-primary);
  font-size: 14px;
  padding: 11px 14px;
  width: 100%;
  box-sizing: border-box;
  transition:
    border-color var(--t-hover) var(--ease),
    background   var(--t-hover) var(--ease),
    box-shadow   var(--t-hover) var(--ease);
}
.form-input::placeholder { color: var(--color-text-tertiary); font-style: italic; }
.form-input:focus {
  outline: none;
  border-color: var(--color-brand);
  background: var(--color-bg);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-brand) 12%, transparent);
}
[data-theme="dark"] .form-input { background: color-mix(in srgb, var(--color-text-primary) 4%, var(--color-bg)); }
[data-theme="dark"] .form-input:focus { background: var(--color-surface); }

/* ── PC Stats Edit modal layout ───────────────────────────────────────────── */
/* Width comes from .modal-box--lg (560px) — the max-width override was
   removed in v748 along with the duplicate sizing logic. This class now
   only contributes the inner flex-column layout that the body needs. */
.pse-modal-box {
  max-height: 92vh;
  display: flex;
  flex-direction: column;
}
.pse-body {
  overflow-y: auto;
  flex: 1;
  padding: var(--space-4) var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.pse-footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  padding: 0.75rem var(--space-5);
  border-top: 1px solid var(--color-border);
  flex-shrink: 0;
}
.pse-section {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--color-border);
}
.pse-section:last-of-type { border-bottom: none; padding-bottom: 0; }
.pse-label {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.pse-label--center { align-items: center; }
.pse-grid-2 { display: grid; grid-template-columns: 1fr 1fr;            gap: var(--space-3); }
.pse-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr);     gap: var(--space-3); }
.pse-grid-5 { display: grid; grid-template-columns: repeat(5, 1fr);     gap: var(--space-2); }
.pse-grid-6 { display: grid; grid-template-columns: repeat(6, 1fr);     gap: var(--space-2); }
.pse-score-input { text-align: center; padding: 0.4rem 0.25rem; }

/* Saving throws */
.pse-saves-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.pse-save-cb {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-secondary);
  cursor: pointer;
  padding: 0.25rem 0.6rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-full);
  transition: background var(--t-hover), border-color var(--t-hover);
}
.pse-save-cb:hover, .pse-save-cb:focus-visible { background: var(--color-surface); }
.pse-save-cb input { accent-color: var(--color-brand); }

/* Skills grid */
.pse-skills-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-2);
}
.pse-skill-cell {
  display: grid;
  grid-template-columns: 3rem 1fr auto;
  align-items: center;
  gap: 0.3rem;
}
.pse-skill-name {
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-text-secondary);
  white-space: nowrap;
}
.pse-skill-input {
  padding: 0.25rem 0.4rem;
  text-align: center;
  font-size: var(--font-size-sm);
  min-width: 0;
}
.pse-skill-adv {
  display: flex;
  align-items: center;
  gap: 2px;
  font-size: var(--font-size-xs);
  font-weight: 700;
  color: var(--color-text-tertiary);
  cursor: pointer;
  white-space: nowrap;
}
.pse-skill-adv input { accent-color: var(--color-brand); }

/* ── Character Import Pipeline ───────────────────────────────────────────── */
.import-review-box {
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-2xl);
  box-shadow: 0 8px 32px rgba(0,0,0,0.20);
  width: min(96vw, 960px);
  max-height: 90vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.import-step { display: flex; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; }

/* ── Step 1: Pick ── */
.import-pick-body {
  flex: 1;
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  overflow-y: auto;
}
.import-reimport-warn {
  background: color-mix(in srgb, var(--color-warning) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-warning) 50%, transparent);
  border-radius: var(--radius-lg);
  padding: 0.65rem 0.9rem;
  font-size: var(--font-size-sm);
  color: var(--color-text-primary);
  line-height: 1.5;
}
.import-pick-desc {
  font-size: var(--font-size-base);
  color: var(--color-text-secondary);
}
.import-pick-options {
  display: flex;
  gap: var(--space-4);
}
.import-pick-option {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-6) var(--space-4);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  cursor: pointer;
  transition: background var(--t-hover) var(--ease), border-color var(--t-hover) var(--ease), box-shadow var(--t-hover) var(--ease);
  text-align: center;
  user-select: none;
}
.import-pick-option:hover, .import-pick-option:focus-visible {
  background: var(--bg-3);
  border-color: var(--color-brand);
  box-shadow: 0 4px 12px rgba(0,0,0,0.12);
}
.import-pick-icon { font-size: 2rem; line-height: 1; }
.import-pick-label { font-size: var(--font-size-base); font-weight: 600; color: var(--color-text-primary); }
.import-pick-hint  { font-size: var(--font-size-sm); color: var(--color-text-tertiary); }
.import-pick-footer {
  display: flex;
  justify-content: flex-end;
  padding: 0.75rem var(--space-6);
  border-top: 1px solid var(--border);
  flex-shrink: 0;
}

/* ── Step 2: Parsing ── */
.import-parsing-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  padding: var(--space-8);
}
.import-parsing-animation {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}
.import-parsing-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--color-brand);
  animation: pulse 1.2s ease-in-out infinite;
}
.import-parsing-dot:nth-child(2) { animation-delay: var(--t-trans); }
.import-parsing-dot:nth-child(3) { animation-delay: var(--t-trans); }
.import-parsing-label { font-size: var(--font-size-lg); font-weight: 600; color: var(--color-text-primary); }
.import-parsing-hint  { font-size: var(--font-size-sm); color: var(--color-text-tertiary); }

/* ── Step 3: Review ── */
.import-review-layout {
  flex: 1;
  min-height: 0;
  display: flex;
  overflow: hidden;
}
.import-review-preview-pane {
  width: 320px;
  flex-shrink: 0;
  border-right: 1px solid var(--border);
  background: var(--bg);
  overflow-y: auto;
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.import-preview-thumb {
  width: 100%;
  border-radius: var(--radius-lg);
  border: 1px solid var(--border);
  display: block;
}
.import-preview-label {
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-tertiary);
  text-align: center;
}
.import-preview-no-preview {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  flex: 1;
  gap: var(--space-3);
  color: var(--color-text-tertiary);
  font-size: var(--font-size-sm);
  font-style: italic;
  min-height: 160px;
}
.import-review-form-pane {
  flex: 1;
  min-width: 0;
  overflow-y: auto;
  padding: var(--space-5);
}
.import-section {
  margin-bottom: var(--space-6);
}
.import-section-title {
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-tertiary);
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border);
}
.import-fields-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: var(--space-3);
}
.import-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.import-field-label {
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-text-secondary);
  display: flex;
  align-items: center;
  gap: 0.35rem;
}
.import-field input,
.import-field textarea,
.import-field select {
  font-size: var(--font-size-sm);
  padding: 0.35rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg);
  color: var(--color-text-primary);
  width: 100%;
  box-sizing: border-box;
}
.import-field textarea { resize: vertical; min-height: 64px; }
.import-field input:focus,
.import-field textarea:focus {
  border-color: var(--color-brand);
  outline: none;
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-brand) 12%, transparent);
}

/* Confidence indicators */
.import-field.conf-low input,
.import-field.conf-low textarea {
  border-color: color-mix(in srgb, var(--color-warning) 60%, transparent);
  background: color-mix(in srgb, var(--color-warning) 6%, var(--bg));
}
.import-field.conf-missing input,
.import-field.conf-missing textarea {
  border-color: color-mix(in srgb, var(--color-danger) 40%, transparent);
  background: color-mix(in srgb, var(--color-danger) 5%, var(--bg));
}
.conf-badge {
  font-size: 0.6rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 1px 5px;
  border-radius: var(--radius-full);
}
.conf-badge--high    { background: color-mix(in srgb, var(--color-success) 18%, transparent); color: var(--color-success); }
.conf-badge--medium  { background: color-mix(in srgb, var(--color-info) 18%, transparent);    color: var(--color-info); }
.conf-badge--low     { background: color-mix(in srgb, var(--color-warning) 18%, transparent); color: var(--color-warning); }
.conf-badge--missing { background: color-mix(in srgb, var(--color-danger) 15%, transparent);  color: var(--color-danger); }

/* Ability scores mini-grid */
.import-ability-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: var(--space-2);
}
.import-ability-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}
.import-ability-cell label {
  font-size: 0.65rem;
  font-weight: 700;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}
.import-ability-cell input {
  width: 100%;
  text-align: center;
  font-size: var(--font-size-sm);
  padding: 0.3rem 0.2rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg);
  color: var(--color-text-primary);
  box-sizing: border-box;
}
.import-ability-cell input:focus {
  border-color: var(--color-brand);
  outline: none;
}
.import-ability-mod {
  font-size: 0.65rem;
  color: var(--color-text-tertiary);
  text-align: center;
}

.import-review-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem var(--space-5);
  border-top: 1px solid var(--border);
  flex-shrink: 0;
}

/* Mobile responsive */
@media (max-width: 700px) {
  .import-review-box { width: 100vw; max-height: 100dvh; border-radius: 0; }
  .import-review-layout { flex-direction: column; }
  .import-review-preview-pane { width: 100%; border-right: none; border-bottom: 1px solid var(--border); max-height: 180px; }
  .import-pick-options { flex-direction: column; }
  .import-ability-grid { grid-template-columns: repeat(3, 1fr); }
}

/* ── Feature Showcase Panels ─────────────────────────────────────────────── */

/* ── Login: hero column ───────────────────────────────────────────────────── */
.login-hero {
  /* v1215: cinematic promo restyle. This panel is intentionally an
     ALWAYS-DARK marketing surface in BOTH themes — it carries its own night
     backdrop, so the light-on-dark child values below are correct in light
     theme too (they never fall through to a near-white surface). The dark-only
     literals are scoped here as --lh-* local tokens. */
  --lh-ink:   #E8F0F2;
  --lh-ink-2: rgba(232,242,243,0.64);
  --lh-ink-3: rgba(232,242,243,0.44);
  --lh-teal:  #7BBAC1;
  --lh-line:  rgba(123,186,193,0.22);
  display: flex;
  flex-direction: column;
  justify-content: center;
  position: relative;
  padding: 3rem 3.5rem 3rem 5%;
  height: 100%;
  overflow: hidden;
  border-right: 1px solid var(--border);
  background: radial-gradient(ellipse 90% 70% at 50% 30%, #0F1820 0%, #06090C 60%, #03060A 100%);
  color: var(--lh-ink);
}
/* faint mountain silhouette anchored to the panel floor */
.login-hero::before {
  content: ""; position: absolute; inset: auto 0 0 0; height: 58%; z-index: 0;
  background:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 600' preserveAspectRatio='none'><path d='M0,520 L120,460 L240,490 L340,400 L460,440 L560,360 L680,420 L780,320 L880,380 L1020,300 L1140,360 L1260,280 L1360,340 L1440,310 L1440,600 L0,600 Z' fill='%230B1418' opacity='0.85'/><path d='M0,560 L160,510 L300,540 L440,470 L600,520 L760,440 L920,500 L1080,420 L1220,490 L1360,430 L1440,470 L1440,600 L0,600 Z' fill='%23070D11' opacity='0.95'/></svg>") center bottom / 100% 100% no-repeat;
  pointer-events: none; opacity: .7;
}
/* film-grain veil */
.login-hero::after {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  mix-blend-mode: overlay; opacity: .04;
  background-image: radial-gradient(rgba(255,255,255,.6) 1px, transparent 1px);
  background-size: 3px 3px;
}
/* hero content rides above the silhouette + grain layers */
.login-hero > * { position: relative; z-index: 1; }
.login-form-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  overflow-y: auto;
  padding: 2rem 1.5rem;
}
.login-hero-eyebrow {
  align-self: center;
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.28em;
  color: var(--lh-teal);
  padding: 0.32rem 0.7rem;
  border: 1px solid var(--lh-line);
  border-radius: var(--radius-full);
}
.login-hero-headline {
  font-family: var(--type-display);
  font-size: clamp(1.9rem, 3.4vw, 2.6rem);
  font-weight: 500;
  color: var(--lh-ink);
  line-height: 1.08;
  letter-spacing: -0.01em;
  margin: 0;
  text-align: center;
  text-wrap: balance;
}
.login-hero-headline em {
  font-style: italic;
  font-weight: 500;
  color: var(--lh-teal);
}
.login-hero-sub {
  font-size: var(--font-size-md);
  color: var(--lh-ink-2);
  line-height: 1.55;
  margin: 0;
  max-width: 35.2rem; /* v1218: +10% so the sub wraps to two lines */
  align-self: center;
  text-align: center;
}
/* v1230: .login-hero-diff retired — the "Threadfall runs this one." contrast
   moved into the Live Capture slide caption (.lh-slide-cap strong). */
/* v1026: trial-promise tagline below the feature accordion. Tertiary
   weight + teal accent on the keyword ("free") via :first-line trick
   isn't reliable across browsers, so we just use one calm subdued line.
   The commitment-reducer ("no card needed") is the most-important word
   pair on this surface — every motivated prospect's bounce-point. */
.login-hero-trial-note {
  margin: 0.85rem 0 0;
  font-size: var(--font-size-sm);
  color: var(--lh-ink-3);
  letter-spacing: 0.01em;
  text-align: left;
  align-self: center; /* v1224: center the box (shrinks to its content) */
}
/* v1220: social-proof pull-quote above the trial note. Left-aligned (sits in
   the panel's left-aligned lower zone with the carousel + trial note); subdued
   italic with a thin teal accent border so it reads as a quiet quote. */
.login-hero-quote {
  margin: 0;
  max-width: 48rem; /* v1229: much wider so the testimonial wraps less */
  align-self: center; /* v1224: center the box; text inside stays left */
  padding-left: 0.85rem;
  border-left: 2px solid color-mix(in srgb, var(--lh-teal) 50%, transparent);
}
.login-hero-quote blockquote {
  margin: 0;
  font-style: italic;
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--lh-ink-2);
}
.login-hero-quote figcaption {
  margin-top: 0.3rem;
  font-size: var(--font-size-xs);
  letter-spacing: 0.04em;
  color: var(--lh-ink-3);
}
/* v1221: the quote is italic, so emphasis reads as UPRIGHT teal (not more
   italic). v1223: applied on top of the GM's emphatic all-caps EVERYTHING. */
.login-hero-quote em {
  font-style: normal;
  font-weight: 600;
  color: var(--lh-teal);
}
.login-hero-features {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.lhf-item {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
}
.lhf-icon {
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.6rem;
  margin-top: 2px;
}
.lhf-icon--teal { background: color-mix(in srgb, var(--color-brand) 15%, transparent); color: var(--color-brand); }
.lhf-icon--violet { background: color-mix(in srgb, var(--color-purple) 15%, transparent); color: var(--color-purple); }
.lhf-icon--ember { background: color-mix(in srgb, var(--color-ember) 15%, transparent); color: var(--color-ember); }
.lhf-title {
  font-size: var(--font-size-base);
  font-weight: 600;
  color: var(--color-text-primary);
  margin-bottom: 0.15rem;
}
.lhf-desc {
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
  line-height: 1.45;
}

/* Login hero — CSS app mockup */
.login-hero-mockup {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
  background: var(--color-surface);
  overflow: hidden;
  flex-shrink: 0;
  font-size: var(--font-size-xs);
  box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}
.lh-mock-bar {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.45rem 0.75rem;
  background: var(--color-surface-elevated);
  border-bottom: 1px solid var(--color-border);
}
.lh-mock-tab {
  font-size: 0.65rem;
  color: var(--color-text-tertiary);
  padding: 0.15rem 0.4rem;
  border-radius: var(--radius-sm);
}
.lh-mock-tab--active {
  color: var(--color-brand);
  background: color-mix(in srgb, var(--color-brand) 12%, transparent);
}
.lh-mock-live-dot {
  margin-left: auto;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--color-success);
  animation: presence-pulse 2s ease-in-out infinite;
}
.lh-mock-session-list {
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--color-border);
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.lh-mock-session {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.25rem 0.4rem;
  border-radius: var(--radius-sm);
  color: var(--color-text-secondary);
  font-size: 0.65rem;
}
.lh-mock-session--active {
  background: color-mix(in srgb, var(--color-brand) 10%, transparent);
  color: var(--color-text-primary);
  border-left: 2px solid var(--color-ember);
}
.lh-mock-date { color: var(--color-text-tertiary); font-size: 0.6rem; }
.lh-mock-recap {
  padding: 0.6rem 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.lh-mock-beat {
  display: flex; align-items: center; gap: 0.3em;
  font-family: var(--type-display);
  font-size: 0.6rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--teal-2, var(--color-brand));
  margin-top: 0.2rem; padding-bottom: 0.15rem;
  border-bottom: 1px solid color-mix(in srgb, var(--color-brand) 35%, transparent);
}
.lh-mock-beat::before {
  content: ''; flex-shrink: 0;
  width: 4px; height: 4px; border-radius: 50%;
  background: var(--color-brand);
}
.lh-mock-text {
  font-size: 0.67rem;
  color: var(--color-text-secondary);
  line-height: 1.5;
}

/* v957: campaign-selector empty state — first-time GMs (zero campaigns).
   Replaces the bare "No campaigns yet" placeholder with a welcome card
   that pairs the primary "create your campaign" CTA with the GM Discord
   invite. Both surfaced together while the user is in "exploring" mode. */
.cs-empty-state {
  margin: var(--space-6) auto;
  max-width: 640px;
  padding: var(--space-6) var(--space-5);
  text-align: center;
  background: color-mix(in srgb, var(--accent-teal) 5%, var(--bg-2));
  border: 1px solid color-mix(in srgb, var(--accent-teal) 22%, var(--border));
  border-radius: var(--radius-xl);
}
.cs-empty-title {
  margin: 0 0 var(--space-3);
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-bold);
  color: var(--text);
  letter-spacing: 0.01em;
}
.cs-empty-body {
  margin: 0 0 var(--space-4);
  font-size: var(--font-size-base);
  line-height: 1.55;
  color: var(--color-text-secondary);
}
.cs-empty-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.cs-empty-cta-secondary {
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}

/* v957: returning-GM Discord footer — subtle, dismissable. Sits below
   the campaign list, above the showcase + archives. Single line, low
   visual weight: meant to be ignorable for engaged users but
   discoverable for users who haven't found the community yet. */
/* v1095: Discord footer CTA aligned to spec .tf-banner--discord — Discord
   brand-blue tinted bg + border. Treated as a footer banner inside the
   campaign selector. */
/* Campaign-selector layout utilities — replace inline style= flagged in the
   design audit; tokens only. */
.cs-select-hdr-left { display: flex; align-items: center; gap: var(--space-2); }
/* Post-grid sections (showcase, archives) share Anthologies' --space-6 rhythm. */
.cs-post-grid-section { margin-top: var(--space-6); padding: 0 var(--space-2); }
.cs-post-grid-section > .form-section-label { display: block; margin-bottom: var(--space-2); }

.cs-discord-footer {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  margin-top: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-lg);
  border: 1px solid color-mix(in srgb, var(--color-discord) 35%, transparent);
  background: color-mix(in srgb, var(--color-discord) 6%, var(--bg));
  font-family: var(--type-ui);
  font-size: var(--font-size-sm);
  color: var(--text-1);
}
[data-theme="dark"] .cs-discord-footer {
  background: color-mix(in srgb, var(--color-discord) 14%, var(--bg-2));
}
.cs-discord-footer-text { line-height: 1.5; }
.cs-discord-footer-link {
  color: var(--accent-teal);
  text-decoration: none;
  font-weight: var(--font-weight-semibold);
  transition: color var(--t-hover) var(--ease);
}
.cs-discord-footer-link:hover,
.cs-discord-footer-link:focus-visible {
  color: var(--text);
  text-decoration: underline;
  outline: none;
}
.cs-discord-footer-dismiss {
  flex-shrink: 0;
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-3);
  font-size: var(--font-size-xs);
  line-height: 1;
  padding: 0.2rem 0.45rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  opacity: 0.55;
  transition:
    opacity      var(--t-hover) var(--ease),
    color        var(--t-hover) var(--ease),
    background   var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease);
}
.cs-discord-footer-dismiss:hover,
.cs-discord-footer-dismiss:focus-visible {
  opacity: 1;
  color: var(--text-1);
  border-color: var(--border);
  background: var(--bg-3);
  outline: none;
}

/* ── Campaign selector: showcase strip ──────────────────────────────────── */
/* Outer spacing now lives on #cs-showcase-section (mirrors the
   "Recently Archived" rhythm). Local margins reset to 0 so the wrapper
   is the single source of truth for vertical spacing. */
.cs-showcase {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
  background: var(--color-surface);
  margin-top: 0;
  margin-bottom: 0;
  overflow: hidden;
  transition: opacity var(--t-trans) var(--ease);
}
.cs-showcase.cs-showcase--dismissed { display: none; }
/* When the showcase is dismissed, hide the wrapping section too — otherwise
   the "Thread|fall Features" label would float alone above the archived list. */
#cs-showcase-section:has(.cs-showcase--dismissed) { display: none; }
/* All slides occupy the same grid cell so the container's height is fixed at
   the tallest slide. Without this, slides with mockups vs. without flicker the
   showcase height when cycling, which (combined with the 16:9 campaign cards
   above) causes the cards to visibly resize as the scrollbar toggles. */
.cs-showcase-slides {
  display: grid;
  grid-template-areas: "stack";
}
.cs-showcase-slide {
  grid-area: stack;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 1.5rem;
  padding: 1.25rem 1.5rem 1.1rem;
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.25s ease;
  pointer-events: none;
}
.cs-showcase-slide--active {
  visibility: visible;
  opacity: 1;
  pointer-events: auto;
}
.css-body { flex: 1 1 auto; min-width: 0; }
.css-icon {
  flex-shrink: 0;
  width: 36px; height: 36px;
  border-radius: var(--radius-lg);
  display: flex; align-items: center; justify-content: center;
  font-size: var(--font-size-sm);
  margin-top: 2px;
}
.css-icon--teal { background: color-mix(in srgb, var(--color-brand) 15%, transparent); color: var(--color-brand); }
.css-icon--violet { background: color-mix(in srgb, var(--color-purple) 15%, transparent); color: var(--color-purple); }
.css-icon--ember { background: color-mix(in srgb, var(--color-ember) 15%, transparent); color: var(--color-ember); }
/* Composes .form-section-label in markup for the canonical eyebrow recipe;
   this only carries the slide-local bottom margin (eyebrow-composition rule). */
.css-eyebrow {
  margin-bottom: 0.2rem;
}
.css-headline {
  font-size: var(--font-size-lg);
  font-weight: 600;
  color: var(--color-text-primary);
  margin-bottom: 0.25rem;
}
.css-desc {
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
  line-height: 1.5;
}
.cs-showcase-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.5rem 1.25rem 0.65rem;
  border-top: 1px solid var(--color-border);
}
.cs-showcase-dots {
  display: flex;
  gap: 0.4rem;
  align-items: center;
}
.cs-dot {
  position: relative;
  width: 7px; height: 7px;
  border-radius: 50%;
  border: none;
  background: var(--color-border);
  cursor: pointer;
  padding: 0;
  transition: background var(--t-trans), transform var(--t-trans);
}
/* Transparent >=24px hit area — keeps the 7px dot visual but meets touch /
   accessibility minimums without enlarging the dot. */
.cs-dot::before {
  content: ""; position: absolute;
  top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 24px; height: 24px;
}
.cs-dot--active {
  background: var(--color-brand);
  transform: scale(1.3);
}
/* Respect reduced-motion: kill the slide cross-fade (JS also halts auto-advance). */
@media (prefers-reduced-motion: reduce) {
  .cs-showcase-slide { transition: none; }
}
.cs-showcase-dismiss {
  background: none;
  border: none;
  color: var(--color-text-tertiary);
  font-size: var(--font-size-sm);
  cursor: pointer;
  padding: 0.2rem 0.4rem;
  border-radius: var(--radius-sm);
  line-height: 1;
  transition: color var(--t-hover);
}
.cs-showcase-dismiss:hover, .cs-showcase-dismiss:focus-visible { color: var(--color-text-secondary); }

/* v1016: subtle pricing-page nudge in the showcase footer. Tertiary
   weight at rest, teal on hover. Left-justified — sits before the dots
   per the UX review so it reads as a footer-level affordance, not a
   slide-body CTA. Rendered conditionally by JS — Master/Pro/Founding
   never see it (no friction for already-paying users). The flex parent
   uses space-between, so with 3 children the natural layout is
   link-left / dots-center / dismiss-right. */
.cs-showcase-plans-link {
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
  text-decoration: none;
  padding: 0.2rem 0;
  cursor: pointer;
  transition: color var(--t-hover);
}
.cs-showcase-plans-link:hover,
.cs-showcase-plans-link:focus-visible {
  color: var(--accent-teal);
  text-decoration: underline;
}
/* Live Capture mockup — verbatim speaker turns + a live indicator. Mirrors the
   real Live transcript panel (speaker chip + utterance), not summarized log
   lines. Replaced the cut AI-Prep-Suite thread mock in the showcase redesign. */
.css-mock-turn { display: flex; align-items: baseline; gap: 0.4rem; }
.css-mock-spk {
  flex-shrink: 0; align-self: flex-start; white-space: nowrap;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 1px 6px; border-radius: var(--radius-sm);
  font-weight: 700; font-size: 0.5rem; line-height: 1.4; letter-spacing: 0.03em;
  background: transparent; border: 1px solid;
  border-color: color-mix(in srgb, var(--spk-color, var(--border)) 50%, transparent);
  color: var(--spk-color, var(--text-2));
}
.css-mock-spk--gm { background: var(--accent-gold); border-color: var(--accent-gold); color: #20170a; }
.css-mock-utter { color: var(--color-text-secondary); line-height: 1.4; }
.css-mock-listening {
  margin-top: 0.15rem;
  font-size: 0.6rem; font-style: italic;
  color: var(--color-brand);
}

/* ── Responsive: collapse login hero on small screens ──────────────────── */
@media (max-width: 860px) {
  .login-screen {
    grid-template-columns: 1fr;
    align-items: flex-start;
    overflow-y: auto;
  }
  /* v1226: show a COMPACT pitch above the form on mobile (was display:none —
     mobile saw a bare login with zero selling). The heavy bits — feature
     accordion, testimonial, promise, differentiator line — stay desktop-only;
     mobile gets eyebrow + headline + sub, then the Start-free form below. */
  .login-hero {
    display: flex;
    height: auto;
    border-right: none;
    border-bottom: 1px solid var(--border);
    padding: 2.25rem 1.25rem 1.75rem;
    gap: 0.7rem;
  }
  .login-hero .lh-showcase,
  .login-hero .login-hero-quote { display: none; }
  .login-hero-headline { font-size: clamp(1.55rem, 7vw, 2rem); }
  .login-form-col {
    min-height: auto;
    padding: 2rem 1rem 3rem;
  }
}
@media (max-width: 768px) {
  .cs-showcase-slide { flex-direction: column; gap: 0.6rem; }
}

/* ── Login hero carousel ─────────────────────────────────────────────────── */
.login-hero { gap: 1rem; }  /* tighten gap since carousel replaces feature list */

.lh-carousel {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  overflow: hidden;
}
/* ── Feature showcase (tabbed, v1228) — replaced the old accordion. A tab
   strip switches a grid-stacked viewport: all slides share one grid cell, so
   the viewport sizes to the TALLEST mock (no clip, no height-jump on rotate),
   and inactive slides crossfade out. */
.lh-showcase {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
.lh-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
}
.lh-tab {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.3rem 0.65rem;
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--lh-ink-2);
  font-family: inherit;
  font-size: var(--font-size-xs);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--t-hover) var(--ease), border-color var(--t-hover) var(--ease), background var(--t-hover) var(--ease);
}
.lh-tab:hover { color: var(--lh-ink); border-color: rgba(123,186,193,0.4); }
.lh-tab--active {
  color: var(--lh-ink);
  border-color: rgba(123,186,193,0.5);
  background: rgba(123,186,193,0.08);
}
.lha-dot { font-size: 0.45rem; }
.lha-dot--teal   { color: var(--color-brand); }
.lha-dot--violet { color: var(--color-purple); }
.lha-dot--ember  { color: var(--color-ember); }
.lha-dot--gold   { color: var(--color-gold); }
.lh-viewport {
  display: grid;
}
.lh-slide {
  grid-area: 1 / 1;        /* all slides occupy one cell → viewport = tallest */
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.35s var(--ease);
}
.lh-slide--active {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}
.lh-slide-cap {
  margin: 0;
  font-size: var(--font-size-sm);
  line-height: 1.45;
  color: var(--lh-ink-2);
}
.lh-slide-cap strong { color: var(--lh-teal); font-weight: 600; }

/* Shared frame for all slide mockups */
.lh-mock-frame {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
  background: var(--color-surface);
  overflow: hidden;
  font-size: var(--font-size-xs);
  box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}

/* ── Codex mockup (slide 2) ─────────────────────────────────────────── */
.lh-mock-codex-filters {
  display: flex; gap: 0.3rem; align-items: center;
  margin-left: auto;
}
.lh-mock-pill {
  font-size: 0.6rem; padding: 0.1rem 0.4rem;
  border-radius: var(--radius-full);
  background: var(--color-surface-elevated);
  color: var(--color-text-secondary);
  border: 1px solid var(--color-border);
}
.lh-mock-pill--active {
  background: color-mix(in srgb, var(--color-brand) 15%, transparent);
  color: var(--color-brand);
  border-color: color-mix(in srgb, var(--color-brand) 30%, transparent);
}
.lh-mock-codex-grid {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 0; padding: 0.5rem 0.6rem;
}
.lh-mock-entity {
  display: flex; flex-direction: column; align-items: flex-start;
  padding: 0.35rem 0.4rem;
  border-bottom: 1px solid var(--color-border);
  gap: 0.15rem;
}
.lh-mock-entity:nth-last-child(-n+3) { border-bottom: none; }
.lh-mock-entity-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 5px; flex-shrink: 0;
  background: color-mix(in srgb, currentColor 14%, transparent);
  border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
}
.lh-mock-entity-icon svg { width: 12px; height: 12px; }
.lh-mock-entity-icon--npc  { color: var(--color-muted-violet, #7C6FA0); }
.lh-mock-entity-icon--loc  { color: var(--text-3); }
.lh-mock-entity-icon--fac  { color: var(--accent-amber); }
.lh-mock-entity-icon--item { color: var(--accent-gold); }
.lh-mock-entity-name { font-size: 0.65rem; font-weight: 600; color: var(--color-text-primary); }
.lh-mock-entity-type { font-size: 0.55rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }
.lh-mock-entity-type--npc   { color: var(--color-muted-violet, #7C6FA0); }
.lh-mock-entity-type--loc   { color: var(--text-3); }
.lh-mock-entity-type--fac   { color: var(--accent-amber); }
.lh-mock-entity-type--item  { color: var(--accent-gold); }

/* ── Live capture mockup (slide 3) ──────────────────────────────────── */
.lh-mock-rec-indicator {
  margin-left: auto;
  display: flex; align-items: center; gap: 0.25rem;
  font-size: 0.6rem; font-weight: 700;
  color: var(--color-danger);
}
.lh-mock-rec-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--color-danger);
  animation: rec-blink 1.2s ease-in-out infinite;
}
/* v1236: live transcript — mirrors the real .transcript-turn / .transcript-speaker
   render (2-char teal speaker chip + verbatim text, bordered turns, a
   "Listening…" cursor), scaled to the mock frame. */
.lh-mock-transcript { padding: 0.4rem 0.6rem; display: flex; flex-direction: column; }
.lh-mock-turn {
  display: flex; align-items: baseline; gap: 0.4rem;
  padding: 0.28rem 0; border-bottom: 1px solid var(--color-border);
}
.lh-mock-speaker {
  flex-shrink: 0; align-self: flex-start; white-space: nowrap;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 1px 6px; border-radius: var(--radius-sm);
  font-size: 0.5rem; font-weight: 700; line-height: 1.4; letter-spacing: 0.03em;
  background: transparent; border: 1px solid;
  border-color: color-mix(in srgb, var(--spk-color, var(--border)) 50%, transparent);
  color: var(--spk-color, var(--text-2));
}
.lh-mock-speaker--gm { background: var(--accent-gold); border-color: var(--accent-gold); color: #20170a; }
.lh-mock-line { flex: 1; font-size: 0.65rem; line-height: 1.45; color: var(--color-text-secondary); }
.lh-mock-turn--new .lh-mock-line { color: var(--color-text-primary); }
.lh-mock-listening { font-size: 0.62rem; font-style: italic; color: var(--color-brand); padding: 0.35rem 0 0; }

/* ── Loose Threads mockup (AI Prep Suite slide) — heat-scored thread list
   with a Weave CTA. Replaced the retired Party-assessment mock in v1219. */
.lh-mock-thread-count {
  margin-left: auto;
  font-size: 0.6rem; font-weight: 700;
  color: var(--color-gold);
  background: color-mix(in srgb, var(--color-gold) 14%, transparent);
  padding: 0.1rem 0.4rem; border-radius: var(--radius-full);
}
.lh-mock-threads { padding: 0.5rem 0.6rem; display: flex; flex-direction: column; gap: 0.32rem; }
.lh-mock-thread-row { display: flex; align-items: center; gap: 0.4rem; }
.lh-mock-heat { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.lh-mock-heat--hot  { background: var(--color-ember); box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-ember) 20%, transparent); }
.lh-mock-heat--warm { background: var(--color-gold); }
.lh-mock-heat--cool { background: var(--color-text-tertiary); }
.lh-mock-thread-title { flex: 1; min-width: 0; font-size: 0.65rem; font-weight: 600; color: var(--color-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lh-mock-thread-meta { flex-shrink: 0; font-size: 0.58rem; color: var(--color-text-tertiary); }
.lh-mock-weave-cta {
  margin-top: 0.2rem; padding: 0.3rem 0.45rem;
  font-size: 0.62rem; font-weight: 600;
  color: var(--color-brand);
  background: color-mix(in srgb, var(--color-brand) 8%, transparent);
  border: 1px dashed color-mix(in srgb, var(--color-brand) 35%, transparent);
  border-radius: var(--radius-sm);
  text-align: center;
}

/* ── Campaign selector: mockup column ────────────────────────────────── */
.css-mockup {
  flex-shrink: 0;
  width: clamp(240px, 32vw, 360px);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-surface-elevated);
  padding: 0.75rem 0.9rem;
  font-size: 0.72rem;
  display: flex; flex-direction: column; gap: 0.4rem;
}
.css-mock-beat {
  display: flex; align-items: center; gap: 0.3em;
  font-family: var(--type-display);
  font-size: 0.6rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.08em; color: var(--teal-2, var(--color-brand));
  margin-top: 0.2rem; padding-bottom: 0.15rem;
  border-bottom: 1px solid color-mix(in srgb, var(--color-brand) 35%, transparent);
}
.css-mock-beat::before {
  content: ''; flex-shrink: 0;
  width: 4px; height: 4px; border-radius: 50%;
  background: var(--color-brand);
}
.css-mock-text { color: var(--color-text-secondary); line-height: 1.45; }

.css-mock-entity-row { display: flex; align-items: center; gap: 0.35rem; padding: 0.2rem 0; border-bottom: 1px solid var(--color-border); }
.css-mock-entity-row:last-child { border-bottom: none; }
.css-mock-icon-npc, .css-mock-icon-loc, .css-mock-icon-fac, .css-mock-icon-item {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 5px; flex-shrink: 0;
  background: color-mix(in srgb, currentColor 14%, transparent);
  border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
}
.css-mock-icon-npc svg, .css-mock-icon-loc svg, .css-mock-icon-fac svg, .css-mock-icon-item svg { width: 12px; height: 12px; }
.css-mock-icon-npc  { color: var(--color-muted-violet, #7C6FA0); }
.css-mock-icon-loc  { color: var(--text-3); }
.css-mock-icon-fac  { color: var(--accent-amber); }
.css-mock-icon-item { color: var(--accent-gold); }
.css-mock-ename { flex: 1; font-weight: 600; color: var(--color-text-primary); }
.css-mock-etype {
  font-size: 0.55rem; font-weight: 700; text-transform: uppercase;
  padding: 0.1rem 0.3rem; border-radius: var(--radius-full);
}
.css-mock-etype--npc  { background: color-mix(in srgb, var(--color-muted-violet, #7C6FA0) 15%, transparent); color: var(--color-muted-violet, #7C6FA0); }
.css-mock-etype--loc  { background: color-mix(in srgb, var(--text-3) 18%, transparent); color: var(--text-3); }
.css-mock-etype--fac  { background: color-mix(in srgb, var(--accent-amber) 15%, transparent); color: var(--accent-amber); }
.css-mock-etype--item { background: color-mix(in srgb, var(--accent-gold) 15%, transparent); color: var(--accent-gold); }

.css-mock-log-row { display: flex; align-items: baseline; gap: 0.3rem; }
.css-mock-time { color: var(--color-text-tertiary); font-size: 0.58rem; flex-shrink: 0; }
.css-mock-tag {
  font-size: 0.52rem; font-weight: 700; text-transform: uppercase;
  padding: 0.1rem 0.3rem; border-radius: var(--radius-full); flex-shrink: 0;
}
.css-mock-tag--story  { background: color-mix(in srgb, var(--color-brand) 15%, transparent); color: var(--color-brand); }
.css-mock-tag--combat { background: color-mix(in srgb, var(--color-danger) 15%, transparent); color: var(--color-danger); }
.css-mock-log-text { color: var(--color-text-secondary); line-height: 1.4; }

@media (max-width: 960px) { .css-mockup { display: none; } }

/* ── Session tally editor (Chronicle page) ─────────────────────────────── */
.tally-panel {
  border: 1px solid var(--border);
  border-left: 3px solid var(--color-brand);
  border-radius: var(--radius-md);
  background: var(--bg-2);
  padding: 0.6rem 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}
.tally-subsection { display: flex; flex-direction: column; gap: 0.35rem; }
.tally-subhdr {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--text-2);
  letter-spacing: 0.02em;
}
.tally-subcount { color: var(--text-3); font-weight: var(--font-weight-normal); margin-left: 0.25rem; }
.tally-rows { display: flex; flex-direction: column; gap: 0.25rem; }
.tally-row {
  display: grid;
  grid-template-columns: minmax(120px, 1.4fr) 60px minmax(160px, 2fr) auto;
  gap: 0.35rem;
  align-items: center;
}
.tally-row input {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.3rem 0.45rem;
  font-size: var(--font-size-sm);
  color: var(--text);
  font-family: inherit;
  min-width: 0;
}
.tally-row input:focus { outline: 2px solid var(--color-brand); outline-offset: -1px; border-color: var(--color-brand); }
.tally-count { text-align: right; }
.tally-del { color: var(--color-danger); line-height: 1; padding: 0.25rem 0.4rem; }
.tally-add-row { align-self: flex-start; margin-top: 0.15rem; }
.tally-row-ro {
  padding: 0.25rem 0;
  font-size: var(--font-size-sm);
  color: var(--text);
  line-height: 1.5;
  border-bottom: 1px dashed var(--border);
  display: grid;
  grid-template-columns: 52px 1fr;
  align-items: baseline;
  column-gap: 0.5rem;
}
.tally-row-ro:last-child { border-bottom: none; }
.tally-count-ro {
  color: var(--color-brand);
  font-weight: var(--font-weight-semibold);
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.tally-note-ro { color: var(--text-3); font-style: italic; }
.tally-empty {
  font-size: var(--font-size-sm);
  color: var(--text-3);
  padding: 0.2rem 0;
  font-style: italic;
}
.tally-hint {
  font-size: var(--font-size-sm);
  color: var(--text-2);
  padding: 0.5rem 0.75rem;
  background: var(--bg-2);
  border: 1px dashed var(--border);
  border-radius: var(--radius-md);
  margin: 0;
}
.tally-save-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding-top: 0.25rem;
  border-top: 1px solid var(--border);
  flex-wrap: wrap;
}
.tally-meta { font-size: var(--font-size-xs); color: var(--text-3); margin-left: auto; }
.tally-dirty-badge {
  display: inline-block;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  color: var(--color-warning);
  background: color-mix(in srgb, var(--color-warning) 12%, transparent);
  padding: 0.05rem 0.4rem;
  border-radius: var(--radius-full);
  margin-left: 0.4rem;
}

@media (max-width: 600px) {
  .tally-row {
    grid-template-columns: 1fr 60px auto;
    grid-template-rows: auto auto;
  }
  .tally-note { grid-column: 1 / -1; }
}

/* ── Campaign To-Date card (Party tab) ───────────────────────────────────
   v1123: rebuilt to spec .tf-ktd-* recipe (Section 11 / mockup line 6839).
   Card chassis is zero-padded so the 4-col stat strip + 2x2 section grid
   bleed to the card edges; sections separate via 1px background-color gaps
   (no dividers). Row is a 3-col grid: name | detail (italic, truncated) |
   meta (mono, session label or count). Section headers gain color-coding
   modifiers (--kills/--collateral/--conquests/--nearwipes).

   Class names keep the legacy .ktd-* prefix per project rule
   ("internal identifiers keep the legacy names") — the tf-* prefix in the
   spec is the design-system family, the .ktd-* prefix is this app's CSS
   hook. The two are intentionally aliased to the same recipe values. */
/* v1145: rebuilt the Accomplishments card to fit the rest of the app's
   visual language (per user "make an exception to the spec — it's ugly"
   override). The prior recipe used grid-with-1px-gaps to create faux
   table dividers — busy, foreign to every other surface, and the
   per-row card-in-card nesting compounded it. New chassis:
     • Standard .tf-card padding (20/24) — matches every other Party
       surface so the watermark + border treatment from .tf-card cascades
       cleanly.
     • Stats inline as a flex row with breathing-room gap; a single
       horizontal rule separates them from the section grid below.
     • Section grid is a real 1fr 1fr grid with var(--space-5) gap (no
       1px-gap bg trick). Section headers keep their color-coded
       eyebrows (kills ember / collateral amber / conquests green /
       nearwipes red).
     • Rows are flat — dashed bottom divider, no per-row bg/border. The
       BBEG variant keeps the gold left-rail as a single accent. */
/* v1147: bumped padding to var(--space-6) all around for more breath. */
.ktd-card {
  grid-column: 1 / -1;
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  overflow: hidden;
}
.ktd-summary {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-around;
  gap: var(--space-5);
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--border);
}
/* v1150: TL;DR mode — collapse the 4-up sections grid; show only the
   stat strip. The bottom rule + padding under the summary disappears
   since there's nothing below it. */
.ktd-card--tldr .ktd-sections { display: none; }
.ktd-card--tldr .ktd-summary {
  border-bottom: none;
  padding-bottom: 0;
}
.ktd-card--tldr { gap: 0; }
/* v1154: unified segmented control — pill-shaped container with two
   halves, active half teal-tinted. Replaces the v1150/v1152 ghost-
   button pair pattern. Used by every Party-tab toggle:
   .ktd-mode-toggle (Accomplishments), .party-mode-toggle (PC cards),
   .pa-mode-toggle (Assessment). Same visual everywhere = the UX reads
   as a single canonical control. */
.tf-segctl {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: color-mix(in srgb, var(--text-1) 3%, var(--bg));
  padding: 2px;
  flex-shrink: 0;
  margin-left: auto;
}
.tf-segctl-btn {
  padding: 3px 12px;
  font-family: var(--type-ui);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  background: transparent;
  border: none;
  border-radius: 999px;
  cursor: pointer;
  color: var(--text-3);
  transition: background 160ms ease, color 160ms ease;
  white-space: nowrap;
}
.tf-segctl-btn:hover { color: var(--text-1); }
.tf-segctl-btn.active {
  background: color-mix(in srgb, var(--accent-teal) 22%, transparent);
  color: var(--accent-teal);
}
[data-theme="dark"] .tf-segctl-btn.active {
  background: color-mix(in srgb, var(--accent-teal) 30%, transparent);
  color: #BFE7EA;
}
/* v1152: Assessment TL;DR mode — hide summary text, the 2x2 grid, and
   the tip line, leaving only the headline tldr paragraph + chrome. */
.party-box--tldr .pa-summary,
.party-box--tldr .pa-grid,
.party-box--tldr .pa-tip { display: none; }
/* v1147: center each stat column so the big number sits over its label. */
.ktd-stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 4px;
  min-width: 80px;
  flex: 1 1 auto;
}
.ktd-stat-num {
  font-family: var(--type-display);
  font-weight: 500;
  font-size: 28px;
  letter-spacing: -0.012em;
  color: var(--text-1);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.ktd-stat-lbl {
  font-family: var(--type-ui);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--text-3);
}
.ktd-sections {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-5) var(--space-6);
}
@media (max-width: 700px) { .ktd-sections { grid-template-columns: 1fr; } }
.ktd-section {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
  min-height: 180px;
}
.ktd-section-hdr {
  font-family: var(--type-ui);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--text-2);
  display: flex;
  align-items: center;
  gap: 6px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--border);
}
/* v1147: section header color variants retired per user direction —
   "don't use different colors for the headers." All four (Kills /
   Collateral / Conquests / Near-Wipes) now share the base
   .ktd-section-hdr typography + --text-2 color. The class hooks
   remain on the markup so future variants can be added back without
   re-touching the JS. */
.ktd-scroll {
  overflow-y: auto;
  max-height: 180px;
  display: flex;
  flex-direction: column;
  padding-right: 4px;
}
/* v1145: flat row — dashed bottom divider only, no per-row card chrome.
   The grid stays 3-col (name | detail | meta).
   v1147: per-row left-rail in entity-type color via --row-color CSS var
   (injected inline by the row helpers). Rows that don't match a known
   entity (generic mob types like "Cultists") fall through to a
   transparent rail = no visible bar, which reads cleanly. The BBEG
   gold override is retired — BBEGs are typically NPCs and now share
   the standard NPC color via --row-color. */
.ktd-row {
  display: grid;
  grid-template-columns: minmax(0, auto) minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 8px;
  padding: 6px 0 6px 8px;
  border-bottom: 1px dashed color-mix(in srgb, var(--border) 65%, transparent);
  border-left: 3px solid var(--row-color, transparent);
  font-family: var(--type-ui);
  font-size: 12px;
  color: var(--text-1);
  line-height: 1.4;
}
.ktd-row:last-child { border-bottom: none; }
.ktd-row-name {
  font-weight: 600;
  color: var(--text-1);
  min-width: 0;
  overflow-wrap: break-word;
  word-break: break-word;
}
.ktd-row-detail {
  color: var(--text-3);
  font-style: italic;
  font-size: 11.5px;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ktd-row-detail:empty { display: none; }
.ktd-row-meta {
  font-family: var(--type-mono);
  font-size: 10px;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.ktd-subhdr {
  display: block;
  font-family: var(--type-ui);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-top: 4px;
  margin-bottom: 2px;
}
.ktd-subhdr:first-child { margin-top: 0; }
.ktd-empty {
  font-family: var(--type-display);
  font-style: italic;
  font-size: 12px;
  color: var(--text-3);
  padding: 8px 4px;
}

/* ═══════════════════════════════════════════════════════════════════════════
   LIGHT-THEME PREMIUM PARITY — v798
   Dark theme had a polished "TTRPG by candlelight" treatment via
   [data-theme="dark"] overrides; light theme inherited bare base rules with
   no equivalent premium signal, leaving it feeling like an engineering
   placeholder. The block below adds light-only counterparts using a
   parchment / ink / gold-leaf idiom that matches the dark version's
   perceived depth + hierarchy without inverting the palette.

   Convention: `html:not([data-theme="dark"]) .x { ... }` — same
   specificity as `[data-theme="dark"] .x` so neither steals from the
   other. Keep rules in this block scoped to that selector ONLY.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Body atmosphere — top-anchor brand bloom + warm corner vignette so the
   page doesn't feel like an unstyled dump on light. */
html:not([data-theme="dark"]) body {
  background-image:
    radial-gradient(1200px 600px at 50% -120px,
      color-mix(in srgb, var(--color-brand) 7%, transparent) 0%,
      transparent 70%),
    radial-gradient(800px 500px at 100% 110%,
      color-mix(in srgb, var(--color-ember) 5%, transparent) 0%,
      transparent 60%),
    linear-gradient(rgba(241,245,249,0.70), rgba(241,245,249,0.70)),
    url('/static/bg.png');
  background-blend-mode: normal, normal, normal, normal;
}

/* Active tab — light-mode glow signal (replaces the dark text-shadow). */
html:not([data-theme="dark"]) .tab-btn.active {
  text-shadow: 0 0 6px color-mix(in srgb, var(--color-brand) 35%, transparent);
}
html:not([data-theme="dark"]) .tab-btn.active .tf-tab-mark {
  filter: drop-shadow(0 0 3px color-mix(in srgb, var(--color-brand) 55%, transparent));
}

/* Card hover shadow — light theme needs more depth than rgba(0,0,0,0.12)
   which is barely perceptible on near-white. Bump opacity + add subtle
   inset highlight for lifted-vellum feel. */
html:not([data-theme="dark"]) .campaign-card:hover,
html:not([data-theme="dark"]) .campaign-card:focus-visible,
html:not([data-theme="dark"]) .session-card:hover,
html:not([data-theme="dark"]) .session-card:focus-visible,
html:not([data-theme="dark"]) .entity-row:hover,
html:not([data-theme="dark"]) .entity-row:focus-visible,
html:not([data-theme="dark"]) .party-pc-card:hover,
html:not([data-theme="dark"]) .party-pc-card:focus-visible,
html:not([data-theme="dark"]) .quest-nav-row:hover,
html:not([data-theme="dark"]) .quest-nav-row:focus-visible,
html:not([data-theme="dark"]) .map-manage-item:hover,
html:not([data-theme="dark"]) .map-manage-item:focus-visible,
html:not([data-theme="dark"]) .quotes-pc-section:hover,
html:not([data-theme="dark"]) .quotes-pc-section:focus-visible {
  box-shadow:
    0 6px 20px rgba(15,23,42,0.18),
    inset 0 1px 0 rgba(255,255,255,0.7);
}

/* Resting card shadows — dark gets `0 4px 12px rgba(0,0,0,0.35)`; give light
   a softer drop-shadow so cards feel grounded instead of pasted. */
html:not([data-theme="dark"]) .session-card,
html:not([data-theme="dark"]) .party-pc-card {
  box-shadow: 0 1px 3px rgba(15,23,42,0.06);
}

/* v1092: retired the v800 PNG-era drop-shadow stack on light theme.
   Original purpose: stamp a dark outline on the cream/gold PNG wordmark
   so it read against near-white pages (bitmap couldn't be re-colored
   per theme). Since v1071 the lockup is an SVG that re-colors via
   currentColor + CSS variables — no outline workaround needed in
   either theme. Light .login-logo / .nav-logo / .campaign-selector-logo
   now render the spec's ink-on-paper colors directly. */

/* Live header — light parchment gradient + warm gold border-bottom mirrors
   the dark version's gold-tint + linear-gradient. Without this the Live
   screen looks identical to the rest of the app on light theme. */
html:not([data-theme="dark"]) .live-header {
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--color-gold) 5%, var(--bg)) 0%,
    var(--bg) 50%,
    color-mix(in srgb, var(--color-gold) 5%, var(--bg)) 100%);
  border-bottom: 2px solid color-mix(in srgb, var(--color-gold) 35%, transparent);
  box-shadow: inset 0 -1px 0 rgba(15,23,42,0.05);
}

/* Live sidebar / drawer — warm cream inset glow mirrors the dark theme's
   teal glow. Quiet, not distracting; same premium "lit room" effect. */
html:not([data-theme="dark"]) .live-sidebar,
html:not([data-theme="dark"]) .live-ai-drawer {
  box-shadow: inset 0 0 50px color-mix(in srgb, var(--color-gold) 4%, transparent);
}

/* Live preamble — engraved inset on light. */
html:not([data-theme="dark"]) .live-preamble {
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.6),
    inset 0 -1px 0 rgba(15,23,42,0.06);
}

/* Sessions panel — soft daytime mist mirroring dark's faint teal radial. */
html:not([data-theme="dark"]) #sessions-panel {
  background: radial-gradient(circle at 50% 0%,
    color-mix(in srgb, var(--color-brand) 4%, transparent) 0%,
    transparent 60%);
}

/* Party PC portrait vignette — light counterpart to dark's right-side
   gradient. Soft ink shadow at bottom-right gives depth on light too. */
html:not([data-theme="dark"]) .party-pc-portrait::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(115deg,
    transparent 60%,
    rgba(15,23,42,0.06) 88%,
    rgba(15,23,42,0.10) 100%);
}

/* Login hero — warm ivory→teal-tinted gradient gives the form column the
   same atmospheric weight dark gets from its 5%-brand wash. */
/* v1215: the hero is now an always-dark cinematic panel in BOTH themes (it
   carries its own night backdrop — see .login-hero), so the former light-theme
   parchment override is intentionally removed. The dark default applies in
   light theme too; inner mock-frame cards stay theme-aware and read as light
   "devices" on the night stage in the default (light) theme. */

/* Map hover-card / popup shadow — 55% black is correct on dark map but
   bruises on light Atlas tiles. Soften. */
html:not([data-theme="dark"]) .map-hover-card,
html:not([data-theme="dark"]) .leaflet-popup-content-wrapper {
  box-shadow: 0 8px 28px rgba(15,23,42,0.20) !important;
}

/* Map pin — pale-gray on light map = washed-out silhouette. Use warm
   ivory so the teardrop reads against terrain art. */
html:not([data-theme="dark"]) .map-pin {
  background: #FAF6EE;
}

/* v801: cover-image cards on light theme — INVERSE of the dark-mode tint.
   Dark theme uses a black wash (rgba(11,15,20,0.78→0.55→0.82)) so white
   text reads against bright cover art. v800 first attempt thinned that
   black wash on light theme — but the title is var(--text) (dark on light),
   so a thin dark wash + busy art still ate the text. The correct inverse
   is a WHITE/parchment wash at the same opacity progression, which mutes
   the art to a bright surface that dark text reads against cleanly.
   Slight warm tint (252,247,237) keeps it sympathetic to the parchment
   atmosphere rather than a clinical pure-white. */
html:not([data-theme="dark"]) .campaign-card.campaign-card--has-cover {
  background-image:
    url("data:image/svg+xml,%3Csvg opacity='0.18' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='46' fill='none' stroke='%232F7C85' stroke-width='2.2'/%3E%3Ccircle cx='50' cy='50' r='34' fill='none' stroke='%232F7C85' stroke-width='1.4'/%3E%3Ccircle cx='50' cy='50' r='19' fill='none' stroke='%232F7C85' stroke-width='1.4'/%3E%3Cline x1='50' y1='4' x2='50' y2='96' stroke='%232F7C85' stroke-width='1.2'/%3E%3Cline x1='4' y1='50' x2='96' y2='50' stroke='%232F7C85' stroke-width='1.2'/%3E%3Cline x1='18' y1='18' x2='82' y2='82' stroke='%232F7C85' stroke-width='0.9'/%3E%3Cline x1='82' y1='18' x2='18' y2='82' stroke='%232F7C85' stroke-width='0.9'/%3E%3Cpath d='M50 16 L84 50 L50 84 L16 50 Z' fill='none' stroke='%232F7C85' stroke-width='1.2'/%3E%3C/svg%3E"),
    var(--cover-tint, linear-gradient(transparent, transparent)),
    linear-gradient(180deg, rgba(252,247,237,0.70) 0%, rgba(252,247,237,0.48) 50%, rgba(252,247,237,0.74) 100%),
    var(--cover-img, linear-gradient(transparent, transparent));
}
/* Cover-card meta text — the dark-mode rule mixes white into --text-3 to
   keep the muted line readable against the dark wash. On light theme that
   washes the meta into the parchment wash. Restore the canonical --text-3
   reading by mixing toward INK instead of white. */
html:not([data-theme="dark"]) .campaign-card.campaign-card--has-cover .campaign-card-meta {
  color: color-mix(in srgb, var(--text-3) 55%, var(--text) 45%);
}

/* ── Threads tab (Phase 1) ─────────────────────────────────────────────────
   Loose-thread cards mirror .session-card / .entity-row recipe exactly:
   shadowless rest, hover lift + teal-tinted border + drop shadow. Status
   pill colors compose existing semantic tokens — no new color values added.
   The 4-state status enum is class-keyed (.thread-status-pill--hot etc.)
   rather than CSS-var-keyed because the colors are shared across all
   instances of a given status, not per-row.
   Tab layout composes .compendium-section-hdr for the header (eyebrow
   LEFT + CTA RIGHT, no padding) — .tab-panel default 1.5rem handles
   outer spacing. Don't add padding here or it'll double-pad.
*/
.threads-body {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

/* F1: thread cards are raw material for weaves now, not the feature.
   Compact: ~40% less vertical real estate per card so the GM can scan a
   dozen threads without scrolling. Context truncates to 2 lines via
   line-clamp; full text on hover via title attribute (added in JS). */
.thread-card {
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 0.55rem 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  transition: transform var(--t-hover) var(--ease), border-color var(--t-hover) var(--ease), box-shadow var(--t-hover) var(--ease);
}
.thread-card:hover {
  transform: translateY(-1px);
  border-color: rgba(47, 154, 163, 0.45);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.10);
}
/* v935: muted "ghost pill" recipe — same pattern as .camp-role-member
   (bg-3 + neutral border on light; rgba(255,255,255,0.07) + 0.18 on
   dark). No shadow halo; just a quiet tonal lift so the selected
   row reads "active" without shouting. */
.thread-card--selected {
  background: var(--bg-3);
  border-color: var(--border);
  box-shadow: none;
}
[data-theme="dark"] .thread-card--selected {
  background: rgba(255, 255, 255, 0.07);
  border-color: rgba(255, 255, 255, 0.18);
}

/* ── Fates F1.5: unified loom-table for Possible / Player / Threads ─────── */

/* Junction-thread callout — single line above the Possible Weaves
   carousel naming the thread(s) that connect 2+ weaves. Surfaces the
   campaign's structural connector once instead of letting it appear
   implicitly by repetition across cards. */
.fates-junction-callout {
  margin: 0.4rem 0 0.7rem;
  padding: 0.45rem 0.7rem;
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
  letter-spacing: 0.02em;
  border-left: 2px solid color-mix(in srgb, var(--accent-teal) 50%, transparent);
  background: color-mix(in srgb, var(--accent-teal) 7%, transparent);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.fates-junction-callout strong {
  color: var(--color-text-secondary);
  font-weight: var(--font-weight-bold);
}

/* Section wrapper — Possible Weaves carousel + Player Weave Surface
   share this container shape. The threads-list section uses the
   existing .compendium-section-hdr above #threads-body, so it doesn't
   need this. */
.fates-section {
  margin-bottom: var(--space-4);
}
.fates-section-hdr {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
  flex-wrap: wrap;
}
.fates-section-actions {
  display: flex;
  gap: 0.4rem;
  align-items: center;
}
/* .fates-section-meta removed — was an unused re-implementation of
   the .form-section-label eyebrow recipe. Compose .form-section-label
   if a meta line is needed here later. */

/* Suggestions modal — opened from the GM Weave header's See Suggestions
   trigger. Body composes the existing loom-table + a synthesis line
   below + nav/actions. Carousel prev/next buttons are small chevrons. */
.fates-suggestions-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.fates-suggestions-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.fates-suggestions-counter {
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}
.fates-suggestions-nav {
  display: flex;
  gap: 0.4rem;
  align-items: center;
}
.fates-carousel-btn {
  min-width: 2rem;
  padding: 0.2rem 0.6rem;
  font-size: var(--font-size-md);
  line-height: 1;
}
.fates-carousel-btn:disabled {
  opacity: 0.4;
  cursor: default;
}
.fates-suggestion-meta {
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
  letter-spacing: 0.01em;
}
/* Synthesis area — forward-looking narrative one-liner from
   /fates/weave-synthesis. Lazy-fetched on cursor change inside the
   modal. Eyebrow label "Possible scene" + the one-liner body. */
.fates-suggestion-synthesis {
  padding: 0.7rem 0.85rem;
  background: color-mix(in srgb, var(--accent-teal) 8%, var(--bg-2));
  border-left: 2px solid color-mix(in srgb, var(--accent-teal) 55%, transparent);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  min-height: 3.4rem;
}
.fates-suggestion-synthesis:empty {
  display: none;
}
.fates-suggestion-synthesis-label {
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}
.fates-suggestion-synthesis-label--error {
  color: var(--color-danger);
}
.fates-suggestion-synthesis-text {
  margin: 0;
  font-size: var(--font-size-md);
  color: var(--text);
  line-height: 1.4;
}
.fates-suggestion-synthesis-text--loading {
  color: var(--color-text-tertiary);
  font-style: italic;
}
.fates-suggestion-synthesis-text--error {
  color: var(--color-danger);
}
.fates-suggestions-footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  margin-top: 0.2rem;
}
.fates-suggestions-add {
  /* The primary CTA inside the modal — a touch heavier than the rest. */
  background: color-mix(in srgb, var(--accent-teal) 18%, var(--bg-2));
  border-color: color-mix(in srgb, var(--accent-teal) 55%, var(--border));
  color: var(--text);
}
.fates-suggestions-add:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent-teal) 28%, var(--bg-2));
  border-color: color-mix(in srgb, var(--accent-teal) 75%, var(--border));
}
.fates-suggestions-add:disabled {
  opacity: 0.5;
  cursor: default;
}

/* Thread description tooltip — shared across the threads list +
   GM Weave thread rail. Side-positioned by JS so it never sits on
   top of the cursor's read; a leader line (CSS ::before, drawn per
   data-placement attribute) connects the tooltip to the hovered
   row. Solid opaque background with strong elevation so it reads
   cleanly against busy backgrounds (codex tab, the loom-table,
   the GM Weave SVG ribbons). pointer-events:none so the cursor
   never accidentally hovers the tooltip and snags. */
.fates-thread-tooltip {
  position: fixed;
  /* Sit above everything — modals (~1000), the suggestions overlay,
     and the GM Weave SVG ribbons. The earlier z-index of 100 was
     beaten by the SVG paths' compositing layer in some browsers,
     leaving the tooltip text behind the ribbons and dots. */
  z-index: 10000;
  width: max-content;
  max-width: min(26rem, calc(100vw - 4rem));
  padding: 0.7rem 0.9rem;
  /* Layered background: hardcoded near-black fallback first, theme
     token on top. Defends against any token going semi-transparent
     in a future theme — the tooltip stays modal-grade opaque. */
  background-color: #131823;
  background: var(--bg-2, #131823);
  border: 1px solid color-mix(in srgb, var(--accent-teal) 50%, var(--border));
  border-radius: var(--radius-md);
  font-size: 0.84rem;
  line-height: 1.45;
  color: var(--text);
  /* Two-layer shadow for clean separation from busy backgrounds. */
  box-shadow:
    0 12px 32px rgba(0, 0, 0, 0.55),
    0 2px 6px rgba(0, 0, 0, 0.35);
  pointer-events: none;
  opacity: 1;
  transition: opacity var(--t-hover) var(--ease);
}
.fates-thread-tooltip.hidden {
  /* Override the global .hidden { display:none } so we can fade in/
     out smoothly. JS still flips the class, but visibility is the
     opacity transition rather than display:none → display:block. */
  display: block;
  opacity: 0;
  pointer-events: none;
  /* Move offscreen when hidden so it doesn't intercept anything
     during the fade. */
  left: -9999px;
  top: -9999px;
}
.fates-thread-tooltip-title {
  font-weight: var(--font-weight-bold);
  font-size: 0.88rem;
  color: var(--text);
  margin-bottom: 0.35rem;
  line-height: 1.3;
}
.fates-thread-tooltip-context {
  color: var(--color-text-secondary);
  white-space: pre-wrap;
}

/* Leader line — drawn as a CSS pseudo on the tooltip, positioned by
   the data-placement attribute that JS sets. The line points from
   the tooltip's edge back toward the hovered row's center; the
   row's center y-coord (or x, for below/above placements) is
   threaded in via --leader-y / --leader-x as a CSS variable. */
.fates-thread-tooltip[data-placement="right"]::before,
.fates-thread-tooltip[data-placement="left"]::before {
  content: '';
  position: absolute;
  top: var(--leader-y, 50%);
  width: 28px;
  height: 1.5px;
  background: color-mix(in srgb, var(--accent-teal) 65%, transparent);
  transform: translateY(-50%);
  pointer-events: none;
}
.fates-thread-tooltip[data-placement="right"]::before { left: -28px; }
.fates-thread-tooltip[data-placement="left"]::before  { right: -28px; }
.fates-thread-tooltip[data-placement="below"]::before,
.fates-thread-tooltip[data-placement="above"]::before {
  content: '';
  position: absolute;
  left: var(--leader-x, 50%);
  width: 1.5px;
  height: 28px;
  background: color-mix(in srgb, var(--accent-teal) 65%, transparent);
  transform: translateX(-50%);
  pointer-events: none;
}
.fates-thread-tooltip[data-placement="below"]::before { top: -28px; }
.fates-thread-tooltip[data-placement="above"]::before { bottom: -28px; }
/* Small tipped end cap on the tooltip side of the leader — a tiny
   teal dot where line meets box. Reads as a callout endpoint. */
.fates-thread-tooltip::after {
  content: '';
  position: absolute;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--accent-teal) 80%, transparent);
  pointer-events: none;
}
.fates-thread-tooltip[data-placement="right"]::after {
  left: -3px;
  top: var(--leader-y, 50%);
  transform: translateY(-50%);
}
.fates-thread-tooltip[data-placement="left"]::after {
  right: -3px;
  top: var(--leader-y, 50%);
  transform: translateY(-50%);
}
.fates-thread-tooltip[data-placement="below"]::after {
  top: -3px;
  left: var(--leader-x, 50%);
  transform: translateX(-50%);
}
.fates-thread-tooltip[data-placement="above"]::after {
  bottom: -3px;
  left: var(--leader-x, 50%);
  transform: translateX(-50%);
}

/* GM Weave empty hint — visible only when the section header is
   present but no threads are selected. The whole section stays
   visible so the See Suggestions trigger is reachable from the
   default view. */
/* v967: .fates-player-empty retired — typography flows from canonical
   .placeholder, padding from .placeholder--inline, strong-tag styling
   from the global .placeholder strong rule. The ID #fates-player-empty
   is preserved on the element for the existing JS show/hide handler. */

/* Drag-link discoverability hint — shown below the Weave graph
   whenever it's populated. Quiet typography so it sits as guidance,
   not a banner. */
.fates-player-drag-hint {
  margin: 0.5rem 0 0;
  padding: 0.45rem 0.7rem;
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
  letter-spacing: 0.01em;
  border-left: 2px solid color-mix(in srgb, var(--accent-teal) 40%, transparent);
  background: color-mix(in srgb, var(--accent-teal) 7%, transparent);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.fates-player-drag-hint strong {
  color: var(--color-text-secondary);
  font-weight: var(--font-weight-bold);
}

/* ── F1.7 GM Weave relationship-map graph ──
   Bipartite ribbon viz. Threads stack as labeled rows on the left
   rail; entities stack as type-colored pills on the right rail;
   colored ribbons curve between them per thread→entity touchpoint.
   Ribbons are colored by the thread's status pill, so when two
   threads pass through the same entity their ribbons converge and
   cross — that's the visual knot. SVG handles the ribbon paths;
   the rails are HTML for truncation + click-target ergonomics. */
.fates-weave-graph {
  position: relative;
  display: grid;
  /* 4-column layout (F1.7 v2):
       1: thread rail
       2: ribbon SVG gap
       3: entity rail (auto-touched + manually-added entities)
       4: entity picker sidebar (full codex search/checkbox list) */
  grid-template-columns:
    minmax(11rem, 18rem)
    minmax(5rem, 1fr)
    minmax(8rem, 14rem)
    minmax(11rem, 14rem);
  align-items: stretch;
  min-height: 6rem;
  /* Hard ceiling so the entity picker (which can carry hundreds of
     rows in a mature campaign) doesn't stretch the graph row to the
     full codex height. Inside the picker, the list scrolls. Any
     thread/entity rail exceeding 36rem also scrolls via overflow on
     each rail. */
  max-height: 36rem;
  padding: 0.6rem 0;
  background: color-mix(in srgb, var(--accent-teal) 7%, var(--bg-2));
  border: 1px solid color-mix(in srgb, var(--accent-teal) 18%, var(--border));
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.fates-weave-graph-rail {
  /* Allow each rail to scroll independently if it exceeds the
     graph's max-height (rare but possible for big weaves). */
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--accent-teal) 30%, transparent) transparent;
}

/* Ribbons SVG — overlays the entire graph, painted behind the rail
   rows. pointer-events:auto on the ribbons themselves (set per-path)
   so hover wiring picks them up. */
.fates-weave-graph-ribbons {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 1;
}
.fates-weave-graph-ribbons .fates-weave-ribbon {
  pointer-events: stroke;
}

/* Rails — HTML stack of rows. Threads on the left, entities on the
   right. Both rails sit ABOVE the SVG (z-index 2) so their text and
   pills are crisp and clickable. */
.fates-weave-graph-rail {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.4rem 0.7rem;
  min-width: 0;
}
.fates-weave-graph-rail--threads {
  grid-column: 1;
  grid-row: 1;
  align-items: stretch;
}
.fates-weave-graph-rail--entities {
  grid-column: 3;
  grid-row: 1;
  align-items: flex-start;
  padding-left: 0.4rem;
}

/* Thread row on the left rail — status pill + truncated title +
   heat + hover-revealed snip ×. Background tint on hover so the
   row reads as the active anchor for any ribbons that originate
   from it. */
.fates-weave-thread-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.35rem 0.5rem;
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--bg-1) 70%, transparent);
  border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
  font-size: var(--font-size-base);
  cursor: default;
  min-height: 2.1rem;
  transition: background var(--t-hover) var(--ease), border-color var(--t-hover) var(--ease);
}
.fates-weave-thread-row:hover {
  background: color-mix(in srgb, var(--accent-teal) 8%, var(--bg-2));
  border-color: color-mix(in srgb, var(--accent-teal) 45%, var(--border));
}
.weave-thread-title {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-weight: var(--font-weight-bold);
  color: var(--text);
}
.weave-thread-heat {
  flex-shrink: 0;
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-tertiary);
}
.fates-weave-thread-row .fates-row-action-btn--snip {
  opacity: 0;
  transition: opacity var(--t-hover) var(--ease);
}
.fates-weave-thread-row:hover .fates-row-action-btn--snip,
.fates-weave-thread-row:focus-within .fates-row-action-btn--snip {
  opacity: 1;
}

/* Entity row on the right rail — single pill aligned to its
   ribbon's endpoint. Hover bumps the pill so it reads as the
   highlighted anchor, and reveals the × snip button on the right
   edge so the GM can drop the entity from this weave's view. */
.fates-weave-entity-row {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.35rem 0.45rem;
  border-radius: var(--radius-md);
  min-height: 2.1rem;
  cursor: default;
  transition: background var(--t-hover) var(--ease);
}
.fates-weave-entity-row:hover {
  background: color-mix(in srgb, var(--accent-teal) 5%, transparent);
}
.fates-weave-entity-row .fates-warp-head {
  /* Stretch the pill so a longer entity name has more room to
     breathe; flex:1 hands the leftover row width to the pill so
     the × sits flush right. */
  flex: 1 1 auto;
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}
.fates-weave-entity-row .fates-row-action-btn--snip {
  flex-shrink: 0;
  opacity: 0;
  transition: opacity var(--t-hover) var(--ease);
}
.fates-weave-entity-row:hover .fates-row-action-btn--snip,
.fates-weave-entity-row:focus-within .fates-row-action-btn--snip {
  opacity: 1;
}

/* Per-row visual subordination so the GM can read the rail at a
   glance and know which entities anchor multiple selected threads
   (load-bearing) vs. which only touch one (supplementary) vs.
   which they added by hand (intentional outliers).

   Anchors get the default treatment — full pill saturation.
   Singletons soften slightly so the eye lands on anchors first
   without losing the type-color cue. Added entities sit at full
   presence (the GM put them there on purpose) and get a small
   "+" mark on the left so it's obvious at a glance they're not
   thread-derived. */
.fates-weave-entity-row--singleton .fates-warp-head {
  opacity: 0.78;
}
.fates-weave-entity-row--singleton:hover .fates-warp-head,
.fates-weave-entity-row--singleton:focus-within .fates-warp-head {
  opacity: 1;
}
.fates-weave-entity-added {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 0.85rem;
  height: 0.85rem;
  font-size: var(--font-size-xs);
  font-weight: 700;
  color: var(--accent-teal);
  opacity: 0.72;
  cursor: help;
}

/* Expander row — ghost button at the bottom of the entity rail.
   Surfaces low-commonality singletons that were collapsed under
   "+N more touched" so the GM can pull them into the AI input
   without leaving the workspace. Full-width, low-key, sits flush
   with the rail's vertical rhythm. */
.fates-weave-entity-expander {
  display: block;
  width: 100%;
  margin-top: var(--space-1);
  padding: 0.4rem 0.5rem;
  background: transparent;
  border: 1px dashed color-mix(in srgb, var(--accent-teal) 28%, var(--border));
  border-radius: var(--radius-md);
  color: var(--text-3);
  font-size: var(--font-size-xs);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: background var(--t-hover) var(--ease), color var(--t-hover) var(--ease), border-color var(--t-hover) var(--ease);
}
.fates-weave-entity-expander:hover,
.fates-weave-entity-expander:focus-visible {
  background: color-mix(in srgb, var(--accent-teal) 6%, transparent);
  border-color: color-mix(in srgb, var(--accent-teal) 45%, var(--border));
  color: var(--text-2);
  outline: none;
}
.fates-weave-entity-expander-glyph {
  display: inline-block;
  margin-right: 0.3rem;
  color: color-mix(in srgb, var(--accent-teal) 70%, var(--text-3));
  transition: transform var(--t-hover) var(--ease);
}
.fates-weave-entity-expander-glyph--up { transform: rotate(180deg); }

/* Entity picker sidebar — 4th column of the Weave graph. Search
   input at top + scrollable grouped checkbox list below. */
.fates-weave-entity-picker {
  grid-column: 4;
  grid-row: 1;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  padding: 0.4rem 0.55rem 0.4rem 0.65rem;
  border-left: 1px solid color-mix(in srgb, var(--accent-teal) 16%, var(--border));
  min-width: 0;
  max-height: 100%;
  overflow: hidden;
}
.fates-weave-entity-picker-search {
  width: 100%;
  padding: 0.35rem 0.55rem;
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  font-size: 0.8rem;
  color: var(--text);
  font-family: inherit;
}
.fates-weave-entity-picker-search:focus {
  outline: none;
  border-color: color-mix(in srgb, var(--accent-teal) 60%, var(--border));
}
.fates-weave-entity-picker-search::placeholder {
  color: var(--color-text-tertiary);
}
.fates-weave-entity-picker-list {
  flex: 1 1 auto;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding-right: 0.15rem;
  /* Scrollbar styling — keeps the inner panel from looking like
     it's punched through to the page chrome. */
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--accent-teal) 30%, transparent) transparent;
}
.fates-weave-entity-picker-list::-webkit-scrollbar { width: 6px; }
.fates-weave-entity-picker-list::-webkit-scrollbar-track { background: transparent; }
.fates-weave-entity-picker-list::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--accent-teal) 30%, transparent);
  border-radius: 3px;
}

.fates-entity-picker-placeholder {
  margin: 0.5rem 0;
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
}

.fates-entity-picker-group {
  display: flex;
  flex-direction: column;
  gap: 0.05rem;
}
.fates-entity-picker-eyebrow {
  margin: 0 0 0.2rem;
  /* form-section-label already gives uppercase + letter-spacing */
}

.fates-entity-picker-row {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.2rem 0.35rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: var(--font-size-sm);
  line-height: 1.25;
  transition: background 100ms ease;
  user-select: none;
}
.fates-entity-picker-row:hover {
  background: color-mix(in srgb, var(--accent-teal) 6%, transparent);
}
.fates-entity-picker-row input[type="checkbox"] {
  flex-shrink: 0;
  width: 0.95rem;
  height: 0.95rem;
  accent-color: var(--accent-teal);
  cursor: pointer;
  margin: 0;
}
.fates-entity-picker-name {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-weight: var(--font-weight-medium, 500);
}

/* Color-code the name by entity type — same canon as warp pills.
   Tinted text only (no background) so the picker stays compact and
   the eyebrow groups still read as the primary structure. */
.fates-entity-picker-row--pc        .fates-entity-picker-name { color: var(--accent-blue); }
.fates-entity-picker-row--companion .fates-entity-picker-name { color: var(--accent-teal); }
.fates-entity-picker-row--deity     .fates-entity-picker-name { color: var(--accent-gold); }
.fates-entity-picker-row--location  .fates-entity-picker-name { color: var(--accent-teal); }
.fates-entity-picker-row--npc       .fates-entity-picker-name { color: var(--color-muted-violet); }
.fates-entity-picker-row--item      .fates-entity-picker-name { color: var(--accent-ember); }
.fates-entity-picker-row--faction   .fates-entity-picker-name { color: var(--color-muted-violet); }
.fates-entity-picker-row--lore      .fates-entity-picker-name { color: var(--accent-green); }
.fates-entity-picker-row--spell     .fates-entity-picker-name { color: var(--accent-purple); }

/* Drag-and-drop: entity rows show a grab cursor; thread rows highlight
   when a draggable entity is hovering them as a valid drop target. */
.fates-weave-entity-row[draggable="true"],
.fates-entity-picker-row[draggable="true"] {
  cursor: grab;
}
.fates-weave-entity-row[draggable="true"]:active,
.fates-entity-picker-row[draggable="true"]:active {
  cursor: grabbing;
}
body.is-fates-dragging .fates-weave-thread-row {
  /* While a drag is in flight, prep the threads visually as
     potential targets (subtle border tint). The actual hovered
     row gets a stronger highlight via .is-drop-target below. */
  outline: 1px dashed color-mix(in srgb, var(--accent-teal) 25%, transparent);
  outline-offset: -1px;
}
.fates-weave-thread-row.is-drop-target {
  background: color-mix(in srgb, var(--accent-teal) 18%, var(--bg-2));
  border-color: color-mix(in srgb, var(--accent-teal) 70%, var(--border));
  outline: 1px solid color-mix(in srgb, var(--accent-teal) 80%, transparent);
}

/* ── F2: Saved Fates strip + Weave Fate workspace modal ────────────────── */

/* v947 first-touch intro banner — canonical dismissible per-tab tip.
   Subtle teal-tinted card with leading emoji + title + body. The Fates
   tab adds a workflow chart inside the body via .fates-workflow-* (kept
   Fates-specific). v978: class renamed from .fates-intro-* → .tab-intro-*
   when the pattern was extended to every top-level tab. Dismissable via
   ✕ in the corner; persistent per tab via tf_{tab}_intro_dismissed_v1
   localStorage flag — bump _vN when copy changes meaningfully so
   returning GMs see the updated intro once. */
.tab-intro-banner {
  position: relative;
  /* v985b: OUTER card max-width now matches the INNER .tab-intro-content
     max-width (56rem ≈ 896px). Previously OUTER stretched up to 904/1344px
     while INNER stayed at 56rem, leaving the card visibly wider than its
     own text on every tab EXCEPT Atlas — Atlas's flex-column parent
     happened to constrain OUTER to INNER's intrinsic size, which made it
     look "right". Locking OUTER to 56rem gives every tab that same
     content-hugging width without depending on parent layout quirks.
     Top margin stays load-bearing for split-layout tabs (Sessions /
     Party / Quests / Atlas / Manage have padding:0 on their tab-panel);
     Codex + Threads panels carry their own padding-top, so they override
     margin-top: 0 below. */
  max-width: 56rem;
  margin: var(--space-5) auto var(--space-4);
  padding: var(--space-4) var(--space-5);
  background: color-mix(in srgb, var(--accent-teal) 5%, var(--bg-2));
  border: 1px solid color-mix(in srgb, var(--accent-teal) 22%, var(--border));
  border-left: 3px solid color-mix(in srgb, var(--accent-teal) 55%, transparent);
  border-radius: var(--radius-lg);
  text-align: center;
}
/* Codex + Threads panels already have padding-top from the canonical
   single-column eyebrow pattern (var(--space-5) var(--space-4)). The
   banner's own margin-top would double the gap there, so suppress it. */
#tab-compendium > .tab-intro-banner,
#tab-threads > .tab-intro-banner {
  margin-top: 0;
}
/* v965b: pegged-width centered wrapper. Used by the Fates banner whose
   body contains a workflow chart — the pure inline-block trick doesn't
   work there because the body paragraph's max-content (single-line
   width with no wrapping) is much wider than the chart's natural
   width, so the inline-block grew to the body's preferred size and
   the chart sat left-aligned in a wide container. The explicit
   max-width caps the wrapper at the chart's typical row width
   (~40rem covers the four cards + three arrows + gaps with a small
   buffer); the body wraps inside that, the chart fills that, the
   wrapper centers as a unit via banner's text-align: center.
   Plain-text banners (no chart) compose this same wrapper for centered
   tip text with a sane line-length cap. */
.tab-intro-content {
  display: inline-block;
  text-align: left;
  max-width: 56rem;
  width: 100%;
  vertical-align: top;
}
.tab-intro-dismiss {
  position: absolute;
  top: 0.4rem;
  right: 0.4rem;
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-3);
  font-size: var(--font-size-sm);
  line-height: 1;
  padding: 0.2rem 0.45rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  opacity: 0.55;
  transition:
    opacity      var(--t-hover) var(--ease),
    color        var(--t-hover) var(--ease),
    background   var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease);
}
.tab-intro-dismiss:hover,
.tab-intro-dismiss:focus-visible {
  opacity: 1;
  color: var(--text-1);
  border-color: var(--border);
  background: var(--bg-3);
  outline: none;
}
.tab-intro-title {
  margin: 0 0 var(--space-2);
  padding-right: 2rem;
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-bold);
  color: var(--text);
  letter-spacing: 0.01em;
}
.tab-intro-body {
  margin: 0 0 var(--space-3);
  font-size: var(--font-size-sm);
  line-height: 1.55;
  color: var(--color-text-secondary);
}
.tab-intro-body em {
  font-style: normal;
  color: var(--accent-teal);
  font-weight: var(--font-weight-semibold);
}

/* ── Party tab: Chrome extension CTA card ──────────────────────────────
   Composes .tab-intro-banner for the chrome (dismiss button, padding,
   surface) but overrides the accent and adds a primary-action button.
   The teal-tinted strip + chrome puzzle emoji read as "this is a real
   tool offer," not just an informational tip. */
.party-extension-cta {
  border-left: 3px solid var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 6%, var(--bg-2));
}
.party-extension-cta .tab-intro-title {
  color: var(--text);
}
.party-extension-cta-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-top: var(--space-1);
  text-decoration: none;
}
/* Persistent secondary link at the foot of the Party tab — outlives the
   dismissible banner. Low visual weight: helper-text type, muted color,
   centered, teal anchor on hover so it reads as a link not a CTA. */
.party-tab-footer-link {
  margin: var(--space-4) 0 var(--space-2);
  text-align: center;
}
.party-tab-footer-link a {
  color: var(--accent-teal);
  text-decoration: none;
  font-weight: var(--font-weight-semibold);
}
.party-tab-footer-link a:hover,
.party-tab-footer-link a:focus-visible {
  text-decoration: underline;
}

/* ── Profile-button tier indicator ─────────────────────────────────────
   The header-tier-badge ribbon was retired (2026-05-13) because it ate
   too much real estate at the top of every screen. Tier is now indicated
   as a colored ring around the existing profile-button avatars (both
   #cs-profile-btn on campaign-selector and #profile-nav-btn on main-app
   nav share the .nav-profile-btn class). data-tier is set by
   _renderHeaderTierBadge after the billing-status fetch returns.

   Outline (not box-shadow) is the right tool here — both avatar buttons
   carry `overflow: hidden` to clip the inner image, which would also
   clip any positioned pseudo-element. `outline` sits OUTSIDE the
   element entirely, doesn't interact with the existing box-shadow on
   hover, and doesn't reflow the surrounding flex layout. Tier name
   reaches the user via the title attribute (set by JS) so a tooltip
   tells them which color means which plan. */
.nav-profile-btn[data-tier]:not([data-tier="free"]) {
  outline: 2px solid var(--tier-ring-color, var(--accent-teal));
  outline-offset: 2px;
}
.nav-profile-btn[data-tier="hobby"]       { --tier-ring-color: color-mix(in srgb, var(--accent-teal)   55%, transparent); }
.nav-profile-btn[data-tier="standard"]    { --tier-ring-color: color-mix(in srgb, var(--accent-teal)   75%, transparent); }
.nav-profile-btn[data-tier="founding_gm"] { --tier-ring-color: var(--accent-gold); }
.nav-profile-btn[data-tier="master"]      { --tier-ring-color: var(--accent-teal); }
.nav-profile-btn[data-tier="pro"]         { --tier-ring-color: var(--accent-purple); }

/* ── Pricing screen ────────────────────────────────────────────────────
   Public tier comparison. Centered column with a tier-card grid that
   reflows from 5-wide on desktop to a stack on mobile. The Founding
   GM card is visually featured with a ribbon + accent border. */
#pricing-screen {
  padding: var(--space-5) var(--space-4);
  overflow-y: auto;
}
.pricing-screen-inner {
  max-width: 1200px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.pricing-screen-hdr {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
}
.pricing-back-btn {
  align-self: flex-start;
}
.pricing-screen-title {
  margin: 0;
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-bold);
}
.pricing-screen-sub {
  margin: 0;
  font-size: var(--font-size-md);
  line-height: 1.55;
  color: var(--color-text-secondary);
  max-width: 720px;
}
.pricing-screen-sub--gm {
  /* The "For the GM: hit Record..." workflow line — slightly darker than
     the player-tagline above so it reads as the more important sentence. */
  color: var(--text);
  margin-top: var(--space-2);
}
.pricing-screen-sub--gm strong {
  color: var(--accent-teal);
}

/* v1203: Free/Player strip — the always-free starting point, a band
   above the priced grid (not a $0 column). Replaces the old
   .pricing-recap-bank-cta; folds the trial-recaps promise into the
   broader free-tier story. */
.pricing-free-strip {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: var(--space-3) var(--space-4);
  background: color-mix(in srgb, var(--accent-teal) 6%, var(--bg-2));
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.pricing-free-strip-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
}
.pricing-free-strip-badge {
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-bold);
  color: var(--text);
}
.pricing-free-strip-sub {
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-tertiary);
}
.pricing-free-strip-body {
  margin: 0;
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-secondary);
}
.pricing-free-strip-body strong { color: var(--accent-teal); }

/* v1203: Discord-on-every-plan callout. Teal band, framed as the
   delivery mechanism that makes the session paywall feel fair. One
   structural row — Discord is intentionally NOT a per-tier checkmark. */
.pricing-discord-callout {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: linear-gradient(
    135deg,
    color-mix(in srgb, var(--accent-teal) 14%, var(--bg-2)),
    color-mix(in srgb, var(--accent-teal) 5%, var(--bg-2))
  );
  border: 1px solid color-mix(in srgb, var(--accent-teal) 40%, transparent);
  border-radius: var(--radius-md);
}
.pricing-discord-callout-icon { font-size: var(--font-size-xl); flex-shrink: 0; }
.pricing-discord-callout-body {
  margin: 0;
  font-size: var(--font-size-sm);
  line-height: 1.45;
  color: var(--color-text-secondary);
}
.pricing-discord-callout-body strong { color: var(--accent-teal); }
.pricing-discord-callout-body code {
  font-family: var(--type-mono, ui-monospace, SFMono-Regular, monospace);
  font-size: 0.92em;
  padding: 0.05rem 0.3rem;
  background: color-mix(in srgb, var(--accent-teal) 12%, transparent);
  border-radius: var(--radius-sm);
}
.pricing-result-banner {
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  font-size: var(--font-size-md);
  line-height: 1.5;
}
.pricing-result-banner--success {
  background: color-mix(in srgb, var(--accent-teal) 10%, var(--bg-2));
  border: 1px solid color-mix(in srgb, var(--accent-teal) 40%, transparent);
  color: var(--text);
}
.pricing-result-banner--cancel {
  background: color-mix(in srgb, var(--accent-gold) 8%, var(--bg-2));
  border: 1px solid color-mix(in srgb, var(--accent-gold) 30%, transparent);
  color: var(--text);
}
.pricing-founding-banner {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: linear-gradient(
    135deg,
    color-mix(in srgb, var(--accent-gold) 14%, var(--bg-2)),
    color-mix(in srgb, var(--accent-teal) 10%, var(--bg-2))
  );
  border: 1px solid color-mix(in srgb, var(--accent-gold) 35%, transparent);
  border-radius: var(--radius-md);
}
.pricing-founding-icon {
  font-size: var(--font-size-xl);
  flex-shrink: 0;
}
.pricing-founding-body {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  font-size: var(--font-size-sm);
  line-height: 1.4;
}
.pricing-founding-body strong {
  color: var(--accent-gold);
}
.pricing-interval-toggle {
  display: inline-flex;
  align-self: center;
  padding: 0.25rem;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  gap: 0.15rem;
}
.pricing-interval-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.4rem 1rem;
  font: inherit;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-secondary);
  background: transparent;
  border: none;
  border-radius: var(--radius-full);
  cursor: pointer;
  transition:
    color      var(--t-hover) var(--ease),
    background var(--t-hover) var(--ease);
}
.pricing-interval-btn:hover {
  color: var(--text);
}
.pricing-interval-btn--active {
  color: var(--text);
  background: var(--bg-1);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.pricing-interval-save {
  padding: 0.05rem 0.45rem;
  font-size: var(--font-size-xs);
  color: var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 12%, transparent);
  border-radius: var(--radius-full);
}
.pricing-tier-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-3);
}
.pricing-tier-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  transition:
    transform    var(--t-hover) var(--ease),
    box-shadow   var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease);
}
.pricing-tier-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.14);
}
.pricing-tier-card--featured {
  border-color: color-mix(in srgb, var(--accent-gold) 55%, transparent);
  background: color-mix(in srgb, var(--accent-gold) 5%, var(--bg-2));
}
.pricing-tier-card--current {
  border-color: color-mix(in srgb, var(--accent-teal) 55%, transparent);
  background: color-mix(in srgb, var(--accent-teal) 5%, var(--bg-2));
}
.pricing-tier-ribbon {
  position: absolute;
  top: -0.6rem;
  right: 0.8rem;
  padding: 0.2rem 0.65rem;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--bg-1);
  background: var(--accent-gold);
  border-radius: var(--radius-full);
}
.pricing-tier-name {
  margin: 0;
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-bold);
}
/* Per-tier tagline — small italic positioning line right under the name.
   ("Solo GM, one campaign", "Active GM, multiple campaigns", etc.) */
.pricing-tier-tagline {
  margin: 0;
  font-size: var(--font-size-xs);
  font-style: italic;
  color: var(--color-text-tertiary);
  letter-spacing: 0.02em;
}
/* Founding GM's permanent-rate promise — lives in the card body so the
   strongest line of copy on the page sits at the point of decision. */
.pricing-tier-founding-promise {
  margin: var(--space-1) 0 0;
  padding: var(--space-2) var(--space-3);
  font-size: var(--font-size-sm);
  line-height: 1.4;
  color: var(--text);
  background: color-mix(in srgb, var(--accent-gold) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent-gold) 30%, transparent);
  border-radius: var(--radius-sm);
}
.pricing-tier-founding-promise strong {
  color: var(--accent-gold);
}
.pricing-tier-price {
  display: flex;
  align-items: baseline;
  gap: 0.25rem;
}
.pricing-tier-price-amount {
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-bold);
  color: var(--text);
}
.pricing-tier-price-per {
  font-size: var(--font-size-sm);
  color: var(--color-text-tertiary);
}
.pricing-tier-features {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  font-size: var(--font-size-sm);
  line-height: 1.4;
  color: var(--text);
}
/* v1203: rows carry an explicit ✓ span (.pricing-feature-check) now,
   not a global ::before — the flex row lets the check sit beside a
   stacked title+sub without the absolute-position alignment drift the
   ::before had on multi-line subs. */
.pricing-feature-row,
.pricing-tier-features > li.pricing-feature-inherit {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
}
.pricing-feature-check {
  color: var(--accent-teal);
  font-weight: var(--font-weight-bold);
  flex-shrink: 0;
  line-height: 1.3;
}
.pricing-feature-text {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}
/* Volume block — sessions/mo hero in campaign-cadence language, then
   the campaign count. Structural lead-in, dashed rule below it. */
.pricing-tier-features > li.pricing-feature-volume {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding-bottom: 0.45rem;
  margin-bottom: 0.2rem;
  border-bottom: 1px dashed color-mix(in srgb, var(--border) 60%, transparent);
}
.pricing-vol-sessions {
  font-weight: var(--font-weight-bold);
  color: var(--text);
  font-size: var(--font-size-md);
}
.pricing-vol-cadence {
  font-size: var(--font-size-xs);
  font-style: italic;
  color: var(--accent-teal);
  line-height: 1.35;
}
.pricing-vol-campaigns {
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
}
/* Title + sub stacked layout — bold title line + muted sub-line. */
.pricing-feature-title {
  font-weight: var(--font-weight-semibold);
  color: var(--text);
  line-height: 1.3;
}
.pricing-feature-sub {
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
  line-height: 1.4;
}
/* "Everything in Hobby / Master" compression line. */
.pricing-tier-features > li.pricing-feature-inherit em {
  font-style: italic;
  color: var(--color-text-secondary);
}
/* AI Prep Suite block — Master's lead bullet. The nested sublist holds
   Loose Threads / Weave / Fates, each with their own benefit line. */
.pricing-feature-sublist {
  margin: 0.35rem 0 0;
  padding: 0 0 0 1rem;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  font-size: var(--font-size-xs);
  color: var(--text);
  line-height: 1.4;
}
.pricing-feature-sublist li {
  position: relative;
  padding-left: 0.8rem;
}
.pricing-feature-sublist li::before {
  content: '•';
  position: absolute;
  left: 0;
  color: var(--accent-purple);
}
.pricing-cta-btn {
  margin-top: auto;
  width: 100%;
  justify-content: center;
}
/* v1203: GP voice demoted from the button to a flavor caption beneath
   it on subscribe/upgrade cards. Small, centered, teal — keeps the
   brand wink without making a $40 decision feel like a joke purchase. */
.pricing-cta-flavor {
  margin: 0.4rem 0 0;
  text-align: center;
  font-size: var(--font-size-xs);
  font-style: italic;
  color: var(--accent-teal);
}
.pricing-footnote {
  margin: var(--space-3) 0 0;
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
  text-align: center;
  line-height: 1.5;
}

/* v1203: expansion-revenue strip below the grid — top-ups + referral.
   Two equal cards, secondary visual weight (they're not the core
   subscribe decision). Stacks on narrow viewports. */
.pricing-expansion-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--space-3);
  margin-top: var(--space-4);
}
.pricing-expansion-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.35rem;
  padding: var(--space-3) var(--space-4);
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  font-size: var(--font-size-sm);
}
.pricing-expansion-card strong { color: var(--text); }
.pricing-expansion-sub {
  color: var(--color-text-secondary);
  line-height: 1.45;
}
.pricing-expansion-card .btn-ghost { margin-top: 0.2rem; }

/* v1207: two labeled top-up options (Recap vs Full session) so the
   "$1.99 a recap / $5.99 a full session" distinction is legible to a
   GM who doesn't yet know the difference. Each option: bold name, a
   one-line plain-English description of what it actually does, then
   the price. Thin rule between them. */
.pricing-topup-options {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  width: 100%;
  margin: 0.2rem 0;
}
.pricing-topup-option {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding-left: 0.6rem;
  border-left: 2px solid color-mix(in srgb, var(--accent-teal) 35%, transparent);
}
.pricing-topup-option-name {
  font-weight: var(--font-weight-semibold);
  color: var(--text);
}
.pricing-topup-option-desc {
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
  line-height: 1.4;
}
.pricing-topup-option-price {
  font-size: var(--font-size-xs);
  color: var(--accent-teal);
  font-weight: var(--font-weight-semibold);
}

/* v1080: GM-only "Unrevealed" section that sits above the Codex list
   when /tf-reveal has staged hidden entries. The dashed amber border +
   --pending tint signals "not yet public" without competing with the
   teal selected-state of revealed cards below. Compact horizontal
   cards because the GM is in a triage workflow — eyeball the thumb,
   click Reveal, move on. */
.pending-reveal-section {
  margin: 0 0 var(--space-4);
  padding: var(--space-3) var(--space-4);
  background: color-mix(in srgb, var(--accent-gold) 5%, var(--bg-2));
  border: 1px dashed color-mix(in srgb, var(--accent-gold) 45%, transparent);
  border-radius: var(--radius-lg);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.pending-reveal-eyebrow {
  color: color-mix(in srgb, var(--accent-gold) 80%, var(--text));
}
.pending-reveal-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--space-2);
}
.pending-reveal-card {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 0.6rem 0.75rem;
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.pending-reveal-thumb {
  flex: 0 0 auto;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--radius-sm);
  object-fit: cover;
  background: var(--bg-2);
  border: 1px solid var(--border);
}
.pending-reveal-thumb--empty {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--font-size-sm);
  color: var(--color-text-tertiary);
}
.pending-reveal-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}
.pending-reveal-type {
  font-size: var(--font-size-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}
.pending-reveal-name {
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-bold);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pending-reveal-btn {
  flex: 0 0 auto;
}

/* v1080: profile-modal usage rows that act as nav into #topup.
   v1196: row-trailing wrapper + inline "Top up →" chip give the row a
   resting affordance (the cursor:pointer-only treatment was invisible
   on touch). Chip is muted at rest and brightens on hover so the row
   still reads as a stat first, action second. */
.profile-usage-row--clickable {
  cursor: pointer;
  border-radius: var(--radius-sm);
  transition: background-color var(--t-hover) var(--ease);
  padding: 0.2rem 0.4rem;
  margin: 0 -0.4rem;
}
.profile-usage-row--clickable:hover {
  background: color-mix(in srgb, var(--accent-teal) 6%, transparent);
}
.profile-usage-row-trailing {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
}
.profile-usage-row-cta {
  font-size: var(--font-size-xs);
  letter-spacing: 0.02em;
  color: var(--color-text-tertiary);
  text-transform: none;
  transition: color var(--t-hover) var(--ease);
}
.profile-usage-row--clickable:hover .profile-usage-row-cta {
  color: var(--accent-teal);
}

/* v1080: top-up screen — reuses #pricing-screen chassis (padding,
   overflow, max-width via .pricing-screen-inner). Pack grid is its own
   thing because the pack card shape (icon + price + per-credit blurb)
   is simpler than tier cards. */
#topup-screen {
  padding: var(--space-5) var(--space-4);
  overflow-y: auto;
}
.topup-balance {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: var(--space-3) var(--space-4);
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}
.topup-balance-row {
  display: flex;
  justify-content: space-between;
  font-size: var(--font-size-sm);
}
.topup-balance-label {
  color: var(--color-text-secondary);
}
.topup-balance-value {
  font-weight: var(--font-weight-bold);
  color: var(--text);
}
.topup-pack-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-3);
}
.topup-pack-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  transition:
    transform    var(--t-hover) var(--ease),
    box-shadow   var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease);
}
.topup-pack-card:hover {
  transform: translateY(-2px);
  border-color: rgba(47, 154, 163, 0.45);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
.topup-pack-card--featured {
  border-color: color-mix(in srgb, var(--accent-teal) 55%, transparent);
  background: color-mix(in srgb, var(--accent-teal) 5%, var(--bg-2));
}
.topup-pack-label {
  margin: 0;
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-bold);
  color: var(--text);
}
.topup-pack-sublabel {
  margin: 0;
  font-size: var(--font-size-xs);
  font-style: italic;
  color: var(--color-text-tertiary);
  letter-spacing: 0.02em;
}
.topup-pack-price {
  display: flex;
  align-items: baseline;
  gap: 0.25rem;
}
.topup-pack-price-amount {
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-bold);
  color: var(--text);
}
.topup-pack-price-per {
  font-size: var(--font-size-sm);
  color: var(--color-text-tertiary);
}
.topup-pack-credits {
  font-size: var(--font-size-sm);
  color: var(--text);
}
.topup-pack-cta {
  margin-top: auto;
}
.pricing-topup-link {
  display: flex;
  justify-content: center;
  margin-top: var(--space-3);
}
/* v974: small caption above the chart — primes it as a process
   explainer ("here's how a Fate gets built") rather than a page-scroll
   map. Composes .form-section-label for the canonical eyebrow recipe;
   only adds margin so it doesn't crash into the chart. */
.fates-workflow-caption {
  margin: 0 0 var(--space-2);
}
/* Workflow chart — four step cards in a horizontal flex row, joined
   by ─→ arrows. Each step: emoji top, label, one-line subtitle. The
   row stacks vertically on narrow viewports with ↓ arrows. */
.fates-workflow-chart {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
}
.fates-workflow-step {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0.2rem;
  padding: 0.55rem 0.85rem;
  min-width: 6.5rem;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.fates-workflow-step-glyph {
  font-size: var(--font-size-lg);
  line-height: 1;
}
.fates-workflow-step-label {
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text);
}
.fates-workflow-step-sub {
  font-size: var(--font-size-xs);
  color: var(--text-3);
  line-height: 1.3;
}
.fates-workflow-step-sub em {
  font-style: normal;
  color: var(--accent-teal);
  font-weight: var(--font-weight-semibold);
}
/* v977: terminal LIVE card echoes the gold "Live" pill in the nav so
   the chart's last step reads as a forward pointer to that already-
   learned landmark (encoding-specificity: same retrieval cue across
   two surfaces collapses two lookup tasks into one). Subtle gold tint
   on the card surface, full --color-gold on the label + glyph. No
   flicker animation — the chart card is informational, not a control. */
.fates-workflow-step--live {
  border-color: color-mix(in srgb, var(--color-gold) 55%, transparent);
  background: color-mix(in srgb, var(--color-gold) 8%, var(--bg-2));
}
.fates-workflow-step--live .fates-workflow-step-label,
.fates-workflow-step--live .fates-workflow-step-glyph {
  color: var(--color-gold);
}
/* v977: equivalence connector (Fate = Live) instead of the arrow used
   between build steps. Tinted gold to reinforce the visual link to the
   gold LIVE card on the right. Slight font-weight bump so the equals
   reads at the same visual weight as the wider → glyph. */
.fates-workflow-arrow--equiv {
  color: color-mix(in srgb, var(--color-gold) 70%, var(--text-3));
  font-weight: var(--font-weight-bold);
  opacity: 0.85;
}
.fates-workflow-arrow {
  font-size: var(--font-size-lg);
  color: color-mix(in srgb, var(--accent-teal) 55%, var(--text-3));
  font-weight: var(--font-weight-regular);
  line-height: 1;
  user-select: none;
  padding: 0 0.15rem;
  opacity: 0.75;
}
@media (max-width: 720px) {
  /* Stack vertically with ↓ arrows on narrow viewports. */
  .fates-workflow-chart { flex-direction: column; align-items: stretch; }
  .fates-workflow-step { min-width: 0; }
  .fates-workflow-arrow { transform: rotate(90deg); align-self: center; padding: 0.1rem 0; }
}

/* My Fates strip — horizontal scrollable list of saved Fate cards above
   the Weave section. Hidden when zero saved Fates. Each card: small
   premise-type pill + title; click opens the workspace in 'saved' mode. */
.fates-saved-section {
  margin-bottom: var(--space-4);
}
.fates-saved-hdr {
  margin-bottom: var(--space-2);
}
/* v965: .fates-saved-empty retired — replaced by canonical
   .placeholder.placeholder--padded composition in the JS render. */
/* v924: quest-style split layout — left nav of compact rows, right
   detail panel. Mirrors .quest-layout / .quest-nav-col / .quest-
   detail-col patterns. */
.fates-saved-layout {
  display: flex;
  align-items: flex-start;
  min-height: 100%;
  gap: var(--space-4);
}
.fates-saved-nav-col {
  /* v930: matches .quest-nav-col verbatim — 42% width, 320px min. */
  flex: 0 0 42%;
  min-width: 320px;
  position: relative;
}
/* Fade divider between columns — same recipe as .quest-nav-col::after */
.fates-saved-nav-col::after {
  content: '';
  position: absolute;
  top: 0; right: calc(var(--space-4) * -0.5); bottom: 0;
  width: 1px;
  background: linear-gradient(to bottom,
    transparent 0%,
    color-mix(in srgb, var(--border) 50%, transparent) 15%,
    var(--border) 50%,
    color-mix(in srgb, var(--border) 50%, transparent) 85%,
    transparent 100%);
}
/* v941: matches .quest-detail-col verbatim — column owns the outer
   padding, the inner .journey-summary-card owns the surface bg + border
   + radius + internal padding. JS renders the .journey-summary-card
   chassis directly so Fates and Quests share one recipe instead of two
   parallel ones. */
/* v1162: see .quest-detail-col — outer col padding zeroed so the inner
   .journey-summary-card chassis is the only padded element. Without
   this, Fates detail content sat 24px further from its list than
   Chronicle content does from its session list. */
.fates-saved-detail-col {
  flex: 1;
  min-width: 0;
  padding: 0;
  position: sticky;
  top: 0;
  max-height: 100vh;
  overflow-y: auto;
  box-sizing: border-box;
}
.fates-saved-detail-panel {
  display: flex;
  flex-direction: column;
  min-height: 100%;
}
/* v976: .fates-saved-detail-placeholder retired (see quest sibling). */
/* Detail-header target suffix — same purple chip the list rows show,
   sized up slightly since the detail header has more breathing room. */
.fates-saved-detail-pc {
  font-size: var(--font-size-base);
  flex-shrink: 0;
}
.fates-saved-list {
  /* v931: gap matches .quest-nav-list. NO horizontal padding —
     unlike Quest (whose #tab-quests is padding:0, so the list
     provides the 16px inset), our #tab-threads already pads
     var(--space-4) horizontally per the v906 RCCA, so adding
     padding here doubled the left edge to 32px and made Fate
     rows look pushed right vs Quest rows. */
  padding: 0 0 var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
/* Stack to single column on narrow viewports — Quests does the same
   via a media query. */
@media (max-width: 720px) {
  .fates-saved-layout { flex-direction: column; }
  .fates-saved-nav-col { flex: 0 0 auto; min-width: 0; width: 100%; }
  .fates-saved-nav-col::after { display: none; }
  /* Mobile padding matches .quest-detail-col mobile (~line 3648). */
  .fates-saved-detail-col { position: static; max-height: none; width: 100%; padding: 1rem; }
}

/* Foretelling — Phase 2 Seeds-to-Plant panel. Sits between My Fates
   and Weave; surfaces every unplanted seed across every saved Fate
   so the GM sees them during routine prep, not just when they
   open the parent Fate card. Hidden when zero seeds pending. */
.fates-seeds-section {
  margin-bottom: var(--space-4);
}
.fates-seeds-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.fates-seeds-group {
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-left: 3px solid color-mix(in srgb, var(--accent-green) 50%, var(--border));
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
}
.fates-seeds-group-hdr {
  display: flex;
  align-items: center;
  margin-bottom: var(--space-2);
}
.fates-seeds-group-title {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.25rem 0.5rem;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  color: var(--text);
  font: inherit;
  font-weight: var(--font-weight-bold);
  cursor: pointer;
  transition:
    background   var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease);
}
.fates-seeds-group-title:hover,
.fates-seeds-group-title:focus-visible {
  background: color-mix(in srgb, var(--accent-teal) 6%, transparent);
  border-color: rgba(47, 154, 163, 0.45);
  outline: none;
}
.fates-seeds-group-name { flex: 0 1 auto; }
.fates-seeds-group-count {
  font-size: var(--font-size-xs);
  font-weight: 500;
  color: var(--text-3);
  letter-spacing: 0.04em;
}

/* Foretelling seed list — used in BOTH the inline scene render and
   the Seeds-to-Plant panel. Each row: checkbox + placement chip on
   the left, seed text + plausible-cover footnote on the right. The
   --planted state strikes through and dims so the eye scans for
   what's still pending. */
.fates-foretelling-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.fates-foretelling-seed {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-3);
  align-items: start;
  padding: var(--space-2) var(--space-3);
  background: color-mix(in srgb, var(--accent-teal) 4%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent-teal) 18%, var(--border));
  border-radius: var(--radius-md);
  transition: opacity var(--t-hover) var(--ease);
}
.fates-foretelling-seed--planted {
  opacity: 0.55;
}
.fates-foretelling-seed--planted .fates-foretelling-seed-text {
  text-decoration: line-through;
  text-decoration-color: var(--text-3);
}
.fates-foretelling-check {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  cursor: pointer;
  user-select: none;
}
.fates-foretelling-check input {
  cursor: pointer;
  accent-color: var(--accent-teal);
}
.fates-foretelling-placement {
  display: inline-block;
  padding: 0.05rem 0.45rem;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border-radius: var(--radius-full);
  border: 1px solid transparent;
  white-space: nowrap;
}
/* Placement urgency colors — same_session = ember (do this NOW),
   late = gold, mid = teal, early = muted. */
.fates-foretelling-placement--same_session {
  color: var(--accent-ember);
  border-color: color-mix(in srgb, var(--accent-ember) 35%, var(--border));
  background: color-mix(in srgb, var(--accent-ember) 10%, transparent);
}
.fates-foretelling-placement--late {
  color: var(--accent-gold);
  border-color: color-mix(in srgb, var(--accent-gold) 35%, var(--border));
  background: color-mix(in srgb, var(--accent-gold) 10%, transparent);
}
.fates-foretelling-placement--mid {
  color: var(--accent-teal);
  border-color: color-mix(in srgb, var(--accent-teal) 35%, var(--border));
  background: color-mix(in srgb, var(--accent-teal) 10%, transparent);
}
.fates-foretelling-placement--early {
  color: var(--text-3);
  border-color: var(--border);
  background: color-mix(in srgb, var(--text-1) 4%, transparent);
}
.fates-foretelling-body {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  min-width: 0;
}
.fates-foretelling-seed-text {
  margin: 0;
  font-size: var(--font-size-md);
  line-height: 1.45;
  color: var(--text);
}
.fates-foretelling-cover {
  margin: 0;
  font-size: var(--font-size-sm);
  font-style: italic;
  color: var(--text-3);
  line-height: 1.4;
}
.fates-foretelling-cover-label {
  font-style: normal;
  font-weight: 500;
  color: var(--text-2);
  margin-right: 0.3rem;
}
/* Inline-only — collapsed footnote when applicability=none. */
.fates-scene-section--foretelling-skipped p {
  margin-top: 0.25rem;
}

/* Edit-form — Foretelling fieldset borrows the npc_moves grid pattern
   but with a 3-column header (placement + remove × stretching to row
   right) above the two textareas (seed + plausible_cover). */
.fates-edit-foretelling-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
/* v942: footer of the in-edit foretelling fieldset — flex row of two
   ghost-pill actions (+ Add foreshadow / 🪄 Generate) sitting under
   the seed list. Hidden when applicability=`none`. */
.fates-edit-foretelling-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-top: var(--space-2);
}
/* v947: Quick Save tip line in the manual edit form — sits under the
   premise field, signals to free-tier GMs that they don't need to fill
   every field before saving. Subdued teal-tinted helper line. */
.fates-quick-save-hint {
  margin: -0.2rem 0 var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: color-mix(in srgb, var(--accent-teal) 6%, transparent);
  border-left: 2px solid color-mix(in srgb, var(--accent-teal) 50%, transparent);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
  line-height: 1.4;
}
.fates-edit-foretelling-row {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: var(--space-3);
  background: color-mix(in srgb, var(--accent-teal) 4%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent-teal) 18%, var(--border));
  border-radius: var(--radius-md);
}
.fates-edit-foretelling-row-hdr {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.fates-edit-foretelling-placement {
  flex: 0 0 auto;
  width: 9rem;
}
.fates-edit-foretelling-row-hdr .fates-edit-npc-remove {
  margin-left: auto;
}
/* Saved Fate row — v925: dimensions match .quest-nav-row exactly so
   the Fates list and Quests list read as siblings (radius-xl,
   0.85rem 1rem inner padding, gap 0.75rem, 600-weight title). The
   v924 button-with-direct-padding shape was tighter than Quests; the
   v925 outer-card + inner-hdr split mirrors Quests' chrome. */
/* v1087: chassis (bg, border, radius, hover) now from composing
   .tf-card --sm. .fates-saved-card keeps the <button> reset bits
   (display block, full-width, text-align left, font inherit) and zeros
   outer padding so the inner .fates-saved-card-hdr owns its padding. */
.fates-saved-card {
  display: block;
  width: 100%;
  text-align: left;
  font: inherit;
  color: var(--text-1);
  overflow: hidden;
}
.tf-card.fates-saved-card { padding: 0; }
.tf-card.fates-saved-card:focus-visible { outline: none; }
/* v1087: .fates-saved-card now composes .tf-card --sm chassis; selected
   state uses the spec .is-selected class which handles border + bg via
   the canonical .tf-card.is-selected rule. Watermark clear is automatic
   (spec: .tf-card.is-selected { background-image: none }). */
.fates-saved-card-hdr {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.85rem 1rem;
  user-select: none;
  /* Arcane sigil watermark added below via the shared selector list
     alongside .session-card-hdr / .quest-nav-hdr. v929: dropped the
     v928 per-type colored glyph in favor of the shared sigil — that
     IS the watermark Quests use, and the user wants Fates to
     duplicate the Quest formatting. */
}
.fates-saved-card-title {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-weight: 600;
  color: var(--text-1);
}
/* Purple target suffix — mirrors .quest-nav-pc verbatim. PC name
   when one was detected in the premise; otherwise the configured
   party label. */
.fates-saved-card-pc {
  color: var(--accent-purple);
  font-size: var(--font-size-sm);
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex-shrink: 1;
  min-width: 0;
}
.fates-saved-card-count {
  flex-shrink: 0;
  font-size: var(--font-size-xs);
  color: var(--text-3);
  font-weight: 500;
  white-space: nowrap;
}
.fates-saved-card-popout {
  flex-shrink: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--text-3);
  font-size: var(--font-size-base);
  line-height: 1;
  padding: 0.15rem 0.4rem;
  cursor: pointer;
  transition:
    opacity      var(--t-hover) var(--ease),
    color        var(--t-hover) var(--ease),
    background   var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease);
}
.fates-saved-card-popout:hover,
.fates-saved-card-popout:focus-visible {
  color: var(--accent-teal);
  border-color: color-mix(in srgb, var(--accent-teal) 40%, var(--border));
  background: color-mix(in srgb, var(--accent-teal) 8%, transparent);
  outline: none;
}
/* Brief teal pulse on the just-saved card so the GM sees their save
   land in the list without having to scan for it. Class is removed
   after 1.8s by the save handler. */
@keyframes fates-saved-pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent-teal) 60%, transparent); }
  60%  { box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent-teal) 0%,  transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent-teal) 0%,  transparent); }
}
.fates-saved-card--just-saved {
  border-color: color-mix(in srgb, var(--accent-teal) 70%, var(--border));
  animation: fates-saved-pulse 1.8s ease-out;
}
.fates-saved-card-type {
  flex-shrink: 0;
  padding: 0.05rem 0.45rem;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border-radius: var(--radius-full);
  border: 1px solid transparent;
}
.fates-saved-card-type--collision   { color: var(--accent-ember);  border-color: color-mix(in srgb, var(--accent-ember)  35%, var(--border)); background: color-mix(in srgb, var(--accent-ember)  10%, transparent); }
.fates-saved-card-type--convergence { color: var(--accent-purple); border-color: color-mix(in srgb, var(--accent-purple) 35%, var(--border)); background: color-mix(in srgb, var(--accent-purple) 10%, transparent); }
.fates-saved-card-type--catalyst    { color: var(--accent-gold);   border-color: color-mix(in srgb, var(--accent-gold)   35%, var(--border)); background: color-mix(in srgb, var(--accent-gold)   10%, transparent); }
.fates-saved-card-type--custom      { color: var(--color-text-secondary); border-color: var(--border); background: color-mix(in srgb, var(--text-1) 4%, transparent); }
.fates-saved-card-title {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-weight: var(--font-weight-bold);
}

/* Workspace modal — wider than the suggestions modal because the scene
   render needs room for multiple sections + an action bar. */
.fates-workspace-box {
  /* modal-box--xl already gives the size; this hook is for any
     workspace-specific tweaks. */
}
.fates-workspace-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  max-height: 80vh;
  overflow-y: auto;
}
.fates-workspace-stage {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* Stage 1: premise picker */
.fates-premise-status,
.fates-scene-status {
  min-height: 1.4rem;
}
.fates-premise-status .placeholder.error,
.fates-scene-status   .placeholder.error {
  color: var(--color-danger);
}
/* "Edits applied — click Save" inline status — subtle teal tint so it
   reads as a successful state-change cue, not an error. Auto-clears
   from JS after 4s. */
.fates-scene-status--applied {
  color: var(--accent-teal);
  font-style: normal;
}
/* "Saved Fate · read-only" eyebrow — surfaces the read-only contract
   at the top of the saved-Fate render so the GM doesn't hunt for an
   Edit button that isn't there. Composes .form-section-label for
   typography; just adds spacing + the subtle teal accent. */
.fates-scene-saved-eyebrow {
  display: block;
  margin-bottom: var(--space-2);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid color-mix(in srgb, var(--accent-teal) 18%, var(--border));
  color: var(--accent-teal);
}
.fates-premise-cards {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.55rem;
}

/* Loading-state skeleton — three faded shimmering placeholders
   that mirror the real premise card structure (left-border type
   color + title bar + body lines). The 8-second AI call previously
   showed a static "Proposing premises…" line; now the structure
   fills in as the model returns. Same animation drives the scene-
   stage skeleton below. */
@keyframes fates-skeleton-shimmer {
  0%   { background-position: -200% 0; }
  100% { background-position:  200% 0; }
}
.fates-skeleton-card,
.fates-skeleton-scene {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.85rem 1rem;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.fates-skeleton-card {
  border-left: 3px solid var(--border);
  margin-bottom: 0.55rem;
}
.fates-skeleton-card--collision   { border-left-color: color-mix(in srgb, var(--accent-ember) 40%, var(--border)); }
.fates-skeleton-card--convergence { border-left-color: color-mix(in srgb, var(--accent-purple) 40%, var(--border)); }
.fates-skeleton-card--catalyst    { border-left-color: color-mix(in srgb, var(--accent-gold) 40%, var(--border)); }
.fates-skeleton-line {
  height: 0.7rem;
  border-radius: var(--radius-sm);
  background: linear-gradient(
    90deg,
    color-mix(in srgb, var(--text-1) 6%, transparent) 0%,
    color-mix(in srgb, var(--text-1) 12%, transparent) 50%,
    color-mix(in srgb, var(--text-1) 6%, transparent) 100%
  );
  background-size: 200% 100%;
  animation: fates-skeleton-shimmer 1.6s linear infinite;
}
.fates-skeleton-line--title { height: 1rem; width: 50%; }
.fates-skeleton-line--short { width: 70%; }
.fates-skeleton-section {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin-top: 0.4rem;
}
.fates-premise-card {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 0.85rem 1rem;
  font: inherit;
  text-align: left;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-left: 3px solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text);
  transition:
    background   var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease),
    box-shadow   var(--t-hover) var(--ease),
    transform    var(--t-hover) var(--ease);
}
/* Card-hover lift — the card body is no longer a click target (rewriting
   replaced the single-click pick model), but a subtle hover lift still
   signals "this is the unit you're working with." The picking action lives
   in the explicit .fates-premise-pick-btn inside. */
.fates-premise-card:hover:not(.fates-premise-card--unavailable):not(.fates-premise-card--rewriting) {
  background: color-mix(in srgb, var(--accent-teal) 4%, var(--bg-2));
  border-color: rgba(47, 154, 163, 0.35);
}
.fates-premise-card--rewriting {
  opacity: 0.7;
  pointer-events: none;
}
.fates-premise-card--collision   { border-left-color: var(--accent-ember); }
.fates-premise-card--convergence { border-left-color: var(--accent-purple); }
.fates-premise-card--catalyst    { border-left-color: var(--accent-gold); }
.fates-premise-card--unavailable {
  cursor: default;
  opacity: 0.55;
}
.fates-premise-card-hdr {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
}
.fates-premise-card-type {
  flex-shrink: 0;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-text-tertiary);
}
.fates-premise-card--collision   .fates-premise-card-type { color: var(--accent-ember); }
.fates-premise-card--convergence .fates-premise-card-type { color: var(--accent-purple); }
.fates-premise-card--catalyst    .fates-premise-card-type { color: var(--accent-gold); }
.fates-premise-card-title {
  font-weight: var(--font-weight-bold);
  font-size: var(--font-size-md);
}
.fates-premise-card-text {
  margin: 0;
  font-size: var(--font-size-md);
  line-height: 1.4;
  color: var(--text);
}
.fates-premise-card-tension {
  margin: 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-tertiary);
  font-style: italic;
}

/* ── Per-premise rewrite affordances ──────────────────────────────────────
   The rewrite row sits below the premise text + tension and above the
   "Use this →" CTA. Revealed by default (per UX spec) so GMs see the
   action without hovering — important for mobile + keyboard nav. */
.fates-premise-rewrite-row {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin-top: 0.4rem;
  padding-top: 0.55rem;
  border-top: 1px dashed color-mix(in srgb, var(--border) 60%, transparent);
}
.fates-premise-chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}
.fates-premise-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.25rem 0.65rem;
  font-family: inherit;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-medium);
  letter-spacing: 0.02em;
  color: var(--text-2);
  background: color-mix(in srgb, var(--text-1) 3%, transparent);
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  cursor: pointer;
  white-space: nowrap;
  transition:
    background   var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease),
    color        var(--t-hover) var(--ease);
}
.fates-premise-chip:hover:not(:disabled), .fates-premise-chip:focus-visible {
  color: var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 8%, transparent);
  border-color: color-mix(in srgb, var(--accent-teal) 45%, transparent);
}
.fates-premise-chip:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.fates-premise-rewrite-input-row {
  display: flex;
  align-items: center;
  gap: 0.35rem;
}
.fates-premise-rewrite-input {
  flex: 1;
  padding: 0.4rem 0.6rem;
  font: inherit;
  font-size: var(--font-size-sm);
  color: var(--text);
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.fates-premise-rewrite-input:focus {
  outline: none;
  border-color: color-mix(in srgb, var(--accent-teal) 60%, transparent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-teal) 18%, transparent);
}
.fates-premise-rewrite-input::placeholder {
  color: var(--color-text-tertiary);
  font-style: italic;
}
.fates-premise-rewrite-submit {
  flex-shrink: 0;
}
.fates-premise-rewrite-status {
  min-height: 1em;
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
  font-style: italic;
}
.fates-premise-undo-btn {
  margin-left: auto;
  padding: 0.15rem 0.4rem;
  font: inherit;
  font-size: var(--font-size-sm);
  color: var(--color-text-tertiary);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition:
    background   var(--t-hover) var(--ease),
    color        var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease);
}
.fates-premise-undo-btn:hover, .fates-premise-undo-btn:focus-visible {
  color: var(--accent-teal);
  border-color: color-mix(in srgb, var(--accent-teal) 45%, transparent);
  background: color-mix(in srgb, var(--accent-teal) 6%, transparent);
}
.fates-premise-card-pick-row {
  display: flex;
  justify-content: flex-end;
  margin-top: 0.3rem;
}

.fates-premise-custom {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.7rem 0.85rem;
  background: color-mix(in srgb, var(--text-1) 3%, transparent);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.fates-premise-custom-input {
  width: 100%;
  padding: 0.5rem 0.65rem;
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  font-family: inherit;
  font-size: var(--font-size-sm);
  color: var(--text);
  resize: vertical;
}
.fates-premise-custom-input:focus {
  outline: none;
  border-color: color-mix(in srgb, var(--accent-teal) 60%, var(--border));
}
.fates-premise-custom-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

/* Stage 2: scene render */
.fates-scene-render {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}
.fates-scene-hdr {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding-bottom: 0.6rem;
  border-bottom: 1px solid color-mix(in srgb, var(--accent-teal) 18%, var(--border));
}
.fates-scene-type {
  display: inline-block;
  align-self: flex-start;
  padding: 0.1rem 0.55rem;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  border-radius: var(--radius-full);
  border: 1px solid transparent;
}
.fates-scene-type--collision   { color: var(--accent-ember);  border-color: color-mix(in srgb, var(--accent-ember)  40%, var(--border)); background: color-mix(in srgb, var(--accent-ember)  10%, transparent); }
.fates-scene-type--convergence { color: var(--accent-purple); border-color: color-mix(in srgb, var(--accent-purple) 40%, var(--border)); background: color-mix(in srgb, var(--accent-purple) 10%, transparent); }
.fates-scene-type--catalyst    { color: var(--accent-gold);   border-color: color-mix(in srgb, var(--accent-gold)   40%, var(--border)); background: color-mix(in srgb, var(--accent-gold)   10%, transparent); }
.fates-scene-type--custom      { color: var(--color-text-secondary); border-color: var(--border); background: color-mix(in srgb, var(--text-1) 4%, transparent); }
.fates-scene-title {
  margin: 0;
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-bold);
}
.fates-scene-premise {
  margin: 0;
  padding: 0.4rem 0.7rem;
  font-style: italic;
  color: var(--color-text-secondary);
  border-left: 2px solid color-mix(in srgb, var(--accent-teal) 50%, transparent);
  background: color-mix(in srgb, var(--accent-teal) 7%, transparent);
}
/* Eyebrow on each section composes .form-section-label per CLAUDE.md
   hard rule — typography lives there. We only override the small
   gap so the label hugs the body paragraph. */
.fates-scene-section .form-section-label {
  margin-bottom: 0.25rem;
}
.fates-scene-section p {
  margin: 0;
  font-size: var(--font-size-md);
  line-height: 1.5;
  color: var(--text);
}
.fates-scene-section--secret p {
  /* GM-only intel — slightly tinted so the GM can spot it as
     player-invisible at a glance. */
  color: color-mix(in srgb, var(--accent-gold) 25%, var(--text));
}
.fates-scene-section--secret .form-section-label {
  color: var(--accent-gold);
}

/* ── Per-section inline edit ────────────────────────────────────────────
   Each narrative section header pairs its eyebrow with a small ✎ pencil
   to the right. The pencil is unobtrusive in rest and brightens on hover
   so the GM picks up that sections are editable without the affordance
   shouting. The editing mode tints the section with a faint teal wash so
   it reads as "this paragraph is the one being touched." */
.fates-scene-section-hdr {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
}
.fates-scene-section-hdr .form-section-label {
  /* Eyebrow keeps its margin-bottom but the header row owns spacing now. */
  margin-bottom: 0;
}
.fates-scene-edit-pencil {
  margin-left: auto;
  padding: 0.1rem 0.4rem;
  font: inherit;
  font-size: var(--font-size-sm);
  line-height: 1;
  color: var(--color-text-tertiary);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  opacity: 0.55;
  transition:
    opacity      var(--t-hover) var(--ease),
    color        var(--t-hover) var(--ease),
    background   var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease);
}
.fates-scene-edit-pencil:hover,
.fates-scene-edit-pencil:focus-visible {
  opacity: 1;
  color: var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 8%, transparent);
  border-color: color-mix(in srgb, var(--accent-teal) 35%, transparent);
}
.fates-scene-section--editing {
  padding: 0.55rem 0.7rem;
  margin: 0 -0.7rem;
  background: color-mix(in srgb, var(--accent-teal) 4%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent-teal) 22%, var(--border));
  border-radius: var(--radius-md);
}
.fates-scene-section--editing .form-section-label {
  margin-bottom: 0.3rem;
}
.fates-scene-section--empty p.placeholder {
  margin: 0;
  font-size: var(--font-size-sm);
  font-style: italic;
}
.fates-scene-inline-edit {
  width: 100%;
  padding: 0.5rem 0.65rem;
  font: inherit;
  font-size: var(--font-size-md);
  line-height: 1.5;
  color: var(--text);
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  resize: vertical;
}
.fates-scene-inline-edit--single {
  resize: none;
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-bold);
}
.fates-scene-inline-edit:focus {
  outline: none;
  border-color: color-mix(in srgb, var(--accent-teal) 60%, transparent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-teal) 18%, transparent);
}
.fates-scene-inline-edit-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.4rem;
}
.fates-scene-inline-edit-hint {
  margin-left: auto;
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
}
/* Title + premise carry the pencil on a row beside the heading element,
   so the pencil aligns with the title baseline + sits to the right. */
.fates-scene-title-row,
.fates-scene-premise-row {
  display: flex;
  align-items: flex-start;
  gap: 0.4rem;
}
.fates-scene-title-row .fates-scene-title,
.fates-scene-premise-row .fates-scene-premise {
  flex: 1;
}
.fates-scene-title-row .fates-scene-edit-pencil,
.fates-scene-premise-row .fates-scene-edit-pencil {
  flex-shrink: 0;
  align-self: flex-start;
}
.fates-scene-inline-title-edit,
.fates-scene-inline-premise-edit {
  display: flex;
  flex-direction: column;
}

.fates-scene-npc {
  margin: 0;
  padding-left: 1.2rem;
  list-style: disc;
}
.fates-scene-npc li {
  margin-bottom: 0.25rem;
  font-size: var(--font-size-md);
  line-height: 1.45;
  color: var(--text);
}
.fates-scene-npc strong { color: var(--accent-teal); }
/* npc_moves presence badge — small label after the actor name showing
   whether they're in-scene, incoming, or offstage. F2 v901+. */
.fates-scene-npc-presence {
  display: inline-block;
  margin-left: 0.4rem;
  padding: 0.05rem 0.4rem;
  font-size: 0.65rem;
  font-weight: var(--font-weight-bold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border-radius: var(--radius-full);
  border: 1px solid transparent;
  vertical-align: middle;
}
.fates-scene-npc-presence--in-scene {
  color: var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 10%, transparent);
  border-color: color-mix(in srgb, var(--accent-teal) 35%, var(--border));
}
.fates-scene-npc-presence--incoming {
  color: var(--accent-ember);
  background: color-mix(in srgb, var(--accent-ember) 10%, transparent);
  border-color: color-mix(in srgb, var(--accent-ember) 35%, var(--border));
}
.fates-scene-npc-presence--offstage {
  color: var(--color-text-tertiary);
  background: color-mix(in srgb, var(--text-1) 4%, transparent);
  border-color: var(--border);
}
.fates-scene-npc-sep { color: var(--color-text-tertiary); }

.fates-scene-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: var(--space-2);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
  flex-wrap: wrap;
}
.fates-scene-actions-spacer { flex: 1 1 auto; }

/* Edit-before-save form — replaces the read-only scene render when
   the GM clicks Edit. Reuses .form-row for labels + controls so the
   typography matches the rest of the app's edit forms (entity, session,
   quest). NPC moves are a dynamic list with a 3-cell row (who / presence
   / what) + remove button. */
.fates-scene-edit-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  /* Subtle visual frame so the GM perceives "I'm in edit mode" without
     a heavy chrome change. Left rule + faint teal wash mirrors the
     inline-edit pattern used elsewhere in the app. */
  padding: var(--space-3) var(--space-4);
  border-left: 3px solid color-mix(in srgb, var(--accent-teal) 55%, transparent);
  background: color-mix(in srgb, var(--accent-teal) 4%, transparent);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
}
.fates-scene-edit-form .form-row label {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.fates-edit-input {
  width: 100%;
  padding: 0.4rem 0.55rem;
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text-1);
  font-family: inherit;
  font-size: var(--font-size-sm);
  line-height: 1.45;
  transition: border-color var(--t-hover) var(--ease), background var(--t-hover) var(--ease);
}
.fates-edit-input:focus {
  outline: none;
  border-color: color-mix(in srgb, var(--accent-teal) 60%, var(--border));
  background: color-mix(in srgb, var(--accent-teal) 6%, var(--bg-1));
}
.fates-edit-textarea {
  resize: vertical;
  min-height: 3rem;
}
.fates-edit-row-meta {
  align-self: flex-end;
  max-width: 14rem;
}
.fates-edit-fieldset {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-3) var(--space-2);
  margin: 0;
}
.fates-edit-fieldset legend {
  padding: 0 0.4rem;
  font-size: var(--font-size-sm);
  font-weight: 500;
  color: var(--text-2);
}
.fates-edit-npc-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.fates-edit-npc-row {
  display: grid;
  grid-template-columns: minmax(8rem, 14rem) minmax(6rem, 8rem) 1fr auto;
  gap: 0.5rem;
  align-items: start;
}
.fates-edit-npc-remove {
  align-self: stretch;
  padding: 0 0.55rem;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text-3);
  font-size: var(--font-size-sm);
  cursor: pointer;
  transition: background var(--t-hover) var(--ease), color var(--t-hover) var(--ease), border-color var(--t-hover) var(--ease);
}
.fates-edit-npc-remove:hover,
.fates-edit-npc-remove:focus-visible {
  background: color-mix(in srgb, var(--color-danger) 8%, transparent);
  border-color: color-mix(in srgb, var(--color-danger) 35%, var(--border));
  color: var(--color-danger);
  outline: none;
}
.fates-edit-add-npc-btn {
  margin-top: var(--space-2);
  align-self: flex-start;
  font-size: var(--font-size-sm);
}
/* On narrow viewports the 4-cell NPC row gets cramped; collapse to a
   stacked layout so each control gets full width. */
@media (max-width: 720px) {
  .fates-edit-npc-row {
    grid-template-columns: 1fr;
  }
  .fates-edit-npc-remove {
    align-self: flex-end;
    width: auto;
  }
}
.fates-scene-copy {
  background: color-mix(in srgb, var(--accent-teal) 18%, var(--bg-2));
  border-color: color-mix(in srgb, var(--accent-teal) 55%, var(--border));
}
.fates-scene-copy:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent-teal) 28%, var(--bg-2));
}

/* Manual-link modal — small modal-box, textarea + GM-only warning.
   Note input gets enough room for a couple of sentences without
   feeling cramped. */
.fates-manual-link-note {
  width: 100%;
  padding: 0.55rem 0.7rem;
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  font-family: inherit;
  font-size: var(--font-size-sm);
  color: var(--text);
  resize: vertical;
  min-height: 5rem;
  margin-top: 0.5rem;
}
.fates-manual-link-note:focus {
  outline: none;
  border-color: color-mix(in srgb, var(--accent-teal) 60%, var(--border));
}
.fates-manual-link-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-3);
  margin-top: var(--space-3);
}

/* Ribbon paths — the actual weave. Two visual signals encode the
   data:
     COLOR  = entity type (codex-canonical palette — same hue as
              the entity's pill on the right rail and the entity's
              row in the Codex tab). Reads consistently across the
              app — a Tiamat ribbon is gold whether the thread it
              comes from is hot or cold.
     WEIGHT = how many SELECTED threads the destination entity
              binds. An entity in 4 selected threads gets the
              heaviest stroke (it's the load-bearing weave bone);
              an entity in 1 thread gets the thinnest. The visual
              hierarchy reads at a glance: heavy ribbons converging
              on one entity = a real anchor; sparse thin ribbons
              flying past = context. */
.fates-weave-ribbon {
  fill: none;
  stroke-width: 2.2;          /* default — overridden by tier modifier */
  stroke-linecap: round;
  opacity: 0.75;
  transition: opacity var(--t-hover) var(--ease), stroke-width var(--t-hover) var(--ease);
}
/* Weight tiers — anchor count of the destination entity drives this. */
.fates-weave-ribbon--thin   { stroke-width: 1.6; opacity: 0.6;  }
.fates-weave-ribbon--medium { stroke-width: 2.4; opacity: 0.78; }
.fates-weave-ribbon--bold   { stroke-width: 3.2; opacity: 0.86; }
.fates-weave-ribbon--heavy  { stroke-width: 4.0; opacity: 0.92; }

/* Codex-canonical type palette — mirrors ENTITY_CFG in static/app.js
   and the .fates-warp-head--{type} variants on the entity-rail pills.
   A ribbon's stroke matches the hue of the pill it lands on. */
.fates-weave-ribbon--pc        { stroke: var(--accent-blue); }
.fates-weave-ribbon--companion { stroke: var(--accent-teal); }
.fates-weave-ribbon--deity     { stroke: var(--accent-gold); }
.fates-weave-ribbon--location  { stroke: var(--accent-teal); }
.fates-weave-ribbon--npc       { stroke: var(--color-muted-violet); }
.fates-weave-ribbon--item      { stroke: var(--accent-ember); }
.fates-weave-ribbon--faction   { stroke: var(--color-muted-violet); }
.fates-weave-ribbon--lore      { stroke: var(--accent-green); }
.fates-weave-ribbon--spell     { stroke: var(--accent-purple); }

/* Highlight state — when the GM hovers a thread row, an entity row,
   or a ribbon path, all matching ribbons go .is-active and the rest
   go .is-dim. Active stroke bumps one tier heavier; dim drops to
   ghost opacity so the overall shape stays legible. */
.fates-weave-graph.is-highlighting .fates-weave-ribbon.is-dim {
  opacity: 0.14;
}
.fates-weave-graph.is-highlighting .fates-weave-ribbon.is-active {
  opacity: 1;
  /* Bump stroke beyond the tier — the active ribbon should always
     read as the focal point regardless of weight tier. */
  stroke-width: calc(var(--ribbon-stroke, 2.4px) + 1.2px);
}
.fates-weave-ribbon--thin.is-active   { stroke-width: 2.8; }
.fates-weave-ribbon--medium.is-active { stroke-width: 3.6; }
.fates-weave-ribbon--bold.is-active   { stroke-width: 4.4; }
.fates-weave-ribbon--heavy.is-active  { stroke-width: 5.2; }
.fates-weave-ribbon:hover { opacity: 1; }

/* ── Loom-table — the unifying visual across all three Fates surfaces ──
   2-column CSS grid:
      col 1: thread-info  (checkbox + status pill + title + heat + actions)
      col 2: warp/weft bar (entity pills as headers; knots as data)
   All cells emit flat into the parent grid (no row wrappers) so column
   widths stay aligned across header + every data row. The warp-bar
   (col 2 row 1) and each weft-bar (col 2 rows 2..N) both use flex
   space-around with the same N items, so an entity pill's centerline
   matches its knot's centerline column-by-column. */
.fates-loom-table {
  display: grid;
  grid-template-columns: clamp(14rem, 32%, 22rem) minmax(0, 1fr);
  row-gap: 0.35rem;
  column-gap: 0.7rem;
  align-items: center;
}

/* Info column — checkbox + status pill + title + heat badge + actions */
.fates-loom-info {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.3rem 0.45rem;
  border-radius: var(--radius-md);
  min-width: 0;       /* allow title to truncate */
  font-size: var(--font-size-base);
}
.fates-loom-info--head {
  /* empty corner cell above the row stack — keeps col 1 aligned */
  padding: 0;
  min-height: 0;
}
.fates-loom-info--compact {
  padding: 0.2rem 0.35rem;
  font-size: 0.8rem;
}

/* Title — single line truncate. Tooltip on hover (set in JS via title=)
   restores the full string for long names. */
.fates-loom-title {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-weight: var(--font-weight-bold);
  color: var(--text);
}

/* Action icons — dismiss / mark resolved. Small, low-key, right-aligned. */
.fates-loom-actions {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  margin-left: auto;
  flex-shrink: 0;
}
.fates-row-action-btn {
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-full);
  padding: 0.05rem 0.4rem;
  font-size: var(--font-size-base);
  line-height: 1;
  color: var(--color-text-tertiary);
  cursor: pointer;
  transition: background var(--t-hover) var(--ease), color var(--t-hover) var(--ease), border-color var(--t-hover) var(--ease);
}
.fates-row-action-btn:hover {
  background: color-mix(in srgb, var(--text-1) 6%, transparent);
  color: var(--text);
  border-color: var(--border);
}

/* "+N" shares-with-weave badge — only rendered on threads-list rows
   when GM Weave is non-empty AND the row is not already in the
   weave. Tells the GM "this thread is adjacent to your working
   weave by N shared anchor entities" — surfaces near-misses without
   forcing the GM to scan column-by-column. Sits inline with the
   title, before the heat score. */
.fates-weave-adj-badge {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  padding: 0.05rem 0.4rem;
  font-size: 0.7rem;
  font-weight: var(--font-weight-bold);
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
  border-radius: var(--radius-full);
  color: var(--accent-teal);
  background: color-mix(in srgb, var(--accent-teal) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent-teal) 35%, var(--border));
  cursor: help;
}

/* Snip × on Your Weave rows — hover-revealed only. The working surface
   should read as quiet at rest; the × appears when the GM declares
   intent (hovering the row pair). On its own hover the icon tints
   toward --color-danger so the destructive read is unambiguous. */
.fates-loom-actions--snip {
  opacity: 0;
  transition: opacity var(--t-hover) var(--ease);
}
.fates-loom-info:hover .fates-loom-actions--snip,
.fates-loom-info:has(+ .fates-loom-bar:hover) .fates-loom-actions--snip,
.fates-loom-info:focus-within .fates-loom-actions--snip {
  opacity: 1;
}
.fates-row-action-btn--snip:hover {
  color: var(--color-danger);
  background: color-mix(in srgb, var(--color-danger) 8%, transparent);
  border-color: color-mix(in srgb, var(--color-danger) 30%, var(--border));
}

/* Bar column — both warp (header) and weft (per-row) live in col 2 */
.fates-loom-bar {
  position: relative;
  display: flex;
  justify-content: space-around;
  align-items: center;
  gap: 0.3rem;
  min-height: 1.4rem;
  padding: 0.15rem 0.45rem;
  border-radius: var(--radius-md);
}
.fates-loom-bar--warp {
  align-items: flex-end;
  padding-bottom: 0.45rem;
  border-bottom: 1px dashed color-mix(in srgb, var(--accent-teal) 28%, transparent);
}
/* Weft rail — drawn behind the knots; ::before is a thin teal line
   spanning the bar's full width. The knots sit on top, masked by the
   bg-2 ring so they look like beads on a string. */
.fates-loom-bar--weft::before {
  content: '';
  position: absolute;
  left: 0.45rem;
  right: 0.45rem;
  top: 50%;
  height: 1.5px;
  background: color-mix(in srgb, var(--accent-teal) 40%, transparent);
  z-index: 1;
  pointer-events: none;
}

/* Hover the row pair — both info and bar light up together. :has()
   lets us style the info cell when its sibling bar is hovered;
   adjacent + handles the reverse. */
.fates-loom-info:hover,
.fates-loom-info:has(+ .fates-loom-bar:hover),
.fates-loom-info:hover + .fates-loom-bar,
.fates-loom-info + .fates-loom-bar:hover {
  background: color-mix(in srgb, var(--accent-teal) 5%, transparent);
}
/* Selected — checkbox checked. Slightly heavier teal across the pair. */
.fates-loom-info--selected,
.fates-loom-bar--selected {
  background: color-mix(in srgb, var(--accent-teal) 8%, transparent);
}
/* Selected weft rail: muted teal track flips to a luminous line so the
   connection between the active loose thread and its entity columns reads
   as live linkage. The pulse is a quiet breathing of the .type-filter-btn
   hover halo (~0.18 white alpha at rest), not a throb — same family of
   light, just slower. White on dark; saturated ink-teal on light (white
   wouldn't survive against the parchment surface). */
.fates-loom-bar--weft.fates-loom-bar--selected::before {
  background: var(--accent-teal);
  height: 1.5px;
  box-shadow: 0 0 8px 1px color-mix(in srgb, var(--accent-teal) 18%, transparent);
  animation: tf-weft-pulse 2.4s ease-in-out infinite;
}
[data-theme="dark"] .fates-loom-bar--weft.fates-loom-bar--selected::before {
  background: rgba(255, 255, 255, 0.85);
  box-shadow: 0 0 8px 1px rgba(255, 255, 255, 0.18);
  animation: tf-weft-pulse-dark 2.4s ease-in-out infinite;
}
@keyframes tf-weft-pulse {
  0%, 100% { box-shadow: 0 0 8px 1px color-mix(in srgb, var(--accent-teal) 16%, transparent); }
  50%      { box-shadow: 0 0 12px 2px color-mix(in srgb, var(--accent-teal) 26%, transparent); }
}
@keyframes tf-weft-pulse-dark {
  0%, 100% { box-shadow: 0 0 8px 1px rgba(255, 255, 255, 0.16); }
  50%      { box-shadow: 0 0 12px 2px rgba(255, 255, 255, 0.26); }
}
@media (prefers-reduced-motion: reduce) {
  .fates-loom-bar--weft.fates-loom-bar--selected::before { animation: none; }
}

/* Warp head pill — entity name above its column.
   Per-type --type-color drives the tint; Codex-canonical palette
   (matches ENTITY_CFG in static/app.js). Hubs get an outlined treatment
   so the GM can spot "this column is everywhere — probably setting." */
.fates-warp-head {
  --type-color: var(--accent-teal);
  display: inline-block;
  padding: 0.08rem 0.45rem;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
  border-radius: var(--radius-full);
  white-space: nowrap;
  color: var(--type-color);
  background: color-mix(in srgb, var(--type-color) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--type-color) 45%, var(--border));
  max-width: 8rem;
  overflow: hidden;
  text-overflow: ellipsis;
}
.fates-warp-head--hub {
  /* Hubs (>40% of threads) — visually muted so they read as
     "background context, not signal." Earlier we used a dashed
     border, but UX agent's note: dashed reads as "incomplete /
     disabled" before it reads as "ubiquitous." Subdued opacity +
     desaturated tint communicates "this column is everywhere"
     without looking like an error state. JS sets a tooltip naming
     the actual count. */
  opacity: 0.55;
  filter: saturate(0.55);
}

/* Suggestions modal — × snip on each warp pill. Replaces the old
   "Mark setting" button trigger. Click the × on an entity that's
   driving suggestions you don't want, and the entity gets added
   to the campaign blocklist (excluded from affinity scoring) and
   the suggestions list refreshes. The pill stays its full size;
   the × sits flush to the right edge of the pill, hover-revealed
   so the rest state stays clean. */
.fates-warp-head--snip {
  /* Reserve a hair of space on the right so the × doesn't crowd
     the entity name when it appears on hover. */
  padding-right: 0.3rem;
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
}
.fates-warp-head-name {
  /* Lets the name truncate via the existing max-width / overflow
     rules on .fates-warp-head, while the × stays full-size. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.fates-warp-snip {
  flex-shrink: 0;
  width: 1rem;
  height: 1rem;
  padding: 0;
  border: none;
  background: transparent;
  color: inherit;
  opacity: 0;
  font-size: var(--font-size-base);
  line-height: 1;
  cursor: pointer;
  border-radius: 50%;
  transition: opacity var(--t-hover) var(--ease), background var(--t-hover) var(--ease), color var(--t-hover) var(--ease);
}
.fates-warp-head--snip:hover .fates-warp-snip,
.fates-warp-head--snip:focus-within .fates-warp-snip {
  opacity: 0.85;
}
.fates-warp-snip:hover {
  opacity: 1 !important;
  background: color-mix(in srgb, var(--color-danger) 15%, transparent);
  color: var(--color-danger);
}

/* Suggestions modal hint — small explanatory line above the loom
   table. Tells the GM the × on each pill is the way to mark setting
   noise, replacing the old Mark Setting button. Quiet typography so
   it doesn't compete with the loom or the synthesis text. */
.fates-suggestions-hint {
  margin: 0;
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
  font-style: italic;
}
.fates-warp-head--pc        { --type-color: var(--accent-blue); }
.fates-warp-head--companion { --type-color: var(--accent-teal); }
.fates-warp-head--deity     { --type-color: var(--accent-gold); }
.fates-warp-head--location  { --type-color: var(--accent-teal); }
.fates-warp-head--npc       { --type-color: var(--color-muted-violet); }
.fates-warp-head--item      { --type-color: var(--accent-ember); }
.fates-warp-head--faction   { --type-color: var(--color-muted-violet); }
.fates-warp-head--lore      { --type-color: var(--accent-green); }
.fates-warp-head--spell     { --type-color: var(--accent-purple); }

/* Knot — colored disc where a thread crosses an entity column.
   The bg-2 shadow ring masks the rail line so the knot reads as a
   bead. Empty knots are transparent placeholders that preserve grid
   spacing without drawing anything. */
.fates-knot {
  --type-color: var(--accent-teal);
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 50%;
  background: var(--type-color);
  box-shadow: 0 0 0 2px var(--bg-2);
  position: relative;
  z-index: 2;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
  flex-shrink: 0;
}
.fates-knot--empty {
  width: 0.7rem;
  height: 0.7rem;
  background: transparent;
  box-shadow: none;
  z-index: 0;
  flex-shrink: 0;
}
.fates-knot--pc        { --type-color: var(--accent-blue); }
.fates-knot--companion { --type-color: var(--accent-teal); }
.fates-knot--deity     { --type-color: var(--accent-gold); }
.fates-knot--location  { --type-color: var(--accent-teal); }
.fates-knot--npc       { --type-color: var(--color-muted-violet); }
.fates-knot--item      { --type-color: var(--accent-ember); }
.fates-knot--faction   { --type-color: var(--color-muted-violet); }
.fates-knot--lore      { --type-color: var(--accent-green); }
.fates-knot--spell     { --type-color: var(--accent-purple); }

/* When the row pair is hovered, knots bloom — same affordance as the
   v875 weave card. Hovering an entity column suggests "this column is
   what binds these threads." */
.fates-loom-info:hover + .fates-loom-bar .fates-knot:not(.fates-knot--empty),
.fates-loom-bar:hover .fates-knot:not(.fates-knot--empty),
.fates-loom-promote:hover .fates-knot:not(.fates-knot--empty),
.fates-loom-promote:focus-visible .fates-knot:not(.fates-knot--empty) {
  transform: scale(1.25);
  box-shadow:
    0 0 0 2px var(--bg-2),
    0 0 0 4px color-mix(in srgb, var(--type-color) 38%, transparent);
}

/* Multi-select checkbox on each thread card */
.thread-select {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  margin-right: 0.35rem;
}
/* v933: custom checkbox — kills the OS-default grey backdrop (which
   read as visual noise on the thread row). Transparent at rest with
   a subtle border; teal fill + white check on selected. */
.thread-select-cb {
  appearance: none;
  -webkit-appearance: none;
  width: 1.05rem;
  height: 1.05rem;
  cursor: pointer;
  background: transparent;
  border: 1.5px solid color-mix(in srgb, var(--text-3) 60%, transparent);
  border-radius: var(--radius-sm);
  position: relative;
  flex-shrink: 0;
  transition:
    border-color var(--t-hover) var(--ease),
    background   var(--t-hover) var(--ease);
}
.thread-select-cb:hover {
  border-color: var(--accent-teal);
}
.thread-select-cb:checked {
  background: var(--accent-teal);
  border-color: var(--accent-teal);
}
.thread-select-cb:checked::after {
  content: '✓';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: #fff;
  font-size: 0.78rem;
  font-weight: 700;
  line-height: 1;
}
.thread-select-cb:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--accent-teal) 60%, transparent);
  outline-offset: 2px;
}

/* "shares: X, Y" inline tag row */
.thread-shared-tags {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.35rem;
  margin-top: 0.1rem;
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
}
.thread-shared-label {
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: var(--font-weight-bold);
}
.thread-shared-tag {
  padding: 0.1rem 0.5rem;
  background: color-mix(in srgb, var(--text-1) 4%, transparent);
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
  white-space: nowrap;
}

/* Blocklist editor — list of toggleable rows */
.fates-blocklist-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.45rem 0.6rem;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
}
.fates-blocklist-row:hover {
  background: color-mix(in srgb, var(--text-1) 3%, transparent);
}
.fates-blocklist-row .fates-blocklist-cb {
  flex-shrink: 0;
  width: 1.05rem;
  height: 1.05rem;
  accent-color: var(--accent-teal);
}
.fates-blocklist-type {
  flex-shrink: 0;
  width: 4.5rem;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
  letter-spacing: 0.05em;
  color: var(--color-text-tertiary);
}
.fates-blocklist-name {
  flex: 1;
  color: var(--text);
}
.fates-blocklist-meta {
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
  font-variant-numeric: tabular-nums;
}

.thread-card-hdr {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  flex-wrap: wrap;
}

.thread-card-title {
  font-weight: 600;
  font-size: var(--font-size-sm);
  color: var(--text);
  flex: 1;
  min-width: 0;
}

.thread-status-pill {
  display: inline-flex;
  align-items: center;
  padding: 0.05rem 0.45rem;
  border-radius: 999px;
  font-size: 0.65rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  border: 1px solid currentColor;
  background: transparent;
}
.thread-status-pill--hot      { color: var(--color-danger); }
.thread-status-pill--warm     { color: var(--color-warning); }
.thread-status-pill--cold     { color: var(--color-info); }
.thread-status-pill--dormant  { color: var(--color-text-tertiary); }
.thread-status-pill--resolved { color: var(--color-success); }

.thread-heat-score {
  font-size: 0.7rem;
  color: var(--color-text-tertiary);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

/* F1: context truncates to 2 lines via line-clamp. GMs scanning a dozen
   threads benefit from density; full title attribute on the card surfaces
   the rest on hover. Reduced font + tighter line-height. */
.thread-context {
  margin: 0;
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
  line-height: 1.35;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Provenance line — single breadcrumb under every hook for trust calibration.
   Pattern: "Surface › Entity · last seen Session N"  (e.g., "Codex › Roth ·
   last seen Session 22"). Tertiary-text register so it sits below the title
   visually but stays scannable. The entity portion is a clickable link
   (.entity-link recipe). Separators sit at low contrast so the eye lands on
   the entity, not the punctuation. */
.thread-provenance {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.05rem;
  font-size: var(--font-size-xs);
  color: var(--color-text-tertiary);
  line-height: 1.45;
}
.thread-prov-surface {
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: 600;
  color: var(--color-text-secondary);
}
.thread-prov-sep {
  color: var(--color-text-tertiary);
  opacity: 0.55;
}
.thread-prov-entity {
  /* Composes .entity-link visually but renders as a button — keep it
     inline-baseline so it tracks alongside the surrounding text. */
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--accent-teal);
  cursor: pointer;
  text-decoration: none;
}
.thread-prov-entity:hover, .thread-prov-entity:focus-visible {
  text-decoration: underline;
}
.thread-prov-ref {
  color: var(--color-text-tertiary);
  font-variant-numeric: tabular-nums;
}

.thread-entity-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}
.thread-entity-chip {
  font-size: var(--font-size-xs);
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  background: var(--bg-3);
  color: var(--text);
  border: 1px solid var(--border);
}

.thread-card-actions {
  display: flex;
  gap: 0.3rem;
  flex-wrap: wrap;
  margin-top: 0.15rem;
}
.thread-card-actions .btn-new {
  /* Smaller buttons for the now-compact card. Match the .btn-sort/.btn-filter
     visual register used in toolbars. */
  padding: 0.2rem 0.6rem;
  font-size: 0.72rem;
}

/* Locked preview cards (hobby/standard tier). Blur + mute interaction;
   pointer-events:none so the upgrade CTA is the only viable target. */
.thread-card--locked {
  filter: blur(4px);
  opacity: 0.55;
  pointer-events: none;
  user-select: none;
}

.thread-upgrade-banner {
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  padding: 1.25rem 1.25rem 1.1rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
}
.thread-upgrade-body {
  margin: 0;
  color: var(--text);
  font-size: var(--font-size-sm);
  line-height: 1.45;
}
.thread-upgrade-banner .btn-new {
  align-self: flex-start;
  margin-top: 0.4rem;
}

.thread-dismissed-link {
  margin: 0.75rem 0 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-tertiary);
}
.thread-dismissed-link a {
  color: var(--color-info);
  cursor: pointer;
  text-decoration: underline;
}

/* Spinner used in the "Detecting hooks…" inline state. Single-character
   rotating glyph — keeps the message lightweight without pulling in an
   icon system. The form-section-label container provides positioning. */
.threads-spinner {
  display: inline-block;
  animation: tf-threads-spin 1.2s linear infinite;
  margin-right: 0.25rem;
}
@keyframes tf-threads-spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

/* ── Brainstorm Workspace (Hooks Phase 2 Sprint A) ────────────────────────
   Sprint A renders only the center column (conversation + composer);
   Sprint C adds the left rail for pinned context and Sprint D adds the
   right rail for save-to-campaign drafts. The .brainstorm-box modifier
   layered on top of .modal-box--xxl tunes the internal flex layout. */
.brainstorm-box {
  display: flex;
  flex-direction: column;
  height: min(85vh, 800px);
}
.brainstorm-hdr {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  flex-wrap: wrap;
}
.brainstorm-hook-title {
  margin: 0;
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-bold);
  color: var(--text);
}
.brainstorm-hdr-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.brainstorm-refresh-btn[disabled] {
  cursor: progress;
  opacity: 0.6;
}
.brainstorm-mobile-banner {
  display: none;  /* desktop default — flipped on at 900px breakpoint below */
  margin: 0 var(--space-5);
  padding: 0.6rem 0.85rem;
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
  background: color-mix(in srgb, var(--color-info) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-info) 30%, transparent);
  border-radius: var(--radius-md);
}
.brainstorm-conversation {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  min-height: 240px;
}
.brainstorm-msg {
  /* Per-message tint flows through --brainstorm-mode-color so mode-specific
     coloring is one CSS var instead of inline style — matches the
     per-instance-color hard rule. */
  padding: 0.75rem 1rem;
  border-radius: var(--radius-lg);
  border: 1px solid var(--border);
  background: var(--bg-2);
  color: var(--text);
  font-size: var(--font-size-sm);
  line-height: 1.55;
  white-space: pre-wrap;
}
.brainstorm-msg--user {
  align-self: flex-end;
  max-width: 78%;
  background: color-mix(in srgb, var(--text-1) 4%, transparent);
}
.brainstorm-msg--assistant {
  align-self: flex-start;
  max-width: 88%;
  border-color: color-mix(in srgb, var(--brainstorm-mode-color, var(--accent-teal)) 50%, var(--border));
  border-left: 3px solid var(--brainstorm-mode-color, var(--accent-teal));
}
.brainstorm-msg--surface { --brainstorm-mode-color: var(--color-info); }
.brainstorm-msg--suggest { --brainstorm-mode-color: var(--color-warning); }
.brainstorm-msg-meta {
  display: block;
  margin-bottom: 0.35rem;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--brainstorm-mode-color, var(--color-text-tertiary));
}
.brainstorm-msg-body {
  white-space: pre-wrap;
  /* Body inherits the message font/color from .brainstorm-msg above. */
}

/* Follow-up chips — render at the bottom of every assistant message.
   Click populates the textarea; GM keeps editorial control. Numbered
   prefix matches the model's "1. 2. 3. 4." output convention. */
.brainstorm-followups {
  margin-top: 0.85rem;
  padding-top: 0.7rem;
  border-top: 1px dashed color-mix(in srgb, var(--brainstorm-mode-color, var(--border)) 35%, var(--border));
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.brainstorm-followups-label {
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
  margin-bottom: 0.15rem;
}
.brainstorm-followup-chip {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.55rem 0.85rem;
  font-family: inherit;
  font-size: var(--font-size-sm);
  text-align: left;
  color: var(--text);
  background: color-mix(in srgb, var(--brainstorm-mode-color, var(--accent-teal)) 6%, transparent);
  border: 1px solid color-mix(in srgb, var(--brainstorm-mode-color, var(--accent-teal)) 28%, var(--border));
  border-radius: var(--radius-md);
  cursor: pointer;
  line-height: 1.45;
  transition: background var(--t-hover), border-color var(--t-hover), transform var(--t-hover);
}
.brainstorm-followup-chip:hover, .brainstorm-followup-chip:focus-visible {
  background: color-mix(in srgb, var(--brainstorm-mode-color, var(--accent-teal)) 14%, transparent);
  border-color: color-mix(in srgb, var(--brainstorm-mode-color, var(--accent-teal)) 55%, var(--border));
  transform: translateY(-1px);
}
.brainstorm-followup-num {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.4rem;
  height: 1.4rem;
  border-radius: var(--radius-full);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
  color: var(--brainstorm-mode-color, var(--accent-teal));
  background: color-mix(in srgb, var(--brainstorm-mode-color, var(--accent-teal)) 18%, transparent);
}
.brainstorm-followup-text {
  flex: 1;
}
/* Free-entry chip — same shape as model chips so the row reads as one
   list of options, but dashed border + italic + tertiary text color
   signal "this one is blank, you write the prompt." */
.brainstorm-followup-chip--custom {
  border-style: dashed;
  background: transparent;
}
.brainstorm-followup-chip--custom .brainstorm-followup-text {
  color: var(--color-text-tertiary);
  font-style: italic;
}
/* Expanded state for the free-entry chip 4 — fills the chip body with an
   inline input + Send button. Same outer shape as the collapsed chip so
   the chip-row layout doesn't reflow when the GM clicks "Write your own…". */
.brainstorm-followup-chip--expanded {
  border-style: solid;
  background: color-mix(in srgb, var(--brainstorm-mode-color, var(--accent-teal)) 8%, transparent);
  cursor: text;
}
.brainstorm-followup-chip--expanded:hover {
  transform: none;
}
.brainstorm-custom-input {
  flex: 1;
  min-width: 0;
  padding: 0.35rem 0.55rem;
  font-family: inherit;
  font-size: var(--font-size-sm);
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.brainstorm-custom-input:focus {
  outline: 2px solid color-mix(in srgb, var(--brainstorm-mode-color, var(--accent-teal)) 50%, transparent);
  outline-offset: 1px;
}
.brainstorm-custom-send {
  flex-shrink: 0;
  padding: 0.35rem 0.85rem;
  font-family: inherit;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
  letter-spacing: 0.04em;
  color: var(--brainstorm-mode-color, var(--accent-teal));
  background: color-mix(in srgb, var(--brainstorm-mode-color, var(--accent-teal)) 18%, transparent);
  border: 1px solid color-mix(in srgb, var(--brainstorm-mode-color, var(--accent-teal)) 55%, var(--border));
  border-radius: var(--radius-md);
  cursor: pointer;
}
.brainstorm-custom-send:hover, .brainstorm-custom-send:focus-visible {
  background: color-mix(in srgb, var(--brainstorm-mode-color, var(--accent-teal)) 28%, transparent);
}

/* Mobile cutoff per HOOKS_PHASE_2_FRAMING.md Q2: read-only history with
   "best on desktop" banner. All input affordances hidden — desktop only. */
@media (max-width: 900px) {
  .brainstorm-box { height: min(92vh, 600px); }
  .brainstorm-mobile-banner { display: block; }
  .brainstorm-followups { display: none; }
}

/* ── Anthologies (feature/anthologies — docs/ANTHOLOGIES_DESIGN.md) ───────────
   Campaign-selector discovery section + the Manage Anthologies modal. New
   surfaces compose canonical patterns — .form-section-label eyebrows, the
   app-wide card-hover recipe, .modal-box sizing, the status-badge pill idiom,
   design tokens — rather than inventing a new visual family. */

/* Campaign-selector "Anthologies" section — below the campaign grid. */
.cs-anthologies-section {
  margin-top: var(--space-6);
  padding-top: var(--space-5);
  border-top: 1px solid var(--border);
}
.cs-anthologies-hdr {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.cs-anthologies-blurb {
  margin: var(--space-2) 0 var(--space-3);
  color: var(--text-2);
  font-size: var(--font-size-sm);
  line-height: 1.5;
}
.cs-manage-anthologies-btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

/* Section anthology list — each anthology's name + its campaigns in timeline
   order, shown as a "›"-chained sequence. */
.cs-anthologies-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-3);
}
.cs-anthologies-list:empty { display: none; }
.cs-anthology-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.cs-anthology-name {
  font-size: var(--font-size-sm);
  font-weight: 700;
  color: var(--text-1);
}
.cs-anthology-chain {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
}
.cs-anthology-chain-campaign {
  font-size: var(--font-size-xs);
  color: var(--text-2);
}
.cs-anthology-chain-sep {
  color: var(--text-3);
  font-size: var(--font-size-xs);
}
.cs-anthology-empty {
  font-size: var(--font-size-xs);
  font-style: italic;
  color: var(--text-3);
}

/* Locked / Unlocked state tag — status-badge pill idiom. */
.anthology-state-tag {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--space-2);
  border-radius: var(--radius-full);
  border: 1px solid var(--border);
  font-size: var(--font-size-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.anthology-state-tag--locked {
  color: var(--text-3);
  background: var(--bg-2);
  border-color: var(--border);
}
.anthology-state-tag--unlocked {
  color: var(--color-brand);
  background: color-mix(in srgb, var(--color-brand) 8%, var(--bg-2));
  border-color: color-mix(in srgb, var(--color-brand) 45%, transparent);
}

/* Manage Anthologies modal */
#anthologies-modal-body {
  max-height: calc(100vh - 12rem);
  overflow-y: auto;
}
.anthology-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.anthology-card {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  cursor: pointer;
  transition: transform 0.12s ease, border-color 0.12s ease, box-shadow 0.12s ease;
}
.anthology-card:hover,
.anthology-card:focus-visible {
  transform: translateY(-1px);
  border-color: rgba(47, 154, 163, 0.45);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
  outline: none;
}
.anthology-card-info {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}
.anthology-card-name {
  font-size: var(--font-size-md);
  font-weight: 700;
  color: var(--text-1);
}
.anthology-card-meta {
  font-size: var(--font-size-xs);
  color: var(--text-3);
}
.anthology-card-chevron {
  flex-shrink: 0;
  color: var(--text-3);
  font-size: var(--font-size-lg);
}
.anthology-create-block {
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}
.anthology-create-block .form-section-label {
  display: block;
  margin-bottom: var(--space-3);
}

/* Per-anthology detail view */
.anthology-detail {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.anthology-back-btn {
  align-self: flex-start;
}
.anthology-type-note {
  margin: 0;
}
/* Save row at the bottom of the detail view — above the danger zone. */
.anthology-save-row {
  display: flex;
}
.anthology-camp-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.anthology-camp-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3);
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
/* Campaign row title line: name left-justified, Active toggle right-justified. */
.anthology-camp-titlerow {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.anthology-camp-name {
  flex: 1;
  min-width: 0;
  font-weight: 600;
  color: var(--text-1);
}
/* "Active" toggle switch — at most one campaign per anthology is active. */
.anthology-camp-active-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex-shrink: 0;
  cursor: pointer;
}
.anthology-active-toggle-input {
  position: absolute;
  width: 0;
  height: 0;
  opacity: 0;
}
.anthology-active-toggle-track {
  position: relative;
  flex-shrink: 0;
  width: 32px;
  height: 18px;
  border-radius: var(--radius-full);
  background: var(--bg-3);
  border: 1px solid var(--border);
  transition: background 0.15s ease, border-color 0.15s ease;
}
.anthology-active-toggle-track::after {
  content: "";
  position: absolute;
  top: 1px;
  left: 1px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--text-3);
  transition: transform 0.15s ease, background 0.15s ease;
}
.anthology-active-toggle-input:checked + .anthology-active-toggle-track {
  background: color-mix(in srgb, var(--color-brand) 30%, var(--bg-2));
  border-color: color-mix(in srgb, var(--color-brand) 55%, transparent);
}
.anthology-active-toggle-input:checked + .anthology-active-toggle-track::after {
  transform: translateX(14px);
  background: var(--color-brand);
}
.anthology-active-toggle-input:focus-visible + .anthology-active-toggle-track {
  outline: 2px solid var(--color-brand);
  outline-offset: 1px;
}
.anthology-active-toggle-label {
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--text-2);
}
.anthology-camp-fields {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}
.anthology-camp-fields input,
.anthology-camp-fields select,
.anthology-add-camp select {
  padding: var(--space-1) var(--space-2);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text-1);
  font-size: var(--font-size-sm);
}
.anthology-era-order { width: 3.5rem; }
.anthology-era-label { flex: 1; min-width: 8rem; }
.anthology-camp-remove { margin-left: auto; }
.anthology-add-camp {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.anthology-add-camp select { flex: 1; }
.anthology-danger-zone {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-top: var(--space-2);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}

/* Entity cross map — inside the Manage Anthologies detail view. */
#anthology-crossmap-mount {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-2);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}
#anthology-crossmap-mount > .form-section-label {
  display: block;
}
.anth-xmap-tier {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.anth-xmap-tier-hdr {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) 0;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-1);
  font-size: var(--font-size-sm);
  font-weight: 700;
}
.anth-xmap-tier-caret {
  color: var(--text-3);
  font-size: var(--font-size-xs);
}
.anth-xmap-tier-label { letter-spacing: 0.02em; }
.anth-xmap-tier-count {
  color: var(--text-3);
  font-weight: 600;
  font-size: var(--font-size-xs);
}
.anth-xmap-tier-hint {
  margin: 0 0 var(--space-1);
}
.anth-xmap-bulk {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  font-size: var(--font-size-xs);
  color: var(--text-2);
}
.anth-xmap-bulk-select {
  padding: var(--space-1) var(--space-2);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text-1);
  font-size: var(--font-size-sm);
}
.anth-xmap-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3);
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.anth-xmap-row--dismissed { opacity: 0.55; }
.anth-xmap-row-entities {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
}
.anth-xmap-entity {
  cursor: help;
  color: var(--text-1);
  font-weight: 600;
  font-size: var(--font-size-sm);
  border-bottom: 1px dotted var(--border);
}
.anth-xmap-entity:hover,
.anth-xmap-entity:focus-visible {
  border-bottom-color: var(--color-brand);
  outline: none;
}
.anth-xmap-entity-camp {
  color: var(--text-3);
  font-weight: 400;
}
.anth-xmap-link-glyph { color: var(--text-3); }
.anth-xmap-src-tag {
  padding: 1px var(--space-1);
  border-radius: var(--radius-sm);
  background: var(--bg-3);
  color: var(--text-3);
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.anth-xmap-row-controls {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.anth-xmap-flow {
  padding: var(--space-1) var(--space-2);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text-1);
  font-size: var(--font-size-sm);
}
.anth-xmap-dismiss { margin-left: auto; }
.anth-xmap-note-wrap { display: flex; }
.anth-xmap-note {
  flex: 1;
  padding: var(--space-1) var(--space-2);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text-1);
  font-size: var(--font-size-sm);
}

/* Hover-preview popover — sits above the modal (.modal-overlay is z-index 1000). */
.anth-xmap-preview {
  position: fixed;
  z-index: 1100;
  width: 320px;
  max-height: 320px;
  overflow-y: auto;
  padding: var(--space-3);
  background: var(--bg-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
  pointer-events: none;
}
.anth-xmap-preview-hdr {
  margin-bottom: var(--space-2);
  font-weight: 700;
  font-size: var(--font-size-md);
  color: var(--text-1);
}
.anth-xmap-preview-type {
  font-size: var(--font-size-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--text-3);
}
.anth-xmap-preview-desc,
.anth-xmap-preview-note,
.anth-xmap-preview-empty {
  margin: 0;
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--text-2);
}
.anth-xmap-preview-empty,
.anth-xmap-preview-note {
  font-style: italic;
  color: var(--text-3);
}
.anth-xmap-preview-foot {
  margin: var(--space-2) 0 0;
  padding-top: var(--space-2);
  border-top: 1px solid var(--border);
  font-size: var(--font-size-xs);
  color: var(--text-3);
}

/* Entity "Also Appears In" — cross-campaign appearances inside an expanded
   Codex entity-row, plus the appearance detail modal. */
.entity-crosslinks-mount:empty { display: none; }
.entity-crosslinks-mount {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}
.entity-crosslinks-mount > .form-section-label {
  display: block;
  margin-bottom: var(--space-2);
}
.entity-crosslinks-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.entity-xlink-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: transform 0.12s ease, border-color 0.12s ease, box-shadow 0.12s ease;
}
.entity-xlink-row:hover,
.entity-xlink-row:focus-visible {
  transform: translateY(-1px);
  border-color: rgba(47, 154, 163, 0.45);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
  outline: none;
}
.entity-xlink-row-main {
  display: flex;
  flex-direction: column;
  gap: 1px;
  flex: 1;
  min-width: 0;
}
.entity-xlink-name {
  font-weight: 600;
  font-size: var(--font-size-sm);
  color: var(--text-1);
}
.entity-xlink-meta {
  font-size: var(--font-size-xs);
  color: var(--text-3);
}
.entity-xlink-note {
  margin: 0;
  flex: 2;
  min-width: 0;
  font-size: var(--font-size-xs);
  font-style: italic;
  color: var(--text-2);
  line-height: 1.4;
}
.entity-xlink-chevron {
  flex-shrink: 0;
  color: var(--text-3);
  font-size: var(--font-size-lg);
}
.entity-xlink-modal-type {
  margin-left: auto;
  font-size: var(--font-size-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--text-3);
}
.entity-xlink-modal-meta {
  margin: 0 0 var(--space-2);
  font-size: var(--font-size-sm);
  color: var(--text-3);
}
.entity-xlink-desc,
.entity-xlink-gmnote {
  margin: 0;
  font-size: var(--font-size-base);
  line-height: 1.7;
  color: var(--text-1);
}
.entity-xlink-gmnote {
  font-style: italic;
  color: var(--text-2);
}

/* Campaign-card anthology block — shown only when the grid is grouped by
   anthology. Sits after .campaign-card-meta inside .campaign-card-info:
   anthology name + Active/Legacy pill, then the era label, then the world. */
.campaign-card-anthology {
  display: flex;
  flex-direction: column;
  gap: 1px;
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--border);
}
.cca-name-line {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.cca-anthology-name {
  font-size: var(--font-size-xs);
  font-weight: 700;
  color: var(--text-1);
}
.cca-pill {
  padding: 1px var(--space-2);
  border-radius: var(--radius-full);
  border: 1px solid var(--border);
  font-size: var(--font-size-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.cca-pill--active {
  color: var(--color-brand);
  background: color-mix(in srgb, var(--color-brand) 8%, var(--bg-2));
  border-color: color-mix(in srgb, var(--color-brand) 45%, transparent);
}
.cca-pill--legacy {
  color: var(--text-3);
  background: var(--bg-2);
  border-color: var(--border);
}
.cca-detail-line {
  font-size: var(--font-size-xs);
  color: var(--text-2);
}
.cca-empty {
  font-style: italic;
  color: var(--text-3);
}

/* ── v1071 Threadfall brand lockup ─────────────────────────────────────────
   Render with:
     <span class="tf-lockup tf-lockup--hero">
       <svg class="tf-lockup-mark"><use href="#tf-mark"/></svg>
       <span class="tf-wordmark">Thread<span class="pipe">|</span>fall</span>
     </span>
   Pipe-to-mountain-gap alignment is applied at runtime by _tfAlignLockup()
   in app.js (visible ink, not bounding box — the | glyph has wide bearings).
   Mark transitions enable the alignment shift to be animated.            */
.tf-lockup {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--color-text-primary);
}
.tf-lockup-mark {
  display: block;
  height: 1em;
  /* v1073 fix: inline <svg> with `width: auto` defaults to the 300px CSS
     intrinsic-sizing fallback when only height is constrained — that's why
     the nav lockup showed a 200+px gap between mark and wordmark. The
     aspect-ratio reservation matches the symbol's 556:330 viewBox so the
     SVG actually sizes proportionally to the constrained height. */
  aspect-ratio: 556 / 330;
  width: auto;
  flex-shrink: 0;
  transition: transform 0.25s ease-out;
}
.tf-wordmark {
  font-family: var(--type-display);
  font-weight: 500;
  letter-spacing: -0.022em;
  line-height: 1;
  color: inherit;
  transition: transform 0.25s ease-out;
}
.tf-wordmark .pipe {
  color: inherit;
  font-weight: 300;
  margin: 0 2px;
}
/* v1073: size modifiers + the canonical "primary vertical" lockup from the
   spec (mark stacked on top of wordmark, both centered). Used wherever the
   brand is the focus of the surface (login + campaign-selector). The
   horizontal "header" and "nav" variants stay side-by-side but small. */
.tf-lockup--primary {
  flex-direction: column;
  align-items: center;
  /* v1079: ratios are locked from the brand spec's hero-lockup, NOT
     invented. Spec values:
       .hero-mark     width: 340px → height: 202px (via 556:330 ratio)
       .hero-wordmark font-size: 84px
       .hero-lockup   gap: 36px
     Locked ratios (relative to wordmark font-size):
       mark height = 2.40em   (202 / 84)
       gap         = 0.43em   (36 / 84)
     Font-size varies by viewport but the ratios DON'T. */
  gap: 0.43em;
  font-size: 40px;
}
.tf-lockup--primary .tf-lockup-mark {
  height: 2.40em;
}
.tf-lockup--header { font-size: 22px; gap: 6px; }
.tf-lockup--nav    { font-size: 18px; gap: 4px; }
@media (min-width: 760px) {
  .tf-lockup--primary { font-size: 52px; }
}
@media (max-width: 480px) {
  .tf-lockup--primary { font-size: 36px; }
}

/* ── v1071 profile-modal theme toggle ──────────────────────────────────────
   Brought back with Dark as default (matches the source-app convention
   pre-v1006). State persists via localStorage key 'theme'.              */
.profile-theme-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) 0;
}
/* v1196: row-label paired with each preference toggle in the unified
   Preferences section (Theme + Profanity filter). Sentence-case, plain
   body text — not an eyebrow — so it reads as "the thing this toggle
   controls" rather than another section heading. */
.profile-pref-label {
  font-size: var(--font-size-sm);
  color: var(--text);
}
.profile-theme-toggle {
  display: inline-flex;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  padding: 2px;
  gap: 2px;
}
.profile-theme-toggle button {
  appearance: none;
  background: transparent;
  border: 0;
  color: var(--text-2);
  font-family: var(--type-ui, system-ui, sans-serif);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.04em;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-full);
  cursor: pointer;
  transition: background var(--t-hover) var(--ease), color var(--t-hover) var(--ease);
}
.profile-theme-toggle button.active {
  background: var(--color-brand);
  color: #fff;
}
.profile-theme-toggle button:not(.active):hover,
.profile-theme-toggle button:not(.active):focus-visible {
  color: var(--color-text-primary);
  outline: none;
}

/* ── v1071/v1073 lockup overrides for legacy logo-class containers ────────
   The new .tf-lockup wrappers compose the existing .nav-logo /
   .campaign-selector-logo / .login-logo class names so JS lookups still
   work. But those classes carry rules that were tuned for the old <img>
   (fixed height, max-width, drop-shadow filter). Reset them, and switch
   the centered placements to display:flex so margin-auto actually
   centers (inline-flex + margin-auto is a no-op). */
.tf-lockup.nav-logo {
  height: auto;
  width: auto;
  opacity: 1;
  filter: none;
}
.tf-lockup.login-logo,
.tf-lockup.campaign-selector-logo {
  display: flex;        /* block-level — inline-flex doesn't accept margin auto */
  width: fit-content;
  max-width: none;
  filter: none;
  margin: 0 auto 1.25rem;
  transform: none;
}
.tf-lockup.campaign-selector-logo {
  margin: 0 auto;
}
