@charset "UTF-8";.btn {
  padding: 0.3125rem 1.6875rem;
  min-height: 2.5rem;
  display: inline-flex;
  align-items: center;
  justify-content: flex-start;
  gap: 0.5rem;
  font-size: 0.7525rem;
  font-weight: 700;
  line-height: 1;
  text-align: center;
  white-space: nowrap;
  border-radius: 0.375rem;
  background-color: hsl(214, 97.6%, 50.6%);
  outline: none;
  color: hsl(240, 20%, 98%);
  transition: all 0.125s;
  cursor: pointer;
}
.btn:hover:not([disabled]), .btn:focus:not([disabled]), .btn:active:not([disabled]) {
  background-color: hsl(214, 98%, 40%);
}
.btn[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}
.btn__icon {
  height: 1;
}
.btn--secondary {
  color: hsl(214, 97.6%, 50.6%);
  background-color: hsl(240, 20%, 98%);
  border: 2px solid currentColor;
}
.btn--secondary:hover:not([disabled]), .btn--secondary:focus:not([disabled]), .btn--secondary:active:not([disabled]) {
  color: hsl(214, 98%, 40%);
  background-color: hsl(240, 20%, 98%);
}
.btn--link, .btn--link-small, .btn--link-indent {
  background-color: transparent !important;
  font-size: 0.7525rem;
  min-height: auto;
  padding: 0;
  text-decoration: none;
  border-radius: 0;
  color: hsl(228, 20%, 4.9%);
}
.btn--link:hover:not([disabled]), .btn--link-small:hover:not([disabled]), .btn--link-indent:hover:not([disabled]), .btn--link:focus:not([disabled]), .btn--link-small:focus:not([disabled]), .btn--link-indent:focus:not([disabled]), .btn--link:active:not([disabled]), .btn--link-small:active:not([disabled]), .btn--link-indent:active:not([disabled]) {
  color: hsl(214, 98%, 40%);
}
.btn--link:not([disabled]) .icon, .btn--link-small:not([disabled]) .icon, .btn--link-indent:not([disabled]) .icon {
  color: hsl(214, 97.6%, 50.6%) !important;
}
.btn--link-indent {
  background-color: transparent !important;
  padding-left: 0.375rem;
}
.btn--link-small {
  background-color: transparent !important;
  font-size: 0.875rem;
  color: hsl(227, 17%, 50%);
}
.btn--link-small:hover:not([disabled]), .btn--link-small:focus:not([disabled]), .btn--link-small:active:not([disabled]) {
  color: hsl(228, 20%, 4.9%);
}
.btn--link-small:not([disabled]) .icon {
  color: currentColor !important;
}
.btn--icon {
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  justify-content: center;
}
.btn--block {
  width: 100%;
  justify-content: center;
}
.btn--sm {
  font-size: 0.645rem;
  padding: 0.25rem 0.75rem;
}
.btn--danger {
  background-color: hsl(349, 83.9%, 46.3%);
  color: hsl(240, 20%, 98%);
  border: 1px solid hsl(349, 83.9%, 46.3%);
}
.btn--danger:hover:not([disabled]), .btn--danger:focus:not([disabled]), .btn--danger:active:not([disabled]) {
  background-color: hsl(349, 83.9%, 36%);
}

.xls-section .btn {
  padding: 0.1875rem 0.9375rem;
  min-height: 1.875rem;
  font-size: 0.75rem;
}

.expand-chevron {
  display: inline-flex;
  transition: transform 0.125s;
}
/* -------- TOKENS — locked across 08/09/10/11 ----------------------- */
:root {
  /* Version B (locked 2026-05-01): the screen behind views carries the
     cool-grey tint, so the lifted-card views (.view, with shadow) read as
     "floating" cards. In Version A this was reversed — view-bg was grey,
     screen was white. */
  --very-bg: hsl(214, 8%, 92%);
  /* WORKSHOP HISTORY — view-bg candidates explored across 2026-04-29 / 04-30.
       A — hsl(214, 50%, 90%)   brand-blue tint, S50/L90    (was locked 04-29)
       B — hsl(214, 100%, 92%)  brand-blue tint, S100/L92
       C — hsl(190, 50%, 90%)   cool teal, S50/L90
       D — hsl(190, 40%, 90%)   cool teal, S40/L90
       Cool-grey hsl(214, 8%, 92%) — locked 2026-04-30 alongside the per-tab
       color system (workshop 15) for "Version A".
     Version B (locked 2026-05-01): view-bg flipped to white. The cool-grey
     moved to --very-bg (the screen behind views), and views got a layered
     shadow to lift off it. Cards inside views (.section-card, rail .ws-card,
     .source-card) carry the same layered shadow. */
  --view-bg: #ffffff;
  --card-bg: #ffffff;
  --brand-purple: hsl(274, 91%, 39%);
  --accent: hsl(214, 97%, 50%);
  --accent-hover: hsl(214, 97%, 42%);
  --accent-bg: hsl(214, 97%, 96%);
  --accent-bg-strong: hsl(214, 97%, 92%);
  /* Table-system surfaces (locked across workshops 13/13a/13b/13c).
     Encoding triad: bg = type · text = state · ring = focus. */
  --header-bg: hsl(214, 14%, 92%); /* cool blue-grey, same hue family as accent */
  --editable-text: var(--accent); /* accent text on plain card BG */
  --dirty-bg: hsl(38, 90%, 92%);
  --dirty-bg-strong: hsl(38, 92%, 86%);
  --dirty-text: hsl(28, 80%, 28%);
  --dirty-pill-bg: hsl(38, 92%, 50%);
  --dirty-pill-bg-hover: hsl(38, 92%, 44%);
  --stale-text: hsl(28, 50%, 65%); /* ~60% washed-out amber for stale-card body */
  --actual-text: hsl(151, 65%, 30%); /* accounting green for output Actuals */
  --border: #e6e8ec;
  --border-soft: #ecedf0;
  --border-strong: #d1d5db;
  --text: #0f1420;
  --text-muted: #5c6476;
  --text-subtle: #98a0ae;
  /* Model state palette — bible §2.5 (amendment 2026-05-09, revised
     2026-05-09 PM). Color = model identity within the canvas, paired with
     bold weight as the on-canvas signal. Main = bold accent blue (the
     "you're here right now" signal, mirroring §4.2's selected-state rule);
     secondaries = bold teal / amber / pink in opening order. */
  --model-main: var(--accent);
  --model-sec-1: hsl(189, 91%, 36%); /* teal  */
  --model-sec-2: hsl(27, 89%, 49%); /* amber */
  --model-sec-3: hsl(330, 75%, 47%); /* pink  */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 24px;
  --s-6: 32px;
  --s-7: 48px;
  --r-sm: 4px;
  --r-md: 6px;
  --r-lg: 8px;
  /* Neutral cool-grey at the same lightness level as the per-tab `tray`
     variants — matches the visual weight of the Model Detail tabrow so
     the rail's Sources / Projects toggle (which has no per-tab semantic)
     still reads as a "tracked" tab row. */
  --track-bg: hsl(214, 14%, 86%);
  /* -------- TAB COLOR SYSTEM (workshop 15, 2026-04-30) ----------------
     Each Model Detail section tab carries an identity color in three
     variants — bold (active text + chart line), tray (TabRow track bg),
     header (stx-table column / row header bg). Hues differ per tab; the
     three S/L profiles are shared so the system stays coherent.

     Wired to the active tab via :style on each `.view` element — the
     view binds `--active-{bold,tray,header}` to its current sectionId,
     so swapping tabs (Main or any Comparison) cascades through that
     view's TabRow tray + table headers + chart lines.
     ----------------------------------------------------------------- */
  --tab-bold-s: 69%;
  --tab-bold-l: 40%;
  --tab-tray-s: 35%;
  --tab-tray-l: 86%;
  --tab-header-s: 22%;
  --tab-header-l: 92%;
  /* Hue keys match the tab IDs in projection/views/redesign_views.py
     so the Alpine `:style` binding (`var(--tab-${sectionId}-bold)`) resolves. */
  --hue-fts: 214; /* blue (brand) */
  --hue-reacts: 265; /* violet — sibling of blue */
  --hue-reactivation: 340; /* rose */
  --hue-inputs: 30; /* warm orange */
  --hue-outputs: 174; /* teal-green */
  --tab-fts-bold: hsl(var(--hue-fts), var(--tab-bold-s), var(--tab-bold-l));
  --tab-fts-tray: hsl(var(--hue-fts), var(--tab-tray-s), var(--tab-tray-l));
  --tab-fts-header: hsl(var(--hue-fts), var(--tab-header-s), var(--tab-header-l));
  --tab-reacts-bold: hsl(var(--hue-reacts), var(--tab-bold-s), var(--tab-bold-l));
  --tab-reacts-tray: hsl(var(--hue-reacts), var(--tab-tray-s), var(--tab-tray-l));
  --tab-reacts-header: hsl(var(--hue-reacts), var(--tab-header-s), var(--tab-header-l));
  --tab-reactivation-bold: hsl(var(--hue-reactivation), var(--tab-bold-s), var(--tab-bold-l));
  --tab-reactivation-tray: hsl(var(--hue-reactivation), var(--tab-tray-s), var(--tab-tray-l));
  --tab-reactivation-header: hsl(var(--hue-reactivation), var(--tab-header-s), var(--tab-header-l));
  --tab-inputs-bold: hsl(var(--hue-inputs), var(--tab-bold-s), var(--tab-bold-l));
  --tab-inputs-tray: hsl(var(--hue-inputs), var(--tab-tray-s), var(--tab-tray-l));
  --tab-inputs-header: hsl(var(--hue-inputs), var(--tab-header-s), var(--tab-header-l));
  --tab-outputs-bold: hsl(var(--hue-outputs), var(--tab-bold-s), var(--tab-bold-l));
  --tab-outputs-tray: hsl(var(--hue-outputs), var(--tab-tray-s), var(--tab-tray-l));
  --tab-outputs-header: hsl(var(--hue-outputs), var(--tab-header-s), var(--tab-header-l));
}

/* Per-view default — falls through to FTS for any view that hasn't bound
   its `:style` yet (SSR / pre-Alpine-mount). Each `.view` overrides via
   `:style` based on its local `sectionId`. */
.view {
  --active-bold: var(--tab-fts-bold);
  --active-tray: var(--tab-fts-tray);
  --active-header: var(--tab-fts-header);
  --chart-line: var(--active-bold);
}

* {
  box-sizing: border-box;
}

html, body {
  margin: 0;
  padding: 0;
}

/* Alpine: hide x-cloak'd elements until the framework mounts. Used on
   templates that would otherwise render with empty bindings during the
   brief window between page load and store hydration. */
[x-cloak] {
  display: none !important;
}

body {
  background: var(--very-bg);
  color: var(--text);
  font-family: "Assistant", -apple-system, BlinkMacSystemFont, sans-serif;
  font-size: 13px;
  min-height: 100vh;
}

/* -------- APP SHELL — rail (left) + canvas (right) ----------------
   Tool, not website (bible §1) — the app stays bounded to the viewport
   so the layout never scrolls as a whole. Internal scrolling lives
   inside individual canvas-body wrappers. The previous `min-height: 100vh`
   let the page grow when a tall view's content didn't fit; now any
   overflow is contained inside the view that owns it. */
.app {
  display: flex;
  height: 100vh;
  overflow: hidden;
  gap: var(--s-4);
  padding: var(--s-4);
}

.app__rail {
  flex: 0 0 280px;
}

.app__canvas {
  flex: 1 1 auto;
  background: var(--view-bg);
  border-radius: var(--r-lg);
  border: 1px solid var(--border);
  overflow: hidden;
  /* Single-pane pages (e.g. home) keep the .app__canvas chrome. The
     Model Detail page is a multi-View grid — it neutralizes this chrome
     via :has(.canvas-grid) below and lets each .view carry its own. */
}

.app__canvas:has(.canvas-grid),
.app__canvas:has(.sources-canvas .view) {
  background: transparent;
  border: none;
  border-radius: 0;
  /* Stay clipped — each .view inside the grid carries its own surface
     chrome; nothing should overflow .app__canvas's bounds. */
  overflow: hidden;
}

/* -------- TAB ROW — locked Track flavor (09 §4.3) ---------------- */
.tab-row {
  display: flex;
  align-items: stretch;
  /* position: relative is REQUIRED — it makes the tab-row the offsetParent
     of its child tabs. Without it, the tab's offsetParent walks all the
     way up the static-positioned chain to <body>, and active.offsetLeft
     returns the tab's page-X coordinate (1500+ for a right comparison
     view) instead of its position within the row. The centering math
     in ensureActiveTabVisible (redesign.js) then computes a target that
     clamps to scrollWidth-clientWidth, parking the row at maximum
     scroll-right — which is exactly the "left tabs bleeding off-left"
     bug Matt hit when narrow right-pop-out views resized. */
  position: relative;
}

.tab-row__tab {
  appearance: none;
  background: transparent;
  border: none;
  margin: 0;
  cursor: pointer;
  font: inherit;
  font-family: inherit;
  color: var(--text-muted);
  transition: background 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;
  white-space: nowrap;
  user-select: none;
  line-height: 1.2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Position context for the absolutely-placed pop-out icon. The icon sits
     to the right of the tab without participating in the label's centering,
     so the label is true-centered whether the icon is visible or not. */
  position: relative;
}

.tab-row.track {
  /* Tinted by the active tab's tray variant when this row lives inside a
     `.view` (which defines --active-tray). Falls back to neutral track-bg
     for tab-rows outside any view (none currently — the rail's old
     Sources / Projects tabrow was retired when the contextual-rail
     pattern landed). */
  background: var(--active-tray, var(--track-bg));
  border-radius: var(--r-lg);
  padding: 3px;
  gap: 2px;
  justify-content: flex-start;
  transition: background 0.18s ease;
}

.tab-row.track .tab-row__tab {
  flex: 1;
  padding: 6px 14px;
  border-radius: 6px;
  font-size: 12.5px;
  font-weight: 500;
  text-align: center;
  min-height: 28px;
  max-width: 200px; /* per-tab cap (09 §4.3, raised 2026-04-30 to fit "Retention (Reactivated)" + pop-out icon without ellipsis) */
}

.tab-row.track .tab-row__tab:hover:not(.active) {
  color: var(--text);
  background: rgba(0, 0, 0, 0.03);
}

.tab-row.track .tab-row__tab.active {
  /* Version B (locked 2026-05-01): inverted active treatment — solid colored
     pill (per-tab bold variant) with white text. In Version A the active tab
     was a white pill with colored text. Rail's active tab falls back to
     --accent (no per-tab semantic). */
  background: var(--active-bold, var(--accent));
  color: white;
  font-weight: 700;
  box-shadow: 0 1px 2px rgba(15, 20, 32, 0.08);
  transition: background 0.18s ease, color 0.18s ease;
}

/* Disabled state — locked in workshop 12. Mutually exclusive with active. */
.tab-row.track .tab-row__tab.disabled {
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: none;
}

/* Per-row pop-out opt-in (workshop 09 §4.2) — Model Detail section tabs use it,
   the rail does NOT (zone tabs are nav, not pop-out targets). Hidden by default;
   reveals at 70% opacity on tab hover (active or inactive); solid + tinted bg
   on direct icon hover. Width is always reserved — no layout jitter. */
