/* =============================================================================
   Migration Suite — Design System
   -----------------------------------------------------------------------------
   The single, canonical stylesheet for the dmweb SPA: tokens + base + shared
   components. LOAD-BEARING since 2026-06-13 — the shell (index.html) links it
   render-blocking before app.css, so every in-shell route inherits it; the
   styleguide (../design-system/styleguide.html) renders from this same file.
   Tweak a value HERE and both the app and the styleguide retune together.

   Cascade contract: this file loads FIRST. app.css (cross-route glue),
   sidebar.css (chrome layout) and routes/<route>.css (page-specific) load
   after and may override deliberately. The standalone pages (login.html,
   help.html, api-reference.html) do NOT link it — login is pre-auth minimal,
   help is deliberately warm-styled.

   --- Curated to feedback (2026-06-08) -------------------------------------
   Captured from the live UI as "liked, make it canonical": sidebar nav +
   selection + active-cycle dot; the tier-cascade "Applies to" selector;
   check-type pills; dialog title/subtitle/rule-number + icon close/save;
   collapsible sections; object-row inline icons + counters; the "⋯" kebab and
   its dropdown menu (now THE reusable menu); file rows with status spinners;
   drag-and-drop drop targets; the transform/validate run-bar.
   Adjusted per feedback: disclosure triangle on the LEFT; lighter list-row
   titles; run-bar squared at the top inside a card; subtle borders between file
   rows; and — globally — icon buttons are BORDERLESS, with hover showing only a
   touch of color + a subtle background (never a border).

   Layers, top to bottom:
     1. Tokens         9. Overlays (dialog, menu, tooltip)
     2. Reset / base   10. Disclosure / collapsible
     3. Layout         11. Sidebar navigation
     4. Forms          12. Tier cascade ("Applies to")
     5. Buttons        13. File / list rows + status spinners
     6. Feedback       14. Run bar (pipeline stages)
     7. Data display   15. Utilities + keyframes
     8. (overlays)
   ========================================================================== */

/* 1. TOKENS ================================================================ */
:root {
  /* --- Core palette (the established 10; unchanged) --- */
  --fg: #1d1f23;            /* primary text */
  --muted: #6b7280;         /* secondary text, labels, meta */
  --bg: #f7f7f8;            /* app background / selected-row tint / icon hover */
  --card: #ffffff;          /* surface: cards, header, sidebar, dialogs */
  --border: #e3e5ea;        /* hairline dividers + control borders */
  --border-hover: #c3c8d1;  /* input/control hover border — a subtle step up from --border, not the strong --muted */
  --accent: #0f62fe;        /* primary action / links / active state */
  --accent-hover: #0050d4;  /* accent pressed/hover (solid fills) */
  --ok: #168f4b;            /* success */
  --err: #c0392b;           /* error / destructive */
  --warn: #b07d00;          /* warning / attention */

  /* --- Surfaces --- */
  --surface-hover: #f3f4f6; /* neutral row/link hover */
  --surface-sunken: #fafbfc;/* nested panels, open rows, table head zebra */
  --surface-info: #f3f6fb;  /* informational band / menu-item hover / summary */
  --row-hover: #f7f8fa;     /* canonical interactive-row hover wash (distinct from --surface-sunken, which is the resting/open-row + zebra tint) */
  --control-disabled: #b8c0cc;
  --control-disabled-fg: #b9bdc5;
  --hover-strong: #e5e7eb;

  /* --- Accent variants --- */
  --accent-soft-bg: #f0f4ff;        /* download tiles */
  --accent-soft-bg-hover: #e3ebff;
  --accent-soft-border: #cdd9f9;
  --accent-icon-hover-bg: #eff4ff;  /* icon-button hover wash (accent intent) */
  --accent-chip-bg: #dbeafe;
  --accent-chip-fg: #1e40af;
  --accent-chip-border: #bfdbfe;
  --accent-wash: rgba(15, 98, 254, 0.06);   /* drop-target fill, faint */
  --accent-tint: rgba(15, 98, 254, 0.08);   /* lit cascade segment */
  --accent-tint-strong: rgba(15, 98, 254, 0.18); /* origin cascade segment */

  /* --- Status-soft --- */
  --ok-soft-bg: #ecfdf5;    --ok-soft-fg: #065f46;
  --err-soft-bg: #fff5f5;   --err-soft-border: #f5c2c2;  --err-strong: #a52a1c;
  --sev-error-bg: #fde2e1;
  --sev-warning-bg: #fcefd4; --sev-warning-border: #fbe0b0;
  --sev-info-bg: #e5e7eb;

  /* --- Pipeline stage semantics (used by run-bars, stage spinners, stage nav) --- */
  --stage-assess-bg: #dbeafe;    --stage-assess-fg: #1d4ed8;     /* blue */
  --stage-transform-bg: #fdecd2; --stage-transform-fg: #b9770e;  /* amber */
  --stage-validate-bg: #d9f3e4;  --stage-validate-fg: var(--ok); /* green */
  --run-bar-bg: #eef0f3;

  /* --- Elevation --- */
  --backdrop: rgba(0, 0, 0, 0.3);
  --shadow-menu: 0 6px 20px rgba(20, 24, 33, 0.14);
  --shadow-dialog: 0 14px 48px rgba(20, 24, 33, 0.28);
  --shadow-tooltip: 0 6px 18px rgba(0, 0, 0, 0.20);
  /* The styleguide's close/cancel icon as a mask (paint it with a background
     color): the clear cross of every search input uses it. */
  --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'/%3E%3Cline x1='6' y1='6' x2='18' y2='18'/%3E%3C/svg%3E");
  --tooltip-bg: #4a4f57;
  --tooltip-width: 320px; /* tooltips are uniformly wide — set the block width, not just a cap */

  /* --- Typography --- */
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --lh: 1.45;

  /* THE type scale: FOUR sizes only (uniformized 2026-07-18) — 18 titles,
     14 headings+body, 12 all secondary text, 10 all chip/badge-tier text.
     The seven token names remain (too many call sites to rename) but they
     collapse onto the four values: h2 joins body, sm joins meta, micro
     joins nano. Pick by SEMANTICS; the value follows. */
  --fs-h1: 18px;   /* page title / login heading */
  --fs-h2: 14px;   /* card / section / dialog heading (= body size, semibold) */
  --fs-body: 14px; /* body copy */
  --fs-sm: 12px;   /* buttons, labels, status, nav, row titles */
  --fs-meta: 12px; /* secondary detail, counts, subtitles */
  --fs-micro: 10px;/* chips, badges, pills, table headers, file dates */
  --fs-nano: 10px; /* sidebar/tiny section labels */

  --fw-regular: 400;
  --fw-medium: 500;
  --fw-semibold: 600;
  --fw-bold: 700;

  /* --- Spacing (4/6/8/12/16/20/24/32) --- */
  --sp-1: 4px;  --sp-2: 6px;  --sp-3: 8px;  --sp-4: 12px;
  --sp-5: 16px; --sp-6: 20px; --sp-7: 24px; --sp-8: 32px;

  /* --- Radii --- */
  --r-sm: 4px;     /* inputs, buttons, icon-button hover pill */
  --r-md: 6px;     /* nav links, menus, lanes/sub-cards, tooltips */
  --r-lg: 8px;     /* cards, dialogs, collapsible sections */
  --r-pill: 999px; /* pills, dots, accent bars */

  /* --- Structure --- */
  --sidebar-w: 200px;
  --content-max: 720px;
  --content-max-wide: 1100px;

  --t-fast: 0.12s;
}

/* 2. RESET / BASE ========================================================== */
*, *::before, *::after { box-sizing: border-box; }

/* Suppress the browser's default focus ring (the dark rounded box Chrome draws
   on buttons/links/any focusable element). Focus styling is opt-in: only the
   explicit :focus / :focus-visible rules in this stylesheet apply. */
:focus, :focus-visible { outline: none; }
::-moz-focus-inner { border: 0; }

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  color: var(--fg);
  background: var(--bg);
  line-height: var(--lh);
}

h1 { margin: 0; font-size: var(--fs-h1); font-weight: var(--fw-semibold); }
h2 { margin: 0 0 var(--sp-5); font-size: var(--fs-h2); font-weight: var(--fw-semibold); }
h3 { margin: 0; font-size: var(--fs-h2); font-weight: var(--fw-semibold); }

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

code, pre, .mono { font-family: var(--font-mono); }

/* "Eyebrow": a quiet small label — muted, semibold, SENTENCE CASE (no uppercase,
   no tracking, per feedback). Shared by table headers, sidebar section labels,
   lane headings, menu headings, dialog sub-headings — all kept consistent. */
.eyebrow {
  font-size: var(--fs-micro);
  color: var(--muted);
  font-weight: var(--fw-semibold);
}

/* Canonical list-row title. Lighter than a card heading — per feedback the
   object rows should match this rules-row weight, NOT a bold 14/600. */
.row-title {
  font-size: var(--fs-sm);
  font-weight: var(--fw-regular);
  color: var(--fg);
  letter-spacing: -0.005em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* 3. LAYOUT ================================================================ */
/* Fixed-viewport shell (sidebar.js): only <main> scrolls; header + sidebar fixed. */
body.app-shell { height: 100vh; overflow: hidden; display: flex; flex-direction: column; }
body.app-shell header { flex: 0 0 auto; position: relative; z-index: 50; }
body.app-shell .shell { flex: 1 1 auto; min-height: 0; align-items: stretch; overflow: hidden; }
body.app-shell main { overflow-y: auto; min-height: 0; }

header {
  padding: 10px var(--sp-8); /* 18px→10px, ~20% shorter (sidebar.css overrides this in the shell — keep the two in sync) */
  border-bottom: 1px solid var(--border);
  background: var(--card);
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-7);
}
header .sub { color: var(--muted); font-size: var(--fs-sm); margin-top: 2px; }
header .brand { display: flex; align-items: center; gap: var(--sp-4); }
header .brand .brand-logo { width: auto; display: block; }
header .brand .brand-sub {
  font-size: var(--fs-body); font-weight: var(--fw-medium); color: var(--muted);
  padding-left: var(--sp-4); border-left: 1px solid var(--border); white-space: nowrap;
}

.shell { display: flex; align-items: flex-start; }
main { flex: 1; min-width: 0; padding: var(--sp-7) var(--sp-7); }
main > * { max-width: var(--content-max); margin-left: auto; margin-right: auto; }
main.wide > * { max-width: var(--content-max-wide); }

/* Breadcrumb page title — the <h1> the router renders into the page <header>
   (it replaced the header tab bar and wears its styling): pill-shaped
   segments, <project> / <etl> / <page>, with the current page in accent.
   Centered in the header via the `header .page-crumb` context rule so the
   styleguide specimen can render it inline. */
.page-crumb h1 {
  display: flex; align-items: center; gap: 2px;
  font-size: var(--fs-sm); font-weight: var(--fw-regular); min-width: 0;
}
/* In the header the crumb bar mirrors the content column's geometry — it spans
   the area above `main` (200px sidebar + main's side padding) and centers a
   box of the content max-width inside it, so the first pill's left edge lines
   up with the content cards below. The wrapper is click-transparent. */
header .page-crumb {
  position: absolute; top: 50%; transform: translateY(-50%);
  left: calc(var(--sidebar-w) + var(--sp-7)); right: calc(var(--sp-7) + var(--chat-w, 0px));
  display: flex; justify-content: center;
  pointer-events: none;
  transition: right 160ms ease;
}
header .page-crumb h1 {
  width: 100%; max-width: 1100px; /* the shared content width (routes/objects.css) */
  justify-content: flex-start;
  pointer-events: auto;
}
/* Pull the first pill's padding out of the line so its TEXT (not its hover
   pill) sits flush with the content boxes' left border. */
