@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%);
  /* Help owns purple (2026-09-06): the `?` buttons and the help modal are
     the only things in the app that wear it, so the color reads as a
     category rather than a control. */
  --help: var(--brand-purple);
  --help-bg: hsl(274, 91%, 96%);
  --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/page_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 (main.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;
}

/* D4: rail model-state icon → fill-up recalc progress pie. The chart glyph is
   hidden (kept in the DOM — no lucide re-init) and the pie shown while
   .is-recalcing. The pie fills empty → full clockwise from the job's progress %,
   always in the selected-model blue (--model-main), regardless of this row's
   state color, so a background recalc reads the same on any model. */
.rail-recalc-pie {
  display: none;
}

.model-state-icon.is-recalcing svg {
  display: none;
}

.model-state-icon.is-recalcing .rail-recalc-pie {
  display: block;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: conic-gradient(var(--model-main) calc(var(--recalc-pct, 0) * 1%), transparent calc(var(--recalc-pct, 0) * 1%));
  box-shadow: inset 0 0 0 1.5px var(--model-main);
}

.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;
}

/* The rail logo doubles as the home affordance (→ Project Library). */
.rail-logo {
  cursor: pointer;
  transition: opacity 120ms ease;
}

.rail-logo:hover {
  opacity: 0.75;
}

.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;
}

/* Destructive variant (e.g. the remove-passkey trash button) — red on hover,
   matching the .danger convention used in menu popovers. */
.menu-icon-btn--danger:hover {
  background: hsl(0, 65%, 96%);
  color: hsl(0, 65%, 45%);
}

/* -------- HELP BUTTON ---------------------------------------------
   The `?` that opens a View's help topic (2026-09-06). Always the
   rightmost control of whatever header it sits in, so "same spot" holds
   across screens whose headers differ. Wears the help purple at rest (a
   grey ring read as a disabled control — Matt, 2026-09-06) but stays an
   outline; the tint fills in on hover and while its topic is open. A
   solid purple button on every header would read as a badge. A typographic
   `?` rather than an icon: it needs no lucide pass, and `font: inherit`
   keeps it in the app face instead of the browser's button default. */
.help-btn {
  appearance: none;
  margin: 0;
  padding: 0;
  cursor: pointer;
  width: 26px;
  height: 26px;
  border-radius: 999px;
  border: 1.5px solid var(--help);
  background: transparent;
  color: var(--help);
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}

.help-btn:hover, .help-btn.is-open {
  background: var(--help-bg);
}

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

.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;
}

/* -------- THE SOURCES SIGNAL (Workshop 25) ------------------------
   A Model's menu slot lights when its Sources need attention: amber when
   a Source holds data the Model hasn't been written with, red when two
   Sources feed the same channel. The classes land on the SLOT
   (.view-header__menu) so the button, its badge and the open popover
   share one color: `has-alert` switches the treatment on, `alert-amber`
   / `alert-red` pick the tokens. No new colors — amber is the dirty
   pill's, red is the menu's danger red. The ring is an inset shadow (no
   layout shift); the `!` badge is a ::after in the button's corner. The
   words explaining the color live under the menu's Sources line. */
.alert-amber {
  --alert-c: var(--dirty-pill-bg);
  --alert-bg: var(--dirty-bg);
  --alert-text: var(--dirty-text);
}

.alert-red {
  --alert-c: hsl(0, 65%, 45%);
  --alert-bg: hsl(0, 65%, 96%);
  --alert-text: hsl(0, 60%, 32%);
}

.view-header__menu.has-alert .menu-icon-btn {
  color: var(--alert-c);
  box-shadow: inset 0 0 0 1.5px var(--alert-c);
}

.view-header__menu.has-alert .menu-icon-btn:hover,
.view-header__menu.has-alert .menu-icon-btn.is-open {
  background: var(--alert-bg);
  color: var(--alert-c);
}

.view-header__menu.has-alert .menu-icon-btn::after {
  content: "!";
  position: absolute;
  top: -5px;
  right: -5px;
  width: 14px;
  height: 14px;
  border-radius: 999px;
  background: var(--alert-c);
  color: white;
  font-size: 10px;
  font-weight: 800;
  line-height: 14px;
  text-align: center;
  box-shadow: 0 0 0 2px var(--view-bg);
}

.view-header__menu.has-alert .menu-popover {
  border-color: var(--alert-c);
  box-shadow: 0 0 0 1px var(--alert-c), 0 8px 24px rgba(15, 20, 32, 0.14);
}

/* The same signal on a Library rail Model row: a `!` badge riding the text
   baseline just after the Model name (amber = unwritten data, red = Source
   collision) and, for red only, a ring around the row. Amber stays
   badge-only on purpose — a live connection stages new data on every sync,
   so amber is the rail's normal weather and a ring on every such row would
   stop being read. The row's text and icon keep their identity color
   (state-main / state-sec-N); the alert is a second channel, not a recolor.

   The badge sits in the flex flow between `.item-label` and `.card-actions`
   rather than on the state icon — overlapping the glyph read as mess, and
   the hover buttons then open to the right of it. It renders for every row
   and is revealed by the row's own alert class, so nothing re-derives the
   verdict a second time. The Sources rail's cards do the same, keyed on
   `_source_alerts` — the Source-side view of the same verdicts. */
.rail-alert-badge {
  display: none;
}

/* Ancestor-scoped, not `.nav-item`-scoped: the Sources rail's cards wear the
   identical badge off the identical class (2026-09-08). */
.rail-alert .rail-alert-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 13px;
  height: 13px;
  border-radius: 999px;
  background: var(--alert-c);
  color: white;
  font-size: 9.5px;
  font-weight: 800;
  line-height: 1;
}

.nav-item.rail-alert.alert-red {
  border-radius: var(--r-sm);
  box-shadow: inset 0 0 0 1.5px var(--alert-c);
}

/* A stacked item — label plus a one-line reason beneath it. */
.menu-popover-item--stacked {
  display: grid;
  grid-template-columns: 14px 1fr;
  column-gap: 10px;
  align-items: center;
}

.menu-popover-item--stacked .menu-popover-item__sub {
  grid-column: 2;
  margin-top: 1px;
  font-size: 10.5px;
  font-weight: 700;
  color: var(--alert-text);
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.menu-popover-item--stacked .menu-popover-item__sub svg {
  width: 11px;
  height: 11px;
  color: var(--alert-c);
}

.menu-popover-item--stacked.has-alert {
  color: var(--alert-text);
}

.menu-popover-item--stacked.has-alert > svg {
  color: var(--alert-c);
}

/* -------- 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);
}

/* Collision ring — outline rather than an inset box-shadow so it composes with
   the card's own layered shadow instead of replacing it. Amber stays
   badge-only: an unwritten drop is the rail's normal weather. */
.source-card.rail-alert.alert-red {
  outline: 1.5px solid var(--alert-c);
  outline-offset: -1.5px;
}

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

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

/* Rail sync pie (source-page analog of the D4 model-rail recalc pie). A
   determinate incremental sync fills clockwise from --sync-pct; a full backfill
   (indeterminate, percent null) spins instead, matching the .job-progress bar's
   determinate/shimmer split. */
.source-sync-pie {
  /* inline-block, not the span default of inline — inline ignores width/height,
     which would collapse the conic pie to nothing (only the spinner's border
     showed, hence "spinning, not filling"). */
  display: inline-block;
  flex-shrink: 0;
  box-sizing: border-box;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: conic-gradient(var(--accent) calc(var(--sync-pct, 0) * 1%), var(--accent-bg) calc(var(--sync-pct, 0) * 1%));
  box-shadow: inset 0 0 0 1.5px var(--accent);
}

.source-sync-pie.is-indeterminate {
  background: none;
  box-shadow: none;
  border: 2px solid var(--accent-bg);
  border-top-color: var(--accent);
  animation: source-sync-spin 0.7s linear infinite;
}

@keyframes source-sync-spin {
  to {
    transform: rotate(360deg);
  }
}
.source-name {
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.005em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 0 1 auto;
  min-width: 0;
}

/* How the data ARRIVES — Upload / Connection / Connection (Live). It rides
   line 2 now (margin-left:auto right-justifies it against the meta), so the
   name has the whole of line 1. */
.source-tag {
  margin-left: auto;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  flex-shrink: 0;
}

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

.source-tag.connection {
  background: hsl(214, 100%, 94%);
  color: hsl(214, 97%, 42%);
}

/* The green dot is the "and it is syncing on a schedule" half — it appears
   only with the word, never instead of it. */
.source-tag.is-live {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

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

/* Line 2: when the data last changed, and how it arrives. */
.source-meta {
  margin-top: 5px;
  font-size: 11.5px;
  color: var(--text-subtle);
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 18px;
}

/* The stamp yields first — a long absolute date must not push the pill off
   the card. */
.source-meta__updated {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* -------- 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. Dressed in the marketing
   site's frame (subscriptix.com, repo MattEddy/subscriptix-website) so that
   crossing from the site to sign-in feels like one place: nameplate card upper
   left, menu card upper right, a giant faded breathing mark behind, the card
   drifting in. Markup: projection/components/auth_frame.html + auth_footer.html.
   The frame's numbers (mark, shadow, menu) are copied from the site's
   src/index.css + Header.tsx — change one, change both. */
.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-x: hidden;
  -webkit-font-smoothing: antialiased;
}

/* The site's card: a deeper shadow than the app's card-shadow mixin, only on
   this surface. */
.auth-header {
  position: relative;
  z-index: 3;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 16px;
}

@media (min-width: 768px) {
  .auth-header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
  }
}
.auth-rail-logo {
  display: flex;
  align-items: center;
  background: #fff;
  border: 1px solid #e6e8ee;
  border-radius: 12px;
  box-shadow: 0 1px 2px rgba(10, 11, 15, 0.09), 0 8px 20px rgba(10, 11, 15, 0.105), 0 20px 40px rgba(10, 11, 15, 0.105);
  padding: 12px 18px;
}

.auth-rail-logo img {
  display: block;
  height: 40px;
  width: auto;
}

@media (min-width: 768px) {
  .auth-rail-logo img {
    height: 51px;
  }
}
.auth-rail-logo--link {
  text-decoration: none;
  cursor: pointer;
}

.auth-nav {
  background: #fff;
  border: 1px solid #e6e8ee;
  border-radius: 12px;
  box-shadow: 0 1px 2px rgba(10, 11, 15, 0.09), 0 8px 20px rgba(10, 11, 15, 0.105), 0 20px 40px rgba(10, 11, 15, 0.105);
  display: flex;
  align-items: center;
  gap: 4px;
  max-width: 100%;
  overflow-x: auto;
  padding: 6px;
}

.auth-nav__item,
.auth-nav__login {
  white-space: nowrap;
  padding: 8px 10px;
  border-radius: 8px;
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  transition: background-color 0.15s, color 0.15s;
}

.auth-nav__item {
  color: #4a5565;
}

.auth-nav__item:hover {
  color: #101828;
  background: #f9fafb;
}

.auth-nav__login {
  margin-left: 4px;
  padding-left: 12px;
  padding-right: 12px;
  background: #055cda;
  color: #fff;
}

.auth-nav__login:hover {
  background: #044ab8;
}

@media (min-width: 768px) {
  .auth-nav__item, .auth-nav__login {
    padding: 8px 14px;
    font-size: 15px;
  }
  .auth-nav__login {
    padding-left: 16px;
    padding-right: 16px;
  }
}
/* The breathing mark — 20 s each way (a 40 s cycle), phase keyed to the clock
   by the script in auth_frame.html, same as the site. 0.708 = 56.7vw / 80vw. */
.auth-bg-mark {
  position: fixed;
  top: 50%;
  left: 50%;
  width: 80vw;
  height: auto;
  opacity: 0.2;
  z-index: 0;
  pointer-events: none;
  user-select: none;
  transform: translate(-50%, -50%) scale(0.708);
  animation: auth-mark-pulse 20s ease-in-out infinite alternate;
  animation-delay: var(--mark-delay, 0s);
  will-change: transform;
}

@keyframes auth-mark-pulse {
  from {
    transform: translate(-50%, -50%) scale(0.708);
  }
  to {
    transform: translate(-50%, -50%) scale(1);
  }
}
/* The card drifts up into place on arrival, like a site scene card. */
@keyframes auth-card-in {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
.auth-stage {
  position: relative;
  z-index: 1;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-5) var(--s-5) 56px;
}

/* Phones: the frame stacks in the flow above the card (as on the site), so
   centring the card in a whole extra screen would leave a gulf under the menu. */
@media (max-width: 767px) {
  .auth-stage {
    min-height: 0;
    padding: var(--s-6) var(--s-4);
  }
}
.auth-card {
  background: #fff;
  border: 1px solid #e6e8ee;
  border-radius: 12px;
  box-shadow: 0 1px 2px rgba(10, 11, 15, 0.09), 0 8px 20px rgba(10, 11, 15, 0.105), 0 20px 40px rgba(10, 11, 15, 0.105);
  padding: var(--s-5);
  width: 100%;
  max-width: 380px;
}

.auth-card,
.legal-doc {
  animation: auth-card-in 0.7s cubic-bezier(0.22, 1, 0.36, 1) 0.12s backwards;
}

/* Leaving for the site (auth_footer.html adds the class): the middle fades out
   while the frame and mark hold still — the site does the same coming here. */
html.leaving .auth-stage,
html.leaving .legal-stage {
  opacity: 0;
  transform: scale(0.985);
  transition: opacity 0.3s ease-in, transform 0.3s ease-in;
}

@media (prefers-reduced-motion: reduce) {
  .auth-bg-mark {
    animation: none;
    transform: translate(-50%, -50%) scale(0.6);
  }
  .auth-card, .legal-doc {
    animation: none;
  }
}
.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);
}

/* Add-in code-flow extras (Phase 7): muted helper line under the form, and a
   text-only "Resend code" button on the confirm step. */
.auth-card__hint {
  margin: var(--s-3) 0 0;
  text-align: center;
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.4;
}

.auth-resend {
  margin-top: var(--s-3);
  text-align: center;
}

.auth-textbutton {
  appearance: none;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--accent);
  cursor: pointer;
}

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

/* The footer line, pinned to the bottom of the sign-in surface — the same
   line as the site's (email · LinkedIn · Privacy · Terms · ©). Privacy + Terms
   are load-bearing: OAuth verification (Google, Microsoft) needs them
   reachable logged-out. Rendered by projection/components/auth_footer.html. */
.auth-legal-footer {
  position: fixed;
  bottom: var(--s-4);
  left: 0;
  right: 0;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 4px 10px;
  padding: 0 var(--s-4);
  font-size: 13px;
  color: #6a7282;
}

.auth-legal-footer a {
  color: inherit;
  text-decoration: none;
}

.auth-legal-footer a:hover {
  color: #1e2939;
}

/* Cookie-support notice on the sign-in pages (layer 1). Hidden until the inline
   round-trip test in allauth/layouts/base.html reveals it for a browser with
   cookies disabled. An amber heads-up, not a hard error. */
.cookie-warning {
  margin: 0 0 var(--s-4);
  padding: 10px 12px;
  border-radius: var(--r-md);
  background: hsl(38, 92%, 95%);
  border: 1px solid hsl(38, 80%, 85%);
  font-size: 12.5px;
  line-height: 1.4;
  color: hsl(28, 80%, 28%);
}

/* Fallback note on the internal /team-access/ page — points a stray customer
   at the passwordless sign-in. */
.auth-fallback-note {
  margin: var(--s-4) 0 0;
  text-align: center;
  font-size: 12.5px;
  color: var(--text-muted);
}

.auth-fallback-note a {
  color: var(--accent);
  text-decoration: none;
  font-weight: 500;
}

.auth-fallback-note a:hover {
  text-decoration: underline;
}

/* -------- LEGAL / POLICY PAGES ----------------------------------
   Public, login-free content pages (Privacy Policy, Terms of Service),
   reachable for OAuth verification and from the sign-in footer. They reuse
   the auth surface chrome (.auth-body, .auth-rail-logo) but present a single
   readable text column instead of the sign-in card. */
/* Legal docs are long and scroll (and phones stack everything), so there the
   footer line sits under the content instead of pinned over it. */
.legal-body .auth-legal-footer {
  position: static;
  padding: var(--s-2) var(--s-4) var(--s-5);
}

@media (max-width: 767px) {
  .auth-legal-footer {
    position: static;
    padding: var(--s-2) var(--s-4) var(--s-5);
  }
}
.legal-stage {
  position: relative;
  z-index: 1;
  display: flex;
  justify-content: center;
  padding: var(--s-2) var(--s-4) var(--s-5);
}

@media (min-width: 768px) {
  /* Top padding clears the fixed frame. */
  .legal-stage {
    padding: 112px var(--s-5) var(--s-5);
  }
}
.legal-doc {
  width: 100%;
  max-width: 720px;
  background: #fff;
  border: 1px solid #e6e8ee;
  border-radius: 12px;
  box-shadow: 0 1px 2px rgba(10, 11, 15, 0.09), 0 8px 20px rgba(10, 11, 15, 0.105), 0 20px 40px rgba(10, 11, 15, 0.105);
  padding: var(--s-5) clamp(var(--s-4), 5vw, 56px);
  line-height: 1.6;
  color: var(--text);
}

.legal-doc__title {
  margin: 0 0 var(--s-2);
  font-size: 26px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text);
}

.legal-doc__meta {
  margin: 0 0 var(--s-5);
  font-size: 12.5px;
  color: var(--text-muted);
}

.legal-doc h2 {
  margin: var(--s-5) 0 var(--s-2);
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.005em;
  color: var(--text);
}

.legal-doc h3 {
  margin: var(--s-4) 0 var(--s-1);
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}

.legal-doc p,
.legal-doc li {
  font-size: 13.5px;
  color: var(--text);
}

.legal-doc p {
  margin: 0 0 var(--s-3);
}

.legal-doc ul {
  margin: 0 0 var(--s-3);
  padding-left: 20px;
}

.legal-doc li {
  margin: 0 0 var(--s-1);
}

.legal-doc a {
  color: var(--accent);
  text-decoration: none;
}

.legal-doc a:hover {
  text-decoration: underline;
}

.legal-doc__footer {
  margin-top: var(--s-5);
  padding-top: var(--s-4);
  border-top: 1px solid var(--border);
  display: flex;
  gap: var(--s-4);
  font-size: 12.5px;
}

.legal-doc__footer a {
  color: var(--text-muted);
  text-decoration: none;
  font-weight: 500;
}

.legal-doc__footer a:hover {
  color: var(--accent);
  text-decoration: underline;
}

/* --- allauth passwordless pages -----------------------------------------
   allauth renders class-less semantic markup (h1 / p / <p><label><input></p>
   / button) inside our `.auth-card--allauth` wrapper. These rules map that
   markup onto the same tokens the hand-rolled `.auth-*` classes above use, so
   the login-by-code, confirm-code, signup, and verification pages all read as
   the v2 sign-in surface. Button classes come from the element override in
   accounts/templates/allauth/elements/button.html. */
.auth-card--allauth h1 {
  margin: 0 0 var(--s-3);
  text-align: center;
  font-size: 18px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text);
}
.auth-card--allauth p {
  margin: 0 0 var(--s-3);
  font-size: 13px;
  color: var(--text-muted);
}
.auth-card--allauth p a {
  color: var(--accent);
  text-decoration: none;
  font-weight: 500;
}
.auth-card--allauth p a:hover {
  text-decoration: underline;
}
.auth-card--allauth form {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  margin: 0;
}
.auth-card--allauth {
  /* Each field is a <p> wrapper; collapse its default margin into the form gap. */
}
.auth-card--allauth form p {
  margin: 0;
}
.auth-card--allauth {
  /* Auth forms are placeholder-driven (every input carries a descriptive
     placeholder). Hide the field labels visually but keep them in the DOM for
     screen readers / autofill rather than removing them. allauth renders
     fields via Django's form.as_p, so labels arrive whether or not we ask. */
}
.auth-card--allauth label {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
.auth-card--allauth input[type=text],
.auth-card--allauth input[type=email],
.auth-card--allauth input[type=password],
.auth-card--allauth input[type=number],
.auth-card--allauth input[type=tel],
.auth-card--allauth input:not([type]) {
  width: 100%;
  box-sizing: border-box;
  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-card--allauth input[type=text]:focus,
.auth-card--allauth input[type=email]:focus,
.auth-card--allauth input[type=password]:focus,
.auth-card--allauth input[type=number]:focus,
.auth-card--allauth input[type=tel]:focus,
.auth-card--allauth input:not([type]):focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-bg);
}
.auth-card--allauth .aauth-btn {
  appearance: none;
  display: block;
  width: 100%;
  box-sizing: border-box;
  background: var(--accent);
  color: white;
  border: 1px solid var(--accent);
  border-radius: var(--r-md);
  padding: 11px 16px;
  font: inherit;
  font-weight: 600;
  font-size: 14px;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.12s ease;
}
.auth-card--allauth .aauth-btn:hover {
  background: var(--accent-hover);
}
.auth-card--allauth .aauth-btn--outline {
  background: transparent;
  color: var(--accent);
  border-color: var(--border);
}
.auth-card--allauth .aauth-btn--outline:hover {
  background: var(--accent-bg);
}
.auth-card--allauth {
  /* Stack the confirm/resend/cancel button group with consistent spacing. */
}
.auth-card--allauth .aauth-btn + .aauth-btn {
  margin-top: var(--s-2);
}
.auth-card--allauth {
  /* --- Social provider buttons (Phase 5: Google / Microsoft) ----------
     Each provider is its own POST <form> (see elements/provider.html), laid
     out in a vertical stack. The button reuses the shared outline look and
     adds a brand glyph to the left of a centered label. */
}
.auth-card--allauth .aauth-provider-list {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}
.auth-card--allauth {
  /* The provider form must not inherit the card's tall column-gap form rule. */
}
.auth-card--allauth .aauth-provider-form {
  display: block;
  margin: 0;
  gap: 0;
}
.auth-card--allauth .aauth-provider {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
}
.auth-card--allauth .aauth-provider__glyph {
  display: inline-flex;
  flex-shrink: 0;
  width: 18px;
  height: 18px;
}
.auth-card--allauth .aauth-provider__glyph svg {
  width: 100%;
  height: 100%;
  display: block;
}
.auth-card--allauth .aauth-provider__label {
  font-weight: 600;
}
.auth-card--allauth .auth-messages {
  list-style: none;
  margin: 0 0 var(--s-3);
  padding: 8px 12px;
  border-radius: var(--r-md);
  background: var(--accent-bg);
  font-size: 12.5px;
  color: var(--text);
}
.auth-card--allauth {
  /* Django field error lists. */
}
.auth-card--allauth .errorlist {
  list-style: none;
  margin: 0 0 var(--s-1);
  padding: 8px 12px;
  border-radius: var(--r-md);
  background: hsl(0, 65%, 96%);
  border: 1px solid hsl(0, 65%, 88%);
  font-size: 12.5px;
  color: hsl(0, 65%, 35%);
}
.auth-card--allauth {
  /* Divider between sign-in methods (appears once passkey / social land). */
}
.auth-card--allauth hr {
  border: none;
  border-top: 1px solid var(--border);
  margin: var(--s-3) 0;
}
.auth-card--allauth {
  /* Footer prompt (e.g. "New to Subscriptix? Create an account") — give it
     breathing room from the action above it; it sits flush at the bottom. */
}
.auth-card--allauth > p:last-child {
  margin-top: var(--s-5);
  margin-bottom: 0;
  text-align: center;
}
.auth-card--allauth {
  /* allauth's `panel` element (the Security index "Passkeys" panel) renders an
     unclassed `<section><h2>title</h2> body <ul><li>action</li></ul>`. Style the
     title and — the actual bug here — strip the default list marker + padding
     from the actions list so the button doesn't render with a stray bullet. */
}
.auth-card--allauth section {
  margin: 0 0 var(--s-4);
}
.auth-card--allauth section h2 {
  margin: 0 0 var(--s-1);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: -0.005em;
  color: var(--text);
}
.auth-card--allauth section ul {
  list-style: none;
  margin: var(--s-3) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}
