/* Wynxx-aligned theme — derived from the LIVE site CSS (new_style/extracted.css).
   Geist is the platform face; the brand is indigo #2f47e6 on white / neutral-gray
   surfaces with near-black text. This is the LIGHT theme; pair with style.dark.css. */
/* Geist — self-hosted (OFL), matching the live Wynxx site exactly. Served from
   /static/fonts (cli/web/docs/fonts). Avoids a Google-Fonts dependency that
   silently fell back to a system font. */
/* Geist as a single VARIABLE font (weight axis 100–900) — matches the original
   Wynxx site, which serves the variable font. Static per-weight instances rasterize
   a touch heavier than the variable font at the same nominal weight under Windows
   DirectWrite, so the variable file makes our text weight render identically. */
@font-face {
  font-family: "Geist";
  src: url("/static/fonts/Geist-Variable.woff2") format("woff2");
  font-weight: 100 900; font-style: normal; font-display: swap;
}
/* Metrics-matched fallback — Arial scaled/overridden to occupy the SAME box as Geist
   (em 1000, asc 1005, desc -295, xAvg 580 vs Arial em 2048, xAvg 1038), so the
   swap-in of Geist causes zero layout shift / no apparent size change. Pinned to
   local("Arial") (present on Win+Mac) so the override math is identical everywhere. */
@font-face {
  font-family: "Geist Fallback";
  src: local("Arial");
  size-adjust: 114.44%;
  ascent-override: 87.82%;
  descent-override: 25.78%;
  line-gap-override: 0%;
}

:root {
  /* COLOUR tokens live in tokens.css (plan #P17) — loaded before this file. Only non-colour
     tokens (type, icons, layout, radius, markdown metrics) are defined here. */

  /* --- Typography (Wynxx = Geist) --- */
  --font-sans: "Geist", "Geist Fallback", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  --font-display: "Geist", var(--font-sans);   /* Wynxx wordmark face */

  /* Type scale — the canonical Tailwind/shadcn steps, matching the live
     /typography page exactly (root = 16px). (Was a compressed admin scale.) */
  --text-3xs:  0.4375rem;  /* 7px  — app superscript-badge extension (`.pill-micro` only) */
  --text-2xs:  0.6875rem;  /* 11px — app micro-label extension (no live step; just below xs) */
  --text-xs:   0.75rem;    /* 12px — live --text-xs */
  --text-sm:   0.875rem;   /* 14px — live --text-sm */
  --text-base: 1rem;       /* 16px — live --text-base */
  --text-lg:   1.125rem;   /* 18px — live --text-lg */
  --text-xl:   1.25rem;    /* 20px — live --text-xl */
  --text-2xl:  1.5rem;     /* 24px — live --text-2xl */
  --text-3xl:  1.875rem;   /* 30px — live --text-3xl */
  --text-4xl:  2.25rem;    /* 36px — live --text-4xl */
  --text-5xl:  3rem;       /* 48px — live --text-5xl */

  /* Font WEIGHTS are written as plain numeric values (400 normal / 500 medium / 600 semibold /
     700 bold; plus 300 light + 800 for the one "excellent" glow) — a tiny, stable, closed set that
     needs no token indirection. line-height / letter-spacing are likewise literal per-component. */

  /* Rendered-Markdown scale — the ONE size/leading/weight every markdown surface reads at,
     wherever it comes from (a plan/quest popup, a section page, a docs page; client markdown-it or
     the server-side docs renderer). Tokens, not repeated literals, so the surfaces CANNOT drift:
     change these lines and every rendered doc, plan body and advice moves together. The full ladder
     built on them lives in the "Markdown rendered content" block below; only that block may consume
     these. `--md-weight` is deliberately BELOW the 400 default: Geist is a variable face (100-900,
     so 350 is a real weight, never synthesised) and prose set at 400 reads heavy at --text-sm on a
     full-contrast foreground. Emphasis is unaffected — `bolder` on a 350 parent still resolves to
     700, so `**bold**` keeps its full contrast against the body text. */
  --md-text: var(--text-sm);
  --md-line: 1.5;
  --md-weight: 350;
  /* Body colour sits a step off the full-contrast foreground — mixed TOWARD --muted rather than
     picked as a literal, so it adapts per theme (light #0a0a0a→#737373, dark #fafafa→#a1a1a1) and
     tracks any future palette change. Only prose dims: the ladder pins headings and table headers
     to var(--text) explicitly, so document structure keeps full contrast. */
  --md-color: color-mix(in srgb, var(--text) 82%, var(--muted));

  /* Icon glyph sizes — sidebar nav icons + dropdown carets. SEPARATE from the --text-* type scale
     (these size GLYPHS, not text). Nav icons keep their exact sizes; the small decorative carets
     normalise onto --icon-sm. Chrome must size glyphs from these, never a raw rem/px. */
  --icon-lg: 1.05rem;   /* top-level sidebar nav icon */
  --icon-md: 0.95rem;   /* nested / subgroup nav icon */
  --icon-sm: 0.7rem;    /* dropdown carets (▾) + collapse toggle */

  /* --- Layout --- */
  --header-h:            64px;
  --footer-h:            24px;   /* brand footer (GFT logo + version/docs pin) */
  --sidebar-w:           256px;
  --sidebar-collapsed-w: 46px;
  /* Effective side-panel width — flips to the collapsed rail via body.sb-collapsed
     so the docked header `left` and any other consumer update atomically. */
  --current-sidebar-w:   var(--sidebar-w);
  --row-h:               32px;
  --row-h-hdr:           26px;

  /* --- Border radius (Wynxx base 10px — live --radius .625rem) --- */
  --r-sm:   6px;
  --r-md:   8px;
  --r-lg:   10px;
  --r-xl:   14px;
  --r-2xl:  14px;
  --r-pill: 999px;
}

/* -----------------------------------------------------------------------
   Reset + scrollbar
   ----------------------------------------------------------------------- */
* {
  box-sizing: border-box;
  scrollbar-width: thin;
  scrollbar-color: var(--divider) transparent;
}
/* One consistent gray scrollbar — does NOT brighten on hover / focus / drag. */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--divider); border-radius: var(--r-sm); }
::-webkit-scrollbar-thumb:hover,
::-webkit-scrollbar-thumb:active { background: var(--divider); }
::-webkit-scrollbar-corner { background: transparent; }

/* -----------------------------------------------------------------------
   Documentation pages — typography, prose, layout
   ----------------------------------------------------------------------- */
html { height: 100%; }
body {
  margin: 0; padding-top: var(--header-h);
  font-family: var(--font-sans); background: var(--bg); color: var(--text);
  font-size: var(--text-base); line-height: 1.5; height: 100%; overflow: hidden;
}
/* Native form controls don't inherit font-family — without this, buttons/inputs
   render in the UA default font instead of Geist (matches Tailwind's preflight,
   which the live Wynxx site relies on). This is why buttons "looked different". */
button, input, select, textarea, optgroup { font-family: inherit; }
/* Docs prose is anchored to the same token scale as the portal so the two
   render at one density. Headings are em-based (relative to this --text-base),
   and the em-based prose below (code, pre, tables, .ascii, .tier) scales with it. */
.container { width: 100%; padding: 40px 24px 80px; box-sizing: border-box; font-size: var(--text-base); }
/* Headings — live Wynxx/shadcn type: Geist display face, tight tracking, and the
   FOREGROUND colour (brand is reserved for links / active / the logo, never title text). */
h1, h2, h3, h4, h5, h6 { font-family: var(--font-display); letter-spacing: -0.01em; color: var(--text); }
h1 { font-size: var(--text-3xl); border-bottom: 1px solid var(--border); padding-bottom: 12px; }
h2 { margin-top: 2.2em; border-bottom: 1px dashed var(--border); padding-bottom: 6px; }
h3 { margin-top: 1.6em; }
.container h1 { font-size: 1.6em; }
.container h2 { font-size: 1.3em; }
.container h3 { font-size: 1.1em; }
code { font-family: var(--font-mono); background: var(--code-bg); padding: 1px 6px; border-radius: var(--r-md); font-size: 0.9em; color: var(--code-string); }
pre {
  background: var(--code-bg); border: 1px solid var(--border); border-radius: var(--r-lg);
  padding: 14px 16px; overflow-x: auto; font-size: 0.85em; line-height: 1.5;
}
pre code { background: none; padding: 0; color: var(--text); }
table { border-collapse: collapse; width: 100%; margin: 14px 0; font-size: 0.9em; }
th, td { border: 1px solid var(--border); padding: 8px 10px; text-align: left; vertical-align: top; }
th { background: var(--panel); color: var(--text); }
.tier { background: var(--panel); border-left: 4px solid var(--accent); padding: 14px 18px; margin: 16px 0; border-radius: var(--r-md); }
.tier.t2 { border-left-color: var(--accent2); }
.tier.t3 { border-left-color: var(--green); }
.badge { display: inline-block; box-sizing: border-box; font-size: 0.7em; padding: 2px 8px; border-radius: var(--r-2xl); font-weight: 600; vertical-align: middle; margin-right: 6px;
  /* Soft-tinted (live convention) — --c hue -> 14% tint + coloured text + hairline. */
  --c: var(--accent);
  background: color-mix(in srgb, var(--c) 14%, var(--panel));
  color: var(--c);   /* deepen the hue toward the foreground so coloured text stays legible on the pale tint (light) / dark panel — adapts per theme via --text */
  border: 1px solid color-mix(in srgb, var(--c) 32%, var(--panel)); }
.badge.t2 { --c: var(--accent2); }
.badge.t3 { --c: var(--green); }
.muted { color: var(--muted); }
.warn { color: var(--red); font-weight: 600; }
ul li { margin: 4px 0; }
.toc { background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-lg); padding: 14px 22px; }
.toc ol, .toc ul { margin: 4px 0; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
.ascii { background: var(--code-bg); padding: 14px; border: 1px solid var(--border); border-radius: var(--r-lg); font-family: var(--font-mono); font-size: 0.78em; white-space: pre; overflow-x: auto; line-height: 1.3; }
.diagram { background: var(--code-bg); border: 1px solid var(--border); border-radius: var(--r-lg); padding: 20px; margin: 18px 0; overflow-x: auto; text-align: center; }
/* Diagram text uses the same scale as the regular prose: primary labels at
   --text-base, muted annotations at --text-sm. The svg font-size is the inherited
   base, so a label with no explicit size renders at prose size; densely-packed
   diagrams that set their own smaller font-size keep it (so long lines still fit).
   The .diagram-compact modifier keeps a tight/dense diagram legible at a fixed
   small size (e.g. the landing hero's narrow fixed-width boxes). */
.diagram svg { max-width: 100%; height: auto; font-size: var(--text-base); }
.diagram svg text[fill="#64748b"] { font-size: var(--text-sm); }
.diagram-compact svg { font-size: var(--text-2xs); }
.diagram-compact svg text[fill="#64748b"] { font-size: var(--text-2xs); }
/* Diagram container theming — see the --dg-* tokens. Leaf shapes keep their solid
   fill attribute; only CONTAINER boxes (and their labels) are token-driven so they
   flip dark on the dark canvas instead of glaring as light boxes. A CSS rule beats a
   presentation attribute, so the light fill="" attrs stay as a no-CSS fallback. */
.diagram .dg-cli-box    { fill: var(--dg-cli-bg);  stroke: var(--dg-cli-stroke); }
.diagram .dg-wrap-box   { fill: var(--dg-wrap-bg); stroke: var(--dg-wrap-stroke); }
.diagram .dg-wrap-tab   { fill: var(--dg-wrap-bg); }
.diagram .dg-cli-label  { fill: var(--dg-cli-label); }
.diagram .dg-wrap-label { fill: var(--dg-wrap-label); }
/* Muted in-diagram annotations/captions — theme-aware (SVG text can't read a var
   via a presentation attribute, so route it through this class). */
.diagram .dg-note       { fill: var(--muted); }
.legend { display: flex; gap: 18px; flex-wrap: wrap; font-size: 0.82em; margin-top: 10px; color: var(--muted); }
.legend span { display: inline-flex; align-items: center; gap: 6px; }
.legend i { display: inline-block; width: 12px; height: 12px; border-radius: var(--r-sm); }
/* Breadcrumb line. Element-agnostic: the Markdown docs set it with attr_list
   (`{: .crumbs }`), which lands on a `<p>`, while the legacy hand-authored partial uses
   `<nav class="crumbs">` — a `nav`-only selector left every rendered doc's crumb line
   unstyled (plain body text). */
.crumbs { font-size: 0.85em; color: var(--muted); margin-bottom: 18px; }
.crumbs a { color: var(--muted); }
.crumbs a:hover { color: var(--accent); }
.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; margin: 22px 0; }
.card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-lg); box-shadow: var(--shadow-sm); padding: 16px 18px; }
.card h3 { margin: 0 0 6px 0; }
.card p { margin: 6px 0 0 0; color: var(--muted); font-size: 0.9em; }
/* Services cards (admin Services view + /services) — regular .card; title uses the
   SYSTEM title style (--text, not brand), icon muted. Moved here from a page <style>. */
.svc-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 6px; }
.svc-head-left { display: flex; align-items: center; gap: 8px; min-width: 0; }
.svc-head-left h3 { margin: 0; font-size: var(--text-lg); font-weight: 700; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.svc-icon { display: flex; align-items: center; color: var(--muted); flex-shrink: 0; }
.svc-icon svg { width: 18px; height: 18px; display: block; }
.svc-status { display: flex; align-items: center; gap: 6px; font-size: var(--text-sm); color: var(--muted); flex-shrink: 0; }
.card .status { display: flex; gap: 6px; margin-bottom: 8px; font-size: 0.68em; text-transform: uppercase; letter-spacing: 0.05em; align-items: center; }
.card .status .ver { margin-left: auto; color: var(--muted); font-family: var(--font-mono); letter-spacing: 0; text-transform: none; font-size: 0.95em; }

/* Docs prose utilities — replace per-element inline styles (single source of truth) */
.feature-teaser { text-align: center; font-size: var(--text-xl); font-weight: 600; color: var(--text); margin: 24px 0 16px; }
.ascii.accent  { color: var(--accent); }
.ascii.accent2 { color: var(--accent2); }
.ascii .hl { font-weight: 600; color: var(--text); }
.ascii .hl-accent2 { color: var(--accent2); }
.caption { color: var(--muted); font-size: var(--text-sm); margin: 6px 0 14px; }
.footer-note { color: var(--muted); font-size: var(--text-sm); margin-top: 48px; border-top: 1px solid var(--border); padding-top: 16px; }
.ol-compact { margin: 6px 0 0 18px; }
.ascii-toggle { margin: 12px 0; }
.ascii-toggle > summary { cursor: pointer; color: var(--muted); }

/* -----------------------------------------------------------------------
   Portal chrome — fixed header, nav, content wrapper
   ----------------------------------------------------------------------- */
/* Collapsed rail: flip the shared width var so the docked header `left`, and any
   other consumer, follow the sidebar to the narrow rail in one place. */
body.sb-collapsed { --current-sidebar-w: var(--sidebar-collapsed-w); }
.portal-header {
  /* Docked to the RIGHT of the side panel: starts where the rail ends and tracks
     its width (transition matches the sidebar's width transition). */
  position: fixed; top: 0; left: var(--current-sidebar-w); right: 0; z-index: 300;
  transition: left 0.18s ease;
  background: var(--header-bg);
  border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow-header);
  /* Left padding matches the logo↔text gap (10px) so the brand hugs the left edge with the same
     breathing room on each side of the logo; right is 14px to match the right-group's inner gap
     (`.portal-right`), so the project name / org tag have equal spacing to each other AND to the
     right border in every mode. */
  padding: 0 14px 0 10px; display: flex; align-items: center; gap: 32px; height: var(--header-h);
}
/* "Mind" aligns to the wordmark's vertical centre: the inline Wynxx vector centres
   its letters within its viewBox, so centre-align (not bottom) lines "Mind" up with
   the "Wynxx" text. Server mode also centres the stacked Mind+server. */
.portal-brand {
  text-decoration: none; display: inline-flex; align-items: center; flex-shrink: 0;
}
.portal-brand--server { align-items: center; }
.portal-brand:hover { text-decoration: none; }
/* Wynxx wordmark logo (replaces the old ASCII synapse art). */
.portal-logo-img { height: 37px; width: auto; display: block; }
/* Tool name + optional server sub-label, stacked to the right of the logo — so
   "server" sits BELOW "Mind" in server mode. Centre-aligned with the wordmark
   (no baseline lift needed now the logo is a centred inline vector). */
.portal-brand-tool { display: inline-flex; align-items: center; gap: 6px; margin-left: -3px; }
.portal-brand--server .portal-brand-tool { padding-bottom: 0; }
/* "Mind" = the neural mark + the "Mind" wordmark text after it. Text reuses the
   monospace two-blue gradient (the icon carries the same two brand blues, solid).
   In server mode this becomes a ROW so the "server" mark sits to the RIGHT of "Mind". */
.portal-brand-textcol { display: inline-flex; flex-direction: column; justify-content: center; }
/* Server mode: "server" is stacked BELOW "Mind" but ABSOLUTELY positioned, so it never displaces
   "Mind" — Mind keeps the exact size AND position it has in standalone. */
.portal-brand--server .portal-brand-textcol { align-items: center; position: relative; }
/* "mind" is the real brand letterform — a SEPARATE inline SVG (portal-mind-word), glyphs extracted
   from the official lockup and transformed onto the logo's baseline in the SAME 170-unit viewBox —
   NOT CSS text. Same height as the logo so the vertical mapping matches (baseline aligns); the small
   margin-left pulls it closer to the "Wynxx" word (countering the flex gap + the logo's right
   whitespace). Themes via the path's var(--logo-color). */
.portal-mind-word { height: 37px; width: auto; display: block; margin-left: -4px; }
/* Server-mode sub-label — a small mark hugging the underside of "Mind" (absolute, so Mind stays
   put), widened with UNIFORM letter-spacing (not justify-stretch) toward "MIND" width. `text-indent`
   offsets the trailing letter-spacing so the spaced word stays centred. */
.portal-brand-server {
  position: absolute; left: 0; right: 0; top: 100%; margin-top: -8px; transform: translateX(-3px);
  font-family: var(--font-display); font-size: 0.5rem; font-weight: 300;
  line-height: 1; text-transform: lowercase;
  /* The span is exactly "Mind" wide (left/right:0 inside the Mind-width textcol); inter-character
     justify then spreads "server" edge-to-edge to fill it — so its length matches Mind exactly. */
  text-align: justify; text-align-last: justify; text-justify: inter-character;
  color: var(--brand-server-color); opacity: 0.4;
}
.portal-nav { display: flex; gap: 4px; align-items: center; flex: 1; }
/* Most pages put their sections in the sidebar, so the header nav is empty — collapse it
   then, so it doesn't add a second flex gap that would offset the ticker from centre. */
.portal-nav:empty { display: none; }
.nav-link { padding: 6px 14px; border-radius: var(--r-lg); font-size: var(--text-base); color: var(--muted); text-decoration: none; transition: background 0.12s, color 0.12s; }
.nav-link:hover { background: rgba(var(--accent-rgb),0.08); color: var(--text); text-decoration: none; }
.nav-link.active { background: rgba(var(--accent-rgb),0.14); color: var(--accent); font-weight: 600; }
/* Right-side header group: project name + org tag + the server "admin" link, in that order.
   nav's `flex: 1` pushes this group to the right edge; the group's own `gap` (14px) spaces its
   members evenly, matching the header's 14px right padding — so name↔tag, tag↔border and
   name↔border (standalone, no tag) are all the same distance. */
.portal-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }

/* -----------------------------------------------------------------------
   Portal footer — the lowest layer: full-width bar pinned to the very bottom;
   every other region ends at its top edge. GFT logo left, version/docs pin right.
   Same surface as the side panel (var(--panel)) with a hairline top border, so it
   reads as one continuous chrome — auto-themes via the token. The GFT mark is
   inlined with currentColor and tinted with --logo-color so it stays legible on
   the panel in both light and dark (the shipped svg is white-on-brand-blue).
   ----------------------------------------------------------------------- */
.portal-footer {
  position: fixed; left: 0; right: 0; bottom: 0; height: var(--footer-h); z-index: 100;
  display: flex; align-items: center; justify-content: space-between; padding: 0 14px;
  background: var(--panel); border-top: 1px solid var(--border);
}
.portal-footer-logo { display: inline-flex; align-items: center; color: var(--logo-color, var(--text)); }
.portal-footer-logo svg, .portal-footer-logo img { height: 12px; width: auto; display: block; }   /* gft wordmark (gft.png 78×18) */
/* Reuse the side panel's version-pin styling as-is (muted slate on the panel), only
   dropping its sidebar padding. */
.portal-footer .sidebar-version { padding: 0; }
/* Admin/org badge: a stretched, vertically-centered box tinted system-yellow — reads as an
   "admin area" badge. Scoped under .portal-header so it beats `.nav-link.active`. Horizontal
   placement now comes from `.portal-right`'s gap (no margin-left:auto), keeping the 8px
   top/bottom inset for the boxed look. */
/* Local/org/admin header tag — ghost-style pill matching the sun/moon buttons:
   same 32px height, gray border, side-menu (--panel) surface, small muted text. */
.portal-header .nav-admin {
  flex-shrink: 0; display: inline-flex; align-items: center;
  height: 32px; padding: 0 10px; box-sizing: border-box;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-md);
  color: var(--muted); font-size: var(--text-sm); font-weight: 500;
}
.portal-header .nav-admin:hover { background: var(--panel); border-color: var(--accent); color: var(--accent); }
.portal-header .nav-admin.active { background: var(--panel); border-color: var(--border); color: var(--muted); }
/* Org tag — same ghost pill but accent-tinted text to stay distinct. */
.portal-header .nav-org,
.portal-header .nav-org.active { color: var(--accent); font-size: var(--text-sm); }
.portal-header .nav-org:hover { color: var(--accent); border-color: var(--accent); }
/* Project switcher in the header — a widened ghost pill (like the org tag) whose TRIGGER shows the
   current project name + caret, opening a modern popup (project-switcher.js) grouped under Local/org
   dividers. */
.hdr-project { position: relative; flex-shrink: 0; display: inline-flex; }
.hdr-project-btn {
  display: inline-flex; align-items: center; gap: 6px; height: 32px; box-sizing: border-box;
  min-width: 190px; max-width: 340px; padding: 0 10px;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-md);
  color: var(--text); font-size: var(--text-sm); font-weight: 500; cursor: pointer;
  transition: border-color 0.12s;
}
.hdr-project-btn:hover, .hdr-project-btn[aria-expanded="true"] { border-color: var(--accent); }
.hdr-project-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; }
.hdr-project-caret { color: var(--muted); font-size: var(--icon-sm); flex-shrink: 0; }
/* Popup — mirrors the user-menu popup: floating panel, rounded, shadowed. Fixed-positioned by JS. */
.hdr-project-pop {
  position: fixed; z-index: 1200; min-width: 200px; max-width: 360px; max-height: 60vh; overflow-y: auto;
  background: var(--panel, var(--bg)); border: 1px solid var(--border); border-radius: 10px;
  box-shadow: var(--shadow-pop); padding: 6px;
}
/* Type-to-filter box, pinned at the top of the popup. */
.hdr-project-search {
  width: 100%; box-sizing: border-box; margin: 0 0 6px; padding: 6px 10px;
  background: var(--input, var(--surface-3)); border: 1px solid var(--border); border-radius: var(--r-md);
  color: var(--text); font-size: var(--text-sm); font-family: inherit;
}
.hdr-project-search:focus { outline: none; border-color: var(--accent); }
/* Group header (Local / <org>) — SAME look as a side-panel root group label
   (.sidebar-group-hdr:not(.sidebar-panel-trigger)): brighter-gray, mixed-case, small, weight 300.
   A division line separates each group from the one above. */
.hdr-project-group {
  font-size: var(--text-xs); font-weight: 300; text-transform: none; letter-spacing: normal;
  line-height: 18px; padding: 6px 10px 4px;
  color: var(--muted);
  color: color-mix(in srgb, var(--muted), var(--text) 45%);
}
.hdr-project-pop .hdr-project-group:not(:first-of-type) { margin-top: 8px; padding-top: 10px; border-top: 1px solid var(--border); }
/* Client org header: org name + its server domain (small, dimmed) so two servers hosting the same
   org name read as distinct groups. */
.hdr-project-group-server { margin-left: 6px; font-size: calc(var(--text-xs) - 1px); color: var(--muted); opacity: 0.7; }
/* Project row — full-width option, rounded hover pill (like the sidebar nav links). */
.hdr-project-item {
  display: flex; align-items: center; width: 100%; box-sizing: border-box; gap: 8px;
  padding: 7px 10px; background: none; border: none; border-radius: var(--r-md);
  color: var(--text); font-size: var(--text-sm); font-family: inherit; text-align: left; cursor: pointer;
}
/* `hidden` must win over display:flex (filter hides non-matching rows / empty group headers). */
.hdr-project-item[hidden], .hdr-project-group[hidden] { display: none; }
.hdr-project-item:hover { background: var(--sidebar-accent); color: var(--accent); }
.hdr-project-item.active { background: var(--sidebar-accent); color: var(--text); font-weight: 600; }
.hdr-project-item-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Client-init affordance — a terminal icon BESIDE the project switcher (frontend-server mode only)
   that opens a popup with the `wmind init …` command + a Copy button. */
/* No margin — spacing comes from `.portal-right { gap: 10px }`, uniform with the other header items. */
.hdr-init { position: relative; flex-shrink: 0; display: inline-flex; }
/* Match the header theme-toggle buttons (.header-theme-opt): 36x32 ghost pill. */
.hdr-init-btn { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 32px;
  padding: 0; box-sizing: border-box; background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--r-md); color: var(--muted); cursor: pointer; }
.hdr-init-btn:hover, .hdr-init-btn[aria-expanded="true"] { color: var(--accent); border-color: var(--accent); }
/* Header live count (#Q281, `HeaderIndicator` → header-live.js): a ring spinning around the number,
   e.g. agents running now, left of the project picker. No box: a bare 30px arc (accent quarter on a
   faint track) with the count centred inside, sized to the 32px header row. Hidden at 0 (the
   `hidden` attribute, so the gap collapses). Reduced motion: the ring stays still. */
.hdr-live { position: relative; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; color: var(--text); text-decoration: none; }
.hdr-live[hidden] { display: none; }
.hdr-live:hover { color: var(--accent); text-decoration: none; }
.hdr-live-ring { position: absolute; top: 50%; left: 50%; width: 30px; height: 30px; margin: -15px 0 0 -15px;
  box-sizing: border-box; border-radius: 50%;
  /* The system blue (`--primary`, the .btn-primary fill) — not `--accent`, the lighter LINK blue. */
  border: 2.5px solid color-mix(in srgb, var(--primary) 22%, transparent); border-top-color: var(--primary);
  animation: hdr-live-spin 1s linear infinite; }
.hdr-live-num { position: relative; font-size: var(--text-xs); font-weight: 700; line-height: 1;
  font-variant-numeric: tabular-nums; }
@keyframes hdr-live-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .hdr-live-ring { animation: none; } }
.hdr-init-btn svg { display: block; width: 16px; height: 16px; }
.hdr-init-pop { position: fixed; z-index: 1200; display: flex; flex-direction: column; gap: 8px;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-md);
  box-shadow: var(--shadow-pop); padding: 12px 14px; max-width: min(560px, 92vw); }
/* `display:flex` above would defeat the `hidden` attribute — restore hide when hidden (JS toggles it). */
.hdr-init-pop[hidden] { display: none; }
.hdr-init-hd { font-size: var(--text-2xs); font-weight: 600; color: var(--muted);
  text-transform: uppercase; letter-spacing: .04em; }
/* Command line + copy icon on ONE row. */
.hdr-init-row { display: flex; align-items: center; gap: 8px; }
.hdr-init-cmd { flex: 1 1 auto; min-width: 0; font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--text-xs); color: var(--text); background: var(--secondary); border-radius: var(--r-sm, 6px);
  padding: 6px 8px; overflow-x: auto; white-space: nowrap; user-select: all; }
.hdr-init-copy { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; background: var(--secondary); border: 1px solid var(--border);
  border-radius: var(--r-sm, 6px); color: var(--muted); cursor: pointer; }