.tab-row__label {
  display: inline-block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.tab-row__popout {
  /* Absolutely positioned at the right edge of the tab so it doesn't push
     the label off-center. When invisible, the label is true-centered in the
     button; when visible, the icon overlays the right edge. */
  position: absolute;
  right: 5px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border-radius: 3px;
  color: var(--accent); /* always accent blue, regardless of tab state */
  opacity: 0; /* hidden until tab hover */
  transition: opacity 0.12s ease, background 0.12s ease;
}

.tab-row__popout svg {
  width: 12px;
  height: 12px;
}

.tab-row.has-popout .tab-row__tab:hover .tab-row__popout {
  opacity: 0.7;
}

.tab-row.has-popout .tab-row__popout:hover {
  opacity: 1;
  background: var(--accent-bg);
}

/* At the 4-view cap (Main + 3 Comparisons), suppress the pop-out icons on
   the Main tabrow entirely — no hover reveal, no hover bg. The icon stays
   in the DOM (so the row geometry doesn't reflow) but reads as unavailable.
   Driven by `:class="{ 'at-max-views': $store.v2Store.canvasIsFull }"` on
   the Main view's tab-row in pages/model_detail.html. Wave 2 generalizes
   this to every pop-out trigger system-wide. */
.tab-row.at-max-views .tab-row__tab:hover .tab-row__popout,
.tab-row.at-max-views .tab-row__popout:hover {
  opacity: 0;
  background: transparent;
}

.tab-row.scrollable {
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.tab-row.scrollable::-webkit-scrollbar {
  display: none;
}

.tab-row.scrollable .tab-row__tab {
  flex: 0 0 auto;
}

/* -------- MODEL STATE-ICON (09 §4.2) ----------------------------- */
.model-state-icon {
  width: 16px;
  height: 16px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  color: var(--text-subtle);
  transition: color 0.12s ease;
}

.model-state-icon svg {
  width: 16px;
  height: 16px;
  stroke-width: 2.25;
}

.nav-item.has-state-icon {
  padding-left: 22px;
}

/* Model state palette — bible §2.5 (amendment 2026-05-09; weight split
   2026-05-11). Color encodes model identity within the current canvas.
   Rail nav-item rows pair color with bold weight (the row is the "active
   in canvas" signal in a list of inactive siblings). ViewHeader titles
   wear color ONLY and inherit the default title weight — matching plain
   (non-state) headers like Reports and the rail mode label, so the
   Title component reads with a single, lighter weight everywhere. */
.nav-item.state-main {
  color: var(--model-main);
  font-weight: 700;
}

.view-header__title.state-main {
  color: var(--model-main);
}

.nav-item.state-main .model-state-icon,
.view-header__title.state-main .view-header__title-icon {
  color: var(--model-main);
}

.nav-item.state-sec-1 {
  color: var(--model-sec-1);
  font-weight: 700;
}

.view-header__title.state-sec-1 {
  color: var(--model-sec-1);
}

.nav-item.state-sec-1 .model-state-icon,
.view-header__title.state-sec-1 .view-header__title-icon {
  color: var(--model-sec-1);
}

.nav-item.state-sec-2 {
  color: var(--model-sec-2);
  font-weight: 700;
}

.view-header__title.state-sec-2 {
  color: var(--model-sec-2);
}

.nav-item.state-sec-2 .model-state-icon,
.view-header__title.state-sec-2 .view-header__title-icon {
  color: var(--model-sec-2);
}

.nav-item.state-sec-3 {
  color: var(--model-sec-3);
  font-weight: 700;
}

.view-header__title.state-sec-3 {
  color: var(--model-sec-3);
}

.nav-item.state-sec-3 .model-state-icon,
.view-header__title.state-sec-3 .view-header__title-icon {
  color: var(--model-sec-3);
}

/* -------- VIEW HEADER COMPONENT (10 §4.4) ------------------------ */
.view-header {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: 12px 8px 10px 12px;
  flex-wrap: nowrap;
}

.view-header__title {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  /* Default: natural content size up to title-text's max-width. */
  flex: 0 0 auto;
  min-width: 0;
}

.view-header__title-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text);
  flex-shrink: 0;
}

.view-header__title-icon svg {
  width: 22px;
  height: 22px;
  stroke-width: 2.25;
}

.view-header__title-icon img {
  height: 26px;
  width: auto;
  display: block;
}

.view-header__title-text {
  /* Color + weight inherit from the parent .view-header__title so its
     state-main / state-sec-N classes (bible §2.5) drive both the icon and
     the text in lockstep. Default falls through via inherit. */
  font-size: 20px;
  color: inherit;
  font-weight: inherit;
  letter-spacing: -0.01em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* Cap at ~40 average characters so absurdly long names can't crowd out
     the tabs / menu / action cluster. `min-width: 0` lets the text
     shrink BELOW its intrinsic content width when the parent slot
     shrinks — without it, the default `min-width: auto` on a flex item
     equals the text's content width, and ellipsis would never engage on
     a narrow view. The auto-truncate tooltip surfaces the full name on
     hover whenever ellipsis kicks in. */
  min-width: 0;
  max-width: 360px;
}

/* Rail mode label — its own row beneath the logo/menu strip in the
   contextual rail. Reuses the standard `view-header__title state-main`
   pair (icon + text) so it visually matches the Model View Header title
   format. Renders "Projects" by default; swaps to side-mode name
   ("Data Sources", future: "Reports") with a matching icon. */
.rail-mode-label {
  padding: 8px 16px 2px;
}

.rail-mode-label .view-header__title {
  gap: 8px;
}

/* The tabs slot fills the space between title and the right-side cluster.
   The tab-row track is centered within the slot — combined with the
   .view-header parent's symmetric gap on either side, this produces equal
   whitespace between (title.right → tab-row.left) and (tab-row.right →
   menu.left). The track's flex-basis is computed from --n-tabs (set by JS
   at init, fallback 5) so it knows its "ideal" width: n × 175 + gaps + pad.
   On wide slots, the track sits at that ideal and the slot absorbs the
   leftover symmetrically. On narrow slots, the track shrinks and tabs
   share the smaller width via the inner flex: 1 + max-width: 175.
   (Workshop 12 lock + 2026-04-29 centering amendment.) */
.view-header__tabs {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden; /* enables scrollWidth > clientWidth detection */
  display: flex;
  align-items: stretch;
  justify-content: center;
}

.view-header__tabs > .tab-row {
  --n-tabs: 5;
  flex: 0 1 calc(var(--n-tabs) * 175px + (var(--n-tabs) - 1) * 2px + 6px);
  min-width: 0;
}

.view-header__menu {
  flex: 0 0 auto;
  margin-left: auto;
  position: relative;
}

/* Primary-action slot — Main View only. Sits to the right of the menu inside
   the right-pinned cluster (the menu's `margin-left: auto` pushes the cluster
   right; the action sits immediately after). Comparison Views replace this
   slot with window controls — TBD. */
.view-header__action {
  flex: 0 0 auto;
}

.view-header.is-broken {
  flex-wrap: wrap;
  row-gap: 10px;
}

.view-header.is-broken .view-header__tabs {
  flex-basis: 100%;
  order: 3;
  overflow: visible;
}

/* When broken, let the title share row 1 with the menu/action cluster
   instead of pushing the cluster to its own line. `flex: 1 1 0` makes
   hypothetical main size 0 — the wrap algorithm doesn't see the title as
   "too wide" and doesn't wrap menu/action away. flex-grow then expands
   the title to fill row 1's remaining width, capped naturally by the
   title-text's max-width (so the icon + text never stretch past their
   natural layout — extra space just sits empty between the text and the
   menu, same visual as the auto-margin behavior in the unbroken state).
   Menu's `margin-left: auto` is dropped here because the title's grow
   already handles right-edge alignment of the cluster; keeping the auto
   margin would steal all free space and starve the title to width 0. */
.view-header.is-broken .view-header__title {
  flex: 1 1 0;
}

.view-header.is-broken .view-header__menu {
  margin-left: 0;
}

/* -------- MENU ICON BUTTON --------------------------------------- */
.menu-icon-btn {
  appearance: none;
  background: transparent;
  border: none;
  margin: 0;
  padding: 0;
  cursor: pointer;
  width: 30px;
  height: 30px;
  border-radius: var(--r-sm);
  color: var(--text-muted);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background 0.12s ease, color 0.12s ease;
}

.menu-icon-btn:hover, .menu-icon-btn.is-open {
  background: rgba(0, 0, 0, 0.06);
  color: var(--text);
}

.menu-icon-btn svg {
  width: 18px;
  height: 18px;
}

.menu-popover {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  background: white;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: 0 8px 24px rgba(15, 20, 32, 0.14), 0 1px 3px rgba(15, 20, 32, 0.06);
  min-width: 170px;
  padding: 4px;
  z-index: 50;
  display: none;
}

.menu-icon-btn.is-open ~ .menu-popover {
  display: block;
}

.menu-popover-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 10px;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text);
  cursor: pointer;
  border-radius: 4px;
  user-select: none;
  text-decoration: none;
}

.menu-popover-item svg {
  width: 14px;
  height: 14px;
  color: var(--text-muted);
}

.menu-popover-item:hover {
  background: rgba(0, 0, 0, 0.05);
}

.menu-popover-item.danger {
  color: hsl(0, 65%, 45%);
}

.menu-popover-item.danger svg {
  color: currentColor;
}

.menu-popover-item.is-disabled {
  color: var(--text-muted);
  cursor: default;
  pointer-events: none;
}

.menu-popover-item.is-disabled svg {
  opacity: 0.55;
}

.menu-popover-divider {
  height: 1px;
  background: var(--border-soft);
  margin: 4px 0;
}

/* -------- LAYOUT SELECTOR POPOVER -------------------------------- */
.layout-popover {
  min-width: 0;
  width: max-content;
}

.layout-option {
  display: grid !important;
  grid-template-columns: 28px 14px;
  align-items: center;
  gap: 10px !important;
}

.layout-option.is-active {
  color: var(--accent);
}

.layout-option__check {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  color: var(--accent);
}

.layout-option.is-active .layout-option__check {
  opacity: 1;
}

.layout-option__check svg {
  width: 14px;
  height: 14px;
}

/* Pictograms — pure CSS using inset box-shadows. Each pictogram is a
   24×16 px frame; child cells are drawn via shadows on a single span,
   keeping markup minimal. Cells use --pict-c (cell color, ~muted text);
   the frame uses --pict-b (border, --border-strong) implicitly via
   shadow inset positions. .layout-option.is-active boosts contrast. */
.layout-option__pictogram {
  position: relative;
  display: block;
  width: 24px;
  height: 16px;
  border-radius: 2px;
  background: transparent;
  /* Each rect inside is created with a child ::before/::after or an
     inner pseudo. We use background gradients for compactness. */
  --pict-c: hsl(220, 8%, 70%);
}

.layout-option.is-active .layout-option__pictogram {
  --pict-c: var(--accent);
}

/* side-by-side-2 → two equal columns, 1 row */
.layout-option__pictogram[data-shape=side-by-side-2] {
  background: linear-gradient(var(--pict-c), var(--pict-c)) 0 0/calc(50% - 1px) 100% no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 100% 0/calc(50% - 1px) 100% no-repeat;
}

/* above-below-2 → 1 column, 2 rows */
.layout-option__pictogram[data-shape=above-below-2] {
  background: linear-gradient(var(--pict-c), var(--pict-c)) 0 0/100% calc(50% - 1px) no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 0 100%/100% calc(50% - 1px) no-repeat;
}

/* big-left-3 → big left col + 2 small stacked right */
.layout-option__pictogram[data-shape=big-left-3] {
  background: linear-gradient(var(--pict-c), var(--pict-c)) 0 0/calc(60% - 1px) 100% no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 100% 0/calc(40% - 1px) calc(50% - 1px) no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 100% 100%/calc(40% - 1px) calc(50% - 1px) no-repeat;
}

/* side-by-side-3 → 3 equal columns */
.layout-option__pictogram[data-shape=side-by-side-3] {
  background: linear-gradient(var(--pict-c), var(--pict-c)) 0 0/calc(33.333% - 1px) 100% no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 50% 0/calc(33.333% - 1px) 100% no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 100% 0/calc(33.333% - 1px) 100% no-repeat;
}

/* stacked-3 → 3 rows */
.layout-option__pictogram[data-shape=stacked-3] {
  background: linear-gradient(var(--pict-c), var(--pict-c)) 0 0/100% calc(33.333% - 1px) no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 0 50%/100% calc(33.333% - 1px) no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 0 100%/100% calc(33.333% - 1px) no-repeat;
}

/* quarters-4 → 2x2 */
.layout-option__pictogram[data-shape=quarters-4] {
  background: linear-gradient(var(--pict-c), var(--pict-c)) 0 0/calc(50% - 1px) calc(50% - 1px) no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 100% 0/calc(50% - 1px) calc(50% - 1px) no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 0 100%/calc(50% - 1px) calc(50% - 1px) no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 100% 100%/calc(50% - 1px) calc(50% - 1px) no-repeat;
}

/* side-by-side-4 → 4 equal columns */
.layout-option__pictogram[data-shape=side-by-side-4] {
  background: linear-gradient(var(--pict-c), var(--pict-c)) 0 0/calc(25% - 1px) 100% no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 33.33% 0/calc(25% - 1px) 100% no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 66.66% 0/calc(25% - 1px) 100% no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 100% 0/calc(25% - 1px) 100% no-repeat;
}

/* big-left-4 → big left col + 3 small stacked right */
.layout-option__pictogram[data-shape=big-left-4] {
  background: linear-gradient(var(--pict-c), var(--pict-c)) 0 0/calc(60% - 1px) 100% no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 100% 0/calc(40% - 1px) calc(33.333% - 1px) no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 100% 50%/calc(40% - 1px) calc(33.333% - 1px) no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 100% 100%/calc(40% - 1px) calc(33.333% - 1px) no-repeat;
}

/* stacked-4 → 4 rows */
.layout-option__pictogram[data-shape=stacked-4] {
  background: linear-gradient(var(--pict-c), var(--pict-c)) 0 0/100% calc(25% - 1px) no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 0 33.33%/100% calc(25% - 1px) no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 0 66.66%/100% calc(25% - 1px) no-repeat, linear-gradient(var(--pict-c), var(--pict-c)) 0 100%/100% calc(25% - 1px) no-repeat;
}

/* -------- SAVE & RECALCULATE — icon-only AMBER pill --------------
   Stateful: muted/disabled when clean, AMBER when dirty (one or more cells
   pending recalc). Now that dirty cells are amber and the pill goes amber
   when dirty, the coupling is so explicit that we drop the text label —
   the icon is the verb, amber is the alert. Tooltip carries the full label
   for accessibility. Supersedes workshop 12's accent-blue lock per the
   bible §4.4 amendment in CLAUDE_MATT. */
.save-recalc {
  appearance: none;
  border: none;
  font: inherit;
  width: 30px;
  height: 30px;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease, box-shadow 0.12s ease, transform 0.06s ease;
}

.save-recalc svg {
  width: 15px;
  height: 15px;
  stroke-width: 2.25;
}

.save-recalc:not(.is-dirty) {
  background: rgba(0, 0, 0, 0.06);
  color: var(--text-subtle);
  cursor: default;
}

.save-recalc.is-dirty {
  background: var(--dirty-pill-bg);
  color: white;
  box-shadow: 0 1px 2px rgba(15, 20, 32, 0.1);
}

.save-recalc.is-dirty:hover {
  background: var(--dirty-pill-bg-hover);
}

.save-recalc.is-dirty:active {
  transform: translateY(1px);
}

/* Loading state — swap the refresh-cw icon for the brand S-mark, pulsing
   in opacity to signal "working." Two-second cycle reads as deliberate
   without feeling stalled. The S inherits `currentColor` so it stays
   white on the amber dirty pill. */
.save-recalc .save-recalc__spinner {
  display: none;
}

.save-recalc.is-loading .save-recalc__icon {
  display: none;
}

.save-recalc.is-loading .save-recalc__spinner {
  display: block;
  width: 17px;
  height: 17px;
  animation: save-recalc-pulse 2s ease-in-out infinite;
}

@keyframes save-recalc-pulse {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.35;
  }
}
/* -------- RAIL CONTAINER (10) ------------------------------------ */
.nav {
  background: var(--view-bg);
  border-radius: var(--r-lg);
  border: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  box-shadow: 0 6px 24px rgba(15, 20, 32, 0.06), 0 1px 3px rgba(15, 20, 32, 0.04);
  width: 280px;
  height: 100%; /* fill .app__rail (which fills .app's flex-row height) */
}

