/* ═══════════════════════════════════════════════════════════════════════════
   Loom UI Toolkit — Shared styles for all Loom apps
   Campaign Brain · https://loom.campaignbrain.dev
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Reset ──────────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* ── Theme: loaded from /lib/themes/*.css ───────────────────────────────── */
/* Oxblood is the fallback — matches v2 home default. Loaded inline so the
   page doesn't flash unstyled before a theme stylesheet arrives. */
:root {
  --bg: #0F0808; --surface: #1C1214; --surface2: #2E1E20;
  /* MUST track oxblood.css. This block is the pre-stylesheet fallback, so a
     token that drifts here is invisible on a warm cache and wrong on a cold
     one — which is exactly what happened: the 2026-08-02 border pass raised
     every theme FILE and left this copy at #2E1E20 (1.11:1), so the first
     paint of every page still drew the old invisible borders. Guarded now by
     theme-token-contrast.test.ts, which measures this block like a theme. */
  --border: #83565B; --muted: #A28183; --text: #EDE8E3; --subdued: #AA9C9E;
  /* Third surface shade — inset wells, chips, tracks, native-control bg.
     Derived per-theme from --surface2 nudged toward --text so it tracks EVERY
     theme. ~20 apps used var(--surface3,#232936) but it was never defined, so
     it stuck dark (#232936) in light themes → "theme not respected". color-mix
     has fleet precedent (ai/auth/cc-master css). */
  --surface3: color-mix(in srgb, var(--surface2) 86%, var(--text) 14%);
  --accent: #E8461C; --green: #2D9B5A; --amber: #D4A024; --red: #E25D5D; --red-fill: #DB3939;
  /* Categorical hues for charts / type-badges / source-pills — fixed identity
     colors (don't theme; blue==blue for recognition). Loom's semantic tokens
     (accent/red/green/amber) cover status; these cover the "N distinct
     categories" need so apps stop hardcoding their own blue/purple/pink/cyan. */
  --blue: #3b82f6; --purple: #8b5cf6; --pink: #ec4899; --cyan: #06b6d4;
  /* Typography tokens (style guide: Bricolage Grotesque display h1–h2 ·
     Hanken Grotesk body · JetBrains Mono labels/numerals). All three faces
     are self-hosted on cbcdn (css/fonts.css). Tenants can override these
     the same way they override the palette tokens. */
  --font-display: 'Bricolage Grotesque';
  --font-body: 'Hanken Grotesk'; --font-mono: 'JetBrains Mono';
  /* Radius scale (style guide r-sm/md/lg/xl + pill). Use these instead of
     ad-hoc px so cards/chips/inputs share one geometry platform-wide. */
  --r-sm: 5px; --r-md: 9px; --r-lg: 13px; --r-xl: 18px; --r-pill: 999px;
  /* Elevation (style guide sh-1/sh-2). Dark-theme defaults here; the light
     theme block overrides with the soot-based originals. */
  --sh-1: 0 1px 2px rgba(0,0,0,0.35), 0 1px 1px rgba(0,0,0,0.22);
  --sh-2: 0 6px 16px -6px rgba(0,0,0,0.5), 0 2px 6px -2px rgba(0,0,0,0.32);
  --gap: 8px; --theme-type: dark; color-scheme: dark;
  /* Canonical OUTER-LAYOUT horizontal inset — the single source of truth for
     how far the chrome stack (tab strip · crumb bar · content) sits from the
     left/right viewport edges. Tab strip, crumb bar, and #main all reference
     this so the whole stack shares ONE vertical alignment line on every app
     (the /app/earshot reference layout). Tune here once, not per-element. */
  --app-pad-x: 20px;
  /* Canonical OUTER-LAYOUT vertical inset — the top/bottom breathing room
     #main gives content below the crumb bar and above the footer. The sibling
     of --app-pad-x; was a hardcoded 20px (a "ghost band" under the crumb on
     every standalone page). Full-bleed apps (earshot, docs, maps, …) still opt
     out with `#main { padding: 0 }`. Tune here once, not per-page. */
  --app-pad-y: 12px;
}

/* …and again for any element carrying a SCOPED theme.
   A custom property inherits its COMPUTED value, so this color-mix resolves
   once at :root against the app theme and every descendant inherits that fixed
   colour — including a subtree that has just redefined --surface2 and --text
   under its own [data-theme]. Helm scopes a reading theme to #helm-left that
   way, and the result was buttons with themed text and border on the APP
   theme's background: light on light, unreadable, and worst in full screen
   where those buttons are the only navigation left. Derived tokens have to be
   re-declared wherever their inputs are. */
[data-theme] {
  --surface3: color-mix(in srgb, var(--surface2) 86%, var(--text) 14%);
}

/* Native controls (select popups, scrollbars, date/colour pickers) follow the
   active theme — without color-scheme the OS draws <select> dropdowns dark on
   light themes ("black dropdowns"). Light themes flip it; dark inherit :root.
   Keep this list in sync with the --theme-type:light themes in /lib/themes. */
[data-theme="light"], [data-theme="parchment"], [data-theme="warm"],
[data-theme="arctic"], [data-theme="rose"] { color-scheme: light; }

/* Categorical tokens — light-theme contrast tuning (prototype, app-sweep audit).
   The bright defaults (--blue #3b82f6 … --cyan #06b6d4) are tuned for dark
   surfaces; as SMALL text on light surfaces (type-badges, source-pills at ~10px)
   they fail WCAG AA — sky/cyan worst (~2:1 on white). These darker variants pass
   AA (≥4.5 on #fff: blue 5.2 · purple 5.7 · pink 4.6 · cyan 5.4), stay
   recognizably blue/purple/pink/cyan, and read better as chart fills on light
   too. Dark themes keep the bright defaults (recognition). Same theme list as
   the color-scheme rule above. */
[data-theme="light"], [data-theme="parchment"], [data-theme="warm"],
[data-theme="arctic"], [data-theme="rose"] {
  --blue: #2563eb; --purple: #7c3aed; --pink: #db2777; --cyan: #0e7490;
  /* Soot-based elevation from the style guide — softer than the dark ramp. */
  --sh-1: 0 1px 2px rgba(15,8,8,0.06), 0 1px 1px rgba(15,8,8,0.04);
  --sh-2: 0 6px 16px -6px rgba(15,8,8,0.14), 0 2px 6px -2px rgba(15,8,8,0.08);
}

/* ── Base ───────────────────────────────────────────────────────────────── */
/* html+body paint --surface so iOS Safari 18+'s translucent URL bar (which
   samples body bg, not theme-color) matches the header. The actual page
   canvas --bg lives on #main. */
html, body {
  height: 100%; background: var(--surface); color: var(--text);
  font-family: var(--font-body), -apple-system, BlinkMacSystemFont, sans-serif;
  font-size: 13px;
  /* Long unbroken tokens — object keys, file paths, URLs, IDs — must WRAP
     rather than run off the edge and get clipped (gh #284: /app/publications'
     Inbox on a phone showed `scan-intake/ftp/4da0236df5a0/2026072018▮` with the
     rest simply gone — no scrollbar, no ellipsis, unreadable).

     Before this, wrapping was opt-in per selector: ~10 ad-hoc declarations in
     this file and 52 per-app stylesheets hand-rolling their own. Anything not
     covered clipped. Inheriting a default here flips the failure mode from
     "silently unreadable" to "wraps ugly", which is the right way round.

     `break-word`, deliberately NOT `anywhere`: both break a long word, but
     `anywhere` also counts the break opportunities when computing min-content
     size, so it can shrink flex/grid items fleet-wide and move layouts that
     are currently fine. `break-word` changes only what happens when the text
     has already overflowed. Selectors that genuinely want the stronger form
     (.kv-v, and the .wrap-any utility below) still opt into `anywhere`, and a
     more specific rule always wins over this inherited default.

     NOTE this does not help a flex item on its own: a flex child defaults to
     `min-width: auto` and refuses to shrink below its content, so there is
     nothing to wrap into. That half needs `min-width: 0` — which is why
     `.wrap-any` sets both, and why an app that set only `min-width: 0` (as
     publications had) still clipped. */
  overflow-wrap: break-word;
}

/* Both halves of "make this text behave", in one class — because the two are
   only useful together and the pair is what everyone gets half-right. Use on
   any flex/grid child that holds user- or machine-generated strings. */
.wrap-any { min-width: 0; overflow-wrap: anywhere; }

/* Display face for page-level headings (style guide: Bricolage Grotesque
   for h1–h2; h3+ stay on the body face). Slight negative tracking — the
   grotesque sets wide at display sizes. */
h1, h2 {
  font-family: var(--font-display), var(--font-body), system-ui, sans-serif;
  letter-spacing: -0.015em;
}

/* ── Links (site-wide) ─────────────────────────────────────────────────── */
/* Dotted underline at 75% opacity. Opt out by adding class .no-underline
   or wrapping the link in an element that resets text-decoration. */
a {
  color: var(--accent);
  text-decoration: underline dotted;
  text-decoration-color: color-mix(in srgb, var(--accent) 75%, transparent);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: text-decoration-color 0.15s, color 0.15s;
}
a:hover {
  text-decoration-color: var(--accent);
}
a.no-underline,
a.no-underline:hover,
.btn a,
.icon-btn,
.tab a,
.loom-home,
.hdr-brand,
.tb-logo {
  text-decoration: none;
}

/* ── App Shell ──────────────────────────────────────────────────────────── */
/* iOS PWA / standalone mode: viewport-fit=cover (set by loomInit) lets
   content extend under the status bar + home indicator. We then pad #app
   to the safe-area insets so #hdr / #footer stop short of those zones.
   On non-iOS or browsers without notch, env() is 0 — no visual change. */
#app {
  display: flex; flex-direction: column;
  height: 100vh; height: 100dvh;
  /* `overflow: hidden` still lets the browser programmatically scroll
     this container (e.g. when an active .tab gets scrollIntoView'd or
     focus-into-view kicks in). On narrow viewports where header content
     is wider than 375px, that auto-scroll shifted the whole app left
     by ~55px. `overflow-x: clip` is stricter — content past the box is
     truly clipped, no scrollable axis. Keep overflow-y:hidden so the
     shell still anchors #main as the scrollable region. */
  overflow-x: clip;
  overflow-y: hidden;
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
  box-sizing: border-box;
}

/* ── Header ─────────────────────────────────────────────────────────────── */
#hdr, #header {
  height: 48px; flex-shrink: 0; background: var(--surface);
  border-bottom: 1px solid var(--border);
  display: flex; align-items: center; padding: 0 20px; gap: 12px;
  /* Clip header overflow at its own box. Without this, header content
     wider than the viewport (e.g. tenant pill + page title + icon row
     on a 375px phone) gets reported as part of #app's scrollWidth, and
     the browser auto-scrolls #app on load to bring the active .tab into
     view — shifting the entire page left by ~55px. With overflow:hidden
     here, the header's overflow is contained and #app stays anchored. */
  overflow: hidden; min-width: 0;
}
.back-btn {
  background: none; border: none; color: var(--subdued); cursor: pointer;
  font-size: 16px; padding: 4px 8px; border-radius: 4px;
}
.back-btn:hover { color: var(--text); background: var(--border); }
.hdr-brand, .loom-home {
  font-size: 11px; font-weight: 700; letter-spacing: 0.18em;
  color: var(--accent); text-transform: uppercase; text-decoration: none;
}
.hdr-brand:hover, .loom-home:hover { opacity: 0.8; }

/* Default brand mark — the Campaign Brain eagle emblem (cbcdn), replacing the
   old "LOOM" wordmark as the header home link. loom.js renders it INLINE so the
   eagle line-art follows the theme via currentColor (color:var(--text) — always
   contrasts the header in any tenant theme + mode) while the brain keeps its red
   facets. Until the inline svg loads (or if the CDN is unreachable) the static
   two-tone variants stand in, theme-swapped. */
.loom-home-mark {
  display: block; width: 30px; height: 30px; flex-shrink: 0; text-decoration: none;
  color: var(--text);
  background: url('https://cdn.campaignbrain.dev/images/brand/logo.svg') center / contain no-repeat;
}
.loom-home-mark:hover { opacity: 0.85; }
.loom-home-mark.has-inline-svg { background: none; }
.loom-home-mark svg { display: block; width: 100%; height: 100%; }
@media (prefers-color-scheme: dark) {
  .loom-home-mark:not(.has-inline-svg) { background-image: url('https://cdn.campaignbrain.dev/images/brand/logo-white.svg'); }
}
:root[data-theme="light"] .loom-home-mark:not(.has-inline-svg) { background-image: url('https://cdn.campaignbrain.dev/images/brand/logo.svg'); }
:root[data-theme="dark"] .loom-home-mark:not(.has-inline-svg) { background-image: url('https://cdn.campaignbrain.dev/images/brand/logo-white.svg'); }
.hdr-sep { width: 1px; height: 20px; background: var(--border); }
.hdr-tenant-logo {
  height: 24px; width: auto; max-width: 120px; object-fit: contain;
  display: block; flex-shrink: 0;
}
.hdr-home-logo { display: flex; align-items: center; text-decoration: none; }
.hdr-home-logo:hover { opacity: 0.8; }
.hdr-title {
  font-size: 13px; font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.hdr-right { display: flex; align-items: center; gap: 10px; font-size: 11px; color: var(--subdued); margin-left: auto; }

/* Global search bar (loomSearch) — sits between the title and the right-side
   chrome icons. Shrinks gracefully; the results panel is body-mounted so it
   escapes #hdr's overflow:hidden. */
.hdr-search {
  display: flex; align-items: center; gap: 6px;
  background: var(--surface2); border: 1px solid var(--border);
  border-radius: 8px; padding: 0 8px; height: 30px;
  margin-left: 16px; min-width: 0; flex: 0 1 280px;
  transition: border-color 0.12s;
}
.hdr-search:focus-within { border-color: var(--accent); }
.hdr-search-icon { width: 14px; height: 14px; opacity: 0.55; flex-shrink: 0; }
[data-theme="light"] .hdr-search-icon,
[data-theme="warm"] .hdr-search-icon,
[data-theme="parchment"] .hdr-search-icon { filter: brightness(0); opacity: 0.45; }
.hdr-search-input {
  background: none; border: none; outline: none; color: var(--text);
  font-size: 12px; width: 100%; min-width: 0; padding: 0;
}
.hdr-search-input::placeholder { color: var(--muted); }

#loom-search-panel {
  position: fixed; z-index: 1200; display: none;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 10px; box-shadow: 0 12px 32px rgba(0,0,0,0.28);
  max-height: 60vh; overflow-y: auto; padding: 5px;
}
#loom-search-panel.open { display: block; }

/* Apps / This-page mode toggle. */
.loom-search-modes {
  display: flex; gap: 4px; padding: 4px 4px 6px; margin-bottom: 4px;
  border-bottom: 1px solid var(--border);
}
.loom-search-modes button {
  flex: 1; background: none; border: 1px solid transparent; border-radius: 6px;
  color: var(--subdued); font-size: 12px; font-weight: 600; padding: 5px 8px; cursor: pointer;
}
.loom-search-modes button:hover { color: var(--text); }
.loom-search-modes button.active { background: var(--surface2); color: var(--text); border-color: var(--border); }
.loom-search-count { padding: 4px 10px 6px; color: var(--subdued); font-size: 11px; }
.loom-search-ctx { font-size: 12.5px; color: var(--subdued); line-height: 1.4; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.loom-search-ctx mark { background: color-mix(in srgb, var(--accent) 32%, transparent); color: var(--text); border-radius: 3px; padding: 0 1px; }

/* Non-destructive find-on-page highlights (CSS Custom Highlight API). */
::highlight(loom-find) { background: color-mix(in srgb, var(--accent) 28%, transparent); }
::highlight(loom-find-cur) { background: var(--accent); color: #fff; }

.loom-search-empty { padding: 12px 14px; color: var(--subdued); font-size: 13px; }
.loom-search-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border-radius: 7px; cursor: pointer;
}
.loom-search-row.active { background: color-mix(in srgb, var(--accent) 14%, transparent); }
.loom-search-ico { width: 18px; height: 18px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; opacity: 0.85; }
.loom-search-ico img { width: 16px; height: 16px; }
.loom-search-lbl { flex: 1; min-width: 0; font-size: 13px; color: var(--text); display: flex; flex-direction: column; line-height: 1.25; }
.loom-search-tag { font-size: 11px; color: var(--subdued); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.loom-search-go { color: var(--muted); font-size: 12px; opacity: 0; flex-shrink: 0; }
.loom-search-row.active .loom-search-go { opacity: 0.7; }

/* On tablet + mobile the header search squishes too small to be usable —
   hide it entirely (the in-page Find boxes still cover on-page search). */
@media (max-width: 1024px) {
  .hdr-search { display: none !important; }
}
@media (max-width: 640px) {
  .hdr-title { display: none; }
}
.hdr-user-display { color: var(--subdued); font-size: 11px; white-space: nowrap; }
.hdr-user-display:empty { display: none; }

/* Tenant context badge — shows the property the session is scoped to. */
.hdr-tenant {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11px; color: var(--text);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  border-radius: 999px; padding: 3px 10px 3px 8px;
  white-space: nowrap;
}
.hdr-tenant-dot {
  width: 8px; height: 8px; border-radius: 50%;
  box-shadow: 0 0 6px currentColor;
}
.hdr-tenant-name { font-weight: 600; letter-spacing: 0.01em; }
.hdr-tenant-caret { font-size: 9px; color: var(--muted); margin-left: 2px; }
.hdr-tenant.switchable { cursor: pointer; transition: background 0.12s, border-color 0.12s; }
.hdr-tenant.switchable:hover {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
}

/* Tenant switcher dropdown — anchored beneath the header pill. */
.loom-tenant-menu {
  position: fixed; min-width: 220px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.35);
  padding: 4px; z-index: 9999;
  font-size: 12px; color: var(--text);
}
.loom-tenant-menu .ltm-header {
  font-size: 9px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.12em; color: var(--muted);
  padding: 8px 10px 4px;
}
.loom-tenant-menu .ltm-section {
  font-size: 9px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.12em; color: var(--accent);
  padding: 8px 10px 2px; border-top: 1px solid var(--border);
  margin-top: 2px;
}
.loom-tenant-menu .ltm-section:first-of-type { border-top: none; margin-top: 0; }
.loom-tenant-menu .ltm-item {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 10px; border-radius: 5px;
  cursor: pointer; text-decoration: none; color: var(--text);
}
.loom-tenant-menu .ltm-item:hover { background: var(--surface2); }
.loom-tenant-menu .ltm-item.active { color: var(--accent); }
.loom-tenant-menu .ltm-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent); flex-shrink: 0;
}
.loom-tenant-menu .ltm-slug { flex: 1; font-family: var(--font-mono), monospace; font-size: 11px; }
.loom-tenant-menu .ltm-role {
  font-size: 9px; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--muted); padding: 1px 6px; border-radius: 999px;
  border: 1px solid var(--border);
}
.loom-tenant-menu .ltm-check { color: var(--accent); font-weight: 700; }

/* ── Breadcrumb Strip ───────────────────────────────────────────────────── */
#loom-crumb-bar {
  /* v0.0.525 — dropped border-bottom so the tab strip below (surface2 bg)
     reads flush against the crumb bar (bg) with no hairline separator.
     The color transition between --bg and --surface2 is the visual divider. */
  flex-shrink: 0; background: var(--bg);
  padding: 5px var(--app-pad-x); display: flex; align-items: center; overflow: hidden; gap: 6px;
  /* Pre-size to match the nav buttons' natural height so the row doesn't
     shift when the back/forward buttons go from disabled to enabled. */
  min-height: 28px;
}
/* v0.0.525 — Back + Forward live inside a single segmented-control chip
   on the right edge of the crumb bar, mirroring the dashboard's
   .layout-toggle visual exactly (bordered surface2 container, subdued
   buttons with hover->text color). The container's `margin-left: auto`
   pushes it flush-right while keeping crumbs flush-left.

   Brad's ask: "Steal the exact layout / theme of the view switcher on
   the dashboard, repurpose them into Next / Back buttons - and move
   flush-right (like on dashboard page). Cookie crumb flush left now."
*/
.loom-nav {
  display: inline-flex; gap: 0; margin-left: auto; flex-shrink: 0;
  border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
  background: var(--surface2);
}
.loom-nav-btn {
  background: transparent; border: 0; cursor: pointer;
  padding: 4px 10px; color: var(--subdued);
  display: inline-flex; align-items: center; justify-content: center;
  transition: background 0.12s ease, color 0.12s ease;
  font-family: inherit;
}
.loom-nav-btn:hover {
  color: var(--text);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}
.loom-nav-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.loom-nav-btn:disabled:hover { color: var(--subdued); background: transparent; }
.loom-nav-btn .nav-icon {
  display: inline-block; width: 12px; height: 12px;
  background-color: currentColor;
}
.loom-nav-btn[data-dir="back"] .nav-icon {
  -webkit-mask: url('https://cdn.campaignbrain.dev/svg/icons/arrow-left.svg') center / contain no-repeat;
          mask: url('https://cdn.campaignbrain.dev/svg/icons/arrow-left.svg') center / contain no-repeat;
}
.loom-nav-btn[data-dir="forward"] .nav-icon {
  -webkit-mask: url('https://cdn.campaignbrain.dev/svg/icons/arrow-right.svg') center / contain no-repeat;
          mask: url('https://cdn.campaignbrain.dev/svg/icons/arrow-right.svg') center / contain no-repeat;
}
#loom-crumbs {
  display: flex; align-items: center; gap: 0; min-width: 0; flex: 1;
  overflow: hidden;
}
#loom-crumbs a, #loom-crumbs span {
  font-size: 11px; white-space: nowrap; text-decoration: none;
}
#loom-crumbs a { color: var(--subdued); cursor: pointer; }
#loom-crumbs a:hover { color: var(--accent); }
#loom-crumbs .crumb-sep { color: var(--muted); margin: 0 5px; font-size: 9px; flex-shrink: 0; }
#loom-crumbs .crumb-current {
  color: var(--text); font-weight: 600;
  overflow: hidden; text-overflow: ellipsis;
}
.icon-btn.copied { color: var(--green, #22c55e); border-color: var(--green, #22c55e); }
@media (max-width: 640px) {
  #loom-crumbs a.crumb-middle { display: none; }
  #loom-crumbs .crumb-sep.crumb-middle-sep { display: none; }
  #loom-crumbs a, #loom-crumbs span { font-size: 10px; }
}

/* ════════════════════════════════════════════════════════════════════════════
   MOBILE DENSITY REDESIGN (app-sweep concept, prototype) — reclaim vertical
   space on phones: compact header, breadcrumb → back-chevron + title, denser
   tables. Scoped to ≤640px; desktop/tablet untouched. Chrome-only + .data-table
   so all apps inherit. tenant/account/theme stay one-tap; nav moves to the
   hamburger → loomNavDrawer.
   ════════════════════════════════════════════════════════════════════════════ */
/* Hamburger + mobile back/title are desktop-hidden by default. */
.hdr-menu-btn { display: none; }
.crumb-back-m, .crumb-title-m { display: none; }

/* App-launcher drawer (the hamburger target). */
.loom-navdrawer { position: fixed; inset: 0; z-index: 1300; display: none; }
.loom-navdrawer.open { display: block; }
.loom-navdrawer-scrim { position: absolute; inset: 0; background: rgba(0,0,0,0.45); }
.loom-navdrawer-panel {
  position: absolute; top: 0; left: 0; bottom: 0; width: 84%; max-width: 320px;
  background: var(--surface); border-right: 1px solid var(--border);
  display: flex; flex-direction: column; overflow-y: auto;
  transform: translateX(-100%); transition: transform 0.18s ease;
  padding-top: env(safe-area-inset-top, 0);
}
.loom-navdrawer.open .loom-navdrawer-panel { transform: translateX(0); }
.loom-navdrawer-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 16px; border-bottom: 1px solid var(--border);
  font-weight: 700; font-size: 13px; flex-shrink: 0;
}
.loom-navdrawer-home {
  display: block; padding: 13px 16px; color: var(--accent); text-decoration: none;
  font-weight: 600; font-size: 14px; border-bottom: 1px solid var(--border); flex-shrink: 0;
}
.loom-navdrawer-list { padding: 6px; }
.loom-navdrawer-item {
  display: flex; align-items: center; gap: 11px; padding: 12px; border-radius: 8px;
  color: var(--text); text-decoration: none; font-size: 14px; min-height: 44px; box-sizing: border-box;
}
.loom-navdrawer-item:hover, .loom-navdrawer-item.active {
  background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--accent);
}
.loom-navdrawer-ico { width: 18px; height: 18px; display: flex; align-items: center; flex-shrink: 0; opacity: 0.85; }
.loom-navdrawer-ico img { width: 18px; height: 18px; }