.hdr-init-copy:hover { color: var(--accent); border-color: var(--accent); }
.portal-content { width: 100%; padding: 28px 24px 80px; box-sizing: border-box; }
.portal-content { width: 100%; padding: 28px 24px 80px; box-sizing: border-box; }
.portal-project { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.portal-project-name { font-size: var(--text-sm); font-weight: 600; color: var(--text); white-space: nowrap; }
.portal-project-pid  { font-size: var(--text-xs); color: var(--muted); font-family: var(--font-mono); white-space: nowrap; }
/* Header event ticker — vertically cycles recent audit events */
.hdr-ticker {
  /* In-flow flex item filling the gap between the nav (left) and .portal-right (right).
     The header's 32px flex `gap` gives equal breathing room on each side, and
     .portal-right (flex-shrink:0) reserves its own width — so the ticker can never
     overlap the project name. The div is ALWAYS rendered (empty when the polling is
     suppressed) and a dominant flex-grow makes IT — not the nav — take the free middle
     space, so it right-pins .portal-right in every mode. The empty header nav is hidden
     (.portal-nav:empty), so the ticker stays centred between the toggle and the project
     group with equal gaps. min-width:0 lets it self-hide when the header is narrow. */
  position: relative; flex: 100 1 auto; min-width: 0;
  height: var(--header-h);
  overflow: hidden; pointer-events: none;
}
.hdr-tick-item {
  position: absolute; width: 100%;
  /* Center the dynamic event content within the ticker grower (which itself fills the header's
     middle), so the event reads centered in the header. Paired with .tick-data's flex:0 1 auto
     below — a growing data span would eat the free space and defeat the centering. */
  display: flex; align-items: center; justify-content: center; gap: 6px;
  height: var(--header-h);
  font-size: var(--text-sm); white-space: nowrap; overflow: hidden;
  animation: tick-drop 5s ease-in-out forwards;
}
/* Event type: regular weight, same size as the event-data text (.hdr-tick-item
   --text-sm) — only the severity colour distinguishes it, not a heavier weight. */
.tick-info  { color: var(--divider); font-weight: 400; }
.tick-warn  { color: var(--tick-warn); font-weight: 400; }
.tick-error { color: var(--tick-error); font-weight: 400; }
.tick-debug { color: var(--border); font-weight: 400; }
.tick-chat  { color: var(--divider); font-weight: 400; }
/* Sized to its content (no flex-grow) so the whole event group stays centered; min-width:0 +
   overflow lets it still shrink and CSS-ellipsis-truncate when the data is too long for the row. */
.tick-data  { color: var(--muted); flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* warn/error carry their severity colour across the WHOLE line (message included), identical
   treatment for both — a warning is as visible as an error (info/debug keep the muted message). */
.hdr-tick-item.tick-sev-warn  .tick-data { color: var(--tick-warn); }
.hdr-tick-item.tick-sev-error .tick-data { color: var(--tick-error); }
/* Provider mark LEADS the row, a touch smaller than the audit pages' 1.2rem, with a
   little extra breathing room before the event type (on top of the row's 6px gap). */
.tick-prov { flex: 0 0 auto; margin-right: -2px; opacity: 0.5; }
.hdr-tick-item .tick-prov.prov-badge::before { width: 0.95rem; height: 0.95rem; }
.tick-origin { color: var(--divider); font-size: var(--text-xs); font-family: var(--font-mono); }
/* Owner icon-only, LEADING every ticker item — the same icon schema + gray
   (--muted) tint as the Audit event list's origin badge (rootBadge): user ->
   user, agent -> cpu, wynxx/system -> mind. Masked so it colours via
   background-color (textContent-only markup, no inline SVG). */
.tick-owner { flex: 0 0 auto; display: inline-flex; align-items: center; opacity: 0.5; margin-right: -3px; }
.tick-owner::before {
  content: ""; display: block; width: 1rem; height: 1rem;
  background-color: var(--muted);
  -webkit-mask: var(--icon-mind) center/contain no-repeat;
          mask: var(--icon-mind) center/contain no-repeat;
}
.tick-owner-user::before  { -webkit-mask-image: var(--icon-user); mask-image: var(--icon-user); }
.tick-owner-agent::before { -webkit-mask-image: var(--icon-bot);  mask-image: var(--icon-bot); }

/* ── Data-ownership controls (FRAMEWORK-reusable; markup from cli/web/static/visibility.js) ───────
   `.jm-visibility` = the privacy SELECTOR (owner → a button opening a tier picker; non-owner → the
   same glyph, fixed). `.jm-ownership` = the list OWNERSHIP badge (right of the ID). Both draw a glyph
   via CSS mask over `--icon-<name>` tinted with the row's semantic colour — the visibility language is
   defined ONCE here (+ visibility.js), never re-inlined per ability. public/shared share the `users`
   glyph, distinguished by colour (yellow/green). */
.jm-visibility, .jm-ownership {
  --jm-vis-glyph: var(--icon-user-shield);
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.35rem; height: 1.35rem; flex: 0 0 auto; padding: 0; border: 0; background: none;
  color: var(--muted); vertical-align: middle;
}
.jm-visibility::before, .jm-ownership::before, .jm-vis-glyph::before {
  content: ""; display: block; width: 1rem; height: 1rem; background-color: currentColor;
  -webkit-mask: var(--jm-vis-glyph) center/contain no-repeat;
          mask: var(--jm-vis-glyph) center/contain no-repeat;
}
/* privacy tiers — glyph + colour */
.jm-visibility[data-visibility="private"] { --jm-vis-glyph: var(--icon-user-shield); color: var(--red); }
.jm-visibility[data-visibility="public"]  { --jm-vis-glyph: var(--icon-user-lock);   color: var(--accent2); }
.jm-visibility[data-visibility="shared"]  { --jm-vis-glyph: var(--icon-users);       color: var(--green); }
/* owner selector = a real button (hover/focus affordance); non-owner = fixed */
.jm-visibility--btn { cursor: pointer; border-radius: 6px; transition: background-color .12s ease; }
.jm-visibility--btn:hover { background-color: color-mix(in srgb, currentColor 14%, transparent); }
.jm-visibility--btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.jm-visibility--fixed { cursor: default; }
/* ownership badge — the PRIVACY TIER glyph (SAME as the selector, so the tier reads identically in
   list + detail); COLOUR conveys ownership: your own rows in the tier colour, others' in muted grey. */
.jm-ownership[data-visibility="private"] { --jm-vis-glyph: var(--icon-user-shield); }
.jm-ownership[data-visibility="public"]  { --jm-vis-glyph: var(--icon-user-lock); }
.jm-ownership[data-visibility="shared"]  { --jm-vis-glyph: var(--icon-users); }
.jm-ownership[data-mine="0"] { color: var(--muted); }
.jm-ownership[data-mine="1"][data-visibility="private"] { color: var(--red); }
.jm-ownership[data-mine="1"][data-visibility="public"]  { color: var(--accent2); }
.jm-ownership[data-mine="1"][data-visibility="shared"]  { color: var(--green); }
/* owner tier-picker popup */
.jm-visibility-menu {
  z-index: 1300; min-width: 9rem; width: max-content; max-width: 15rem; padding: 3px;
  background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0,0,0,.18); display: flex; flex-direction: column; gap: 2px;
}
.jm-visibility-opt {
  display: flex; align-items: center; gap: .45rem; width: 100%; padding: .3rem .45rem;
  border: 0; border-radius: 6px; background: none; cursor: pointer; text-align: left; color: var(--text);
}
.jm-visibility-opt:hover, .jm-visibility-opt:focus-visible { background: color-mix(in srgb, var(--accent) 12%, transparent); outline: none; }
.jm-visibility-opt.active { background: color-mix(in srgb, var(--accent) 8%, transparent); }
.jm-visibility-opt .jm-vis-text { display: flex; flex-direction: column; line-height: 1.25; }
.jm-visibility-opt .jm-vis-text small { color: var(--muted); font-size: var(--text-xs); }
.jm-vis-glyph { display: inline-flex; align-items: center; justify-content: center; width: 1.1rem; height: 1.1rem; flex: 0 0 auto; }
.jm-vis-glyph[data-visibility="private"] { --jm-vis-glyph: var(--icon-user-shield); color: var(--red); }
.jm-vis-glyph[data-visibility="public"]  { --jm-vis-glyph: var(--icon-user-lock);   color: var(--accent2); }
.jm-vis-glyph[data-visibility="shared"]  { --jm-vis-glyph: var(--icon-users);       color: var(--green); }
@keyframes tick-drop {
  0%   { transform: translateY(-52px); opacity: 0; }
  12%  { transform: translateY(0);     opacity: 1; }
  80%  { transform: translateY(0);     opacity: 1; }
  100% { transform: translateY(52px);  opacity: 0; }
}
.portal-upgrade-btn {
  display: flex; align-items: center; gap: 6px; flex-shrink: 0;
  padding: 5px 12px; border-radius: var(--r-lg);
  background: rgba(var(--sw-yellow-rgb),0.12); border: 1px solid rgba(var(--sw-yellow-rgb),0.35);
  color: var(--sw-yellow); font-size: var(--text-sm); font-family: inherit;
  cursor: pointer; transition: background 0.12s, border-color 0.12s; white-space: nowrap;
}
.portal-upgrade-btn:hover { background: rgba(var(--sw-yellow-rgb),0.22); border-color: rgba(var(--sw-yellow-rgb),0.60); }
.portal-upgrade-btn:disabled { opacity: 0.55; cursor: not-allowed; }

/* Self-upgrade progress modal (upgrade.js) — non-closable while running. Each
   step is a shared `.task-row`; the layout mirrors the document-type creation
   rows (label on the left, status indicator on the right: the `.doc-spinner`
   ring while in-flight, a colour-coded ✓ / ! / ✗ when done). */
.jm-upg-sub { margin: 2px 0 12px; font-size: var(--text-sm); }
/* Sync modal subline: spinner ring + text inline while syncing. */
.jm-sync-sub { display: flex; align-items: center; gap: 8px; }
/* The sync progress popup is a slim transient — only as wide as its one-line description text
   (hug the content, with a sensible cap), tight padding, no close button. */
.modal-box.jm-sync-box { width: max-content; max-width: 320px; padding: 14px 16px; }
.modal-box.jm-sync-box .modal-header { margin-bottom: 8px; }
.modal-box.jm-sync-box .jm-upg-sub { margin: 0; }
.jm-upg-sub.jm-upg-ok-text { color: var(--green); }
.jm-upg-sub.jm-upg-err-text { color: var(--red); }
.jm-upg-steps { max-height: 46vh; overflow-y: auto; }
.jm-upg-step.task-row { align-items: center; font-size: var(--text-sm); }
.jm-upg-step .tr-activity { flex: 1; min-width: 0; }
.jm-upg-glyph { flex: 0 0 auto; width: 1.1em; text-align: center; font-weight: 700; }
.jm-upg-ok   .jm-upg-glyph { color: var(--green); }
.jm-upg-warn .jm-upg-glyph { color: var(--sw-yellow); }
.jm-upg-err  .jm-upg-glyph { color: var(--red); }
.jm-upg-info .jm-upg-glyph { color: var(--muted); }
.jm-upg-active .jm-upg-label { color: var(--text); }
/* Not-yet-started phase: muted label + hollow dot, so the full list reads as
   upcoming work while only the active phase spins. */
.jm-upg-pending .jm-upg-glyph,
.jm-upg-pending .jm-upg-label { color: var(--muted); }

/* -----------------------------------------------------------------------
   Portal sidebar — collapsible nav panel
   ----------------------------------------------------------------------- */
/* Frame spanning the whole viewport down to the footer's top edge. The side
   panel is the left column (top → footer); the header docks over the right column's
   top strip (.docs-main reserves header-h so content clears it).
   `position: absolute` (NOT fixed) is deliberate: body is height:100%/overflow:hidden so
   it never scrolls — absolute pins to the same viewport rect as fixed would — but a fixed
   ancestor ALWAYS creates a stacking context, which would trap every overlay rendered
   inside this frame (modals z1000, toasts z10000, …) below the header's z-index:300.
   Absolute with no z-index creates no stacking context, so those fixed overlays escape to
   the root and stack by their own z-index per the documented ladder. Do NOT change to fixed. */
/* Load flash guard: hide the content + the rail until sidebar.js has assembled `.docs-layout`
   (it wraps `main` and builds the sidebar client-side, after first paint). `html.sb-loading` is set
   pre-paint in shell.py's <head> and cleared at the end of the sidebar build, so the page reveals in
   its final form instead of flashing full-width content that then reflows beside the rail. `visibility`
   (not display) keeps the box model so the reflow happens while hidden. A timeout backstop in that head
   script clears the class even if the build fails, and JS-off never adds it — so content is never
   permanently hidden. */
html.sb-loading main,
html.sb-loading .docs-sidebar,
html.sb-loading .overlay-panel { visibility: hidden; }
.docs-layout { position: absolute; top: 0; left: 0; right: 0; bottom: var(--footer-h); display: flex; align-items: stretch; overflow: hidden; }
.docs-sidebar {
  position: relative; /* containing block for absolutely-pinned children */
  width: var(--sidebar-w); flex-shrink: 0; background: var(--panel); border-right: 1px solid var(--border);
  transition: width 0.18s ease; overflow: hidden; display: flex; flex-direction: column; height: 100%; z-index: 10;
}
.docs-sidebar.collapsed { width: var(--sidebar-collapsed-w); }
.docs-main { flex: 1; min-width: 0; height: 100%; overflow-y: auto; padding-top: var(--header-h); }
/* Brand at the very top of the side panel (relocated from the header). Expanded:
   the Wynxx wordmark + "Mind". Collapsed (46px rail): just the favicon — the
   wordmark is hidden and the favicon shown, mirroring the icon/text swap used by
   the sidebar groups. */
/* The brand sits in a top band the same height as the header, so the Wynxx logo
   lines up on the header's centre-line; left-aligned within the side panel. */
.sidebar-brand {
  flex-shrink: 0; height: var(--header-h); box-sizing: border-box;
  display: flex; align-items: center; justify-content: flex-start; gap: 4px;
  padding: 0 14px 0 16px; text-decoration: none;
}
.sidebar-brand:hover { text-decoration: none; }
.sidebar-brand .sidebar-wordmark { display: inline-flex; align-items: center; gap: 4px; min-width: 0; }
/* Wynxx wordmark — tuned brand size in the side panel. */
.sidebar-brand .portal-logo-img { height: 36.5px; }
/* "mind" wordmark shares the logo's 170-unit viewBox + height, so its baseline lines up with the
   Wynxx letters (same vertical mapping); sidebar logo is 36.5px. */
.sidebar-brand .portal-mind-word { height: 36.5px; }
/* Server mode keeps "Mind" the SAME size/place as standalone — the "server" label is a compact
   vertical mark beside it (see .portal-brand-server), not a stacked second line, so no shrink. */
.sidebar-brand-favicon { display: none; align-items: center; justify-content: center; }
.sidebar-brand-favicon img { width: 22px; height: 22px; display: block; }
/* The mind node-graph mark sits AFTER the "Mind" word (expanded wordmark only — it lives inside
   .sidebar-wordmark, so it's hidden when collapsed, where the wynxx mark takes over via the favicon).
   Flex-centered so the mark aligns to the optical middle of the "Mind" text, not its baseline. */
.sidebar-brand-mind { display: inline-flex; align-items: center; align-self: center; margin-left: 0; }
/* Same 28px mark in BOTH standalone and server mode (the standalone shrink-to-1.5rem override was
   removed so the "mind" mark keeps the exact size/position it has in server mode). */
.sidebar-brand-mind img { height: 28px; width: auto; display: block; }
.docs-sidebar.collapsed .sidebar-brand { justify-content: center; padding-left: 0; padding-right: 0; }
.docs-sidebar.collapsed .sidebar-brand .sidebar-wordmark { display: none; }
.docs-sidebar.collapsed .sidebar-brand-favicon { display: flex; }
.sidebar-toggle {
  /* Pinned to the top-right, on the Dashboard title's line. Absolute (out of flow)
     so its position never depends on which siblings are hidden when collapsed —
     fixed top in both states. top centers it on the 44px title row (nav pad 2 + 22 - 13 = 11). */
  position: absolute; top: 11px; right: 10px; z-index: 2;
  display: flex; align-items: center; justify-content: center;
  width: 26px; height: 26px;
  background: none; border: 1px solid var(--border); border-radius: var(--r-md);
  color: var(--muted); cursor: pointer; font-size: var(--icon-sm); flex-shrink: 0;
  transition: color 0.12s, border-color 0.12s;
}
.sidebar-toggle:hover { color: var(--text); border-color: var(--accent); }
.sidebar-toggle-icon { display: inline-block; transition: transform 0.18s ease; }
/* Header expand/contract control (first child, left) — icon button with a subtle
   button-style background (a muted tint that reads on both light and dark headers).
   Its collapse state is keyed off body.sb-collapsed. */
.header-sidebar-toggle {
  display: flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; flex-shrink: 0; margin-left: 3px;
  /* Background matches the PAGE background (black in dark, white in light) so the control reads
     flush with the canvas rather than as a tinted button. */
  background: var(--bg); border: none; border-radius: var(--r-md);
  color: var(--text); cursor: pointer; padding: 0;   /* --text reads white on the dark header */
  transition: background 0.12s, color 0.12s;
}
.header-sidebar-toggle:hover { background: var(--bg); color: var(--accent); }
.header-sidebar-toggle svg { display: block; width: 16px; height: 16px; }
/* Back-context variant (see sidebar.js syncHeaderExit): while a Config/Documentation overlay (or an
   admin/server-edit context) is open, this header control becomes an arrow + "Back" label instead of
   the collapse icon — so it grows past the 26px square and lays icon + text inline. */
.header-sidebar-toggle.sidebar-toggle--back { width: auto; gap: 5px; padding: 0 9px; }
.header-sidebar-toggle.sidebar-toggle--back .sidebar-back-label { font-size: var(--text-sm); line-height: 1; }
/* Light/dark toggle (last child, far right) — both sun + moon always visible, each in
   its OWN button container. The non-selected option is GHOST (transparent, faded); the
   selected option is REGULAR (filled button background) with a blue (--accent) icon. */
.header-theme-toggle { display: flex; align-items: center; gap: 10px; flex-shrink: 0; margin-right: 1px; }
.header-theme-opt {
  display: flex; align-items: center; justify-content: center;
  width: 36px; height: 32px; padding: 0; box-sizing: border-box; border-radius: var(--r-md);
  /* ghost: the side-menu surface (var(--panel) — dark gray in dark mode) + gray border. */
  background: var(--panel); border: 1px solid var(--border);
  color: var(--text); cursor: pointer;
  transition: background 0.18s, border-color 0.18s, color 0.18s, box-shadow 0.18s;
}
.header-theme-opt:hover { color: var(--accent); border-color: var(--accent); }
.header-theme-opt.active {
  /* selected = .btn-primary: solid --primary fill, --on-accent text, medium shadow. */
  background: var(--primary); border-color: var(--primary); color: var(--on-accent);
  box-shadow: var(--shadow-md);
}
.header-theme-opt.active:hover { background: var(--primary-hover); border-color: var(--primary-hover); color: var(--on-accent); }
.header-theme-opt svg { display: block; width: 16px; height: 16px; flex-shrink: 0; }
/* Nudge the moon (second) button 1px left. */
.header-theme-toggle .header-theme-opt:last-child { margin-left: -1px; }
.docs-sidebar.collapsed .sidebar-toggle-icon { transform: rotate(180deg); }
/* Project switcher — pinned at the bottom of the sidebar (above the footer), below
   the Configuration trigger. Built by sidebar.js, filled by project-switcher.js.
   A modern <select>: native arrow removed, ▾ caret via ::after; hidden when collapsed. */
/* ┌──────────────────────────────────────────────────────────────────────────────────────┐
   │ COLLAPSED-RAIL CONVENTION — keep ALL rail controls consistent, or they drift/jump.     │
   │ This bit it twice (project/user selectors + org-combo). The rules:                     │
   │  1. LEFT-ANCHOR every leading icon at the 12px column — NEVER `justify-content:center`. │
   │     The rail width animates (240→46px); a centered icon re-flows rightward during that  │
   │     transition while left-anchored nav icons stay put, so the centered one visibly      │
   │     drifts and lands off the column. 12px = items'/box's 8px left pad + 4px icon inset  │
   │     = (46-22)/2, the centre of the collapsed rail. Same value in BOTH states → no jump. │
   │  2. RESERVE the dropdown's height on the icon's row (`min-height`, ≈30px for these,     │
   │     42px for org-combo). Collapsed the <select> is display:none; without a reserved     │
   │     height the row shrinks to bare icon height and stacked selectors bunch together.    │
   │  3. Expanded-only tightening (negative margins, padding-top:0) must be UNDONE in the    │
   │     `.collapsed` rules so the equal-height containers space evenly like the nav rows.   │
   └──────────────────────────────────────────────────────────────────────────────────────┘ */
.sidebar-project { order: 100; flex-shrink: 0; padding: 3px 8px; display: flex; flex-direction: column; align-items: stretch; gap: 4px; }
/* Small caption above the selector — labels the switcher, no icon. */
.sidebar-project-label { font-size: var(--text-2xs); font-weight: 500; text-transform: uppercase; letter-spacing: 0.06em; color: rgba(var(--muted-rgb),0.55); padding: 0 2px; }
/* The control row holds the select + rename pencil + collapsed icon; it's the positioning context
   for the ▾ caret so the caret centers on the select, not the label+row wrapper. */
/* min-height reserves the expanded <select>'s height (≈30px), so when collapsed (icon only)
   the row stays a full-height container — the icon centres in it and the two selectors keep the
   same vertical rhythm as the dropdowns instead of shrinking to bare icon height and bunching up. */
.sidebar-project-row { position: relative; display: flex; align-items: center; gap: 6px; min-height: 30px; }
.sidebar-project-row::after {
  content: "▾"; position: absolute; right: 42px; top: 50%;
  transform: translateY(-50%); pointer-events: none;
  color: var(--muted); font-size: var(--icon-sm);
}
/* When the rename pencil is hidden (server mode) the select fills the row (the collapsed-rail
   icon is display:none when expanded), so the caret sits at the select's own right edge — inside
   its 28px padding-right reserve, matching the org-combo caret (14px). */
.sidebar-project.no-edit .sidebar-project-row::after { right: 14px; }

/* Manager USERS (data-visibility) selector — pinned directly under the Project box (drop the top
   padding so the two read as one stacked control). Select-only row, so it reuses `.no-edit`. */
/* Users box sits tight under the Project box (no top padding) — they read as one stacked control.
   In the collapsed rail it stays visible: the select hides and its leading icon centers (clicking
   it expands the sidebar), exactly like the Project switcher. */
.sidebar-view-scope { padding-top: 0; margin-top: -2px; }
/* Collapsed rail: drop the expanded-only tightening so the two equal-height icon containers
   (Project + Users) space evenly, like the nav rows. */
.docs-sidebar.collapsed .sidebar-view-scope { margin-top: 0; padding-top: 3px; }
.sidebar-proj-edit { background: none; border: none; color: var(--muted); cursor: pointer; font-size: var(--text-base); line-height: 1; padding: 4px; flex-shrink: 0; }
/* Constrain the pencil SVG to 16px — otherwise it renders at its intrinsic size, widening the
   button and pushing the row's ▾ caret (positioned a fixed distance from the right) off the
   select's edge. 16px + 4px padding ≈ the old '✎' glyph width the caret offset was tuned for. */
.sidebar-proj-edit svg { width: 16px; height: 16px; display: block; }
.sidebar-proj-edit:hover { color: var(--accent); }
.sidebar-project select {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  flex: 1; width: auto; box-sizing: border-box; padding: 6px 28px 6px 12px;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--r-lg);
  font-family: inherit; font-size: var(--text-sm); font-weight: 600;
  cursor: pointer; outline: none;
  transition: border-color 0.12s, box-shadow 0.12s, background-color 0.12s;
}
.sidebar-project select:hover { border-color: var(--muted); background: var(--panel); }
.sidebar-project select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.18); }
.sidebar-project option { background: var(--panel); color: var(--text); }
/* In-group combobox (server Organization current-org switcher) — same control look as the project
   switcher, sized to the group's indented items. */
/* Sits inside .sidebar-items (already 8px side padding) — so only a small 4px inset to align with
   the group's links, no extra horizontal padding (which doubled the side space). The native arrow
   is removed (appearance:none), so a ▾ caret is drawn via ::after, like the project switcher. */
/* min-height reserves the expanded select's height so that, collapsed (icon only), the rows
   BELOW the combo stay at the same Y — no vertical jump on contract. */
.sidebar-org-combo { position: relative; padding: 6px 4px; display: flex; align-items: center; gap: 6px; min-height: 42px; box-sizing: border-box; }
.sidebar-org-combo::after {
  content: "▾"; position: absolute; right: 14px; top: 50%;
  transform: translateY(-50%); pointer-events: none;
  color: var(--muted); font-size: var(--icon-sm);
}
.sidebar-org-combo-icon { flex-shrink: 0; color: var(--muted); background: none; border: none; padding: 0; cursor: pointer; }
/* Hidden when expanded (the select carries the label); shown only as the collapsed-rail
   affordance — same pattern as the project switcher's icon. High-specificity so it beats the
   generic `.sidebar-icon { display:flex }` (the combo icon carries both classes). */
.docs-sidebar:not(.collapsed) .sidebar-org-combo-icon { display: none; }
.sidebar-org-combo-icon:hover { color: var(--accent); }
.sidebar-org-combo-icon svg { width: 16px; height: 16px; display: block; }
.sidebar-org-combo select {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  flex: 1; width: auto; box-sizing: border-box; padding: 6px 24px 6px 10px;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--r-md);
  font-family: inherit; font-size: var(--text-sm); font-weight: 600; cursor: pointer; outline: none;
}
.sidebar-org-combo select:hover { border-color: var(--muted); background: var(--panel); }
.sidebar-org-combo select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.18); }
/* Collapsed rail: hide the <select> + caret but KEEP the leading icon, LEFT-anchored at the same
   12px edge as every other rail icon (items' 8px + 4px). Don't center — centering re-flows the
   icon rightward as the rail width animates, so it drifts away from the fixed project/user/nav
   icons during the collapse. Left-anchored = pinned, no drift. */
.docs-sidebar.collapsed .sidebar-org-combo { padding: 6px 0 6px 4px; justify-content: flex-start; }
.docs-sidebar.collapsed .sidebar-org-combo-icon { display: flex; align-items: center; justify-content: center; }
.docs-sidebar.collapsed .sidebar-org-combo select,
.docs-sidebar.collapsed .sidebar-org-combo::after { display: none; }
/* Project icon — the leading meaning-icon (tooltip "Project") to the LEFT of the select, always
   visible; in the collapsed rail it also replaces the hidden <select> and, clicked, expands the
   sidebar + opens the picker (wired in sidebar.js). */
/* Leading meaning-icons for the Project + Users selectors. Sized to the 22px nav-icon slot with a
   4px margin (inside the box's 8px padding ⇒ left edge at 12px), so they sit on the SAME vertical
   line as the nav-group icons, and the select that follows starts at ~40px — the nav-text inset. */
.sidebar-proj-icon, .sidebar-sel-icon {
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 22px; padding: 0; margin-left: 4px; line-height: 0;
  background: none; border: none; color: var(--muted); cursor: pointer;
}
.sidebar-proj-icon:hover, .sidebar-sel-icon:hover { color: var(--accent); }
.sidebar-proj-icon svg, .sidebar-sel-icon svg { width: 18px; height: 18px; display: block; }
/* Collapsed rail: hide the select / rename pencil / caption / caret, but KEEP the box + icon at
   their EXPANDED left positioning (box padding-left 8px + icon margin-left 4px = the 12px left
   edge every nav icon uses). Do NOT recenter — recentering makes the icon slide right during the
   collapse animation and land off the nav-icon line. The leading icon stays put, fixed like the
   rest. */
.docs-sidebar.collapsed .sidebar-project-label,
.docs-sidebar.collapsed .sidebar-project-row > select,
.docs-sidebar.collapsed .sidebar-proj-edit,
.docs-sidebar.collapsed .sidebar-project-row::after { display: none; }
.sidebar-nav { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 2px 0 24px; }
.sidebar-version { flex-shrink: 0; padding: 8px 14px 12px; font-size: var(--text-2xs); color: rgba(var(--muted-rgb),0.55); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: right; }
/* The Config trigger's synced-revision badge — same muted color/size as the footer version pin. */
.sidebar-rev { margin-left: 6px; font-size: var(--text-2xs); color: rgba(var(--muted-rgb),0.55); }
/* Server-edit config-page banner: the page's unapplied draft → Apply / Discard (plan #20).
   Same surface as the side panel (--panel bg + --border). The mount lives IN `.portal-header`,
   absolutely centred over the header's middle band, so it takes NO space in the page flow and the
   controls being configured never move when it appears. While it shows, config-banner.js sets
   `body.jm-cfg-active`, which hides the activity ticker beneath it. Below 900px the header's middle
   band is too narrow (project switcher, toggles), so it becomes a floating pill just under the
   header instead — still out of flow. The mount is an empty div when there is no draft. */
/* Pending changes must be UNMISSABLE: a floating bar OVER the page (fixed, just under the header,
   centred on the content area right of the side panel), larger than a header pill, with the
   pending (`--accent2`) edge and a pop shadow. Still out of the page flow: nothing shifts. */
#jm-config-banner { position: fixed; top: calc(var(--header-h) + 12px); left: calc(var(--current-sidebar-w) + (100vw - var(--current-sidebar-w)) / 2); transform: translateX(-50%); z-index: 50; width: max-content; max-width: calc(100vw - var(--current-sidebar-w) - 32px); }
#jm-config-banner:empty { display: none; }
#jm-config-banner .jm-cfg-banner { margin: 0; padding: 12px 14px 12px 18px; gap: 14px; font-size: var(--text-base); font-weight: 600; border: 2px solid var(--accent2); box-shadow: var(--shadow-pop); flex-wrap: wrap; }
#jm-config-banner .jm-cfg-banner-icon { width: 22px; height: 22px; }
#jm-config-banner .jm-cfg-banner-msg { min-width: 0; }
body.jm-cfg-active .hdr-ticker { opacity: 0; }
.jm-cfg-banner { display: flex; align-items: center; gap: 12px; margin: 0 0 14px; padding: 10px 14px; border: 1px solid var(--border); background: var(--panel); border-radius: var(--r-md); font-size: var(--text-sm); box-shadow: var(--shadow-header); }
.jm-cfg-banner-icon { flex: 0 0 auto; width: 24px; height: 24px; background-color: var(--accent2); -webkit-mask: var(--icon-alert-triangle) center/contain no-repeat; mask: var(--icon-alert-triangle) center/contain no-repeat; }
.jm-cfg-banner-msg { flex: 1; color: var(--text); }
.jm-cfg-banner-actions { display: flex; gap: 8px; flex-shrink: 0; }
/* In the sidebar, the version is its OWN line — the very last entry (after Sign out, order 220),
   smaller than the trigger rows above it. */
.docs-sidebar > .sidebar-version { order: 230; padding: 4px 14px 6px; font-size: var(--text-2xs); }
/* Compact "docs ↗" link on the version line. */
.sidebar-docs-link { color: rgba(var(--muted-rgb),0.55); text-decoration: none; display: inline-flex; align-items: center; gap: 3px; }
.sidebar-docs-link:hover { color: var(--accent); }
.sidebar-docs-icon { display: inline-flex; }
.sidebar-docs-icon svg { width: 11px; height: 11px; display: block; }
.docs-sidebar.collapsed .sidebar-version { display: none; }
/* Collapsed: keep the standalone version line's SPACE (just hide its text), so the items above
   it don't shift up when the sidebar contracts. */
.docs-sidebar.collapsed > .sidebar-version { display: block; visibility: hidden; }
.sidebar-group-hdr {
  display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 12px;
  background: none; border: none; color: var(--text); font-size: var(--text-sm); font-weight: 600;
  cursor: pointer; text-align: left; white-space: nowrap; transition: color 0.12s;
}
.sidebar-group-hdr:hover { color: var(--accent); }
/* Root group headers (Journey, System, …): line ends at the left of the name
   (──────── NAME ▾). Clicking collapses/expands the group. In collapsed mode the
   text/caret are hidden leaving just the ::before line as a thin separator. */
/* Root group headers (Journey, System, …): reworked into plain WHITE, left-aligned,
   mixed-case section labels — no leading divider line, no caret, not collapsible
   (so styled as a label, not a button). */
.sidebar-group-hdr:not(.sidebar-panel-trigger) {
  height: auto; padding: 6px 10px 6px 14px; min-height: 0; overflow: visible; gap: 6px;
  margin-top: 16px;   /* breathing room above each section label */
  line-height: 18px;
  font-size: var(--text-xs); font-weight: 300; text-transform: none; letter-spacing: normal;
  color: var(--muted);   /* fallback for browsers without color-mix */
  color: color-mix(in srgb, var(--muted), var(--text) 45%);   /* brighter gray (toward white in dark) */
  cursor: default;
}
.sidebar-group-hdr:not(.sidebar-panel-trigger):hover {
  color: var(--muted);
  color: color-mix(in srgb, var(--muted), var(--text) 45%);
}
/* No leading divider line, no dropdown caret. */
.sidebar-group-hdr:not(.sidebar-panel-trigger)::before { content: none; }
.sidebar-group-hdr:not(.sidebar-panel-trigger) .sidebar-caret { display: none; }
/* Label sits at the left; the group icon is hidden (every side panel — the collapsed rail hides
   the header's text and icon anyway, see the `.docs-sidebar.collapsed` rules). */
.sidebar-group-hdr:not(.sidebar-panel-trigger) .sidebar-text { flex: 0 0 auto; }
.sidebar-group-hdr:not(.sidebar-panel-trigger) .sidebar-icon { display: none; }
/* No separate border or extra top margin — the header's own symmetric padding gives
   equal space above and below the divider line. */
.sidebar-group + .sidebar-group { margin-top: 0; padding-top: 0; }
/* Bare separator line for a header-less group (`divider: true`) — same colour/thickness as a group
   header's ::before line, with the header's symmetric 4px+18px+4px vertical rhythm so rows below sit
   at the same Y as a labeled group's would. */
.sidebar-divider { height: 1px; margin: 12.5px 10px; background: rgba(var(--muted-rgb),0.25); }
/* Governed "No access" placeholder shown in place of the ability groups when the signed-in user
   is not a data participant on the active project (see build_sidebar_payload). A muted empty-state
   message centred in the sidebar body: the nav is `flex:1` (fills the height), so centring the nav
   itself when it holds only the note puts the message at the true vertical middle. */
.sidebar-nav:has(.sidebar-note) { display: flex; align-items: center; justify-content: center; }
.sidebar-note {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; padding: 0 14px; text-align: center;
  color: var(--muted); font-size: var(--text-xs); opacity: 0.5;   /* deliberately faint — a quiet empty state */
}
.sidebar-note-icon { color: var(--muted); opacity: 0.7; }
.sidebar-note-icon svg { width: 30px; height: 30px; }
.docs-sidebar.collapsed .sidebar-note { display: none; }
.docs-sidebar.collapsed .sidebar-divider { margin: 12.5px 0; }
/* Collapsed rail: group header becomes a thin divider line. Keep the SAME padding,
   min-height and inter-group margin as the expanded header (the text just goes
   invisible) so every row below sits at the same Y in both states — no vertical
   jump. The ::before line centers in the reserved height as the separator. */
/* Collapsed rail: with no divider line, the root label has nothing to show, so hide
   it entirely (its items still render as the icon rail below). */
/* Collapsed rail: the section label KEEPS its space (margin + min-height matching the
   expanded box) so icons below stay at the same Y; its text/caret/icon are hidden and
   a small centered divider line (::before) marks the section boundary. */
.docs-sidebar.collapsed .sidebar-group-hdr:not(.sidebar-panel-trigger) { min-height: 30px; }
.docs-sidebar.collapsed .sidebar-group-hdr:not(.sidebar-panel-trigger)::before {
  content: ''; flex: 1; height: 1px; background: rgba(var(--muted-rgb),0.25); align-self: center;
}
.docs-sidebar.collapsed .sidebar-group-hdr:not(.sidebar-panel-trigger) .sidebar-icon { display: none; }
/* Every other row (flat links, direct section links, subgroup headers, panel
   triggers) KEEPS its expanded padding/margin in collapsed mode — text/caret are
   hidden but the icon stays at its expanded 14px left edge and same Y, so nothing
   jumps when the sidebar toggles. No per-row collapsed override is needed. */
/* Icon box is as tall as the text line (≥ inherited 1.6 line-height) so the icon —
   not the text — sets the row height. The row is then the SAME height whether text
   shows (expanded) or is hidden (collapsed): no cumulative vertical drift. */
.sidebar-icon { font-size: var(--icon-lg); flex-shrink: 0; width: 22px; height: 22px; text-align: center; display: flex; align-items: center; justify-content: center; }
.sidebar-icon--svg { color: inherit; }
.sidebar-icon--svg svg { display: block; }
.sidebar-link .sidebar-icon { color: var(--sidebar-fg); }
.sidebar-link:hover .sidebar-icon { color: var(--accent); }
.sidebar-link.active .sidebar-icon { color: var(--text); }
/* Caret SVG sizing */
.sidebar-caret--svg { display: flex; align-items: center; }
.sidebar-caret--svg svg { width: 10px; height: 10px; transition: transform 0.14s; }
.sidebar-group-hdr[aria-expanded="false"] .sidebar-caret--svg svg,
.sidebar-subgroup-hdr[aria-expanded="false"] .sidebar-caret--svg svg { transform: rotate(-90deg); }
.sidebar-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.docs-sidebar.collapsed .sidebar-text,
.docs-sidebar.collapsed .sidebar-caret { display: none; }
/* Items container stays visible in collapsed mode. Author display:block beats the
   UA [hidden]{display:none} by origin precedence, so direct section links
   (Documents/Planner/Quests) and subgroup-header icons show even for closed groups.
   Padding is left at the base (0 8px) — identical to expanded — so icons never move. */
.docs-sidebar.collapsed .sidebar-items { display: block; }
.sidebar-caret { font-size: var(--icon-sm); color: var(--muted); transition: transform 0.14s; }
/* Rotate ONLY the text-fallback caret span (▾). When the caret is an SVG, the inner <svg> is rotated
   instead (see .sidebar-caret--svg svg rule); rotating BOTH the span AND the svg compounds to -180°,
   which is why the collapsed chevron pointed UP instead of right. */
.sidebar-group-hdr[aria-expanded="false"] .sidebar-caret:not(.sidebar-caret--svg),
.sidebar-subgroup-hdr[aria-expanded="false"] .sidebar-caret:not(.sidebar-caret--svg) { transform: rotate(-90deg); }
/* No bottom padding: the group divider's own symmetric padding provides the gap, so
   the space above a separator equals the space below it (no extra trailing gap). */
.sidebar-items { padding: 0 8px; }
.sidebar-link {
  display: flex; align-items: center; gap: 6px; padding: 4px 8px 4px 26px;
  font-size: var(--text-sm); color: var(--sidebar-fg); text-decoration: none; border-radius: var(--r-md);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: background 0.1s, color 0.1s;
}
.sidebar-link:hover { background: var(--sidebar-accent); color: var(--accent); text-decoration: none; }
.sidebar-link.disabled { opacity: 0.45; pointer-events: none; }   /* e.g. Config with no active project */
.sidebar-link.active { background: var(--sidebar-accent); color: var(--text); font-weight: 600; }
/* Framework release-stage pill (ability.yaml `stage:`) beside a sidebar link — poc=red,
   alpha=yellow, beta=green, rc=blue. A generic mechanism: any ability declaring `stage:` gets one. */
.stage-pill { flex: 0 0 auto; margin-left: 4px; font-size: var(--text-2xs); font-weight: 700; line-height: 1;
  letter-spacing: 0.04em; padding: 2px 5px; border-radius: 999px; border: 1px solid transparent;
  text-transform: uppercase; }