.nav-body {
  flex: 1;
  /* Top padding intentionally zero — the rail-mode-label above provides
     its own bottom padding (2px), which is the canonical gap between the
     mode label and the first card. */
  padding: 0 0 6px;
  overflow-y: auto;
}

.tab-panel {
  display: none;
}

.tab-panel.active {
  display: block;
}

/* -------- SOURCE CARDS (08 — 3A locked) -------------------------- */
.source-card {
  background: var(--card-bg);
  border-radius: var(--r-md);
  padding: 9px 12px 10px;
  margin: 0 10px 6px;
  cursor: pointer;
  /* Property-specific transitions only — `transition: all` would include
     `visibility`, which interacts badly with the FOUC guard's hidden→visible
     flip on css-ready (causes a brief flicker even though spec says visibility
     should stay `visible` throughout a hidden→visible transition). */
  transition: transform 0.12s ease, box-shadow 0.12s ease;
  display: block;
  text-decoration: none;
  color: inherit;
  /* Version B layered shadow — matches .view / .section-card. */
  box-shadow: 0 1px 1px rgba(15, 20, 32, 0.06), 0 4px 10px rgba(15, 20, 32, 0.12), 0 10px 20px rgba(15, 20, 32, 0.1);
}

.source-card:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 10px rgba(15, 20, 32, 0.08);
}

.source-card.selected .source-name {
  color: var(--accent);
}

.source-card-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
}

.source-name {
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.005em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1;
}

.source-tag {
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  flex-shrink: 0;
}

.source-tag.csv {
  background: hsl(151, 55%, 92%);
  color: hsl(151, 82%, 24%);
}

.source-tag.live {
  background: hsl(214, 100%, 94%);
  color: hsl(214, 97%, 42%);
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.source-tag.live::before {
  content: "";
  display: inline-block;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: hsl(151, 72%, 42%);
}

.source-meta {
  margin-top: 4px;
  font-size: 11.5px;
  color: var(--text-subtle);
  display: flex;
  gap: 6px;
}

.source-meta span + span::before {
  content: "·";
  margin-right: 6px;
  color: var(--border-strong);
}

/* -------- PROJECT CARDS ------------------------------------------ */
.ws-card {
  background: var(--card-bg);
  border-radius: var(--r-md);
  margin: 0 10px 6px;
  overflow: hidden;
  /* Property-specific transitions — see .source-card note above for why
     we avoid `transition: all`. */
  transition: transform 0.12s ease, box-shadow 0.12s ease;
  /* Version B layered shadow — matches .view / .section-card. */
  box-shadow: 0 1px 1px rgba(15, 20, 32, 0.06), 0 4px 10px rgba(15, 20, 32, 0.12), 0 10px 20px rgba(15, 20, 32, 0.1);
}

.ws-card:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 10px rgba(15, 20, 32, 0.08);
}

.ws-card.expanded:hover {
  transform: none;
  box-shadow: 0 1px 2px rgba(15, 20, 32, 0.04);
}

.ws-card.expanded .ws-card-header:hover {
  background: rgba(0, 0, 0, 0.05);
}

.ws-card-header {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: 8px 12px;
  cursor: pointer;
}

.ws-card.expanded .ws-card-header {
  border-bottom: 1px solid var(--border-soft);
}

.ws-name {
  flex: 0 1 auto;
  min-width: 0;
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.005em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ws-count {
  margin-left: auto;
  font-size: 10.5px;
  font-weight: 700;
  color: var(--text-subtle);
  background: rgba(0, 0, 0, 0.05);
  border-radius: 999px;
  padding: 1px 8px;
  font-variant-numeric: tabular-nums;
}

.ws-card-body {
  padding: 2px 0 4px;
}

.ws-card:not(.expanded) .ws-card-body {
  display: none;
}

/* Multi-child wrapper inside the .ws-card-header's x-if template — Alpine
   templates allow only one root child, so the wrapper exists for that
   reason alone. display:contents makes it layout-invisible so its
   children participate in .ws-card-header's flex row directly. */
.ws-card-header-content {
  display: contents;
}

/* Per-model row inside .ws-card-body. data-model-id is the real anchor
   used by drag-and-drop reorder and railApplyStoredOrder. display:contents
   so the inner template children (<a> nav-item or <input> rename field)
   inherit the body's flow rather than getting an extra wrapping box that
   would strand them during sort. */
.model-rail-row {
  display: contents;
}

/* Empty-state hint inside the Projects panel when the user has no projects
   yet. Single-line muted prompt nudging toward the New Project flow. */
.rail-empty-state {
  padding: 12px 16px;
  color: var(--text-subtle);
  font-size: 12.5px;
}

.nav-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: 3px 12px 3px 28px;
  color: var(--text);
  font-size: 12.5px;
  font-weight: 500;
  cursor: pointer;
  user-select: none;
  line-height: 1.3;
  min-height: 24px;
  text-decoration: none;
}

.nav-item:hover {
  background: rgba(0, 0, 0, 0.06);
  color: var(--text);
}

.nav-item.selected {
  color: var(--accent);
  font-weight: 600;
}

/* `.state-main` / `.state-sec-N` rules live up top with the model state-icon
   block (§2.5 amendment 2026-05-09). The duplicates that used to live here
   pinning the active Model row to accent blue have been removed — Models on
   the canvas now wear the identity palette (bold black / teal / amber / pink). */
.item-label {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.nav-action {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: 3px 12px 3px 28px;
  color: var(--text-subtle);
  font-size: 12;
  cursor: pointer;
  user-select: none;
  min-height: 24px;
}

.nav-action:hover {
  color: var(--accent);
}

.nav-action svg {
  width: 12px;
  height: 12px;
}

/* -------- INLINE-EDIT INPUTS (rail New Project / New Model) ------
   Pending cards/rows render an autofocused input where the static
   text would normally live. Style is "edit mode" — neutral fill,
   focus ring uses --accent. The input inherits the surrounding row's
   typography via two variants:
     .rail-inline-input        — model row (matches .item-label)
     .rail-inline-input--ws    — project header (matches .ws-name) */
.rail-inline-input {
  flex: 1;
  min-width: 0;
  appearance: none;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 2px 6px;
  font: inherit;
  color: var(--text);
  outline: none;
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}

.rail-inline-input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-bg);
}

.rail-inline-input--ws {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: -0.005em;
}

/* Hide the model state-icon in the pending row so the input's box-left
   sits flush with the "+ New Model" icon below — no awkward indent. */
.nav-item--pending .model-state-icon {
  display: none;
}

/* Inline rename input for the Main View's model title. Auto-grows to
   the typed text via field-sizing, matches the title's typography. */
.view-title-input {
  appearance: none;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 2px 8px;
  font: inherit;
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text);
  outline: none;
  field-sizing: content;
  min-width: 8ch;
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}

.view-title-input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-bg);
}

/* -------- AUTH (sign-in) page ----------------------------------
   Standalone surface — does NOT extend base.html. White page bg with
   the brand wordmark in the upper-left (matches rail header position
   so the surface reads as part of the app). A giant faded iconmark
   fills the center as ambient background. The sign-in card sits
   above it. Uses the locked v2 tokens for inputs / accent. */
.auth-body {
  background: #ffffff;
  min-height: 100vh;
  margin: 0;
  font-family: "Assistant", -apple-system, BlinkMacSystemFont, sans-serif;
  font-size: 13px;
  color: var(--text);
  position: relative;
  overflow: hidden;
}

.auth-rail-logo {
  position: fixed;
  top: 16px;
  left: 16px;
  display: flex;
  align-items: center;
  z-index: 2;
  background: var(--card-bg);
  border-radius: var(--r-lg);
  border: 1px solid var(--border);
  padding: 12px 18px;
  /* Match the sign-in card's layered shadow so both surfaces read as the
     same "lifted card" treatment. */
  box-shadow: 0 1px 1px rgba(15, 20, 32, 0.06), 0 4px 10px rgba(15, 20, 32, 0.12), 0 10px 20px rgba(15, 20, 32, 0.1);
}

.auth-rail-logo img {
  display: block;
}

.auth-bg-mark {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  height: 110vh;
  width: auto;
  opacity: 0.2;
  z-index: 0;
  pointer-events: none;
  user-select: none;
}

.auth-stage {
  position: relative;
  z-index: 1;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-5);
}

.auth-card {
  background: var(--card-bg);
  border-radius: var(--r-lg);
  border: 1px solid var(--border);
  padding: var(--s-5);
  /* Layered shadow — close ambient + far drop — so the card lifts cleanly
     above the giant faded iconmark behind it. */
  box-shadow: 0 1px 1px rgba(15, 20, 32, 0.06), 0 4px 10px rgba(15, 20, 32, 0.12), 0 10px 20px rgba(15, 20, 32, 0.1);
  width: 100%;
  max-width: 380px;
}

.auth-card__lede {
  margin: 0 0 var(--s-4);
  text-align: center;
  font-size: 14.5px;
  font-weight: 600;
  color: var(--text);
  letter-spacing: -0.005em;
}

.auth-form {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

.auth-field {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
}

.auth-field label {
  font-size: 11.5px;
  font-weight: 600;
  color: var(--text-muted);
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.auth-field input {
  appearance: none;
  background: white;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 10px 12px;
  font: inherit;
  color: var(--text);
  outline: none;
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}

.auth-field input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-bg);
}

.auth-error {
  background: hsl(0, 65%, 96%);
  border: 1px solid hsl(0, 65%, 88%);
  border-radius: var(--r-md);
  padding: 8px 12px;
  font-size: 12.5px;
  color: hsl(0, 65%, 35%);
}

.auth-error ul {
  margin: 0;
  padding-left: 18px;
}

.auth-row-between {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  margin-top: 2px;
}

.auth-checkbox {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  color: var(--text-muted);
  cursor: pointer;
  user-select: none;
}

.auth-checkbox input {
  margin: 0;
  cursor: pointer;
}

.auth-link {
  font-size: 12.5px;
  color: var(--accent);
  text-decoration: none;
  font-weight: 500;
}

.auth-link:hover {
  text-decoration: underline;
}

.auth-submit {
  appearance: none;
  background: var(--accent);
  color: white;
  border: none;
  border-radius: var(--r-md);
  padding: 11px 16px;
  font: inherit;
  font-weight: 600;
  font-size: 14px;
  cursor: pointer;
  margin-top: var(--s-2);
  transition: background 0.12s ease;
}

.auth-submit:hover {
  background: var(--accent-hover);
}

/* Empty-state row inside an empty Project — italic placeholder text. */
.nav-item.empty-state-row {
  color: var(--text-subtle);
  font-style: italic;
  cursor: default;
}

/* -------- RAIL DRAG-TO-REORDER (Phase 1, local-only) ------------
   Faded source while dragging; insertion-point line; subtle accent
   glow on collapsed cards that are about to auto-expand. Both the
   `user-select: none !important` and the click-suppression handler
   in redesign.js are needed in tandem (per the multi-view drag-to-
   swap gotcha — bible §9.5). */
.is-rail-drag-source {
  opacity: 0.4;
  pointer-events: none;
}

body.is-rail-dragging,
body.is-rail-dragging * {
  cursor: grabbing !important;
  user-select: none !important;
}

.rail-drop-line {
  height: 2px;
  margin: 3px 12px;
  background: var(--accent);
  border-radius: 1px;
  pointer-events: none;
}

.is-rail-auto-expand-target {
  background: var(--accent-bg);
  transition: background 0.18s ease;
}

/* -------- PANEL-ADD ROW (dashed) --------------------------------- */
.panel-add {
  margin: 8px 10px 6px;
  padding: 8px 10px;
  border-radius: var(--r-md);
  border: 1px dashed var(--border-strong);
  color: var(--text-subtle);
  font-size: 12px;
  font-weight: 500;
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  user-select: none;
  /* Property-specific transitions — see .source-card note above for why
     we avoid `transition: all`. */
  transition: color 0.12s ease, border-color 0.12s ease, background-color 0.12s ease;
  text-decoration: none;
}

.panel-add:hover {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--accent-bg);
}

.panel-add svg {
  width: 13px;
  height: 13px;
}

/* -------- IDENTITY FOOTER ---------------------------------------- */
.nav-footer {
  border-top: 1px solid rgba(0, 0, 0, 0.08);
  padding: 10px var(--s-4);
  display: flex;
  align-items: center;
  gap: var(--s-2);
}