@media (max-width: 640px) {
  /* Compact header — shorter, tighter. Hamburger stays hidden (Brad): the
     header is too tight on a phone for it once a long tenant name is in play;
     the nav-drawer's job is covered by the breadcrumb back-chevron below.
     Keeping account/tenant/theme one-tap is the priority for the right side. */
  #hdr, #header { height: 44px; padding: 0 8px; gap: 6px; }
  .hdr-right { gap: 4px; }
  .hdr-user-display { display: none; }          /* name text is noise on a phone */

  /* A long tenant name ("Perry Johnson for Governor") must not shove the
     right-side icons off-screen. Let the badge shrink and ellipsize the name. */
  .hdr-tenant { min-width: 0; overflow: hidden; }
  .hdr-tenant-name {
    max-width: 34vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }

  /* Breadcrumb trail → back-chevron + bold page title. */
  #loom-crumbs a, #loom-crumbs .crumb-sep, #loom-crumbs .crumb-current { display: none; }
  #loom-nav { display: none; }
  .crumb-back-m {
    display: inline-flex; align-items: center; background: none; border: none;
    color: var(--text); cursor: pointer; padding: 6px 8px 6px 0; flex-shrink: 0;
  }
  .crumb-title-m {
    display: inline-block; font-size: 15px; font-weight: 700; color: var(--text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
  }
  #loom-crumb-bar { padding-top: 4px; padding-bottom: 4px; }

  /* Denser shared tables — more rows above the fold. */
  .data-table th { padding: 7px 8px; }
  .data-table td { padding: 7px 8px; font-size: 12px; }
}

/* ── Theme Selector ─────────────────────────────────────────────────────── */
.theme-select {
  background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
  color: var(--subdued); cursor: pointer; font-family: inherit; font-size: 11px;
  padding: 4px 8px; outline: none;
  position: absolute; opacity: 0; width: 100%; height: 100%; top: 0; left: 0;
}
.theme-trigger { position: relative; }
.icon-btn {
  background: none; border: 1px solid var(--border); border-radius: 4px;
  color: var(--subdued); cursor: pointer; font-size: 14px; padding: 2px 6px; line-height: 1;
}
.icon-btn:hover { color: var(--text); border-color: var(--accent); }

/* ── loomNotify — alert inbox bell + popover (cbloom#90) ─────────────── */
.loom-notify-btn { position: relative; }
.loom-notify-btn.has-unread { color: var(--accent); }
.loom-notify-badge {
  position: absolute;
  top: -4px; right: -4px;
  min-width: 14px; height: 14px; padding: 0 3px;
  background: var(--red, #D92B2B); color: #fff;
  border-radius: 8px;
  font-size: 9px; font-weight: 700; line-height: 14px;
  text-align: center;
  box-sizing: border-box;
}
.loom-notify-popover {
  position: fixed;
  top: 56px; right: 14px;
  width: 360px; max-width: calc(100vw - 28px);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.35);
  z-index: 1000;
  display: flex; flex-direction: column;
  max-height: 70vh;
  font-size: 12px;
}
.loom-notify-popover[hidden] { display: none; }
.loom-notify-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  font-size: 12px;
}
.loom-notify-body { overflow-y: auto; max-height: 50vh; }
.loom-notify-empty {
  padding: 20px; text-align: center; color: var(--subdued); font-style: italic;
}
.loom-notify-row {
  display: block; padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  color: var(--text); text-decoration: none;
  background: transparent;
}
.loom-notify-row:last-child { border-bottom: none; }
.loom-notify-row:hover { background: var(--surface2); }
.loom-notify-row.unread { background: color-mix(in srgb, var(--accent) 4%, transparent); }
.loom-notify-row.unread:hover { background: color-mix(in srgb, var(--accent) 8%, transparent); }
.loom-notify-row-top {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  margin-bottom: 4px;
}
.loom-notify-row-top strong { font-size: 12px; flex: 1; min-width: 0; }
.loom-notify-row-body {
  font-size: 11px; color: var(--text);
  line-height: 1.4; margin-bottom: 4px;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.loom-notify-row-meta {
  font-size: 10px; color: var(--subdued);
  font-family: var(--font-mono), monospace;
}
.loom-notify-foot {
  padding: 8px 12px; text-align: center;
  border-top: 1px solid var(--border);
  font-size: 11px; color: var(--subdued);
}
.loom-notify-foot a { color: var(--accent); }

/* ── loomInbox — chrome DM inbox (cbloom#171 v2.3) ──────────────────────── */
.loom-inbox-btn { position: relative; }
.loom-inbox-btn.has-unread { color: var(--accent); }
.loom-inbox-badge {
  position: absolute;
  top: -4px; right: -4px;
  min-width: 14px; height: 14px; padding: 0 3px;
  background: var(--accent); color: #fff;
  border-radius: 8px;
  font-size: 9px; font-weight: 700; line-height: 14px;
  text-align: center;
  box-sizing: border-box;
}
.loom-inbox-popover {
  position: fixed;
  top: 56px; right: 14px;
  width: 340px; max-width: calc(100vw - 28px);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.35);
  z-index: 1000;
  display: flex; flex-direction: column;
  max-height: 70vh;
  font-size: 12px;
}
.loom-inbox-popover[hidden] { display: none; }
.loom-inbox-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 12px; border-bottom: 1px solid var(--border);
}
.loom-inbox-openall { color: var(--accent); text-decoration: none; font-size: 11px; }
.loom-inbox-body { overflow-y: auto; max-height: 46vh; }
.loom-inbox-empty {
  padding: 20px; text-align: center; color: var(--subdued); font-style: italic;
}
.loom-inbox-row {
  padding: 9px 12px; border-bottom: 1px solid var(--border);
  cursor: pointer; color: var(--text);
}
.loom-inbox-row:last-child { border-bottom: none; }
.loom-inbox-row:hover { background: var(--surface2); }
.loom-inbox-row.active { background: color-mix(in srgb, var(--accent) 8%, transparent); }
.loom-inbox-row-top {
  display: flex; align-items: center; gap: 6px; margin-bottom: 2px;
}
.loom-inbox-row-top strong { font-size: 12px; flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.loom-inbox-dot {
  background: var(--accent); color: #fff; border-radius: 8px;
  min-width: 16px; height: 16px; padding: 0 5px; box-sizing: border-box;
  font-size: 10px; font-weight: 700; line-height: 16px; text-align: center;
}
.loom-inbox-row-prev {
  font-size: 11px; color: var(--subdued); line-height: 1.4;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.loom-inbox-reply {
  display: flex; gap: 6px; padding: 8px 10px;
  border-top: 1px solid var(--border);
}
.loom-inbox-reply input {
  flex: 1; background: var(--surface2); border: 1px solid var(--border);
  border-radius: 6px; padding: 6px 8px; color: var(--text);
  font-family: inherit; font-size: 12px; outline: none;
}
.loom-inbox-reply input:focus { border-color: var(--accent); }

/* ── Control Select (visible dropdowns in filter bars) ──────────────────── */
.ctrl-select {
  background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
  color: var(--text); cursor: pointer; font-family: inherit; font-size: 11px;
  padding: 6px 10px; outline: none;
  /* Overflow guard: a native <select> sizes to its widest option, which blows
     past a narrow (mobile) container with long option labels. Cap at the
     container + truncate the shown text instead of pushing the page wide. */
  max-width: 100%; box-sizing: border-box; text-overflow: ellipsis;
}
.ctrl-select:focus { border-color: var(--accent); }

/* Inline styled text input — the sibling of .ctrl-select for toolbar/drawer
   search + filter boxes (where the block .field pattern is too heavy). Prefer
   .field for stacked form fields; use .ctrl-input for inline single controls. */
.ctrl-input {
  background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
  color: var(--text); font-family: inherit; font-size: 12px;
  padding: 6px 10px; outline: none;
}
.ctrl-input::placeholder { color: var(--subdued); }
.ctrl-input:focus { border-color: var(--accent); }

/* ── loomChecklist ─── inline multi-select checkbox list (pick one or more of a
   known set). Generalized from the Cast room-member picker. */
.loom-checklist { display: flex; flex-direction: column; gap: 6px; }
.loom-checklist-search { width: 100%; box-sizing: border-box; }
.loom-checklist-bar { display: flex; align-items: center; gap: 10px; }
.loom-checklist-act {
  background: none; border: none; padding: 0; cursor: pointer;
  font: inherit; font-size: 12px; color: var(--accent);
}
.loom-checklist-act:hover { text-decoration: underline; }
.loom-checklist-count { margin-left: auto; font-size: 12px; color: var(--subdued); }
.loom-checklist-items {
  max-height: 44vh; overflow-y: auto;
  border: 1px solid var(--border); border-radius: 8px;
}
.loom-checklist-row {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 10px; border-bottom: 1px solid var(--border);
  font-size: 13px; cursor: pointer;
}
.loom-checklist-items .loom-checklist-row:last-child { border-bottom: none; }
.loom-checklist-row.lc-hidden { display: none; }
.lc-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lc-sub { flex-shrink: 0; font-size: 11px; color: var(--subdued); }
.loom-checklist-empty { padding: 12px 10px; color: var(--subdued); font-size: 13px; }

/* ── Main Content ───────────────────────────────────────────────────────── */
#main { flex: 1; overflow: auto; padding: var(--app-pad-y) var(--app-pad-x); width: 100%; background: var(--bg); }
/* Brad bumped this from 1200 to 1400 in v0.0.521 — a scootch more horizontal
   room on wider screens. Affects every flash-app's content body. */
#main > #content { max-width: 1400px; margin: 0 auto; }

/* ── Footer ─────────────────────────────────────────────────────────────── */
/* Legacy clock strip — hidden on inner app pages to reduce bottom stacking.
   Dashboard (v2 home) keeps its own clock in its inline footer. */
#ft-clock-bar { display: none; }
/* Footer block reads top-down as: ONE fine rule → the control row → the version
   line at the very bottom. The page title is NOT repeated down here (it already
   sits in the header + crumb bar), so .ft is hidden globally. */
#footer {
  min-height: 0; flex-shrink: 0; background: var(--surface);
  border-top: 1px solid var(--border);
  display: flex; align-items: center; justify-content: center;
  padding: 4px 14px 0; gap: 10px;
  /* Last element in the block — clear the iOS home-indicator / gesture bar. */
  padding-bottom: calc(4px + env(safe-area-inset-bottom, 0px));
}
/* The page title belongs to the header, not the footer. */
#footer .ft { display: none; }
#footer .fv { font-size: 10px; color: var(--subdued); margin-left: 0; }

/* ── Footer control panel (injected by loom.js) ───────────────────────────────
   A full-width control bar: the instrument icons (left) and identity icons
   (right) sit on the SAME left/right guides as the content above (var(--app-pad-x)),
   so they line up with the panels in the content area; the version colophon
   rides centered between them. Shaded like one control panel — a top-lit surface
   gradient under one fine top rule — so the icons and the bar read as a single
   material. Taller than a plain strip to give the bigger glyphs room. */
#loom-footer-actions {
  flex-shrink: 0; position: relative;
  background: linear-gradient(180deg, var(--surface2), var(--surface));
  border-top: 1px solid var(--border);
  display: flex; align-items: center; gap: 12px; flex-wrap: nowrap;
  /* Tighter: 11px → 8px. The row is four 30px pills and a version stamp; the
     old padding gave it the height of a toolbar it isn't. */
  padding: 8px var(--app-pad-x, 20px);
  /* Last element in the shell — clear the iOS home-indicator / gesture bar. */
  padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
}
/* THE FLAIR. One accent hairline across the top edge, fading out at both ends —
   the whole chrome is otherwise a single grey, and this is the cheapest way to
   say "this strip is deliberate" without painting anything. Drawn from the
   theme's own --accent, so it re-tints per tenant instead of pinning a hue. */
#loom-footer-actions::before {
  content: ''; position: absolute; left: 0; right: 0; top: -1px; height: 2px;
  background: linear-gradient(90deg,
    transparent,
    color-mix(in srgb, var(--accent) 55%, transparent) 18%,
    color-mix(in srgb, var(--accent) 75%, transparent) 50%,
    color-mix(in srgb, var(--accent) 55%, transparent) 82%,
    transparent);
  pointer-events: none;
}
/* The version colophon now lives INSIDE the bar (centered), so the old bottom
   #footer line is redundant — collapse it. */
#loom-footer-actions + #footer { display: none; }
/* Left/right icon clusters + the centered version colophon between them. */
#loom-footer-actions .ff-group { display: flex; align-items: center; gap: 8px; }
#loom-footer-actions .ff-ver {
  flex: 1 1 auto; min-width: 0; text-align: center;
  font-size: 11px; color: var(--subdued);
  font-family: var(--font-mono), monospace; letter-spacing: 0.02em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The version number itself carries the accent; the "· 9 hours ago" that
   follows stays subdued. One coloured token in the middle of the bar balances
   the hairline above it. */
#loom-footer-actions .ff-ver b,
#loom-footer-actions .ff-ver .ff-ver-num {
  color: var(--accent); font-weight: 600;
}

/* Footer control pills — consistently sized capsules, icon centered, no label.
   The whole row (LIVE + Shot + Account + Sign out) shares ONE size so it reads
   as one set of pills rather than mixed squares. Id-scoped so the pill radius
   beats .loom-hot-btn.sm's later 5px rule. */
#loom-footer-actions .loom-hot-btn.icon-only {
  gap: 0; padding: 0;
  /* 44×34 → 38×30. Still well past the 34px tap minimum on the long axis, and
     it stops four pills reading as a button bar competing with the app. */
  width: 38px; height: 30px; min-height: 30px;
  border-radius: var(--r-pill, 999px);
  justify-content: center; flex: none;
}
/* The two clusters are different KINDS of control, so they get different
   resting tints rather than four identical grey capsules: instruments (LIVE,
   Shot) stay neutral, identity (Sign out, Account) sits on a faint accent
   wash. Enough to read as two groups; nowhere near enough to shout. */
#loom-footer-actions .ff-right .loom-hot-btn.icon-only.outline {
  background: color-mix(in srgb, var(--accent) 7%, var(--surface3, var(--surface2)));
}
/* Sign out: subdued at rest, red on approach. Solid red in the corner was an
   invitation to press the one control nobody wants to press by accident. */
#loom-footer-actions .loom-hot-btn.logout { color: var(--subdued); }
#loom-footer-actions .loom-hot-btn.logout:hover,
#loom-footer-actions .loom-hot-btn.logout:focus-visible {
  color: var(--red, #D92B2B);
  border-color: color-mix(in srgb, var(--red, #D92B2B) 55%, var(--border));
  background: color-mix(in srgb, var(--red, #D92B2B) 12%, transparent);
}
#loom-footer-actions .loom-hot-btn.logout:hover .cdn-ico-mask { opacity: 1; }
/* The version stamp is NOT hidden on narrow screens. It was, briefly, to make
   room for the account pill — and that pill is gone, so the rule outlived its
   reason and took the deploy stamp with it on anything under 560px (an iPad in
   split view, a phone). "Did my push land" is the single most-checked thing in
   this bar; it shrinks and truncates rather than disappearing. */
/* Cohesive control-panel skin: the non-status pills share the bar's own surface
   family (surface3 fill + one fine border + the soft shadow), so the icons and
   the bar read as one material. LIVE keeps its status color (amber/green/red) —
   the single functional signal in the row. */
#loom-footer-actions .loom-hot-btn.icon-only.outline {
  background: var(--surface3, var(--surface2));
  border: 1px solid var(--border);
  box-shadow: var(--sh-1);
}
#loom-footer-actions .loom-hot-btn.icon-only.outline:hover {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, var(--surface3, var(--surface2)));
}
/* Hide the label span only — NOT the icon (svgMask renders as an empty
   span.cdn-ico-mask, so `:empty` would wrongly hide the glyph). */
#loom-footer-actions .loom-hot-btn.icon-only > span:not(.cdn-ico-mask) { display: none; }
/* BOLD the action glyphs. A mask-rendered icon can't take a stroke-width, so
   dilate the shape with four zero-blur drop-shadows in currentColor (~+0.6px
   of weight in each direction). Scoped to the footer + mask icons, so the LIVE
   pill's plain <img> and every other svgMask on the page are untouched. */
#loom-footer-actions .loom-hot-btn .cdn-ico-mask {
  filter: drop-shadow(0.6px 0 0 currentColor) drop-shadow(-0.6px 0 0 currentColor)
          drop-shadow(0 0.6px 0 currentColor) drop-shadow(0 -0.6px 0 currentColor);
}
/* The outline variant leaves icons at their native (dark) color, which all
   but vanishes on the dark footer strip (#228). The footer always sits on
   --surface, so lighten the outline-button icons to a subdued tone matching
   the text. Scoped to the footer so outline buttons elsewhere are untouched. */
#loom-footer-actions .loom-hot-btn.outline img {
  filter: brightness(0) invert(1);
  opacity: 0.6;
}
/* Legacy footer-link styling. Hot buttons (the current default for every
   action here) carry their own typography + color via .loom-hot-btn, so
   exclude them — otherwise this rule's id+tag specificity (0,1,0,1) wins
   against the class-only .loom-hot-btn (0,0,1,0), turning white text on
   the solid variant into accent-on-accent (invisible). */
#loom-footer-actions a:not(.loom-hot-btn) {
  font-family: var(--font-mono), monospace;
  font-size: 11px; font-weight: 600; letter-spacing: 0.05em;
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, var(--accent) 55%, transparent);
  padding: 3px 10px;
  border-radius: 6px;
  transition: color 0.15s, background 0.15s, text-decoration-color 0.15s;
  display: inline-flex; align-items: center; gap: 6px;
}
#loom-footer-actions a:not(.loom-hot-btn):hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  text-decoration-color: var(--accent);
}
/* Sign out uses site-wide link color (accent) — same as Change password */

/* Spacer pushes the right-side links away from the left-side WS pill */
.loom-footer-spacer { flex: 1; }

/* Chrome extension badge — compact chip, distinct from the regular footer
   links so it reads as an installable artifact rather than navigation. */
#loom-footer-actions a.loom-ext-badge {
  text-decoration: none;
  border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--border));
  background: color-mix(in srgb, var(--accent) 6%, var(--surface));
  font-size: 10px;
  text-transform: uppercase;
  padding: 3px 8px;
  gap: 5px;
}
#loom-footer-actions a.loom-ext-badge:hover {
  background: color-mix(in srgb, var(--accent) 14%, var(--surface));
  border-color: var(--accent);
}
#loom-footer-actions a.loom-ext-badge img { opacity: 0.85; }
[data-theme="dark"] #loom-footer-actions a.loom-ext-badge img,
[data-theme="midnight"] #loom-footer-actions a.loom-ext-badge img,
[data-theme="oxblood"] #loom-footer-actions a.loom-ext-badge img,
[data-theme="ember"] #loom-footer-actions a.loom-ext-badge img,
[data-theme="forest"] #loom-footer-actions a.loom-ext-badge img,
[data-theme="ocean"] #loom-footer-actions a.loom-ext-badge img,
[data-theme="neon"] #loom-footer-actions a.loom-ext-badge img {
  filter: brightness(0) invert(70%);
}

/* ─────────────────────────────────────────────────────────────────────────
   loomHotBtn — the "hot button". Bright, tactile, scriptable. Used for
   primary actions, footer macros, and any place a button should feel like
   something you can press. CSS-var driven: --hot-bg is the color knob;
   defaults to --accent so unstyled hot buttons inherit the theme.

   Anatomy:
     base   — solid color background, white text, inset top-highlight
              (subtle 3D), soft drop shadow tinted to match the background
              (the "halo" that makes it pop)
     :hover — brightness up 8%, shadow grows + glows a hair more
     :active — translateY(1px) + scale(0.98) + shadow shrinks +
              inset shadow flips (the "pressed in" feeling)

   Variants:
     .outline — transparent bg, currentColor border + text
     .tint    — 14% bg + 25% inset border, currentColor text (subtle)

   Sizes:
     .sm — 22px tall, 10px font (footer-fit)
     md  — default, 26px tall, 11px font
     .lg — 38px tall, 13px font (page-level CTAs)
   ───────────────────────────────────────────────────────────────────── */
.loom-hot-btn {
  --hot-bg: var(--accent);
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 6px 14px;
  min-height: 26px;
  background: var(--hot-bg);
  color: #fff;
  font: 600 11px/1 var(--font-body), -apple-system, BlinkMacSystemFont, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border: 0;
  border-radius: 6px;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.10),
    0 2px 8px color-mix(in srgb, var(--hot-bg) 30%, transparent),
    inset 0 1px 0 rgba(255, 255, 255, 0.20);
  transition:
    transform 0.08s ease-out,
    box-shadow 0.15s ease,
    filter 0.15s ease;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  user-select: none;
}
.loom-hot-btn:hover {
  filter: brightness(1.08);
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.12),
    0 4px 14px color-mix(in srgb, var(--hot-bg) 42%, transparent),
    inset 0 1px 0 rgba(255, 255, 255, 0.26);
}
.loom-hot-btn:active {
  transform: translateY(1px) scale(0.98);
  filter: brightness(0.96);
  box-shadow:
    0 0 0 rgba(0, 0, 0, 0),
    0 1px 3px color-mix(in srgb, var(--hot-bg) 25%, transparent),
    inset 0 1px 2px rgba(0, 0, 0, 0.16);
}
.loom-hot-btn:disabled,
.loom-hot-btn[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
  filter: none;
  transform: none;
}
/* maskIcon glyphs (svgMask) follow the button's currentColor — used for the
   footer Account/Sign out so they stay visible on a light --surface (an <img>
   is force-inverted to white by the rule below and vanishes there). */
.loom-hot-btn .cdn-ico-mask { flex: none; pointer-events: none; }
.loom-hot-btn img {
  width: 12px;
  height: 12px;
  flex: none;
  pointer-events: none;
  filter: brightness(0) invert(100%);
}

/* Sizes */
.loom-hot-btn.sm {
  font-size: 10px;
  padding: 4px 10px;
  min-height: 22px;
  border-radius: 5px;
  gap: 5px;
}
.loom-hot-btn.sm img { width: 11px; height: 11px; }
.loom-hot-btn.lg {
  font-size: 13px;
  padding: 10px 22px;
  min-height: 38px;
  border-radius: 8px;
  gap: 8px;
  letter-spacing: 0.05em;
}
.loom-hot-btn.lg img { width: 14px; height: 14px; }

/* Variant: outline — same color knob, just transparent + bordered */
.loom-hot-btn.outline {
  background: transparent;
  color: var(--hot-bg);
  box-shadow: inset 0 0 0 1.5px currentColor;
}
.loom-hot-btn.outline:hover {
  background: color-mix(in srgb, var(--hot-bg) 10%, transparent);
  box-shadow: inset 0 0 0 1.5px currentColor;
  filter: none;
}
.loom-hot-btn.outline:active {
  background: color-mix(in srgb, var(--hot-bg) 18%, transparent);
  box-shadow: inset 0 0 0 1.5px currentColor, inset 0 1px 2px rgba(0,0,0,0.08);
  transform: translateY(1px) scale(0.98);
  filter: none;
}
.loom-hot-btn.outline img { filter: none; }

