/* cockpit.css — the cockpit layout. One set of regions; two arrangements
   (desktop / mobile) driven by a breakpoint, not two codebases.

   Grid:
   ┌───────────────────────────────────────────────┐
   │ header (brand · breadcrumbs)                   │  tier 1 frame
   ├──────────────────────────────┬─────────────────┤
   │ workspace (Story Dashboard)  │ co-pilot        │  tier 1
   └──────────────────────────────┴─────────────────┘
                                          edge tabs → tier 3 drawers

   docs/adr/0040 Phase 12f retired the old WHO/WHERE/WHAT/WHY/HOW tab
   strip — the workspace is now a single consolidated Story Dashboard, so
   the "strip" grid row/area that used to hold it is gone; the breadcrumb
   row now spans the full width instead of sharing that row. */

* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
  font-family: var(--font);
  background: var(--bg);
  color: var(--text);
  overflow: hidden;
}
button { font: inherit; color: inherit; cursor: pointer; }

.cockpit {
  display: grid;
  grid-template-rows: var(--header-h) var(--strip-h) 1fr;
  grid-template-columns: 1fr var(--drawer-w); /* same token .mc-drawer's own width uses — see tokens.css */
  grid-template-areas:
    "header      header"
    "breadcrumb  breadcrumb"
    "work        copilot";
  height: 100vh;
  padding-right: var(--edge-w); /* room for the right edge tabs */
}

/* --- Header (brand) ------------------------------------------------- */
.mc-header {
  grid-area: header;
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 var(--sp-4);
  background: var(--tier1-bg);
  border-bottom: 1px solid var(--tier1-border);
}
/* Direct follow-up request: "Move the campaign picker to the right of the
   app title separated by a '|'" — the campaign switcher now lives INSIDE
   .brand (not off in .header-actions on its own), right after the "|",
   so title/separator/picker read as one group on every width, including
   mobile (see that media query's own updated note below — the earlier
   "hide the title, show just the campaign name on phone" treatment is
   superseded, since the request now wants the FULL "GMAtlas | Campaign"
   picker working there too, not just the bare name). .brand's own
   flex-wrap lets the picker drop to its own line on a genuinely narrow
   phone rather than clipping or overflowing. */
.mc-header .brand { display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: nowrap; flex: 0 1 auto; min-width: 0; }
/* One-row top nav (shell.js's fitHeaderToOneRow): nothing wraps; when the
   full "GMAtlas | Campaign" doesn't fit beside the buttons, the title and
   separator drop out, then the campaign name truncates with an ellipsis. */
.mc-header { gap: var(--sp-3); flex-wrap: nowrap; }
.mc-header .header-actions { flex: 0 0 auto; flex-wrap: nowrap; }
.mc-header h1, .mc-header .brand-sep { flex: 0 0 auto; white-space: nowrap; }
.mc-header-measuring .brand { flex-shrink: 0; }
.mc-header-measuring .campaign-title { max-width: none; }
.mc-header-compact .brand h1, .mc-header-compact .brand-sep { display: none; }
.mc-header h1 { font-size: 1.05rem; margin: 0; letter-spacing: 0.02em; }
.mc-header .tagline { font-size: 0.72rem; color: var(--text-dim); }
.mc-header .brand-sep { color: var(--text-dim); }
.mc-header .campaign-title { font-size: 0.85rem; color: var(--accent); }
/* Phone: the "GMAtlas" wordmark and "|" give way to a wireframe-planet
   icon to save space — kept even when fitHeaderToOneRow goes compact,
   since it costs barely more than nothing. */
.mc-header .brand-planet { display: none; color: var(--accent); }
@media (max-width: 480px) {
  .mc-header .brand { align-items: center; }
  .mc-header .brand h1, .mc-header-compact .brand h1 { display: flex; align-items: center; }
  .mc-header .brand-planet { display: block; }
  .mc-header .brand-name, .mc-header .brand-sep { display: none; }
}

/* Campaign switcher (direct follow-up request) — .campaign-title reverts
   to being a real clickable control (it used to open Settings directly,
   pre-header-gear-menu; now it's a <button> opening this new dropdown
   instead), so it needs a bare-button reset the old plain <span> never did. */
.campaign-switcher-wrap { position: relative; min-width: 0; }
.mc-header .campaign-title { display: block; background: none; border: none; font: inherit; padding: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* z-index above the compact-width drawer panel (20) and edge nav (21),
   which otherwise paint over this menu on phone/tablet — the same fix the
   ⚙ settings menu already has. */
.campaign-switcher-menu {
  position: absolute; top: 100%; left: 0; margin-top: 0.3rem; z-index: 70;
  min-width: 14rem; max-width: min(20rem, calc(100vw - 2 * var(--sp-4))); background: var(--tier2-bg); border: 1px solid var(--border);
  border-radius: var(--radius-sm); box-shadow: 0 4px 16px rgba(0,0,0,0.35); padding: var(--sp-2);
}
.campaign-switcher-menu[hidden] { display: none; }
.campaign-switcher-list { display: flex; flex-direction: column; gap: var(--sp-1); margin-bottom: var(--sp-2); }
.campaign-switcher-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); padding: 0.3rem 0.4rem; border-radius: var(--radius-sm); }
.campaign-switcher-row.active { background: var(--bg-inset); }
.campaign-switcher-row-title { font-size: 0.85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.campaign-switcher-new { width: 100%; }

/* Header gear menu (New Campaign / Settings / About — replaces the old
   click-the-campaign-title-to-open-Settings convention). */
.settings-menu-wrap { position: relative; }
/* The gear's label stays hidden in its normal header spot (icon-only, by
   direct earlier request) and only appears once shell.js physically
   relocates .settings-menu-wrap into .mc-edge on phone widths, where it
   should look like every other edge-nav tile. .mc-edge button's own
   descendant-selector rules are too low-specificity to beat the button's
   own .btn.ghost.sm classes (2 classes beats 1 class + 1 type), so this
   restyles it explicitly instead of relying on inheriting that rule. */
.settings-menu-wrap b { display: none; }
.mc-edge .settings-menu-wrap [data-settings-menu-toggle] {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  background: none; border: none; padding: var(--sp-2) 0; width: 100%;
  color: var(--text-dim); font-size: 0.6rem;
}
.mc-edge .settings-menu-wrap [data-settings-menu-toggle] .glyph { font-size: 1.1rem; }
.mc-edge .settings-menu-wrap b { display: block; }
/* Real reported bug (still visible after giving .mc-edge its own z-index,
   see that rule's own comment below): .mc-edge got `overflow-y: auto` a
   while back so a tall edge-nav button list could scroll instead of
   clipping (see that rule's own comment, styles/cockpit.css). Setting
   EITHER overflow axis to non-`visible` forces the OTHER axis to compute
   as `auto` too (a real CSS rule, not a bug) — so once relocated into
   .mc-edge on phone, this dropdown (min-width 9rem, wider than the narrow
   edge column, opening bottom:100% relative to a position:relative
   ancestor) was getting clipped by that same scroll container, no matter
   what z-index either element had — overflow clipping and stacking order
   are separate mechanisms; a higher z-index can't rescue a clipped box.
   Fix: position:fixed unconditionally, with the actual top/left/right/
   bottom set as inline styles by shell.js's render() (computed fresh from
   the toggle button's real getBoundingClientRect() every time the menu
   opens) — position:fixed is NEVER clipped by an ancestor's overflow, only
   by an ancestor that establishes its own containing block (transform/
   filter/will-change), which nothing between here and the viewport does.
   Same technique .inline-prompt (openInlinePrompt, shell.js) already uses
   for exactly this reason. */
.settings-menu {
  /* Direct follow-up request: "the settings menu hides behind the combat
     tracker but should be on top regardless of any menus, panels, or
     windows" — highest z-index of any IN-APP menu/panel on purpose (the
     dice roller/combat tracker panels top out at 60), so it's never
     covered by anything else that can be open at the same time. Only
     .startup-screen (90) sits above this — that's a full-screen takeover
     shown before the cockpit is even reachable, not a competing panel. */
  position: fixed; z-index: 70;
  display: flex; flex-direction: column; min-width: 9rem;
  background: var(--tier3-bg); border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  box-shadow: var(--shadow); overflow: hidden;
}
.settings-menu[hidden] { display: none; }
.settings-menu button { text-align: left; background: none; border: none; color: var(--text); padding: 0.65rem 0.9rem; font-size: 1rem; }
.settings-menu button:hover { background: var(--tier2-bg); }

/* Party/Colony/Journal's header tab group (right-aligned in .header-actions,
   just left of the ⚙ menu). Their label always shows now, the same as the
   right-hand edge nav's own icon+label buttons (.mc-edge — bold label,
   never hidden at any width) — a prior narrow-viewport rule that hid just
   this group's label text (not the whole button, unlike an even earlier,
   worse bug that hid the buttons entirely) is gone; .header-actions wraps
   instead of overflowing if that ever runs out of room on a narrow phone. */
.header-drawer-tabs { display: flex; gap: var(--sp-2); }
.header-drawer-tabs .btn-label { font-weight: bold; }

/* About overlay — same centered-modal shape as .mc-search-overlay, simpler
   content (no search head/results). */
.mc-about-overlay {
  position: fixed; inset: 0; z-index: 30;
  background: rgba(0, 0, 0, 0.35);
  display: flex; align-items: center; justify-content: center;
}
.mc-about-overlay[hidden] { display: none; }
.mc-about-card {
  position: relative; width: min(24rem, 92vw);
  background: var(--tier3-bg); border: 1px solid var(--border-strong); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: var(--sp-4);
}
.mc-about-card .icon-btn { position: absolute; top: var(--sp-2); right: var(--sp-2); }
.mc-about-card h2 { margin: 0 0 0.2rem; }

/* Startup landing page (direct request) — a full-screen takeover, not a
   dim modal over the cockpit like .mc-about-overlay above (this replaces
   the whole screen rather than sitting over visible app content), so its
   background is the requested solid color, not a translucent one. Highest
   z-index in the app on purpose — it's the very first/only thing
   reachable on launch, above even .settings-menu's own 70. */
.startup-screen {
  position: fixed; inset: 0; z-index: 90;
  background: #4D4D4D;
  display: flex; align-items: center; justify-content: center;
}
.startup-screen[hidden] { display: none; }
.startup-card {
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-3);
  width: min(22rem, 92vw); text-align: center;
}
.startup-logo { width: 6rem; height: 6rem; object-fit: contain; }
.startup-card h1 { margin: 0; font-size: 1.4rem; }
.startup-campaigns { display: flex; flex-direction: column; gap: var(--sp-2); width: 100%; }
.startup-campaign-btn { width: 100%; }
.startup-version { margin-top: var(--sp-4); }

/* --- Breadcrumbs (secondary tier, above Co-Pilot) ------------------- */
.mc-breadcrumb {
  grid-area: breadcrumb;
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  background: var(--bg-raised);
  border-bottom: 1px solid var(--border);
  border-left: 1px solid var(--border);
  font-size: 0.75rem; color: var(--text-dim);
  overflow-x: auto; white-space: nowrap;
}
.mc-breadcrumb .crumb { color: var(--text-dim); }
.mc-breadcrumb .crumb:last-child { color: var(--text); }
.mc-breadcrumb .sep { opacity: 0.5; }

/* --- Adaptive workspace (primary) ----------------------------------- */
/* Desktop/tablet (≥768px): .mc-workspace no longer scrolls itself — it's a
   fixed-height flex column so .storyboard-grid's row can stretch to fill
   it, giving Composer and Navigator each their OWN scrollbar
   (.workspace-card-body, below) instead of sharing one via
   position:sticky. Sticky quietly clipped whichever card's content was
   taller than the viewport (a real reported bug — Navigator's own bottom
   was unreachable until Composer had been scrolled all the way down,
   since a stuck element has no scroll of its own and only releases once
   the taller column next to it runs out). Phone (<768px, media query
   below) reverts to the simple everything-stacks-and-the-whole-page-
   scrolls behavior — there's no split pane to give independent scroll to
   once the two cards stack into one column. */
.mc-workspace {
  grid-area: work;
  padding: var(--sp-4);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  min-height: 0;
}
/* Direct follow-up request: Composer/Navigator's gap below the breadcrumb
   (the "line above") is .mc-workspace's own top padding PLUS
   .storyboard-grid's margin-top stacked together — overridden here to a
   combined exact 3px so it matches .mc-copilot's own 3px top padding
   below, instead of the two having drifted to different totals. */
.mc-workspace { padding-top: 3px; }
/* Padding is deliberately tight on three sides now, on direct request:
   5px top/left (the title sits right in the corner) and 2px right (so
   .workspace-card-body's scrollbar sits right at the panel's own edge
   instead of inset by a full padding) — only the bottom keeps the normal
   spacious padding. The title's own 10px bottom margin is the gap to
   whatever comes right under it (the body, now that neither card has a
   lead paragraph). The scrolling content's own inner wrapper
   (.workspace-card-body-inner, below) restores a normal right padding of
   its own, so text still reads with comfortable spacing — only the
   scrollbar track itself extends all the way to the edge. */
.workspace-card {
  background: var(--tier1-bg);
  border: 1px solid var(--tier1-border);
  border-radius: var(--radius);
  padding: 5px 2px var(--sp-6) 5px;
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.workspace-card h2 { margin: 0 var(--sp-6) 10px 0; font-size: 1.2rem; flex: 0 0 auto; }
.workspace-card .lead { color: var(--text-dim); margin: 0 var(--sp-6) var(--sp-4) 0; flex: 0 0 auto; }
/* Direct bug report: "Hexcrawl fails to load... when you make it a
   selected position on the Storyboard" — reproduces on Chrome too (ruled
   out via a real-bundle smoke test that the underlying render is correct:
   full, well-formed Hexcrawl content, including .hexcrawl-viewport,
   genuinely exists inside this exact card's own DOM, every time), so this
   is a real CSS layout issue in the Storyboard card chain itself, not a
   logic bug. .workspace-card-body's own `min-height: 0` (needed so it CAN
   shrink below its content size — the whole point of giving Composer and
   Navigator their own independent scrollbars instead of one shared
   position:sticky, see this file's own doc comment above) is the exact
   same "flex item allowed to collapse" shape that turned out to be the
   real culprit for Hexcrawl's OTHER blank-panel reports (the drawer
   body's own :has()-driven flex-fill, since removed outright). A floor
   here can't be removed the same way without reintroducing THAT original
   sticky-position bug, so instead: a safety-net min-height, well below
   any genuine Composer/Navigator content's own natural height, that
   guarantees this card can never collapse to zero/near-zero regardless
   of how the surrounding CSS Grid stretch + nested flex-column chain
   actually resolves on a given browser. */
.workspace-card { min-height: 240px; }
.workspace-card-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
/* Direct follow-up request ("reduce the margin space between scroll bar
   and text") — narrowed from a full --sp-6 gutter to --sp-3, still enough
   that the scrollbar track never touches text, just noticeably closer
   than before. */
.workspace-card-body-inner { padding-right: var(--sp-3); }
@media (max-width: 1023px) {
  /* Composer/Navigator render inside the drawer panel instead now
     (design/UX-ROADMAP.md Steps 4/5 — permanently-pinned, unclosable tabs
     shared with Advisor and any opened drawer, phone AND tablet) —
     shell.js's render() also skips populating this element's innerHTML at
     this tier (isCompactTab()), so this is belt-and-suspenders against an
     empty box still taking up layout space. Phone-only originally; a real
     phone test found the two-column-then-stacked approaches both left
     Navigator effectively unreachable in practice, and tablet has the same
     "no room for 3 columns plus a drawer side by side" problem desktop
     doesn't, so it gets the same tab-menu treatment rather than a third,
     separate layout. */
  .mc-workspace { display: none; }
}

/* WHERE's header-row summary (docs/adr/0038 follow-up) — title/lead on
   the left, a read-only System/Star/Colony-Base/District readout
   top-right on the same row; wraps under the title on narrow viewports
   rather than overflowing. */
.workspace-card-head-row { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--sp-4); flex-wrap: wrap; }
.workspace-card-head-text { flex: 1 1 14rem; }
.workspace-card-head-row .workspace-card-head-text h2,
.workspace-card-head-row .workspace-card-head-text .lead { margin-bottom: 0; }
.location-summary { display: flex; flex-direction: column; gap: 0.15rem; font-size: 0.8rem; flex: 0 0 auto; }
/* A read-only System/Star reference row (drawers/index.js's
   worldProfileSection) — label left, resolved entity chip right, split
   into two clusters so a future action control has somewhere to sit
   without restyling this. */
.location-summary-item { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }
.location-summary-label { display: flex; align-items: center; gap: 0.3rem; }
.location-summary-item .entity-chip { font-size: 0.75rem; padding: 0.15rem 0.5rem; }

/* WHERE's docked Faction Events card (direct request: a down-arrow on the
   Faction Events card relocates it here as a right column; an up-arrow
   there pops it back to the drawer tab group) — the only place today the
   workspace ever shows two side-by-side panels, so this is its own rule
   rather than a general-purpose grid class. Collapses to one column under
   the same 1023px tier every other workspace/drawer layout already folds
   at (see the @media block further down). */
.workspace-with-side {
  display: grid;
  grid-template-columns: 1fr minmax(18rem, 26rem);
  gap: var(--sp-4);
  align-items: start;
}
.workspace-docked-panel {
  background: var(--tier2-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--sp-4);
  max-height: calc(100vh - var(--header-h) - var(--strip-h) - 2 * var(--sp-4));
  overflow: auto;
}
.faction-events-dock-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.faction-events-dock-row h3 { margin: 0; font-size: 1rem; }
/* A drawer whose render threw (direct bug report: a panel going blank
   with no way to recover) — renderActiveDrawerHtml's own catch block,
   shell.js. Danger-tinted so it reads as a real error, not another empty
   state. */
.drawer-render-error { border: 1px solid var(--danger); border-radius: var(--radius-sm); padding: var(--sp-4); }
.drawer-render-error p:first-child { color: var(--danger); margin-top: 0; }
.ws-placeholder {
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-sm);
  padding: var(--sp-6);
  color: var(--text-dim);
  text-align: center;
}
.ws-fields { display: grid; gap: var(--sp-3); margin-top: var(--sp-4); }
.ws-field { display: grid; gap: 2px; }
.ws-field .k { font-size: 0.68rem; letter-spacing: 0.1em; color: var(--text-dim); }
.ws-field .v { font-size: 0.95rem; }

/* --- Co-Pilot (primary, always visible) ----------------------------- */
/* Direct follow-up request: the Advisor's default (always-visible desktop
   sidebar) appearance should read as the same kind of rounded card as
   Composer/Navigator (.workspace-card) rather than a flat, differently-
   styled strip — background/border/radius/shadow and the header's own
   size/spacing now match .workspace-card/.workspace-card h2 exactly. This
   is deliberately scoped to ONLY this always-visible .mc-copilot aside —
   "no change to how the Advisor looks when in the tab menu" — the SAME
   renderCopilot() content rendered inside the drawer-panel tab strip
   (phone/tablet's pinned Advisor tab, or a desktop drawer-opened Advisor)
   goes through a completely different wrapper (.mc-drawer, shell.js's
   renderActiveDrawerHtml) untouched by this rule. */
.mc-copilot {
  grid-area: copilot;
  padding: var(--sp-4);
  /* Direct follow-up request: 3px of visible gap below the breadcrumb (the
     "line above") — a MARGIN, not padding, since .mc-copilot IS the
     rounded box itself (unlike Composer/Navigator, where .mc-workspace is
     a separate unstyled wrapper around the actual rounded .workspace-card
     — see its own padding-top:3px override above) — padding here would
     only add space between the border and the content inside it, not
     between the border and the breadcrumb above it. */
  margin-top: 3px;
  background: var(--tier1-bg);
  border: 1px solid var(--tier1-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: auto;
}
.mc-copilot h2 { margin: 0 var(--sp-6) 10px 0; font-size: 1.2rem; }
.copilot-card {
  background: var(--tier2-bg);
  border: 1px solid var(--tier2-border);
  border-radius: var(--radius-sm);
  padding: var(--sp-3);
  margin-bottom: var(--sp-3);
}
.copilot-card h3 { margin: 0 0 var(--sp-1); font-size: 0.72rem; letter-spacing: 0.08em; color: var(--text-dim); text-transform: uppercase; }
.copilot-card p { margin: 0; font-size: 0.85rem; }
/* Condensed Story Options card (docs/adr/0039 Phase 2) — several stacked
   suggestions inside one .copilot-card, each with its own roll/journal
   chips right under it. */
.copilot-story-option { margin-top: var(--sp-2); }
/* Suggested oracles' history stack (direct follow-up request) — each push
   (WHAT's "Suggest oracles", WHERE's Regional faction activity 💡) is its
   own boxed entry, newest at the top; the 3 most recent render open, the
   next 3 as a native <details> (no new JS state needed for the expand/
   collapse itself) so a GM can still look one up without it competing for
   space by default. */
.advisor-history-entry { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--sp-2); margin-top: var(--sp-2); }
.advisor-history-entry-head { margin-bottom: var(--sp-1); }
.advisor-history-entry-collapsed summary { cursor: pointer; }
.advisor-history-entry-collapsed[open] summary { margin-bottom: var(--sp-1); }
/* Advisor "stage, then apply" mechanic (direct follow-up request) — a
   rolled-but-not-yet-accepted oracle result reads as plain text next to a
   ✓ (not yet another chip, so it can't be confused with "roll again"); the
   editable draft + Apply button sits at the bottom of whichever card owns
   it (Suggested oracles / If nothing changes), consistent regardless of
   how many entries/chips are above it. */
.advisor-oracle-result { display: inline-flex; align-items: center; gap: 4px; font-size: 0.8rem; background: var(--bg-inset); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0.2rem 0.4rem; }
.advisor-oracle-result-text { max-width: 16rem; }
.advisor-suggestion-editor { display: flex; flex-direction: column; gap: var(--sp-2); margin-top: var(--sp-3); padding-top: var(--sp-2); border-top: 1px solid var(--border); }
.advisor-suggestion-input { width: 100%; resize: vertical; }
.advisor-suggestion-editor .chip.sm { align-self: flex-start; }
.advisor-suggestion-editor .chip.sm:disabled { opacity: 0.5; cursor: default; }
.copilot-story-option:first-child { margin-top: 0; }
.copilot-story-option p { margin-bottom: 0.25rem; }

/* Cast's "Generate…" select (2026-07-06: Cast is now a real drawer, so this
   is the one case of a drawer-specific head control — see shell.js's
   headExtraForDrawer/castGenerateSelectHtml) — deliberately small/
   unobtrusive (a utility control, not a primary action) so it doesn't
   compete with the drawer title; .drawer-head-actions groups it with the
   close ✕/unpin ▶ so both stay right-aligned together (a bare third flex
   child would otherwise land in the middle of .drawer-head's
   space-between). Cast's own entity list also needs a bit more room than
   the Entity Inspector's typical 32vh cap (see the plain .entity-list rule
   below) since it's the whole drawer's content, not one section of it. */
.drawer-head-actions { display: flex; align-items: center; gap: var(--sp-2); }
.entity-generate-select {
  width: auto; font-size: 0.7rem; padding: 0.1rem 0.35rem; border-radius: var(--radius-sm);
  background: var(--bg-inset); color: var(--text-dim); border: 1px solid var(--border);
}
.mc-drawer .entity-list { max-height: none; margin-top: var(--sp-2); }

/* --- Edge tabs → tertiary drawers ----------------------------------- */
.mc-edge {
  position: fixed; top: var(--header-h); right: 0; bottom: 0;
  width: var(--edge-w);
  display: flex; flex-direction: column; gap: var(--sp-2);
  padding: var(--sp-2) 0;
  background: var(--tier3-bg);
  border-left: 1px solid var(--tier3-border);
  /* Real reported bug: .mc-edge had no explicit z-index (auto), so
     .mc-drawer's z-index:20 — an entirely separate stacking context, since
     `position: fixed` always creates one regardless of z-index — outranked
     it wholesale. On desktop this never showed (the drawer sits to the
     LEFT of the edge nav there, not overlapping it), but at compact widths
     .mc-drawer spans left:0 up to the edge nav (see the <=1023px rule
     below), so the Settings gear's dropdown (relocated into .mc-edge on
     phone, .settings-menu's own z-index — which pops open wider than
     the narrow edge column and so visually overlaps the drawer's area —
     was rendering BEHIND the open drawer's background instead of over it,
     regardless of its own z-index (a child's z-index only ever competes
     within its own parent's stacking context). One level above .mc-drawer
     fixes it for the edge nav generally, not just this one dropdown. */
  z-index: 21;
}
.mc-edge button {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: var(--sp-2) 0; background: none; border: none;
  color: var(--text-dim); font-size: 0.6rem;
  position: relative; transition: color 0.15s;
}
.mc-edge button:hover, .mc-edge button[aria-expanded="true"] { color: var(--accent); }
.mc-edge .glyph { font-size: 1.1rem; }

.mc-drawer {
  position: fixed; top: var(--header-h); right: var(--edge-w); bottom: 0;
  width: min(var(--drawer-w, 420px), 88vw);
  background: var(--tier3-bg);
  border-left: 1px solid var(--border-strong);
  box-shadow: var(--shadow);
  padding: var(--sp-4);
  overflow: auto;
  transform: translateX(100%);
  transition: transform 0.2s ease;
  z-index: 20;
  visibility: hidden;      /* closed drawer must not intercept edge-tab clicks */
  pointer-events: none;
}
.mc-drawer[data-open="true"] { transform: translateX(0); visibility: visible; pointer-events: auto; }
.mc-drawer .drawer-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--sp-3); }
.mc-drawer h2 { margin: 0; font-size: 1rem; }
/* Cast drawer only (see render()'s [data-drawer-title] handling in shell.js)
   — the drawer's own title doubles as the Cast-list collapse toggle, so
   there's one "Cast" label instead of the title plus a second collapsible
   header row underneath it. */
.mc-drawer h2.drawer-title-toggle { cursor: pointer; user-select: none; }
.mc-drawer h2.drawer-title-toggle:hover { color: var(--accent); }

/* Tabbed drawer switching (2026-07-04) — hidden entirely unless 2+ drawers
   are pinned open at once, so the common single-drawer case is unchanged.
   Same tab-strip pattern as the doc viewer's own tabs (.doc-viewer-tab). No
   anchor icon (docs/adr/0032 removed the second, side-by-side panel a tab
   used to be pinned into) — a tab only ever switches or closes now. */
.drawer-tabs {
  display: flex; align-items: stretch; margin: calc(var(--sp-4) * -1) calc(var(--sp-4) * -1) var(--sp-3);
  background: var(--tier1-bg); border-bottom: 1px solid var(--tier1-border);
  /* Direct follow-up request: "do not scroll the tab menu to keep it
     visible when the content is scrolling" — .mc-drawer itself is the
     scrolling element (overflow:auto on the whole aside, not just the
     body), so a plain sticky top:0 on this child keeps it pinned to the
     top of that scroll regardless of how far the content below scrolls. */
  position: sticky; top: 0; z-index: 2;
}
.drawer-tabs[hidden] { display: none; }
.drawer-tabs-scroll { display: flex; gap: 2px; overflow-x: auto; flex: 1 1 auto; min-width: 0; padding: 0 var(--sp-2); }
.drawer-tab {
  display: flex; align-items: center; gap: var(--sp-1); flex: 0 0 auto; max-width: 12rem;
  padding: var(--sp-2) var(--sp-2); background: none; border: none; border-right: 1px solid var(--tier1-border);
  font-size: 0.78rem; color: var(--text-dim); cursor: pointer;
}
.drawer-tab.active { background: var(--tier3-bg); color: var(--accent); }
/* The Advisor's pseudo-tab flagged with a decision waiting (design/UX-
   ROADMAP.md Step 3) — a warm highlight plus the ★ shell.js already adds to
   the label text, so the signal survives even where color alone wouldn't
   (a11y: never color-only). */
.drawer-tab.pending { color: var(--warn); }
.drawer-tab.pending.active { background: var(--tier3-bg); color: var(--warn); }
.drawer-tab-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.drawer-tab-close { border-radius: 3px; padding: 0 3px; }
.drawer-tab-close:hover { background: var(--bg-inset); color: var(--text); }
/* Panel anchoring (direct follow-up request: "Navbar panels in the right
   section need to have expanding buttons that move the panel to anchor to
   the left of the navbar window") — same small-icon treatment as the
   close ✕ right next to it; .active (currently anchored) picks up the
   accent color so it reads as a toggled-on state, same convention
   .drawer-tab.active itself already uses. */