.avatar {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: linear-gradient(135deg, #FB923C, #F97316);
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 600;
  flex-shrink: 0;
}

.avatar-name {
  flex: 1;
  font-size: 12.5px;
  font-weight: 500;
}

/* -------- VIEW SYSTEM — multi-pane canvas (bible §2 / §2.6) ------- */
.model-detail {
  /* Grid: canvas in column 1, optional Parameters panel in column 2.
     `has-parameters-open` is bound by Alpine on the .model-detail wrapper
     and grids the panel in beside the canvas. The canvas-grid (col 1)
     keeps its full multi-view system unchanged — Parameters is a sibling,
     not a participant in the View system. */
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: 1fr;
  gap: var(--s-3);
  height: 100%;
  min-height: 0;
}

.model-detail.has-parameters-open,
.model-detail.has-upload-open {
  grid-template-columns: 1fr 340px;
}

.canvas-grid {
  flex: 1 1 auto;
  display: grid;
  gap: var(--s-3);
  min-height: 0;
  /* Default — solo Main View, no comparisons. Each layout below
     overrides via [data-layout="..."]. layoutId comes from the v2
     store and snaps to the count-appropriate default whenever a
     comparison opens or closes. Track sizes themselves are bound
     via inline style from the store (gridTemplateStyle()) so resize
     drags can mutate them. */
  grid-template-columns: 1fr;
  grid-auto-rows: 1fr;
  /* Anchor for absolute-positioned resizer overlays — JS adds them at
     each track boundary in the layout (no markup per layout). */
  position: relative;
}

/* Invisible drag zones at adjacent-view boundaries (bible §2.6 phase
   1.5b — 2026-04-29). 12 px hit zones centered on the boundary line;
   no visible chrome by design — cursor is the affordance. JS in
   redesign.js creates / repositions these whenever the layout or grid
   size changes. */
.grid-resizer {
  position: absolute;
  z-index: 5;
  background: transparent;
  user-select: none;
  touch-action: none;
}

.grid-resizer--col {
  cursor: col-resize;
}

.grid-resizer--row {
  cursor: row-resize;
}

/* Visible only while the user actively drags (handler adds .is-dragging) —
   gives a tasteful confirmation without a permanent handle. */
.grid-resizer.is-dragging {
  background: rgba(0, 0, 0, 0.06);
}

/* Layout recipes (bible §2.6 + phase-1.5 expansion 2026-04-29).
   Naming: <arrangement>-<total-view-count>. Main is always the upper-
   left cell — for "big-left" recipes it spans multiple rows. */
/* 2 views (Main + 1 Comparison) */
.canvas-grid[data-layout=side-by-side-2] {
  grid-template-columns: 1fr 1fr;
}

.canvas-grid[data-layout=above-below-2] {
  grid-template-columns: 1fr;
  grid-template-rows: 1fr 1fr;
}

/* 3 views (Main + 2) */
.canvas-grid[data-layout=big-left-3] {
  grid-template-columns: 2fr 1fr;
  grid-template-rows: 1fr 1fr;
}

.canvas-grid[data-layout=big-left-3] .view--main {
  grid-row: 1/span 2;
}

.canvas-grid[data-layout=side-by-side-3] {
  grid-template-columns: 1fr 1fr 1fr;
}

.canvas-grid[data-layout=stacked-3] {
  grid-template-columns: 1fr;
  grid-template-rows: 1fr 1fr 1fr;
}

/* 4 views (Main + 3) */
.canvas-grid[data-layout=quarters-4] {
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
}

.canvas-grid[data-layout=side-by-side-4] {
  grid-template-columns: 1fr 1fr 1fr 1fr;
}

.canvas-grid[data-layout=big-left-4] {
  grid-template-columns: 2fr 1fr;
  grid-template-rows: 1fr 1fr 1fr;
}

.canvas-grid[data-layout=big-left-4] .view--main {
  grid-row: 1/span 3;
}

.canvas-grid[data-layout=stacked-4] {
  grid-template-columns: 1fr;
  grid-template-rows: 1fr 1fr 1fr 1fr;
}

.view {
  background: var(--view-bg);
  border-radius: var(--r-lg);
  border: 1px solid var(--border);
  /* Version B: no shadow on the view itself — the screen edges clipped it
     awkwardly. Cards INSIDE the view (.section-card) still carry the layered
     shadow, and rail cards do too. The view-header is transparent on the
     view's surface, so the view reads as a single panel with header on top. */
  overflow: hidden;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* Comparison palette retired 2026-05-09 (bible §2.5 amendment). The
   per-comparison color dot was replaced by a `state-main` / `state-sec-N`
   class on the title pair itself — the bold-colored icon + name carries
   the canvas-state signal directly. The `.view-color-dot` element and
   `.palette-X` classes are no longer rendered. */
/* Read-only View modifier — bible §2.2 / §4.4 (Phase 2 amendment 2026-05-09).
   Applied to any view whose model is NOT the current Main model (i.e., a
   secondary-model Comparison). Wave 1 ships only the CSS scaffolding —
   no logic wires this on yet (Wave 2 introduces per-view modelId). The
   rules neutralize every editable affordance so the view reads as a
   passive data display, not an editable surface. */
.view.view--read-only .editable {
  color: var(--text);
  font-weight: 400;
  cursor: default;
  background: transparent;
  box-shadow: none;
}

.view.view--read-only .editable:focus-within,
.view.view--read-only .editable.is-focused {
  box-shadow: none;
}

.view.view--read-only .editable.dirty {
  background: transparent;
  color: var(--text);
}

.view.view--read-only .save-recalc {
  display: none;
}

.view.view--read-only .cell-fill-handle {
  display: none;
}

/* Editable inputs themselves: take their cell's pointer events out and
   strip the affordance. Wave 2 will additionally remove the `editable`
   class server-side; this is belt + suspenders for the CSS-only phase. */
.view.view--read-only .editable input,
.view.view--read-only .editable [contenteditable] {
  pointer-events: none;
  caret-color: transparent;
}

/* Drag-to-swap affordance — bible §2.6 phase 1.5c. The Comparison View's
   title slot is the drag handle. Cursor is `grab` at rest, `grabbing`
   while a drag is active. Main is anchored upper-left and never
   participates in drag-swap (its title slot stays default cursor). */
.view--comparison .view-header__title {
  cursor: grab;
}

.view--comparison .view-header__title:active {
  cursor: grabbing;
}

/* Visual states applied imperatively by the drag handler in redesign.js.
   Source dims so it's clear what's "in flight"; drop target gets an
   accent ring so the prospective swap is obvious. */
.view.is-dragging-source {
  opacity: 0.4;
  transition: opacity 0.12s ease;
  pointer-events: none;
}

.view.is-drop-target {
  /* Inset rings — staying inside the view's box keeps them from being
     clipped when the view is flush against the canvas-grid's edge
     (the canvas-grid's ancestor has overflow: hidden). */
  box-shadow: inset 0 0 0 2px var(--accent), inset 0 0 0 4px hsla(214, 97%, 50%, 0.18);
  transition: box-shadow 0.12s ease;
}

/* Global grabbing cursor while a drag is in flight — keeps the affordance
   even when the pointer leaves the source title. user-select: none
   disables text selection so dragging across cells doesn't paint a
   selection highlight. */
body.is-view-dragging,
body.is-view-dragging * {
  cursor: grabbing !important;
  user-select: none !important;
}

/* Comparison View close button — sits in the action slot where Main has
   Save & Recalculate. Same 30 × 30 hit area as the .save-recalc spec for
   visual consistency; muted neutral chrome. */
.view-close-btn {
  appearance: none;
  background: transparent;
  border: none;
  width: 30px;
  height: 30px;
  padding: 0;
  margin: 0;
  border-radius: 50%;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  transition: background 0.12s ease, color 0.12s ease;
}

.view-close-btn:hover {
  background: rgba(0, 0, 0, 0.06);
  color: var(--text);
}

.view-close-btn svg {
  width: 17px;
  height: 17px;
  stroke-width: 2.25;
}

.canvas-body {
  flex: 1 1 auto;
  padding: var(--s-3);
  overflow: auto;
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

/* Per-tab wrapper inside the canvas-body. Each tab is itself a flex column
   with the same gap so cards inside any tab space themselves consistently
   — the canvas-body's own gap only applies between tab wrappers (which are
   never simultaneously visible thanks to x-show, so it's a no-op). */
.canvas-body__tab {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

/* Inner stack used one level deeper than .canvas-body__tab, where an
   x-if (data-presence gate) wraps multiple cards. Same flex column +
   gap so spacing is consistent whether the wrapper is the tab itself
   or a conditional inside it. */
.canvas-body__tab-stack {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

/* -------- PARAMETERS PANEL (v1 sidebar-filters port) -------------- */
.parameters-panel {
  background: var(--view-bg);
  border-radius: var(--r-lg);
  border: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}

.parameters-panel__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  padding: 12px 8px 12px 14px;
  flex: 0 0 auto;
}

.parameters-panel__title {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.01em;
}

.parameters-panel__body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: var(--s-3);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  background: var(--view-bg);
}

/* Each section inside the panel is a SectionCard (bible §4.5). The body
   holds the labeled-input rows. */
.param-card {
  /* Without flex-shrink:0, flex column children get squashed when the
     panel runs out of vertical space — and .section-card's overflow:hidden
     then clips the bottom of each card. Lock natural height and let the
     panel body scroll instead. (Same fix applies in the upload panel,
     which also uses .param-card.) */
  flex-shrink: 0;
}

.param-card .section-card-header {
  /* Card headers in the panel can't open kebabs, so skip the cursor
     change that .section-card-header gets globally. */
  cursor: default;
}

.param-card .section-card-header:hover {
  background: transparent;
}

/* Shape-parameter dirty state — currently used by the Forecast Range card
   when pendingForecast* diverges from the saved values. The dirty signal
   lives on the individual inputs (so the visual matches regular dirty
   cells across v2) plus the section title; the body wash is intentionally
   omitted so the amber fields can pop against the normal card bg. The
   Apply / Cancel row below carries the explicit commit gesture. */
.param-card--dirty .section-card-title {
  color: var(--dirty-text);
}

.param-card--dirty .param-datepicker__trigger {
  background: var(--dirty-bg);
  border-color: var(--dirty-text);
  color: var(--dirty-text);
  font-weight: 700;
}

.param-card--dirty .param-datepicker__trigger svg {
  color: var(--dirty-text);
}

.param-card--dirty .param-datepicker__trigger:hover {
  background: var(--dirty-bg-strong);
  border-color: var(--dirty-text);
}

.param-card--dirty .param-datepicker__trigger.is-open,
.param-card--dirty .param-datepicker__trigger:focus-visible {
  border-color: var(--dirty-text);
  box-shadow: 0 0 0 3px var(--dirty-bg-strong);
}

.param-card__body {
  padding: var(--s-3);
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* Apply / Cancel button row for shape-parameter cards. Tight, right-aligned,
   only present while the card is dirty. Buttons inside come pre-sized
   from .cancel-btn / .action-btn — no overrides needed. */
.param-card__actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--s-2);
  margin-top: var(--s-1);
}

.param-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  min-height: 28px;
}

.param-row--stack {
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
}

.param-row--combo {
  /* Two-column row: label+checkbox on the left, suffix-input on the right. */
  align-items: center;
}

.param-row--combo .param-row__main {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex: 1 1 auto;
  min-width: 0;
}

.param-label {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text);
  line-height: 1.3;
  flex: 1 1 auto;
  min-width: 0;
}

.param-label__hint {
  display: block;
  font-weight: 400;
  font-size: 11.5px;
  color: var(--text-muted);
  margin-top: 1px;
}

/* Vertical stack of radio choices used inside a param-row--stack.
   Promo Pricing + When-promo-ends sit under this pattern. */
.param-radio-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-left: 2px;
}

.param-radio {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12.5px;
  color: var(--text);
  cursor: pointer;
  line-height: 1.3;
}

.param-radio input[type=radio] {
  appearance: none;
  -webkit-appearance: none;
  width: 14px;
  height: 14px;
  border: 1.5px solid var(--border-strong);
  border-radius: 50%;
  background: white;
  margin: 0;
  flex: 0 0 auto;
  cursor: pointer;
  position: relative;
}

.param-radio input[type=radio]:checked {
  border-color: var(--accent);
}

.param-radio input[type=radio]:checked::after {
  content: "";
  position: absolute;
  inset: 2.5px;
  border-radius: 50%;
  background: var(--accent);
}

/* Plain-English readback of the promo configuration. Sits at the
   bottom of the Inputs card; styled as a quiet rule-bracketed summary
   so it reads as derived/computed copy, not user-editable. */
.param-summary {
  margin-top: 6px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  font-size: 12px;
  line-height: 1.45;
  color: var(--text-muted);
  font-style: italic;
}

.param-input {
  appearance: none;
  background: white;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  padding: 5px 8px;
  font: inherit;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  min-width: 0;
  flex: 0 0 auto;
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}

.param-input--num {
  width: 80px;
  text-align: right;
}

.param-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-bg-strong);
}

.param-input:disabled {
  background: var(--header-bg);
  color: var(--text-muted);
  cursor: not-allowed;
}

.param-input-suffix {
  position: relative;
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
}

.param-input-suffix .param-input--num {
  padding-right: 22px;
  width: 90px;
}

.param-suffix {
  position: absolute;
  right: 8px;
  font-size: 12px;
  color: var(--text-muted);
  pointer-events: none;
}

.param-checkbox {
  appearance: none;
  width: 32px;
  height: 18px;
  background: var(--border-strong);
  border-radius: 9px;
  position: relative;
  cursor: pointer;
  flex: 0 0 auto;
  transition: background 0.15s ease;
}

.param-checkbox::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  background: white;
  border-radius: 50%;
  box-shadow: 0 1px 2px rgba(15, 20, 32, 0.2);
  transition: left 0.15s ease;
}

.param-checkbox:checked {
  background: var(--accent);
}

.param-checkbox:checked::after {
  left: 16px;
}

/* -------- DATEPICKER — V2 (parameters panel) --------------------- *
 * Used for Forecast Range + View Range. Click trigger opens a small
 * popup with year nav + month grid. Emits "Mon YYYY" strings.
 * Ported from V1's Alpine component (main.js:152) with V2 visuals. */
.param-datepicker {
  position: relative;
  flex: 0 0 auto;
}

.param-datepicker__trigger {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px 5px 8px;
  background: white;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  font: inherit;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  cursor: pointer;
  min-width: 110px;
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.param-datepicker__trigger svg {
  width: 13px;
  height: 13px;
  color: var(--accent);
  flex: 0 0 auto;
}

.param-datepicker__trigger:hover {
  border-color: var(--accent);
}

.param-datepicker__trigger.is-open,
.param-datepicker__trigger:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-bg-strong);
}

.param-datepicker__value {
  flex: 1 1 auto;
  text-align: right;
}

.param-datepicker__dropdown {
  /* position: fixed so the dropdown escapes the parameters-panel's
     overflow:hidden + each section-card's overflow:hidden. top/right
     are written inline by Alpine based on the trigger's viewport
     rect at open time. */
  position: fixed;
  z-index: 1000;
  background: white;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: 0 8px 24px rgba(15, 20, 32, 0.14), 0 1px 3px rgba(15, 20, 32, 0.06);
  padding: 10px;
  min-width: 200px;
  user-select: none;
}

.param-datepicker__year {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 8px;
  padding: 0 4px;
}

.param-datepicker__year-label {
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  flex: 1 1 auto;
  text-align: center;
}

.param-datepicker__year-btn {
  appearance: none;
  background: transparent;
  border: none;
  padding: 4px;
  border-radius: var(--r-sm);
  color: var(--text-muted);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background 0.12s ease, color 0.12s ease;
}
.param-datepicker__year-btn svg {
  width: 14px;
  height: 14px;
}
.param-datepicker__year-btn:hover {
  background: rgba(0, 0, 0, 0.05);
  color: var(--text);
}

.param-datepicker__months {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 2px;
}

.param-datepicker__month {
  appearance: none;
  background: transparent;
  border: none;
  padding: 7px 4px;
  border-radius: var(--r-sm);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  color: var(--text);
  cursor: pointer;
  transition: background 0.08s ease, color 0.08s ease;
}
.param-datepicker__month:hover {
  background: var(--accent-bg);
  color: var(--accent);
}
.param-datepicker__month.is-selected {
  background: var(--accent);
  color: white;
}
.param-datepicker__month.is-selected:hover {
  background: var(--accent-hover);
  color: white;
}

.param-checkbox:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* -------- Stepper — ± icon buttons flanking a number input ------ */
/* Round 22 px buttons in the panel's accent color, mirroring the
   v1 input-stepper but with v2 chrome (no chunky border, hover
   reveals an accent-tinted fill). */
.param-stepper {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
}

.param-stepper__btn {
  appearance: none;
  background: transparent;
  border: 1.5px solid var(--accent);
  border-radius: 50%;
  width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--accent);
  cursor: pointer;
  padding: 0;
  transition: background 0.12s ease, border-color 0.12s ease;
}

.param-stepper__btn:hover {
  background: var(--accent-bg);
}

.param-stepper__btn:active {
  background: var(--accent-bg-strong);
}

.param-stepper__btn svg {
  width: 12px;
  height: 12px;
  stroke-width: 2.5;
}

.param-stepper__input {
  appearance: none;
  -moz-appearance: textfield;
  background: white;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  padding: 4px 6px;
  font: inherit;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  text-align: center;
  width: 48px;
  color: var(--text);
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}

.param-stepper__input::-webkit-outer-spin-button,
.param-stepper__input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

.param-stepper__input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-bg-strong);
}

/* -------- Dual-thumb slider — Remove Outliers ------------------- */
/* Two stacked native <input type="range"> overlap on a single track.
   The upper one uses dir="rtl" so its thumb anchors to the right edge
   of the wrapper, giving the user independent low/high handles. The
   inputs themselves are pointer-events:none; only the thumb pseudo-
   element accepts pointer events, so clicking the empty track between
   the thumbs falls through harmlessly. The .__fill div between the
   thumbs visually marks the kept (un-removed) cohort range. */
.param-slider {
  display: flex;
  align-items: center;
  gap: 8px;
}

.param-slider__input {
  appearance: none;
  -moz-appearance: textfield;
  background: white;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  padding: 4px 4px;
  font: inherit;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  text-align: center;
  width: 40px;
  color: var(--text);
  flex: 0 0 auto;
}

.param-slider__input::-webkit-outer-spin-button,
.param-slider__input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

.param-slider__input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-bg-strong);
}

.param-slider__track-wrap {
  flex: 1 1 auto;
  position: relative;
  height: 22px;
  display: flex;
  align-items: center;
  /* Bound from Alpine — value/max as a 0..1 fraction. CSS uses these
     to position track/fill so they align with the native range
     input's thumb-center geometry (see __fill below for the calc). */
  --lo-frac: 0;
  --hi-frac: 0;
  /* Half-thumb inset on each end. The native thumb's center is
     constrained to [thumb_radius, width - thumb_radius]; matching that
     range here keeps the track and fill from extending past the thumbs
     at the extremes. */
  --thumb-inset: 7px;
}

/* Empty track — neutral gray pill behind everything else. Inset by a
   half-thumb each side so the track ends where the thumb's outer edge
   sits at min/max. */
