/* =============================================================================
   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);
  --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(200px + var(--sp-7)); right: var(--sp-7);
  display: flex; justify-content: center;
  pointer-events: none;
}
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(200px + var(--sp-7)); right: var(--sp-7);
  display: flex; justify-content: center;
  pointer-events: none; z-index: 1;
}
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); }

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

/* 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); }
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);
}
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. */
.filters { display: flex; flex-wrap: wrap; gap: var(--sp-4); align-items: flex-end; }
.filters label {
  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, .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; }

/* 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;
}
pre.code {
  background: var(--surface-hover); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: 10px 12px; 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); }
th, td { text-align: left; padding: 9px var(--sp-3); border-top: 1px solid var(--border); vertical-align: middle; }
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 re-inset it so text stays aligned
   with the heading above. 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. A page whose cards
   scroll horizontally must opt back out (see routes/usage.css). */
.card > table { width: calc(100% + 2 * var(--sp-5)); margin-inline: calc(-1 * var(--sp-5)); }
.card > table th:first-child, .card > table td:first-child { padding-left: var(--sp-5); }
.card > table th:last-child, .card > table td:last-child { padding-right: var(--sp-5); }
.empty { color: var(--muted); font-size: var(--fs-sm); padding: var(--sp-3) 0; }

/* 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. */
.pill { display: inline-block; font-size: var(--fs-micro); padding: 2px var(--sp-3); border-radius: var(--r-pill); background: var(--accent-soft-bg); color: var(--accent); }
.pill.ok { background: var(--ok-soft-bg); color: var(--ok); }
.pill.bad { background: var(--err-soft-bg); color: var(--err); }

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

/* 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; background: transparent; color: var(--muted);
  border: 0; border-radius: var(--r-sm); cursor: pointer;
}
.disclosure svg { width: 15px; height: 15px; display: block; transition: transform var(--t-fast); }
.is-open > .disclosure svg, .disclosure.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; }
[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); } }