.auth-card--allauth section li {
  margin: 0;
}

/* -------- PASSKEY LIST (allauth WebAuthn manage page) ------------
   Custom row layout replacing allauth's stock table: name + meta on the left,
   pencil/trash .menu-icon-btn actions on the right. Lives inside .auth-card. */
.passkey-list {
  list-style: none;
  margin: 0 0 var(--s-4);
  padding: 0;
}

.passkey-item {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: 10px 0;
  border-bottom: 1px solid var(--border-soft);
}

.passkey-item:first-child {
  border-top: 1px solid var(--border-soft);
}

.passkey-item__info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.passkey-item__name {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.passkey-item__meta {
  font-size: 11.5px;
  color: var(--text-muted);
}

.passkey-item__actions {
  display: flex;
  gap: 2px;
  flex-shrink: 0;
}

/* -------- CHANGE-EMAIL PAGE -------------------------------------
   Override of allauth's account/email_change.html (rendered when
   ACCOUNT_CHANGE_EMAIL is on). The page lives inside .auth-card--allauth, so
   its blanket sr-only `label` rule would hide our field caption — re-show it
   with a higher-specificity rule. Plus the current-email readout and the
   pending-change notice. */
.auth-card--allauth .email-change label {
  position: static;
  width: auto;
  height: auto;
  margin: 0 0 var(--s-1);
  padding: 0;
  clip: auto;
  overflow: visible;
  white-space: normal;
  border: 0;
  display: block;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--text-muted);
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.email-current {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 12px;
  margin: 0 0 var(--s-4);
  background: var(--very-bg);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-md);
}

.email-current__label {
  font-size: 11px;
  font-weight: 600;
  color: var(--text-muted);
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.email-current__value {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}

.email-pending {
  padding: 10px 12px;
  margin: 0 0 var(--s-4);
  background: hsl(38, 92%, 95%);
  border: 1px solid hsl(38, 80%, 85%);
  border-radius: var(--r-md);
}

.email-pending__text {
  margin: 0 0 var(--s-2);
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--text);
}

.email-pending__actions {
  display: flex;
  gap: var(--s-2);
  flex-wrap: wrap;
}

/* Pending-action buttons sit inline + auto-width, not the full-width default. */
.email-pending__actions .aauth-btn {
  display: inline-block;
  width: auto;
  padding: 6px 12px;
  font-size: 12.5px;
}

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

/* 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 main.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;
}

/* -------- PASSKEY NUDGE TOAST (Phase 6 Stage B) ------------------ */
/* Dismissible post-login "add a passkey" prompt. Fixed to the lower-right
   of the viewport so it floats over the canvas without shifting layout.
   Sits above page content but below modals (z 200+). Rendered from
   projection/base.html, gated on the show_passkey_nudge context flag. */
.passkey-nudge {
  position: fixed;
  right: var(--s-5);
  bottom: var(--s-5);
  z-index: 100;
  width: 320px;
  max-width: calc(100vw - 2 * var(--s-5));
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  padding: var(--s-4);
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: 0 8px 24px rgba(15, 20, 32, 0.16), 0 2px 6px rgba(15, 20, 32, 0.08);
}

.passkey-nudge__icon {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--accent-bg);
  color: var(--accent);
}

.passkey-nudge__icon svg {
  width: 18px;
  height: 18px;
}

.passkey-nudge__body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}

.passkey-nudge__text {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.4;
  color: var(--text);
}

.passkey-nudge__action {
  align-self: flex-start;
  font-size: 13px;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
}

.passkey-nudge__action:hover {
  color: var(--accent-hover);
  text-decoration: underline;
}

.passkey-nudge__dismiss {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--text-subtle);
  border-radius: var(--r-sm);
  cursor: pointer;
}

.passkey-nudge__dismiss:hover {
  background: var(--very-bg);
  color: var(--text);
}

.passkey-nudge__dismiss svg {
  width: 16px;
  height: 16px;
}

/* -------- SETTINGS SECTION (rail-driven) -------------------------
   Settings as a v2 section: the rail flips to Settings mode (see rail.html's
   settings panel) and the allauth-served pages (change email, passkeys) render
   in the canvas via allauth/layouts/shell.html. `.settings-content` rides the
   `.auth-card--allauth` form/typography mapping but re-tunes the card-centric
   bits for a canvas surface. */
.settings-canvas {
  height: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.settings-view {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.settings-view__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  padding: var(--s-3);
}

.settings-card {
  max-width: 560px;
  padding: var(--s-5);
}

/* The Legal document tabs hug the Settings title (the default view-header
   centers its TabRow, which strands two small tabs mid-header). */
.settings-view .view-header__tabs {
  justify-content: flex-start;
  margin-left: var(--s-3);
}

.settings-content h1 {
  text-align: left;
  font-size: 16px;
  padding-bottom: var(--s-3);
  border-bottom: 1px solid var(--border-soft);
}
.settings-content {
  /* Buttons size to their label on a canvas surface (the auth card wants
     full-width; a settings form doesn't). */
}
.settings-content .aauth-btn {
  display: inline-block;
  width: auto;
  min-width: 160px;
}
.settings-content form {
  align-items: flex-start;
}
.settings-content form .aauth-btn {
  width: auto;
}

/* Settings > Legal — the documents embedded in the settings card. The card is
   already the surface, so strip .legal-doc's standalone card chrome; keep its
   typography (h2/h3/meta/list rhythm). Wide modifier gives the text a
   readable column. */
.settings-card--wide {
  max-width: 800px;
}

.legal-doc--embedded {
  max-width: none;
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 0;
  box-shadow: none;
}

/* -------- 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-sources-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
   main.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 main.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;
}

/* A parameter shown but not editable here (e.g. a fitted curve's length). */
.param-static-value {
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
  padding-right: 8px;
}

/* 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;
}

/* -------- SOURCES PANEL (Workshop 25) ------------------------------ */
/* The Parameters panel's sibling — same 340px right column, same surface.
   A provenance table: one card per Source feeding the Model, one row per
   channel. Read-only; every door out is a full navigation. The panel
   carries the same `alert-amber` / `alert-red` tokens as the menu slot
   (set by the store's sourcesAlertClass), so a lit card and a lit row
   read in the same color the button does. */
.sources-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;
}

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

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

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

.sources-panel__foot {
  flex: 0 0 auto;
  padding: 6px 14px 12px;
  font-size: 11px;
  color: var(--text-subtle);
  line-height: 1.5;
}

/* One Source = one section card. */
.src-card {
  flex-shrink: 0;
  background: var(--card-bg);
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  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;
}

.src-card.is-lit {
  border-color: var(--alert-c);
}

.src-card__header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px 8px 12px;
  border-bottom: 1px solid var(--border-soft);
}
.src-card__header .source-tag {
  margin-left: auto;
}

/* The name + arrow as one button (the door to the Source Dashboard), the
   mirror of the Flow View's .model-card__open. */
.src-card__open {
  appearance: none;
  border: none;
  background: transparent;
  font: inherit;
  color: inherit;
  text-align: left;
  display: inline-flex;
  align-items: center;
  gap: 2px;
  flex: 0 1 auto;
  min-width: 0;
  margin: 0 0 0 -4px;
  padding: 0 0 0 4px;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: background 0.12s ease;
}
.src-card__open:hover {
  background: var(--accent-bg);
}
.src-card__open:hover .src-card__name, .src-card__open:hover .src-card__open-arrow {
  color: var(--accent);
}
.src-card__open:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.src-card__name {
  min-width: 0;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: -0.005em;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: color 0.12s ease;
}

.src-card__open-arrow {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  opacity: 0;
  transition: opacity 0.12s ease, color 0.12s ease;
}
.src-card__open-arrow svg {
  width: 13px;
  height: 13px;
}

.src-card:hover .src-card__open-arrow,
.src-card__open:focus-visible .src-card__open-arrow {
  opacity: 1;
}

.src-card__meta {
  padding: 6px 12px 0;
  font-size: 11px;
  color: var(--text-subtle);
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

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

/* One row per channel the Source feeds INTO THIS MODEL. The bar wears the
   channel's palette color (the .ch-* classes from _flow.scss); a lit row
   swaps it for the alert color. */
.src-rows {
  padding: 6px 0 4px;
}

.src-row {
  display: grid;
  grid-template-columns: 4px 1fr auto auto;
  gap: 0 10px;
  align-items: center;
  padding: 6px 12px 6px 8px;
}

.src-row__bar {
  width: 4px;
  align-self: stretch;
  border-radius: 2px;
  background: var(--ch, var(--accent));
}

.src-row__text {
  min-width: 0;
}

.src-row .src-row__remove {
  width: 22px;
  height: 22px;
  margin-left: -4px;
  opacity: 0;
  transition: opacity 0.12s ease, background 0.12s ease, color 0.12s ease;
}
.src-row .src-row__remove svg {
  width: 13px;
  height: 13px;
}

.src-row:hover .src-row__remove,
.src-row__remove:focus-visible {
  opacity: 1;
}

.src-row__channel {
  font-size: 12px;
  font-weight: 700;
  color: var(--text);
}

.src-row__dest {
  font-size: 11px;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.src-row__dest .sep {
  color: var(--text-subtle);
  margin: 0 4px;
}

.src-row__dest .tbl {
  font-weight: 700;
  color: var(--text);
}

.src-row.is-staged {
  background: var(--alert-bg);
}

.src-row.is-collide {
  background: var(--alert-bg);
}

.src-row.is-staged .src-row__bar,
.src-row.is-collide .src-row__bar {
  background: var(--alert-c);
}

.src-row.is-collide .src-row__channel {
  color: var(--alert-text);
}

.src-state {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px 2px 6px;
  border-radius: 999px;
  font-size: 10.5px;
  font-weight: 700;
  white-space: nowrap;
}

.src-state svg {
  width: 11px;
  height: 11px;
  stroke-width: 3;
}

.src-state.is-written {
  background: hsl(142, 60%, 95%);
  color: var(--actual-text);
}

.src-state.is-staged {
  background: var(--alert-bg);
  color: var(--alert-text);
}

.src-state.is-collide {
  background: var(--alert-bg);
  color: var(--alert-text);
}

/* The reason line under a lit card — what is waiting, and the door. */
.src-card__why {
  display: flex;
  align-items: flex-start;
  gap: 7px;
  padding: 8px 12px 10px;
  border-top: 1px dashed var(--border-soft);
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--alert-text);
}

.src-card__why svg {
  width: 13px;
  height: 13px;
  flex: 0 0 auto;
  margin-top: 1px;
  color: var(--alert-c);
}

.src-card__why a {
  color: var(--accent);
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
}

.src-card__why a:hover {
  text-decoration: underline;
}

/* The collision trace: one red band above the implicated cards. */
.collision-trace {
  flex-shrink: 0;
  display: flex;
  gap: 8px;
  align-items: flex-start;
  padding: 8px 12px;
  border-radius: var(--r-md);
  background: var(--alert-bg);
  border: 1px solid var(--alert-c);
  color: var(--alert-text);
  font-size: 11.5px;
  line-height: 1.45;
}

.collision-trace svg {
  width: 14px;
  height: 14px;
  flex: 0 0 auto;
  margin-top: 1px;
  color: var(--alert-c);
}

.collision-trace strong {
  color: var(--alert-text);
}

/* Empty state — the door into intake with this Model pre-picked. */
.src-empty {
  padding: 28px 18px 24px;
  text-align: center;
}

.src-empty__icon {
  width: 40px;
  height: 40px;
  border-radius: 999px;
  background: var(--accent-bg);
  color: var(--accent);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 10px;
}

.src-empty__icon svg {
  width: 20px;
  height: 20px;
}

.src-empty__title {
  font-size: 13.5px;
  font-weight: 700;
  margin-bottom: 4px;
}

.src-empty__sub {
  font-size: 11.5px;
  color: var(--text-muted);
  line-height: 1.5;
  max-width: 30ch;
  margin: 0 auto 14px;
}

.src-empty .action-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.src-empty .action-btn svg {
  width: 14px;
  height: 14px;
}

.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;
}

/* -------- First-run welcome canvas (no Models yet) ---------------- */
.canvas-welcome {
  /* Centered in the canvas both ways. .app__canvas is a stretched flex child,
     so 100% height gives us the full pane to center within. */
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: var(--s-7);
  text-align: center;
}

.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;
}

.canvas-welcome__lead {
  font-weight: 600;
  color: var(--text);
}

.canvas-welcome__options {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  /* The list is centered as a BLOCK, but its items stay left-aligned: centered
     bullet text would ragged-edge against the dot markers and read poorly. */
  text-align: left;
  max-width: 520px;
}

.canvas-welcome__options li {
  position: relative;
  padding-left: var(--s-4);
  color: var(--text-muted);
  line-height: 1.6;
}

.canvas-welcome__options li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.6em;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--text-subtle);
}

.canvas-welcome__options a {
  font-weight: 600;
}

/* ====================================================================
 * 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,
.source-card: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, model rows, and source cards. */
.ws-card-header .card-action-btn,
.nav-item .card-action-btn,
.source-card .card-action-btn {
  width: 22px;
  height: 22px;
}

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