.param-slider__track-wrap::before {
  content: "";
  position: absolute;
  left: var(--thumb-inset);
  right: var(--thumb-inset);
  top: 50%;
  transform: translateY(-50%);
  height: 4px;
  background: var(--border-soft);
  border-radius: 2px;
  z-index: 0;
}

/* Fill — accent-colored band between the two thumbs. Position math:
     left  = thumb-radius + lo-frac × (width - 14px)
     right = thumb-radius + hi-frac × (width - 14px)
   This matches the native input's thumb-center mapping (`thumb_radius +
   value/max × (width - thumb_diameter)`), so the fill ends are always
   anchored to the thumb centers regardless of the values. */
.param-slider__fill {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  height: 4px;
  background: var(--accent);
  border-radius: 2px;
  z-index: 1;
  pointer-events: none;
  left: calc(var(--thumb-inset) + var(--lo-frac) * (100% - 14px));
  right: calc(var(--thumb-inset) + var(--hi-frac) * (100% - 14px));
}

.param-slider__range {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  background: transparent;
  -webkit-appearance: none;
  appearance: none;
  pointer-events: none;
  z-index: 2;
  margin: 0;
  padding: 0;
}

/* Custom thumb — small white circle with accent border. The
   `margin-top` corrects for webkit's default behavior of anchoring the
   thumb's TOP to the runnable-track's top: shift up by half the
   (thumb_height - track_height) so the thumb centers vertically on
   the 4 px track. */
.param-slider__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  pointer-events: auto;
  width: 14px;
  height: 14px;
  margin-top: -5px; /* (4px track − 14px thumb) / 2 */
  border-radius: 50%;
  background: white;
  border: 2px solid var(--accent);
  cursor: pointer;
  box-shadow: 0 1px 2px rgba(15, 20, 32, 0.18);
  transition: transform 0.12s ease;
}

.param-slider__range::-moz-range-thumb {
  pointer-events: auto;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: white;
  border: 2px solid var(--accent);
  cursor: pointer;
  box-shadow: 0 1px 2px rgba(15, 20, 32, 0.18);
}

.param-slider__range:active::-webkit-slider-thumb {
  transform: scale(1.1);
}

.param-slider__range::-webkit-slider-runnable-track {
  background: transparent;
  height: 4px;
}

.param-slider__range::-moz-range-track {
  background: transparent;
  height: 4px;
}

.param-slider__hints {
  display: flex;
  justify-content: space-between;
  margin-top: 4px;
  font-size: 11px;
  color: var(--text-subtle);
  /* Pad in by the number-input widths so the labels sit *under*
     the inputs rather than at the wrap edges. */
  padding: 0 4px;
}

/* -------- UPLOAD PANEL (v1 upload-modal port) --------------------- */
/* Mirrors .parameters-panel — same 340px right column, same surface
   chrome. Body scrolls; footer sticks to the bottom for the Cancel /
   Upload buttons (longer content can push past the viewport). */
.upload-panel {
  background: var(--view-bg);
  border-radius: var(--r-lg);
  border: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}

.upload-panel__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  padding: 12px 8px 12px 14px;
  flex: 0 0 auto;
}

.upload-panel__title {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.01em;
}

.upload-panel__body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: var(--s-3);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  background: var(--view-bg);
}

.upload-panel__footer {
  flex: 0 0 auto;
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 10px var(--s-3);
  border-top: 1px solid var(--border);
  background: var(--view-bg);
}

/* Radio + checkbox rows in the upload panel. We use plain styled inputs
   here (not the toggle-switch .param-checkbox) — multi-select lists need
   familiar checkboxes, not switches. */
.upload-radio,
.upload-checkbox {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 0;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text);
  cursor: pointer;
  user-select: none;
}

.upload-radio input[type=radio],
.upload-checkbox input[type=checkbox] {
  appearance: auto;
  margin: 0;
  flex: 0 0 auto;
  accent-color: var(--accent);
  cursor: pointer;
}

.upload-warning {
  margin: 4px 0 0;
  font-size: 11.5px;
  line-height: 1.4;
  color: hsl(28, 70%, 35%);
  background: hsl(40, 90%, 96%);
  border: 1px solid hsl(40, 70%, 80%);
  border-radius: var(--r-sm);
  padding: 6px 8px;
}

.upload-section {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.upload-section + .upload-section {
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px dashed var(--border);
}

.upload-section__detail {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-left: 22px;
  margin-bottom: 4px;
}

.upload-columns {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.upload-column-pill {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 10.5px;
  color: var(--text-muted);
  background: var(--header-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 1px 6px;
  white-space: nowrap;
}

.upload-section__file {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-top: 2px;
}

.upload-file-btn {
  display: inline-flex;
  align-items: center;
  font-size: 12px;
  font-weight: 600;
  color: var(--text);
  background: var(--card-bg);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  padding: 4px 10px;
  cursor: pointer;
  transition: background 0.12s ease;
}

.upload-file-btn:hover {
  background: rgba(0, 0, 0, 0.04);
}

.upload-file-name {
  font-size: 11.5px;
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.upload-file-name.has-file {
  color: var(--text);
  font-weight: 500;
}

.upload-file-name.no-file {
  color: var(--text-muted);
}

.upload-template-link {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
}

.upload-template-link:hover {
  text-decoration: underline;
}

.upload-template-link svg {
  width: 12px;
  height: 12px;
}

.upload-new-model-input {
  width: 100%;
  margin-top: 4px;
}

.upload-errors {
  background: hsl(0, 80%, 97%);
  border: 1px solid hsl(0, 70%, 85%);
  border-radius: var(--r-sm);
  padding: 8px 10px;
}

.upload-errors p {
  margin: 0;
  font-size: 12px;
  color: hsl(0, 65%, 38%);
  line-height: 1.4;
}

.upload-errors p + p {
  margin-top: 4px;
}

.section-card {
  background: var(--card-bg);
  border-radius: var(--r-md);
  /* Version B layered shadow — same as .view, scaled-down login-card values. */
  box-shadow: 0 1px 1px rgba(15, 20, 32, 0.06), 0 4px 10px rgba(15, 20, 32, 0.12), 0 10px 20px rgba(15, 20, 32, 0.1);
  overflow: hidden;
}

/* Collapsed state — header alone is visible; body / table-wrap / add-row
   / chart-body / placeholder all hide. State is persisted per-card via
   localStorage (see v2.store.js → setCardCollapsed). Click on header
   toggles in either direction. (2026-04-29 lock — universal across all
   section cards.) */
.section-card.collapsed > *:not(.section-card-header) {
  display: none;
}

.section-card-header {
  cursor: pointer;
}

.section-card-header:hover {
  background: rgba(0, 0, 0, 0.025);
}

/* Inactive state — applied to .section-card when the section has no data
   (empty Adjustments) or is redundant (Adjusted Retention Curve identical
   to baseline). Header de-emphasizes: gray title, hidden meta, suppressed
   hover-revealed actions. Card body still renders normally. */
.section-card.is-inactive .section-card-title {
  color: var(--text-subtle);
  font-weight: 600;
}

.section-card.is-inactive .section-card-meta {
  display: none;
}

.section-card.is-inactive .card-actions {
  display: none;
}

/* Skeleton/shimmer state — applied to every section-card while the
   active model's data is loading (initial mount, rail-click swap).
   Replaces the old "x-show=modelLoaded" blanking that left the entire
   canvas-body empty for the duration of the cascade.

   Strategy: keep the card header visible (title still tells the user
   what they're looking at), hide every other child, and overlay a
   gradient-driven shimmer. The shimmer animation is GPU-composited,
   so it keeps animating even while Alpine's setModelData cascade has
   the main thread blocked.

   Card meta + hover-revealed actions also hide during skeleton so the
   header doesn't render half-populated chrome against missing data. */
.section-card.is-skeleton {
  position: relative;
  min-height: 200px;
}

.section-card.is-skeleton > *:not(.section-card-header) {
  visibility: hidden;
}

.section-card.is-skeleton .section-card-meta,
.section-card.is-skeleton .card-actions {
  visibility: hidden;
}

/* Two-pseudo-element shimmer:
     ::before = static light-grey base block (the loading rectangle)
     ::after  = brighter highlight band, translateX sweeps it across
   transform: translateX is GPU-accelerated and animation-reliable in
   every browser. */
.section-card.is-skeleton::before {
  content: "";
  position: absolute;
  left: 14px;
  right: 14px;
  top: 52px; /* clear the header strip (~41 px + a small breather) */
  bottom: 14px;
  border-radius: var(--r-sm, 4px);
  background: #e3e7ec;
  pointer-events: none;
}

.section-card.is-skeleton::after {
  content: "";
  position: absolute;
  top: 52px;
  bottom: 14px;
  left: 14px;
  width: 40%;
  border-radius: var(--r-sm, 4px);
  /* Highlight gradient — transparent edges so the band blends into the
     base block, brighter center for the sweep effect. */
  background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.85) 50%, rgba(255, 255, 255, 0) 100%);
  transform: translateX(-100%);
  animation: section-card-skeleton-sweep 1.6s linear infinite;
  pointer-events: none;
  will-change: transform;
}

/* Collapsed skeleton — only the header bar should be visible, no shimmer. */
.section-card.is-skeleton.collapsed {
  min-height: 0;
}

.section-card.is-skeleton.collapsed::before,
.section-card.is-skeleton.collapsed::after {
  display: none;
}

/* translateX percentages refer to the element's own width (40% of card).
   -100% parks the highlight just off the left edge of the base block;
   250% pushes it past the right edge (40% × 350% sweep = 140% of card
   width travelled — covers the body plus full entry/exit). Section-card
   has overflow:hidden so the offscreen portions clip cleanly. */
@keyframes section-card-skeleton-sweep {
  0% {
    transform: translateX(-100%);
  }
  100% {
    transform: translateX(250%);
  }
}
/* Empty-state filler. Lives inside a .section-card and provides its own
   centered layout + min-height since the card itself no longer pads or
   centers (it's now a frame for header / table-wrap / add-row sections). */
.section-card-placeholder {
  min-height: 320px;
  padding: var(--s-5);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-subtle);
  font-size: 12.5px;
  font-weight: 500;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  letter-spacing: 0.01em;
}

/* -------- TEMPORARY canvas placeholder --------------------------- */
.canvas-welcome {
  padding: var(--s-7);
  max-width: 720px;
}

.canvas-welcome h1 {
  margin: 0 0 var(--s-3);
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.02em;
}

.canvas-welcome p {
  margin: 0 0 var(--s-3);
  color: var(--text-muted);
  line-height: 1.6;
}

.canvas-welcome a {
  color: var(--accent);
  text-decoration: none;
}

.canvas-welcome a:hover {
  text-decoration: underline;
}

/* ====================================================================
 * TABLE SYSTEM — locked across workshops 13/13a/13b/13c.
 *
 * Encoding triad: bg = type · text = state · ring = focus.
 *
 * One base (.stx-table) + three flavor classes:
 *   .flavor-baseline     — Baseline Retention Curve (13a). Editable + dirty cells.
 *   .flavor-adjustments  — Cohort Adjustments (13b). Multi-line row labels, +Add row.
 *   .flavor-output       — Model Outputs (13c). Two-tier header, green Actuals,
 *                          row-level expand. Read-only.
 *
 * Cell-level dirty (amber fill + bold) only applies on flavors with a
 * calculated baseline (Baseline Retention, Reactivation Model). Adjustments
 * have no calculated baseline — every value is direct user input — so they
 * use only the view-level "needs recalc" signal (Save & Recalc pill amber).
 * ==================================================================== */
/* -------- SECTION CARD HEADER STRIP (used by all flavors) -------- */
.section-card-header {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: 11px 10px 10px 14px;
  border-bottom: 1px solid var(--border-soft);
}

.section-card-title {
  font-size: 13.5px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.005em;
  flex: 0 0 auto;
}

.section-card-meta {
  margin-left: var(--s-3);
  color: var(--text-muted);
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.section-card-meta strong {
  color: var(--text);
  font-weight: 600;
}

.section-card-meta .sep {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: var(--border-strong);
  display: inline-block;
}

/* Per-chip wrapper inside .section-card-meta — groups the optional
   separator dot (chips after the first) with the chip's value+label
   span so they stay tight together when the meta row wraps. */
.section-card-meta__chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

/* -------- CARD ACTIONS — hover-revealed icon button cluster ------
 * Used in section-card headers (card_actions.html partial). Designed
 * to be reusable for any card-like surface (rail items next).
 * ----------------------------------------------------------------- */
.card-actions {
  margin-left: var(--s-2);
  display: flex;
  align-items: center;
  gap: 2px;
  opacity: 0;
  transition: opacity 0.12s ease;
}

.section-card:hover .card-actions,
.ws-card-header:hover .card-actions,
.nav-item:hover .card-actions,
.view-header__title:hover .card-actions,
.card-actions:focus-within {
  opacity: 1;
}

/* Rail-context override: tighter buttons to fit the smaller row heights
   of project headers and model rows. */
.ws-card-header .card-action-btn,
.nav-item .card-action-btn {
  width: 22px;
  height: 22px;
}

.ws-card-header .card-action-btn svg,
.nav-item .card-action-btn svg {
  width: 13px;
  height: 13px;
}

.ws-card-header .card-actions,
.nav-item .card-actions {
  margin-left: 0;
}

.card-action-btn {
  appearance: none;
  background: transparent;
  border: none;
  margin: 0;
  padding: 0;
  cursor: pointer;
  width: 28px;
  height: 28px;
  border-radius: var(--r-sm);
  color: var(--text-muted);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background 0.12s ease, color 0.12s ease;
  text-decoration: none;
}

.card-action-btn:hover {
  background: rgba(0, 0, 0, 0.06);
  color: var(--text);
}

.card-action-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.card-action-btn svg {
  width: 16px;
  height: 16px;
}

/* Pop-out variant of card-action-btn — bible §2.3 universal pop-out
   (rail-item variant, Wave 2c 2026-05-09). Lives inside the rail row's
   `.card-actions` cluster as a third button after rename + delete. Always
   accent blue per bible §4.2. Disabled state used when the canvas is at
   the 4-view ceiling (§2.6) — same `card-actions` hover-reveal as the
   neutral siblings, but greys out + non-clickable on disabled. */
.card-action-btn--popout {
  color: var(--accent);
}

.card-action-btn--popout:hover {
  background: var(--accent-bg);
  color: var(--accent-hover);
}

.card-action-btn--popout.is-disabled {
  cursor: not-allowed;
  color: var(--text-subtle);
}

.card-action-btn--popout.is-disabled:hover {
  background: transparent;
  color: var(--text-subtle);
}

/* Copy-success affordance — icon briefly swaps to a check; tint it the
   accounting-green token so the success state reads at a glance without
   needing a toast. Force opacity:1 so the copied state stays visible
   even if the cursor leaves the section-card mid-feedback. */
.card-action-btn.is-copy-success {
  color: var(--actual-text);
  background: transparent;
}

.card-actions:has(.card-action-btn.is-copy-success) {
  opacity: 1;
}

/* -------- TOOLTIPS — generic [data-tooltip] -----------------------
 * Any element with `data-tooltip="..."` gets a CSS-only tooltip below
 * it on hover (or keyboard focus). Short delay so quick mouse passes
 * don't trigger it. Dark-text-on-light card to match the surface
 * language of the redesign.
 *
 * `position: fixed` (with viewport coords from JS via --tt-top / --tt-left)
 * so the tooltip escapes overflow ancestors — `.table-wrap` clips with
 * `overflow-x: auto; overflow-y: hidden`, and CSS spec forces visible-on-
 * one-axis to compute as auto when the other is scroll/auto, so a pure-CSS
 * `position: absolute` tooltip inside a table can't escape the wrap. JS
 * sets the vars on `mouseover`; see redesign.js. Caveat: a `transform` on
 * the [data-tooltip] element (or any ancestor) creates a fixed-positioning
 * containing block, breaking this — so the cell-fill-handle uses
 * `top: calc(50% - 7px)` instead of `transform: translateY(-50%)` for
 * vertical centering. */
[data-tooltip] {
  position: relative;
}

[data-tooltip]::after {
  content: attr(data-tooltip);
  position: fixed;
  top: var(--tt-top, 0);
  left: var(--tt-left, 0);
  transform: translateX(-50%);
  background: var(--card-bg);
  color: var(--text);
  border: 1px solid var(--border);
  box-shadow: 0 4px 10px rgba(15, 20, 32, 0.1), 0 1px 2px rgba(15, 20, 32, 0.06);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.01em;
  padding: 4px 8px;
  border-radius: 4px;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.12s ease;
  z-index: 1000;
}

[data-tooltip]:hover::after,
[data-tooltip]:focus-visible::after {
  opacity: 1;
  transition-delay: 0.4s;
}

/* -------- CONFIRM MODAL — generic destructive-action dialog ------
 * Driven by $store.v2Store.confirm. The overlay itself dismisses on
 * click-outside (rendered template handles @click.self + Escape).
 * Surface language matches the redesign: layered shadow, white card,
 * cool-grey borders. Danger-action button is rose-red; primary
 * (non-danger) variant uses --accent for future non-destructive uses. */
.confirm-modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(15, 20, 32, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 200;
  padding: 24px;
}

/* -------- LOADING MODAL — long-load indicator -------------------
 * Driven by $store.v2Store.loadingModalVisible. The store sets it to true
 * synchronously at the start of every user-initiated heavy operation
 * (model swap, recalculate); the CSS animation here gates the *visual*
 * appearance with a 250 ms delay before fading in. Operations that
 * complete inside the delay window unmount the modal before it ever
 * paints — silent for fast ops, visible for slow ones, all without any
 * JS timing.
 *
 * Why CSS-delay instead of JS setTimeout: a setTimeout-based threshold
 * gets cancelled when the swap's _clearLoadingAfterRender runs (or
 * blocked from firing while Alpine's render is JS-blocking the loop).
 * The CSS animation runs on the compositor thread, so it progresses
 * through the delay+fade even while JS is mid-block — meaning the modal
 * actually shows up *during* the freeze, not just after.
 *
 * Same translucent dimmer language as the confirm modal but lighter
 * (this is "wait", not "danger"). Centered card with the brand S-mark
 * pulsing at 2 s — same cadence as the inline Save & Recalc pill
 * spinner so the two indicators read as one system at different
 * scales. No buttons — the user can't cancel mid-load and shouldn't
 * see chrome suggesting they could. */
.loading-modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(15, 20, 32, 0.32);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 250;
  padding: 24px;
  /* Start invisible; fade in after a 250 ms delay so fast ops never
     visually surface. `forwards` keeps the final opacity:1 state. */
  opacity: 0;
  animation: loading-modal-fade-in 0.18s ease-out 1s forwards;
}

.loading-modal {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 20px 50px rgba(15, 20, 32, 0.25), 0 4px 12px rgba(15, 20, 32, 0.1);
  padding: 32px 40px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

/* The pulse animation is on the wrapping <div>, not the <svg> inside —
   SVG elements aren't reliably promoted to their own compositor layer
   even with `will-change`, so the animation would end up running on the
   main thread and freeze during Alpine's JS-blocking setModelData render
   on swap. A block-element wrapper with `translateZ(0)` belt-and-
   suspenders forces a dedicated compositor layer, so the animation
   advances on the compositor thread and stays visibly pulsing across
   the entire freeze. The SVG inside is just static content sized to
   fill the wrapper. */
.loading-modal__mark {
  width: 56px;
  height: 56px;
  display: block;
  will-change: opacity, transform;
  transform: translateZ(0);
  animation: loading-modal-pulse 2s ease-in-out infinite;
}

.loading-modal__mark-svg {
  width: 100%;
  height: 100%;
  display: block;
}

.loading-modal__label {
  font-size: 14px;
  font-weight: 600;
  color: var(--text-muted);
  letter-spacing: 0.01em;
}

/* `scale3d()` instead of `scale()`: the 3D form forces the element into
   a 3D rendering context for the entire animation, keeping it on its
   own compositor layer throughout the cycle. With plain 2D `scale()`
   Chrome will sometimes drop the layer mid-animation if it thinks the
   transform can be done in 2D — which would defeat the whole point of
   keeping this animation off the JS-blocked main thread. */
@keyframes loading-modal-pulse {
  0%, 100% {
    opacity: 1;
    transform: scale3d(1, 1, 1);
  }
  50% {
    opacity: 0.45;
    transform: scale3d(0.94, 0.94, 1);
  }
}
@keyframes loading-modal-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
.confirm-modal {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 20px 50px rgba(15, 20, 32, 0.25), 0 4px 12px rgba(15, 20, 32, 0.1);
  width: 100%;
  max-width: 440px;
  padding: 24px;
}

.confirm-modal__title {
  font-size: 17px;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 12px;
}

.confirm-modal__body {
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-muted);
  margin-bottom: 20px;
}

.confirm-modal__body p {
  margin: 0 0 8px;
}

.confirm-modal__body p:last-child {
  margin-bottom: 0;
}

.confirm-modal__body strong {
  font-weight: 700;
  color: var(--text);
}

.confirm-modal__warning {
  margin-top: 12px !important;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: hsl(0, 65%, 45%);
}

.confirm-modal__actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

/* Standalone Cancel button used everywhere a "Cancel" action appears in
   v2 (confirm modal, upload panel, parameters panel, sources_detail
   inline editors, sources_new). One source of truth so the visual stays
   consistent across surfaces. */
.cancel-btn {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--card-bg);
  color: var(--text);
  font-size: 13px;
  font-weight: 600;
  padding: 8px 16px;
  cursor: pointer;
  text-decoration: none;
  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}