.drawer-tab-anchor { border-radius: 3px; padding: 0 3px; }
.drawer-tab-anchor:hover { background: var(--bg-inset); color: var(--text); }
/* The Storyboard card's own un-anchor control (shell.js's
   renderStoryboardGrid) — sits inline in the <h2> title text, so it needs
   to be small and vertically centered against the title's own line-height
   rather than full icon-btn sizing. */
.storyboard-unanchor { font-size: 0.75em; padding: 0 2px; vertical-align: middle; }
/* Placeholder shown in the ordinary drawer body for a panel that's
   currently anchored elsewhere (its real content lives in the Storyboard
   now) — plain centered text, no special chrome needed since it's a rare,
   transient state. */
.anchored-elsewhere-placeholder { padding: var(--sp-6) var(--sp-4); text-align: center; color: var(--text-dim); }
/* "Close all" — separate from a single tab's own ✕, pinned to the strip's
   right corner outside the scrollable tab list, so it's always reachable
   regardless of how many tabs are open. */
.drawer-tabs-close-all {
  flex: 0 0 auto; display: flex; align-items: center; padding: 0 var(--sp-3);
  background: none; border: none; border-left: 1px solid var(--tier1-border);
  color: var(--text-dim); cursor: pointer; font-size: 0.85rem;
}
.drawer-tabs-close-all:hover { background: var(--bg-inset); color: var(--accent); }
/* Collapse arrow — hides the whole drawer panel (keeping every tab open)
   so the workspace behind it is visible again; same strip-corner
   treatment as "close all" beside it, just a separate action. */
.drawer-tabs-collapse {
  flex: 0 0 auto; display: flex; align-items: center; padding: 0 var(--sp-2);
  background: none; border: none; border-left: 1px solid var(--tier1-border);
  color: var(--text-dim); cursor: pointer; font-size: 0.85rem;
}
.drawer-tabs-collapse:hover { background: var(--bg-inset); color: var(--accent); }
/* The floating icon that replaces the drawer entirely while collapsed —
   fixed just left of the edge nav, near where the drawer itself would
   have opened, so restoring it feels like clicking "back where it was." */
.drawer-collapsed-restore {
  position: fixed; top: calc(var(--header-h) + var(--sp-3)); right: calc(var(--edge-w) + var(--sp-3));
  z-index: 15; width: 2.4rem; height: 2.4rem; border-radius: 50%;
  background: var(--tier3-bg); border: 1px solid var(--border-strong); box-shadow: var(--shadow);
  color: var(--accent); font-size: 1.1rem; cursor: pointer;
}
.drawer-collapsed-restore:hover { background: var(--tier2-bg); }
.drawer-collapsed-restore[hidden] { display: none; }

/* --- Responsive: three tiers (phone / tablet / desktop), per the founding
   brief's §3.5 spec — "Co-Pilot becomes a tablet edge-tab, a phone
   bottom-sheet". Same single-column region restack below 1024px either
   way; what differs is HOW the Co-Pilot is summoned (2026-07-04 design
   review: the single-breakpoint collapse was flagged as worth
   confirming — this replaces it with the three-way split). ------------ */

/* Shared by phone + tablet: single-column stack, breadcrumb folded away
   (no grid area for it in this layout — same as before the three-way
   split; revisit if tablet's extra width earns it back later). */
@media (max-width: 1023px) {
  .cockpit {
    grid-template-rows: var(--header-h) 1fr;
    grid-template-columns: 1fr;
    grid-template-areas:
      "header"
      "work";
    padding-right: var(--edge-w);
  }
  .mc-breadcrumb { display: none; }
  .workspace-with-side { grid-template-columns: 1fr; }
  .workspace-docked-panel { max-height: none; }
}

/* Phone and tablet (<=1023px, design/UX-ROADMAP.md Step 4) — the Advisor's
   own standalone bottom-sheet/slide-in presentation (a separate summon/
   dismiss toggle, data-toggle-copilot) is retired at this tier: Advisor is
   now a permanently-pinned, always-reachable tab in the same drawer panel
   Composer/Navigator use (Step 5's compact tab menu, extended from phone-
   only to also cover tablet — the same "no room for 3 columns plus a
   drawer side by side" reasoning applies at this width too, and tablet's
   own edge-panel treatment before this was already the narrower-than-
   desktop instinct, just not carried all the way through). Its own edge-nav
   button is hidden alongside it (further down, [data-toggle-copilot]) since
   the pinned tab is the one way to reach it now — a second, separate toggle
   for the same content would just be confusing. */
@media (max-width: 1023px) {
  .mc-copilot { display: none; }
  [data-toggle-copilot] { display: none; }
  /* .mc-drawer is the ONE visible panel at every compact width now (Steps
     4/5 make it the Storyboard itself, not just an occasional drawer) — it
     needs to reliably span the full available width up to the edge nav
     (.mc-edge, always visible, own width var(--edge-w)), not the
     `min(var(--drawer-w, 420px), 88vw)` cap the wider desktop/tablet-as-
     occasional-drawer case used. Left unfixed, that cap left a gap between
     the drawer's own (narrower-than-expected) right edge and the edge nav
     on many phone/tablet widths above 480px — visually read as the edge
     nav "covering" the Storyboard content that should have reached it.
     `left: 0` + `width: auto` (with `right: var(--edge-w)` from the base
     rule) is the standard two-inset computed-width form — the box is
     defined by its edges, not a length, so it can never fall short of
     either one. */
  .mc-drawer { left: 0; width: auto; }
}

@media (min-width: 1024px) {
  /* Advisor is a permanent region on desktop — never a sheet/edge-tab. */
  .mc-copilot { transform: none; }
  /* ...except while a real drawer is pinned open (design/UX-ROADMAP.md
     Step 3): the drawer already visually covers this column (.mc-drawer
     and this grid column both size off the same --drawer-w token now, so
     their left edges align exactly, not just approximately), so the
     standalone panel is hidden rather than sitting duplicated/unreachable
     underneath it — its content is reachable from the drawer's own tab
     strip instead (the 'advisor' pseudo-tab, see shell.js's
     renderActiveDrawerHtml/hasPendingAdvisorDecision). */
  .cockpit.has-open-drawer .mc-copilot { display: none; }
}

/* Phone tier (<=480px), docs/adr/0032 — layered under the compact tier
   above, additive not a replacement. .mc-drawer's own full-width-to-the-
   edge-nav treatment now lives in the shared <=1023px compact block above
   (it applies at every compact width, not just this narrowest one); what's
   left here is phone-specific density: a tighter padding, and several
   fixed-column "table row" grids dropping to a single column so every
   field is readable without a horizontal scrollbar — same later-in-
   cascade-wins technique .field-row-2col's own 1023px override already
   established just above, not !important overrides. */
@media (max-width: 480px) {
  .mc-drawer { padding: var(--sp-3); }
  /* Direct request: the header's Party/Colony/World/Journal tab group
     (.header-drawer-tabs, HEADER_ORDER in shell.js) was too compressed at
     phone width once it always showed icon+label (a prior narrow-viewport
     rule hiding just this label used to exist and was removed by an
     earlier direct request — reinstated here, scoped to this one width,
     since the actual complaint is specifically about phone crowding, not
     tablet/desktop where the row has room). Icon-only; the button's own
     title attribute still names it for a long-press/hover. */
  .header-drawer-tabs .btn-label { display: none; }
  /* The header stays one row (shell.js's fitHeaderToOneRow drops the
     "GMAtlas |" title when needed, then the campaign name truncates); the
     campaign picker stays fully usable. Search, header tabs and ⚙ go
     icon-only here, each keeping its title="" tooltip. */
  .mc-header [data-search-toggle] .btn-label { display: none; }
  /* Tighter top nav so everything fits one row on a phone. */
  .mc-header { padding: 0 var(--sp-2); gap: var(--sp-2); }
  .mc-header .header-actions, .mc-header .header-drawer-tabs { gap: var(--sp-1); }
  .mc-header .btn.sm { padding: 0.3rem 0.4rem; }
  .mc-header [data-settings-menu-toggle] b { display: none; }
  .field-row2, .colony-fields, .rel-add, .party-tracker-row, .colony-crew-row,
  .thread-row, .trade-contract-row {
    grid-template-columns: 1fr;
  }

  /* Statblock fields — reworked from cramped ~4.4rem flex-wrap badges (real
     reported bug, screenshot-confirmed: a genuine field name like
     "Disposition" or "Notable Gear" ellipsis-truncated to 1-3 letters, its
     value box barely wide enough to tap) to one full-width row per field,
     label left / control right, deliberately bypassing the desktop density
     goal in favor of every label being fully readable and every control
     being a comfortable tap target — the point of a phone layout is data
     entry working, not matching how many fields fit on a desktop screen.
     A bottom-border list (no per-row background/border box) reads as a
     familiar native settings-list rather than a grid of pills. All three
     field kinds (plain text, attribute/roll, track) share the same
     label-column width so a group's rows still line up with each other. */
  .statblock-block { display: block; }
  /* Plain text fields (Role, Notable Gear, ...) stack label above value
     across two rows — direct request: shrinking the label column to widen
     the value field only goes so far before the label itself gets tight
     again, and free text (unlike a track's boxes or an attribute's short
     number) genuinely needs the room. Track/attribute fields keep a
     side-by-side single row (their controls are inherently small — boxes,
     a short number — so stacking them would just waste vertical space)
     with a narrower label column than before, giving their controls more
     room too. */
  .statblock-row {
    display: flex; flex-direction: column; align-items: stretch; gap: 0.3rem;
    width: auto; min-height: auto;
    padding: 0.55rem 0.15rem;
    margin-bottom: 0;
    background: none; border: none; border-radius: 0;
    border-bottom: 1px solid var(--border);
  }
  .statblock-key {
    font-size: 0.72rem; letter-spacing: 0.02em; text-transform: uppercase;
    white-space: normal; overflow: visible; text-overflow: clip; line-height: 1.3;
  }
  .statblock-val { font-size: 0.95rem; width: 100%; min-height: 2.25rem; padding: 0.4rem 0.5rem; }

  /* Label column narrowed again (5rem/34% -> 4.5rem/28%, direct request) so
     a full track's worth of boxes (Health, Strain, ...) has the best
     chance of fitting in one line on a real phone width without wrapping;
     track-box shrunk slightly too (1.75rem -> 1.55rem) for the same
     reason. align-self:center on the label is redundant with the grid's
     own align-items:center but stated explicitly (direct request to
     ensure it) rather than relying only on the inherited value.
     .attr-row is deliberately NOT included here any more — it's a small
     self-contained box now (see the desktop rule), not a full-width grid
     row, so it doesn't need a phone-specific layout of its own. */
  .statblock-row.track-row {
    display: grid; grid-template-columns: minmax(4.5rem, 28%) 1fr;
    align-items: center; gap: var(--sp-2);
  }
  .track-row .statblock-key { align-self: center; }
  .track-row .track-widget { justify-content: flex-end; }
  .track-row .track-box { width: 1.55rem; height: 1.55rem; font-size: 0.72rem; }

  /* The read-only Combat/Danger-style summary badges above the field list
     (attributeBadges()) get the same "room to breathe" treatment, so a
     multi-word key like "Combat / Danger" wraps cleanly instead of
     crowding a fixed-width pill. */
  .attribute-badges { gap: var(--sp-2); }
  .attr-badge { min-width: 5.5rem; min-height: 2.75rem; padding: 0.45rem 0.65rem; }
  .attr-key { font-size: 0.66rem; }
  .attr-val { font-size: 0.95rem; }
}

/* ============================================================
   Phase 2 — interactive components
   ============================================================ */

.header-actions { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.campaign-title { font-size: 0.85rem; color: var(--accent); cursor: pointer; }
.campaign-title:hover { text-decoration: underline; }

/* Buttons */
.btn {
  background: var(--tier2-bg); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  padding: 0.45rem 0.8rem; font-size: 0.82rem; transition: background 0.15s, border-color 0.15s;
}
.btn:hover { border-color: var(--accent); }
.btn.primary { background: linear-gradient(180deg, #0ea5e9, #0284c7); border-color: #38bdf8; color: #04121f; font-weight: 600; }
.btn.primary:hover { background: linear-gradient(180deg, #38bdf8, #0ea5e9); }
.btn.ghost { background: transparent; }
.btn.sm { padding: 0.3rem 0.6rem; font-size: 0.75rem; }
.btn.danger { color: var(--danger); border-color: var(--danger); }
.btn.danger:hover { background: var(--danger); color: #1a0505; }
.icon-btn { background: none; border: none; color: var(--text-dim); padding: 0.15rem 0.4rem; border-radius: var(--radius-sm); font-size: 0.9rem; }
.icon-btn:hover { color: var(--accent); background: var(--tier2-bg); }

.chip {
  background: var(--tier2-bg); color: var(--text); border: 1px solid var(--border);
  border-radius: 999px; padding: 0.3rem 0.7rem; font-size: 0.75rem;
}
.chip:hover { border-color: var(--accent); color: var(--accent); }

/* Form fields */
.field-label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.7rem; letter-spacing: 0.08em; color: var(--text-dim); text-transform: uppercase; margin-top: var(--sp-3); }
.field-label .metric { color: var(--accent); font-size: 0.85rem; }
/* A field's optional 🔮 "jump to relevant Oracle(s)" link (docs/adr/0016) —
   right-aligned on the label's own line via margin-left:auto. */
.field-label-row { display: flex; align-items: center; width: 100%; }
.field-label-row .icon-btn { margin-left: auto; text-transform: none; letter-spacing: normal; }
/* WHERE's Site Description/Immediate Surroundings (direct follow-up
   request — "the icons should be right-aligned") — a robust alternative
   to the icon-btn-first-child margin-left:auto trick above for a row that
   carries more than one icon: the label text and the icon group are two
   separate flex children, so alignment doesn't depend on which child
   happens to be first in the DOM. */
.field-label-text { text-transform: none; letter-spacing: normal; }
.field-label-actions { display: flex; align-items: center; gap: 2px; margin-left: auto; }
.field-label-actions .icon-btn { margin-left: 0; }
.field-label-hint { text-transform: none; letter-spacing: normal; opacity: 0.8; }
/* Direct follow-up request ("make the crystal ball, face and magnifying
   glass icons grayscale or line-drawing like was done for the pencil") —
   🔮/🎭/🔍 have no Unicode text-presentation variant (unlike ✎, U+270E,
   which is an older dual-presentation symbol VS15 can force to plain
   text), so a CSS filter is the only way to de-color them the same way. */
.icon-mono { filter: grayscale(1); }
/* Visual separator between an entity card's field groups (World
   Demographics: Starport/Bases vs. Tech Level onward, docs/adr/0026). */
.field-divider { border: none; border-top: 1px solid var(--border); margin: var(--sp-3) 0; }
/* Direct follow-up request: NPC/Asset/Lifeform's Type/Tags row sits at the
   top of the statblock area, separated from the character sheet content
   below it by a HALF-width line (not the full-width .field-divider used
   everywhere else) — a lighter visual break for two things that are still
   part of the same general "entity classification" area. */
.field-divider-half { border: none; border-top: 1px solid var(--border); margin: var(--sp-3) 0; width: 50%; }
/* WHAT tab's tracker rows (Intent/Threat/Mystery, Resources/Reputation/
   Stress) — auto-fit instead of a fixed 3-column grid, so they reflow to
   2 or 1 per row as the window narrows instead of jumping straight from
   3 columns to 1 at the single .field-row/.field-row-3col media-query
   breakpoint below (that used to be their only narrow-width behavior). */
.field-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); gap: var(--sp-3); align-items: end; }
/* Narrative Trackers (Resources/Reputation/Stress) — a balanced N-up row, not
   the 3-up Intent/Threat/Mystery layout above. */
.field-row-2col { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); align-items: end; }
.field-row-3col { display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); gap: var(--sp-3); align-items: end; }
/* The Storyboard's two persistent panels (design/UX-ROADMAP.md Step 2) —
   Composer (the 5 former W-tabs, now open/collapsible sections) beside
   Navigator (Scene Summary + trackers), equal-width on direct request
   (previously a 2:1 split), as two genuinely independent columns, each
   scrolling on its own (.workspace-card-body, above) rather than sharing
   one scrollbar. `align-items: stretch` (not
   the CSS default `start`) is what lets both columns fill the row's full
   height so each has real room to scroll within — a fixed-height row via
   the flex/min-height chain on .mc-workspace above it. Previously
   Navigator used position: sticky instead, which clipped its own content
   whenever Navigator was taller than the viewport (a real reported bug —
   its bottom was unreachable until Composer had been scrolled all the way
   down, since a stuck element has no scroll of its own). Collapses to a
   plain stacked single column below 768px, matching .mc-workspace's own
   phone-tier reversion to ordinary whole-page scrolling above — this is
   deliberately still the OLD collapse behavior (not yet the tab-menu
   design/UX-ROADMAP.md Step 5 describes — that's a bigger, separate
   change; tablet's own placement is an open question, Step 4, left
   unchanged here pending real-device verification neither this pass nor
   its authoring environment could do). */
.storyboard-grid { display: grid; grid-template-columns: minmax(18rem, 1fr) minmax(18rem, 1fr); gap: var(--sp-4); margin-top: 0; align-items: stretch; flex: 1 1 auto; min-height: 0; }
@media (max-width: 767px) {
  .storyboard-grid { grid-template-columns: 1fr; display: block; flex: none; }
}
/* Trackers below the Scene Summary — one
   full-width slider per row, not the 3-up .field-row-3col grid the
   header used, so the stack reads as compact and reuses .field-label.sm's
   existing dial styling with no new rules beyond this spacing wrapper. */
.dashboard-trackers { margin-top: var(--sp-4); padding-top: var(--sp-3); border-top: 1px solid var(--border); }
.dashboard-tracker { margin-top: var(--sp-2); }
.dashboard-tracker:first-child { margin-top: 0; }
/* Direct follow-up request: "The trackers on Navigator are for Starforged
   and should be filtered to that ruleset and all rulesets activated
   would have their trackers added to Navigator (separated by section
   header named for the ruleset if there are more than one...)." Each
   ruleset's own .dashboard-trackers block becomes one
   .dashboard-trackers-section; its own border-top already separates it
   from the one above, so the wrapper itself needs no extra chrome beyond
   an optional label. A read-only tracker (another ruleset's own Party
   Trackers, shown here but only ever EDITED on the Party panel) uses the
   Entity Editor's statblock pill (.attr-row-compact, .attr-val-static). */
.dashboard-trackers-section-head { margin: 0 0 var(--sp-1); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim); }
/* Each former W-tab, now a collapsible section (dashboardSection(),
   ui/workspace/index.js) — reuses .section-head-row (drawers/index.js's
   Bases of Influence toggle already established this exact convention)
   for the header row, just adds separation between sections. Direct
   follow-up request: the header should read like the Navigator's own
   "Scene Summary" heading (.threads-head h3 below) rather than a boxed
   ghost button — same small/bold/accent-colored treatment, explicit reset
   properties (not .btn/.icon-btn) since it's a <button> standing in for
   an <h3>, not a real button-shaped control. */
.dashboard-section { border-top: 1px solid var(--border); padding-top: var(--sp-3); margin-top: var(--sp-3); }
.dashboard-section:first-child { border-top: none; padding-top: 0; margin-top: 0; }
.dashboard-section-toggle { background: none; border: none; padding: 0; text-align: left; cursor: pointer; font-size: 0.85rem; font-weight: 700; color: var(--accent); }
.dashboard-section-summary { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 55%; }
.dashboard-section-lead { margin-top: var(--sp-1); }
.narrative-composer-preview { min-height: 3rem; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--sp-3); background: var(--bg-inset); margin: var(--sp-2) 0; }
/* Co-Pilot's Site Concept/Adventure Seed draft cards (2026-07-23) — same
   bordered-box treatment as .narrative-composer-preview, but a real
   editable <textarea> since the draft isn't live-recomputed each render
   (a one-time roll held in ephemeral state) so there's no clobber risk. */
.inspiration-draft { margin-top: var(--sp-2); }
.inspiration-draft textarea { width: 100%; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--sp-3); background: var(--bg-inset); color: var(--text); font: inherit; resize: vertical; }

/* WHO's scene-scoped NPC groups + WHERE's Location Details (docs/adr/0041
   Phase 13a/13b). .npc-scene-group reuses .workspace-mini-section's own
   top margin for spacing between the three groups; .npc-scene-card is a
   compact bordered row per NPC (name chip + toggle, expanding to its
   scene fields) rather than the full .workspace-card treatment, since a
   scene can hold several. .oracle-field-row is the shared label+🎲+input
   shape both NPC fields and Location's sensory fields use. */
.npc-scene-group { margin-top: var(--sp-3); }
.npc-scene-card { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--sp-2) var(--sp-3); margin-top: var(--sp-2); background: var(--bg-inset); }
.npc-scene-card-actions { display: inline-flex; gap: var(--sp-1); align-items: center; }
.npc-scene-card-body { margin-top: var(--sp-2); display: flex; flex-direction: column; gap: var(--sp-2); }
.oracle-field-row { display: grid; grid-template-columns: auto 1.6rem 1fr auto; align-items: center; gap: var(--sp-2); }
.oracle-field-row input { width: 100%; }
/* Direct follow-up request: "make the textboxes expandable... if the text
   is too long to fit, expand the textbox at least 2-3 rows" — a <textarea>
   instead of a single-line <input>, grown by shell.js's autoGrowSceneField
   (scrollHeight-based, same mechanism the Scene Summary field already
   uses) rather than a fixed rows count, so it opens exactly as tall as
   whatever text it already holds and keeps growing as the GM types.
   overflow:hidden avoids a flash of the native scrollbar between the
   'auto' reset and the real height being set; align-self:start keeps it
   from being grid-stretched to the row's own tallest cell. */
.oracle-field-textarea { width: 100%; resize: vertical; overflow: hidden; align-self: start; }
/* Actor thumbnails (Composer WHO redesign) — a mention-style, read-only
   photo+name button for Protagonists/Antagonists/Bystanders, replacing the
   old always-visible chip rows. Sized well below .gallery-thumb-circle's
   5rem so several fit per row. .actor-thumb-badge (expand-scene-details/
   remove) overlays the photo's LOWER-left/lower-right edge per reference
   mockup, pushed further apart and clear of the photo (direct follow-up
   correction — they were still clipping into the circle) so each only
   touches its outside edge, not sitting in a row below it. .actor-thumb-
   circle is the shared position:relative anchor both badges are
   absolutely positioned against; each badge is a sibling of .actor-thumb
   (the photo's own button), never nested inside it, since a <button>
   can't contain another <button>. The whole wrap is draggable (data-drag-
   actor, shell.js) to move an Actor between groups; .npc-scene-group (the
   drop target) picks up the generic .drop-hover outline while a
   compatible drag is over it. */
.actor-thumb-row { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-top: var(--sp-2); }
.actor-thumb-wrap { display: flex; flex-direction: column; align-items: center; width: 4.5rem; cursor: grab; }
.actor-thumb-wrap:hover .actor-thumb-name { color: var(--accent); }
.actor-thumb-circle { position: relative; }
.actor-thumb { display: block; background: none; border: none; color: var(--text); padding: 0; }
.actor-thumb-photo { width: 2.75rem; height: 2.75rem; border-radius: 50%; object-fit: cover; border: 2px solid var(--border); background: var(--bg-inset); display: block; }
.actor-thumb-photo-empty { display: flex; align-items: center; justify-content: center; font-size: 1rem; font-weight: 600; color: var(--text-dim); }
.actor-thumb-name { font-size: 0.7rem; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; margin-top: 0.4rem; }
/* WHERE Location details' "District"/"Site" top label (direct follow-up
   request) — mirrors .actor-thumb-name's own type treatment but sits
   flush above the circle instead of below it, so it reads as a category
   tag rather than a second name. */
.actor-thumb-toplabel { font-size: 0.62rem; text-align: center; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim); line-height: 1; margin-bottom: 0.2rem; }
.actor-thumb-badge {
  position: absolute; bottom: -0.3rem; width: 1.1rem; height: 1.1rem; line-height: 1;
  display: flex; align-items: center; justify-content: center; padding: 0;
  border-radius: 50%; border: 1px solid var(--border-strong); background: var(--tier3-bg); color: var(--text-dim);
  font-size: 0.58rem;
}
.actor-thumb-badge:hover { color: var(--accent); border-color: var(--accent); }
.actor-thumb-badge-expand { left: -0.6rem; }
.actor-thumb-badge-remove { right: -0.6rem; }
.npc-scene-group.drop-hover { border-radius: var(--radius-sm); }
.npc-scene-card-detail { width: 100%; }
/* WHO's five thumbnail groups' collapse toggle (direct follow-up request:
   "without changing the current font style") — the button carries
   .field-label-static directly for its type styling (font-size/color/
   letter-spacing/text-transform, unchanged), this rule only strips the
   browser's own button chrome so it doesn't look like a boxed control. */
button.actor-group-toggle { background: none; border: none; padding: 0; cursor: pointer; text-align: left; }
/* WHERE's consolidated System row (direct follow-up request, reworked
   again after an intermediate stretch-to-fill attempt read as "too
   reduced" once shrunk) — a plain left-aligned, FIXED-size thumbnail
   (see .location-hierarchy-thumb below — 30% larger than a standard NPC
   thumbnail, direct instruction, not sized relative to the row's own
   height) with the info block taking the rest of the row's width
   (flex:1). align-items:center (not stretch) keeps the fixed-size circle
   vertically centered next to the 3-line info block instead of trying to
   grow/shrink to match it; the 🔗 button shares that same centering. */
.location-hierarchy-row { display: flex; align-items: center; gap: var(--sp-3); margin-bottom: var(--sp-2); }
.location-hierarchy-info { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; justify-content: center; gap: 0.1rem; }
/* The System's own name — direct follow-up request: moved off the
   thumbnail (a plain photo now, no name underneath) to this new first
   line, above the Star name. */
.location-hierarchy-system { font-size: 0.95rem; font-weight: 700; }
.location-hierarchy-star { font-size: 0.9rem; font-weight: 600; }
.location-hierarchy-zonehex { font-size: 0.75rem; }
/* The System thumbnail itself — direct instruction: a FIXED size, 30%
   larger than a standard NPC thumbnail (.actor-thumb-photo's own 2.75rem
   elsewhere in this file), not a percentage of the row's own height (the
   previous two attempts both tied it to the row/info-block's dynamic
   height, which read as unpredictably too small once actually shrunk).
   flex:0 0 auto keeps it left-aligned at this fixed size regardless of
   how much space .location-hierarchy-info (flex:1, above) ends up taking. */
.location-hierarchy-thumb { flex: 0 0 auto; display: flex; }
.location-hierarchy-thumb .actor-thumb { display: block; padding: 0; }
.location-hierarchy-thumb .actor-thumb-photo,
.location-hierarchy-thumb .actor-thumb-photo-empty { width: calc(2.75rem * 1.3); height: calc(2.75rem * 1.3); }
/* Latest Scene's split fields (Opening/Driver/Clue/Complication/Decision
   point/Likely Consequence/Current thread) — single column, not a
   multi-column grid: a scene field is often a full sentence or two, and
   reads much better as one legible column than crammed side by side. */
.scene-fields { display: grid; grid-template-columns: 1fr; gap: var(--sp-3); margin: var(--sp-2) 0; }
/* Each field is a <textarea> (not <input>) so it can grow with its
   content: starts at one row (rows="1"), auto-grows on input with no cap
   (autoGrowSceneField, ui/shell.js — also run once per render for fields
   already holding a lot of text) — the old ~4-row cap-then-internal-
   scroll is gone (direct follow-up request: "allow all textboxes to be
   resizeable... to view more text"), replaced by a manual resize handle
   too; autoGrowSceneField never shrinks below a height the GM dragged
   larger (only grows to fit content on typing), so the two don't fight. */
.scene-fields textarea { resize: vertical; overflow-y: auto; line-height: 1.4; min-height: calc(1.4em + 1rem); }
/* Collapsed-by-default scene fields (docs/adr next-request UX batch) —
   the toggle is a <button> standing in for the field's own label text,
   so it needs the label's font reset instead of a default button look. */