.stage-pill.stage-poc   { color: #b91c1c; background: rgba(239, 68, 68, 0.16); border-color: rgba(239, 68, 68, 0.45); }
.stage-pill.stage-alpha { color: #a16207; background: rgba(234, 179, 8, 0.16); border-color: rgba(234, 179, 8, 0.45); }
.stage-pill.stage-beta  { color: #15803d; background: rgba(34, 197, 94, 0.16); border-color: rgba(34, 197, 94, 0.45); }
.stage-pill.stage-rc    { color: #1d4ed8; background: rgba(59, 130, 246, 0.16); border-color: rgba(59, 130, 246, 0.45); }
/* Keep the release-stage pill hugging the ability NAME: the label (.sidebar-text) is flex:1 and would
   otherwise stretch and shove the pill to the far right of the row. On links that carry a pill, let the
   label size to its content (still ellipsis-truncates via min-width:0) so the pill sits right after it. */
.sidebar-link:has(.stage-pill) .sidebar-text { flex: 0 1 auto; min-width: 0; }
/* In the side panel the stage pill is the framework MICRO badge (see `.pill-micro`) — a superscript
   annotation on the ability name rather than a sibling chip. The class comes from sidebar.js; only the
   row's own flex `gap` is trimmed here so the pill sits near the label (6px gap - 2px = 4px). The mirrored
   .section-title pill carries no `.pill-micro`, so it keeps the full-size look beside the heading. */
.sidebar-link .pill-micro { margin-left: -2px; }
/* Collapsed rail hides text — hide the pill too. */
.docs-sidebar.collapsed .stage-pill { display: none; }
/* Same pill mirrored beside a page's main title (see reflectStageOnTitle). Full size / full style —
   only the alignment differs from the side panel: `super` hangs it at the TOP of the heading's text row
   (a raise proportional to the heading's own font-size, so the 30px page title and the 20px modal
   heading both read the same) instead of centring it against a 30px cap height. */
.section-title .stage-pill { margin-left: 8px; vertical-align: super; }
/* All icon rows put the icon's LEFT EDGE at 12px = (46-22)/2, the centre of the 46px
   collapsed rail. Fixed in both states so icons never move when toggling. Here:
   sidebar-items has 8px left padding, so 4px = 12px. */
.sidebar-items > .sidebar-link { padding-left: 4px; }
.sidebar-items > .sidebar-link .sidebar-icon { font-size: var(--icon-md); }
/* Flat-group links (e.g. Home) match the standard item geometry so their active
   pill is the SAME size as the others: 8px container inset + 4px link padding. */
.sidebar-group--flat { padding: 0 8px; }
.sidebar-group--flat .sidebar-link { gap: 6px; padding: 4px 8px 4px 4px; }
/* ── The CORE side-panel nav style (the Documentation look) — ONE rule set for EVERY side panel
   (main project sidebar, admin sidebar, Config and Documentation overlays). Levels:
   • ROOT group header → the faint section label (.sidebar-group-hdr:not(.sidebar-panel-trigger)).
   • 1st-level option — a flat-group link, a group's direct link, or a subgroup header (below):
     regular weight, 4px apart. Icon-BEARING rows keep the base 4px inset so the icon sits on the
     shared 12px rail; icon-LESS rows get a +6px nudge (text at 18px) so they never hug the edge.
   • 2nd level — a subgroup's children (`.sidebar-subgroup-items`, below). */
.sidebar-group--flat > .sidebar-link,
.sidebar-items > .sidebar-link { margin-bottom: 4px; font-weight: 400; font-size: var(--text-sm); }
.sidebar-group--flat > .sidebar-link:not(:has(.sidebar-icon)),
.sidebar-items > .sidebar-link:not(:has(.sidebar-icon)) { padding-left: 10px; }
/* Subgroup — the collapsible middle tier (group → subgroup → links). ONE core look, the
   Documentation panel's sections, for EVERY subgroup: the main side panel's inline subgroups (e.g. the
   admin Organization group's Config) and the overlay panels' (Documentation) render identically.
   Header: a regular-weight --sidebar-fg row with a hover pill; caret pinned right, pointing RIGHT
   when collapsed and down when expanded. */
.sidebar-subgroup-hdr {
  display: flex; align-items: center; gap: 6px; width: 100%;
  margin: 0 0 4px; padding: 5px 8px 5px 6px;
  background: none; border: none; border-radius: var(--r-md);
  color: var(--sidebar-fg); font-size: var(--text-sm); font-weight: 400; line-height: normal;
  text-transform: none; letter-spacing: normal;
  cursor: pointer; text-align: left; white-space: nowrap; transition: color 0.12s, background 0.1s;
}
/* An icon-less header (docs sections) gets the same 10px inset as icon-less option rows. */
.sidebar-subgroup-hdr:not(:has(.sidebar-icon)) { padding-left: 10px; }
.sidebar-subgroup-hdr::before { content: none; }
.sidebar-subgroup-hdr:hover { background: var(--sidebar-accent); color: var(--accent); }
.sidebar-subgroup-hdr .sidebar-icon { font-size: var(--icon-md); }
.sidebar-subgroup-hdr .sidebar-caret { display: flex; margin-left: auto; font-size: var(--icon-sm); color: var(--sidebar-fg); }
.sidebar-subgroup-hdr .sidebar-caret--svg svg { width: 15px; height: 15px; transform-origin: center; }
.sidebar-subgroup-hdr[aria-expanded="false"] .sidebar-caret--svg svg { transform: rotate(-90deg); }
.sidebar-subgroup-hdr[aria-expanded="true"] .sidebar-caret--svg svg { transform: rotate(0deg); }
/* Children — deeper, with the left LINE embracing them; leaf links smaller + lighter. The line sits
   under the 1st-level icon (8px items padding + 16px margin). */
.sidebar-subgroup-items { margin-left: 16px; margin-top: -4px; border-left: 1px solid var(--border); padding-left: 4px; }
.sidebar-subgroup-items > .sidebar-link { padding-left: 14px; margin-bottom: 2px; font-size: var(--text-sm); font-weight: 300; }
/* Linkable collapsible subgroup header (e.g. Architecture's Overview): the label is a normal sidebar
   link (navigates to the landing page) and a trailing caret button collapses/expands the children.
   The double class beats the `:not(:has(.sidebar-icon))` inset (same specificity, later wins) so the
   header contributes ZERO padding; the inner link carries the single standard 10px inset. */
.sidebar-subgroup-hdr.sidebar-subgroup-hdr--link { display: flex; align-items: center; gap: 0; padding: 0; }
.sidebar-subgroup-hdr--link > .sidebar-link { flex: 1 1 auto; margin-bottom: 0; padding: 5px 8px 5px 10px; }
.sidebar-subgroup-hdr--link > .sidebar-caret-btn {
  flex: 0 0 auto; background: none; border: 0; padding: 4px 8px; margin-left: auto;
  display: flex; align-items: center; color: var(--sidebar-fg); cursor: pointer;
  border-radius: var(--r-md);
}
.sidebar-subgroup-hdr--link > .sidebar-caret-btn:hover { background: var(--sidebar-accent); color: var(--accent); }
.sidebar-subgroup-hdr--link .sidebar-caret--svg svg { width: 15px; height: 15px; transform-origin: center; }
.sidebar-subgroup-hdr--link[aria-expanded="false"] .sidebar-caret--svg svg { transform: rotate(-90deg); }
.sidebar-subgroup-hdr--link[aria-expanded="true"] .sidebar-caret--svg svg { transform: rotate(0deg); }
/* Recursive drilldown (plan #P6): subgroups nested INSIDE another subgroup (the CLI/MCP reference
   trees; an org ability with several Config items). The base `.sidebar-subgroup-items` rule already
   compounds the 16px indent + an embracing line per tier; these keep deep tiers legible and
   monotonically quieter. Depth is bounded by a shallow tree (a Phase-6 test caps `_nav.json` depth). */
.sidebar-subgroup-items .sidebar-subgroup-items { border-left-color: rgba(var(--muted-rgb), 0.28); }
/* A nested LINKABLE header's inner link aligns with its tier's leaf links (14px) and steps one weight
   lighter (300); a plain nested header mirrors that inset + weight, muted. */
.sidebar-subgroup-items > .sidebar-subgroup-hdr--link > .sidebar-link { padding-left: 14px; font-weight: 300; }
.sidebar-subgroup-items > .sidebar-subgroup-hdr:not(.sidebar-subgroup-hdr--link) { padding-left: 14px; font-weight: 300; color: var(--muted); }
/* Subgroup headers keep their expanded geometry in collapsed mode (icon stays at
   14px); only the nested leaf links are hidden from the rail. */
.docs-sidebar.collapsed .sidebar-subgroup-items { display: none; }

/* -----------------------------------------------------------------------
   Overlay panels (Documentation, Configuration) — a bottom-pinned trigger
   opens a panel that slides OVER the sidebar. Reuses .modal-header/.modal-close
   for the header, .side-panel-title for the title, and .sidebar-items for the
   body — no duplicate blocks.
   ----------------------------------------------------------------------- */
.overlay-panel {
  /* Slides over the side panel, so it spans the same extent: viewport top → footer
     top edge (matching .docs-sidebar, which now starts at top:0). Width tracks the
     SHARED rail var so it collapses/expands in lockstep with the sidebar + docked
     header (header toggle sets body.sb-collapsed) — never overlapping the header. */
  position: fixed; top: 0; left: 0; bottom: var(--footer-h);
  width: var(--current-sidebar-w); box-sizing: border-box; padding: 0;
  transition: width 0.18s ease;
  background: var(--panel); border-right: 1px solid var(--border);
  box-shadow: var(--shadow-modal); z-index: 20;   /* above sidebar(10), below header(300) */
  /* The panel itself does NOT scroll — the items region does — so the version
     footer stays bottom-pinned (mirrors .docs-sidebar: nav scrolls, version
     fixed). padding-bottom:0 keeps the version flush at the bottom like the
     side panel. */
  display: none; flex-direction: column; overflow: hidden;
}
/* The overlay body (the buildGroup nav, same as the main side panel) fills + scrolls, so the brand
   (top) and the user identity row (bottom) stay pinned — mirroring the main sidebar. */
.overlay-panel > .sidebar-nav { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.overlay-panel .sidebar-items { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
/* The SAME user identity row as the main side panel, pinned at the overlay's bottom. */
.overlay-panel > .sidebar-user-menu-wrap { position: relative; padding: 5px 8px 8px; }
.overlay-panel.open { display: flex; }
/* Collapsed rail: the overlay reuses the sidebar's collapse behavior (driven by the
   shared body.sb-collapsed) — text, carets and the (icon-less) section labels hide,
   leaving the link icons as a 46px rail, so the panel contracts/expands in lockstep
   with the header toggle instead of covering it. */
body.sb-collapsed .overlay-panel .sidebar-text,
body.sb-collapsed .overlay-panel .sidebar-caret { display: none; }
/* Collapsed rail: overlay section headers keep their space + show a small centered
   divider line (matching the main sidebar's root groups), so icons stay aligned. */
/* The overlay's section headers sit INSIDE .sidebar-items (8px side padding), whereas
   the main sidebar's root-group labels are direct children of .sidebar-group (no inset).
   Trim 8px off each side here so the collapsed divider line spans the SAME extent
   ([14px, rail-10px]) as the main sidebar's — otherwise it reads ~16px shorter. */
body.sb-collapsed .overlay-panel .sidebar-subgroup-hdr { min-height: 30px; padding-left: 6px; padding-right: 2px; }
body.sb-collapsed .overlay-panel .sidebar-subgroup-hdr::before {
  content: ''; flex: 1; height: 1px; background: rgba(var(--muted-rgb),0.25); align-self: center;
}
/* Labeled group headers (Config's General/Security/Advanced; Docs' Get started/How-to/…) get the
   SAME collapsed treatment as the main sidebar's root groups (see the .docs-sidebar.collapsed rules):
   the header row becomes a centered divider line — its icon is already hidden above, text/caret are
   hidden by the collapsed rules — so a collapsed Config/Docs menu reads as an icon rail WITH section
   dividers, exactly like the main menu. `content: none` is the expanded default (the global
   `.sidebar-group-hdr::before` rule), so the line must be re-enabled here. Before the header toggle
   stayed a collapse control on these pages the overlay was always force-expanded, so this never ran. */
body.sb-collapsed .overlay-panel .sidebar-group-hdr:not(.sidebar-panel-trigger) { min-height: 30px; }
body.sb-collapsed .overlay-panel .sidebar-group-hdr:not(.sidebar-panel-trigger)::before {
  content: ''; flex: 1; height: 1px; background: rgba(var(--muted-rgb),0.25); align-self: center;
}
/* Items stay visible as the icon rail; flat dividers drop their side margin; subgroup child rows fold
   away — mirroring the main sidebar's collapsed rules so nothing shifts between the two states. */
body.sb-collapsed .overlay-panel .sidebar-items { display: block; }
body.sb-collapsed .overlay-panel .sidebar-divider { margin: 12.5px 0; }
body.sb-collapsed .overlay-panel .sidebar-subgroup-items { display: none; }
body.sb-collapsed .overlay-panel .modal-header { display: none; }   /* no close in the 46px rail */
/* The overlay's cloned brand collapses to the favicon too (the .docs-sidebar.collapsed
   brand rules are scoped to the sidebar, not the overlay). */
body.sb-collapsed .overlay-panel .sidebar-brand { justify-content: center; padding-left: 0; padding-right: 0; }
body.sb-collapsed .overlay-panel .sidebar-brand .sidebar-wordmark { display: none; }
body.sb-collapsed .overlay-panel .sidebar-brand-favicon { display: flex; }
/* No horizontal panel padding (above): the panel slides over the sidebar, so its
   content aligns with the main sidebar's — section links land at the shared 12px
   column (8px .sidebar-items + 4px link), like a main-sidebar direct item. The
   header keeps a 14px inset so its title lines up with the Dashboard title (14px). */
/* The close (×) floats in the top-right of the brand band (height = --header-h),
   so the overlay's top reads identically to the main side panel's brand band — the
   brand sits flush-left at the same Y, with only the × added in the corner. The
   modal-header is taken out of flow so it adds no extra row above the items. */
.overlay-panel .modal-header {
  position: absolute; top: 0; right: 0; z-index: 2;
  height: var(--header-h); margin: 0; padding: 0 10px;
  display: flex; align-items: center;
}
/* Overlay panels run through the SAME buildGroup pipeline as the main side panel and use the CORE
   side-panel nav rules (options, subgroups, carets, children) — no overlay-specific nav copy. */
/* Version pin — its own small last line, matching the main sidebar's version style
   (smaller font + tighter padding). It's bottom-pinned by the flex-grow .sidebar-items
   above it, not margin tricks. */
.overlay-panel .sidebar-version { padding: 4px 14px 6px; font-size: var(--text-2xs); }
/* Bottom panel triggers — reuse .sidebar-group-hdr (which already carries the
   collapsed-state centering); only the bottom pin is new.
   Stacked triggers (direct children of aside, not in footer) appear below the
   project switcher (order:100) but above the footer (order:200). */
.sidebar-panel-trigger { flex-shrink: 0; color: var(--muted); font-weight: 500; }
.docs-sidebar > .sidebar-panel-trigger { order: 150; }
.sidebar-panel-trigger:hover { color: var(--text); }
/* Disabled trigger (e.g. Config with no active project) — greyed and non-clickable. */
.sidebar-panel-trigger.disabled { opacity: .45; pointer-events: none; }
.sidebar-panel-trigger.active { color: var(--accent); }
/* Footer row — the icon-only Configuration cog (left) on the same line as the
   version (right). The row owns the top divider so its children don't each draw
   one; collapsed, it stacks back to a centred cog (the version is hidden). */
.sidebar-footer { order: 200; display: flex; align-items: center; }
.sidebar-footer .sidebar-panel-trigger { width: auto; border-top: none; }
.sidebar-footer .sidebar-version { margin-left: auto; border-top: none; padding: 8px 14px; }
/* The Admin entry is a bottom trigger-styled LINK (reuses .sidebar-group-hdr.sidebar-panel-trigger):
   reset the anchor defaults and tint it system-yellow so it reads as the admin entry. Ordered
   BELOW the Documentation footer (order:200) so Documentation sits above Admin/Projects. */
.sidebar-server-link { text-decoration: none; color: var(--sw-yellow); }
.docs-sidebar > .sidebar-server-link { order: 210; }
/* Sign out — a red bottom option, the LAST entry (below Admin/Projects). */
.docs-sidebar > .sidebar-signout { order: 220; }
.sidebar-signout, .sidebar-signout .sidebar-icon { text-decoration: none; color: var(--red); }
.sidebar-signout:hover, .sidebar-signout:hover .sidebar-icon { color: var(--red); filter: brightness(1.15); }
/* User menu (server mode) — a bottom trigger showing the signed-in username. Clicking it opens a
   FLOATING popup (Admin/Projects + Sign out) that overlays the side panel — it does not expand
   inline, so the rail's layout never shifts. The wrapper takes the slot the standalone Sign out
   used (order 220) and is the positioning context; the version line (order 230) stays below it. */
.docs-sidebar > .sidebar-user-menu-wrap { order: 220; position: relative; padding: 5px 8px 8px; }
.docs-sidebar.collapsed > .sidebar-user-menu-wrap { padding: 4px 0; }
/* Taller identity row: avatar + two-line text + kebab, vertically centred. An inset ROUNDED box
   (like the nav links / .sidebar-link) — the hover pill sits inset from the panel edges. */
.sidebar-user-menu {
  border-top: none; align-items: center; gap: 10px; min-height: 46px;
  padding: 6px 8px; margin: 0; width: 100%; box-sizing: border-box; border-radius: var(--r-md);
  transition: background 0.1s, color 0.1s;
}
.sidebar-user-menu[aria-expanded="true"] { color: var(--accent); }
/* Hover: the same rounded pill highlight the nav links use. */
.sidebar-user-menu:hover { background: var(--sidebar-accent); color: var(--text); }
/* Other bottom side-panel triggers (Configuration, mode switch, Sign out) highlight on hover too. */
.docs-sidebar > .sidebar-panel-trigger:hover { background: rgba(var(--muted-rgb), 0.14); }
/* Rounded-square avatar with the user's initials — like the sun/moon toggle button but NO border. */
.sidebar-user-avatar {
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 32px; height: 32px; margin-left: -2px; border-radius: var(--r-lg); border: none;
  background: var(--secondary, rgba(var(--muted-rgb), 0.18)); color: var(--text);
  font-family: var(--font-display); font-size: var(--text-xs); font-weight: 600; letter-spacing: 0.02em;
  text-transform: uppercase; line-height: 1;
}
/* Workstation connectivity ring (canonical `wmind.contract.connectivity`, via sidebar.js). Inset so the
   avatar keeps its size and is never clipped by the rail's overflow. No class = unknown / not loaded. */
.sidebar-user-avatar.conn-online  { box-shadow: inset 0 0 0 2px var(--green); }
.sidebar-user-avatar.conn-idle    { box-shadow: inset 0 0 0 2px var(--accent2); }
.sidebar-user-avatar.conn-offline { box-shadow: inset 0 0 0 2px var(--red); }
/* A command is executing on one of the user's machines — the solid brand blue of a primary button
   (`--primary`; `--accent` is the TEXT blue, lightened on dark), overriding the status ring. A
   SPINNER: a faint full track plus a SOLID arc 60% of the way round (no fading tail — a fade read as
   a lighter blue than the button), running round the tile's rounded border. The arc is a conic-gradient on `::before`, masked down to the 2px border and
   turned by animating its start ANGLE (`@property`) — rotating the element itself would swing the
   rounded square's corners. Reduced motion: a still, solid ring. */
@property --conn-spin { syntax: '<angle>'; inherits: false; initial-value: 0deg; }
.sidebar-user-avatar.conn-busy {
  position: relative;
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--primary) 22%, transparent);
}
.sidebar-user-avatar.conn-busy::before {
  content: ''; position: absolute; inset: 0; border-radius: inherit; padding: 2px; pointer-events: none;
  background: conic-gradient(from var(--conn-spin), var(--primary) 0% 60%, transparent 60%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
  animation: conn-spin 1.1s linear infinite;
}
@keyframes conn-spin { to { --conn-spin: 360deg; } }
@media (prefers-reduced-motion: reduce) {
  .sidebar-user-avatar.conn-busy::before { animation: none; background: var(--primary); }
}
/* Username (top) + email (bottom, future) stacked; both ellipsize. */
.sidebar-user-text { display: flex; flex-direction: column; justify-content: center; min-width: 0; gap: 1px; line-height: 1.2; }
.sidebar-user-name { font-size: var(--text-sm); font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-user-email { font-size: var(--text-xs); color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-user-email:empty { display: none; }
/* Kebab (3 dots) pinned far right. */
.sidebar-user-kebab { margin-left: auto; margin-right: 0; display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: var(--muted); }
.sidebar-user-kebab svg { width: 16px; height: 16px; display: block; }
.sidebar-user-menu[aria-expanded="true"] .sidebar-user-kebab { color: var(--accent); }
/* The popup opens to the RIGHT of the row. position:fixed (left/bottom set by sidebar.js) so it
   escapes the sidebar's overflow:hidden clip and overlays the page. Items stack in DOM order. */
.sidebar-user-menu-pop {
  position: fixed; z-index: 1200; min-width: 180px;
  background: var(--panel, var(--bg)); border: 1px solid var(--border); border-radius: 8px;
  box-shadow: var(--shadow-pop); padding: 4px 0; overflow: hidden;
}
.sidebar-user-menu-pop .sidebar-panel-trigger { border-top: none; }
/* Highlight each popup option on hover. */
.sidebar-user-menu-pop .sidebar-panel-trigger:hover { background: rgba(var(--muted-rgb), 0.16); color: var(--text); }
.sidebar-user-menu-pop .sidebar-panel-trigger:hover .sidebar-icon { color: var(--text); }
/* Collapsed rail: the username text/caret hide (icon only); the popup would have nowhere sensible
   to anchor, so suppress it entirely (the toggle re-opens the rail first anyway). */
/* Collapsed rail: show the avatar only (hide the text column + kebab), centred; the popup would
   have nowhere sensible to anchor, so suppress it (the toggle re-opens the rail first anyway). */
.docs-sidebar.collapsed .sidebar-user-menu { justify-content: center; gap: 0; padding-left: 0; padding-right: 0; margin-left: 0; margin-right: 0; }
.docs-sidebar.collapsed .sidebar-user-avatar { margin-left: 0; }
.docs-sidebar.collapsed .sidebar-user-text,
.docs-sidebar.collapsed .sidebar-user-kebab { display: none; }
.docs-sidebar.collapsed .sidebar-user-menu-pop { display: none; }
/* Divider line above each bottom stacked trigger (Configuration, and the Server/Client
   mode-switch entry) — regular group headers get a ::before divider but panel-triggers are
   excluded, so the bottom section would otherwise blend into the nav. Scoped under .docs-sidebar
   to out-specify `.sidebar-group-hdr { border: none }`. The footer (Documentation + version) draws
   its own top border, so it's excluded. */
.docs-sidebar > .sidebar-panel-trigger { border-top: 1px solid var(--border); }
/* Config trigger row (client mode): the trigger fills the row, a sync icon sits at the far right.
   The row owns the top divider so the trigger inside doesn't draw its own. */
.sidebar-trigger-row { order: 150; display: flex; align-items: stretch; border-top: 1px solid var(--border); }
.sidebar-trigger-row > .sidebar-panel-trigger { flex: 1; border-top: none; }
.sidebar-sync-btn { flex-shrink: 0; display: flex; align-items: center; background: none; border: none; color: var(--muted); cursor: pointer; padding: 8px 12px; }
.sidebar-sync-btn:hover { color: var(--accent2); }
.sidebar-sync-btn.jm-sync-stale { color: var(--red); }            /* local config is behind the server */
.sidebar-sync-btn.jm-sync-stale:hover { color: var(--red); filter: brightness(1.15); }
.sidebar-sync-btn svg { width: 16px; height: 16px; display: block; }
.docs-sidebar.collapsed .sidebar-sync-btn { display: none; }
.sidebar-server-link:hover { text-decoration: none; color: var(--sw-yellow); }
.sidebar-server-link .sidebar-icon { color: var(--sw-yellow); }
/* "Back to projects" (the client_link / config-panel exit) reads WHITE (--text),
   not the admin-yellow — it's a plain context switch, not the Admin entry. */
.sidebar-back-link, .sidebar-back-link:hover { color: var(--text); }
.sidebar-back-link .sidebar-icon { color: var(--text); }
.docs-sidebar.collapsed .sidebar-footer { display: block; }
.docs-sidebar.collapsed .sidebar-footer .sidebar-panel-trigger { width: 100%; }
/* .sidebar-group-hdr carried only a :hover rule; add the canonical active token
   so the trigger (and any active header) highlights like an active link. */
.sidebar-group-hdr.active { color: var(--accent); }

/* -----------------------------------------------------------------------
   Portal dashboard — home / landing page
   ----------------------------------------------------------------------- */
.lp-hero { margin-bottom: 28px; }
.lp-hero-row { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; border-bottom: 1px solid var(--border); padding-bottom: 12px; }
/* The full-width divider lives on the row; the inline-flex h1 drops the global
   h1 underline so the line spans the whole hero, not just the name. */
.lp-hero h1 { font-size: var(--text-2xl); font-weight: 700; color: var(--text); margin: 0; display: inline-flex; align-items: center; gap: 8px; border-bottom: none; padding-bottom: 0; }
.lp-pid { color: var(--muted); font-size: var(--text-base); font-family: var(--font-mono); white-space: nowrap; }
.lp-edit { background: none; border: none; color: var(--muted); cursor: pointer; font-size: var(--text-base); line-height: 1; padding: 0 2px; }
.lp-edit:hover { color: var(--accent); }
.lp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
.lp-card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-lg); box-shadow: var(--shadow-sm); padding: 18px 20px; }
/* The remaining .lp-* row/kv/link/badge classes were specific to the old static
   landing page and were removed with it; the overview dashboard reuses only
   .lp-hero / .lp-grid / .lp-card above. */

/* -----------------------------------------------------------------------
   Overview dashboard widgets — layered ON the .lp-card / .lp-grid classes
   (markup uses class="lp-grid dash-grid" / "lp-card dash-card"), so grid
   sizing and card surface are NOT duplicated here; only the widget chrome,
   body, drag states, and the mini bar / status dot tokens are added.
   ----------------------------------------------------------------------- */
.dash-card { display: flex; flex-direction: column; }
.dash-card-hdr { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; cursor: grab; }
.dash-card.dragging .dash-card-hdr { cursor: grabbing; }
.dash-card-title { display: flex; align-items: center; gap: 6px; min-width: 0; font-size: var(--text-sm); font-weight: 700; letter-spacing: normal; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dash-card-icon { display: inline-flex; align-items: center; flex-shrink: 0; color: var(--muted); }
.dash-card-icon svg { width: 14px; height: 14px; display: block; }
.dash-grip { color: var(--muted); opacity: 0.4; font-size: var(--text-base); line-height: 1; flex-shrink: 0; }
.dash-card-hdr:hover .dash-grip { opacity: 0.8; }
.dash-card-link { color: var(--accent); text-decoration: none; font-size: var(--text-base); flex-shrink: 0; }
.dash-card-link:hover { opacity: 0.8; text-decoration: none; }
/* Right-side header actions as ONE flex child (preserves the header's two-child
   space-between): × (hover-revealed) left of the always-visible ↗, which stays
   flush right. */
.dash-card-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
/* Remove × reveals on card hover via OPACITY (keeps its slot, so no layout shift —
   mirrors the grip's reveal). */
.dash-card-remove { background: none; border: none; color: var(--muted); cursor: pointer; font-size: var(--text-base); line-height: 1; padding: 0 2px; flex-shrink: 0; opacity: 0; pointer-events: none; transition: opacity 0.12s, color 0.12s; }
.dash-card:hover .dash-card-remove { opacity: 1; pointer-events: auto; }
.dash-card-remove:hover { color: var(--text); }
.dash-card.is-hidden { display: none; }
.dash-card-body { padding: 0; font-size: var(--text-sm); color: var(--text); }
/* Dashboard add affordance — a single circular "+" PINNED to the viewport
   bottom-right (fixed, so it stays in the corner regardless of grid height); its
   dropdown opens UPWARD. z-index sits above content/overlay-panel but below the
   header (300) and modals (1000). */
.dash-toolbar { position: fixed; right: 24px; bottom: calc(var(--footer-h) + 24px); z-index: 50; }
.dash-add-wrap { position: relative; display: inline-block; }
.dash-add { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; padding: 0; border-radius: 50%; font-size: var(--text-xl); line-height: 1; cursor: pointer; border: 1px solid rgba(var(--accent-rgb),0.40); background: rgba(var(--accent-rgb),0.10); color: var(--accent); transition: background 0.12s, border-color 0.12s, opacity 0.12s; }
.dash-add:hover { background: rgba(var(--accent-rgb),0.18); border-color: rgba(var(--accent-rgb),0.60); }
.dash-add:disabled { opacity: 0.4; cursor: not-allowed; }
.dash-add-menu { position: absolute; bottom: 100%; right: 0; margin-bottom: 6px; z-index: 10; min-width: 200px; max-height: 280px; overflow-y: auto; background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-md); box-shadow: var(--shadow-modal); padding: 4px; }
.dash-add-menu[hidden] { display: none; }
.dash-add-item { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; background: none; border: none; color: var(--text); padding: 6px 8px; border-radius: var(--r-sm); cursor: pointer; font-size: var(--text-sm); }
.dash-add-item:hover { background: var(--surface-2); }
.dash-add-item-icon { display: inline-flex; align-items: center; flex-shrink: 0; color: var(--muted); }
.dash-add-item-icon svg { width: 14px; height: 14px; display: block; }
.dash-all-hidden { grid-column: 1 / -1; }
.dash-row { display: flex; align-items: center; gap: 8px; padding: 5px 0; font-size: var(--text-sm); }
.dash-row + .dash-row { border-top: 1px solid var(--border); }
.dash-stat { font-size: var(--text-sm); color: var(--text); }
.dash-stat strong { color: var(--text); font-weight: 700; }
.dash-muted { font-size: var(--text-xs); color: var(--muted); }
.dash-empty { grid-column: 1 / -1; text-align: center; }
/* Governed "No access" main-panel state — centred column (access-denied icon on top, heading,
   then the sub-message), filling the content area so it mirrors the sidebar placeholder. */
/* `.dash-notice` shares this centred icon-on-top layout for other full-panel overview states
   (e.g. the server/client-mode "Overview unavailable" info state). */
.dash-no-access, .dash-notice {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; padding: 0 20px;
  /* Fill the visible content region so the message sits at the TRUE vertical middle: viewport
     minus the fixed header (--header-h) minus .portal-content's own 28px top + 80px bottom
     padding (= 108px). Centring within a short block (e.g. 55vh) reads as "above middle". */
  min-height: calc(100vh - var(--header-h) - 108px);
}
.dash-no-access-icon, .dash-notice-icon { color: var(--muted); opacity: 0.7; line-height: 0; }
.dash-no-access-text, .dash-notice-title { font-size: var(--text-lg, 1.1em); color: var(--fg, inherit); font-weight: 600; }
.dash-no-access-sub, .dash-notice-sub { color: var(--muted); max-width: 420px; }
.dash-card.dragging { opacity: 0.45; outline: 2px dashed var(--accent); }
.dash-card.drop-target { outline: 2px dashed var(--accent); background: rgba(var(--accent-rgb),0.06); }
.dash-bar { display: inline-block; width: 64px; height: 6px; background: var(--surface-3); border-radius: var(--r-sm); overflow: hidden; vertical-align: middle; }
.dash-bar > i { display: block; height: 100%; background: rgba(var(--accent-rgb),0.55); border-radius: var(--r-sm); }
.dash-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; vertical-align: middle; margin-right: 6px; }
.dash-dot.running { background: var(--green); }
.dash-dot.stopped { background: var(--muted); }

/* Widget stat cards — a compact `.pipeline-card` analog for the planner/quests
   widgets: a small grid of cards, each with the status label on top and the
   count below. Both label and number take the per-status colour (the page-level
   `.pipeline-card` colours only the label; here the whole card "respects colours"
   per the dashboard spec), driven by the `--sc` custom property set per status. */
.dash-stats { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 6px; }
.dash-stat-card { position: relative; overflow: hidden; flex: 1 1 0; min-width: 44px; background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-md); padding: 5px 4px 4px; display: flex; flex-direction: column; align-items: center; gap: 1px; }
/* Title + number stay the system text colour (white), exactly like the planner/
   quests page .pipeline-card — a status card conveys its colour via the accent-top
   bar, NOT by tinting the text. */
.dash-stat-card .dash-stat-lbl { font-size: var(--text-2xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text); white-space: nowrap; }
.dash-stat-card .dash-stat-num { font-size: var(--text-lg); font-weight: 700; line-height: 1.1; color: var(--text); }
.dash-stat-card .dash-stat-sub { font-size: var(--text-2xs); font-weight: 600; color: var(--muted); white-space: nowrap; }
/* Accent-top bar only for status cards (a c-<status> modifier sets --sc): the
   planner/quests/events/daemon cards. Status-less metric cards (memory, documents,
   usage) carry no c- class and so get no bar. Bar colours mirror the page
   .pipeline-card --ca mapping (c-open/c-cancelled -> muted). */
.dash-stat-card[class*=" c-"]::before { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 2px; background: var(--sc, var(--accent)); }
.dash-stat-card.c-open      { --sc: var(--muted); }
.dash-stat-card.c-pending   { --sc: var(--accent2); }
.dash-stat-card.c-active    { --sc: var(--accent); }
.dash-stat-card.c-blocked   { --sc: var(--red); }
.dash-stat-card.c-completed { --sc: var(--green); }
.dash-stat-card.c-cancelled { --sc: var(--muted); }
.dash-stat-card.c-reviewing { --sc: var(--cyan); }
.dash-stat-card.c-accepted, .dash-stat-card.c-ready { --sc: var(--accent2); }
.dash-stat-card.c-executed  { --sc: var(--green); }
.dash-stat-card.c-warn      { --sc: var(--accent2); }
.dash-stat-card.c-error     { --sc: var(--red); }
/* Inline row link-icon (the widgets' ⤢ open-detail affordance) — a borderless
   accent glyph that reads like the card-header `↗` link. */
.dash-row-link { background: none; border: 0; padding: 0 2px; margin: 0; color: var(--accent); cursor: pointer; font-size: var(--text-base); line-height: 1; flex-shrink: 0; text-decoration: none; }
.dash-row-link:hover { opacity: 0.8; }
/* Body of the shared dashboard detail popup (jmDashboard.openDetail) — the
   `.td-section`/`.td-value-md` blocks a widget fills it with own their styling. */
.dash-detail-body { padding: 2px 0 4px; }
.dash-detail-body .td-section + .td-section { margin-top: 12px; }
/* Markdown tables (GFM) inside ANY rendered-Markdown surface — a detail body, a section
   view, or a docs page (`.md-doc`). Cell density is overridden per variant below. */
.td-value-md table, .md-body table, .md-doc table { border-collapse: collapse; margin: 6px 0; font-size: var(--text-xs); }
.td-value-md th, .td-value-md td,
.md-body th, .md-body td,
.md-doc th, .md-doc td { border: 1px solid var(--border); padding: 3px 8px; text-align: left; vertical-align: top; }
.td-value-md th, .md-body th, .md-doc th { background: var(--surface-2); font-weight: 700; color: var(--text); }

/* =======================================================================
   Shared portal components — used across all ability pages
   ======================================================================= */

/* Page layout */
.page-header { display: flex; align-items: baseline; gap: 14px; margin-bottom: 6px; }
/* A section heading that FOLLOWS content rather than opening the page (e.g. Inventory's
   Skills list under the Agents list). `.page-header` carries no top margin — right for the
   first one, and too tight for any that come after a table. */
.page-header.stacked { margin-top: 26px; }
/* Page topmost title — text-3xl across all system pages (matches the docs h1). */
.section-title { font-size: var(--text-3xl); font-weight: 700; color: var(--text); margin: 0; }
/* Inside modals/overlays a .section-title is a sub-view heading, not the page title — keep it smaller. */
.modal-overlay .section-title, .modal-box .section-title, .overlay-panel .section-title { font-size: var(--text-xl); }
/* TITLE-DESCRIPTION — the muted description line directly under a page .section-title
   (formerly the "sub-title"). Also used for descriptive section labels. */
.page-subtitle, .section-subtitle, .title-description { color: var(--muted); font-size: var(--text-base); margin-bottom: 14px; }
.empty-state { color: var(--muted); font-size: var(--text-base); padding: 20px 0; }
.empty-state.error   { color: var(--red); }
.empty-state.compact { padding: 8px 0; }
/* Compact "No access" (dashboard widget cards): small muted lock + label, same colour
   as the main no-access state. jmAccessDeniedHtml({compact:true}). */
.jm-noaccess-compact { color: var(--muted); display: flex; align-items: center; justify-content: center; gap: 6px; }
.jm-noaccess-compact .jm-noaccess-lock { line-height: 0; opacity: 0.7; }
.jm-noaccess-compact .jm-noaccess-lock svg { width: 16px; height: 16px; }
/* Spinner ring (reuses the doc-spin keyframe). Large form for standalone use; jm-spinner-sm is
   the small ring used by the flat loader. */
.jm-spinner { display: inline-block; vertical-align: middle; box-sizing: border-box;
  width: 34px; height: 34px; border: 3px solid var(--border); border-top-color: var(--accent);
  border-radius: 50%; animation: doc-spin 0.8s linear infinite; }
/* When a panel list holds ONLY a state placeholder (loading / empty / no-access), drop its table
   chrome (border/background/grid) so the state shows centred on the bare area — the table itself
   appears only once data rows render. */
.task-list:has(> .jm-flat:only-child), .card-grid:has(> .jm-flat:only-child),
.task-list:has(> .dash-no-access:only-child), .card-grid:has(> .dash-no-access:only-child) {
  background: transparent; border: none; display: block !important; }
/* Flat state — the ONE format for loading AND empty (data panels + admin/config): a small icon to
   the LEFT of the text, centred HORIZONTALLY at natural height, so it never expands the section. */
.jm-flat { display: flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 0; padding: 28px 0; color: var(--muted); grid-column: 1 / -1; }
.jm-spinner-sm { width: 14px; height: 14px; border-width: 2px; flex-shrink: 0; }
/* Atomic-operation busy lock (window._jmModal.busy / pageBusy) — a spinner scrim that covers its
   host and eats every click, so an in-flight atomic change can't be double-fired or dismissed. */
.modal-overlay[data-jm-busy] .modal-box { position: relative; }
.jm-busy-scrim { position: absolute; inset: 0; z-index: 20; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 10px; cursor: wait; border-radius: inherit;
  background: color-mix(in srgb, var(--panel) 72%, transparent); }
.jm-busy-scrim .jm-busy-label { color: var(--muted); font-size: var(--text-sm); }
.jm-page-busy { position: fixed; inset: 0; z-index: 3000; border-radius: 0;
  background: color-mix(in srgb, var(--overlay) 80%, transparent); }
/* Shared — jmBusy (busy.js): in-place processing feedback where the user clicked.
   .jm-btn-busy    a control whose content is swapped for a small spinner (width/height pinned inline).
   .jm-busy-lock   a non-disableable control (link) held while busy.
   .jm-busy-locked a row whose controls are held (applied at once); once shown (.jm-row-busy) the
   ONLY visual is a spinner in place of the pressed control while the row's OTHER controls are greyed —
   the row stays readable and there is no label pill (the label is screen-reader only). With nothing
   pressed, .jm-busy-tag is a bare spinner at the row's end. .jm-row-leaving collapses it out. */
.jm-btn-busy { display: inline-flex; align-items: center; justify-content: center; cursor: progress; }
.jm-busy-lock { pointer-events: none; cursor: progress; }
.jm-busy-locked { cursor: progress; }
.jm-busy-locked :is(button, a, input, select, textarea, label, summary, [role="button"]) { pointer-events: none; }
.jm-row-busy :is(button, a, input, select, textarea, label, summary, [role="button"]):not(.jm-btn-busy) {
  opacity: 0.4; transition: opacity 0.15s ease; }
.jm-busy-host { position: relative; }
.jm-busy-tag { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); z-index: 2;
  display: inline-flex; align-items: center; pointer-events: none; }
.jm-row-leaving { opacity: 0; transition: opacity 0.18s ease, height 0.2s ease, padding 0.2s ease,
  margin 0.2s ease; padding-top: 0 !important; padding-bottom: 0 !important;
  margin-top: 0 !important; margin-bottom: 0 !important; border-width: 0 !important; }
tr.jm-row-leaving { padding: 0; margin: 0; }
@media (prefers-reduced-motion: reduce) {
  .jm-row-busy :is(button, a, input, select, textarea, label, summary, [role="button"]),
  .jm-row-leaving { transition: none; }
}
/* Destructive-action warning callout (red, exclamation) — irreversible operations. */
.jm-warn-box { display: flex; align-items: flex-start; gap: 8px; margin: 10px 0; padding: 10px 12px;
  border: 1px solid var(--red); border-radius: var(--r-lg); font-size: var(--text-sm);
  background: color-mix(in srgb, var(--red) 12%, transparent); color: var(--red); }
.jm-warn-box .jm-warn-icon { flex-shrink: 0; font-weight: 700; line-height: 1.2; }
/* Small info glyph for the flat empty state (matches the small spinner). */
.jm-info-sm { display: inline-flex; flex-shrink: 0; opacity: 0.7; line-height: 0; }
.jm-info-sm svg { width: 16px; height: 16px; }
.ellipsis { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* SYSTEM sub-title — the section headings one level below .section-title (formerly
   the "sub-sub-title"; e.g. memory "Interactions", documents "Monitored documents").
   White var(--text), --text-xl, sentence-case (NOT an uppercase muted eyebrow).
   Stat / pipeline cards use the SEPARATE `.card-label` class (smaller, bold) — NOT `.tier-label`. */
.tier-label, .td-label, .usage-section-title { font-size: var(--text-xl); font-weight: 400; text-transform: none; letter-spacing: normal; color: var(--text); margin-bottom: 6px; }
.td-label { display: flex; align-items: center; justify-content: space-between; }
.tier-hdr { display: flex; align-items: center; justify-content: space-between; padding-bottom: 5px; margin-bottom: 4px; flex-shrink: 0; }
.tier-link { font-size: var(--text-xs); color: var(--muted); text-decoration: none; padding: 1px 5px; border: 1px solid var(--border); border-radius: var(--r-sm); }
.tier-link:hover { color: var(--accent); border-color: var(--accent); text-decoration: none; }

/* Shared — toast notifications (window.jmToast).
   z-index ladder: header 300 → sidebar 10 → modal 1000 → modal-top 9999 → toast 10000. */
.jm-toast-layer { position: fixed; right: 16px; bottom: calc(var(--footer-h) + 16px); z-index: 10000; display: flex; flex-direction: column; gap: 8px; align-items: flex-end; pointer-events: none; }
.jm-toast { pointer-events: auto; max-width: 360px; padding: 6px 12px; border-radius: var(--r-md); border: 1px solid var(--border); background: var(--panel); color: var(--text); font-size: var(--text-sm); box-shadow: var(--shadow-modal); cursor: pointer; word-break: break-word; opacity: 0; transform: translateY(8px); transition: opacity 0.18s ease, transform 0.18s ease; }
.jm-toast.show { opacity: 1; transform: translateY(0); }
.jm-toast.ok  { background: rgba(var(--green-rgb),0.12); border-color: rgba(var(--green-rgb),0.35); color: var(--green); }
.jm-toast.err { background: rgba(var(--red-rgb),0.12);   border-color: rgba(var(--red-rgb),0.35);   color: var(--red); }

/* Scope badges */
.scope-badge { display: block; text-align: center; --c: var(--muted); }
.scope-user    { --c: var(--accent); }
.scope-project { --c: var(--muted); }

/* Provider badges — soft-tinted in the brand hue (live convention); --c carries
   the third-party brand colour, applied as tint + coloured text by the shared box rule. */
.prov-badge { display: block; text-align: center; --c: var(--muted); }
.prov-claude  { --c: var(--prov-claude-color); }
.prov-gemini  { --c: var(--prov-gemini-color); }
.prov-codex   { --c: var(--prov-codex-color); }
.prov-droid   { --c: var(--prov-droid-color); }
.prov-copilot { --c: var(--prov-copilot-color); }
.prov-cursor  { --c: var(--prov-cursor-color); }
.prov-antigravity { --c: var(--prov-antigravity-color); }
/* The provider box rendered INLINE inside a flex row (Runs board rows, the Audit
   Agents/event-detail rows) — a compact box, not a column-filling block. The
   single definition; never inline these declarations at the call site. */
.prov-badge.inline { display: inline-block; min-width: 52px; vertical-align: middle; }

/* Pills — docs base, portal compact override, all colour states */
.card .status .pill,
.pill {
  /* EXACT live /components .pill: round, .72rem / 700, .1rem .55rem; hue in --c
     -> 15% tint surface + pure-hue text + 35% tinted border. (No uppercase.) */
  display: inline-block; box-sizing: border-box; white-space: nowrap;
  border-radius: var(--r-pill); padding: 0.1rem 0.55rem;
  font-size: var(--text-2xs); font-weight: 700; line-height: 1.5;
  --c: var(--muted);
  background: color-mix(in srgb, var(--c) 15%, var(--panel));
  color: var(--c);
  border: 1px solid color-mix(in srgb, var(--c) 35%, var(--panel));
}
.portal-content .pill { text-align: center; }
/* ---------------------------------------------------------------------------
   CANONICAL STATUS-COLOUR SEMANTICS (every portal section maps to these tokens
   — do not invent per-section status colours; map your status -> one of these):
     active     (accent  / blue)   — system in-flight / processing
     pending    (accent2 / amber)  — awaiting a human action
     completed  (green)            — positive terminal outcome
     cancelled / blocked / error (red) — negative terminal / failure
     open       (neutral)          — neutral / not started
     on-hold    (muted)            — paused
   Interviews reuse these via JS maps (STATUS_PILL/COMPOSITE_PILL/MATCH_PILL):
     validating/generating->pending (amber, in-flight), validated->completed,
     review->pending (awaiting interviewer), go->completed, no-go->cancelled.
   --------------------------------------------------------------------------- */
/* SOFT-TINTED status chips (live Wynxx convention) — each variant sets ONLY the
   --c hue; the base .pill rule turns it into a 14% tint + coloured text + tinted
   hairline. Hues are the theme-aware tokens so they lighten in dark for legible
   coloured text. A status colour changes in ONE place (the theme token). */
/* docs ability states */
.pill.stable   { --c: var(--green); }
.pill.beta     { --c: var(--accent2); }
.pill.alpha    { --c: var(--red); }
.pill.system   { --c: var(--accent); }
.pill.optional { --c: var(--muted); }
.pill.roadmap  { --c: var(--accent2); }
.pill.milestone{ --c: var(--purple); }
/* Work-item status bubbles — unified with the page cards' colour philosophy:
   idle/draft = neutral, queued/ready = amber, in-progress = blue, blocked = red,
   done = green, cancelled = muted grey. Covers both quest and plan statuses. */
.pill.open, .pill.updated, .pill.reviewing, .pill.on-hold { --c: var(--muted); }
.pill.pending, .pill.verified, .pill.accepted { --c: var(--accent2); }
/* FIXED-WIDTH pill column. A `.pill` is content-sized, so a column of them ends at a different
   place on every row and whatever follows starts raggedly — the same problem the `.tr-badge`
   ghost box and `.verdict-pill` already solve with a fixed width. Put `pill-col` on the `.tr-meta`
   wrapper when pills lead a list's rows, so the column reads as a column. */
.tr-meta.pill-col { flex: 0 0 auto; width: 84px; }
.tr-meta.pill-col > .pill { display: inline-block; width: 100%; box-sizing: border-box; text-align: center; }
.pill.active    { --c: var(--accent); }
.pill.blocked   { --c: var(--red); }
.pill.completed, .pill.executed { --c: var(--green); }
.pill.cancelled { --c: var(--muted); }
/* Inventory promotion (#P22). `override` = a project item hiding the org one (warning tone);
   `rejected` = a declined request (its note is the pill's tooltip). An item's ORIGIN is not a
   pill: an org item carries the org icon at the tree row's right (`originMark`). */
.pill.override { --c: var(--accent2); }
.pill.rejected { --c: var(--red); }
/* planner + quests status bubbles: ONE uniform width on every surface (board,
   detail header, history rows, widgets) so a column of statuses lines up. */
.plan-pill { min-width: 90px; text-align: center; box-sizing: border-box; }
/* audit severity states */
.pill.debug { --c: var(--muted); }
.pill.info  { --c: var(--accent); }
.pill.warn  { --c: var(--accent2); }
.pill.error { --c: var(--red); }
/* provider brand colours — solid brand fill, white text. */
.pill.prov-claude  { --c: var(--prov-claude-color); }
.pill.prov-gemini  { --c: var(--prov-gemini-color); }
.pill.prov-codex   { --c: var(--prov-codex-color); }
.pill.prov-copilot { --c: var(--prov-copilot-color); }
.pill.prov-cursor  { --c: var(--prov-cursor-color); }
.pill.prov-antigravity { --c: var(--prov-antigravity-color); }
/* Droid pill: solid dark-grey box with white text in both themes (brand grey, ~8:1 vs white). */
.pill.prov-droid   { background: var(--prov-droid-pill-bg); color: var(--prov-droid-color); border-color: var(--prov-droid-pill-bg); }
/* Box variant — square-cornered (not a rounded bubble). Used for provider name
   boxes and the system-ability "required" marker. */
.pill.boxed { border-radius: var(--r-sm); }

/* ---------------------------------------------------------------------------
   CANONICAL MICRO BADGE (`.pill-micro`) — the smallest badge in the system: a
   ~1/3-size SUPERSCRIPT annotation that hangs at the TOP of the adjacent text
   line, for marking a label without becoming a chip beside it (release stages
   on the side-panel ability links today; reuse for "new"/"soon"/count marks).
   Sizing + alignment ONLY — never colour: compose it with any badge's colour
   class (`.pill` + a hue, or `.stage-pill.stage-*`), e.g.
       <span class="stage-pill stage-beta pill-micro">BETA</span>
       <span class="pill active pill-micro">NEW</span>
   Works in both layout contexts: `align-self` tops it out in a flex row (which
   otherwise centres it), `vertical-align` in inline/heading text. A flex parent
   with its own `gap` trims that gap itself (see `.sidebar-link .pill-micro`)
   — the class must not assume a row's spacing.
   --------------------------------------------------------------------------- */
.pill-micro {
  font-size: var(--text-3xs); font-weight: 700; line-height: 1.5;
  letter-spacing: 0.02em; padding: 0 3px; margin-left: 1px;
  align-self: flex-start; margin-top: 1px;   /* flex rows: top of the line, not centred */
  vertical-align: super;                     /* inline text: rides the cap height */
}

/* ---------------------------------------------------------------------------
   CANONICAL LABEL BOX — every small inline label box across the portal (row
   type/parent/dep/plan badges, origin, scope, provider, config-name, doc-id)
   shares ONE height + font, matching the status bubble (.portal-content .pill)
   but square-cornered. Per-class rules below set ONLY colour, width, display,
   and overflow — never font-size / weight / line-height / padding here-on, so a
   single change here resizes every box in lockstep. */
.tr-badge, .org-badge, .scope-badge, .prov-badge, .cfg-name-box, .doc-id {
  font-size: var(--text-xs); font-weight: 700; line-height: 1.6;
  padding: 1px 6px; border-radius: var(--r-sm); box-sizing: border-box;
  /* Soft-tinted label boxes (live convention) — same --c mechanism as .pill.
     Variants set ONLY --c; .tr-badge.date opts out (plain muted text). */
  --c: var(--muted);
  background: color-mix(in srgb, var(--c) 14%, var(--panel));
  color: var(--c);   /* deepen the hue toward the foreground so coloured text stays legible on the pale tint (light) / dark panel — adapts per theme via --text */
  border: 1px solid color-mix(in srgb, var(--c) 32%, var(--panel));
}

/* Buttons. .btn-primary is the single solid indigo fill; the default/secondary
   button is a neutral ghost (foreground text + --border); danger is a red outline. bg/fg/border flow through --btn-bg / --btn-fg / --btn-bd.
   Dense rows can opt into .btn-sm. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.45rem;
  box-sizing: border-box; padding: 0.55rem 1.05rem; border-radius: var(--r-md);
  font-size: var(--text-sm); font-weight: 600; cursor: pointer; text-decoration: none; white-space: nowrap;
  background: var(--btn-bg, transparent);
  color: var(--btn-fg, var(--text));
  border: 1px solid var(--btn-bd, var(--border));
  transition: background 0.18s, border-color 0.18s, color 0.18s, box-shadow 0.18s, transform 0.12s;
}
.btn:active:not(:disabled) { transform: translateY(1px); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* `hidden` MUST win over the .btn display above: the UA rule `[hidden]{display:none}` has the SAME
   specificity as `.btn` and loses on source order, so a JS-hidden button (`el.hidden = true`) would
   otherwise still render. This defeated the permission gating on the admin action buttons (Add
   organization/user/member/group, Upload, From source, Add project — all `.btn`, all toggled via
   `.hidden`), leaking them to users who lack the grant. Mirrors the `.hdr-project-item[hidden]` fix. */
.btn[hidden] { display: none !important; }
/* Same for a list row: `.task-row` is flex, so a JS-hidden row (e.g. a config row that applies only
   in some modes) would otherwise still render. */
.task-row[hidden] { display: none !important; }
/* Default (ghost) hover — border + text go brand (live .btn-ghost). */
.btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }

/* Small variant (live .btn-sm). */
.btn.btn-sm { padding: 0.35rem 0.7rem; font-size: var(--text-xs); }

/* Config-card variant — the SLIM button for compact `.cfg-card` rows: a fixed 22px height (matching
   the row's slider/switch controls) so an in-row action lines up instead of overhanging. Canonical:
   any button inside a config card uses `.btn.btn-cfg` (+ its colour, e.g. .btn-primary). */
.btn.btn-cfg { height: 22px; min-height: 22px; padding: 0 8px; font-size: var(--text-xs); border-radius: var(--r-sm); }
/* Icon-only config button (e.g. the inventory row's ✓ accept / ✕ reject): a 22px square. */
.btn.btn-cfg.btn-cfg-icon { width: 22px; padding: 0; }
.btn.btn-cfg.btn-cfg-icon svg { width: 14px; height: 14px; }
/* The neutral "open/manage" pen (inventory rows): a gray glyph that brightens on hover, so it sits
   quietly beside the coloured accept / reject decision icons. */
.btn.btn-cfg.btn-cfg-icon.btn-cfg-edit { color: var(--muted); }
.btn.btn-cfg.btn-cfg-icon.btn-cfg-edit:hover { color: var(--text); }
/* "Request promotion" (inventory rows, #P22): the SAME outlined action box as ✓ accept / ✕ reject /
   Withdraw (coloured border + glyph, faint wash on hover), in the accent blue. */
.btn.btn-cfg-promote, .btn.btn-cfg-demote { --btn-fg: var(--accent); --btn-bd: color-mix(in srgb, var(--accent) 30%, var(--panel)); }
.btn.btn-cfg-promote:hover:not(:disabled), .btn.btn-cfg-demote:hover:not(:disabled) { background: color-mix(in srgb, var(--accent) 10%, var(--panel)); border-color: var(--accent); color: var(--accent); }
/* ACCEPT — the green twin of .btn-danger's outline (same shape, green hue), for ✓ beside ✕. */
.btn.btn-accept { --btn-fg: var(--green); --btn-bd: color-mix(in srgb, var(--green) 30%, var(--panel)); }
.btn.btn-accept:hover:not(:disabled) { background: color-mix(in srgb, var(--green) 10%, var(--panel)); border-color: var(--green); color: var(--green); }
/* WITHDRAW — the submitter takes back what is PENDING: the pending yellow (`--accent2`, as the
   `pending` pill), so it never reads as the reviewer's red ✕ Reject beside it. */
.btn.btn-withdraw { --btn-fg: var(--accent2); --btn-bd: color-mix(in srgb, var(--accent2) 30%, var(--panel)); }
.btn.btn-withdraw:hover:not(:disabled) { background: color-mix(in srgb, var(--accent2) 10%, var(--panel)); border-color: var(--accent2); color: var(--accent2); }

/* The live /components Buttons section has exactly THREE types: primary (solid
   indigo), ghost (neutral outline), danger (red outline). Every app button maps
   onto one of those — no green/amber/purple button colours. */

/* PRIMARY — solid indigo + medium shadow. Affirmative / main actions (save,
   apply, create, queue, confirm-outcome). hover darkens. */
.btn.btn-primary,
.btn.btn-queue,
.btn.btn-new,
.btn.btn-success,
.btn.btn-outcome,
.btn.btn-reopen,
.btn.jm-cfg-apply { --btn-bg: var(--primary); --btn-fg: var(--on-accent); --btn-bd: var(--primary); box-shadow: var(--shadow-md); }
.btn.btn-primary:hover:not(:disabled),
.btn.btn-queue:hover:not(:disabled),
.btn.btn-new:hover:not(:disabled),
.btn.btn-success:hover:not(:disabled),
.btn.btn-outcome:hover:not(:disabled),
.btn.btn-reopen:hover:not(:disabled),
.btn.jm-cfg-apply:hover:not(:disabled) { background: var(--primary-hover); border-color: var(--primary-hover); color: var(--on-accent); }

/* DANGER — red outline over transparent (live .btn-danger); hover faint red wash. */
.btn.btn-danger { --btn-fg: var(--red); --btn-bd: color-mix(in srgb, var(--red) 30%, var(--panel)); }
.btn.btn-danger:hover:not(:disabled) { background: color-mix(in srgb, var(--red) 10%, var(--panel)); border-color: var(--red); color: var(--red); }

/* GHOST — neutral outline (secondary: hold, cancel, and the bare .btn). The base
   .btn already renders ghost + the brand hover, so these just reset to it. */
/* .btn-hold and the bare .btn are the neutral ghost from the base .btn rule. */
/* MUTED — borderless ghost (e.g. the Runs board's Show more toggle). */
.btn.btn-muted    { --btn-fg: var(--muted); --btn-bd: transparent; }
.btn.btn-muted:hover:not(:disabled) { background: color-mix(in srgb, var(--muted) 10%, transparent); border-color: transparent; color: var(--muted); }
/* btn-new is a semantic alias of primary — colour only, NO size override, so it
   follows the central .btn / .btn-sm sizes (the /components button sizes) like every
   other variant. (Previously a padding override here defeated .btn-sm.) */

/* Modals */
.modal-overlay { display: none; position: fixed; inset: 0; background: var(--overlay); z-index: 1000; align-items: center; justify-content: center; }
.modal-overlay.open { display: flex; }
.modal-box { background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-xl); padding: 22px 26px 20px; width: 520px; max-width: calc(100vw - 32px); box-shadow: var(--shadow-modal); }
.modal-box.modal-wide { width: 1000px; max-height: calc(100vh - 48px); display: flex; flex-direction: column; overflow: hidden; }
.modal-box.modal-sm   { width: 480px; }
.modal-box.modal-lg   { width: 720px; }
.modal-overlay.modal-top { z-index: 9999; }
.modal-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 16px; flex-shrink: 0; }
/* ===========================================================================
   SYSTEM RULE — POPUP STYLE (the login popup is the canonical reference).
   Every detail / edit popup across the system MUST follow these three aspects so
   titles never drift (documented in WEB-DEVELOPMENT.md › "Popup style"):
     • TITLE     — white (var(--text)), text-lg, bold (700), the system font
                   (never monospace). Selectors: `.modal-box .modal-header h3`
                   (scoped to popups so it never hits the overlay-panel header,
                   which keeps `.side-panel-title`), the audit
                   `.modal-hdr h3`/`.ev-type-text`, and `.td-id-title`.
     • DESCRIPTION (the tenant crumb under the title) — accent2 (yellow), text-xs;
                   `.modal-ctx` (+ bold `.modal-ctx-user`).
     • SUBTITLES (section / field labels) — system blue (var(--accent)); see the
                   `.modal-box … label` rule below.
   A badge in a popup TITLE sizes to its content + planner's badge height (never a
   list/table column min-width); see `.modal-hdr .org-badge`.
   Side panels (slide-overs: the Configuration / Documentation overlays) are NOT
   popups — they keep the accent2 `.side-panel-title`.
   =========================================================================== */