.cancel-btn:hover {
  background: rgba(0, 0, 0, 0.04);
}

.cancel-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.cancel-btn[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Solid action button — paired with .cancel-btn in any action row.
   Three variants encode the action's nature:
     - default: primary general action (accent blue)
     - --danger: destructive (red); pairs with confirm modals
     - --save:   save / recalc-style commits (amber, matches Save & Recalc pill)
   No border, white text, same chrome as .cancel-btn for consistent rhythm. */
.action-btn {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: var(--r-md);
  background: var(--accent);
  color: #fff;
  font-size: 13px;
  font-weight: 600;
  padding: 8px 16px;
  cursor: pointer;
  text-decoration: none;
  transition: background 0.12s ease;
}

.action-btn:hover {
  background: var(--accent-hover);
}

.action-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.action-btn[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.action-btn--danger {
  background: hsl(0, 65%, 50%);
}

.action-btn--danger:hover {
  background: hsl(0, 65%, 42%);
}

.action-btn--danger:focus-visible {
  outline-color: hsl(0, 65%, 50%);
}

.action-btn--save {
  background: var(--dirty-pill-bg);
}

.action-btn--save:hover {
  background: var(--dirty-pill-bg-hover);
}

.action-btn--save:focus-visible {
  outline-color: var(--dirty-pill-bg);
}

/* Danger styling for the trash button: muted by default, rose-red on
   hover so it visually distinguishes from the rename pencil. */
.card-action-btn--danger:hover {
  background: hsl(0, 70%, 95%);
  color: hsl(0, 65%, 45%);
}

/* -------- TABLE — base ------------------------------------------- */
.table-wrap {
  overflow-x: auto;
  overflow-y: hidden;
  padding-bottom: 8px; /* breathing room above the horizontal scrollbar */
}

.stx-table {
  /* `table-layout: fixed` honors per-cell width/max-width strictly so the
     row-label column stays 200 px and num columns stay 80 px regardless of
     content length or how few columns there are. Without it (auto layout),
     a narrow table forced to fill its card via `min-width: 100%` would
     redistribute the extra space into the row-label column — which is the
     exact bug we hit on the Age-based Summary (only ~9 cohort ages, table
     stretched to fill card, row-label ballooned to ~600 px). Pairing fixed
     layout with `width: max-content` and no `min-width` means narrow
     tables stay at their natural width (small white gap to right of card
     is preferable to runaway columns); wide tables overflow horizontally
     into the scrollable .table-wrap as before. */
  table-layout: fixed;
  width: max-content;
  border-collapse: separate;
  border-spacing: 0;
  font-size: 12.5px;
  color: var(--text);
  background: var(--card-bg);
}

/* Header row — bold, all caps. Background tints to the active tab's
   header variant inside any `.view`; falls back to neutral --header-bg
   for tables outside the view system. */
.stx-table thead th {
  background: var(--active-header, var(--header-bg));
  transition: background 0.18s ease;
  color: var(--text);
  font-weight: 700;
  font-size: 12.5px;
  letter-spacing: 0;
  padding: 6px 10px;
  text-align: left;
  border-bottom: 1px solid var(--border-strong);
  white-space: nowrap;
}

.stx-table thead th.num {
  text-align: right;
}

/* Upper-left header cell (the row-label column's header — "Cohort Age",
   "Period", "Adjustment", etc.). Drops the shaded header bg and right-
   justifies so the header row stops reading as a hard horizontal wall;
   the row-label column flows visually with the body's row labels. */
.stx-table thead th:first-child:not(.num) {
  background: var(--card-bg);
  text-align: right;
}

/* Body cells — TIGHT density. */
.stx-table tbody th,
.stx-table tbody td {
  padding: 4px 10px;
  border-bottom: 1px solid var(--border-soft);
  line-height: 1.35;
  vertical-align: middle;
  background: var(--card-bg); /* sticky cells need their own bg */
}

.stx-table tbody tr:last-child th,
.stx-table tbody tr:last-child td {
  border-bottom: none;
}

.stx-table .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Row labels (left column) — uppercase tracked, plain card BG. Inherits
   --card-bg from the generic `tbody th` rule above. Only the column-header
   row tints per-tab; the row-label column stays white so the table reads
   as "tinted top strip + white body" rather than an L-shaped frame. */
.stx-table tbody th.row-label {
  text-align: left;
  font-weight: 700;
  color: var(--text);
  font-size: 12.5px;
  letter-spacing: 0;
  padding: 4px 12px;
  white-space: nowrap;
}

.stx-table .ci-row td {
  color: var(--text-muted);
  font-weight: 400;
}

.stx-table .na-cell {
  color: var(--text-subtle);
}

/* Standardized column widths: 80 px data · 200 px row label. The
   :not(.num) qualifier matters for the output flavor's two-tier header —
   tier-2's first <th> has class .num (it's the leftmost Actual/Forecast
   tag), so the row-label width + sticky-left rules must not match it. */
.stx-table thead th.num,
.stx-table tbody td.num {
  width: 80px;
  min-width: 80px;
}

.stx-table thead th:first-child:not(.num),
.stx-table tbody th.row-label {
  width: 200px;
  min-width: 200px;
  max-width: 200px;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Sticky chrome — header (top) + first column (left). Sticky cells need
   an explicit background to occlude what's scrolling underneath. */
.stx-table thead th {
  position: sticky;
  top: 0;
  z-index: 2;
}

.stx-table thead th:first-child:not(.num),
.stx-table tbody th.row-label {
  position: sticky;
  left: 0;
  z-index: 1;
  box-shadow: 1px 0 0 var(--border-soft);
}

.stx-table thead th:first-child:not(.num) {
  z-index: 3;
}

/* -------- EDITABLE / DIRTY / FOCUS — the encoding triad ----------
   Editable cells use accent text on plain card BG (no fill at rest). Dirty
   layers an amber fill + bold dark-amber text on top — only on tables with a
   calculated baseline (.flavor-baseline / Reactivation Model). Focus adds an
   accent ring; on dirty cells the text also flips accent so the user sees
   what they're typing against the warm bg. */
.stx-table .editable {
  color: var(--editable-text);
  cursor: text;
  position: relative;
}

.stx-table .editable .cell-input {
  color: var(--editable-text);
}

.stx-table .editable.dirty {
  background: var(--dirty-bg);
  color: var(--dirty-text);
  font-weight: 700;
}

.stx-table .editable.dirty .cell-input {
  color: var(--dirty-text);
  font-weight: 700;
}

/* Two-state focus visual:
   • Selected (focus-within, NOT actively editing): frame only — light
     touch; signals "this cell still has the keyboard, you can tab from
     here" after pressing Enter.
   • Editing (.is-editing + focus-within): frame + accent-bg fill —
     fuller visual; signals "currently typing into this cell". The
     `is-editing` class is managed in redesign.js around focusin/blur
     and the Enter keydown. */
.stx-table .editable:focus-within {
  box-shadow: inset 0 0 0 1.5px var(--accent);
  z-index: 1;
}

.stx-table .editable.is-editing:focus-within {
  background: var(--accent-bg);
}

.stx-table .editable.dirty.is-editing:focus-within {
  background: var(--dirty-bg);
  color: var(--accent);
}

.stx-table .editable.dirty.is-editing:focus-within .cell-input {
  color: var(--accent);
}

.cell-input {
  display: block;
  width: 100%;
  background: transparent;
  border: none;
  outline: none;
  font: inherit;
  font-family: inherit;
  color: inherit;
  text-align: right;
  padding: 0;
  margin: 0;
  font-variant-numeric: tabular-nums;
  font-weight: inherit;
}

/* -------- CELL FILL-HANDLE — copy value forward (v1 right-click port) -----
   A small grip on the LEFT edge of editable numeric cells. Click copies the
   cell's value to every remaining editable cell to its right in the same
   row. Hidden at rest; revealed on cell hover or focus-within. Left edge,
   not right, because numeric content is right-aligned — putting the affordance
   left keeps it clear of the value. tabindex=-1 keeps it out of the keyboard
   tab order so Tab still walks input → input. The 14×18 hit zone is generous
   for clicking even though the visible mark is a 6 px Excel-style square. */
.cell-fill-handle {
  /* Centered over the cell's right edge — half in the cell's right padding
     (the empty zone after a right-aligned numeric value), half extending
     past the cell border. Mirrors Excel's bottom-right fill-handle position
     in spirit; "right edge" is the natural anchor for a forward-direction
     action because the cell's content visually terminates there. z-index
     lifts the button above the next cell so clicks on the half-overhang
     still hit this button rather than the neighboring cell.
     Vertical centering uses `top: calc(50% - 7px)` not `transform:
     translateY(-50%)` because a transform on this element would create
     a fixed-positioning containing block, breaking the global
     `[data-tooltip]` pseudo-element's escape from `.table-wrap`. */
  position: absolute;
  right: -7px;
  top: calc(50% - 7px);
  width: 14px;
  height: 14px;
  padding: 0;
  margin: 0;
  border: none;
  background: transparent;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Dirty-text amber, not a neutral muted gray. Two reasons: (a) amber pops
     against the right-aligned numeric content (which is accent blue or dark
     amber when dirty), and (b) it semantically previews the action — clicking
     this button flips the affected cells to the dirty (amber) state, so the
     icon already wears its outcome. */
  color: var(--dirty-text);
  z-index: 2;
}

.cell-fill-handle svg {
  width: 12px;
  height: 12px;
  display: block;
  transition: color 0.1s ease, transform 0.08s ease;
}

/* Reveal on cell hover OR when the cell is the active edit target.
   Grid cells (.grid-cell) participate too — same affordance, gated on
   non-empty cells so the handle doesn't appear over pre-cohort blanks. */
.stx-table .editable:hover .cell-fill-handle,
.stx-table .editable:focus-within .cell-fill-handle,
.stx-table .grid-cell:not(.is-empty):hover .cell-fill-handle,
.stx-table .grid-cell:not(.is-empty):focus-within .cell-fill-handle {
  opacity: 1;
  pointer-events: auto;
}

/* When the cell is the active edit target, the button gets a "chip"
   treatment — card-bg fill + accent ring matching the focus ring — so it
   reads as a deliberate part of the cell's edge rather than a floating
   icon. The chip sits *on* the focus ring at the cell's right border;
   z-index keeps it above any neighboring cell content. Icon color stays
   dirty-text so the amber/blue contrast carries the "this action will
   dirty cells" preview through the active state. */
.stx-table .editable:focus-within .cell-fill-handle,
.stx-table .grid-cell:focus-within .cell-fill-handle {
  background: var(--card-bg);
  box-shadow: 0 0 0 1.5px var(--accent);
  border-radius: 4px;
  z-index: 5;
}

/* When ANY cell in the table is the active edit target, suppress
   hover-revealed handles on all other cells — only one handle visible at
   a time so the user isn't confronted with a roaming icon while editing.
   :has accepts either .editable or .grid-cell as the focused cell. */
.stx-table:has(.editable:focus-within, .grid-cell:focus-within) .editable:not(:focus-within) .cell-fill-handle,
.stx-table:has(.editable:focus-within, .grid-cell:focus-within) .grid-cell:not(:focus-within) .cell-fill-handle {
  opacity: 0;
  pointer-events: none;
}

.cell-fill-handle:hover svg {
  transform: scale(1.1);
}

.cell-fill-handle:active svg {
  transform: scale(0.96);
}

/* -------- ADJUSTMENT ROW LABEL — multi-line composite (13b) ------
   Layout grid:
     [check]  [editable name              ]  [delete (hover-revealed)]
              [editable date range         ]
   Object names are sentence-case (NOT uppercase) — they're user input,
   not structural scaffolding. */
.stx-table tbody th.adjustment-label {
  text-align: left;
  padding: 4px 10px;
  background: var(--card-bg);
  text-transform: none;
  font-size: inherit;
  letter-spacing: 0;
  font-weight: 400;
  vertical-align: middle;
}

.adjustment-label-inner {
  display: grid;
  grid-template-columns: auto 1fr auto;
  column-gap: 10px;
  row-gap: 0;
  align-items: center;
}

.row-check {
  appearance: none;
  grid-column: 1;
  grid-row: 1/3;
  width: 14px;
  height: 14px;
  border: 1.5px solid var(--border-strong);
  border-radius: 3px;
  cursor: pointer;
  background: white;
  flex-shrink: 0;
  align-self: center;
  transition: background-color 0.12s ease, border-color 0.12s ease;
  background-position: center;
  background-repeat: no-repeat;
  background-size: 11px 11px;
}

.row-check:checked {
  background-color: var(--accent);
  border-color: var(--accent);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='white' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'><polyline points='2.5,6.5 5,9 9.5,4'/></svg>");
}

.adjustment-name {
  grid-column: 2;
  grid-row: 1;
  appearance: none;
  background: transparent;
  border: none;
  outline: none;
  font: inherit;
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 700;
  line-height: 1.25;
  color: var(--accent);
  padding: 0;
  margin: 0;
  width: 100%;
  cursor: text;
  letter-spacing: -0.005em;
  /* Textarea wrap behavior: auto-grow up to 2 lines, then clip. */
  field-sizing: content;
  resize: none;
  overflow: hidden;
  max-height: 2.5em;
  white-space: pre-wrap;
  word-break: break-word;
  display: block;
}

.adjustment-name::placeholder {
  color: var(--text-subtle);
}

.adjustment-name:focus {
  outline: 1.5px solid var(--accent);
  outline-offset: 1px;
  border-radius: 2px;
}

.adjustment-dates {
  grid-column: 2/4;
  grid-row: 2;
  appearance: none;
  background: transparent;
  border: none;
  color: var(--accent);
  font: inherit;
  font-size: 11.5px;
  line-height: 1.2;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
  cursor: pointer;
  padding: 0;
  margin: 0;
  text-align: left;
  letter-spacing: 0;
  justify-self: start;
}

.adjustment-dates:hover {
  color: var(--accent-hover);
}

.row-delete {
  grid-column: 3;
  grid-row: 1;
  appearance: none;
  background: transparent;
  border: none;
  color: var(--text-subtle);
  cursor: pointer;
  padding: 2px;
  width: 20px;
  height: 20px;
  border-radius: var(--r-sm);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.12s ease, color 0.12s ease, background 0.12s ease;
}

.row-delete svg {
  width: 14px;
  height: 14px;
}

.stx-table tbody tr:hover .row-delete {
  opacity: 1;
}

.row-delete:hover {
  color: hsl(0, 65%, 45%);
  background: hsl(0, 65%, 96%);
}

/* Inactive row state — checkbox unchecked. Mutes all chrome so the row reads
   as "stored but currently disabled, here for reference but not in the math." */
tr.row-inactive .editable {
  color: var(--text-subtle);
}

tr.row-inactive .editable .cell-input {
  color: var(--text-subtle);
}

tr.row-inactive .adjustment-name {
  color: var(--text-subtle);
}

tr.row-inactive .adjustment-dates {
  color: var(--text-subtle);
}

tr.row-inactive td {
  color: var(--text-subtle);
}

tr.row-inactive .editable.dirty {
  background: hsl(38, 50%, 95%);
  color: var(--text-subtle);
  font-weight: 400;
}

tr.row-inactive .editable.dirty .cell-input {
  color: var(--text-subtle);
  font-weight: 400;
}

/* Stale-results pill — lives in the section-card-header, right-justified,
   only visible when the card's contents reflect a previous recalc. Whole
   pill is a clickable Save & Recalc shortcut. Same amber as the dirty
   pill, matching the input-side language. */
.stale-banner {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border: 1px solid var(--dirty-bg-strong);
  border-radius: 999px;
  background: var(--dirty-bg);
  color: var(--dirty-text);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: -0.005em;
  cursor: pointer;
  transition: background 80ms ease;
}

.stale-banner:hover {
  background: var(--dirty-bg-strong);
}

.stale-banner svg {
  width: 12px;
  height: 12px;
  stroke-width: 2.25;
}

/* While the recalc is running, the banner keeps reading "Recalculating…"
   and pulses its bg between the two amber tints in sync with the Save &
   Recalc pill (same 2 s cadence). Cursor flips to default — clicking is
   redundant since the recalc is already in flight. */
.stale-banner.is-loading {
  cursor: default;
  animation: stale-banner-pulse 2s ease-in-out infinite;
}

.stale-banner.is-loading svg {
  animation: stale-banner-spin 0.9s linear infinite;
}

@keyframes stale-banner-pulse {
  0%, 100% {
    background: var(--dirty-bg);
  }
  50% {
    background: var(--dirty-bg-strong);
  }
}
@keyframes stale-banner-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
/* Stale card body — text fades to a soft amber (~70% washed-out), keeping
   the language coherent with the input-side amber dirty state. Header
   chrome stays legible. Charts override their --chart-* tokens to amber
   so axis lines, tick labels, and data lines all shift in unison. */
.section-card.is-stale .table-wrap,
.section-card.is-stale .table-wrap th,
.section-card.is-stale .table-wrap td,
.section-card.is-stale .table-wrap .row-label,
.section-card.is-stale .table-wrap .cell-input,
.section-card.is-stale .table-wrap .editable {
  color: var(--stale-text);
  transition: color 120ms ease;
}

/* Header meta strip is also computed (R², period counts, cohort counts) —
   fade in unison with the body so the whole card reads as not-current. Title
   stays at full color since it's identity, not data. */
.section-card.is-stale .section-card-meta,
.section-card.is-stale .section-card-meta strong {
  color: var(--stale-text);
}

.section-card.is-stale .section-card-meta .sep {
  background: var(--stale-text);
  opacity: 0.4;
}

.section-card.is-stale .stx-chart {
  --chart-line: var(--stale-text);
  --chart-axis: hsl(28, 50%, 70%);
  --chart-axis-label: var(--stale-text);
  --chart-tick: var(--stale-text);
  --chart-grid: hsl(28, 30%, 90%);
  transition: filter 120ms ease;
}

/* Adjustment row "unsaved" states (bible §3.8 amendment).
   row-new            — added locally, not yet persisted. Whole row amber.
   row-pending-delete — flagged for delete but kept visible until Save & Recalc.
                        Whole row amber + strikethrough text. Click trash again to undo. */
tr.row-new td,
tr.row-new th,
tr.row-pending-delete td,
tr.row-pending-delete th {
  background: var(--dirty-bg);
}

tr.row-new .editable .cell-input,
tr.row-new .adjustment-name,
tr.row-pending-delete .editable .cell-input,
tr.row-pending-delete .adjustment-name {
  color: var(--dirty-text);
  font-weight: 700;
}

tr.row-pending-delete .adjustment-name,
tr.row-pending-delete .adjustment-dates,
tr.row-pending-delete .cell-input,
tr.row-pending-delete td > span {
  text-decoration: line-through;
  text-decoration-thickness: 1.5px;
}

/* -------- CARD ADD ROW — sits OUTSIDE the scrollable area (13b) -- */
.card-add-row {
  padding: 10px 14px 14px;
  background: var(--card-bg);
  border-top: 1px solid var(--border-soft);
}

.add-button {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-md);
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.12s ease;
}

.add-button:hover {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--accent-bg);
}

.add-button svg {
  width: 14px;
  height: 14px;
}

/* -------- TWO-TIER HEADER (.flavor-output) -----------------------
   Tier 1: Period date (bold, uppercase, cool blue-grey).
   Tier 2: Actual / Forecast sub-label (smaller, color-coded). */
.stx-table.flavor-output thead tr.tier-2 th {
  font-weight: 600;
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
  padding-top: 4px;
  padding-bottom: 5px;
  border-bottom: 1px solid var(--border-strong);
}

.stx-table.flavor-output thead tr.tier-2 th.tag-actual {
  color: var(--actual-text);
}

.stx-table.flavor-output thead tr.tier-2 th.tag-forecast {
  color: var(--text-subtle);
}

/* Sticky 2-row header: tier 1 at top: 0, tier 2 at top: 28px (matches
   tier-1 row height). Period column header spans both via rowspan=2. */
.stx-table.flavor-output thead tr.tier-1 th {
  top: 0;
}

.stx-table.flavor-output thead tr.tier-2 th {
  top: 28px;
  position: sticky;
  z-index: 2;
}

.stx-table.flavor-output thead th.row-header-cell {
  text-align: left;
  top: 0;
  position: sticky;
  left: 0;
  z-index: 3;
  box-shadow: 1px 0 0 var(--border-soft);
}

/* Output flavor — green Actuals, subtle full-row hover, sentence-case row labels. */
.stx-table .num.actual {
  color: var(--actual-text);
}

/* Reactivation Model + Reactivations: past-column cells use the same
   accounting-green token as Output's Actual columns. Same encoding,
   different class (these tables don't have the two-tier output header
   so we mark per-cell instead of per-column-group). Read-only cells
   in the Reactivation Model card pick this up too — past columns are
   non-editable. */
.stx-table .num.is-actual {
  color: var(--actual-text);
}

/* Reactivations: the "Total" row is the server-side aggregate across
   age buckets — bolder weight + a subtle top border so it visually
   separates from the bucket rows above. */
.stx-table tbody tr.total-row th.row-label,
.stx-table tbody tr.total-row td {
  font-weight: 700;
  border-top: 1px solid var(--border-strong);
}

.stx-table.flavor-output tbody tr:hover td {
  background: hsl(214, 25%, 97%);
}

.stx-table.flavor-output tbody th.row-label {
  font-weight: 600;
  font-size: 12.5px;
  text-transform: none;
  letter-spacing: 0;
  padding: 4px 10px;
}

/* -------- EXPANDABLE PARENT ROW (.flavor-output / .flavor-pricing) ---
   14 px chevron sits inline AFTER the row label (moved 2026-05-10 so
   labels left-align across all expandable rows regardless of whether
   they expand). Click anywhere on the parent row toggles for output
   tables; pricing cells are editable, so the toggle button alone carries
   the click (cursor override below). Children render with a small indent
   for visual hierarchy. */
.stx-table tbody tr.parent-row {
  cursor: pointer;
}

.stx-table tbody tr.parent-row th.row-label {
  font-weight: 700;
  color: var(--text);
}

/* Pricing card uses the same parent-row chevron mechanic, but the row
   itself is editable — clicks should land on the cell inputs, not the
   row. Override the cursor and let the chevron button carry the toggle. */
.stx-table.flavor-pricing tbody tr.parent-row {
  cursor: default;
}

.row-toggle {
  appearance: none;
  background: transparent;
  border: none;
  width: 16px;
  height: 16px;
  margin-left: 4px;
  padding: 0;
  color: var(--text-muted);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  vertical-align: middle;
  transition: transform 0.18s ease, color 0.12s ease;
}

.row-toggle svg {
  width: 14px;
  height: 14px;
  stroke-width: 2.25;
}

.stx-table tbody tr.parent-row.is-expanded .row-toggle {
  transform: rotate(90deg);
}

.stx-table tbody tr.parent-row:hover .row-toggle {
  color: var(--text);
}

/* Separator below an expanded parent row — same weight/color as the
   thead's bottom border so the parent reads as a "header" anchoring the
   cohort breakdown below. Hits both <th> and <td> to span the sticky
   left col. */
.stx-table tbody tr.parent-row.is-expanded > th,
.stx-table tbody tr.parent-row.is-expanded > td {
  border-bottom: 1px solid var(--border-strong);
}

/* Children: hidden until parent is .is-expanded. */
.stx-table tbody tr.child-row {
  display: none;
}

.stx-table tbody tr.parent-row.is-expanded ~ tr.child-row {
  display: table-row;
}

.stx-table tbody tr.child-row th.row-label {
  padding-left: 24px; /* 12 default + 12 indent — visual hierarchy
   only, no longer accounts for toggle width
   since the chevron moved to the right. */
  font-weight: 400;
  color: var(--text-muted);
}

/* -------- PRICING COHORT GRID CELLS (Phase 2b, 2026-05-10) --------------
   Two orthogonal visual axes on the per-cohort price grid:

     Identity (blue saturation + dot): is this cell carrying its own value,
       or echoing the default Promo / Renewal row?
         tracking default = faded blue, no dot
         override         = solid brand blue, blue dot to the left

     State (amber bg): is this unsaved?
         clean            = no bg
         dirty            = amber background (existing --dirty-bg)

   Both layers compose: an override cell that's also dirty shows solid
   blue + dot + amber. Empty cells (cohort hadn't started yet) render
   blank — no input, no styling. The cell.dirty visual carries forward to
   tracking-default cells when default-edit propagation lands in 2c. */
:root {
  --cohort-default-text: hsl(214, 70%, 75%);
  --cohort-override-text: var(--accent);
  --cohort-override-dot: var(--accent);
}

.stx-table .grid-cell {
  /* Column widths are locked via `table-layout: fixed`, so the inline-flex
     wrapper (.grid-cell-content) won't push the cell wider — it just
     arranges [dot · input] within the fixed 80 px content box. */
  position: relative;
  cursor: text;
  color: var(--cohort-default-text);
  font-variant-numeric: tabular-nums;
}

.stx-table .grid-cell.is-empty {
  cursor: default;
}

.stx-table .grid-cell.is-empty .grid-cell-dot {
  display: none;
}

.stx-table .grid-cell.is-override {
  color: var(--cohort-override-text);
  font-weight: 600;
}

.stx-table .grid-cell.dirty {
  background: var(--dirty-bg);
}

/* Dirty grid cells take the standard amber dirty-text on top of the dirty
   background — mirrors the regular editable.dirty styling. The selector
   beats both .grid-cell and .grid-cell.is-override because it carries
   .dirty on its own + an extra class, so it has equal-or-higher specificity. */
.stx-table .grid-cell.dirty,
.stx-table .grid-cell.is-override.dirty {
  color: var(--dirty-text);
  font-weight: 700;
}

.stx-table .grid-cell:focus-within {
  box-shadow: inset 0 0 0 1.5px var(--accent);
  z-index: 1;
}

/* Right-justified [dot · input] composite. The input shrinks to fit the
   cell width, keeping its right-aligned value at the same column position
   as the parent row's value. The dot sits just to the LEFT of the value
   with a small gap; visibility:hidden on non-override cells reserves the
   same layout space so the value's right edge stays consistent. */
.stx-table .grid-cell .grid-cell-content {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 5px;
  width: 100%;
  height: 100%;
}

.stx-table .grid-cell .grid-cell-dot {
  flex: 0 0 6px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--cohort-override-dot);
  visibility: hidden;
  pointer-events: none;
}