header .page-crumb h1 > :first-child { margin-left: -8px; }

.page-crumb .crumb-ctx, .page-crumb .crumb-cur {
  display: inline-flex; align-items: center;
  padding: 4px 8px; border-radius: 6px; /* the old header tab-button chrome, half the padding */
  color: var(--fg); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; max-width: 40ch;
}
a.crumb-ctx:hover, a.crumb-ctx:focus-visible {
  background: #f3f4f6; color: var(--fg); text-decoration: none; outline: none;
}
.page-crumb .crumb-cur { font-weight: var(--fw-semibold); color: var(--accent); }
/* The leaf's page toggle (Objects ⇄ Rules): the whole current-page pill is a
   <button> that swaps straight to its sibling page on click. */
.page-crumb button.crumb-cur {
  background: transparent; border: 0; font: inherit; cursor: pointer;
}
.page-crumb button.crumb-cur:hover:not(:disabled) { filter: none; background: #f3f4f6; }
.page-crumb .crumb-swap { width: 12px; height: 12px; margin-left: var(--sp-2); flex: 0 0 auto; opacity: 0.7; }
/* Onward navigation hints after the leaf (e.g. Objects / Rules on the cycle
   overview) — grey filled pills, borderless, darkening on hover. */
.page-crumb .crumb-hint {
  background: #f3f4f6; border: 0; color: var(--muted);
  margin-left: var(--sp-2);
}
/* Extra breathing room between the breadcrumb and the first onward hint. */
.page-crumb :not(.crumb-hint) + .crumb-hint { margin-left: var(--sp-5); }
a.crumb-hint:hover, a.crumb-hint:focus-visible { background: #e8eaee; color: var(--fg); }
/* The ETL segment's quiet ETLnn prefix — when a cycle has a description the
   description reads as the segment's name and the number drops to this
   secondary label (same size, muted + regular weight). */
.page-crumb .crumb-desc { color: var(--muted); font-weight: var(--fw-regular); margin-right: var(--sp-1); }
.page-crumb .crumb-cur .crumb-desc { color: inherit; opacity: 0.55; }
.page-crumb .crumb-sep { color: var(--muted); opacity: 0.45; user-select: none; }

/* Project Settings shortcut in the header, at the breadcrumb's height. It
   borrows the crumb-link chrome (pill, --fs-sm) but a muted tone, a leading cog,
   and its isolated position mark it as a control set apart from the breadcrumb
   nav, not a crumb segment. Appearance is header-independent so the styleguide
   can show it inline; only the positioning below is header-scoped. */
.header-settings {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: 4px 8px; border-radius: 6px;
  font-size: var(--fs-sm); font-weight: var(--fw-medium);
  color: var(--muted); white-space: nowrap; cursor: pointer; text-decoration: none;
}
.header-settings:hover, .header-settings:focus-visible {
  background: #f3f4f6; color: var(--fg); text-decoration: none; outline: none;
}
.header-settings svg { width: 14px; height: 14px; flex: 0 0 auto; }
.header-settings[hidden] { display: none; }
/* Pinned to the RIGHT EDGE OF THE CONTENT COLUMN (where the page cards end), not
   the far edge of the header. The slot mirrors the breadcrumb's geometry — it
   spans the area above `main` and centers a box of the shared content width — so
   the cog rides that box's right edge. Slot + box are click-transparent (the
   crumb sits behind them); only the cog anchor takes pointer events. */
header .header-settings-slot {
  position: absolute; top: 50%; transform: translateY(-50%);
  left: calc(var(--sidebar-w) + var(--sp-7)); right: calc(var(--sp-7) + var(--chat-w, 0px));
  display: flex; justify-content: center;
  pointer-events: none; z-index: 1;
  transition: right 160ms ease;
}
header .header-settings-box {
  width: 100%; max-width: 1100px; /* the shared content width (routes/*.css) */
  display: flex; justify-content: flex-end;
  pointer-events: none;
}
header .header-settings-box > .header-settings { pointer-events: auto; }

.card {
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: var(--sp-5); margin-bottom: var(--sp-6);
}
.card h2 { margin: 0 0 var(--sp-3); }
/* The hint line under a card title is capped at ~two thirds of the card width so
   it reads as a short subtitle, not an edge-to-edge paragraph, on a full-width
   card. Short hints (narrow grid cards) never reach the cap, so they are
   unaffected. */
.card .card-sub { color: var(--muted); font-size: var(--fs-sm); margin-bottom: var(--sp-5); max-width: 66%; }

/* Card / section header with an optional trailing action (an add or upload
   button). The title sits at its natural position and defines the row height;
   the action is taken out of flow and vertically centered on the title, so its
   presence (or a taller button) never pushes the title down. */
.card-head {
  position: relative;
  margin: 0 0 var(--sp-3);
}
.card-head > h2, .card-head > h3, .card-head > h4 { margin: 0; }
.card-head > button, .card-head > .card-head-action {
  position: absolute; top: 50%; right: 0; transform: translateY(-50%);
}
.card-head > button[hidden], .card-head > .card-head-action[hidden] { display: none !important; }

/* Lane / sub-card: a lighter card used inside a card (workspace lanes). Square
   any edge that fuses to a sibling control (e.g. the run-bar below). */
.lane {
  background: #fff; border: 1px solid var(--border); border-radius: var(--r-md);
  padding: var(--sp-4) var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-3);
}
.lane > h4, .lane-head h4 {
  margin: 0; font-size: var(--fs-meta); font-weight: var(--fw-semibold); color: var(--muted);
}
/* Lane heading row: label fills it; a trailing quiet "+" sits at the right edge. */
.lane-head { display: flex; align-items: center; gap: var(--sp-3); }
.lane-head h4 { flex: 1 1 auto; min-width: 0; }

/* Tab strip — an underlined row of view tabs sitting on a hairline, the active
   one underlined in accent over the line. Canonical since 2026-07-21 (was the
   duplicated .eov-tab / .od-tab / .ps-tab); the cycle overview, object page and
   project settings all render `<div class="tabs" role="tablist"> <button
   class="tab">…`. A page toggles the active tab with `.classList.toggle("is-active")`. */
.tabs {
  display: flex; align-items: flex-end; gap: var(--sp-2);
  border-bottom: 1px solid var(--border); margin: 0 0 var(--sp-6);
}
.tab {
  appearance: none; background: transparent; border: 0; cursor: pointer;
  font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-medium);
  color: var(--muted); padding: var(--sp-3) 14px; text-decoration: none;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
  /* Match the base `button` radius so the active accent underline curls the same
     way whether the tab is a <button> (in-page tabs) or an <a> (settings subnav,
     which carries no button radius of its own) — keeps both renders identical. */
  border-radius: var(--r-sm);
}
/* text-decoration stays off on hover too — the tab may be an <a> (settings
   subnav), which the base `a:hover` would otherwise underline. */
.tab:hover { color: var(--fg); text-decoration: none; }
.tab.is-active { color: var(--fg); border-bottom-color: var(--accent); }
.tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: var(--r-sm); }
/* Set apart at the right end of the strip: a regular .tab in a lighter weight,
   or a kebab menu (.menu-wrap.tabs-aside) holding the views that are not the
   page's own (the Audit trail on the project, cycle and object pages). The
   kebab centres on the tabs' text line rather than sitting on the hairline. */
.tabs > .tabs-aside { margin-left: auto; }
.tab.tabs-aside { font-weight: var(--fw-regular); white-space: nowrap; }
.tabs > .menu-wrap { align-self: center; }

/* Empty-state hint — the shared "nothing here yet" placeholder: the column
   profile placeholder's grammar promoted to one component. A quiet centred
   card: optional sequence chip, icon (stroke 1.4, one glyph per location),
   short title, one-line sub and at most one CTA (the quiet accent button,
   contributor-gated by the host). The dashed outline carries drop-zone
   semantics, so it renders where files land (source/load lanes, template);
   `.create` drops it for manual-creation surfaces (rules tiers). `.lane` is
   the compact variant for narrow lane cards. Built by ws-panel's buildWsHint
   and the rules pages' tier empty states. */