.modal-box .modal-header h3 { margin: 0; font-size: var(--text-lg); font-weight: 700; color: var(--text); }
.side-panel-title { margin: 0; font-size: var(--text-lg); color: var(--accent2); }
/* Discreet tenant breadcrumb under a popup title: "User: <user> (org - project)". Pulled up
   close under the title (the modal-header carries a 16px bottom margin). */
.modal-ctx { margin: -10px 0 14px; font-size: var(--text-xs); color: var(--accent2); letter-spacing: 0.02em; }
.modal-ctx-user { color: var(--accent2); font-weight: 700; }
/* Side-panel titles (the main sidebar title + the Documentation / Configuration
   overlay titles) are larger than the shared base — but NOT the modal-dialog
   headers, which keep var(--text-lg). The .overlay-panel selector beats the
   `.modal-header h3` specificity so the overlay h3 titles grow too. */
.side-panel-title, .overlay-panel .side-panel-title { font-size: var(--text-lg); }
/* Main sidebar title — a non-linking heading (its landing page lives under the
   Overview item below it). Scoped to .sidebar-nav so the overlay titles keep the
   modal-header layout. */
/* Fixed-height title row: the "Dashboard" label (left, vertically centred) shares this
   line with the collapse toggle pinned at its right. Fixed height (not text-driven)
   means it reserves the exact same space in both states. Right padding clears the toggle. */
.sidebar-nav .side-panel-title {
  display: flex; align-items: center; height: 44px; box-sizing: border-box;
  padding: 0 44px 0 14px; font-weight: 700; white-space: nowrap; overflow: hidden;
}
/* Collapsed: the title TEXT goes invisible but the 44px box stays, so every section
   icon below keeps the same vertical Y as expanded — no jump when toggling. */
.docs-sidebar.collapsed .sidebar-nav .side-panel-title { color: transparent; }
/* Titles removed from the side panels — the Wynxx brand at the top of each panel
   replaces them. Scoped to the side panel + overlays so modal/popup titles elsewhere
   keep theirs; the overlay close (×) stays pinned top-right with the title gone. */
.sidebar-nav .side-panel-title,
.overlay-panel .side-panel-title { display: none; }
.overlay-panel .modal-header { justify-content: flex-end; }
.modal-close { background: none; border: none; color: var(--muted); font-size: var(--text-xl); cursor: pointer; padding: 0 4px; line-height: 1; flex-shrink: 0; }
.modal-close:hover { color: var(--text); }
/* Maximize button */
.modal-header-actions { display: flex; gap: 4px; align-items: center; flex-shrink: 0; }
.modal-maximize { background: none; border: none; color: var(--muted); cursor: pointer; padding: 2px 4px; line-height: 1; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
.modal-maximize:hover { color: var(--text); }
/* Resizable modal box — add modal-resizable to .modal-box to activate */
.modal-box.modal-resizable { position: relative; overflow: hidden; display: flex; flex-direction: column; max-height: calc(100vh - 48px); }
.modal-box.modal-resizable.modal-maximized { width: calc(100vw - 32px) !important; height: calc(100vh - 32px) !important; max-width: none !important; max-height: none !important; }
/* Body grows into container height — flex: 1 1 auto uses content as basis so sections don't collapse in auto-height boxes */
/* CANONICAL pair for a resizable modal built from bare divs: the body scrolls, the footer is
   pinned. The box is `overflow:hidden`, and a flex item's default `min-height:auto` refuses to
   shrink below its content — so a body without these rules PUSHES THE FOOTER OUT OF THE BOX and
   it is clipped, not scrolled to: the actions become unreachable. Use these two on any new
   modal; the surface-named rules below (.td-value / .sh-list / .dash-detail-body, #pd-body,
   #td-body) predate them and each carries extra per-surface styling. */
.modal-box.modal-resizable > .modal-scroll-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.modal-box.modal-resizable > .modal-foot { flex-shrink: 0; margin-top: auto; }
.modal-box.modal-resizable > .td-value { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.modal-box.modal-resizable > .sh-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.modal-box.modal-resizable > .pd-actions { flex-shrink: 0; margin-top: auto; }
.modal-box.modal-resizable > .sh-detail-footer { flex-shrink: 0; margin-top: auto; }
.modal-box.modal-resizable .pd-md-max { max-height: none; }
/* Resize handle overlay divs — transparent, z-index above content, cursor per edge */
.modal-rh { position: absolute; z-index: 20; }
.modal-rh-n  { top:0;    left:8px;  right:8px;  height:6px; cursor:ns-resize;   }
.modal-rh-s  { bottom:0; left:8px;  right:8px;  height:6px; cursor:ns-resize;   }
.modal-rh-e  { right:0;  top:8px;   bottom:8px; width:6px;  cursor:ew-resize;   }
.modal-rh-w  { left:0;   top:8px;   bottom:8px; width:6px;  cursor:ew-resize;   }
.modal-rh-ne { top:0;    right:0;   width:8px;  height:8px; cursor:nesw-resize; }
.modal-rh-nw { top:0;    left:0;    width:8px;  height:8px; cursor:nwse-resize; }
.modal-rh-se { bottom:0; right:0;   width:12px; height:12px; cursor:nwse-resize; border-radius: 0 0 var(--r-xl) 0; }
.modal-rh-se::after { content:''; position:absolute; right:2px; bottom:2px; width:8px; height:8px; background: repeating-linear-gradient(-45deg, transparent 0, transparent 2px, var(--muted) 2px, var(--muted) 3px); opacity:0.4; }
.modal-rh-sw { bottom:0; left:0;    width:8px;  height:8px; cursor:nesw-resize; }
/* Journey detail modal (plans + quests, abilities/journey/server/web/board.py): the body scrolls,
   the actions footer stays pinned below it. */
#jd-modal #jd-body { flex: 1; overflow-y: auto; min-height: 0; }
#jd-modal .pd-md-max { max-height: none; }
#jd-modal #jd-footer { flex-shrink: 0; border-top: 1px solid var(--border); }

/* Forms */
.form-row { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
.form-field { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 140px; }
/* FIELD RHYTHM (system rule, docs/WEB-DESIGN.md "Form field rhythm"): fields STACKED one under
   another — anywhere, at any nesting depth — are 12px apart, so a label never crowds the field
   above it. Side-by-side fields in a `.form-row` are excluded (the row's `gap` spaces them). Zero
   specificity (`:where`), so a page's own explicit layout of its fields always wins; a popup never
   needs a per-popup margin patch for this again. */
:where(:not(.form-row) > .form-field + .form-field) { margin-top: 12px; }
.form-field label { font-size: var(--text-sm); color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }
.form-field input, .form-field select, .form-field textarea, textarea.jm-textarea { background: var(--bg); border: 1px solid var(--input); border-radius: var(--r-md); color: var(--text); font-size: var(--text-sm); font-family: inherit; outline: none; }
/* shadcn Input size: h-9 (36px) + px-3 py-1; textarea uses px-3 py-2 (no fixed height). */
.form-field input, .form-field select { height: 2.25rem; padding: 4px 12px; }
.form-field input:focus, .form-field select:focus, .form-field textarea:focus, textarea.jm-textarea:focus { border-color: var(--accent); }
.form-field textarea, textarea.jm-textarea { padding: 8px 12px; resize: vertical; min-height: 56px; }
/* A textarea OUTSIDE a `.form-field` (a popup section's own input — compose, refine, a reject
   note) takes the same canonical field look: left-aligned, the body font, full width. */
textarea.jm-textarea { display: block; width: 100%; box-sizing: border-box; text-align: left;
  line-height: 1.5; max-height: 46vh; }
textarea.jm-textarea.jm-textarea-code { font-family: var(--font-mono); }
/* Portal — planner section: Markdown editor textarea (monospace, tall for easy editing) */
.form-field textarea.plan-editor { min-height: 50vh; font-family: var(--font-mono); font-size: var(--text-sm); line-height: 1.5; }
/* Documents — "Create documentation" modal: read-only, tall, scrollable prompt
   box. Not inside a .form-field, so it sets its own surface tokens; background
   is var(--bg) (the dark base) so it reads as inset depth inside the --panel
   modal, matching .plan-editor / .detail-pre. */
.doc-prompt-box { width: 100%; box-sizing: border-box; height: 50vh; resize: vertical; background: var(--bg); border: 1px solid var(--border); border-radius: var(--r-md); color: var(--text); padding: 10px 12px; font-family: var(--font-mono); font-size: var(--text-sm); line-height: 1.5; white-space: pre-wrap; overflow: auto; outline: none; }
.doc-prompt-box:focus { border-color: var(--accent); }
/* Portal — planner section: rendered plan body in the detail popup — tall for easy reading */
.pd-md-max { max-height: 60vh; }
/* Portal — planner section: detail footer — actions left, updated centered, history+close right */
.pd-actions { display: flex; align-items: center; gap: 10px; margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border); }
.modal-box.modal-wide > .td-section { flex-shrink: 0; }
.modal-box.modal-wide > .td-section-def { flex-shrink: 0; margin-bottom: 0; }
.modal-box.modal-wide > .td-section-def .td-value-scroll { max-height: 58vh; overflow-y: auto; }
.modal-box.modal-wide .pd-actions { flex-shrink: 0; }
/* Resizable overrides: sections grow into box height; 1 1 auto keeps content as basis so multi-section auto-height boxes don't collapse */
.modal-box.modal-resizable > .td-section { flex: 1 1 auto; min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
.modal-box.modal-resizable > .td-section > .td-value { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
/* Explicit ratio overrides for two-section popups: 1/3 and 2/3 — flex-basis:0 forces size from flex-grow only */
.modal-box.modal-resizable > .td-section.td-section-sm { flex: 1 1 0; }
.modal-box.modal-resizable > .td-section.td-section-lg { flex: 2 1 0; }
/* Dashboard openDetail body: grows and scrolls within the resizable box */
.modal-box.modal-resizable > .dash-detail-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.pd-actions-left   { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.pd-actions-center { flex: 1; text-align: center; }
.pd-actions-right  { display: flex; gap: 6px; align-items: center; }
/* created / updated timestamps stacked one on top of the other (updated shown only when it differs). */
.pd-updated { color: var(--muted); font-size: var(--text-sm);
  display: flex; flex-direction: column; align-items: center; gap: 1px; line-height: 1.3; }
/* 16px above a form's action row: the buttons read as the form's footer, not as one more field. */
.form-submit { margin-top: 16px; display: flex; align-items: center; gap: 12px; }
.form-field.field-xs { max-width: 80px; }
.form-field.field-sm { max-width: 160px; }
.form-field.field-md { max-width: 110px; }
.form-field.field-lg { max-width: 180px; }

/* Key/value grids and content blocks */
.kv-grid { display: grid; grid-template-columns: 100px 1fr; gap: 6px 10px; font-size: var(--text-sm); margin-bottom: 16px; }
.kv-label { color: var(--muted); font-weight: 700; text-transform: uppercase; font-size: var(--text-xs); letter-spacing: 0.06em; padding-top: 2px; }
.kv-value, .sh-detail-val { color: var(--text); word-break: break-all; }
.content-block { white-space: pre-wrap; font-size: var(--text-sm); color: var(--text); background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--r-md); padding: 8px 10px; line-height: 1.5; }

/* ===========================================================================
   Markdown rendered content — BOTH renderers: window.jmRenderMd / markdown-it
   (client, section pages + popups) and cli/web/markdown_render.py (server, the docs
   portal). CANONICAL definition — this is the ONE place markdown typography is defined
   for the whole portal. Every render surface inherits from here by wrapping its output
   in one of three container variants; no section defines its own markdown sizing:
     `.td-value-md` — a detail popup's value block (embedded: panel chrome)
     `.md-body`     — a section page's inline block (embedded: panel chrome)
     `.md-doc`      — a full docs page (no chrome)
   All three read at ONE scale — the `--md-text` / `--md-line` tokens in `:root` — so a docs
   page, a plan body and an advice are the same size by construction; a variant differs only
   in CHROME, never in scale. The ladder below is built from the system font-size TOKENS
   (--text-2xs … --text-lg), never ad-hoc em/px, so rendered markdown tracks the rest of the
   UI. Tweak markdown appearance here only.
   =========================================================================== */
/* The one scale, shared by every variant. */
.md-body, .td-value.td-value-md, .md-doc {
  font-size: var(--md-text); line-height: var(--md-line); font-weight: var(--md-weight);
  color: var(--md-color); white-space: normal; }
/* EMBEDDED variants only: the panel chrome that sets a markdown block off from the row or
   popup around it. A full page (`.md-doc`) is the page — it takes no fill or padding. */
.md-body, .td-value.td-value-md { background: var(--surface-3); border-radius: var(--r-md); padding: 8px 10px; }

/* Headings map onto the system token scale, pitched to the COMPACT UI: body =
   --text-sm (matching the portal's dense rows/labels, not the 16px page base), and
   each heading is one step up. Where the scale collapses two levels onto one token,
   weight/colour keep the hierarchy readable. Section titles carry a solid divider
   underline (var(--divider)) to set them off from the body. */
.md-body h1, .td-value-md h1, .md-doc h1 { font-size: var(--text-lg);   font-weight: 700; line-height: 1.25; color: var(--text); margin: 0.8em 0 0.4em; padding-bottom: 0.2em; border-bottom: 1px solid var(--divider); }
.md-body h2, .td-value-md h2, .md-doc h2 { font-size: var(--text-base); font-weight: 700; line-height: 1.3;  color: var(--text); margin: 0.8em 0 0.4em; padding-bottom: 0.2em; border-bottom: 1px solid var(--divider); }
.md-body h3, .td-value-md h3, .md-doc h3 { font-size: var(--text-sm);   font-weight: 700; line-height: 1.3;  color: var(--text); margin: 0.7em 0 0.3em; }
.md-body h4, .td-value-md h4, .md-doc h4 { font-size: var(--text-sm);   font-weight: 600; color: var(--text); margin: 0.7em 0 0.3em; }
.md-body h5, .td-value-md h5, .md-doc h5,
.md-body h6, .td-value-md h6, .md-doc h6 { font-size: var(--text-xs); font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; margin: 0.6em 0 0.3em; }
/* A leading heading shouldn't push the whole block down. */
.md-body > :first-child, .td-value-md > :first-child, .md-doc > :first-child { margin-top: 0; }
.md-body p, .td-value-md p, .md-doc p { margin: 0.6em 0; }
.md-body ul, .md-body ol, .td-value-md ul, .td-value-md ol, .md-doc ul, .md-doc ol { margin: 0.6em 0; padding-left: 1.6em; }
.md-body li, .td-value-md li, .md-doc li { margin: 0.25em 0; }
.md-body blockquote, .td-value-md blockquote, .md-doc blockquote { margin: 0.6em 0; padding: 2px 0 2px 12px; border-left: 3px solid var(--border); color: var(--muted); }
.md-body hr, .td-value-md hr, .md-doc hr { border: 0; border-top: 1px solid var(--divider); margin: 0.9em 0; }
.md-body pre, .td-value-md pre, .md-doc pre { margin: 0.6em 0; padding: 8px 10px; background: var(--code-overlay); border-radius: var(--r-md); overflow-x: auto; }
.md-body code, .td-value-md code, .md-doc code { font-size: var(--text-xs); background: var(--code-overlay); padding: 1px 4px; border-radius: var(--r-sm); }
.md-body pre code, .td-value-md pre code, .md-doc pre code { background: none; padding: 0; }
/* Markdown links render as non-navigating styled text (see markdown.js): keep
   the link LOOK (accent + underline) but no anchor, so a click goes nowhere. This is a
   CLIENT-renderer trait — `.md-doc` (server-rendered docs) emits real navigating <a>s,
   styled by the global `a` rule, and so is deliberately absent here. */
.md-body .md-link, .td-value-md .md-link { color: var(--accent); text-decoration: underline; cursor: default; }

/* `.md-doc` (the full docs page, wrapper emitted by
   `cli/web/markdown_render.render_markdown_file`) needs NO rules of its own: it is in the
   shared scale rule at the top of this block and every ladder rule above, and it simply opts
   out of the embedded variants' chrome. Nothing here to drift from a plan body.
   Docs-only helpers — `.crumbs`, `.tier`, `.badge`, `.ascii`, `.diagram` — are em-based, so
   they track the container's size automatically. */
.td-value-scroll { overflow-y: auto; }

/* Flex-table shared base */
.board-table, .ev-list, .agent-list, .usage-list, .task-list { background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-lg); overflow: hidden; }

/* ===== Live /components Table component (.tbl) — used verbatim ============== */
.tbl { border-collapse: collapse; width: 100%; font-size: var(--text-sm); margin: 0; }
.tbl th { text-align: left; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); border-bottom: 1px solid var(--border); background: var(--secondary); padding: 0.6rem 0.75rem; font-size: var(--text-2xs); font-weight: 600; }
.tbl td { border-bottom: 1px solid var(--border); vertical-align: middle; padding: 0.6rem 0.75rem; }
.tbl tr:last-child td { border-bottom: none; }
.tbl tr:hover td { background: color-mix(in srgb, var(--accent) 5%, transparent); }
/* The global prose-table rules (table{margin}; th,td{border:1px solid all sides})
   leak into .tbl — neutralise them: .tbl keeps ONLY the bottom hairline. */
.tbl th, .tbl td { border-top: 0; border-left: 0; border-right: 0; }
/* Audit events table: fixed layout so Data truncates; mono event col; right age/btn. */
.ev-tbl { table-layout: fixed; font-size: var(--text-xs); }
/* Compact (system-size) rows + header for the dense events table. */
.ev-tbl th, .ev-tbl td { padding: 4px 10px; }
/* Column widths are declared ONCE, by the cell's own class — see the block further down that turns
   this table into a flex column. They were previously ALSO declared on a <colgroup>, which was dead
   code (that block sets `colgroup { display:none }` globally, not just full-page), and again by
   `:nth-child()` position, which silently mis-assigned every width whenever a column was inserted. */
.ev-tbl td.ev-c-evt, .ev-tbl td.ev-c-data { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ev-tbl td.ev-c-evt { font-family: var(--font-mono); font-size: var(--text-xs); text-align: center; }
.ev-tbl td.ev-c-data { color: var(--text); }
.ev-tbl td.ev-c-age { text-align: center; color: var(--muted); font-size: var(--text-xs); white-space: nowrap; }
.ev-tbl th { text-align: center; }
.ev-tbl td.ev-c-prov, .ev-tbl td.ev-c-org, .ev-tbl td.ev-c-btn { text-align: center; }
/* Origin column = icon-only: the origin renders via .org-badge-icon (the shared
   icon schema — user/agent/wynxx each a coloured registry icon). */
.ev-tbl td.ev-c-org .org-badge, .uc-root .org-badge { vertical-align: middle; }
.ev-tbl tr.ev-sev-debug td { background: rgba(var(--muted-rgb),0.06); }
.ev-tbl tr.ev-sev-warn  td { background: rgba(var(--accent2-rgb),0.08); }
.ev-tbl tr.ev-sev-error td { background: rgba(var(--red-rgb),0.09); }
.ev-tbl tr.ev-sev-debug:hover td { background: rgba(var(--muted-rgb),0.12); }
.ev-tbl tr.ev-sev-warn:hover  td { background: rgba(var(--accent2-rgb),0.14); }
.ev-tbl tr.ev-sev-error:hover td { background: rgba(var(--red-rgb),0.15); }
/* Audit Events — full-height page: the table card fills the viewport and scrolls
   INTERNALLY (sticky header) so the page (.docs-main) doesn't scroll. :has scopes
   the fill-height flex to this page only; every other page is untouched. */
/* Full-height events page: anchor on .docs-main (definite height) so flex:1
   fills robustly; docs-main stops scrolling, the table scrolls internally. */
.docs-main:has(.ev-fullpage) { overflow: hidden; display: flex; flex-direction: column; }
.docs-main:has(.ev-fullpage) > .portal-content { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; padding-bottom: 24px; }
.ev-fullpage { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.ev-fullpage > #ev-board, .ev-fullpage > #usage-board,
.ev-fullpage > #agent-boards { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
/* Everything ABOVE the scrolling region keeps its natural height, so the title, filter
   bar and the `.pipeline` status cards stay on screen at every viewport size instead of
   being squeezed or scrolled away. */
.ev-fullpage > .audit-header, .ev-fullpage > .section-subtitle,
.ev-fullpage > .pipeline { flex: 0 0 auto; }
.ev-fullpage .ev-list { flex: 1 1 auto; min-height: 0; overflow: hidden; }
/* Inventory (#P40): the org host (`#org-cfg-root > #org-cfg-pane`, cli/web/org_config.py) passes the
   chain through, like Memory's `#ov-page` — only for a pane that is full height; every other org pane is
   untouched. */
#org-cfg-root:has(.ev-fullpage), #org-cfg-pane:has(.ev-fullpage) {
  flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
/* Agents (#Q287): the region splits in two EQUAL halves — the live diagram above, Last 24h
   below. `flex: 1 1 0` (a zero basis, so content never skews the split) on both, and each half
   scrolls inside itself. Heights here are flex-resolved, never percentages. */
#agent-boards { gap: 16px; }
#agent-boards .agent-section { flex: 1 1 0; min-height: 0;
  display: flex; flex-direction: column; }
#agent-boards .agent-section > .td-label { flex: 0 0 auto; }
#agent-boards .agent-section > div:last-child { flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column; }
#agent-boards .agent-list { flex: 1 1 auto; min-height: 0; overflow: hidden;
  display: flex; flex-direction: column; }
#agent-boards .agent-list > .ag-tbl { flex: 1 1 auto; min-height: 0; }

/* --- Agents: live flow (#P26) ------------------------------------------------
   origin › session › wynxx › run › agents › tool, as plain SVG drawn in the system's card language:
   surface fills, `--r-md` corners, the pipeline-card hover, `.card-label` type for the structural
   origin / wynxx boxes, provider colours (`.prov-*` → --c) on session titles. Status rides the card
   status tokens (working --accent, stalled --accent2 dashed, finished --green / --red, no live
   signal --muted) and is never colour alone: stalled is DASHED, and the tooltip / list say it. */
/* The diagram fills its half and is CENTRED in it, both ways. `margin: auto` on a flex child is
   the safe centring: an oversized diagram starts at the top-left and scrolls (both ways on a narrow
   screen) instead of being clipped, as `align-items: center` would. The empty state centres too. */
#agent-boards #agent-flow > .af-scroll { flex: 1 1 auto; min-height: 0; max-height: none; display: flex; }
#agent-boards #agent-flow > .af-scroll > .af-svg { margin: auto; flex: 0 0 auto; }
#agent-boards #agent-flow > .empty-state { margin: auto; }
.af-head { display: flex; align-items: center; gap: 8px; }
/* "Only session …" chip beside Last 24h while a session box filters it. */
.af-starter-tag { display: inline-flex; align-items: center; gap: 6px; padding: 2px 4px 2px 10px;
  border: 1px solid var(--accent); border-radius: var(--r-pill); font-weight: 400; font-size: var(--text-xs); }
.af-starter-tag[hidden] { display: none; }
.af-tag-k { color: var(--muted); }
.af-tag-sid { color: var(--muted); font-family: var(--font-mono); font-size: 0.9em; }
.af-tag-clear { border: 0; background: none; color: var(--muted); cursor: pointer; font-size: 14px;
  line-height: 1; padding: 0 4px; }
.af-tag-clear:hover { color: var(--text); }
.af-dim { opacity: 0.35; }
/* No card frame: the diagram sits on the page, centred at its natural width. */
#agent-flow > .empty-state { margin: 0; }
/* At most 12 rows tall; more scroll (and a narrow screen scrolls sideways). */
.af-scroll { max-height: calc(12 * 36px + 16px); overflow: auto; }
.af-svg { display: block; margin: 0 auto; font-family: var(--font-sans); font-size: var(--text-xs);
  /* Owner colours from the categorical palette: Wynxx work blue, the user's own work green. */
  --af-user: var(--cat-green); --af-wynxx: var(--cat-blue); }
/* The hover tip lives on <body> so the scroll box never clips it. */
.af-tip { position: fixed; z-index: 1000; max-width: 360px; }
/* Last 24h: running agents head the list. */
.ag-live { color: var(--green); font-weight: 600; }
.ag-stalled .ag-live { color: var(--accent2); }
/* Every box: a surface-1 card with the system border and radius. */
.af-box { fill: var(--surface-1); stroke: var(--border); stroke-width: 1; rx: var(--r-md); }
.af-node { cursor: pointer; outline: none; transform-box: fill-box; transform-origin: left center; }
.af-node .af-box { transition: fill 0.1s, stroke 0.1s; }
/* Hover lifts the fill only; the outline keeps its owner / provider colour. */
.af-node:hover .af-box { fill: var(--surface-3); }
.af-node:focus-visible .af-box { stroke: var(--accent); stroke-width: 1.5; }
.af-label { fill: var(--text); font-size: var(--text-xs); font-weight: 600; }
.af-chip { fill: var(--muted); font-family: var(--font-mono); font-size: 0.6rem; }
.af-note { fill: var(--muted); font-size: var(--text-2xs); font-style: italic; }
/* The secondary line (full session id; elapsed · tokens): small, mono, dim. */
.af-sid { fill: var(--muted); font-family: var(--font-mono); font-size: 0.6rem; }
/* Origin (user): the `.card-label` type in the text colour (white on the dark theme) on surface-2,
   with a dim outline; not clickable. */
.af-origin .af-box { fill: var(--surface-2); stroke: var(--border); }
.af-origin .af-label { fill: var(--text); font-size: var(--text-xs); font-weight: 700; }
/* The viewer's own box reads "Me" in the framework's one "Me" emphasis (`.jm-mine`'s accent). */
.af-origin.af-me .af-label { fill: var(--accent); }
/* Session: outlined and titled in its provider's colour (the `.prov-badge` --c). */
.af-session .af-box { stroke: var(--c, var(--border)); }
.af-session .af-label { fill: var(--c, var(--text)); }
/* Selected filter: the SAME outline colour (provider / Wynxx / user), just wider; a deep-linked
   session is dashed. */
.af-starter.af-sel .af-box { stroke-width: 2.5; }
.af-starter.af-hl .af-box { stroke-dasharray: 3 2; }
.af-more { cursor: default; }
/* A running agent is outlined in its owner's colour whether or not its provider reports live
   activity (`working` / `unknown`), with the same faint tint as Wynxx work. */
.af-agent.af-working .af-box, .af-agent.af-unknown .af-box {
  stroke: color-mix(in srgb, var(--af-user) 60%, transparent);
  fill: color-mix(in srgb, var(--af-user) 6%, var(--surface-1)); }
.af-agent.af-stalled { opacity: 0.75; }
/* Stalled: --accent2 outline (solid — only the LINE says it in dashes) and faded. */
.af-agent.af-stalled .af-box { stroke: var(--accent2); }
.af-group .af-label { font-weight: 700; }
/* A group's member count — its own pill after the name (#Q287), so `×9` never reads as part of it. */
.af-count { fill: color-mix(in srgb, var(--accent) 16%, transparent); stroke: var(--accent); stroke-width: 1; }
.af-count-t { fill: var(--accent); font-size: 0.6rem; font-weight: 700; font-variant-numeric: tabular-nums; }
/* Wynxx-owned work (a run such as `journey #P21`, its agents and their tools) in --af-wynxx, the
   user's own work in --af-user. Stalled still wins. */
.af-run .af-box, .af-remote .af-box, .af-agent.af-wx .af-box, .af-doing-box.af-wx .af-box,
.af-agent.af-wx.af-unknown .af-box, .af-doing-box.af-wx.af-working .af-box, .af-doing-box.af-wx.af-unknown .af-box {
  stroke: color-mix(in srgb, var(--af-wynxx) 60%, transparent);
  fill: color-mix(in srgb, var(--af-wynxx) 6%, var(--surface-1)); }
.af-run .af-label, .af-remote .af-label { fill: var(--af-wynxx); }
.af-agent.af-wx.af-stalled .af-box, .af-doing-box.af-wx.af-stalled .af-box { stroke: var(--accent2); }
.af-agent + g .af-spin { stroke: var(--af-user); }
.af-agent.af-wx + g .af-spin, .af-wx .af-spin { stroke: var(--af-wynxx); }
/* Links in their owner's colour (`.af-wx` = Wynxx). */
.af-link { fill: none; stroke-width: 1.25; stroke: var(--border); }
.af-link.af-working, .af-link.af-unknown { stroke: color-mix(in srgb, var(--af-user) 60%, transparent); }
.af-link.af-wx.af-working, .af-link.af-wx.af-unknown { stroke: color-mix(in srgb, var(--af-wynxx) 60%, transparent); }
.af-link.af-to-session { stroke: color-mix(in srgb, var(--c, var(--border)) 60%, transparent); }
/* Stalled: the same dash as a moving line, standing still. */
.af-link.af-stalled { stroke: var(--accent2); stroke-dasharray: 4 4; opacity: 0.55; }
.af-link.af-ok { stroke: var(--green); }
.af-link.af-err { stroke: var(--red); }
/* Marching dashes along the direction of the work; `animation-duration` (the pace) is set per
   link from the agent's idle time. Paused while hidden / off screen; none under reduced motion. */
.af-link.af-flow { stroke-dasharray: 4 4; animation: af-march 1.6s linear infinite; }
.af-paused .af-flow { animation-play-state: paused; }
@keyframes af-march { to { stroke-dashoffset: -16; } }
@media (prefers-reduced-motion: reduce) { .af-link.af-flow { animation: none; } }
/* Tool box: the last box while a tool is the latest step. */
.af-doing-box.af-working .af-box, .af-doing-box.af-unknown .af-box {
  stroke: color-mix(in srgb, var(--af-user) 60%, transparent); }
.af-doing-box.af-stalled .af-box { stroke: var(--accent2); }
.af-doing-box.af-ok .af-box { stroke: var(--green); }
.af-doing-box.af-err .af-box { stroke: var(--red); }
/* Status icon in the row's last box: spinner / still ring / ✓ / ✕. */
.af-spin { fill: none; stroke: var(--af-user); stroke-width: 2; stroke-linecap: round;
  stroke-dasharray: 20 12; transform-box: fill-box; transform-origin: center;
  animation: af-spin 0.9s linear infinite; }
.af-unknown .af-spin { stroke: var(--muted); }
/* Stalled: the app's alert-triangle (as on the config "apply" banner), outlined in --accent2. */
.af-warn path, .af-warn line { fill: none; stroke: var(--accent2); stroke-width: 2.4;
  stroke-linecap: round; stroke-linejoin: round; }
.af-doing-ic { font-size: var(--text-xs); font-weight: 700; }
.af-ok .af-doing-ic { fill: var(--green); }
.af-err .af-doing-ic { fill: var(--red); }
@keyframes af-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .af-spin { animation: none; } }
.af-doing { fill: var(--text); font-size: var(--text-xs); }
.af-doing.af-stalled, .af-doing.af-unknown { fill: var(--muted); }
/* A new agent grows in; a node that moved rows glides; a finished one flashes, then fades out. */
.af-new { animation: af-grow 300ms ease-out; }
.af-glide { transition: transform 300ms ease; }
.af-leave { animation: af-leave 1100ms ease-in forwards; pointer-events: none; }
.af-leave-ok .af-box { stroke: var(--green); stroke-width: 2; }
.af-leave-err .af-box { stroke: var(--red); stroke-width: 2; }
@keyframes af-grow { from { opacity: 0; transform: scale(0.85); } to { opacity: 1; transform: none; } }
/* A finished agent fades IN PLACE (sliding away would detach it from its line), and its line fades
   with it while still marching. */
@keyframes af-leave { 0%, 55% { opacity: 1; } 100% { opacity: 0; } }
.af-link.af-link-leave { animation: af-leave 1100ms ease-in forwards; }
.af-link.af-flow.af-link-leave { animation: af-march 1.6s linear infinite, af-leave 1100ms ease-in forwards; }
@media (prefers-reduced-motion: reduce) {
  .af-new, .af-leave { animation: none; }
  .af-glide { transition: none; }
}
/* Screen-reader twin: starters -> agents -> doing now. */
.af-list, .af-list ul { list-style: none; margin: 0; padding-left: 18px; font-size: var(--text-sm); }
.af-list { padding-left: 0; }
.af-list li { margin: 2px 0; }
.af-li-starter { font-weight: 600; }
.af-li-st { color: var(--muted); }
.af-li-btn { background: none; border: 0; padding: 0; color: var(--accent); cursor: pointer; font: inherit; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
/* Only the BODY scrolls, so the scrollbar starts BELOW the fixed header. The table
   becomes a flex column (thead pinned, tbody scrolls), which makes a table-level
   colgroup inert — so widths live on the CELL CLASSES below.
   They used to be `:nth-child()` position rules, which is a trap: inserting a column
   (the owner cell) shifted every later width by one — the age took the owner's width
   and the Details button lost its own — and the comments had already drifted out of
   sync with the markup. Class selectors are position-independent, so a conditional
   column can be added or removed freely. `.ev-c-data` is deliberately unsized: it is
   the flexing column that absorbs the remainder. */
.ev-tbl { display: flex; flex-direction: column; height: 100%; }
.ev-tbl thead { display: block; flex: 0 0 auto; padding-right: 6px; background: var(--secondary); }
.ev-tbl tbody { display: block; flex: 1 1 auto; min-height: 0; overflow-y: scroll; overflow-x: hidden; }
.ev-tbl thead tr, .ev-tbl tbody tr { display: table; width: 100%; table-layout: fixed; }
.ev-tbl tr > .ev-c-org { width: 46px; }
.ev-tbl tr > .ev-c-prov { width: 30px; padding-left: 4px; padding-right: 4px; }   /* narrow; trim padding so the brand box keeps full size */
.ev-tbl tr > .ev-c-evt { width: 150px; }
.ev-tbl tr > .ev-c-owner { width: 104px; }   /* committer name; only rendered under a manager's All-users view */
.ev-tbl tr > .ev-c-age { width: 58px; }
.ev-tbl tr > .ev-c-btn { width: 92px; }
.task-list { margin-top: 8px; }
.board-head { display: flex; align-items: center; gap: 8px; padding: 0 10px; min-height: var(--row-h-hdr); background: var(--surface-1); font-size: var(--text-xs); font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
.board-row  { display: flex; align-items: center; gap: 8px; padding: 0 10px; min-height: var(--row-h); border-top: 1px solid var(--border); font-size: var(--text-sm); transition: background .1s; }
.board-row:hover { background: var(--surface-2); }

/* Stat chips */
.stat-chip { display: inline-flex; align-items: baseline; gap: 6px; padding: 5px 12px; border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface-1); }
.stat-chip-label { font-size: var(--text-xs); font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.stat-chip-value { font-size: var(--text-base); font-weight: 700; }
.pipeline { display: flex; align-items: stretch; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; }

/* -----------------------------------------------------------------------
   Memory page — tier tabs + connected table panel. The selected card (.mem-tab.active)
   carries the panel surface and merges into the table below (a tab -> body shape); the
   other cards read as de-emphasised, clickable tabs. "Search hits" is a stat, not a tab.
   ----------------------------------------------------------------------- */
.mem-view { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.mem-tabs { margin-bottom: 0; align-items: stretch; position: relative; z-index: 1; flex: 0 0 auto; }
.mem-tab { cursor: pointer; background: var(--bg); box-shadow: none; }
.mem-tab:not(.active) { margin-bottom: 10px; }   /* gap to the table; only the active card connects. */
.mem-tab:hover { border-color: var(--accent); }
.mem-tab > .card-label { color: var(--muted); }
/* The big number is ALWAYS the system text colour (white in dark), active or not — the active tab is
   signalled by its accent-top bar + connected surface + label colour, never by tinting the number
   (the same rule as the dashboard stat cards). */
.mem-tab > .pipeline-count { color: var(--text); }
.mem-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.mem-tab.active { background: var(--bg); box-shadow: none; border-bottom-color: var(--bg);
  border-bottom-left-radius: 0; border-bottom-right-radius: 0; z-index: 2; }
.mem-tab.active > .card-label { color: var(--text); }
.mem-panel { background: var(--bg); border: 1px solid var(--border); border-radius: 0 0 var(--r-lg) var(--r-lg);
  margin-top: -1px; padding: 8px; flex: 1 1 auto; min-height: 0; overflow-y: auto; }   /* square TOP corners so the active tab (squared bottom) merges flush — no rounded-corner "chip" under the leftmost (Interactions) tab */
/* Graph 2D/Flow views: the board becomes a flex column (no scroll) so the canvas fills the whole
   panel and tracks its size, instead of a fixed-height canvas inside the scrolling list panel. */
.mem-panel.mem-panel-graph { display: flex; flex-direction: column; overflow: hidden; }
/* Full-height memory page: anchor on .docs-main (definite height) so the table flex:1 fills. */
.docs-main:has(.mem-view) { overflow: hidden; display: flex; flex-direction: column; }
.docs-main:has(.mem-view) > .portal-content { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; padding-bottom: 24px; }
/* KPI layout: left column = title (top) then description (below); right column = the big number,
   vertically centered against the left stack. */
.pipeline-card { position: relative; flex: 1; min-width: 120px; background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-lg); box-shadow: var(--shadow-sm); padding: 12px 16px; display: grid; grid-template-columns: 1fr auto; column-gap: 12px; align-content: start; transition: background 0.1s, border-color 0.1s; }
.pipeline-card > .card-label, .pipeline-card > .pipeline-sub, .pipeline-card > .pipeline-avg { grid-column: 1; }
/* Stat/pipeline card LABEL — its own class (small bold), distinct from the --text-xl `.tier-label`
   section heading. Carries the status `.c-*` colour and drives the card accent (see below). */
.card-label { font-size: var(--text-sm); font-weight: 700; text-transform: none; letter-spacing: normal; color: var(--text); margin-bottom: 6px; }
/* Span every content row (the span-9 reaches past any realistic row count; surplus rows are
   zero-height) so `align-self: center` centers the number against the full left-column height. */
.pipeline-card > .pipeline-count { grid-column: 2; grid-row: 1 / span 9; align-self: center; }
.pipeline-card:hover { background: var(--surface-3); border-color: rgba(var(--accent-rgb),0.35); }
/* A status top card shows an ACCENT-TOP bar in its status colour (--ca); the
   card body stays a regular card. Cards with no status get no bar. card-alert = red. */
.pipeline-card.card-alert { --ca: var(--red); }
.pipeline-card.card-ok    { --ca: var(--green); }    /* green accent-top (e.g. Wynxx events) */
.pipeline-card.card-warn  { --ca: var(--accent2); }  /* orange/amber accent-top (e.g. Warnings) */
.pipeline-card.accent-top::before, .pipeline-card.card-alert::before,
.pipeline-card.card-ok::before, .pipeline-card.card-warn::before, .mem-tab.active::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0; height: 3px;
  border-radius: var(--r-lg) var(--r-lg) 0 0; background: var(--ca, var(--accent)); }
.pipeline-arrow { display: flex; align-items: center; padding: 0 10px; color: var(--muted); flex-shrink: 0; }
.cfg-header-actions { display: flex; gap: 8px; margin-left: auto; }
/* The framework "Reset to defaults" (config-page.js, plan #20): neutral — it only changes the
   page's config (a draft in server-edit, undone by Discard) — right-aligned at the end of the page
   header, shown only while the ability is customized. */
.page-header > .jm-cfg-reset { margin-left: auto; }
.page-header .jm-cfg-reset { align-self: center; }
/* The number sits in the right column, top-aligned with the title (margin:0); the per-day average
   stays under the title on the left with the rest of the description. */
.pipeline-count { font-size: var(--text-3xl); font-weight: 700; color: var(--text); line-height: 1.15; margin: 0; text-align: right; }
.pipeline-sub { font-size: var(--text-xs); color: var(--muted); }
.pipeline-avg { font-size: var(--text-xs); color: var(--muted); font-weight: 400; opacity: 0.85; }

/* Memory recall-usefulness page — compact KPI layout (verdict line + tiles + slim bars). Colours
   come from theme tokens so it works light/dark with no per-mode overrides. */
.rc-detail { background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm); padding: 12px 16px; margin-top: 10px; }
.rc-row { margin: 10px 0; }
.rc-row:first-child { margin-top: 4px; }
.rc-row-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px;
  font-size: var(--text-sm); color: var(--muted); margin-bottom: 5px; }