.stx-table .grid-cell.is-override .grid-cell-dot {
  visibility: visible;
}

.stx-table .grid-cell .grid-cell-input {
  /* field-sizing: content makes the input shrink-wrap its value text, so
     the flex container can right-align [dot · input] as a snug composite
     — the dot ends up immediately to the left of the value regardless of
     how short or long the value is. width:auto overrides .cell-input's
     width:100% which would otherwise stretch the input across the cell
     and pin the dot to the far left. */
  flex: 0 1 auto;
  min-width: 0;
  width: auto;
  field-sizing: content;
  color: inherit;
  font-weight: inherit;
}

/* ================================================================== */
/*  CHART SYSTEM — locked in workshop 14, spec'd in bible §3.9         */
/* ================================================================== */
/*
  Encoding rule: line color = current view's state-icon color.
  Single-model = always brand blue. Multi-series = each line takes its
  model's palette color (state-icon → tab → chart line, all same hue).
  Charts live in section-cards (§4.5) — frame + header inherited.
*/
/* Tokens — defaults; per-card override via inline `--chart-line` drives
   multi-series in Comparison Views. */
:root {
  --chart-line: var(--accent);
  --chart-grid: var(--border-soft);
  --chart-axis: var(--border-strong);
  --chart-axis-label: var(--text-muted);
  --chart-tick: var(--text-subtle);
}