/* Variant: tint — a softer hot button, surface-friendly */
.loom-hot-btn.tint {
  background: color-mix(in srgb, var(--hot-bg) 14%, transparent);
  color: var(--hot-bg);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--hot-bg) 30%, transparent);
}
.loom-hot-btn.tint:hover {
  background: color-mix(in srgb, var(--hot-bg) 22%, transparent);
  filter: none;
}
.loom-hot-btn.tint:active {
  background: color-mix(in srgb, var(--hot-bg) 30%, transparent);
  transform: translateY(1px) scale(0.98);
  filter: none;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--hot-bg) 40%, transparent),
              inset 0 1px 2px rgba(0,0,0,0.08);
}
.loom-hot-btn.tint img { filter: none; }

/* Live WebSocket status — rendered as a loomHotBtn so it sits flush with
   the right-side hot-button trio. State color comes from --hot-bg
   (driven by setWsStatus in loom.js); the only state-specific styling
   here is the pulse animation on the lightning icon when not LIVE. */
/* LIVE (WS status) pill — a cohesive surface pill (matches the other footer
   controls); the lightning glyph is a currentColor MASK carrying the status
   color: amber connecting → green live → red down. The glyph flashes yellow on
   each data arrival (see .ws-ping below). setWsStatus toggles the state class. */
/* SOLID pill with the bolt CUT OUT of it, rather than a glyph sitting on a
   surface. The whole capsule is filled with the status colour and the lightning
   shape is knocked out — two mask layers composited to subtract: layer 1 is the
   full pill, layer 2 is the bolt, `exclude` removes the overlap. `xor` is the
   WebKit spelling of the same thing and iOS needs it.
   The status colour moves to `background`, so the mask has something to punch
   through; the inner glyph span is hidden — it would be a bolt on a bolt. */
#loom-footer-actions .loom-hot-btn.loom-ws.icon-only {
  --ws-bolt: url('https://cdn.campaignbrain.dev/svg/icons/lightning.svg');
  border: 0; box-shadow: var(--sh-1);
  background: var(--amber, #D4A024);
  -webkit-mask-image: linear-gradient(#000, #000), var(--ws-bolt);
          mask-image: linear-gradient(#000, #000), var(--ws-bolt);
  -webkit-mask-position: center, center;
          mask-position: center, center;
  -webkit-mask-size: 100% 100%, 21px 21px;
          mask-size: 100% 100%, 21px 21px;
  -webkit-mask-repeat: no-repeat, no-repeat;
          mask-repeat: no-repeat, no-repeat;
  -webkit-mask-composite: xor;        /* Safari / iOS */
          mask-composite: exclude;    /* standard */
}
/* The glyph itself is now the hole. */
#loom-footer-actions .loom-hot-btn.loom-ws.icon-only .cdn-ico-mask { display: none; }
#loom-footer-actions .loom-hot-btn.loom-ws.connecting { background: var(--amber, #D4A024); }
#loom-footer-actions .loom-hot-btn.loom-ws.live { background: var(--green, #2D9B5A); }
#loom-footer-actions .loom-hot-btn.loom-ws.down { background: var(--red, #D92B2B); }
/* Not-yet-live states pulse as a "working" cue. The glyph is a hole now, so the
   PILL does the pulsing — same signal, one layer up. */
#loom-footer-actions .loom-hot-btn.loom-ws.connecting,
#loom-footer-actions .loom-hot-btn.loom-ws.down {
  animation: loom-ws-pulse 1.1s ease-in-out infinite;
}
@keyframes loom-ws-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.45; } }

/* ── Confidential line (loomConfidential) ───────────────────────────────────
   Shared primitive for restricted / internal boards. A thin red strip pinned
   at the BOTTOM of the shell (PWA-status-line style) instead of a fat top
   banner — it's the bottom-most flex-column band in #app, so it sits flush at
   the viewport's bottom edge beneath the version colophon. Opt in via
   loomConfidential() or <meta name="loom-confidential">. */
.loom-confidential-bar {
  flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; gap: 7px;
  padding: 4px 12px; min-height: 22px; box-sizing: border-box;
  background: color-mix(in srgb, var(--red) 84%, #000);
  color: #fff; font-size: 11px; font-weight: 600; letter-spacing: 0.03em;
  text-align: center; line-height: 1.3;
}
.loom-confidential-bar .cdn-ico-mask { background: #fff; flex-shrink: 0; }
.loom-confidential-bar .lc-detail { font-weight: 400; opacity: 0.9; }
/* Thin single line on phones — drop the detail, keep the warning. */
@media (max-width: 640px) { .loom-confidential-bar .lc-detail { display: none; } }
/* Data-arrival heartbeat — a one-shot flash each time an API response lands
   (pulseWsStatus in loom.js, throttled). Only fires in the live/green state:
   the lightning glyph fills YELLOW and pops briefly, then settles back to
   green. Reads as a calm heartbeat, not a strobe. */
/* `.live` in the selector, not just `.ws-ping`. The ping keyframes hardcode
   green, and now that they paint the whole capsule rather than a small glyph, a
   ping landing on a DOWN pill would flash the entire control green — saying the
   feed is fine at the exact moment it is not. The comment above always claimed
   this fired "only in the live state"; the selector now says so too. */
#loom-footer-actions .loom-hot-btn.loom-ws.live.ws-ping {
  animation: loom-ws-ping-fill 0.55s ease-out;
}
/* The whole capsule flashes amber and swells a hair — the bolt-shaped hole
   flashes with it, which reads better than a glyph changing colour did. */
@keyframes loom-ws-ping-fill {
  0%, 100% { background-color: var(--green, #2D9B5A); transform: scale(1); }
  35%      { background-color: var(--amber, #D4A024); transform: scale(1.12); }
}
@media (prefers-reduced-motion: reduce) {
  .loom-hot-btn.loom-ws.ws-ping,
  .loom-hot-btn.loom-ws.ws-ping img { animation: none; }
}

/* ── Loading scrim — the universal "Loading…" WINDOW (loomScrim) ────────────
   Promoted from the WARP transition into core so every app gets the same
   loading window on slow, user-directed API loads. Fixed full-viewport, a
   soft vortex glow behind an uppercase label; fades in/out. Auto-driven by the
   fetch wrapper in loom.js (or call loomScrim.show()/hide() directly). */
#loom-scrim {
  position: fixed; inset: 0; z-index: 9998;
  display: grid; place-items: center;
  /* Size the implicit rows to content and centre the GROUP — otherwise the
     spinner and a surfaced status label land in two stretched tracks and drift
     apart vertically instead of reading as one unit. */
  align-content: center; grid-auto-rows: min-content;
  /* Blur the background only — no dim, no fade. The scrim is instant. */
  background: transparent;
  -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px);
  visibility: hidden; pointer-events: none;
}
#loom-scrim.active { visibility: visible; pointer-events: auto; }

/* The global loading spinner is the Weave — warp threads with a woven fill wave
   climbing each (loomSpinner('weave'), scaled up for the scrim). On-brand,
   self-contained, no text. */
#loom-scrim .loom-spinner {
  position: relative;
  width: 66px; height: 46px;
  display: flex; align-items: center; justify-content: space-between;
  filter: drop-shadow(0 2px 10px color-mix(in srgb, var(--surface) 80%, transparent));
}
#loom-scrim .loom-spinner i {                 /* a warp thread */
  position: relative; display: block;
  width: 4px; height: 100%; border-radius: 4px; overflow: hidden;
  background: color-mix(in srgb, var(--accent) 22%, transparent);
}
#loom-scrim .loom-spinner i::after {          /* the woven fill climbing it */
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 100%;
  border-radius: 4px; transform-origin: bottom; transform: scaleY(0.18);
  /* Vermillion weave — the fill climbs from the accent into a lit tint of the
     SAME hue (was accent→gold, which read as a second brand color). */
  background: linear-gradient(to top, var(--accent), color-mix(in srgb, var(--accent) 52%, #fff));
  animation: loom-weave 1.1s ease-in-out infinite;
}
#loom-scrim .loom-spinner i:nth-child(1)::after { animation-delay: 0s; }
#loom-scrim .loom-spinner i:nth-child(2)::after { animation-delay: 0.11s; }
#loom-scrim .loom-spinner i:nth-child(3)::after { animation-delay: 0.22s; }
#loom-scrim .loom-spinner i:nth-child(4)::after { animation-delay: 0.33s; }
#loom-scrim .loom-spinner i:nth-child(5)::after { animation-delay: 0.44s; }
#loom-scrim .loom-scrim-label {               /* sr-only — kept for aria-live */
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* …but a CUSTOM status message (appLoading/appProgress text) is SURFACED under
   the spinner — the hook for narrating multi-step work. Generic boots stay
   textless, so the on-brand spinner is unchanged for everything else. */
#loom-scrim.has-text .loom-scrim-label {
  position: static; width: auto; height: auto; margin: 14px 0 0; padding: 0;
  overflow: visible; clip: auto; white-space: normal;
  max-width: min(78vw, 420px); text-align: center;
  font-size: 11px; font-weight: 700; letter-spacing: 0.07em;
  text-transform: uppercase; color: var(--text);
  text-shadow: 0 1px 4px var(--surface), 0 0 10px var(--surface);
}
@keyframes loom-weave {
  0%, 100% { transform: scaleY(0.18); opacity: 0.55; }
  50%      { transform: scaleY(1);    opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  #loom-scrim .loom-spinner i::after {
    animation: loom-breathe 1.6s ease-in-out infinite; transform: scaleY(0.6);
  }
}

/* ── Live log panel — overlay shown only when WS pill is tapped ────────── */
/* Anchored above the #loom-footer-actions control bar (~58px, version now
   inline). Strictly invisible (display:none) when closed — no peeking sliver. */
#loom-log-panel {
  display: none;
  position: fixed; left: 0; right: 0;
  bottom: calc(58px + env(safe-area-inset-bottom, 0px));
  height: 320px; max-height: 60vh;
  background: var(--surface);
  border-top: 1px solid var(--accent);
  box-shadow: 0 -8px 24px rgba(0,0,0,0.35);
  flex-direction: column;
  z-index: 999;
}
#loom-log-panel.open { display: flex; }