.rc-row-title { font-weight: 600; color: var(--text); }
/* Small dimmed info affordance next to a title — the metric's description shows on hover. */
/* Slim sub-section divider inside a card: a thin rule with a centered label (carries no number). */
.rc-divider { display: flex; align-items: center; gap: 10px; margin: 14px 0 6px; }
.rc-divider::before, .rc-divider::after { content: ""; flex: 1; height: 1px; background: var(--border); }
.rc-divider > span { font-size: var(--text-xs); font-weight: 700; text-transform: uppercase;
  letter-spacing: .05em; color: var(--muted); }
.rc-info { display: inline-block; font-size: var(--text-xs); color: var(--muted); cursor: help;
  border: 1px solid currentColor; border-radius: 999px; width: 13px; height: 13px; line-height: 11px;
  text-align: center; vertical-align: middle; opacity: 0.7; font-weight: 700; }
.rc-info:hover { opacity: 1; }
.rc-bar { display: flex; height: 12px; border-radius: 5px; overflow: hidden; background: var(--secondary); }
.rc-bar > span { height: 100%; display: flex; align-items: center; justify-content: center;
  font-size: var(--text-2xs); font-weight: 700; white-space: nowrap; overflow: hidden; padding: 0 5px;
  box-sizing: border-box; min-width: 0; }
.rc-foot { font-size: var(--text-sm); color: var(--muted); margin-top: 12px; line-height: 1.6; }
/* Verdict pill next to a graded number (Good / OK / Low …). Colour set inline per judgment. */
.rc-badge { display: inline-block; font-size: var(--text-2xs); font-weight: 700; text-transform: uppercase;
  letter-spacing: .03em; padding: 1px 7px; border-radius: 999px; border: 1px solid currentColor;
  vertical-align: middle; }
/* Status-summary card titles — coloured to match the status pill colours
   (the count stays the default text colour). */
.card-label.c-open      { color: var(--text); }
.card-label.c-pending   { color: var(--accent2); }
.card-label.c-active    { color: var(--accent); }
.card-label.c-blocked   { color: var(--red); }
.card-label.c-completed { color: var(--green); }
.card-label.c-cancelled { color: var(--muted); }
.card-label.c-reviewing { color: var(--cyan); }
.card-label.c-accepted  { color: var(--accent2); }
.card-label.c-executed  { color: var(--green); }
/* Planner/Quests status buckets are top-cards WITH a status: show an accent-top bar
   tinted to the bucket's status colour. Title stays the system (--text) title — the
   bar conveys status. (Memory's status-less .pipeline-cards have no c- class -> no bar.) */
.pipeline-card:has(> .card-label.c-open),
.pipeline-card:has(> .card-label.c-cancelled)  { --ca: var(--muted); }
.pipeline-card:has(> .card-label.c-active)      { --ca: var(--accent); }
.pipeline-card:has(> .card-label.c-pending),
.pipeline-card:has(> .card-label.c-accepted)    { --ca: var(--accent2); }
.pipeline-card:has(> .card-label.c-reviewing)   { --ca: var(--cyan); }
.pipeline-card:has(> .card-label.c-blocked)     { --ca: var(--red); }
.pipeline-card:has(> .card-label.c-completed),
.pipeline-card:has(> .card-label.c-executed)    { --ca: var(--green); }
.pipeline-card:has(> .card-label[class*=" c-"])::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0; height: 3px;
  border-radius: var(--r-lg) var(--r-lg) 0 0; background: var(--ca, var(--accent)); }
.pipeline-card > .card-label[class*=" c-"] { color: var(--text); }

/* =======================================================================
   Memory components
   ======================================================================= */

.mem-board { background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-lg); overflow: hidden; }
.mem-card { display: flex; align-items: center; gap: 8px; padding: 4px 10px; min-height: var(--row-h); border-bottom: 1px solid var(--border); font-size: var(--text-xs); transition: background .1s; }
.mem-card:last-child { border-bottom: none; }
.mem-card:hover { background: var(--surface-2); }
/* Document cards (#P37) — a KB item's files as profiles on the Memory › Knowledge base tab: title +
   type box + markers, the description with its source box, then one muted meta line. Markers reuse
   the tree's boxes: "possibly outdated" / "changed" = the pending box (`.tr-badge.is-pending`,
   `--accent2`), ⚠ N = flagged sections. Tokens only. */
.kb-doc-list { background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-lg); }
.kb-doc-card { padding: 8px 12px; border-bottom: 1px solid var(--hairline); font-size: var(--text-xs); }
.kb-doc-card:last-child { border-bottom: none; }
.kb-doc-card:hover { background: var(--surface-2); }
.kb-doc-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-height: 22px; }
.kb-doc-title { font-size: var(--text-sm); font-weight: 600; color: var(--text); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kb-doc-end { margin-left: auto; display: flex; gap: 6px; }
.kb-doc-path { font-family: var(--font-mono); color: var(--muted); }
.kb-doc-desc { margin-top: 4px; color: var(--text); line-height: 1.5; }
.kb-doc-meta { margin-top: 4px; color: var(--muted); }
.kb-doc-flag { color: var(--accent2); cursor: help; font-weight: 700; }
.kb-doc-state { color: var(--accent2); font-size: var(--text-2xs); }
.kb-doc-card .tr-badge.is-pending { --c: var(--accent2); }
.kb-doc-card .tr-badge.kb-doc-type { --c: var(--accent); }
.kb-doc-card .tr-badge.kb-doc-src.is-author { --c: var(--green); }
.kb-doc-card .tr-badge.kb-doc-src.is-agent { --c: var(--accent); }
.kb-doc-card .tr-badge.kb-doc-src.is-extracted { --c: var(--muted); }
/* Possibly outdated in an org-page list (Org › Config › Memory, `outdatedMark`): the
   `alert-triangle` glyph in the pending colour, leading the row's label. */
.mem-org-outdated { display: inline-flex; align-items: center; vertical-align: -1px; color: var(--accent2); }
.mem-org-outdated svg { width: 12px; height: 12px; }
.mc-age   { width: 50px; flex-shrink: 0; font-size: var(--text-xs); color: var(--muted); }
.mc-rel   { width: 34px; flex-shrink: 0; font-size: var(--text-xs); font-weight: 700; }
/* Search-hit count — explicit --cyan (the relevance column has no stable colour,
   it is tinted inline by pctColor(), so there is nothing to "match"). */
.mc-hits  { width: 40px; flex-shrink: 0; font-size: var(--text-xs); font-weight: 700; color: var(--cyan); text-align: center; }
/* Interaction event-type — a memory-family column; do NOT reuse audit's
   .ev-col-evt (150px, sized for the audit row) inside a .mem-card. */
.mc-evt   { width: 90px; flex-shrink: 0; font-family: var(--font-mono); font-size: var(--text-xs); color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mc-prov  { width: 58px; flex-shrink: 0; }
.mc-sid   { width: 52px; flex-shrink: 0; font-family: var(--font-mono); font-size: var(--text-xs); color: var(--accent); text-align: right; }
.mc-scope { width: 58px; flex-shrink: 0; }
.mc-key   { width: 160px; flex-shrink: 0; font-family: var(--font-mono); font-size: var(--text-xs); color: var(--accent2); text-align: right; }
.mc-text  { flex: 1; min-width: 0; font-size: var(--text-xs); color: var(--text); }
/* Action wrappers — auto-width so the central .btn.btn-sm buttons inside size naturally
   (bespoke .mc-btn / .mc-btn-del removed in favour of .btn.btn-sm.btn-primary / .btn-danger). */
.mc-act   { flex-shrink: 0; text-align: right; }
.mc-act-2 { display: flex; gap: 4px; justify-content: flex-end; flex-shrink: 0; }
.mc-val { flex-shrink: 0; font-family: var(--font-mono); font-size: var(--text-xs); font-weight: 700; color: var(--text); }
.mc-val.ok { color: var(--green); }
.mc-val.warn { color: var(--accent2); }
.mc-val.err { color: var(--red); }
.mem-section { margin-bottom: 20px; }
.mem-warn { font-size: var(--text-sm); color: var(--red); padding: 4px 0; }
.ep-card { border: 1px solid var(--border); border-radius: var(--r-md); padding: 8px 12px; background: var(--surface-1); cursor: pointer; transition: background .1s; }
.ep-card:hover { background: var(--surface-2); }
.ep-card-meta { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; }
/* Body of a search-result card. Deliberately the SOFTENED body colour + sub-400 weight (the same
   `--md-*` pair rendered Markdown uses), not full-strength `--text`: at `--text` the snippet reads
   as heavy against the muted meta strip above it, and a highlighted match has nothing left to
   stand out from. Emphasis in this card belongs to `.jm-hit` alone. */
.ep-card-snippet { font-size: var(--text-sm); color: var(--md-color); line-height: 1.4;
  font-weight: var(--md-weight); }
.int-row { flex-shrink: 0; border: 1px solid var(--border); border-radius: var(--r-md); padding: 5px 8px; background: var(--surface-1); }
.int-row.demoted { opacity: .6; }
.int-snippet { font-size: var(--text-sm); color: var(--text); margin-top: 3px; white-space: pre-wrap; }

/* =======================================================================
   Quest components
   ======================================================================= */

.task-row { display: flex; align-items: center; gap: 8px; padding: 4px 10px; min-height: var(--row-h); border-bottom: 1px solid var(--border); font-size: var(--text-xs); transition: background 0.1s; }
.task-row:last-child { border-bottom: none; }
.task-row:hover { background: var(--surface-2); }
.task-row.row-terminal { opacity: 0.62; }
.task-row.row-terminal:hover { opacity: 1; }
/* A button in a config/list row never grows or shrinks — it hugs its content so the row's
   description column keeps the flex. Canonical (was re-scoped per config page). */
.task-row .btn { flex: 0 0 auto; }

/* ── Configuration cards (.cfg-*) — canonical config-page card component ──────
   A `.rc-detail.cfg-card` panel per group: a header (`.cfg-card-head`: title +
   optional master switch), optional `.rc-divider` sub-sections, and compact
   `.task-row` items separated by the faint `--hairline` (subtler than the titled
   `.rc-divider`). Numeric knobs render as a slider with a value-pill thumb. Shared
   by the providers + memory config pages — reuse on any config surface (do NOT
   re-declare these per page; add only page-specific selectors there). */
.cfg-card { margin-top: 16px; }
.cfg-card:first-child { margin-top: 0; }
.cfg-card > .page-subtitle { margin: -2px 0 12px; }
.cfg-card .rc-divider:first-of-type { margin-top: 8px; }
.cfg-card-head { display: flex; align-items: center; gap: 12px; }
.cfg-card-head .tier-label { margin: 0; }
/* A card whose LIST starts right under its title (no subtitle or divider to space it — the
   inventory Agents / Skills cards) takes a little room below the head instead. */
.cfg-card-head.cfg-card-head-gap { margin-bottom: 10px; }
.cfg-card-switch { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px; }
.cfg-card-switch:first-of-type { margin-left: auto; }   /* push the switch group right */
/* A card-head ACTION button (e.g. the "+ Add" on the ideas Refine-instructions card) hugs the
   right edge, mirroring the switch group — so any config card that puts an action in its head lines
   up the same way. */
.cfg-card-head > .btn { margin-left: auto; }
/* A card-head switch's label speaks the portal's control-label idiom (`.filter-bar label`). */
.cfg-card-switch-label { font-size: var(--text-xs); color: var(--muted); white-space: nowrap;
  font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }
.task-row.cfg-row-off { opacity: .4; }                  /* row grayed while its card master switch is off */
/* Rows sit DIRECTLY in the card as separated items: no nested-table border band. Hover tints the
   row like the inventory Assets tree (`background-color`, not the shorthand, so row images survive). */
.cfg-card .task-row { padding: 3px 0; min-height: 26px; border-bottom: none; font-size: var(--text-sm); align-items: center; }
.cfg-card .task-row:hover { background-color: var(--surface-2); }
.cfg-card .task-row .tr-activity { flex: 1; min-width: 0; }
/* Faint hairline BETWEEN items — the canonical inter-item line, fainter than the titled
   `.rc-divider`. Only BETWEEN rows: the first row, and the first row after a `.rc-divider`, get none. */
.cfg-card .task-row + .task-row { border-top: 1px solid var(--hairline); }
/* A JS-filled list dropped inside a card (its rows wrapped in a `.task-list`) must NOT draw the
   standalone list's panel box (bg + border + radius) — that would be a box-in-a-card. Strip the
   chrome so the rows sit flush in the card, matching the direct-in-card rows of other config cards. */
.cfg-card .task-list { background: none; border: none; border-radius: 0; margin-top: 0; overflow: visible; }
/* Friendly derived row label (primary) + a dimmer inline description. */
.cfg-friendly { font-weight: 500; color: var(--text); }
.cfg-desc { color: var(--muted); }
/* Control column alignment: control slot right-aligned; switch slot fixed-width so rows line up. */
.cfg-ctrl-slot { flex: 0 0 auto; display: flex; align-items: center; justify-content: flex-end; gap: 6px; }
.cfg-switch-slot { flex: 0 0 46px; display: flex; align-items: center; justify-content: center; }
/* Trailing action cluster in a config row (e.g. a 'web' badge + the enable switch on the Abilities
   manager rows). Canonical so every config list groups its row-end actions the same way. */
.cfg-actions { display: flex; align-items: center; gap: 6px; }
.cfg-ctrl-text { width: 90px; font-family: var(--font-mono); text-align: right; }
/* Config LIST-EDITOR control (a muted value preview + Edit button — e.g. memory's skip_dirs /
   include free-text lists). Canonical so any config page can render a list editor identically. */
.cfg-list-wrap { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.cfg-list-val { font-size: var(--text-xs); color: var(--muted); max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Config PICK popup — the shared chooser body a config control opens: a multi-select list AND the
   reorder variant (memory's event-types / languages multi-select + the preferred_providers order). */
.cfg-pick { display: flex; flex-direction: column; gap: 2px; max-height: 60vh; overflow-y: auto; }
.cfg-pick-row { display: flex; align-items: center; gap: 8px; padding: 6px 4px; border-bottom: 1px solid var(--border); }
.cfg-pick-row:last-child { border-bottom: none; }
.cfg-pick-lab, label.cfg-pick-row { display: flex; align-items: center; gap: 8px; flex: 1; cursor: pointer; }
.cfg-pick-name { font-size: var(--text-sm); text-transform: capitalize; }
.cfg-ord-ctrls { display: flex; gap: 4px; margin-left: auto; }
.cfg-ord-ctrls .btn { padding: 0 8px; line-height: 22px; }
/* Reorder popup: each entry is its own bordered box (icon + name + ↑/↓), overriding the plain
   bottom-border pick rows. */
.cfg-pick .cfg-ord-row { border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface-2, var(--panel)); padding: 6px 10px; margin-bottom: 6px; }
.cfg-ord-row .prov-badge { flex: 0 0 20px; }
.cfg-info { margin-left: 6px; flex: 0 0 auto; }
.cfg-null-switch { flex: 0 0 auto; margin: 0; }
/* Numeric knob: [min · slider(recommended band) · max] with the value riding the thumb. Accent-blue
   (`--primary`) styled explicitly — the native accent-color renders washed out. */
.cfg-num-wrap { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; justify-content: flex-end; }
.cfg-slider-wrap { position: relative; width: 130px; height: 22px; display: flex; align-items: center; flex: 0 0 auto; }
.cfg-slider-wrap::before { content: ''; position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%); height: 4px; background: var(--border); border-radius: 2px; }
.cfg-slider-band { position: absolute; top: 50%; transform: translateY(-50%); height: 4px; background: rgba(var(--accent-rgb), 0.35); border-radius: 2px; pointer-events: none; }
.cfg-slider { -webkit-appearance: none; appearance: none; accent-color: var(--primary); position: relative; z-index: 2; width: 100%; height: 22px; margin: 0; background: transparent; cursor: pointer; }
.cfg-slider::-moz-range-progress { background: var(--primary); height: 4px; }
.cfg-slider::-webkit-slider-runnable-track { background: transparent; height: 4px; }
.cfg-slider::-moz-range-track { background: transparent; height: 4px; }
.cfg-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 18px; height: 18px; border-radius: 50%; background: transparent; border: none; cursor: pointer; }
.cfg-slider::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: transparent; border: none; cursor: pointer; }
.cfg-thumb { position: absolute; top: 50%; transform: translate(-50%, -50%); z-index: 1;
  min-width: 22px; height: 18px; padding: 0 5px; border-radius: 9px; background: var(--primary);
  color: var(--on-accent); font-family: var(--font-mono); font-size: var(--text-2xs); font-weight: 700; line-height: 18px;
  text-align: center; white-space: nowrap; pointer-events: none; box-shadow: 0 0 0 2px var(--panel); }

.tr-id { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--muted); white-space: nowrap; flex-shrink: 0; min-width: 38px; text-align: left; }
.tr-meta { display: flex; align-items: center; gap: 5px; flex-shrink: 0; flex-wrap: wrap; }
/* Row OWNER (window.jmOwnerCell) — the committer's display name, shown ONLY when a manager is
   viewing "All users" (see cli/web/static/visibility.js). Plain text, not a chip: it is an identity,
   not a state, and the row already carries enough boxes. Sits in the TRAILING metadata cluster,
   immediately left of the age. Fixed width + right-aligned so every name ends on the same gutter
   beside the age instead of raggedly, and a long one ellipsises instead of squeezing the title.
   Your OWN rows read in --text at medium weight, others' in plain --muted — the same "colour =
   whose it is" convention as .jm-ownership, plus a weight cue so it is not colour-alone.
   Works in BOTH row idioms unchanged: `display:inline-block` is what makes `width` + ellipsis apply
   inside a table cell (an inline span ignores width), and a flex container blockifies it for free,
   so `flex:0 0 auto` still pins it in a .task-row. Never re-declare per ability. */
.tr-owner { display: inline-block; vertical-align: middle; flex: 0 0 auto;
  width: 94px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-align: right; font-size: var(--text-xs); color: var(--muted); }
/* Search-hit highlight: the matched terms inside a result snippet. The hit is marked by COLOUR +
   WEIGHT, never a background wash — a highlighter block fragments a sentence into stripes and
   fights the card's own surface tint on hover. `--accent2` is the theme-aware attention colour
   (amber in dark), so the match reads as emphasis on both surfaces. The surrounding snippet stays
   at the softened body weight below, so the hit is the ONLY bold thing in the line. */
.jm-hit { color: var(--accent2); font-weight: 700; }

/* YOUR OWN row ("Me") — ONE emphasis rule for every surface that names a committer, so the audit
   Usage owner column reads identically to the ability owner columns. Carried as a CLASS, not the
   `[data-mine]` attribute, so a host can drop it onto its own cell; `data-mine` stays for semantics.
   Emitted INSTEAD OF a dimming class (e.g. `.uc-dim`) rather than fighting it on specificity.
   ACCENT (blue), not `--text`: plain text colour made "Me" read as just another name — white in the
   dark theme — when the point of the rule is that your own rows stand out. Surfaces must not
   restyle it locally; build the cell with `jmPersonNode` / `jmOwnerCell` or add this class. */
.jm-mine { color: var(--accent); font-weight: 600; }
/* SOLID badge chips — solid fill, white text, no border (match the pills). Neutral
   default; semantic colours where they carry meaning. Square-cornered (--r-sm from
   the shared shape) so badges stay visually distinct from the fully-round pills. */
.tr-badge { white-space: nowrap; text-align: center; }   /* colour from shared box rule (--c, default muted) */
.tr-badge.pri  { width: 26px; --c: var(--accent2); }
.tr-badge.custom { --c: var(--accent); }
/* type chip: solid, white text; per-type colour comes in as the --chip custom
   property (set inline by planner/quests typeBadgeHtml); falls back to neutral. */
.tr-badge.type { width: 100px; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; text-align: center; --c: var(--chip, var(--muted)); }
/* date is a timestamp, not a chip — keep it plain muted text */
.tr-badge.date { color: var(--muted); border: none; background: none; font-size: var(--text-sm); font-weight: 400; }
.tr-badge.par  { --c: var(--muted); }
.tr-badge.dep  { --c: var(--red); }
.tr-badge.depof { --c: var(--purple); }
.tr-badge.plan { --c: var(--green); text-decoration: none; }
.tr-badge.plan:hover { filter: brightness(1.08); text-decoration: none; }
/* ID badge — any chip whose content is an entity id (#Q5, #P3, #A7, …). Add
   `.id-badge` to such a box: it renders in the mono id font (matching the .tr-id
   column), NON-bold and a touch smaller, so ids read as lightweight ids rather
   than emphasized labels. The per-class colour (.dep/.par/.plan/.depof) is
   preserved. CENTRALIZED — every id box across the portal shares this one font
   treatment, so the look changes in lockstep. `.td-id-badge` (the detail-modal /
   dashboard header id) shares the same treatment and only adds its accent colour. */
.id-badge, .td-id-badge { font-family: var(--font-mono); font-weight: 400; font-size: var(--text-2xs); }
.id-badge { padding: 0 5px; }
/* Row age column — fixed-width (right-aligned) so the trailing id-chip group's
   right edge and the Details button line up across rows regardless of how long the
   age text is ("5m" vs "365d"). Shared by the quests list and the advice list
   (their right side mirrors the quests row). Add `.tr-age` to the `.tr-badge.date`. */
.tr-age { min-width: 42px; text-align: right; flex-shrink: 0; }
.tr-meta .pill { text-align: center; }
.tr-activity { flex: 1; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }

/* Quest detail modal */
.td-id-title { font-size: var(--text-lg); font-weight: 700; color: var(--text); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.td-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 16px; }
.td-section { margin-bottom: 12px; }
.td-value { font-size: var(--text-base); color: var(--text); line-height: 1.5; white-space: pre-wrap; }
.td-id-badge { color: var(--accent2); }   /* font treatment shared with .id-badge above */
.td-dates { display: flex; gap: 20px; flex-wrap: wrap; }
.td-dates .td-section { flex: 1; }
.td-header-row { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; flex-wrap: wrap; }
#td-type-badge, #pd-type-badge, .dash-d-type { flex-shrink: 0; }
#td-meta-inline, #pd-meta-inline, .dash-d-meta { display: flex; gap: 5px; align-items: center; flex-wrap: wrap; margin-left: auto; flex-shrink: 0; }
/* An empty type-badge slot shouldn't leave a phantom gap before the title. */
.dash-d-type:empty { display: none; }
.instr-none { color: var(--muted); font-size: var(--text-xs); }
.sh-detail-footer { text-align: right; margin-top: 16px; }