.scene-field-toggle { background: none; border: none; padding: 0; margin: 0; font: inherit; color: inherit; text-transform: inherit; letter-spacing: inherit; cursor: pointer; text-align: left; }
/* Shared collapsible-section-header look (direct request: every such
   header in the Entity Editor — Overview, Revealed/hidden, World Profile,
   World Demographics, Conflict's "Add depth", Enhancements, Add a
   Statblock — reads the same way, small/dim/uppercase like a field label,
   not a boxy .btn). Explicit properties rather than `inherit` (Overview's
   original approach) so it looks identical regardless of which element
   wraps it (a bare <span>, an <h4>, ...) instead of picking up whatever
   font/color that wrapper happens to have. */
.section-toggle {
  background: none; border: none; padding: 0; margin: 0; cursor: pointer; text-align: left;
  font-size: 0.7rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-dim);
}
.section-toggle:hover { color: var(--accent); }
textarea, select, input[type="text"], input[type="search"], .drawer-search {
  width: 100%; box-sizing: border-box;
  background: var(--bg-inset); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 0.5rem 0.6rem; font: inherit; font-size: 0.9rem; text-transform: none; letter-spacing: normal;
}
textarea { resize: vertical; }
textarea:focus, select:focus, input:focus, .drawer-search:focus { outline: none; border-color: var(--accent); }
input[type="range"] { width: 100%; accent-color: var(--accent-strong); }

/* Mention editor — a contenteditable standing in for what used to be a
   plain <textarea>, everywhere an @mention can appear (Journal, Guide,
   WHO/WHERE/WHAT/WHY/HOW). A mention renders as real inline text here (see
   .mention-link below), not a chip in a row underneath the field — the
   point of this whole rewrite (2026-07-05) was making an @mention read like
   a hyperlink inline in the sentence, the way it would in a web page or a
   Word document, with the label editable in place by clicking into it and
   typing, same as Word's own editable-hyperlink convention. */
.mention-editor {
  width: 100%; box-sizing: border-box; min-height: 4.5rem; max-height: 40vh; overflow-y: auto;
  background: var(--bg-inset); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 0.5rem 0.6rem; font: inherit; font-size: 0.9rem;
  /* WHO/WHERE/WHAT/WHY/HOW wrap this in a <label class="field-label">,
     which sets text-transform: uppercase for its own small caption text —
     a real reported bug: without an explicit override here, that inherited
     onto the field's actual content too (entity/doc names in @mentions
     included), shouting every WHO/WHERE/etc. field in caps while Journal
     (wrapped in .drawer-note, no such rule) and Guide (no wrapper at all)
     were unaffected. */
  text-transform: none;
  white-space: pre-wrap; word-break: break-word;
  /* Direct follow-up request ("allow all textboxes to be resizeable
     vertically to view more text") — every rich mention-editor field gets
     a manual drag handle now, not just Guide's own (previously the one
     special-cased instance of this). overflow-y:auto above already makes
     `resize` apply per spec. */
  resize: vertical;
}
.mention-editor:focus { outline: none; border-color: var(--accent); }
.mention-editor:empty::before { content: attr(data-placeholder); color: var(--text-dim); pointer-events: none; }
/* The inline/read-only mention-text variant (no visible box chrome to
   drag) opts back out of the resize handle the base rule above adds. */
.mention-text.mention-editor, .mention-text { min-height: 0; border: none; background: none; padding: 0; resize: none; }
.mention-editor ul, .mention-editor ol { margin: 0.2rem 0; padding-left: 1.4rem; }
.mention-editor li { margin: 0.1rem 0; }
.rt-lg { font-size: 1.3em; }

/* A toolbar-inserted table ("USER CHANGES" batch) — left-aligned cells,
   thin borders, by default, per explicit request; no per-row/column
   controls (see shell.js's insertTableSkeleton) — more rows/columns come
   from typing more "| cell |" syntax by hand. */
.mention-editor table.rt-table { border-collapse: collapse; margin: 0.3rem 0; width: 100%; }
.rt-table th, .rt-table td { border: 1px solid var(--border); padding: 0.25rem 0.5rem; text-align: left; font-weight: normal; }
.rt-table th { color: var(--text-dim); font-size: 0.8em; }

/* Lightweight rich-text toolbar (ADR 0018) — one small row of buttons above
   a .mention-editor that insert plain markup characters (see shell.js's
   data-rich-cmd mousedown handler), not live execCommand formatting. */
.rich-toolbar { display: flex; flex-wrap: wrap; gap: 0.15rem; margin-bottom: 0.25rem; }
.rich-toolbar .icon-btn { border: 1px solid var(--border); border-radius: var(--radius-sm); min-width: 1.6rem; text-align: center; }
.rich-toolbar .icon-btn:hover { color: var(--text); border-color: var(--accent); }
/* Direct report/fix: the collapse toggle used to sit as a position:absolute
   overlay pinned to the textbox's own top-right corner — while collapsed
   (this app's default), that put it directly on top of the editor's own
   top-right corner with nothing else in flow above it, so it silently ate
   clicks/keystrokes meant for the text underneath. Now a normal flex
   sibling instead: .rich-field is a flex-wrap row: the icon row (.rich-
   toolbar, when expanded) claims a full-width first line via flex-basis:
   100%, then the editor and the toggle button share the next line side by
   side — the toggle never overlaps the editable area in either state.
   .rich-field > .mention-editor is scoped narrowly (.mention-editor is
   used bare in a couple of read-only contexts outside this wrapper too). */
.rich-field { display: flex; flex-wrap: wrap; align-items: flex-start; column-gap: 0.3rem; }
.rich-field > .rich-toolbar { flex: 1 0 100%; order: 1; }
.rich-field > .mention-editor { flex: 1 1 auto; min-width: 0; order: 2; }
.rich-toolbar-corner-toggle {
  order: 3; align-self: flex-start; flex: 0 0 auto;
  background: none; border: none; padding: 0; line-height: 1; font-size: 0.8rem;
}
.rich-toolbar-corner-toggle:hover { color: var(--accent); }
/* Visually hidden but still a real, clickable input (display:none blocks
   the native color picker from opening in most engines) — triggered via
   .click() from the 🎨 button, never shown/clicked directly itself. */
.rich-color-swatch { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; opacity: 0; pointer-events: none; }

/* An inline mention — a real hyperlink-like span living in the text, not a
   badge below it. mention-link-active means it resolved to a real entity
   or document (data-open-entity/data-doc-open present, so Ctrl/Cmd+Click —
   see shell.js's onClick guard — opens it); an unresolved mention (matches
   neither) gets the plain, undecorated variant so it visibly reads as "not
   linked yet" rather than a broken-looking link. */
.mention-link {
  border-bottom: 1px dotted var(--text-dim);
}
.mention-link.mention-link-active {
  color: var(--accent); border-bottom-color: var(--accent); cursor: pointer;
}
.mention-link.mention-link-active:hover { color: var(--accent-strong); border-bottom-color: var(--accent-strong); }
/* Direct follow-up request: a mention naming a real, known sourcebook
   that just isn't physically present on this install (mentionEditor.js's
   findExpectedReferenceDoc fallback) — a distinct warm color + solid
   underline so it reads as "actionable, something's missing" rather than
   either a normal working link or plain unlinked text. */
.mention-link.mention-link-missing { color: var(--warn); border-bottom: 1px solid var(--warn); cursor: pointer; }
.mention-link.mention-link-missing:hover { opacity: 0.85; }

/* A plain-text-styled clickable name (Faction Events' faction-name links
   into the Entity Editor) — not a chip/button, just a link-colored,
   underlined inline trigger inside otherwise plain text. */
.link-button {
  background: none; border: none; padding: 0; margin: 0; font: inherit;
  color: var(--accent); border-bottom: 1px dotted var(--accent); cursor: pointer;
}
.link-button:hover { color: var(--accent-strong); border-bottom-color: var(--accent-strong); }

/* An external link (Phase 11 backlog: "[label](url)", always a real,
   already-sanitized href — unlike a mention it can never be "unresolved,"
   so it's always the solid-underline active style, never the dotted
   not-yet-linked one above. */
.ext-link { color: var(--accent); text-decoration: underline; cursor: pointer; }
.ext-link:hover { color: var(--accent-strong); }

.action-bar { display: flex; gap: var(--sp-3); margin-top: var(--sp-4); flex-wrap: wrap; }
.shift-actions { display: flex; gap: var(--sp-2); margin-top: var(--sp-3); flex-wrap: wrap; }
/* Suggestion Lens picker (docs/adr/0009) — "What Happens Next?"'s chip draw. */
.lens-picker { margin-top: var(--sp-2); }
.lens-picker-chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-1); }

.last-scene { margin-top: var(--sp-4); border-top: 1px solid var(--border); padding-top: var(--sp-3); }
.last-scene summary { cursor: pointer; font-size: 0.85rem; color: var(--accent); }

.entity-chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-3); }
.entity-chip { background: var(--tier2-bg); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0.25rem 0.6rem; font-size: 0.8rem; }
.entity-chip .icon-btn { margin-left: 0.3rem; padding: 0 0.2rem; }
/* A chip paired with its own icon-button action (e.g. a "faction operating
   here" chip's ✕), NOT nested inside the chip itself — docs/adr/0038. */
.entity-chip-row { display: inline-flex; align-items: center; gap: 0.15rem; }

/* WHERE's persistent "current location" indicator (docs/adr/0038) — a
   thin accent-colored left border is enough emphasis to read as "this is
   selected" without a new layout system. */
/* This used to claw back part of .workspace-card-body-inner's own right
   gutter with a negative margin (when that gutter was a full --sp-6) so
   this row's +/- controls sat closer to the scrollbar than the rest of
   Composer's content — now that the gutter itself is narrowed to --sp-3
   (direct follow-up request, above), that claw-back would over-correct
   into negative padding, so it's removed; the new baseline gutter alone
   is already tight enough. */
.current-location-banner { border-left: 2px solid var(--accent); padding-left: var(--sp-2); }

/* WHO/WHERE's Faction Events tie-in (docs/adr/0031's Faction Events
   follow-up) — a small read-only summary + jump chips, same entity-chips
   markup the tag pickers above already use. */
.workspace-mini-section { margin-top: var(--sp-3); }

/* Co-Pilot actions */
.copilot-action { width: 100%; text-align: left; background: var(--bg-inset); color: var(--accent); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0.5rem 0.6rem; font-size: 0.82rem; }
.copilot-action:hover { border-color: var(--accent); }
.copilot-quick { display: flex; gap: var(--sp-2); flex-wrap: wrap; }

/* Drawer contents */
.mc-drawer-body { font-size: 0.88rem; }
.drawer-note { margin-bottom: var(--sp-3); }
.drawer-note-actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-2); }
/* The Journal compose box's ➜📖 submit (an icon button, like a chat
   "send") and Clear, left-aligned under the text box. */
.journal-submit-row { display: flex; justify-content: flex-start; align-items: center; gap: var(--sp-2); margin-top: var(--sp-2); }
.journal-submit { min-width: 2.75rem; min-height: 2.25rem; padding: 0.3rem 0.7rem; font-size: 1.05rem; line-height: 1; letter-spacing: -0.05em; }
.doc-card {
  background: var(--tier2-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--sp-3);
  margin-bottom: var(--sp-3);
  transition: border-color 0.15s, transform 0.15s;
}
.doc-card:hover { border-color: var(--accent); transform: translateY(-1px); }
.doc-card[draggable="true"] { cursor: grab; }
.doc-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.doc-card-actions { display: flex; gap: var(--sp-1); align-items: center; }
.document-badges, .journal-entity-badges { display: flex; flex-wrap: wrap; gap: 0.35rem; margin: var(--sp-2) 0 0; }
.doc-badge, .attr-badge { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.35rem 0.55rem; border-radius: 0.75rem; background: var(--bg-inset); border: 1px solid var(--border); font-size: 0.78rem; }
.doc-badge { background: rgba(56, 189, 248, 0.08); border-color: rgba(56, 189, 248, 0.22); }
button.doc-badge { font: inherit; color: inherit; cursor: pointer; }
button.doc-badge:hover { background: rgba(56, 189, 248, 0.18); }
.drawer-search { margin-bottom: var(--sp-3); }
/* Docs library: a short search box with the sort icon beside it (its menu
   reuses the Hexcrawl palette's .hexcrawl-menu look). */
.doc-search-row { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-3); }
.doc-search-row .drawer-search { margin-bottom: 0; flex: 0 1 16rem; width: auto; min-width: 8rem; }
.doc-sort-menu { left: auto; right: 0; }
.oracle-list { display: flex; flex-direction: column; gap: 2px; }
.oracle-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); padding: 0.3rem 0.4rem; border-radius: var(--radius-sm); }
.oracle-row:hover { background: var(--tier2-bg); }
.oracle-label { font-size: 0.82rem; }
/* Tag/edit/roll icon cluster, right-aligned as one group (docs/adr/0016). */
.oracle-row-actions { display: inline-flex; gap: 2px; align-items: center; margin-left: auto; }
/* Oracle tags (docs/adr/0016) — hidden by default behind the row's 🏷
   toggle, same footprint-saving posture as the entries editor below. */
.oracle-tag-editor { margin: 0 0 var(--sp-2) 0.4rem; padding: var(--sp-2); background: var(--bg-inset); border: 1px solid var(--border); border-radius: var(--radius-sm); }
.oracle-tag-filter-badge { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-2); padding: 0.3rem 0.5rem; background: var(--bg-inset); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: 0.8rem; color: var(--text-dim); }
/* Direct follow-up request: oracle rolls temporarily routed to the
   Navigator's Scene Summary instead of the Journal — accent-colored
   border so it reads as a distinct, temporary mode rather than the plain
   tag-filter badge above. */
.oracle-route-banner { border-color: var(--accent); color: var(--accent); }
/* Oracle table entry editor (Phase 8) — collapsed behind the row's ✎ toggle */
.oracle-editor { margin: 0 0 var(--sp-2) 0.4rem; padding: var(--sp-2); background: var(--bg-inset); border: 1px solid var(--border); border-radius: var(--radius-sm); }
.oracle-entry-row, .oracle-entry-add { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: 2px; }
.oracle-entry-input { flex: 1; font-size: 0.8rem; padding: 0.2rem 0.4rem; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text); }
.dim { color: var(--text-dim); }
.small { font-size: 0.75rem; }
.journal-list { display: flex; flex-direction: column; gap: var(--sp-3); }
.journal-entry { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--sp-3); background: var(--tier2-bg); }
.journal-meta { display: flex; align-items: center; justify-content: space-between; font-size: 0.68rem; color: var(--text-dim); margin-bottom: var(--sp-2); }
.journal-text { font-size: 0.85rem; white-space: pre-wrap; line-height: 1.45; }

/* Session Recap ("Previously on...", Phase 6) */
.recap-toggle { width: 100%; text-align: left; margin-bottom: var(--sp-3); }
.recap-panel {
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--tier2-bg);
  padding: var(--sp-3); margin-bottom: var(--sp-3);
}
.recap-panel h4 { margin: var(--sp-3) 0 var(--sp-1); font-size: 0.72rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--accent); }
.recap-panel h4:first-child { margin-top: 0; }
.recap-panel p { margin: 0; font-size: 0.85rem; }
.recap-list { margin: 0; padding-left: 1.1rem; font-size: 0.85rem; }
.recap-list li { margin-bottom: 2px; }
.recap-panel .entity-chips { margin-top: 0; }
.recap-panel [data-recap-save] { margin-top: var(--sp-3); }
.settings-group { margin-bottom: var(--sp-6); }
.settings-group h3 { font-size: 0.85rem; margin: 0 0 var(--sp-2); color: var(--accent); }
/* Settings → General's per-sourcebook Guide TOC list. */
.toc-source-list { list-style: none; margin: var(--sp-2) 0; padding: 0; }
.toc-source-row { display: flex; justify-content: space-between; gap: var(--sp-2); padding: var(--sp-1) 0; border-bottom: 1px solid var(--border); font-size: 0.85rem; }
.toc-source-row > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Settings topical tabs (UX batch) — a plain sticky button row above
   whichever tab's sections are currently rendered. */
.settings-tab-bar { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin-bottom: var(--sp-4); padding-bottom: var(--sp-3); border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--tier2-bg, inherit); z-index: 1; }
.settings-tab-bar .btn.active { color: var(--accent); border-color: var(--accent); }
/* Campaign panel's Colony/Starship tabs (direct follow-up request: "two
   right-aligned links/buttons") — same strip, just right-aligned instead
   of the default left/wrap. */
.settings-tab-bar.align-right { justify-content: flex-end; }

/* Collapsible "?" tip icons ("USER CHANGES" QoL batch) — a section title
   plus its toggle, right-aligned on the same line; the h3/h4 keeps its own
   existing margin, this row just adds the flex layout around it. */
.section-head-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }
.section-head-row h3, .section-head-row h4 { margin: 0; flex: 1; }
.help-icon { border: 1px solid var(--border); border-radius: 50%; width: 1.3rem; height: 1.3rem; line-height: 1; padding: 0; flex: 0 0 auto; }
.help-icon:hover { color: var(--text); border-color: var(--accent); }
.help-text { margin-top: var(--sp-1); }
.btn-col { display: flex; flex-direction: column; gap: var(--sp-2); }
.file-btn { display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }
.content-pack-checks { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.content-pack-checks .chip { display: inline-flex; align-items: center; gap: 0.3rem; cursor: pointer; }

/* Campaigns / Ruleset Profile Editor (design/adr/rules-profiles-multi-campaign.md) */
.campaign-list { display: flex; flex-direction: column; gap: var(--sp-2); margin-bottom: var(--sp-3); }
.campaign-row { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2); border: 1px solid var(--border); border-radius: var(--radius, 6px); flex-wrap: wrap; }
.campaign-row.active { border-color: var(--accent); }
.campaign-row-title { font-weight: 600; flex: 1; }
.campaign-profile-picker { margin: 0; }
/* Settings restructuring: the Campaigns tab's own switcher dropdown + New/
   Rename/Delete icon buttons, same inline-row posture as .campaign-row
   above (which it replaces as the tab's primary control). */
.campaign-switcher-row { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.campaign-switcher-row > select { flex: 1; min-width: 12rem; }
.profile-list { flex-direction: row; flex-wrap: wrap; }
.profile-list .btn.active { color: var(--accent); border-color: var(--accent); }
.module-enable-checks { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.module-enable-checks .chip { display: inline-flex; align-items: center; gap: 0.3rem; cursor: pointer; }
/* Ruleset Profile Editor's Save/Discard bar — sticky so it stays reachable
   below a long profile form; .dirty accents it so an unsaved edit is
   visually obvious even after scrolling past the fields that caused it. */
.profile-draft-bar { position: sticky; top: 0; z-index: 1; background: var(--tier2-bg, inherit); border: 1px solid var(--border); border-radius: var(--radius, 6px); padding: var(--sp-2) var(--sp-3); }
.profile-draft-bar.dirty { border-color: var(--accent); }
.profile-draft-bar p { margin: 0 0 var(--sp-2); }
.btn-col-row { display: flex; gap: var(--sp-2); }

/* Rules Constitution reference table (Settings) */
.tablewrap-narrow { overflow-x: auto; margin: var(--sp-2) 0; }
.rules-constitution-table { border-collapse: collapse; width: 100%; font-size: 0.8rem; }
.rules-constitution-table th, .rules-constitution-table td { text-align: left; padding: 0.35rem 0.5rem; border-bottom: 1px solid var(--border); vertical-align: top; }
.rules-constitution-table th { color: var(--text-dim); font-weight: 600; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em; }
.rules-provider-chip { display: inline-block; margin: 1px 2px 1px 0; cursor: default; }
.rules-provider-legend { margin: var(--sp-3) 0 0; padding-left: 1.1rem; font-size: 0.78rem; color: var(--text-dim); }
.rules-provider-legend li { margin-bottom: var(--sp-1); }
/* Settings -> Campaigns -> Source Data: pack text left, its Import button right. */
.source-data-list { list-style: none; padding-left: 0; }
.source-data-row { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-2); }
.source-data-row .btn { flex: 0 0 auto; }

/* Activity -> Rules Lens suggestion (Phase 9, HOW workspace view) */
.rules-lens-suggestion { margin: var(--sp-2) 0; padding: var(--sp-2); border: 1px solid var(--border); border-radius: 6px; }
.rules-lens-chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-1); align-items: center; }
.rules-lens-row { display: inline-flex; align-items: center; gap: var(--sp-1); }

/* Document library */
.doc-list { display: flex; flex-direction: column; gap: var(--sp-3); }
.doc-card { border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--tier2-bg); padding: var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-2); }
.doc-card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }
.doc-title-input { font-size: 0.92rem; font-weight: 600; }
.doc-card-actions { display: inline-flex; gap: 2px; align-items: center; }
.doc-card-title-group { display: inline-flex; align-items: center; gap: var(--sp-2); min-width: 0; }
.doc-content-input { min-height: 8rem; }

/* Gallery (Phase 11, docs/adr/0021-gallery.md) */
.gallery-toolbar { margin-bottom: var(--sp-2); }
/* The friendly-name confirmation step after picking a file (an inline
   form, not a popup, per this app's standing rule) — preview + name field
   side by side on a wide drawer, stacking on a narrow one. */
.gallery-upload-form { display: flex; gap: var(--sp-3); align-items: flex-start; margin-bottom: var(--sp-3); padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--tier2-bg); flex-wrap: wrap; }
.gallery-upload-preview { width: 5rem; height: 5rem; border-radius: var(--radius-sm); object-fit: cover; border: 1px solid var(--border); background: var(--bg-inset); }
.gallery-upload-fields { flex: 1 1 12rem; min-width: 10rem; }
.gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); gap: var(--sp-3); margin-top: var(--sp-3); }
.gallery-card { position: relative; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--tier2-bg); padding: var(--sp-3); display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); text-align: center; }
.gallery-card > [data-gallery-delete] { position: absolute; top: 0.3rem; right: 0.3rem; }
.gallery-card-meta { display: flex; flex-direction: column; gap: var(--sp-1); align-items: center; width: 100%; }
.gallery-thumb-circle { display: block; width: 5rem; height: 5rem; border-radius: 50%; object-fit: cover; border: 2px solid var(--border); background: var(--bg-inset); }
.gallery-original-img { width: 100%; max-height: 8rem; object-fit: cover; border-radius: var(--radius-sm); border: 1px solid var(--border); }
.tag-chip-locked { opacity: 0.85; }
/* Phase B entitlement stub — a gated Game System an install isn't entitled
   to (domain/entitlements.js's canAccessGameSystem) renders visibly locked
   rather than being silently absent. */
.game-system-locked { cursor: not-allowed; }
/* Supplement checkboxes (e.g. a Compendium volume) nest visually under
   their parent Game System's own activation row above. */
.game-system-supplement-row { margin-left: var(--sp-4); }
/* Direct follow-up request: "group all modules for a game system under
   the header of the game system" — one real-world product line's worth
   of Modules (Core Rules/Compendium/Planetfall, ...) under its own small
   heading, visually separated from the next family. */
.game-system-family { margin-top: var(--sp-3); padding-top: var(--sp-2); border-top: 1px solid var(--border); }
.game-system-family:first-child { margin-top: 0; padding-top: 0; border-top: none; }
.game-system-family h4 { margin: 0 0 var(--sp-1); }

/* Planetfall Grid Battlemap (Phase 11, docs/adr/0023) */
.battlemap-tabs { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; margin-bottom: var(--sp-2); }
.battlemap-toolbar { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; margin-bottom: var(--sp-2); }
.battlemap-name-input { width: auto; flex: 1 1 12rem; min-width: 8rem; }
/* Direct follow-up request: "Make the hexcrawl map title field readonly
   with an edit pencil icon to switch to edit mode" — the read-only
   display counterpart to .battlemap-name-input above. */
.hexcrawl-map-name { flex: 1 1 12rem; min-width: 8rem; font-weight: 600; }
.battlemap-toolbar select, .battlemap-grid-size-input { width: auto; }
.battlemap-palette { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin-bottom: var(--sp-2); }
/* Battlemap's collapsible "Maps" / "Map Objectives" groups; the zoom
   controls ride on the Map Objectives header row (right-aligned), so they
   stay reachable while it's collapsed. */
.battlemap-section { margin-bottom: var(--sp-2); min-width: 0; }
/* Maps | Map Objectives: two collapsible columns of one row (stacking on a
   narrow panel); a collapsed column shrinks to just its header. */
.battlemap-sections-row { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--sp-3); }
.battlemap-sections-row > .battlemap-section { flex: 1 1 14rem; }
.battlemap-sections-row > .battlemap-section + .battlemap-section { border-left: 1px solid var(--border); padding-left: var(--sp-3); }
.battlemap-section-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); margin-bottom: var(--sp-1); }
.battlemap-span-controls { display: inline-flex; gap: 2px; margin-left: var(--sp-2); vertical-align: middle; }
/* The Battlemap panel is a fill-height column: header and its two sections
   on top, the map window taking everything left below Map Objectives —
   in the drawer and on an anchored Storyboard card alike. */