.ws-card-header .card-actions,
.nav-item .card-actions,
.source-card .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 design system.
 *
 * `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 main.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 design system: 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: var(--s-5);
}

/* -------- 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 1 s 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: 12px;
  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%);
}

/* -------- HELP MODAL ----------------------------------------------
   Same scrim and surface as the confirm modal; wider, because a topic
   is prose with a list. The purple is limited to three touches — the
   `?` roundel before the title, the bullet markers, and the resource
   row's glyphs — so the modal reads as help without shouting. */
.help-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: var(--s-5);
}

.help-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);
  width: 100%;
  max-width: 520px;
  padding: 24px;
}

.help-modal__body {
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text-muted);
}

.help-modal__body p {
  margin: 0 0 10px;
}

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

.help-topic__title {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 12px;
  font-size: 17px;
  font-weight: 700;
  color: var(--text);
}

.help-topic__title::before {
  content: "?";
  width: 26px;
  height: 26px;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--help);
  color: white;
  font-size: 15px;
  font-weight: 700;
  line-height: 1;
  flex-shrink: 0;
}

.help-topic__lead {
  margin-bottom: 4px !important;
  font-weight: 600;
  color: var(--text);
}

.help-topic ul {
  margin: 0;
  padding-left: 20px;
}

.help-topic li {
  margin: 0 0 4px;
}

.help-topic li::marker {
  color: var(--help);
}

.help-topic__after {
  margin-top: 12px !important;
}

/* The escalation footer: Watch · Read · Ask. Built before any of them
   exists so the modal advertises what is coming — a slot that is greyed
   is a promise; a slot that is missing gets bolted on later as a
   separate thing. Rows become links as content lands. */
.help-modal__resources {
  margin: 18px 0 20px;
  padding-top: 14px;
  border-top: 1px solid var(--border-soft);
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.help-resource {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 12.5px;
  color: var(--text-muted);
}

.help-resource svg {
  width: 16px;
  height: 16px;
  color: var(--help);
  flex-shrink: 0;
}

.help-resource__label {
  font-weight: 600;
  color: var(--text);
  min-width: 44px;
}

.help-resource__note {
  color: var(--text-subtle);
}

.help-resource[aria-disabled=true] {
  opacity: 0.7;
}

/* The shared right-justified Cancel / action row. `.modal-actions` is the
   generic class any dialog's footer uses; `.confirm-modal__actions` keeps its
   name for the confirm component. */
.confirm-modal__actions,
.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

/* An optional door elsewhere sits on the left, apart from Cancel / action. */
.confirm-modal__secondary {
  margin-right: auto;
}

/* 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.
   Note: neither this nor .cancel-btn sets font-family, so both render in the
   browser's default button face rather than Assistant; .view-back (the Back
   text button) matches that on purpose — change all three together. */
.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 main.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 the legacy /import/ flows under /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);
}

/* -------- INTAKE (Pass 0 · Scene 1) — "Start a new Flow" -----------
   The Data Source creation entry: explainer + process overview + the
   Upload / Connect picker (two equal panels: file dropzone | nameplate
   connector tiles). Lives inside a standard lifted `.view`; this file
   styles only the body content. */
.intake-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: var(--s-6) var(--s-6) var(--s-7);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-5);
}

/* ---- the source picker --------------------------------------------- */
.intake-picker {
  width: 100%;
  max-width: 980px;
  margin-top: var(--s-4); /* extra air above the heading */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-4);
}

/* The picker lead-in — now a full instruction sentence (the old explainer
   lede folded into it), so it reads as prose rather than a giant heading. */
.intake-picker__label {
  max-width: 680px;
  text-align: center;
  font-size: 17px;
  font-weight: 500;
  line-height: 1.5;
  letter-spacing: -0.005em;
  color: var(--text-muted);
}
.intake-picker__label strong {
  color: var(--text);
  font-weight: 700;
}

.intake-picker__cluster {
  width: 100%;
  display: grid;
  /* Connect (first) gets the larger share — it holds the 2-3-2 array */
  grid-template-columns: minmax(0, 1.5fr) auto minmax(0, 1fr);
  align-items: stretch;
  gap: var(--s-4);
}

/* shared panel frame — the locked card format (card-shadow, no border) */
.pick-panel {
  background: var(--card-bg);
  border-radius: var(--r-md);
  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);
  padding: var(--s-5);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

.pick-panel__head {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
}

.pick-panel__icon {
  color: var(--accent);
  display: flex;
}
.pick-panel__icon i, .pick-panel__icon svg {
  width: 20px;
  height: 20px;
}

.pick-panel__title {
  font-size: 16px;
  font-weight: 700;
  color: var(--text);
}

.pick-panel__sub {
  margin-top: calc(-1 * var(--s-2));
  text-align: center;
  font-size: 12.5px;
  color: var(--text-muted);
}

/* OR divider between the panels */
.pick-or {
  display: flex;
  align-items: center;
  justify-content: center;
}
.pick-or span {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-subtle);
}

/* Upload dropzone */
.dropzone {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: var(--s-7) var(--s-4);
  background: hsl(214, 14%, 97%);
  border: 1.5px dashed var(--border-strong);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: border-color 0.12s ease, background-color 0.12s ease;
}
.dropzone:hover {
  border-color: var(--accent);
  background: var(--accent-bg);
}

.dropzone__icon {
  color: var(--text-subtle);
}
.dropzone__icon i, .dropzone__icon svg {
  width: 26px;
  height: 26px;
}

.dropzone__title {
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
}

.dropzone__hint {
  font-size: 12px;
  color: var(--text-subtle);
}

/* Connector array — seven equal, full-color wordmark tiles in a 2-3-2
   array. The short rows center under the middle row of three. */
.provider-array {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-3);
}

.provider-row {
  display: flex;
  justify-content: center;
  gap: var(--s-3);
}

.provider-tile {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 150px;
  min-height: 58px;
  padding: var(--s-3) var(--s-4);
  background: var(--card-bg);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.provider-tile:hover {
  border-color: var(--accent);
  box-shadow: 0 1px 3px hsla(214, 20%, 50%, 0.1), 0 6px 16px hsla(214, 20%, 50%, 0.08);
}

/* A provider that isn't wired in the registry yet — present but inert. */
.provider-tile--soon {
  cursor: default;
  opacity: 0.45;
  filter: grayscale(1);
}
.provider-tile--soon:hover {
  border-color: var(--border-strong);
  box-shadow: none;
}

/* Wordmarks normalized to a common optical height; width floats so each
   keeps its native proportions. */
.provider-logo {
  display: block;
  height: 24px;
  width: auto;
  max-width: 100%;
}

/* =====================================================================
   SCENE 2 — the Stripe setup path: connect → importing → comprehension.
   Each scene is its own .intake-body; --center vertically centers a
   single card (auto margins, not justify-content, so a tall card stays
   scrollable instead of clipping at the top).
   ===================================================================== */
.intake-body--center {
  justify-content: flex-start;
}
.intake-body--center > * {
  margin-top: auto;
  margin-bottom: auto;
}

/* ---- 2a · connect (key entry) -------------------------------------- */
.connect-card {
  position: relative;
  width: 100%;
  max-width: 460px;
  background: var(--card-bg);
  border-radius: var(--r-md);
  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);
  padding: var(--s-6) var(--s-6) var(--s-5);
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
}

.connect-card__back {
  position: absolute;
  top: var(--s-4);
  left: var(--s-4);
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: transparent;
  border: none;
  padding: 2px 4px;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  cursor: pointer;
}
.connect-card__back:hover {
  color: var(--accent);
}
.connect-card__back i, .connect-card__back svg {
  width: 14px;
  height: 14px;
}

.connect-card__head {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--s-2);
  margin-top: var(--s-3);
}

.connect-card__logo {
  height: 30px;
  width: auto;
}

.connect-card__title {
  margin: 0;
  font-size: 18px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text);
}

.connect-card__sub {
  margin: 0;
  max-width: 38ch;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--text-muted);
}

.connect-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.connect-field .param-label__hint {
  margin: 0;
}

/* the shared .param-input is sized for the panel; intake fields are full-width */
.connect-input {
  width: 100%;
}

/* reuse the amber .upload-warning box; lay icon + text side by side */
.connect-warning,
.importing-warning {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  line-height: 1.45;
}
.connect-warning i, .connect-warning svg,
.importing-warning i,
.importing-warning svg {
  width: 14px;
  height: 14px;
  flex: 0 0 auto;
  margin-top: 1px;
}

.connect-error {
  margin: 0;
  font-size: 12px;
  line-height: 1.4;
  color: hsl(0, 65%, 42%);
}

/* Stopping a job on purpose is not a failure, so it reads muted rather than red —
   the connect scene is also where a CANCELLED first import lands, and colouring
   that the same as a broken credential would misreport what happened. */
.connect-notice {
  margin: 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--text-muted);
}

/* Same error style, centered for the entry picker (a refused upload bounces back
   to the centered Scene 1, not the left-aligned connect card). */
.intake-picker__error {
  max-width: 680px;
  text-align: center;
  font-size: 13px;
}

/* Full-width error line above the Review screen's card stack — the surface for
   reclassify / combine / ledgerize failures, which otherwise display nowhere
   on this screen. */
.intake-error-banner {
  width: 100%;
  font-size: 13px;
}

.connect-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--s-2);
  margin-top: var(--s-1);
}
.connect-actions .action-btn i,
.connect-actions .action-btn svg {
  width: 14px;
  height: 14px;
}

/* dev-only sample-data shortcut — quiet, clearly secondary to Begin Import */
.connect-sample {
  align-self: center;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  background: transparent;
  border: none;
  padding: 2px 4px;
  font-size: 11.5px;
  color: var(--text-subtle);
  cursor: pointer;
}
.connect-sample:hover {
  color: var(--accent);
}
.connect-sample i, .connect-sample svg {
  width: 13px;
  height: 13px;
}

/* ---- 2b · importing ------------------------------------------------- */
.importing-panel {
  width: 100%;
  max-width: 440px;
  background: var(--card-bg);
  border-radius: var(--r-md);
  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);
  padding: var(--s-7) var(--s-6) var(--s-6);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--s-3);
}

.importing-panel__spinner {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: 3px solid var(--accent-bg-strong);
  border-top-color: var(--accent);
  animation: intake-spin 0.8s linear infinite;
}

@keyframes intake-spin {
  to {
    transform: rotate(360deg);
  }
}
.importing-panel__title {
  margin: 0;
  font-size: 17px;
  font-weight: 700;
  color: var(--text);
}

.importing-panel__sub {
  margin: 0;
  font-size: 12.5px;
  color: var(--text-muted);
}

/* The cancel affordance under the progress bar. Column, because a pressed cancel
   grows a second line explaining why nothing has stopped yet. */
.importing-panel__foot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-2);
}

.importing-panel__bar {
  position: relative;
  width: 100%;
  height: 4px;
  border-radius: 2px;
  background: var(--border-soft);
  overflow: hidden;
  margin-top: var(--s-1);
}

.importing-panel__bar-fill {
  position: absolute;
  top: 0;
  height: 100%;
  width: 40%;
  border-radius: 2px;
  background: var(--accent);
  animation: intake-indeterminate 1.4s ease-in-out infinite;
}

@keyframes intake-indeterminate {
  0% {
    left: -42%;
  }
  100% {
    left: 100%;
  }
}
/* ---- 2c · comprehension (born-thinking Source card) ----------------
   A first-pass of the bible's Source card (the real component graduates
   with the Plan view). Namespaced comp-* to avoid colliding with the
   future .source-card / .source-anatomy. Frame comes from .section-card. */
.comp-card {
  width: 100%;
  max-width: 880px; /* room for the three anatomy zones + longer Model names */
  padding: 0;
  overflow: hidden;
}

/* The intake header is promoted onto the View surface (Decision 116): the
   Source name + meta + stage pips + the forward action live in .view-header,
   so Direct and Connection share one header and the comp-card body is just
   content. The .comp-card__name / __meta rules below are reused up there. */
.view-header__heading {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

/* Match the standard View-header title size/weight (bible §4.4). */
.view-header__heading .comp-card__name {
  font-size: 20px;
  letter-spacing: -0.01em;
}

.view-header__heading .comp-card__name-input {
  font-size: 20px;
  max-width: 360px;
}

/* Small overline above the name — this surface only ever shows for a brand-new
   source (re-opened sources hydrate straight to the landed view), so it's
   always accurate, and it frames the name as the thing to set. */
.view-header__eyebrow {
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-subtle);
  line-height: 1;
  margin-bottom: 2px;
}

/* stage pips — Review › Route (Direct only; Connection is single-screen) */
.view-steps {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.view-step {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--text-subtle);
}

.view-step__dot {
  width: 17px;
  height: 17px;
  flex: 0 0 auto;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--border);
  color: var(--text-subtle);
  font-size: 9.5px;
  font-weight: 800;
}
.view-step__dot i, .view-step__dot svg {
  width: 11px;
  height: 11px;
  stroke-width: 3.5;
}

.view-step.is-active {
  color: var(--accent);
}

.view-step.is-active .view-step__dot {
  background: var(--accent);
  color: #fff;
}

.view-step.is-done .view-step__dot {
  background: hsl(142, 60%, 40%);
  color: #fff;
}

.view-step__sep {
  display: inline-flex;
  color: var(--text-subtle);
}
.view-step__sep i, .view-step__sep svg {
  width: 13px;
  height: 13px;
}

/* Each comprehension screen stacks its wizard step line above its card(s),
   left-justified to the card's edge. Review is content-sized (cards have no
   fixed width) so flex-start aligns the line to the card; Route is capped to
   the comp-card's width so the line aligns to its left edge. */
.intake-screen {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s-3);
  min-width: 0;
}

.intake-screen--route {
  align-items: stretch;
  width: 100%;
  max-width: 880px; /* match .comp-card so the line aligns to its left edge */
}

/* Cap the Review screen at the same width as Route. Without it the review-card
   (a flex-start item, so it sizes to its content's max-content) stretches to the
   full view whenever a file gets a long AI reasoning paragraph — the paragraph's
   single-line max-content blows the card out. align-items stays flex-start (from
   the base) so a short/Ladder-settled card still shrinks to its content; the cap
   only kicks in to wrap a long reason instead of widening the card. */
.intake-screen--review {
  max-width: 880px;
}

/* The wizard step line (Decision 116 amend): a blue numbered step + the screen's
   title/instruction on one line, replacing the old top-right pips + section
   label. Makes the comprehension scene read as a guided wizard, not a content page. */
.wizard-step {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--accent);
  font-size: 13px;
  font-weight: 700;
  line-height: 1.2;
}

.wizard-step__num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 19px;
  height: 19px;
  border-radius: 999px;
  background: var(--accent);
  color: #fff;
  font-size: 11px;
  font-weight: 800;
}

.wizard-step__body {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.wizard-step__sep {
  width: 13px;
  height: 13px;
  flex: 0 0 auto;
  opacity: 0.55;
}

/* Each screen's action row — Back (left) / Continue or Assemble (right) —
   sits directly under the screen's last card (2026-09-04, Matt; it was a bar
   pinned to the View's bottom). align-self: stretch makes the row span the
   card: Review's screen is content-sized, so the row inherits the card's width
   rather than the View's, and the buttons hug the card's own edges. */
.intake-footer {
  flex: 0 0 auto;
  align-self: stretch;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-top: var(--s-2);
}

.intake-footer__spacer {
  flex: 1 1 auto;
}

.intake-footer__actions {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}

.intake-footer .action-btn {
  gap: 6px;
}
.intake-footer .action-btn i, .intake-footer .action-btn svg {
  width: 15px;
  height: 15px;
}

/* The "why it's locked" note beside a disabled forward action (slice 5). Calm,
   not alarming — it states what's left, the red lives on the offending step. */
.intake-lock {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
}
.intake-lock i, .intake-lock svg {
  width: 13px;
  height: 13px;
  opacity: 0.7;
}

/* Back is a text button, but it sits opposite an .action-btn on the same row,
   so it carries that button's type (13px / 600) and vertical padding — the
   pair reads as one row of equals, not a label beside a button. Deliberately
   NO `font: inherit`: .action-btn / .cancel-btn set no font-family, so they
   render in the browser's default button face (Arial on Mac), which sits
   visibly larger than Assistant at the same 13px. Inheriting here made Back
   look a size smaller than Assemble (Matt, 2026-09-04). If those buttons ever
   get `font-family: inherit`, give Back the same. */
.view-back {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 8px 12px;
  border: none;
  border-radius: var(--r-md);
  background: none;
  cursor: pointer;
  font-weight: 600;
  font-size: 13px;
  line-height: normal;
  color: var(--text);
}
.view-back:hover {
  background: rgba(0, 0, 0, 0.05);
}
.view-back i, .view-back svg {
  width: 15px;
  height: 15px;
}

.comp-card__name {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.005em;
  color: var(--text);
  line-height: 1.3;
}

/* Click-to-rename: the name renders as a borderless button with a persistent
   pencil (renaming the source is encouraged, so the affordance always shows);
   editing swaps it for an inline text input. */
.comp-card__name--edit {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  padding: 2px 6px;
  background: none;
  border: none;
  cursor: pointer;
  font: inherit;
  color: var(--text);
  border-radius: var(--r-sm);
}
.comp-card__name--edit:hover {
  background: rgba(0, 0, 0, 0.04);
}

.comp-card__rename-icon {
  width: 13px;
  height: 13px;
  color: var(--text-subtle);
  transition: color 0.12s ease;
}
.comp-card__name--edit:hover .comp-card__rename-icon {
  color: var(--accent);
}

.comp-card__name-input {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.005em;
  color: var(--text);
  line-height: 1.3;
  padding: 1px 6px;
  border: 1px solid var(--accent);
  border-radius: var(--r-sm);
  outline: none;
  background: var(--card-bg);
  width: 100%;
  max-width: 360px;
}

.comp-card__meta {
  margin-top: 1px;
  font-size: 11.5px;
  color: var(--text-muted);
  line-height: 1.45;
}
.comp-card__meta strong {
  color: var(--text);
  font-weight: 600;
}

/* "Evaluated with / without AI" blurb at the end of the info row — a small pill
   so it reads as an annotation, not another meta segment. AI = accent-tinted
   (the file went to the arbiter); deterministic/Connection = calm neutral. */
.ai-blurb {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-left: 8px;
  padding: 1px 8px;
  border-radius: 999px;
  background: var(--bg-subtle, hsl(220, 14%, 96%));
  color: var(--text-muted);
  font-size: 10.5px;
  font-weight: 600;
  white-space: nowrap;
  vertical-align: 1px;
}
.ai-blurb i, .ai-blurb svg {
  width: 12px;
  height: 12px;
}

.ai-blurb--ai {
  background: var(--accent-bg);
  color: var(--accent);
}

/* Connection Review (Decision 117) — the import-summary card on Screen 1 for a
   Connection (the Direct path uses the WS22 review-card instead). Same greyed-
   header / white-body language as the review card so the two Review faces read
   as one system. */
.conn-review {
  margin: var(--s-4) 0 var(--s-5);
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  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;
}

.conn-review__header {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: 10px var(--s-3);
  border-bottom: 1px solid var(--border-soft);
  background: hsl(220, 14%, 94%);
}

.conn-review__icon {
  display: inline-flex;
  flex-shrink: 0;
  color: var(--text-muted);
}
.conn-review__icon i {
  width: 18px;
  height: 18px;
}

.conn-review__titles {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1 1 auto;
}

.conn-review__title {
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
  line-height: 1.3;
}

.conn-review__meta {
  margin-top: 1px;
  font-size: 11px;
  color: var(--text-muted);
}

.conn-review__flag {
  display: inline-flex;
  align-items: center;
  font-size: 10.5px;
  font-weight: 700;
  padding: 2px 8px;
  border-radius: 999px;
  flex-shrink: 0;
  text-transform: capitalize;
  background: var(--rc-conf-green-soft, hsl(142, 60%, 95%));
  color: var(--rc-conf-green-tag, hsl(142, 60%, 32%));
}

.conn-review__body {
  padding: var(--s-3) var(--s-4);
}

.conn-review__lead {
  margin: 0 0 var(--s-3);
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.5;
}

.conn-review__lines {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.conn-review__line {
  display: flex;
  align-items: flex-start;
  gap: 7px;
  font-size: 12px;
  line-height: 1.45;
  color: var(--text);
}
.conn-review__line i, .conn-review__line svg {
  width: 13px;
  height: 13px;
  flex: 0 0 auto;
  margin-top: 2px;
  color: var(--rc-conf-green, hsl(142, 60%, 40%));
}

/* ── Import range — source-level setting (Review screen + landed Update
   History zone share this block via components/import_range.html). ── */
/* Collapsed it is one line among the zone's other lines, so it drops the card
   frame that used to set it apart — the boxed treatment was costing more height
   than the control itself. */
.intake-range {
  margin-top: var(--s-3);
}

.intake-range__label {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.intake-range__label i, .intake-range__label svg {
  width: 13px;
  height: 13px;
  opacity: 0.7;
}

/* From … Through stays on ONE line inside the narrow Update History zone, so
   the inputs shrink rather than wrap (min-width:0 lets flex actually do it). */
.intake-range__fields {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--s-3);
  margin-top: var(--s-2);
}

.intake-range__field {
  display: flex;
  align-items: center;
  gap: 5px;
  flex: 1 1 0;
  min-width: 0;
}
.intake-range__field span {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  flex: 0 0 auto;
}
.intake-range__field input {
  flex: 1 1 0;
  min-width: 0;
  padding-left: 6px;
  padding-right: 4px;
}
.intake-range__field {
  /* The native calendar glyph costs ~20px we don't have; the field still opens
     its picker from the keyboard and the spinner arrows remain. */
}
.intake-range__field input::-webkit-calendar-picker-indicator {
  display: none;
}

/* Every import-range month field (the connect form's start date, the landed
   Update History range) reads as BLANK until it holds a real month — each one's
   label already says blank means everything, so the browser's own mm/yyyy
   scaffold is noise. It returns on focus, where the format is actually useful.
   Set by an `is-empty` binding on the input; webkit/blink only (Firefox keeps
   its hint, Safari renders these as plain text fields). */
input[type=month].is-empty:not(:focus)::-webkit-datetime-edit {
  color: transparent;
}

.intake-range__hint {
  margin: var(--s-2) 0 0;
  font-size: 10.5px;
  line-height: 1.45;
  color: var(--text-muted);
}

/* ── Reactivation reading — repeats once per slice on both Split surfaces, so
   it is deliberately small: three lines, no card frame, no icon. ── */
.rx {
  margin-top: var(--s-3);
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--text-muted);
  /* Opening its own step (Route screen), the gap above is the step's job. */
}
.rx:first-child {
  margin-top: 0;
}

/* Per-slice opt-outs, below the source's rule and behind a disclosure. Each is
   one checkbox until it's switched on, so ten slices cost ten lines rather than
   ten forms — and closed, they cost one. */
/* No rule above it and barely a gap: the overrides are the last line of the
   reactivation block, not a separate section. */
.rx-overrides {
  margin-top: 3px;
}

/* A one-line disclosure inside an anatomy zone — the shape that keeps a rarely
   touched setting from spending five lines saying nothing. Reads as one more
   line of the block it belongs to, so it matches .rx__label's type exactly.
   Anything it hides must still be REPORTED on the line (a value, a count):
   a setting invisible behind a closed row is a setting that surprises. */
.zone-disclosure {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 0;
  font-size: 11.5px;
  line-height: 1.5;
  font-weight: 600;
  color: var(--text);
  background: none;
  border: 0;
  cursor: pointer;
}
.zone-disclosure:hover {
  color: var(--accent);
}

/* Sized on the ELEMENT, not on a descendant: lucide swaps the <i> for an <svg>
   and carries the class across, so `.zone-disclosure__caret svg` matches
   nothing and the icon renders at its natural 24px. */
.zone-disclosure__caret {
  width: 12px;
  height: 12px;
  flex: 0 0 auto;
  transition: transform 0.15s ease;
}
.zone-disclosure__caret.is-open {
  transform: rotate(90deg);
}

.zone-disclosure__value {
  font-weight: 400;
  color: var(--text-muted);
}

/* A live override must not hide inside a closed section. */
.rx-overrides__count {
  min-width: 15px;
  padding: 0 4px;
  border-radius: 7px;
  font-size: 10px;
  font-weight: 700;
  line-height: 15px;
  text-align: center;
  color: white;
  background: var(--accent);
}

.rx-override + .rx-override {
  margin-top: 4px;
}

.rx-override__toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 11.5px;
  color: var(--text-muted);
  cursor: pointer;
}
.rx-override__toggle input {
  flex: 0 0 auto;
  margin: 0;
  accent-color: var(--accent);
}
.rx-override__toggle strong {
  color: var(--text);
  font-weight: 600;
}

/* Indented under its toggle, so an overriding slice reads as a branch off the
   source's rule rather than as a second rule of equal standing. */
.rx-override__body {
  margin-left: 18px;
  padding-left: var(--s-3);
  border-left: 2px solid var(--border);
}
.rx-override__body .rx {
  margin-top: var(--s-2);
}

.rx__label {
  font-weight: 600;
  color: var(--text);
}

.rx__opt {
  display: flex;
  align-items: baseline;
  gap: 6px;
  cursor: pointer;
}
.rx__opt input[type=radio] {
  flex: 0 0 auto;
  margin: 0;
  accent-color: var(--accent);
}
.rx__opt strong {
  color: var(--text);
  font-weight: 700;
  letter-spacing: 0.01em;
}

.rx__cont {
  margin-top: 2px;
}

/* ── The mixed-frequency guardrail, sitting above the histogram it qualifies.
   Amber, because amber already means "this wants your attention and nothing is
   broken" everywhere else in the app — rose would promise an error, and there
   isn't one: the import succeeded and the numbers are internally consistent,
   they just describe a business this source cannot represent in one piece. No
   icon and no dismiss: it is two sentences that stop being true the moment the
   source is grouped. ── */
.rx-cadence {
  margin-bottom: var(--s-3);
  padding: var(--s-2) var(--s-3);
  background: var(--dirty-bg);
  border-radius: var(--r-md);
  color: var(--dirty-text);
  line-height: 1.5;
}

.rx-cadence__group {
  margin: 0;
  /* Two bold runs: the finding opens the paragraph, the fix closes it, and the
     explanation between them stays plain. */
}
.rx-cadence__group + .rx-cadence__group {
  margin-top: var(--s-2);
}
.rx-cadence__group strong {
  font-weight: 700;
}
.rx-cadence__group {
  /* Only the HEADLINE needs a generated trailing space — it is followed by a
     text node that would otherwise butt against it, whereas the fix strong
     carries its own leading space and is followed by either nothing or a tail
     that has one too. Scoped by :first-child rather than applied to every
     strong, which would double the gap before that tail. */
}
.rx-cadence__group strong:first-child::after {
  content: " ";
}

.rx-cadence__scope {
  margin: var(--s-2) 0 0;
  font-size: 10.5px;
  opacity: 0.8;
}

/* ── The lapse histogram — evidence for the two cutoffs below it. Deliberately
   plain: rows of small type, no card frame, no color coding. It sits inside an
   anatomy zone that already has a frame, and a chart with its own chrome would
   read as a report rather than as the reason the next two controls exist. ── */
.rx-eff {
  margin-bottom: var(--s-3);
}

/* The summary line is a whole sentence, so unlike the short disclosure labels
   elsewhere it wraps in the narrow Flow View zone — align the caret to the
   FIRST line rather than to the vertical middle of a two-line block. */
.rx-eff__toggle {
  align-items: flex-start;
  margin-bottom: var(--s-2);
  text-align: left;
}
.rx-eff__toggle .zone-disclosure__caret {
  margin-top: 4px;
}

.rx-eff__row {
  display: flex;
  align-items: center;
  gap: 6px;
  line-height: 1.7;
}

/* Fixed so the bars share a baseline and the shape is readable down the
   column; the labels are short and predictable ("13+ months" is the longest). */
.rx-eff__key {
  flex: 0 0 72px;
  font-variant-numeric: tabular-nums;
}

.rx-eff__bar {
  flex: 1 1 auto;
  min-width: 0;
  height: 9px;
  background: var(--border);
  border-radius: 2px;
  overflow: hidden;
}
.rx-eff__bar i {
  display: block;
  width: var(--w);
  height: 100%;
  background: var(--accent);
  border-radius: 2px;
}

.rx-eff__n {
  flex: 0 0 28px;
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  font-weight: 600;
}

.rx-eff__note {
  margin: var(--s-2) 0 0;
  font-size: 10.5px;
  line-height: 1.45;
}
.rx-eff__note strong {
  color: var(--text);
  font-weight: 600;
}

.rx-eff__empty {
  margin: 0 0 var(--s-3);
}

/* The proposal reads as a suggestion, not an alert — it is right most of the
   time and wrong some of the time, and its styling should promise exactly
   that. */
.rx-eff__propose {
  margin: var(--s-3) 0 0;
  padding: var(--s-2) var(--s-3);
  background: var(--accent-bg);
  border-radius: var(--r-md);
  line-height: 1.5;
}
.rx-eff__propose button {
  display: block;
  margin-top: 3px;
  padding: 0;
  font: inherit;
  font-weight: 600;
  color: var(--accent);
  background: none;
  border: 0;
  cursor: pointer;
}
.rx-eff__propose button:hover {
  text-decoration: underline;
}

/* The live readout. Tabular numerals so the counts don't jitter sideways while
   someone is typing in the boxes above. */
.rx-eff__bands {
  display: flex;
  gap: var(--s-3);
  margin-top: var(--s-3);
  padding-top: var(--s-2);
  border-top: 1px solid var(--border);
  font-size: 11px;
  color: var(--text-muted);
}
.rx-eff__bands span {
  font-weight: 700;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

/* Inline number box — sized for two digits so the sentence still reads as a
   sentence rather than a form. */
.rx__num {
  width: 44px;
  padding: 1px 4px;
  font-size: 11.5px;
  font-family: inherit;
  color: var(--text);
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  text-align: center;
}
.rx__num::-webkit-inner-spin-button {
  opacity: 0.35;
}

/* Connection variant: the set-once start date as a read-only fact line. */
.intake-range--fact {
  padding: var(--s-2) var(--s-4);
}

.intake-range__fact {
  display: flex;
  align-items: flex-start;
  gap: 7px;
  margin: 0;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--text-muted);
}
.intake-range__fact strong {
  color: var(--text);
  font-weight: 600;
}
.intake-range__fact i, .intake-range__fact svg {
  width: 13px;
  height: 13px;
  flex: 0 0 auto;
  margin-top: 1px;
  opacity: 0.7;
}

/* D121b lifecycle-vs-charge picker — a two-option radio-card group inside the
   Connection Review card. Only rendered for a Billing account (canChooseMode). */
.conn-mode {
  margin: var(--s-3) 0 0;
  padding: var(--s-3) 0 0;
  border: 0;
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}
.conn-mode--busy {
  opacity: 0.6;
  pointer-events: none;
}

.conn-mode__q {
  padding: 0;
  font-size: 12px;
  font-weight: 700;
  color: var(--text);
}

.conn-mode__opt {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: border-color 0.12s ease, background 0.12s ease;
}
.conn-mode__opt:hover {
  border-color: var(--accent);
}
.conn-mode__opt--on {
  border-color: var(--accent);
  background: var(--accent-bg);
}

.conn-mode__radio {
  margin-top: 3px;
  flex: 0 0 auto;
  accent-color: var(--accent);
}

.conn-mode__text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.conn-mode__title {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 12px;
  font-weight: 600;
  color: var(--text);
}

.conn-mode__rec {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--accent);
  background: var(--accent-bg);
  padding: 1px 6px;
  border-radius: 999px;
}

.conn-mode__desc {
  font-size: 11px;
  line-height: 1.45;
  color: var(--text-muted);
}

.conn-mode__busy {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  color: var(--text-muted);
}
.conn-mode__busy i, .conn-mode__busy svg {
  width: 12px;
  height: 12px;
  animation: intake-spin 0.8s linear infinite;
}

/* Source-card anatomy — ports Workshop 21 Scene 1's zones (Update History /
   Split / Process). Scoped under .comp-card so it doesn't collide with the
   Plan-view source card when that lands and defines these classes globally.
   The landed Flow View's source header reuses the same zones read-only when
   expanded (.source-card.is-expanded), so the slice->Model map stays visible. */
.comp-card .source-anatomy,
.source-card.is-expanded .source-anatomy {
  display: flex;
  align-items: stretch;
}
.comp-card .source-anatomy__zone,
.source-card.is-expanded .source-anatomy__zone {
  flex: 1 1 0;
  min-width: 0;
  padding: 10px var(--s-3) 12px;
}
.comp-card .source-anatomy__zone + .comp-card .source-anatomy__zone,
.comp-card .source-anatomy__zone + .source-card.is-expanded .source-anatomy__zone,
.source-card.is-expanded .source-anatomy__zone + .comp-card .source-anatomy__zone,
.source-card.is-expanded .source-anatomy__zone + .source-card.is-expanded .source-anatomy__zone {
  border-left: 1px solid var(--border-soft);
}
.comp-card .source-anatomy__zone--history,
.source-card.is-expanded .source-anatomy__zone--history {
  flex: 1.2 1 0;
}
.comp-card,
.source-card.is-expanded {
  /* Reactivations carries three sentence-shaped lines and needs the width to
     keep them on one line each. Group By is one line per stack field (a name
     chip, then what it groups into), indented under a Projects field, so it
     gets a little more than History's short file list. */
}
.comp-card .source-anatomy__zone--split,
.source-card.is-expanded .source-anatomy__zone--split {
  flex: 1.3 1 0;
}
.comp-card .source-anatomy__zone--reactivations,
.source-card.is-expanded .source-anatomy__zone--reactivations {
  flex: 1.9 1 0;
}
.comp-card .source-anatomy__label,
.source-card.is-expanded .source-anatomy__label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 8px;
}
.comp-card .source-anatomy__label i, .comp-card .source-anatomy__label svg,
.source-card.is-expanded .source-anatomy__label i,
.source-card.is-expanded .source-anatomy__label svg {
  width: 13px;
  height: 13px;
  color: var(--accent);
}
.comp-card,
.source-card.is-expanded {
  /* Edit Group By — a quiet accent text button at the label's right end. */
}
.comp-card .source-anatomy__edit,
.source-card.is-expanded .source-anatomy__edit {
  appearance: none;
  border: 0;
  background: none;
  margin-left: auto;
  padding: 1px 4px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font: inherit;
  font-size: 11px;
  letter-spacing: 0;
  text-transform: none;
  font-weight: 700;
  color: var(--accent);
  border-radius: var(--r-sm);
  cursor: pointer;
}
.comp-card .source-anatomy__edit i, .comp-card .source-anatomy__edit svg,
.source-card.is-expanded .source-anatomy__edit i,
.source-card.is-expanded .source-anatomy__edit svg {
  width: 12px;
  height: 12px;
}
.comp-card .source-anatomy__edit:hover,
.source-card.is-expanded .source-anatomy__edit:hover {
  background: var(--accent-bg);
}
.comp-card .source-anatomy__hint,
.source-card.is-expanded .source-anatomy__hint {
  margin-top: 8px;
  font-size: 10.5px;
  font-style: italic;
  line-height: 1.45;
  color: var(--text-subtle);
}
.comp-card,
.source-card.is-expanded {
  /* Update History — events, not data */
}
.comp-card .event-row,
.source-card.is-expanded .event-row {
  padding: 7px 0;
}
.comp-card .event-row + .event-row,
.source-card.is-expanded .event-row + .event-row {
  border-top: 1px solid var(--border-soft);
}
.comp-card .event-row__head,
.source-card.is-expanded .event-row__head {
  display: flex;
  align-items: center;
  gap: 6px;
}
.comp-card .event-row__head > svg,
.source-card.is-expanded .event-row__head > svg {
  width: 13px;
  height: 13px;
  color: var(--text-subtle);
  flex: 0 0 auto;
}
.comp-card .event-row__name,
.source-card.is-expanded .event-row__name {
  font-size: 12px;
  font-weight: 700;
  color: var(--text);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.comp-card .event-row__time,
.source-card.is-expanded .event-row__time {
  margin-left: auto;
  font-size: 10.5px;
  color: var(--text-subtle);
  flex: 0 0 auto;
}
.comp-card .event-row__stats,
.source-card.is-expanded .event-row__stats {
  margin-top: 2px;
  margin-left: 19px;
  font-size: 10.5px;
  color: var(--text-muted);
  line-height: 1.5;
}
.comp-card .event-row__stats strong,
.source-card.is-expanded .event-row__stats strong {
  color: hsl(150, 55%, 36%);
  font-weight: 700;
}
.comp-card .event-row__stats,
.source-card.is-expanded .event-row__stats { /* "new" — no green token in v2 yet */ }
.comp-card,
.source-card.is-expanded {
  /* Re-drop — "Update with a new file" (Direct sources): recognize → Replace */
}
.comp-card .redrop,
.source-card.is-expanded .redrop {
  margin-top: 10px;
}
.comp-card .redrop__add,
.source-card.is-expanded .redrop__add {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: 100%;
  padding: 8px 10px;
  font-size: 11.5px;
  font-weight: 700;
  color: var(--accent);
  background: var(--accent-bg);
  border: 1px dashed var(--border-strong);
  border-radius: 6px;
  cursor: pointer;
  transition: background 120ms, border-color 120ms;
}
.comp-card .redrop__add i, .comp-card .redrop__add svg,
.source-card.is-expanded .redrop__add i,
.source-card.is-expanded .redrop__add svg {
  width: 14px;
  height: 14px;
}
.comp-card .redrop__add:hover,
.source-card.is-expanded .redrop__add:hover {
  border-color: var(--accent);
  background: var(--card-bg);
}
.comp-card .redrop__busy,
.source-card.is-expanded .redrop__busy {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 2px;
  font-size: 11px;
  color: var(--text-muted);
}
.comp-card .redrop__busy i, .comp-card .redrop__busy svg,
.source-card.is-expanded .redrop__busy i,
.source-card.is-expanded .redrop__busy svg {
  width: 13px;
  height: 13px;
  animation: redrop-spin 1s linear infinite;
}
.comp-card .redrop-preview,
.source-card.is-expanded .redrop-preview {
  margin-top: 8px;
  padding: 10px;
  background: var(--card-bg);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-md);
}
.comp-card .redrop-preview--drift,
.source-card.is-expanded .redrop-preview--drift {
  border-color: hsl(0, 60%, 82%);
  background: hsl(0, 70%, 98%);
}
.comp-card .redrop-preview__head,
.source-card.is-expanded .redrop-preview__head {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  font-size: 12px;
  font-weight: 700;
  line-height: 1.35;
  color: var(--text);
}
.comp-card .redrop-preview__head i, .comp-card .redrop-preview__head svg,
.source-card.is-expanded .redrop-preview__head i,
.source-card.is-expanded .redrop-preview__head svg {
  width: 14px;
  height: 14px;
  flex: 0 0 auto;
  margin-top: 1px;
}
.comp-card .redrop-preview__head--ok i, .comp-card .redrop-preview__head--ok svg,
.source-card.is-expanded .redrop-preview__head--ok i,
.source-card.is-expanded .redrop-preview__head--ok svg {
  color: hsl(142, 60%, 40%);
}
.comp-card .redrop-preview__head--drift i, .comp-card .redrop-preview__head--drift svg,
.source-card.is-expanded .redrop-preview__head--drift i,
.source-card.is-expanded .redrop-preview__head--drift svg {
  color: hsl(0, 72%, 45%);
}
.comp-card .redrop-preview__facts,
.source-card.is-expanded .redrop-preview__facts {
  margin: 4px 0 0 20px;
  font-size: 10.5px;
  color: var(--text-muted);
}
.comp-card .redrop-warn,
.source-card.is-expanded .redrop-warn {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  margin-top: 8px;
  padding: 7px 8px;
  font-size: 10.5px;
  line-height: 1.45;
  color: var(--text-muted);
  border-radius: 6px;
}
.comp-card .redrop-warn i, .comp-card .redrop-warn svg,
.source-card.is-expanded .redrop-warn i,
.source-card.is-expanded .redrop-warn svg {
  width: 13px;
  height: 13px;
  flex: 0 0 auto;
  margin-top: 1px;
  color: var(--text-subtle);
}
.comp-card .redrop-warn--strong,
.source-card.is-expanded .redrop-warn--strong {
  color: hsl(28, 80%, 28%);
  background: hsl(38, 92%, 95%);
  border: 1px solid hsl(38, 80%, 80%);
  font-weight: 600;
}
.comp-card .redrop-warn--strong i, .comp-card .redrop-warn--strong svg,
.source-card.is-expanded .redrop-warn--strong i,
.source-card.is-expanded .redrop-warn--strong svg {
  color: hsl(38, 92%, 46%);
}
.comp-card,
.source-card.is-expanded {
  /* A rebuilt mapping, read-only: column, arrow, field, and any doubt. */
}
.comp-card .redrop-mapping,
.source-card.is-expanded .redrop-mapping {
  list-style: none;
  margin: 8px 0 0 20px;
  padding: 0;
  display: grid;
  gap: 3px;
}
.comp-card .redrop-mapping__row,
.source-card.is-expanded .redrop-mapping__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px;
  font-size: 10.5px;
  color: var(--text-muted);
}
.comp-card .redrop-mapping__column,
.source-card.is-expanded .redrop-mapping__column {
  color: var(--text);
}
.comp-card .redrop-mapping__arrow,
.source-card.is-expanded .redrop-mapping__arrow {
  width: 11px;
  height: 11px;
  color: var(--text-subtle);
}
.comp-card .redrop-mapping__field,
.source-card.is-expanded .redrop-mapping__field {
  font-weight: 600;
  color: var(--text);
}
.comp-card .redrop-mapping__doubt,
.source-card.is-expanded .redrop-mapping__doubt {
  color: hsl(28, 80%, 38%);
}
.comp-card .redrop-preview__actions,
.source-card.is-expanded .redrop-preview__actions {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 10px;
}
.comp-card .redrop-btn,
.source-card.is-expanded .redrop-btn {
  padding: 5px 11px;
  font-size: 11.5px;
  font-weight: 700;
  border: 1px solid transparent;
  border-radius: 6px;
  cursor: pointer;
}
.comp-card .redrop-btn--primary,
.source-card.is-expanded .redrop-btn--primary {
  color: #fff;
  background: var(--accent);
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
.comp-card .redrop-btn--primary i, .comp-card .redrop-btn--primary svg,
.source-card.is-expanded .redrop-btn--primary i,
.source-card.is-expanded .redrop-btn--primary svg {
  width: 13px;
  height: 13px;
}
.comp-card .redrop-btn--primary:hover,
.source-card.is-expanded .redrop-btn--primary:hover {
  filter: brightness(0.94);
}
.comp-card .redrop-btn--primary:disabled,
.source-card.is-expanded .redrop-btn--primary:disabled {
  opacity: 0.6;
  cursor: default;
}
.comp-card .redrop-btn--danger,
.source-card.is-expanded .redrop-btn--danger {
  color: #fff;
  background: hsl(0, 68%, 47%);
}
.comp-card .redrop-btn--danger:hover,
.source-card.is-expanded .redrop-btn--danger:hover {
  background: hsl(0, 68%, 42%);
}
.comp-card .redrop-btn--danger:disabled,
.source-card.is-expanded .redrop-btn--danger:disabled {
  opacity: 0.6;
  cursor: default;
}
.comp-card .redrop-btn--plain,
.source-card.is-expanded .redrop-btn--plain {
  color: var(--text-muted);
  background: transparent;
}
.comp-card .redrop-btn--plain:hover,
.source-card.is-expanded .redrop-btn--plain:hover {
  color: var(--text);
}
.comp-card .redrop__error,
.source-card.is-expanded .redrop__error {
  margin-top: 6px;
  font-size: 10.5px;
  color: hsl(0, 65%, 45%);
}
.comp-card,
.source-card.is-expanded {
  /* Spinning refresh glyph while a manual sync is in flight (lucide preserves the
     class onto the rendered svg). */
}
.comp-card .is-spinning,
.source-card.is-expanded .is-spinning {
  animation: redrop-spin 0.8s linear infinite;
}
@keyframes redrop-spin {
  to {
    transform: rotate(360deg);
  }
}
.comp-card,
.source-card.is-expanded {
  /* zone controls (Split / Process) */
}
.comp-card .zone-select,
.source-card.is-expanded .zone-select {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1.5px solid var(--border-strong);
  border-radius: var(--r-md);
  padding: 5px 9px 5px 10px;
  background: var(--card-bg);
  font-size: 12px;
  font-weight: 700;
  color: var(--text);
  cursor: pointer;
  max-width: 100%;
  transition: border-color 0.12s ease;
}
.comp-card .zone-select:hover,
.source-card.is-expanded .zone-select:hover {
  border-color: var(--accent);
}
.comp-card .zone-select i, .comp-card .zone-select svg,
.source-card.is-expanded .zone-select i,
.source-card.is-expanded .zone-select svg {
  width: 12px;
  height: 12px;
  color: var(--text-subtle);
  flex: 0 0 auto;
}
.comp-card .zone-select span,
.source-card.is-expanded .zone-select span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.comp-card,
.source-card.is-expanded {
  /* A pending REQUIRED choice (app-wide "red needs attention" rule). Today only
     armed for Process; the destination chip uses its own --unmapped variant. */
}
.comp-card .zone-select--needs,
.source-card.is-expanded .zone-select--needs {
  border-color: hsl(351, 75%, 78%);
  background: hsl(351, 80%, 98%);
  color: hsl(351, 64%, 43%);
}
.comp-card .zone-select--needs i, .comp-card .zone-select--needs svg,
.source-card.is-expanded .zone-select--needs i,
.source-card.is-expanded .zone-select--needs svg {
  color: hsl(351, 70%, 56%);
}
.comp-card .zone-select--needs:hover,
.source-card.is-expanded .zone-select--needs:hover {
  border-color: hsl(351, 70%, 62%);
}
.comp-card .zone-select-label,
.source-card.is-expanded .zone-select-label {
  font-size: 11px;
  color: var(--text-muted);
  margin-bottom: 4px;
}
.comp-card,
.source-card.is-expanded {
  /* The "Partition rows by" dropdown — Don't split + each split field. */
}
.comp-card .zone-select-wrap,
.source-card.is-expanded .zone-select-wrap {
  position: relative;
  display: inline-block;
  max-width: 100%;
}
.comp-card .zone-menu,
.source-card.is-expanded .zone-menu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 20;
  min-width: 180px;
  padding: 4px;
  background: var(--card-bg);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: 0 8px 24px rgba(15, 20, 32, 0.12), 0 1px 3px rgba(15, 20, 32, 0.06);
}
.comp-card .zone-menu__item,
.source-card.is-expanded .zone-menu__item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  padding: 6px 8px;
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  font-size: 12px;
  font-weight: 600;
  color: var(--text);
  text-align: left;
  cursor: pointer;
}
.comp-card .zone-menu__item:hover,
.source-card.is-expanded .zone-menu__item:hover {
  background: var(--view-bg);
  color: var(--accent);
}
.comp-card .zone-menu__count,
.source-card.is-expanded .zone-menu__count {
  font-size: 10px;
  font-weight: 700;
  color: var(--text-subtle);
  background: var(--view-bg);
  padding: 1px 7px;
  border-radius: 999px;
}
.comp-card .role-rows-label,
.source-card.is-expanded .role-rows-label {
  margin-top: 10px;
  font-size: 9.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-subtle);
}
.comp-card,
.source-card.is-expanded {
  /* Split — slice → Model routing rows */
}
.comp-card .slice-row,
.source-card.is-expanded .slice-row {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 5px 0;
  margin-top: 4px;
}
.comp-card .slice-row__name,
.source-card.is-expanded .slice-row__name {
  font-size: 11.5px;
  font-weight: 800;
  color: var(--text);
  /* Don't shrink the short left label ("All data") — the dest chip absorbs the
     row's shrinking. A genuinely long slice value still caps at 14ch + ellipsis. */
  flex: 0 0 auto;
  max-width: 14ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.comp-card .slice-row__arrow,
.source-card.is-expanded .slice-row__arrow {
  color: var(--text-subtle);
  display: inline-flex;
  flex: 0 0 auto;
  justify-content: center;
  min-width: 16px;
}
.comp-card .slice-row__arrow i, .comp-card .slice-row__arrow svg,
.source-card.is-expanded .slice-row__arrow i,
.source-card.is-expanded .slice-row__arrow svg {
  width: 11px;
  height: 11px;
}
.comp-card .slice-row__dest,
.source-card.is-expanded .slice-row__dest {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: var(--r-sm);
  font-size: 11px;
  font-weight: 700;
  border: 1px solid var(--border-strong);
  color: var(--text);
  background: var(--card-bg);
  /* Shrink (and ellipsize the name) before overflowing the column — a long
     Model name used to push the NEW badge out of the rounded edge. */
  flex: 1 1 auto;
  min-width: 0;
  max-width: 100%;
  cursor: pointer;
  transition: border-color 0.12s ease, color 0.12s ease;
}
.comp-card .slice-row__dest:hover,
.source-card.is-expanded .slice-row__dest:hover {
  border-color: var(--accent);
  color: var(--accent);
}
.comp-card .slice-row__dest i, .comp-card .slice-row__dest svg,
.source-card.is-expanded .slice-row__dest i,
.source-card.is-expanded .slice-row__dest svg {
  width: 12px;
  height: 12px;
  color: var(--text-muted);
  flex: 0 0 auto;
}
.comp-card .slice-row__dest-name,
.source-card.is-expanded .slice-row__dest-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* Italic labels ("Not included") overhang to the right; without this the
     final glyph's slant gets clipped by overflow: hidden. */
  padding-right: 2px;
}
.comp-card,
.source-card.is-expanded {
  /* "the Model you came from" — why a chip arrives pre-picked (WS25). */
}
.comp-card .slice-row__from,
.source-card.is-expanded .slice-row__from {
  font-size: 10.5px;
  font-weight: 600;
  font-style: italic;
  color: var(--text-subtle);
  white-space: nowrap;
  flex: 0 0 auto;
}
.comp-card .slice-row__new,
.source-card.is-expanded .slice-row__new {
  font-size: 9px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  background: hsl(38, 92%, 50%); /* amber — no amber token in v2 yet */
  color: #fff;
  padding: 1px 6px;
  border-radius: 999px;
  flex: 0 0 auto;
}
.comp-card,
.source-card.is-expanded {
  /* An explicitly-ignored slice — a RESOLVED choice (its data feeds no Model), so
     it reads calm/muted, never red like an unrouted slice. Still clickable to
     change one's mind (re-open the menu, pick a Model). */
}
.comp-card .slice-row--ignored .slice-row__name,
.source-card.is-expanded .slice-row--ignored .slice-row__name {
  color: var(--text-muted);
}
.comp-card,
.source-card.is-expanded {
  /* A merged group (Workshop 24): its values stack in the name cell behind a
     thin bracket, so the row still reads as ONE row with one destination. The
     per-value X is hover-revealed — popping a value out is a correction, not
     the row's main affordance. The Route step and the landed header share
     these; only the Route step renders the X. */
}
.comp-card .slice-row__names,
.source-card.is-expanded .slice-row__names {
  display: flex;
  flex-direction: column;
  gap: 3px;
  flex: 0 0 auto;
  max-width: 16ch;
  padding-left: 8px;
  border-left: 2px solid var(--border-strong);
}
.comp-card .slice-row__value,
.source-card.is-expanded .slice-row__value {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11.5px;
  font-weight: 800;
  color: var(--text);
  white-space: nowrap;
}
.comp-card .slice-row__value > span,
.source-card.is-expanded .slice-row__value > span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.comp-card .slice-row--ignored .slice-row__value,
.source-card.is-expanded .slice-row--ignored .slice-row__value {
  color: var(--text-muted);
}
.comp-card .slice-row__value-x,
.source-card.is-expanded .slice-row__value-x {
  appearance: none;
  border: 0;
  background: transparent;
  padding: 0;
  width: 14px;
  height: 14px;
  display: grid;
  place-items: center;
  border-radius: 999px;
  color: var(--text-subtle);
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.12s ease, background-color 0.12s ease, color 0.12s ease;
}
.comp-card .slice-row__value-x i, .comp-card .slice-row__value-x svg,
.source-card.is-expanded .slice-row__value-x i,
.source-card.is-expanded .slice-row__value-x svg {
  width: 10px;
  height: 10px;
}
.comp-card .slice-row__value-x:hover,
.source-card.is-expanded .slice-row__value-x:hover {
  background: hsl(351, 80%, 96%);
  color: hsl(351, 70%, 55%);
}
.comp-card .slice-row--merged:hover .slice-row__value-x,
.comp-card .slice-row__value-x:focus-visible,
.source-card.is-expanded .slice-row--merged:hover .slice-row__value-x,
.source-card.is-expanded .slice-row__value-x:focus-visible {
  opacity: 1;
}
.comp-card,
.source-card.is-expanded {
  /* the row the open Library picker is choosing for */
}
.comp-card .slice-row__dest--picking,
.source-card.is-expanded .slice-row__dest--picking {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--accent-bg);
  box-shadow: 0 0 0 2px var(--accent-bg-strong);
}
.comp-card .slice-row__dest--ignored,
.source-card.is-expanded .slice-row__dest--ignored {
  border-style: dashed;
  border-color: var(--border-strong);
  background: hsl(220, 14%, 96%);
  color: var(--text-muted);
  font-style: italic;
}
.comp-card .slice-row__dest--ignored i, .comp-card .slice-row__dest--ignored svg,
.source-card.is-expanded .slice-row__dest--ignored i,
.source-card.is-expanded .slice-row__dest--ignored svg {
  color: var(--text-muted);
}
.comp-card .slice-row__dest--ignored:hover,
.source-card.is-expanded .slice-row__dest--ignored:hover {
  border-color: var(--accent);
  color: var(--accent);
}
.comp-card .slice-row__dest--ignored:hover i, .comp-card .slice-row__dest--ignored:hover svg,
.source-card.is-expanded .slice-row__dest--ignored:hover i,
.source-card.is-expanded .slice-row__dest--ignored:hover svg {
  color: var(--accent);
}
.comp-card,
.source-card.is-expanded {
  /* The "Set Target" control: a relative wrapper anchoring the two-option menu
     (Pick a Model / Don't include slice) under the full-width dest button. */
}
.comp-card .slice-row__target,
.source-card.is-expanded .slice-row__target {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
}
.comp-card .slice-row__target > .slice-row__dest,
.source-card.is-expanded .slice-row__target > .slice-row__dest {
  width: 100%;
}
.comp-card .slice-target-menu,
.source-card.is-expanded .slice-target-menu {
  min-width: 200px;
}
.comp-card .slice-target-menu .zone-menu__item,
.source-card.is-expanded .slice-target-menu .zone-menu__item {
  justify-content: flex-start;
}
.comp-card .slice-target-menu,
.source-card.is-expanded .slice-target-menu {
  /* "Or merge into…" — the other groups, listed as destinations under the
     three standing choices. A section header, not a fourth verb. */
}
.comp-card .slice-target-menu .zone-menu__sep,
.source-card.is-expanded .slice-target-menu .zone-menu__sep {
  height: 1px;
  background: var(--border-soft);
  margin: 4px 2px;
}
.comp-card .slice-target-menu .zone-menu__head,
.source-card.is-expanded .slice-target-menu .zone-menu__head {
  padding: 6px 8px 3px;
  font-size: 9.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-subtle);
}
.comp-card .slice-target-menu .zone-menu__label,
.source-card.is-expanded .slice-target-menu .zone-menu__label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.comp-card .slice-target-menu .zone-menu__sub,
.source-card.is-expanded .slice-target-menu .zone-menu__sub {
  margin-left: auto;
  padding-left: 10px;
  font-size: 10.5px;
  font-weight: 600;
  font-style: italic;
  color: var(--text-subtle);
  white-space: nowrap;
}
.comp-card .slice-target-menu,
.source-card.is-expanded .slice-target-menu {
  /* the choice in force — the default reads as chosen, not as "nothing yet" */
}
.comp-card .slice-target-menu .zone-menu__item.is-current,
.source-card.is-expanded .slice-target-menu .zone-menu__item.is-current {
  color: var(--accent);
}
.comp-card .slice-target-menu .zone-menu__item.is-current i, .comp-card .slice-target-menu .zone-menu__item.is-current svg,
.source-card.is-expanded .slice-target-menu .zone-menu__item.is-current i,
.source-card.is-expanded .slice-target-menu .zone-menu__item.is-current svg {
  color: var(--accent);
}
.comp-card .slice-target-menu i, .comp-card .slice-target-menu svg,
.source-card.is-expanded .slice-target-menu i,
.source-card.is-expanded .slice-target-menu svg {
  width: 14px;
  height: 14px;
  color: var(--text-muted);
  flex: 0 0 auto;
}
.comp-card .slice-target-menu .zone-menu__item:hover i, .comp-card .slice-target-menu .zone-menu__item:hover svg,
.source-card.is-expanded .slice-target-menu .zone-menu__item:hover i,
.source-card.is-expanded .slice-target-menu .zone-menu__item:hover svg {
  color: var(--accent);
}

.comp-card__foot {
  padding: 10px var(--s-4);
  border-top: 1px solid var(--border-soft);
  font-size: 12px;
  color: var(--text-muted);
}
.comp-card__foot strong {
  color: var(--text);
  font-weight: 600;
}

.comp-card__foot--next {
  display: flex;
  align-items: center;
  gap: 7px;
  color: var(--accent);
}
.comp-card__foot--next i, .comp-card__foot--next svg {
  width: 14px;
  height: 14px;
}

/* The comprehension card in flight (Assemble choreography): reparented to the
   view root, absolutely positioned, animating top/left/width/height into the
   header slot while its body folds away (clipped by overflow, faded by
   .is-folding). See intakeFlow.runFlight(). */
.comp-card--flight {
  position: absolute;
  z-index: 50;
  margin: 0;
  overflow: hidden;
  transition: top 0.6s cubic-bezier(0.4, 0, 0.2, 1), left 0.6s cubic-bezier(0.4, 0, 0.2, 1), width 0.6s cubic-bezier(0.4, 0, 0.2, 1), height 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}

.comp-card--flight.is-folding .comp-card__steps,
.comp-card--flight.is-folding .comp-card__foot {
  opacity: 0;
  transition: opacity 0.3s ease;
}

/* ── Library panel — the Slice Model Picker (F6) ──────────────────────────
   A PUSHING panel, not an overlay: the intake canvas is a push-grid (mirrors
   .model-detail) whose second column is the full-height Library panel. Opening
   it narrows the Source view rather than covering it. One component, two entry
   points (openPicker context): the comprehension card's destination chip
   (pre-wand) and the landed Models-column +. Standalone for now; eventually
   reuses the home rail's Models tree (Decision 52). */
.intake-shell {
  position: relative; /* offset parent for the Assemble flight (reparents here) */
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: 1fr;
  gap: var(--s-3);
  height: 100%;
  min-height: 0;
}
.intake-shell.has-library-open {
  grid-template-columns: 1fr 340px;
}

.slice-row__caret {
  width: 14px;
  height: 14px;
  flex: 0 0 auto;
  color: var(--text-subtle);
}

/* The panel is the rail's twin — same surface chrome as .nav (view-bg, border,
   radius, layered shadow) so it reads as a right-hand rail. Its body renders the
   rail's own global .ws-card / .nav-item / .nav-action / .panel-add classes. */
.library-panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  background: var(--view-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: 0 6px 24px rgba(15, 20, 32, 0.06), 0 1px 3px rgba(15, 20, 32, 0.04);
}
.library-panel__header {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--s-4) var(--s-4) var(--s-3);
}
.library-panel__body {
  flex: 1 1 auto;
  padding-bottom: var(--s-2);
  overflow-y: auto;
}
.library-panel {
  /* Model rows route on click — give them the pointer the rail's <a> had. */
}
.library-panel .nav-item {
  cursor: pointer;
}
.library-panel {
  /* A Model already routed to another slice (Decision 87) — greyed, unclickable. */
}
.library-panel .nav-item--taken {
  cursor: not-allowed;
  opacity: 0.5;
}
.library-panel .nav-item--taken:hover {
  background: transparent;
}
.library-panel .nav-item__taken {
  margin-left: auto;
  font-size: 9px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-subtle);
}
.library-panel {
  /* The Model currently routed for this picker context — highlighted blue so a
     reopened picker shows what's already selected (with a trailing check). */
}
.library-panel .nav-item--selected {
  background: var(--accent-bg);
  color: var(--accent);
  font-weight: 600;
}
.library-panel .nav-item--selected:hover {
  background: var(--accent-bg-strong);
  color: var(--accent);
}
.library-panel .nav-item--selected .model-state-icon {
  color: var(--accent);
}
.library-panel .nav-item__selected {
  margin-left: auto;
  display: inline-flex;
  color: var(--accent);
}
.library-panel .nav-item__selected svg {
  width: 15px;
  height: 15px;
}

/* Which slice the picker is routing (Decision 83 context bar). */
.lib-context {
  flex: 0 0 auto;
  padding: 6px var(--s-4) 10px;
  font-size: 11.5px;
  color: var(--text-muted);
}
.lib-context strong {
  color: var(--accent);
  font-weight: 800;
}

.lib-loading {
  padding: var(--s-5);
  font-size: 13px;
  color: var(--text-subtle);
  text-align: center;
}

/* ── File-upload dropzone (Direct path entry) ──────────────────────────────── */
.dropzone__input {
  display: none;
}

.dropzone--over {
  border-color: var(--accent);
  background: var(--accent-bg);
}

/* ── The Review Card (Workshop 22) — the Comprehension Engine's third layer ───
   The detected channel + column mapping, posed as the machine's OPINION across
   four numbered steps. Ported from docs/redesign/22-review-card.html. Prefixed
   `review-card` / `rc-*` so it never collides with the flow-canvas `.step` /
   `.card-header` / `.field-row` names (this scene isn't `.flow-canvas`). */
/* Doubt + confidence color tiers — shared by the rc-step family on BOTH the
   Review card (Review screen) and the comp-card (Route screen, which reuses the
   numbered-step format). The flow-canvas defines its own, scoped separately. */
.review-card,
.comp-card {
  --rc-conf-green: hsl(142, 60%, 40%);
  --rc-conf-green-soft: hsl(142, 60%, 95%);
  --rc-conf-green-tag: hsl(142, 60%, 32%);
  --rc-rose: hsl(351, 70%, 55%);
  --rc-rose-soft: hsl(351, 80%, 97%);
  --rc-rose-edge: hsl(351, 75%, 85%);
  --rc-rose-tag: hsl(351, 64%, 43%);
  --rc-red: hsl(0, 72%, 51%);
  --rc-red-soft: hsl(0, 72%, 96%);
  --rc-red-edge: hsl(0, 72%, 83%);
  --rc-red-tag: hsl(0, 72%, 40%);
  --rc-amber: hsl(34, 90%, 38%);
  --rc-amber-soft: hsl(38, 90%, 94%);
}

.review-card {
  /* Stretch to the review column's width so a multi-tab stack is uniform; the
     parent flex container's gap spaces the cards (no per-card margin). */
  width: 100%;
  margin: 0;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  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);
  /* No overflow clip. The field/channel menus no longer need it (x-anchor
     positions them against the viewport), but the header already rounds its
     own top corners, so there's nothing for a clip to do. */
}

.review-card .rc-header {
  border-radius: var(--r-md) var(--r-md) 0 0;
}

/* A collapsed card (multi-tab stack, Decision 114) shows only its header. */
.review-card--collapsed .rc-header {
  border-bottom: none;
  border-radius: var(--r-md);
}

/* A reclassify round-trip in flight: the body dims and stops taking clicks (the
   component drops them anyway — reviewBusy guards _reclassify), and the header
   flag becomes a spinner, so the wait reads as work rather than a dead menu. */
.review-card--busy .rc-body {
  opacity: 0.55;
  pointer-events: none;
  transition: opacity 0.15s ease;
}

.rc-flag--busy {
  background: var(--view-bg);
  color: var(--text-muted);
}
.rc-flag--busy i {
  width: 12px;
  height: 12px;
  animation: rc-spin 1s linear infinite;
}

/* greyed card-header — channel title + rows meta + a state flag pill */
.rc-header {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: 10px var(--s-3);
  border-bottom: 1px solid var(--border-soft);
  background: hsl(220, 14%, 94%);
}

.rc-header__icon {
  display: inline-flex;
  flex-shrink: 0;
  color: var(--text-muted);
}
.rc-header__icon i {
  width: 18px;
  height: 18px;
}

.rc-header__text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1 1 auto;
}

.rc-header__title {
  display: flex;
  align-items: baseline;
  gap: 7px;
  min-width: 0;
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
  line-height: 1.3;
}

/* a small neutral type tag — "Sheet" / "File" — names what the card IS */
.rc-header__kind {
  flex-shrink: 0;
  padding: 1px 7px;
  border-radius: 999px;
  background: hsl(220, 14%, 88%);
  color: var(--text-muted);
  font-size: 10px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.rc-header__name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rc-header__meta {
  margin-top: 1px;
  font-size: 11px;
  color: var(--text-muted);
}

/* Multi-tab stack header (Decision 114): a click target with a sheet badge and a
   rotating chevron; a lone card's header is inert (no toggle class). */
.rc-header--toggle {
  cursor: pointer;
  user-select: none;
}

.rc-header--toggle:hover {
  background: hsl(220, 14%, 91%);
}

.rc-header__chev {
  display: inline-flex;
  flex-shrink: 0;
  color: var(--text-subtle);
}
.rc-header__chev i {
  width: 16px;
  height: 16px;
  transition: transform 0.15s ease;
}
.rc-header__chev i.is-open {
  transform: rotate(180deg);
}

/* the state flag — lives on the header (a collapsed card would still show it) */
.rc-flag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 10.5px;
  font-weight: 700;
  padding: 2px 8px;
  border-radius: 999px;
  flex-shrink: 0;
  text-transform: capitalize;
}
.rc-flag.is-red {
  background: var(--rc-red-soft);
  color: var(--rc-red-tag);
}
.rc-flag.is-rose {
  background: var(--rc-rose-soft);
  color: var(--rc-rose-tag);
}
.rc-flag.is-green {
  background: var(--rc-conf-green-soft);
  color: var(--rc-conf-green-tag);
}

/* a STEP — label-left / content-right; the number badge IS the attention router */
.rc-step {
  display: grid;
  grid-template-columns: 180px 1fr;
  gap: var(--s-2) var(--s-4);
  align-items: start;
  padding: var(--s-3);
  border-bottom: 1px solid var(--border-soft);
}
.rc-step:last-child {
  border-bottom: none;
}

.rc-step__label {
  margin: 0;
  padding-top: 2px;
  line-height: 1.5;
}

.rc-step__body {
  min-width: 0;
}

.rc-step__num {
  width: 18px;
  height: 18px;
  border-radius: 999px;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  vertical-align: middle;
  margin-right: 7px;
  font-size: 10.5px;
  font-weight: 800;
  background: hsl(220, 14%, 90%);
  color: var(--text-muted);
}

.rc-step__title {
  vertical-align: middle;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--text-subtle);
}

/* number-badge color carries the step state */
.rc-step--ok .rc-step__num {
  background: var(--rc-conf-green);
  color: white;
}

.rc-step--rose .rc-step__num {
  background: var(--rc-rose);
  color: white;
}

.rc-step--block .rc-step__num {
  background: var(--rc-red);
  color: white;
}

.rc-step--block .rc-step__title {
  color: var(--rc-red-tag);
}

/* helper line inside a step body (Route card). Goes red-pink when it's an
   essential instruction still waiting on the user (e.g. pick a destination). */
.rc-hint {
  margin: 10px 0 0;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--text-muted);
}

.rc-hint--needs {
  color: var(--rc-rose-tag);
  font-weight: 600;
}

/* the Route card's step bodies hold stacked controls — give them rhythm without
   touching the Review card's tighter step bodies. */
.comp-card .rc-step__body .zone-select-wrap + div,
.comp-card .rc-step__body .role-rows-label {
  margin-top: 10px;
}

/* the inert step button (Open Grid / View Data) */
.rc-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 7px var(--s-3);
  border-radius: var(--r-md);
  cursor: pointer;
  font: inherit;
  font-weight: 700;
  font-size: 12px;
  border: 1px solid var(--border-strong);
  background: white;
  color: var(--text);
  transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.rc-btn i {
  width: 14px;
  height: 14px;
}
.rc-btn:hover {
  border-color: var(--accent);
  color: var(--accent);
}
.rc-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  border-color: var(--border);
  color: var(--text-subtle);
}
.rc-btn:disabled:hover {
  border-color: var(--border);
  color: var(--text-subtle);
}

/* the channel picker (editable, slice 2) + its dropdown */
.rc-picker-wrap {
  position: relative;
}

.rc-picker {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  width: 100%;
  padding: 8px var(--s-3);
  border-radius: var(--r-md);
  cursor: pointer;
  background: hsl(220, 14%, 98%);
  border: 1px solid var(--border-strong);
  text-align: left;
  font: inherit;
  color: var(--text);
  transition: border-color 0.12s, background 0.12s;
}
.rc-picker:hover {
  border-color: var(--accent);
  background: var(--accent-bg);
}

/* dot picks up the channel hue (--ch set by the .ch-* class on the picker) */
.rc-picker__dot {
  width: 8px;
  height: 8px;
  border-radius: 999px;
  flex-shrink: 0;
  background: var(--ch, var(--rc-conf-green));
}

.rc-picker__name {
  font-weight: 700;
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rc-picker__chev {
  color: var(--text-subtle);
  display: inline-flex;
}
.rc-picker__chev i {
  width: 14px;
  height: 14px;
}

/* A Processed file's type is fixed (Transaction Log) — show it locked, not as a
   pickable menu (Decision 119c). */
.rc-picker--locked {
  cursor: default;
  background: hsl(220, 14%, 98%);
}
.rc-picker--locked:hover {
  border-color: var(--border-strong);
  background: hsl(220, 14%, 98%);
}

.rc-picker__lock {
  color: var(--text-subtle);
  display: inline-flex;
}
.rc-picker__lock i {
  width: 13px;
  height: 13px;
}

.rc-step--block .rc-picker {
  border-color: var(--rc-red-edge);
  background: var(--rc-red-soft);
}
.rc-step--block .rc-picker__dot {
  background: var(--rc-red);
}
.rc-step--block .rc-picker__name {
  color: var(--rc-red-tag);
}

/* the dropdown shared by the Channel picker (Step 2) and the field menus (Step 3) */
.rc-menu {
  position: absolute;
  z-index: 30;
  top: calc(100% + 4px);
  left: 0;
  min-width: 248px;
  max-height: 320px;
  overflow-y: auto;
  padding: var(--s-2);
  background: white;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  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);
}
.rc-menu--field {
  min-width: 200px;
}

.rc-menu__group + .rc-menu__group {
  margin-top: 4px;
  border-top: 1px solid var(--border-soft);
  padding-top: 4px;
}

.rc-menu__grouplabel {
  padding: 4px 8px 3px;
  font-size: 9.5px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-subtle);
}

.rc-menu__item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 7px 8px;
  border: none;
  border-radius: var(--r-sm);
  background: none;
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: var(--text);
  transition: background 0.1s;
}
.rc-menu__item:hover {
  background: var(--ch-bg, var(--accent-bg));
}

.rc-menu__dot {
  width: 8px;
  height: 8px;
  border-radius: 999px;
  flex-shrink: 0;
  background: var(--ch, var(--text-subtle));
}

.rc-menu__name {
  font-weight: 700;
  font-size: 12px;
  flex-shrink: 0;
}

.rc-menu__hint {
  font-size: 10.5px;
  color: var(--text-muted);
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rc-menu__check {
  width: 14px;
  height: 14px;
  color: var(--accent);
  flex-shrink: 0;
}

.rc-menu__item--field {
  justify-content: space-between;
  /* The two ROLE options ("Use for Grouping" / "Do Not Import") are their own
     section below the channel's fields: a hairline above the first, and each
     colored like the state it produces — accent = kept for grouping (the
     leftover chip's color), amber = dropped. */
}
.rc-menu__item--field.rc-menu__item--roles-start {
  position: relative;
  margin-top: 9px;
}
.rc-menu__item--field.rc-menu__item--roles-start::before {
  content: "";
  position: absolute;
  top: -5px;
  left: 4px;
  right: 4px;
  height: 1px;
  background: var(--border-soft);
}
.rc-menu__item--field.rc-menu__item--groupby .rc-menu__field {
  color: var(--accent);
}
.rc-menu__item--field {
  /* Not applicable (too many distinct values): the app's inactive grey, not a
     washed-out accent — a faded blue still reads as "available, just quiet". */
}
.rc-menu__item--field.rc-menu__item--groupby:disabled {
  opacity: 1;
}
.rc-menu__item--field.rc-menu__item--groupby:disabled .rc-menu__field, .rc-menu__item--field.rc-menu__item--groupby:disabled .rc-menu__memo {
  color: var(--text-subtle);
}
.rc-menu__item--field.rc-menu__item--noimport .rc-menu__field {
  color: var(--rc-amber);
}
.rc-menu__item--field.rc-menu__item--noimport:hover {
  background: var(--rc-amber-soft);
}
.rc-menu__item--field:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}
.rc-menu__item--field:disabled:hover {
  background: none;
}

.rc-menu__fieldwrap {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.rc-menu__field {
  font-size: 11.5px;
  font-weight: 700;
}

.rc-menu__memo {
  font-size: 10.5px;
  color: var(--text-muted);
  font-style: normal;
}

/* A required field with no column on it. Rose, matching the Step 3 badge and the
   card's "needs mapping" flag — red on this card is reserved for a dataset the
   machine could not classify at all. */
.rc-missing {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 10px 0 0;
  font-size: 11.5px;
  font-weight: 600;
  line-height: 1.45;
  color: var(--rc-rose-tag);
}
.rc-missing svg {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
}

.rc-reason {
  margin: var(--s-2) 0 0;
  font-size: 11.5px;
  color: var(--text-muted);
  line-height: 1.5;
}
.rc-reason--block {
  color: var(--rc-red-tag);
}
.rc-reason--empty {
  font-style: italic;
  color: var(--text-subtle);
}

/* the red/pink "torn" box (runner-up channels) */
/* Blue, not red: runner-up channels are information, not a problem to fix
   (Matt, 2026-09-04). Red on this card is reserved for what blocks Continue. */
.rc-torn {
  margin-top: var(--s-2);
  padding: var(--s-2) var(--s-3);
  background: var(--accent-bg);
  border: 1px solid hsl(214, 97%, 84%);
  border-radius: var(--r-md);
  font-size: 11.5px;
  color: var(--accent);
  display: flex;
  align-items: center;
  gap: 6px;
}
.rc-torn i {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
}

/* field-mapping rows — source name → arrow → NEUTRAL target chip (no channel hue) */
.rc-field-row {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: 7px 4px;
}

/* One mapping = its row plus any key-integrity lines beneath it; the divider sits
   between items so a note stays with the row it describes. */
.rc-field-item + .rc-field-item {
  border-top: 1px solid var(--border-soft);
}

/* Key integrity (comprehension.keys) — lines under an identifying field's row. */
.rc-key-count {
  margin: -3px 4px 7px;
  font-size: 10.5px;
  color: var(--text-muted);
}

.rc-key-note {
  margin: -2px 4px 8px;
  display: flex;
  align-items: flex-start;
  gap: 6px;
  font-size: 11px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--rc-amber);
}
.rc-key-note i, .rc-key-note svg {
  width: 12px;
  height: 12px;
  flex: 0 0 auto;
  margin-top: 1px;
}

.rc-key-note__text {
  flex: 1 1 auto;
  min-width: 0;
}

.rc-key-note--applied {
  color: var(--text-muted);
  font-weight: 500;
}

.rc-key-note__fix {
  flex: 0 0 auto;
  appearance: none;
  border: 1px solid currentColor;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--rc-amber);
  font: inherit;
  font-weight: 700;
  padding: 1px 7px;
  cursor: pointer;
  white-space: nowrap;
}
.rc-key-note__fix:hover:not(:disabled) {
  background: var(--rc-amber-soft);
}
.rc-key-note__fix:disabled {
  opacity: 0.5;
  cursor: default;
}

.rc-field-row__source {
  display: flex;
  flex-direction: column;
  flex: 0 1 auto;
  min-width: 0;
}

.rc-field-row__source-name {
  font-size: 11.5px;
  font-weight: 800;
  color: var(--text);
  line-height: 1.25;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rc-field-row__source-info {
  margin-top: 1px;
  font-size: 10.5px;
  color: var(--text-muted);
}

.rc-field-row__arrow {
  color: var(--text-subtle);
  flex: 1 1 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.rc-field-row__arrow i {
  width: 11px;
  height: 11px;
}

.rc-field-pick-wrap {
  position: relative;
  flex-shrink: 0;
}

.rc-field-target {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 8px 4px 7px;
  border-radius: var(--r-sm);
  font-size: 11px;
  font-weight: 700;
  flex-shrink: 0;
  border: 1px solid var(--border-strong);
  background: white;
  color: var(--text);
}
.rc-field-target code {
  font-size: 11px;
  color: inherit;
}
.rc-field-target {
  /* editable variant — a real reassignable column (Step 3, slice 2) */
}
.rc-field-target--editable {
  cursor: pointer;
  font: inherit;
  font-size: 11px;
  font-weight: 700;
  transition: border-color 0.12s, background 0.12s;
}
.rc-field-target--editable:hover {
  border-color: var(--accent);
  background: var(--accent-bg);
}
.rc-field-target--ignored code {
  color: var(--text-subtle);
  font-style: italic;
}
.rc-field-target--ignored .rc-field-target__dot {
  background: var(--border-strong);
}
.rc-field-target {
  /* leftover (unmapped, kept) — a Direct Split candidate. Reads as intentional,
     not forgotten: accent-tinted with a split glyph (Decision 118). */
}
.rc-field-target--leftover {
  border-color: var(--accent);
  border-style: dashed;
  background: var(--accent-bg);
}
.rc-field-target--leftover code {
  color: var(--accent);
}
.rc-field-target--leftover .rc-field-target__dot {
  background: var(--accent);
}

.rc-field-target__dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--text-subtle);
  flex-shrink: 0;
}

.rc-field-target__chev {
  display: inline-flex;
  color: var(--text-subtle);
}
.rc-field-target__chev i {
  width: 12px;
  height: 12px;
}

.rc-field-target__guess {
  width: 12px;
  height: 12px;
  color: var(--rc-rose);
  flex-shrink: 0;
}

.rc-field-target__leftover {
  width: 12px;
  height: 12px;
  color: var(--accent);
  flex-shrink: 0;
}

/* a still-guessed mapping (low confidence) gets a faint rose source name */
.rc-field-row--guess .rc-field-row__source-name {
  color: var(--rc-rose-tag);
}

/* ── Combine columns (Decision 120) ─────────────────────────────────────────
   A composite mapping row + the "Combine columns…" composer. */
.rc-field-row__source-name {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

.rc-combine-glyph {
  color: var(--accent);
  flex-shrink: 0;
}
.rc-combine-glyph i {
  width: 13px;
  height: 13px;
}

.rc-field-row--composite .rc-field-row__source-name {
  color: var(--accent);
}

.rc-uncombine {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border: none;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--text-subtle);
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
}
.rc-uncombine i {
  width: 14px;
  height: 14px;
}
.rc-uncombine:hover {
  background: var(--rc-rose-soft);
  color: var(--rc-rose-tag);
}

.rc-combine {
  margin-top: 10px;
}

.rc-btn--combine {
  border-style: dashed;
  color: var(--accent);
  border-color: var(--border-strong);
}

.rc-composer {
  margin-top: 4px;
  padding: var(--s-3);
  border: 1px dashed var(--accent);
  border-radius: var(--r-md);
  background: var(--accent-bg);
}

.rc-composer__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.rc-composer__title {
  font-size: 12px;
  font-weight: 800;
  color: var(--text);
}

.rc-composer__close {
  display: inline-flex;
  border: none;
  background: transparent;
  cursor: pointer;
  color: var(--text-subtle);
  padding: 2px;
  border-radius: var(--r-sm);
}
.rc-composer__close i {
  width: 14px;
  height: 14px;
}
.rc-composer__close:hover {
  color: var(--text);
}

.rc-composer__hint {
  margin: 4px 0 8px;
  font-size: 11px;
  color: var(--text-muted);
}

.rc-composer__cols {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.rc-composer__col {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 9px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: white;
  cursor: pointer;
  font: inherit;
  transition: border-color 0.12s, background 0.12s;
}
.rc-composer__col:hover {
  border-color: var(--accent);
}
.rc-composer__col.is-on {
  border-color: var(--accent);
  background: var(--accent);
}
.rc-composer__col.is-on .rc-composer__colname {
  color: white;
}
.rc-composer__col.is-on .rc-composer__colsample {
  color: rgba(255, 255, 255, 0.8);
}

.rc-composer__order {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: white;
  color: var(--accent);
  font-size: 10px;
  font-weight: 800;
  flex-shrink: 0;
}

.rc-composer__colname {
  font-size: 11.5px;
  font-weight: 700;
  color: var(--text);
}

.rc-composer__colsample {
  font-size: 10.5px;
  color: var(--text-subtle);
  max-width: 90px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rc-composer__controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-top: 10px;
}

.rc-composer__ctl {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.rc-composer__ctl-label {
  font-size: 11px;
  font-weight: 700;
  color: var(--text-muted);
}

.rc-composer__sep {
  width: 56px;
  text-align: center;
  padding: 5px 7px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: white;
  font: inherit;
  font-size: 12px;
}
.rc-composer__sep:focus {
  outline: none;
  border-color: var(--accent);
}

.rc-composer__preview {
  /* Label on top, the joined preview text below it (Matt's tweak). */
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  margin-top: 10px;
  padding: 7px 9px;
  background: white;
  border-radius: var(--r-sm);
  border: 1px solid var(--border-soft);
}
.rc-composer__preview code {
  font-size: 14px;
  color: var(--text);
}
.rc-composer__preview { /* ~20% larger than a field row */ }

.rc-composer__preview-label {
  font-size: 9.5px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-subtle);
}

.rc-composer__actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 12px;
}

.rc-composer__btn {
  padding: 6px 14px;
  font-size: 12px;
  gap: 6px;
}
.rc-composer__btn i {
  width: 14px;
  height: 14px;
}

/* footer lock note — only when a step blocks */
.rc-actions {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-3);
  background: hsl(220, 14%, 98%);
}

.rc-actions__lock {
  font-size: 11px;
  color: var(--rc-red-tag);
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
.rc-actions__lock i {
  width: 13px;
  height: 13px;
}

/* ============================================================================
   Review-card modals (WS22): Adjust Grid Rows (the raw grid) + View Data (the
   canonical output). Same scrim + surface as the confirm modal (the shared
   mixins), sized larger because they hold tables: the grid gets the most
   width, the preview a little less (Matt, 2026-09-04).
   ========================================================================= */
.rc-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: var(--s-5);
}

.rc-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);
  display: flex;
  flex-direction: column;
  width: min(960px, 100%);
  max-height: min(85vh, 900px);
  overflow: hidden;
}
.rc-modal--out {
  width: min(760px, 100%);
}

.rc-modal__head {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--border-soft);
}

.rc-modal__icon {
  display: inline-flex;
  color: var(--accent);
}
.rc-modal__icon i {
  width: 20px;
  height: 20px;
}

.rc-modal__title {
  flex: 1 1 auto;
  min-width: 0;
}
.rc-modal__title h3 {
  margin: 0;
  font-size: 14px;
  font-weight: 700;
  color: var(--text);
}
.rc-modal__title p {
  margin: 1px 0 0;
  font-size: 11.5px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rc-modal__body {
  padding: var(--s-4);
  overflow: auto;
  flex: 1 1 auto;
}

.rc-modal__loading {
  font-size: 12.5px;
  color: var(--text-muted);
  font-style: italic;
  margin: 0 0 var(--s-2);
  display: flex;
  align-items: center;
  gap: 6px;
}
.rc-modal__loading i {
  width: 14px;
  height: 14px;
  animation: rc-spin 1s linear infinite;
}

@keyframes rc-spin {
  to {
    transform: rotate(360deg);
  }
}
.rc-modal__error {
  font-size: 12.5px;
  color: var(--rc-red-tag);
  margin: 0;
}

/* Footer = the modal's note/legend on the left, the action row on the right,
   both on one baseline. */
.rc-modal__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: 10px var(--s-4);
  border-top: 1px solid var(--border-soft);
  background: hsl(220, 14%, 98%);
}

/* Inspect grid — clickable raw rows, role-tinted (noise / header / data) */
.rc-inspect-hint {
  margin: 0 0 var(--s-3);
  font-size: 11.5px;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  gap: 6px;
}
.rc-inspect-hint strong {
  color: var(--text);
  font-weight: 700;
}
.rc-inspect-hint i {
  width: 14px;
  height: 14px;
  color: var(--accent);
  flex-shrink: 0;
}

.rc-grid {
  border-collapse: collapse;
  width: 100%;
  font-size: 11.5px;
  transition: opacity 0.15s;
}
.rc-grid.is-busy {
  opacity: 0.45;
  pointer-events: none;
}
.rc-grid td {
  padding: 5px 8px;
  border: 1px solid var(--border-soft);
  white-space: nowrap;
  color: var(--text);
}

.rc-grid__num {
  color: var(--text-subtle);
  font-variant-numeric: tabular-nums;
  text-align: right;
  width: 1%;
  background: hsl(220, 14%, 98%);
}

.rc-grid__row--noise td {
  background: hsl(220, 14%, 95%);
  color: var(--text-subtle);
}

.rc-grid__row--data td {
  background: hsl(168, 50%, 97%);
}

.rc-grid__row--header td {
  background: var(--accent-bg);
  color: var(--text);
  font-weight: 700;
  border-color: var(--accent);
}

/* a non-header row is a candidate header on hover */
.rc-grid__row--noise,
.rc-grid__row--data {
  cursor: pointer;
}

.rc-grid__row--noise:hover td,
.rc-grid__row--data:hover td {
  outline: 1px solid var(--accent);
  outline-offset: -1px;
}

/* Period columns (the un-pivoted run): tinted down the whole grid; in the header
   row every cell is a column pick, so it gets the pointer and hover ring. */
.rc-grid td.rc-grid__cell--run {
  background: hsl(205, 85%, 91%);
}

.rc-grid__row--header td.rc-grid__cell--run {
  background: hsl(205, 80%, 78%);
  border-color: hsl(205, 60%, 58%);
}

.rc-grid__row--header td:not(.rc-grid__num) {
  cursor: pointer;
}

.rc-grid__row--header td:not(.rc-grid__num):hover {
  outline: 1px solid hsl(210, 60%, 55%);
  outline-offset: -1px;
}

.rc-grid-legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-4);
}

.rc-grid-run-toggle {
  border: none;
  background: none;
  padding: 0;
  font: inherit;
  font-size: 11px;
  font-weight: 600;
  color: var(--accent);
  cursor: pointer;
}
.rc-grid-run-toggle:hover {
  text-decoration: underline;
}
.rc-grid-run-toggle:disabled {
  opacity: 0.5;
  cursor: default;
}

.rc-legend-key {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  color: var(--text-muted);
}

.rc-legend-swatch {
  width: 13px;
  height: 13px;
  border-radius: 3px;
  border: 1px solid var(--border);
}
.rc-legend-swatch--noise {
  background: hsl(220, 14%, 95%);
}
.rc-legend-swatch--header {
  background: var(--accent-bg);
  border-color: var(--accent);
}
.rc-legend-swatch--data {
  background: hsl(168, 50%, 97%);
  border-color: hsl(168, 40%, 85%);
}
.rc-legend-swatch--run {
  background: hsl(205, 80%, 78%);
  border-color: hsl(205, 60%, 58%);
}

/* Step 1's account of the un-pivot, under Adjust Grid Rows */
.rc-unpivot {
  margin: var(--s-2) 0 0;
  display: flex;
  align-items: flex-start;
  gap: 6px;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--text-muted);
}
.rc-unpivot i {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  margin-top: 1px;
  color: hsl(210, 60%, 50%);
}
.rc-unpivot--note {
  padding-left: 20px;
  color: var(--text-subtle);
}
.rc-unpivot--off i {
  color: var(--text-subtle);
}

/* Preview — the clean canonical output table */
.rc-clean {
  border-collapse: collapse;
  width: 100%;
  font-size: 11.5px;
}
.rc-clean th, .rc-clean td {
  padding: 6px 10px;
  border: 1px solid var(--border-soft);
  text-align: left;
  white-space: nowrap;
}
.rc-clean thead th {
  background: hsl(220, 14%, 97%);
}
.rc-clean thead th code {
  font-size: 11px;
  font-weight: 700;
  color: var(--text);
}
.rc-clean tbody td {
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.rc-preview-note {
  margin: 0;
  font-size: 11px;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  gap: 6px;
}
.rc-preview-note i {
  width: 14px;
  height: 14px;
  color: var(--rc-conf-green);
  flex-shrink: 0;
}

/* ── Group By stack + Destinations (Workshop 26) ─────────────────────────
   The Route step's grouping control is a STACK of fields, each wearing its
   role ("Group into: Models" / "Group into: Projects"), and Destinations is
   the Library previewed: a Project header holding Model rows. Ported from
   docs/redesign/26-multi-dim-groups.html. */
.comp-card .gb-stack,
.source-card.is-expanded .gb-stack {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
}
.comp-card .gb-field-wrap,
.source-card.is-expanded .gb-field-wrap {
  display: flex;
  flex-direction: column;
  gap: 3px;
  max-width: 100%;
}
.comp-card .gb-field,
.source-card.is-expanded .gb-field {
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: 100%;
}
.comp-card,
.source-card.is-expanded {
  /* The split-customer caveat under a Group By field: the thin-sample line's
     voice (amber, small, information not a gate), muted grey when it touches
     under 1% of customers. */
}
.comp-card .gb-movers,
.source-card.is-expanded .gb-movers {
  font-size: 10.5px;
  font-weight: 600;
  color: var(--amber-solid);
  display: flex;
  align-items: flex-start;
  gap: 5px;
  max-width: 560px;
  padding-left: 2px;
}
.comp-card .gb-movers svg,
.source-card.is-expanded .gb-movers svg {
  width: 11px;
  height: 11px;
  color: currentColor;
  flex: 0 0 auto;
  margin-top: 1px;
}
.comp-card .gb-movers--muted,
.source-card.is-expanded .gb-movers--muted {
  color: var(--text-muted);
}
.comp-card .gb-chip,
.source-card.is-expanded .gb-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 150px;
  max-width: 260px;
  border: 1.5px solid var(--border-strong);
  border-radius: var(--r-md);
  padding: 5px 7px 5px 10px;
  background: var(--card-bg);
  font-size: 12px;
  font-weight: 700;
  color: var(--text);
}
.comp-card .gb-chip__label,
.source-card.is-expanded .gb-chip__label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.comp-card .gb-chip__ord,
.source-card.is-expanded .gb-chip__ord {
  width: 16px;
  height: 16px;
  border-radius: 999px;
  background: var(--view-bg);
  color: var(--text-muted);
  font-size: 10px;
  font-weight: 800;
  display: grid;
  place-items: center;
  flex: 0 0 auto;
}
.comp-card .gb-chip__x,
.source-card.is-expanded .gb-chip__x {
  appearance: none;
  border: 0;
  background: transparent;
  padding: 0;
  width: 16px;
  height: 16px;
  display: grid;
  place-items: center;
  border-radius: 999px;
  color: var(--text-subtle);
  cursor: pointer;
  margin-left: auto;
}
.comp-card .gb-chip__x svg,
.source-card.is-expanded .gb-chip__x svg {
  width: 11px;
  height: 11px;
}
.comp-card .gb-chip__x:hover,
.source-card.is-expanded .gb-chip__x:hover {
  background: var(--rc-rose-soft);
  color: var(--rc-rose);
}
.comp-card .gb-role,
.source-card.is-expanded .gb-role {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  font-weight: 700;
  border-radius: 999px;
  padding: 3px 9px;
  white-space: nowrap;
}
.comp-card .gb-role svg,
.source-card.is-expanded .gb-role svg {
  width: 11px;
  height: 11px;
}
.comp-card .gb-role--models,
.source-card.is-expanded .gb-role--models {
  color: var(--text-subtle);
  font-weight: 600;
  padding-left: 2px;
  padding-right: 2px;
}
.comp-card .gb-role--on,
.source-card.is-expanded .gb-role--on {
  border: 1px solid hsl(214, 60%, 80%);
  background: var(--accent-bg);
  color: var(--accent);
  padding-right: 4px;
}
.comp-card .gb-role--on .gb-chip__x,
.source-card.is-expanded .gb-role--on .gb-chip__x {
  color: var(--accent);
  opacity: 0.7;
  margin-left: 0;
}
.comp-card .gb-role--on .gb-chip__x:hover,
.source-card.is-expanded .gb-role--on .gb-chip__x:hover {
  opacity: 1;
  background: transparent;
}
.comp-card,
.source-card.is-expanded {
  /* the one legal "Group into Projects instead" — dashed until hovered */
}
.comp-card .gb-role--make,
.source-card.is-expanded .gb-role--make {
  appearance: none;
  border: 1px dashed var(--border-strong);
  background: var(--card-bg);
  color: var(--text-muted);
  cursor: pointer;
}
.comp-card .gb-role--make:hover,
.source-card.is-expanded .gb-role--make:hover {
  border-color: var(--accent);
  color: var(--accent);
  border-style: solid;
}
.comp-card,
.source-card.is-expanded {
  /* The landed header's read-only Group By summary: the Route step's chips and
     roles, without their controls, plus the result and any exceptions. */
}
.comp-card .gb-summary,
.source-card.is-expanded .gb-summary {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 2px;
}
.comp-card,
.source-card.is-expanded {
  /* One line per field: its name, then what it groups into. A field under a
     Projects field is indented — its Models live inside those Projects. In a
     narrow zone the role wraps under the name rather than cutting the name. */
}
.comp-card .gb-summary__field,
.source-card.is-expanded .gb-summary__field {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 6px;
}
.comp-card .gb-summary__field--child,
.source-card.is-expanded .gb-summary__field--child {
  padding-left: 22px;
}
.comp-card .gb-chip--static,
.source-card.is-expanded .gb-chip--static {
  min-width: 0;
  max-width: 100%;
}
.comp-card .gb-summary__role,
.source-card.is-expanded .gb-summary__role {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  white-space: nowrap;
}
.comp-card .gb-summary__arrow,
.source-card.is-expanded .gb-summary__arrow {
  width: 12px;
  height: 12px;
  color: var(--text-subtle);
  flex: 0 0 auto;
}
.comp-card .gb-summary__into,
.source-card.is-expanded .gb-summary__into {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  font-weight: 600;
  color: var(--text-subtle);
}
.comp-card .gb-summary__into svg,
.source-card.is-expanded .gb-summary__into svg {
  width: 11px;
  height: 11px;
}
.comp-card .gb-summary__into--projects,
.source-card.is-expanded .gb-summary__into--projects {
  color: var(--accent);
  font-weight: 700;
}
.comp-card .gb-summary__count,
.source-card.is-expanded .gb-summary__count {
  font-size: 11px;
  font-weight: 600;
  color: var(--text-muted);
  white-space: nowrap;
}
.comp-card .gb-summary__result,
.source-card.is-expanded .gb-summary__result {
  font-size: 11.5px;
  font-weight: 700;
  color: var(--text);
  margin-top: 2px;
}
.comp-card .gb-summary__note,
.source-card.is-expanded .gb-summary__note {
  font-size: 11px;
  color: var(--text-muted);
}
.comp-card .gb-summary__note--warn,
.source-card.is-expanded .gb-summary__note--warn {
  color: var(--amber-solid);
  font-weight: 600;
}
.comp-card .gb-add,
.source-card.is-expanded .gb-add {
  position: relative;
}
.comp-card .gb-add .zone-select--add,
.source-card.is-expanded .gb-add .zone-select--add {
  border-style: dashed;
  color: var(--text-muted);
}
.comp-card .gb-add .zone-select--add:hover,
.source-card.is-expanded .gb-add .zone-select--add:hover {
  border-color: var(--accent);
  color: var(--accent);
}
.comp-card .gb-add .zone-select svg:first-child,
.source-card.is-expanded .gb-add .zone-select svg:first-child {
  width: 12px;
  height: 12px;
}
.comp-card .gb-add .zone-menu,
.source-card.is-expanded .gb-add .zone-menu {
  min-width: 260px;
}
.comp-card .gb-add .zone-menu__item,
.source-card.is-expanded .gb-add .zone-menu__item {
  justify-content: flex-start;
}
.comp-card .gb-add .zone-menu__label,
.source-card.is-expanded .gb-add .zone-menu__label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.comp-card .gb-add .zone-menu__sub,
.source-card.is-expanded .gb-add .zone-menu__sub {
  margin-left: auto;
  padding-left: 10px;
  font-size: 10.5px;
  font-weight: 600;
  font-style: italic;
  color: var(--text-subtle);
  white-space: nowrap;
}
.comp-card .gb-add .zone-menu__empty,
.source-card.is-expanded .gb-add .zone-menu__empty {
  padding: 6px 8px;
  font-size: 11px;
  color: var(--text-subtle);
  font-style: italic;
}
.comp-card .dest-label,
.source-card.is-expanded .dest-label {
  margin-top: 14px;
  font-size: 9.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-subtle);
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
}
.comp-card .dest-label .muted,
.source-card.is-expanded .dest-label .muted {
  font-weight: 600;
  text-transform: none;
  letter-spacing: 0;
}
.comp-card .dest-project,
.source-card.is-expanded .dest-project {
  margin-top: 6px;
  border: 1px solid var(--border-soft);
  border-radius: var(--r-md);
}
.comp-card .dest-project__head,
.source-card.is-expanded .dest-project__head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 8px;
  background: hsl(220, 14%, 98%);
  border-radius: var(--r-md) var(--r-md) 0 0;
  position: relative;
  cursor: pointer;
  user-select: none;
}
.comp-card .dest-project__head:hover,
.source-card.is-expanded .dest-project__head:hover {
  background: hsl(220, 14%, 96%);
}
.comp-card .dest-project.is-collapsed .dest-project__head,
.source-card.is-expanded .dest-project.is-collapsed .dest-project__head {
  border-radius: var(--r-md);
}
.comp-card .dest-project__caret,
.source-card.is-expanded .dest-project__caret {
  margin-left: auto;
  width: 18px;
  height: 18px;
  display: grid;
  place-items: center;
  color: var(--text-subtle);
  flex: 0 0 auto;
}
.comp-card .dest-project__caret svg,
.source-card.is-expanded .dest-project__caret svg {
  width: 14px;
  height: 14px;
  transition: transform 0.12s ease;
}
.comp-card .dest-project.is-collapsed .dest-project__caret svg,
.source-card.is-expanded .dest-project.is-collapsed .dest-project__caret svg {
  transform: rotate(-90deg);
}
.comp-card .dest-project__chip,
.source-card.is-expanded .dest-project__chip {
  position: relative;
  display: flex;
  flex: 0 1 auto;
  min-width: 0;
  max-width: 62%;
  cursor: default;
}
.comp-card .dest-project__chip .slice-row__dest,
.source-card.is-expanded .dest-project__chip .slice-row__dest {
  font-size: 11.5px;
}
.comp-card .dest-project__facts,
.source-card.is-expanded .dest-project__facts {
  font-size: 11px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.comp-card .dest-project__body,
.source-card.is-expanded .dest-project__body {
  padding: 2px 8px 6px 12px;
}
.comp-card .dest-project.is-collapsed .dest-project__body,
.source-card.is-expanded .dest-project.is-collapsed .dest-project__body {
  display: none;
}
.comp-card .dest-project--ignored .dest-project__head,
.source-card.is-expanded .dest-project--ignored .dest-project__head {
  opacity: 0.6;
}
.comp-card .slice-row__dest--project svg:first-child,
.source-card.is-expanded .slice-row__dest--project svg:first-child {
  color: var(--accent);
}
.comp-card .slice-row__dest--project.slice-row__dest--ignored svg:first-child,
.source-card.is-expanded .slice-row__dest--project.slice-row__dest--ignored svg:first-child {
  color: var(--text-subtle);
}
.comp-card .slice-target-menu--project,
.source-card.is-expanded .slice-target-menu--project {
  min-width: 250px;
}
.comp-card,
.source-card.is-expanded {
  /* the row lead: the Model's own part, its counts beneath (customers first) */
}
.comp-card .slice-row__lead,
.source-card.is-expanded .slice-row__lead {
  display: flex;
  flex-direction: column;
  gap: 1px;
  flex: 0 0 auto;
  max-width: 210px;
  min-width: 0;
}
.comp-card .slice-row__lead .slice-row__name,
.source-card.is-expanded .slice-row__lead .slice-row__name {
  max-width: 100%;
}
.comp-card .slice-row__lead .slice-row__names,
.source-card.is-expanded .slice-row__lead .slice-row__names {
  max-width: 100%;
}
.comp-card .slice-row__count,
.source-card.is-expanded .slice-row__count {
  font-size: 10.5px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.comp-card,
.source-card.is-expanded {
  /* "Sample set is small for this grouping (19 customers)" — its own line */
}
.comp-card .slice-row__thin,
.source-card.is-expanded .slice-row__thin {
  margin: -2px 0 4px 0;
  font-size: 10.5px;
  font-weight: 600;
  color: var(--amber-solid);
  display: flex;
  align-items: center;
  gap: 5px;
}
.comp-card .slice-row__thin svg,
.source-card.is-expanded .slice-row__thin svg {
  width: 11px;
  height: 11px;
  color: var(--amber-solid);
  flex: 0 0 auto;
}
.comp-card .dest-project__body .slice-row,
.source-card.is-expanded .dest-project__body .slice-row {
  margin-top: 0;
}
.comp-card,
.source-card.is-expanded {
  /* Grouped rows: the chips right-align at one width so a Project's Models
     read as a column; the arrow takes the slack and centers on it (WS26). */
}
.comp-card .slice-row__lead ~ .slice-row__arrow,
.source-card.is-expanded .slice-row__lead ~ .slice-row__arrow {
  flex: 1 1 auto;
}
.comp-card .slice-row__lead ~ .slice-row__target,
.source-card.is-expanded .slice-row__lead ~ .slice-row__target {
  flex: 0 0 auto;
  width: 320px;
}
.comp-card .dest-project__body .slice-row + .slice-row,
.comp-card .dest-project__body div + div > .slice-row,
.source-card.is-expanded .dest-project__body .slice-row + .slice-row,
.source-card.is-expanded .dest-project__body div + div > .slice-row {
  border-top: 1px solid var(--border-soft);
}

/* The Library panel choosing a PROJECT (WS26): the cards are the picks. */
.ws-card--pickable .ws-card-header {
  cursor: pointer;
}

.ws-card--pickable .ws-card-header:hover {
  background: var(--accent-bg);
}

.ws-card--pickable .ws-card-header.is-selected {
  color: var(--accent);
}

.ws-card--pickable .ws-card-header .nav-item__selected {
  margin-left: 4px;
}

/* Spin for the Model card's "Computing…" build-state pill (G2 Phase 3). */
@keyframes model-status-spin {
  to {
    transform: rotate(360deg);
  }
}
/* =====================================================================
   THE FLOW VIEW — the assembled Data Source (Workshop 19 / 20)

   The landed state of the intake: the Source card becomes the header
   strip, pouring (vertical = "produces") into a three-column canvas
   (horizontal = "routes"): Datasets → Import Channels → Target Models,
   joined by SVG tube-map ribbons.

   Ported from docs/redesign/19-flow-view.html. Everything is scoped under
   `.flow-canvas` so the workshop's generic names (.card-header, the flow
   grid, .field-row …) never collide with the global design system — in
   particular the report-comparison `.canvas-grid` in _views.scss. The grid
   structural classes are renamed flow-* for the same reason. `.section-card`
   is the one deliberately shared primitive (the card frame).
   ===================================================================== */
/* ---- channel-type palette: identity hue per Import Channel (Decision 40).
   Global tokens (promoted to :root) so every surface that paints a channel —
   the Flow View AND the Comprehension review card's channel/field menus — reads
   one palette. Full 10 even though the Stripe path uses 3 (forward-looking). ---- */
:root {
  --ch-cohort: hsl(214, 72%, 50%);
  --ch-cohort-bg: hsl(214, 72%, 94%);
  --ch-cohort-soft: hsla(214, 72%, 50%, 0.22);
  --ch-fts: hsl(190, 80%, 42%);
  --ch-fts-bg: hsl(190, 80%, 94%);
  --ch-fts-soft: hsla(190, 80%, 42%, 0.22);
  --ch-renewal: hsl(168, 62%, 36%);
  --ch-renewal-bg: hsl(168, 62%, 94%);
  --ch-renewal-soft: hsla(168, 62%, 36%, 0.22);
  --ch-pricing: hsl(266, 60%, 56%);
  --ch-pricing-bg: hsl(266, 60%, 94%);
  --ch-pricing-soft: hsla(266, 60%, 56%, 0.22);
  --ch-retadj: hsl(238, 52%, 58%);
  --ch-retadj-bg: hsl(238, 52%, 95%);
  --ch-retadj-soft: hsla(238, 52%, 58%, 0.22);
  --ch-subadj: hsl(288, 46%, 52%);
  --ch-subadj-bg: hsl(288, 46%, 95%);
  --ch-subadj-soft: hsla(288, 46%, 52%, 0.22);
  --ch-reactadj: hsl(330, 62%, 54%);
  --ch-reactadj-bg: hsl(330, 62%, 95%);
  --ch-reactadj-soft: hsla(330, 62%, 54%, 0.22);
  --ch-reacthist: hsl(310, 52%, 50%);
  --ch-reacthist-bg: hsl(310, 52%, 95%);
  --ch-reacthist-soft: hsla(310, 52%, 50%, 0.22);
  --ch-revenue: hsl(80, 55%, 38%);
  --ch-revenue-bg: hsl(80, 45%, 92%);
  --ch-revenue-soft: hsla(80, 55%, 38%, 0.22);
  --ch-revadj: hsl(24, 46%, 42%);
  --ch-revadj-bg: hsl(24, 46%, 92%);
  --ch-revadj-soft: hsla(24, 46%, 42%, 0.22);
}

.flow-canvas {
  /* ---- build-state + write-pill tokens (Model status; Decision 36/39) ---- */
  --conf-green: hsl(142, 60%, 40%);
  --conf-green-tag: hsl(142, 60%, 32%);
  --conf-red: hsl(0, 72%, 51%);
  --conf-red-tag: hsl(0, 72%, 40%);
  --amber-solid: hsl(38, 92%, 48%);
  --amber-solid-hover: hsl(38, 92%, 42%);
  --write-rest-bg: hsl(220, 14%, 96%);
  --write-rest-text: var(--text-muted);
  display: flex;
  flex-direction: column;
  flex: 1 1 auto; /* fill the flex-column .view */
  min-height: 0;
  overflow-y: auto;
  background: white;
}

/* The channel class sets the active hue triad consumed by rails, pills,
   ribbons, and block tints downstream. Global (not nested under .flow-canvas) so
   the review card's channel/field menus share them — they only set custom
   properties, so they paint nothing on their own. */
.ch-cohort {
  --ch: var(--ch-cohort);
  --ch-bg: var(--ch-cohort-bg);
  --ch-soft: var(--ch-cohort-soft);
}

.ch-fts {
  --ch: var(--ch-fts);
  --ch-bg: var(--ch-fts-bg);
  --ch-soft: var(--ch-fts-soft);
}

.ch-renewal {
  --ch: var(--ch-renewal);
  --ch-bg: var(--ch-renewal-bg);
  --ch-soft: var(--ch-renewal-soft);
}

.ch-pricing {
  --ch: var(--ch-pricing);
  --ch-bg: var(--ch-pricing-bg);
  --ch-soft: var(--ch-pricing-soft);
}

.ch-retadj {
  --ch: var(--ch-retadj);
  --ch-bg: var(--ch-retadj-bg);
  --ch-soft: var(--ch-retadj-soft);
}

.ch-subadj {
  --ch: var(--ch-subadj);
  --ch-bg: var(--ch-subadj-bg);
  --ch-soft: var(--ch-subadj-soft);
}

.ch-reactadj {
  --ch: var(--ch-reactadj);
  --ch-bg: var(--ch-reactadj-bg);
  --ch-soft: var(--ch-reactadj-soft);
}

.ch-reacthist {
  --ch: var(--ch-reacthist);
  --ch-bg: var(--ch-reacthist-bg);
  --ch-soft: var(--ch-reacthist-soft);
}

.ch-revenue {
  --ch: var(--ch-revenue);
  --ch-bg: var(--ch-revenue-bg);
  --ch-soft: var(--ch-revenue-soft);
}

.ch-revadj {
  --ch: var(--ch-revadj);
  --ch-bg: var(--ch-revadj-bg);
  --ch-soft: var(--ch-revadj-soft);
}

/* The 3-track template the strip + headers + grid all share so the columns
   stay aligned across the seam. */
/* ================================================================== */
/*  SOURCE STRIP — the Source card IS the header (Decision 66)         */
/* ================================================================== */
.flow-canvas .source-strip {
  display: grid;
  grid-template-columns: minmax(280px, 1fr) minmax(220px, 1.1fr) minmax(280px, 1fr);
  gap: 0;
  padding: var(--s-4) var(--s-5) 0;
  background: white;
  align-items: start;
  position: relative;
  z-index: 3; /* the down-tail crosses over the flow-body below */
  flex: 0 0 auto;
}
.flow-canvas .source-strip__cell--card {
  padding-right: var(--s-5);
  position: relative;
}
.flow-canvas {
  /* Expanded header: the card grows across datasets+channels so the read-only
     anatomy's three zones have room; actions hold their place at column 3. */
}
.flow-canvas .source-strip__cell--card.is-card-expanded {
  grid-column: 1/3;
}
.flow-canvas {
  /* ...and once expanded the strip stops pretending to be the canvas's 3-track
     grid: the card takes every pixel the actions don't need, so the anatomy's
     sentences ("NEW CUSTOMERS after 12 months away") get to stay sentences
     instead of wrapping mid-phrase. The canvas below keeps its own tracks; the
     only thing that has to stay aligned across the seam is the down-tail, which
     is anchored to the strip rather than to this cell. */
}
.flow-canvas .source-strip--expanded {
  grid-template-columns: 1fr auto;
}
.flow-canvas .source-strip--expanded .source-strip__cell--card.is-card-expanded {
  grid-column: 1;
}
.flow-canvas .source-strip--expanded .source-strip__cell--actions {
  grid-column: 2;
}
.flow-canvas .source-strip__cell--actions {
  grid-column: 3;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s-3);
  padding-left: var(--s-5);
  /* top-justified so actions hold position if the card later expands */
  align-self: start;
  padding-top: 7px;
}
.flow-canvas {
  /* The Source card. Accent border = the flow's anchor object (it carries the
     Flow's name; exactly one per flow). Collapsed = header only. */
}
.flow-canvas .source-card {
  border-color: var(--accent);
  box-shadow: 0 1px 1px rgba(15, 20, 32, 0.06), 0 4px 10px hsla(214, 97%, 50%, 0.14), 0 10px 22px hsla(214, 97%, 50%, 0.1);
}
.flow-canvas .source-card .card-header {
  padding-right: var(--s-3);
  background: hsl(220, 14%, 94%);
}
.flow-canvas .source-card .card-header__icon {
  color: var(--accent);
}
.flow-canvas {
  /* The source name is the page title — sized to the view-header title token. */
}
.flow-canvas .source-card .card-header__title {
  font-size: 20px;
  letter-spacing: -0.01em;
}
.flow-canvas .source-card.is-collapsed .card-header {
  border-bottom: none;
}
.flow-canvas {
  /* Collapsed = a header strip that hugs its content (meta stays one line),
     left-aligned in its cell; expanded, it spans columns 1-2 (rule above). */
}
.flow-canvas .source-strip__cell--card:not(.is-card-expanded) {
  justify-self: start;
}
.flow-canvas .source-card.is-collapsed {
  width: max-content;
  max-width: 100%;
}
.flow-canvas .source-card.is-collapsed .card-header__meta {
  white-space: nowrap;
}
.flow-canvas {
  /* Click anywhere on the header to expand/collapse (no chevron carat). */
}
.flow-canvas .source-card__header--clickable {
  cursor: pointer;
}
.flow-canvas .source-card__header--clickable:hover {
  background: hsl(220, 14%, 91%);
}
.flow-canvas {
  /* The name + its hover-rename pencil share one row; meta sits below. */
}
.flow-canvas .source-card__name-row {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
.flow-canvas {
  /* Inline rename editor on the landed header — matches the large title size. */
}
.flow-canvas .source-card__name-input {
  font: inherit;
  font-size: 20px;
  letter-spacing: -0.01em;
  font-weight: 700;
  color: var(--text);
  max-width: 360px;
  width: 100%;
  padding: 1px var(--s-2);
  border: 1px solid var(--accent);
  border-radius: var(--r-sm);
  background: var(--surface);
}
.flow-canvas {
  /* Rename is a quiet hover affordance right beside the name. */
}
.flow-canvas .source-card__rename {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  color: var(--text-subtle);
  border-radius: var(--r-sm);
  opacity: 0;
  transition: opacity 120ms ease, color 120ms ease, background 120ms ease;
}
.flow-canvas .source-card__rename:hover {
  color: var(--accent);
  background: rgba(0, 0, 0, 0.05);
}
.flow-canvas .source-card__rename svg {
  width: 14px;
  height: 14px;
}
.flow-canvas .source-card:hover .source-card__rename,
.flow-canvas .source-card__rename:focus-visible {
  opacity: 1;
}
.flow-canvas .source-card__actions {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-left: auto;
  flex-shrink: 0;
}
.flow-canvas .source-card__actions .icon-btn-large {
  width: 30px;
  height: 30px;
}
.flow-canvas .source-card__actions .icon-btn-large svg {
  width: 15px;
  height: 15px;
}
.flow-canvas {
  /* Down-tail: the source pours into its dataset stack. Neutral gray (the
     source→dataset edge is "produces", not a typed channel). Sized to match
     the SVG ribbons: 14px bar + 16px arrowhead.

     Anchored to the STRIP rather than to the card, so it lands over the canvas's
     Datasets column at every card width — collapsed, expanded, or expanded to
     fill. The strip and .flow-body share the same var(--s-5) side padding, and
     Datasets is the first track of 1 : 1.1 : 1, so its centre is 0.5/3.1 of the
     padded width. (While the card is COLLAPSED it hugs its content and can be
     narrower than that, which is fine — the tail belongs to the column, not to
     the card.) */
}
.flow-canvas .source-tail {
  position: absolute;
  left: calc(var(--s-5) + (100% - 2 * var(--s-5)) * 0.1613);
  top: 100%;
  width: 14px;
  height: 58px;
  margin-left: -7px;
  background: var(--text-subtle);
  opacity: 0.4;
}
.flow-canvas .source-tail::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border-left: 7px solid transparent;
  border-right: 7px solid transparent;
  border-top: 16px solid var(--text-subtle);
}

/* ================================================================== */
/*  CANVAS — Datasets | Channels | Models + the ribbon zone            */
/* ================================================================== */
.flow-canvas .flow-body {
  padding: var(--s-5);
  background: white;
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
}
.flow-canvas {
  /* Full-width error line above the canvas grid — the landed view's surface
     for Write / unroute / ribbon / reslice failures (they set `error`, which
     otherwise displays nowhere on this scene). */
}
.flow-canvas .flow-error-banner {
  font-size: 13px;
  padding-bottom: var(--s-3);
}
.flow-canvas .flow-headers {
  display: grid;
  grid-template-columns: minmax(280px, 1fr) minmax(220px, 1.1fr) minmax(280px, 1fr);
  gap: 0;
  padding-bottom: var(--s-3);
}
.flow-canvas .flow-header {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-subtle);
  min-height: 40px;
}
.flow-canvas .flow-header__text {
  flex: 0 0 auto;
}
.flow-canvas .flow-header__spacer {
  flex: 1 1 auto;
}
.flow-canvas .flow-header--datasets {
  padding: 0 var(--s-5) 0 var(--s-1);
}
.flow-canvas .flow-header--channels {
  padding: 0 var(--s-3);
  justify-content: center;
}
.flow-canvas .flow-header--models {
  padding: 0 var(--s-1) 0 var(--s-5);
}
.flow-canvas .flow-grid {
  display: grid;
  grid-template-columns: minmax(280px, 1fr) minmax(220px, 1.1fr) minmax(280px, 1fr);
  gap: 0;
  position: relative;
}
.flow-canvas .flow-col {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  min-width: 0;
  position: relative;
  z-index: 2; /* above ribbons */
  /* Release empty-space hits so the ribbon strokes underneath can take the
     pointer (mid-ribbon delete); the cards/pills re-enable themselves. */
  pointer-events: none;
}
.flow-canvas .flow-col > * {
  pointer-events: auto;
}
.flow-canvas .flow-col--datasets {
  padding-right: var(--s-5);
}
.flow-canvas .flow-col--models {
  padding-left: var(--s-5);
  gap: var(--s-5);
}
.flow-canvas .flow-col--channels {
  padding: 0 var(--s-3);
  align-items: center;
  z-index: 2;
}
.flow-canvas {
  /* SVG ribbon layer — fills the grid, sits behind the cards (filled by the
     ribbon engine in a later slice; the empty SVG is harmless until then). */
}
.flow-canvas .ribbon-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 1;
}
.flow-canvas .ribbon-layer svg {
  width: 100%;
  height: 100%;
  display: block;
  overflow: visible;
}
.flow-canvas .ribbon-svg {
  filter: drop-shadow(0 2px 3px rgba(15, 20, 32, 0.2));
}
.flow-canvas .ribbon-path {
  fill: none;
  stroke: var(--ch);
  stroke-width: 14;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.45;
}
.flow-canvas .ribbon-path--out {
  stroke-linecap: butt;
}
.flow-canvas .ribbon-arrow {
  fill: var(--ch);
  opacity: 0.45;
}
.flow-canvas {
  /* Mid-ribbon delete: outflow ribbons take the pointer on their stroke (the
     layer itself stays pointer-transparent), brighten on hover, and float the
     delete pill at their midpoint. Requires the flow columns to release
     empty-space hits — see the .flow-col pointer-events rules below. */
}
.flow-canvas .ribbon-path--out {
  pointer-events: stroke;
  cursor: pointer;
  transition: opacity 0.12s ease;
}
.flow-canvas .ribbon-path--out.is-hot {
  opacity: 0.75;
}
.flow-canvas {
  /* The WS18 problem trace: a ribbon into a colliding table runs red, so the
     eye follows the channel to the block that names the other Source. */
}
.flow-canvas .ribbon-path.is-collision {
  stroke: var(--conf-red);
  opacity: 0.55;
}
.flow-canvas .ribbon-path--out.is-collision.is-hot {
  opacity: 0.8;
}
.flow-canvas .ribbon-arrow.is-collision {
  fill: var(--conf-red);
  opacity: 0.7;
}
.flow-canvas .ribbon-delete-pill {
  appearance: none;
  border: none;
  padding: 0;
  cursor: pointer;
  position: absolute;
  left: var(--pill-x);
  top: var(--pill-y);
  transform: translate(-50%, -50%);
  z-index: 3; /* above the columns, so it never hides under a card edge */
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 2px solid var(--ch);
  background: white;
  color: var(--text-muted);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 6px rgba(15, 20, 32, 0.22);
  transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.flow-canvas .ribbon-delete-pill svg {
  width: 13px;
  height: 13px;
}
.flow-canvas .ribbon-delete-pill:hover {
  background: hsl(0, 70%, 95%);
  color: hsl(0, 65%, 45%);
  border-color: hsl(0, 65%, 45%);
}
.flow-canvas .ribbon-delete-pill:disabled {
  opacity: 0.4;
  cursor: default;
}
.flow-canvas {
  /* Drag-to-Model rubber-band (Decision 43): a thin marching-ants line in the
     channel hue while dragging — deliberately NOT tube-map, so a transient
     gesture reads as not-yet-real until it settles into a committed ribbon. */
}
.flow-canvas .ribbon-rubber {
  fill: none;
  stroke: var(--ch);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-dasharray: 7 6;
  opacity: 0.95;
  animation: ribbon-march 0.5s linear infinite;
}

@keyframes ribbon-march {
  to {
    stroke-dashoffset: -13;
  }
}
.flow-canvas {
  /* Drop targeting while a ribbon is being dragged. Valid Models lift; the one
     under the cursor is hot; a Model already fed by this channel recedes (a
     channel feeds a Model once, Decision 43). */
}
.flow-canvas .is-dragging-ribbon {
  cursor: grabbing;
  user-select: none;
}
.flow-canvas .model-card.is-drop-target {
  box-shadow: 0 0 0 1.5px var(--accent-soft, hsl(214, 90%, 80%));
}
.flow-canvas .model-card.is-drop-hot {
  box-shadow: 0 0 0 2.5px var(--accent);
  transform: translateY(-1px);
}
.flow-canvas .model-card.is-drop-fed {
  opacity: 0.45;
}

/* ================================================================== */
/*  CARDS — shared .section-card frame, flow-specific anatomy          */
/* ================================================================== */
.flow-canvas .section-card {
  overflow: hidden;
}
.flow-canvas .card-header {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: 10px var(--s-3) 9px;
  padding-right: 32px;
  border-bottom: 1px solid var(--border-soft);
  background: hsl(220, 14%, 94%);
}
.flow-canvas .card-header__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  color: var(--text-muted);
}
.flow-canvas .card-header__icon svg {
  width: 18px;
  height: 18px;
  stroke-width: 2.25;
}
.flow-canvas .card-header__text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1 1 auto;
}
.flow-canvas .card-header__title {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: -0.005em;
  color: var(--text);
  line-height: 1.3;
}
.flow-canvas .card-header__meta {
  margin-top: 1px;
  font-size: 11px;
  color: var(--text-muted);
  line-height: 1.45;
}
.flow-canvas .card-header__meta strong {
  color: var(--text);
  font-weight: 600;
}
.flow-canvas {
  /* E4: the collapse-proof "· Pulling… 45%" cue in the meta line. */
}
.flow-canvas .card-header__pulling {
  color: var(--accent);
  font-weight: 600;
}
.flow-canvas .card-section {
  padding: 9px var(--s-3);
  border-bottom: 1px solid var(--border-soft);
}
.flow-canvas .card-section:last-child {
  border-bottom: none;
}
.flow-canvas .card-section__label {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-subtle);
  margin-bottom: 6px;
}
.flow-canvas {
  /* Two-ended section header: "Fields" left, "Maps to" right (over the
     exact-match badge / target-chip column). */
}
.flow-canvas .card-section__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2);
}
.flow-canvas .card-section__head .card-section__label {
  margin-bottom: 6px;
}

/* ================================================================== */
/*  FIELD ROWS — Dataset card (source col → channel-tinted target)     */
/* ================================================================== */
.flow-canvas .field-row {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: 6px 0 6px 4px;
  position: relative;
  /* channel tint fading in from the right rail; unmapped rows have no
     --ch-bg, so the fallback leaves them clean */
  margin-right: calc(-1 * var(--s-3));
  padding-right: var(--s-3);
  background: linear-gradient(to left, var(--ch-bg, transparent) 0%, transparent 60%);
}
.flow-canvas .field-row + .field-row {
  border-top: 1px solid var(--border-soft);
}
.flow-canvas {
  /* right-edge colored rail the ribbon visually attaches to */
}
.flow-canvas .field-row::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  right: -1.5px;
  width: 4px;
  background: var(--ch, transparent);
}
.flow-canvas .field-row__source {
  display: flex;
  flex-direction: column;
  flex: 0 1 auto;
  min-width: 0;
}
.flow-canvas .field-row__source-name {
  font-size: 11.5px;
  font-weight: 800;
  color: var(--text);
  line-height: 1.25;
}
.flow-canvas .field-row__source-info {
  margin-top: 1px;
  font-size: 10.5px;
  color: var(--text-muted);
  line-height: 1.4;
}
.flow-canvas .field-row__arrow {
  color: var(--text-subtle);
  flex: 1 1 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.flow-canvas .field-row__arrow svg {
  width: 11px;
  height: 11px;
  stroke-width: 2.5;
}
.flow-canvas .field-row__target {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px 3px 7px;
  border-radius: var(--r-sm);
  font-size: 11px;
  font-weight: 600;
  flex-shrink: 0;
  border: 1px solid var(--ch);
  background: var(--ch-bg);
  color: var(--ch);
  /* extend past the card edge so the ribbon latches onto the chip */
  margin-right: calc(-1 * var(--s-3) - 1.5px);
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}
.flow-canvas .field-row__target-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  flex-shrink: 0;
  background: var(--ch);
}
.flow-canvas {
  /* "canonical" badge — replaces the X → X mapping for a born-canonical
     (Processed) field. A white status pill in the channel hue, distinct from
     the square target chip so it reads as "already native", not a destination.
     The channel-tinted rail still flows right, so the ribbon attaches as usual. */
}
.flow-canvas .field-row__badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-left: auto;
  flex-shrink: 0;
  padding: 2px 9px 2px 6px;
  border-radius: 999px;
  background: white;
  border: 1px solid var(--ch);
  color: var(--ch);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.01em;
}
.flow-canvas .field-row__badge svg {
  width: 10px;
  height: 10px;
  stroke-width: 3;
}

/* ================================================================== */
/*  CHANNEL PILL — the conduit identity                                */
/* ================================================================== */
.flow-canvas .channel-pill-anchor {
  display: flex;
}
.flow-canvas .channel-pill {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  padding: 7px 14px 8px;
  border-radius: var(--r-md);
  border: 1.5px solid var(--ch);
  background: var(--ch-bg);
  box-shadow: 0 1px 2px rgba(15, 20, 32, 0.08), 0 4px 12px rgba(15, 20, 32, 0.1);
  width: 220px;
  cursor: pointer;
  transition: transform 0.12s ease, box-shadow 0.12s ease;
  position: relative;
  z-index: 3;
  text-align: left;
  appearance: none;
  font-family: inherit;
}
.flow-canvas .channel-pill:hover {
  transform: translateY(-1px);
  box-shadow: 0 2px 4px rgba(15, 20, 32, 0.1), 0 8px 20px rgba(15, 20, 32, 0.14);
}
.flow-canvas .channel-pill__name {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: -0.005em;
  line-height: 1.2;
  color: var(--ch);
}
.flow-canvas .channel-pill__info {
  margin-top: 2px;
  font-size: 10.5px;
  color: var(--text-muted);
  line-height: 1.35;
}
.flow-canvas {
  /* Output port — the drag-to-Model handle on the pill's right edge (F4,
     Decision 43). A ring in the channel hue at rest, filling on pill hover.
     Rendered as the affordance; the drag-to-wire interaction is a later slice. */
}
.flow-canvas .channel-pill__port {
  position: absolute;
  right: -8px;
  top: 50%;
  transform: translateY(-50%);
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: white;
  border: 2.5px solid var(--ch);
  cursor: grab;
  z-index: 4;
  box-shadow: 0 1px 3px rgba(15, 20, 32, 0.18);
  transition: background 0.12s ease;
}
.flow-canvas .channel-pill:hover .channel-pill__port {
  background: var(--ch);
}

/* ================================================================== */
/*  MODEL CARD — incoming destination-table blocks + status pill       */
/* ================================================================== */
.flow-canvas .model-flow-head {
  padding: 9px var(--s-3) 5px;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-subtle);
}
.flow-canvas .model-flow-block {
  display: flex;
  flex-direction: column;
  padding: 8px 26px 8px var(--s-3); /* right pad reserves the hover-trash slot */
  border-top: 1px solid var(--border-soft);
  border-left: 4px solid var(--ch);
  background: linear-gradient(to right, var(--ch-bg) 0%, white 60%);
  position: relative;
}
.flow-canvas {
  /* Withhold one channel — hover-revealed trash on the destination block, the
     single-ribbon inverse of drag-to-Model (Decision 99 partial feed). Rose-red
     on hover, matching card-action-btn--danger. */
}
.flow-canvas .model-flow-block__remove {
  appearance: none;
  border: none;
  cursor: pointer;
  padding: 0;
  position: absolute;
  top: 5px;
  right: 5px;
  width: 20px;
  height: 20px;
  border-radius: var(--r-sm);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  color: var(--text-subtle);
  opacity: 0;
  transition: opacity 0.12s ease, background 0.12s ease, color 0.12s ease;
}
.flow-canvas .model-flow-block__remove svg {
  width: 12px;
  height: 12px;
}
.flow-canvas .model-flow-block__remove:hover {
  background: hsl(0, 70%, 95%);
  color: hsl(0, 65%, 45%);
}
.flow-canvas .model-flow-block__remove:disabled {
  opacity: 0.4 !important;
  cursor: default;
}
.flow-canvas .model-flow-block:hover .model-flow-block__remove,
.flow-canvas .model-flow-block__remove:focus-visible {
  opacity: 1;
}
.flow-canvas {
  /* The colliding table (WS25 follow-up): the block keeps its channel hue on
     the left bar (the ribbon still lands here) but wears the problem red as a
     ring, and names the other Source beneath its table line. */
}
.flow-canvas .model-flow-block.is-collision {
  box-shadow: inset 0 0 0 1.5px var(--conf-red);
}
.flow-canvas .model-flow-block.is-collision .model-flow-block__remove {
  opacity: 1;
}
.flow-canvas .model-flow-block__collision {
  margin-top: 3px;
  font-size: 10.5px;
  font-weight: 700;
  color: var(--conf-red-tag);
}
.flow-canvas {
  /* Vacant slot (Decision 104) — where a ribbon WOULD land: a dashed,
     channel-hued ghost of a destination block. NOT .model-flow-block (the
     ribbon engine + flowDrag key on that class). Click = wire that channel
     (same single-edge op as drag-to-Model). */
}
.flow-canvas .model-flow-vacant {
  appearance: none;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  width: 100%;
  text-align: left;
  padding: 8px var(--s-3);
  border: none;
  border-top: 1px solid var(--border-soft);
  border-left: 4px dashed var(--ch);
  background: transparent;
  opacity: 0.75;
  transition: opacity 0.12s ease, background 0.12s ease;
}
.flow-canvas .model-flow-vacant:hover, .flow-canvas .model-flow-vacant:focus-visible {
  opacity: 1;
  background: linear-gradient(to right, var(--ch-bg) 0%, white 60%);
}
.flow-canvas .model-flow-vacant:hover .model-flow-vacant__hint, .flow-canvas .model-flow-vacant:focus-visible .model-flow-vacant__hint {
  color: var(--text-muted);
}
.flow-canvas .model-flow-vacant:disabled {
  opacity: 0.4;
  cursor: default;
}
.flow-canvas .model-flow-vacant__name {
  font-size: 11px;
  font-weight: 700;
  line-height: 1.4;
  color: var(--text-muted);
}
.flow-canvas .model-flow-vacant__hint {
  font-size: 10px;
  font-style: italic;
  color: var(--text-subtle);
  transition: color 0.12s ease;
}
.flow-canvas {
  /* Connect every vacant slot in one press — the picker's /route op through a
     second door. Mirrors the rail's dashed panel-add affordance. */
}
.flow-canvas .model-flow-connect-all {
  appearance: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: calc(100% - 2 * var(--s-3));
  margin: 8px var(--s-3) 10px;
  padding: 6px 0;
  font-size: 11px;
  font-weight: 600;
  color: var(--text-muted);
  background: transparent;
  border: 1px dashed var(--border);
  border-radius: var(--r-sm);
  transition: color 0.12s ease, border-color 0.12s ease, background-color 0.12s ease;
}
.flow-canvas .model-flow-connect-all svg {
  width: 12px;
  height: 12px;
}
.flow-canvas .model-flow-connect-all:hover, .flow-canvas .model-flow-connect-all:focus-visible {
  color: var(--text);
  border-color: var(--text-subtle);
  background: var(--very-bg);
}
.flow-canvas .model-flow-connect-all:disabled {
  opacity: 0.4;
  cursor: default;
}
.flow-canvas .model-flow-table {
  padding: 3px 0;
  font-size: 11px;
  line-height: 1.4;
}
.flow-canvas .model-flow-table + .model-flow-table {
  border-top: 1px dashed var(--border-soft);
}
.flow-canvas .model-flow-table .tab {
  color: var(--text-muted);
}
.flow-canvas .model-flow-table .sep {
  color: var(--text-subtle);
  margin: 0 4px;
}
.flow-canvas .model-flow-table .tbl {
  color: var(--text);
  font-weight: 700;
}
.flow-canvas .model-flow-table .count {
  margin-left: 5px;
  font-size: 10.5px;
  font-variant-numeric: tabular-nums;
}
.flow-canvas .model-flow-table .count.added {
  color: var(--conf-green-tag);
}
.flow-canvas .model-flow-table .count.updated {
  color: var(--text-muted);
}
.flow-canvas .model-flow-table .count.note {
  color: var(--text-muted);
  font-style: italic;
}
.flow-canvas .model-flow-table .count.rest {
  color: var(--text-subtle);
  font-style: italic;
}
.flow-canvas .model-flow-table .count.broken {
  color: var(--conf-red-tag);
  font-style: italic;
}
.flow-canvas .model-status {
  /* Inline on the name's line, right-aligned (title grows to push it over). */
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex-shrink: 0;
  padding: 2px 10px 2px 7px;
  border-radius: 999px;
  color: white;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: -0.005em;
}
.flow-canvas .model-status svg {
  width: 12px;
  height: 12px;
  stroke-width: 3;
  flex-shrink: 0;
}
.flow-canvas .model-status.is-rest {
  background: var(--conf-green);
}
.flow-canvas .model-status.is-pending {
  background: var(--amber-solid);
}
.flow-canvas .model-status.is-broken {
  background: var(--conf-red);
}
.flow-canvas .model-status {
  /* Another Source feeds one of this Model's tables (WS25 follow-up). Same
     red as broken — a Model-level problem, not a build state. */
}
.flow-canvas .model-status.is-collision {
  background: var(--conf-red);
}
.flow-canvas .model-status {
  /* G2 Phase 3: background recompute in flight — neutral "working" pill with a
     spinning glyph, from the shared recalcJobs poller. */
}
.flow-canvas .model-status.is-computing {
  background: var(--accent, hsl(214, 84%, 56%));
}
.flow-canvas .model-status.is-computing svg {
  animation: model-status-spin 0.8s linear infinite;
}
.flow-canvas {
  /* Slice pill (Decision 87) — which slice of a split source feeds this Model,
     and the picker to move it. One per card: slice is Model-level, never per
     table. Sits under the meta, above the build-state pill. */
}
.flow-canvas .model-slice {
  position: relative;
  align-self: flex-start;
  margin-top: 4px;
}
.flow-canvas .model-slice__pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 7px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: var(--card-bg);
  color: var(--text);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: -0.005em;
  cursor: pointer;
  transition: border-color 0.12s ease, color 0.12s ease;
}
.flow-canvas .model-slice__pill:hover {
  border-color: var(--accent);
  color: var(--accent);
}
.flow-canvas .model-slice__pill:disabled {
  cursor: default;
  opacity: 0.5;
}
.flow-canvas .model-slice__pill .model-slice__field {
  color: var(--text-muted);
  font-weight: 600;
  white-space: nowrap;
}
.flow-canvas .model-slice__pill .model-slice__value {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.flow-canvas .model-slice__pill svg {
  width: 12px;
  height: 12px;
  color: var(--text-subtle);
  flex-shrink: 0;
}
.flow-canvas .model-slice__pill .model-slice__caret {
  width: 11px;
  height: 11px;
}
.flow-canvas .slice-menu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 20;
  min-width: 160px;
  max-width: 300px;
  padding: 4px;
  background: var(--card-bg);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: 0 6px 20px rgba(15, 20, 32, 0.16);
  max-height: 260px;
  overflow-y: auto;
}
.flow-canvas .slice-menu__item {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 5px 8px;
  border: none;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--text);
  font-size: 12px;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.flow-canvas .slice-menu__item:hover {
  background: var(--accent-bg, hsl(214, 100%, 96%));
}
.flow-canvas .slice-menu__item .slice-menu__name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.flow-canvas .slice-menu__item .slice-menu__check {
  width: 13px;
  height: 13px;
  color: var(--accent);
  flex-shrink: 0;
}
.flow-canvas .slice-menu__item.is-current {
  color: var(--accent);
}
.flow-canvas .slice-menu__item {
  /* The pill's menu is a checkbox list over the group's values (Workshop 24):
     a drawn box rather than a native input, so it sits in the menu's rhythm. */
}
.flow-canvas .slice-menu__item .slice-menu__box {
  width: 13px;
  height: 13px;
  border: 1.5px solid var(--border-strong);
  border-radius: 3px;
  display: grid;
  place-items: center;
  flex: 0 0 auto;
}
.flow-canvas .slice-menu__item .slice-menu__box svg {
  width: 10px;
  height: 10px;
  stroke-width: 3.5;
  color: white;
}
.flow-canvas .slice-menu__item.is-current .slice-menu__box {
  background: var(--accent);
  border-color: var(--accent);
}
.flow-canvas .slice-menu__item .slice-menu__hint {
  max-width: 45%;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-left: auto;
  padding-left: 8px;
  font-size: 10px;
  font-weight: 600;
  font-style: italic;
  color: var(--text-subtle);
  white-space: nowrap;
}
.flow-canvas .slice-menu__item:disabled {
  color: var(--text-subtle);
  cursor: default;
}
.flow-canvas .slice-menu__item:disabled:hover {
  background: transparent;
}
.flow-canvas {
  /* section heads — the Project of the Model holding each combination (WS26) */
}
.flow-canvas .slice-menu__head {
  padding: 6px 8px 2px;
  font-size: 9.5px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-subtle);
}
.flow-canvas .slice-menu__foot {
  margin-top: 3px;
  padding: 5px 8px 3px;
  border-top: 1px solid var(--border-soft);
  font-size: 10px;
  font-style: italic;
  color: var(--text-subtle);
}
.flow-canvas .model-slice__pill.is-open {
  border-color: var(--accent);
  color: var(--accent);
}
.flow-canvas {
  /* No group yet (an edgeless Model on a grouped source): dashed and muted,
     the same "unfilled slot" language as the vacant table rows below it. */
}
.flow-canvas .model-slice__pill.is-empty {
  border-style: dashed;
  color: var(--text-muted);
}
.flow-canvas .model-slice__pill.is-empty:hover, .flow-canvas .model-slice__pill.is-empty.is-open {
  color: var(--accent);
}
.flow-canvas {
  /* Remove-from-flow trash — inline just right of the Model name, hover-revealed.
     Reuses the rail's danger card-action-btn (rose-red on hover) + the generic
     [data-tooltip]. Drops the ribbons; never destroys the Project (Decision 50). */
  /* The flow cards' header reserves 32px on the right; the Model card's
     right edge belongs to its pills. */
}
.flow-canvas .model-card .card-header {
  padding-right: var(--s-3);
}
.flow-canvas .model-card__titlerow {
  display: flex;
  align-items: center;
  gap: 2px;
  min-width: 0;
  /* Pills right-justify; the name button and trash keep their slot. */
}
.flow-canvas .model-card__titlerow .model-status {
  margin-left: auto;
}
.flow-canvas {
  /* The name + arrow as one button (the door to Model Detail). */
}
.flow-canvas .model-card__open {
  appearance: none;
  border: none;
  background: transparent;
  font: inherit;
  color: inherit;
  text-align: left;
  display: inline-flex;
  align-items: center;
  gap: 2px;
  flex: 0 1 auto;
  min-width: 0;
  margin: 0 2px 0 -4px;
  padding: 0 0 0 4px;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: background 0.12s ease;
}
.flow-canvas .model-card__open .card-header__title {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: color 0.12s ease;
}
.flow-canvas .model-card__open:hover {
  background: var(--accent-bg);
}
.flow-canvas .model-card__open:hover .card-header__title, .flow-canvas .model-card__open:hover .model-card__open-arrow {
  color: var(--accent);
}
.flow-canvas .model-card__open:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.flow-canvas .model-card__open-arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
}
.flow-canvas .model-card__hover {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  opacity: 0;
  transition: opacity 0.12s ease, background 0.12s ease, color 0.12s ease;
}
.flow-canvas .model-card__hover svg {
  width: 13px;
  height: 13px;
}
.flow-canvas .model-card__hover:disabled {
  cursor: default;
  opacity: 0.4 !important;
}
.flow-canvas .model-card:hover .model-card__hover,
.flow-canvas .model-card__open:focus-visible .model-card__hover,
.flow-canvas .model-card__hover:focus-visible {
  opacity: 1;
}
.flow-canvas {
  /* Empty Models column — the calm invitation to aim the flow at a Model.
     Assemble never manufactures a destination, so this is the landed state of
     the unsplit case until the user routes one. */
}
.flow-canvas .flow-models-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 5px;
  padding: 22px 18px;
  border: 1.5px dashed var(--border-strong);
  border-radius: var(--r-md);
  background: hsl(220, 14%, 98.5%);
}
.flow-canvas .flow-models-empty__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: white;
  border: 1px solid var(--border);
  color: var(--text-subtle);
  margin-bottom: 3px;
}
.flow-canvas .flow-models-empty__icon svg {
  width: 17px;
  height: 17px;
  stroke-width: 2;
}
.flow-canvas .flow-models-empty__title {
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
}
.flow-canvas .flow-models-empty__sub {
  font-size: 11.5px;
  color: var(--text-muted);
  line-height: 1.5;
  max-width: 32ch;
}
.flow-canvas .flow-models-empty__sub strong {
  color: var(--accent);
  font-weight: 800;
}
.flow-canvas {
  /* The Models column sectioned by Project, echoing the Library rail. The
     group releases its own empty space too, so a ribbon running under the
     gap between cards still takes the pointer. */
}
.flow-canvas .flow-model-group {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  pointer-events: none;
}
.flow-canvas .flow-model-group > * {
  pointer-events: auto;
}
.flow-canvas .flow-model-group__name {
  margin-bottom: calc(-1 * var(--s-2));
  padding-left: 2px;
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.005em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.flow-canvas .add-affordance {
  appearance: none;
  background: transparent;
  border: 1.5px dashed var(--border-strong);
  color: var(--text-muted);
  border-radius: var(--r-md);
  padding: 10px;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.flow-canvas .add-affordance:hover {
  background: var(--accent-bg);
  color: var(--accent);
  border-color: var(--accent);
  border-style: solid;
}
.flow-canvas .add-affordance svg {
  width: 13px;
  height: 13px;
  stroke-width: 2.25;
}

/* ================================================================== */
/*  BUTTONS — flow chrome (Save Flow, Write Data, Close, header +)     */
/* ================================================================== */
.flow-canvas .icon-btn-large {
  appearance: none;
  border: none;
  cursor: pointer;
  padding: 0;
  width: 38px;
  height: 38px;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;
  flex-shrink: 0;
  background: white;
  color: var(--text-muted);
  border: 1px solid var(--border-strong);
  box-shadow: 0 1px 2px rgba(15, 20, 32, 0.06);
  position: relative;
}
.flow-canvas .icon-btn-large svg {
  width: 18px;
  height: 18px;
  stroke-width: 2.25;
}
.flow-canvas .icon-btn-large:hover {
  background: var(--accent-bg);
  color: var(--accent);
  border-color: var(--accent);
}
.flow-canvas .icon-btn-large.is-active {
  background: var(--accent);
  color: white;
  border-color: var(--accent);
  box-shadow: 0 1px 2px rgba(15, 20, 32, 0.12), 0 4px 10px hsla(214, 97%, 50%, 0.25);
}
.flow-canvas .icon-btn-large.is-active:hover {
  background: var(--accent-hover);
  color: white;
  border-color: var(--accent-hover);
}
.flow-canvas .icon-btn-round {
  appearance: none;
  border: none;
  cursor: pointer;
  padding: 0;
  width: 30px;
  height: 30px;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background 0.12s ease, color 0.12s ease;
  flex-shrink: 0;
  background: transparent;
  color: var(--text-muted);
  position: relative;
}
.flow-canvas .icon-btn-round svg {
  width: 16px;
  height: 16px;
  stroke-width: 2.25;
}
.flow-canvas .icon-btn-round:hover {
  background: rgba(0, 0, 0, 0.06);
  color: var(--text);
}
.flow-canvas {
  /* tooltip for both icon button sizes */
}
.flow-canvas .icon-btn-large[data-label]:hover::after,
.flow-canvas .icon-btn-round[data-label]:hover::after {
  content: attr(data-label);
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-top: 6px;
  background: white;
  color: var(--text);
  border: 1px solid var(--border-strong);
  box-shadow: 0 2px 8px rgba(15, 20, 32, 0.12);
  font-size: 11px;
  font-weight: 600;
  padding: 4px 8px;
  border-radius: var(--r-sm);
  white-space: nowrap;
  pointer-events: none;
  z-index: 10;
}
.flow-canvas .write-pill {
  appearance: none;
  border: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 9px 18px 9px 14px;
  height: 38px;
  border-radius: 999px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: -0.005em;
  transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
  white-space: nowrap;
}
.flow-canvas .write-pill svg {
  width: 15px;
  height: 15px;
  stroke-width: 2.5;
}
.flow-canvas .write-pill.is-pending {
  background: var(--amber-solid);
  color: white;
  box-shadow: 0 1px 2px rgba(15, 20, 32, 0.1), 0 4px 10px hsla(38, 92%, 48%, 0.28);
}
.flow-canvas .write-pill.is-pending:hover {
  background: var(--amber-solid-hover);
  box-shadow: 0 1px 2px rgba(15, 20, 32, 0.14), 0 6px 16px hsla(38, 92%, 48%, 0.38);
}
.flow-canvas .write-pill.is-rest {
  background: var(--write-rest-bg);
  color: var(--write-rest-text);
  border: 1px solid var(--border);
  cursor: default;
}
.flow-canvas .write-pill.is-rest svg {
  color: var(--conf-green-tag);
}
.flow-canvas .write-pill.is-blocked {
  background: var(--conf-red);
  color: white;
  cursor: not-allowed;
}

/* ================================================================== */
/*  ASSEMBLE FLIGHT — staging + staggered reveal (intakeFlow.runFlight) */
/* ================================================================== */
/* The intake view hosts the reparented flying comp-card (position: absolute). */
.intake-view {
  position: relative;
}

/* Staged: the canvas is laid out (so the flight can measure the header's
   target geometry) but invisible — no flash before the hand-off. */
.flow-canvas.is-staging {
  visibility: hidden;
}

/* Revealing: each part starts hidden and is switched on (.is-in) in sequence by
   the choreography. Removing .is-revealing at the end restores normal resting
   styles. */
.flow-canvas.is-revealing .source-card,
.flow-canvas.is-revealing .flow-col--datasets .section-card,
.flow-canvas.is-revealing .channel-pill-anchor,
.flow-canvas.is-revealing .flow-model-group > *,
.flow-canvas.is-revealing .source-strip__cell--actions {
  opacity: 0;
  transition: opacity 0.45s ease, transform 0.45s ease;
}
.flow-canvas.is-revealing {
  /* The header source-card reveals opacity-only — it must sit exactly where the
     flying card landed (a transform would make it jump). */
}
.flow-canvas.is-revealing .flow-col--datasets .section-card {
  transform: translateX(-14px);
}
.flow-canvas.is-revealing .channel-pill-anchor {
  transform: scale(0.9);
}
.flow-canvas.is-revealing .flow-model-group > * {
  transform: translateX(10px);
}
.flow-canvas.is-revealing {
  /* .is-in must beat the more-specific per-column reveal selectors above
     (e.g. `.flow-col--datasets .section-card` is more specific than `.is-in`),
     so the shown-state is forced — a reveal system shouldn't fight a
     specificity war. */
}
.flow-canvas.is-revealing .is-in {
  opacity: 1 !important;
  transform: none !important;
}
.flow-canvas.is-revealing {
  /* The down-tail grows (rather than fades) and rests at 0.4 opacity. */
}
.flow-canvas.is-revealing .source-tail {
  opacity: 0;
  transform: scaleY(0);
  transform-origin: top center;
  transition: transform 0.35s ease, opacity 0.25s ease;
}
.flow-canvas.is-revealing .source-tail.is-in {
  opacity: 0.4 !important;
  transform: scaleY(1) !important;
}

/* Offline-processing job progress (Milestone C3). A generic bar + message +
   cancel; per-task-type surfaces can present richer detail later. */
/* Dev harness page (jobs_demo). */
.job-demo {
  padding: var(--s-4);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  max-width: 560px;
}

.job-demo__title {
  font-size: 18px;
  font-weight: 700;
  color: var(--text);
}

.job-demo__lead {
  font-size: 13px;
  color: var(--text-muted);
}

.job-demo__btn {
  align-self: flex-start;
  padding: 8px 16px;
  border: 1px solid var(--accent);
  border-radius: var(--r-md);
  background: var(--accent);
  color: #fff;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.job-demo__btn:disabled {
  opacity: 0.5;
  cursor: default;
}

.job-progress__cancel {
  padding: 4px 12px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: transparent;
  color: var(--text);
  font-size: 12px;
  cursor: pointer;
}
.job-progress__cancel:hover {
  border-color: var(--accent);
  color: var(--accent);
}
.job-progress__cancel {
  /* Pressed, but the runner only stops at the next chunk boundary — so the
     button stays visible (the job IS still running) and simply stops taking
     clicks. Hiding it would read as "cancelled" before anything had. */
}
.job-progress__cancel:disabled {
  opacity: 0.55;
  cursor: default;
  border-color: var(--border-strong);
  color: var(--text-muted);
}

.job-progress {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  max-width: 460px;
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface, #fff);
}

.job-progress__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3);
}

.job-progress__label {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
}

.job-progress__pct {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* The track. An indeterminate job (null percent) gets a sliding shimmer instead
   of a filled width. */
.job-progress__track {
  position: relative;
  height: 8px;
  border-radius: 999px;
  background: hsl(214, 18%, 90%);
  overflow: hidden;
}

.job-progress__fill {
  height: 100%;
  border-radius: 999px;
  background: var(--accent);
  transition: width 0.3s ease;
}

.job-progress__track--indeterminate .job-progress__fill {
  width: 40% !important;
  position: absolute;
  animation: job-progress-slide 1.1s ease-in-out infinite;
}

@keyframes job-progress-slide {
  0% {
    left: -40%;
  }
  100% {
    left: 100%;
  }
}
.job-progress__msg {
  font-size: 12px;
  line-height: 1.4;
  color: var(--text-muted);
  min-height: 1.1em;
}

.job-progress__error {
  font-size: 12px;
  color: var(--rc-conf-red, hsl(0, 65%, 48%));
}

.job-progress__foot {
  display: flex;
  justify-content: flex-end;
}

/* Terminal state tints the bar (done = green, error/canceled = muted). */
.job-progress--done .job-progress__fill {
  background: var(--rc-conf-green, hsl(142, 60%, 40%));
}

.job-progress--error .job-progress__fill,
.job-progress--canceled .job-progress__fill {
  background: var(--text-muted);
}

/* Recalc overlay (Milestone D2). Greys out a single model's main view while
   its background recalc runs — header and body both. The overlay is scoped to
   `.view--main`, which is given a containing block below; the rest of the app
   (rail, other views) stays live, so a slow recalc never blocks navigation. */
.view--main {
  position: relative;
}

.recalc-overlay {
  position: absolute;
  inset: 0;
  z-index: 60; /* above the view-header's menu-popover (z-index 50) */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-4);
  background: color-mix(in srgb, var(--view-bg) 62%, transparent);
  backdrop-filter: blur(1.5px);
  cursor: progress;
}

/* Failed recalc — the overlay lingers showing the error + a Dismiss button, so
   it's no longer a "busy" surface. */
.recalc-overlay--error {
  cursor: default;
}

/* The panel reuses the shared .job-progress card; widen it a touch and lift it
   off the greyed surface so the bar reads clearly against the blur. */
.recalc-overlay__panel {
  width: min(420px, 80%);
  max-width: none;
  box-shadow: 0 10px 30px rgba(15, 20, 32, 0.16), 0 2px 6px rgba(15, 20, 32, 0.08);
}