/* _jmModal.prompt() — the in-app text-input dialog (replaces window.prompt). */
.jm-prompt-body { padding: 14px 16px 4px; display: flex; flex-direction: column; gap: 8px; }
.jm-prompt-msg { margin: 0; color: var(--muted); font-size: var(--text-sm); }
.jm-prompt-input { width: 100%; box-sizing: border-box; background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 6px; padding: 7px 9px; font: inherit; outline: none; }
textarea.jm-prompt-input { resize: vertical; min-height: 62px; }
.jm-prompt-input:focus { border-color: var(--accent); }
.jm-prompt-input.jm-prompt-err { border-color: var(--red, #e5484d); }
.jm-prompt-body + .sh-detail-footer { margin-top: 0; padding: 4px 16px 14px;
  display: flex; gap: 8px; justify-content: flex-end; }

/* Status history */
.sh-list { display: flex; flex-direction: column; gap: 4px; margin-top: 4px; }
.sh-row { display: grid; grid-template-columns: 90px 152px 1fr auto; align-items: center; gap: 8px; padding: 4px 10px; border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface-1); font-size: var(--text-sm); }
.sh-range { color: var(--muted); font-size: var(--text-sm); }
.sh-row .pill { justify-self: center; text-align: center; }
.sh-title { color: var(--muted); font-style: italic; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* (Removed bespoke .sh-link — quest history "Details" now uses the central .btn.btn-sm.btn-primary.) */
.sh-detail-grid { display: grid; grid-template-columns: 90px 1fr; gap: 6px 10px; font-size: var(--text-base); margin-top: 10px; }
.sh-detail-key { color: var(--muted); font-weight: 600; text-transform: uppercase; font-size: var(--text-xs); letter-spacing: 0.04em; padding-top: 2px; }
.sh-detail-note { grid-column: 1/-1; margin-top: 4px; padding: 8px 10px; background: var(--surface-2); border-radius: var(--r-md); border: 1px solid var(--border); white-space: pre-wrap; color: var(--text); font-size: var(--text-base); line-height: 1.5; }

/* =======================================================================
   Audit components
   ======================================================================= */

/* Origin badge */
/* min-width fits the widest root tier ("wynxx"/"system") so the owner-type boxes
   (user / wynxx / agent) all render at the SAME width — in the event list, the
   usage table, and the dashboard widgets. Longer full labels grow past it; the
   usage column overrides min-width to 0 (it sets its own width:100%). */
.org-badge { display: inline-block; white-space: nowrap; text-align: center; min-width: 56px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; box-sizing: border-box; --c: var(--muted); }
/* Origin + provider are ROUND, uppercase pills (per the /components badge spec),
   not the square label boxes. Placed after the label-box group so r-pill wins. */
.org-badge, .prov-badge { display: inline-flex; align-items: center; justify-content: center; gap: 0.3rem; border-radius: var(--r-pill); text-transform: uppercase; letter-spacing: 0.05em; font-size: var(--text-2xs); font-weight: 600; padding: 0.2rem 0.55rem; }

/* Provider shown as ICON ONLY — no pill, no name. Every .prov-badge across the
   app (audit / inventory runs / documents / usage) becomes just the brand icon; the
   provider name stays in the DOM (font-size:0) for screen readers + title hover.
   Central: a provider icon is added/changed ONLY here, no call-site edits.
   Every mark renders as a MASK tinted to the system gray (--muted, theme-aware),
   so ALL provider icons share one muted-gray hue in both themes. The SVG's own
   fill is irrelevant under a mask — only its shape is used. Add/point a provider
   icon ONLY here via its --prov-icon url; no call-site edits, no per-theme SVG
   swaps. */
.prov-badge { background: none; border: none; padding: 0; gap: 0; min-width: 0; border-radius: 0; font-size: 0; line-height: 0; vertical-align: middle; }
.prov-badge.inline { display: inline-flex; min-width: 0; }
.prov-badge::before {
  content: ""; flex: 0 0 auto; display: inline-block;
  width: 1.2rem; height: 1.2rem; margin: 0;
  background-color: var(--muted);
  -webkit-mask: var(--prov-icon) center/contain no-repeat;
          mask: var(--prov-icon) center/contain no-repeat;
}
.prov-claude      { --prov-icon: url("/static/providers/claude.svg"); }
.prov-gemini      { --prov-icon: url("/static/providers/gemini.svg"); }
.prov-codex       { --prov-icon: url("/static/providers/codex.svg"); }
.prov-antigravity { --prov-icon: url("/static/providers/antigravity.svg"); }
.prov-cursor      { --prov-icon: url("/static/providers/cursor.svg"); }
.prov-copilot     { --prov-icon: url("/static/providers/copilot.svg"); }
.prov-droid       { --prov-icon: url("/static/providers/droid.svg"); }
/* Droid's pinwheel reads a touch small at contain — zoom the mask slightly. */
.prov-droid::before { -webkit-mask-size: 112%; mask-size: 112%; }
/* Unknown provider (no icon) -> fall back to its name as plain text (no empty box). */
.prov-badge:not(.prov-claude):not(.prov-gemini):not(.prov-codex):not(.prov-cursor):not(.prov-copilot):not(.prov-antigravity):not(.prov-droid) { font-size: var(--text-xs); }
.prov-badge:not(.prov-claude):not(.prov-gemini):not(.prov-codex):not(.prov-cursor):not(.prov-copilot):not(.prov-antigravity):not(.prov-droid)::before { display: none; }
/* Providers config: brand-icon leading column (icon-only .prov-badge). */
#prov-root .task-row > .prov-badge { flex: 0 0 22px; }
.org-badge.user, .org-badge.agent, .org-badge.wynxx { --c: var(--muted); }
/* Origin shown as ICON ONLY (no pill) — follows the icon schema, coloured by --c
   per the origin scheme above. Central: change an origin icon in dashboard.js. */
.org-badge-icon { background: none; border: none; padding: 0; gap: 0; min-width: 0; border-radius: 0; line-height: 0; }
.org-badge-icon svg { width: 15px; height: 15px; display: block; color: var(--c); }

/* Audit page header */
.audit-header { display: flex; align-items: baseline; gap: 14px; margin-bottom: 6px; flex-wrap: wrap; }

/* Filter bar */
.filter-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
.filter-bar label { font-size: var(--text-xs); color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }
.filter-bar select, .filter-bar input { background: var(--bg); border: 1px solid var(--input); border-radius: var(--r-md); color: var(--text); height: 2.25rem; padding: 4px 12px; font-size: var(--text-sm); font-family: inherit; outline: none; }
.filter-bar select { cursor: pointer; }
.filter-bar select:focus, .filter-bar input:focus { border-color: var(--accent); }
/* Audit Events: origin/severity dropdowns live in the header, pushed right so
   they sit directly to the left of the main filter box (which no longer needs
   its own auto margin inside this group). */
.audit-header-filters { margin-left: auto; margin-bottom: 0; }
.audit-header-filters .list-filter { margin-left: 0; }
.qp-search { width: 100%; box-sizing: border-box; }
/* Canonical top-right list filter (page-header): one shared look across every
   list section (positions/candidates/interviews/plans/quests). */
.list-filter { margin-left: auto; background: var(--bg); border: 1px solid var(--input);
  border-radius: var(--r-md); color: var(--text); height: 2.25rem; padding: 4px 12px; font-size: var(--text-sm);
  font-family: inherit; width: 220px; max-width: 40vw; box-sizing: border-box; }
.list-filter:focus { outline: none; border-color: var(--accent); }
/* Clear-✕ for every .list-filter (added at runtime by list-filter.js). The wrapper
   takes the input's right-push so layout is unchanged; the ✕ shows only when the box
   has text and clears the filter on click. */
.list-filter-wrap { position: relative; display: inline-flex; align-items: center; margin-left: auto; max-width: 40vw; }
.audit-header-filters .list-filter-wrap { margin-left: 0; }
.list-filter-wrap > .list-filter { margin-left: 0; max-width: none; }
.list-filter-wrap.has-text > .list-filter { padding-right: 28px; }
.list-filter-clear { position: absolute; right: 5px; top: 50%; transform: translateY(-50%); display: none;
  align-items: center; justify-content: center; width: 20px; height: 20px; padding: 0; border: 0;
  border-radius: var(--r-sm); background: none; color: var(--muted); cursor: pointer;
  font-size: var(--text-lg); line-height: 1; }
.list-filter-clear:hover { color: var(--text); background: var(--surface-2); }
.list-filter-wrap.has-text .list-filter-clear { display: inline-flex; }

/* Flex table rows — shared base for events / usage (the agents lists are `.ag-tbl`
   tables now, so no `.agent-row` variants here). */
.ev-row, .usage-row { display: flex; align-items: center; gap: 8px; padding: 4px 10px; border-bottom: 1px solid var(--border); font-size: var(--text-xs); }
.ev-row:last-child, .usage-row:last-child { border-bottom: none; }
.ev-row.ev-data-row, .usage-row.ur-data { min-height: var(--row-h); }
/* All system tables share ONE row height: var(--row-h) (events / agents / usage / task lists).
   `.ag-tbl` is a plain (non-flex) table, where a `tr` ignores min-height — so its cells carry
   `height` instead, which a table-cell treats AS a minimum. Same 32px result. */
.ev-tbl tbody tr, .usage-row.ur-data { min-height: var(--row-h); }
.ev-tbl thead tr, .usage-row.ur-hdr { min-height: var(--row-h-hdr); }
.ag-tbl tbody td { height: var(--row-h); }
/* The in-table Details button follows the central .btn.btn-sm size (same as every
   other row/list button) — no vertical-padding override. */
/* On the full-height usage page: Totals-by-provider (first) keeps its natural
   height and is always fully visible — capped at 40vh so a long provider list
   scrolls internally rather than overflowing a short viewport (self-contained,
   not reliant on the base .usage-list cap). By-owner (2nd) and Sessions (last)
   split the remaining space EQUALLY (flex:1 1 0 — a zero basis, so row count
   doesn't skew the division) and each scrolls internally, so both stay
   on-screen on short viewports (previously By-owner was fixed-height and
   pushed Sessions below the fold). The sticky
   ur-hdr background comes from the base .usage-row.ur-hdr rule. */
.ev-fullpage #usage-board .usage-section { flex: 0 0 auto; }
.ev-fullpage #usage-board .usage-section:first-child .usage-list { max-height: 40vh; overflow-y: auto; }
.ev-fullpage #usage-board .usage-section:nth-child(2),
.ev-fullpage #usage-board .usage-section:last-child { flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; }
.ev-fullpage #usage-board .usage-section:last-child { margin-bottom: 0; }
.ev-fullpage #usage-board .usage-section:nth-child(2) .usage-list,
.ev-fullpage #usage-board .usage-section:last-child .usage-list { flex: 1 1 auto; min-height: 0; max-height: none; overflow-y: auto; }
.ev-fullpage #usage-board .usage-section:nth-child(2) .usage-row.ur-hdr,
.ev-fullpage #usage-board .usage-section:last-child .usage-row.ur-hdr { position: sticky; top: 0; z-index: 3; }

/* === /audit/usage, range view (#P131) ======================================
   The positional rules ABOVE (:first-child / :nth-child(2) / :last-child) were written for a
   three-section board. The range view has seven, and under those rules children 3-6 fall through
   to `flex: 0 0 auto` with no height constraint — which crushes the chart and turns the page into
   a scroll well. So the range board opts into CLASS-based layout instead of position-based:
   `#usage-board.usage-board-range`. The old rules stay for any board that is not range-scoped. */
#usage-board.usage-board-range { display: flex; flex-direction: column; min-height: 0; }

/* The chart leads: fixed height, full width, before any table. A stacked bar at 7d/1h is 168
   buckets (~5px each at ~900px) — legible as bars, mush as a line, which is why it is bars. */
/* Shorter now that the zero line is placed by the data: the split no longer reserves half the plot
   for whichever side happens to be a hairline, so the same shape reads in less height. */
/* 220px, down from 280: the plot earns its height from the RANGE of the data, not from the number
   of bars, and at 280 it was taking a fifth of the viewport from the breakdown cards and the
   sessions list below it. The JS floor in `renderChart` is 180, so this stays the binding value. */
/* 254px, up from 220: the turns strip grew 46 -> 74px and the plot gained 6px of top padding for
   clipped-bar labels; the card grew by both so the TOKEN plot kept its height. */
#usage-board.usage-board-range .usage-chart { flex: 0 0 254px; min-height: 0; margin-bottom: 20px; }
/* Every breakdown table shares the remaining space and scrolls internally, so no section is
   pushed below the fold by the ones above it. */
#usage-board.usage-board-range .usage-section { flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; }
#usage-board.usage-board-range .usage-section .usage-list { flex: 1 1 auto; min-height: 0; max-height: none; overflow-y: auto; }
#usage-board.usage-board-range .usage-section .usage-row.ur-hdr { position: sticky; top: 0; z-index: 3; }
#usage-board.usage-board-range .usage-section:last-child { margin-bottom: 0; }

/* A section body is a flex pass-through: the table fills it exactly as it did before the wrapper
   existed. (Donuts moved out to their own `.usage-share` row when they replaced the provider and
   model tables, rather than sitting beside a table that is no longer there.) */
.usage-section-body { display: flex; gap: 16px; align-items: stretch; flex: 1 1 auto; min-height: 0; }
.usage-section-body > .usage-list { flex: 1 1 auto; min-width: 0; }
.usage-pie { flex: 0 0 auto; display: flex; flex-direction: column; align-items: center;
             justify-content: flex-start; gap: 6px; }
.usage-pie svg { width: 140px; height: 140px; }
.usage-pie .pie-cap { order: -1; font-weight: 600; color: var(--text); }
/* A 1.5px surface ring on each arc, matching the 2px gap the stacked bars use, so adjacent slices
   read as separate marks instead of one continuous ring. */
.pie-arc { transition: opacity 0.1s; }
.usage-pie:hover .pie-arc { opacity: 0.65; }
.usage-pie .pie-arc:hover { opacity: 1; }
/* Centre text wears TEXT tokens, never a series colour — identity is carried by the arc + legend. */
.pie-c1 { fill: var(--text); font-size: 15px; font-weight: 700; font-family: var(--font-mono); }
.pie-c2 { fill: var(--muted); font-size: 10px; letter-spacing: 0.04em; }
/* The token ring with the smaller TURNS ring stacked under it, one column beside the rate tiles. */
.usage-pie-sm svg { width: 84px; height: 84px; }
/* Turns ring slices, in the turn palette (`--turn-*`, defined with the strip). */
.turn-arc.tk-prompt   { stroke: var(--turn-prompt); }
.turn-arc.tk-response { stroke: var(--turn-response); }
.turn-arc.tk-tool     { stroke: var(--turn-tool); }
/* Turns ring centre. Sized in viewBox units, which the 84px ring scales by 0.6 — 22px renders at
   ~13px and 16px at ~10px, the same visual weight as the token ring's figure and caption. */
.pie-t1 { fill: var(--text); font-size: 22px; font-weight: 700; font-family: var(--font-mono); }
.pie-t2 { fill: var(--muted); font-size: 16px; font-family: var(--font-mono); }
/* The total the mark is sized by, set off from the parts that compose it by WEIGHT and space —
   not a rule. The grid reset strips table lines, and a rule here would read as "everything above
   is one group", which is already what the weight says. The ONE place a rule earns its place is
   `.tip-sec` below, where a named rule marks a change of KIND rather than a subtotal. */
.tip-grid .tip-sum td { padding-top: 5px; font-weight: 700; color: var(--text); }
/* NAMED section break: a rule plus a heading for the group beneath it. The figures under a heading
   are not addends of the total above — `cached` breaks down what the cache did (its `write` is
   already inside the out row; its `read` is counted nowhere, being the same tokens re-read on a
   later turn), and `injected` is an estimate in a different unit. An unlabelled rule said "these
   differ" without saying how. Overrides the `.tip-grid td { border: 0 }` reset on specificity,
   which is why it is scoped through `.tip-grid`. */
.tip-grid .tip-sec td { padding: 7px 0 3px; }
/* The label sits ON the divider — `--- cache ---` — so one line both separates and names, instead
   of a rule plus a heading row. The span (not the cell) is the flex container: making a `td`
   display:flex takes it out of table layout, and the numbers in the rows around it are aligned by
   that layout. The rules are pseudo-elements that simply take the leftover space either side. */
.tip-grid .tip-sec span { display: flex; align-items: center; gap: 6px; color: var(--muted);
                          font-family: var(--font-sans); font-size: var(--text-2xs);
                          letter-spacing: 0.04em; }
.tip-grid .tip-sec span::before, .tip-grid .tip-sec span::after {
  content: ""; flex: 1; border-top: 1px solid var(--border); }
.pie-cap { color: var(--muted); font-size: var(--text-xs); text-align: center; }

/* Usage: "refreshing" affordance. The chip sits on the subtitle line, NOT over the board: the
   board is replaced wholesale on render, so an overlay inside it would be destroyed by the very
   update it is reporting. Dimming `.is-busy` keeps the previous numbers readable while the new
   ones load, rather than blanking to a spinner and losing the reader's place. Only a
   reader-initiated load sets it — the 1h/24h live poll refreshes silently. */
.usage-subtitle-row { display: flex; align-items: center; gap: 10px; }
.usage-busy { display: inline-flex; align-items: center; gap: 6px; color: var(--muted);
              font-size: var(--text-xs); }
.usage-busy[hidden] { display: none; }
#usage-board.is-busy { opacity: 0.55; transition: opacity 0.12s ease; }

/* Estimated-tokens marker. Was an inline `style="color:var(--accent2,#ca8a04);…"` on the emitted
   span — a raw hex literal inside an inline style block, which the CI guard forbids and which no
   theme could override. */
.uc-est {
  color: var(--accent2);
  cursor: help;
  display: inline-flex;
  align-items: center;
  vertical-align: middle;
  line-height: 0;
}

/* Data-quality caveat (estimated tokens; any future approximate-data note). Deliberately NOT
   `.empty-state`, which means "there is no data" — this says "there IS data, read it with care",
   and conflating the two would teach people to ignore both. */
.data-caveat {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 8px 10px;
  margin-bottom: 12px;
  background: var(--panel);
  border-left: 3px solid var(--accent2);
  border-radius: var(--r-sm, 4px);
  font-size: var(--text-sm);
  color: var(--muted);
}
.data-caveat strong { color: var(--text); font-weight: 500; }

/* By-user breakdown column, alongside the existing .uc-* group. */
.uc-user { width: 200px; flex: 0 0 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- Chart ---------------------------------------------------------------- */
.usage-chart { display: flex; flex-direction: column; }
.usage-chart-plot { flex: 1 1 auto; min-height: 0; position: relative; }
.usage-chart-plot svg { width: 100%; height: 100%; display: block; }

/* Recessive axes/grid: the data is the figure, the scaffolding is ground. */
.usage-chart-plot .axis line, .usage-chart-plot .axis path { stroke: var(--border); }
/* DOTTED, so the value gridlines read as a measuring aid rather than as marks in their own right
   — and so the one SOLID horizontal rule on the plot is unambiguously the zero axis.
   Drawn in `--muted`, not `--border`: dashing a line removes over half its ink, and #e5e5e5 at
   half opacity broken into 1px dots came out invisible. A lower opacity on a darker token keeps
   the same recessive weight while leaving something actually on screen. */
.usage-chart-plot .grid line { stroke: var(--muted); stroke-opacity: 0.18; stroke-dasharray: 2 4; }
.usage-chart-plot .axis text { fill: var(--muted); font-size: var(--text-xs); }

/* Legend — always present for >=2 series, so identity is never carried by colour alone. */
.usage-chart-legend { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 8px; font-size: var(--text-sm); }
.usage-chart-legend .lg { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); }
/* The swatch carries the identity; the label stays in text ink rather than the series colour. */
.usage-chart-legend .sw { width: 10px; height: 10px; border-radius: 2px; flex: 0 0 auto; }

/* Hover tooltip. An HTML/SVG chart is interactive by default — a bar the reader cannot
   interrogate is a picture of data, not a view of it. */
.usage-chart-tip {
  position: absolute; pointer-events: none; z-index: 5;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-sm, 4px);
  padding: 6px 8px; font-size: var(--text-xs); color: var(--text);
  box-shadow: 0 2px 8px rgba(0,0,0,0.15); white-space: nowrap;
}
.usage-chart-tip .tip-b { color: var(--muted); display: flex; align-items: center; gap: 6px;
                          margin-bottom: 4px; }
.usage-chart-tip[hidden] { display: none; }
/* Tooltips are the only place series identity is spelled out now that the legend strip is gone,
   so they read as a GRID: names in one column, numbers aligned in the next. Run-on lines
   ("user in 300.0k · out 70.00k · cache 12.00M") made the reader parse prose to compare two rows. */
/* The global prose-table rules (`table { width:100%; margin }`, `th, td { border: 1px solid }`)
   apply to every <table> on the site, so a tooltip grid inherits a full 1px cell grid and a
   14px block margin unless it opts out — the same reset `.tbl` makes above. */
/* `line-height: 1.3`, against the 1.5 inherited from `body`: this is a dense numeric readout, not
   prose, and at --text-xs the inherited leading put ~20px between consecutive figures — the rows
   read as separate statements rather than as a column to be scanned and added. The section breaks
   and the total keep their own padding, so tightening here compresses the FIGURES without
   flattening the groups apart from each other. */
.tip-grid { border-collapse: collapse; font-size: var(--text-xs); width: auto; margin: 0;
            line-height: 1.3; }
.tip-grid th, .tip-grid td { border: 0; }
.tip-grid th { color: var(--muted); font-weight: 500; text-align: right; padding: 0 0 2px 10px;
               background: none; }
.tip-grid td { text-align: right; padding: 0 0 0 10px; font-family: var(--font-mono); }
.tip-grid .tip-k { text-align: left; padding-left: 0; font-family: var(--font-sans);
                   white-space: nowrap; }
.tip-grid .tip-dim { color: var(--muted); }
.tip-grid-2 .tip-k { padding-right: 10px; }
.tip-sw { width: 9px; height: 9px; border-radius: 2px; display: inline-block; flex: 0 0 auto;
          margin-right: 5px; vertical-align: -1px; }
.tip-empty { color: var(--muted); font-size: var(--text-xs); }

/* --- Breakdown bars --------------------------------------------------------- */
/* Providers / models / users as ranked lengths. These carry the identity the chart legend used to
   (same names, same fixed colours), which is what keeps colour from being the only channel. */
/* The row reuses the Events page's `.pipeline` / `.pipeline-card` idiom rather than defining a
   second card look. `.hbar-card` only overrides what a stat card assumes: a stat card is a 2-column
   grid built around one big number, while these hold a list. */
.hbar-row-wrap { flex: 0 0 auto; margin-bottom: 20px; }
.hbar-card { flex: 1 1 260px; min-width: 0; display: flex; flex-direction: column; gap: 8px;
             grid-template-columns: none; }
.hbar-card > .card-label { margin-bottom: 0; }
/* NO gap between rows: the 0% and 100% rules on each track stretch the full row height, so rows
   that touch turn them into two continuous verticals down the card — an axis the bars are read
   against. With a gap they were 10px dashes with 4px holes, which reads as decoration rather than
   as a scale. Row rhythm now comes from the row's own height instead of the list's gap. */
/* Columns are shared by every row of a card (a grid + `subgrid`): the NAME column is as wide as the
   card's longest name — capped at 45%, then ellipsised — so the bars start right after the names
   instead of after a fixed 34% that left a gap on short names, and every bar in the card still
   starts and ends on the same two x positions (the scale the end rules draw). */
.hbar-list { display: grid; grid-template-columns: fit-content(45%) auto minmax(0, 1fr) auto;
             column-gap: 8px; row-gap: 0; }
.hbar-row { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; align-items: center;
            font-size: var(--text-xs); min-height: 18px; }
.hbar-row > :only-child { grid-column: 1 / -1; }          /* "Show less" */
.hbar-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
             color: var(--text); }
/* End rules at 0% and 100%, spanning the FULL row height (`align-self: stretch`) so that with rows
   touching they join into two continuous verticals running the length of the card. That is what
   makes the bars comparable: every one starts and ends on the same two x positions, so a reader
   judges each against the scale rather than against whichever bar happens to be above it.
   The 10px groove that used to be the element's own background is now a centred gradient band,
   because the element itself has to be row-tall to carry the rules. `box-sizing: border-box` is
   global, so the 1px borders come out of the track's width and the fill's percentage is unchanged. */
.hbar-track { flex: 1 1 auto; min-width: 0; align-self: stretch; display: flex; align-items: center;
              /* `--muted` mixed down, not `--border`: as a 1px vertical, #e5e5e5 on white (and 10%
                 white on black) reads as almost nothing. Mixing toward transparent keeps the rule
                 recessive against the bars while leaving it actually visible in both themes. */
              border-left: 1px solid color-mix(in srgb, var(--muted) 45%, transparent);
              border-right: 1px solid color-mix(in srgb, var(--muted) 45%, transparent);
              background: linear-gradient(var(--surface-3), var(--surface-3)) 0 50% / 100% 10px
                          no-repeat; }
.hbar-fill { display: block; height: 10px; border-radius: 2px; }
/* Clickable card rows (card filters). A hover wash says "this does something"; the SELECTED row is
   marked by a left accent rule and weight and is the ONLY row that keeps its series colour. The other
   rows go GRAY — not a faded tint of their own colour, which read as "a lighter shade of the same
   thing" rather than "not selected" — but stay clickable, and hover previews their colour back, so
   switching to a neighbour is one click. `!important` because a bar carries its colour inline. */
.hbar-click { cursor: pointer; border-radius: var(--r-sm, 4px); padding: 0 4px; margin: 0 -4px; }
.hbar-click:hover, .hbar-click:focus-visible { background: var(--surface-2); outline: none; }
/* The selected row's left rule wears the member's OWN series colour (`--sel-c`, set inline by the
   card), so it ties to the same colour on the chart and ring — the portal accent said nothing about
   which series it was. Its bar is a touch taller, so the selection reads at a glance. */
.hbar-click.is-sel { box-shadow: inset 4px 0 0 var(--sel-c, var(--accent)); background: var(--surface-2);
                     padding-left: 10px; }
.hbar-click.is-sel .hbar-fill { height: 14px; }
.hbar-click.is-sel .hbar-name { font-weight: 600; }
.hbar-click.is-dim { color: var(--muted); }
.hbar-click.is-dim .hbar-name { color: inherit; }
.hbar-card:not(.hbar-brand) .hbar-click.is-dim:not(:hover):not(:focus-visible) .hbar-fill {
  background: color-mix(in srgb, var(--muted) 55%, transparent) !important; }
/* The Providers card (`.hbar-brand`) never goes gray: a provider is recognised by its brand colour
   across the portal, so an unselected provider keeps it, faded. */
.hbar-brand .hbar-click.is-dim:not(:hover):not(:focus-visible) .hbar-fill { opacity: 0.45; }
/* "Other (N more)" / "Show less" — the fold row. STRIPED neutral fill: it is a bucket, not a member,
   and the stripes keep it from reading as one more grayed-out (unselected) member. */
.hbar-other { cursor: pointer; color: var(--muted); border-radius: var(--r-sm, 4px);
              padding: 0 4px; margin: 0 -4px; }
.hbar-other:hover, .hbar-other:focus-visible { background: var(--surface-2); color: var(--text);
                                               outline: none; }
.hbar-other .hbar-name { color: inherit; font-style: italic; }
.hbar-fill-other { background: repeating-linear-gradient(135deg,
                     color-mix(in srgb, var(--muted) 45%, transparent) 0 3px, transparent 3px 6px); }

/* --- Usage: subscription chips in the Providers card (#P28) --------------------
   On their own line (`.sub-line`) right under each enabled provider's spend bar, INSIDE the bar's
   column (grid column 3), so the meters start and end exactly where the spend bar does: one
   segmented meter per window length — `5h ▰▰▰▱▱ 51%` — the chips sharing that width evenly and the
   segments stretching to fill it. Filled segments wear the provider's colour (set inline); the
   THRESHOLD lives in the % text — warning from 70%, destructive (+ ⚠) from 90% — never in colour
   alone. Not a filter: the line has no click affordance. Tokens only. */
.sub-line { cursor: default; min-height: 16px; padding-bottom: 4px; }
/* When the most-used window resets, under the token count (same column, same left edge). */
.sub-reset { color: var(--muted); }
.sub-chips { grid-column: 3 / 4; display: flex; align-items: center; gap: 10px; min-width: 0;
             white-space: nowrap; }
.sub-chip { flex: 1 1 0; min-width: 0; display: flex; align-items: center; gap: 4px; }
.sub-len { color: var(--muted); font-family: var(--font-mono); }
.sub-segs { flex: 1 1 auto; min-width: 20px; display: flex; gap: 1px; }
.sub-segs > i { flex: 1 1 0; display: block; height: 8px; border-radius: 1px; background: var(--surface-3); }
.sub-pct { font-family: var(--font-mono); color: var(--text); min-width: 3ch; text-align: right; }
.sub-pct.is-warn { color: var(--warning-text); font-weight: 600; }
.sub-pct.is-crit { color: var(--destructive-text); font-weight: 700; }
.sub-state { color: var(--muted); font-style: italic; }

/* Card-filter chips in the header, beside the filter box: `dim: value ×`, plus "Clear all". */
.usage-chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 6px 0 2px; }
.usage-chips[hidden] { display: none; }
.usage-chip { display: inline-flex; align-items: center; gap: 2px; font-size: var(--text-xs);
              padding: 2px 4px 2px 8px; border: 1px solid var(--border); border-radius: 999px;
              background: var(--surface-2); color: var(--text); white-space: nowrap; }
.usage-chip-k { color: var(--muted); }
.usage-chip-x { border: 0; background: none; color: var(--muted); cursor: pointer; font-size: 14px;
                line-height: 1; padding: 0 4px; border-radius: 999px; }
.usage-chip-x:hover { color: var(--text); background: var(--surface-3); }
.usage-chip-all { border: 0; background: none; color: var(--muted); cursor: pointer;
                  font-size: var(--text-xs); text-decoration: underline; padding: 0 2px; }
.usage-chip-all:hover { color: var(--text); }
/* LEFT-aligned, no min-width: the shared grid column is already as wide as the card's widest value,
   so the number sits right after the bar's end instead of across a gap from it. */
.hbar-val { font-family: var(--font-mono); color: var(--muted); white-space: nowrap; }
/* Share of the card's total, left of its bar. Fixed width + right-aligned mono so every card's
   percentages line up in one column (`100%` is the widest it gets). */
.hbar-pct { flex: 0 0 auto; width: 34px; text-align: right; font-family: var(--font-mono);
            color: var(--muted); }

/* Bidirectional chart: input above the zero line, output below, ONE shared scale mirrored (never
   two y-scales). The zero rule is the reference the reader compares both halves against, so it is
   the one piece of scaffolding allowed to be as strong as a border. */
/* The zero line is the chart's ANCHOR — it is what makes the plot readable as bidirectional
   (input above, output below), and it moves with the data rather than sitting at the midpoint, so
   it has to be findable. It was `--border` at full opacity: the same colour as the gridlines and
   only twice their contrast, which on the light theme is #e5e5e5 against #e5e5e5-at-half. Stepped
   up to the same ink the axis labels use, plus a hair more width. `--muted` resolves per theme
   (#737373 light / #a1a1a1 dark), so this reads on both without a dark-sheet override. */
   Width 2 on a whole-pixel `zeroY` (rounded in `renderChart`) so the rule lands exactly on the
   device pixels either side of the axis — 1px into each direction, symmetric. An odd width, or a
   fractional y, antialiases across three pixels and reads as a misaligned smudge rather than a
   line. Both bar directions are drawn flush to `zeroY`, so the overlap is even top and bottom. */
/* The token plot's zero. Identical to `.turns-base` below — each plot's own zero is the same KIND
   of mark, so both wear the same token, opacity and width and read as one convention repeated.
   What separates either from the value gridlines is being SOLID against their dots, not weight.
   The emitted element also carries these values inline (so the shared `.grid line` rule cannot
   dash it); keep the two in step. */
.usage-chart-plot .grid line.zero { stroke: var(--muted); stroke-opacity: 0.45; stroke-width: 1; stroke-dasharray: none; }
/* A day rollover keeps the SAME weight as every other interval tick — the dated label beneath it
   is what marks it, and that is enough. Drawing it darker made one vertical (usually the leftmost,
   since a range so often opens on a day boundary) look like a different kind of line for no reason
   the reader could see, which read as a rendering fault rather than as information. */
.usage-chart-plot .grid line.tick-day { stroke-dasharray: 2 3; }
.usage-chart-plot .axis text.ax-date { font-size: var(--text-3xs); }
/* The turns line, rising from the zero axis over the token bars. DOTTED and in plain ink, so it
   reads as an annotation across the plot rather than as another data series competing with the
   bars — which matters here because it carries its own scale: turns are a count, the bars are
   tokens, and where the line crosses a bar means nothing.
   `--text` is the ink, which resolves to white on the dark theme and near-black on the light one.
   A literal `white` would be invisible on the light theme's panel. */
/* The TURNS STRIP — its own plot under the token one, sharing the time axis. Bars in `--muted`,
   not a series colour: this strip answers "how much work", while the colours above encode WHO,
   and reusing one would imply a relationship that is not there. Recessive on purpose — the token
   plot is the figure, this is context beneath it. */
/* THE TURN PALETTE — one grey per KIND, shared by the strip (fill) and the turns ring (stroke)
   so a kind is the same shade everywhere. Three shades of the neutral ink, never a series colour:
     prompt   — the darkest: what YOU sent
     response — mid: what the model answered
     tool     — lightest: tool steps the model took and their results sent back (automatic
                back-and-forth within a turn, so it recedes behind the two solid kinds)
   Mixed from `--text` / `--muted`, so both themes get the right direction of contrast. */
#usage-board {
  --turn-prompt:   color-mix(in srgb, var(--text) 60%, var(--muted));
  --turn-response: color-mix(in srgb, var(--muted) 80%, transparent);
  --turn-tool:     color-mix(in srgb, var(--muted) 35%, transparent);
}
.usage-chart-plot .turns-bar { fill: var(--turn-response); }
.usage-chart-plot .turns-bar.turns-prompt { fill: var(--turn-prompt); }
.usage-chart-plot .turns-bar.turns-tool { fill: var(--turn-tool); }
/* The strip's OWN zero. Visibly its own baseline, so the strip reads as a separate plot with a
   separate scale rather than as more marks belonging to the chart above. */
.usage-chart-plot .grid line.turns-base { stroke: var(--muted); stroke-opacity: 0.45;
                                          stroke-width: 1; stroke-dasharray: none; }
/* The y scale is reference, not content — same weight as the in/out markers rather than competing
   with the x axis, which carries the reading the chart is actually about. */
.usage-chart-plot .axis-y text { font-size: var(--text-3xs); }

.usage-chart-plot .axis-side text { fill: var(--muted); font-size: var(--text-3xs);
                                    letter-spacing: 0.06em; text-transform: uppercase; }
/* The real value of a bar the outlier cap clipped, just beyond the plot edge it was cut at. In
   `--text`, not `--muted`: it is data (the scale no longer reaches this bar), not axis reference. */
.usage-chart-plot .bar-clip text { fill: var(--text); font-size: var(--text-3xs); font-weight: 600; }
.usage-chart-legend .lg-note { color: var(--muted); font-size: var(--text-xs); opacity: 0.85; }

/* --- Share donuts ---------------------------------------------------------- */
/* These REPLACED the By-provider and By-model tables: provider and model are share questions, and
   a two-row table is a verbose way to answer one. The token counts the tables carried live on the
   slice hover, bound once per board, which is why the board is the positioning context.
   They sit in the CHART's row, not a band below it — "how much over time" and "of what" read as
   one glance, and a row of their own spent a screen of height on two circles. */
.usage-chart-row { display: flex; gap: 12px; align-items: stretch; flex: 1 1 auto; min-height: 0; }
.usage-chart-row > .usage-chart-host { flex: 1 1 auto; min-width: 0; display: flex;
                                       flex-direction: column; }
/* An empty-state or loading message in the chart box fills it and sits in its CENTRE — not as a
   line in the top-left corner of a 254px card. (With no data the share rail is omitted too, so
   the message owns the whole box.) */
.usage-chart-host > .empty-state, .usage-chart-host > .jm-loading {
  flex: 1 1 auto; display: flex; align-items: center; justify-content: center; gap: 8px;
  margin: 0; text-align: center; }
/* The board's first-load spinner, before any chart exists: centred in the board, not pinned to
   its top edge. */
#usage-board > .jm-loading:only-child { flex: 1 1 auto; min-height: 254px; }
/* The ring, with the rate tiles to its right — "how much, at what pace" in one glance.
   Sized to its CONTENT (`0 0 auto`), not to a fixed 280px: the fixed width left whatever the ring
   and tiles did not use as dead space between the plot and the ring, and gave the plot less room
   than it could have had. */
/* TWO rows — the token ring with its rate tiles, the turns ring with its per-kind counts — each
   ring with its breakdown beside it, vertically centred on that ring. The ring COLUMN has one fixed
   width (the token ring's), so the smaller turns ring centres under it and both tile columns start
   at the same x. */
.usage-share-rail { flex: 0 0 auto; display: flex; flex-direction: column; gap: 4px;
                    justify-content: center; overflow: hidden; }
.usage-share-row { display: flex; align-items: center; gap: 12px; }
.usage-share-row > .usage-pie { width: 140px; }
.avg-rail { display: flex; flex-direction: column; gap: 8px; flex: 0 0 auto; }
.avg-tile { display: flex; flex-direction: column; line-height: 1.15; }
.avg-v { font-family: var(--font-mono); font-size: var(--text-sm); font-weight: 700;
         color: var(--text); }
.avg-k { font-size: var(--text-3xs); color: var(--muted); letter-spacing: 0.04em; }
/* Narrower viewports COMPACT the rail instead of wrapping it: the ring and tiles stay beside the
   plot (the plot re-renders to its host's width on resize), the ring shrinks — its SVG scales by
   viewBox, centre text included — and the gaps tighten. Only a phone-width screen, where the plot
   would be squeezed below readability, still puts the rail under it. */
@media (max-width: 1100px) {
  .usage-chart-row { gap: 8px; }
  .usage-share-row { gap: 8px; }
  .usage-share-rail .usage-pie svg { width: 110px; height: 110px; }
  .usage-share-rail .usage-pie-sm svg { width: 66px; height: 66px; }
  .usage-share-row > .usage-pie { width: 110px; }
}
@media (max-width: 760px) {
  .usage-share-rail .usage-pie svg { width: 88px; height: 88px; }
  .usage-share-rail .usage-pie-sm svg { width: 54px; height: 54px; }
  .usage-share-row > .usage-pie { width: 88px; }
  .usage-share-rail .avg-rail { gap: 4px; }
}
@media (max-width: 480px) {
  .usage-chart-row { flex-direction: column; }
  .usage-share-rail { width: 100%; }
}
.usage-share { flex: 0 0 auto; }
.pie-legend { display: flex; flex-wrap: wrap; gap: 8px 12px; justify-content: center;
              font-size: var(--text-xs); max-width: 220px; }
.pie-legend .lg { display: inline-flex; align-items: center; gap: 5px; color: var(--muted); }
.pie-legend .sw { width: 9px; height: 9px; border-radius: 2px; flex: 0 0 auto; }
.jm-hint { color: var(--muted); font-weight: 400; font-size: var(--text-xs); }
#usage-board { position: relative; }

/* Custom range as an anchored POPUP, in the `.hdr-init-pop` idiom (fixed, JS-placed, shadowed).
   Inline in the header it added four controls to the filter row, which then wrapped or overflowed
   at exactly the moment the reader wanted to use them; `position: fixed` takes it out of flow, so
   opening it cannot reflow the bar. Fixed rather than absolute because the header sits inside a
   scrolling flex column — an absolute child would be clipped by the first ancestor with
   `overflow` and drift on scroll. */
.usage-range-pop {
  position: fixed; z-index: 1200; display: flex; flex-direction: column; gap: 8px;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-md);
  box-shadow: var(--shadow-pop); padding: 12px 14px; max-width: min(460px, 92vw);
}
/* `display:flex` above would defeat the `hidden` attribute — restore hide when hidden (JS toggles
   it), the same guard `.hdr-init-pop` needs for the same reason. */
.usage-range-pop[hidden] { display: none; }
/* One field per line, label above control. Side by side, two datetime-locals make the widest row
   on the page, and the `to` between them was doing the labelling for both. */