.mc-drawer.drawer-battlemap { display: flex; flex-direction: column; }
.mc-drawer.drawer-battlemap .mc-drawer-body { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.workspace-card.battlemap-card .workspace-card-body { display: flex; flex-direction: column; }
.workspace-card.battlemap-card .workspace-card-body-inner { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.battlemap-panel { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.battlemap-panel .battlemap-viewport { flex: 1 1 auto; min-height: 18rem; padding-top: 0; container-type: size; }
/* The map image box keeps the map's 5:3 shape, as large as fits the window. */
.battlemap-panel .battlemap-world { inset: auto; left: 0; top: 0; width: min(100cqw, 100cqh * 5 / 3); aspect-ratio: 5 / 3; }
/* An anchored Battlemap widened over the Storyboard: 2 = both columns,
   3 = the whole Storyboard including the right-hand column. */
.storyboard-grid > .storyboard-span-2 { grid-column: 1 / -1; }
.workspace-card.storyboard-span-3 {
  position: fixed; top: var(--header-h); left: 0; right: var(--edge-w); bottom: 0; z-index: 22;
  margin: var(--sp-2);
}
/* Width over the Storyboard (desktop): 2 = this column plus Navigator (half
   the workspace), 3 = the whole Storyboard. */
.mc-drawer.drawer-span-2 { width: calc(var(--drawer-w) + (100vw - var(--edge-w) - var(--drawer-w)) / 2); }
.mc-drawer.drawer-span-3 { width: calc(100vw - var(--edge-w)); }
@media (max-width: 1023px) { .battlemap-span-controls { display: none; } }
/* The viewport: a fixed-aspect box (padding-top trick — no fixed height,
   since a GM's background image could be any shape), overflow:hidden so
   panning/zooming the world layer inside it never spills outside the
   drawer. .placing shows a crosshair cursor while an icon is armed from
   the palette (openInlinePrompt-adjacent battlemapPlacingIcon, shell.js).
   Pan/zoom camera (UX batch): the world layer below is the SAME size as
   this viewport at rest (scale 1, no translate) — an icon's 0-1 x/y
   fraction is always relative to the world layer, so it pans/zooms for
   free as the world layer's own transform changes, no per-icon math. */
.battlemap-viewport {
  position: relative; width: 100%; padding-top: 60%; overflow: hidden;
  background-color: var(--bg-inset);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.battlemap-viewport.placing { cursor: crosshair; }
.battlemap-viewport.drop-hover { outline: 2px solid var(--accent); outline-offset: -2px; }
/* transform-origin: 0 0 is load-bearing — shell.js's screenToWorldFraction/
   zoomBattlemapCamera math assumes translate/scale apply relative to the
   viewport's own top-left corner (matching getBoundingClientRect()'s
   .left/.top), not CSS's default center origin. cursor:grab hints the
   drag-to-pan gesture (mousedown+move, shell.js) even though nothing here
   is natively draggable. */
.battlemap-world {
  position: absolute; inset: 0; transform-origin: 0 0; cursor: grab;
  background-size: cover; background-position: center;
}
.battlemap-world:active { cursor: grabbing; }
/* An MP4 background — the same cover/center fit as an image background.
   pointer-events:none so pan/drop/placing clicks land on .battlemap-world
   exactly as they do over a still image. */
.battlemap-world .battlemap-bg-video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: center; pointer-events: none;
}
.battlemap-world .battlemap-placeholder { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; margin: 0; }
.battlemap-camera-controls { display: inline-flex; gap: 2px; margin-left: auto; }
/* A CSS-only grid (no SVG/canvas drawing) — --battlemap-grid-size is set
   inline per map (shell.js) so gridSize stays real campaign data, not a
   hardcoded constant; sized in background-size so it scales with the cell
   size regardless of the canvas's own rendered width. */
.battlemap-grid-overlay {
  position: absolute; inset: 0; pointer-events: none;
  background-image:
    repeating-linear-gradient(to right, var(--border) 0, var(--border) 1px, transparent 1px, transparent var(--battlemap-grid-size, 40px)),
    repeating-linear-gradient(to bottom, var(--border) 0, var(--border) 1px, transparent 1px, transparent var(--battlemap-grid-size, 40px));
}
.battlemap-icon {
  position: absolute; transform: translate(-50%, -50%); display: flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem; border-radius: 50%; background: var(--tier3-bg); border: 1px solid var(--border-strong);
  cursor: grab; font-size: 1.1rem; box-shadow: var(--shadow);
}
.battlemap-icon:active { cursor: grabbing; }
.battlemap-token-img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
.battlemap-token-fallback { font-weight: bold; color: var(--accent); }
.battlemap-icon-remove {
  position: absolute; top: -0.5rem; right: -0.5rem; width: 1.1rem; height: 1.1rem; min-width: 0;
  display: flex; align-items: center; justify-content: center; font-size: 0.65rem; line-height: 1;
  background: var(--bg-raised); border: 1px solid var(--border-strong); border-radius: 50%; opacity: 0;
}
.battlemap-icon:hover .battlemap-icon-remove, .battlemap-icon:focus-within .battlemap-icon-remove { opacity: 1; }

/* Hexcrawl — a genre-agnostic hex-grid overworld map, distinct from both
   Battlemap above (tactical, gridless, fixed-size fractional canvas) and
   World Tracker below (strategic, but a fixed 6x6 SQUARE grid). Reuses
   Battlemap's own viewport/toolbar/camera-controls classes verbatim
   (identical box/toolbar shape) but needs its own world/cell classes
   since the coordinate system is real unbounded pixels, not a 0-1
   fraction of a fixed box. transform-origin: 0 0 is load-bearing here for
   the exact same reason as .battlemap-world's own — shell.js's
   screenToHexcrawlWorldPixel/zoomHexcrawlCamera math assumes translate/
   scale apply relative to the viewport's own top-left corner. */
/* Hexcrawl palette: one row of icon buttons (Geography, Locations,
   Encounter, Overlays); each menu drops down under its own button. */
.hexcrawl-palette { display: flex; align-items: center; gap: var(--sp-1); flex-wrap: nowrap; margin: var(--sp-2) 0; }
.hexcrawl-menu-wrap { position: relative; }
.hexcrawl-menu-btn {
  display: inline-flex; align-items: center; gap: 0.15rem;
  background: var(--tier2-bg); color: var(--text); border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  padding: 0.25rem 0.45rem; font-size: 1.05rem; line-height: 1;
}
.hexcrawl-menu-btn.active { color: var(--accent); border-color: var(--accent); background: var(--bg-inset); }
.hexcrawl-menu-caret { font-size: 0.65rem; color: var(--text-dim); }
.hexcrawl-menu {
  position: absolute; top: 100%; left: 0; margin-top: 0.25rem; z-index: 30;
  display: flex; flex-direction: column; min-width: 12rem; max-height: 18rem; overflow-y: auto;
  background: var(--tier2-bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
  box-shadow: 0 4px 16px rgba(0,0,0,0.35); padding: var(--sp-1);
}
.hexcrawl-menu button { text-align: left; background: none; border: none; color: var(--text); padding: 0.35rem 0.5rem; border-radius: var(--radius-sm); font-size: 0.85rem; white-space: nowrap; }
.hexcrawl-menu button:hover { background: var(--bg-inset); }
.hexcrawl-menu button.selected { color: var(--accent); }
/* min-height 690px ~= 10 hex rows tall at HEX_SIZE=44 (domain/hexcrawls.js
   — row spacing 1.5*HEX_SIZE=66px, so 9 row-steps + one full hex height:
   66*9 + 2*44 = 682px, rounded up a bit) — direct follow-up request:
   "expand the hexcrawl vertical window to show 10 hexes, or extend to the
   bottom of the browser window, whichever fits best."

   This used to also flex-grow to genuinely reach the panel's bottom edge
   via a `.mc-drawer:has(.hexcrawl-viewport) { display:flex; ... }` rule
   that turned the fixed-position drawer into a flex column while Hexcrawl
   was open. Direct bug report, twice, with a screenshot confirming it
   both times (once with only the try/catch render-error guard in place,
   once after a first attempt at softening the :has() rule's own
   `overflow: hidden` to `overflow: auto`): "The hexcrawl does not display
   on mobile devices at all. There is the title 'Hexcrawl' and a close
   'x' and that is it" — the ENTIRE body, including the always-first head
   row ("+ New Map" etc.), went invisible on a real device (Firefox for
   Android) even with real maps already pinned. A real-bundle headless
   check already confirmed hexcrawl()'s own render always produces real
   content and never throws, and sw.js is already network-first (ruling
   out a stale cache) — so the remaining, only-reproducible-on-an-actual-
   mobile-browser culprit was this flex-column mechanism itself: SOME
   combination of a fixed-position ancestor's available height, a nested
   flex child's min-height:0, and that specific mobile engine's own
   layout quirks was collapsing the whole body to zero-height, and merely
   changing overflow:hidden to overflow:auto (the first attempt) wasn't
   enough to actually restore visibility, only to make a zero-height box
   scrollable-but-still-empty. Rather than guess at a third CSS patch
   blind (no mobile device or browser CSS engine available in this
   working environment to reproduce it directly), the :has()-driven
   flex-fill mechanism is removed outright — this plain width-based
   aspect-ratio box (padding-top:60%, floored at the 10-row min-height
   above) is simple, has no dependency on an ancestor's flex-height
   arithmetic, and is exactly the fallback every browser was ALREADY
   using safely whenever :has() wasn't supported at all. Trades away
   "reaches the exact bottom of a tall window" for "reliably visible on
   every device" — the right tradeoff after two blank-panel reports. */
.hexcrawl-viewport {
  position: relative; width: 100%; padding-top: 60%; min-height: 690px; overflow: hidden;
  background-color: var(--bg-inset);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  /* Direct bug report, persisting across two prior CSS attempts (the
     :has()-driven flex-fill mechanism, then its own removal) — confirmed
     via a real-bundle headless run that JS rendering is correct and the
     bundler isn't the problem either, so the remaining, untestable-in-
     this-environment suspect is `touch-action` never being set anywhere
     in this stylesheet, for ANY pan/zoom surface (Hexcrawl, Battlemap,
     Graph). Without it, a real mobile browser's DEFAULT behavior for a
     touch-drag over this element is to hand it to the page's own native
     scroll/zoom machinery (the nearest scrollable ancestor is
     .mc-drawer-body, overflow:auto) instead of this app's own custom
     camera-drag handling (onMouseDown/onMouseMove, which only
     preventDefault()s on the SYNTHETIC mouse event a touch generates,
     not the real touch event itself, too late to stop the browser's own
     gesture recognition from already engaging). Firefox for Android
     specifically has known compositor (APZ) desync issues with nested
     overflow:hidden-inside-overflow:auto regions exactly like this one,
     where a touch-scroll gesture can leave the inner region's own
     rendered layer blank until an unrelated repaint — real chrome
     (title/✕, outside this scrolling region) stays visible, only the
     scrollable body's own content disappears, which matches the report
     exactly. `touch-action: none` tells the browser outright "don't
     apply your own touch gesture handling here, this element manages
     its own" — the standard, well-established fix for custom pan/zoom
     surfaces nested inside a scrollable page. */
  touch-action: none;
}
.hexcrawl-viewport.placing { cursor: crosshair; }
.hexcrawl-world { position: absolute; inset: 0; transform-origin: 0 0; cursor: grab; }
.hexcrawl-world:active { cursor: grabbing; }
/* --hex-default-fill is the solid color an unpainted hex's own SVG
   polygon uses (drawers/index.js reads it via fill="var(--hex-default-
   fill)" when a hex has no Geography set) — same neutral cell tone the
   old plain-div background used to be. */
:root { --hex-default-fill: #1a2438; }
/* The hex SHAPE (solid Geography fill + border lines on every edge,
   direct follow-up request) is a real SVG <polygon> per hex, not a CSS
   clip-path'd div — a clip-path border is unreliable at shared edges
   between neighboring hexes (see drawers/index.js's own comment on
   HEX_SVG_POINTS for why), where an SVG polygon's stroke always draws
   cleanly regardless of neighboring shapes. pointer-events:none so every
   click passes through to .hex-cell below, which is what's actually
   clickable (and stays clip-path'd, purely for correct hex-shaped hit-
   testing — never painted with a visible background of its own). */
.hex-shape { position: absolute; pointer-events: none; overflow: visible; }
.hex-shape-outline { stroke: var(--border-strong); stroke-width: 1.5; }
.hex-shape-outline.active { stroke: var(--accent); stroke-width: 2.5; }
.hex-cell {
  position: absolute; background: transparent;
  display: flex; align-items: center; justify-content: center;
}
/* Direct follow-up request: "increase the geography icon to fill the hex
   so it is visible when the location icon is selected" — sized inline
   (drawers/index.js, relative to HEX_SIZE) rather than a fixed rem value
   here, so it scales if the hex's own size ever changes; inset:0 + flex
   centering guarantees it lands dead-center regardless of .hex-cell's own
   flex/absolute-positioning quirks (an abs-positioned flex child with no
   explicit offsets has a spec-ambiguous static position otherwise). */
.hex-geo-glyph {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  opacity: 0.7; pointer-events: none; text-shadow: 0 1px 4px rgba(0,0,0,0.7);
}
/* The pen-and-ink art variant (hexGeoGlyphHtml) is a real <img>, not text —
   inset:0 on a sized replaced element pins it top-left instead of
   centering it, so it overrides the emoji rule's layout with explicit
   centering via transform instead. */
.hex-geo-glyph-img {
  inset: auto; left: 50%; top: 50%; transform: translate(-50%, -50%);
  display: block; opacity: 0.85; object-fit: contain;
}
/* The inline-SVG variant (hexGeoGlyphHtml's `svgArt` branch — a biome
   with no source-crop art, e.g. Ocean's waves) needs the same centering
   treatment as the <img> variant just above. */
.hex-geo-glyph-svg {
  inset: auto; left: 50%; top: 50%; transform: translate(-50%, -50%);
  display: block; opacity: 0.85;
}
/* Direct follow-up request: "the location icon... have a white background
   with thin black border to stand out from the location background" —
   deliberately literal black border (kept) rather than this app's own
   dark-theme tokens, so the icon reads clearly against ANY Geography fill
   color behind it, not just the app's usual dark surfaces. A LATER direct
   follow-up ("fill the thumbnail icon in the hexmap hex so there is no
   white background") dropped the white FILL specifically — a real photo's
   object-fit:cover already reaches every edge, so that white only ever
   showed through a photo with its own transparent regions (a token-style
   PNG); transparent lets the Geography color underneath show through
   there instead. Applies once a Location is actually linked
   (.hex-center-filled) — the EMPTY "+" state is a separate,
   invisible-until-hover control (below), so it doesn't cover the (now
   much bigger) Geography glyph while unused. */
.hex-center {
  position: relative; width: 1.8rem; height: 1.8rem; border-radius: 50%; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  font-size: 1rem;
}
.hex-center-filled { background: transparent; border: 1px solid #000; box-shadow: 0 1px 3px rgba(0,0,0,0.5); }
/* Direct follow-up request: "make the + and circle in the hex invisible
   so that the geography icon is visible" — fully transparent at rest
   (no fill/border/shadow/"+" glyph), same "invisible but still a real,
   full-size clickable target, revealed on hover for discoverability"
   posture as .hex-vertex just below. */
.hex-center-empty { background: transparent; border: none; box-shadow: none; opacity: 0; color: #000; }
.hex-center-empty:hover { opacity: 1; background: #fff; border: 1px solid #000; box-shadow: 0 1px 3px rgba(0,0,0,0.5); }
/* display:block (direct follow-up clarification: "the location thumbnail
   image must be increased as needed to fill the entire circle with no
   background showing... [transparency] was only to explain how the
   thumbnail image needed to fill the location circle") fixed a plain
   <img>'s inline-baseline phantom gap. That alone wasn't enough — direct
   follow-up report + attached screenshot showed a ring of background
   still visible around the artwork: the uploaded image itself often has
   its own baked-in empty/transparent margin around a circular or oval
   vignette (common for token-style portrait art), which object-fit:cover
   alone can't crop away since the image's own overall aspect ratio
   already roughly matches the circle. transform:scale(1.4) forces a
   further, deliberate over-crop around the image's own center — trading a
   bit of its outer content for a guarantee that the circle is always
   fully covered by real image pixels, regardless of how much padding any
   given upload happens to carry. */
.hex-center-img { display: block; width: 100%; height: 100%; border-radius: 50%; object-fit: cover; transform: scale(1.4); }
.hex-center-fallback { font-weight: bold; color: #000; }
/* Direct follow-up request: "update the image of the location icon to fill
   the circle to remove the need for the whitespace" — sized to the same
   1.8rem diameter as .hex-center itself (minus a hair so the glyph's own
   internal padding never clips against the circle's overflow:hidden edge)
   instead of the much smaller original 1.1rem, so it reads as a filled
   icon the same way .hex-center-img's object-fit:cover already does for a
   real uploaded photo, rather than a small glyph floating in a white disc. */
.hex-center-type-icon { font-size: 1.7rem; line-height: 1; }
/* Direct follow-up request: "make the circles in the corners not visible"
   — invisible at rest (no fill/border/glyph chrome), still a real
   clickable hit target at full size. LATER direct follow-up request: "The
   corner circles for encounter/conflicts should not be visible during a
   mouseover without being populated with an icon for a conflict" — the
   earlier faint hover hint for an EMPTY corner is removed entirely (an
   empty corner was never a manual placement target anyway — see
   data-hex-vertex's own doc comment in shell.js — so a hover hint there
   only suggested a click would do something, when it wouldn't).
   .hex-vertex-filled (a placed Encounter) stays fully visible regardless
   of hover, unaffected by this. */
.hex-vertex {
  position: absolute; transform: translate(-50%, -50%);
  width: 1.1rem; height: 1.1rem; min-width: 0; border-radius: 50%; padding: 0;
  display: flex; align-items: center; justify-content: center; font-size: 0.7rem;
  background: transparent; border: none; opacity: 0;
}
.hex-vertex-filled { opacity: 1; background: var(--tier3-bg); border: 1px solid var(--border-strong); }
.hex-vertex-filled:hover { opacity: 1; }
.hex-detail-panel { margin-top: var(--sp-2); }
.hex-detail-threat-row { display: flex; align-items: center; gap: var(--sp-2); margin-top: var(--sp-1); }
/* Direct follow-up request (river feature): a hex's 6 edge midpoints —
   invisible at rest, same "real full-size hit target, hover-revealed"
   posture as .hex-vertex, just smaller and elongated along the border
   rather than round, so it doesn't read as a 7th corner. Only meaningful
   while the River tool is armed (shell.js no-ops an edge click
   otherwise), so it stays fully transparent even on hover unless armed —
   .hexcrawl-viewport.placing (already toggled while ANY tool is armed)
   is what turns hover feedback on. */
.hex-edge {
  position: absolute; transform: translate(-50%, -50%);
  width: 0.9rem; height: 0.5rem; min-width: 0; border-radius: 999px; padding: 0;
  background: transparent; border: none; opacity: 0; cursor: default;
}
.hexcrawl-viewport.placing .hex-edge:hover { opacity: 0.8; background: #2d6a8f; cursor: pointer; }
.hex-edge-pending { opacity: 1; background: #2d6a8f; box-shadow: 0 0 0 2px #fff; }
/* Lake/river overlays (direct follow-up request) — rendered inside
   .hex-shape's own SVG, same coordinate space as the Geography polygon,
   so they sit above its solid fill but (DOM order) below the actual
   .hex-cell click target/center icon. pointer-events:none throughout —
   same reasoning as .hex-shape-outline, clicks must reach the real hit-
   test targets (.hex-edge/.hex-cell) underneath/alongside, not this
   purely-visual layer. */
.hex-lake { fill: #1d4e6e; fill-opacity: 0.88; stroke: #0a0a0a; stroke-width: 1.2; pointer-events: none; }
.hex-river { fill: none; stroke: #2d6a8f; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; }
.hex-river-mouth { fill: #2d6a8f; pointer-events: none; }
/* Direct follow-up request: "when the river icon is selected, any river
   lines already placed should be selectable and clicking a line should
   prompt to remove it." This whole extra svg only renders at all while
   the River tool is armed (hexcrawlGrid's own riverToolArmed check), and
   sits AFTER .hex-cell in DOM order (same "must paint on top to actually
   receive the click" fix .hex-edge itself needed — see the comment on
   riverToolArmed's call site) — pointer-events:none on the svg root
   itself so it doesn't block clicks on .hex-cell/.hex-center/.hex-edge
   wherever a river ISN'T, re-enabled per-path via stroke-only hit-testing
   on a much wider invisible stroke than the 3px visible line, so the
   actual click target is generous without changing how the river looks. */
.hex-river-hit-layer { pointer-events: none; }
.hex-river-hit { fill: none; stroke: transparent; stroke-width: 14; stroke-linecap: round; pointer-events: stroke; cursor: pointer; }
.hex-river-hit:hover { stroke: rgba(255,255,255,0.25); }
/* Direct follow-up request: "Create a road overlay that duplicates the
   river functionality using a brown line for a road that connects to
   locations, not lakes or oceans" — same layering/hit-test posture as
   .hex-river/.hex-river-hit(-layer) just above, brown instead of blue, no
   .hex-river-mouth equivalent (roads never widen at an ocean, they only
   ever end at a border or a hex's own Location). */
.hex-road { fill: none; stroke: #8a5a2b; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; }
.hex-road-hit-layer { pointer-events: none; }
.hex-road-hit { fill: none; stroke: transparent; stroke-width: 14; stroke-linecap: round; pointer-events: stroke; cursor: pointer; }
.hex-road-hit:hover { stroke: rgba(255,255,255,0.25); }

/* Dungeon Crawl — a FIXED 6x6 square-tile grid (unlike Hexcrawl's own
   unbounded pan/zoom hex plane above), so the same plain CSS grid + flat
   cells approach as .wt-grid just below is enough; no camera needed. */
.dungeoncrawl-grid {
  display: grid; grid-template-columns: repeat(6, 1fr);
  gap: 3px; max-width: 480px; margin: 0 auto;
}
.dungeoncrawl-cell {
  position: relative; aspect-ratio: 1; min-width: 44px; min-height: 44px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg-inset); padding: 0; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
/* Direct follow-up request: "allow an unused cell in dungeon crawl to be
   selected from a double click or long press... to add another tile" —
   cursor:pointer now that an empty cell is a genuine (if secondary,
   double-click-gated) interaction target, same as .filled below. */
.dungeoncrawl-cell.empty { border-style: dashed; opacity: 0.35; cursor: pointer; }
.dungeoncrawl-cell.filled { cursor: pointer; }
.dungeoncrawl-cell.filled.sel { outline: 2px solid var(--accent); outline-offset: -2px; }
.dungeoncrawl-cell.append {
  color: var(--accent); font-size: 1.3rem; background: var(--tier2-bg);
  border-style: dashed; border-color: var(--accent); cursor: pointer;
}
.dungeoncrawl-cell.pending { outline: 2px solid var(--accent); outline-offset: -2px; }
.dungeoncrawl-tile-art, .dungeoncrawl-tile-art img, .dungeoncrawl-tile-art svg {
  width: 100%; height: 100%; display: block; object-fit: cover;
}
.dungeoncrawl-tile-art { position: absolute; inset: 0; transition: transform 0.15s ease; }
/* Direct follow-up request: "allow user to touch a corner and have the
   tile rotate 90 degrees ... click on the center of the tile to commit" —
   4 transparent corner zones (each a quarter of the cell) plus a center
   commit button, all absolutely positioned over the pending Tile's own
   art. A reroll/cancel pair rides along at the very top of the cell
   (small, out of the way of the 4 corners) since a GM may want a
   different random Tile entirely, not just a different rotation of the
   one they got. */
.dc-corner {
  position: absolute; width: 50%; height: 50%; background: transparent;
  border: none; padding: 0; cursor: pointer; z-index: 2;
}
.dc-corner-tl { top: 0; left: 0; }
.dc-corner-tr { top: 0; right: 0; }
.dc-corner-bl { bottom: 0; left: 0; }
.dc-corner-br { bottom: 0; right: 0; }
.dc-commit {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 38%; height: 38%; border-radius: 50%; z-index: 3;
  background: var(--accent); color: var(--bg); border: 2px solid var(--bg);
  font-size: 1.1rem; cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.dc-reroll, .dc-cancel {
  position: absolute; top: 2px; z-index: 3; width: 20px; height: 20px; border-radius: 50%;
  background: var(--bg-raised); border: 1px solid var(--border); color: var(--text);
  font-size: 0.7rem; line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.dc-reroll { right: 24px; }
.dc-cancel { right: 2px; }
.dungeoncrawl-tile-detail textarea { width: 100%; }
/* Direct follow-up request: "Show the active tile as full width of panel
   under the title below the map when you click a tile. allow zoom
   capability to read markings like secret doors and color coded
   encounter zones." .dc-detail-art's own width is set inline per-render
   as a percentage of .dc-detail-viewport's content box (100% = no zoom,
   200% = 2x, ...) — overflow:auto on the viewport is what then makes the
   zoomed-in art scrollable/pannable (mouse wheel, scrollbar, or a touch
   swipe all work for free, no extra JS needed), and aspect-ratio:1 on the
   art itself keeps it square at any zoom level without needing to also
   compute an explicit pixel height to match. */
.dc-detail-viewport {
  width: 100%; max-height: 60vh; overflow: auto;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg-inset); margin-bottom: var(--sp-2);
}
.dc-detail-art { position: relative; aspect-ratio: 1; margin: 0 auto; }
.dc-detail-art-inner { width: 100%; height: 100%; display: block; }
.dc-detail-art-inner svg { width: 100%; height: 100%; display: block; }
.dc-detail-zoom-controls { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-2); }
/* Direct follow-up request: "have an invisible 1" grid overlay to each
   tile that will allow graphic objects to snap to the grid... doors,
   secret doors, porcullis, stairs up or down, pillars, tree trunks,
   chests, campfires... This would also be seen on the enlarged image in
   the lower section below the map." One shared rule for BOTH the small
   grid cell's own dressing icons and the big Tile Detail view's — left/top
   are set inline per-icon as percentages (data/dungeonCrawlDressing.js's
   gx/gy out of DUNGEON_TILE_GRID_SIZE), so the same markup reads correctly
   at either size with no separate "zoomed" variant. `%` font-size keeps
   each glyph proportioned to whatever box it's actually rendered in
   (tiny at the small grid, bigger once zoomed in the detail view) rather
   than a fixed px size that would look oversized on the small grid or
   undersized once zoomed in. */
.dc-dressing-icon {
  position: absolute; transform: translate(-50%, -50%);
  width: 50%; height: 50%; max-width: 22px; max-height: 22px;
  font-size: 65%; line-height: 1; padding: 0; border: none; background: transparent;
  cursor: pointer; display: flex; align-items: center; justify-content: center;
  filter: drop-shadow(0 0 2px rgba(0,0,0,0.9));
}
.dc-detail-viewport .dc-dressing-icon { max-width: none; max-height: none; width: 7%; height: 7%; font-size: 1.4rem; }
.dc-dressing-icon.secret { outline: 1px dashed var(--accent); outline-offset: 2px; border-radius: 50%; }
.dc-dressing-icon.secret::after {
  content: '?'; position: absolute; top: -0.4em; right: -0.4em;
  font-size: 0.6em; font-weight: bold; color: var(--accent);
  background: var(--bg); border-radius: 50%; width: 1.2em; height: 1.2em;
  display: flex; align-items: center; justify-content: center; line-height: 1;
}
/* Direct follow-up request: "Allow selection of multiple adjacent tiles
   and replace them with a larger tile that gives a bigger room." A
   selected-for-merge Tile gets a dashed accent outline (distinct from the
   solid one .sel uses for the Tile Detail panel's own "currently open"
   state, so the two never read as the same thing if a GM has a Tile
   selected AND is mid-merge-selection). The merged Room itself
   (.dc-room, rendered at its own top-left/anchor cell only, CSS Grid
   spanning the rest of its footprint — see drawers/index.js's
   dungeoncrawlCells) otherwise looks like any other filled cell. */
.dungeoncrawl-cell.filled.merge-sel { outline: 2px dashed var(--accent); outline-offset: -2px; }
/* Direct follow-up request: "an icon showing Merge for merging the
   tiles" / Merge mode's own active highlight — the icon-only button
   lives in the SAME .battlemap-toolbar Battlemap/Hexcrawl already use,
   so this is scoped there rather than a bare, too-broad .icon-btn.active
   rule (mirrors .wt-sector-detail/.turn-step-row-actions' own identically
   narrow-scoped .icon-btn.active rules elsewhere in this file). */
.battlemap-toolbar .icon-btn.active { color: var(--accent-strong); background: var(--tier2-bg); }
/* Direct follow-up request: "The floor levels dropdown should be an icon
   of several stacked rectangles representing levels or floors. the
   dropdown would appear when clicking the icon." The real <select>
   (data-dungeoncrawl-floor-select, shell.js's own onChange handler,
   unchanged) sits fully transparent on top of the decorative icon glyph
   — clicking/tapping anywhere in the icon hits the invisible select and
   opens its real native dropdown (every OS's own accessible picker, no
   bespoke popover component needed). */
.dc-floor-icon { position: relative; display: inline-flex; align-items: center; justify-content: center; }
.dc-floor-select-native { position: absolute; inset: 0; opacity: 0; width: 100%; cursor: pointer; }
/* Direct follow-up request: "Under the menu and above the map should be
   the list of linked location chips" — Link Location's own row, same
   entity-chip-row shape used everywhere else a handful of linked
   entities need to show as clickable chips. */
.dc-location-row { margin: var(--sp-2) 0; }
/* Direct follow-up request: "Below the enlarged map tile picture needs to
   be a key explaining the color and number coding used in the tiles to
   represent encounter options per FlexTale." */
.dc-content-legend { margin: var(--sp-2) 0; }
.dc-content-legend-rows { display: flex; flex-direction: column; gap: 0.3rem; margin-top: var(--sp-1); }
.dc-content-legend-row { display: flex; align-items: center; gap: var(--sp-2); }
.dc-content-legend-swatch {
  flex: 0 0 auto; width: 1rem; height: 1rem; border-radius: 3px; border: 1px solid var(--border-strong);
}
.dc-content-legend-secret {
  display: flex; align-items: center; justify-content: center; background: var(--bg-inset);
  font-size: 0.65rem; font-weight: 700; color: var(--text-dim);
}
.dc-content-legend-label { flex: 0 0 auto; font-weight: 600; min-width: 5.5rem; }

/* World Tracker (requirements/PLANETFALL_world_tracker.md) — a fixed,
   small integer grid, so plain CSS grid + flat buttons is enough; no
   canvas/pan-zoom camera needed the way battlemap's variable-size freeform
   canvas above requires one. --wt-resource/--wt-hazard (0-6, set inline
   per cell) drive a green/red intensity wash via color-mix rather than six
   hardcoded level classes — spec 4: resource as base cell shading, hazard
   kept to its own corner badge so the two scales never visually collide. */
.wt-grid {
  display: grid; grid-template-columns: repeat(var(--wt-grid-size, 6), 1fr);
  gap: 2px; max-width: 420px; margin: 0 auto;
}
.wt-sector {
  position: relative; aspect-ratio: 1; min-width: 44px; min-height: 44px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: color-mix(in srgb, #4ade80 calc(var(--wt-resource, 0) * 10%), var(--bg-inset));
  display: flex; align-items: center; justify-content: center; padding: 0;
}
.wt-sector-unexplored { background: var(--bg-inset); filter: grayscale(1) brightness(0.7); }
.wt-sector.active { outline: 2px solid var(--accent); outline-offset: -2px; }
.wt-corner {
  position: absolute; font-size: 0.55rem; line-height: 1; color: var(--text-dim);
  padding: 1px 2px; pointer-events: none;
}
.wt-corner-top_left { top: 1px; left: 2px; }
.wt-corner-top_right { top: 1px; right: 2px; }
/* Hazard's own "H#" corner label defaults here now (direct follow-up
   request — was top_right); the danger/bold treatment moves with it. */
.wt-corner-bottom_left { bottom: 1px; left: 2px; color: var(--danger); font-weight: bold; }
.wt-corner-bottom_right { bottom: 1px; right: 2px; }
.wt-sector-icon { font-size: 1rem; opacity: 0.7; }
.wt-corner-row { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-1); }
.wt-corner-row > span:first-child { min-width: 6rem; text-transform: capitalize; }
.wt-migrate-picker { display: flex; flex-wrap: wrap; gap: var(--sp-1); align-items: center; margin-top: var(--sp-2); }
.wt-feature-card .doc-card-head { align-items: center; }
/* Direct follow-up request: a Feature's own label opens/closes its
   Description textarea (worldTrackerFeatureRow, drawers/index.js) — reset
   button chrome so it reads as plain text with just a pointer cursor,
   inheriting whatever font/color .doc-card-head already gives it rather
   than picking up a button's own default styling. */
.wt-feature-label-toggle {
  background: none; border: none; padding: 0; margin: 0; cursor: pointer; text-align: left;
  font: inherit; color: inherit;
}
.wt-feature-label-toggle:hover { color: var(--accent); }
/* Sector detail's Features "+" add buttons, two per row (direct follow-up
   request — was one per row in a plain .btn-col). */
.wt-feature-buttons { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--sp-2); }
/* The Investigation Site icon toggle's "on" state — same accent treatment
   .profile-list .btn.active already uses for a selected pick, reused here
   since this app has no other "toggled icon button" precedent to match. */
.wt-sector-detail .icon-btn.active { color: var(--accent); background: var(--tier2-bg); }

/* Toast */
.toast {
  position: fixed; bottom: 1.2rem; left: 50%; transform: translateX(-50%) translateY(1rem);
  background: var(--bg-raised); color: var(--text);
  border: 1px solid var(--accent); border-radius: var(--radius);
  padding: 0.6rem 1rem; font-size: 0.82rem; box-shadow: var(--shadow);
  opacity: 0; transition: opacity 0.2s, transform 0.2s; z-index: 40; max-width: 80vw;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }

@media (max-width: 1023px) {
  /* field-row/field-row-3col are auto-fit now (see their rules above) and
     reflow on their own; field-row-2col is currently unused but kept
     forced here since it predates auto-fit and nothing depends on it
     changing. */
  .field-row-2col { grid-template-columns: 1fr; }
}

/* Threads (progress clocks) */
.threads { margin-top: var(--sp-4); border-top: 1px solid var(--border); padding-top: var(--sp-3); }
.threads-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--sp-2); }
.threads-head h3 { margin: 0; font-size: 0.85rem; color: var(--accent); }
.threads-head-actions { display: inline-flex; gap: var(--sp-1); }
.thread-row { display: grid; grid-template-columns: 1fr auto auto auto auto; align-items: center; gap: var(--sp-2); padding: 0.35rem 0; }
/* Expedition trackers (docs/adr/0009): a compact 3-slider block below an
   expedition-tagged Thread's own grid row — spans the full row width via
   grid-column, since .thread-row's 5 explicit columns have no room to fit
   it inline next to the clock. */
.expedition-dials { grid-column: 1 / -1; display: flex; gap: var(--sp-3); flex-wrap: wrap; padding: 0.2rem 0 0.4rem; }
.expedition-dials .field-label.sm { margin-top: 0; font-size: 0.62rem; min-width: 6rem; }
.thread-row.done .thread-name { text-decoration: line-through; color: var(--text-dim); }
.thread-name { font-size: 0.88rem; }
.thread-clock { display: inline-flex; gap: 3px; }
.pip { width: 0.7rem; height: 0.7rem; border-radius: 50%; border: 1px solid var(--border-strong); background: transparent; }
.pip.on { background: var(--accent-strong); border-color: var(--accent-strong); }
.thread-actions { display: inline-flex; gap: 2px; }
.thread-status-select, .thread-priority-select { font-size: 0.72rem; padding: 1px 4px; border-radius: 4px; border: 1px solid var(--border); background: var(--bg-inset); color: var(--text-dim); }
/* World State Flags reuse .thread-row's grid; the notes field is the one
   free-text cell in that row, so it gets its own narrow input style
   rather than a full field-label block. */
.thread-name-input { font-size: 0.78rem; width: 8rem; padding: 2px 5px; border-radius: 4px; border: 1px solid var(--border); background: var(--bg-inset); color: var(--text); }
/* A quiet/settled stage dims the row without the strikethrough .done reserves for a full clock. */
.thread-row.thread-status-dormant .thread-name, .thread-row.thread-status-archived .thread-name { color: var(--text-dim); }
.thread-row.thread-priority-high .thread-status-select { border-color: var(--accent-strong); }

/* Entity Inspector (Phase 3A) */
.entity-list { display: flex; flex-direction: column; gap: 2px; max-height: 32vh; overflow: auto; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--sp-2); }
.entity-list-row { display: flex; align-items: center; gap: var(--sp-2); text-align: left; background: none; border: none; color: var(--text); padding: 0.35rem 0.4rem; border-radius: var(--radius-sm); }
.entity-list-row:hover { background: var(--tier2-bg); }
.entity-list-row.sel { background: var(--tier1-bg); outline: 1px solid var(--accent); }
/* The whole row is draggable="true" (2026-07-04, reverted from a
   grip-only restriction — a real mouse's tiny jitter between mousedown/
   mouseup can occasionally get read as a drag instead of a click on a
   draggable row; explicitly accepted in exchange for "grab it anywhere"
   being more discoverable than a small icon at the left edge). The ⠿ is
   now a purely decorative affordance icon, not a separate draggable target.
   cursor: grab lives on the row itself for the same reason. */
.entity-list-row { cursor: grab; }
.entity-list-row:active { cursor: grabbing; }
.entity-drag-handle {
  color: var(--text-dim); flex-shrink: 0;
  font-size: 1rem; line-height: 1; padding: 0.15rem 0.3rem; margin-left: -0.15rem;
}
.entity-type-tag { font-size: 0.6rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent); border: 1px solid var(--border); border-radius: 999px; padding: 0.05rem 0.4rem; }
.entity-list-name { flex: 1; font-size: 0.85rem; }
.entity-inspector { border-top: 1px solid var(--border); padding-top: var(--sp-3); }
/* Entity Editor's own name/type header — direct follow-up request: "the
   tab bar still scrolls while the entity name field is sticky. It should
   be the other way around. Only the tab bar should be held in view while
   the rest can scroll" — this used to be its own "freeze pane" (position:
   sticky, same top:0 as .drawer-tabs above it), which meant the two
   competed for the same stuck position once a second drawer tab was
   pinned open: this element's higher z-index/opaque background painted
   over .drawer-tabs, hiding the actual tab strip rather than the drawer-
   tabs staying visibly on top as intended. No longer sticky — a plain
   flow header, just like the rest of the entity's own content, so
   .drawer-tabs is the only thing that stays put while scrolling. */
.inspector-head {
  display: flex; align-items: center; gap: var(--sp-2);
  margin-bottom: var(--sp-3); padding-bottom: var(--sp-2);
  border-bottom: 1px solid var(--border);
}
.inspector-name { flex: 1; font-size: 1rem; font-weight: 600; }

/* Entity thumbnail (Phase 11, docs/adr/0021-gallery.md) — left-aligned
   beside Overview (direct follow-up request: "move overview to the same
   row as thumbnail" — Type/Tags moved out to their own row further down,
   below the field-divider). */
.inspector-photo-row { display: flex; gap: var(--sp-3); align-items: flex-start; margin-top: var(--sp-3); }
/* 40% of the row's width (direct follow-up request: "increase the
   thumbnail size by 30% in the entity editor" — 3.5rem -> 4.55rem, kept
   proportionate to the explicit circle size below, same ratio as the
   earlier 40%-width/4.5rem pairing) whether or not a photo is assigned
   yet — an unassigned entity still reserves the same space (via
   .inspector-photo-empty's dashed placeholder box below) so the layout
   doesn't jump once a photo is added. */
.inspector-photo { flex: 0 0 40%; max-width: 40%; box-sizing: border-box; display: flex; flex-direction: column; align-items: center; gap: var(--sp-1); }
.inspector-photo-fields { flex: 1 1 auto; min-width: 0; }
/* Type and Tags share one row instead of stacking (direct follow-up
   request), now its own standalone row below the photo/Overview row and
   the field-divider — not confined to .inspector-photo-fields' own width
   any more, so it spans the Entity Editor's full width. Type keeps its
   natural (shrink-to-fit) width; Tags' head row takes the rest.
   align-items: flex-end lines up Type's <select> (below its own label
   text) with Tags' single-line label+input row on their shared bottom
   edge. The chosen-tags chip list (tagEditorList) still renders as its
   own full-width row below this one, collapsible via the Tags label — see
   .tag-editor-toggle. */
.inspector-type-tags-row { display: flex; align-items: flex-end; gap: var(--sp-3); }
.inspector-type-field { flex: 0 0 auto; min-width: 0; }
/* 3.5rem -> 4.55rem (direct follow-up request: "increase the thumbnail
   size by 30%"). */
.inspector-photo .gallery-thumb-circle { width: 4.55rem; height: 4.55rem; }
/* Direct follow-up request: clicking the thumbnail shows the full-size
   image (see image-lightbox-overlay below). Bare-button reset so the
   circular photo inside doesn't pick up default button chrome. */
/* overflow:hidden turns this button into a fixed clip container — a
   direct follow-up report + screenshot ("the fill is still not
   complete") showed display:block/object-fit:cover alone wasn't enough:
   a source portrait/token-style image very often has its own baked-in
   empty margin around a circular or oval vignette, close enough to this
   box's own aspect ratio that cover has little left to crop. The img
   below is transform:scale(1.4)'d to force a further, deliberate
   over-crop around ITS OWN center — CSS transform never affects layout
   size, only paint, so this button still sizes itself to the img's
   normal (unscaled) dimensions and this overflow:hidden clips the
   now-larger painted image back down to fit it exactly, same "fixed-size
   parent clips a transform-scaled child" technique .hex-center/
   .hex-center-img already use for the identical problem on the Hexcrawl
   map. Scoped to just the .gallery-thumb-circle used as an entity
   portrait (inside this button) — the Gallery library's own card view
   shows a plain, unscaled thumbnail deliberately, since that view is
   for confirming exactly what was uploaded. */
.gallery-thumb-circle-btn { background: none; border: none; padding: 0; cursor: zoom-in; border-radius: 50%; overflow: hidden; display: inline-block; }
.gallery-thumb-circle-btn .gallery-thumb-circle { transform: scale(1.4); }
.gallery-thumb-circle-btn:hover .gallery-thumb-circle { border-color: var(--accent); }
/* Direct follow-up request: "move the edit icon to the 60 degree or 5pm
   position in the bottom right of the thumbnail" — back to an overlay
   badge (an earlier request had moved it off the thumbnail entirely; this
   supersedes that), but at the precise 5-o'clock point on the circle
   itself rather than the generic bottom-right corner the very first
   overlay version used. Clock position -> percentage offset from center:
   angle clockwise from 12 = hour*30° = 150° for 5 o'clock; offset from
   center = radius*(sin150°, -cos150°) = radius*(0.5, 0.866); at radius =
   50% of the box (a diameter-100% circle), that's +25%/+43.3% from
   center (50%,50%), i.e. left:75%, top:93.3%. */
.inspector-photo-edit-overlay { position: relative; display: inline-flex; }
.photo-edit-btn {
  position: absolute; left: 75%; top: 93.3%; transform: translate(-50%, -50%);
  width: 1.3rem; height: 1.3rem; display: flex; align-items: center; justify-content: center; padding: 0;
  background: #fff; color: #000; border: 1px solid #000; border-radius: 50%;
  box-shadow: 0 1px 3px rgba(0,0,0,0.5); font-size: 0.7rem; cursor: pointer;
}
.photo-edit-btn:hover { background: var(--accent); border-color: var(--accent); color: #04121f; }
.inspector-photo-empty {
  display: flex; align-items: center; justify-content: center; width: 100%;
  aspect-ratio: 1 / 1; border: 1px dashed var(--border); border-radius: var(--radius-sm);
  color: var(--text-dim); font-size: 0.8rem; cursor: pointer; text-align: center; padding: var(--sp-2); box-sizing: border-box;
  /* Bare-button reset (direct follow-up request's Starship "+ Select
     Starship" placeholder is a real <button>, not the upload <label> this
     class was originally written for) so neither picks up default button
     chrome. */
  background: none; font: inherit;
}
.inspector-photo-empty:hover { border-color: var(--accent); color: var(--accent); }
/* Starship tab's thumbnail (direct follow-up request) reuses .gallery-
   thumb-circle for the assigned entity's real photo; this modifier is the
   initial-letter fallback when that entity has no thumbnailId yet — same
   recipe .actor-thumb-photo-empty already uses for the same purpose. */
.gallery-thumb-circle-empty { display: flex; align-items: center; justify-content: center; font-weight: 600; color: var(--text-dim); }
.field-row2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
.rel-block { margin-top: var(--sp-3); border-top: 1px solid var(--border); padding-top: var(--sp-3); }
.rel-block h4 { margin: 0 0 var(--sp-2); font-size: 0.8rem; color: var(--accent); }
.rel-chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.rel-chip { display: inline-flex; align-items: center; gap: 4px; background: var(--tier2-bg); border: 1px solid var(--border); border-radius: 999px; padding: 0.2rem 0.5rem; font-size: 0.78rem; flex-wrap: wrap; }
.rel-chip-name { background: none; border: none; padding: 0; font: inherit; color: var(--accent); cursor: pointer; text-decoration: underline dotted; text-underline-offset: 2px; }
.rel-chip-name:hover { color: var(--accent-strong); }
.rel-chip.rel-flagged { border-color: var(--warn); }
.rel-flag { color: var(--warn); }
.rel-type-select { max-width: 6.5rem; padding: 0.15rem 0.3rem; font-size: 0.72rem; background: var(--bg-inset); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text-dim); }
.rel-strength-input { width: 2.6rem; padding: 0.15rem 0.25rem; font-size: 0.72rem; background: var(--bg-inset); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text-dim); text-align: center; }
/* Read-only Bond progress (entities.js's ensureBondTrack) — replaces the
   generic strength input for exactly the bond that has a matching
   Character Sheet track, so there's one editable number, not two. */
.rel-bond-value { font-size: 0.72rem; font-weight: 600; color: var(--accent); white-space: nowrap; }
.rel-bond-value small { font-weight: 400; color: var(--text-dim); }
.rel-add { display: grid; grid-template-columns: 1fr auto 1fr auto; gap: var(--sp-2); align-items: center; }
/* Match the relationship type/target dropdowns to .rel-label-input's font
   size (direct request) — the generic textarea/select/input rule further
   up defaults every <select> to 0.9rem, noticeably larger than this row's
   own note field once it's the only one left unstyled. */
.rel-add select { font-size: 0.72rem; }
/* "Find entity to link" sits on its own full-width row below the 4-column
   type/target/label/Link form above it, rather than awkwardly starting a
   5th implicit column. */
.rel-add [data-rel-picker-open] { grid-column: 1 / -1; }
/* A smaller floating window over the top half of the Entity Editor panel
   (direct request, phone-focused — Cast can't be open side-by-side with
   the editor at compact widths to drag an entity in the way desktop can),
   not an inline block like .catalog-picker above reuses its row/item
   classes from. Positioned `fixed` — .mc-drawer always carries a
   `transform` (its open/closed slide animation), which per spec makes it
   the containing block for any `position: fixed` descendant, so this
   anchors to THAT panel's own box instead of the full viewport with no
   extra positioning math needed; it also stays pinned over the top half
   regardless of how far .mc-drawer's own content has been scrolled,
   since fixed positioning doesn't participate in that scroll. z-index
   only needs to clear its own drawer's in-flow content (a local stacking
   context, since .mc-drawer sets its own z-index) — 20/25 elsewhere in
   this file are unrelated, wider-scoped overlays. */
.rel-picker {
  position: fixed; top: 0; left: 0; right: 0; height: 50%;
  z-index: 5;
  display: flex; flex-direction: column;
  background: var(--tier3-bg);
  border-bottom: 2px solid var(--accent);
  box-shadow: var(--shadow);
  padding: var(--sp-3);
}
.rel-picker .catalog-picker-list { flex: 1 1 auto; }
.faction-card { margin-top: var(--sp-3); border-top: 1px solid var(--border); padding-top: var(--sp-3); }
.faction-card h4 { margin: 0 0 var(--sp-2); font-size: 0.8rem; color: var(--accent); }
.faction-pressure-head { display: flex; align-items: center; justify-content: space-between; margin-top: var(--sp-3); margin-bottom: var(--sp-1); }
.faction-pressure-rolls { display: flex; align-items: center; gap: 2px; }

/* Force/Cunning/Wealth + Assets (SWN-inspired faction depth, 2026-07-06) */
.faction-stats-row { display: flex; gap: var(--sp-2); margin-top: var(--sp-2); }
.faction-stat { flex: 1; }
.faction-stat input { width: 100%; }
.faction-assets { margin-top: var(--sp-2); display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-1); }
.faction-asset-list { display: inline-flex; flex-wrap: wrap; gap: var(--sp-1); }
.faction-asset-chip { display: inline-flex; align-items: center; gap: 4px; }

/* Living Faction Engine Phase D: Turn History (Entity Editor) grouped by
   round, and the highlighted entry a specific-turn faction-name link
   lands on (data-open-entity-event) — a subtle accent-bordered callout,
   same "flag it, don't hide the rest" posture as this app's other
   highlight treatments. */
.faction-turn-history-round { margin-bottom: var(--sp-2); }
.faction-turn-history-round .field-label-static { display: block; margin-bottom: var(--sp-1); }
.thread-row.focused { border-left: 2px solid var(--accent); padding-left: var(--sp-2); background: var(--bg-inset); }

/* Enhancements (CWN-inspired strain/augmentation; renamed from
   "Cybernetics" 2026-07-06, docs/adr/next-request.md) */
.enhancements-card { margin-top: var(--sp-3); border-top: 1px solid var(--border); padding-top: var(--sp-3); }
.enhancements-card h4 { margin: 0 0 var(--sp-2); font-size: 0.8rem; color: var(--accent); display: flex; align-items: center; gap: var(--sp-1); }
.enhancement-list { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin: var(--sp-1) 0; }
.enhancement-chip { display: inline-flex; align-items: center; gap: 4px; }
.enhancement-chip.over-strain, .over-strain { color: var(--danger); }
/* Strain now reuses .statblock-row.track-row's own markup/CSS wholesale
   (drawers/index.js's strainMeter()) instead of a bespoke fill-bar, so it
   looks and behaves exactly like Health/XP/Momentum's counters (direct
   request) and inherits every track-row layout fix automatically. */
.enhancement-add { display: flex; gap: var(--sp-1); margin-top: var(--sp-1); }
.enhancement-add input[data-enhancement-name-input] { flex: 1; }
.enhancement-add input[data-enhancement-strain-input] { width: 4rem; }

/* Tag editor (Phase 7, unified 2026-07-04 with the Documents drawer's tag
   UX): removable chips + a single auto-committing input, same pill/chip
   language as .rel-chip above. */
.tag-editor { flex: 1 1 auto; min-width: 0; }
/* Label + add-tag input share one row (right-aligned input) to save the
   vertical space a separate label-then-input stack used to cost. */
.tag-editor-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); margin-bottom: 0.35rem; }
/* Direct follow-up request: "the tag field needs a dropdown listing the
   protected and entity-specific options for consistency" — the <select>
   and the free-text <input> read as one combined "add a tag" control,
   same visual grouping the Base/Trade-Code add-selects use elsewhere. */
.tag-editor-add-row { display: flex; align-items: center; gap: 0.3rem; flex-wrap: wrap; justify-content: flex-end; }
.tag-editor-select { width: 6.5rem; padding: 0.15rem 0.3rem; font-size: 0.65rem; }
.field-label-static { font-size: 0.7rem; letter-spacing: 0.08em; color: var(--text-dim); text-transform: uppercase; }
/* The "Tags" label is now also the expand/collapse toggle for the chosen-
   tags list below it (direct follow-up request) — reset to plain button
   chrome so it keeps reading as the same static-looking label as before. */
.tag-editor-toggle { background: none; border: none; padding: 0; cursor: pointer; font: inherit; white-space: nowrap; }
.tag-editor-toggle:hover { color: var(--accent); }
.tag-chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-2); margin-bottom: var(--sp-2); }
.tag-chip { display: inline-flex; align-items: center; gap: 4px; background: var(--tier2-bg); border: 1px solid var(--border); border-radius: 999px; padding: 0.15rem 0.45rem; font-size: 0.68rem; }
/* The tag's own text is a button (jumps to Cast filtered by this tag) —
   reset to read as plain chip text, not a nested button. */
.tag-chip-jump { background: none; border: none; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; }
.tag-chip-jump:hover { text-decoration: underline; color: var(--accent); }

/* clickable entity chips in WHO/WHERE cards */
.entity-chip { cursor: pointer; }
.entity-chip:hover { border-color: var(--accent); color: var(--accent); }

/* Relationship graph (Phase 3B) */
.graph-legend { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-bottom: var(--sp-2); font-size: 0.72rem; color: var(--text-dim); }
.graph-legend-item { display: inline-flex; align-items: center; gap: 4px; }
.graph-legend-item .dot { width: 0.6rem; height: 0.6rem; border-radius: 50%; display: inline-block; }
.graph-toolbar { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.graph-svg { width: 100%; height: auto; background: var(--bg-inset); border: 1px solid var(--border); border-radius: var(--radius-sm); cursor: grab; }
.graph-svg:active { cursor: grabbing; }
.graph-edge { stroke: rgba(148,163,184,0.45); stroke-width: 1.2; }
.graph-edge-label { fill: var(--text-dim); font-size: 9px; text-anchor: middle; pointer-events: none; }
.graph-node { cursor: pointer; }
.graph-node circle { stroke: #0b1120; stroke-width: 2; transition: stroke 0.15s; }
.graph-node:hover circle { stroke: #fff; }
.graph-node.sel circle { stroke: var(--accent); stroke-width: 3; }
.graph-node-label { fill: var(--text); font-size: 11px; text-anchor: middle; pointer-events: none; }
/* Graph filter ("USER CHANGES" QoL batch) — dims non-matches and outlines
   matches rather than removing nodes, since the force-directed layout
   (buildGraph/computeLayout) depends on the WHOLE node set to stay stable. */
.graph-node.graph-node-dim { opacity: 0.25; }
.graph-node.graph-node-match circle { stroke: var(--accent); stroke-width: 3; }
.graph-node.graph-node-match .graph-node-label { fill: var(--accent); font-weight: 600; }

/* Statblocks (Phase 3C) */
.statblock-block { margin-top: var(--sp-3); border-top: 1px solid var(--border); padding-top: var(--sp-3); }
.statblock-head { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.statblock-head h4 { flex: 1; margin: 0; font-size: 0.8rem; color: var(--accent); }
/* Direct follow-up request: "Add an up & down arrow to the left of the
   delete icon for each statblock so it can be reordered" — a tight pair
   (no gap between them, own smaller font) so they read as one up/down
   control, distinct from the looser-spaced icons around them. */
.statblock-move-buttons { display: inline-flex; gap: 0; }
.statblock-move-buttons .icon-btn { font-size: 0.7rem; padding: 0.15rem 0.25rem; }
/* Party drawer's collapsible section headers (direct follow-up request) —
   same size/color/weight as .statblock-head h4 above so a section reads
   identically whether or not it happens to be collapsible, just as a real
   button (background/border reset) instead of a plain heading. */
.party-section-toggle { flex: 1; margin: 0; padding: 0; background: none; border: none; cursor: pointer; text-align: left; font: inherit; font-size: 0.8rem; color: var(--accent); }
.party-section-toggle:hover { color: var(--accent-strong); }
.statblock-collapse-toggle { font-size: 0.7rem; color: var(--text-dim); }
.statblock-add-choices { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-bottom: var(--sp-2); }
.chip.sm { padding: 0.2rem 0.5rem; font-size: 0.7rem; }
.statblock-add-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-2); }
/* Collapsed behind a gear icon so several rulesets/templates worth of "+ Add"
   chips don't dominate the inspector once a few groups already exist. */
.statblock-add-toggle-row { display: flex; align-items: center; gap: var(--sp-2); margin-top: var(--sp-3); padding-top: var(--sp-2); border-top: 1px solid var(--border); }
.statblock-add-toggle-row .icon-btn { font-size: 1rem; }
/* Label + value only — a field's name/shape/kind is Settings' job (the
   Bestiary/character templates), not something edited per-instance here.
   width:60% (direct follow-up request: "the statblock fields for entity
   editor are too wide... the 60% is in relation to the statblock field's
   current width... needs to be narrower for all statblock field") — no
   earlier narrower version was found anywhere in this file's git history
   (every desktop .statblock-row revision on record fills 100% of
   .statblock-block, its plain block container), so this is the requested
   fallback instead: the WHOLE field row (label+value together, not just
   the value input alone — narrowing only the input left the row itself
   still full-width with dead space stranded after a narrow box) now caps
   at 60% of that. Its grid columns (1fr 1fr) split that 60% proportionally,
   so both the label and the value input end up narrower together — no
   separate width needed on .statblock-val itself. .statblock-row.track-row
   (no width override of its own) inherits this too, matching "all
   statblock field"; .attr-row's own width:fit-content below is already a
   compact self-sized pill, not a full-width row, so it's correctly
   unaffected. */
.statblock-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2); margin-bottom: 4px; align-items: center; width: 60%; }
.statblock-key { font-size: 0.8rem; color: var(--text-dim); }
.statblock-val { font-size: 0.85rem; }

/* Attribute badges (Phase 3D preview) — read-only summary badges (entity
   inspector's top-of-statblock Stats row, Journal entity-mention badges,
   Party member cards). Narrowed 2026-07-04 alongside the character-sheet
   compact rows (.attr-row-compact/.track-row-compact) for the same reason:
   these wrap onto their own line(s) fine inside a drawer's fixed width, but
   a Party member card has no width cap of its own, so a run of 5+ full-size
   badges just kept the card growing wider instead of wrapping. */
.attribute-badges, .journal-entity-badges { display: flex; gap: 4px; margin-bottom: var(--sp-2); flex-wrap: wrap; }
.attr-badge { display: flex; flex-direction: column; align-items: center; justify-content: center; min-width: 44px; min-height: 40px; border-radius: 8px; background: rgba(56,189,248,0.08); border: 1px solid rgba(56,189,248,0.22); padding: 0.3rem 0.35rem; }
.attr-key { font-size: 0.6rem; color: var(--text-dim); letter-spacing: 0.1em; text-transform: uppercase; }
.attr-val { font-size: 0.82rem; font-weight: 700; color: var(--accent-strong); margin-top: 0.15rem; }

/* Numeric click-to-set / double-click-to-roll tracks (Crew-Link-style) */
.statblock-row.track-row { grid-template-columns: 1fr 2fr; }
.track-widget { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.track-boxes { display: flex; gap: 2px; }
.track-box {
  width: 22px; height: 22px; padding: 0; border-radius: 4px;
  border: 1px solid var(--border); background: transparent; color: var(--text-dim);
  font-size: 0.7rem; line-height: 1; cursor: pointer; transition: background 0.1s, color 0.1s, border-color 0.1s;
}
.track-box:hover { border-color: var(--accent-strong); }
.track-box.on { background: var(--accent-strong); border-color: var(--accent-strong); color: #0b1120; font-weight: 700; }
/* Retinted to the same dark bg-inset/border recipe attr-row/party-stat-pill
   use (direct follow-up request — one consistent "statblock box" look
   everywhere), narrower padding included; only the value TEXT keeps the
   accent color as the "this is interactive" cue. */
.track-value-badge {
  border: 1px solid var(--border); background: var(--bg-inset); color: var(--accent);
  border-radius: var(--radius-sm); padding: 2px 6px; font-size: 0.8rem; font-weight: 700; cursor: pointer; user-select: none;
}
.track-value-badge:hover { border-color: var(--accent-strong); }
.track-value-badge small { font-weight: 400; color: var(--text-dim); margin-left: 1px; }

/* Attribute/stat row — a directly-editable, validated-numeric value
   (Starforged/5PFH-style: "EDGE +3"), not a 1-5 click-to-set meter (that's
   trackRow, reserved for depleting resources like Health). The label is the
   roll trigger when the field has a dice model; format (sign/inches/plain)
   is rendered as part of the input's own text, not a separate affix
   element, so it reads as part of the value like a real character sheet. */
.statblock-key-roll {
  background: none; border: none; padding: 0; font: inherit; color: var(--accent);
  cursor: pointer; text-decoration: underline dotted; text-underline-offset: 2px;
}
.statblock-key-roll:hover { color: var(--accent-strong); }
.attr-val-input {
  /* Narrowed to fit a modifier-style value (2-3 chars, e.g. "+2") — direct
     follow-up request; the box was previously sized as if it needed to fit
     a wider ratio like a track's old "X/Y" display. */
  width: 2.6rem; min-width: 0; text-align: center; border: 1px solid var(--accent-strong); background: rgba(56,189,248,0.08); color: var(--accent);
  border-radius: 4px; padding: 2px 4px; font-size: 0.85rem; font-weight: 700;
}
.attr-val-input:focus { outline: 2px solid var(--accent-strong); outline-offset: 1px; }
/* Direct follow-up request ("update all statblock boxes to have narrow
   margins and look like [a small dark bordered pill: stat name on top,
   bold value below]"): .statblock-row.attr-row itself becomes that one
   pill — label and value stacked inside ONE narrow-padded bordered box,
   sized to its own content (not stretched across a grid row) — instead of
   the label and the input sitting as two separate side-by-side grid
   cells. The label/input's own chrome (border/background/color) moves
   here, onto the shared box, so they read as one unit; .attr-row-compact/
   .track-row-compact/.party-stat-pill/.track-value-badge below are
   retinted to the exact same background/border recipe for one consistent
   "statblock box" look across the whole app, not four slightly different
   ones. */
.statblock-row.attr-row {
  display: inline-flex; flex-direction: column; align-items: center; gap: 1px;
  width: fit-content; min-width: 3.2rem; max-width: 100%;
  padding: 0.25rem 0.5rem; margin-right: var(--sp-2);
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-inset);
}
.attr-row .statblock-key, .attr-row .statblock-key-roll {
  font-size: 0.62rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-dim);
  text-decoration: none; white-space: nowrap;
}
.attr-row .attr-val-input {
  width: auto; min-width: 2rem; max-width: none;
  border: none; background: none; padding: 0;
  text-align: center; font-size: 1rem; font-weight: 700; color: var(--accent);
}
.attr-row .attr-val-input:focus { outline: none; }

/* Character sheet (ruleset-driven stats + resource meters). Direct
   follow-up request ("statblock fields should be only the width of the
   text in the first row of the box... all statblocks should be viewable
   at one time within the panel"): wraps instead of the old nowrap+
   overflow-x scroll, so a ruleset's whole stat row lands fully in view by
   flowing onto more lines rather than requiring horizontal scrolling.
   inline-flex/width:fit-content so this row hugs its actual stat pills
   instead of always stretching into one full-width bar; max-width keeps
   it from overflowing the drawer. */
.character-sheet-stats {
  display: inline-flex; flex-wrap: wrap; gap: var(--sp-1);
  width: fit-content; max-width: 100%;
  padding: var(--sp-2); margin-bottom: var(--sp-3);
  background: var(--tier2-bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
}
/* Same wrap-to-fit posture as .character-sheet-stats above, for every
   remaining (track/text) field in a statblock — direct follow-up request. */
.character-sheet-resources { display: flex; flex-wrap: wrap; gap: var(--sp-1); }
/* Direct follow-up request: "make Armor/Notes and Special fields share the
   width of the row 50/50" — reuses .text-row-compact's own pill markup
   (label on top, value below) unchanged, just stretched to exactly half
   this row's width instead of its own content-sized default. */
.statblock-half-row { display: flex; gap: var(--sp-2); margin-top: var(--sp-1); }
.statblock-half-row .text-row-compact { flex: 1 1 50%; width: auto; min-width: 0; }
/* Direct follow-up request: "keep all the buttons in the tracker on the
   same line as the title... the appearance used several builds ago" — the
   label sits beside the box row on one line (same recipe
   .party-stat-track-expansion already uses: label + a never-wrapping
   .track-boxes row, min-width:0 on the flex parent so overflow-x can
   actually engage) instead of the label centered on its own line above a
   box grid that wrapped onto a second line for any track past ~4-5
   points. */
.track-row-compact {
  display: flex; flex-direction: row; align-items: center; gap: var(--sp-2); flex: 0 0 auto;
  width: fit-content; max-width: 100%; min-width: 0; margin-bottom: 0; padding: 0.25rem 0.5rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-inset);
}
.track-row-compact .statblock-key { font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.68rem; white-space: nowrap; line-height: 1.2; }
.track-row-compact .track-widget { flex-wrap: nowrap; min-width: 0; }
.track-row-compact .track-boxes { flex-wrap: nowrap; overflow-x: auto; }
.track-row-compact .track-box { width: 18px; height: 18px; font-size: 0.62rem; flex: 0 0 auto; }
/* Direct follow-up request: "Change Will, XP and Luck to number fields
   instead of trackers" — same bordered-pill family as track-row-compact
   just above, swapping the click-to-set box row for a narrow number
   <input>. Direct follow-up requests since: (1) "change... to a currency
   field format" — the input is wrapped in numStepper (its own generic
   ＋/－ pair); (2) "drop the '/' separator and the last number. Just show
   the label, the textfield and the +-. Also, locate the +- under the
   textbox" — the old "/ max" suffix text is gone (dropped from the markup
   entirely, drawers/index.js's numberFieldRow), and the stepper's ＋/－
   buttons move from beside the input to below it via this 2-column grid:
   the input (first child) spans both columns on row 1, the two buttons
   (numStepper's own fixed 3-child shape) auto-place into row 2 — no
   markup change needed, and every OTHER numStepper call site (Party
   Tracker Size, Navigator workflow cards) keeps its normal inline row. */
.number-row-compact {
  display: flex; flex-direction: row; align-items: center; gap: var(--sp-2); flex: 0 0 auto;
  width: fit-content; max-width: 100%; min-width: 0; margin-bottom: 0; padding: 0.25rem 0.5rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-inset);
}
.number-row-compact .statblock-key { font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.68rem; white-space: nowrap; line-height: 1.2; }
.number-row-input-wrap { display: flex; align-items: center; gap: 0.2em; }
.number-row-compact .num-stepper { display: grid; grid-template-columns: 1fr 1fr; gap: 2px; justify-items: center; }
.number-row-compact .attr-val-input { grid-column: 1 / -1; width: 2.6em; text-align: center; padding: 0.1rem 0.2rem; font-size: 0.85rem; }
/* Plain text/value field's own compact pill (direct follow-up request —
   textRow had no compact variant before; every statblock field now gets
   one). Same family as track-row-compact/attr-row-compact: content-sized
   via the label's nowrap width; the value input uses the width:0/
   min-width:100% trick so its own default intrinsic size never inflates
   the pill past what the label actually needs — it only stretches to match
   the label's width once that's been resolved. */
.text-row-compact {
  display: flex; flex-direction: column; align-items: center; gap: 4px; flex: 0 0 auto;
  width: fit-content; min-width: 3rem; max-width: 100%; margin-bottom: 0; padding: 0.25rem 0.5rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-inset);
}
.text-row-compact .statblock-key { display: block; width: 100%; text-align: center; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.68rem; white-space: nowrap; line-height: 1.2; color: var(--text-dim); }
.text-row-compact .statblock-val { width: 0; min-width: 100%; box-sizing: border-box; text-align: center; padding: 2px 4px; font-size: 0.82rem; }

/* 5PFH character sheet weapon table + Gear (direct follow-up request) —
   real <table>, same recipe .trade-market-table already uses for editable
   tabular data (inputs inside <td>s, not a flex/grid mimic), full width
   (not .statblock-row's own 60% field-row cap — a table of several columns
   needs the room, unlike a single label+value field). */
.statblock-weapons { margin-top: var(--sp-3); }
.statblock-weapons h4 { margin: 0 0 var(--sp-2); font-size: 0.8rem; color: var(--accent); }
.statblock-weapon-table { width: 100%; table-layout: fixed; border-collapse: collapse; margin-bottom: var(--sp-2); font-size: 0.85rem; }
.statblock-weapon-table th { text-align: left; font-size: 0.68rem; letter-spacing: 0.06em; color: var(--text-dim); text-transform: uppercase; padding: 0.3rem 0.3rem; border-bottom: 1px solid var(--border); }
.statblock-weapon-table td { padding: 0.25rem 0.3rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
.statblock-weapon-table input[type="text"] { width: 100%; padding: 0.3rem 0.4rem; font-size: 0.82rem; }
/* Direct follow-up request: Weapon/Traits get the most room (25% each),
   Range/Shots/Damage are short values and stay narrow, the remove-icon
   column is just wide enough for the button itself. */
.statblock-weapon-col-name, .statblock-weapon-col-traits { width: 30%; }
.statblock-weapon-table th:nth-child(2), .statblock-weapon-table th:nth-child(3), .statblock-weapon-table th:nth-child(4) { width: 12%; }
.statblock-weapon-remove-cell { width: 1.8rem; padding-left: 0.1rem; padding-right: 0.1rem; text-align: center; }
.statblock-weapon-remove-btn { padding-left: 0.15rem; padding-right: 0.15rem; }
/* Five Leagues weapon cards (direct follow-up request: "use two rows where
   Weapon name is wide enough... Move traits and location and the delete
   icon to a second row") — Five Leagues' extra Catalog + Location columns
   left no room in 5PFH's original single-row table for readable headers,
   so each weapon becomes its own two-row card instead of a <table> row.
   Row 1 (name/range/shots/damage) shares a grid-template-columns with the
   header row above it so the two line up; row 2 (traits/location/delete)
   is a second, independent grid since it has no shared header. Direct
   follow-up request moved the Catalog picker out of row 2 entirely (into
   the "+ Weapon" add row below, five-leagues-item-add-row), so row 2 is
   now 3 columns, not 4. Direct follow-up request: "widen the weapon name
   field by 15% and widen the Dmg field 15%... ensure that field will show
   the entire string (ex. '+0/+0')" — Name's minmax(6rem, 2fr) and Dmg's
   3.4rem both scaled by 1.15 (6.9rem/2.3fr and 3.9rem); the catalog's own
   real damage format is "+N / +N" (formatFiveLeaguesOvercome, domain/
   fiveLeagues.js — WITH spaces around the slash, slightly wider than the
   request's own space-free example), and 3.9rem comfortably fits that at
   this row's 0.82rem font plus its input's own horizontal padding. */
.statblock-weapon-headrow, .statblock-weapon-row1 { display: grid; grid-template-columns: minmax(6.9rem, 2.3fr) 3.4rem 3rem 3.9rem; gap: var(--sp-2); }
.statblock-weapon-headrow { font-size: 0.68rem; letter-spacing: 0.06em; color: var(--text-dim); text-transform: uppercase; padding: 0 0.1rem 0.2rem; }
.statblock-weapon-headrow span:not(.statblock-weapon-name-head) { text-align: center; }
.statblock-weapon-list { display: flex; flex-direction: column; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.statblock-weapon-card { border: 1px solid var(--border); border-radius: var(--radius-sm, 4px); padding: 0.35rem 0.4rem; }
.statblock-weapon-row1 input { width: 100%; padding: 0.3rem 0.4rem; font-size: 0.82rem; text-align: center; }
.statblock-weapon-row1 .statblock-weapon-name-input { text-align: left; font-weight: 600; }
.statblock-weapon-row2 { display: grid; grid-template-columns: 1fr 5rem 1.8rem; gap: var(--sp-2); margin-top: 0.3rem; }
.statblock-weapon-row2 input, .statblock-weapon-row2 select { width: 100%; padding: 0.25rem 0.35rem; font-size: 0.78rem; }
.statblock-weapon-row2 .statblock-weapon-remove-btn { width: 100%; padding-left: 0.15rem; padding-right: 0.15rem; }
.statblock-gear-field { margin-top: var(--sp-3); }
.statblock-gear-field textarea { width: 100%; }
/* Direct follow-up request: "update the character sheet for 5lfb to track
   character type, skills, origins, background, armor, gear storage in
   self, backpack or stash, weapon traits, and spells... per the ruleset" —
   same vertical rhythm for every section; .five-leagues-chip/-chip-list is
   shared by Skills and the Armor flags/improvements (a checkbox wrapped in
   a labeled pill, wrapping flex row). .five-leagues-origin-traits is the
   Origin info panel's own trait list (see .five-leagues-info-panel below). */
.five-leagues-type-origin-row, .five-leagues-background, .five-leagues-skills,
.five-leagues-armor, .five-leagues-items, .five-leagues-spells { margin-top: var(--sp-3); }
.five-leagues-background select, .five-leagues-armor select { width: 100%; }
.five-leagues-origin-traits { margin: var(--sp-2) 0 0; padding-left: 1.1em; font-size: 0.82rem; color: var(--text-dim); }
.five-leagues-origin-traits li { margin-bottom: var(--sp-1); }
.five-leagues-chip-list { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-2); }
.five-leagues-chip { display: inline-flex; align-items: center; gap: 0.3em; padding: 0.2rem 0.5rem; border: 1px solid var(--border); border-radius: 999px; font-size: 0.82rem; cursor: pointer; }
/* Direct follow-up request: "Narrow the '+ Add from Items catalog...'
   dropdown and move '+ Custom Item' button to the same row" — nowrap (was
   wrap) plus a capped select width (was a 10rem MINIMUM pushing the button
   onto its own line in a narrow drawer) with ellipsis for the now-truncated
   closed-box text; the open dropdown list itself still shows every option
   in full. */
.five-leagues-item-add-row { display: flex; gap: var(--sp-2); margin-top: var(--sp-2); flex-wrap: nowrap; align-items: center; }
.five-leagues-item-add-row select { flex: 1 1 auto; min-width: 0; max-width: 60%; text-overflow: ellipsis; }
.five-leagues-item-add-row button { flex: 0 0 auto; white-space: nowrap; }
/* Direct follow-up request: "the item 'notes' field should be an icon that
   opens/collapses the notes field on a second line" — the toggle button
   sits in the table's own Notes column; .has-notes accents it so a
   collapsed-but-filled row is still visually distinct from an empty one.
   The expanded textarea's own row spans every column. */
.five-leagues-item-notes-cell { text-align: center; }
.five-leagues-item-notes-toggle.has-notes { color: var(--accent-strong); }
.five-leagues-item-notes-row textarea { width: 100%; box-sizing: border-box; }
.five-leagues-spell-list { display: flex; flex-direction: column; gap: var(--sp-1); margin-top: var(--sp-2); max-height: 16rem; overflow-y: auto; }
.five-leagues-spell-row { display: flex; align-items: baseline; gap: var(--sp-2); font-size: 0.85rem; cursor: pointer; }
.five-leagues-spell-name { font-weight: 600; min-width: 6rem; }
/* Direct follow-up request: "Move the text '(+2 on a relevant Proficiency
   Test, p.52)' to a tooltip" — a plain hover tooltip (short text, unlike
   the expand/collapse panel below). */
.five-leagues-hint { cursor: help; color: var(--text-dim); font-size: 0.85em; }
/* "Skills ⓘ" — the hint sits on the header's own line, right after the word. */
.five-leagues-skills-label { display: inline-flex; align-items: baseline; gap: 0.3em; margin: 0; white-space: nowrap; }
/* Direct follow-up request: "move those two fields to the same row... The
   descriptive text for 'character type' and 'origin' should be hidden and
   connected to a question mark tool tip that... expand[s]/collapse[s]
   where the text exists right now (instead [of] the usual floating tool
   tip display since it would be too small to see all the text)." Two
   field-labels side by side (wraps on a genuinely narrow drawer, same
   flex-wrap posture as .character-sheet-stats); each select's own "?"
   sits right beside it; the expand panel renders BELOW the whole row,
   not inside either label, so whichever one is open reads full-width. */
.five-leagues-type-origin-row { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.five-leagues-type-origin-row .field-label { flex: 1 1 10rem; min-width: 0; }
.five-leagues-info-row { display: flex; align-items: center; gap: var(--sp-1); }
.five-leagues-info-row select { flex: 1 1 auto; min-width: 0; }
.five-leagues-info-toggle { flex: 0 0 auto; width: 1.6rem; height: 1.6rem; border-radius: 50%; border: 1px solid var(--border); font-weight: 700; }
.five-leagues-info-toggle.open { background: var(--accent); color: var(--bg-main); border-color: var(--accent); }
.five-leagues-info-toggle:disabled { opacity: 0.4; cursor: default; }
.five-leagues-info-panel { margin-top: var(--sp-2); padding: var(--sp-2); border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-inset); }
.five-leagues-info-panel p { margin: 0; }

/* Compact attribute pill (Edge/Heart/... stat row atop a character sheet) —
   same bordered-pill language as .track-row-compact, single-number spinner
   instead of boxes. Markup always carries BOTH .attr-row and
   .attr-row-compact together (attrRow()'s opts.compact just appends the
   modifier class); .statblock-row.attr-row's own two-class selector
   outranks this one-class selector on every property they both set
   (padding/gap/margin-right/display), so the override right below is
   still needed for those — but width now matches the base pill's own
   width:fit-content (direct follow-up request — "the width of the text in
   the first row of the box"), just content-sized via the label's
   white-space:nowrap instead of the old fixed 5.2rem. */
.attr-row-compact {
  display: flex; flex-direction: column; align-items: center; gap: 4px; flex: 0 0 auto;
  width: fit-content; max-width: 100%; margin-bottom: 0; padding: 0.25rem 0.5rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-inset);
}
.statblock-row.attr-row.attr-row-compact {
  display: flex; width: fit-content; max-width: 100%; padding: 0.25rem 0.5rem; margin-right: 0; gap: 4px;
}
.attr-row-compact .statblock-key { display: block; width: 100%; text-align: center; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.68rem; white-space: nowrap; line-height: 1.2; }
.attr-row-compact .attr-val-input { width: 0; min-width: 100%; box-sizing: border-box; padding: 0; font-size: 0.85rem; }
/* The same pill's value when it's display-only (Navigator's Party Trackers). */
.attr-val-static { font-size: 0.85rem; font-weight: 700; color: var(--accent); text-align: center; white-space: nowrap; }
/* Direct follow-up request: "narrow the padding around the text and
   number of the statblocks for the 5LFB character sheet so that all six
   fields fit on one row" — tighter pill padding/gap scoped to just Five
   Leagues' own 6-stat compact row (attrRow's opts.ruleset === 'fiveleagues'
   check), so Starforged/5PFH's own compact attribute pills keep their
   normal spacing. */
.attr-row-compact-fiveleagues,
.statblock-row.attr-row.attr-row-compact-fiveleagues {
  padding: 0.2rem 0.3rem; gap: 2px;
}

/* Drag-and-drop (entity ↔ entity, entity → text) */
[data-drag-entity] { cursor: grab; }
[data-drag-entity]:active { cursor: grabbing; }
.drop-hover { outline: 2px dashed var(--accent-strong); outline-offset: 2px; background: rgba(56,189,248,0.08); }
/* Touch drag ghost — HTML5 DnD has a native drag image for free on desktop;
   touch has nothing equivalent, so this floating pill follows the finger
   instead. pointer-events: none is load-bearing, not decorative: without
   it, document.elementFromPoint(touch.x, touch.y) in onTouchMove would hit
   this element itself (it's positioned exactly at the touch point) instead
   of whatever's actually underneath it. */
.touch-drag-ghost {
  position: fixed; z-index: 50; transform: translate(-50%, -150%);
  padding: 0.35rem 0.6rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600;
  background: var(--accent-strong); color: #06121f; box-shadow: var(--shadow);
  pointer-events: none; white-space: nowrap;
}

/* @-mention autocomplete popup — a single reusable floating element (same
   pattern as .touch-drag-ghost above), repositioned under whichever field
   is being typed into rather than one per field. */
.mention-suggest {
  position: fixed; z-index: 60; max-height: 14rem; overflow-y: auto;
  background: var(--tier3-bg); border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
}
.mention-suggest-item {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
  width: 100%; text-align: left; padding: 0.4rem 0.6rem; background: none; border: none; color: var(--text);
  font-size: 0.82rem; cursor: pointer;
}
.mention-suggest-item.active, .mention-suggest-item:hover { background: var(--tier2-bg); }
.mention-suggest-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Generic inline text-entry prompt (openInlinePrompt/shell.js) — every
   former window.prompt() data-entry popup now opens this instead, a small
   fixed-position field next to whatever triggered it, same technique as
   .mention-suggest above. Usually one field (.inline-prompt-fields holds
   just one label+input), but a multi-field kind (the Link button's
   description + URL) stacks more than one. */
.inline-prompt {
  position: fixed; z-index: 60; width: 260px; padding: var(--sp-2);
  background: var(--tier3-bg); border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
}
.inline-prompt-fields { display: flex; flex-direction: column; gap: var(--sp-2); }
.inline-prompt-label { display: block; font-size: 0.7rem; letter-spacing: 0.08em; color: var(--text-dim); text-transform: uppercase; }
.inline-prompt-label .inline-prompt-input { margin-top: 0.3rem; }
.inline-prompt-row { display: flex; gap: 0.3rem; align-items: center; justify-content: flex-end; margin-top: var(--sp-2); }

/* ============================================================
   Phase 5 — Party / Colony / Guide / Oracle tree / Documents /
   Bestiary templates / in-app document viewer
   ============================================================ */

/* Edge tabs grew from 6 to 9 — let the strip scroll instead of clipping. */
.mc-edge { overflow-y: auto; }

/* In-app PDF viewer — full width beside the edge tabs, exactly like
   .mc-drawer's own footprint. Mutually exclusive with the main drawer
   (docs/adr/0032: "just tab groups," one panel visible at once) — shell.js's
   render() hides .mc-drawer whenever a document tab is open, so the two
   never compete for z-index/click-through the way they used to when shown
   side by side. */
.mc-doc-viewer {
  position: fixed; top: var(--header-h); left: 0; right: var(--edge-w); bottom: 0;
  background: var(--bg); border-left: 1px solid var(--border-strong);
  display: flex; flex-direction: column; z-index: 15;
}
.mc-doc-viewer[hidden] { display: none; }
/* Tab strip — one tab per open document, switch by click, close via its own
   ✕ (no single global close button anymore now that more than one document
   can be open at once). */
.doc-viewer-tabs {
  display: flex; align-items: stretch; gap: 2px; overflow-x: auto;
  background: var(--tier1-bg); border-bottom: 1px solid var(--tier1-border);
}
.doc-viewer-tab {
  display: flex; align-items: center; gap: var(--sp-2); flex: 0 0 auto; max-width: 16rem;
  padding: var(--sp-2) var(--sp-3); background: none; border: none; border-right: 1px solid var(--tier1-border);
  font-size: 0.85rem; color: var(--text-dim); cursor: pointer;
}
.doc-viewer-tab.active { background: var(--bg); color: var(--accent); }
.doc-viewer-tab-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.doc-viewer-tab-close { border-radius: 3px; padding: 0 3px; }
.doc-viewer-tab-close:hover { background: var(--bg-inset); color: var(--text); }
.doc-viewer-empty {
  display: flex; align-items: center; justify-content: center; flex: 1;
  padding: var(--sp-4); text-align: center; color: var(--text-dim); font-size: 0.9rem;
}
.doc-viewer-empty[hidden] { display: none; }
.mc-doc-viewer iframe { flex: 1; border: none; width: 100%; background: #fff; }
.mc-doc-viewer iframe[hidden] { display: none; }

/* Faction Events (docs/adr/0031-swn-faction-turn-engine.md, docs/adr/0032)
   is an ordinary drawer tab now, not its own fixed panel — .mc-drawer's
   CSS above covers it. Only its body content classes remain here. */
.faction-events-body { flex: 1; overflow: auto; padding: var(--sp-4); }

/* Faction Events follow-up: co-located ally/rival/neutral chips and the
   "News from {location}" framing tag for a non-witnessed entry — plain
   color cues, same tier-token palette as every other chip in this app. */
.faction-event-colocated { margin: var(--sp-1) 0 var(--sp-2); display: flex; flex-wrap: wrap; gap: var(--sp-1); }
.chip.stance-ally { border-color: var(--accent); }
.chip.stance-rival { border-color: #b33; color: #b33; }
.chip.stance-neutral { opacity: 0.8; }
.faction-event-news-tag { font-style: italic; color: var(--text-dim); }

/* Living Faction Engine Phase D: structured "assets affected" and the
   per-location recent-events context panel shown alongside a draft under
   review — plain nested lists, same dim/small text treatment as every
   other secondary detail line in this drawer. */
.faction-event-impact ul, .faction-event-location-log p { margin: 0.15rem 0 0 1.1rem; }
.faction-event-impact ul { padding: 0; }
.faction-event-location-log { margin: var(--sp-2) 0; padding: var(--sp-2); background: var(--bg-inset); border-radius: var(--radius-sm); }

/* Universal Search (Phase 8, pack 23) — a top-anchored overlay above
   everything else (drawer included) so a result stays reachable regardless
   of what's currently open; the panel itself is the only thing that
   captures clicks, the dimmed backdrop is decorative (closing happens via
   the ✕ button or toggling the header button again, not a backdrop click,
   to avoid the delegated click handler's closest() matching descendants).
   Direct bug report: "when displaying the combat tracker and selecting a
   NPC that causes the NPC selection window to pop up, it must pop up on
   top" — this overlay is also reused for every entity-picker popup (the
   Combat Tracker's own "+ Add NPC", Location linking, Vehicle creation,
   ...), but its z-index (30) sat BELOW .floating-tools-stack's 55 (the
   Dice Roller/Combat Tracker panels' own fixed-position container), so a
   picker opened from inside either of those panels rendered behind them
   instead of on top — the comment above was already the intent, the
   number just didn't back it up. Raised past every floating-tools-stack
   descendant (including .dice-roll-card's own 60) so it's reachable
   regardless of what else is open; still below Settings' menu (70) and
   the top-level import/export blocker (90), which must stay above
   everything including this. */
.mc-search-overlay {
  position: fixed; inset: 0; z-index: 65;
  background: rgba(0, 0, 0, 0.35);
  display: flex; justify-content: center;
  padding-top: 8vh;
}
.mc-search-overlay[hidden] { display: none; }
.search-panel {
  width: min(38rem, 92vw); max-height: 76vh;
  background: var(--tier3-bg); border: 1px solid var(--border-strong); border-radius: var(--radius);
  box-shadow: var(--shadow); display: flex; flex-direction: column; overflow: hidden;
}
.search-panel-head { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3); border-bottom: 1px solid var(--border); }
/* Entity picker overlay (Composer WHO redesign) — reuses the .mc-search-
   overlay/.search-panel/.search-panel-head/.search-input/.search-results
   shell wholesale (same centered-modal shape, same typed-filter behavior);
   only the candidate row differs from a search result, so it's the only
   new rule needed here. */
.entity-picker-row { display: block; width: 100%; text-align: left; background: none; border: none; color: var(--text); padding: 0.4rem 0.6rem; border-radius: var(--radius-sm); font-size: 0.85rem; }
.entity-picker-row:hover { background: var(--tier2-bg); color: var(--accent); }
.search-input { flex: 1; font-size: 1rem; padding: 0.5rem 0.7rem; background: var(--bg-inset); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text); }
.search-results { overflow-y: auto; padding: var(--sp-2) var(--sp-3) var(--sp-3); }
.search-group-head { margin: var(--sp-3) 0 var(--sp-1); font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent); }
.search-group-head:first-child { margin-top: 0; }
.search-result-row {
  display: flex; flex-direction: column; align-items: flex-start; gap: 1px; width: 100%;
  text-align: left; background: none; border: none; color: var(--text);
  padding: 0.4rem 0.5rem; border-radius: var(--radius-sm);
}
.search-result-row:hover { background: var(--tier2-bg); }
.search-result-label { font-size: 0.9rem; }
.search-result-sub { font-size: 0.75rem; }

/* Direct follow-up request: "the combat tracker should dock to the bottom
   of the dice roller... when dice roller, combat tracker and the dice roll
   results are all open, they should not overlap each other on mobile...
   they should all dock to each other when abutting" — the dice-roller
   toggle, its own panel, the Combat Tracker panel, and the dice-roll
   results card (previously four independently `position: fixed` elements
   that could only avoid overlapping each other by never actually being
   open at the same time — no longer true once the Combat Tracker gained
   its own manual open button INSIDE the dice roller panel) now share one
   real flex column, so an open one always pushes the next down instead of
   sitting on top of it. Anchored where .dice-roller-toggle always was;
   scrolls internally (rather than running off-screen) if every panel is
   open at once on a short phone viewport. */
.floating-tools-stack {
  position: fixed; top: var(--sp-3); right: var(--sp-3); z-index: 55;
  display: flex; flex-direction: column; align-items: flex-end; gap: var(--sp-2);
  /* Direct bug report: "make sure the dice results box is fully in view
     on mobile devices where the bottom can be overlapped with the URL
     field" — 100vh is the LARGEST possible viewport (as if the browser's
     own address bar/toolbar were hidden), not the actually-visible one;
     on a real mobile device where that chrome is showing, a max-height
     computed off 100vh lets this container (and the dice-roll-card flex
     child docked inside it) extend past the real visible area, with the
     overlapping browser chrome covering whatever fell past the edge.
     100dvh (dynamic viewport height) tracks the TRUE visible viewport as
     that chrome shows/hides; the plain vh line stays first as the
     fallback for a browser old enough not to understand dvh at all (that
     whole second declaration is simply invalid there and dropped,
     leaving the first one in effect — the standard dvh/vh fallback
     pattern, not a progressive override). */
  max-height: calc(100vh - 2 * var(--sp-3));
  max-height: calc(100dvh - 2 * var(--sp-3));
  overflow-y: auto; overflow-x: visible;
  /* Real bug, direct report ("party/campaign/world/journal/settings don't
     respond to click, but Search and the campaign switcher do"): .dice-
     roll-overlay below keeps its full width: min(26rem, 92vw) even while
     CLOSED (only its height collapses to 0) -- its own pointer-events:none
     stops IT from intercepting clicks, but this parent container's own
     layout box is still exactly as wide as that child, and a plain <div>
     captures clicks across its whole box regardless of what's actually
     painted there. That invisible ~26rem-wide box, anchored top-right,
     sat directly over the header's own right-aligned buttons (which are
     further right than Search/the campaign switcher, both unaffected).
     pointer-events:none here makes the empty slack space click-through;
     every real control below opts back in with its own pointer-events:auto,
     the same "parent none, children auto" pattern .dice-roll-overlay[data-
     open="true"] already uses for itself. */
  pointer-events: none;
}
.floating-tools-stack > * { pointer-events: auto; }

/* Dice roll window (performFieldRoll's replacement for a plain toast) — a
   graphical breakdown of a statblock roll: the die(s) + field value added
   up on one row, whatever it's measured against on another, and a big
   color-coded outcome banner. No backdrop to click through, since there
   isn't one; ✕ or Escape closes it. Docks below the dice roller/Combat
   Tracker panels (see .floating-tools-stack above) rather than a fixed
   bottom-right corner, so it can never overlap either of them. */
.dice-roll-overlay {
  flex: 0 0 auto; width: min(26rem, 92vw);
  max-height: 0; overflow: hidden;
  transform: translateY(1rem); transition: transform 0.2s ease, max-height 0.2s ease;
  pointer-events: none; /* closed window must not intercept clicks */
}
.dice-roll-overlay[data-open="true"] { max-height: 40rem; transform: translateY(0); pointer-events: auto; }

/* The 3D dice module's own render target (src/ui/diceBox3d.js) — a single
   fixed, viewport-sized, transparent overlay dice-box's WebGL canvas fills
   completely; created once and reused for every roll (never rebuilt per
   roll). Sits below the existing 2D `.dice-roll-card` (z-index 60) so the
   outcome banner it renders once the animation settles is always the
   topmost thing on screen, and below `.settings-menu` (70, deliberately
   the app's highest) for the same "always reachable" reason that one is
   pinned there. Invisible/non-interactive at rest — `.active` (added only
   for the ~1s the dice are actually tumbling) is what makes it show and
   catch no clicks either, so nothing GM-facing is ever blocked by an idle
   canvas sitting over the whole page. */
.dice-box-3d-container {
  position: fixed; inset: 0; z-index: 58;
  opacity: 0; pointer-events: none; transition: opacity 0.15s ease;
}
.dice-box-3d-container.active { opacity: 1; }
/* Real bug, direct report ("the dice roller does not roll the 3d dice
   animation"): dice-box's own vendored style.css only sets opacity/
   transition on its <canvas> — it never sizes it, so left unstyled it
   rendered at the browser's default 300x150px canvas size instead of
   filling this container, making the whole animation tiny/effectively
   invisible. dice-box always appends exactly one <canvas> as this
   container's only child, so a plain child selector is enough. */
.dice-box-3d-container canvas { width: 100%; height: 100%; display: block; }

/* Dice settings tab's quick-pick color swatches (direct follow-up request:
   "add dice color options including maroon, pink and dark green",
   DICE_3D_COLOR_PRESETS in data/dice3dThemes.js) — small clickable circles
   showing the actual preset color, sitting above the native <input
   type="color"> as a faster shortcut to it, not a replacement. */
.dice3d-color-swatches { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: var(--sp-1) 0 var(--sp-2); }
.dice3d-color-swatch {
  width: 1.6rem; height: 1.6rem; border-radius: 50%; padding: 0;
  border: 2px solid var(--border-strong); cursor: pointer;
}
.dice3d-color-swatch:hover { border-color: var(--accent); }
.dice3d-color-swatch.active { border-color: var(--accent-strong); box-shadow: 0 0 0 2px var(--accent-strong); }
.dice3d-color-swatch:disabled { cursor: not-allowed; opacity: 0.4; }
.dice-roll-card {
  width: min(26rem, 92vw);
  background: var(--tool-panel-bg); border: 1px solid var(--border-strong); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: var(--sp-4);
}
.dice-roll-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--sp-4); }
.dice-roll-label { margin: 0; font-size: 1.05rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--accent); }
.dice-roll-head-actions { display: flex; align-items: center; gap: var(--sp-1); }
.dice-roll-body { display: flex; gap: var(--sp-3); align-items: stretch; }
.dice-roll-rows { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: var(--sp-4); justify-content: center; }
.dice-roll-row { display: flex; align-items: center; gap: var(--sp-3); }
.dice-roll-icon { display: flex; align-items: center; justify-content: center; width: 2.4rem; flex-shrink: 0; color: var(--text-dim); }
.dice-face-pair { display: flex; gap: 2px; }
.dice-roll-formula { font-size: 1.25rem; font-weight: 700; }
.dice-roll-outcome {
  flex: 0 0 42%; display: flex; align-items: center; justify-content: center; text-align: center;
  border-radius: var(--radius-sm); font-size: 1.15rem; font-weight: 800; letter-spacing: 0.03em;
  padding: var(--sp-3); line-height: 1.25;
}
.dice-outcome-strong-hit, .dice-outcome-success { background: rgba(74, 222, 128, 0.16); color: var(--ok); border: 1px solid rgba(74, 222, 128, 0.4); }
.dice-outcome-weak-hit { background: rgba(251, 191, 36, 0.16); color: var(--warn); border: 1px solid rgba(251, 191, 36, 0.4); }
.dice-outcome-miss, .dice-outcome-fail { background: rgba(248, 113, 113, 0.16); color: var(--danger); border: 1px solid rgba(248, 113, 113, 0.4); }
.dice-outcome-neutral { background: rgba(56, 189, 248, 0.12); color: var(--accent); border: 1px solid rgba(56, 189, 248, 0.35); }