.ws-hint {
  border: 1px dashed var(--border); border-radius: var(--r-md); background: transparent;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; gap: 10px; padding: 34px 24px; min-height: 170px;
  color: var(--muted);
}
.ws-hint.create { border: 0; }
.ws-hint .ws-hint-ic { width: 32px; height: 32px; color: #b9bdc6; flex: 0 0 auto; }
.ws-hint-title { font-size: var(--fs-body); font-weight: var(--fw-medium); color: #808795; }
.ws-hint-sub { font-size: var(--fs-sm); max-width: 46ch; margin: 0; line-height: 1.5; }
.ws-hint-step {
  font-size: var(--fs-micro); letter-spacing: 0.06em; text-transform: uppercase;
  font-weight: var(--fw-semibold); color: #9aa1ad; margin-bottom: -2px;
}
.ws-hint > .ws-hint-cta { margin-top: 2px; }
.ws-hint.lane { padding: 26px 16px; min-height: 150px; }
.ws-hint.lane .ws-hint-ic { width: 26px; height: 26px; }
.ws-hint.lane .ws-hint-title { font-size: var(--fs-sm); }
.ws-hint.lane .ws-hint-sub { font-size: var(--fs-micro); max-width: 28ch; }

/* "Not applicable this cycle" marker bar (H53 STAGE 2). A compact, content-
   width, left-aligned bar shown wherever a stage is out of scope for the cycle:
   the object page's rule tabs (above the rule list) and the source-data lane.
   Unlike .ws-hint it never fills its container, so it reads as an inline marker
   over the content that follows. The bar itself mimics the stage strip's n/a
   cell (a dashed outline over a bare background) so the marker and the strip read
   as one "not applicable" state; the icon is just the n/a stage glyph, a
   horizontal dash. */
.scope-na-bar {
  display: flex; width: fit-content; max-width: 100%;
  align-items: center; gap: 10px;
  /* A clear, symmetric break above and below the marker: two card paddings
     (2 x --sp-5 = 32px) on each side. */
  margin: calc(var(--sp-5) * 2) 0;
  padding: 8px 12px;
  border: 1px dashed var(--border); border-radius: var(--r-md);
  background: transparent; color: var(--muted);
}
.scope-na-ico {
  /* Just the horizontal dash (the na stage glyph), no cell around it. Kept as
     soft as the bar's dashed edges (same tone as the border), with extra room on
     each side so the dash sits clear of the bar edge and the text. */
  flex: 0 0 auto; margin: 0 6px;
  color: var(--border); font-size: 20px; line-height: 1;
}
.scope-na-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.scope-na-title { font-size: var(--fs-body); font-weight: var(--fw-medium); color: #808795; }
.scope-na-sub { font-size: var(--fs-sm); color: var(--muted); }

#sidebarHost { flex: 0 0 var(--sidebar-w); align-self: stretch; min-height: 0; }

/* --- Docked side panel -------------------------------------------------- *
   A panel that docks to the right of the shell and slides open/shut rather
   than mounting and unmounting (K3, the assistant). `.side-panel` owns the
   open/collapse mechanics, the edge rail tab and the close chevron, and the
   narrow-viewport overlay; a consumer supplies its own content and drives
   --panel-w / --panel-open-w (each default to 380px) from whatever governs
   its own layout (chat-panel.css bridges these off the body-level --chat-w /
   --chat-open-w the shell also reads to inset the header, see there). Tokens
   only, no colours of its own, so a second docked panel reuses the same
   chrome instead of defining it again. Promoted from chat-panel.css once the
   assistant panel shipped (K3, 2026-09-23): it had no owner here before that,
   deliberately, on the grounds that promoting unproven chrome into a
   load-bearing file was a bet not worth making until the panel survived. */
.side-panel {
  flex: 0 0 var(--panel-w, 380px);
  min-width: 0; min-height: 0;
  display: flex; flex-direction: column;
  position: relative;
  background: var(--bg);
  font-size: var(--fs-sm);
  color: var(--fg);
  transition: flex-basis 160ms ease;
  overflow: hidden;
}
.side-panel.is-collapsed { flex-basis: var(--panel-w, 40px); }

/* The panel opens by sliding in, not by growing: a flex child only stretches
   to the column while its cross size is auto, so pinning the contents to the
   open width at every frame is what stops them re-wrapping as the column
   animates. Once fully closed they are hidden (not just clipped), which is
   what takes them out of the tab order and off a screen reader; `is-closing`
   is the length of the slide (put on and taken off by the consumer), so the
   contents ride the column out instead of vanishing on the first frame. */
.side-panel > :not(.side-panel-rail) { width: var(--panel-open-w, 380px); }
.side-panel.is-collapsed:not(.is-closing) > :not(.side-panel-rail) { visibility: hidden; }

/* The lip: one control that opens and closes the panel, pinned to the middle
   of the WINDOW's right edge so the control a reader reaches for never moves,
   open or shut. Fixed rather than a flex child: closed there is no column to
   sit in. It is the same element in both states and only its icon changes
   (the consumer swaps it, with its tooltip and aria-expanded). */
.side-panel-rail {
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  width: 24px; height: 44px;
  padding: 0;
  cursor: pointer;
  align-items: center; justify-content: center;
  right: 0;
  z-index: 40;
}
/* Edged on three sides, rounded on the left and square against the window's
   edge, so it reads as a handle of the panel behind that edge, and on screen
   in every state. Written on `.side-panel .side-panel-rail` because
   `.icon-btn`, further down this file at the same one-class specificity,
   would otherwise put back its all-round radius, its transparent border and
   its inline-flex display. */
.side-panel .side-panel-rail {
  display: flex;
  background: var(--card);
  border: 1px solid var(--border); border-right: 0;
  border-radius: var(--r-md) 0 0 var(--r-md);
}

/* Below a narrow viewport a docked column leaves the content nothing to live
   in, so the panel overlays it instead of squeezing it: parked off the right
   edge when shut, flush with it when open, positioned in both states or a
   close would have nothing to animate from. A transform is avoided because it
   would make the panel the containing block for the two fixed controls and
   carry them off screen with it. A consumer still decides whether it docks
   at all below this width (chat-panel.css zeroes the body-level width var
   here so the shell stops squeezing for it). */
@media (max-width: 900px) {
  .side-panel {
    position: fixed;
    top: 0; bottom: 0;
    right: calc(-1 * var(--panel-open-w, 380px));
    width: min(380px, 100vw);
    z-index: 40;
    box-shadow: -8px 0 24px rgba(0, 0, 0, 0.18);
    transition: right 160ms ease;
  }
  .side-panel:not(.is-collapsed) { right: 0; }
}

/* 4. FORMS ================================================================= */
/* Muted label color adopted from the live SPA (review 2026-06-12) — the SPA's
   quiet form labels are canonical; data, not chrome, carries the contrast. */
label {
  display: block; font-size: var(--fs-sm); font-weight: var(--fw-medium);
  color: var(--muted); margin-bottom: var(--sp-2);
}
label .hint { font-weight: var(--fw-regular); color: var(--muted); }
label .req  { color: var(--err); font-weight: var(--fw-bold); }

/* `input:where([type="date"])` keeps date at ELEMENT-level specificity (0,0,1);
   the other listed types sit at (0,1,1) via their attribute selector. That
   matters because a date input must carry type="date", so a naive
   `input[type="date"]` (0,1,1) would outrank the app's quiet/inline date
   controls (.eov-hero-date, .od-date, .eo-dates-input — all class-styled at
   (0,1,0)) and force a box onto them. At element level, those deliberate
   deviations win; only a bare, unstyled date field falls back to this box. */
input[type="text"], input[type="number"], input[type="email"],
input[type="password"], input[type="search"], input:where([type="date"]), select, textarea {
  width: 100%; padding: var(--sp-3) 10px; font-size: var(--fs-sm); font-family: inherit;
  border: 1px solid var(--border); border-radius: var(--r-sm); background: #fff; color: var(--fg);
  transition: border-color var(--t-fast);
}
/* Date inputs render a touch taller (native spinner/icon box); box-sizing is
   already border-box globally, so line-height: normal is all it takes to pin
   them to the same height as the text field beside them. */
input:where([type="date"]) { line-height: normal; }
/* Hover nudges the border to --border-hover (a subtle step up from --border,
   not the strong --muted), but at deliberately LOW specificity (:where() adds
   none, so this stays element-level): any page that gives an input its own
   hover treatment — the ETL overview's quiet inline dates (.eov-hero-date),
   click-to-edit fields — keeps winning. The :where(:not(.invalid)) also leaves
   an invalid field's red border on hover. */
input:hover:where(:not(:focus)):where(:not(:disabled)):where(:not(.invalid)),
select:hover:where(:not(:focus)):where(:not(:disabled)),
textarea:hover:where(:not(:focus)):where(:not(:disabled)) { border-color: var(--border-hover); }
/* Focus mirrors hover: the same subtle --border-hover, no accent ring — the
   caret + active field already signal focus; the border just stays consistent
   with the hover step (kept at element specificity so page-level focus
   treatments still win). */
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--border-hover); }
input:disabled, select:disabled, textarea:disabled {
  background: var(--bg); color: var(--muted); cursor: not-allowed;
}
input::placeholder, textarea::placeholder { color: var(--muted); }
/* The clear cross of a search input: the browser's own drawing is swapped for
   the styleguide close icon in icon-btn gray (fg while the field is hovered
   or focused), on every search input, not per page. Firefox draws no cross. */
input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
input[type="search"]::-webkit-search-cancel-button {
  -webkit-appearance: none; appearance: none;
  width: 12px; height: 12px; margin-left: 4px; cursor: pointer;
  background-color: var(--muted);
  -webkit-mask: var(--icon-close) center / 100% 100% no-repeat; mask: var(--icon-close) center / 100% 100% no-repeat;
}
input[type="search"]:hover::-webkit-search-cancel-button,
input[type="search"]:focus::-webkit-search-cancel-button { background-color: var(--fg); }
input.invalid { border-color: var(--err); background: var(--err-soft-bg); }
input[type="file"] { width: 100%; font-size: var(--fs-body); padding: var(--sp-2) 0; }

/* Checkboxes -----------------------------------------------------------------
   ONE custom control for the whole app, forced here so no page sets its own
   size or colour. A small (14px) rounded box that fills with the accent when
   checked and shows a centred SVG tick (the border-rotate trick drifts the mark
   to the bottom-right corner, so the tick is a background image centred by
   background-position). The mixed state is a centred dash.

   The quiet variant (grey) is for secondary / reveal toggles that should
   recede: apply the .dm-check--quiet modifier, or use one of the shared
   reveal-row containers (.show-inactive-row, .ov-og-oos-row, .activation-inline,
   .profile-empty-check), which adopt it automatically. */
input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  flex: none; box-sizing: border-box;
  width: 14px; height: 14px; margin: 0; vertical-align: middle;
  border: 1.5px solid var(--border-hover); border-radius: 4px;
  background: #fff center / 12px 12px no-repeat; cursor: pointer;
  transition: background-color var(--t-fast), border-color var(--t-fast);
}
input[type="checkbox"]:checked {
  background-color: var(--accent); border-color: var(--accent);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M3.8 7.2 L6.1 9.5 L10.2 4.6'/%3E%3C/svg%3E");
}
input[type="checkbox"]:indeterminate {
  background-color: var(--accent); border-color: var(--accent);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cline x1='4' y1='7' x2='10' y2='7' stroke='%23fff' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
}
input[type="checkbox"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
input[type="checkbox"]:disabled { opacity: 0.45; cursor: not-allowed; }
/* Quiet variant: the checked box drops to muted grey (the tick stays white). */
input[type="checkbox"].dm-check--quiet:checked,
input[type="checkbox"].dm-check--quiet:indeterminate,
.show-inactive-row input[type="checkbox"]:checked,
.show-inactive-row input[type="checkbox"]:indeterminate,
.ov-og-oos-row input[type="checkbox"]:checked,
.ov-og-oos-row input[type="checkbox"]:indeterminate,
.activation-inline input[type="checkbox"]:checked,
.activation-inline input[type="checkbox"]:indeterminate,
.profile-empty-check input[type="checkbox"]:checked,
.profile-empty-check input[type="checkbox"]:indeterminate {
  background-color: var(--muted); border-color: var(--muted);
}

.field { margin-bottom: var(--sp-5); }
.field:last-child { margin-bottom: 0; }
.row { display: flex; gap: var(--sp-4); align-items: center; }
.row > * { flex: 1; }

/* --- Integrated (click-to-edit) controls -------------------------------- *
   Display and edit are ONE control — no separate edit mode to toggle. Mirrors
   the rules-modal title: the value reads as plain text; hovering hints a subtle
   gray surface; focusing turns it into an edit field (blue-tinted). Put it on a
   contenteditable element; an empty one shows its data-placeholder in muted
   italic. There's no border shift, so the surrounding layout never jumps.     */
.editable {
  cursor: text; padding: 1px var(--sp-1); margin: -1px -4px;
  border-radius: var(--r-sm); outline: none; transition: background var(--t-fast);
}
.editable:hover { background: rgba(0, 0, 0, 0.05); }
.editable:focus, .editable.editing { background: rgba(59, 130, 246, 0.10); }
.editable:empty::before { content: attr(data-placeholder); color: var(--muted); font-style: italic; }

/* Edit-in-place row: a display value that swaps for an input + save/cancel
   icon buttons while editing (sidebar ETL rename, file rename). The input wears
   a full accent border to signal it's the live edit field; save/cancel are
   icon-btns tinted green/red on hover. */
.edit-row { display: inline-flex; align-items: center; gap: var(--sp-2); }
.edit-row input {
  flex: 1 1 auto; min-width: 0; padding: 3px 7px; font-size: var(--fs-sm);
  border: 1px solid var(--accent); border-radius: var(--r-sm); background: #fff; color: var(--fg);
}
.edit-row input.invalid { border-color: var(--err); background: var(--err-soft-bg); }
.edit-row .icon-btn.save:hover:not(:disabled)   { color: var(--ok);  background: var(--ok-soft-bg);  border-color: transparent; }
.edit-row .icon-btn.cancel:hover:not(:disabled) { color: var(--err); background: var(--err-soft-bg); border-color: transparent; }

/* 5. BUTTONS =============================================================== */
/* Regular buttons are TEXT-ONLY (icons live on .btn-quiet below, not here).
   Adopted from the live SPA (review 2026-06-12): solid borderless accent fill,
   9px 16px footprint, and hover dims the whole fill via brightness(0.96) —
   the brightness filter is canonical for the primary button. Quiet controls
   that must NOT dim (icon buttons, menu items, link buttons, the filter bar)
   reset it with filter: none on their own hover rules. */
button {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  background: var(--accent); color: #fff; border: 0;
  padding: 9px 16px; border-radius: var(--r-sm); font-size: var(--fs-sm);
  font-family: inherit; font-weight: var(--fw-medium); cursor: pointer;
}
button:hover:not(:disabled) { filter: brightness(0.96); }
button:disabled { background: var(--control-disabled); cursor: not-allowed; }

button.secondary, .btn-secondary {
  background: transparent; color: var(--accent); border: 1px solid var(--border);
}
button.secondary:hover:not(:disabled), .btn-secondary:hover:not(:disabled) {
  filter: none; background: var(--accent-icon-hover-bg); border-color: var(--accent); color: var(--accent);
}
/* A link that acts as a button. The colours above already apply to any element
   carrying .btn-secondary; the geometry lives on the `button` element rule, so
   an anchor has to be given it here. Use this only where the target is a real
   destination (it keeps the href, so middle-click and cmd-click still open a
   tab); a control that acts on the page stays a <button>. */
a.btn-secondary {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  padding: 6px 12px; border-radius: var(--r-sm);
  font-size: var(--fs-micro); font-weight: var(--fw-medium); text-decoration: none;
}
a.btn-secondary:hover { text-decoration: none; }
button.ghost, .btn-ghost { background: transparent; color: var(--fg); border: 1px solid var(--border); }
button.ghost:hover:not(:disabled) { filter: none; background: var(--surface-hover); border-color: var(--border); }
button.danger, .btn-danger { background: var(--err); }

.link-btn {
  background: transparent; color: var(--accent); border: 0; padding: 0;
  font-size: var(--fs-sm); font-weight: var(--fw-medium); cursor: pointer;
}
.link-btn:hover:not(:disabled) { filter: none; text-decoration: underline; }
.link-btn:disabled { background: transparent; color: var(--muted); cursor: default; }

/* Filter bar (usage dashboards) — adopted verbatim from the live SPA (review
   2026-06-12): a flex bar of stacked label+control pairs with compact 13px
   controls. Its own buttons are smaller than the regular button and hover by
   color swap (no dim); the ghost variant is white with a hairline border.
   The label and input rules skip a dropdown menu's checkbox rows (a
   `label.menu-item` holding an `input[type=checkbox]`), so a .menu opened from
   the bar keeps the menu and checkbox look instead of the bar's text boxes. */
.filters { display: flex; flex-wrap: wrap; gap: var(--sp-4); align-items: flex-end; }
.filters label:not(.menu-item) {
  display: flex; flex-direction: column; font-size: var(--fs-sm);
  font-weight: var(--fw-medium); color: var(--fg); gap: var(--sp-2); margin-bottom: 0;
}
.filters input:not([type="checkbox"]), .filters select {
  font: inherit; font-size: var(--fs-sm); padding: 6px 8px; border: 1px solid var(--border);
  border-radius: var(--r-sm); background: #fff; color: var(--fg); min-width: 130px; width: auto;
}
.filters button {
  background: var(--accent); color: #fff; border: 0; padding: 7px 14px;
  border-radius: var(--r-sm); font-size: var(--fs-sm); font-weight: var(--fw-medium); cursor: pointer;
}
.filters button:hover:not(:disabled) { filter: none; background: var(--accent-hover); }
.filters button.ghost { background: #fff; color: var(--fg); border: 1px solid var(--border); }
.filters button.ghost:hover:not(:disabled) { filter: none; background: var(--surface-hover); }

/* --- Icon buttons -------------------------------------------------------- *
   THE rule (per feedback): an icon button is BORDERLESS at rest. On hover it
   gets a touch of color + a subtle background + a subtle border. The border is
   reserved as transparent at rest (so revealing it on hover causes no 1px
   layout jump) and simply colored in on hover — matching the objects-page "⋯".
   Variants only change the hover color/wash/border color.                    */
.icon-btn, .add-btn, .kebab-btn {
  background: transparent; color: var(--muted);
  border: 1px solid transparent; /* reserved; invisible until hover */
  padding: var(--sp-1) var(--sp-2); cursor: pointer; border-radius: var(--r-sm);
  display: inline-flex; align-items: center; justify-content: center;
  transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
.icon-btn:hover:not(:disabled) {
  filter: none; background: var(--surface-hover); color: var(--fg); border-color: var(--border);
}
/* Kebab "more" trigger — adopted from the live object-row obj-more: hover/open
   shows a WHITE fill + border + fg icon (so it lifts off a tinted/hovered row),
   not the gray icon-button wash. */
.kebab-btn:hover:not(:disabled),
.kebab-btn[aria-expanded="true"] {
  filter: none; background: var(--card); color: var(--fg); border-color: var(--border);
}
/* Fixed footprint (matches the live obj-more) so the kebab never makes a list
   row taller than its disclosure chevron / pills. */
.kebab-btn { width: 30px; height: 22px; padding: 0; }
.kebab-btn svg { width: 18px; height: 18px; }
/* Accent intent (e.g. "+" add) — kept QUIET (per feedback): the icon turns
   accent on hover but rides the same neutral gray wash as a plain icon-btn,
   with NO border. Less prominent than a blue wash + accent border. */
.icon-btn.accent:hover:not(:disabled),
.add-btn:hover:not(:disabled),
.add-btn[aria-expanded="true"] {
  filter: none; background: var(--surface-hover); color: var(--accent); border-color: transparent;
}
/* Destructive intent — softer than the neutral/accent variants (per feedback):
   transparent at rest (NO background, NO border). The icon delete then takes
   just a plain WHITE fill + red icon on hover — matching the live object-page
   delete (.ws-del / .ws-art-del), which is subtler than a pink wash. (Text
   deletes — the danger menu items — keep the pale pink hover; see below.) The
   explicit rest rule is required because the shared `.danger` class also matches
   the filled `button.danger` rule, whose (0,1,1) specificity would otherwise
   paint a solid red fill + border here; `.icon-btn.danger` (0,2,0) overrides it
   back. */
.icon-btn.danger, .btn-quiet.danger { background: transparent; border-color: transparent; }
.icon-btn.danger:hover:not(:disabled) { background: var(--card); color: var(--err); border-color: transparent; }
.icon-btn svg, .add-btn svg, .kebab-btn svg { width: 16px; height: 16px; display: block; }
.icon-btn:disabled, .add-btn:disabled, .kebab-btn:disabled,
.icon-btn:disabled:hover { background: transparent; border-color: transparent; opacity: 0.4; cursor: not-allowed; }

/* The "+" add control renders a glyph; size it down a touch. */
.add-btn { font-size: var(--fs-h1); line-height: 1; }
.add-btn svg { width: 13px; height: 13px; }

/* --- Quiet icon+text button --------------------------------------------- *
   A labelled action that is BORDERLESS at rest — just an icon + text, no
   background — and picks up the same subtle hover as .icon-btn (touch of color
   + faint background + a border reserved transparent at rest). The text-bearing
   sibling of .icon-btn; use it where a filled button would be too heavy.       */
.btn-quiet {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  background: transparent; color: var(--fg); border: 1px solid transparent;
  padding: var(--sp-2) 10px; border-radius: var(--r-sm);
  font-size: var(--fs-sm); font-family: inherit; font-weight: var(--fw-medium); cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
.btn-quiet svg { width: 13px; height: 13px; flex: 0 0 auto; display: block; }
.btn-quiet:hover:not(:disabled) { background: var(--surface-hover); color: var(--fg); border-color: var(--border); }
.btn-quiet.accent { color: var(--accent); }
.btn-quiet.accent:hover:not(:disabled) { background: var(--accent-icon-hover-bg); color: var(--accent); border-color: var(--accent); }
.btn-quiet.danger { color: var(--err); }
/* Compact quiet button: the small trailing action inside a .card-head (the
   load-sheet upload button, the add-object trigger). Kept short enough that it
   never grows the .card-head row. */
.btn-quiet.compact { font-size: var(--fs-meta); padding: 3px 6px; gap: 5px; line-height: 1.4; }
.btn-quiet.compact svg { width: 12px; height: 12px; }
.btn-quiet.danger:hover:not(:disabled) { background: var(--err-soft-bg); color: var(--err); border-color: transparent; }
.btn-quiet:disabled { background: transparent; border-color: transparent; opacity: 0.4; cursor: not-allowed; }
/* Segmented control: two or three views of one thing, side by side in one
   bordered strip (the Billing tab's Project + WBS | Lines). A role="group"
   of real buttons, the chosen one aria-pressed="true" on the info wash. The
   (0,4,1) hover out-ranks the filled button:hover:not(:disabled) (0,2,1). */
.seg { display: inline-flex; border: 1px solid var(--border); border-radius: var(--r-md); overflow: hidden; }
.seg > button.seg-btn, .seg > button.seg-btn:hover:not(:disabled) {
  background: var(--card); color: var(--muted); border: 0; border-radius: 0; filter: none;
  padding: 3px 10px; font: inherit; font-size: var(--fs-sm); cursor: pointer;
}
.seg > button.seg-btn + button.seg-btn { border-left: 1px solid var(--border); }
.seg > button.seg-btn:hover:not(:disabled) { color: var(--fg); }
.seg > button.seg-btn[aria-pressed="true"] { background: var(--surface-info); color: var(--fg); font-weight: var(--fw-semibold); }

/* Quiet affordance: an icon button that is invisible until its row/group is
   hovered or focused (delete/more/upload on file rows, "+" on lane headings). */
.is-quiet { opacity: 0; transition: opacity var(--t-fast); }
:hover > .is-quiet, :focus-within > .is-quiet,
.is-quiet[aria-expanded="true"] { opacity: 1; }

/* 6. FEEDBACK ============================================================== */
.status { font-size: var(--fs-sm); color: var(--muted); margin-top: var(--sp-4); min-height: 18px; }
.status.ok { color: var(--ok); } .status.err { color: var(--err); } .status.warn { color: var(--warn); }

/* Informational callout / section divider: left accent bar + tinted band. */
.callout, .action-desc, .section-divider {
  background: var(--surface-info); border-left: 3px solid var(--accent);
  padding: 10px 14px; font-size: var(--fs-sm); color: var(--fg);
  border-radius: 0 var(--r-sm) var(--r-sm) 0; margin-bottom: var(--sp-5);
}
.section-divider { padding: var(--sp-2) var(--sp-4); font-size: var(--fs-meta); font-weight: var(--fw-semibold); margin: var(--sp-4) 0 var(--sp-3); }
.callout.ok   { border-left-color: var(--ok);   background: var(--ok-soft-bg); }
.callout.warn { border-left-color: var(--warn); background: var(--sev-warning-bg); }
.callout.err  { border-left-color: var(--err);  background: var(--err-soft-bg); }
.callout:empty, .action-desc:empty { display: none; }

pre.err-detail {
  background: var(--err-soft-bg); border: 1px solid var(--err-soft-border); color: var(--err);
  padding: 10px 12px; border-radius: var(--r-sm); font-size: var(--fs-meta);
  white-space: pre-wrap; word-break: break-word; max-height: 200px; overflow: auto;
}
/* Code: the sunken surface an open row wears, so a block inside an expanded
   row reads as part of it; muted text; an even 10px inset on every side. */
pre.code {
  background: var(--surface-sunken); border: 1px solid var(--border); color: var(--muted);
  border-radius: var(--r-sm); padding: 10px; font-size: var(--fs-meta);
  white-space: pre-wrap; word-break: break-word; max-height: 240px; overflow: auto;
}

/* Toast (no live equivalent yet — defined for uniformity). */
.toast {
  position: fixed; right: var(--sp-6); bottom: var(--sp-6); z-index: 6000;
  display: inline-flex; align-items: center; gap: var(--sp-3);
  padding: 10px 14px; max-width: 360px; background: var(--fg); color: #fff;
  border-radius: var(--r-md); font-size: var(--fs-sm); box-shadow: var(--shadow-dialog);
}
.toast.ok { background: var(--ok); } .toast.err { background: var(--err); }
.toast.warn { background: var(--warn); color: var(--fg); }

/* 7. DATA DISPLAY ========================================================== */
table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
/* Cell padding is two tokens: the vertical one so a preset changes a number
   and the header filter card can anchor to the label's bottom edge (top: 100%
   of a positioned th is its padding edge); the horizontal one so the rhythm
   between columns is one number too (8px a side, so neighbouring content
   sits 16px apart). Every row's end cells inset 16px instead, card or not,
   so the first label lines up with a card heading and a table nested deeper
   (a lane, a settings group) reads the same. */
table { --cell-py: 9px; --cell-px: 8px; }
th, td { text-align: left; padding: var(--cell-py) var(--cell-px); border-top: 1px solid var(--border); vertical-align: middle; }
th:first-child, td:first-child { padding-left: var(--sp-5); }
th:last-child, td:last-child { padding-right: var(--sp-5); }
th { font-size: var(--fs-micro); color: var(--muted); font-weight: var(--fw-semibold); border-top: 0; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
/* A table sitting directly in a section card runs edge to edge by DEFAULT —
   the row separator lines reach the card's borders (the objects page's row
   list and the rules tiers set this house style). The bleed cancels the
   card's --sp-5 padding; the end cells' own 16px inset (above) puts the text
   back under the heading. margin-inline only, so a table's own top/bottom
   margins survive. Direct child on purpose: nested tables (overlay/profile
   grids inside a card's subtree) keep normal geometry. The `scroll` preset
   below keeps the bleed: the negative margin equals the card padding, so
   the card's overflow box clips nothing. */
.card > table { width: calc(100% + 2 * var(--sp-5)); margin-inline: calc(-1 * var(--sp-5)); }
.empty { color: var(--muted); font-size: var(--fs-sm); padding: var(--sp-3) 0; }

/* --- Table presets + the shared table grammar (J1, spa-table-primitive.md) --
   The LOOK of every SPA table lives here, usable with or without table-core.js
   (the JS reads a look, it never sets one). One `data-table` attribute on the
   <table>, space-separated tokens, each matched with [data-table~=]. */
/* dense: the rollup rhythm, 5px vertical padding (the base is 9px); cells
   never wrap, a long value widens the table (pair with `scroll`) instead of
   growing the row. */
[data-table~="dense"] { --cell-py: 5px; }
[data-table~="dense"] th, [data-table~="dense"] td { white-space: nowrap; }
/* compact: the admin-list rhythm (workstreams, roles): 2px, so a row is its
   tallest control (a kebab, a select) plus a hairline of air. */
[data-table~="compact"] { --cell-py: 2px; }
/* headerless: a row list with no <thead>; the first row has no line above it. */
[data-table~="headerless"] tbody tr:first-child td { border-top: 0; }
/* scroll: cells never wrap (a long value widens the table instead of the row
   growing taller, whatever the density token) and the card scrolls sideways
   instead of the page. Nothing else: the edge-to-edge bleed above survives an
   overflow box (the table starts exactly at the card's padding edge, which is
   the clip edge), so a scrolling card looks like every other card. */
[data-table~="scroll"] th, [data-table~="scroll"] td { white-space: nowrap; }
.card:has(> table[data-table~="scroll"]) { overflow-x: auto; }
/* fit: the table takes its content's width instead of the card's, so the
   columns sit together on the left and the spare room is on the right. It
   keeps the card's left bleed; (0,2,1) out-ranks .card > table's width. */
table[data-table~="fit"], .card > table[data-table~="fit"] { width: auto; }
/* bleed: the card-table geometry (edge to edge; the end cells' 16px inset is
   the base rule's) for a table nested deeper than a card's direct child,
   such as inside a settings group whose own dividers already run to the
   card edges. Same numbers as `.card > table` above. */
table[data-table~="bleed"] { width: calc(100% + 2 * var(--sp-5)); margin-inline: calc(-1 * var(--sp-5)); }
/* Stacked identity cell: a primary line over a muted meta line (name over
   email, a name over a date). Sort reads the primary, search reads both. The
   primary may be an anchor (row-as-link pages): it reads as plain text, with
   an underline only on direct hover. */
.cell-primary { display: inline-block; font-weight: var(--fw-medium); color: var(--fg); text-decoration: none; }
a.cell-primary:hover { text-decoration: underline; }
.cell-meta { display: block; font-size: var(--fs-meta); color: var(--muted); }
/* A quiet note on the primary's own line: italic, muted, meta size (the rule
   title beside an Audit trail sentence). */
.cell-aside { font-size: var(--fs-meta); color: var(--muted); font-style: italic; margin-left: var(--sp-2); }
/* table-group: a one-level group band inside a tbody (a workstream, a sheet),
   one cell spanning every column. The ETL roster's band adopted: a sunken
   strip framed by the hairline pair, a quiet spaced micro label in muted so
   the rows under it lead; the count rides in .table-group-meta. Painted by
   attach's groupBy, or host-rendered on a presets-only table. With the
   collapsed border model the band's bottom line and the next row's top line
   are one. */
tr.table-group > td {
  padding-block: 4px; background: var(--surface-sunken);
  border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
  font-size: var(--fs-micro); font-weight: var(--fw-semibold); letter-spacing: 0.04em; color: var(--muted);
}
.table-group-meta { font-weight: var(--fw-regular); }
/* The trailing actions slot (a kebab per row): tight, right-aligned, so every
   row's trigger lines up under the header; excluded from sort and search, and
   a click inside it never counts as a row click (table-core.js). */
th.actions-col, td.actions-col { text-align: right; width: 1%; white-space: nowrap; }
/* The leading pick column (a checkbox per row, a select-all or nothing in
   the header): the 14px box in a 16px gutter each side, and the column after
   it drops its left padding, so a row reads [16][box][16][label] and the
   label sits where a first column's would in a table without one (16px in).
   One width, the box plus its gutters, so a fixed-layout table needs no pin
   of its own. The box is a block, so the cell's middle alignment centres it
   exactly (an inline box rides the line's baseline and sits a few px high). */
th.pick-col, td.pick-col { width: calc(14px + 2 * var(--sp-5)); padding-left: var(--sp-5); padding-right: var(--sp-5); white-space: nowrap; vertical-align: middle; }
th.pick-col + th, td.pick-col + td { padding-left: 0; }
.pick-col > input[type="checkbox"] { display: block; }
/* Clipped cell text: .cell-clip is a one-line box that clips with an ellipsis.
   When the text really is cut, tooltip.js marks the box .is-clipped while the
   pointer is over it (measured, so a name that shows in full never pops a
   panel) and the box floats its whole text over the row while hovered: the
   cell anchors the panel and lets it escape. The trigger is the box, never
   its cell: a sibling in the same cell (the object page's row "view" button)
   stays hoverable and clickable, where a cell-hover panel would cover it. A
   host adds the class to the box and nothing else, or data-clip-trigger="tail"
   beside it to open the reveal from the ellipsis only (the load order cards,
   whose name is a link's label first). No containment here: a
   column with no width would collapse to nothing; a host that wants its
   column to give way when the table is constrained adds `contain: inline-size`
   beside a cell width (the object page's lanes do) and the reveal lifts it. */
.cell-clip {
  display: block; max-width: 100%; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
:where(td, th):has(.cell-clip) { position: relative; overflow: visible; }
.cell-clip.is-clipped:hover {
  position: absolute; left: 0; top: 50%; transform: translateY(-50%); z-index: 6;
  width: max-content; max-width: min(560px, 88vw);
  overflow: visible; white-space: normal; contain: none;
  padding: 5px 12px 5px 18px;
  background: var(--card, #fff); border: 1px solid var(--border); border-radius: 6px;
  box-shadow: var(--shadow-menu, 0 6px 20px rgba(16, 24, 40, 0.16));
}
/* Search toolbar above a table: a fixed-width search input, left-aligned.
   (0,2,1) so it out-ranks the input[type="search"] base rule, which would
   otherwise force width:100% and its own padding. */
.table-toolbar { display: flex; align-items: center; gap: var(--sp-3); margin: 0 0 14px; }
.table-toolbar input[type="search"] { width: 260px; max-width: 100%; flex: 0 0 auto; padding: 8px 10px; font-size: var(--fs-sm); border-radius: 4px; }
/* Count marker beside a card title: "(12)", or "(3 of 12)" while filtered. */
.table-count { color: var(--muted); font-weight: var(--fw-regular); font-size: var(--fs-sm); }
/* Sort chrome: table-core.js wraps a sortable header's label in a real
   <button> (keyboard for free) and keeps aria-sort on the th. The button is
   reset to read as the header itself; the arrow is a ::after glyph, hidden at
   rest (opacity, so it keeps its box and nothing shifts on reveal), shown on
   hover or keyboard focus, solid on the active column. The (0,3,2) hover rule
   out-specifies the global filled button:hover:not(:disabled) (0,2,1). */
th > button.th-sort, th > button.th-sort:hover:not(:disabled) {
  background: transparent; border: 0; padding: 0; margin: 0; box-shadow: none; filter: none;
  font: inherit; color: inherit; letter-spacing: inherit; text-align: inherit; cursor: pointer;
  border-radius: 2px;
}
/* The label box: the sort button, or on a column that filters but does not
   sort the focusable span.th-label table-core.js wraps the text in. Both are
   inline-flex so the sort arrow and the filter mark sit beside the text as
   flex items, reversed on a .num header so the label stays flush right. */
th > button.th-sort, th > span.th-label { display: inline-flex; align-items: center; gap: 4px; vertical-align: top; white-space: nowrap; }
th.num > button.th-sort, th.num > span.th-label { flex-direction: row-reverse; }
/* The arrow is a fixed 8x4 box (a border triangle with no bottom edge) that
   ascending flips in place with a transform, so its geometry never changes
   with the state. A flex container's baseline comes from its main-start item,
   which under row-reverse (.num headers) is the arrow, not the label: a box
   that changed per state moved the label with it on every click. The button's
   vertical-align: top takes that baseline out of the placement altogether. It
   is exactly one line tall, so its label sits where plain header text sits,
   in either direction. */
th > button.th-sort::after {
  content: ""; flex: none; width: 0; height: 0;
  border: 4px solid transparent; border-bottom-width: 0; border-top-color: currentColor;
  opacity: 0;
}
/* The arrow's 12px (gap + box) overhangs into the cell padding instead of
   widening the header, so a column stays as wide as its label and the sort
   chrome never makes a table scroll (the usage rollups tipped over by a few
   px). Text headers overhang to the right, .num headers (arrow before the
   label) to the left; cells carry --cell-px (8px) each side, so the overhang
   never reaches a neighbour's content. Headers with a filter card keep the
   reserved width: their funnel mark sits beyond the arrow and would collide. */
th:not(.has-filter) > button.th-sort::after { margin-right: -12px; }
th.num:not(.has-filter) > button.th-sort::after { margin-right: 0; margin-left: -12px; }
th[aria-sort="ascending"] > button.th-sort::after { transform: scaleY(-1); opacity: 1; }
th[aria-sort="descending"] > button.th-sort::after { opacity: 1; }
th[aria-sort="none"] > button.th-sort:hover::after,
th[aria-sort="none"] > button.th-sort:focus-visible::after { opacity: 0.5; }
th[aria-sort="ascending"], th[aria-sort="descending"] { color: var(--fg); }
th > button.th-sort:focus-visible, th > span.th-label:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
/* Column filters (columnFilters: true): table-core.js gives each filterable
   header a hover card (.th-filter) holding its search input, shown while the
   library holds th.filter-open (pointer over the label or anywhere in the
   header, or keyboard focus inside it). Anchored 2px under the label (the
   th's padding edge minus its --cell-py bottom padding), its edge flush with
   the label's: left on a text column, right on a .num one, stepped by the
   wider end-cell inset and pulled to zero after a pick column. z-index
   matches .menu, above the header/sidebar chrome. */
th.has-filter { position: relative; }
th > .th-filter {
  display: none; position: absolute; top: calc(100% - var(--cell-py) + 2px); left: var(--cell-px); z-index: 100;
  min-width: 200px; padding: var(--sp-2); background: #fff;
  border: 1px solid var(--border); border-radius: var(--r-md); box-shadow: var(--shadow-menu);
  font-weight: var(--fw-regular); text-align: left;
}
th:first-child > .th-filter { left: var(--sp-5); }
th.pick-col + th > .th-filter { left: 0; }
th.num > .th-filter { left: auto; right: var(--cell-px); }
th.num:last-child > .th-filter { right: var(--sp-5); }
th.filter-open > .th-filter { display: block; }
th > .th-filter input.col-filter { width: 100%; min-width: 0; padding: 6px 8px; font-size: var(--fs-sm); border-radius: var(--r-sm); }
/* Filter mark: a small funnel on the outer side of the label (after the text,
   before it on a .num header) shown whenever the column's filter entry is
   non-blank, card open or closed (typing already filters). Painted in the
   muted gray explicitly, not currentColor, so a sorted column (th color: fg)
   does not darken it. It keeps its box when hidden, so nothing shifts. */
th.has-filter > button.th-sort::before, th.has-filter > span.th-label::before {
  content: ""; order: 1; flex: none; width: 9px; height: 9px; visibility: hidden;
  background: var(--muted); clip-path: polygon(0 0, 100% 0, 62% 46%, 62% 100%, 38% 100%, 38% 46%);
}
th.has-filter.filtered > button.th-sort::before,
th.has-filter.filtered > span.th-label::before { visibility: visible; }
/* Status rows painted by table-core.js (loading / error / empty / no match)
   when the host owns no empty elements of its own. */
tr.table-status > td { color: var(--muted); font-size: var(--fs-sm); }
tr.table-status[data-status="error"] > td { color: var(--err); }
/* The live region table-core.js prepends as a visually hidden caption. */
caption.table-live { caption-side: top; text-align: left; }

/* Data-grid column header (the in-place data viewer) — adopted from the live
   objects page (review 2026-06-12). A dense three-line header cell: SAP
   structure name (accent, uppercase + tracked — structure names are
   intrinsically uppercase codes, a deliberate exception), technical field
   name (bold), human label (muted). The grid body is monospace 10px. */
.dm-dv-grid { font-family: var(--font-mono); font-size: var(--fs-nano); line-height: 1.2; }
.dm-dv-rowhead { background: #fafbfd; border-bottom: 1px solid var(--border); }
.dm-dv-hcell {
  padding: 3px 6px; border-right: 1px solid #eef0f4;
  display: flex; flex-direction: column; gap: 1px; overflow: hidden;
}
.dm-dv-hgroup {
  font-weight: var(--fw-semibold); color: var(--accent); font-size: 8px;
  text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.dm-dv-hname { font-weight: var(--fw-bold); color: var(--fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dm-dv-hlabel { font-weight: var(--fw-regular); color: var(--muted); font-size: 9px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Pill: rounded status tag. Neutral by default, because a plain pill is a
   count or a label and must not read as a link; a pill that carries a verdict
   says so with a modifier. This is the only copy: app.css used to hold a
   second one that loaded later and won (the trap the table copy fell into). */
.pill { display: inline-block; font-size: var(--fs-micro); padding: 2px var(--sp-3); border-radius: var(--r-pill); background: var(--surface-hover); color: var(--fg); }
.pill.ok, .pill.good { background: var(--ok-soft-bg); color: var(--ok); }
.pill.bad { background: var(--err-soft-bg); color: var(--err); }
.pill.off { background: var(--surface-hover); color: var(--muted); }

/* Badge: small emphasis chip — sentence case (no uppercase, per feedback),
   with the FIRST LETTER capitalized (review 2026-06-12: "Error", not "error"). */
.badge { display: inline-block; padding: 1px 7px; border-radius: 10px; font-size: var(--fs-micro); font-weight: var(--fw-semibold); }
.badge::first-letter { text-transform: uppercase; }
.badge.sev-error { background: var(--sev-error-bg); color: var(--err); }
.badge.sev-warning { background: var(--sev-warning-bg); color: var(--warn); }
.badge.sev-info { background: var(--sev-info-bg); color: var(--muted); }
.badge.subtle { background: #eef2f7; color: var(--fg); font-weight: var(--fw-medium); }
.badge.edited { background: var(--ok-soft-bg); color: var(--ok-soft-fg); font-weight: var(--fw-medium); }
/* History chips: an audit action as a pill, first shared by the rule history
   modal and the Audit trail. Create is green, update blue, delete red; restore
   and revert keep their soft tints; any other action is the neutral chip.
   Sentence case with a first-letter cap, like .badge. */
.history-chip {
  display: inline-block; font-size: var(--fs-micro); font-weight: 600; padding: 2px 8px;
  border-radius: 999px; background: var(--surface-hover); color: var(--muted); white-space: nowrap;
}
.history-chip::first-letter { text-transform: uppercase; }
.history-chip-create { background: #dcfce7; color: #166534; }
.history-chip-update { background: var(--accent-chip-bg); color: var(--accent-chip-fg); }
.history-chip-delete { background: #fee2e2; color: #991b1b; }
.history-chip-restore { background: var(--ok-soft-bg); color: var(--ok-soft-fg); }
.history-chip-revert { background: var(--sev-warning-bg); color: var(--warn); }

/* Environment badge: loud header-chrome marker for non-production stacks
   (sidebar.js injects it on quality). Adopted verbatim from the live SPA
   (review 2026-06-12) as THE deliberate uppercase exception — a warning
   wants to shout; everything else stays sentence case. */
.env-badge {
  display: inline-block; margin-left: 10px; padding: 2px 9px;
  border-radius: var(--r-pill); background: #fde047; color: #713f12;
  font-size: var(--fs-micro); font-weight: var(--fw-bold);
  text-transform: uppercase; letter-spacing: 0.04em;
  vertical-align: middle; line-height: 1.6;
}

/* Object tag: small outlined pill flag on object/file rows — adopted from the
   live objects page (review 2026-06-12), uppercase by design (it renders codes
   and flags, e.g. the custom-loadsheet marker). .obj-tag-custom is the compact
   circular "C" variant. */
.obj-tag {
  display: inline-block; font-size: var(--fs-nano); font-weight: var(--fw-bold); line-height: 1;
  padding: 4px 9px; border-radius: var(--r-pill); border: 1.5px solid var(--border);
  text-transform: uppercase; letter-spacing: 0.03em;
}
.obj-tag-custom {
  box-sizing: border-box; width: 16px; height: 16px; padding: 0;
  border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
  font-size: 9px; letter-spacing: 0;
}

/* Progress bar: quiet meter for progress rollups (project/cycle overviews,
   C4 progress reporting). Track rides the run-bar grey; the fill is YELLOW
   (--warn) while under way (review 2026-07-16 — progress is status semantics,
   not an action, so it doesn't wear accent) and --ok at completion (add
   .done). Blocked is NEVER encoded in the bar — surface it beside the % as a
   pill/count (spec: a flag, not a deduction). Pair with a tabular-nums %
   label. */
.progress-bar {
  height: 6px; border-radius: var(--r-pill); background: var(--run-bar-bg);
  overflow: hidden;
}
.progress-bar > i {
  display: block; height: 100%; border-radius: inherit;
  background: var(--warn); transition: width 0.3s ease;
}
.progress-bar.done > i { background: var(--ok); }
@media (prefers-reduced-motion: reduce) { .progress-bar > i { transition: none; } }
/* The % label riding next to a bar. */
.progress-pct {
  font-size: var(--fs-sm); font-weight: var(--fw-medium);
  font-variant-numeric: tabular-nums; text-align: right;
}
.progress-pct.muted { color: var(--muted); font-weight: var(--fw-regular); }

/* Status dot: bare colored dot (sidebar active-cycle, adoption status). */
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #cbd5e1; vertical-align: middle; }
.dot.ok { background: var(--ok); } .dot.warn { background: var(--warn); }
.dot.err { background: var(--err); } .dot.busy { background: #2563eb; animation: dm-pulse 1.2s infinite; }

/* Chip: field token, monospace, removable. */
.chip { display: inline-flex; align-items: center; gap: var(--sp-1); padding: 1px var(--sp-3); border-radius: 12px; font-size: var(--fs-micro); font-family: var(--font-mono); cursor: pointer; border: 1px solid transparent; }
.chip.primary { background: var(--accent-chip-bg); color: var(--accent-chip-fg); border-color: var(--accent-chip-border); }
.chip.condition { background: var(--surface-hover); color: var(--muted); border-color: var(--hover-strong); }
.chip.target { background: var(--sev-warning-bg); color: var(--warn); border-color: var(--sev-warning-border); }
.chip:hover { filter: brightness(0.95); }
.chip .role { font-size: 9px; opacity: 0.65; text-transform: uppercase; }

/* Check-type pill: interactive category pill. Cool-only palette — warm tones
   (red/amber/green) stay reserved for severity. The full taxonomy below. */
.check-type-pill {
  /* inline-block (not flex): ::first-letter is inert on flex containers. */
  display: inline-block; padding: 1px var(--sp-3);
  border-radius: 10px; font-size: var(--fs-micro); font-weight: var(--fw-medium);
  letter-spacing: 0.02em; background: #eef2f7; color: var(--fg);
}
.check-type-pill:focus-within { outline: 2px solid var(--accent); outline-offset: 1px; }
.check-type-pill::first-letter { text-transform: uppercase; } /* matches .badge (2026-06-12) */
.check-type-pill.ck-mandatory        { background: #e2e8f0; color: #475569; } /* slate */
.check-type-pill.ck-format           { background: #dbeafe; color: #1e40af; } /* blue */
.check-type-pill.ck-allowed_values   { background: #e0e7ff; color: #3730a3; } /* indigo */
.check-type-pill.ck-regex            { background: #ede9fe; color: #6d28d9; } /* purple */
.check-type-pill.ck-uniqueness       { background: #cffafe; color: #155e75; } /* cyan */
.check-type-pill.ck-cross_field      { background: #e0f2fe; color: #075985; } /* sky */
.check-type-pill.ck-cross_sheet      { background: #ddd6fe; color: #5b21b6; } /* violet */
.check-type-pill.ck-cross_object_ref { background: #f3e8ff; color: #7e22ce; } /* lavender */
.check-type-pill.ck-structural       { background: #fef3c7; color: #92400e; } /* amber (structural only) */
.check-type-pill.ck-value_map        { background: #cffafe; color: #155e75; } /* cyan, shared with uniqueness (2026-06-12) */
.check-type-pill.ck-direct_copy      { background: #e3ecd5; color: #4b6b32; } /* moss — adopted from the live SPA (2026-06-12) */
.check-type-pill.ct-auto, .check-type-pill.deriving { background: #eef2f7; color: var(--muted); font-style: italic; }

/* Download / result links: accent-soft tiles. */
.downloads a, a.download-tile {
  display: block; padding: var(--sp-2) var(--sp-3); background: var(--accent-soft-bg);
  border: 1px solid var(--accent-soft-border); border-radius: var(--r-sm);
  color: var(--accent); text-decoration: none; margin-bottom: var(--sp-1); font-size: var(--fs-sm);
}
.downloads a:hover, a.download-tile:hover { background: var(--accent-soft-bg-hover); text-decoration: none; }

/* Inline count cluster: small icon + number, repeated (object-row L3 counts). */
.count-group { display: inline-flex; align-items: center; gap: var(--sp-4); font-size: var(--fs-meta); color: var(--muted); }
.count-group .cnt { display: inline-flex; align-items: center; gap: 5px; }
.count-group .cnt svg { width: 13px; height: 13px; display: block; }

/* 8/9. OVERLAYS — dialog, menu, tooltip =================================== */
dialog { border: 1px solid var(--border); border-radius: var(--r-lg); padding: 0; background: var(--card); color: var(--fg); }
/* showModal() focuses the dialog itself; suppress the resulting focus ring on
   the container (the "dark edge"). Interactive children keep their own rings. */
dialog:focus, dialog:focus-visible { outline: none; }
dialog::backdrop { background: var(--backdrop); }

.dialog-head { display: flex; align-items: center; gap: var(--sp-3); padding: 14px 18px; border-bottom: 1px solid var(--border); }
.dialog-head h3 { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Muted markers riding inline after the title: area label + rule number. The
   title itself stays subtle-but-present; the meta recedes. */
.dialog-head h3 .rule-meta { color: var(--muted); font-weight: var(--fw-regular); font-size: var(--fs-sm); margin-left: var(--sp-1); }
.dialog-head h3 .rule-num  { color: var(--muted); font-weight: var(--fw-medium);  font-size: var(--fs-sm); margin-left: var(--sp-2); }
/* The dialog title is click-to-edit: give it the .editable control (§4) —
   display and edit integrated in one element, no border shift so the header
   never jumps. */
/* Standard-dialog convention: save / cancel / delete / close live in the HEADER
   as icon-btns (see §5) — there is NO bottom button row. (Only the compact
   .confirm-dialog below carries its own actions row.) */
.dialog-head-actions { display: flex; gap: var(--sp-1); align-items: center; }
.dialog-body { padding: var(--sp-5) 18px; overflow: auto; max-height: 70vh; }
.dialog-body.roomy { padding-top: var(--sp-8); padding-bottom: var(--sp-8); }

.confirm-dialog { min-width: 320px; max-width: 420px; padding: var(--sp-6); border: 1px solid var(--border); border-radius: var(--r-lg); background: #fff; }
.confirm-dialog .actions { display: flex; gap: var(--sp-3); justify-content: flex-end; margin-top: var(--sp-5); }
.confirm-dialog button { padding: 7px 14px; font-size: var(--fs-sm); }

/* Field grid ("Applies to" detail): muted label column + value column. */
.field-grid { display: grid; grid-template-columns: 80px 1fr; gap: var(--sp-2) var(--sp-4); align-items: baseline; }
.field-grid .fg-label { color: var(--muted); font-size: var(--fs-meta); font-weight: var(--fw-semibold); }
.field-grid .fg-value { font-size: var(--fs-sm); }
.field-grid .fg-empty { color: var(--muted); font-style: italic; }

/* --- Menu: THE reusable dropdown (generalized from the "⋯" kebab menu). ---
   Trigger it with a .kebab-btn (icon-btn). Anchor a relatively-positioned
   wrapper (.menu-wrap) and drop this absolutely beneath it, right edge aligned
   to the trigger. Used for object/file actions and intended for any contextual
   action list across the app. */
.menu-wrap { position: relative; display: inline-flex; }
.menu {
  /* Anchored to .menu-wrap and opening downward, right-aligned to the trigger.
     z-index 100 lifts the menu ABOVE the header/sidebar chrome (both at z-index
     50) — at 50 it tied with them and lost to later-painted chrome. */
  position: absolute; right: 0; top: calc(100% + 4px); z-index: 100;
  min-width: 180px; padding: var(--sp-1); background: #fff;
  border: 1px solid var(--border); border-radius: var(--r-md); box-shadow: var(--shadow-menu);
}
/* Menu heading — sentence case, first letter capitalized (review 2026-06-12:
   "Validation reports", not "VALIDATION REPORTS"). */
.menu-head {
  font-size: var(--fs-micro); font-weight: var(--fw-semibold); color: var(--muted);
  padding: 2px 6px 6px;
}
.menu-head::first-letter { text-transform: uppercase; }
.menu-item {
  /* Block layout (mirrors the live .obj-menu-item / .ws-menu-item) so a wrapped
     multi-line label keeps the same left padding on every line. */
  display: block; width: 100%; text-align: left; padding: var(--sp-3) 10px;
  background: transparent; color: var(--muted); border: 0; border-radius: var(--r-sm);
  font-size: var(--fs-sm); font-weight: var(--fw-regular); cursor: pointer;
}
/* :not(:disabled) lifts this to (0,3,0) so it out-specifies the global filled
   `button:hover:not(:disabled)` (0,2,1) that would otherwise wash these <button>
   menu rows in accent-blue. Soft neutral gray, not the bluish --surface-info. */
.menu-item:hover:not(:disabled) { filter: none; background: var(--surface-hover); }
/* Subtle danger — mirrors .btn-quiet.danger: red icon + text on a transparent
   ground, soft-red wash on hover. The `background`/`border-color` resets and the
   :not(:disabled) on hover are REQUIRED to out-specify the filled `button.danger`
   rule (0,1,1 at rest / 0,3,1 on hover) that would otherwise paint a solid red
   fill here and a dark --err-strong fill on hover (same trap as .icon-btn.danger). */
.menu-item.danger { color: var(--err); background: transparent; border-color: transparent; }
.menu-item.danger:hover:not(:disabled) { color: var(--err); background: var(--err-soft-bg); }
.menu-sep { height: 1px; margin: var(--sp-1) 2px; background: var(--border); }

/* Tooltip: dark, compact, shadowed. THE single source of truth for the tip
   skin — the CSS-hover styleguide specimen (.tooltip) and the runtime JS tip
   (.dm-tooltip, driven by tooltip.js) render from the SAME declarations so they
   can never drift. tooltip.js injects no styles of its own. */
.tooltip-host { position: relative; display: inline-flex; }
.tooltip, .dm-tooltip {
  /* Hug the content, but never grow past a comfortable reading width: short
     tips are only as wide as their text; long ones wrap at the cap. */
  white-space: normal; width: max-content; max-width: min(var(--tooltip-width), 90vw);
  background: var(--tooltip-bg); color: #fff; font-size: var(--fs-micro);
  line-height: 1.45; padding: var(--sp-3) 10px; border-radius: var(--r-md);
  box-shadow: var(--shadow-tooltip); pointer-events: none;
}
/* Long tooltips lead with a title line (semibold, body size), the body follows
   beneath. ONE title treatment for every tip — the dark tips here and the light
   .stg-tip card all render the header identically. */
.tooltip-title, .dm-tooltip-title {
  display: block; font-weight: var(--fw-semibold); font-size: var(--fs-micro);
  margin-bottom: var(--sp-1);
}
/* CSS-hover specimen (styleguide): anchored above its host, revealed on hover. */
.tooltip {
  position: absolute; bottom: calc(100% + 6px); left: 0; z-index: 60;
  opacity: 0; visibility: hidden;
}
.tooltip-host:hover .tooltip { opacity: 1; visibility: visible; }
/* Runtime JS tip (tooltip.js): viewport-fixed, JS sets top/left, fades in. */
.dm-tooltip {
  position: fixed; z-index: 1000; top: 0; left: 0;
  opacity: 0; visibility: hidden; transition: opacity 0.14s ease;
}
.dm-tooltip.is-visible { opacity: 1; visibility: visible; }
/* Short single-line label tips ("Close", "More actions") get a tighter pad. */
.dm-tooltip.is-compact { padding: 5px 9px; }
.dm-tooltip-title[hidden] { display: none; }
.dm-tooltip-body { display: block; white-space: pre-line; }

/* Rich hover CARD skin (light): the shared base for the two floating cards that
   carry structured content a flat tip can't — the stage hover card (.stg-tip,
   stage-strip.css) and the data-viewer column card (.col-tip-float, ws-panel.css).
   They live in per-route stylesheets that never load together, so the skin lives
   here, once; each route file keeps only its own inner-content rules. Same tokens
   as the dark tip (radius, shadow, micro text) on a light surface. */
.tip-card {
  position: fixed; z-index: 1000; width: max-content; max-width: 360px;
  white-space: normal; background: var(--card); border: 1px solid var(--border);
  border-radius: var(--r-md); padding: var(--sp-2) var(--sp-3);
  box-shadow: var(--shadow-tooltip); pointer-events: none; font-size: var(--fs-micro);
}

/* 10. DISCLOSURE / COLLAPSIBLE ============================================= */
/* Canonical disclosure: a chevron on the LEFT of the title that rotates 90°
   when its container opens (per feedback — triangle leads the title). */
.disclosure {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; padding: 0; background: transparent; color: var(--muted);
  border: 0; border-radius: var(--r-sm); cursor: pointer;
}
/* padding: 0 because the base button's 9px 16px would leave the 22px box no
   room for its chevron; a quiet control, so no dimming on hover either. */
.disclosure:hover:not(:disabled) { filter: none; color: var(--fg); }
.disclosure svg { width: 15px; height: 15px; display: block; transition: transform var(--t-fast); }
.is-open > .disclosure svg, .disclosure.is-open svg { transform: rotate(90deg); }
/* The history chevron: the rule history modal's expander (rules.js), also the
   leading cell of every Audit trail row (history.js). The disclosure's quiet
   22px box with a 14px glyph. The modal rotates it from its entry's .is-open;
   a row's button carries .is-open itself. */
.history-expand {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; padding: 0; background: transparent; color: var(--muted);
  border: 0; border-radius: var(--r-sm); cursor: pointer;
}
.history-expand:hover:not(:disabled) { filter: none; color: var(--fg); }
.history-expand svg { width: 14px; height: 14px; display: block; transition: transform var(--t-fast); }
.history-expand.is-open svg { transform: rotate(90deg); }

/* Collapsible section (e.g. rules tiers): bordered card, sticky header with the
   chevron leading, title + count, actions trailing. */
.collapsible { border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--card); margin-bottom: var(--sp-6); overflow: hidden; }
.collapsible > .collapsible-head {
  display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-4) var(--sp-4);
  cursor: pointer; user-select: none; position: sticky; top: 0; z-index: 10;
  background: var(--card); border-bottom: 1px solid var(--border);
}
.collapsible.collapsed > .collapsible-head { border-bottom: 0; }
.collapsible.collapsed > .collapsible-body { display: none; }
.collapsible-head .title { font-weight: var(--fw-semibold); font-size: var(--fs-h2); letter-spacing: -0.01em; }
.collapsible-head .count { color: var(--muted); font-size: var(--fs-sm); }
.collapsible-head .head-actions { margin-left: auto; display: inline-flex; align-items: center; gap: 10px; }
.collapsible-body { padding: 0; }

/* List rows inside a collapsible / list (rule rows, object rows): no chrome,
   hairline-separated, hover wash, open state tints. The chevron leads (left). */
.list-row { border: 0; border-bottom: 1px solid var(--border); background: #fff; cursor: pointer; transition: background 0.1s; }
.list-row:last-child { border-bottom: 0; }
.list-row.is-open { background: var(--surface-sunken); }

/* Platform default — the canonical interactive-row hover wash. Any row that
   opts in — a `data-row` attribute, or the .list-row primitive — gets it for
   free, so a new row list needs NO per-page :hover rule. Kept at :where()-zero
   specificity (only the :hover pseudo-class counts) so a page can still
   override for an open/active/selected resting state at normal specificity
   (e.g. .list-row.is-open above still wins when a sunken row is hovered). */
:where([data-row], .list-row):hover { background: var(--row-hover); }
.list-row .row-top { display: flex; align-items: center; gap: var(--sp-2); padding: 10px var(--sp-7); }
.list-row .row-id { color: var(--muted); font-family: var(--font-mono); font-size: var(--fs-micro); opacity: 0.7; }
.list-row .row-id:hover { color: var(--accent); opacity: 1; }

/* 11. SIDEBAR NAVIGATION =================================================== */
/* The 200px sidebar's selectors, captured as a reusable nav vocabulary. */
.nav { background: var(--card); border-right: 1px solid var(--border); padding: 12px 0; font-size: var(--fs-meta); }
.nav-label { font-size: var(--fs-meta); color: var(--muted); font-weight: var(--fw-semibold); padding: var(--sp-1) 14px; }
.nav-list { list-style: none; margin: 0; padding: 0; }
.nav-list li { padding: var(--sp-1) 14px; cursor: pointer; color: var(--fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.nav-list li:hover { background: var(--surface-hover); }
/* Subtle selection — a quiet gray fill, not an accent block. */
.nav-list li.is-selected { background: var(--bg); }
/* Nested child (ETL cycle): indented, muted, with a rounded accent bar when
   active and a small green status dot when it's the active cycle. */
.nav-list li.nav-child { position: relative; margin-left: 16px; padding-left: 14px; color: var(--muted); border-radius: var(--r-sm) 0 0 var(--r-sm); }
.nav-list li.nav-child.is-selected { color: var(--fg); }
.nav-list li.nav-child.is-selected::before { content: ""; position: absolute; left: 5px; top: 4px; bottom: 4px; width: 3px; background: var(--accent); border-radius: var(--r-pill); }
.nav-list li.nav-child.is-active { padding-right: 24px; }
.nav-list li.nav-child.is-active::after { content: ""; position: absolute; right: 12px; top: 50%; transform: translateY(-50%); width: 6px; height: 6px; border-radius: 50%; background: var(--ok); }

/* 12. TIER CASCADE ("Applies to" selector) ================================ */
/* Chevron-shaped segments in one bordered strip: General → Object → Project.
   General is text-only; Object/Project tint when "lit", stronger when "origin"
   (the rule's own level); a disabled-for-project segment goes muted + struck. */
.cascade { display: inline-flex; align-items: stretch; height: 30px; font-size: var(--fs-sm); border: 1px solid var(--border); border-radius: var(--r-sm); overflow: hidden; color: var(--muted); }
.cascade-seg { display: inline-flex; align-items: center; background: transparent; color: inherit; border: 0; padding: 0 18px 0 14px; font: inherit; cursor: pointer; position: relative; clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 50%, calc(100% - 10px) 100%, 0 100%); margin-right: -10px; }
.cascade-seg:first-child { width: 100px; justify-content: center; padding: 0; }
.cascade-seg + .cascade-seg { padding-left: 22px; clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 50%, calc(100% - 10px) 100%, 0 100%, 10px 50%); }
.cascade-seg:last-child { padding-right: 16px; margin-right: 0; clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 10px 50%); }
.cascade-seg:first-child:last-child { padding: 0; clip-path: none; }
/* State model (mirrors rules.js): `origin` = the segment the rule lives on;
   `lit` = the origin AND every segment DOWNSTREAM of it (so the origin is also
   lit). General as lit-only shows just fg color; Object/Project lit get the
   medium tint; any origin gets the strong tint. Specificity is split first-
   child vs not, and origin/disabled are declared AFTER lit so they win where
   they coexist (origin always also carries lit). */
.cascade-seg:first-child.lit { color: var(--fg); }
.cascade-seg:not(:first-child).lit { background: var(--accent-tint); color: var(--fg); }
.cascade-seg:first-child.origin,
.cascade-seg:not(:first-child).origin { background: var(--accent-tint-strong); color: var(--accent); font-weight: var(--fw-semibold); cursor: default; }
.cascade-seg:not(.origin):not(:first-child):hover { background: var(--accent-wash); }
.cascade-seg.disabled-for-project,
.cascade-seg.disabled-for-project.lit,
.cascade-seg.disabled-for-project.origin { background: transparent; color: var(--muted); font-weight: var(--fw-regular); text-decoration: line-through; }

/* 13. FILE / LIST ROWS + STATUS SPINNERS =================================== */
/* Workspace file rows (source / transformed / reports). Filenames read as
   plain text (not blue links) with a hover underline; trailing actions are
   quiet (hover-revealed). Per feedback: a SUBTLE border now separates rows. */
.file-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.file-row { display: flex; align-items: center; gap: var(--sp-3); font-size: var(--fs-sm); padding: var(--sp-2) 0; border-top: 1px solid var(--border); }
.file-row:first-child { border-top: 0; }
.file-row a, .file-row .file-name { flex: 1 1 auto; min-width: 0; color: var(--fg); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-row a:hover { text-decoration: underline; }
.file-row .file-meta { font-size: var(--fs-micro); color: var(--muted); white-space: nowrap; }
/* Status slot (20px): indeterminate spinner, determinate upload ring, or icon. */
.status-slot { flex: 0 0 auto; width: 20px; height: 20px; display: inline-flex; align-items: center; justify-content: center; }

/* Indeterminate spinner — tint via --spin-color (accent for profiling, ok for
   validation). */
.spinner { width: 13px; height: 13px; border-radius: 50%; border: 2px solid #c7d0dd; border-top-color: var(--spin-color, var(--accent)); animation: ws-spin 0.7s linear infinite; }
.spinner.ok { --spin-color: var(--ok); }
.spinner.lg { width: 30px; height: 30px; border-width: 3px; }
/* Determinate upload ring — fills clockwise to --pct%. */
.upload-ring { width: 14px; height: 14px; border-radius: 50%; background: conic-gradient(var(--accent) calc(var(--pct, 0) * 1%), #c7d0dd 0); -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 2px)); mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 2px)); }

/* Drop target: while a file is dragged over a lane it lights up as a receiver —
   dashed accent border + faint accent wash + inset ring. Toggle via JS. */
.dropzone { position: relative; }
.dropzone.is-dragover { border-color: var(--accent); border-style: dashed; background: var(--accent-wash); box-shadow: inset 0 0 0 1px var(--accent); }
.dropzone.is-dragover * { pointer-events: none; }
/* The "drop a file" hint is an OPAQUE overlay covering the whole lane — incl.
   the run-bar — so nothing underneath shows through it while dragging. */
.drop-hint { display: none; }
.dropzone.is-dragover .drop-hint {
  display: flex; align-items: center; justify-content: center;
  position: absolute; inset: 0; z-index: 2; border-radius: inherit;
  background: var(--accent-soft-bg); color: var(--accent);
  font-size: var(--fs-sm); font-weight: var(--fw-medium); text-align: center; padding: 0 var(--sp-4);
}

/* 14. RUN BAR (pipeline stages) =========================================== */
/* A full-width action bar fused to the BOTTOM of a lane/card: it breaks out of
   the lane padding to span edge-to-edge and sits on the card's bottom border.
   Per feedback: TOP corners are square (only the bottom corners follow the
   card's radius); separated from content above by a single quiet hairline.
   Resting state is a neutral grey; on hover it takes its stage color —
   amber for transform, green for validate. */
.run-bar {
  margin: 0 calc(-1 * var(--sp-4)) calc(-1 * var(--sp-4));
  width: auto; height: 33px; border: 0; border-top: 1px solid var(--border);
  border-radius: 0 0 var(--r-md) var(--r-md); /* square top, rounded bottom */
  display: flex; align-items: center; justify-content: center; gap: var(--sp-3);
  background: var(--run-bar-bg); color: var(--fg);
  font-size: var(--fs-meta); font-weight: var(--fw-semibold); letter-spacing: 0.02em;
  cursor: pointer; transition: background var(--t-fast), color var(--t-fast);
}
.run-bar svg { width: 15px; height: 15px; }
/* Keep the top hairline neutral on hover — otherwise the global button:hover
   rule recolors it to accent blue. */
.run-bar:not(:disabled):hover { filter: none; border-color: var(--border); }

/* Pipeline stage selector (rules-page top bar) — adopted from the live SPA
   (review 2026-06-12), restyled in the same review: NO shadow/lift (keeps
   "shadows only on transient overlays" intact); the active stage instead
   fills with its tint and carries a thin (0.5px), lightened border in its own
   icon color (50% currentColor — reserved transparent at rest, so no jump).
   Each stage owns a hue: assessment blue, transformation amber, validation
   green. */
.stage-nav { display: inline-flex; align-items: center; gap: 0; }
/* Regular button proportions — icon + area name inline (2026-07-16; formerly
   44px icon-only circles). */
.stage {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  gap: 10px; padding: 0 14px 0 0; background: transparent;
  border: 0.5px solid transparent; border-radius: var(--r-sm); color: var(--muted); cursor: pointer;
  transition: background 0.16s ease, color 0.16s ease, border-color 0.16s ease;
}
.stage .stage-label { font-size: var(--fs-sm); font-weight: var(--fw-medium); white-space: nowrap; }
/* The icon owns the button's left end as a full-height square block — only
   that block carries the stage tint when active; the label half of the button
   stays on white. Its padding (not the button's) sets the control height. */
.stage svg {
  width: 18px; height: 18px; display: block; flex: 0 0 auto;
  box-sizing: content-box; padding: 7px;
  border-radius: var(--r-sm) 0 0 var(--r-sm);
  background: transparent; transition: background 0.16s ease;
}
/* No background change on hover — the buttons sit on the grey page, where the
   global button:hover brightness filter would grey the active stage's white
   fill into the page. Hover speaks through color only (the per-stage hue
   rules below out-rank the plain --fg fallback). */
.stage:hover { color: var(--fg); }
.stage:hover:not(:disabled) { filter: none; }
.stage.is-active { background: #fff; border-color: color-mix(in srgb, currentColor 50%, transparent); }
.stage-assessment:hover                { color: var(--stage-assess-fg); }
.stage-assessment.is-active            { color: var(--stage-assess-fg); }
.stage-assessment.is-active svg        { background: var(--stage-assess-bg); }
.stage-transformation:hover            { color: var(--stage-transform-fg); }
.stage-transformation.is-active        { color: var(--stage-transform-fg); }
.stage-transformation.is-active svg    { background: var(--stage-transform-bg); }
.stage-validation:hover                { color: var(--stage-validate-fg); }
.stage-validation.is-active            { color: var(--stage-validate-fg); }
.stage-validation.is-active svg        { background: var(--stage-validate-bg); }
.stage-link { width: 38px; height: 2px; border-radius: 1px; background: var(--border); margin: 0 6px; pointer-events: none; } /* 48→38px */
.run-bar.transform:not(:disabled):hover { filter: none; background: var(--stage-transform-bg); color: var(--stage-transform-fg); border-color: var(--border); }
.run-bar.validate:not(:disabled):hover  { filter: none; background: var(--stage-validate-bg);  color: var(--stage-validate-fg); border-color: var(--border); }
.run-bar:disabled { color: var(--control-disabled-fg); background: #f6f7f9; cursor: not-allowed; }

/* Clickable summary block (e.g. "View rules ›"): edge-to-edge inside a lane,
   hairlined top/bottom, hover wash, with a hover-revealed accent affordance. */
.linked-block { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 0 calc(-1 * var(--sp-4)) var(--sp-3); padding: var(--sp-2) var(--sp-4); cursor: pointer; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); transition: background var(--t-fast); }
.linked-block:hover, .linked-block:focus-visible { background: var(--surface-hover); outline: none; }
.linked-block .link-cue { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 2px; color: var(--accent); font-size: var(--fs-sm); font-weight: var(--fw-medium); white-space: nowrap; opacity: 0; transition: opacity var(--t-fast); }
.linked-block:hover .link-cue, .linked-block:focus-visible .link-cue { opacity: 1; }

/* 15. UTILITIES + KEYFRAMES ================================================ */
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-6); }
@media (max-width: 720px) { .two-col { grid-template-columns: 1fr; } }
.muted { color: var(--muted); }
.text-ok { color: var(--ok); } .text-err { color: var(--err); } .text-warn { color: var(--warn); }
.tabular { font-variant-numeric: tabular-nums; }
/* Visually hidden, still read by assistive tech (live regions, labels). */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
[hidden] { display: none !important; }

@keyframes dm-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
@keyframes ws-spin { to { transform: rotate(360deg); } }
@keyframes spin { to { transform: rotate(360deg); } }