.usage-range-field { display: flex; flex-direction: column; gap: 4px; }
/* The label, in the .filter-bar label idiom — this popup came out of that bar and should still
   read as part of it. */
.usage-range-field > span {
  font-size: var(--text-xs); color: var(--muted); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.04em;
}
/* The controls, in the .filter-bar input idiom. Spelled out here rather than inherited: these
   inputs carried `.jm-input`, which matches NO rule anywhere in the portal — inside the bar they
   were styled by the element selector `.filter-bar input`, so moving them into a popup silently
   stripped every one of these properties and left raw browser controls. */
.usage-range-pop input {
  width: 100%; box-sizing: border-box;
  background: var(--bg); border: 1px solid var(--input); border-radius: var(--r-md);
  color: var(--text); height: 2.25rem; padding: 4px 12px;
  font-size: var(--text-sm); font-family: inherit; outline: none;
}
.usage-range-pop input:focus { border-color: var(--accent); }
/* The action sits on its own line, right-aligned — a popup is a small form, and Apply reads as
   the thing that closes it rather than a fourth control in a row of inputs. */
.usage-range-actions { display: flex; justify-content: flex-end; }
/* Shown only while the range is inverted; Apply is disabled at the same moment, so the line says
   why rather than leaving a dead button unexplained. */
.usage-range-warn { margin: 0; color: var(--red); font-size: var(--text-xs); }
.usage-range-pop .btn[disabled] { opacity: 0.5; cursor: not-allowed; }

/* A filtered-out row. `.usage-row` sets `display:flex`, and an author rule beats the UA's
   `[hidden]{display:none}` whatever its specificity. Kept although the page no longer hides rows
   itself: any future `hidden` on one of these would otherwise silently do nothing. */
.usage-row[hidden] { display: none; }

.ev-row.ev-data-row:hover, .usage-row.ur-data:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); transition: background 0.1s; }
/* Severity row tint — non-info audit events get a subtle background wash (info
   stays neutral). Colours map to the canonical status tokens: debug→muted,
   warn→amber (accent2), error→red. The .ev-data-row hover variants are scoped
   one class deeper so they keep (a stronger) tint on hover instead of the
   neutral surface-2 default. */
.ev-row.ev-sev-debug { background: rgba(var(--muted-rgb),0.06); }
.ev-row.ev-sev-warn  { background: rgba(var(--accent2-rgb),0.08); }
.ev-row.ev-sev-error { background: rgba(var(--red-rgb),0.09); }
.ev-row.ev-data-row.ev-sev-debug:hover { background: rgba(var(--muted-rgb),0.12); }
.ev-row.ev-data-row.ev-sev-warn:hover  { background: rgba(var(--accent2-rgb),0.14); }
.ev-row.ev-data-row.ev-sev-error:hover { background: rgba(var(--red-rgb),0.15); }
.ev-row.ev-header-row, .usage-row.ur-hdr { min-height: var(--row-h-hdr); background: var(--secondary); border-radius: var(--r-lg) var(--r-lg) 0 0; }
.ev-header-row .ev-hdr, .ur-hdr .uc-hdr {
  /* Higher specificity than the .ev-col-* data rules so headers don't inherit
     their size/colour/mono. Live .tbl th: .7rem / 600 / uppercase / muted. */
  font-size: var(--text-2xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--muted); font-family: var(--font-sans); }