/* Floating top-right dice roller (direct request, modeled on the Iron
   Fellowship/Crew-Link Ironsworn companion app's "Custom Dice Roll"
   button) — a free-form NdX+modifier roller, always reachable regardless of
   which drawer/panel is open, pinned to the actual top-right corner of the
   browser window rather than living inside .mc-header (unlike every other
   header control, this one shouldn't disappear behind a drawer's own head
   row on a narrow screen). Its result reuses .dice-roll-overlay/-card above
   (a new method:'custom' case) rather than a second result display. Sits as
   the first flex child of .floating-tools-stack (see above) rather than its
   own `position: fixed` anchor. */
.dice-roller-toggle {
  flex: 0 0 auto;
  width: 2rem; height: 2rem; border-radius: 50%; padding: 0; /* same badge size as .mc-edge's own glyphs (1.1rem) plus their usual padding, direct follow-up request */
  background: #fff; border: 1px solid var(--border-strong); /* white — the d20-thm.png icon's own background, not the app's dark tier2-bg, so it doesn't show a mismatched edge */
  display: flex; align-items: center; justify-content: center; overflow: hidden;
  box-shadow: var(--shadow); cursor: pointer;
}
.dice-roller-toggle:hover { border-color: var(--accent); }
/* Combat Initiative Tracker's own drop target (direct follow-up request —
   see the .combat-tracker-panel comment below): visible feedback while
   dragging an entity thumbnail over the dice icon, same accent-border cue
   every other drop target in this app already uses. */