/* Two-up layout — sibling cards in a row. Below 980 px collapses. */
.chart-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-3);
}

@media (max-width: 980px) {
  .chart-row {
    grid-template-columns: 1fr;
  }
}
/* Chart body inside a section-card. */
.section-card .chart-body {
  padding: var(--s-3) var(--s-3) var(--s-2);
}

/* The SVG primitive itself. */
.stx-chart {
  display: block;
  width: 100%;
  height: 280px;
  overflow: visible; /* hover markers can grow past viewBox edges */
}

.stx-chart .grid-line {
  stroke: var(--chart-grid);
  stroke-width: 1;
  shape-rendering: crispEdges;
}

.stx-chart .axis-line {
  stroke: var(--chart-axis);
  stroke-width: 1;
  shape-rendering: crispEdges;
}

.stx-chart .tick-label {
  font: 500 10.5px "Assistant", sans-serif;
  fill: var(--chart-tick);
}

.stx-chart .axis-label {
  font: 600 11px "Assistant", sans-serif;
  fill: var(--chart-axis-label);
  text-anchor: middle;
  letter-spacing: 0.01em;
}

.stx-chart .data-line {
  stroke: var(--chart-line);
  stroke-width: 1.75;
  fill: none;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.stx-chart .data-marker {
  fill: var(--chart-line);
  stroke: none;
  cursor: pointer;
  transition: r 0.08s ease;
}

.stx-chart .data-marker:hover {
  r: 4;
}

.stx-chart .hover-guide {
  stroke: var(--chart-line);
  stroke-width: 1;
  stroke-dasharray: 2 3;
  opacity: 0.35;
  pointer-events: none;
}

/* Floating tooltip — section-card-styled mini popover. One global
   instance shared by all charts; repositioned per hover. */
.chart-tooltip {
  position: fixed;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 6px 10px;
  box-shadow: 0 4px 14px rgba(15, 20, 32, 0.1), 0 1px 3px rgba(15, 20, 32, 0.05);
  pointer-events: none;
  z-index: 200;
  transform: translate(-50%, calc(-100% - 10px));
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.chart-tooltip .ct-label {
  color: var(--text-subtle);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.chart-tooltip .ct-value {
  color: var(--text);
  font-weight: 700;
  font-size: 13.5px;
  line-height: 1.2;
}

.chart-tooltip[hidden] {
  display: none;
}

/* ====================================================================
 * SOURCES PAGES (MVP port of v1 /import/ flows under /v2/sources/...)
 *
 * These pages port v1's wizard + detail UIs into the v2 chrome with
 * minimal restyling — they reuse the v1 `import-*` class set wholesale.
 * The shell here is just the canvas + view + body wrappers and a couple
 * of v2-flavored CTA / placeholder bits. Bible §7's Source Detail
 * diagram redesign is a later effort.
 * ==================================================================== */
.sources-canvas {
  height: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.sources-view {
  /* Same chrome as a Model Detail view — single-pane fills the canvas. */
  flex: 1 1 auto;
  min-height: 0;
}

.sources-view__body {
  flex: 1 1 auto;
  overflow: auto;
  /* Match Model Detail's .canvas-body padding — full-width section-cards
     with a small breathing margin from the view edge. */
  padding: var(--s-3);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

/* Stack of section-cards inside the body. */
.source-detail-cards {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

/* -------- Section-card body chrome --------------------------------- */
/* Each Source Detail section-card uses this for its body padding +
   internal vertical rhythm. The header strip handles its own padding. */
.source-detail-card__body {
  padding: var(--s-3) var(--s-4) var(--s-4);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

.source-detail-card__actions {
  display: flex;
  gap: var(--s-2);
}

.source-detail-card__warning {
  font-size: 12.5px;
  color: hsl(35, 80%, 38%);
  font-style: italic;
  line-height: 1.45;
}

.source-detail-card__hint {
  margin: 0;
  font-size: 12.5px;
  color: var(--text-muted);
  line-height: 1.5;
}

.source-detail-card__empty {
  margin: 0;
  font-size: 12.5px;
  color: var(--text-subtle);
  font-style: italic;
}

.source-detail-card__add-row {
  display: flex;
}

.source-detail-link {
  font-size: 13px;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
}

.source-detail-link:hover {
  text-decoration: underline;
}

/* -------- Definition-list grid (Configuration / Data Summary) ------ */
/* Two-column grid: muted label on the left, value (or input) on the
   right. Rows wrap in a `__row` div using `display: contents` so
   <template x-if> blocks can gate a whole row without breaking the
   shared grid alignment across all rows. */
.source-detail-list {
  display: grid;
  grid-template-columns: minmax(180px, max-content) 1fr;
  gap: var(--s-2) var(--s-4);
  align-items: baseline;
  font-size: 12.5px;
}

.source-detail-list__row {
  display: contents;
}

.source-detail-list__label {
  color: var(--text-muted);
  font-weight: 500;
}

.source-detail-list__value {
  color: var(--text);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  word-break: break-word;
}

.source-detail-list__input {
  font: inherit;
  font-weight: 600;
  color: var(--text);
  padding: 4px 8px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--card-bg);
  min-width: 0;
  width: 100%;
  max-width: 320px;
}

.source-detail-list__input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(56, 110, 200, 0.18);
}

.sources-canvas--landing {
  align-items: center;
  justify-content: center;
}

.sources-canvas__placeholder {
  max-width: 520px;
  margin: auto;
  padding: var(--s-6);
  text-align: center;
  color: var(--text-muted);
}

.sources-canvas__placeholder h1 {
  margin: 0 0 var(--s-3);
  font-size: 22px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.02em;
}

.sources-canvas__placeholder p {
  margin: 0 0 var(--s-4);
  line-height: 1.55;
}

.sources-canvas__cta-row {
  display: flex;
  justify-content: center;
}

.sources-cta-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: var(--r-md);
  background: var(--accent);
  color: white;
  font-size: 13px;
  font-weight: 600;
  text-decoration: none;
  transition: background 0.12s ease;
}

.sources-cta-btn:hover {
  background: var(--accent-hover);
}

.sources-cta-btn svg {
  width: 14px;
  height: 14px;
}

/* ====================================================================
 * REPORTS — Model Aggregation. V2 port of the v1 /model-aggregation/
 * page. Two stacked section-cards: model selection (rail-style project
 * cards w/ checkboxes) and the aggregated results table.
 * ==================================================================== */
.reports-canvas {
  height: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.reports-view {
  flex: 1 1 auto;
  min-height: 0;
  /* Flex column so .reports-view__body's `flex: 1 1 auto` actually
     constrains its height — without this the body can't establish
     overflow:auto and the Generate Report button slides off-screen
     when the project list exceeds the viewport. */
  display: flex;
  flex-direction: column;
}

.reports-view__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  padding: var(--s-3);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

/* -------- Reports card body chrome ------------------------------- */
.reports-card__body {
  padding: var(--s-3) var(--s-4) var(--s-4);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

.reports-card__hint {
  margin: 0;
  font-size: 12.5px;
  color: var(--text-muted);
  line-height: 1.5;
}

.reports-card__empty {
  font-size: 12.5px;
  color: var(--text-subtle);
  font-style: italic;
}

.reports-card__error {
  font-size: 12.5px;
  color: hsl(0, 65%, 45%);
  background: hsl(0, 65%, 96%);
  padding: 8px 12px;
  border-radius: var(--r-sm);
}

.reports-card__footer {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding-top: var(--s-2);
}

/* -------- Pick list — rail-style project cards ------------------- */
/* These cards now live INSIDE the left rail when in reports mode
   (refactored 2026-05-21). They mirror rail .ws-card visually + by
   margin so they feel native to the rail surface, but keep Reports-
   scoped class names so rail-only behaviors (drag-reorder, kebab
   actions, pop-out, rename, delete) don't leak in. */
.report-pick-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.report-project-card {
  background: var(--card-bg);
  border-radius: var(--r-md);
  overflow: hidden;
  /* Match rail .ws-card horizontal/vertical placement: 10 px gutter
     from rail edge, 6 px between cards. */
  margin: 0 10px 6px;
  /* Same layered shadow as rail .ws-card so cards lift cleanly off
     the rail's --view-bg surface. */
  box-shadow: 0 1px 1px rgba(15, 20, 32, 0.06), 0 4px 10px rgba(15, 20, 32, 0.12), 0 10px 20px rgba(15, 20, 32, 0.1);
}

.report-project-card__header {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: 8px 12px;
  cursor: pointer;
  user-select: none;
}

.report-project-card.is-expanded .report-project-card__header {
  border-bottom: 1px solid var(--border-soft);
}

.report-project-card__header:hover {
  background: rgba(0, 0, 0, 0.05);
}

.report-project-card__name {
  flex: 0 1 auto;
  min-width: 0;
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.005em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.report-project-card__count {
  margin-left: auto;
  font-size: 10.5px;
  font-weight: 700;
  color: var(--text-subtle);
  background: rgba(0, 0, 0, 0.05);
  border-radius: 999px;
  padding: 1px 8px;
  font-variant-numeric: tabular-nums;
}

.report-project-card__body {
  /* Match rail's .ws-card-body — tight top/bottom because the model
     rows carry their own vertical rhythm. */
  padding: 2px 0 4px;
}

/* -------- Model row — checkbox in the state-icon slot ----------- */
/* Mirrors rail .nav-item.has-state-icon: 3px vertical rhythm, 22px
   left padding to leave room for the checkbox in the same footprint
   the rail reserves for its state-icon. */
.report-model-row {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: 3px 12px 3px 22px;
  color: var(--text);
  font-size: 12.5px;
  font-weight: 500;
  cursor: pointer;
  user-select: none;
  line-height: 1.3;
  min-height: 24px;
}

.report-model-row:hover {
  background: rgba(0, 0, 0, 0.06);
}

.report-model-row__icon {
  /* Same 22 px footprint the rail's .has-state-icon reserves so the
     left edge of model labels lines up with the project header text. */
  width: 14px;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.report-model-row__label {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.report-model-row--empty {
  color: var(--text-subtle);
  font-style: italic;
  cursor: default;
  padding-left: 36px;
}

.report-model-row--empty:hover {
  background: transparent;
}

/* -------- Rail-mode chrome (hint / empty / error / footer) ------ */
/* Sits inside .nav-body when active_panel=='reports'. The footer
   (with the Generate Report button) is placed BETWEEN .nav-body
   and .nav-footer in rail.html so it stays pinned above the avatar
   regardless of how many projects the user has. */
.reports-rail-hint {
  margin: 0 10px 10px;
  padding-top: 4px;
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.45;
}

.reports-rail-empty {
  margin: 0 10px;
  padding: 12px;
  font-size: 12.5px;
  color: var(--text-subtle);
  font-style: italic;
  line-height: 1.4;
}

.reports-rail-error {
  margin: 0 10px 6px;
  padding: 8px 12px;
  font-size: 12px;
  color: hsl(0, 65%, 45%);
  background: hsl(0, 65%, 96%);
  border-radius: var(--r-sm);
  line-height: 1.4;
}

.reports-rail-footer {
  /* Sticky-by-flex-position: sits between .nav-body (flex: 1) and
     .nav-footer (natural), so always reachable. Top border separates
     it from the scrollable project list above. */
  padding: 10px 12px;
  border-top: 1px solid var(--border-soft);
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.reports-rail-footer .reports-generate-btn {
  /* Full width inside the narrow rail. */
  width: 100%;
  justify-content: center;
}

.reports-rail-footer__hint {
  font-size: 11.5px;
  color: var(--text-muted);
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* -------- Canvas empty state (no report generated yet) --------- */
.reports-canvas-empty {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: var(--s-4);
  color: var(--text-muted);
  text-align: center;
}

/* Lucide replaces the <i> with an <svg>, carrying the class through.
   Size + stroke land directly on the svg via the same class. */
.reports-canvas-empty__icon {
  width: 56px;
  height: 56px;
  color: var(--text-subtle);
  margin-bottom: 8px;
  opacity: 0.7;
  stroke-width: 1.5;
}

.reports-canvas-empty__title {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
}

.reports-canvas-empty__body {
  margin: 0;
  font-size: 13px;
  color: var(--text-muted);
  max-width: 360px;
  line-height: 1.5;
}

/* -------- Custom checkbox primitive ------------------------------ */
/* The native input is hidden but kept in the DOM (so x-model and
   keyboard activation continue to work). The .report-checkbox__box
   sibling mimics a checkbox visually, with an inline-SVG check mark
   that becomes visible when the input is :checked. */
.report-checkbox {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  cursor: pointer;
}

.report-checkbox input[type=checkbox] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.report-checkbox__box {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  border: 1.5px solid var(--border-strong);
  border-radius: 3px;
  background: var(--card-bg);
  transition: background 0.12s ease, border-color 0.12s ease;
}

.report-checkbox input:hover + .report-checkbox__box {
  border-color: var(--text-muted);
}

.report-checkbox input:focus-visible + .report-checkbox__box {
  outline: none;
  box-shadow: 0 0 0 3px rgba(56, 110, 200, 0.22);
}

.report-checkbox input:checked + .report-checkbox__box {
  background: var(--accent);
  border-color: var(--accent);
}

/* SVG check mark — drawn via background-image so we don't need an
   extra DOM node. White stroke on accent fill when :checked. */
.report-checkbox input:checked + .report-checkbox__box::after {
  content: "";
  width: 10px;
  height: 10px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>");
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
}

/* -------- Generate button ---------------------------------------- */
.reports-generate-btn {
  appearance: none;
  border: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: var(--r-md);
  background: var(--accent);
  color: white;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.12s ease;
}

.reports-generate-btn:hover:not(:disabled) {
  background: var(--accent-hover);
}

.reports-generate-btn:disabled {
  background: var(--text-subtle);
  cursor: not-allowed;
  opacity: 0.7;
}

.reports-generate-btn svg {
  width: 14px;
  height: 14px;
}

/* -------- Header tag-mixed (Actual/Forecast) -------------------- */
/* Aggregation reports surface a third tier-2 state for periods that
   span the actuals/forecast boundary across the selected models. */
.stx-table.flavor-output thead tr.tier-2 th.tag-mixed {
  color: var(--text-muted);
}