/* Event columns */
.ev-col-age  { width: 36px; flex-shrink: 0; text-align: right; font-size: var(--text-xs); color: var(--muted); white-space: nowrap; }
.ev-col-org  { width: 60px; flex-shrink: 0; display: flex; align-items: center; justify-content: flex-end; overflow: hidden; }
.ev-col-prov { width: 62px; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--text-xs); color: var(--muted); text-align: center; }
.ev-col-evt  { width: 150px; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--text-xs); font-family: var(--font-mono); color: var(--muted); text-align: center; }
.ev-col-data { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
.ev-col-btn  { flex-shrink: 0; width: 74px; display: flex; justify-content: flex-end; }

/* (Removed bespoke .btn-details — detail/row buttons now use the central
   .btn.btn-sm.btn-primary, like every other button.) */

/* Modern on/off switch — shared portal control (documents catalog + edit popup,
   planner/quests instruction list). Off = muted knob on a faint track; on = the system
   accent colour. Markup: <label class="ui-switch"><input type=checkbox><span
   class="ui-switch-slider"></span></label>. */
.ui-switch { position: relative; display: inline-block; width: 34px; height: 18px; flex-shrink: 0; }
.ui-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.ui-switch-slider { position: absolute; inset: 0; cursor: pointer; background: var(--surface-3); border: 1px solid var(--border); border-radius: 999px; transition: background 0.15s, border-color 0.15s; }
.ui-switch-slider::before { content: ''; position: absolute; height: 12px; width: 12px; left: 2px; top: 2px; background: var(--muted); border-radius: 50%; transition: transform 0.15s, background 0.15s; }
.ui-switch input:checked + .ui-switch-slider { background: var(--primary); border-color: var(--primary); }
.ui-switch input:checked + .ui-switch-slider::before { transform: translateX(16px); background: var(--on-accent, #fff); }
/* Colour variants — SAME style as the default (solid coloured track + plain white knob), each
   keeping its own enabled colour. */
/* Amber variant — on but in a "warn" state (e.g. a document enabled but not tracked). */
.ui-switch.sw-warn input:checked + .ui-switch-slider { background: var(--accent2); border-color: var(--accent2); }
/* Blue "global scope" variant (the instruction popup's Global toggle). */
.ui-switch.sw-global input:checked + .ui-switch-slider { background: var(--accent); border-color: var(--accent); }
/* Yellow variant — diagnostic / debug mode (e.g. guardian debug toggle). */
.ui-switch.sw-yellow input:checked + .ui-switch-slider { background: var(--accent2); border-color: var(--accent2); }
/* Red variant — "on" carries extra risk (e.g. the beta-versions update opt-in). */
.ui-switch.sw-red input:checked + .ui-switch-slider { background: var(--red); border-color: var(--red); }
/* Orange variant — a dependency ability (other enabled abilities require it), so turning it off
   cascades. Uses the system WARNING colour (--accent2 = design-tokens --warning), not the
   off-palette --sw-orange. */
.ui-switch.sw-orange input:checked + .ui-switch-slider { background: var(--accent2); border-color: var(--accent2); }
/* Locked switch — a required SYSTEM ability (the mandatory `providers`, which can never be toggled
   off) or an unavailable/not-ready provider. Rendered UNMISTAKABLY greyed + non-interactive in BOTH
   on/off states so it never reads as a live toggle. Declared AFTER the base/variant `input:checked`
   colours (and with the extra `:disabled` specificity) so it overrides the blue/orange "on" fill and
   is never left looking active. */
.ui-switch input:disabled + .ui-switch-slider,
.ui-switch input:disabled:checked + .ui-switch-slider { cursor: not-allowed; opacity: 0.5;
  background: var(--surface-3); border-color: var(--border); }
.ui-switch input:disabled + .ui-switch-slider::before,
.ui-switch input:disabled:checked + .ui-switch-slider::before { background: var(--muted); }

/* Config-page id box — the leading key/name box in an ability Configuration list
   row (planner/quests instructions). Shared so the box shape and
   the enabled/disabled tint live in ONE place; each config page scopes only its own
   width (e.g. #pq-cfg-root .cfg-name-box { width:150px }). The base carries no colour — a state modifier always applies.
   The blue "global scope" override (.pq-global) stays planner/quests-scoped. */
.cfg-name-box { display: inline-block; flex: 0 0 auto; box-sizing: border-box; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: center; }
.cfg-name-box.cfg-enabled  { --c: var(--accent); }
.cfg-name-box.cfg-disabled { --c: var(--muted); }
/* Canonical config-row NAME text (plain, non-pill) — e.g. the ability name on the Abilities config
   list. Distinct from `.cfg-name-box` (the tinted knob-key pill); column width lives on the wrapper. */
.cfg-name-txt { font-size: var(--text-sm); font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Config-page text/combo inputs — shared base so every config page gets the same
   input shape without duplicating the rules; each page scopes only size overrides
   (e.g. #mem-cfg-root .cfg-ctrl-text { width: 90px; … }). */
.cfg-ctrl-text,
.cfg-ctrl-combo {
  background: var(--bg); border: 1px solid var(--input); border-radius: var(--r-md);
  color: var(--text); height: 2.25rem; padding: 4px 12px; font-size: var(--text-sm); font-family: inherit;
  box-sizing: border-box; outline: none; transition: border-color 0.12s;
}
.cfg-ctrl-text:focus,
.cfg-ctrl-combo:focus { border-color: var(--accent); }
/* Text boxes INSIDE a table row (.task-row) or card (.lp-card) shrink to the small
   in-row control size — matching the .btn.btn-sm height (~30px) so inputs and row
   buttons line up. Inputs in page headers / modals keep the full h-9 (36px) size.
   Checkboxes/radios (the .ui-switch toggle) are excluded. */
.task-row .cfg-ctrl-text, .task-row .cfg-ctrl-combo,
.task-row input:not([type="checkbox"]):not([type="radio"]), .task-row select,
.lp-card .cfg-ctrl-text, .lp-card .cfg-ctrl-combo,
.lp-card input:not([type="checkbox"]):not([type="radio"]), .lp-card select {
  height: 1.9rem; padding: 0.35rem 0.7rem; font-size: var(--text-xs);
}
/* A dropdown inside a `.cfg-card` is tighter still (26px) so it matches the card's compact row
   controls. Canonical: every config-card page (memory + the shared ideas/plans privacy card) gets
   the same height here — declared AFTER the `.task-row` rule above so it wins on equal specificity. */
.cfg-card .cfg-ctrl-combo { height: 26px; padding: 0 6px; }

/* Agents page — the Active + last-24h lists, both in the audit-events TABLE style
   (`.tbl` + the compact 4px/10px cells). Its OWN class rather than `.ev-tbl` because the
   column SET differs (8 columns vs 6) and `.ev-tbl` pins widths per position; sharing it
   would stretch the wrong cells.
   HEADERLESS like the events feed, and built the SAME way: a flex column whose rows are
   `display:table` so only the list body scrolls. Widths therefore live per column
   POSITION (a table-level colgroup is inert once rows are their own tables), and the
   shared columns take the events numbers verbatim — owner 46, provider 30 with trimmed
   4px padding (both are ICONS: `.org-badge-icon` + the icon-only `.prov-badge`), mono id
   150, flexible text, age 58, Details 92 — at the same `4px 10px` cell padding and the
   same `--row-h` row height, so the two pages read as one system. */
.ag-tbl { display: flex; flex-direction: column; min-height: 0; font-size: var(--text-xs); }
.ag-tbl tbody { display: block; flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; }
.ag-tbl tbody tr { display: table; width: 100%; table-layout: fixed; }
.ag-tbl td { padding: 4px 10px; }
/* Widths by CELL CLASS, not `:nth-child` position — the same fix as `.ev-tbl` above: a position
   rule silently re-assigns every width after any inserted column (the conditional committer cell),
   handing the age the owner's width and leaving the Details button unsized. `.ag-c-task` is
   deliberately unsized: it is the flexing column that absorbs the remainder. */
.ag-tbl tr > .ag-c-org { width: 46px; }
.ag-tbl tr > .ag-c-prov { width: 30px; padding-left: 4px; padding-right: 4px; }  /* provider icon — trim padding so the mark keeps full size */
.ag-tbl tr > .ag-c-from { width: 96px; }    /* owner: what started the run — `user` or the ability */
.ag-tbl tr > .ag-c-agent { width: 220px; }   /* one name: identity + role when it adds (`visual-designer · advise`) */
.ag-tbl tr > .ag-c-elapsed { width: 140px; }
.ag-tbl tr > .ag-c-owner { width: 104px; }   /* committer name; only under a manager's All-users view */
.ag-tbl tr > .ag-c-age { width: 58px; }
.ag-tbl tr > .ag-c-btn { width: 92px; }
/* Badge + identity columns centre; only the flexible task text reads left. Both text columns
   truncate. */
.ag-tbl td { text-align: center; }
.ag-tbl td.ag-c-task { text-align: left; }
.ag-tbl td.ag-c-from, .ag-tbl td.ag-c-agent, .ag-tbl td.ag-c-task {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ag-tbl td.ag-c-from  { color: var(--text); font-weight: 600; }
.ag-tbl td.ag-c-agent { font-family: var(--font-mono); color: var(--accent); }
.ag-tbl td.ag-c-task  { color: var(--muted); }
.ag-tbl td.ag-c-age   { color: var(--muted); white-space: nowrap; }
.ag-tbl td .org-badge, .ag-tbl td .prov-badge, .ag-tbl td .tr-badge { vertical-align: middle; }
/* The ghost `Elapsed: …` box needs `display:inline-block` in a table cell (a flex row
   blockifies it for free), and a FIXED width so every box in the column is the same size
   whatever its value — the `.verdict-pill` / `.tr-badge.type` convention. */
.ag-tbl td .tr-badge { display: inline-block; }
.ag-tbl td.ag-c-elapsed .tr-badge { width: 120px; text-align: center; box-sizing: border-box;
  overflow: hidden; }
/* Stalled worker (open agent-start past the staleness cutoff — almost certainly hard-killed):
   the row recedes and its age reads red. A failed run tints red like an error event row. */
.ag-tbl tr.ag-stalled { opacity: 0.72; }
.ag-tbl tr.ag-stalled td.ag-c-age { color: var(--red); font-weight: 600; }
.ag-tbl tr.ag-err td { background: rgba(var(--red-rgb),0.09); }
.ag-tbl tr.ag-err:hover td { background: rgba(var(--red-rgb),0.15); }
/* (Layout — the full-height page and the two lists' equal/reclaimed split — lives with the
   other `.ev-fullpage` rules above, beside the Events and Usage pages it mirrors.) */

/* Usage columns */
.usage-section { margin-bottom: 24px; }
.usage-row.ur-total { min-height: var(--row-h); font-weight: 700; color: var(--accent); }
/* Scroll a usage table once it passes ~10 data rows; the header stays pinned. */
.usage-list { max-height: calc(var(--row-h-hdr) + 10 * var(--row-h) + 10px); overflow-y: auto; }
.usage-list .usage-row.ur-hdr { position: sticky; top: 0; z-index: 1; }
.uc-dim   { color: var(--muted); }
/* The committer's name. Fixed and narrow, NOT flexible: it holds "Me" or a person's name, and
   letting it flex pushed the owner column far to its right — the two answer adjacent questions and
   read best adjacent. The `+N agents` marker that used to live here moved to `uc-owner`, so this
   is a plain ellipsised cell again. Shown only under a multi-person view (`showUsers`). */
.uc-name  { width: 96px; flex-shrink: 0; font-size: var(--text-xs); overflow: hidden;
            text-overflow: ellipsis; white-space: nowrap; }
/* A session's sub-agents sub-row (#P133): a THIN line attached under its conversation — no divider
   between the two (the parent drops its bottom border), a smaller type step, a shorter row, a
   scaled-down badge and unbolded figures, so it reads as a footnote of the row above, not a peer. */
.usage-row.ur-data:has(+ .ur-sub) { border-bottom-color: transparent; }
.usage-row.ur-data.ur-sub { min-height: 20px; padding-block: 0 3px; font-size: var(--text-2xs);
                            background: var(--surface-1); }
.usage-row.ur-sub .uc-root { transform: scale(0.75); }
.usage-row.ur-sub .uc-total strong { font-weight: 500; }
.usage-row.ur-sub .uc-owner, .usage-row.ur-sub .uc-model, .usage-row.ur-sub .uc-total { font-size: var(--text-2xs); }
.usage-row.ur-sub .uc-owner-label { color: var(--muted); }
/* Conversation-volume columns on the by-provider / by-owner usage tables (prompts = user turns,
   responses = assistant turns). Sized to fit the header words at --text-xs. */
.uc-prompts   { width: 58px; flex-shrink: 0; text-align: right; font-family: var(--font-mono); font-size: var(--text-xs); }
.uc-responses { width: 68px; flex-shrink: 0; text-align: right; font-family: var(--font-mono); font-size: var(--text-xs); }
/* Tool steps — their own counter, beside responses (model calls that asked for a tool). */
.uc-tools     { width: 48px; flex-shrink: 0; text-align: right; font-family: var(--font-mono); font-size: var(--text-xs); }
.uc-in    { width: 72px;  flex-shrink: 0; text-align: right; font-family: var(--font-mono); font-size: var(--text-xs); }
.uc-out   { width: 72px;  flex-shrink: 0; text-align: right; font-family: var(--font-mono); font-size: var(--text-xs); }
.uc-total { width: 90px;  flex-shrink: 0; text-align: right; font-family: var(--font-mono); font-size: var(--text-sm); }
.uc-cache { width: 72px;  flex-shrink: 0; text-align: right; font-family: var(--font-mono); font-size: var(--text-xs); }
.uc-age   { width: 36px;  flex-shrink: 0; text-align: right; font-size: var(--text-xs); white-space: nowrap; }
/* WHAT produced the tokens, immediately beside the WHO in `uc-name`. Fixed width rather than
   flexible: `uc-model` already flexes, and a second flexible column here would leave both ragged
   across rows. Wide enough for `inventory (visual-designer)`; longer values ellipse, with the full
   owner in the title.
   A flex CONTAINER because it carries the `+N agents` marker as well as the name — the marker
   moved here from the user cell, which is gated off entirely under a single-committer view. */
.uc-owner { width: 150px; flex-shrink: 0; font-size: var(--text-xs); overflow: hidden;
            display: flex; align-items: baseline; gap: 4px; }
/* The NAME absorbs the compression, so the marker beside it never truncates. `flex: 0 1 auto`,
   not `flex: 1`: growing to fill the cell pushed `(+3 agents)` out to the far edge, where it read
   as a separate column value instead of part of the phrase `user (+3 agents)`. Shrink still 1, so
   a long owner name is what gives way when the cell is tight. */
.uc-owner-label { flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The chart split's colour, in the session column that matches the split (model / user / owner) —
   the same hue as that member's bar and ring slice. Inline so it sits inside an ellipsised cell. */
.uc-dot { display: inline-block; flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%;
          margin-right: 5px; vertical-align: middle; align-self: center; }
/* GROWS into spare width instead of sitting at a fixed 220px. A session id is a 36-character UUID
   that exactly fills 220px at --text-2xs, so on a wide table it was the one column truncating
   while empty space sat beside it. `flex: 1 1 220px` keeps 220 as the floor it never shrinks
   below, and lets it take slack alongside `uc-model` when the row has room. */
.uc-sid   { flex: 1 1 220px; min-width: 220px; font-size: var(--text-xs); font-family: var(--font-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.uc-root  { width: 30px;  flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
/* Owner badges fill the column so every root (user/agent/wynxx) is one uniform
   box sized to the widest value ("wynxx"). */
.uc-root .org-badge { width: 100%; min-width: 0; }
.uc-prov  { width: 40px;  flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
.uc-model { flex: 1; min-width: 0; font-size: var(--text-xs); font-family: var(--font-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.uc-pctcol { width: 52px; flex-shrink: 0; text-align: right; font-family: var(--font-mono); font-size: var(--text-sm); }
/* Est. Wynxx-injected tokens column (chars/4) — mirrors the Sessions table's injected cell; sits
   alongside the tax% column on the By-provider totals. Distinct from .uc-in (input tokens). */
.uc-inj   { width: 72px;  flex-shrink: 0; text-align: right; font-family: var(--font-mono); font-size: var(--text-xs); }
/* #P94 Wynxx-context-tax column (est. injected tokens). Width matches the Sessions table's tax
   cell (.uc-pctcol, 52px) so the injected+tax pair reads the same. */
.uc-tax   { width: 52px;  flex-shrink: 0; text-align: right; font-family: var(--font-mono); font-size: var(--text-xs); }
/* The injected figure is a link only when that session has a per-ability breakdown to show. */
.uc-inj-link { cursor: pointer; text-decoration: underline; text-decoration-style: dotted;
               text-underline-offset: 2px; color: inherit; }
.uc-inj-link:hover { color: var(--accent); }
/* An identifier, not code: muted like the row's other secondary cells, not the `--code-string` tint. */
.uc-sid code { font-size: var(--text-2xs); color: var(--muted); }

/* Audit detail modal */
.modal-box.modal-audit { width: 820px; max-height: 88vh; display: flex; flex-direction: column; }
/* #P94 slim variant for the compact "Context injected by ability" popup — 3-class specificity so it
   beats the 820px .modal-box.modal-audit rule above (equal-specificity tie would otherwise win here). */
.modal-box.modal-audit.modal-slim { width: 400px; padding: 16px 18px 14px; }
.modal-hdr { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; flex-shrink: 0; }
/* Title matches the login popup heading: white, text-lg, bold (system-wide popup title style). */
.modal-hdr h3 { margin: 0; font-size: var(--text-lg); font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: flex; align-items: center; gap: 7px; min-width: 0; }
/* Title text uses the system title font (not mono), matching planner/login popup titles. */
.ev-type-text { color: var(--text); overflow: hidden; text-overflow: ellipsis; }
/* Provider icon + model grouped as ONE header unit (kept tight together, set off from the event
   type by margin — so it reads as "type … [provider] model"). Model uses the small muted scale. */
.ev-type-meta { display: inline-flex; align-items: center; gap: 5px; margin-left: 6px; flex-shrink: 0; }
.ev-type-model { font-size: var(--text-xs); font-weight: 500; color: var(--muted); }
/* The owner box in the popup TITLE sizes to its content (like planner's detail badge), not the
   56px column-alignment min-width the .org-badge carries in list/table rows; trim its vertical
   footprint so its height matches planner's inline type badge. */
.modal-hdr .org-badge { min-width: 0; line-height: 1.4; padding-top: 0; padding-bottom: 0; }
.ev-hdr-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.ev-id-pill { display: flex; align-items: center; gap: 6px; font-size: var(--text-xs); color: var(--muted); font-family: var(--font-mono); white-space: nowrap; }
.modal-body { overflow-y: auto; flex: 1; }
.modal-box.modal-resizable > .modal-body { min-height: 0; }
.detail-kv { margin-bottom: 10px; }
.detail-label { font-size: var(--text-xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin-bottom: 2px; }
/* Popup FIELD/detail captions (.modal-box form-field / detail / kv labels) stay
   MUTED — they are field captions, distinct from a section sub-sub-title
   (.tier-label / .td-label, which is white var(--text); see the SYSTEM
   sub-sub-title rule above). The popup TITLE is white var(--text) and the tenant
   crumb is accent2 — see the "POPUP STYLE" rule near `.modal-header h3`. */
.modal-box .form-field label,
.modal-box .doc-skill-line label,
.modal-box .doc-track-lbl,
.modal-box .detail-label,
.modal-box .kv-label { color: var(--muted); }
.detail-val { font-size: var(--text-base); color: var(--text); word-break: break-all; }
.detail-val.mono { font-family: var(--font-mono); font-size: var(--text-sm); }
.detail-pre { font-family: var(--font-mono); font-size: var(--text-sm); white-space: pre-wrap; word-break: break-word; background: var(--bg); border: 1px solid var(--border); border-radius: var(--r-md); padding: 8px 10px; margin-top: 3px; max-height: 220px; overflow-y: auto; color: var(--text); line-height: 1.5; transition: max-height 0.15s ease; }
.detail-pre.expanded { max-height: none; }
.detail-expand-btn { float: right; font-size: var(--text-xs); color: var(--muted); cursor: pointer; padding: 0; background: none; border: none; font-family: inherit; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }
.detail-expand-btn:hover { color: var(--accent); }
.detail-meta { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; align-items: center; }
.detail-footer { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; flex-shrink: 0; align-items: center; }
/* Event detail footer: spacer | timestamp (center) | close (right) */
.ev-detail-footer { justify-content: space-between; }
.ev-footer-pad { flex: 1; }
.ev-footer-ts { color: var(--muted); font-size: var(--text-xs); font-family: var(--font-mono); }
.ev-footer-end { flex: 1; display: flex; justify-content: flex-end; gap: 8px; }
/* Event info box: floats right, groups provider + model + tokens */
/* Audit modal body: flex-column so top meta shrinks and data section grows */
.modal-box.modal-audit .modal-body { overflow-y: hidden; display: flex; flex-direction: column; }
.ev-body-top { flex-shrink: 0; }
.ev-body-data { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 10px; }
/* Each data/metadata kv block grows proportionally and its pre fills it */
.ev-data-kv { display: flex; flex-direction: column; flex: 1; min-height: 0; margin-bottom: 0; }
.ev-data-kv .detail-label { flex-shrink: 0; }
.ev-data-kv .detail-pre { flex: 1; min-height: 0; max-height: none; overflow-y: auto; }
.ev-info-box { float: right; margin: 0 0 12px 16px; display: inline-flex; flex-direction: column; gap: 5px; min-width: 128px; max-width: 200px; border: 1px solid var(--border); border-radius: var(--r-md); padding: 8px 10px; background: var(--surface-2); align-self: flex-start; }
.ev-info-row { display: flex; flex-direction: row; align-items: baseline; gap: 6px; white-space: nowrap; }
.ev-info-label { font-size: var(--text-2xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); flex-shrink: 0; }
.ev-info-val { font-size: var(--text-xs); color: var(--text); overflow: hidden; text-overflow: ellipsis; }
.ev-info-val.mono { font-family: var(--font-mono); }
/* (Removed bespoke .btn-close-detail — the detail panel's Close uses the central .btn.) */

/* ───────────────────────────────────────────────────────────────────────
   Portal — documents section
   Reuses the shared list / event / modal / pill families; only the
   documents-specific column widths and a few muted helpers live here. Colours
   come from design tokens only (no hex literals).
   ─────────────────────────────────────────────────────────────────────── */

/* Monitored-documents list: a fixed mtime column sits between the path
   (.tr-activity, flex) and the Details button. */
.doc-mtime { flex-shrink: 0; width: 150px; text-align: right; font-size: var(--text-xs); color: var(--muted); white-space: nowrap; }

/* Monitored row, left → right: type badge · path · green "tracked" box · flexible
   gap · "updated" bubble · date · Details. The path (.doc-path) shrinks but does
   NOT grow, so the green box hugs it on the left; .doc-row-spacer takes the slack
   so the "updated" bubble sits right against the date. The "tracked" box is the
   compact in-row .tr-badge (same size as the type badge), tinted green; the
   system-amber (--accent2) "updated" bubble (a fixed-width slot that keeps the
   date column aligned) marks a file modified in the last 12h. No new tokens. */
.tr-activity.doc-path { flex: 0 1 auto; }
/* Merged Documents page: a section head — sub-sub-title (left) + that table's
   filter (and the events Show-more) pushed right. */
.doc-section-head { display: flex; align-items: center; gap: 10px; margin: 18px 0 6px; }
.doc-section-head .td-label { margin-bottom: 0; }
.doc-section-head .btn { margin-left: auto; }
.doc-row-spacer { flex: 1 1 auto; min-width: 8px; }
.doc-updated-col { flex-shrink: 0; width: 80px; display: flex; align-items: center; justify-content: flex-end; }
.pill.doc-tracked { flex-shrink: 0; --c: var(--green); }
.pill.doc-fresh { --c: var(--accent2); }

/* Change-events feed columns. The shared .ev-col-* set is sized for the audit
   feed; documents needs its own when / user / type widths (reusing .ev-col-prov
   and .ev-col-data as-is for the provider and document cells). */
.ev-col-time  { width: 150px; flex-shrink: 0; font-size: var(--text-xs); color: var(--muted); white-space: nowrap; }
.ev-col-user  { width: 110px; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ev-col-dtype { width: 112px; flex-shrink: 0; display: flex; align-items: center; }

/* Config catalog editor: the document-id box (first column) mirrors the
   planner/quests instruction id box — system `--text-xs`, fixed width — and is
   colour-coded by status (enabled + tracked = green, enabled + untracked =
   amber, disabled = grey). Enable/Disable sits right after it and Edit at the
   row's end; both reuse the central `.btn.btn-sm`. The read-only
   "managed by" note replaces the buttons for foreign-owned types. */
.doc-id { display: inline-block; flex: 0 0 auto; width: 120px; box-sizing: border-box; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: center; --c: var(--muted); }
/* Enabled = indigo tint; disabled = neutral tint (status colour also on the switch). */
.doc-id.st-on   { --c: var(--accent); }
.doc-id.st-off  { --c: var(--muted); }
/* Edit popup: Track is a modern on/off switch (the shared .ui-switch) pinned at
   title level in the header; the skill shows as a single read-only line. */
.doc-head-right { display: flex; align-items: center; gap: 14px; }
.doc-track-toggle { display: flex; align-items: center; gap: 8px; }
.doc-track-lbl { font-size: var(--text-xs); color: var(--muted); font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }
.doc-skill-line { display: flex; align-items: baseline; gap: 8px; margin-bottom: 12px; }
.doc-skill-line label { font-size: var(--text-sm); color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }
.doc-skill-val { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--text); }
.doc-managed { flex-shrink: 0; margin-left: 6px; font-size: var(--text-xs); color: var(--muted); font-style: italic; }
/* Edit/create popups (document type + path token) stack bare .form-field divs directly in the
   .modal-box. Their 12px spacing now comes from the system FIELD RHYTHM rule (see "Forms"); the
   bottom margin here only keeps the last field off the next non-field block (a .doc-skill-line,
   the footer), and collapses with the rhythm margin between two fields. Plus a taller textarea
   floor so multi-line When/What/Guidance are comfortable to edit. */
.modal-box > .form-field { margin-bottom: 12px; }
.modal-box > .form-field textarea { min-height: 76px; padding: 8px 12px; }
/* Guidance seeds the generated SKILL.md body — give it a much taller box than
   the short When/What triggers. The class selector out-specifies the shared
   textarea rule above so this min-height wins. */
.modal-box > .form-field textarea.doc-guidance { min-height: 200px; }
/* Float a footer action to the right edge (Save + Delete stay clustered left).
   The planner uses #pq-cfg-root-scoped .pq-cancel for this; documents popups are
   not under that root, so this unscoped utility carries the same intent. */
.form-submit .btn-push-right { margin-left: auto; }

/* Details popup footer: metadata on the left, Close on the right. */
.doc-detail-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.doc-foot-meta { font-size: var(--text-xs); color: var(--muted); }

/* Thin token-based spinner ring (neutral --border with an --accent leading edge), sized to sit
   where a .ui-switch normally does — used by in-flight rows (upgrade steps, the inventory tree). */
@keyframes doc-spin { to { transform: rotate(360deg); } }
.doc-spinner { flex-shrink: 0; width: 14px; height: 14px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: var(--r-pill); animation: doc-spin 0.7s linear infinite; }
/* Larger standalone spinner + a compact busy popup (e.g. memory "Fix indexes"). */
.spinner-lg { width: 30px; height: 30px; border: 3px solid var(--border); border-top-color: var(--accent); border-radius: var(--r-pill); animation: doc-spin 0.8s linear infinite; }
.modal-box.modal-busy { width: auto; min-width: 0; max-width: 320px; display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 28px 34px; text-align: center; color: var(--text); font-size: var(--text-sm); }
.doc-pending-x { flex-shrink: 0; font-size: var(--text-lg); }

/* Fixed-width status pill so a run board's verdict bubbles align across rows — inventory's
   Runs board and the plan/quest review lists (mirrors .plan-pill). Verdict pills reuse the
   existing .pill colour classes (mapped in JS), so only the width is declared here. */
.verdict-pill { width: 92px; text-align: center; box-sizing: border-box; }
/* Analysis-duration box (request → completion): a GHOST .tr-badge — transparent
   surface, neutral --border, muted text (NOT the tinted-fill or plain .date variant).
   In a board row it sits before the requestor; in the detail popup it rides the header
   meta line, to the left of the #R id badge. */
.tr-badge.analysis { background: none; border-color: var(--border); color: var(--muted); flex-shrink: 0; white-space: nowrap; }

/* =======================================================================
   Guardian config components
   ======================================================================= */

#grd-cfg-root .cfg-name-box { width: 160px; }
#grd-cfg-root #grd-groups { margin-top: 20px; }
#grd-cfg-root #grd-groups .td-label { margin-top: 20px; }
#grd-cfg-root #grd-groups .td-label:first-child { margin-top: 0; }
/* Rules modal list — scrollable, fixed height so the modal stays compact */
.grd-rules-list { max-height: 360px; overflow-y: auto; }
/* Rule rows inside the Customize modal: severity badge → description → switch */
.grd-rule-row { display: flex; align-items: center; gap: 12px; padding: 6px 0; border-top: 1px solid var(--border); }
.grd-rule-row:first-child { border-top: none; }
.grd-rule-desc { flex: 1; font-size: var(--text-sm); }
/* Severity badge — fixed-width so all badges align; colour overrides inherit from .tr-badge.dep / .tr-badge.warn */
.grd-rule-sev { flex-shrink: 0; min-width: 62px; text-align: center; }
.grd-rule-sev.warn { --c: var(--accent2); }
/* Header action strip (debug toggle + Save / Reset buttons) */
#grd-cfg-root .cfg-header-actions { display: flex; align-items: center; gap: 12px; margin-left: auto; }
.grd-debug-wrap { display: flex; align-items: center; gap: 8px; font-size: var(--text-sm); }

/* ---- Server project view-only (non-admin viewing via ?server=) -------------
   The hard read-only boundary is server-side (mutating requests are 403'd); this is the
   matching visual treatment: a muted notice plus dimmed, non-interactive form controls so a
   viewer sees they cannot edit. Navigation anchors stay live (only form controls are gated). */
.view-only-note { margin: 0 0 14px; padding: 8px 12px; border: 1px solid var(--border); border-radius: var(--r-md); background: var(--panel); color: var(--muted); font-size: var(--text-sm); }
.portal-content.view-only button, .portal-content.view-only input, .portal-content.view-only select, .portal-content.view-only textarea { pointer-events: none; opacity: .55; }

/* ---- Backend-unavailable takeover (backend-guard.js) ------------------------
   Full-screen "reconnecting" overlay shown when the stateless frontend cannot reach the backend.
   Sits above EVERYTHING (header z300, modals z1000, toasts z10000) so nothing shows through, and
   reuses the login page's dimmed brand watermark background. */
html.jm-backend-down-lock { overflow: hidden; }
.jm-backend-down { position: fixed; inset: 0; z-index: 100000; display: none; align-items: center; justify-content: center; background: var(--bg); }
.jm-backend-down.open { display: flex; }
.jm-backend-down .login-bg { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 0; opacity: 0.06; pointer-events: none; user-select: none; color: var(--accent); }
.jm-backend-down .login-bg img { width: min(90vw, 900px); height: auto; }
.jm-backend-down-card { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; gap: 14px; text-align: center; padding: 28px 32px; }
.jm-backend-down-card .section-title { margin: 0; }
.jm-backend-down-card .title-description { margin: 0; color: var(--muted); }

/* ============================================================================
   Docs Overview landing page (.ov-*) — plan #P4 flagship redesign.
   CLASS-BASED ONLY: the docs markdown renderer strips inline `style`, so every
   colour here is a portal token (themes light/dark with the app for free). Card
   grids use auto-fit so they reflow when the sidebar collapses (container-driven,
   not a viewport media query). Scoped under `.ov` so nothing leaks portal-wide.
   ============================================================================ */
.ov { max-width: 1560px; }
.ov :where(h1, h2, h3, p, ul, li) { border: 0; }
.ov code { font-family: var(--font-mono); font-size: 0.86em; background: var(--surface-2);
  color: var(--accent2); padding: 0.08em 0.4em; border-radius: var(--r-sm); }

.ov-hero { margin-bottom: 6px; }
.ov-hero-fig { margin-top: 24px; }
.ov-eb { font-size: var(--text-xs); font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--accent); margin: 0 0 10px; }
.ov-hero h1 { font-size: clamp(26px, 3.2vw, 34px); line-height: 1.12; letter-spacing: -0.02em;
  font-weight: 700; color: var(--text); margin: 0 0 14px; text-wrap: balance; }
.ov-sub { font-size: var(--text-lg); line-height: 1.6; color: var(--muted); max-width: 66ch; margin: 0 0 20px; }
.ov-sub b { color: var(--text); font-weight: 600; }
.ov-cta { display: flex; gap: 10px; flex-wrap: wrap; }
.ov-btn { display: inline-flex; align-items: center; gap: 6px; font-size: var(--text-sm); font-weight: 600;
  border-radius: var(--r-lg); padding: 10px 17px; text-decoration: none; border: 1px solid transparent; }
.ov-btn-primary { background: var(--accent); color: #fff; }
.ov-btn-primary:hover { filter: brightness(1.07); text-decoration: none; }
.ov-btn-ghost { color: var(--text); border-color: var(--border); background: var(--bg); }
.ov-btn-ghost:hover { background: var(--panel); text-decoration: none; }

.ov-figure { margin: 0; border: 1px solid var(--border); border-radius: var(--r-xl);
  background: var(--surface-2); padding: 22px; overflow-x: auto; }
.ov-figure .dg { display: block; width: 100%; height: auto; max-width: 697px; margin: 0 auto; }
/* Overview diagram — Brand-mono palette (plan #P4): one indigo accent + neutral
   frame, everything token-based so it themes light/dark. "Your flow" is solid
   indigo; Wynxx's wrapper is a neutral frame; its abilities & storage are indigo
   tints; not-yet-working provider CLIs are outlined. */
.dg .lbl { fill: var(--text); }
.dg .lblm { fill: var(--muted); }
.dg .r-flow { fill: var(--accent); }
.dg .r-flowtxt { fill: #fff; }
.dg .r-flowsub { fill: rgba(255, 255, 255, 0.72); }
.dg .r-cli { fill: var(--bg); stroke: var(--accent); stroke-opacity: 0.5; }
.dg .r-clilbl { fill: var(--accent); }
.dg .r-prov { fill: var(--accent); }
.dg .r-provtxt { fill: #fff; }
.dg .r-provo { fill: none; stroke: var(--accent); stroke-width: 0.7; }
.dg .r-provot { fill: var(--accent); }
.dg .r-arrow { stroke: var(--accent); }
.dg .r-arrowhead { fill: var(--accent); }
.dg .r-wrap { fill: var(--panel); stroke: var(--border); }
.dg .r-wraplbl { fill: var(--text); }
.dg .r-abil { fill: rgba(var(--accent-rgb), 0.12); stroke: var(--accent); stroke-width: 0.7; stroke-opacity: 0.55; }
.dg .r-abiltxt { fill: var(--accent); }
.dg .r-abilsub { fill: var(--muted); }
.dg .r-hooks { stroke: var(--accent); stroke-opacity: 0.5; }
.dg .r-store { fill: var(--green); }
.dg .r-storetxt { fill: #fff; }
.dg .r-num { fill: var(--accent); }
.dg .r-numtxt { fill: #fff; font-weight: 700; }
.dg .r-arrowd { stroke: var(--muted); stroke-dasharray: 4 3; }
.dg .r-arrowheadd { fill: var(--muted); }
/* Two-diagram Architecture hub: the components map wants more width than the
   default 697px figure cap; the flow figure keeps the default. */
.ov-figure.ov-figure--wide .dg { max-width: 920px; }
.ov-legend { display: flex; gap: 18px; flex-wrap: wrap; justify-content: center; margin-top: 14px;
  font-size: var(--text-xs); color: var(--muted); }
.ov-legend span { display: inline-flex; align-items: center; gap: 7px; }
.ov-legend .d { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }
.ov-legend .di { background: var(--accent); }
.ov-legend .da { background: rgba(var(--accent-rgb), 0.20); border: 1px solid var(--accent); }
.ov-legend .dg { background: var(--green); }

.ov-providers { display: flex; flex-wrap: wrap; gap: 8px 22px; margin: 20px 0 4px; padding: 13px 16px;
  border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--surface-2); }
.ov-ptier { font-size: var(--text-sm); color: var(--muted); display: inline-flex; align-items: baseline; gap: 8px; }
.ov-ptier b { color: var(--text); font-weight: 600; }
.ov-pdot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; position: relative; top: 1px; }
.ov-pdot-on { background: var(--green); }
.ov-pdot-wip { background: var(--accent2); }
.ov-pdot-plan { background: var(--muted); }

.ov-h2 { font-size: var(--text-2xl); font-weight: 700; letter-spacing: -0.01em; color: var(--text);
  line-height: 1.2; margin: 44px 0 4px; }
.ov-lead { font-size: var(--text-sm); color: var(--muted); margin: 0 0 18px; }

.ov-pillars { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 14px; margin-top: 16px; }
.ov-pill { border: 1px solid var(--border); border-radius: var(--r-xl); background: var(--panel); padding: 17px 18px; }
.ov-cap { display: block; width: 26px; height: 3px; border-radius: 2px; margin-bottom: 12px; background: var(--accent); }
.ov-cap-i { background: var(--accent); }
.ov-cap-g { background: var(--green); }
.ov-cap-a { background: #d97706; }
.ov-pill h3 { font-size: var(--text-base); font-weight: 700; color: var(--text); margin: 0 0 6px; }
.ov-pill p { font-size: var(--text-sm); color: var(--muted); margin: 0; line-height: 1.55; }

.ov-abilities { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 18px; margin-top: 16px; }
.ov-ch { font-size: var(--text-xs); font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;
  display: flex; align-items: center; gap: 8px; margin-bottom: 2px; padding-bottom: 10px; border-bottom: 1px solid var(--border); }
.ov-ch::before { content: ""; width: 9px; height: 9px; border-radius: 3px; }
.ov-col-j .ov-ch { color: var(--accent); }
.ov-col-j .ov-ch::before { background: var(--accent); }
.ov-col-s .ov-ch { color: var(--accent2); }
.ov-col-s .ov-ch::before { background: var(--accent2); }
.ov-col-p .ov-ch { color: var(--green); }
.ov-col-p .ov-ch::before { background: var(--green); }
.ov-ab { padding: 11px 0; border-bottom: 1px solid var(--border); }
.ov-ab b { font-size: var(--text-sm); font-weight: 600; color: var(--text); display: block; margin-bottom: 1px; }
.ov-ab span { font-size: var(--text-xs); color: var(--muted); }
.ov-ab a { color: inherit; text-decoration: none; }
.ov-ab a:hover { color: var(--accent); }

.ov-fits { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; margin-top: 16px; }
.ov-fcard { border: 1px solid var(--border); border-radius: var(--r-xl); background: var(--panel); padding: 18px 20px; }
.ov-eb2 { font-size: var(--text-2xs); font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); margin: 0 0 6px; }
.ov-fcard h3 { font-size: var(--text-base); font-weight: 700; color: var(--text); margin: 0 0 10px; }
.ov-fcard p { font-size: var(--text-sm); line-height: 1.6; color: var(--muted); margin: 0 0 10px; }
.ov-fcard p:last-child { margin-bottom: 0; }
.ov-fcard p b { color: var(--text); font-weight: 600; }

.ov-next { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
.ov-next li { font-size: var(--text-sm); color: var(--muted); }
.ov-next b { color: var(--text); font-weight: 600; }
.ov-next a { color: var(--accent); text-decoration: none; font-weight: 500; }
.ov-next a:hover { text-decoration: underline; }
.ov-foot { margin-top: 38px; padding-top: 18px; border-top: 1px solid var(--border); font-size: var(--text-sm); color: var(--muted); }

/* Architecture page extras: linked concept cards + prose body sections. */
.ov-fcard-link { display: block; text-decoration: none; color: inherit; transition: border-color 0.12s; }
.ov-fcard-link:hover { border-color: var(--accent); text-decoration: none; }
.ov-h3 { font-size: var(--text-lg); font-weight: 700; letter-spacing: -0.01em; color: var(--text); margin: 26px 0 6px; }
.ov-prose { margin-top: 8px; }
.ov-prose p { font-size: var(--text-base); line-height: 1.65; color: var(--text); margin: 0 0 14px; max-width: 84ch; }
.ov-prose ul { margin: 2px 0 18px; padding-left: 20px; max-width: 84ch; }
.ov-prose li { font-size: var(--text-base); line-height: 1.6; color: var(--text); margin: 0 0 9px; }
.ov-prose b { color: var(--text); font-weight: 600; }
.ov-prose i { color: inherit; }
.ov-note { color: var(--muted) !important; font-style: italic; }

/* =======================================================================
   Print / "Save as PDF"
   -----------------------------------------------------------------------
   On screen the portal is a FIXED-HEIGHT APP SHELL: body is height:100% +
   overflow:hidden, .docs-layout is position:absolute pinned to the viewport
   rect, and the article scrolls INSIDE .docs-main (overflow-y:auto). The
   print engine lays out the BODY flow to paper — but the content lives in a
   nested scroll box clamped to one screen, so a naive print captures only the
   visible slice and clips everything past the scroll fold. This block un-pins
   the shell so the article flows down the page and paginates normally; the
   browser's built-in "Save as PDF" then produces a full PDF with the inline
   <svg> diagrams rendered exactly as on screen (vector, crisp) — no separate
   markdown→PDF pipeline (which would mangle or drop the injected SVGs).
   ----------------------------------------------------------------------- */
@media print {
  /* Sane paper margins (the chrome that used to frame the content is hidden). */
  @page { margin: 1.5cm; }

  /* Un-pin the app shell: let the document use its natural height and flow. */
  html, body { height: auto !important; overflow: visible !important; }
  body { margin: 0 !important; padding-top: 0 !important; }
  .docs-layout {
    position: static !important; display: block !important;
    overflow: visible !important;
  }
  .docs-main {
    height: auto !important; overflow: visible !important;
    padding-top: 0 !important; display: block !important;
  }
  .portal-content { padding: 0 !important; }

  /* Drop the chrome — only the article prints. Hiding .portal-header removes
     everything docked in it (toggle, nav, ticker, project switcher). The
     overlay/toast/modal layers are display:none until opened; force them off
     so a left-open panel can't bleed onto the page. */
  .portal-header, .portal-footer, .docs-sidebar, #portal-brand-src,
  .header-sidebar-toggle, .jm-toast-layer, .dash-toolbar,
  .overlay-panel, .modal-overlay, #jm-config-banner { display: none !important; }

  /* Force the LIGHT palette for paper regardless of the viewer's theme. Dark is
     re-declared later in the cascade (style.dark.css: @media prefers-color-scheme
     AND [data-theme="dark"]/.theme-dark), so !important on the tokens is what wins
     here — otherwise a dark-theme viewer prints near-white text onto white paper.
     Only the tokens that affect printed prose / code / diagrams are reset. */
  :root {
    color-scheme: light !important;
    --bg:          #ffffff !important;
    --panel:       #f5f5f5 !important;
    --border:      #e5e5e5 !important;
    --text:        #0a0a0a !important;
    --muted:       #737373 !important;
    --code-bg:     #f5f5f5 !important;
    --code-string: #b45309 !important;
    --accent:      #2f47e6 !important;
    --dg-cli-bg:   #ffffff !important; --dg-cli-stroke:  #c7d2fe !important; --dg-cli-label:  #2f47e6 !important;
    --dg-wrap-bg:  #fffaf2 !important; --dg-wrap-stroke: #f0d4a8 !important; --dg-wrap-label: #b45309 !important;
  }
  body { background: #ffffff !important; color: var(--text) !important; }

  /* Keep the subtle backgrounds/borders of code + diagram blocks (browsers drop
     backgrounds by default) so those blocks stay visually bounded on paper. */
  pre, .ascii, .diagram, .toc, code, .badge, .pill, .tr-badge {
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }

  /* Don't split a diagram, code block, table, or card across a page break; and
     keep a heading with the content it introduces. (A block taller than a page
     still breaks — the browser overrides break-inside when it must.) */
  .diagram, .ascii, pre, table, .toc, .card, .dash-card, .legend,
  blockquote, figure { break-inside: avoid; }
  h1, h2, h3, h4 { break-after: avoid; }
  .diagram { overflow: visible !important; }
}

/* ---------------------------------------------------------------------------
   Canonical copy button (`.jm-copy-btn`) + `window.jmCopy` (toast.js).
   The copy glyph is a CSS ::before mask (shares the SAME SVG source as window.jmIcons via
   /assets/icons.css — no inline SVG); on a successful copy jmCopy adds `.is-copied` for ~1.2s,
   swapping the glyph to a green check and tinting the button green. That swap IS the confirmation.
   Composable BY DESIGN: this class owns only the glyph + centered layout + copied-state color — the
   box (size / border / background / padding) comes from the host button class (`btn`, `.hdr-init-copy`,
   the `#adm-modal-body .jm-copy-btn` layout rule, …), so it drops onto any button unchanged.
   --------------------------------------------------------------------------- */
.jm-copy-btn { display: inline-flex; align-items: center; justify-content: center; }
.jm-copy-btn::before { content: ""; display: inline-block; width: 14px; height: 14px;
  background-color: currentColor;
  -webkit-mask: var(--icon-copy) center/contain no-repeat; mask: var(--icon-copy) center/contain no-repeat; }
.jm-copy-btn.is-copied { color: var(--green); border-color: color-mix(in srgb, var(--green) 45%, var(--border)); }
.jm-copy-btn.is-copied::before { -webkit-mask-image: var(--icon-check); mask-image: var(--icon-check); }

/* ───────────────────────────────────────────────────────────────────────
   Bundle viewer (inventory, plan #8) — the review popup (a pending item's instructions and its
   files to open, flagged ones ticked) and the shared single-file viewer (code, rendered Markdown,
   image preview). Declared ONCE here; tokens only. Both open inside the standard
   `.modal-box.modal-wide` (`.modal-scroll-body` scrolls, `.modal-foot` pins the actions).
   ─────────────────────────────────────────────────────────────────────── */
.bv-path { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1 1 auto; min-width: 0; }
/* The review popup's file list: a clickable row per pending file (opens the file viewer). */
.bv-row { cursor: pointer; gap: 6px; }
/* A/M/D + risk badges reuse `.tr-badge`; only the colour differs. */
.tr-badge.bv-a { color: var(--green); border-color: color-mix(in srgb, var(--green) 45%, var(--border)); }
.tr-badge.bv-m { color: var(--accent2); border-color: color-mix(in srgb, var(--accent2) 45%, var(--border)); }
.tr-badge.bv-d { color: var(--red); border-color: color-mix(in srgb, var(--red) 45%, var(--border)); }
.tr-badge.bv-flag { color: var(--accent2); }
.tr-badge.bv-info { color: var(--muted); }
/* The flagged-file tick — the `.jm-copy-btn.is-copied` glyph swap, on a tree row. */
.bv-tick { display: inline-block; width: 14px; height: 14px; flex: 0 0 auto; background-color: var(--muted);
  -webkit-mask: var(--icon-check) center/contain no-repeat; mask: var(--icon-check) center/contain no-repeat;
  opacity: .25; }
.task-row.is-checked .bv-tick { background-color: var(--green); opacity: 1; }
.bv-viewer { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
.bv-viewer-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* Code and diffs: one monospace block; line numbers from a CSS counter, never a vendor library. */
pre.bv-code { margin: 0; background: var(--code-bg); border: 1px solid var(--border); border-radius: var(--r-md);
  font-family: var(--font-mono); font-size: var(--text-xs); line-height: 1.55; overflow: auto; max-height: 56vh;
  padding: 6px 0; counter-reset: bv-line; white-space: pre; }
pre.bv-code .bv-ln { display: block; padding: 0 10px 0 0; }
pre.bv-code .bv-ln::before { counter-increment: bv-line; content: counter(bv-line); display: inline-block;
  width: 3.2em; margin-right: 10px; text-align: right; color: var(--muted); font-size: var(--text-xs);
  user-select: none; }
.bv-img { max-width: 100%; display: block; border: 1px solid var(--border); border-radius: var(--r-md); }

/* Inventory Assets tree (POC) — the registry drawn as a filesystem. Rows are `.task-row`s so the
   pills, badges, switches and buttons line up with the cards above; only the indent, caret and
   folder/file glyphs are new. Glyphs are CSS masks over the core icons, tinted by tokens. */
/* On the configuration page the SWITCH — or, while something waits, the ✓ ✕ Withdraw decision
   buttons — leads every row (`.ic-tree-lead`, 60px; 26px on the board) BEFORE the indent, so they
   form one column at any depth; the status pill closes the row.
   The indent is therefore a margin on the caret, not row padding. Each level indents 22px and
   draws a guide line under its parent's caret — x = lead (60px + gap 6px, or 0 on the
   read-only board) + parent indent + 5px — so a child reads as INSIDE its folder. Scoped under
   `.ic-tree` to outrank `.cfg-card .task-row`; images, not the `background` shorthand, so hover
   still tints. */
/* Agent Manage popup: Description (left, takes the room) and Settings (right, sized to its
   controls) on one row; wraps under each other when the popup is too narrow for both. */
/* The version picker in an inventory popup's HEADER, right of the name (in place of the `vN`
   box): compact, sized like the header's pill and badges rather than a 36px form control. */
.ic-version-combo { flex: 0 0 auto; height: 24px; padding: 0 6px; font-size: var(--text-xs); }
.ic-desc-row { display: flex; flex-wrap: wrap; gap: 8px 24px; align-items: flex-start; }
.ic-desc-row > .td-section { flex: 1 1 280px; min-width: 0; }
.ic-desc-row > .td-section:last-child:not(:first-child) { flex: 0 1 auto; margin-left: auto; }
/* A run deadline (seconds) in the agent popup's Settings: the `.cfg-ctrl-text` input shape, sized
   for a few digits beside the Model / Access combos. */
.ic-run-num { width: 6.5rem; }
/* Inventory pages, FULL HEIGHT (#P40) — the board, the project configuration page and the org
   inventory page reuse `.ev-fullpage` (the Audit / Memory flex chain): the header and subtitle keep
   their height, the Assets card fills the rest and only the tree scrolls, never the page. */
.ev-fullpage > .page-header, .ev-fullpage > .page-subtitle { flex: 0 0 auto; }
.ev-fullpage > .ic-assets-card { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.ev-fullpage > .ic-assets-card > .cfg-card-head { flex: 0 0 auto; }
.ev-fullpage > .ic-assets-card > .ic-tree { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
/* The configuration pages' ADVANCED header action sits BELOW the filter bar: both stack in one
   right-aligned column (`.ic-header-right`); a settings glyph as a CSS mask (never inline SVG). */
.page-header > .ic-header-right { margin-left: auto; display: flex; flex-direction: column;
  align-items: flex-end; gap: 6px; }
.ic-header-right > .audit-header-filters, .ic-header-right > .cfg-header-actions { margin-left: 0; }
.ic-adv-btn { display: inline-flex; align-items: center; gap: 6px; }
.ic-adv-btn[hidden] { display: none; }
.ic-adv-btn::before { content: ""; display: inline-block; width: 14px; height: 14px; flex: 0 0 auto;
  background-color: currentColor;
  -webkit-mask: var(--icon-settings) center/contain no-repeat; mask: var(--icon-settings) center/contain no-repeat; }
/* The Advanced popup's body is a `.cfg-card` so its rows keep the card anatomy; no card margin. */
.modal-scroll-body.ic-adv-body.cfg-card { margin-top: 0; }
.ic-tree { --ic-lead: 0px; }
/* The leading column: the switch (always at its RIGHT edge) with the promotion control to its left,
   OR the decision buttons (✓ ✕ Withdraw — `endRow` moves them there while something waits).
   EXACTLY 60px: a 22px Promote + 4px gap + the 34px switch (no slack), or THREE 19px decision
   buttons + 1px gaps; + the 6px row gap. */
.ic-tree.has-lead { --ic-lead: 66px; }
.ic-tree .cfg-switch-slot.ic-tree-lead { flex: 0 0 60px; justify-content: flex-end; gap: 4px; }
.ic-tree .ic-tree-lead.is-decision { justify-content: flex-end; gap: 1px; }
.ic-tree .ic-tree-lead.is-decision .btn.btn-cfg.btn-cfg-icon { width: 19px; }
/* The BOARD's slim leading column: only the submitter's Withdraw (one button) — same place as on
   configuration, 26px + the 6px row gap. */
.ic-tree.has-lead.is-slim { --ic-lead: 32px; }
.ic-tree.is-slim .cfg-switch-slot.ic-tree-lead { flex: 0 0 26px; justify-content: center; }
.ic-tree .ic-tree-row { gap: 6px; padding-left: 0; padding-right: 4px; border-bottom: none;
  --ic-guide: linear-gradient(var(--border), var(--border)); }
/* Row separators start at the row's ICON, not the card edge: the switch column and the indent
   stay clean, so the tree's shape reads first. `.cfg-card .task-row + .task-row` draws a
   full-width hairline; replaced by a pseudo-element from the icon (lead + indent + caret 12px +
   gap 6px) to the right edge. */
.ic-tree .ic-tree-row { position: relative; --ic-indent: 0px; }
.ic-tree .ic-tree-row.ic-tree-d1 { --ic-indent: 22px; }
.ic-tree .ic-tree-row.ic-tree-d2 { --ic-indent: 44px; }
/* Depth 3: a kb item's files (kb > project > item > file, #P30). */
.ic-tree .ic-tree-row.ic-tree-d3 { --ic-indent: 66px; }
/* Depths 4-6: sub-folders inside an item, drawn as the filesystem has them. */
.ic-tree .ic-tree-row.ic-tree-d4 { --ic-indent: 88px; }
.ic-tree .ic-tree-row.ic-tree-d5 { --ic-indent: 110px; }
.ic-tree .ic-tree-row.ic-tree-d6 { --ic-indent: 132px; }
.ic-tree .ic-tree-row + .ic-tree-row { border-top: none; }
.ic-tree .ic-tree-row + .ic-tree-row::before { content: ""; position: absolute; top: 0; right: 0;
  left: calc(var(--ic-lead) + var(--ic-indent) + 18px); height: 1px; background: var(--hairline);
  pointer-events: none; }
/* Folders and files read alike: one regular font for every name — the icon says which is which.
   What sits INSIDE an item (its files) is muted, like its icon. */
.ic-tree .ic-tree-name { font-family: inherit; font-weight: 400; font-size: inherit; }
.ic-tree .ic-tree-name.bv-path { color: var(--muted); }
.ic-tree .ic-tree-row.is-gap { margin-top: 12px; }
.ic-tree-size { color: var(--muted); font-size: var(--text-2xs); flex: 0 0 auto; }
.ic-tree .ic-tree-row.is-toggle { cursor: pointer; }
/* The leading switch column and the control slot never open the row (asset_tree.py) — no pointer. */
.ic-tree .ic-tree-row.is-toggle > .ic-tree-lead,
.ic-tree .ic-tree-row.is-toggle > .cfg-ctrl-slot { cursor: default; }
.ic-tree .ic-tree-row:hover { background-color: var(--surface-2); }
/* An ABILITY's item: its owner tag (the ability's name) in blue — the one mark of an ability item
   (its folder icon stays muted; no status tint on icons). */
.ic-tree .tr-badge.is-owner { --c: var(--accent); }
/* An ORGANIZATION item in a project's tree: an `org` tag in the org colour (#P22, `--org-fg`). */
.ic-tree .tr-badge.is-org-tag { --c: var(--org-fg); }
/* The origin as an ICON at the row's right (asset_tree `originMark`): the owning ability's own
   representative glyph, or the organization's `building` — muted like every tree glyph, no
   colour. The tag forms above are the fallback when the icon is unknown. */
.ic-tree .ic-tree-origin { display: inline-flex; align-items: center; flex: 0 0 auto;
  color: var(--muted); opacity: .6; }
/* Every row's TRAIL (asset_tree `endRow`): the origin icon column + the row's last control slot
   (Manage / Details / the board's Withdraw), at least 64px (36 + 6 + 22) on EVERY row — empty on rows
   with none — so the META column before it is at the same x everywhere, tight against it. Other
   controls (✓ ✕ Withdraw, Promote) sit before the META column. A folder row's wider `+ Add` grows it
   (that row has no META to misalign). */
.ic-tree .ic-tree-trail { flex: 0 0 auto; min-width: 64px; display: flex; align-items: center;
  justify-content: flex-end; gap: 6px; }
/* A kb project's MANAGEMENT ICONS (#P38, Q305, config `kbIcons`): the row's Manage column
   (`.ic-tree-manage`, after the origin column), right-aligned so the RULES gear sits where an
   agent's Manage pen does, HISTORY and PUBLISH NOW to its left — at least one 22px column on every
   kb row, drawn empty where the viewer may use none. Muted like Manage. */
.ic-tree .cfg-ctrl-slot.ic-kb-icons { flex: 0 0 auto; min-width: 22px; gap: 6px;
  justify-content: flex-end; }
.btn.btn-cfg.btn-cfg-icon.btn-cfg-remove:hover:not(:disabled) { color: var(--red); }
/* The tree's META column: a kb item's `<path> (<size>)` and a file's size — one FIXED column at the
   row's right (after the spacer, before the trail), right-aligned; a path too long for it ends in
   an ellipsis, the whole text in its tooltip. */
.ic-tree .ic-tree-meta { flex: 0 0 220px; width: 220px; text-align: right; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
/* A kb row's `<path> (<size>)` label: dimmed the same as the origin icon beside it. */
.ic-tree .ic-tree-size.is-dim { opacity: .6; }
/* On the configuration page the origin icons have their OWN fixed column right before the Manage
   slot (asset_tree `placeOrigin`; drawn empty on rows with none), so Manage is a column alone. */
.ic-tree .ic-tree-origin-col { display: inline-flex; align-items: center; justify-content: flex-end;
  gap: 4px; flex: 0 0 36px; width: 36px; }
/* Two fixed slots inside it (asset_tree `originCol`): the organization mark, then the owning
   ability's icon — separate columns, each drawn empty when the row has none. */
.ic-tree .ic-origin-slot { display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 16px; width: 16px; }
/* The MANAGE column (the row's last control slot): at least the icon button's 22px — so an org
   item's EMPTY slot keeps the column, and the Manage pen sits flush against the origin icons with
   no blank gap. (A pending row's wider `Review` also carries ✓ / ✕, so it differs anyway.) */
.ic-tree .cfg-ctrl-slot.ic-tree-manage { min-width: 22px; justify-content: flex-end; }
.ic-tree .ic-tree-origin svg { width: 14px; height: 14px; }
/* The pending box: the owner/version box shape in the pending pill's colour. */
.ic-tree .tr-badge.is-pending { --c: var(--accent2); }
.ic-tree .tr-badge.is-rejected { --c: var(--red); }
/* kb (#P30): `auto` — a live item, published on every change to its source; `source missing` —
   its source matched no files at the last publish (the last version stays). */
.ic-tree .tr-badge.is-auto { --c: var(--green); }
.ic-tree .tr-badge.is-source-missing { --c: var(--red); }
/* The agent's cost box: the same box, an icon instead of text — red for `expensive`, green for
   `cheap` (the default tier draws none). */
.ic-tree .ic-tree-cost { display: inline-flex; align-items: center; justify-content: center; }
.ic-tree .ic-tree-cost svg { width: 14px; height: 14px; }
.ic-tree .tr-badge.is-expensive { --c: var(--red); }
.ic-tree .tr-badge.is-cheap { --c: var(--green); }
/* Clients view (#P33): the card's single status box (top right), shown only when something is
   wrong — an incompatible core, or bundle drift ("N outdated" against the org's served manifest). */
.cli-side .tr-badge.is-incompat { --c: var(--red); }
.cli-side .tr-badge.is-drift, #cli-proj-pop .tr-badge.is-drift { --c: var(--accent2); }
/* A switched-off item and its children: greyed, except the controls that switch it back on. */
.ic-tree .ic-tree-row.is-off > :not(.cfg-switch-slot):not(.cfg-ctrl-slot) { opacity: .45; }
/* A kb file WAITING for an agent to publish it (#P38): the established de-emphasised row (.72, as a
   stalled agent run); its switch stays at full strength. */
.ic-tree .ic-tree-row.is-waiting > :not(.cfg-switch-slot):not(.cfg-ctrl-slot) { opacity: .72; }
/* The KB rules popup (#P38): an include pattern matching no file — the pending box colour. */
.ic-kb-rules .tr-badge.is-pending { --c: var(--accent2); }
.ic-tree .ic-tree-d1 .ic-tree-caret { margin-left: 22px; }
.ic-tree .ic-tree-d2 .ic-tree-caret { margin-left: 44px; }
.ic-tree .ic-tree-d3 .ic-tree-caret { margin-left: 66px; }
.ic-tree .ic-tree-d4 .ic-tree-caret { margin-left: 88px; }
.ic-tree .ic-tree-d5 .ic-tree-caret { margin-left: 110px; }
.ic-tree .ic-tree-d6 .ic-tree-caret { margin-left: 132px; }
.ic-tree .ic-tree-d1 {
  background-image: var(--ic-guide); background-size: 1px 100%; background-repeat: no-repeat;
  background-position: calc(var(--ic-lead) + 5px) 0; }
.ic-tree .ic-tree-d2 {
  background-image: var(--ic-guide), var(--ic-guide); background-size: 1px 100%, 1px 100%;
  background-repeat: no-repeat;
  background-position: calc(var(--ic-lead) + 5px) 0, calc(var(--ic-lead) + 27px) 0; }
.ic-tree .ic-tree-d3 {
  background-image: var(--ic-guide), var(--ic-guide), var(--ic-guide);
  background-size: 1px 100%, 1px 100%, 1px 100%; background-repeat: no-repeat;
  background-position: calc(var(--ic-lead) + 5px) 0, calc(var(--ic-lead) + 27px) 0,
    calc(var(--ic-lead) + 49px) 0; }
.ic-tree .ic-tree-d4, .ic-tree .ic-tree-d5, .ic-tree .ic-tree-d6 {
  background-image: var(--ic-guide), var(--ic-guide), var(--ic-guide), var(--ic-guide);
  background-size: 1px 100%, 1px 100%, 1px 100%, 1px 100%; background-repeat: no-repeat;
  background-position: calc(var(--ic-lead) + 5px) 0, calc(var(--ic-lead) + 27px) 0,
    calc(var(--ic-lead) + 49px) 0, calc(var(--ic-lead) + 71px) 0; }
.ic-tree-caret { display: inline-block; width: 12px; height: 12px; flex: 0 0 auto; background-color: var(--muted);
  -webkit-mask: var(--icon-chevron-down) center/contain no-repeat; mask: var(--icon-chevron-down) center/contain no-repeat;
  transform: rotate(-90deg); transition: transform 0.1s; }
.ic-tree-caret.is-open { transform: none; }
.ic-tree-caret.is-leaf { visibility: hidden; }
.ic-tree-icon { display: inline-block; width: 14px; height: 14px; flex: 0 0 auto; }
/* A generating item's spinner takes the file icon's exact slot, so the name doesn't move. */
.ic-tree-icon-busy { width: 14px; height: 14px; flex: 0 0 auto; box-sizing: border-box; }
/* Muted like a file: only an ITEM folder is coloured, by its pill (set in the page script). */
.ic-tree-icon.is-folder { background-color: var(--muted);
  -webkit-mask: var(--icon-folder) center/contain no-repeat; mask: var(--icon-folder) center/contain no-repeat; }
/* The content-type folders carry their type's glyph — an agent is a bot, a skill a gear — at
   16px, in the same muted gray as the item/file glyphs. */
.ic-tree-icon.is-folder.is-agents, .ic-tree-icon.is-folder.is-skills { width: 16px; height: 16px;
  background-color: var(--muted); }
.ic-tree-icon.is-folder.is-agents { -webkit-mask-image: var(--icon-bot); mask-image: var(--icon-bot); }
.ic-tree-icon.is-folder.is-skills { -webkit-mask-image: var(--icon-settings); mask-image: var(--icon-settings); }
/* The knowledge base (#P30): its root folder is a library (`library-big`), each kb item a folder
   with a pen (`folder-pen`) — both 16px, never tinted by a status; the root folder is muted gray
   like the other type folders. */
.ic-tree-icon.is-folder.is-kb { width: 16px; height: 16px; background-color: var(--muted);
  -webkit-mask-image: var(--icon-library-big); mask-image: var(--icon-library-big); }
.ic-tree-icon.is-kb-item { width: 16px; height: 16px; background-color: var(--sidebar-fg);
  -webkit-mask: var(--icon-folder-pen) center/contain no-repeat;
  mask: var(--icon-folder-pen) center/contain no-repeat; }
/* An item an ABILITY owns: the cog folder, muted like every glyph (its owner icon says whose). An
   ORGANIZATION item keeps the plain folder: its origin is the org icon at the row's right. */
.ic-tree-icon.is-folder.is-owned { -webkit-mask-image: var(--icon-folder-cog); mask-image: var(--icon-folder-cog); }
/* Its inner cog makes the folder read smaller at 14px: drawn at 16px, the type-folder size, with a
   -1px margin so the row keeps the same 14px footprint. */
.ic-tree-icon.is-folder.is-owned { width: 16px; height: 16px; margin: -1px; }
.ic-tree-icon.is-file { background-color: var(--muted);
  -webkit-mask: var(--icon-file) center/contain no-repeat; mask: var(--icon-file) center/contain no-repeat; }
/* A file's glyph says its TYPE (the tree's `fileKind`): text / Markdown, code, JSON / data, binary. */
.ic-tree-icon.is-file.is-text { -webkit-mask-image: var(--icon-file-text); mask-image: var(--icon-file-text); }
/* An AGENT is its one manifest file: `file-cog`, drawn at 16px like the cog folder. */
.ic-tree-icon.is-file.is-agent { -webkit-mask-image: var(--icon-file-cog); mask-image: var(--icon-file-cog);
  width: 16px; height: 16px; margin: -1px; }
.ic-tree-icon.is-file.is-code { -webkit-mask-image: var(--icon-file-code); mask-image: var(--icon-file-code); }
.ic-tree-icon.is-file.is-data { -webkit-mask-image: var(--icon-file-braces-corner); mask-image: var(--icon-file-braces-corner); }
.ic-tree-icon.is-file.is-binary { -webkit-mask-image: var(--icon-file-digit); mask-image: var(--icon-file-digit); }
/* The version beside the name is a tag, not a column: a notch smaller than a row badge. */
.ic-tree .tr-badge.ic-tree-ver { font-size: var(--text-2xs); font-weight: 600; line-height: 1.3;
  padding: 0 4px; }
.ic-tree-count { color: var(--muted); flex: 0 0 auto; }
.ic-tree-name { flex: 0 1 auto; min-width: 0; max-width: 18rem; }
.ic-tree-note .ic-tree-icon { visibility: hidden; }
.ic-tree-note .ic-tree-name { color: var(--muted); font-family: inherit; }

/* ── AI-assisted creation (window.jmAiAssist, cli/web/static/ai-assist.js) ─────────────────────
   Framework rule (docs/WEB-DESIGN.md "AI-assisted creation"): every popup that can create with AI
   carries ONE bar in its header, top right — the switch, the sparkles icon beside it, a label.
   Off = the manual fields; on = one prompt box. Unavailable = the whole bar greyed out with its
   reason as the tooltip. (Without a header it falls back to the top of the body.) */
.ai-assist-bar { display: flex; align-items: center; gap: 8px; padding-bottom: 10px; margin-bottom: 10px;
  border-bottom: 1px solid var(--border); }
.ai-assist-icon { flex: 0 0 auto; width: 16px; height: 16px; background-color: var(--accent);
  -webkit-mask: var(--icon-sparkles) center/contain no-repeat; mask: var(--icon-sparkles) center/contain no-repeat; }
.ai-assist-label { font-size: var(--text-sm); font-weight: 600; color: var(--text); cursor: pointer; }
.ai-assist-bar.is-disabled { opacity: 0.5; cursor: not-allowed; }
.ai-assist-bar.is-disabled .ai-assist-icon { background-color: var(--muted); }
.ai-assist-bar.is-disabled .ai-assist-label,
.ai-assist-bar.is-disabled .ui-switch-slider { cursor: not-allowed; }
.ai-assist-panel[hidden], .ai-assist-manual[hidden] { display: none; }
/* In the header: pushed to the right edge, just before the × close; no body separator. */
.modal-header .ai-assist-bar.in-header { margin: 0 8px 0 auto; padding: 0; border-bottom: 0; }

/* ── AI refine (window.jmAiRefine, cli/web/static/ai-assist.js) ─────────────────────────────────
   Framework AI function #2: refining an EXISTING item. The "AI refine" button sits in the edit
   popup's header top right (before the ×) — a `.btn.btn-cfg` with the create bar's sparkles icon;
   unavailable = greyed with its reason as the tooltip. The item's row shows its refine state in its
   `.cfg-ctrl-slot` (`.ai-refine-state`); the revision popup shows the diff with a +N/−M stat and a
   Combined / Side by side switch. Reuses .modal-top, .modal-resizable, .modal-scroll-body,
   .modal-foot, .btn-push-right and .td-id-title; tokens only. */
.modal-header .ai-refine-btn { margin: 0 8px 0 auto; flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px; }
.ai-refine-btn .ai-assist-icon { width: 14px; height: 14px; }
.ai-refine-btn.is-disabled { opacity: 0.5; cursor: not-allowed; }
.ai-refine-btn.is-disabled .ai-assist-icon { background-color: var(--muted); }
.ai-refine-state { display: inline-flex; align-items: center; gap: 6px; min-width: 0; margin-right: 6px; }
.ai-refine-err { max-width: 16rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ai-refine-head { align-items: center; }
.ai-refine-stat { flex: 0 0 auto; display: inline-flex; gap: 6px; font-family: var(--font-mono); font-size: var(--text-xs); font-weight: 600; }
.ai-refine-add { color: var(--green); }
.ai-refine-del { color: var(--red); }
.ai-refine-view { flex: 0 0 auto; display: inline-flex; }
.ai-refine-view .btn.btn-cfg { border-radius: 0; }
.ai-refine-view .btn.btn-cfg:first-child { border-radius: var(--r-sm) 0 0 var(--r-sm); }
.ai-refine-view .btn.btn-cfg:last-child { border-radius: 0 var(--r-sm) var(--r-sm) 0; margin-left: -1px; }
.ai-refine-view .btn.btn-cfg[aria-pressed="true"] { background: color-mix(in srgb, var(--accent) 14%, var(--panel)); border-color: var(--accent); color: var(--accent); }
.ai-refine-review .modal-box.modal-resizable { width: 960px; height: 70vh; }
.ai-refine-diff { min-height: 8rem; }
.ai-refine-plain { margin: 0; white-space: pre-wrap; }

/* ── Line diff (window.jmDiff, cli/web/static/diff.js) ───────────────────────────────────────
   The framework before/after line diff (plan #P43 "AI refine"). `.diff-view.is-unified` = one
   column of `.diff-line` rows; `.diff-view.is-split` = `.diff-row`s of two `.diff-cell`s (old left,
   new right). Every line: line-number gutter (`.diff-ln`), sign (`.diff-sign`: + / − / blank),
   `.diff-text`. Added = green wash, removed = red wash. Long lines scroll horizontally; the HEIGHT
   cap belongs to the host (popup), not to the view. On a narrow screen the split view stacks into
   a unified-looking single column. */
.diff-view { font-family: var(--font-mono); font-size: var(--text-xs); line-height: 1.5;
  color: var(--text); background: var(--bg); border: 1px solid var(--border); border-radius: var(--r-md);
  overflow: auto; }
.diff-view.is-unified { display: grid; grid-template-columns: minmax(max-content, 1fr); }
.diff-line, .diff-cell { display: flex; align-items: flex-start; }
.diff-ln { flex: 0 0 auto; min-width: 4ch; padding: 0 6px; text-align: right; color: var(--muted);
  user-select: none; border-right: 1px solid var(--hairline); }
.diff-sign { flex: 0 0 auto; width: 2ch; text-align: center; user-select: none; color: var(--muted); }
.diff-text { flex: 1 1 auto; white-space: pre; padding-right: 12px; }
.diff-line.is-add, .diff-cell.is-add { background: color-mix(in srgb, var(--green) 14%, transparent); }
.diff-line.is-del, .diff-cell.is-del { background: color-mix(in srgb, var(--red) 14%, transparent); }
.diff-line.is-add .diff-sign, .diff-cell.is-add .diff-sign { color: var(--green); font-weight: 600; }
.diff-line.is-del .diff-sign, .diff-cell.is-del .diff-sign { color: var(--red); font-weight: 600; }
.diff-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.diff-cell { min-width: 0; }
.diff-cell.is-new { border-left: 1px solid var(--border); }
.diff-cell .diff-text { overflow-x: auto; }
.diff-cell.is-empty { background: color-mix(in srgb, var(--muted) 6%, transparent); }
.diff-skip { padding: 2px 12px; color: var(--muted); cursor: pointer; user-select: none;
  background: color-mix(in srgb, var(--muted) 8%, transparent);
  border-top: 1px solid var(--hairline); border-bottom: 1px solid var(--hairline); }
.diff-skip:hover, .diff-skip:focus-visible { color: var(--text); }
.diff-empty { padding: 8px 12px; color: var(--muted); font-family: var(--font-sans); }
@media (max-width: 640px) {
  .diff-row { grid-template-columns: minmax(0, 1fr); }
  .diff-cell.is-new { border-left: 0; }
  .diff-row .diff-cell.is-empty, .diff-row.is-ctx .diff-cell.is-new { display: none; }
}