.dice-roller-toggle.drop-hover { border-color: var(--accent-strong); box-shadow: 0 0 0 2px var(--accent-strong); }
/* d20-thm.png has a lot of built-in white margin around the die graphic
   itself (matches this button's own white background, so it's invisible as
   padding rather than as a visible border) — scaled up so the die shape,
   not the margin, is what actually fills the circle (direct follow-up
   request), cropped by this button's own overflow:hidden. */
.dice-roller-toggle img { width: 100%; height: 100%; object-fit: cover; transform: scale(1.5); }
.dice-roller-panel {
  flex: 0 0 auto;
  width: min(19rem, 92vw);
  background: var(--tool-panel-bg); border: 1px solid var(--border-strong); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: var(--sp-3);
}
.dice-roller-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--sp-2); }
.dice-roller-head h3 { margin: 0; font-size: 0.9rem; }
.dice-roller-sides { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin-bottom: var(--sp-3); }
/* The pool list (direct follow-up request: click a die type to add one to
   a list, roll them all separately, unsummed) — each entry is a plain chip
   (same visual language as the "add" chips above it) until clicked, which
   SELECTS/highlights it (.chip.active's existing accent style, direct
   follow-up request — no more per-chip ✕; Clear removes just the selected
   one instead, or the whole pool if nothing's selected) rather than
   removing it outright. */