.loom-log-head {
  flex-shrink: 0;
  display: flex; align-items: center; gap: 12px;
  padding: 8px 16px; border-bottom: 1px solid var(--border);
  background: var(--surface2);
}
.loom-log-title {
  font-family: var(--font-mono), monospace;
  font-size: 10px; font-weight: 700; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--accent);
}
.loom-log-meta {
  font-family: var(--font-mono), monospace;
  font-size: 10px; color: var(--subdued);
}
.loom-log-spacer { flex: 1; }
.loom-log-btn {
  font-family: var(--font-mono), monospace;
  font-size: 10px; font-weight: 600; letter-spacing: 0.05em;
  text-transform: uppercase;
  background: transparent; border: 1px solid var(--border);
  color: var(--subdued); padding: 4px 10px; border-radius: 4px;
  cursor: pointer; margin-left: 6px;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.loom-log-btn:hover { color: var(--accent); border-color: var(--accent); }
.loom-log-btn.paused {
  color: var(--amber, #D4A024);
  border-color: var(--amber, #D4A024);
  background: color-mix(in srgb, var(--amber, #D4A024) 10%, transparent);
}
.loom-log-btn.ok { color: var(--green, #2D9B5A); border-color: var(--green, #2D9B5A); }
.loom-log-btn.err { color: var(--red, #D92B2B); border-color: var(--red, #D92B2B); }

.loom-log-body {
  flex: 1; overflow-y: auto;
  padding: 6px 0;
  font-family: var(--font-mono), monospace;
  font-size: 11px; line-height: 1.5;
}
.loom-log-empty {
  text-align: center; padding: 40px 20px;
  color: var(--muted); font-size: 11px;
}
.loom-log-row {
  display: grid;
  grid-template-columns: 100px 60px 1fr;
  gap: 12px; padding: 3px 16px;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
}
.loom-log-row:hover { background: var(--surface2); }
.loom-log-time { color: var(--muted); white-space: nowrap; }
.loom-log-tag {
  font-weight: 700; letter-spacing: 0.06em; font-size: 10px;
  color: var(--subdued); text-transform: uppercase;
}
.loom-log-msg {
  color: var(--text); word-break: break-word;
  white-space: pre-wrap;
}
.loom-log-row.level-error .loom-log-tag,
.loom-log-row.level-error .loom-log-msg { color: var(--red, #D92B2B); }
.loom-log-row.level-warn .loom-log-tag,
.loom-log-row.level-warn .loom-log-msg { color: var(--amber, #D4A024); }
.loom-log-row.level-info .loom-log-tag { color: var(--accent); }

/* ── Harness — right-side slide-out for function tracing ──────────────── */

#loom-harness {
  position: fixed; top: 0; right: 0; bottom: 0;
  width: 480px; max-width: 90vw;
  background: var(--surface);
  border-left: 1px solid var(--accent);
  box-shadow: -8px 0 24px rgba(0,0,0,0.45);
  display: flex; flex-direction: column;
  transform: translateX(100%);
  transition: transform 0.18s ease-out;
  z-index: 1100;  /* above log panel (999) */
}
#loom-harness.open { transform: translateX(0); }

.loom-harness-head {
  flex-shrink: 0;
  display: flex; align-items: center; gap: 10px;
  padding: 10px 14px; border-bottom: 1px solid var(--border);
  background: var(--surface2);
}
.loom-harness-title {
  font-family: var(--font-mono), monospace;
  font-size: 10px; font-weight: 700; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--accent);
}
.loom-harness-page {
  font-family: var(--font-mono), monospace;
  font-size: 10px; color: var(--subdued);
}
.loom-harness-spacer { flex: 1; }
.loom-harness-btn {
  font-family: var(--font-mono), monospace;
  font-size: 10px; font-weight: 600; letter-spacing: 0.05em;
  text-transform: uppercase;
  background: transparent; border: 1px solid var(--border);
  color: var(--subdued); padding: 4px 10px; border-radius: 4px;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s;
}
.loom-harness-btn:hover { color: var(--accent); border-color: var(--accent); }
.loom-harness-btn.close { padding: 4px 9px; font-size: 13px; line-height: 1; }

.loom-harness-body {
  flex: 1; overflow-y: auto;
  padding: 8px 0;
  font-family: var(--font-mono), monospace;
  font-size: 11px;
}
.loom-harness-empty {
  padding: 40px 24px; text-align: center;
  color: var(--muted); font-size: 11px; line-height: 1.6;
}
.loom-harness-empty code {
  background: var(--surface2); padding: 1px 5px; border-radius: 3px;
  color: var(--accent);
}

.loom-harness-master {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 14px; margin-bottom: 4px;
  background: color-mix(in srgb, var(--accent) 8%, var(--surface2));
  border-bottom: 1px solid var(--accent);
  position: sticky; top: 0; z-index: 1;
}
.loom-harness-master input[type=checkbox] {
  cursor: pointer; margin: 0; accent-color: var(--accent);
  transform: scale(1.2);
}
.loom-harness-master-label {
  font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  font-size: 10px; color: var(--accent);
}
.loom-harness-master-meta {
  margin-left: auto;
  font-size: 10px; color: var(--subdued);
}

.loom-harness-group {
  border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
}
.loom-harness-row {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 14px;
}
.loom-harness-row.ns {
  cursor: pointer;
  background: color-mix(in srgb, var(--surface2) 50%, transparent);
  font-weight: 600;
}
.loom-harness-row.ns:hover { background: var(--surface2); }
.loom-harness-row.member { padding-left: 36px; }
.loom-harness-row input[type=checkbox] {
  cursor: pointer; margin: 0;
  accent-color: var(--accent);
}
.loom-harness-caret {
  display: inline-block; width: 12px;
  color: var(--subdued); font-size: 10px;
}
.loom-harness-ns-name { color: var(--text); }
.loom-harness-fn { color: var(--text); flex: 1; }
.loom-harness-tag {
  font-size: 9px; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent); border: 1px solid var(--accent);
  padding: 1px 5px; border-radius: 3px;
}
.loom-harness-meta {
  font-size: 10px; color: var(--subdued);
}

.loom-harness-history {
  padding: 2px 14px 6px 36px;
  background: color-mix(in srgb, var(--surface2) 30%, transparent);
}
.loom-harness-call {
  display: grid;
  grid-template-columns: minmax(0,1fr) 12px minmax(0,1fr) 50px;
  gap: 6px; padding: 2px 0;
  font-size: 10px; line-height: 1.5;
}
.loom-harness-call.err .loom-harness-call-ret { color: var(--red, #D92B2B); }
.loom-harness-call-args { color: var(--subdued); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.loom-harness-arrow { color: var(--muted); text-align: center; }
.loom-harness-call-ret { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.loom-harness-call-ms { color: var(--muted); text-align: right; }

.loom-harness-foot {
  flex-shrink: 0;
  display: flex; align-items: center; gap: 8px;
  padding: 6px 14px; border-top: 1px solid var(--border);
  background: var(--surface2);
  font-family: var(--font-mono), monospace;
  font-size: 10px; color: var(--subdued);
}
/* The overload brake is dropping calls. Amber rather than red: the page is
   healthy BECAUSE of it, but the event count is no longer a complete record of
   what ran, and that has to be visible. */
.loom-harness-brake { color: var(--amber); font-weight: 600; }

/* ── Shared widgets — promoted from app-level inline CSS ──────────────── */

/* Drop zone — file upload target. Used in invoice-recon, ingest. */
.drop-zone {
  border: 2px dashed var(--border); border-radius: 10px;
  padding: 24px; text-align: center; cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
}
.drop-zone:hover, .drop-zone.dragover {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 6%, transparent);
}
.drop-zone .dz-icon { font-size: 28px; color: var(--muted); margin-bottom: 6px; }
.drop-zone .dz-text { font-size: 13px; color: var(--text); }
.drop-zone .dz-hint { font-size: 11px; color: var(--subdued); margin-top: 4px; }

/* Key/Value table — left-aligned label column, right-aligned values.
   Used in admin, tenant, infra, intel, etc. */
.kv-table { width: 100%; font-size: 12px; border-collapse: collapse; }
.kv-table td { padding: 6px 12px; border-bottom: 1px solid var(--surface2); }
.kv-table td:first-child {
  font-weight: 600; color: var(--subdued);
  white-space: nowrap; width: 180px;
}
.kv-table tr:last-child td { border-bottom: none; }

/* Inline key:value list — the lightweight sibling of .kv-table for compact
   card/detail metadata. Lines every VALUE up in one column (no more squished
   "label:value" runs). Markup is flat alternating spans:
     <div class="kv">
       <span class="kv-k">Channel</span><span class="kv-v">lobby</span>
       <span class="kv-k">Last seen</span><span class="kv-v">3m ago</span>
     </div>
   The label column is sized to the widest label (max-content); values fill the
   rest and wrap. Inherits the host's font-size, so it drops into any card. */
/* Works with either flat span pairs (.kv-k/.kv-v) OR semantic <dl><dt><dd> —
   add class="kv" to an existing <dl> to adopt the shared grid (replaces the
   per-app .hb-kv etc. copies). */
.kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 3px 12px; align-items: baseline; margin: 0; }
.kv-k, .kv dt { color: var(--subdued); white-space: nowrap; }
/* Values are fixed-width: data (ids, counts, times, statuses) lines up cleanly
   down the column and reads like a spec sheet (tabular-nums for clean digits).
   Wrap a value in a plain <span> if you specifically want prose. */
.kv-v, .kv dd { color: var(--text); margin: 0; min-width: 0; overflow-wrap: anywhere; font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.92em; font-variant-numeric: tabular-nums; }
.kv-v code, .kv dd code { font-family: inherit; }

/* Bar chart — horizontal bars with label/track/value. Used in admin, vpn,
   tor, infra. Override --bar-color per row for per-bar colors. */
.bar-chart { display: flex; flex-direction: column; gap: 6px; }
.bar-row { display: flex; align-items: center; gap: 8px; font-size: 11px; }
.bar-label {
  min-width: 140px; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; color: var(--text);
}
.bar-track {
  flex: 1; height: 16px; background: var(--border);
  border-radius: 4px; overflow: hidden;
}
.bar-fill {
  height: 100%; background: var(--bar-color, var(--accent));
  border-radius: 4px; transition: width 0.3s;
}
.bar-value {
  min-width: 40px; text-align: right;
  color: var(--subdued); font-size: 11px;
}

/* Utility — the most-common inline style across the codebase
   (color: var(--subdued); font-size: 11px; font-family: mono). */
.text-meta {
  color: var(--subdued); font-size: 11px;
  font-family: var(--font-mono), monospace;
}

/* ── Wizard — inline multi-step panel (loomWizard.mount) ────────────────── */
.loom-wizard {
  display: flex; flex-direction: column;
  background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
  overflow: hidden; min-height: 320px;
}
.loom-wizard-steps {
  display: flex; align-items: center; gap: 0;
  padding: 14px 20px; background: var(--surface2);
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap; row-gap: 8px;
}
.loom-wizard-step {
  appearance: none; border: 0; background: transparent; cursor: pointer;
  display: inline-flex; align-items: center; gap: 8px;
  padding: 4px 6px; border-radius: 6px;
  color: var(--subdued); font: 600 12px/1 var(--font-body), -apple-system, BlinkMacSystemFont, sans-serif;
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
}
.loom-wizard-step:hover:not(:disabled):not(.active) { color: var(--text); background: color-mix(in srgb, var(--accent) 6%, transparent); }
.loom-wizard-step.active { color: var(--text); }
.loom-wizard-step.done   { color: var(--text); }
.loom-wizard-step.locked,
.loom-wizard-step:disabled { cursor: not-allowed; opacity: 0.55; }
.loom-wizard-dot {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--surface3); color: var(--subdued);
  font-size: 11px; font-weight: 600;
  border: 1px solid var(--border);
}
.loom-wizard-step.done   .loom-wizard-dot { background: var(--green, #16a34a); color: #fff; border-color: var(--green, #16a34a); }
.loom-wizard-step.done   .loom-wizard-dot img { filter: brightness(0) invert(100%); }
.loom-wizard-step.active .loom-wizard-dot { background: var(--accent); color: #fff; border-color: var(--accent); }
.loom-wizard-step-label { white-space: nowrap; }
.loom-wizard-sep {
  flex: 1; min-width: 18px; max-width: 60px;
  height: 1px; background: var(--border);
  margin: 0 6px;
}
.loom-wizard-body {
  flex: 1; padding: 24px 20px;
  background: var(--surface);
  overflow-y: auto;
}
.loom-wizard-err {
  padding: 10px 14px; border-radius: 6px;
  background: color-mix(in srgb, var(--red, #dc2626) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--red, #dc2626) 35%, var(--border));
  color: var(--red, #dc2626); font-size: 12px;
}
.loom-wizard-err-inline {
  font-size: 11px; color: var(--red, #dc2626);
  margin-right: 12px; white-space: nowrap;
}
.loom-wizard-footer {
  display: flex; align-items: center; gap: 8px;
  padding: 12px 16px;
  background: var(--surface2);
  border-top: 1px solid var(--border);
}
.loom-wizard-spacer { flex: 1; }
.loom-wizard-btn {
  appearance: none; border: 1px solid var(--border); background: var(--surface);
  border-radius: 6px; padding: 8px 16px; cursor: pointer;
  font: 600 12px/1 var(--font-body), -apple-system, BlinkMacSystemFont, sans-serif;
  color: var(--text);
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 34px; -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
.loom-wizard-btn:hover:not(:disabled) { background: var(--surface2); border-color: var(--accent); }
.loom-wizard-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.loom-wizard-btn.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
.loom-wizard-btn.primary:hover:not(:disabled) { background: var(--accent-hover, var(--accent)); }
.loom-wizard-btn.primary img { filter: brightness(0) invert(100%); }
.loom-wizard-btn img { pointer-events: none; }
[data-arrow-right="1"] img { order: 2; }
[data-theme="dark"] .loom-wizard-btn:not(.primary) img,
[data-theme="midnight"] .loom-wizard-btn:not(.primary) img,
[data-theme="oxblood"] .loom-wizard-btn:not(.primary) img,
[data-theme="ember"] .loom-wizard-btn:not(.primary) img,
[data-theme="forest"] .loom-wizard-btn:not(.primary) img,
[data-theme="ocean"] .loom-wizard-btn:not(.primary) img,
[data-theme="neon"] .loom-wizard-btn:not(.primary) img { filter: brightness(0) invert(85%); }

@media (max-width: 540px) {
  .loom-wizard-steps { padding: 10px 12px; gap: 4px; }
  .loom-wizard-sep { max-width: 18px; }
  .loom-wizard-step-label { display: none; }       /* dots only on narrow screens */
  .loom-wizard-step.active .loom-wizard-step-label { display: inline; }  /* current step still shows label */
  .loom-wizard-body { padding: 16px 12px; }
}

/* ── Modal — used by loomConfirm() and any reusable confirm dialogs ────── */
.loom-modal-wrap {
  position: fixed; inset: 0; z-index: 1000;
  display: flex; align-items: center; justify-content: center;
}
.loom-modal-backdrop {
  position: absolute; inset: 0;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(2px);
}
.loom-modal {
  position: relative;
  min-width: 320px; max-width: 480px; width: calc(100% - 40px);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6);
  padding: 22px 22px 18px;
  /* Cap height + scroll the field body so tall forms (e.g. the Sim field
     builder) keep their title + actions reachable on small screens. */
  display: flex; flex-direction: column;
  max-height: calc(100dvh - 32px);
}
.loom-modal-title, .loom-modal-actions { flex: none; }
.loom-modal .loom-form-body { overflow-y: auto; min-height: 0; }
.loom-modal-actions { margin-top: 14px; }

/* ── loomScreenshot result popup ─────────────────────────────────────────── */
.loom-shot-result { display: flex; flex-direction: column; gap: 10px; }
.loom-shot-thumb {
  display: block; border: 1px solid var(--border); border-radius: 8px;
  overflow: hidden; background: var(--surface2); max-height: 320px;
}
.loom-shot-thumb img { display: block; width: 100%; height: auto; }
.loom-shot-url {
  width: 100%; box-sizing: border-box; font-family: ui-monospace, monospace;
  font-size: 11px; padding: 7px 9px; border: 1px solid var(--border);
  border-radius: 6px; background: var(--surface); color: var(--text);
}
.loom-shot-hint { font-size: 11px; color: var(--subdued); }
.loom-modal-title {
  font-size: 15px; font-weight: 700;
  color: var(--text); margin-bottom: 8px;
}
.loom-modal-body {
  font-size: 13px; color: var(--subdued);
  line-height: 1.5; margin-bottom: 18px;
}
.loom-modal-actions {
  display: flex; justify-content: flex-end; gap: 10px;
}
.loom-modal-btn {
  font-family: inherit; font-size: 12px; font-weight: 600;
  padding: 7px 14px; border-radius: 6px;
  cursor: pointer; transition: background 0.12s, border-color 0.12s, color 0.12s;
  border: 1px solid var(--border);
  background: transparent; color: var(--subdued);
}
.loom-modal-btn:hover { color: var(--text); border-color: var(--accent); }

/* ── Modal in-flight (cbloom#177) ───────────────────────────────────────────
   The third Waiting surface. A modal given `onConfirm` stays open and owns the
   wait, so the confirm button borrows the same spinner `.loading` puts on a
   booting page — one glyph across all three surfaces. */
.loom-modal-btn.loading {
  padding-left: 12px;                      /* the ::before adds its own 12+8 */
  cursor: progress; opacity: 0.85;
}
.loom-modal-btn.loading::before { margin-right: 6px; }
.loom-modal-btn:disabled { opacity: 0.55; cursor: not-allowed; }
.loom-modal-btn.loading:disabled { cursor: progress; opacity: 0.85; }
/* Dim the fields while the work runs so the modal reads as busy, without
   hiding what was typed — a rejection puts the form back in play. */
.loom-modal-busy .loom-form-body { opacity: 0.6; pointer-events: none; }
.loom-modal-err {
  margin-top: 12px; padding: 8px 10px; border-radius: 6px;
  font-size: 12px; line-height: 1.45; color: var(--red, #dc2626);
  border: 1px solid color-mix(in srgb, var(--red, #dc2626) 35%, transparent);
  background: color-mix(in srgb, var(--red, #dc2626) 10%, transparent);
}
.loom-modal-err[hidden] { display: none; }
.loom-modal-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.loom-modal-btn.primary {
  background: var(--accent); color: #fff; border-color: var(--accent);
}
.loom-modal-btn.primary:hover { background: color-mix(in srgb, var(--accent) 85%, #000); }
.loom-modal-btn.danger {
  background: var(--red, #D92B2B); color: #fff;
  border-color: var(--red, #D92B2B);
}
.loom-modal-btn.danger:hover { background: color-mix(in srgb, var(--red, #D92B2B) 85%, #000); }

/* Run-mode log + spinner — async tasks via loomModal.run() get a live log
   pane and a spinning indicator that flips green on done() / red on fail().
   The spinner sits in the title row; the log occupies the body area below. */
.loom-modal-title .loom-modal-spinner {
  display: inline-block; width: 14px; height: 14px;
  border: 2px solid var(--border); border-top-color: var(--accent);
  border-radius: 50%; vertical-align: middle; margin-right: 8px;
  animation: loom-modal-spin 0.7s linear infinite;
}
.loom-modal-spinner.done {
  border: 2px solid var(--green, #2D9B5A); animation: none;
  position: relative;
}
.loom-modal-spinner.done::after {
  content: ''; position: absolute; left: 3px; top: 0px;
  width: 4px; height: 8px; border: solid var(--green, #2D9B5A);
  border-width: 0 2px 2px 0; transform: rotate(45deg);
}
.loom-modal-spinner.fail {
  border: 2px solid var(--red, #D92B2B); animation: none;
  position: relative;
}
.loom-modal-spinner.fail::before,
.loom-modal-spinner.fail::after {
  content: ''; position: absolute; left: 5px; top: 1px;
  width: 2px; height: 8px; background: var(--red, #D92B2B);
}
.loom-modal-spinner.fail::before { transform: rotate(45deg); }
.loom-modal-spinner.fail::after  { transform: rotate(-45deg); }
@keyframes loom-modal-spin { to { transform: rotate(360deg); } }

.loom-modal-log {
  background: var(--surface2); color: var(--text);
  font: 12px/1.5 var(--font-mono, ui-monospace, Menlo, monospace);
  margin: 8px 0 14px; padding: 10px 12px; border-radius: 6px;
  max-height: 200px; min-height: 60px; overflow-y: auto;
  white-space: pre-wrap; word-break: break-word;
  border: 1px solid var(--border);
}
.loom-modal-log:empty::before {
  content: '— starting…'; color: var(--subdued); font-style: italic;
}

/* Form modal — fields rendered by loomFormModal */
.loom-form-body { display: flex; flex-direction: column; gap: 12px; margin-bottom: 18px; }
.loom-form-row { display: flex; flex-direction: column; gap: 4px; }
.loom-form-row.inline { flex-direction: row; align-items: center; gap: 8px; }
.loom-form-row.inline label { order: 1; }
.loom-form-row label {
  font-family: var(--font-mono), monospace; font-size: 10px; font-weight: 600;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--subdued);
}
.loom-form-row input[type=text],
.loom-form-row input[type=number],
.loom-form-row input[type=date],
.loom-form-row input[type=time],
.loom-form-row input[type=datetime-local],
.loom-form-row input[type=email],
.loom-form-row input[type=tel],
.loom-form-row input[type=password],
.loom-form-row textarea,
.loom-form-row select {
  background: var(--surface2); border: 1px solid var(--border); border-radius: 6px;
  padding: 8px 10px; color: var(--text); font-family: inherit; font-size: 13px;
  outline: none; transition: border-color 0.12s;
}
.loom-form-row textarea { resize: vertical; min-height: 60px; line-height: 1.5; }
.loom-form-row input:focus,
.loom-form-row textarea:focus,
.loom-form-row select:focus { border-color: var(--accent); }
.loom-form-row input[type=checkbox] {
  width: 16px; height: 16px; accent-color: var(--accent); cursor: pointer; margin: 0;
}
.loom-form-hint { font-size: 11px; color: var(--muted); }
.loom-form-err { font-size: 11px; color: var(--red, #D92B2B); min-height: 14px; }

/* Combo field — a visible native <select> beside a free-text box, sharing one
   value (loomFormModal `type: 'combo'`). Row on anything wide enough for both
   halves to stay usable, stacked below that: side-by-side at 320px gives two
   ~150px controls where the picker truncates every option to "cbmodel…". */
.loom-combo { display: flex; gap: 6px; min-width: 0; }
.loom-combo > select { flex: 0 1 45%; min-width: 0; }
.loom-combo > input { flex: 1 1 55%; min-width: 0; }
@media (max-width: 440px) {
  .loom-combo { flex-direction: column; }
  .loom-combo > select, .loom-combo > input { flex: 0 0 auto; }
}

/* Search picker — search-as-you-type entity selector */
.loom-search-picker {
  position: relative;
  background: var(--surface2); border: 1px solid var(--border); border-radius: 6px;
  padding: 4px 6px;
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px;
}
.loom-search-picker:focus-within { border-color: var(--accent); }
.lsp-tags { display: contents; }
.lsp-tag {
  display: inline-flex; align-items: center; gap: 4px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 4px;
  padding: 2px 6px 2px 8px; font-size: 12px; color: var(--text);
}
.lsp-tag-x {
  background: none; border: none; color: var(--muted); cursor: pointer;
  font-size: 14px; line-height: 1; padding: 0 2px;
}
.lsp-tag-x:hover { color: var(--red, #D92B2B); }
/* Scoped under .loom-search-picker so the shared `.field input` rule (0,1,1 —
   surface2 bg + border + 8px padding) can't out-specify it: pickers mounted
   inside a .field previously rendered box-in-a-box (the cc-launch double
   border) and lost the 16px iOS-zoom guard below. */
.loom-search-picker .lsp-input {
  flex: 1; min-width: 100px; background: transparent; border: none;
  outline: none; color: var(--text); font-family: inherit; font-size: 13px;
  padding: 4px;
}
.loom-search-picker .lsp-input::placeholder { color: var(--muted); }
.lsp-results {
  position: absolute; top: 100%; left: 0; right: 0; z-index: 100;
  margin-top: 4px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.35);
  max-height: 280px; overflow-y: auto;
}
.lsp-row { padding: 8px 12px; cursor: pointer; border-bottom: 1px solid var(--surface2); }
.lsp-row:last-child { border-bottom: none; }
.lsp-row:hover { background: var(--surface2); }
.lsp-row-name { font-size: 13px; color: var(--text); }
.lsp-row-sub { font-size: 11px; color: var(--subdued); margin-top: 2px; }
.lsp-empty { padding: 12px; text-align: center; color: var(--muted); font-size: 12px; }

/* ── loomCheckDropdown — chonky multi-select dropdown ───────────────────── */
.loom-ckdd { position: relative; display: inline-block; }
.loom-ckdd-btn {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 10px 15px; border-radius: 11px;
  background: var(--surface2); border: 1px solid var(--border); color: var(--text);
  font: inherit; font-size: 14px; font-weight: 600; cursor: pointer;
}
.loom-ckdd-btn:hover { border-color: var(--accent); }
.loom-ckdd-count {
  font-size: 12px; font-weight: 700; color: var(--subdued);
  background: var(--surface3); border-radius: 20px; padding: 2px 9px;
  font-variant-numeric: tabular-nums;
}
.loom-ckdd-caret { color: var(--subdued); transition: transform 0.15s ease; font-size: 11px; }
.loom-ckdd.open .loom-ckdd-caret { transform: rotate(180deg); }
.loom-ckdd-panel {
  position: absolute; z-index: 60; margin-top: 6px; min-width: 248px;
  max-height: min(60vh, 460px); overflow: auto;
  background: var(--surface); border: 1px solid var(--border); border-radius: 13px;
  padding: 8px; box-shadow: 0 12px 34px rgba(0, 0, 0, 0.38);
}
.loom-ckdd-quick { display: flex; gap: 6px; padding: 2px 4px 8px; border-bottom: 1px solid var(--border); margin-bottom: 6px; }
.loom-ckdd-quick button {
  flex: 1; padding: 7px; border-radius: 8px;
  background: var(--surface2); border: 1px solid var(--border); color: var(--text);
  font: inherit; font-size: 12px; font-weight: 700; cursor: pointer;
}
.loom-ckdd-quick button:hover { border-color: var(--accent); color: var(--accent); }
.loom-ckdd-opt {
  display: flex; align-items: center; gap: 11px;
  padding: 10px 11px; border-radius: 9px; cursor: pointer;
  font-size: 14px; color: var(--text);
}
.loom-ckdd-opt:hover { background: var(--surface2); }
.loom-ckdd-opt input { width: 18px; height: 18px; accent-color: var(--accent); cursor: pointer; flex-shrink: 0; }
.loom-ckdd-opt-label { flex: 1; min-width: 0; }
.loom-ckdd-opt-hint { font-size: 11px; font-style: italic; color: var(--subdued); white-space: nowrap; flex-shrink: 0; }
/* loomMenuBtn — action rows in the same popover chassis. */
.loom-menu-item {
  display: flex; align-items: center; gap: 11px; width: 100%; text-align: left;
  padding: 10px 11px; border-radius: 9px; cursor: pointer;
  background: none; border: none; font: inherit; font-size: 14px; color: var(--text);
}
.loom-menu-item:hover { background: var(--surface2); }

/* ── loomSection — canonical boxed section ──────────────────────────────────
 * NB: distinct from .loom-panel (the flex/grid sizing utility further down).
 *   <div class="loom-section">
 *     <div class="loom-section-header">
 *       <span class="loom-section-title">Tenant Status <span class="loom-section-count">(5)</span></span>
 *       <span class="badge ...">DEMO</span>
 *       <span class="loom-section-actions">…buttons…</span>
 *     </div>
 *     <div class="loom-section-body">…content…</div>
 *     <div class="loom-section-note info">Legal wall: …</div>   ← optional
 *   </div>
 *
 * Built via loomSection({title, count, badges, actions, body, footerNote, footerKind}).
 */
.loom-section {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 18px 20px;
  margin-bottom: 16px;
}
.loom-section-header {
  display: flex; align-items: center; gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}
.loom-section-title {
  font-size: 14px; font-weight: 600; color: var(--text);
}
.loom-section-count {
  color: var(--subdued); font-weight: 400; margin-left: 2px;
}
.loom-section-actions {
  margin-left: auto;
  display: flex; gap: 8px; flex-wrap: wrap;
}
.loom-section-body { font-size: 13px; color: var(--text); }
.loom-section-note {
  margin-top: 14px;
  padding: 10px 14px;
  border-radius: 4px;
  border-left: 3px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 6%, transparent);
  font-size: 12px; color: var(--text);
}
.loom-section-note.warn {
  border-left-color: var(--amber, #D4A024);
  background: color-mix(in srgb, var(--amber, #D4A024) 8%, transparent);
}
.loom-section-note.success {
  border-left-color: var(--green, #2D9B5A);
  background: color-mix(in srgb, var(--green, #2D9B5A) 8%, transparent);
}

/* iOS Safari auto-zooms the page when a focused input's font-size is
   below 16px. Bump the typeahead input to 16px on touch devices only
   (desktop keeps the compact 13px). `hover: none` + `pointer: coarse`
   is the canonical touch-only media query. */
@media (hover: none) and (pointer: coarse) {
  .loom-search-picker .lsp-input { font-size: 16px; }
}

/* ── Timeline — vertical activity feed (CSS-only widget) ──────────────── */
/* Markup:
 *   <div class="loom-timeline">
 *     <div class="lt-row">
 *       <div class="lt-time">2026-04-16 14:32</div>
 *       <div class="lt-icon" style="--c: var(--accent)"></div>
 *       <div class="lt-body">
 *         <div class="lt-title">Write-up sent to Maria Santos</div>
 *         <div class="lt-meta">by Jake Thompson · BoldSign #abc123</div>
 *       </div>
 *     </div>
 *     ...
 *   </div>
 */
.loom-timeline {
  position: relative;
  padding-left: 12px;
}
.loom-timeline::before {
  content: ''; position: absolute;
  left: 7px; top: 6px; bottom: 6px;
  width: 1px; background: var(--border);
}
.loom-timeline .lt-row {
  position: relative;
  display: grid;
  grid-template-columns: 110px 16px 1fr;
  gap: 12px;
  padding: 8px 0 12px 0;
  align-items: start;
}
.loom-timeline .lt-time {
  font-family: var(--font-mono), monospace; font-size: 10px;
  color: var(--muted); text-align: right;
  padding-top: 2px;
}
.loom-timeline .lt-icon {
  position: relative; width: 12px; height: 12px;
  border-radius: 50%;
  background: var(--c, var(--accent));
  margin-left: -7px; margin-top: 4px;
  box-shadow: 0 0 0 3px var(--bg);
}
.loom-timeline .lt-body {
  font-size: 13px; color: var(--text); line-height: 1.45;
}
.loom-timeline .lt-title { font-weight: 500; }
.loom-timeline .lt-meta {
  font-size: 11px; color: var(--subdued);
  margin-top: 2px;
  font-family: var(--font-mono), monospace;
}

/* ── Drawer — left sidebar with collapse-to-rail pattern ──────────────── */
/* Pages opt in by overriding #main:
 *   #main { display: flex; flex-direction: row; padding: 0; overflow: hidden; }
 * Markup:
 *   <div id="hr-drawer" class="loom-drawer collapsed" data-store-key="hr:drawerOpen">
 *     <button class="loom-drawer-toggle">SVG</button>
 *     <div class="loom-drawer-rail-label">LABEL</div>
 *     <div class="loom-drawer-header">…</div>
 *     <div class="loom-drawer-body">…scrollable…</div>
 *   </div>
 *
 * Wire it up:
 *   loomDrawer.init('#hr-drawer'); // reads data-store-key, persists state
 */
.loom-drawer {
  width: 300px; flex-shrink: 0;
  background: var(--surface); border-right: 1px solid var(--border);
  display: flex; flex-direction: column; overflow: hidden;
  transition: width 0.2s ease;
}
.loom-drawer.collapsed {
  width: 24px; cursor: pointer;
  background: var(--surface2);
  border-right: 2px solid var(--accent);
}
.loom-drawer.collapsed:hover { background: color-mix(in srgb, var(--accent) 15%, var(--surface2)); }
.loom-drawer.collapsed .loom-drawer-toggle { color: var(--accent); pointer-events: none; }
.loom-drawer.collapsed .loom-drawer-toggle svg { transform: rotate(180deg); }
.loom-drawer.collapsed .loom-drawer-header,
.loom-drawer.collapsed .loom-drawer-body { display: none; }
.loom-drawer.collapsed .loom-drawer-rail-label { display: flex; }

.loom-drawer-toggle {
  flex-shrink: 0; display: flex; align-items: center; justify-content: center;
  width: 100%; height: 40px;
  padding: 0; cursor: pointer; border: none; background: none;
  color: var(--subdued); transition: color 0.15s, background 0.15s;
}
.loom-drawer-toggle:hover { color: var(--accent); background: var(--surface2); }
.loom-drawer-toggle svg { transition: transform 0.2s; }

.loom-drawer-rail-label {
  display: none;
  flex: 1; align-items: center; justify-content: center;
  writing-mode: vertical-lr; text-orientation: mixed;
  font-family: var(--font-mono), monospace; font-size: 11px;
  font-weight: 700; letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--accent); user-select: none; pointer-events: none;
}

.loom-drawer-header {
  flex-shrink: 0; padding: 0 14px 12px; border-bottom: 1px solid var(--border);
}
.loom-drawer-body { flex: 1; overflow-y: auto; padding: 4px 0; }

/* Default drawer = a PERSISTENT sidebar: it sits beside fully-usable content
   (open by default; clicking the main pane does NOT dim or disable it). The
   old "open = blur + disable the content beside it" behaviour is now opt-in
   via `.loom-drawer-modal` (loomDrawer.init({modal:true}) / data-modal="true")
   for the rare temporary-overlay-menu case. Changed for usability — a sidebar
   you open shouldn't lock the page. */
.loom-drawer-modal:not(.collapsed) + * {
  filter: blur(2px); opacity: 0.4;
  pointer-events: none; user-select: none;
  transition: filter 0.2s, opacity 0.2s;
}
.loom-drawer-modal.collapsed + * {
  filter: none; opacity: 1;
  pointer-events: auto; user-select: auto;
  transition: filter 0.2s, opacity 0.2s;
}

/* Mobile: rail stays inline; expanded becomes an overlay over the content,
   with a tap-to-dismiss scrim behind it (injected by loomDrawer.init). On
   desktop the drawer is inline (side-by-side) so the scrim never shows. */
.loom-drawer-backdrop { display: none; }
@media (max-width: 768px) {
  .loom-drawer:not(.collapsed) {
    position: absolute; top: 0; left: 0; bottom: 0; z-index: 30;
    width: 280px;
    box-shadow: 4px 0 16px rgba(0,0,0,0.5);
  }
  .loom-drawer-backdrop.active {
    display: block; position: fixed; inset: 0; z-index: 29;
    background: rgba(0,0,0,0.45);
  }
}

/* ── loomAppGroup — iOS-folder-style app groups ───────────────────────────
 * A group renders as a folder tile (2×2 mini-icon preview) sized to match the
 * dashboard's .app-tile so it sits in the same grid; tap opens an overlay grid.
 * Single-app groups render as a plain .app-tile (handled in JS). */
.loom-appgroup-tile {
  background: none; border: none; padding: 12px 8px; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  text-align: center; color: var(--text); transition: transform 0.2s ease;
}
.loom-appgroup-tile:hover { transform: translateY(-3px); }
.loom-appgroup-folder {
  width: 56px; height: 56px; border-radius: 15px; box-sizing: border-box;
  background: color-mix(in srgb, var(--group-accent, var(--accent)) 30%, var(--surface2));
  display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr;
  gap: 3px; padding: 7px; transition: transform 0.2s;
}
.loom-appgroup-tile:hover .loom-appgroup-folder { transform: scale(1.08); }
.loom-appgroup-mini {
  display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.92); border-radius: 5px; overflow: hidden;
  /* The tile is fixed-white in every theme, so the glyph must be a fixed dark
     ink — the desktop svgImg() now paints a currentColor .cdn-ico-mask, which
     would otherwise inherit --text (white on dark) and vanish into the tile. */
  color: #20242e;
}
.loom-appgroup-mini img,
.loom-appgroup-mini .cdn-ico-mask { width: 14px; height: 14px; }
.loom-appgroup-name {
  font-size: 13.5px; font-weight: 600; line-height: 1.3; max-width: 100%;
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.loom-appgroup-count { color: var(--subdued); font-weight: 400; font-size: 11px; }

.loom-appgroup-overlay {
  position: fixed; inset: 0; z-index: 1200;
  background: rgba(0,0,0,0.5); backdrop-filter: blur(3px);
  display: flex; align-items: center; justify-content: center; padding: 24px;
}
.loom-appgroup-overlay:not(.active) { display: none; }
.loom-appgroup-panel {
  background: var(--surface); border: 1px solid var(--border);
  border-top: 3px solid var(--group-accent, var(--accent));
  border-radius: 14px; box-shadow: 0 12px 40px rgba(0,0,0,0.45);
  width: min(560px, 100%); max-height: 80vh; overflow-y: auto; padding: 16px 18px 20px;
}
.loom-appgroup-panel-head { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; }
.loom-appgroup-panel-title { font-size: 15px; font-weight: 700; flex: 1; }
.loom-appgroup-close {
  background: none; border: none; color: var(--subdued); cursor: pointer;
  font-size: 22px; line-height: 1; padding: 0 4px;
}
.loom-appgroup-close:hover { color: var(--text); }

/* ── KPI grid — aligned stat tiles for dashboards ─────────────────────────
 * Responsive: auto-fits columns, wraps cleanly on narrow screens. Intents
 * (ok / warn / danger) colorize the value only, keeping labels consistent.
 *
 *   <div class="loom-kpi-grid">
 *     <div class="loom-kpi-card">
 *       <div class="loom-kpi-label">Headcount</div>
 *       <div class="loom-kpi-value">47</div>
 *     </div>
 *     <div class="loom-kpi-card warn">
 *       <div class="loom-kpi-label">Open Discipline</div>
 *       <div class="loom-kpi-value">3</div>
 *     </div>
 *     ...
 *   </div>
 */
.loom-kpi-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 10px;
}
.loom-kpi-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 10px; padding: 14px 16px;
  display: flex; flex-direction: column; gap: 6px;
  min-height: 72px;
}
.loom-kpi-label {
  font-size: 10px; font-weight: 700; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--subdued);
}
.loom-kpi-value {
  font-size: 22px; font-weight: 700; color: var(--text);
  font-family: var(--font-mono), monospace;
  line-height: 1.05;
  display: flex; align-items: baseline; gap: 6px;
  overflow-wrap: anywhere;
}
.loom-kpi-value .loom-kpi-suffix {
  font-size: 12px; color: var(--muted); font-weight: 500;
  font-family: var(--font-mono), monospace;
}
.loom-kpi-card.ok     .loom-kpi-value { color: var(--green, #2D9B5A); }
.loom-kpi-card.warn   .loom-kpi-value { color: var(--amber, #D4A024); }
.loom-kpi-card.danger .loom-kpi-value { color: var(--red, #D92B2B); }
.loom-kpi-sub {
  font-size: 11px; color: var(--muted); margin-top: 2px;
}

/* ── Section Title ──────────────────────────────────────────────────────── */
.section-title {
  font-size: 10px; font-weight: 600; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--subdued);
  margin-bottom: 10px; margin-top: 24px;
}
/* loomSectionTitle({ actions }) — heading left, its control right. The actions
   slot opts out of the heading's uppercase/tracking so button text reads
   normally, and wraps rather than squashing the label on a phone. */
.section-title.has-actions {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
}
.section-title .section-actions {
  text-transform: none; letter-spacing: normal; font-weight: 400;
  display: flex; gap: 6px; align-items: center;
}

/* ── Badges ─────────────────────────────────────────────────────────────── */
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 600; }
.badge-green  { background: #22c55e22; color: var(--green); }
.badge-amber  { background: #f59e0b22; color: var(--amber); }
.badge-red    { background: #ef444422; color: var(--red); }
.badge-blue   { background: #3b82f622; color: var(--accent); }
.badge-muted  { background: #3a3f5222; color: var(--subdued); }
.badge-purple { background: #8b5cf622; color: #8b5cf6; }

/* ── Panel utility ──────────────────────────────────────────────────────── */
/* Container that obeys its parent's width and contains its children. Drop on
   any panel-like element (cards, detail blocks, list rows — anything inside
   a flex/grid context) to get sane mobile shrink without writing the same
   four properties every time. The all-built-in panels below (.detail-panel,
   .kpi-card, .loom-card, .app-card) already include this baseline. */
.loom-panel        { min-width: 0; max-width: 100%; overflow: hidden; box-sizing: border-box; }
.loom-panel-scroll { min-width: 0; max-width: 100%; overflow: auto;   box-sizing: border-box; }

/* ── KPI Cards ──────────────────────────────────────────────────────────── */
.kpi-row { display: grid; gap: 12px; margin-bottom: 20px; }
.kpi-card {
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  padding: 16px 20px; display: flex; flex-direction: column;
  align-items: center; text-align: center; gap: 4px;
  /* loom-panel baseline */
  min-width: 0; max-width: 100%; box-sizing: border-box;
}
.kpi-card.clickable { cursor: pointer; transition: border-color 0.15s; }
.kpi-card.clickable:hover { border-color: var(--accent); }
.kpi-card .kpi-label { font-size: 10px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--subdued); }
.kpi-card .kpi-value { font-size: 24px; font-weight: 700; }
.kpi-card .kpi-sub { font-size: 11px; color: var(--subdued); }

/* ── Data Table ─────────────────────────────────────────────────────────── */
.data-table {
  width: 100%; border-collapse: collapse; font-size: 12px;
  background: var(--surface); border-radius: 8px; overflow: hidden;
  border: 1px solid var(--border);
}
.data-table th {
  text-align: left; padding: 10px 12px; font-weight: 600; color: var(--subdued);
  font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase;
  border-bottom: 1px solid var(--border); background: var(--surface);
}
.data-table td { padding: 10px 12px; border-bottom: 1px solid var(--surface2); }
.data-table tbody tr:last-child td { border-bottom: none; }
.data-table tbody tr.clickable { cursor: pointer; transition: background 0.1s; }
.data-table tbody tr.clickable:hover { background: var(--surface2); }
/* loomDataTable rowLink rows are focusable — show where the keyboard is. */
.data-table tbody tr[role="link"]:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.data-table .num { text-align: right; }
.data-table .truncate { max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* loomDataTable — grouped rows (collapsible header / subtotal row). */
.data-table tr.dt-group { cursor: pointer; user-select: none; }
.data-table tr.dt-group td {
  background: var(--surface2); font-weight: 600;
  border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
}
.data-table tr.dt-group:hover td { background: var(--surface3, var(--surface2)); }
.data-table .dt-caret { display: inline-block; width: 12px; color: var(--subdued); }
.data-table .dt-gcount,
.dt-gcard .dt-gcount {
  font-size: 10px; color: var(--subdued); background: var(--surface3, var(--border));
  padding: 0 6px; border-radius: 8px; margin-left: 6px; font-weight: 500;
}

/* The card view's group header (loomDataTable groupBy, viewports ≤768px).
   Same collapse affordance as tr.dt-group — a tap target, a caret, a count —
   restyled as a card so it reads as the section head of the member cards
   below it rather than as one of them. */
.dt-gcard {
  display: flex; align-items: baseline; gap: 8px;
  background: var(--surface2); border: 1px solid var(--border); border-radius: 8px;
  padding: 10px 14px; cursor: pointer; user-select: none; font-weight: 600;
  min-width: 0; max-width: 100%; box-sizing: border-box;
}
.dt-gcard:hover { background: var(--surface3, var(--surface2)); }
.dt-gcard .dt-caret { color: var(--subdued); flex: none; }
.dt-gcard .dt-gcard-h { min-width: 0; }
/* Member cards sit under their header; a following header opens the next
   section. The extra top margin separates sections without a wrapper div. */
.dt-gcard + .dt-gcard, * + .dt-gcard { margin-top: 8px; }

/* loomDataTable — editable cells (the keyboard grid). */
.data-table td.dt-editcell { padding: 3px 6px; }
.data-table input.dt-edit,
.data-table select.dt-edit {
  width: 100%; box-sizing: border-box; background: var(--surface);
  border: 1px solid var(--border); border-radius: 5px; padding: 3px 6px;
  color: var(--text); font: inherit; font-variant-numeric: tabular-nums;
}
/* Same overflow guard as .ctrl-select: a native <select> sizes to its widest
   option, which would widen the column past its content. Truncate instead. */
.data-table select.dt-edit { cursor: pointer; max-width: 100%; text-overflow: ellipsis; }
.data-table td.dt-editcell.num input.dt-edit { text-align: right; }
.data-table input.dt-edit:focus,
.data-table select.dt-edit:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent); }

/* loomDataTable — frozen first column + sticky header (dense-grid feel).
   table.dt-sticky-first freezes column 1; .table-wrap.dt-scroll adds the
   vertical scroller with a sticky header row. */
.table-wrap.dt-scroll { overflow: auto; border: 1px solid var(--border); border-radius: 10px; }
.table-wrap.dt-scroll thead th { position: sticky; top: 0; z-index: 3; }
.data-table.dt-sticky-first th:first-child,
.data-table.dt-sticky-first td:first-child {
  position: sticky; left: 0; z-index: 1; background: var(--surface);
}
.data-table.dt-sticky-first tbody tr.clickable:hover td:first-child { background: var(--surface2); }
.data-table.dt-sticky-first tr.dt-group td:first-child { background: var(--surface2); z-index: 3; }
.table-wrap.dt-scroll .data-table.dt-sticky-first thead th:first-child { z-index: 5; }
/* Sticky group headers: each group is its own <tbody>, so its header sticks
   just under the column header until the next group scrolls up and replaces it. */
.table-wrap.dt-scroll tr.dt-group td { position: sticky; top: var(--dt-header-h, 34px); z-index: 2; }

/* ── Filter Bar ─────────────────────────────────────────────────────────── */
.filter-bar { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; align-items: center; }
.filter-bar select, .filter-bar input {
  background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
  padding: 6px 10px; color: var(--text); font-family: inherit; font-size: 11px; outline: none;
}
.filter-bar select:focus, .filter-bar input:focus { border-color: var(--accent); }
/* The loomDataTable toolbar's search box (a BARE child input — panel inputs
   live inside <label>s and are untouched) takes the full row on a phone.
   At its natural width it clipped its own placeholder ("Find a race or
   candidate or…") on every app that passes filterPlaceholder. */
@media (max-width: 768px) {
  .filter-bar > input[type="text"] { flex: 1 1 100%; min-width: 0; }
}

/* ── loom-input — canonical standalone text field ──────────────────────────
   The robust text input for use OUTSIDE a modal (.loom-form-row) or filter bar
   (.filter-bar input) — same surface/border/focus language, reusable anywhere
   (wizard steps, inline forms, settings). Full-width by default; cap with an
   inline max-width or a wrapper. Widget Zoo → Pickers. */
.loom-input,
textarea.loom-input,
select.loom-input {
  width: 100%; box-sizing: border-box;
  background: var(--surface2); border: 1px solid var(--border); border-radius: 8px;
  padding: 9px 12px; color: var(--text);
  font-family: inherit; font-size: 14px; line-height: 1.4; outline: none;
  transition: border-color 0.12s, box-shadow 0.12s;
}
.loom-input::placeholder { color: var(--subdued); }
.loom-input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
.loom-input:disabled { opacity: 0.55; cursor: not-allowed; }
textarea.loom-input { resize: vertical; min-height: 64px; }

/* ── Filter Pills (canonical) ───────────────────────────────────────────────
   The rounded count-filter pills from the /app/earshot reference layout
   ("All 15,573 · Heard 964 · Resolved 3,415 · No stream 11,194"). The standard
   widget for a small set of mutually-exclusive filters with optional counts —
   use this instead of rolling a per-app `.st-chip`/`.kind-pill`/etc. Markup is
   emitted by loomPills() in loom.js, but the classes are plain so any app can
   hand-roll the same bar. Active pill = filled accent; idle = bordered well.
   For dropdown/text filters use .filter-bar above; pills are the count chips. */
.loom-pills { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; align-items: center; }
/* Compact mode (loomPills opts.compact) — the host is layout-neutral
   (display:contents, so children join the parent's flex row); the badge-styled
   native select replaces the bar ≤600px, where chips eat a full row each. */
.loom-pills-host { display: contents; }
.loom-pill-select {
  display: none; border: 1px solid var(--border); background: var(--surface2);
  color: var(--text); border-radius: var(--r-pill, 999px); padding: 5px 26px 5px 12px;
  font-size: 12px; font-weight: 600; font-family: inherit; outline: none;
  max-width: 100%; appearance: none; -webkit-appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--subdued) 50%),
                    linear-gradient(135deg, var(--subdued) 50%, transparent 50%);
  background-position: calc(100% - 14px) 50%, calc(100% - 10px) 50%;
  background-size: 4px 4px, 4px 4px; background-repeat: no-repeat;
}
.loom-pill-select:focus { border-color: var(--accent); }

/* ── ctrl-date — month/date field with a calendar affordance ──────────────
   Applied by loomDecorateDates() (loomInit runs it on boot): wraps
   input.ctrl-input[type=month|date] and pins a masked CDN calendar glyph on
   the right. Native pickers show no hint they're tappable, and stretch
   full-width on phones — capped here (half-width ≤540px). */
.ctrl-date { position: relative; display: inline-flex; align-items: center; max-width: 100%; }
.ctrl-date .ctrl-input { padding-right: 28px; width: 100%; box-sizing: border-box; min-width: 0; }
.ctrl-date .ctrl-date-ico {
  position: absolute; right: 9px; pointer-events: none;
  color: var(--subdued); display: inline-flex;
}
input.ctrl-input[type="month"], input.ctrl-input[type="date"] { max-width: 175px; }
@media (max-width: 540px) {
  .cc-filters .ctrl-date, .filter-bar .ctrl-date { width: 50%; }
}

/* ── loomStatStrip — compact summary-stat bar (the cc-donations look) ─────── */
.loom-stats {
  display: flex; flex-wrap: wrap; gap: 1.25rem; align-items: baseline;
  margin: 0.75rem 0 1rem; padding: 0.75rem 1rem;
  border: 1px solid var(--border); border-radius: 8px; background: var(--surface);
}
.loom-stat { display: flex; flex-direction: column; }
.loom-stat .v { font-size: 1.25rem; font-weight: 600; }
.loom-stat .k {
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--subdued);
}
.loom-stat.good .v { color: var(--green); }
.loom-stat.bad .v { color: var(--red); }
@media (max-width: 540px) { .loom-stats { gap: .75rem; } }

/* ── loomSwitch — checkbox with chrome (pill track + sliding knob) ────────── */
.loom-switch {
  display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
  font-size: 12px; color: var(--subdued); user-select: none;
}
.loom-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.loom-switch-track {
  width: 34px; height: 18px; border-radius: var(--r-pill, 999px); flex-shrink: 0;
  background: var(--surface3, var(--border)); border: 1px solid var(--border);
  position: relative; transition: background 0.18s, border-color 0.18s;
}
.loom-switch-knob {
  position: absolute; top: 1px; left: 1px; width: 14px; height: 14px;
  border-radius: 50%; background: var(--subdued);
  transition: transform 0.18s, background 0.18s;
}
.loom-switch input:checked + .loom-switch-track { background: var(--accent); border-color: var(--accent); }
.loom-switch input:checked + .loom-switch-track .loom-switch-knob { transform: translateX(16px); background: #fff; }
.loom-switch input:focus-visible + .loom-switch-track { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent); }
.loom-switch:hover .loom-switch-label { color: var(--text); }
.loom-switch input:disabled + .loom-switch-track { opacity: 0.5; cursor: not-allowed; }

/* ── loomRange — slider + live value readout ──────────────────────────────
   A bare <input type="range"> inherits the UA's chrome and, dropped into a
   .cc-filters label (a COLUMN), pushes its value readout onto a third line —
   caption / slider / "0%" stacked, which is what /app/pipeline-results shipped.
   The row keeps slider and value on one line under the caption. Track/thumb
   generalized from mi-sim's .sim-range, the row from elections-sim. */
.loom-range-row { display: flex; align-items: center; gap: 10px; min-width: 170px; }
.ctrl-range {
  -webkit-appearance: none; appearance: none;
  flex: 1 1 auto; min-width: 0; height: 6px; margin: 0;
  border-radius: 4px; background: var(--surface3, var(--border));
  accent-color: var(--accent); outline: none; cursor: pointer;
}
/* Thumb rules can't be grouped — an unknown pseudo-element voids the whole
   selector list, so WebKit and Firefox each need their own declaration. */
.ctrl-range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--accent); border: 2px solid var(--surface); cursor: pointer;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
.ctrl-range::-moz-range-thumb {
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--accent); border: 2px solid var(--surface); cursor: pointer;
}
.ctrl-range:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent); }
.ctrl-range:disabled { opacity: 0.5; cursor: not-allowed; }
.loom-range-val {
  font: 600 12px var(--font-mono, ui-monospace, monospace);
  color: var(--text); text-align: right; flex-shrink: 0; min-width: 4ch;
}
@media (max-width: 600px) {
  .loom-pills-host .loom-pills { display: none; }
  .loom-pills-host .loom-pill-select { display: inline-block; margin-bottom: 12px; }
  .cc-filters .loom-pills-host .loom-pill-select { margin-bottom: 0; }
}
.loom-pill {
  border: 1px solid var(--border); background: var(--surface2); color: var(--subdued);
  border-radius: 999px; padding: 5px 12px; font-size: 12px; font-weight: 600;
  font-family: inherit; cursor: pointer; transition: all 0.12s; white-space: nowrap;
}
.loom-pill:hover { border-color: var(--accent); color: var(--text); }
.loom-pill.active { background: var(--accent); border-color: var(--accent); color: #fff; }
.loom-pill b { font-weight: 700; opacity: 0.85; margin-left: 3px; }

/* ── Pagination ─────────────────────────────────────────────────────────── */
.pagination {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 8px;
  margin-top: 12px; font-size: 11px; color: var(--subdued);
}
.pagination button, .paginator button {
  background: var(--surface); border: 1px solid var(--border); border-radius: 4px;
  padding: 4px 12px; color: var(--text); font-family: inherit; font-size: 11px; cursor: pointer;
}
.pagination button:disabled, .paginator button:disabled { opacity: 0.3; cursor: default; }
.pagination button:hover:not(:disabled), .paginator button:hover:not(:disabled) { border-color: var(--accent); }
/* Numbered mode (loomPagerBar with page/pages/onPage). */
.pagination .pg-ctl {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
  justify-content: flex-end;
}
.pagination .pg-num { padding: 4px 8px; min-width: 28px; }
.pagination .pg-num.active,
.pagination .pg-num.active:disabled {
  background: var(--accent); border-color: var(--accent);
  color: #fff; opacity: 1;
}
.pagination .pg-gap { color: var(--subdued); padding: 0 2px; user-select: none; }

.paginator {
  display: flex; align-items: center; justify-content: space-between;
  margin-top: 8px; font-size: 11px; color: var(--subdued);
}

/* ── View Toggle (List / Card) ──────────────────────────────────────────── */
.view-toggle { display: flex; gap: 4px; }
.view-toggle button {
  background: var(--surface); border: 1px solid var(--border); border-radius: 4px;
  color: var(--subdued); cursor: pointer; font-size: 13px; padding: 4px 8px; line-height: 1;
}
.view-toggle button.active { border-color: var(--accent); color: var(--accent); }

/* ── Table / Card Responsive Wrappers ───────────────────────────────────── */
/* Card-first default — toggling to list view is opt-in via the view-toggle
   buttons. setListView()/setGlobalListView() flips display:none inline. */
.table-wrap { display: none; }
.cards-wrap { }
@media (max-width: 768px) {
  /* Card-first swap — EXCEPT listOnly tables (dense grids with no card form,
     e.g. the elections board). Those stay as a horizontally-scrollable table. */
  .table-wrap:not(.dt-listonly) { display: none !important; }
  /* Flex column (not block) so the cards-wrap's own `gap` actually applies —
     block drops gap, leaving mobile card lists flush (0px). Each wrap's gap
     wins; this default covers wraps that set none. */
  .cards-wrap { display: flex !important; flex-direction: column; gap: 12px; }
  .table-wrap.dt-listonly + .cards-wrap { display: none !important; }
  .view-toggle { display: none; }
}

/* ── Cards (generic) ────────────────────────────────────────────────────── */
.loom-card, .app-card {
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  padding: 12px 14px; margin-bottom: 8px; cursor: pointer; transition: border-color 0.15s;
  /* loom-panel baseline */
  min-width: 0; max-width: 100%; overflow: hidden; box-sizing: border-box;
}
.loom-card:hover, .app-card:hover { border-color: var(--accent); }
.card-title { font-size: 12px; font-weight: 600; margin-bottom: 6px; }
.card-meta { font-size: 11px; color: var(--subdued); display: flex; flex-wrap: wrap; gap: 8px; }
.card-meta span { display: flex; align-items: center; gap: 4px; }

/* ── Tabs ───────────────────────────────────────────────────────────────── */
/* Canonical tab strip used across every multi-section app (kb, auth, admin,
   banking, hr, fb-ops, …). Visual treatment: surface2 background strip with
   a 1px bottom border, accent-colored underline on the active tab, subdued→
   text on hover. Horizontal scroll with right-edge fade-out kicks in when
   the tabs overflow the viewport (mobile + crowded tab bars). */
.tabs {
  display: flex; gap: 0;
  /* v0.0.526 — chrome layout flipped. Tab strip is now a direct child of
     #app, sitting BETWEEN the header and crumb bar. The negative margins
     from v0.0.520-0.0.525 (which compensated for #main's 20px padding when
     the strip lived inside #main) are gone — the strip spans full-width
     naturally as a flex-column sibling. Internal horizontal padding keeps
     tabs inset from the viewport edge.

     position: sticky is a no-op here (no scrolling ancestor at this level —
     only #main scrolls, and we're outside it) but stays for back-compat in
     case any per-app CSS depends on it. The strip is visually always at the
     top of the chrome stack because its layout slot is fixed there. */
  padding: 0 var(--app-pad-x);
  background: var(--surface2);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 5;
  flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden;
  scroll-behavior: smooth; -webkit-overflow-scrolling: touch;
  scrollbar-width: none; /* Firefox */
}
/* v0.0.524 — restored right-edge fade indicator (Brad: "I like the fade —
   it's an indication to the user something is over here") via a sticky
   pseudo-element that overlays the rightmost 28px with a transparent →
   --surface2 gradient. Distinct from the v0.0.521 mask-image approach
   which faded the whole .tabs element (bg + content) — when the user
   scrolled the page, content text bled through the right edge. This
   overlay paints OVER content with the surface2 color so the bg stays
   fully opaque while still telegraphing "more tabs over here". */
.tabs::after {
  content: '';
  flex: 0 0 28px;
  align-self: stretch;
  margin-left: -28px;          /* sit over the last 28px of tabs, not after them */
  position: sticky;
  right: 0;
  background: linear-gradient(to right, transparent, var(--surface2));
  pointer-events: none;
}
.tabs::-webkit-scrollbar { display: none; }
/* Tab overflow scroll chevrons (v0.0.628) — desktop has no swipe gesture, so
   when the strip overflows we surface ‹ › buttons that scroll it. Like the
   ::after fade, they're sticky to the strip edges (ride along as it scrolls)
   and use negative margins so they OVERLAY the edge tabs instead of consuming
   layout width. Injected + toggled by loomTabScroll in loom.js — `.show` only
   when there's hidden content in that direction. Mobile keeps touch-swipe, so
   they're hidden ≤768px. */
.tab-chevron {
  flex: 0 0 28px; align-self: stretch;
  position: sticky; z-index: 2;
  display: none; align-items: center; justify-content: center;
  padding: 0; border: 0; cursor: pointer;
  color: var(--subdued); font-size: 18px; line-height: 1; font-family: inherit;
}
.tab-chevron:hover { color: var(--text); }
.tab-chevron.left  { left: 0;  margin-right: -28px; background: linear-gradient(to right, var(--surface2) 55%, transparent); }
.tab-chevron.right { right: 0; margin-left: -28px;  background: linear-gradient(to left,  var(--surface2) 55%, transparent); }
.tab-chevron.show { display: flex; }
@media (max-width: 768px) { .tab-chevron { display: none !important; } }
.tab {
  /* v0.0.523 — tightened padding-top from 10 → 6 + bottom from 10 → 8 so
     the tab labels sit closer to the crumb bar above. With the tab strip
     already flush against the crumb bar (margin-top: -20px), the buttons'
     own 10px top padding was the only remaining "gap" Brad could see. */
  padding: 6px 16px 8px; cursor: pointer;
  font-size: 13px; font-weight: 500;
  color: var(--subdued);
  white-space: nowrap; flex-shrink: 0;
  /* Reset button defaults so <button class="tab"> renders identical to
     <div class="tab">. The canonical helper loomTabBar() emits divs, but
     kb + future a11y-focused apps use buttons for focusability. */
  background: transparent; border: 0;
  border-bottom: 2px solid transparent;
  font-family: inherit;
}
.tab.active { color: var(--text); border-bottom-color: var(--accent); }
.tab:hover { color: var(--text); }

/* ── loom-subnav ──────────────────────────────────────────────────────────
   Cross-page pill-link bar for a bundle of sibling flash-apps (loomSubnav()).
   Replaces per-app ".cc-nav"-style link bars duplicated across member CSS.
   Distinct from .tabs (in-page router tabs) — these are real <a> page links. */
.loom-subnav { display: flex; gap: 6px; margin: 0 0 16px; flex-wrap: wrap; }
/* Crumb-docked variant (platform review P3b): loomInit moves a top-of-page
   bundle subnav out of #content and into the crumb bar — crumbs flush-left,
   pills right beside the back/forward chip — collapsing one whole nav band.
   Wide sets scroll horizontally inside the row; on narrow screens the pills
   wrap onto their own line below the crumbs (same height as the old band,
   nothing lost). Opt out per-app: data-subnav-inline on the container. */
#loom-crumb-bar .loom-subnav {
  margin: 0 0 0 auto; gap: 4px; flex-wrap: nowrap; min-width: 0;
  overflow-x: auto; scrollbar-width: none;
}
#loom-crumb-bar .loom-subnav::-webkit-scrollbar { display: none; }
#loom-crumb-bar .loom-subnav-link { padding: 2px 10px; font-size: 12px; white-space: nowrap; }
#loom-crumb-bar .loom-subnav ~ .loom-nav, #loom-crumb-bar .loom-nav { flex-shrink: 0; }
@media (max-width: 820px) {
  #loom-crumb-bar { flex-wrap: wrap; }
  #loom-crumb-bar .loom-subnav { order: 10; flex-basis: 100%; margin-left: 0; }
}
.loom-subnav-link {
  padding: 5px 14px; border-radius: 999px; font-size: 13px; line-height: 1.4;
  text-decoration: none; background: var(--surface2); color: var(--subdued);
  border: 1px solid var(--border);
}
.loom-subnav-link:hover { background: var(--surface3); color: var(--text); }
.loom-subnav-link.active { background: var(--accent); color: #fff; border-color: var(--accent); }
/* Compact variant — sits inline inside a header / crumb bar (no bottom margin). */
.loom-subnav--compact { margin: 0; gap: 4px; }
.loom-subnav--compact .loom-subnav-link { padding: 2px 10px; font-size: 12px; }

@media (max-width: 768px) {
  /* v0.0.955 — the strip's horizontal inset is NO LONGER tightened here. It
     inherits the canonical --app-pad-x (20px) just like the crumb bar and
     #main, so the tab strip, crumb, and content all share ONE left/right
     alignment line on mobile (the earshot reference). The old 0 12px override
     pulled the tabs ~8px further left than everything below them. Overflow on
     crowded strips is handled by horizontal scroll + the right-edge fade, not
     by shaving the edge inset. Only the per-tab inner padding tightens here. */
  .tab { padding: 10px 12px; font-size: 12px; }
}

/* ── Buttons ────────────────────────────────────────────────────────────── */
.btn {
  border: none; border-radius: 6px; padding: 8px 20px;
  font-family: inherit; font-size: 12px; font-weight: 600;
  cursor: pointer; transition: opacity 0.15s;
  /* Variant-less default. Without an explicit fill, a bare `.btn` falls through
     to the UA button style — which paints iOS-Safari's tint BLUE on our themed
     pages (the "transparent blue button"). This makes `.btn` alone a proper
     neutral/secondary button everywhere; every color variant below overrides
     both properties, so they are unaffected. */
  background: var(--surface3); color: var(--text);
}
.btn:hover { opacity: 0.85; }
.btn:disabled { opacity: 0.4; cursor: default; }
.btn-green { background: var(--green); color: #fff; }
.btn-red { background: var(--red-fill); color: #fff; }
.btn-blue { background: var(--accent); color: #fff; }
/* --text, not --subdued. A subdued label on a --border fill measured 2.31:1 —
   this button got LESS readable when the 2026-08-02 pass raised --border for
   visibility, because the fill moved toward the label. A quiet button still
   has to be a readable one; --text on --border is 5.00:1 and the button stays
   quiet through its flat fill, not through a label you cannot read. */
.btn-muted { background: var(--border); color: var(--text); }
/* Relay flavor — a hand-off that pipes one service's output into another (Loom
   Relay). The left→right accent→indigo gradient reads as "A ⟶ B", setting it
   apart from the solid status fills; use it for "send to <service>" actions. */
.btn-relay { background: linear-gradient(90deg, var(--accent) 0%, #6366f1 100%); color: #fff; }
.btn-relay:hover { opacity: 1; filter: brightness(1.08); }
/* Three standard sizes — the canonical scale across app bundles:
   .btn-sm (compact card/row actions) · .btn (default, md) · .btn-lg (primary
   page actions / touch). Don't invent per-app sizes; pick one of these. */
/* loomActionBtn — a .btn that owns its own wait (disable + spinner until the
   handler settles). Inline-flex so the spinner and the label share a baseline
   in both states; the width is pinned in JS so the swap doesn't shove a
   toolbar around. */
.loom-action-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; }
.loom-action-btn .cdn-ico-mask { flex: none; pointer-events: none; }
.loom-action-btn[aria-busy="true"] { cursor: progress; opacity: 0.75; }
.btn-sm { padding: 6px 14px; font-size: 11px; }
.btn-lg { padding: 11px 26px; font-size: 14px; border-radius: 8px; }

/* ── .btn-add — canonical "Add X" primary-create affordance ───────────────────
   The obvious "create new" button: plus glyph (svgMask, follows currentColor) +
   "Add <Label>" in accent styling. On narrow viewports the label collapses to
   icon-only — kept as aria-label + title for screen readers and tooltips — so
   it shrinks gracefully in tight mobile toolbars without losing clarity. Use
   loomAddButton() (lib/loom.js) or this class directly. Layers on .btn-blue. */
.btn-add { display: inline-flex; align-items: center; gap: 6px; }
.btn-add .btn-add-label { white-space: nowrap; }
@media (max-width: 600px) {
  .btn-add { padding-left: 12px; padding-right: 12px; }
  .btn-add .btn-add-label {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }
}

/* .visually-hidden — accessible label kept off-screen but read by SR / tooltip.
   The fleet's canonical sr-only utility (no such class existed before #252). */
.visually-hidden {
  position: absolute !important; width: 1px; height: 1px; padding: 0;
  margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ── Form Fields ────────────────────────────────────────────────────────── */
.field { margin-bottom: 12px; }
/* The caption above a field. `:not(.loom-switch)` because loomSwitch IS a
   <label> too — dropped into a .field (the documented pattern: caption above,
   switch below) it was matching this rule and getting display:block +
   uppercase + 10px, which collapsed the track to a hairline and shouted the
   switch's own label. Excluding it lets both live in a .field: the caption
   styles, the switch renders as a switch. */
.field label:not(.loom-switch) {
  display: block; font-size: 10px; font-weight: 600; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--subdued); margin-bottom: 4px;
}
.field input, .field select, .field textarea {
  width: 100%; max-width: 100%; background: var(--surface2);
  border: 1px solid var(--border); border-radius: 6px;
  padding: 8px 10px; color: var(--text); font-family: inherit; font-size: 12px;
  outline: none; box-sizing: border-box;
}
.field input:focus, .field select:focus, .field textarea:focus { border-color: var(--accent); }
.field textarea { resize: vertical; min-height: 60px; }

/* ── Detail Panels ──────────────────────────────────────────────────────── */
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 768px) { .detail-grid { grid-template-columns: 1fr; } }
.detail-panel {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 8px; padding: 16px;
  /* loom-panel baseline */
  min-width: 0; max-width: 100%; overflow: hidden; box-sizing: border-box;
}
.detail-panel h3 {
  font-size: 10px; font-weight: 600; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--subdued); margin-bottom: 12px; margin-top: 16px;
}
.detail-panel h3:first-child { margin-top: 0; }

/* ── Expandable Panel ───────────────────────────────────────────────────── */
.expand-bar {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 8px; margin-top: 16px; overflow: hidden;
}
.expand-toggle {
  display: flex; align-items: center; padding: 10px 16px;
  cursor: pointer; gap: 8px; font-size: 11px; color: var(--subdued);
  transition: color 0.15s;
}
.expand-toggle:hover { color: var(--text); }
.toggle-icon { transition: transform 0.2s; }
.toggle-icon.open { transform: rotate(90deg); }
.expand-panel { padding: 0 16px 16px; display: none; }
.expand-panel.open { display: block; }

/* ── Modal — generic backdrop + dialog. Apps inject their own DOM, but the
   chrome (backdrop blur, dialog shell, action row) comes from here. */
.modal-backdrop {
  position: fixed; inset: 0; background: rgba(0,0,0,0.55);
  display: none; align-items: center; justify-content: center;
  z-index: 1000;
}
.modal-backdrop.active { display: flex; }
.modal {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 8px; padding: 18px;
  max-width: 560px; width: 92%; max-height: 85vh; overflow-y: auto;
  box-shadow: 0 12px 40px rgba(0,0,0,0.5);
}
.modal h2 { margin: 0 0 4px; font-size: 15px; }
.modal .modal-sub { color: var(--subdued); font-size: 12px; margin-bottom: 12px; }
.modal-section { margin: 10px 0; }
.modal-section h4 {
  margin: 0 0 6px; font-size: 11px;
  text-transform: uppercase; letter-spacing: 0.05em; color: var(--subdued);
}
.modal textarea {
  width: 100%; min-height: 60px; padding: 8px;
  font: inherit; font-size: 12px;
  background: var(--bg, #0a0a0a); color: var(--text);
  border: 1px solid var(--border); border-radius: 4px;
  box-sizing: border-box;
}
.modal-actions {
  display: flex; gap: 8px; justify-content: flex-end; margin-top: 14px;
}

/* ── Chat tool card — collapsible block showing a single tool call ───────
   Header is always visible (chevron / spinner / name / elapsed). Detail
   block (inputs / result) renders when .open is on the card itself; the
   chevron rotates 90° via the same class. */
.tool-card {
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  padding: 10px 14px; margin: 8px 0;
  transition: border-color 0.15s;
}
.tool-card:hover { border-color: var(--accent); }
.tool-card-hdr {
  display: flex; align-items: center; gap: 8px; cursor: pointer;
  color: var(--subdued); font-size: 11px;
  user-select: none;
}
.tool-card-hdr:hover { color: var(--text); }
.tool-chevron {
  display: inline-block; transition: transform 0.15s;
  font-size: 10px; color: var(--subdued);
  width: 10px; text-align: center;
  flex-shrink: 0;
}
.tool-card.open .tool-chevron { transform: rotate(90deg); color: var(--accent); }
.tool-name { font-weight: 600; color: var(--accent); }
.tool-elapsed { color: var(--subdued); font-size: 10px; margin-left: auto; }
/* The global inline spinner is Dual Ring (loomSpinner('dual')). Every
   <span class="tool-spinner"> across the fleet renders as it. */
.tool-spinner {
  display: inline-block; position: relative; width: 14px; height: 14px;
  border-radius: 50%; flex-shrink: 0; vertical-align: middle;
}
.tool-spinner::before, .tool-spinner::after {
  content: ''; position: absolute; inset: 0; border-radius: 50%; border: 2px solid transparent;
}
.tool-spinner::before { border-top-color: var(--accent); animation: loom-spin-rot 0.9s linear infinite; }
.tool-spinner::after { inset: 3px; border-bottom-color: color-mix(in srgb, var(--accent) 55%, #fff);
  animation: loom-spin-rot 0.65s linear infinite reverse; }

/* ── loomSpinner — themeable inline spinners (see loomSpinner in loom.js) ──── */
/* Sized via --spin-size, coloured off --accent so they adapt to tenant themes. */
.loom-spin { display: inline-flex; align-items: center; justify-content: center; vertical-align: middle;
  width: var(--spin-size, 24px); height: var(--spin-size, 24px); flex-shrink: 0; }
/* Dual ring — two counter-rotating arcs. */
.loom-spin-dual { position: relative; }
.loom-spin-dual::before, .loom-spin-dual::after { content: ''; position: absolute; inset: 0;
  border-radius: 50%; border: max(2px, calc(var(--spin-size, 24px) * 0.08)) solid transparent; }
.loom-spin-dual::before { border-top-color: var(--accent); animation: loom-spin-rot 0.9s linear infinite; }
.loom-spin-dual::after { inset: calc(var(--spin-size, 24px) * 0.22);
  border-bottom-color: color-mix(in srgb, var(--accent) 55%, #fff);
  animation: loom-spin-rot 0.65s linear infinite reverse; }
/* Weave — warp threads with a fill wave climbing each (reuses loom-weave). */
.loom-spin-weave { display: inline-flex; align-items: center; justify-content: space-between;
  width: calc(var(--spin-size, 24px) * 1.15); height: var(--spin-size, 24px); }
.loom-spin-weave i { position: relative; display: block; width: 12%; height: 100%; border-radius: 3px;
  overflow: hidden;
  /* Solid fallback FIRST. color-mix voids the whole declaration on Safari < 16.2
     (and any iPad capped at iOS 15) — without a fallback the track is
     transparent, the animation runs on nothing, and it reads as "the spinner
     isn't firing". The faded track is the enhancement; a solid bar degrades
     fine. */
  background: var(--accent);
  background: color-mix(in srgb, var(--accent) 20%, transparent); }
.loom-spin-weave i::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 100%;
  border-radius: 3px; transform-origin: bottom; transform: scaleY(0.2);
  /* Same guard for the animated fill: a plain accent gradient renders on old
     iOS; the color-mix top-highlight is progressive enhancement above it. */
  background: linear-gradient(to top, var(--accent), var(--accent));
  background: linear-gradient(to top, var(--accent), color-mix(in srgb, var(--accent) 55%, #fff));
  animation: loom-weave 1.1s ease-in-out infinite; }
.loom-spin-weave i:nth-child(2)::after { animation-delay: 0.11s; }
.loom-spin-weave i:nth-child(3)::after { animation-delay: 0.22s; }
.loom-spin-weave i:nth-child(4)::after { animation-delay: 0.33s; }
.loom-spin-weave i:nth-child(5)::after { animation-delay: 0.44s; }
@keyframes loom-spin-rot { to { transform: rotate(360deg); } }
/* Reduce Motion must not mean "no signal". `animation: none` froze every
   working indicator solid — on an iPad with Settings → Accessibility → Motion →
   Reduce Motion on, a busy agent looked exactly like an idle one, which is the
   one thing a spinner exists to prevent.
   What Reduce Motion asks us to drop is MOVEMENT — spin, parallax, travel. A
   slow, shallow opacity pulse carries no motion vector, so the indicator keeps
   saying "still working" without the vestibular cost. */
@keyframes loom-breathe { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
@media (prefers-reduced-motion: reduce) {
  .loom-spin-dual::before, .loom-spin-dual::after {
    animation: loom-breathe 1.8s ease-in-out infinite;
  }
  .loom-spin-dual::after { animation-delay: 0.9s; }
  .loom-spin-weave i::after {
    animation: loom-breathe 1.6s ease-in-out infinite; transform: scaleY(0.6);
  }
  .loom-spin-weave i:nth-child(2)::after { animation-delay: 0.2s; }
  .loom-spin-weave i:nth-child(3)::after { animation-delay: 0.4s; }
  .loom-spin-weave i:nth-child(4)::after { animation-delay: 0.6s; }
}
.tool-done {
  display: inline-flex; align-items: center; justify-content: center;
  width: 14px; height: 14px; color: var(--green);
  font-size: 13px; line-height: 1; flex-shrink: 0;
}
.tool-detail { margin-top: 10px; display: none; }
.tool-card.open .tool-detail { display: block; }

/* ── loomOnboardingGrid — clickable suggestion chips ──────────────────────
   Used on chat-app empty panes. Grid auto-collapses to a single column on
   narrow viewports; tab keys cycle naturally because each chip is a button. */
.onboarding-grid {
  display: grid; gap: 8px; max-width: 560px;
  width: 100%;
}
.onboarding-btn {
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  padding: 10px 14px; font-size: 11px; color: var(--text); cursor: pointer;
  font-family: inherit; transition: border-color 0.15s, color 0.15s;
  text-align: left; line-height: 1.4;
}
.onboarding-btn:hover { border-color: var(--accent); color: var(--accent); }
@media (max-width: 520px) {
  .onboarding-grid { grid-template-columns: 1fr !important; }
}

/* ── loomToolResult — smart-rendered tool input/output ─────────────────────
   Used inside chat tool cards (cc-chat, victory-chat) to make tool JSON
   readable: tables for arrays of objects, receipt rows for plain objects,
   bullets for arrays of primitives, error blocks for error envelopes. */
.tool-result-primitive {
  font-family: var(--font-mono, monospace);
  font-size: 12px; color: var(--text);
  word-break: break-word;
}
.tool-result-empty {
  font-style: italic; font-size: 11px; color: var(--subdued);
}
.tool-result-error {
  color: var(--red); font-size: 11px;
  padding: 6px 10px; border-left: 3px solid var(--red);
  background: color-mix(in srgb, var(--red) 8%, transparent);
  border-radius: 0 4px 4px 0;
}
.tool-result-list {
  margin: 2px 0; padding-left: 18px;
  font-size: 11px; color: var(--text);
}
.tool-result-list li { padding: 1px 0; line-height: 1.5; }
.tool-result-more {
  font-size: 10px; color: var(--subdued); font-style: italic;
  padding: 4px 0 2px;
}
.tool-result-table-wrap {
  overflow-x: auto; max-width: 100%;
  border: 1px solid var(--border); border-radius: 4px;
  background: var(--surface2);
}
.tool-result-table {
  font-size: 11px; border-collapse: collapse; width: max-content; min-width: 100%;
  font-family: var(--font-mono, monospace);
}
.tool-result-table th, .tool-result-table td {
  padding: 4px 8px; text-align: left;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.tool-result-table th {
  font-weight: 700; color: var(--subdued);
  text-transform: uppercase; letter-spacing: 0.04em;
  font-size: 10px; background: var(--surface);
  position: sticky; top: 0;
}
.tool-result-table tr:last-child td { border-bottom: none; }
.tool-result-table td { color: var(--text); }
.tool-result-receipt {
  display: flex; flex-direction: column; gap: 2px;
}
.tool-result-row {
  display: grid; grid-template-columns: minmax(80px, max-content) 1fr;
  gap: 10px; padding: 3px 0; align-items: baseline;
  font-size: 11px; border-bottom: 1px dotted var(--border);
}
.tool-result-row:last-child { border-bottom: none; }
.tool-result-key {
  color: var(--subdued); font-family: var(--font-mono, monospace);
}
.tool-result-val {
  color: var(--text); word-break: break-word; min-width: 0;
}
.tool-result-json {
  background: var(--surface2); border-radius: 4px; padding: 8px;
  font-size: 11px; overflow-x: auto; white-space: pre-wrap;
  max-height: 240px; overflow-y: auto;
  color: var(--subdued); font-family: var(--font-mono, monospace);
  margin: 0;
}
.tool-section-label {
  font-size: 10px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--subdued); margin: 8px 0 4px;
}
.tool-section-label:first-child { margin-top: 0; }

/* ── Toast Notifications — centered top, larger, theme-aware ─────────────── */
#toast-container {
  position: fixed; top: 20%; left: 50%; transform: translate(-50%, -20%);
  z-index: 9999; pointer-events: none;
  display: flex; flex-direction: column; align-items: center; gap: 10px;
}
.toast {
  pointer-events: auto;
  min-width: 280px; max-width: 520px;
  background: var(--surface);
  border: 1px solid var(--accent);
  border-left-width: 4px;
  border-radius: 10px;
  padding: 14px 22px;
  font-size: 14px; font-weight: 500;
  color: var(--text);
  box-shadow: 0 12px 40px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.04);
  transition: opacity 0.25s, transform 0.25s;
  display: flex; align-items: center; gap: 12px;
  animation: toast-in 0.25s ease-out;
}
.toast::before {
  content: ''; width: 10px; height: 10px; border-radius: 50%;
  background: var(--accent); flex-shrink: 0;
}
.toast.toast-green { border-color: var(--green); }
.toast.toast-green::before { background: var(--green); }
.toast.toast-red { border-color: var(--red); }
.toast.toast-red::before { background: var(--red); }
.toast.toast-sticky { font-weight: 400; line-height: 1.45; align-items: flex-start; }
.toast.toast-sticky::before { margin-top: 5px; }
.toast .toast-msg { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.toast .toast-close {
  flex: 0 0 auto; margin: -6px -10px -6px 0; padding: 4px 8px;
  background: none; border: 0; border-radius: 6px; color: var(--subdued);
  font: inherit; font-size: 18px; line-height: 1; cursor: pointer;
}
.toast .toast-close:hover { color: var(--text); background: var(--surface2); }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(-10px) scale(0.97); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}

[data-theme="light"] .toast,
[data-theme="warm"] .toast,
[data-theme="parchment"] .toast {
  box-shadow: 0 12px 40px rgba(15,8,8,0.18), 0 0 0 1px rgba(0,0,0,0.04);
}

/* ── Global Loading Overlay ─────────────────────────────────────────────── */
#loom-loader {
  position: fixed; top: 0; left: 0; right: 0; z-index: 9999;
  height: 3px; pointer-events: none; opacity: 0; transition: opacity 0.2s;
}
#loom-loader.active { opacity: 1; }
#loom-loader .loader-bar {
  height: 100%; width: 30%; background: var(--accent);
  border-radius: 0 2px 2px 0;
  animation: loom-load 1.2s ease-in-out infinite;
}
@keyframes loom-load {
  0% { margin-left: -30%; }
  100% { margin-left: 100%; }
}

/* Network-trouble chip — companion to #loom-loader, driven by the same
   global fetch wrapper. Appears bottom-right while the backend is
   restarting/unreachable ("Reconnecting…"), self-clears on the first
   successful /api response. States: .warn (amber, restarting) / .err
   (red, connection lost). */
#loom-net-chip {
  position: fixed; bottom: 52px; right: 12px; z-index: 9999;
  display: none; align-items: center; gap: 7px;
  padding: 5px 12px; border-radius: 999px;
  font-size: 11px; font-weight: 600; letter-spacing: 0.02em;
  pointer-events: none;
}
#loom-net-chip.active { display: inline-flex; }
#loom-net-chip .dot { width: 7px; height: 7px; border-radius: 50%; animation: loom-pulse 1.2s infinite; }
#loom-net-chip.warn {
  background: color-mix(in srgb, var(--amber) 16%, var(--surface));
  color: var(--amber); border: 1px solid color-mix(in srgb, var(--amber) 45%, transparent);
}
#loom-net-chip.warn .dot { background: var(--amber); }
#loom-net-chip.err {
  background: color-mix(in srgb, var(--red) 16%, var(--surface));
  color: var(--red); border: 1px solid color-mix(in srgb, var(--red) 45%, transparent);
}
#loom-net-chip.err .dot { background: var(--red); }

/* ── CDN icon inversion ─────────────────────────────────────────────────────
   CDN SVG icons are authored in black. On dark-family themes they vanish, so
   opt an icon-bearing container into inversion by adding `cdn-ico-invert` — it
   inverts every descendant <img> to light. Replaces the per-app
   [data-theme="…"] selector lists that were copy-pasted (and drifted) across
   apps. Apply to the container, not the <img>, so it composes with svgImg(). */
[data-theme="dark"]     .cdn-ico-invert img,
[data-theme="midnight"] .cdn-ico-invert img,
[data-theme="oxblood"]  .cdn-ico-invert img,
[data-theme="ember"]    .cdn-ico-invert img,
[data-theme="forest"]   .cdn-ico-invert img,
[data-theme="ocean"]    .cdn-ico-invert img,
[data-theme="neon"]     .cdn-ico-invert img {
  filter: brightness(0) invert(85%);
}

/* ── Recolorable CDN icon (CSS mask) ────────────────────────────────────────
   Output of svgMask(name, size): a CDN SVG painted with the element's `color`
   (currentColor) via a mask, instead of an <img> with the SVG's baked-in black.
   Lets action/button icons follow theme + state color (hover→accent, .danger→
   red) while staying 100% CDN-sourced — no inline <svg>, and no cdn-ico-invert
   needed (currentColor already adapts to the theme). The SVG's alpha is the
   mask; the strokes become currentColor. */
.cdn-ico-mask {
  display: inline-block;
  background-color: currentColor;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain;     mask-size: contain;
  vertical-align: middle;
}

/* loomShareBtn — labeled "copy a shareable link" button. Inherits .btn; the
   svgMask share glyph follows currentColor so it's visible in light + dark. */
.loom-share-btn { display: inline-flex; align-items: center; gap: 6px; }

/* ── loomTags — Tags-as-a-Service chip editor (cbtags) ───────────────────── */
.loom-tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px;
}
.loom-tag {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: 11px;
  line-height: 1;
  padding: 4px 6px 4px 8px;
  border-radius: 999px;
  color: var(--text);
  background: color-mix(in srgb, var(--loom-tag-color, var(--accent)) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--loom-tag-color, var(--accent)) 45%, transparent);
}
.loom-tag-x {
  background: transparent;
  border: 0;
  color: var(--subdued);
  cursor: pointer;
  font-size: 13px;
  line-height: 1;
  padding: 0 1px;
  border-radius: 50%;
}
.loom-tag-x:hover { color: var(--red); }
.loom-tag-input {
  flex: 0 1 96px;
  min-width: 64px;
  background: transparent;
  border: 1px dashed var(--border);
  border-radius: 999px;
  padding: 4px 8px;
  color: var(--text);
  font: inherit;
  font-size: 11px;
}
.loom-tag-input:focus { outline: none; border-color: var(--accent); border-style: solid; }
.loom-tags-empty { color: var(--subdued); font-size: 11px; font-style: italic; }

/* ── Bulk select (loomBulkSelect) — checkbox chip + floating action bar ──── */
.loom-bulk-box {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; flex-shrink: 0;
  border: 1.5px solid var(--border); border-radius: 4px;
  background: color-mix(in srgb, var(--surface) 85%, transparent);
  cursor: pointer; transition: border-color .12s, background .12s;
}
.loom-bulk-box:hover { border-color: var(--accent); }
.loom-bulk-box.on { background: var(--accent); border-color: var(--accent); }
.loom-bulk-box.on img { filter: brightness(0) invert(1); width: 12px; height: 12px; }
/* The floating bar — sticks to the bottom of the viewport, themed surface. */
.loom-bulk-bar {
  position: sticky; bottom: 12px; z-index: 40;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 14px auto 4px; max-width: 720px;
  padding: 8px 10px 8px 14px;
  background: var(--surface2); border: 1px solid var(--accent); border-radius: 10px;
  box-shadow: 0 6px 24px rgba(0,0,0,0.35);
}
.loom-bulk-count { font-size: 12px; font-weight: 600; color: var(--text); }
.loom-bulk-grow { flex: 1 1 auto; }
.loom-bulk-all {
  appearance: none; background: transparent; border: 1px solid var(--border);
  border-radius: 6px; padding: 5px 10px; cursor: pointer; color: var(--subdued);
  font-size: 11px; font-weight: 600;
}
.loom-bulk-all:hover { color: var(--text); border-color: var(--accent); }
.loom-bulk-act {
  appearance: none; border: 1px solid var(--accent); background: transparent;
  color: var(--accent); border-radius: 6px; padding: 6px 12px; cursor: pointer;
  font-size: 12px; font-weight: 600; display: inline-flex; align-items: center; gap: 6px;
}
.loom-bulk-act:hover { background: var(--accent); color: #fff; }
.loom-bulk-act.danger { border-color: var(--red); color: var(--red); }
.loom-bulk-act.danger:hover { background: var(--red-fill); color: #fff; }
.loom-bulk-act img { width: 13px; height: 13px; }
.loom-bulk-act:hover img, .loom-bulk-x:hover img { filter: brightness(0) invert(1); }
.loom-bulk-x {
  appearance: none; background: transparent; border: 0; cursor: pointer;
  padding: 4px; display: inline-flex; align-items: center; color: var(--subdued);
}
.loom-bulk-x img { width: 12px; height: 12px; opacity: 0.7; }
.loom-bulk-x:hover img { opacity: 1; }

/* ── Waiting states ─────────────────────────────────────────────────────────
   ONE visual language for "we're working on it", across three surfaces
   (cbloom#177). Which to reach for:

     page / route booting   →  <div class="loading">Loading…</div>
                               or loomLoad(), which paints it for you
     data filling a region  →  loomSkeleton({kind}) — shapes, not words, so a
                               slow fetch never reads as an empty result
     a modal doing work     →  loomModal.prompt({onConfirm}) — spinner in the
                               button, actions disabled until it settles

   `.loading` carries the affordance itself rather than asking 95 pages to
   migrate to a new class: it was a static line of centred text, which is
   indistinguishable from a page that has simply stopped. Now it spins. Nothing
   about the box changes — same centring, colour and padding — so no layout
   moves and no page needed editing.

   `.loading` means LOADING. An "empty" message belongs in loomEmptyState();
   two dashboard call sites were using `.loading` for "No matches" and would
   otherwise now show a spinner beside them. */
/* Box unchanged — `.loom-load-state` keeps its own tighter box further down;
   only the spinner is shared, so the three surfaces read as one system without
   any of them changing size.
   `:not(button)` matters: the modal's confirm button wears `.loading` to borrow
   the glyph, and must not also inherit a 40px block padding. */
.loading:not(button) { text-align: center; color: var(--subdued); padding: 40px; }
.loading::before,
.loom-load-state:not(.err)::before {
  content: ""; display: inline-block; vertical-align: -2px;
  width: 12px; height: 12px; margin-right: 8px;
  border: 2px solid currentColor; border-right-color: transparent;
  border-radius: 50%;
  animation: loom-spin-rot 0.7s linear infinite;
  opacity: 0.75;
}
/* Reduced motion: keep the mark, drop the rotation — a breathing dot still
   reads as activity without spinning. */
@media (prefers-reduced-motion: reduce) {
  .loading::before,
  .loom-load-state:not(.err)::before {
    animation: loom-breathe 1.6s ease-in-out infinite;
    border-right-color: currentColor;
  }
}

.empty-state { text-align: center; color: var(--muted); padding: 40px; font-size: 12px; }
.empty-state-action { margin-top: 14px; }
/* The object form — loomEmptyState({icon, title, body}). One shape for the
   icon-over-heading-over-sentence block every app was rebuilding by hand. */
.empty-state-icon { color: var(--subdued); opacity: 0.7; margin-bottom: 10px; }
.empty-state-icon .cdn-ico-mask { display: inline-block; }
.empty-state-title { font-size: 14px; font-weight: 600; color: var(--text); margin-bottom: 6px; }
.empty-state-body { font-size: 12px; color: var(--subdued); max-width: 46ch; margin: 0 auto; line-height: 1.5; }

/* ── Progress Bar ───────────────────────────────────────────────────────── */
.progress-bar { height: 12px; background: var(--border); border-radius: 6px; overflow: hidden; }
.progress-fill { height: 100%; border-radius: 6px; transition: width 0.3s; }
.progress-green { background: var(--green); }
.progress-amber { background: var(--amber); }
.progress-red { background: var(--red); }

/* ── Responsive KPI Grid ────────────────────────────────────────────────── */
@media (max-width: 900px) { .kpi-row { grid-template-columns: repeat(3, 1fr) !important; } }
@media (max-width: 600px) { .kpi-row { grid-template-columns: repeat(2, 1fr) !important; } }

/* ═══════════════════════════════════════════════════════════════════════════
   LOOM WIDGET LIBRARY — Extended Components
   Multi-layer elevation, glass, chips, pills, gradients, viz
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Extended variable defaults (themes override these) ───────────────── */
:root {
  --layer-2:       #1a1d26;
  --layer-3:       #22252f;
  --border-subtle: #181b24;
  --border-accent: rgba(59,130,246,0.2);
  --accent-hover:  #2563eb;
  --accent-glow:   rgba(59,130,246,0.15);
  --disabled:      #2a2d36;
  --info:          #3b82f6;
  --shadow-sm:     0 1px 3px rgba(0,0,0,0.2), 0 1px 2px rgba(0,0,0,0.12);
  --shadow-md:     0 4px 12px rgba(0,0,0,0.25), 0 2px 4px rgba(0,0,0,0.15);
  --shadow-lg:     0 12px 32px rgba(0,0,0,0.35), 0 4px 8px rgba(0,0,0,0.2);
  --shadow-glow:   0 0 16px var(--accent-glow);
  --glass-bg:      rgba(17,19,24,0.82);
  --glass-border:  rgba(255,255,255,0.06);
  --glass-blur:    12px;
  --gradient-accent:  linear-gradient(135deg, var(--accent), var(--accent-hover));
  --gradient-hero:    linear-gradient(135deg, var(--accent), #8b5cf6);
  --gradient-surface: linear-gradient(180deg, var(--surface), var(--bg));
  --viz-1: #E8461C; --viz-2: #A63D20; --viz-3: #D4873A;
  --viz-4: #5E8C6A; --viz-5: #4A6B8A; --viz-6: #C48E8E;
}

/* ── Elevation Layers ──────────────────────────────────────────────────── */
.loom-layer-0 { background: var(--bg); }
.loom-layer-1 { background: var(--surface); box-shadow: var(--shadow-sm); }
.loom-layer-2 { background: var(--layer-2); box-shadow: var(--shadow-md); }
.loom-layer-3 { background: var(--layer-3); box-shadow: var(--shadow-lg); }

.loom-elevated {
  background: var(--surface);
  border-radius: 14px;
  box-shadow: var(--shadow-md);
}

/* ── Glass Panel ───────────────────────────────────────────────────────── */
.loom-glass {
  background: var(--glass-bg);
  backdrop-filter: blur(var(--glass-blur));
  -webkit-backdrop-filter: blur(var(--glass-blur));
  border: 1px solid var(--glass-border);
  border-radius: 14px;
}

.loom-glass-subtle {
  background: var(--glass-bg);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 1px solid var(--border-subtle);
  border-radius: 10px;
}

/* ── Chip (small status/tag pill) ──────────────────────────────────────── */
.loom-chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 10px; border-radius: 100px;
  font-size: 10px; font-weight: 600;
  font-family: var(--font-mono), monospace;
  white-space: nowrap; line-height: 1.4;
}
.loom-chip-green   { background: rgba(45,155,90,0.12); color: var(--green); }
.loom-chip-amber   { background: rgba(212,160,36,0.12); color: var(--amber); }
.loom-chip-red     { background: rgba(217,43,43,0.12); color: var(--red); }
.loom-chip-blue    { background: rgba(43,122,204,0.12); color: var(--info, var(--accent)); }
.loom-chip-accent  { background: var(--accent-glow); color: var(--accent); }
.loom-chip-muted   { background: rgba(100,100,120,0.1); color: var(--subdued); }

/* Status dot inside chip */
.loom-chip-dot {
  width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0;
}
.loom-chip-dot.pulse { animation: loom-pulse 2s ease-in-out infinite; }
@keyframes loom-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }

/* ── Pill Button ───────────────────────────────────────────────────────── */
.loom-pill {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 16px; border-radius: 100px;
  font-size: 12px; font-weight: 600;
  font-family: inherit;
  border: none; cursor: pointer;
  transition: transform 0.15s, box-shadow 0.15s, opacity 0.15s;
}
.loom-pill:hover { transform: translateY(-1px); }
.loom-pill:active { transform: translateY(0); }
.loom-pill:disabled { opacity: 0.4; cursor: default; transform: none; }

.loom-pill-solid {
  background: var(--accent); color: #fff;
}
.loom-pill-solid:hover { box-shadow: var(--shadow-glow); }

.loom-pill-outline {
  background: transparent; color: var(--accent);
  border: 1px solid var(--accent);
}
.loom-pill-outline:hover { background: var(--accent-glow); }

.loom-pill-ghost {
  background: transparent; color: var(--subdued);
}
.loom-pill-ghost:hover { color: var(--text); background: var(--surface2); }

/* ── Gradient Text ─────────────────────────────────────────────────────── */
.loom-gradient-text {
  background: var(--gradient-accent);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

.loom-gradient-text-hero {
  background: var(--gradient-hero);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

/* ── Glow Border ───────────────────────────────────────────────────────── */
.loom-glow {
  box-shadow: var(--shadow-glow);
  border-color: var(--accent);
}

.loom-glow-hover { transition: box-shadow 0.25s, border-color 0.25s; }
.loom-glow-hover:hover {
  box-shadow: var(--shadow-glow);
  border-color: var(--accent);
}

/* ── Divider (gradient fade) ───────────────────────────────────────────── */
.loom-divider {
  height: 1px; border: none; margin: 20px 0;
  background: linear-gradient(90deg, var(--border), transparent 80%);
}

.loom-divider-center {
  height: 1px; border: none; margin: 20px 0;
  background: linear-gradient(90deg, transparent, var(--border) 30%, var(--border) 70%, transparent);
}

/* ── Avatar ────────────────────────────────────────────────────────────── */
.loom-avatar {
  width: 36px; height: 36px; border-radius: 50%;
  background: var(--surface2); border: 2px solid var(--border);
  display: flex; align-items: center; justify-content: center;
  font-size: 14px; font-weight: 700; color: var(--subdued);
  flex-shrink: 0; position: relative; overflow: visible;
}
.loom-avatar-dot {
  position: absolute; bottom: -1px; right: -1px;
  width: 10px; height: 10px; border-radius: 50%;
  border: 2px solid var(--surface);
}

/* ── Card (elevated, rounded) ──────────────────────────────────────────── */
.loom-card-elevated {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 20px;
  box-shadow: var(--shadow-sm);
  transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
}
.loom-card-elevated:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
  border-color: var(--border-accent);
}

/* ── Viz Color Dots ────────────────────────────────────────────────────── */
.loom-viz-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
.loom-viz-1 { background: var(--viz-1); }
.loom-viz-2 { background: var(--viz-2); }
.loom-viz-3 { background: var(--viz-3); }
.loom-viz-4 { background: var(--viz-4); }
.loom-viz-5 { background: var(--viz-5); }
.loom-viz-6 { background: var(--viz-6); }

/* ── Skeleton Loader ───────────────────────────────────────────────────── */
.loom-skeleton {
  background: linear-gradient(90deg, var(--surface2) 25%, var(--border) 50%, var(--surface2) 75%);
  background-size: 200% 100%;
  animation: loom-shimmer 1.5s ease-in-out infinite;
  border-radius: 6px;
}
@keyframes loom-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
/* Skeleton shape helpers — render via loomSkeleton() while data loads so a slow
   fetch shows "loading" shapes, NOT a misleading empty state ("No results" when
   it's really still loading → users click around on empty UI). */
.loom-skeleton-line { height: 12px; margin: 10px 0; }
.loom-skeleton-row  { height: 38px; margin: 8px 0; }
.loom-skeleton-card { height: 84px; margin: 0 0 12px; }
/* 'pane' — fills the host. A grid pane is a fixed box, so four 12px lines leave
   a dead band under them that reads as "loaded, and mostly empty". */
.loom-skeleton-fill { height: 100%; }
.loom-skeleton-pane { height: 100%; min-height: 40px; }
/* loom-busy — dim + block interaction on a region whose data is still loading,
   so controls can't be clicked on stale/empty state. Non-blocking: scoped to
   the element, no full-screen scrim (loomBusy() in loom.js toggles it). */
.loom-busy { opacity: 0.55; pointer-events: none; }
.loom-busy, .loom-busy * { cursor: progress; }

/* ── Scrollbar Thin ────────────────────────────────────────────────────── */
.loom-scroll-thin::-webkit-scrollbar { width: 5px; height: 5px; }
.loom-scroll-thin::-webkit-scrollbar-track { background: transparent; }
.loom-scroll-thin::-webkit-scrollbar-thumb { background: var(--muted); border-radius: 3px; }
.loom-scroll-thin { scrollbar-width: thin; scrollbar-color: var(--muted) transparent; }

/* ═══════════════════════════════════════════════════════════════════════════
   ACCESSIBILITY — colorblind filters, font scaling, motion, focus
   Ported from cbmobile. Toggled by loomA11y in loom.js via body classes.
   ═══════════════════════════════════════════════════════════════════════════ */
body.dyslexic-font,
body.dyslexic-font * {
  font-family: 'OpenDyslexic', var(--font-body), sans-serif !important;
}
body.custom-line-height {
  line-height: var(--custom-line-height, 1.6);
}
body.custom-line-height p,
body.custom-line-height li,
body.custom-line-height span,
body.custom-line-height td {
  line-height: var(--custom-line-height, 1.6);
}
body.reduce-motion *,
body.reduce-motion *::before,
body.reduce-motion *::after {
  animation-duration: 0.01ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.01ms !important;
  scroll-behavior: auto !important;
}
body.enhanced-focus *:focus-visible {
  outline: 4px solid var(--accent) !important;
  outline-offset: 3px !important;
  box-shadow: 0 0 0 7px color-mix(in srgb, var(--accent) 25%, transparent) !important;
}
body.high-contrast { filter: contrast(1.4) !important; }
body.high-contrast img, body.high-contrast video { filter: contrast(0.7); }

/* ── Modal Info Panel (loomInfoModal) ─────────────────────────────────── */
.loom-info-backdrop {
  position: fixed; inset: 0;
  background: color-mix(in srgb, #000 56%, transparent);
  z-index: 9000;
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
  opacity: 0; pointer-events: none;
  transition: opacity 160ms ease-out;
}
.loom-info-backdrop.open { opacity: 1; pointer-events: auto; }

.loom-info-modal {
  position: relative;
  width: min(640px, calc(100vw - 48px));
  max-height: min(82vh, 720px);
  background: var(--surface, #fff);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.35);
  display: flex; flex-direction: column;
  overflow: visible;
  transform: translateY(8px) scale(0.985);
  transition: transform 180ms ease-out;
}
.loom-info-backdrop.open .loom-info-modal {
  transform: translateY(0) scale(1);
}

.lim-head {
  display: flex; gap: 12px; align-items: flex-start; justify-content: space-between;
  padding: 18px 20px 8px;
  border-bottom: 1px solid var(--border);
}
.lim-head-text { flex: 1; min-width: 0; }
.lim-title {
  font-size: 16px; font-weight: 700; color: var(--text);
  letter-spacing: 0.01em;
}
.lim-subtitle {
  margin-top: 4px; font-size: 12px; color: var(--subdued);
  line-height: 1.45;
}
.lim-dots { display: flex; gap: 6px; align-items: center; padding-top: 4px; }
.lim-dot {
  width: 8px; height: 8px; border-radius: 50%;
  border: none; padding: 0; cursor: pointer;
  background: color-mix(in srgb, var(--text) 20%, transparent);
  transition: background 120ms ease-out, transform 120ms ease-out;
}
.lim-dot:hover { background: color-mix(in srgb, var(--text) 35%, transparent); }
.lim-dot.active {
  background: var(--accent, #f59e0b);
  transform: scale(1.15);
}

.lim-body {
  flex: 1; min-height: 80px;
  padding: 16px 20px 8px;
  overflow-y: auto;
  font-size: 13.5px; line-height: 1.55;
}
.lim-body p { margin: 0 0 10px 0; }
.lim-body p:last-child { margin-bottom: 0; }
.lim-body strong { color: var(--text); }
.lim-body code {
  background: var(--muted);
  padding: 1px 5px; border-radius: 3px;
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 12px;
}
.lim-page-h {
  margin: 0 0 8px 0;
  font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--subdued);
}
.lim-list, .lim-links {
  margin: 0; padding: 0; list-style: none;
  display: flex; flex-direction: column; gap: 8px;
}
.lim-list li, .lim-links li {
  padding: 8px 12px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 13px;
}
.lim-links a {
  color: var(--accent, #f59e0b); text-decoration: none; font-weight: 600;
}
.lim-links a:hover { text-decoration: underline; }
.lim-link-note { color: var(--subdued); font-weight: 400; }
.lim-figure { margin: 0; }
.lim-figure img, .lim-figure video {
  width: 100%; height: auto; border-radius: 6px; border: 1px solid var(--border);
}
.lim-figure figcaption {
  margin-top: 6px; font-size: 11px; color: var(--subdued); text-align: center;
}
.lim-code {
  background: var(--surface2); border: 1px solid var(--border); border-radius: 6px;
  padding: 10px 12px; margin: 0;
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 12px; line-height: 1.5;
  overflow-x: auto;
}
.lim-unknown {
  padding: 8px 10px; border-radius: 6px;
  background: color-mix(in srgb, var(--amber, #D4A024) 15%, transparent);
  color: var(--amber, #D4A024);
  font-size: 12px;
}

.lim-siblings {
  display: flex; align-items: center;
  padding: 8px 16px 18px;
  /* Add right padding so the buttons don't slide under the cutout. */
  padding-right: 168px;
  gap: 10px;
}
.lim-sib {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 10px; border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface2); color: var(--text);
  font-size: 12px; cursor: pointer;
  max-width: 50%;
}
.lim-sib:hover { background: var(--muted); }
.lim-sib-prev .lim-sib-arrow { font-size: 16px; line-height: 1; }
.lim-sib-next .lim-sib-arrow { font-size: 16px; line-height: 1; }
.lim-sib-label {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  max-width: 12em;
}
.lim-sib-spacer { flex: 1; }

/* Cutout: bottom-right rounded panel that visually "bites" into the modal
   corner and houses the page-prev / index / page-next / close controls. */
.lim-cutout {
  position: absolute; right: -1px; bottom: -1px;
  display: flex; align-items: center; gap: 2px;
  padding: 6px 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-bottom-right-radius: 14px;
  border-top-left-radius: 14px;
  box-shadow: -2px -2px 0 var(--surface);
}
.lim-cut-btn {
  width: 30px; height: 30px;
  display: inline-flex; align-items: center; justify-content: center;
  border: none; border-radius: 6px; background: transparent;
  color: var(--text); cursor: pointer;
  font-size: 18px; font-weight: 700;
  transition: background 120ms ease-out;
  font-variant-numeric: tabular-nums;
}
.lim-cut-btn:hover { background: var(--muted); }
.lim-cut-btn:disabled { opacity: 0.35; cursor: default; }
.lim-cut-idx {
  min-width: 36px; text-align: center;
  font-size: 11px; font-weight: 700; color: var(--subdued);
  font-variant-numeric: tabular-nums;
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
}
.lim-cut-close {
  margin-left: 4px;
  border-left: 1px solid var(--border);
  border-radius: 0 6px 6px 0;
  padding-left: 6px;
  font-size: 14px;
}
.lim-cut-close:hover {
  background: color-mix(in srgb, var(--red, #D92B2B) 18%, transparent);
  color: var(--red, #D92B2B);
}

/* Mobile adjustments — the cutout swaps to a full bottom strip. */
@media (max-width: 540px) {
  .loom-info-modal { width: 100vw; max-height: 92vh; border-radius: 12px 12px 0 0; }
  .loom-info-backdrop { align-items: flex-end; padding: 0; }
  .lim-siblings { padding: 8px 14px 14px; flex-wrap: wrap; }
  .lim-cutout {
    position: relative; right: auto; bottom: auto;
    width: 100%; justify-content: center;
    border-radius: 0;
    border: none; border-top: 1px solid var(--border);
    box-shadow: none;
  }
}

/* Help button in the header — uses the standard .icon-btn shape. */
.loom-help-btn { color: var(--text); position: relative; }
.loom-help-btn:hover {
  background: color-mix(in srgb, var(--accent, #f59e0b) 18%, transparent);
  color: var(--accent, #f59e0b);
}

/* Unread / "updated" LED indicator — small accent dot in the top-right
   corner of any help button (per-app or section-level). Pulsing animation
   draws the eye without shouting. The dot is removed by JS when the user
   opens the corresponding bundle's help, and the persisted timestamp
   stops it re-rendering on next page load. */
.loom-led {
  position: absolute;
  top: 2px; right: 2px;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--accent, #f59e0b);
  box-shadow: 0 0 0 2px var(--surface),
              0 0 8px color-mix(in srgb, var(--accent, #f59e0b) 70%, transparent);
  animation: loomLedPulse 1.8s ease-in-out infinite;
  pointer-events: none;
}
@keyframes loomLedPulse {
  0%, 100% { transform: scale(1); opacity: 1; }
  50%      { transform: scale(1.4); opacity: 0.55; }
}
body.reduce-motion .loom-led { animation: none; }

/* ── loomSvgReader — paginated SVG reader overlay ───────────────────── */
.loom-svgr {
  position: relative;
  width: 92vw; height: 94vh;
  display: flex; flex-direction: column;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: 12px;
  box-shadow: var(--shadow-lg); overflow: hidden;
}
.loom-svgr-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 10px 14px; border-bottom: 1px solid var(--border);
  background: var(--surface2);
}
.loom-svgr-title { font-weight: 700; font-size: 14px; }
.loom-svgr-tools { display: flex; align-items: center; gap: 8px; }
.loom-svgr-page { color: var(--subdued); font-size: 12px; min-width: 86px; text-align: center; }
.loom-svgr-btn {
  padding: 5px 11px; border-radius: 7px; font-size: 12px; font-weight: 600;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); cursor: pointer;
}
.loom-svgr-btn:hover:not(:disabled) { background: var(--surface3, var(--surface2)); }
.loom-svgr-btn.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
.loom-svgr-btn:disabled { opacity: 0.4; cursor: default; }
.loom-svgr-stage {
  flex: 1; overflow: auto; padding: 20px;
  background: var(--bg);
  display: flex; justify-content: center; align-items: flex-start;
}
.loom-svgr-doc { width: 100%; max-width: 860px; }
.loom-svgr-doc svg {
  display: block; width: 100%; height: auto;
  background: #fff; box-shadow: var(--shadow-md); border-radius: 4px;
}
.loom-svgr-err { color: var(--subdued); padding: 24px; }

/* Print: lay out every page-window with a page break → multi-page vector PDF. */
#loom-svgr-print { display: none; }
.loom-svgr-printpage svg { width: 100%; height: auto; }
@media print {
  /* Scoped with :has(): the box exists only while the viewer prints (loom.js
     printDoc). Unscoped, this blanked every printed page (cbloom#360). */
  body:has(> #loom-svgr-print) > *:not(#loom-svgr-print) { display: none !important; }
  #loom-svgr-print { display: block !important; }
  .loom-svgr-printpage { page-break-after: always; }
  .loom-svgr-printpage:last-child { page-break-after: auto; }
}

/* ── loomMarkdown typography — pair with class="loom-md" on the container ── */
.loom-md p { margin: 0 0 0.7em; }
.loom-md p:last-child { margin-bottom: 0; }
.loom-md h1, .loom-md h2, .loom-md h3, .loom-md h4 {
  margin: 0.9em 0 0.4em; line-height: 1.3; color: var(--text);
}
.loom-md h1 { font-size: 1.25em; }
.loom-md h2 { font-size: 1.15em; }
.loom-md h3 { font-size: 1.05em; }
.loom-md h4 { font-size: 1em; }
.loom-md ul, .loom-md ol { margin: 0.3em 0 0.7em; padding-left: 1.4em; }
.loom-md li { margin: 0.15em 0; }
.loom-md pre {
  background: var(--surface2); border: 1px solid var(--border); border-radius: 8px;
  padding: 10px 12px; overflow-x: auto; margin: 0.5em 0 0.8em; font-size: 0.9em;
}
.loom-md code { background: var(--surface2); border-radius: 4px; padding: 1px 5px; font-size: 0.92em; }
.loom-md pre code { background: none; padding: 0; }
.loom-md blockquote {
  margin: 0.5em 0; padding: 4px 12px; border-left: 3px solid var(--accent);
  color: var(--subdued); background: var(--surface2); border-radius: 0 6px 6px 0;
}
.loom-md hr { border: none; border-top: 1px solid var(--border); margin: 1em 0; }
.loom-md img { max-width: 100%; border-radius: 8px; }
.loom-md a { color: var(--accent); }
.loom-md-table-wrap { overflow-x: auto; margin: 0.6em 0 1em; max-width: 100%; }
.loom-md table { border-collapse: collapse; font-size: 0.92em; min-width: 50%; }
.loom-md th, .loom-md td { border: 1px solid var(--border); padding: 5px 10px; text-align: left; }
.loom-md th { background: var(--surface2); font-weight: 700; }

/* ── Embed mode (?embed=1) ───────────────────────────────────────────────
   Chromeless, body-only render for iframing an app inside a shell (Earshot
   tabs). Set by loomInit() as body.loom-embed. Hides the chrome the shell
   already provides; the app body renders flush + scrolls within the iframe. */
body.loom-embed #hdr,
body.loom-embed #loom-crumb-bar,
body.loom-embed #tab-strip,
body.loom-embed .tabs[data-default-tabs],
body.loom-embed #footer,
body.loom-embed #ft-clock-bar,
body.loom-embed #loom-log-panel,
body.loom-embed #loom-footer-actions { display: none !important; }
/* The embedding shell owns navigation, so the app's own tab strip is hidden
   (above) — the shell deep-links each leaf view via the app's hash router. */
body.loom-embed #main { padding-top: 0 !important; margin-top: 0 !important; }
body.loom-embed { background: transparent; }

/* ── receipt-panel rows (loomReceiptPanel) ───────────────────────────────────
   The receipt-panel block emits .receipt-row markup, but these styles only
   lived in app-specific sheets (intel.css / tenant.css) — so a LoomScript app,
   which loads only loom.css, rendered label+value scrunched together. Promoted
   here so every receipt-panel reads as a proper key/value list. */
.receipt-row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 6px 0;
  border-bottom: 1px dotted var(--border);
}
.receipt-row:last-child { border-bottom: none; }
.receipt-key { color: var(--subdued); }
.receipt-val { font-weight: 500; text-align: right; max-width: 60%; word-break: break-word; }

/* ── LoomScript layout + media blocks (loomscript.js: columns / image) ───────
   `columns` is a responsive grid: equal split by default (--ls-cols set inline
   from the spec's widths), stacking to a single column on narrow viewports.
   `image` renders a figure/gallery of source-document page scans. */
.ls-columns {
  display: grid;
  grid-template-columns: var(--ls-cols, repeat(2, minmax(0, 1fr)));
  gap: 16px;
  align-items: start;
  margin: 12px 0;
}
.ls-col { min-width: 0; }            /* let grid children shrink + wrap, not overflow */
@media (max-width: 720px) {
  .ls-columns { grid-template-columns: 1fr; }
}

/* ── Period chips (loomscript.js: format "periods") ──────────────────────────
   Per-period completion history for recurring obligations (EDC, cbloom#512).
   done ✓ green · not-done ✗ red · unknown ? amber. Theme tokens so it adapts to
   oxblood. The data-gap rule lives in the engine: an unconfirmed period (null)
   renders as `unknown`, never as a failure. */
.ls-chips { display: inline-flex; flex-wrap: wrap; gap: 4px; }
.ls-chip {
  display: inline-flex; align-items: center; gap: 2px;
  padding: 1px 6px; border-radius: 10px;
  font-size: 11px; font-weight: 600; line-height: 1.5;
  border: 1px solid transparent; white-space: nowrap;
}
.ls-chip-done {
  color: var(--green, #2D9B5A);
  border-color: color-mix(in srgb, var(--green, #2D9B5A) 35%, var(--border));
  background: color-mix(in srgb, var(--green, #2D9B5A) 12%, transparent);
}
.ls-chip-no {
  color: var(--red, #D92B2B);
  border-color: color-mix(in srgb, var(--red, #D92B2B) 35%, var(--border));
  background: color-mix(in srgb, var(--red, #D92B2B) 12%, transparent);
}
.ls-chip-unknown {
  color: var(--amber, #D4A024);
  border-color: color-mix(in srgb, var(--amber, #D4A024) 35%, var(--border));
  background: color-mix(in srgb, var(--amber, #D4A024) 12%, transparent);
}

.ls-figure {
  margin: 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.ls-doc-img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 6px;
  background: var(--surface2);
}
.ls-figure figcaption {
  font-size: 12px;
  color: var(--subdued);
  text-align: center;
}
.ls-figure-link { text-align: center; }
.ls-figure-link a { font-size: 13px; color: var(--accent); text-decoration: none; }
.ls-figure-link a:hover { text-decoration: underline; }
.ls-figure-empty, .ls-figure-loading {
  align-items: center;
  justify-content: center;
  min-height: 160px;
}
.ls-figure-msg { color: var(--subdued); font-size: 13px; text-align: center; }

/* ── loomSparkline + loomTrendDelta ──────────────────────────────────────── */
.loom-spark { display:inline-block; vertical-align:middle; }
.loom-spark polyline { stroke:var(--subdued); stroke-width:1.5; }
.loom-spark rect { fill:var(--subdued); }
.loom-spark-neutral polyline { stroke:var(--accent); } .loom-spark-neutral rect { fill:var(--accent); }
.loom-spark-warn polyline { stroke:var(--amber,#B7791F); } .loom-spark-warn rect { fill:var(--amber,#B7791F); }
.loom-spark-danger polyline { stroke:var(--red,#D92B2B); } .loom-spark-danger rect { fill:var(--red,#D92B2B); }
.loom-spark-baseline { stroke:var(--subdued); stroke-width:1; stroke-dasharray:2 2; opacity:.6; }
.loom-spark-empty { color:var(--subdued); font-size:12px; font-style:italic; }
.loom-delta { font-size:12px; font-weight:600; white-space:nowrap; }
.loom-delta-danger { color:var(--red,#D92B2B); }
.loom-delta-good { color:var(--green,#28A745); }
.loom-delta-neutral { color:var(--subdued); }

/* ── CC bundle chrome (Conservative Connector suite) ────────────────────────
   Shared intro · framed filter bar · counts · loading · responsive Row/Card for
   cc-launch/results/close/master/list-owners/donations/ccreports. Lives here on
   the always-present loom.css link rather than a per-app sheet — an @import from
   per-app CSS proved flaky (silently dropped on cc-list-owners). App-unique
   rules in each /lib/<slug>.css still win the cascade. */
.cc-intro { margin: 0 0 1rem; }
.cc-intro h1 { margin: 0 0 .25rem; font-size: 1.5rem; }
/* Hero with a primary action pinned top-right (CC standard — e.g. "+ Research",
   "Upload"). Stacks on narrow screens so the CTA never crowds the title. */
.cc-intro-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.cc-intro-row .cc-intro-cta { flex-shrink: 0; margin-top: 2px; }
@media (max-width: 560px) {
  .cc-intro-row { flex-direction: column; }
  .cc-intro-row .cc-intro-cta { align-self: flex-start; }
}
.cc-sub { color: var(--subdued); max-width: 72ch; line-height: 1.45; margin: 0; }
.cc-sub code { background: var(--surface3); padding: 0 .3em; border-radius: 3px; }
.cc-filters { align-items: end; padding: .85rem 1rem; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; }
/* .filter-group = the label-styled column for NON-input controls (pill bars,
   toggles) — a real <label> would forward clicks on the caption to its first
   button. Inputs/pickers keep using <label> for the focus affordance.
   :not(.loom-switch) — loomSwitch renders a <label> too, and without the
   exclusion this caption rule captured it: every switch in a filter panel got
   flex-direction:column, stacking its own label under the track. */
.cc-filters label:not(.loom-switch), .cc-filters .filter-group { display: flex; flex-direction: column; gap: .25rem; font-size: .78rem; color: var(--subdued); }
/* …but a group holding TWO OR MORE switches is a switch ROW, not a caption
   column: the column direction exists for "<caption> over <control>", and with
   no caption a stack of loomSwitches reads as a ragged column inside the panel
   (the /app/pipeline-results bug). The bottom pad drops the 18px track onto the
   ~29px .ctrl-select's centre line, since .cc-filters bottom-aligns its items. */
.cc-filters .filter-group:has(> .loom-switch ~ .loom-switch) {
  flex-direction: row; flex-wrap: wrap; align-items: center;
  column-gap: 16px; row-gap: .3rem; padding-bottom: 5px;
}
/* display:flex above beats the UA's [hidden] styling — restore it so panels
   can conditionally hide a control (e.g. payments' custom-code field). */
.cc-filters label[hidden], .cc-filters .filter-group[hidden] { display: none; }
.cc-filters .loom-pills { margin-bottom: 0; }
.cc-count { color: var(--subdued); font-size: .85rem; white-space: nowrap; }
.cc-filters .cc-count { margin-left: auto; align-self: center; }
.cc-loading, .cc-err { padding: 1rem; color: var(--subdued); }
.cc-err { color: var(--red); }
.cc-muted { color: var(--subdued); }
.cc-dash { color: var(--border); }
.cc-req { color: var(--red); }
.cc-card-title { font-size: 14px; font-weight: 600; color: var(--text); margin-bottom: 8px; word-break: break-word; }
.cc-card-row { display: flex; justify-content: space-between; gap: 12px; padding: 3px 0; font-size: 12px; }
.cc-card-k { color: var(--subdued); flex-shrink: 0; }
.cc-card-v { text-align: right; word-break: break-word; }
.cc-card-actions { margin-top: 10px; }
@media (max-width: 540px) {
  .cc-filters { flex-direction: column; align-items: stretch; }
  .cc-filters label:not(.loom-switch), .cc-filters .filter-group, .cc-filters .ctrl-input, .cc-filters .ctrl-select { width: 100%; box-sizing: border-box; }
  .cc-filters .cc-count { margin-left: 0; align-self: flex-start; }
}

/* loomLoad — data-load lifecycle states (loading / error) */
.loom-load-state { padding: 1rem; color: var(--subdued); font-size: 13px; }
.loom-load-state.err { color: var(--red); }

/* ══ ANSI terminal output — the surface loom-ansi.js paints onto ══════════════
   Used by any view that mirrors a real terminal (Helm's Screens class is the
   first). loom-ansi.js emits `var(--aN)` for the 16 base colours, so the palette
   lives here and stays tunable; the 6×6×6 cube and greyscale ramp are fixed by
   the xterm spec and come through as literal rgb().

   THIS BLOCK IS DELIBERATELY DARK IN BOTH THEMES, which is the one place Loom's
   "always use theme tokens" rule is set aside on purpose. The content being
   mirrored chose its own colours for a dark terminal — Claude's TUI emits 256
   colour 231 (near-white) for emphasis and 244/246 mid-greys for chrome. Repaint
   that on the parchment light theme and the emphasised text is the first thing
   to disappear. A terminal reading as a terminal is also the honest signal: this
   rectangle is another program's screen, not Loom's own UI.
   Override --term-bg/--term-fg/--aN on an ancestor to retune. */
.loom-ansi {
  --term-bg: #1a1a1e;
  --term-fg: #d8d8dc;
  --a0:  #2a2a30;  --a1:  #f14c4c;  --a2:  #23d18b;  --a3:  #e5c07b;
  --a4:  #3b8eea;  --a5:  #d670d6;  --a6:  #29b8db;  --a7:  #cccccc;
  --a8:  #6e7681;  --a9:  #ff6e6e;  --a10: #4ae08a;  --a11: #f4f99d;
  --a12: #6fb3ff;  --a13: #ff92df;  --a14: #7ce7ff;  --a15: #ffffff;
  background: var(--term-bg);
  color: var(--term-fg);
  /* A terminal is a grid: never re-wrap it, or every box-drawing character and
     aligned column the pane painted comes apart. Overflow scrolls instead. */
  white-space: pre;
  overflow-x: auto;
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  tab-size: 8;
}

/* ── loomCanvasGrid — the draggable 2D pane grid (lib/loom-canvas.js) ────────
 * Shared because two apps need the same board: /app/canvas (the drag reference)
 * and /app/votebook. The drag is deliberately cheap — while moving, a ghost
 * outline follows the pointer via `transform` only (compositor, no layout), the
 * real pane is dimmed with its content hidden, and nothing reflows until drop,
 * where the pane animates into its snapped cell.
 *
 * `lcv-` namespace: `.pane`/`.canvas` collide with per-app styles (onboard-admin
 * has its own `.pane`) and `lc-` is already loom-checklist's.
 */
.lcv-canvas {
  position: relative; margin: 0 auto; min-height: 320px;
  border: 1px solid var(--border); border-radius: 12px; background: var(--surface);
  background-image: radial-gradient(circle, var(--border) 1px, transparent 1.4px);
  background-size: 118px 118px; background-position: 10px 10px; padding: 10px;
}
.lcv-pane {
  position: absolute; display: flex; flex-direction: column; overflow: hidden;
  background: var(--surface); border: 1px solid var(--border);
  border-left: 3px solid var(--lcv-pane, var(--accent)); border-radius: 10px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
  /* Stop the mobile long-press URL preview / callout from hijacking tap-hold drag. */
  -webkit-touch-callout: none; -webkit-user-select: none; user-select: none;
  transition: left 0.18s cubic-bezier(0.2, 0.7, 0.3, 1), top 0.18s cubic-bezier(0.2, 0.7, 0.3, 1),
    box-shadow 0.15s, transform 0.12s;
}
.lcv-pane.lcv-no-trans { transition: none; }
.lcv-pane-header {
  display: flex; align-items: center; gap: 8px; padding: 8px 12px;
  font-size: 12px; font-weight: 600; color: var(--text); background: var(--surface2);
  border-bottom: 1px solid var(--border);
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
.lcv-pane-header[data-drag] { cursor: grab; touch-action: none; }
.lcv-pane-header[data-drag]:active { cursor: grabbing; }
.lcv-pane-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lcv-pane-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--lcv-pane, var(--accent)); flex-shrink: 0; }
.lcv-pane-x {
  flex-shrink: 0; border: 0; background: none; cursor: pointer; padding: 0 2px;
  font-size: 15px; line-height: 1; color: var(--subdued);
}
.lcv-pane-x:hover { color: var(--red, #e74c3c); }
.lcv-pane-body { flex: 1; padding: 12px; overflow: hidden; }

/* loading: the pane keeps whatever it was already showing, dimmed, under a
   sweeping highlight — the React-style "this is refreshing" cue. Deliberately
   NOT a spinner-over-blank: on a live board most refreshes change two numbers,
   and blanking the pane every 60 seconds is worse than showing slightly old
   figures with an obvious "working" wash over them. A pane with nothing to keep
   renders loomSkeleton({kind:'pane'}) underneath instead (loom-canvas.js). */
.lcv-pane.lcv-loading > .lcv-pane-body { opacity: 0.45; transition: opacity 0.2s ease; }
.lcv-pane.lcv-loading::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
  background: linear-gradient(100deg,
    transparent 18%,
    color-mix(in srgb, var(--text) 10%, transparent) 45%,
    transparent 72%);
  background-size: 220% 100%;
  animation: loom-shimmer 1.4s ease-in-out infinite;
}
.lcv-pane.lcv-loading .lcv-pane-dot { animation: loom-pulse 1.2s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .lcv-pane.lcv-loading::after { animation: none; background: none; }
  .lcv-pane.lcv-loading .lcv-pane-dot { animation: none; }
}

/* resize: an EDGE, not a corner grip. The pane border is where a hand goes
   first, and a 12px corner is a poor touch target. Invisible until the pane is
   hovered, so a view-mode-looking board doesn't sprout handles. */
.lcv-rs { position: absolute; z-index: 3; opacity: 0; transition: opacity 0.12s ease; }
.lcv-rs::after {
  content: ''; position: absolute; border-radius: 3px;
  background: var(--lcv-pane, var(--accent));
}
.lcv-rs-x { top: 12%; bottom: 12%; right: -3px; width: 10px; cursor: ew-resize; touch-action: none; }
.lcv-rs-x::after { top: 0; bottom: 0; left: 3px; width: 4px; }
.lcv-rs-y { left: 12%; right: 12%; bottom: -3px; height: 10px; cursor: ns-resize; touch-action: none; }
.lcv-rs-y::after { left: 0; right: 0; top: 3px; height: 4px; }
.lcv-pane:hover > .lcv-rs { opacity: 0.5; }
.lcv-rs:hover, .lcv-pane.lcv-sizing > .lcv-rs { opacity: 1; }
/* While sizing, kill the left/top transition or the pane lags the pointer. */
.lcv-pane.lcv-sizing { transition: none; z-index: 4; }
@media (pointer: coarse) {
  /* No hover on touch — the handles have to be there all the time, and bigger. */
  .lcv-pane > .lcv-rs { opacity: 0.45; }
  .lcv-rs-x { right: -7px; width: 18px; }
  .lcv-rs-y { bottom: -7px; height: 18px; }
}

/* dragging: the source pane becomes a faint placeholder; content hidden. */
.lcv-pane.lcv-dragging { opacity: 0.22; transition: none; box-shadow: none; }
.lcv-pane.lcv-dragging .lcv-pane-body { visibility: hidden; }
/* covering another pane → positive "will swap" feedback on the target. */
.lcv-pane.lcv-will-swap {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent), 0 8px 22px rgba(0, 0, 0, 0.14);
  transform: scale(0.97); z-index: 2;
}
/* landed → a brief accent pulse. */
.lcv-pane.lcv-just-dropped { animation: lcv-pop 0.4s ease-out; }
@keyframes lcv-pop {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--lcv-pane, var(--accent)) 55%, transparent); }
  100% { box-shadow: 0 0 0 16px transparent, 0 1px 3px rgba(0, 0, 0, 0.06); }
}
/* the outline that follows the pointer — transform-only, no layout cost. */
.lcv-ghost {
  position: absolute; z-index: 20; display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 600; color: var(--text); pointer-events: none; will-change: transform;
  border: 2px dashed var(--lcv-pane, var(--accent)); border-radius: 10px;
  background: color-mix(in srgb, var(--lcv-pane, var(--accent)) 12%, transparent);
}
/* the snapped-target placeholder. */
.lcv-slot {
  position: absolute; z-index: 1; pointer-events: none; border-radius: 10px;
  border: 2px dashed var(--border); background: color-mix(in srgb, var(--accent) 6%, transparent);
  transition: left 0.1s ease, top 0.1s ease, width 0.1s, height 0.1s;
}
.lcv-slot.lcv-over { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 16%, transparent); }

/* ── loomVim — the modal-editing affordance ───────────────────────────────────
   Shared, not Helm's: loomVim.attach() puts `vim-normal` / `vim-insert` on the
   textarea it owns, and any consumer of the primitive gets the same readout for
   free. Only what a MODE has to look like lives here — an editor's own chrome
   (a bar, a footer, a drafts drawer) belongs to that editor.

   The caret is the whole tell. In NORMAL mode a keystroke is a command rather
   than a character, and the one unrecoverable confusion in modal editing is not
   knowing which mode you are in — so normal mode gets a BLOCK caret (vim's own
   signal) and a calmer border, insert gets the accent and the usual bar. */
textarea.vim-normal {
  caret-shape: block;                     /* honoured where supported */
  border-color: color-mix(in srgb, var(--text) 28%, transparent);
}
textarea.vim-insert {
  caret-shape: bar;
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
}
/* caret-shape is young. Where it is absent the accent caret-color still carries
   insert, and the border difference above still carries normal — the readout
   degrades rather than disappearing. */
textarea.vim-insert { caret-color: var(--accent); }

/* The mode READOUT. Shared with the affordance above, because every consumer of
   loomVim needs to say which mode it is in and a per-app copy of this badge is
   the drift the audit's theming axis exists to catch.

   Monospace and fixed-width: NORMAL/INSERT differ in length, and letting the
   badge resize as you switch reads at a glance as the whole toolbar twitching. */
.loom-vim-mode {
  flex: 0 0 auto;
  display: inline-block;
  min-width: 5.2em;
  text-align: center;
  font-family: ui-monospace, monospace;
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  padding: 4px 7px;
  border-radius: 5px;
  border: 1px solid var(--border);
  background: var(--surface3);
  color: var(--subdued);
}
.loom-vim-mode.is-insert {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