.dice-roller-pool { display: flex; flex-wrap: wrap; gap: var(--sp-1); align-items: center; min-height: 1.8rem; margin-bottom: var(--sp-2); }
.dice-roller-pool-actions { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
.dice-roller-modifier-field { flex: 0 0 auto; margin: 0; }
.dice-roller-modifier-field input[type="number"] { width: 3.2rem; }
.dice-roller-error { color: var(--danger); margin-top: 0.25rem; }
.dice-roller-foot { display: flex; align-items: center; justify-content: space-between; margin-top: var(--sp-3); padding-top: var(--sp-2); border-top: 1px solid var(--border); }

/* Combat Initiative Tracker (direct request; no dedicated toggle icon of
   its own — direct follow-up request: "do not display a combat tracker
   icon next to the dice roller... just open the combat tracker window
   when a thumbnail is dragged and dropped onto the dice roller icon") —
   docks below the dice roller panel as the next flex child of
   .floating-tools-stack (direct follow-up request: "the combat tracker
   should dock to the bottom of the dice roller" — both CAN be open
   together now, via the dice roller panel's own manual open button, so
   they need real flow, not two identical `position: fixed` coordinates).
   Also a drop target itself (data-drop-combat-tracker) for "drop into the
   open tracker" once it's already showing. */
.combat-tracker-panel {
  flex: 0 0 auto;
  width: min(22rem, 92vw); max-height: 70vh; overflow-y: auto;
  background: var(--tool-panel-bg); border: 1px solid var(--border-strong); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: var(--sp-3);
}
.combat-tracker-panel.drop-hover { outline: 2px dashed var(--accent-strong); outline-offset: -2px; }
.combat-tracker-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--sp-2); }
.combat-tracker-head h3 { margin: 0; font-size: 0.9rem; }
.combat-tracker-list { display: flex; flex-direction: column; gap: var(--sp-2); }
.combat-tracker-row { display: flex; align-items: flex-start; gap: var(--sp-2); padding: 0.2rem; border-radius: var(--radius-sm); cursor: pointer; }
.combat-tracker-row.drop-hover { outline: 2px dashed var(--accent-strong); outline-offset: 2px; border-radius: var(--radius-sm); }
/* The "active combatant" highlight (direct follow-up request: "click-to-
   highlight a record") — only one row at a time; click the row's own
   background (not one of its buttons/the card itself) to set or clear it. */
.combat-tracker-row.active { background: var(--tier2-bg); outline: 2px solid var(--accent-strong); }
/* Direct follow-up request: "select the attacker NPC first which is
   highlighted in red and then select the defender highlighted in blue"
   (5LFB ruleset only — every other ruleset keeps the plain .active
   highlight just above). Reuses --danger (red) and --accent-strong (the
   app's existing blue-tinted accent) rather than inventing new tokens. */
.combat-tracker-row.combat-attacker { background: rgba(248, 113, 113, 0.14); outline: 2px solid var(--danger); }
.combat-tracker-row.combat-defender { background: rgba(56, 189, 248, 0.14); outline: 2px solid var(--accent-strong); }
/* The Attacker's own weapon/spell picker (direct follow-up request: "when
   selecting the attacker, determine if they have more than one weapon or
   spell capability and prompt which is being used in the attack") — a
   sibling block right under that row, not nested inside its flex layout. */
.combat-tracker-action-picker { margin: 0 0 var(--sp-2) 1.8rem; }
.combat-tracker-action-picker select { width: 100%; max-width: 16rem; }
/* Direct follow-up request: "Factor in armor and all other combat
   conditions... Prompt with questions for optional attributes or
   abilities or buffs... to be applied" — the Tactic/Counter Attack/
   Stunned form shown once both Attacker and Defender are picked. */
.combat-tracker-options { margin-top: var(--sp-3); padding-top: var(--sp-2); border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: var(--sp-2); }
.combat-tracker-options-head { margin: 0; }
.combat-tracker-options select { width: 100%; max-width: 16rem; }
.combat-tracker-option-check { display: flex; align-items: center; gap: 0.4em; font-size: 0.85rem; cursor: pointer; }
/* Left-aligned drag handle (direct request) — only THIS element carries
   draggable, not the whole row, so expanding/opening the card underneath
   still works as a plain click. */
.combat-tracker-handle { flex: 0 0 auto; padding-top: 0.5rem; cursor: grab; color: var(--text-dim); user-select: none; }
.combat-tracker-handle:active { cursor: grabbing; }
.combat-tracker-card-wrap { flex: 1 1 auto; min-width: 0; }
/* Direct follow-up request: "Display in the statblock box in the combat
   tracker the active weapon selected. have this be a link to select from
   a list of possible weapons in the entity's worn gear" — a plain
   <select>, same "a link to pick one of several things" idiom every
   other inline picker in this app already uses (the weapon catalog
   quick-fill, the Items-table catalog add, ...); nested right under
   partyMemberCard's own output inside the same card-wrap, so it reads as
   part of the statblock box rather than a separate control below it. */
.combat-tracker-active-weapon { margin-top: var(--sp-1); }
.combat-tracker-active-weapon select { width: 100%; max-width: 14rem; font-size: 0.78rem; }
/* Direct follow-up request: "update the bestiary 5lfb statblock to...
   those fields must be narrow to fit on one row in this sequence:
   Speed, Combat, Damage, Tough, and Armor." The row itself reuses
   .character-sheet-stats (already a wrap-to-fit flex row of compact
   pills — the Character Sheet's own stat row uses it too); only the
   Damage pill's own two-number-plus-slash layout and the Num+Ranged
   row's "Ranged fills the rest of the panel" behavior need new rules. */
.fiveleagues-enemy-damage .fiveleagues-enemy-damage-inputs { display: flex; align-items: center; gap: 0.2rem; }
.fiveleagues-enemy-damage-inputs input { width: 2.6rem; text-align: center; padding: 0.1rem; }
.fiveleagues-enemy-damage-sep { color: var(--text-dim); font-weight: 700; }
.fiveleagues-enemy-num-ranged-row { display: flex; align-items: flex-end; gap: var(--sp-2); margin-top: var(--sp-2); }
.fiveleagues-enemy-num-ranged-row .fiveleagues-enemy-wide-field { flex: 1 1 auto; min-width: 0; }
.fiveleagues-enemy-wide-field { display: flex; flex-direction: column; gap: 2px; }
.fiveleagues-enemy-wide-field .statblock-key { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim); }
.fiveleagues-enemy-wide-field input { width: 100%; }
.fiveleagues-enemy-aberration-stats { margin-top: var(--sp-2); }
/* "If the entity is an 'Aberration' put a small subtitle under the
   name stating 'Aberration'" — isFiveLeaguesAberrationEntity, drawers/
   index.js's entityDetail, right below .inspector-head. */
.fiveleagues-enemy-subtitle { margin: -0.2rem 0 var(--sp-2); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--accent-strong); }
/* "include the Traits or Aberrant [Aberration] Traits as separate lines
   where the title is in bold just like in the rulebook... make it a
   tooltip link to show the definition of the trait" — a hover-title
   tooltip on each bold name (cursor:help signals it, same affordance
   the Skills/Options "ⓘ" hints elsewhere already use); an unrecognized
   trait name (findFiveLeaguesEnemyTrait returned null) just has no
   title attribute at all, reading as inert bold text instead of a
   broken link. */
.fiveleagues-enemy-traits { margin-top: var(--sp-3); }
.fiveleagues-enemy-traits-list { margin-top: var(--sp-2); display: flex; flex-direction: column; gap: 0.2rem; }
.fiveleagues-enemy-trait-line { margin: 0; font-size: 0.82rem; }
.fiveleagues-enemy-trait-name { cursor: help; border-bottom: 1px dotted var(--text-dim); }

/* Full-size image lightbox (direct follow-up request) — a centered,
   backdrop-dimmed modal (same visual language as .mc-search-overlay)
   showing whatever thumbnail was clicked at its natural size, capped to
   the viewport. Closing: the ✕ button, Escape, or clicking the dimmed
   backdrop itself — that last one reads shell.js's click target directly
   (not .closest()) so a click ON the image (a descendant of the overlay)
   doesn't also trigger it, the same closest()-matches-descendants pitfall
   .mc-search-overlay's own comment already documents avoiding differently. */
.image-lightbox-overlay {
  position: fixed; inset: 0; z-index: 40;
  background: rgba(0, 0, 0, 0.75);
  display: flex; align-items: center; justify-content: center;
  padding: var(--sp-6);
}
.image-lightbox-overlay[hidden] { display: none; }
.image-lightbox-img { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: var(--radius-sm); box-shadow: var(--shadow); }
.image-lightbox-close {
  position: absolute; top: var(--sp-4); right: var(--sp-4);
  background: var(--tier3-bg); border: 1px solid var(--border-strong); color: var(--text);
  width: 2.25rem; height: 2.25rem; border-radius: 50%; font-size: 1.1rem;
}

/* Oracle: grouped, collapsible, searchable tree */
.oracle-toolbar { display: flex; gap: var(--sp-2); align-items: center; margin-bottom: var(--sp-3); }
.oracle-toolbar .drawer-search { margin-bottom: 0; flex: 1; }
.oracle-tree { display: flex; flex-direction: column; gap: 2px; }
.oracle-group { border-radius: var(--radius-sm); }
.oracle-group-head {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%; text-align: left;
  background: none; border: none; color: var(--text); padding: 0.35rem 0.4rem; border-radius: var(--radius-sm);
}
.oracle-group-head:hover { background: var(--tier2-bg); }
.oracle-toggle-caret { color: var(--text-dim); width: 1em; display: inline-block; }
.oracle-group.category > .oracle-group-head { font-weight: 600; color: var(--accent); }
.oracle-group.category { margin-top: var(--sp-2); }
.oracle-group.group { margin-left: var(--sp-3); }
.oracle-group.group > .oracle-group-head { font-size: 0.88rem; }
.oracle-group[data-roll-group] { position: relative; }
.oracle-group > [data-roll-group] { margin-left: calc(var(--sp-3) + 1.4em); margin-top: -0.3rem; }
.oracle-group-children { margin-left: var(--sp-3); }

/* Party — one full-width card per member (was a wrapping row of ~13rem-cap
   cards, which forced the per-ruleset stat rows below into their own
   horizontal scrollbar even though the drawer itself had room to spare).
   Spanning the drawer's width means a full ruleset's worth of badges
   usually needs no scrolling at all. */
.party-member-list { display: flex; flex-direction: column; gap: var(--sp-2); margin: var(--sp-2) 0 var(--sp-3); }
.party-member-card {
  background: var(--tier2-bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: var(--sp-3); width: 100%; box-sizing: border-box;
}
.party-member-card:hover { border-color: var(--accent); }
/* Thumbnail (.actor-thumb-photo — same Composer/WHO convention, direct
   request) on the left, name (a button standing in for the collapse
   toggle) next, then the right-aligned headline counters, then the ↗
   open-in-editor icon (UX batch — the whole card used to open the editor
   on any click, which fought with a plain collapse click on the name).
   The name gets flex:1 1 auto (the row's one growable item) so it both
   claims left-aligned space AND pushes everything after it — counters,
   icon-btn — to the row's right edge; min-width:0 + ellipsis lets a long
   name truncate instead of squeezing the counters off narrow widths. */
.party-member-row { display: flex; align-items: center; gap: var(--sp-2); }
.party-member-thumb-btn { flex: 0 0 auto; background: none; border: none; padding: 0; }
.party-member-thumb-btn .actor-thumb-photo { width: 2rem; height: 2rem; }
.party-member-name {
  background: none; border: none; padding: 0; margin: 0; font: inherit; font-weight: 600; color: inherit; cursor: pointer;
  text-align: left; flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.party-member-row .icon-btn { flex: 0 0 auto; margin-left: auto; }
/* Direct request: "size the boxes smaller to the scale of the statblock
   boxes" — smaller than even .track-row-compact's own 18px, since several
   of these need to fit inline on a name row rather than their own line.
   overflow-x is the same escape valve every other tight row in this app
   already falls back on if a GM configures several headline fields. */
.party-headline-counters { display: flex; align-items: center; gap: var(--sp-2); flex: 0 0 auto; overflow-x: auto; max-width: 60%; }
.party-headline-counter { display: flex; align-items: center; gap: 3px; flex: 0 0 auto; }
.party-headline-counter-label { font-size: 0.58rem; letter-spacing: 0.04em; color: var(--text-dim); text-transform: uppercase; white-space: nowrap; }
/* Direct follow-up request: "the single value total that is at the end of
   the counter" — a plain number badge, replacing the earlier row-of-boxes
   attempt entirely. */
.party-headline-value { font-size: 0.72rem; padding: 1px 6px; }
/* Expanded Party Roster card's stat fields (direct follow-up correction —
   "simple stat (1st line) and value (2nd line) in a button-like box that
   is clickable to roll dice but not editable"): same bordered-pill
   language .attr-row-compact/.track-row-compact already established for a
   compact stat box, just standing alone as a real <button> (or a <span>
   for a non-rollable/text field) instead of wrapping an editable input.
   The button element IS the whole pill (label + value both sit inside
   one <button data-party-stat-roll>, drawers/index.js's partyStatPill) —
   the entire box is the click target, not just the label text the way
   attrRow's own roll trigger works in the main Entity Editor — direct
   request, specifically to make it a bigger, easier target here since
   there's no competing "click to edit" affordance on a read-only pill.
   Sizing (direct follow-up request — "double the size of just one box in
   a meter/counter like Health"): a single .track-box is 22px/1.375rem at
   its base size, so this pill's width is fixed at exactly double that —
   a real, deliberately small "consolidated and minimal" footprint (down
   from an earlier, more arbitrary 4.4rem), not the compact character-
   sheet pill's own wider 5.2rem. A longer field name (e.g. "Momentum")
   wraps across two lines (white-space:normal below) rather than pushing
   the box wider. */
.party-stat-group { margin-bottom: var(--sp-2); }
.party-stat-group-label { font-size: 0.6rem; letter-spacing: 0.08em; color: var(--text-dim); text-transform: uppercase; margin-bottom: 2px; }
/* "narrow the button-only statblock fields... so that all five attribute
   fields fit on one line and then all five tracker counters fit on one
   line" was fixed by SPLITTING attributes and tracks into two separate
   rows (drawers/index.js's partyMemberStatblocks) — a mixed 9+-pill row
   was the real cause of wrapping, not 5 alone. A direct follow-up then
   asked for the pill size itself back to its original larger scale (the
   split alone was enough; narrowing on top of it wasn't wanted), so the
   size/padding/font values below are back to what they were before that
   narrowing pass. */
.party-stat-pills { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.party-stat-pill {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px;
  width: 2.75rem; min-height: 2.75rem; padding: 0.15rem 0.2rem; box-sizing: border-box;
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-inset); color: var(--text);
  font: inherit;
}
button.party-stat-pill { cursor: pointer; }
button.party-stat-pill:hover { border-color: var(--accent); }
/* A track pill currently toggled open (direct follow-up request —
   "clicking... acts as a toggle") — same accent-border cue an active
   toggle uses elsewhere in this app, so it's visually obvious which
   pill's tracker is the one showing below. */
.party-stat-pill-expanded { border-color: var(--accent-strong); background: rgba(56,189,248,0.1); }
.party-stat-pill-static { opacity: 0.85; }
/* Direct follow-up request: "revise the tracking meters to have the same
   width as before where the tracking numbers are on one row instead of a
   small block of two rows... the narrow statblock is just for the number
   stats like Edge and Shadow" — a tracker's own name (Health/Spirit/
   Supply) doesn't fit a tiny stacked square the way a one-letter attribute
   modifier does, so it gets its own wider, single-row layout instead of
   .party-stat-pill's default narrow stacked square. */
.party-stat-pill-track { flex-direction: row; width: auto; min-height: 0; padding: 0.3rem 0.55rem; gap: 0.4rem; }
.party-stat-pill-track .party-stat-pill-label { white-space: nowrap; text-align: left; }
.party-stat-pill-label { font-size: 0.56rem; letter-spacing: 0.03em; color: var(--text-dim); text-transform: uppercase; text-align: center; line-height: 1.1; white-space: normal; }
.party-stat-pill-value { font-size: 0.85rem; font-weight: 700; color: var(--accent); }
/* Direct follow-up request: "reduce padding and space between statblocks
   so all six blocks are on the same row" — scoped to the Combat Tracker's
   own card wrapper only (the Party Roster's own expanded card keeps the
   normal, larger pill size above); combined with the shortened "Combat"/
   "Tough" labels (partyStatPill, drawers/index.js) this is what actually
   lets Agility/Speed/Combat/Tough/Armor/Casting fit one row in the
   narrower space a tracker row leaves (handle + remove button also take
   width there). */
.combat-tracker-card-wrap .party-stat-group { margin-bottom: 0.25rem; }
.combat-tracker-card-wrap .party-stat-pills { gap: 0.2rem; }
.combat-tracker-card-wrap .party-stat-pill { width: 2.05rem; min-height: 2.05rem; padding: 0.05rem; }
.combat-tracker-card-wrap .party-stat-pill-label { font-size: 0.48rem; }
.combat-tracker-card-wrap .party-stat-pill-value { font-size: 0.7rem; }
/* A toggled-open track pill's full box-row view (partyStatTrackExpansion,
   drawers/index.js) — "the tracker for that one tracker is displayed in
   the next row." One shared column below the tracks row so several open
   at once stack cleanly instead of each pushing the next pill row down
   further. */
.party-stat-track-expansions { display: flex; flex-direction: column; gap: var(--sp-1); margin-top: var(--sp-1); }
.party-stat-track-expansion {
  display: flex; align-items: center; gap: var(--sp-2); padding: 0.25rem 0.4rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-inset);
  min-width: 0; /* lets the child .track-boxes' own overflow-x actually engage inside a flex row */
}
.party-stat-track-expansion-label { font-size: 0.62rem; letter-spacing: 0.04em; color: var(--text-dim); text-transform: uppercase; white-space: nowrap; }
/* Direct follow-up request: "trackers such as Momentum with 10 pills must
   keep all 10 pills on the same row" — .track-boxes itself never wraps
   (flex-wrap defaults to nowrap), but this expansion had been reusing
   .party-tracker-boxes (the PARTY-LEVEL Party Trackers section's own
   class), whose flex-wrap:wrap + max-width:14rem was wrapping a 10-box
   Momentum row onto two lines. A dedicated class instead — no wrap ever,
   overflow-x:auto as the escape valve for a custom ruleset with an even
   larger max, same posture .character-sheet-stats already uses. */
.party-stat-track-boxes { flex-wrap: nowrap; overflow-x: auto; }
.party-tracker-list { display: flex; flex-direction: column; gap: var(--sp-2); }
.party-tracker-row { display: grid; grid-template-columns: 1fr auto auto auto; gap: var(--sp-2); align-items: center; }
.party-tracker-name-input { min-width: 0; }
/* Direct follow-up request: the tracker's name reads as a plain, fully-
   sized label by default (view mode — no ✕, no input-box chrome) —
   wrapping rather than the old always-editable input's single-line
   truncation/scroll. Edit mode (the section's shared pencil toggle) still
   renders the original input+✕ row unchanged. */
.party-tracker-row-view { grid-template-columns: 1fr auto auto; }
.party-tracker-name-label { font-weight: 600; white-space: normal; overflow: visible; word-break: break-word; }
/* Both +/- buttons sit after the field, "+" then "-" (direct follow-up
   request — previously "+" sat before the field and "-" after it); gap
   tightened and the buttons' own horizontal padding trimmed (direct
   follow-up request: "move the +/- closer [to] the text box") so they
   read as part of the same control instead of sitting apart from it. */
.party-tracker-counter { display: inline-flex; align-items: center; gap: 2px; white-space: nowrap; }
.party-tracker-counter .icon-btn { padding-left: 0.15rem; padding-right: 0.15rem; }
.party-tracker-value-input { width: 3.2rem; text-align: center; }
/* Direct follow-up request: "text fields... without the value changer
   built into the text field... just use the +/- [buttons]," then
   generalized ("use this number counter textbox as the default for all
   textboxes used for numbers") — EVERY number input in the app suppresses
   the browser's own native spin arrows now, since numStepper's own +/-
   buttons (drawers/index.js) are the intended stepper everywhere a number
   field appears, not just Party Trackers. */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
input[type="number"] { -moz-appearance: textfield; }
/* numStepper's own wrapper (drawers/index.js) — the same tight, no-gap
   "+/- flanking a plain textbox" look Party Trackers' counter fields
   established, generalized to every number field in the app. */
.num-stepper { display: inline-flex; align-items: center; gap: 2px; }
.num-stepper .icon-btn { padding-left: 0.15rem; padding-right: 0.15rem; }
.num-stepper input[type="number"] { width: 3.2rem; text-align: center; }
/* Type is fixed once a tracker exists (see domain/party.js) — a plain
   caption, not a control, replaces the old per-tracker kind <select>. */
.party-tracker-kind-label { font-size: 0.7rem; color: var(--text-dim); white-space: nowrap; }
.party-tracker-boxes { flex-wrap: wrap; max-width: 14rem; }
/* Momentum-style gauge (domain/party.js's gaugeWindow) — a single current
   POSITION, not a fill-from-zero count, so boxes are slightly wider than a
   plain meter's to fit a negative 2-character value like "-6"; the box
   whose number is 0 (Momentum's reset point) gets a distinguishing border
   so it stays a visible landmark as the 10-box window slides. */
.party-tracker-gauge .track-box { width: 26px; }
.party-tracker-gauge .track-box.gauge-zero { border-color: var(--accent); border-width: 2px; }
.party-tracker-gauge .track-box.gauge-zero.on { border-color: #0b1120; }
/* Inline "+ Tracker" creation form (replaces a window.prompt() popup) — one
   row: name, type, a type-specific field (meter size / Starforged
   difficulty), Create, cancel. */
.party-tracker-add-form { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; margin: var(--sp-2) 0; padding: var(--sp-2); background: var(--tier2-bg); border: 1px solid var(--border); border-radius: var(--radius-sm); }
.party-tracker-add-form .party-tracker-draft-name { flex: 1 1 10rem; min-width: 0; }
.party-tracker-draft-size { display: inline-flex; align-items: center; gap: var(--sp-1); font-size: 0.7rem; color: var(--text-dim); white-space: nowrap; }
.party-tracker-draft-max-input { width: 3.2rem; text-align: center; }

/* Colony */
.colony-fields { display: grid; grid-template-columns: 1fr 1fr; row-gap: var(--sp-2); column-gap: var(--sp-3); margin: var(--sp-2) 0 var(--sp-3); }
.colony-fields .field-label { margin-top: 0; }
/* :has(textarea) alone never matched — Colony's own textarea-type fields
   (Condition Notes, Notes) render as a contenteditable .rich-field div, not
   a real <textarea>, so both sat side by side in the 2-col grid instead of
   each getting its own full-width row (direct report: "make Condition
   Notes and Notes separate rows"). */
.colony-fields .field-label:has(textarea), .colony-fields .field-label:has(.rich-field) { grid-column: 1 / -1; }
/* Campaign Turn + Turn Step widget, plus Step Text right below (direct
   follow-up request: "move Campaign Turn, Turn Step and the... turn step
   description above the Colony Turn Sheet section header") — this cluster
   now renders BEFORE that collapsible section instead of inside its
   .colony-fields grid, so it stays visible even with that section
   collapsed; neither needs a grid-column span any more since they're not
   grid children at all now, just plain block-level siblings above it.
   Campaign Turn + Turn Step (direct follow-up request: "move the Turn Step
   button more to the left, aligned to Campaign Turn") are a flex row
   packing the two fields together by their actual content width, instead
   of each sitting in its own 1fr grid column (which used to pin Turn Step
   to the fixed 50% column line regardless of how narrow Campaign Turn's
   own content was, leaving a big dead gap between them). align-items:
   flex-end lines up the number-stepper and the ◂/▸ nav on the same
   baseline the way the two grid columns used to. flex-wrap is deliberately
   nowrap (direct follow-up request — it was dropping Turn Step to its own
   row on a narrow panel): .turn-step-current already wraps its OWN text
   internally past ~30 characters, so the row itself never needs the extra
   width a wrap would have bought it — min-width:0 on the Turn Step
   field-label lets it actually shrink that far instead of refusing to go
   below its content's natural min-content width. */
.colony-turn-row { display: flex; align-items: flex-end; gap: var(--sp-6); flex-wrap: nowrap; }
.colony-turn-row > .field-label:last-child { min-width: 0; flex: 0 1 auto; }
.colony-crew-list { display: flex; flex-direction: column; gap: var(--sp-2); }
/* Direct follow-up request: the character column is now a Composer-style
   thumbnail (crewCharacterThumb, drawers/index.js) instead of a <select> —
   content-sized (auto) rather than stretched to 1fr, so it doesn't leave a
   wide empty gutter around the narrow thumb; the role <select> takes the
   rest of the row unchanged. */
.colony-crew-row { display: grid; grid-template-columns: auto 1fr auto; gap: var(--sp-2); align-items: center; }
/* Unassigned crew slot — same circle footprint as a filled .actor-thumb-
   photo (2.75rem) so the row's height/alignment doesn't jump once a
   character is picked. */
.actor-thumb-add {
  width: 2.75rem; height: 2.75rem; border-radius: 50%;
  border: 2px dashed var(--border); background: none; color: var(--text-dim);
  font-size: 1.1rem; line-height: 1; cursor: pointer;
}
.actor-thumb-add:hover { border-color: var(--accent); color: var(--accent); }
/* Direct follow-up request: a fixed 10-row grid (numbered index, no more
   add/remove-row controls) instead of a growable list; the note field is a
   plain expandable <textarea> (native resize:vertical drag handle — direct
   follow-up request: "expandable... but just text and not a texteditor
   formatting field", so no .mention-editor/toolbar here at all), starting
   at a single line and growing only as far as the GM drags it. */
.colony-encounter-list { display: flex; flex-direction: column; gap: var(--sp-2); }
.colony-encounter-row { display: flex; align-items: flex-start; gap: var(--sp-2); }
/* Direct follow-up request: reproduces the rulebook's own "Campaign
   Lifeform Encounters table" three-column shape (% range / no. / entry) —
   this new range column, plus the existing index column, both sit at
   their own fixed width so they read as a stable table rather than
   shifting per row; padding-top on both aligns them with the taller
   thumbnail's own top edge instead of its vertically-centered name label. */
.colony-encounter-range { flex: 0 0 auto; padding-top: 0.4rem; font-size: 0.75rem; color: var(--text-dim); min-width: 2.6rem; text-align: right; }
.colony-encounter-index { flex: 0 0 auto; padding-top: 0.4rem; font-size: 0.75rem; color: var(--text-dim); min-width: 1.4rem; text-align: right; }
.colony-encounter-row textarea { flex: 1 1 auto; min-width: 0; resize: vertical; overflow-y: auto; }
.entity-chip-wrap { display: inline-flex; align-items: center; gap: 2px; }
/* The Lifeform Encounters picker's own "Create New Lifeform" row (direct
   follow-up request) — visually distinguished from a plain existing-entity
   candidate below it, so rolling up a brand new creature never reads as
   picking one already named that. */
.entity-picker-row-create { color: var(--accent-strong); font-weight: 600; }
/* Direct follow-up request: "remove link to this [entity type]" — the
   Hexcrawl pickers' own escape hatch for backing out of a placed-but-
   unlinked map icon, styled like .entity-picker-row-create's own standout
   treatment but in the danger color instead of the accent, so it doesn't
   get mistaken for another candidate in the list. LATER direct follow-up
   request: "move the 'X remove link to [entity type]' to the same row as
   the 'Create new [entity type]'" — .entity-picker-row-pair (below) is
   what puts them side by side; the divider that used to live on each
   button's own border now lives on the pair wrapper instead. */
.entity-picker-row-remove { color: var(--danger); }
.entity-picker-row-pair { display: flex; gap: var(--sp-2); border-bottom: 1px solid var(--border); margin-bottom: 0.2rem; padding-bottom: 0.6rem; }
.entity-picker-row-pair .entity-picker-row { flex: 1 1 0; width: auto; text-align: center; }

/* Turn Step widget (Colony) + Step Text row (design/adr/rules-profiles-
   multi-campaign.md) */
/* width:100% + justify-content:flex-start (direct follow-up request) — the
   .colony-fields grid column stretches this flex item to the full column
   width (a bare inline-flex still gets stretched as a FLEX ITEM by its
   parent's default align-items:stretch, even though inline-flex only
   governs its OWN children's layout), so without an explicit left-justify
   the ◂/name/▸ group read as centered in that column instead of flush with
   the "Turn Step" label above it — this also gives a long group name room
   to expand rightward instead of staying visually boxed to a short width. */
.turn-step-nav { display: inline-flex; align-items: center; justify-content: flex-start; gap: var(--sp-2); width: 100%; }
/* Wraps once the group label + position (e.g. "Settlement Event Steps
   12/17") runs past ~40 characters, instead of forcing every label onto one
   line regardless of length (direct follow-up request, tuned up from 14,
   18, then 30) — 40ch approximates character count using the font's own
   "0" glyph width. display: inline-block is load-bearing: max-width/
   white-space:normal wrapping has no effect on a <b>'s default inline
   display otherwise. text-align left (direct follow-up request) so a
   wrapped second line stays flush with the first instead of centering. */
.turn-step-current { display: inline-block; max-width: 40ch; white-space: normal; text-align: left; }
.turn-step-text-row { display: flex; align-items: flex-start; gap: var(--sp-3); margin: 0 0 var(--sp-3); }
.turn-step-text-body {
  flex: 1 1 auto; min-width: 0;
  font-size: 0.8rem; padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.turn-step-text-row .btn { flex: 0 0 auto; }

/* Navigator workflow manager (direct follow-up request: bring the
   Campaign panel's own Turn Step walkthrough into the Navigator as a
   second, parallel way to step through it) — .workflow-step-card reuses
   the Campaign panel's own .turn-step-* classes above for its nav row/
   current-step label/text row verbatim, plus its own border+spacing
   since it's not living inside a .colony-fields grid here.
   .navigator-workflow itself (direct follow-up request: "put a box around
   both workflows and shade it slightly differently to stand out from
   other options") gets its own bordered/shaded panel — --bg-raised rather
   than .oracle-links-block's own --bg-inset, so the two boxed areas read
   as distinct from each other, not just from the unboxed sections around
   them. No longer carries .threads (that class's own margin-top/border-
   top "separator from the section above" treatment would double up with
   a full box border) — its own margin-top below covers the same spacing
   job now that Scene Summary sits directly above it (moved there per the
   same follow-up request). */
.navigator-workflow { margin-top: var(--sp-4); padding: var(--sp-3); background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius-sm); }
.navigator-workflow .workflow-step-card + .workflow-step-card { margin-top: var(--sp-4); padding-top: var(--sp-3); border-top: 1px solid var(--border); }
.workflow-step-card .btn { margin-top: var(--sp-2); }
/* One collapsible header per Game System when 2+ workflows show at once
   (showHeaders, workspace/index.js) — plain spacing between them; each
   one's own toggle button reuses .actor-group-toggle/.field-label-static
   verbatim (WHO/WHERE's own collapsible-group header styling) so it reads
   as the same interaction pattern, not a one-off. */
.workflow-group + .workflow-group { margin-top: var(--sp-3); padding-top: var(--sp-2); border-top: 1px solid var(--border); }
.workflow-group .workflow-step-card { margin-top: var(--sp-2); }
.turn-step-map-row { display: flex; align-items: center; gap: var(--sp-2); margin: 0 0 var(--sp-2); }
/* "Helpful Oracles" (Navigator workflow card, direct follow-up request:
   "display all relevant oracles that might be helpful for that scene...
   group them all under a section header 'helpful oracles'") — a compact
   chip-wrap list, same .chip.sm treatment every other oracle-tag-link
   chip in this app already uses. Direct follow-up request: "Put a panel-
   wide box around helpful and supplemental oracles to make it stand out.
   Do not have a subheader called Supplemental oracles. Just show the
   oracles provided" — hand-tagged and supplemental matches are now one
   flat chip list (workspace/index.js's helpfulOraclesHtml), and the block
   itself is a real bordered/background box spanning the full card width
   instead of a bare margin wrapper, so it reads as a distinct panel. */
.oracle-links-block { margin: 0 0 var(--sp-2); padding: var(--sp-2) var(--sp-3); background: var(--bg-inset); border: 1px solid var(--border); border-radius: var(--radius-sm); }
.oracle-links-head { font-size: 0.75rem; font-weight: 600; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.02em; margin-bottom: var(--sp-1); }
.oracle-links-chips { display: flex; flex-wrap: wrap; gap: var(--sp-1); }

/* Turn Step Settings tab — each group's reorderable step list. flex-wrap
   (direct follow-up request: "revise the 'Move To' and 'Branches to'
   dropdowns to be right-aligned icons that open a dropdown on the next
   row if clicked into edit mode") is what lets .turn-step-edit-row below
   force itself onto a fresh line without a separate grid/absolute layout —
   same flex-basis:100%-forces-a-wrap technique this app already uses for
   the rich-text toolbar's own reflow. */
.turn-step-list { display: flex; flex-direction: column; gap: var(--sp-2); }
.turn-step-row { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--sp-2); }
.turn-step-order { display: flex; flex-direction: column; flex: 0 0 auto; }
.turn-step-row textarea { flex: 1 1 auto; min-width: 0; }
/* Per-step "attach Crew Tasks" checkbox (direct follow-up request: "make
   the Crew Tasks a reusable functionality for other Turn Steps to be
   added later") — a plain inline checkbox+label. */
.turn-step-crew-tasks-toggle { flex: 0 0 auto; display: flex; align-items: center; gap: 0.3rem; white-space: nowrap; padding-top: var(--sp-1); cursor: pointer; }
/* Right-aligned "Move to"/"Branches to" icon pair (direct follow-up
   request, replacing the old always-visible inline dropdowns) — margin-
   left:auto pushes this to the row's right edge, same "icon button motif"
   every other make-a-change trigger in this app already uses. .active
   (a step that already has a branchTo set, or whichever icon's own editor
   is currently open) gets the same accent-border treatment other active
   toggles use. */
.turn-step-row-actions { flex: 0 0 auto; display: flex; gap: 0.2rem; margin-left: auto; align-self: flex-start; }
.turn-step-row-actions .icon-btn.active { border-color: var(--accent-strong); color: var(--accent-strong); }
/* The revealed dropdown itself — forced onto its own full-width row below
   the main one via flex-basis:100% (see .turn-step-row's flex-wrap above). */
.turn-step-edit-row { flex: 1 1 100%; margin-top: var(--sp-1); }

/* Crew Tasks (design/adr/rules-profiles-multi-campaign.md, direct
   follow-up request) — Settings tab reuses .turn-step-row/-order/-list
   wholesale (a flat list, same reorder-row shape, just no .turn-step-branch
   slot); .crew-task-settings-row-body stacks a task's label above its
   editable text within that shared row instead. */
.crew-task-settings-row-body { flex: 1 1 auto; min-width: 0; }
.crew-task-settings-row-body p { margin: 0 0 0.2rem; }

/* Colony's Crew Tasks box (attached per-step via showCrewTasks, direct
   follow-up request) — sits directly below Step Text, both now rendered
   above .colony-fields entirely (see the .colony-turn-row comment above),
   so neither needs a grid-column span any more. */
.crew-task-box { margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid var(--border); }
.crew-task-box h4 { margin: 0 0 var(--sp-2); font-size: 0.85rem; color: var(--accent); }
/* Crew Tasks table: one row per Party member (task picker, ▶ Start), the
   picked task's rules on a full-width row beneath it. */
.crew-task-steps { margin: 0 0 var(--sp-2); padding-left: 1.2rem; }
.crew-task-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
.crew-task-table th { text-align: left; font-size: 0.7rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-dim); padding: 0 var(--sp-1) var(--sp-1); }
.crew-task-table td { padding: var(--sp-1); vertical-align: middle; border-top: 1px solid var(--border); }
.crew-task-table .crew-task-rules-row td { border-top: none; padding-top: 0; }
.crew-task-table .crew-task-rules-row .crew-task-description { margin-top: 0; }
.crew-task-table select { width: 100%; }
.crew-task-member { font-weight: 600; white-space: nowrap; }
.crew-task-action { text-align: right; white-space: nowrap; width: 1%; }
.crew-task-row-done .crew-task-member { color: var(--text-dim); }
.crew-task-done { color: var(--ok); border-color: var(--ok); }
.crew-task-description { margin-top: var(--sp-2); padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-inset); }

/* Trade (Merchant Rules Lens, ADR 0003/0004) */
.trade-market-table { width: 100%; border-collapse: collapse; margin: var(--sp-2) 0 var(--sp-3); font-size: 0.85rem; }
.trade-market-table th { text-align: left; font-size: 0.7rem; letter-spacing: 0.06em; color: var(--text-dim); text-transform: uppercase; padding: 0.3rem 0.4rem; border-bottom: 1px solid var(--border); }
.trade-market-table td { padding: 0.3rem 0.4rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
.trade-dial-input { width: 3.6rem; text-align: center; }
.trade-price { font-weight: 700; color: var(--accent-strong); }
.trade-buy-sell { display: flex; align-items: center; gap: var(--sp-1); white-space: nowrap; }
.trade-qty-input { width: 3rem; text-align: center; }
.trade-manifest-list { display: flex; flex-direction: column; gap: 2px; margin: var(--sp-2) 0 var(--sp-3); }
.trade-manifest-row { display: flex; justify-content: space-between; padding: 0.25rem 0.4rem; background: var(--tier2-bg); border: 1px solid var(--border); border-radius: var(--radius-sm); }
.trade-contract-head-actions { display: flex; gap: var(--sp-2); }
.trade-contract-add-form { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; margin: var(--sp-2) 0; padding: var(--sp-2); background: var(--tier2-bg); border: 1px solid var(--border); border-radius: var(--radius-sm); }
.trade-contract-add-form .trade-contract-draft-name { flex: 1 1 10rem; min-width: 0; }
.trade-contract-list { display: flex; flex-direction: column; gap: var(--sp-2); margin-top: var(--sp-2); }
.trade-contract-row {
  display: grid; grid-template-columns: 1fr auto auto auto auto; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2); background: var(--tier2-bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.trade-contract-row.done .trade-contract-name { text-decoration: line-through; color: var(--text-dim); }
.trade-contract-head { grid-column: 1 / -1; display: flex; align-items: center; gap: var(--sp-2); }
.trade-contract-name { background: none; border: none; padding: 0; margin: 0; font: inherit; font-weight: 600; color: inherit; cursor: pointer; text-align: left; }
.trade-contract-payout { margin-left: auto; color: var(--accent-strong); font-weight: 700; }
.trade-contract-route { grid-column: 1 / -1; display: flex; align-items: center; gap: var(--sp-2); }
/* Description, Conflict, Opportunity, then the tracker controls (clock,
   status, priority, advance/set back/remove) — one full-width line each. */
.trade-contract-row > .field-label { grid-column: 1 / -1; margin: 0; }
.trade-contract-row > .field-label textarea { display: block; width: 100%; box-sizing: border-box; }
.trade-contract-tracker-row { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }

/* Guide (docs/adr/0017: multi-doc tree, was one freeform field). Direct
   follow-up request ("move the open/close arrow to the editor icons to the
   right-aligned of the field label"): the title input and the rich-text
   toolbar's collapse toggle now share this row, toggle right-aligned. */
.guide-doc-head { margin-top: var(--sp-3); display: flex; align-items: center; gap: var(--sp-2); }
.guide-title-input { flex: 1 1 auto; min-width: 0; box-sizing: border-box; font-size: 1rem; font-weight: 600; padding: 0.35rem 0.5rem; background: var(--bg-inset); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius-sm); }
/* resize:vertical/overflow-y:auto now come from the base .mention-editor
   rule (every rich field gained a resize handle, direct follow-up
   request) — this just keeps Guide's own taller starting height. */
.guide-editor { min-height: 16rem; font-family: var(--font); }
.guide-tree-section { margin-top: var(--sp-4); border-top: 1px solid var(--border); padding-top: var(--sp-3); }
.guide-tree-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--sp-2); }
.guide-tree-head h4 { margin: 0; font-size: 0.85rem; color: var(--accent); }
.guide-tree-drop-root { padding: 0.4rem 0.6rem; margin-bottom: var(--sp-2); border: 1px dashed var(--border-strong); border-radius: var(--radius-sm); font-size: 0.8rem; color: var(--text-dim); text-align: center; }
.guide-tree { display: flex; flex-direction: column; gap: 1px; }
.guide-tree-row { display: flex; align-items: center; gap: var(--sp-1); padding: 0.3rem 0.4rem; border-radius: var(--radius-sm); cursor: grab; }
.guide-tree-row:active { cursor: grabbing; }
.guide-tree-row:hover { background: var(--tier2-bg); }
.guide-node-caret, .guide-node-caret-spacer { width: 1.1rem; flex: none; background: none; border: none; color: var(--text-dim); text-align: center; padding: 0; }
.guide-node-title { flex: 1; text-align: left; background: none; border: none; color: var(--text); font-size: 0.88rem; padding: 0.1rem 0.2rem; cursor: pointer; }
.guide-node-title.sel { color: var(--accent-strong); font-weight: 600; }
.guide-node-actions { display: inline-flex; gap: 2px; align-items: center; margin-left: auto; }

/* Documents: tags, search, link-style name, rename */
.doc-card-title-link, .doc-card-title-static { font-size: 0.92rem; font-weight: 600; color: var(--text); }
.doc-card-title-link { color: var(--accent); text-decoration: none; cursor: pointer; }
.doc-card-title-link:hover { text-decoration: underline; }
/* Small footprint by design: the tag row only renders at all when its 🏷
   toggle (left of the ✎ rename icon, data-doc-tag-toggle/data-ref-tag-toggle)
   is on, and the chips themselves are deliberately smaller than a regular
   .chip — tighter vertical padding/gap than the rest of the card so a
   record's tags read as a quiet footnote, not another UI row. */
.doc-card-tags { display: flex; flex-wrap: wrap; gap: 0.25rem; align-items: center; margin-top: 0.25rem; }
.doc-tag-chip { display: inline-flex; align-items: center; gap: 2px; background: var(--bg-inset); border: 1px solid var(--border); border-radius: 999px; padding: 0.03rem 0.4rem; font-size: 0.65rem; line-height: 1.4; color: var(--text-dim); }
.doc-tag-input { width: 7rem; padding: 0.15rem 0.4rem; font-size: 0.65rem; }
.doc-tag-filter-chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-bottom: var(--sp-3); }
.chip.active { border-color: var(--accent); color: var(--accent); background: rgba(56,189,248,0.1); }
.entity-type-filter-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: var(--sp-2) 0; }

/* Cast drawer tag sub-filter + gear catalog picker (ADR 0012) */
.entity-tag-filter { margin: var(--sp-2) 0; }
.entity-tag-filter-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-2); }
.catalog-picker { border: 1px solid var(--border); border-radius: var(--radius-sm); margin: var(--sp-2) 0; padding: var(--sp-2); background: var(--bg-inset); }
.catalog-picker-head { display: flex; gap: var(--sp-2); align-items: center; }
.catalog-picker-head .drawer-search { flex: 1; }
.catalog-picker-list { max-height: 16rem; overflow-y: auto; margin-top: var(--sp-2); display: flex; flex-direction: column; gap: 2px; }
.catalog-item-row { display: flex; align-items: center; gap: var(--sp-2); width: 100%; text-align: left; padding: 0.3rem 0.5rem; border-radius: var(--radius-sm); border: 1px solid transparent; background: none; cursor: pointer; }
.catalog-item-row:hover { border-color: var(--border); background: var(--bg-raised); }
.catalog-item-name { font-weight: 600; }
.catalog-item-tags { display: flex; flex-wrap: wrap; gap: 2px; margin-left: auto; }
.ref-doc-card { padding: 0.5rem var(--sp-3); }
.ref-doc-card .doc-card-head { margin-bottom: 0; }

/* Relationship note (editable label) */
.rel-label-input { width: 8rem; padding: 0.15rem 0.4rem; font-size: 0.72rem; background: var(--bg-inset); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text-dim); }

/* Non-rollable track badge (Bestiary progress-bar field) */
.track-value-badge-static { opacity: 0.85; cursor: default; }

/* Settings: Bestiary statblock template editor */
.tpl-system-list { display: flex; flex-direction: column; gap: var(--sp-2); margin: var(--sp-2) 0; }
.tpl-system { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--sp-2) var(--sp-3); }
.tpl-system summary { cursor: pointer; font-size: 0.85rem; color: var(--accent); }
.tpl-field-list { margin-top: var(--sp-2); display: flex; flex-direction: column; gap: 4px; }
.tpl-field-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; padding: 0.3rem 0; border-top: 1px solid var(--border); }
.tpl-field-key-input { width: 8rem; }
.tpl-field-max { display: inline-flex; align-items: center; gap: 4px; font-size: 0.7rem; color: var(--text-dim); }
.tpl-field-max input { width: 3.5rem; }
.tpl-field-actions { margin-left: auto; display: inline-flex; gap: 2px; }

/* A right-nav icon re-selecting its already-open panel (shell.js's
   focusDrawerPanel): a brief accent outline where focus landed. */
.panel-focus-flash { outline: 2px solid var(--accent); outline-offset: -2px; transition: outline-color 0.8s ease-out; }
[data-drawer-body]:focus, .storyboard-grid [tabindex="-1"]:focus, [data-copilot]:focus { outline: none; }
.panel-focus-flash:focus { outline: 2px solid var(--accent); }

/* Entity Editor: statblocks the Bestiary <-> Character swap kept but made
   inactive — listed, each removable. */
.statblock-inactive { margin: var(--sp-2) 0; padding: var(--sp-2); border: 1px dashed var(--border); border-radius: var(--radius-sm); }
.statblock-inactive h4 { margin: 0 0 var(--sp-1); }
.statblock-inactive-row { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-2); }
.statblock-inactive-row + .statblock-inactive-row { margin-top: var(--sp-2); }
