/*
 * Xore Theme
 * A quiet, warm desktop-productivity interface for self-hosted applications.
 * Framework-free and safe to copy into another repository.
 */

/* Typography is web-loaded, but NOT from here. An @import at the top of the
   main stylesheet is the worst place for it: the browser has to fetch and
   parse this file before it even discovers the import, then fetch the font
   CSS, then the font files — three serial round trips on the critical path,
   repeated on every page load, with first paint of the whole application
   blocked behind a third-party host.

   Consumers add these two lines to <head> instead, so the font CSS is
   discovered immediately and resolves in parallel with this stylesheet:

     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
     <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&family=Fira+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap">

   The --font-* stacks below carry platform-native fallbacks, so a blocked or
   offline font host degrades rather than breaks. See docs/ADOPTION.md. */


/* ── Mode ──────────────────────────────────────────────────────────────
   :root declares both schemes, so the operating system's preference wins
   by default; [data-theme] pins an explicit choice. Every colour token
   below resolves through light-dark() against this, which is what lets a
   theme be one block instead of the three it used to take (:root for dark,
   [data-theme="light"], and a prefers-color-scheme copy of the second). */
:root { color-scheme: light dark; }
[data-theme="light"] { color-scheme: light; }
[data-theme="dark"] { color-scheme: dark; }

:root {
  /* The semantic status ramp is theme-scoped and lives in the generated
     block below, one set per theme. Each keeps its meaning-bearing hue
     family -- green reads as success, red as danger -- but is nudged toward
     its theme's ground so it sits in the theme rather than on top of it,
     then AA-tuned against that theme's own surfaces. */

  /* Fallback inks drawn on top of saturated or solid fills, where following
     the surface makes no sense. Not fixed values any more (#134): the
     generated blocks below recast both against each theme's actual
     --switch-on paint, because that fill *is* the link colour -- light
     pastels under dark modes -- and fixed white cannot serve as ink there.
     These literals remain what renders if the generated surface is ever
     stripped out, which only helps where light themes' darker fills are
     painted. */
  --control-knob: #ffffff;
  --text-on-status: #ffffff;

  /* Backdrop for artwork that fills its own container -- promo media and
     the auth split aside. Both cover it with an <img>, so what this paints
     is the moment before that image loads, and any letterboxing around it.
     It used to be a single cream value in all three token blocks: a token
     shaped like a theme token that never varied, which flashed a light
     plate into a dark shell. Now it follows the mode, keeping the brand
     cream in light. A theme may override it. */
  --artwork-plate: light-dark(#e9e3d6, #2a2724);
  /* Deprecated alias. Prefer --artwork-plate; kept so consumers reading the
     old name keep working. */
  --artwork-bg: var(--artwork-plate);

  /* ── Elevation geometry ──────────────────────────────────────────────
     The colours are per-theme (see the generated block below); the shape is
     shared. --shadow-inset and --shadow-pill are new: six rules used to
     hardcode rgba(0, 0, 0, ...) box-shadows and so never followed the theme
     at all, which was visibly wrong in light mode long before themes. */
  --shadow-raised: 0 1px 2px var(--shadow-raised-near), 0 8px 26px var(--shadow-raised-far);
  --shadow-dialog: 0 2px 6px var(--shadow-dialog-near), 0 24px 80px var(--shadow-dialog-far);
  --shadow-inset: 0 1px 4px var(--shadow-inset-color);
  --shadow-pill: 0 3px 12px var(--shadow-pill-color);

  /* Design refresh (APIARY design lab, Xore's picks): larger, calmer radii
     in the claude.ai direction. Every consumer shares the shift -- that is
     the point of the refresh, not a side effect. */
  --radius-xs: 6px;
  --radius-control: 12px;
  --radius-panel: 16px;
  --radius-dialog: 24px;
  --radius-pill: 999px;

  /* Xore typography is web-loaded above, with platform-native fallbacks for
     offline or blocked requests. */
  --font-sans: "Fira Sans", "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
  --font-display: "Space Grotesk", "Fira Sans", "Segoe UI", system-ui, sans-serif;
  --font-mono: "Fira Code", "SFMono-Regular", "Cascadia Code", "Roboto Mono", ui-monospace, monospace;
  /* Serif voice for humane, claude.ai-style greeting headlines (design
     refresh); platform serifs only, nothing web-loaded. */
  --font-serif: Georgia, "Times New Roman", "DejaVu Serif", serif;
  /* A standard ease-out curve (fast start, gentle settle) rather than the
     linear-ish default `ease` keyword -- every hover/focus/panel transition
     in the theme reads slightly more deliberate with the same durations.
     Exposed on its own so the entrance-animation utilities below (and any
     consumer's own keyframes) can reuse the exact same curve. */
  --ease-out: cubic-bezier(0, 0, 0.2, 1);
  --transition-fast: 120ms var(--ease-out);
  --transition: 160ms var(--ease-out);

  --toolbar-height: 32px;
  --sidebar-width: 224px;
  --sidebar-width-wide: 288px;
  --content-width: 1120px;

  /* Spacing scale — inspired by consistent layout rhythms */
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 12px;
  --space-lg: 16px;
  --space-xl: 24px;
  --space-2xl: 32px;
  --space-3xl: 40px;

  /* Typography scale — explicit sizes for predictable hierarchy */
  --font-size-xs: 11px;
  --font-size-caption: 12px;
  --font-size-footnote: 13px;
  --font-size-body: 14px;
  --font-size-lg: 16px;
  --font-size-xl: 18px;

  /* Compatibility aliases used by existing applications. */
  --bg: var(--bg-000);
  --bg-primary: var(--bg-000);
  --bg-secondary: var(--bg-200);
  --green: var(--success);
  --green-subtle: var(--success-soft);
  --blue: var(--info);
  --blue-subtle: var(--info-soft);
  --orange: var(--warning);
  --orange-subtle: var(--warning-soft);
  --red: var(--danger);
  --red-subtle: var(--danger-soft);
  --accent-subtle: var(--accent-soft);
  --radius-sm: var(--radius-xs);
  --radius-md: 8px;
  --shadow-card: none;
  --shadow-modal: var(--shadow-dialog);
}

/* ── Mode-dependent values that are not colours ────────────────────────
   light-dark() takes <color> only, so the handful of tokens whose value is
   a filter or a display keyword still need a per-mode rule. Keeping them as
   *intent* tokens is the point: rules elsewhere read the token instead of
   matching on [data-theme="dark"] themselves, so a theme beyond light/dark
   cannot silently miss them. That is exactly how the Leaflet basemap ended
   up unfiltered and the brand mark ended up wrong outside the two names the
   selectors happened to enumerate. */
:root {
  --basemap-filter: none;
  --theme-art-dark-display: none;
  --theme-art-light-display: block;
}
[data-theme="dark"] {
  --basemap-filter: brightness(0.62) invert(1) contrast(1.25) hue-rotate(180deg) saturate(0.35);
  --theme-art-dark-display: block;
  --theme-art-light-display: none;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --basemap-filter: brightness(0.62) invert(1) contrast(1.25) hue-rotate(180deg) saturate(0.35);
    --theme-art-dark-display: block;
    --theme-art-light-display: none;
  }
}

/* >>> GENERATED THEME SURFACES — scripts/gen-themes.mjs — do not edit by hand */
/*
 * Generated — edit scripts/theme-tokens.mjs and re-run
 * `node scripts/gen-themes.mjs`. `node scripts/check-contrast.mjs`
 * enforces WCAG AA across every theme and mode in CI.
 *
 * A theme owns the whole surface: ground, sidebar, toolbar, the surface
 * ramp, borders, the text ramp, elevation and the accent family. The
 * semantic status ramp (success/info/warning/danger/critical) is shared
 * tuned per theme: each status keeps its meaning-bearing hue family
 * (green reads as success, red as danger) but is nudged toward the
 * theme's ground so it sits in the theme rather than on top of it.
 *
 * Mode is decided by color-scheme: :root declares `light dark`, so the
 * system preference wins by default and [data-theme="light"|"dark"]
 * pins it. That is why one block covers both modes.
 *
 * `claude` is the default; its values are byte-identical to the ones
 * that shipped before this block became generated, and the generator
 * asserts that identity on every run.
 */

/* Claude — The default. Warm charcoal and ivory with the copper brand accent — identity values, unchanged. */
:root,
[data-hp-theme="claude"],
[data-hp-palette="claude"] {
  --bg-000: light-dark(#f7f6f2, #20201f);
  --bg-sidebar: light-dark(#f0efeb, #1e1e1c);
  --bg-toolbar: light-dark(#f5f4f0, #242422);
  --bg-100: light-dark(#fbfaf7, #242422);
  --bg-200: light-dark(#f4f2ed, #2c2c2a);
  --bg-300: light-dark(#ebe9e3, #343432);
  --bg-400: light-dark(#e1dfd8, #3d3d3b);
  --bg-500: light-dark(#e9e7e1, #42423f);
  --bg-raised: light-dark(#fffefa, #383835);
  --border-100: light-dark(rgba(34, 31, 28, 0.07), rgba(255, 255, 255, 0.05));
  --border-200: light-dark(rgba(34, 31, 28, 0.14), rgba(255, 255, 255, 0.11));
  --border-focus: light-dark(#af593f, #db7d5f);
  --text-000: light-dark(#2f2b27, #e9e6df);
  --text-100: light-dark(#68615a, #a8a49c);
  --text-200: light-dark(#66615b, #a5a9a6);
  --text-300: light-dark(#aaa49d, #68635e);
  --accent: light-dark(#af593f, #d97757);
  --accent-hover: light-dark(#a34d35, #e18768);
  --accent-pressed: light-dark(#93422d, #c9684b);
  --accent-soft: light-dark(rgba(199, 101, 72, 0.12), rgba(217, 119, 87, 0.14));
  --accent-text-on-soft: light-dark(#a2472b, #e38f74);
  --text-on-accent: light-dark(#fffaf5, #211a17);
  --text-link: light-dark(#9b4f3a, #de866b);
  --text-link-hover: light-dark(#854432, #e49a84);
  --switch-on: light-dark(#9b4f3a, #de866b);
  --text-on-status: light-dark(#ffffff, #1c1613);
  --control-knob: light-dark(#ffffff, #1c1613);
  --btn-inverted-bg: light-dark(#262421, #f5f4ef);
  --btn-inverted-bg-hover: light-dark(#151412, #ffffff);
  --btn-inverted-text: light-dark(#f5f4ef, #161513);
  --overlay-bg: light-dark(rgba(34, 31, 28, 0.36), rgba(0, 0, 0, 0.5));
  --success: light-dark(#3f8764, #79c99e);
  --success-soft: light-dark(rgba(63, 135, 100, 0.18), rgba(121, 201, 158, 0.2));
  --success-text-on-soft: light-dark(#2b6246, #8dcfad);
  --success-badge-fill: light-dark(#4f9f7c, #79c99e);
  --info: light-dark(#487eaa, #78a9d4);
  --info-soft: light-dark(rgba(72, 126, 170, 0.18), rgba(120, 169, 212, 0.2));
  --info-text-on-soft: light-dark(#335a7a, #a0c0de);
  --info-badge-fill: light-dark(#6795c0, #78a9d4);
  --warning: light-dark(#9b6b25, #deb36a);
  --warning-soft: light-dark(rgba(155, 107, 37, 0.19), rgba(222, 179, 106, 0.21));
  --warning-text-on-soft: light-dark(#734d1b, #e4bf82);
  --warning-badge-fill: light-dark(#bf862e, #deb36a);
  --danger: light-dark(#b34f4c, #dc7774);
  --danger-soft: light-dark(rgba(179, 79, 76, 0.2), rgba(220, 119, 116, 0.21));
  --danger-text-on-soft: light-dark(#8b3b3a, #e8a7a4);
  --danger-badge-fill: light-dark(#c87f7c, #dc7774);
  --critical: light-dark(#c53030, #ff5c5c);
  --critical-soft: light-dark(rgba(197, 48, 48, 0.13), rgba(255, 92, 92, 0.16));
  --critical-text-on-soft: light-dark(#a92929, #ff8e8e);
  --terminal-bg: light-dark(#474336, #181817);
  --terminal-fg: light-dark(#e6e6e6, #e9e6df);
  --framebuffer-bg: light-dark(#3e3b2f, #101010);
  --diagram-plate: #ffffff;
  --shadow-raised-near: light-dark(rgba(48, 42, 36, 0.11), rgba(0, 0, 0, 0.18));
  --shadow-raised-far: light-dark(rgba(48, 42, 36, 0.16), rgba(0, 0, 0, 0.24));
  --shadow-dialog-near: light-dark(rgba(48, 42, 36, 0.12), rgba(0, 0, 0, 0.3));
  --shadow-dialog-far: light-dark(rgba(48, 42, 36, 0.19), rgba(0, 0, 0, 0.52));
  --shadow-inset-color: light-dark(rgba(48, 42, 36, 0.12), rgba(0, 0, 0, 0.18));
  --shadow-pill-color: light-dark(rgba(48, 42, 36, 0.16), rgba(0, 0, 0, 0.24));
}

/* Slate — Cool zinc/slate neutrals — the dominant direction for technology products. */
[data-hp-theme="slate"],
[data-hp-palette="slate"] {
  --bg-000: light-dark(#f2f4f7, #1d1f22);
  --bg-sidebar: light-dark(#ebedf0, #171c23);
  --bg-toolbar: light-dark(#f0f2f5, #1d2229);
  --bg-100: light-dark(#f7f9fb, #1d2229);
  --bg-200: light-dark(#edf0f4, #252a31);
  --bg-300: light-dark(#e3e7eb, #2d3239);
  --bg-400: light-dark(#d8dce1, #363b42);
  --bg-500: light-dark(#e1e5e9, #383f49);
  --bg-raised: light-dark(#fafcff, #2e353f);
  --border-100: light-dark(rgba(28, 31, 34, 0.07), rgba(255, 255, 255, 0.05));
  --border-200: light-dark(rgba(28, 31, 34, 0.14), rgba(255, 255, 255, 0.11));
  --border-focus: light-dark(#44618a, #8aa2c0);
  --text-000: light-dark(#272b2f, #dfe3e9);
  --text-100: light-dark(#5a6068, #a2a7ac);
  --text-200: light-dark(#5b6066, #a5a7a9);
  --text-300: light-dark(#9da3aa, #5e6268);
  --accent: light-dark(#44618a, #8aa2c0);
  --accent-hover: light-dark(#3d577c, #9bafc9);
  --accent-pressed: light-dark(#374e6f, #7995b7);
  --accent-soft: light-dark(rgba(68, 97, 138, 0.13), rgba(138, 162, 192, 0.16));
  --accent-text-on-soft: light-dark(#3e5b81, #9cb4cc);
  --text-on-accent: light-dark(#ffffff, #1c1613);
  --text-link: light-dark(#44618a, #8aa2c0);
  --text-link-hover: light-dark(#3a5376, #9eb2cb);
  --switch-on: light-dark(#44618a, #8aa2c0);
  --text-on-status: light-dark(#ffffff, #1c1613);
  --control-knob: light-dark(#ffffff, #1c1613);
  --btn-inverted-bg: light-dark(#212326, #eff2f5);
  --btn-inverted-bg-hover: light-dark(#121315, #ffffff);
  --btn-inverted-text: light-dark(#eff2f5, #131416);
  --overlay-bg: light-dark(rgba(28, 31, 34, 0.36), rgba(0, 0, 0, 0.5));
  --success: light-dark(#3f8764, #79c99e);
  --success-soft: light-dark(rgba(63, 135, 100, 0.18), rgba(121, 201, 158, 0.2));
  --success-text-on-soft: light-dark(#295e43, #89ceaa);
  --success-badge-fill: light-dark(#4f9f7c, #79c99e);
  --info: light-dark(#487eaa, #78a9d4);
  --info-soft: light-dark(rgba(72, 126, 170, 0.18), rgba(120, 169, 212, 0.2));
  --info-text-on-soft: light-dark(#335a7a, #9cbedd);
  --info-badge-fill: light-dark(#6795c0, #78a9d4);
  --warning: light-dark(#9b6b25, #deb36a);
  --warning-soft: light-dark(rgba(155, 107, 37, 0.19), rgba(222, 179, 106, 0.21));
  --warning-text-on-soft: light-dark(#734d1b, #e2bb7a);
  --warning-badge-fill: light-dark(#bf862e, #deb36a);
  --danger: light-dark(#b34f4c, #dc7774);
  --danger-soft: light-dark(rgba(179, 79, 76, 0.2), rgba(220, 119, 116, 0.21));
  --danger-text-on-soft: light-dark(#873938, #e7a3a0);
  --danger-badge-fill: light-dark(#c87f7c, #dc7774);
  --critical: light-dark(#c53030, #ff5c5c);
  --critical-soft: light-dark(rgba(197, 48, 48, 0.13), rgba(255, 92, 92, 0.16));
  --critical-text-on-soft: light-dark(#a52828, #ff8989);
  --terminal-bg: light-dark(#363d47, #13171c);
  --terminal-fg: light-dark(#e6e6e6, #dfe3e9);
  --framebuffer-bg: light-dark(#2f363e, #0d1013);
  --diagram-plate: #ffffff;
  --shadow-raised-near: light-dark(rgba(36, 41, 48, 0.11), rgba(0, 0, 0, 0.18));
  --shadow-raised-far: light-dark(rgba(36, 41, 48, 0.16), rgba(0, 0, 0, 0.24));
  --shadow-dialog-near: light-dark(rgba(36, 41, 48, 0.12), rgba(0, 0, 0, 0.3));
  --shadow-dialog-far: light-dark(rgba(36, 41, 48, 0.19), rgba(0, 0, 0, 0.52));
  --shadow-inset-color: light-dark(rgba(36, 41, 48, 0.12), rgba(0, 0, 0, 0.18));
  --shadow-pill-color: light-dark(rgba(36, 41, 48, 0.16), rgba(0, 0, 0, 0.24));
}

/* Sage — Olive and stone — the earth-tone direction, calm and low-fatigue. */
[data-hp-theme="sage"],
[data-hp-palette="sage"] {
  --bg-000: light-dark(#f2f7f2, #1d221e);
  --bg-sidebar: light-dark(#ebf0eb, #182219);
  --bg-toolbar: light-dark(#f0f5f0, #1e281f);
  --bg-100: light-dark(#f7fbf7, #1e281f);
  --bg-200: light-dark(#edf4ed, #263027);
  --bg-300: light-dark(#e3ebe3, #2e382f);
  --bg-400: light-dark(#d8e1d9, #374138);
  --bg-500: light-dark(#e1e9e1, #39483a);
  --bg-raised: light-dark(#fafffa, #2f3e31);
  --border-100: light-dark(rgba(28, 34, 28, 0.07), rgba(255, 255, 255, 0.05));
  --border-200: light-dark(rgba(28, 34, 28, 0.14), rgba(255, 255, 255, 0.11));
  --border-focus: light-dark(#4d7a42, #8fb27b);
  --text-000: light-dark(#272f27, #dfe9e0);
  --text-100: light-dark(#566256, #abb2ac);
  --text-200: light-dark(#596359, #aeb1ae);
  --text-300: light-dark(#9daa9d, #5e685f);
  --accent: light-dark(#4d7a42, #8fb27b);
  --accent-hover: light-dark(#456d3b, #9dbb8b);
  --accent-pressed: light-dark(#3c6034, #81a96b);
  --accent-soft: light-dark(rgba(77, 122, 66, 0.13), rgba(143, 178, 123, 0.16));
  --accent-text-on-soft: light-dark(#3f6534, #a4c090);
  --text-on-accent: light-dark(#ffffff, #1c1613);
  --text-link: light-dark(#456e3a, #8fb27b);
  --text-link-hover: light-dark(#385a2f, #9fbd8e);
  --switch-on: light-dark(#456e3a, #8fb27b);
  --text-on-status: light-dark(#ffffff, #1c1613);
  --control-knob: light-dark(#ffffff, #1c1613);
  --btn-inverted-bg: light-dark(#212621, #eff5f0);
  --btn-inverted-bg-hover: light-dark(#121512, #ffffff);
  --btn-inverted-text: light-dark(#eff5ef, #131613);
  --overlay-bg: light-dark(rgba(28, 34, 28, 0.36), rgba(0, 0, 0, 0.5));
  --success: light-dark(#3f8764, #79c99e);
  --success-soft: light-dark(rgba(63, 135, 100, 0.18), rgba(121, 201, 158, 0.2));
  --success-text-on-soft: light-dark(#2b6246, #91d1b0);
  --success-badge-fill: light-dark(#4f9f7c, #79c99e);
  --info: light-dark(#487eaa, #78a9d4);
  --info-soft: light-dark(rgba(72, 126, 170, 0.18), rgba(120, 169, 212, 0.2));
  --info-text-on-soft: light-dark(#335a7a, #a4c3df);
  --info-badge-fill: light-dark(#6795c0, #78a9d4);
  --warning: light-dark(#9b6b25, #deb36a);
  --warning-soft: light-dark(rgba(155, 107, 37, 0.19), rgba(222, 179, 106, 0.21));
  --warning-text-on-soft: light-dark(#734d1b, #e5c186);
  --warning-badge-fill: light-dark(#bf862e, #deb36a);
  --danger: light-dark(#b34f4c, #dc7774);
  --danger-soft: light-dark(rgba(179, 79, 76, 0.2), rgba(220, 119, 116, 0.21));
  --danger-text-on-soft: light-dark(#8b3b3a, #e9aba8);
  --danger-badge-fill: light-dark(#c87f7c, #dc7774);
  --critical: light-dark(#c53030, #ff5c5c);
  --critical-soft: light-dark(rgba(197, 48, 48, 0.13), rgba(255, 92, 92, 0.16));
  --critical-text-on-soft: light-dark(#a52828, #ff9393);
  --terminal-bg: light-dark(#364738, #141b15);
  --terminal-fg: light-dark(#e6e6e6, #dfe9e0);
  --framebuffer-bg: light-dark(#2f3e31, #0d120e);
  --diagram-plate: #ffffff;
  --shadow-raised-near: light-dark(rgba(36, 48, 36, 0.11), rgba(0, 0, 0, 0.18));
  --shadow-raised-far: light-dark(rgba(36, 48, 36, 0.16), rgba(0, 0, 0, 0.24));
  --shadow-dialog-near: light-dark(rgba(36, 48, 36, 0.12), rgba(0, 0, 0, 0.3));
  --shadow-dialog-far: light-dark(rgba(36, 48, 36, 0.19), rgba(0, 0, 0, 0.52));
  --shadow-inset-color: light-dark(rgba(36, 48, 36, 0.12), rgba(0, 0, 0, 0.18));
  --shadow-pill-color: light-dark(rgba(36, 48, 36, 0.16), rgba(0, 0, 0, 0.24));
}

/* Lavender — Digital lavender on a cool violet-grey ground. */
[data-hp-theme="lavender"],
[data-hp-palette="lavender"] {
  --bg-000: light-dark(#f4f2f7, #1f1d22);
  --bg-sidebar: light-dark(#edebf0, #1c1822);
  --bg-toolbar: light-dark(#f2f0f5, #221e28);
  --bg-100: light-dark(#f8f7fb, #221e28);
  --bg-200: light-dark(#efeef3, #2a2630);
  --bg-300: light-dark(#e6e4ea, #322e38);
  --bg-400: light-dark(#dbd9e0, #3b3741);
  --bg-500: light-dark(#e4e2e8, #3e3849);
  --bg-raised: light-dark(#fcfaff, #342e3f);
  --border-100: light-dark(rgba(30, 28, 34, 0.07), rgba(255, 255, 255, 0.05));
  --border-200: light-dark(rgba(30, 28, 34, 0.14), rgba(255, 255, 255, 0.11));
  --border-focus: light-dark(#6d4fc4, #ab93e3);
  --text-000: light-dark(#29272f, #e3dfe9);
  --text-100: light-dark(#5e5a68, #a6a2ac);
  --text-200: light-dark(#5e5b66, #a6a5a9);
  --text-300: light-dark(#a19daa, #625e68);
  --accent: light-dark(#6d4fc4, #ab93e3);
  --accent-hover: light-dark(#6040bf, #bba7e8);
  --accent-pressed: light-dark(#593baf, #9b7fde);
  --accent-soft: light-dark(rgba(109, 79, 196, 0.13), rgba(171, 147, 227, 0.16));
  --accent-text-on-soft: light-dark(#613fc0, #baa7e8);
  --text-on-accent: light-dark(#ffffff, #1c1613);
  --text-link: light-dark(#6d4fc4, #ab93e3);
  --text-link-hover: light-dark(#5d3db7, #beabe9);
  --switch-on: light-dark(#6d4fc4, #ab93e3);
  --text-on-status: light-dark(#ffffff, #1c1613);
  --control-knob: light-dark(#ffffff, #1c1613);
  --btn-inverted-bg: light-dark(#232126, #f1eff5);
  --btn-inverted-bg-hover: light-dark(#131215, #ffffff);
  --btn-inverted-text: light-dark(#f1eff5, #141316);
  --overlay-bg: light-dark(rgba(30, 28, 34, 0.36), rgba(0, 0, 0, 0.5));
  --success: light-dark(#3f8764, #79c99e);
  --success-soft: light-dark(rgba(63, 135, 100, 0.18), rgba(121, 201, 158, 0.2));
  --success-text-on-soft: light-dark(#295e43, #81cba4);
  --success-badge-fill: light-dark(#4f9f7c, #79c99e);
  --info: light-dark(#487eaa, #78a9d4);
  --info-soft: light-dark(rgba(72, 126, 170, 0.18), rgba(120, 169, 212, 0.2));
  --info-text-on-soft: light-dark(#335a7a, #98bbdc);
  --info-badge-fill: light-dark(#6795c0, #78a9d4);
  --warning: light-dark(#9b6b25, #deb36a);
  --warning-soft: light-dark(rgba(155, 107, 37, 0.19), rgba(222, 179, 106, 0.21));
  --warning-text-on-soft: light-dark(#734d1b, #e1b976);
  --warning-badge-fill: light-dark(#bf862e, #deb36a);
  --danger: light-dark(#b34f4c, #dc7774);
  --danger-soft: light-dark(rgba(179, 79, 76, 0.2), rgba(220, 119, 116, 0.21));
  --danger-text-on-soft: light-dark(#873938, #e7a3a0);
  --danger-badge-fill: light-dark(#c87f7c, #dc7774);
  --critical: light-dark(#c53030, #ff5c5c);
  --critical-soft: light-dark(rgba(197, 48, 48, 0.13), rgba(255, 92, 92, 0.16));
  --critical-text-on-soft: light-dark(#a52828, #ff8989);
  --terminal-bg: light-dark(#3c3845, #17141b);
  --terminal-fg: light-dark(#e6e6e6, #e3dfe9);
  --framebuffer-bg: light-dark(#35313d, #100d12);
  --diagram-plate: #ffffff;
  --shadow-raised-near: light-dark(rgba(40, 36, 48, 0.11), rgba(0, 0, 0, 0.18));
  --shadow-raised-far: light-dark(rgba(40, 36, 48, 0.16), rgba(0, 0, 0, 0.24));
  --shadow-dialog-near: light-dark(rgba(40, 36, 48, 0.12), rgba(0, 0, 0, 0.3));
  --shadow-dialog-far: light-dark(rgba(40, 36, 48, 0.19), rgba(0, 0, 0, 0.52));
  --shadow-inset-color: light-dark(rgba(40, 36, 48, 0.12), rgba(0, 0, 0, 0.18));
  --shadow-pill-color: light-dark(rgba(40, 36, 48, 0.16), rgba(0, 0, 0, 0.24));
}

/* Lime — Fintech lime against cool whites — bright and dashboard-first, the ground stays crisp. */
[data-hp-theme="lime"],
[data-hp-palette="lime"] {
  --bg-000: light-dark(#f7f8f1, #21221d);
  --bg-sidebar: light-dark(#f0f1ea, #1f2218);
  --bg-toolbar: light-dark(#f5f6ef, #25281e);
  --bg-100: light-dark(#fbfcf6, #25281e);
  --bg-200: light-dark(#f3f5ec, #2d3026);
  --bg-300: light-dark(#eaede1, #35382e);
  --bg-400: light-dark(#e0e3d6, #3e4137);
  --bg-500: light-dark(#e8ebdf, #444839);
  --bg-raised: light-dark(#fefffa, #3a3e2f);
  --border-100: light-dark(rgba(33, 34, 28, 0.07), rgba(255, 255, 255, 0.05));
  --border-200: light-dark(rgba(33, 34, 28, 0.14), rgba(255, 255, 255, 0.11));
  --border-focus: light-dark(#5f7d1f, #b3cf5a);
  --text-000: light-dark(#2d2f27, #e6e9df);
  --text-100: light-dark(#5f6256, #b0b2ab);
  --text-200: light-dark(#616359, #b1b1ae);
  --text-300: light-dark(#a7aa9d, #65685e);
  --accent: light-dark(#5f7d1f, #b3cf5a);
  --accent-hover: light-dark(#536d1b, #bcd56e);
  --accent-pressed: light-dark(#465c17, #aac946);
  --accent-soft: light-dark(rgba(95, 125, 31, 0.13), rgba(179, 207, 90, 0.16));
  --accent-text-on-soft: light-dark(#4d6519, #b3cf5a);
  --text-on-accent: light-dark(#ffffff, #1c1613);
  --text-link: light-dark(#536d1b, #b3cf5a);
  --text-link-hover: light-dark(#405415, #bed672);
  --switch-on: light-dark(#536d1b, #b3cf5a);
  --text-on-status: light-dark(#ffffff, #1c1613);
  --control-knob: light-dark(#ffffff, #1c1613);
  --btn-inverted-bg: light-dark(#252621, #f3f5ef);
  --btn-inverted-bg-hover: light-dark(#141512, #ffffff);
  --btn-inverted-text: light-dark(#f4f5ef, #151613);
  --overlay-bg: light-dark(rgba(33, 34, 28, 0.36), rgba(0, 0, 0, 0.5));
  --success: light-dark(#3f8764, #79c99e);
  --success-soft: light-dark(rgba(63, 135, 100, 0.18), rgba(121, 201, 158, 0.2));
  --success-text-on-soft: light-dark(#2b6246, #95d3b3);
  --success-badge-fill: light-dark(#4f9f7c, #79c99e);
  --info: light-dark(#487eaa, #78a9d4);
  --info-soft: light-dark(rgba(72, 126, 170, 0.18), rgba(120, 169, 212, 0.2));
  --info-text-on-soft: light-dark(#355d7e, #a4c3df);
  --info-badge-fill: light-dark(#6795c0, #78a9d4);
  --warning: light-dark(#9b6b25, #deb36a);
  --warning-soft: light-dark(rgba(155, 107, 37, 0.19), rgba(222, 179, 106, 0.21));
  --warning-text-on-soft: light-dark(#77501c, #e6c38a);
  --warning-badge-fill: light-dark(#bf862e, #deb36a);
  --danger: light-dark(#b34f4c, #dc7774);
  --danger-soft: light-dark(rgba(179, 79, 76, 0.2), rgba(220, 119, 116, 0.21));
  --danger-text-on-soft: light-dark(#8b3b3a, #e9aba8);
  --danger-badge-fill: light-dark(#c87f7c, #dc7774);
  --critical: light-dark(#c53030, #ff5c5c);
  --critical-soft: light-dark(rgba(197, 48, 48, 0.13), rgba(255, 92, 92, 0.16));
  --critical-text-on-soft: light-dark(#a92929, #ff9393);
  --terminal-bg: light-dark(#454a33, #191b14);
  --terminal-fg: light-dark(#e6e6e6, #e6e9df);
  --framebuffer-bg: light-dark(#3d412d, #11120d);
  --diagram-plate: #ffffff;
  --shadow-raised-near: light-dark(rgba(46, 48, 36, 0.11), rgba(0, 0, 0, 0.18));
  --shadow-raised-far: light-dark(rgba(46, 48, 36, 0.16), rgba(0, 0, 0, 0.24));
  --shadow-dialog-near: light-dark(rgba(46, 48, 36, 0.12), rgba(0, 0, 0, 0.3));
  --shadow-dialog-far: light-dark(rgba(46, 48, 36, 0.19), rgba(0, 0, 0, 0.52));
  --shadow-inset-color: light-dark(rgba(46, 48, 36, 0.12), rgba(0, 0, 0, 0.18));
  --shadow-pill-color: light-dark(rgba(46, 48, 36, 0.16), rgba(0, 0, 0, 0.24));
}

/* Amber — Sandstone and warm sand — pushed distinctly warmer and drier than the default. */
[data-hp-theme="amber"],
[data-hp-palette="amber"] {
  --bg-000: light-dark(#faf5ef, #23201c);
  --bg-sidebar: light-dark(#f3efe8, #231e17);
  --bg-toolbar: light-dark(#f8f3ed, #2a241d);
  --bg-100: light-dark(#fdfaf5, #2a241d);
  --bg-200: light-dark(#f8f2ea, #322c25);
  --bg-300: light-dark(#efe9df, #3a342d);
  --bg-400: light-dark(#e5ded4, #433d35);
  --bg-500: light-dark(#ede7dd, #4a4137);
  --bg-raised: light-dark(#fffdfa, #40372d);
  --border-100: light-dark(rgba(34, 32, 28, 0.07), rgba(255, 255, 255, 0.05));
  --border-200: light-dark(rgba(34, 32, 28, 0.14), rgba(255, 255, 255, 0.11));
  --border-focus: light-dark(#96690e, #d9a842);
  --text-000: light-dark(#2f2c27, #e9e4df);
  --text-100: light-dark(#655f58, #b0aea8);
  --text-200: light-dark(#635f59, #aeadab);
  --text-300: light-dark(#aaa59d, #68645e);
  --accent: light-dark(#96690e, #d9a842);
  --accent-hover: light-dark(#835c0c, #ddb257);
  --accent-pressed: light-dark(#714f0b, #d59e2d);
  --accent-soft: light-dark(rgba(150, 105, 14, 0.13), rgba(217, 168, 66, 0.16));
  --accent-text-on-soft: light-dark(#73540d, #deb256);
  --text-on-accent: light-dark(#ffffff, #1c1613);
  --text-link: light-dark(#825d0e, #d9a842);
  --text-link-hover: light-dark(#66490b, #deb45b);
  --switch-on: light-dark(#825d0e, #d9a842);
  --text-on-status: light-dark(#ffffff, #1c1613);
  --control-knob: light-dark(#ffffff, #1c1613);
  --btn-inverted-bg: light-dark(#262421, #f5f2ef);
  --btn-inverted-bg-hover: light-dark(#151412, #ffffff);
  --btn-inverted-text: light-dark(#f5f3ef, #161513);
  --overlay-bg: light-dark(rgba(34, 32, 28, 0.36), rgba(0, 0, 0, 0.5));
  --success: light-dark(#3f8764, #79c99e);
  --success-soft: light-dark(rgba(63, 135, 100, 0.18), rgba(121, 201, 158, 0.2));
  --success-text-on-soft: light-dark(#2b6246, #91d1b0);
  --success-badge-fill: light-dark(#4f9f7c, #79c99e);
  --info: light-dark(#487eaa, #78a9d4);
  --info-soft: light-dark(rgba(72, 126, 170, 0.18), rgba(120, 169, 212, 0.2));
  --info-text-on-soft: light-dark(#335a7a, #a0c0de);
  --info-badge-fill: light-dark(#6795c0, #78a9d4);
  --warning: light-dark(#9b6b25, #deb36a);
  --warning-soft: light-dark(rgba(155, 107, 37, 0.19), rgba(222, 179, 106, 0.21));
  --warning-text-on-soft: light-dark(#734d1b, #e5c186);
  --warning-badge-fill: light-dark(#bf862e, #deb36a);
  --danger: light-dark(#b34f4c, #dc7774);
  --danger-soft: light-dark(rgba(179, 79, 76, 0.2), rgba(220, 119, 116, 0.21));
  --danger-text-on-soft: light-dark(#8b3b3a, #e9aba8);
  --danger-badge-fill: light-dark(#c87f7c, #dc7774);
  --critical: light-dark(#c53030, #ff5c5c);
  --critical-soft: light-dark(rgba(197, 48, 48, 0.13), rgba(255, 92, 92, 0.16));
  --critical-text-on-soft: light-dark(#a92929, #ff9393);
  --terminal-bg: light-dark(#4e412f, #1c1814);
  --terminal-fg: light-dark(#e6e6e6, #e9e4df);
  --framebuffer-bg: light-dark(#443929, #13100e);
  --diagram-plate: #ffffff;
  --shadow-raised-near: light-dark(rgba(48, 43, 36, 0.11), rgba(0, 0, 0, 0.18));
  --shadow-raised-far: light-dark(rgba(48, 43, 36, 0.16), rgba(0, 0, 0, 0.24));
  --shadow-dialog-near: light-dark(rgba(48, 43, 36, 0.12), rgba(0, 0, 0, 0.3));
  --shadow-dialog-far: light-dark(rgba(48, 43, 36, 0.19), rgba(0, 0, 0, 0.52));
  --shadow-inset-color: light-dark(rgba(48, 43, 36, 0.12), rgba(0, 0, 0, 0.18));
  --shadow-pill-color: light-dark(rgba(48, 43, 36, 0.16), rgba(0, 0, 0, 0.24));
}

/* Ocean — Cool marine blue on a crisp blue-grey ground. */
[data-hp-theme="ocean"],
[data-hp-palette="ocean"] {
  --bg-000: light-dark(#f2f5f7, #1d2022);
  --bg-sidebar: light-dark(#ebeef0, #171f23);
  --bg-toolbar: light-dark(#f0f3f5, #1d2529);
  --bg-100: light-dark(#f7f9fb, #1d2529);
  --bg-200: light-dark(#edf1f4, #252d31);
  --bg-300: light-dark(#e3e8eb, #2d3539);
  --bg-400: light-dark(#d8dee1, #363e42);
  --bg-500: light-dark(#e1e6e9, #384349);
  --bg-raised: light-dark(#fafdff, #2e393f);
  --border-100: light-dark(rgba(28, 32, 34, 0.07), rgba(255, 255, 255, 0.05));
  --border-200: light-dark(rgba(28, 32, 34, 0.14), rgba(255, 255, 255, 0.11));
  --border-focus: light-dark(#1f6fa8, #55a7d8);
  --text-000: light-dark(#272c2f, #dfe6e9);
  --text-100: light-dark(#586065, #a8aeb0);
  --text-200: light-dark(#595f63, #a8abab);
  --text-300: light-dark(#9da5aa, #5e6568);
  --accent: light-dark(#1f6fa8, #55a7d8);
  --accent-hover: light-dark(#1c6497, #6ab2dd);
  --accent-pressed: light-dark(#195886, #409cd3);
  --accent-soft: light-dark(rgba(31, 111, 168, 0.13), rgba(85, 167, 216, 0.16));
  --accent-text-on-soft: light-dark(#195d90, #7dbbe2);
  --text-on-accent: light-dark(#ffffff, #1c1613);
  --text-link: light-dark(#1d69a0, #55a7d8);
  --text-link-hover: light-dark(#185886, #6eb4de);
  --switch-on: light-dark(#1d69a0, #55a7d8);
  --text-on-status: light-dark(#ffffff, #1c1613);
  --control-knob: light-dark(#ffffff, #1c1613);
  --btn-inverted-bg: light-dark(#212426, #eff3f5);
  --btn-inverted-bg-hover: light-dark(#121415, #ffffff);
  --btn-inverted-text: light-dark(#eff3f5, #131516);
  --overlay-bg: light-dark(rgba(28, 32, 34, 0.36), rgba(0, 0, 0, 0.5));
  --success: light-dark(#3f8764, #79c99e);
  --success-soft: light-dark(rgba(63, 135, 100, 0.18), rgba(121, 201, 158, 0.2));
  --success-text-on-soft: light-dark(#2b6246, #8dcfad);
  --success-badge-fill: light-dark(#4f9f7c, #79c99e);
  --info: light-dark(#487eaa, #78a9d4);
  --info-soft: light-dark(rgba(72, 126, 170, 0.18), rgba(120, 169, 212, 0.2));
  --info-text-on-soft: light-dark(#335a7a, #a0c0de);
  --info-badge-fill: light-dark(#6795c0, #78a9d4);
  --warning: light-dark(#9b6b25, #deb36a);
  --warning-soft: light-dark(rgba(155, 107, 37, 0.19), rgba(222, 179, 106, 0.21));
  --warning-text-on-soft: light-dark(#734d1b, #e4bf82);
  --warning-badge-fill: light-dark(#bf862e, #deb36a);
  --danger: light-dark(#b34f4c, #dc7774);
  --danger-soft: light-dark(rgba(179, 79, 76, 0.2), rgba(220, 119, 116, 0.21));
  --danger-text-on-soft: light-dark(#8b3b3a, #e8a7a4);
  --danger-badge-fill: light-dark(#c87f7c, #dc7774);
  --critical: light-dark(#c53030, #ff5c5c);
  --critical-soft: light-dark(rgba(197, 48, 48, 0.13), rgba(255, 92, 92, 0.16));
  --critical-text-on-soft: light-dark(#a52828, #ff8e8e);
  --terminal-bg: light-dark(#364147, #13191c);
  --terminal-fg: light-dark(#e6e6e6, #dfe6e9);
  --framebuffer-bg: light-dark(#2f393e, #0d1113);
  --diagram-plate: #ffffff;
  --shadow-raised-near: light-dark(rgba(36, 44, 48, 0.11), rgba(0, 0, 0, 0.18));
  --shadow-raised-far: light-dark(rgba(36, 44, 48, 0.16), rgba(0, 0, 0, 0.24));
  --shadow-dialog-near: light-dark(rgba(36, 44, 48, 0.12), rgba(0, 0, 0, 0.3));
  --shadow-dialog-far: light-dark(rgba(36, 44, 48, 0.19), rgba(0, 0, 0, 0.52));
  --shadow-inset-color: light-dark(rgba(36, 44, 48, 0.12), rgba(0, 0, 0, 0.18));
  --shadow-pill-color: light-dark(rgba(36, 44, 48, 0.16), rgba(0, 0, 0, 0.24));
}

/* Rose — Muted rose against a warm blush-grey ground. */
[data-hp-theme="rose"],
[data-hp-palette="rose"] {
  --bg-000: light-dark(#f7f2f3, #221d1e);
  --bg-sidebar: light-dark(#f0ebec, #22181a);
  --bg-toolbar: light-dark(#f5f0f1, #281e20);
  --bg-100: light-dark(#fbf7f8, #281e20);
  --bg-200: light-dark(#f4edef, #302628);
  --bg-300: light-dark(#ebe3e5, #382e30);
  --bg-400: light-dark(#e1d8da, #413739);
  --bg-500: light-dark(#e9e1e3, #48393d);
  --bg-raised: light-dark(#fffafb, #3e2f33);
  --border-100: light-dark(rgba(34, 28, 29, 0.07), rgba(255, 255, 255, 0.05));
  --border-200: light-dark(rgba(34, 28, 29, 0.14), rgba(255, 255, 255, 0.11));
  --border-focus: light-dark(#b04a66, #d98298);
  --text-000: light-dark(#2f2729, #e9dfe2);
  --text-100: light-dark(#685a5e, #aca2a5);
  --text-200: light-dark(#665b5e, #a9a5a6);
  --text-300: light-dark(#aa9da0, #685e61);
  --accent: light-dark(#b04a66, #d98298);
  --accent-hover: light-dark(#a2445e, #df96a8);
  --accent-pressed: light-dark(#933e55, #d36e88);
  --accent-soft: light-dark(rgba(176, 74, 102, 0.13), rgba(217, 130, 152, 0.16));
  --accent-text-on-soft: light-dark(#903c56, #e09ead);
  --text-on-accent: light-dark(#ffffff, #1c1613);
  --text-link: light-dark(#a0435e, #d98298);
  --text-link-hover: light-dark(#8a3a51, #e099ab);
  --switch-on: light-dark(#a0435e, #d98298);
  --text-on-status: light-dark(#ffffff, #1c1613);
  --control-knob: light-dark(#ffffff, #1c1613);
  --btn-inverted-bg: light-dark(#262122, #f5eff1);
  --btn-inverted-bg-hover: light-dark(#151213, #ffffff);
  --btn-inverted-text: light-dark(#f5eff1, #161314);
  --overlay-bg: light-dark(rgba(34, 28, 29, 0.36), rgba(0, 0, 0, 0.5));
  --success: light-dark(#3f8764, #79c99e);
  --success-soft: light-dark(rgba(63, 135, 100, 0.18), rgba(121, 201, 158, 0.2));
  --success-text-on-soft: light-dark(#295e43, #85cca7);
  --success-badge-fill: light-dark(#4f9f7c, #79c99e);
  --info: light-dark(#487eaa, #78a9d4);
  --info-soft: light-dark(rgba(72, 126, 170, 0.18), rgba(120, 169, 212, 0.2));
  --info-text-on-soft: light-dark(#335a7a, #98bbdc);
  --info-badge-fill: light-dark(#6795c0, #78a9d4);
  --warning: light-dark(#9b6b25, #deb36a);
  --warning-soft: light-dark(rgba(155, 107, 37, 0.19), rgba(222, 179, 106, 0.21));
  --warning-text-on-soft: light-dark(#734d1b, #e2bb7a);
  --warning-badge-fill: light-dark(#bf862e, #deb36a);
  --danger: light-dark(#b34f4c, #dc7774);
  --danger-soft: light-dark(rgba(179, 79, 76, 0.2), rgba(220, 119, 116, 0.21));
  --danger-text-on-soft: light-dark(#873938, #e7a3a0);
  --danger-badge-fill: light-dark(#c87f7c, #dc7774);
  --critical: light-dark(#c53030, #ff5c5c);
  --critical-soft: light-dark(rgba(197, 48, 48, 0.13), rgba(255, 92, 92, 0.16));
  --critical-text-on-soft: light-dark(#a52828, #ff8989);
  --terminal-bg: light-dark(#47363a, #1b1416);
  --terminal-fg: light-dark(#e6e6e6, #e9dfe2);
  --framebuffer-bg: light-dark(#3e2f33, #120d0f);
  --diagram-plate: #ffffff;
  --shadow-raised-near: light-dark(rgba(48, 36, 39, 0.11), rgba(0, 0, 0, 0.18));
  --shadow-raised-far: light-dark(rgba(48, 36, 39, 0.16), rgba(0, 0, 0, 0.24));
  --shadow-dialog-near: light-dark(rgba(48, 36, 39, 0.12), rgba(0, 0, 0, 0.3));
  --shadow-dialog-far: light-dark(rgba(48, 36, 39, 0.19), rgba(0, 0, 0, 0.52));
  --shadow-inset-color: light-dark(rgba(48, 36, 39, 0.12), rgba(0, 0, 0, 0.18));
  --shadow-pill-color: light-dark(rgba(48, 36, 39, 0.16), rgba(0, 0, 0, 0.24));
}

/* Neon — Restrained neon on near-black — micro-glow accents against a deep ground. */
[data-hp-theme="neon"],
[data-hp-palette="neon"] {
  --bg-000: light-dark(#f2f7f6, #121615);
  --bg-sidebar: light-dark(#ebf0ef, #0f1514);
  --bg-toolbar: light-dark(#f0f5f4, #141b1a);
  --bg-100: light-dark(#f7fbfa, #141b1a);
  --bg-200: light-dark(#eef3f3, #1c2322);
  --bg-300: light-dark(#e4eae9, #242b2a);
  --bg-400: light-dark(#d9e0df, #2c3533);
  --bg-500: light-dark(#e2e8e7, #2f3b3a);
  --bg-raised: light-dark(#fbfffe, #25312f);
  --border-100: light-dark(rgba(28, 34, 33, 0.07), rgba(244, 244, 244, 0.05));
  --border-200: light-dark(rgba(28, 34, 33, 0.14), rgba(244, 244, 244, 0.11));
  --border-focus: light-dark(#0e806e, #3ee6c8);
  --text-000: light-dark(#272f2e, #d1e0de);
  --text-100: light-dark(#566260, #99a3a1);
  --text-200: light-dark(#57605f, #9fa2a2);
  --text-300: light-dark(#9daaa8, #535c5b);
  --accent: light-dark(#0f8f7a, #3ee6c8);
  --accent-hover: light-dark(#0e8672, #55e9ce);
  --accent-pressed: light-dark(#0b6a5a, #27e3c2);
  --accent-soft: light-dark(rgba(15, 143, 122, 0.13), rgba(62, 230, 200, 0.16));
  --accent-text-on-soft: light-dark(#0b675a, #3ee6c8);
  --text-on-accent: light-dark(#040303, #1c1613);
  --text-link: light-dark(#0c7162, #3ee6c8);
  --text-link-hover: light-dark(#09554a, #59ead0);
  --switch-on: light-dark(#0c7162, #3ee6c8);
  --text-on-status: light-dark(#ffffff, #1c1613);
  --control-knob: light-dark(#ffffff, #1c1613);
  --btn-inverted-bg: light-dark(#212625, #e1eceb);
  --btn-inverted-bg-hover: light-dark(#121515, #f4f4f4);
  --btn-inverted-text: light-dark(#eff5f4, #080a0a);
  --overlay-bg: light-dark(rgba(28, 34, 33, 0.36), rgba(0, 0, 0, 0.5));
  --success: light-dark(#3f8764, #79c99e);
  --success-soft: light-dark(rgba(63, 135, 100, 0.18), rgba(121, 201, 158, 0.2));
  --success-text-on-soft: light-dark(#2b6246, #79c99e);
  --success-badge-fill: light-dark(#4f9f7c, #79c99e);
  --info: light-dark(#487eaa, #78a9d4);
  --info-soft: light-dark(rgba(72, 126, 170, 0.18), rgba(120, 169, 212, 0.2));
  --info-text-on-soft: light-dark(#335a7a, #8cb4d9);
  --info-badge-fill: light-dark(#6795c0, #78a9d4);
  --warning: light-dark(#9b6b25, #deb36a);
  --warning-soft: light-dark(rgba(155, 107, 37, 0.19), rgba(222, 179, 106, 0.21));
  --warning-text-on-soft: light-dark(#734d1b, #deb36a);
  --warning-badge-fill: light-dark(#bf862e, #deb36a);
  --danger: light-dark(#b34f4c, #dc7774);
  --danger-soft: light-dark(rgba(179, 79, 76, 0.2), rgba(220, 119, 116, 0.21));
  --danger-text-on-soft: light-dark(#8b3b3a, #e59b98);
  --danger-badge-fill: light-dark(#c87f7c, #dc7774);
  --critical: light-dark(#c53030, #ff5c5c);
  --critical-soft: light-dark(rgba(197, 48, 48, 0.13), rgba(255, 92, 92, 0.16));
  --critical-text-on-soft: light-dark(#a52828, #ff7a7a);
  --terminal-bg: light-dark(#384543, #0a0e0d);
  --terminal-fg: light-dark(#e6e6e6, #d1e0de);
  --framebuffer-bg: light-dark(#313d3b, #040505);
  --diagram-plate: #ffffff;
  --shadow-raised-near: light-dark(rgba(36, 48, 46, 0.11), rgba(0, 0, 0, 0.18));
  --shadow-raised-far: light-dark(rgba(36, 48, 46, 0.16), rgba(0, 0, 0, 0.24));
  --shadow-dialog-near: light-dark(rgba(36, 48, 46, 0.12), rgba(0, 0, 0, 0.3));
  --shadow-dialog-far: light-dark(rgba(36, 48, 46, 0.19), rgba(0, 0, 0, 0.52));
  --shadow-inset-color: light-dark(rgba(36, 48, 46, 0.12), rgba(0, 0, 0, 0.18));
  --shadow-pill-color: light-dark(rgba(36, 48, 46, 0.16), rgba(0, 0, 0, 0.24));
}

/* <<< END GENERATED THEME SURFACES */



*, *::before, *::after { box-sizing: border-box; }

html {
  min-height: 100%;
  background: var(--bg-000);
  font-size: 14px;
  /* Reserves the scrollbar's track width up front instead of letting it
     appear/disappear as content settles during load (webfont metrics,
     late-loading iframe content, a table that only grows past viewport
     height once its rows render). Without this, any width computed from
     100vw -- .modal's width: min(1280px, calc(100vw - 40px)) and
     .auth-split's siblings among them -- visibly jumps by the scrollbar's
     width the moment it appears or disappears, which reads as a brief
     resize flash. Falls back to nothing (no gutter reserved, the old
     behavior) on a browser that doesn't support the property; it is not
     polyfillable and there is no worse-than-before regression in not
     having it. */
  scrollbar-gutter: stable;
}

body {
  min-height: 100%;
  margin: 0;
  background: var(--bg-000);
  color: var(--text-000);
  font-family: var(--font-sans);
  font-size: var(--font-size-body);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

button, input, select, textarea { font: inherit; }
button, a, input, select, textarea { -webkit-tap-highlight-color: transparent; }
a { color: var(--text-link); text-underline-offset: 3px; }
a:hover { color: var(--text-link-hover); }
img, svg { display: block; }

h1, h2, h3, h4, p { margin-top: 0; }
h1, h2, h3, h4 { color: var(--text-000); font-family: var(--font-display); font-weight: 600; line-height: 1.2; }
h1 { font-size: clamp(1.65rem, 2.4vw, 2.2rem); letter-spacing: -0.025em; }
h2 { font-size: 1.25rem; letter-spacing: -0.012em; }
h3 { font-size: var(--font-size-lg); }
p { color: var(--text-100); }

.heading-serif {
  font-family: var(--font-display);
  font-weight: 400;
  letter-spacing: -0.035em;
}

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

.text-muted { color: var(--text-200); }
.text-secondary { color: var(--text-100); }
.text-danger { color: var(--danger); }
.label-section, .sidebar__section-label {
  color: var(--text-200);
  font-size: var(--font-size-xs);
  font-weight: 600;
  letter-spacing: 0.075em;
  text-transform: uppercase;
}

/* Visually hidden, still readable by assistive tech -- the standard
   clip-rect technique, not display:none (which screen readers skip). */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}

/* .skip-link ships combined with .sr-only (clipped offscreen by default);
   this undoes the clip so keyboard focus reveals it as a real control. */
.skip-link:focus-visible {
  position: fixed; top: var(--space-sm); left: var(--space-sm);
  width: auto; height: auto; overflow: visible; clip: auto; white-space: normal;
  z-index: 2000;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--border-200);
  border-radius: 8px;
  background: var(--bg-raised);
  color: var(--text-000);
  box-shadow: var(--shadow-raised);
}

:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: 2px;
}

::selection { background: var(--accent-soft); color: var(--text-000); }

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* #672 (APIARY): dropped the X component (was translate(-50%, ...)) --
   .modal's horizontal centering no longer uses a transform at all (see
   its own comment), only the vertical translateY(-50%) still does. The
   Y motion itself (starts 8px lower, settles up into place while
   scaling in) is unchanged. */
@keyframes dialog-in {
  from { opacity: 0; transform: translateY(calc(-50% + 8px)) scale(0.99); }
  to { opacity: 1; transform: translateY(-50%) scale(1); }
}

/* ── Entrance/attention motion utilities ─────────────────────────────────
   Four small, additive animation utilities -- opt-in classes, not tied to
   any specific component, so any consumer can reach for them. All four
   are automatically neutralized by the prefers-reduced-motion rule at the
   bottom of this file (animation-duration drops to ~0). */

/* A freshly-rendered list's rows fade/settle in with a slight stagger
   instead of all appearing at once -- add .stagger-in to the row
   container (e.g. a <tbody> or a menu's item list). Pure CSS: nth-child
   selectors carry the delay, so there is nothing to wire up per row and
   no per-element custom property to set (contrast with .mini-chart/
   .heatmap, where the value truly is per-request data -- a stagger index
   is not). Covers the first 12 rows explicitly; rows beyond that share
   the last delay rather than keep growing, so a long list doesn't leave
   its tail waiting a full second to appear. */
@keyframes stagger-in {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: none; }
}
.stagger-in > * {
  animation: stagger-in 220ms var(--ease-out) backwards;
}
.stagger-in > *:nth-child(1) { animation-delay: 0ms; }
.stagger-in > *:nth-child(2) { animation-delay: 45ms; }
.stagger-in > *:nth-child(3) { animation-delay: 90ms; }
.stagger-in > *:nth-child(4) { animation-delay: 135ms; }
.stagger-in > *:nth-child(5) { animation-delay: 180ms; }
.stagger-in > *:nth-child(6) { animation-delay: 225ms; }
.stagger-in > *:nth-child(7) { animation-delay: 270ms; }
.stagger-in > *:nth-child(8) { animation-delay: 315ms; }
.stagger-in > *:nth-child(9) { animation-delay: 360ms; }
.stagger-in > *:nth-child(10) { animation-delay: 405ms; }
.stagger-in > *:nth-child(11) { animation-delay: 450ms; }
.stagger-in > *:nth-child(n + 12) { animation-delay: 495ms; }

/* A one-shot attention ring for a value that just changed -- a live-
   refreshed table row, a cell whose count just updated. Add the class,
   remove it on `animationend` (or just leave it: the animation does not
   repeat on its own). Color comes from --accent so it reads as "this
   changed", not as a semantic success/warning/danger signal. */
@keyframes flash-ring {
  from {
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 55%, transparent),
      0 0 16px 2px color-mix(in srgb, var(--accent) 25%, transparent);
  }
  to { box-shadow: 0 0 #0000, 0 0 #0000; }
}
.flash { animation: flash-ring 900ms var(--ease-out); }

/* A shaped placeholder for content still loading -- a card, table row, or
   map rectangle rendered as its own layout shape instead of a plain text
   line. No per-element dynamic value and no JS: static markup a consumer
   renders instead of (or before replacing with) real content. The sweep
   animation is already neutralized by the prefers-reduced-motion catch-all
   above (animation-duration: 0.01ms !important on *, *::before, *::after). */
.skeleton, .skeleton-line {
  position: relative;
  overflow: hidden;
  background: var(--bg-300);
  border-radius: var(--radius-control);
}
.skeleton::after, .skeleton-line::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--bg-400) 60%, transparent), transparent);
  animation: skeleton-sweep 1.4s ease-in-out infinite;
}
@keyframes skeleton-sweep {
  from { transform: translateX(-100%); }
  to { transform: translateX(100%); }
}
/* .skeleton-line is a standalone shimmering bar (its own background+sweep,
   same as .skeleton above) sized for text-line placeholders -- stack
   several directly, no wrapping .skeleton parent needed. .skeleton itself
   is for a single rectangular block (a map, an image, a whole card). */
.skeleton-line { display: block; height: 12px; margin-bottom: var(--space-sm); border-radius: var(--radius-xs); }
.skeleton-line:last-child { margin-bottom: 0; }

/* A timeline/session-replay entry settling into place -- a touch more
   pronounced than .stagger-in's plain fade+lift, since a timeline reads
   one block at a time rather than as a batch of equivalent rows. */
@keyframes timeline-block-in {
  from { opacity: 0; filter: blur(4px); transform: translateY(5px) scale(0.98); }
  to { opacity: 1; filter: blur(0); transform: none; }
}
.timeline-block { animation: timeline-block-in 320ms var(--ease-out) backwards; }

/* #1533 (APIARY): a session-replay timeline that reads left-to-right,
   oldest entry first, like an actual timeline -- not top-to-bottom like a
   plain event list. .timeline-track turns a generic overflow:auto
   container into a horizontal filmstrip; each .timeline-block becomes a
   fixed-width column with a top rule (the horizontal equivalent of a
   vertical layout's left rule) instead of stretching full-width. */
.timeline-track { display: flex; align-items: stretch; gap: var(--space-lg); }
.timeline-track .timeline-block {
  flex: 0 0 auto;
  width: 240px;
  padding: var(--space-md) var(--space-md) var(--space-sm);
  border-top: 2px solid var(--border-100);
}
.timeline-block__meta { font-size: .875rem; color: var(--text-200); }

.btn {
  display: inline-flex;
  min-height: 34px;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-lg);
  border: 1px solid transparent;
  /* Design refresh 2 (pick 6E, supersedes 6D): claude.ai's actual buttons
     -- quiet surface pills, fully rounded, no borders; only the primary
     carries the accent as a solid fill (below). */
  border-radius: var(--radius-pill);
  background: var(--bg-300);
  color: var(--text-000);
  font-size: var(--font-size-footnote);
  font-weight: 500;
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--transition-fast), border-color var(--transition-fast),
    color var(--transition-fast), transform var(--transition-fast);
}

.btn:hover { text-decoration: none; }
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: 0.42; cursor: not-allowed; transform: none; }
.btn svg { width: 15px; height: 15px; }
.btn:hover { background: var(--bg-400); }
.btn-primary {
  /* 6E: the one solid accent pill on the page. */
  background: var(--accent);
  color: var(--text-on-accent);
  font-weight: 600;
}
.btn-primary:hover { background: var(--accent-hover); color: var(--text-on-accent); }
/* #1531 (APIARY): surface-1 sits only one step above --bg-000 -- next to a
   real .btn-primary (an inverted, high-contrast fill) in an Apply/Reset or
   Filter/Reset pair, it read flat and barely-there rather than as a
   deliberately muted second button. Other secondary-weight interactive
   surfaces already in this file (.hp-rp-theme button, .hp-rp-payload-row,
   the .segmented control's active state) go a full two steps -- surface-1/2
   idle, surface-hover/3 on interaction -- so this gives .btn-secondary that
   same, already-established two-step presence instead of a new one. */
.btn-secondary {
  /* 6E: secondary == the quiet surface pill the base .btn already is. */
  background: var(--bg-300);
  color: var(--text-000);
  /* The hairline is what makes it a control rather than a tinted patch.
     .btn sets `border: 1px solid transparent`, and a secondary button's
     fill is --bg-300 -- which is exactly what .data-table's row hover
     paints the cell behind it. On a hovered row the two matched and the
     button vanished outright: same fill, no edge, nothing left to see.

     Bordering it rather than changing either fill keeps the quiet pill
     quiet, and keeps it visible on any surface it is ever placed on --
     including ones that do not exist yet, which is the point of solving it
     here rather than at the one call site where it was noticed. */
  border-color: var(--border-100);
}
.btn-secondary:hover { background: var(--bg-400); border-color: var(--border-200); }
.btn-ghost { color: var(--text-100); }
.btn-ghost:hover { background: var(--bg-300); color: var(--text-000); }
.btn-danger { background: var(--danger-soft); color: var(--danger-text-on-soft); }
.btn-danger:hover { background: var(--danger-soft); }
.btn-sm { min-height: 28px; padding: var(--space-xs) var(--space-sm); font-size: var(--font-size-caption); }
.btn-lg { min-height: 42px; padding: var(--space-md) var(--space-lg); font-size: var(--font-size-body); }
.btn-icon { width: 32px; min-height: 32px; padding: 0; border-radius: var(--radius-pill); }

.form-group { margin-bottom: var(--space-lg); }
.form-label {
  display: block;
  margin-bottom: var(--space-sm);
  color: var(--text-100);
  font-size: var(--font-size-caption);
  font-weight: 500;
}

.form-help, .form-error {
  margin-top: var(--space-xs);
  font-size: var(--font-size-xs);
  line-height: 1.4;
}
.form-help { color: var(--text-200); }
.form-error { color: var(--danger); }

.form-input {
  width: 100%;
  min-height: 38px;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--border-100);
  border-radius: var(--radius-control);
  background: var(--bg-200);
  color: var(--text-000);
  font-size: var(--font-size-footnote);
  transition: background var(--transition-fast), border-color var(--transition-fast),
    box-shadow var(--transition-fast);
}

.form-input:hover { border-color: var(--border-200); }
.form-input:focus {
  border-color: var(--border-focus);
  box-shadow: 0 0 0 3px var(--accent-soft);
  background: var(--bg-100);
}
.form-input::placeholder { color: var(--text-200); }
.form-input:disabled { color: var(--text-300); opacity: 0.62; cursor: not-allowed; }
textarea.form-input { min-height: 92px; resize: vertical; }
select.form-input { cursor: pointer; }

.search-field, .sidebar__search {
  display: flex;
  min-height: 32px;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-xs) var(--space-sm);
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  background: var(--bg-300);
  color: var(--text-200);
}
.search-field:focus-within, .sidebar__search:focus-within {
  border-color: var(--border-focus);
  box-shadow: 0 0 0 2px var(--accent-soft);
}
.search-field input, .sidebar__search input {
  width: 100%;
  border: 0;
  outline: 0;
  background: transparent;
  color: var(--text-000);
  font-size: var(--font-size-caption);
}
.search-field svg, .sidebar__search-icon { width: 14px; height: 14px; flex: none; }

.check {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  color: var(--text-100);
  font-size: var(--font-size-footnote);
  cursor: pointer;
}
.check input { width: 15px; height: 15px; accent-color: var(--accent); }

.switch {
  position: relative;
  display: inline-flex;
  width: 34px;
  height: 20px;
  flex: none;
}
.switch input { position: absolute; opacity: 0; pointer-events: none; }
.switch span {
  width: 100%;
  border-radius: var(--radius-pill);
  background: var(--bg-400);
  transition: background var(--transition-fast);
}
.switch span::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--text-100);
  transition: transform var(--transition-fast), background var(--transition-fast);
}
.switch input:checked + span { background: var(--switch-on); }
.switch input:checked + span::after { transform: translateX(14px); background: var(--control-knob); }

/* #24: display:flex/flex-direction:column so a .card__footer can pin
   itself to the card's bottom edge via margin-top:auto regardless of how
   much (or little) .card__content holds -- this is what actually fixes
   "cards look like the same kind of thing but their footers land at
   different heights" (confirmed live against Xore/honeypot-stack's
   dashboard: 5 same-class cards on one page measured 458-2850px tall).
   Safe for every existing .card usage that doesn't opt into the new
   title/content/footer structure: a flex column still stacks plain block
   children top-to-bottom exactly like normal flow, the only behavioral
   change is that adjacent margins stop collapsing between them, which
   isn't visible for this file's existing card content (titles/rows/tables
   that already carry their own explicit spacing, not relying on margin
   collapse). */
.card {
  display: flex;
  flex-direction: column;
  grid-column: span 4;
  margin-bottom: 0;
  padding: var(--space-lg) var(--space-xl);
  /* Design refresh 2 (pick 1E, supersedes 1D): claude floating surface --
     borderless, big radius, soft layered shadow. */
  border: 0;
  border-radius: var(--radius-panel);
  background: var(--bg-200);
  box-shadow: var(--shadow-raised);
  overflow: hidden;
}
.card--raised { background: var(--bg-raised); border-color: var(--border-200); box-shadow: var(--shadow-raised); }
.card__title { margin-bottom: var(--space-xs); color: var(--text-000); font-size: var(--font-size-body); font-weight: 600; }
.card__desc { margin-bottom: var(--space-lg); color: var(--text-100); font-size: var(--font-size-caption); }
/* #24: the only region that flexes. min-height:0 overrides flex's own
   default (min-height:auto on a column-flex child refuses to shrink below
   its content's intrinsic size), which is what actually lets a nested
   .card__scroll's max-height/overflow-y:auto engage -- without it the
   content pushes the card taller instead of scrolling internally, the
   exact bug this issue exists to fix. Content-shape variants (a plain
   list of rows for a short/bounded set, .card__scroll for a long/
   unbounded one, .metric-grid for key/value summaries, a chart/sparkline
   sized to this box) all nest inside unchanged -- this class only owns
   the flex sizing, not what's displayed inside it. */
.card__content { flex: 1 1 auto; min-height: 0; }
/* #24: fixed height (sized to its own content -- a line of text/links/
   counts, never meant to hold something that grows), pinned to the
   card's bottom edge via margin-top:auto regardless of how tall
   .card__content ends up. A card with no .card__footer is unaffected --
   there's nothing here to push anywhere. */
.card__footer {
  flex: 0 0 auto;
  margin-top: auto;
  padding-top: var(--space-md);
  color: var(--text-100);
  font-size: var(--font-size-caption);
}
.card__row {
  display: flex;
  min-height: 50px;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  padding: var(--space-md) 0;
  border-bottom: 1px solid var(--border-100);
}
.card__row:last-child { border-bottom: 0; }
.card__label { color: var(--text-000); font-size: var(--font-size-footnote); flex: 0 0 auto; }
/* min-width:0 for the same reason .card__content needs it above: a flex
   item's default min-width is auto, which means it refuses to shrink
   below its content's intrinsic width no matter what word-break says --
   word-break only has anything to do once the box is actually allowed to
   get narrower than an unbroken run of text. word-break:break-all itself
   mirrors this dashboard's own td.v (a long SHA-256/hash value already
   wraps correctly in a <table> cell; .card__value had no equivalent,
   which is why several long-value tables stayed on <table> markup
   instead of converting to this component -- see honeypot-stack#790). */
.card__value {
  color: var(--text-100);
  font-size: var(--font-size-caption);
  min-width: 0;
  word-break: break-all;
}
.card__value--mono {
  padding: 3px var(--space-sm);
  border-radius: var(--radius-xs);
  background: var(--bg-300);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
}

.metric-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--space-md);
}
.metric {
  min-width: 0;
  padding: var(--space-sm) var(--space-lg);
  /* Design refresh 2 (pick 3E): boxless stat strip -- no tile chrome, a
     hairline divider between neighbours (grid gaps keep them apart; the
     border marks the seam), serif numerals. */
  border: 0;
  border-left: 1px solid var(--border-100);
  border-radius: 0;
  background: transparent;
}
.metric:first-child { border-left: 0; padding-left: 0; }
/* A metric that is also a filter, and is the one currently applied.

   Marked with aria-current rather than a class so the selected state is one
   fact in the DOM rather than two that can disagree, and so assistive tech
   is told the same thing the accent stripe says. The stripe replaces the
   hairline seam on that tile instead of sitting beside it -- the seam marks a
   boundary between neighbours, and this marks the tile itself. */
.metric[aria-current] {
  border-left: 2px solid var(--accent);
  padding-left: calc(var(--space-lg) - 1px);
}
.metric[aria-current]:first-child { border-left: 2px solid var(--accent); padding-left: var(--space-sm); }
.metric[aria-current] .metric__label { color: var(--accent-text-on-soft); }

/* A token URL in a table cell.

   A URL has no spaces and no break opportunities, so left alone it sets the
   cell's minimum width and pushes past the card edge -- the value decides the
   layout instead of the layout containing the value. `anywhere` lets it break
   mid-string, which is the only sensible place to break a URL.

   The copy control sits on the same line and does not shrink: the reason a
   token URL is on screen at all is to be pasted somewhere else. */
.hp-token-url { display: flex; align-items: flex-start; gap: var(--space-sm); min-width: 0; }
.hp-token-url code { min-width: 0; overflow-wrap: anywhere; }
.hp-token-url .btn { flex: none; }
.metric__label { color: var(--text-200); font-size: var(--font-size-xs); }
.metric__value { margin-top: var(--space-sm); color: var(--text-000); font-family: var(--font-serif); font-size: 26px; font-weight: 500; letter-spacing: -0.01em; }
.metric__trend { margin-top: var(--space-xs); color: var(--text-100); font-size: var(--font-size-xs); }
/* Discrete, server-chosen modifiers (not a per-element continuous value),
   so unlike .mini-chart/.heatmap below this needs no CSP.md nonce pattern.
   Mapping: positive/spike-like states -> --up, negative/low states ->
   --down, neutral/baseline-unavailable states -> no modifier (plain
   .metric__trend, today's look). */
.metric__trend--up { color: var(--success); }
.metric__trend--down { color: var(--danger); }
.metric__trend--up::before { content: "\2191 "; }
.metric__trend--down::before { content: "\2193 "; }

/* Each bar's height comes from --v (0-100), not an inline height/width.
   Read docs/CSP.md before setting --v: under a strict style-src (a nonce
   with no 'unsafe-inline', the only policy that actually stops injected
   CSS) a nonce covers a style or link element, never a style attribute --
   so style="--v:58" is silently dropped exactly like style="height:58%"
   would be. Set --v per bar from a nonced <style> element instead. --v
   defaults to 0 here so a consumer that never sets it renders a flat,
   empty chart rather than an invalid computed height. Height is a
   component default; wrap in a taller container and add height: 100% to
   .mini-chart, or set a taller default with a modifier class, for a
   larger chart. */
.mini-chart { display: flex; align-items: flex-end; gap: var(--space-xs); height: 64px; }
.mini-chart span {
  --v: 0;
  flex: 1;
  min-width: 0;
  height: calc(var(--v) * 1%);
  min-height: 2px;
  border-radius: 3px 3px 0 0;
  background: var(--accent-soft);
  border-top: 2px solid var(--accent);
}

/* A row-labeled grid of small shaded cells (contribution-graph style) for
   a two-dimensional count -- e.g. one row per category, one column per time
   bucket. Each cell reads its intensity from --v (0-100), the same
   nonced-<style>-element pattern as .mini-chart above: see docs/CSP.md,
   which already names "a heatmap cell's color" as a covered case. --v
   defaults to 0 so an unset cell renders as the flattest shade, not an
   invalid color-mix() percentage.
   Consumers should quantize --v into a small number of steps (5 is a good
   default, matching the reference contribution-graph look) rather than
   passing raw continuous counts -- a heatmap reads as "roughly how much",
   not a precise value, and 100 subtly different shades is visual noise a
   human can't actually use. Precise values belong in the cell's title
   attribute (or a data-tip/tooltip layer the consumer adds), not the shade
   itself. */
.heatmap { display: flex; flex-direction: column; gap: 3px; }
.heatmap__row { display: flex; align-items: center; gap: var(--space-sm); }
.heatmap__label {
  flex: none;
  width: 96px;
  overflow: hidden;
  color: var(--text-200);
  font-size: var(--font-size-xs);
  text-align: right;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.heatmap__cells { display: flex; flex: 1; min-width: 0; gap: 3px; }
.heatmap__cell {
  --v: 0;
  flex: 1;
  min-width: 0;
  aspect-ratio: 1;
  border-radius: var(--radius-xs);
  background: color-mix(in srgb, var(--accent) calc(var(--v) * 1%), var(--bg-300));
}
/* Optional low→high key, e.g. under the grid: reuses the same --v cells so
   the key's shades always match the grid's, even if a consumer changes the
   accent token. */
.heatmap__legend { display: flex; align-items: center; gap: var(--space-xs); color: var(--text-200); font-size: var(--font-size-xs); }
.heatmap__legend .heatmap__cell { flex: none; width: 10px; height: 10px; }

/* CSS-only tooltip, reading its text straight from the DOM attribute via
   attr() inside this stylesheet rule rather than a computed inline style
   value -- unlike .mini-chart/.heatmap above, it needs no nonce and no
   per-page <style> block at all, since attr() here is a static rule, not
   a per-request dynamic one. :focus-visible makes it keyboard-reachable
   only on elements that are already focusable (links, buttons); a bare
   element (e.g. a <span> label) needs tabindex="0" added to pick up
   keyboard/focus access, this primitive doesn't grant that by itself.
   white-space:nowrap suits short strings; pair a max-width + white-space:
   normal modifier alongside it for longer copy. */
[data-tip] { position: relative; }
[data-tip]::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-control);
  background: var(--bg-raised);
  color: var(--text-000);
  font-size: var(--font-size-xs);
  white-space: nowrap;
  box-shadow: var(--shadow-raised);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--transition-fast), transform var(--transition-fast);
  z-index: 40;
}
[data-tip]:hover::after, [data-tip]:focus-visible::after {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 2px var(--space-sm);
  border-radius: var(--radius-control);
  background: var(--bg-300);
  color: var(--text-100);
  font-size: var(--font-size-xs);
  font-weight: 500;
  line-height: 1.45;
  white-space: nowrap;
  /* .badge is also used on <a> (a clickable filter/pivot chip -- e.g.
     Xore/honeypot-stack's event tables link a country-code badge to a
     filtered view) -- text-decoration:none here means a consumer doesn't
     need its own override just to stop the badge looking like an
     underlined hyperlink. No effect on the common <span> case. */
  text-decoration: none;
}
.badge--accent { background: var(--accent-soft); color: var(--accent-text-on-soft); }
/* Design refresh 2 (pick 5E, supersedes 5D's solid fills -- and #82's
   --*-badge-fill light-theme retune of them, which the neutral-chip
   design no longer needs; the dot uses the badge-fill token so it
   keeps #82's light-theme legibility): one quiet neutral chip
   everywhere; only a leading dot and the ink carry the status hue.
   Ink uses the *-text-on-soft tokens (#82's raised AA values). */
.badge--green, .badge--success, .badge--blue, .badge--info,
.badge--orange, .badge--warning, .badge--red, .badge--danger { background: var(--bg-300); }
.badge--green::before, .badge--success::before, .badge--blue::before, .badge--info::before,
.badge--orange::before, .badge--warning::before, .badge--red::before, .badge--danger::before {
  content: ""; display: inline-block; width: 6px; height: 6px;
  border-radius: 50%; margin-right: var(--space-sm); background: currentColor;
}
.badge--green, .badge--success { color: var(--success-text-on-soft); }
.badge--green::before, .badge--success::before { background: var(--success-badge-fill); }
.badge--blue, .badge--info { color: var(--info-text-on-soft); }
.badge--blue::before, .badge--info::before { background: var(--info-badge-fill); }
.badge--orange, .badge--warning { color: var(--warning-text-on-soft); }
.badge--orange::before, .badge--warning::before { background: var(--warning-badge-fill); }
.badge--red, .badge--danger { color: var(--danger-text-on-soft); }
.badge--red::before, .badge--danger::before { background: var(--danger-badge-fill); }
.badge--critical { border: 1px solid color-mix(in srgb, var(--critical) 45%, transparent); background: var(--critical-soft); color: var(--critical-text-on-soft); }
.badge--muted { border: 1px solid var(--border-100); }
.badge--current { background: var(--info-soft); color: var(--info-text-on-soft); }
.badge--solid { border: 0; background: var(--switch-on); color: var(--text-on-status); }
.status-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }

.alert {
  display: flex;
  gap: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--border-100);
  border-radius: var(--radius-control);
  background: var(--bg-200);
  color: var(--text-100);
  font-size: var(--font-size-caption);
}
.alert--info { border-color: color-mix(in srgb, var(--info) 28%, transparent); background: var(--info-soft); color: var(--info-text-on-soft); }
.alert--success { border-color: color-mix(in srgb, var(--success) 28%, transparent); background: var(--success-soft); color: var(--success-text-on-soft); }
.alert--warning { border-color: color-mix(in srgb, var(--warning) 28%, transparent); background: var(--warning-soft); color: var(--warning-text-on-soft); }
.alert--danger { border-color: color-mix(in srgb, var(--danger) 28%, transparent); background: var(--danger-soft); color: var(--danger-text-on-soft); }
.alert--critical { border-color: color-mix(in srgb, var(--critical) 40%, transparent); background: var(--critical-soft); color: var(--critical-text-on-soft); }

.tabs {
  display: inline-flex;
  gap: var(--space-sm);
  /* Design refresh 2 (pick 7E): free-standing pills, no container box --
     the active one floats raised on a soft shadow. */
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
}
.tab {
  min-height: 28px;
  padding: var(--space-xs) var(--space-lg);
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-100);
  font-size: var(--font-size-caption);
  cursor: pointer;
}
.tab:hover { color: var(--text-000); background: var(--bg-300); }
.tab.active, .tab[aria-selected="true"] {
  background: var(--bg-raised);
  color: var(--text-000);
  box-shadow: var(--shadow-inset);
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  border-spacing: 0;
}
.data-table th {
  padding: var(--space-sm) var(--space-md);
  border-bottom: 1px solid var(--border-100);
  color: var(--text-200);
  font-size: var(--font-size-xs);
  font-weight: 500;
  text-align: left;
}
.data-table td {
  padding: var(--space-md);
  border-bottom: 1px solid var(--border-100);
  color: var(--text-000);
  font-size: var(--font-size-caption);
  vertical-align: middle;
  /* table-layout:auto sizes a column by its narrowest reasonable content --
     often the header word, not the (frequently longer) data underneath.
     Without a wrap fallback, a squeezed column with unbroken content (an
     IP, a country name with no natural break point, an ASN string) spills
     visibly past its own cell instead of staying inside the row.
     break-word (not the older anywhere value this used to be) still
     protects against that -- a single run wider than the whole cell still
     gets forced to break -- but tries every normal line-breaking
     opportunity first (spaces, hyphens), so a short, atomic token that
     fits the column at all (an IP address, a 2-letter country code, a
     port number) renders on one line instead of being cut at an arbitrary
     mid-character point wherever the available width happened to run out.
     Confirmed live (Xore/honeypot-stack dashboard, #24 follow-up): with
     the old `anywhere` value, "203.0.113.55" in a merely-narrow (not
     truly too-small) column rendered as three broken lines with no
     relation to the address's own digit groups. Only a genuinely
     unbreakable long run (a 64-char hash with no natural break point at
     all) still force-wraps here, which is the case this rule actually
     exists for. .n's own white-space: nowrap below is untouched since
     numeric counts should never wrap either way. */
  overflow-wrap: break-word;
}
.data-table tbody tr:last-child td { border-bottom: 0; }
.data-table tbody tr:hover td { background: var(--bg-500); }
.table-scroll { width: 100%; overflow-x: auto; }

/* #24 follow-up: opt-in responsive variant for a .data-table whose columns
   can't all fit a phone-width viewport at once (more than 2-3 columns, or
   columns whose content is inherently wide -- IPs, timestamps, hashes).
   Below the breakpoint each row becomes a stacked label:value block
   instead of a horizontally-scrolling or truncated table -- every column
   stays reachable without side-scrolling, and the label makes it
   unambiguous which value is which without a human having to cross-
   reference a header row that's no longer visually aligned with its
   column. The header row itself is visually hidden rather than
   display:none, so it's still announced by a screen reader before each
   stacked row's labels repeat the same information visually.

   Requires each <td> to carry a data-label="<column header text>"
   attribute -- this can't be generated from the <th> text automatically
   (CSS has no "read this other element's text content" mechanism), so
   the consuming page's own markup/template is responsible for keeping
   the two in sync. */
@media (max-width: 720px) {
  .data-table--responsive, .data-table--responsive thead,
  .data-table--responsive tbody, .data-table--responsive th,
  .data-table--responsive td, .data-table--responsive tr {
    display: block;
  }
  .data-table--responsive thead tr {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    clip: rect(0 0 0 0);
    overflow: hidden;
  }
  .data-table--responsive tbody tr {
    margin-bottom: var(--space-md);
    border: 1px solid var(--border-100);
    border-radius: var(--radius-panel);
    padding: var(--space-xs) 0;
    overflow: hidden;
  }
  .data-table--responsive tbody tr:last-child { margin-bottom: 0; }
  /* Label stacked above value, not beside it -- a beside-it flex row
     works fine for short fixed-width values (a time, a port) but a
     column like a free-text detail/message has no predictable length,
     and squeezing it into "whatever's left of the row next to the
     label" either overflows past the card or needs its own extra
     min-width:0/wrap plumbing to behave. Stacking sidesteps that
     entirely: the value always gets the row's full width to wrap into,
     which is correct for both the short and the long case. */
  .data-table--responsive td {
    padding: var(--space-sm) var(--space-lg);
    border-bottom: 1px solid var(--border-100);
    white-space: normal;
    overflow-wrap: break-word;
  }
  .data-table--responsive tr td:last-child { border-bottom: 0; }
  .data-table--responsive td::before {
    content: attr(data-label);
    display: block;
    margin-bottom: 2px;
    color: var(--text-200);
    font-size: var(--font-size-xs);
    font-weight: 500;
  }
  .data-table--responsive td:empty { display: none; }
  /* The cell's own white-space:normal above never reaches a value wrapped in
     an <a> (td.v a) or a bare numeric cell that also gets nowrap on desktop
     (td.n) -- both are set back to nowrap by the desktop-only
     "td.v a, td.n { white-space: nowrap; }" rule, which has no phone-width
     exception. At 390px a long single-line value (a user-agent, an AS name)
     then breaks out of the stacked card instead of wrapping into it, and
     because an ancestor clips overflow-x, the rest of the value is
     unreachable -- there is no gesture that reveals it. overflow-wrap:
     anywhere rather than break-word: these are single unbroken tokens with
     no natural wrap opportunity, so break-word alone would not break them. */
  .data-table--responsive td.v a,
  .data-table--responsive td.n { white-space: normal; overflow-wrap: anywhere; }
  /* Row quick-actions (copy IP / replay session / attacker profile,
     etc.) reveal on tr:hover / tr.selected in the desktop table -- hover
     has no touch equivalent, so at this width the cell was pure empty
     dead space on every card until a row happened to get selected. The
     same actions are already reachable from the row's own visible
     content or the opened detail pane once tapped. */
  .data-table--responsive .hp-row-actions-cell {
    display: none;
  }
  /* .card.wide table's own min-width:720px (below, a desktop-only
     "don't let a wide card's table get too cramped" rule) otherwise
     wins here too, since it targets the same <table> element -- found
     live: the responsive layout rules above were all correctly applying
     (td became a block, the label rendered), but the table's outer box
     still measured 720px wide inside a 390px card, so unwrapped content
     ran off into space nothing could scroll to reach rather than
     visibly wrapping. */
  .card.wide .data-table--responsive, .data-table--responsive {
    min-width: 0;
  }
}

.progress {
  height: 6px;
  overflow: hidden;
  border-radius: var(--radius-pill);
  background: var(--bg-400);
}
/* --v (0-100) drives the fill width, the same nonced-<style> pattern as
   .mini-chart/.heatmap above: see docs/CSP.md. Defaults to 0 so an unset
   fill renders empty, not an invalid width. */
.progress > span { display: block; width: calc(var(--v) * 1%); height: 100%; border-radius: inherit; background: var(--accent); --v: 0; }

.breadcrumb {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  color: var(--text-200);
  font-size: var(--font-size-caption);
}
.breadcrumb a { color: var(--text-100); text-decoration: none; }
.breadcrumb a:hover { color: var(--text-000); }

.pagination { display: inline-flex; gap: 3px; }
.pagination .btn { min-width: 30px; padding-inline: var(--space-sm); }

.empty-state {
  display: grid;
  min-height: 220px;
  place-items: center;
  padding: var(--space-2xl);
  text-align: center;
}
.empty-state__icon {
  display: grid;
  width: 42px;
  height: 42px;
  margin: 0 auto var(--space-md);
  place-items: center;
  border-radius: var(--radius-panel);
  background: var(--bg-300);
  color: var(--text-200);
}
.empty-state h3 { margin-bottom: var(--space-xs); font-family: var(--font-serif); font-weight: 500; font-size: var(--font-size-xl); }
/* 8E: quiet pill action under the sentence; optional inline icon. */
.empty-state .btn svg { width: 14px; height: 14px; }
.empty-state p { max-width: 380px; margin-bottom: var(--space-lg); font-size: var(--font-size-caption); }

.avatar {
  display: inline-grid;
  width: 30px;
  height: 30px;
  place-items: center;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent-text-on-soft); /* a11y: raw accent measured 3.7-4.4:1 */
  font-size: var(--font-size-xs);
  font-weight: 700;
}

.dropdown {
  min-width: 190px;
  padding: var(--space-xs);
  border: 1px solid var(--border-200);
  border-radius: var(--radius-panel);
  background: var(--bg-raised);
  box-shadow: var(--shadow-raised);
}
.dropdown__item {
  display: flex;
  width: 100%;
  min-height: 32px;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-sm);
  border: 0;
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--text-000);
  font-size: var(--font-size-caption);
  text-align: left;
  cursor: pointer;
}
.dropdown__item:hover { background: var(--bg-300); }
.dropdown__divider { height: 1px; margin: var(--space-xs); background: var(--border-100); }
.dropdown__shortcut { margin-left: auto; color: var(--text-200); font-size: var(--font-size-xs); letter-spacing: 0.02em; }
.dropdown__item--accent { color: var(--text-link); }
.dropdown__chevron { margin-left: auto; color: var(--text-200); }

.action-menu { position: relative; display: inline-block; }
/* Generic floating panel for non-menu content -- a details/summary
   disclosure that isn't a role="menu" (a hash-disclosure list, an
   account/settings popup body, anything that isn't a set of
   role="menuitem" actions). Built on the same native <details> mechanism
   as .action-menu, so it inherits outside-click-close and
   close-siblings-on-toggle for free via theme.js's :is(.action-menu,
   .popover) selectors. Panel chrome is shared with .action-menu__popover
   below (same surface-raised/border-strong/shadow-raised trio); only the
   menu-specific summary button styling stays exclusive to .action-menu. */
.popover { position: relative; display: inline-block; --popover-width: 200px; }
/* :not(.btn): the filter-bar's trigger reuses this shell with real button
   chrome (summary.btn btn-sm btn-secondary) -- the kebab box/font sizing
   below must not out-specify it (it rendered as 20px text spilling out of
   a 32px box). */
.action-menu > summary:not(.btn) {
  display: flex;
  width: 32px;
  height: 32px;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border-100);
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--text-100);
  font-size: 20px;
  line-height: 1;
  list-style: none;
  cursor: pointer;
}
.action-menu > summary::-webkit-details-marker { display: none; }
.action-menu > summary:not(.btn):hover,
.action-menu[open] > summary:not(.btn) { background: var(--bg-300); color: var(--text-000); }
.action-menu__popover,
.popover__panel {
  position: absolute;
  z-index: 30;
  top: calc(100% + 6px);
  right: 0;
  /* Per-instance width is a nonced <style> rule setting --popover-width
     (docs/CSP.md's existing --v-style pattern), not a page-local class --
     see docs/CSP.md for the pattern this follows. */
  width: var(--popover-width, 200px);
  padding: var(--space-sm);
  border: 1px solid var(--border-200);
  border-radius: var(--radius-panel);
  background: var(--bg-raised);
  box-shadow: var(--shadow-raised);
}
.action-menu__item {
  display: block;
  width: 100%;
  padding: var(--space-sm) var(--space-md);
  border: 0;
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--text-000);
  font: inherit;
  font-size: var(--font-size-caption);
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
}
.action-menu__item:hover { background: var(--bg-500); }
.action-menu__item--danger {
  margin-top: var(--space-xs);
  padding-top: var(--space-md);
  border-top: 1px solid var(--border-100);
  border-radius: 0;
  color: var(--danger);
}

/* Direction-agnostic by default (a toast can be stacked top-right,
   bottom-center, wherever the consumer places it) -- .toast--slide-in-right
   below is the opt-in modifier for a consumer that knows its own stack
   sits on one side and wants the toast to visibly arrive from off-screen. */
@keyframes toast-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}
@keyframes toast-slide-in-right {
  from { opacity: 0; transform: translateX(16px); }
  to { opacity: 1; transform: none; }
}
.toast {
  display: flex;
  max-width: 360px;
  align-items: flex-start;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-md);
  border: 1px solid var(--border-200);
  border-radius: 8px;
  background: var(--bg-raised);
  box-shadow: var(--shadow-raised);
  color: var(--text-000);
  font-size: var(--font-size-caption);
  animation: toast-in 200ms var(--ease-out) backwards;
}
.toast--slide-in-right { animation: toast-slide-in-right 200ms var(--ease-out) backwards; }
.toast__dismiss {
  background: transparent;
  border: none;
  padding: 0;
  color: inherit;
  font: inherit;
  cursor: pointer;
}

/* Severity, as a left rule.

   A toast that reports a condition has to say which kind at a glance --
   "a sensor stopped reporting" and "the sensor is reporting again" are
   opposite news in identical boxes otherwise, and an operator scanning a
   corner of the screen reads the colour before the sentence.

   A rule rather than a fill: the box already carries the surface tokens,
   and tinting the whole thing would mean re-checking contrast for text on
   nine themes' worth of tinted backgrounds to say something a 3px edge
   says just as well. The tokens are the same ones badges and buttons use,
   so the vocabulary matches the rest of the interface. */
.toast--info { border-left: 3px solid var(--accent); }
.toast--success { border-left: 3px solid var(--success); }
.toast--warning { border-left: 3px solid var(--warning); }
.toast--danger { border-left: 3px solid var(--danger); }

kbd {
  padding: 2px var(--space-xs);
  border: 1px solid var(--border-200);
  border-bottom-width: 2px;
  border-radius: 4px;
  background: var(--bg-300);
  color: var(--text-100);
  font-size: var(--font-size-xs);
}

pre {
  overflow: auto;
  padding: var(--space-lg);
  border: 1px solid var(--border-100);
  border-radius: var(--radius-control);
  background: var(--bg-sidebar);
  color: var(--text-100);
  font-size: var(--font-size-caption);
}

/* ── Segmented control — icon variant ────────────────────────────── */

.segmented {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  border: 1px solid var(--border-100);
  border-radius: calc(var(--radius-control) + 3px);
  background: var(--bg-200);
}
.segmented button {
  display: inline-flex;
  min-height: 28px;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  padding: var(--space-xs) var(--space-md);
  border: 0;
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--text-100);
  font-size: var(--font-size-caption);
  cursor: pointer;
}
.segmented button:hover { color: var(--text-000); }
.segmented button.active, .segmented button[aria-pressed="true"] { background: var(--bg-400); color: var(--text-000); }
.segmented button svg { width: 14px; height: 14px; }

.segmented--icon button { min-height: 32px; padding: var(--space-xs) var(--space-md); }
.segmented--icon button svg { width: 16px; height: 16px; }

/* ── Chip — pill shortcut near prompts and lists ─────────────────── */

.chip {
  display: inline-flex;
  min-height: 32px;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-lg);
  border: 1px solid var(--border-100);
  border-radius: var(--radius-pill);
  background: var(--bg-300);
  color: var(--text-000);
  font-size: 12.5px;
  cursor: pointer;
  transition: background var(--transition-fast), border-color var(--transition-fast);
}
.chip:hover { background: var(--bg-500); border-color: var(--border-200); }
/* Selected/applied state, same accent-outline treatment .segmented and the
   reports gallery already use for their own pressed controls. Two selectors
   because chips carry this state for two different reasons: a real toggle
   sets aria-pressed, while a chip that merely *represents* something already
   applied -- an active filter scope, a count that is non-zero -- is not a
   toggle and must not claim to be one, so it opts in with .is-active. */
.chip[aria-pressed="true"], .chip.is-active { background: var(--accent-soft); border-color: var(--accent); }
.chip[aria-pressed="true"]:hover, .chip.is-active:hover { background: var(--accent-soft); border-color: var(--accent); }
.chip svg { width: 14px; height: 14px; opacity: 0.75; }

/* ── Onboarding card — first-run checklist with progress ─────────── */

.onboarding-card {
  padding: var(--space-lg);
  border: 1px solid var(--border-100);
  border-radius: var(--radius-panel);
  background: var(--bg-200);
}
.onboarding-card__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-lg);
}
.onboarding-card__title { color: var(--text-000); font-size: var(--font-size-body); font-weight: 600; }
.onboarding-card__count { color: var(--text-200); font-size: var(--font-size-xs); }
.onboarding-card__progress {
  display: flex;
  gap: var(--space-sm);
  margin-bottom: var(--space-lg);
}
.onboarding-card__step {
  flex: 1;
  height: 3px;
  border-radius: var(--radius-pill);
  background: var(--bg-400);
}
.onboarding-card__step--completed { background: var(--accent); }
.onboarding-card__checklist {
  list-style: none;
  padding: 0;
  margin: 0;
}
.onboarding-card__checklist li {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-sm) 0;
  color: var(--text-100);
  font-size: var(--font-size-footnote);
  border-bottom: 1px solid var(--border-100);
}
.onboarding-card__checklist li:last-child { border-bottom: 0; }
.onboarding-card__checklist li.completed { color: var(--text-200); text-decoration: line-through; }

/* ── Message / chat bubble — assistant interface ──────────────────── */

.message {
  display: flex;
  gap: var(--space-lg);
  padding: var(--space-lg) 0;
}
.message__avatar { flex: none; }
.message__content { flex: 1; min-width: 0; }
.message__meta {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-sm);
  color: var(--text-200);
  font-size: var(--font-size-xs);
}
.message__bubble {
  padding: var(--space-lg) var(--space-lg);
  border-radius: var(--radius-panel);
  background: var(--bg-200);
  color: var(--text-000);
  font-size: var(--font-size-body);
  line-height: 1.6;
}
.message--user .message__bubble { background: var(--bg-300); }

/* ── Project card — dashboard grid item with metadata ────────────── */

.project-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: var(--space-lg);
}
.project-card {
  /* A grid item's default min-width is auto (content-based), so an
     unbreakable long title (a hash, a URL) would widen the whole track
     instead of respecting the title's own overflow:hidden/ellipsis -- the
     classic CSS Grid + nowrap-text trap. min-width: 0 lets the item (and
     everything nested in it) actually shrink to the track's size. */
  min-width: 0;
  padding: var(--space-lg);
  border: 1px solid var(--border-100);
  border-radius: var(--radius-panel);
  background: var(--bg-200);
  transition: background var(--transition-fast), border-color var(--transition-fast);
  cursor: pointer;
}
.project-card:hover { background: var(--bg-300); border-color: var(--border-200); }
.project-card__header {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  margin-bottom: var(--space-sm);
}
/* An icon and/or multiple badges are both optional -- a header with just a
   title behaves exactly as before (the title filling the row takes over
   space-between's old job of pushing a lone badge to the far end). */
.project-card__icon {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  flex: none;
  border-radius: var(--radius-control);
  background: var(--accent-soft);
  color: var(--accent);
}
.project-card__icon svg { width: 16px; height: 16px; }
.project-card__title { flex: 1; min-width: 0; color: var(--text-000); font-size: var(--font-size-body); font-weight: 600; }
.project-card__badge { flex: none; }
.project-card__badges { display: flex; flex: none; flex-wrap: wrap; gap: var(--space-xs); }
.project-card__desc {
  color: var(--text-100);
  font-size: var(--font-size-caption);
  line-height: 1.5;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.project-card__meta {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-md);
  color: var(--text-200);
  font-size: var(--font-size-xs);
}

/* ── Catalog card ─────────────────────────────────────────────────────
   .project-card (above) is "your own items" -- a plain title/description/
   meta list. This is for a browsable directory of things you *add*
   (skills, connectors, plugins): a leading icon slot, a badge row for
   flags like New/Community/#N popular, and a trailing round add button. */
.catalog-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: var(--space-lg);
}
.catalog-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  padding: var(--space-lg);
  border: 1px solid var(--border-100);
  border-radius: var(--radius-panel);
  background: var(--bg-200);
  transition: background var(--transition-fast), border-color var(--transition-fast);
}
.catalog-card:hover { background: var(--bg-300); border-color: var(--border-200); }
.catalog-card__header { display: flex; align-items: flex-start; gap: var(--space-md); }
.catalog-card__icon {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  flex: none;
  border-radius: var(--radius-control);
  background: var(--accent-soft);
  color: var(--accent);
}
.catalog-card__icon svg { width: 18px; height: 18px; }
.catalog-card__title-group { flex: 1; min-width: 0; }
.catalog-card__title { color: var(--text-000); font-size: var(--font-size-body); font-weight: 600; }
.catalog-card__badges { display: flex; flex-wrap: wrap; gap: var(--space-xs); margin-top: var(--space-xs); }
.catalog-card__desc { flex: 1; color: var(--text-100); font-size: var(--font-size-caption); line-height: 1.5; }
.catalog-card__add {
  display: grid;
  place-items: center;
  align-self: flex-end;
  width: 28px;
  height: 28px;
  border: 1px solid var(--border-200);
  border-radius: 50%;
  background: transparent;
  color: var(--text-100);
  cursor: pointer;
  transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
}
.catalog-card__add:hover { background: var(--accent); border-color: var(--accent); color: var(--text-on-accent); }
.catalog-card__add svg { width: 14px; height: 14px; }

/* ── Modal header with search ────────────────────────────────────── */

.modal__header {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--border-100);
}
.modal__header-title { flex: 1; color: var(--text-000); font-size: var(--font-size-lg); font-weight: 600; }
.modal__header .search-field { flex: 1; max-width: 320px; }

/* ── App shell ───────────────────────────────────────────────────── */

.app-shell {
  display: grid;
  min-height: 100dvh;
  /* auto, not var(--toolbar-height): the 11E pill toolbar's box is
     toolbar-height + its 8px inset (see .app-toolbar) -- a hard-sized row
     kept the old 32px and let the pill overflow on top of .app-main
     (browser matrix seam check, CI). The row now takes the toolbar's real
     height whatever it is. */
  grid-template: auto 1fr / var(--sidebar-width) minmax(0, 1fr);
  background: var(--bg-000);
}
.app-shell--wide { grid-template-columns: var(--sidebar-width-wide) minmax(0, 1fr); }
.app-toolbar {
  position: relative;
  z-index: 20;
  grid-column: 1 / -1;
  display: flex;
  /* Design refresh 2 (pick 11E): a detached floating pill bar instead of a
     full-bleed bordered strip -- breadcrumb left, status right. The pill
     is painted by ::before INSIDE the toolbar's own box (padding makes the
     room) rather than with margins: a grid item's margin does not grow its
     row track, so the margined pill visually overlapped the main region
     below (caught by the browser matrix's toolbar/main seam check). */
  height: calc(var(--toolbar-height) + 8px);
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-sm) var(--space-xl) 0;
  border: 0;
  background: transparent;
}
.app-toolbar::before {
  content: "";
  position: absolute;
  inset: 8px 14px 0;
  z-index: -1;
  border-radius: var(--radius-pill);
  background: var(--bg-200);
  box-shadow: var(--shadow-raised);
}
/* 11E breadcrumb: section / page, left-aligned in the pill. min-width:0 +
   ellipsis on both halves: the crumb is the toolbar's only variable-width
   text, and a long nowrap page name ("Source & pipeline health") made the
   whole toolbar's min-content wider than a phone viewport -- the grid
   stretched to it and every page scrolled sideways (browser matrix). */
.hp-crumb { display: flex; min-width: 0; max-width: 48vw; flex: 0 1 auto; align-items: center; gap: var(--space-sm); padding: 0 var(--space-sm); font-size: var(--font-size-caption); color: var(--text-100); white-space: nowrap; }
.hp-crumb b { color: var(--text-000); font-weight: 600; }
.hp-crumb b, .hp-crumb [data-hp-crumb-section] { min-width: 0; flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; }
.hp-crumb .sep { opacity: .45; flex: none; }
/* Toolbar icon controls never shrink -- the crumb is the flexible part. */
.app-toolbar > .btn-icon, .hp-toolbar-actions .btn-icon, .hp-toolbar-avatar { flex: none; }
.app-toolbar__title { flex: 1; color: var(--text-200); font-size: var(--font-size-xs); text-align: center; }
.app-sidebar {
  display: flex;
  min-height: 0;
  flex-direction: column;
  padding: var(--space-md) var(--space-sm);
  /* Design refresh 2 (pick 12E): borderless minimal rail -- the ground
     itself separates it from the content. */
  border-right: 0;
  background: var(--bg-sidebar);
}
.app-sidebar__body { flex: 1; overflow-y: auto; }
.app-main { min-width: 0; overflow: auto; }
.app-content { width: min(100% - 40px, var(--content-width)); margin: 0 auto; padding: var(--space-2xl) 0 96px; }
/* A flat 1360px cap left large unused margins on any screen wider than
   that -- confirmed live (Xore/honeypot-stack dashboard) on a 1920px
   display the content area used barely 70% of the available width, and
   on a 3840px 4K display well under half. 94vw scales the cap with the
   viewport instead of ignoring it past 1360px; the 2200px ceiling is
   still there so table columns/paragraphs don't get uncomfortably wide
   and sparse on a genuinely huge display -- this is a density-heavy data
   dashboard's own content width, not a marketing page's reading column. */
.app-content--wide { --content-width: min(94vw, 2200px); }

.sidebar__search { margin-bottom: var(--space-md); }
.sidebar__section-label { padding: var(--space-md) var(--space-sm) var(--space-xs); }
.sidebar__item {
  display: flex;
  width: 100%;
  min-height: 32px;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-sm);
  border: 0;
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--text-100);
  font-size: var(--font-size-caption);
  text-align: left;
  cursor: pointer;
  transition: background var(--transition-fast), color var(--transition-fast);
}
/* Design refresh 2 (pick 10E, supersedes 10C's accent bar): the active
   page floats on a raised pill with a soft shadow -- claude sidebar's
   current-conversation treatment. */
.sidebar__item { border-radius: 10px; }
.sidebar__item:hover { background: var(--bg-300); color: var(--text-000); }
.sidebar__item.active {
  background: var(--bg-raised);
  color: var(--text-000);
  box-shadow: var(--shadow-inset);
}
.sidebar__item svg { width: 15px; height: 15px; opacity: 0.72; }
.sidebar__profile {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm);
  border-top: 1px solid var(--border-100);
}

.command-bar {
  position: fixed;
  z-index: 30;
  right: 24px;
  bottom: 18px;
  left: calc(var(--sidebar-width) + 24px);
  width: min(720px, calc(100vw - var(--sidebar-width) - 48px));
  margin: auto;
  padding: var(--space-sm);
  border: 1px solid var(--border-200);
  border-radius: var(--radius-panel);
  background: var(--bg-raised);
  box-shadow: var(--shadow-raised);
}
.command-bar .form-input { border: 0; background: transparent; box-shadow: none; }

.modal-backdrop {
  position: fixed;
  z-index: 100;
  inset: 0;
  visibility: hidden;
  background: var(--overlay-bg);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--transition), visibility var(--transition);
}
.modal-backdrop.open { visibility: visible; opacity: 1; pointer-events: auto; }

.modal {
  position: fixed;
  z-index: 101;
  top: 50%;
  /* #672 (APIARY): horizontal centering was left: 50% + a matching
     -50% in the transform below, the same percentage-offset trick the
     vertical axis still uses (kept -- see why below). It has a real bug
     with this codebase's own scrollbar-gutter: stable (html rule, this
     file): confirmed live, at a 390px viewport with a modal at width:
     100vw (the <=720px breakpoint further down), `left: 50%` resolved
     to 187.5px instead of 195px -- a 7.5px discrepancy, exactly half
     the reserved scrollbar gutter, reproducing even with no scrollbar
     actually visible. left: 0 + right: 0 + margin-inline: auto centers
     a box with an explicit width identically, but never computes a
     percentage-of-viewport value at all, so it can't inherit that
     interaction -- confirmed live, the same 390px case now lands flush
     at both edges, zero overflow.
     Vertical (top: 50% + translateY(-50%)) is deliberately NOT changed
     the same way: scrollbar-gutter reserves horizontal space for a
     vertical scrollbar, so the vertical axis was never actually
     affected by this bug in the first place -- and unlike the
     horizontal fix, an inset-based vertical equivalent (top: 0;
     bottom: 0; margin-block: auto) has its own real cost: it only
     centers when height is an explicit value. .modal--promo's
     height: auto (content-sized) would stop being centered at all
     under that scheme -- confirmed live before settling on this mixed
     approach instead of applying the same fix to both axes uniformly. */
  left: 0;
  right: 0;
  margin-inline: auto;
  display: none;
  /* Caps were fixed at 960x680 regardless of viewport, so a modal (PDF
     viewer, settings, log viewer) stayed small on a large/4K display even
     though there was plenty of room. Scaling the cap with viewport size
     (still leaving a margin, still bounded so it doesn't sprawl on an
     ultrawide) uses that space without the modal ever exceeding it. */
  width: min(1280px, calc(100vw - 40px));
  height: min(840px, calc(100dvh - 40px));
  margin-block: 0;
  padding: 0;
  overflow: hidden;
  transform: translateY(-50%);
  border: 1px solid var(--border-200);
  border-radius: var(--radius-dialog);
  background: var(--bg-100);
  box-shadow: var(--shadow-dialog);
  color: var(--text-000);
  opacity: 0;
}
.modal.open { display: flex; opacity: 1; animation: dialog-in var(--transition); }
.modal--permanent {
  inset: 0;
  display: flex;
  width: 100vw;
  height: 100dvh;
  max-width: none;
  max-height: none;
  transform: none;
  border: 0;
  border-radius: 0;
  opacity: 1;
  visibility: visible;
}
.modal--permanent.open { transform: none; }
.modal--permanent::backdrop { background: var(--bg-000); }
.modal__sidebar {
  width: 220px;
  flex: none;
  padding: var(--space-md) var(--space-sm);
  overflow-y: auto;
  border-right: 1px solid var(--border-100);
  background: var(--bg-sidebar);
}
.modal__content { position: relative; flex: 1; min-width: 0; padding: var(--space-2xl) var(--space-2xl); overflow-y: auto; }
.modal__close {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 28px;
  height: 28px;
  border: 0;
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--text-200);
  cursor: pointer;
}
.modal__close:hover { background: var(--bg-300); color: var(--text-000); }

.settings-layout {
  display: grid;
  min-height: 100dvh;
  grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
}
.settings-layout__sidebar {
  padding: var(--space-md) var(--space-sm);
  border-right: 1px solid var(--border-100);
  background: var(--bg-sidebar);
}
.settings-layout__content { min-width: 0; padding: var(--space-2xl) var(--space-2xl); overflow-y: auto; }
.modal--permanent > .settings-layout { width: 100%; min-height: 0; }

.settings-grid {
  display: grid;
  grid-template-columns: minmax(220px, 1fr) minmax(260px, 1.4fr);
  gap: var(--space-lg) var(--space-lg);
  align-items: end;
}
.settings-field { min-width: 0; }
.settings-field .form-label { display: flex; justify-content: space-between; gap: var(--space-sm); }
.settings-field__meta {
  color: var(--text-200);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
}
.settings-field__desc { margin-top: var(--space-xs); color: var(--text-200); font-size: var(--font-size-xs); line-height: 1.4; }
.settings-actions { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-sm); margin-top: var(--space-lg); }
.restart-note { color: var(--warning); font-size: var(--font-size-caption); }

.passkey-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  padding: var(--space-lg) 0;
  border-bottom: 1px solid var(--border-100);
}
.passkey-row:last-child { border-bottom: 0; }
.passkey-row__name {
  color: var(--text-000);
  font-family: var(--font-mono);
  font-size: var(--font-size-footnote);
  font-weight: 600;
}
.passkey-row__meta { margin-top: var(--space-xs); color: var(--text-200); font-size: var(--font-size-xs); }

.edit-dialog-backdrop {
  position: fixed;
  z-index: 300;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--space-xl);
  background: var(--overlay-bg);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
.edit-dialog-backdrop.open { display: flex; }
.edit-dialog {
  width: min(520px, 100%);
  max-height: calc(100dvh - 32px);
  padding: var(--space-xl);
  overflow-y: auto;
  border: 1px solid var(--border-200);
  border-radius: var(--radius-dialog);
  background: var(--bg-200);
  box-shadow: var(--shadow-dialog);
}
.edit-dialog__title { margin: 0 0 var(--space-sm); font-size: 18px; font-weight: 600; }
.edit-dialog__desc { margin: 0 0 var(--space-lg); color: var(--text-100); font-size: var(--font-size-caption); line-height: 1.5; }
.edit-dialog__actions { display: flex; justify-content: flex-end; gap: var(--space-sm); margin-top: var(--space-lg); }
.danger-dialog__warning {
  padding: var(--space-md) var(--space-lg);
  border: 1px solid color-mix(in srgb, var(--danger) 35%, transparent);
  border-radius: var(--radius-control);
  background: var(--danger-soft);
  color: var(--text-000);
  font-size: var(--font-size-caption);
  line-height: 1.55;
}
.danger-dialog__warning ul { margin: var(--space-sm) 0 0; padding-left: var(--space-lg); }

.auth-page {
  display: grid;
  min-height: 100dvh;
  place-items: center;
  padding: var(--space-2xl);
}
.auth-wrap { width: min(100%, 520px); }
.auth-brand { margin-bottom: var(--space-2xl); text-align: center; }
.auth-brand h1 { margin-bottom: var(--space-sm); font-size: clamp(2.4rem, 8vw, 4rem); }
.auth-brand p { margin: 0; color: var(--text-100); }
.auth-card {
  padding: var(--space-xl);
  border: 1px solid var(--border-100);
  border-radius: var(--radius-dialog);
  background: var(--bg-100);
  box-shadow: var(--shadow-raised);
}
.auth-card .btn { width: 100%; }

/* Split promo dialog — announcement content beside an artwork panel. */
.modal--promo {
  width: min(760px, calc(100vw - 40px));
  height: auto;
  max-height: min(620px, calc(100dvh - 40px));
}
.modal--promo.open { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.modal--promo .modal__body { min-width: 0; padding: var(--space-2xl) var(--space-2xl); overflow-y: auto; }
.modal--promo .modal__media { background: var(--artwork-plate); }
.modal--promo .modal__media img { width: 100%; height: 100%; object-fit: cover; }

/* Command palette — a search-everything dialog, anchored near the top of
   the viewport rather than dead-centered like .modal's default, sized to
   its content instead of a fixed box. .modal__close (already absolute,
   top-right) drops in unchanged; .command-palette__field is the input row
   in place of .modal__header, and .command-palette__results is optional --
   a palette with only a field and no results list is a complete, valid
   instance of this component. */
.modal--palette {
  /* Anchored near the top, not vertically centered like the base
     .modal -- top: 14dvh is meant literally, so the base's
     translateY(-50%) has to be cancelled (transform: none) or it would
     shift this well above its intended position. Horizontal centering
     needs no override at all: the base's left: 0; right: 0;
     margin-inline: auto (not a transform) already centers any width,
     including this variant's own narrower one. */
  top: 14dvh;
  transform: none;
  height: auto;
  max-height: min(72dvh, 560px);
  flex-direction: column;
  width: min(680px, calc(100vw - 40px));
}
.command-palette__field {
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-lg) 44px var(--space-lg) var(--space-lg);
  border-bottom: 1px solid var(--border-100);
}
.command-palette__field svg { flex: none; width: 18px; height: 18px; color: var(--text-200); }
.command-palette__field input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  color: var(--text-000);
  font-size: var(--font-size-lg);
}
.command-palette__field input::placeholder { color: var(--text-200); }
.command-palette__results { min-height: 0; padding: var(--space-sm); overflow-y: auto; }
.command-palette__row {
  display: flex;
  width: 100%;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  border: 0;
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--text-000);
  font-size: var(--font-size-footnote);
  text-align: left;
  cursor: pointer;
}
.command-palette__row:hover, .command-palette__row.active { background: var(--bg-300); }
.command-palette__row svg { flex: none; width: 15px; height: 15px; color: var(--text-200); }
.command-palette__row-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.command-palette__row-meta { flex: none; color: var(--text-200); font-size: var(--font-size-xs); }
/* #1575: the preselected row's meta already swaps to "Enter" in JS
   (setActiveRow) -- render that swap as a small hint chip, claude.ai's own
   search-overlay treatment, instead of plain muted text indistinguishable
   from the group label it replaces. */
.command-palette__row.active .command-palette__row-meta {
  padding: 2px var(--space-sm);
  border-radius: var(--radius-pill);
  background: var(--bg-400);
  color: var(--text-100);
  font: 10.5px var(--hp-mono);
  letter-spacing: 0.03em;
}
.command-palette__empty { padding: var(--space-2xl) var(--space-lg); color: var(--text-200); font-size: var(--font-size-caption); text-align: center; }
@media (max-width: 767.98px) {
  .modal--palette { top: 8dvh; width: calc(100vw - 24px); }
}

/* Pill button — fully rounded calls to action. */
.btn--pill { padding-inline: var(--space-lg); border-radius: var(--radius-pill); }

/* Split authentication layout. */
.auth-split {
  display: grid;
  min-height: 100dvh;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
.auth-split__main { position: relative; display: grid; place-items: center; padding: 56px var(--space-2xl); }
.auth-split__brand {
  position: absolute;
  top: 26px;
  left: 32px;
  font-family: var(--font-display);
  font-size: 19px;
  letter-spacing: -0.02em;
  color: var(--text-000);
  text-decoration: none;
}
.auth-split__aside {
  margin: var(--space-lg);
  overflow: hidden;
  border-radius: var(--radius-dialog);
  background: var(--artwork-plate);
}
.auth-split__aside img { width: 100%; height: 100%; object-fit: cover; }

@media (max-width: 900px) {
  .auth-split { grid-template-columns: 1fr; }
  .auth-split__aside { display: none; }
  .auth-split__brand { position: static; justify-self: start; margin-bottom: var(--space-xl); }
}

@media (max-width: 640px) {
  .modal--promo.open { grid-template-columns: 1fr; }
  .modal--promo .modal__media { display: none; }
}

@media (max-width: 720px) {
  :root { --sidebar-width: 76px; }
  .app-sidebar .sidebar__item span,
  .app-sidebar .sidebar__section-label,
  .app-sidebar .sidebar__profile > div { display: none; }
  .app-sidebar .sidebar__item { justify-content: center; }
  .app-content { width: min(100% - 28px, var(--content-width)); padding-top: var(--space-xl); }
  .command-bar { right: 14px; left: calc(var(--sidebar-width) + 14px); width: auto; }
  /* Stacking to one column here leaves .settings-layout's two rows both
     sized "auto" with no explicit grid-template-rows -- grid then splits
     the container's leftover definite height (it's height:100% inside a
     fixed-height modal, see .hp-dash-settings > .settings-layout below)
     roughly evenly between them regardless of actual content, so a short
     nav list and a tall settings pane end up ~50/50 and the pane a viewer
     actually needs is left cramped. Sizing the nav row to its own content
     and giving the content row the rest fixes that; capping the nav's
     height (with its own scrollbar) keeps a long admin nav list from
     re-creating the same problem in reverse. */
  .settings-layout { grid-template-columns: 1fr; grid-template-rows: auto minmax(0, 1fr); }
  .settings-layout__sidebar { border-right: 0; border-bottom: 1px solid var(--border-100); max-height: 25dvh; overflow-y: auto; }
  .settings-grid { grid-template-columns: 1fr; }
  .modal { width: 100vw; height: 100dvh; border: 0; border-radius: 0; }
  .modal__sidebar { width: 190px; }
}

@media (max-width: 520px) {
  :root { --sidebar-width: 0px; }
  /* minmax(0, 1fr), not bare 1fr: 1fr's auto minimum floors the track at
     the toolbar's intrinsic width, so one long nowrap toolbar item (the
     11E breadcrumb) stretched the whole shell past a phone viewport. */
  .app-shell { grid-template-columns: minmax(0, 1fr); }
  .app-sidebar { display: none; }
  .app-main { grid-column: 1; }
  .app-toolbar__title { text-align: left; }
  .command-bar { left: 14px; }
  .modal { flex-direction: column; }
  .modal__sidebar { width: 100%; max-height: 36dvh; border-right: 0; border-bottom: 1px solid var(--border-100); }
  .modal__content { padding: var(--space-xl) var(--space-lg); }
  .card { padding: var(--space-lg); }
  .card__row { align-items: flex-start; flex-direction: column; gap: var(--space-sm); }
  .project-grid { grid-template-columns: 1fr; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    scroll-behavior: auto !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ═══════════════════════════════════════════════════════════════════════
   Xore/honeypot-stack — application-specific
   ═══════════════════════════════════════════════════════════════════════
   Not part of the shared design system below this point -- consumed only
   by that app's dashboard, kept here so it links a single stylesheet
   instead of vendoring theme.css plus its own separate file. Layout glue,
   one-off dashboard components the system above has no concept of
   (sensor badges, the attack map, the event-detail metadata grouping,
   payload workbench states, ...), and the small number of one-off
   spacing/layout rules that used to be a Tailwind utility build (#191) --
   see the "one-off utilities" section at the end. */

:root {
  --hp-font: var(--font-sans);
  --hp-mono: var(--font-mono);
}

/* theme.css sets `svg { display: block }`, which would defeat the hidden
   attribute used by the dashboard's live badges, panels, and theme icons.
   body[hidden] already outranks a bare svg{} selector on specificity alone
   (extra attribute selector), but !important is kept here defensively --
   this rule silently breaking would show every hidden icon at once. */
body [hidden] { display: none !important; }

/* .mono was a full duplicate of theme.css's own `.mono, code, kbd` rule --
   deleted rather than kept in sync twice (Xore/theme is the reference). */

/* ── App shell adaptations (theme.css owns .app-shell geometry) ─────────── */
.app-shell { height: 100dvh; transition: grid-template-columns var(--transition); }
/* The modal host is a direct app-shell child. It must not consume a grid cell;
   its fixed-position backdrops remain layout-neutral through display:contents. */
#hp-modal-root { display: contents; }
.app-sidebar { grid-column: 1; grid-row: 2; }
.app-main { grid-column: 2; grid-row: 2; }
.app-shell.hp-collapsed { --sidebar-width: 0px; }
.app-shell.hp-collapsed .app-sidebar { padding: 0; border-right: none; overflow: hidden; }
/* Accessible off-canvas navigation under 520px (sidebar is display:none). */
@media (max-width: 520px) {
  .app-main { grid-column: 1; }
  .app-shell.hp-nav-open .app-sidebar {
    display: flex; position: fixed; z-index: 60; inset: 0 auto 0 0;
    width: 224px; padding: var(--space-md) var(--space-sm);
    background: var(--bg-sidebar);
    border-right: 1px solid var(--border-100);
    box-shadow: var(--shadow-dialog);
  }
  /* Scrim behind the off-canvas drawer, same dim/blur treatment as
     .modal-backdrop, z-index just under the sidebar's 60 so it sits
     between .app-main and the open drawer. Always present in the DOM;
     theme.js toggles visibility via .hp-nav-open on the shell, same as
     the sidebar rule above. */
  .app-shell__nav-scrim {
    position: fixed; z-index: 50; inset: 0;
    visibility: hidden; opacity: 0; pointer-events: none;
    background: var(--overlay-bg);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    transition: opacity var(--transition), visibility var(--transition);
  }
  .app-shell.hp-nav-open .app-shell__nav-scrim {
    visibility: visible; opacity: 1; pointer-events: auto;
  }
  /* The <=720px compact-rail rule below (icons only, text hidden) still
     matches at this narrower width too -- override it back to labeled
     items while the full-width off-canvas drawer is open, since there's
     room for text here and the rail's icon-only affordance assumes a
     76px-wide strip, not a 224px drawer. */
  .app-shell.hp-nav-open .app-sidebar .sidebar__item span { display: inline; }
  .app-shell.hp-nav-open .app-sidebar .sidebar__section-label,
  .app-shell.hp-nav-open .app-sidebar .sidebar__profile > div {
    display: block;
  }
  .app-shell.hp-nav-open .app-sidebar .sidebar__item { justify-content: flex-start; }
  /* #210/#215: the live indicator is now the single global one (was
     previously a page-local pill with room to say "Reconnecting…" on the
     overview page only). In the compact toolbar that text is the widest
     content it ever shows -- at this width it overflowed the viewport by a
     couple of pixels. Same "icon only, drop the text" pattern the sidebar
     brand already uses at its own compact breakpoint above; the dot's
     color (green/gray/orange) and the title attribute still carry the
     state. */
  .hp-live-state [data-hp-live-label] { display: none; }
}

.hp-brand {
  display: flex; align-items: center; gap: var(--space-sm);
  padding: var(--space-xs) var(--space-sm) var(--space-md);
  /* 12E: serif wordmark, claude-style. */
  font-family: var(--font-serif);
  color: var(--text-000); text-decoration: none;
}
.hp-brand:hover { color: var(--text-000); }
.hp-brand-mark {
  display: grid; place-items: center;
  width: 28px; height: 28px; flex: none;
  border-radius: var(--radius-control);
  background: var(--accent-soft); color: var(--accent);
}
.hp-brand-mark picture,
.hp-brand-mark img {
  display: block;
  width: 100%;
  height: 100%;
}
.hp-brand-mark img { object-fit: contain; border-radius: inherit; }
/* Theme-aware artwork. Both images are emitted and the theme picks one via
   its own intent tokens, so a third theme cannot fall through to the
   dark-optimised asset the way the old light/dark selector list did. */
.theme-art--dark { display: var(--theme-art-dark-display); }
.theme-art--light { display: var(--theme-art-light-display); }
.hp-brand-text { line-height: 1.25; }
.hp-brand-text strong { display: block; font-size: 13px; font-weight: 700; letter-spacing: 0.04em; }
.hp-brand-accent { color: var(--accent); }
.hp-brand-text small { display: block; font-size: 10px; color: var(--text-200); }

.hp-profile-name { font-size: 13px; color: var(--text-000); }

/* Compact rail ≤720px: icons only (theme.css hides item text/profile div). */
@media (max-width: 720px) {
  .app-sidebar .hp-brand { justify-content: center; padding-inline: 0; }
  .app-sidebar .hp-brand-text { display: none; }
}

/* ── Toolbar ────────────────────────────────────────────────────────────── */
.hp-toolbar-actions { display: flex; align-items: center; gap: 2px; }
.app-toolbar__brand {
  display: none;
  width: 22px;
  height: 22px;
  flex: none;
  place-items: center;
  border-radius: var(--radius-xs);
  background: var(--accent-soft);
}
.app-toolbar__brand img { width: 100%; height: 100%; object-fit: contain; }
@media (max-width: 520px) {
  .app-toolbar__brand { display: grid; }
}
/* #208: was an inline style="" on the operator-configured maintenance/
   incident banner -- silently dropped by the CSP (a nonce covers a style
   or link element, never a style attribute).
   #283: #208's own fix kept the banner as a flex child of .app-toolbar
   (flex-basis: 100%, order: 5), which pushed it onto its own row *inside*
   the toolbar's own flex flow -- growing the toolbar's height and shoving
   its other children left/up rather than floating independently. Taken out
   of flow entirely instead: fixed, centered, sitting just below the
   toolbar, the same floating-overlay pattern .hp-modal-status already
   uses for a similar top-of-viewport status message. */
.app-toolbar__banner {
  position: fixed;
  top: calc(var(--toolbar-height) + 16px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 300;
  max-width: min(720px, calc(100vw - 32px));
  margin: 0;
}
/* .hp-icon-btn was a parallel reimplementation of theme.css's own
   .btn.btn-icon.btn-ghost combination -- same transparent background,
   text-secondary color, surface-2/text-primary hover, and even the same
   15x15 svg sizing (theme's `.btn svg` rule). Replaced at every call site
   instead of kept in sync twice (Xore/theme is the reference); only the
   26px->32px size and the border reset->transparent-border swap differ,
   both accepted as-is from theme. */
.btn-icon:has(.hp-alert-badge) { position: relative; }
/* .btn-icon is exactly 32px (width and min-height) -- the same value as
   --toolbar-height -- so the icon button has no vertical centering slack
   inside the toolbar and sits flush against its top edge. This badge's
   own negative top offset used to push it a further ~3px above that,
   landing it partly above the toolbar's (and the viewport's, since the
   toolbar sits at the very top of the page) top edge -- clipped and
   unreadable rather than an overlapping notification dot. Reported live
   on a mobile phone. */
.hp-alert-badge {
  position: absolute; top: 1px; right: -4px;
  min-width: 14px; height: 14px; padding: 0 3px;
  display: grid; place-items: center;
  border-radius: var(--radius-pill);
  background: var(--danger); color: var(--bg-000);
  font-size: 9px; font-weight: 700; line-height: 1;
}

/* ── Live indicator ─────────────────────────────────────────────────────── */
/* Refresh switch, not just an indicator: click to pause every live update. */
.hp-live-state {
  display: inline-flex; align-items: center; gap: var(--space-sm);
  padding: 2px var(--space-sm);
  border: 1px solid transparent; border-radius: var(--radius-control);
  background: transparent; font-family: inherit;
  font-size: 10px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--success); cursor: pointer;
  transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}
.hp-live-state:hover { border-color: color-mix(in srgb, currentColor 35%, transparent); background: var(--bg-300); }
.hp-live-state--paused { color: var(--text-200); }
/* #210: the one live indicator now covers what a separate overview-only
   .live-pill used to (paused, and a stalled/reconnecting SSE connection) --
   consolidated here since this toolbar control is global and clickable,
   where the old pill was neither. */
.hp-live-state--stalled { color: var(--warning); }
/* .live-dot was a duplicate of theme.css's own .status-dot (identical 7x7,
   50% radius, currentColor) that additionally hardcoded --success as its
   background -- redundant, since .hp-live-state and its --paused/--stalled
   modifiers above already set `color` for exactly this purpose, and
   .status-dot already reads currentColor. Renamed at every call site
   (the toolbar live/pause toggle, and the two "preparing inventory,
   reloading shortly" notes on payloads.html/payload_workbench.html); the
   pulse animation is a real addition theme's plain status dot has no
   concept of, so it's kept here as a default for every use in this app. */
.status-dot { animation: hp-live-pulse 2s ease-in-out infinite; }
.hp-live-state--paused .status-dot, .hp-live-state--stalled .status-dot { animation: none; }
@keyframes hp-live-pulse {
  0%, 100% { box-shadow: 0 0 0 3px var(--success-soft); }
  50%      { box-shadow: 0 0 0 6px color-mix(in srgb, var(--success) 4%, transparent); }
}

/* ── KPI tiles ──────────────────────────────────────────────────────────
   theme.css's .metric/.metric__* owns the tile box, value, label, and
   trend text (#191 -- this used to be a parallel .hp-stat implementation
   of the exact same component). Several dashboard tiles are links
   (overview, source-health link a KPI to the page it summarizes); theme's
   .metric has no anchor variant, so that's the one bit of glue left here. */
a.metric { display: block; text-decoration: none; transition: border-color var(--transition-fast), background var(--transition-fast); }
a.metric:hover { border-color: var(--border-200); background: var(--bg-300); }
.metric__value--text { font-size: 14px; font-weight: 600; line-height: 1.4; }

/* ── Card grid placement (theme.css owns .card visuals) ─────────────────── */
.card.half { grid-column: span 6; }
.card.wide, .wide { grid-column: 1 / -1; }
.card h2 {
  display: flex; align-items: center; gap: var(--space-sm);
  margin: 0 0 var(--space-md);
  /* Design refresh 2 (pick 1E, supersedes 1D's header strip): serif card
     titles, no accent tick, no contrasting band -- claude's card anatomy. */
  font-family: var(--font-serif);
  font-size: 16px; font-weight: 500; color: var(--text-000);
}
.card:has(table) { overflow: auto; }
.card table { min-width: 0; }
.card.wide table { min-width: 720px; }
/* A card whose list/table content can grow without bound (every row of a
   config-driven list, not a fixed top-N) needs its own bounded, scrollable
   region instead of relying on .card:has(table)'s overflow:auto, which only
   engages once something ELSE already constrains the card's height -- with
   no such constraint the card just grows forever and the page scrolls
   instead. Wrap the growing part (table, list, ...) in this rather than the
   whole card, so a heading/note/toolbar above it stays pinned in view while
   only the content below scrolls. */
/* overflow-y:auto alone gives no visual sign there's more content below the
   fold until a viewer happens to hover/scroll it -- on any OS using
   overlay/auto-hiding scrollbars (macOS default, many Linux desktops), an
   8+-row list truncated to a handful of visible rows looked identical to a
   hard cutoff, not a scrollable region (reported live against the
   honeypot-stack dashboard's Sensor feeds card: 21 real rows, only the
   first ~8 ever visible, zero indication the rest existed). Two independent
   fixes, not one: */
.card__scroll {
  /* #1536 (APIARY): .card__scroll is sometimes a card's whole content
     region (no separate wrapper -- directly after the <h2> header).
     Without flex:1 it only takes its own content's height, same as a plain
     block child; when a CSS-grid row stretches the card taller than that
     (a sibling .half card in the same row has more rows), the extra height
     lands as dead space below the table/list instead of the region growing
     into it. flex:1 1 auto with min-height:0 (the same pairing
     .card__content already uses) grows this region to fill exactly the
     space between the header and whatever follows it, still capped at
     max-height with a scrollbar past that point. */
  flex: 1 1 auto; min-height: 0;
  /* theme#78: viewport-driven instead of a fixed 340px peephole -- never
     smaller than before, grows with the screen. The 320px constant is the
     shell chrome + card header budget above/below the region. */
  max-height: max(340px, calc(100vh - 320px)); overflow: auto;
  overscroll-behavior: contain;
  /* 1. A thin, always-rendered scrollbar (not overlay-hidden) makes the
     region legible as "scrollable" at a glance, matching this file's own
     scrollbar-gutter precedent for the same class of invisible-affordance
     bug (see the html rule above). Standard `scrollbar-width` covers
     Firefox; the three ::-webkit-scrollbar-* rules below are the
     Chromium/Safari equivalent -- there is no shorthand that covers both
     engines in one declaration. */
  scrollbar-width: thin;
  scrollbar-color: var(--border-200) transparent;
  /* 2. The scrollbar alone only reads as "scrollable" to a viewer who
     already notices it -- it says nothing about whether more rows exist
     past the visible edge until they scroll and find out. A bottom fade
     into the card's own surface answers that on sight: the last row
     visibly dissolving into the background reads as "this cuts off, not
     ends" the instant the region renders, no interaction needed.
     --card-scroll-fade carries the card's real background (.card--raised
     overrides it below) so the gradient's opaque end always matches
     instead of a literal color. */
  --card-scroll-fade: var(--bg-200);
}
.card__scroll::after {
  /* 3. position: sticky (not absolute) pins this to the visible scroll
     edge instead of drifting with the content transform, and since a
     stuck sticky box paints as a relative offset rather than a reflow,
     it overlaps whatever row is currently at that edge with no z-index
     needed. Deliberately no margin-top pull-up: that would also shift
     its unstuck resting position, washing the last row on a short list
     or once scrolled to the true end -- exactly where nothing is left
     to reveal and the fade should disappear instead. */
  content: "";
  display: block;
  position: sticky;
  bottom: 0;
  height: var(--space-2xl);
  background: linear-gradient(to top, var(--card-scroll-fade), transparent);
  pointer-events: none;
}
.card__scroll::-webkit-scrollbar { width: 8px; height: 8px; }
.card__scroll::-webkit-scrollbar-track { background: transparent; }
.card__scroll::-webkit-scrollbar-thumb { background: var(--border-200); border-radius: 4px; }
.card--raised .card__scroll { --card-scroll-fade: var(--bg-raised); }

/* Render-first data surfaces. These are intentionally content-neutral so a
   server-rendered skeleton and its hydrated result keep exactly the same
   footprint across diagnostics, search, and report-library pages. */
.hp-data-card-list { display: flex; flex-direction: column; gap: var(--space-md); }
.hp-data-card {
  padding: var(--space-md) var(--space-lg);
  border: 1px solid var(--border-100);
  border-radius: var(--radius-control);
  background: var(--bg-100);
}
.hp-data-card__header {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-sm) var(--space-lg);
  margin-bottom: var(--space-md);
}
.hp-data-card__title { min-width: 0; color: var(--text-000); font-size: var(--font-size-footnote); word-break: break-word; }
.hp-data-card__meta { color: var(--text-200); font-size: var(--font-size-xs); }
.hp-code-results { min-height: 180px; max-height: 340px !important; overflow: auto !important; }
.hp-table-state td { padding: var(--space-lg) var(--space-md); text-align: center; }
.hp-table-state .empty { margin: 0; }
.hp-skeleton-cell { min-width: 150px; padding: var(--space-lg) var(--space-md) !important; }
.hp-project-grid-scroll { max-height: 520px; }
/* The payloads list row ends in a kebab menu (#205) whose popover is
   position:absolute and must escape the row -- overflow:auto on an
   ancestor clips it regardless of scroll position, popover open or not
   (CSS has no "auto but only clip the axis that's actually overflowing"
   mode: setting overflow-x:auto alone still forces overflow-y to compute
   as auto too, per the overflow spec's own visible-becomes-auto rule).
   Every other cell here already wraps (td.v's word-break:break-all
   handles the hash column), so this table was never actually depending
   on the horizontal scroll .card:has(table) exists for. */
#payloads-table-card { overflow: visible; }
#payloads-table-card table { min-width: 0; }
@media (max-width: 1180px) {
  .card { grid-column: span 6; }
  .card.wide, .wide { grid-column: 1 / -1; }
}
@media (max-width: 800px) {
  .card, .card.half { grid-column: 1 / -1; }
}

/* #1565: overview's ASNs/Providers card pair -- ASNs is routinely a dozen-
   plus dense rows of long "AS15169 Google LLC" strings, Providers usually
   a handful of short single-word labels, so a forced even half/half split
   read as an unbalanced pair. Same responsive fallbacks as every other
   .card.half above, just an 8/4 split instead of 6/6 above that. */
#overview-asns-card { grid-column: span 8; }
#overview-providers-card { grid-column: span 4; }
@media (max-width: 1180px) {
  #overview-asns-card, #overview-providers-card { grid-column: span 6; }
}
@media (max-width: 800px) {
  #overview-asns-card, #overview-providers-card { grid-column: 1 / -1; }
}

.section-heading {
  grid-column: 1 / -1;
  display: flex; align-items: flex-end; justify-content: space-between; gap: var(--space-lg);
  margin-top: var(--space-md); padding: var(--space-xs) 2px 0;
}
.section-heading:first-child { margin-top: 0; }
.section-heading h2 { margin: 0; font-size: 16px; font-weight: 600; color: var(--text-000); }
.section-heading p { margin: var(--space-xs) 0 0; font-size: 12px; color: var(--text-100); }
.section-link {
  font-size: 12px; font-weight: 500; white-space: nowrap;
  color: var(--text-link); text-decoration: none;
}
.section-link:hover { text-decoration: underline; }

/* ── Overview header ────────────────────────────────────────────────────── */
.overview-header {
  grid-column: 1 / -1;
  /* The right column used to be a bare `auto` track. Grid sizes `auto`
     tracks to their max-content (full unwrapped width) before handing
     whatever's left to the `1fr` sibling -- so a long, wrappable
     .live-panel string (.gen's text, white-space: normal) still claimed
     its entire unwrapped width first, squeezing the left column to its
     minmax(0, ...) floor of literally 0px in the gap between the 800px
     stacking breakpoint and wherever both sides' natural widths actually
     fit (confirmed live: getComputedStyle().gridTemplateColumns was
     "0px 477px" at 820px). Capping the right column forces its own text
     to wrap instead of stealing the left column's space.
  */
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 40%);
  align-items: end; gap: var(--space-lg); margin-bottom: var(--space-xl);
}
/* .eyebrow was a near-duplicate of theme.css's .label-section (same size/
   weight/letter-spacing/uppercase treatment) -- renamed to .label-section
   at every call site instead of keeping a parallel definition (Xore/theme
   is the reference). theme's own version has no margin-bottom since it
   doesn't assume a heading always follows; every use here does. */
.label-section { margin-bottom: var(--space-sm); }
.overview-header h1 {
  margin: 0; font-size: clamp(1.5rem, 2.2vw, 2rem);
  font-family: var(--font-display); font-weight: 400; letter-spacing: -0.035em; line-height: 1.1;
  color: var(--text-000);
}
.overview-header .subtitle {
  max-width: 700px; margin: var(--space-sm) 0 0;
  font-size: 13px; line-height: 1.55; color: var(--text-100);
}
/* #210: the overview header used to duplicate the toolbar's LIVE toggle
   with a separate, non-interactive .live-pill (added in #201). Removed --
   the toolbar toggle is now the single indicator, global and clickable,
   and covers the same paused/stalled states (see .hp-live-state--stalled
   above). */
.live-panel { display: flex; flex-direction: column; align-items: flex-end; gap: var(--space-sm); }
.gen { font: 11px var(--hp-mono); color: var(--text-200); }
@media (max-width: 800px) {
  .overview-header { grid-template-columns: 1fr; align-items: start; }
  .live-panel { align-items: flex-start; }
}

/* ── Dashboard tabs ─────────────────────────────────────────────────────
   theme.css's .tabs/.tab owns the chrome (border, background, radius,
   active/hover color) -- this used to be a parallel .dashboard-tabs/
   .dashboard-tab implementation of the same component (#191). What's left:
   the tab bar sits in a CSS grid of .card elements and must span every
   column (theme's .tabs is inline-flex, sized for a plain row); and each
   tab carries a numbered badge <span> theme's plain-text .tab has no
   concept of. */
.tabs { grid-column: 1 / -1; display: flex; flex-wrap: wrap; margin-bottom: var(--space-lg); }
.tabs .tab { display: flex; align-items: center; gap: var(--space-sm); }
.tabs .tab span {
  display: grid; place-items: center;
  width: 20px; height: 20px; border-radius: 5px;
  background: var(--bg-300); color: var(--text-200);
  font: 700 9px var(--hp-mono);
}
.tabs .tab.active span { background: var(--accent); color: var(--text-on-accent); }
.dashboard-panel { animation: hp-panel-in 150ms ease-out; }

/* Cards space themselves.

   Spacing used to be the container's job, and every version of that reached
   only cards whose parent was a .dashboard-panel. Measured on the live
   dashboard, most cards are not: /auth-events has two in an unclassed div and
   one inside .hp-md__list, all three computing margin-top: 0. A card that
   relies on its container is flush wherever someone wrapped it.

   Owning the margin makes it correct at any nesting depth. :last-child drops
   the trailing one so a container does not gain a phantom gap at its end, and
   flex/grid parents that already set their own gap are unaffected because
   nothing here adds to a gap they control. */
.card { margin-block-end: var(--space-lg); }
.card:last-child { margin-block-end: 0; }
@keyframes hp-panel-in { from { opacity: 0.35; transform: translateY(4px); } to { opacity: 1; transform: none; } }

/* ── Data tables ────────────────────────────────────────────────────────
   theme.css's .data-table owns the box model (padding, border-bottom
   rules, row hover) -- every table in the dashboard now carries that
   class (#191; used to be unclassed and reimplemented here). What's left
   is honeypot-specific column semantics theme has no concept of: numeric/
   value/age/state columns and the sensor-hue badges below. */
.data-table tbody tr { transition: background var(--transition-fast); }
td.n { color: var(--accent); font-family: var(--hp-mono); font-size: 12px; text-align: right; white-space: nowrap; }
td.n a { color: inherit; text-decoration: none; border-bottom: 1px dotted color-mix(in srgb, var(--accent) 45%, transparent); }
td.n a:hover { color: var(--text-000); border-color: var(--accent); }
td.v { color: var(--text-100); font-family: var(--hp-mono); font-size: 12px; word-break: break-all; }
/* #1566: /commands' "sources" column (up to 6 space-joined IPs plus a
   "+N" overflow marker -- intel.html's commandRow.Sources, already capped
   by campaigns.go's sortedSet) is plain text, not links, so td.v's own
   break-all -- needed elsewhere for unbroken hash-like values -- could
   still fracture a single IP mid-character once the column gets squeezed.
   Space-separated tokens already give the browser a wrap point between
   IPs; only fall back to a mid-token break if one still doesn't fit. */
.hp-ip-list { word-break: normal; overflow-wrap: break-word; }
/* min-width (desktop only): nowrap sibling columns (td.n, td.v a) could
   crush a plain-text value column (clusters' "coverage") to
   letter-per-line width -- word-break:break-all then breaks every
   character. A readable floor stops the collapse without giving up
   hash-wrapping. Scoped off phones: on a 390px viewport the floor made
   every multi-value table wider than the page (browser matrix mobile
   overflow, CI). */
@media (min-width: 700px) {
  td.v { min-width: 180px; }
  /* #1566: a floor under every nowrap timestamp cell too -- without it a
     crowded row (campaigns' 8 visible columns, commands' first/last) can
     still squeeze a [data-hp-utc] cell narrower than its own text, and
     nowrap alone just pushes the row wider instead of the character-level
     wrap that used to happen; the floor keeps "2026-08-16 21:55:00" (19
     chars) on one line without the cell fighting its neighbors for space.
     Off phones for the same reason td.v's own floor is: it would make a
     narrow table wider than a 390px viewport. */
  .data-table td[data-hp-utc] { min-width: 19ch; }
}
td.v a, td a.lnk { color: inherit; text-decoration: none; border-bottom: 1px dotted var(--border-200); }
td.v a:hover, td a.lnk:hover { color: var(--accent); border-color: var(--accent); }
td.ago { color: var(--text-200); font-family: var(--hp-mono); font-size: 11px; text-align: right; white-space: nowrap; }
td.state { font-family: var(--hp-mono); font-size: 11px; text-transform: uppercase; text-align: right; }
td.s-active { color: var(--success); }
td.s-quiet { color: var(--warning); }
td.s-stale { color: var(--danger); }
td.s-waiting { color: var(--text-200); }
/* Design refresh 2 (pick 4E, supersedes 4C's slabs): chrome-free open
   rows -- airy padding, hairline separators only, and the whole row lifts
   on a soft rounded hover wash (claude list feel). */
.data-table { border-collapse: separate; border-spacing: 0; }
.data-table thead th { border-bottom: 0; padding-bottom: var(--space-xs); }
.data-table td { background: transparent; border-bottom: 1px solid var(--border-100); padding: var(--space-md) var(--space-md); }
.data-table tbody tr:last-child td { border-bottom: 0; }
.data-table tbody tr:hover td { background: var(--bg-300); }
.data-table tbody tr:hover td:first-child { border-radius: 8px 0 0 8px; }
.data-table tbody tr:hover td:last-child { border-radius: 0 8px 8px 0; }
.hp-md__list tbody tr.selected td:first-child { border-radius: 8px 0 0 8px; }
.hp-md__list tbody tr.selected td:last-child { border-radius: 0 8px 8px 0; }
/* #1566: linked identifiers (IPs, CIDRs, sessions), timestamps and ports
   never wrap mid-value -- the "85.14.245.1(newline)22" class of bug. Plain
   text in value cells (alert keys, long hashes as text) keeps wrapping;
   a too-wide row scrolls inside its card instead of shattering.
   [data-hp-utc] is included directly (not just data-hp-time/
   data-label="time") so every table that already carries the
   machine-readable UTC twin gets the fix for free: campaigns'/commands'
   first/last, attackers' list, agent-campaigns' event rows, and any future
   table that follows the same twin convention, without each needing its
   own extra marker attribute -- exactly the two/three-line date and
   ISO-string wraps the issue reported on /campaigns, /commands and
   /attackers. */
td.v a, td.n,
.data-table td[data-label="time"],
.data-table td[data-label="port"],
.data-table td[data-hp-time],
.data-table td[data-hp-utc] { white-space: nowrap; }
td.v .eventdetail, td.v article, td.v .eventmeta { white-space: normal; }

/* Sticky header while a table scrolls inside its card region. */
.card__scroll .data-table thead th {
  position: sticky; top: 0; z-index: 2;
  background: var(--bg-200);
}

/* table.recent matches the theme's .data-table density and first-column meta. */
.recent td:nth-child(1) { color: var(--text-200); font-family: var(--hp-mono); font-size: 11px; white-space: nowrap; }

/* ── Sensor badges (based on the theme's .badge + per-sensor hues) ──────── */
/* A11y pass (design refresh): ink uses the *-text-on-soft tokens -- the
   raw state colors measured 3.0-4.2:1 on their soft fills over cards. */
.b-cowrie   { background: var(--info-soft);    color: var(--info-text-on-soft); }
.b-http     { background: var(--success-soft); color: var(--success-text-on-soft); }
.b-multipot { background: var(--warning-soft); color: var(--warning-text-on-soft); }
.b-dionaea  { background: var(--danger-soft);  color: var(--danger-text-on-soft); }
.b-conpot   { background: var(--bg-300);    color: var(--text-100); border: 1px solid var(--border-100); }
.b-tanner   { background: var(--accent-soft);  color: var(--accent-text-on-soft); }
.b-suricata { background: var(--bg-300);    color: var(--text-100); border: 1px solid var(--border-100); }

/* ── Filter rows and chips ──────────────────────────────────────────────── */
.filters { display: flex; gap: var(--space-sm); flex-wrap: wrap; align-items: center; margin-bottom: var(--space-lg); }
.filters .chip { font-family: var(--hp-mono); font-size: 11px; text-decoration: none; }
.hp-input, input.search {
  flex: 1; min-width: 180px; min-height: 34px;
  background: var(--bg-200);
  border: 1px solid var(--border-100);
  border-radius: var(--radius-control);
  padding: var(--space-sm) var(--space-md);
  font-size: 13px; color: var(--text-000); font-family: inherit;
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
/* A control holding a small number -- an hour, a minute, a day of the month,
   a retention count. Left at its natural width these stretch to the width of
   a text field and read as though they expect a sentence.

   Deliberately not .hp-input--num: this codebase has two base classes for a
   text control, .hp-input and .form-input, and these four call sites use
   both. A BEM modifier would claim one parent and then be worn by the other.

   Added for APIARY#1893, where four such controls carried a hand-written
   width each (80px three times, 6em once, for the same shape). */
.hp-num { width: 5rem; }
.hp-input::placeholder, input.search::placeholder { color: var(--text-200); }
.hp-input:focus, input.search:focus {
  border-color: var(--border-focus);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--border-focus) 22%, transparent);
}
button.copy {
  background: transparent; border: 1px solid var(--border-200);
  color: var(--text-100); border-radius: var(--radius-control);
  padding: var(--space-xs) var(--space-sm); font: 12px var(--hp-font); cursor: pointer;
  transition: color var(--transition-fast), border-color var(--transition-fast), background var(--transition-fast);
}
button.copy:hover { color: var(--text-000); background: var(--bg-300); }

/* Clipboard-copy primitive: any [data-copy] element toggles .is-copied via
   theme.js's click delegate -- a plain class toggle, not a per-element
   dynamic value, so no CSP nonce is needed here (contrast .mini-chart/
   .heatmap above). .copy-field is a suggested wrapper for a value+copy
   affordance pairing; [data-copy] itself is what theme.js listens for and
   works on any element, with or without this class. */
.copy-field { display: inline-flex; align-items: center; gap: var(--space-sm); }
[data-copy].is-copied::after { content: "copied"; margin-left: var(--space-sm); color: var(--success); font-size: var(--font-size-xs); }

/* ── Legacy content hooks ───────────────────────────────────────────────── */
.note { font-size: 12px; color: var(--text-200); margin: 0 0 var(--space-md); }
/* #135: the -4px pull-up was written as -var(...), which parses as an
   unknown function and dropped the whole declaration silently. */
.card .note { margin: calc(-1 * var(--space-xs)) 0 var(--space-md); }
.empty { font: 12px var(--hp-mono); color: var(--text-200); margin: 0; }
.cc {
  padding: 1px var(--space-xs); border-radius: var(--radius-xs);
  /* a11y: ink on the soft fill, not the fill's own hue -- var(--info) on
     var(--info-soft) read as white-on-light-blue in dark theme. Same
     pairing the sensor badges use. */
  background: var(--info-soft); color: var(--info-text-on-soft);
  font: 10px var(--hp-mono); text-decoration: none;
}
.lnk.sess { color: var(--success); border-color: color-mix(in srgb, var(--success) 40%, transparent); }
/* Event detail cell: the sensor message is the headline, and its pivots are
   bundled underneath in labelled groups (decoy identity, pivots, network
   origin, detection, analysis) instead of one undifferentiated run of links. */
.eventdetail { color: var(--text-000); }
.eventmeta {
  display: flex; flex-direction: column; gap: 3px;
  margin-top: var(--space-sm); padding-left: var(--space-sm);
  border-left: 1px solid var(--border-100);
}
.eventmeta__group { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-xs); }
.eventmeta__label {
  flex: none; width: 66px;
  font-size: var(--font-size-xs); font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--text-200); cursor: help;
}
.eventmeta .lnk {
  display: inline-block; max-width: 420px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--font-size-xs); color: var(--accent);
}

@media (max-width: 720px) {
  .eventmeta__label { width: auto; }
}

/* The nested settings confirmation is a native dialog, so its scrim is the
   browser's own ::backdrop (MODALS.md strategy 1 — never a sibling element).
   Match the .edit-dialog-backdrop treatment theme.css gives the
   application-managed overlays. */
#hp-settings-confirm::backdrop {
  background: var(--overlay-bg);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

/* ── Evidence viewer (hp-evidence.js) ───────────────────────────────────── */
.hp-evidence-modal {
  flex-direction: column;
  width: min(1320px, calc(100vw - 40px));
  max-height: calc(100dvh - 48px);
}
.hp-evidence-modal .modal__header { flex: none; background: var(--bg-sidebar); }
.hp-evidence-modal .modal__close { position: static; flex: none; }
.hp-evidence-count { flex: none; color: var(--text-200); font-size: var(--font-size-xs); white-space: nowrap; }
.hp-evidence-note { flex: none; margin: var(--space-md) var(--space-lg) 0; }
.hp-evidence-body { flex: 1; min-height: 0; padding: var(--space-md) var(--space-lg) var(--space-lg); overflow: auto; }
.hp-evidence-body pre { max-height: none; margin: 0; }
.hp-evidence-body table { width: 100%; }
/* The card chrome belongs to the page; inside the viewer the block is the
   whole surface. */
.hp-evidence-body .card { padding: 0; border: 0; background: transparent; }
.hp-evidence-body .card > h2 { display: none; }

@media (max-width: 720px) {
  .hp-evidence-modal { width: 100vw; max-height: 100dvh; border-radius: 0; }
  .hp-evidence-modal .modal__header { flex-wrap: wrap; }
  .hp-evidence-modal .modal__header .search-field { max-width: none; }
}

/* Search results: supporting context under each matched value. */
.searchhit__detail {
  margin-top: 2px;
  font-size: var(--font-size-xs); color: var(--text-200);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ── "Open in…" dropdown (event explorer rows) ──────────────────────────── */
.hp-open-in { position: relative; display: inline-flex; }
.hp-open-in > summary {
  display: inline-flex; align-items: center; gap: var(--space-xs);
  list-style: none; cursor: pointer;
}
/* Plain-link trigger styling only for summaries that do NOT opt into real
   button chrome -- the :not(.btn) scoping keeps this from out-specifying
   .btn/.btn-sm/.btn-secondary on the filter-bar's trigger (the bug behind
   "the Filter button is not in the normal button theme": these rules used
   to win on color/font-size and the border-bottom:none patch below cut the
   button's own bottom border). */
.hp-open-in > summary:not(.btn) {
  font-size: 11px; color: var(--accent);
  border-bottom: 1px dotted color-mix(in srgb, var(--accent) 45%, transparent);
  transition: color var(--transition-fast), border-color var(--transition-fast);
}
.hp-open-in > summary::-webkit-details-marker { display: none; }
.hp-open-in > summary::marker { content: ""; }
.hp-open-in > summary:not(.btn):hover, .hp-open-in[open] > summary:not(.btn) { color: var(--text-000); border-color: var(--accent); }
.hp-open-in > summary svg { width: 13px; height: 13px; }
/* #303: the filter-bar's trigger opts into real button chrome via .btn on
   the <summary> itself (dashboard.html); the :not(.btn) scoping above is
   what keeps the plain-link styling off it, so .btn/.btn-sm/.btn-secondary
   apply untouched here. */
/* Panel chrome (background/border/radius/shadow/padding) now comes from
   theme.css's own .dropdown, combined at every call site instead of
   duplicated here (Xore/theme is the reference) -- this is only the
   positioning and size .dropdown deliberately leaves to its consumer. */
.hp-open-in-menu {
  position: absolute; z-index: 70; top: calc(100% + 6px); left: 0;
  min-width: 240px;
  animation: hp-panel-in 150ms ease-out;
}
.hp-open-in-heading {
  padding: var(--space-sm) var(--space-md) var(--space-xs);
  font-size: var(--font-size-xs); font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--text-200);
}
.hp-open-in-item {
  display: flex; align-items: center; gap: var(--space-md);
  padding: var(--space-sm) var(--space-md); border-radius: var(--radius-control);
  color: var(--text-100); text-decoration: none;
  transition: background var(--transition-fast), color var(--transition-fast);
}
.hp-open-in-item:hover { background: var(--bg-300); color: var(--text-000); }
.hp-open-in-item > svg { width: 15px; height: 15px; flex-shrink: 0; }
.hp-open-in-item > svg:first-child { color: var(--accent); }
.hp-open-in-item > svg:last-child { margin-left: auto; opacity: 0.45; }
.hp-open-in-item > span { flex: 1; min-width: 0; line-height: 1.3; }
.hp-open-in-item strong { display: block; font-size: 12px; font-weight: 600; color: var(--text-000); }
.hp-open-in-item small { display: block; font-size: 11px; color: var(--text-200); }

/* ── Shared multi-attribute filter bar (#280 Phase 4) ─────────────────────
   {{template "filterbar" .}}, dashboard/ui/partials/dashboard.html.
   Reuses .hp-open-in's floating-panel shell above; these rules cover only
   the form-specific parts, shared across every page that includes it. */
.hp-filterbar-menu { min-width: 260px; padding: var(--space-md); }
/* #1567: a visible close affordance -- click-away already dismissed this
   panel (delegated document click handler, hp-app.js), but nothing in the
   panel itself said so. .hp-open-in-menu (above) is already the
   positioned ancestor, so this only needs its own corner offset. */
.hp-filterbar-menu__close {
  position: absolute; top: 6px; right: 6px;
  display: flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; padding: 0;
  border: 0; border-radius: var(--radius-control);
  background: transparent; color: var(--text-200);
  font-size: 11px; line-height: 1; cursor: pointer;
  transition: background var(--transition-fast), color var(--transition-fast);
}
.hp-filterbar-menu__close:hover { background: var(--bg-300); color: var(--text-000); }
.hp-filterbar-field { display: block; margin-bottom: var(--space-sm); }
.hp-filterbar-field span { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-200); margin-bottom: var(--space-xs); }
.hp-filterbar-field input, .hp-filterbar-field select { width: 100%; background: var(--bg-100); border: 1px solid var(--border-100); border-radius: var(--radius-control); color: var(--text-000); padding: var(--space-sm) var(--space-md); font-size: 12.5px; }
.hp-filterbar-field input:focus, .hp-filterbar-field select:focus { border-color: var(--border-focus); }
/* #1531 (APIARY): a "since" field pairs its existing duration text input
   (the actual submitted value) with a native datetime-local picker.
   .hp-filterbar-field's own `input { width: 100% }` rule above is exactly
   right for a single field but would stack two full-width inputs
   awkwardly here, so this narrows it back down to a normal flex row -- the
   picker sized to its own content, the text input filling the rest. */
.hp-since-field { display: flex; align-items: center; gap: var(--space-sm); }
.hp-since-field input[type="text"] { flex: 1 1 auto; width: auto; min-width: 0; }
.hp-since-field input[type="datetime-local"] { flex: none; width: auto; }
.hp-filterbar-actions { display: flex; gap: var(--space-sm); margin-top: var(--space-xs); }

/* ── Shared filter-value autocomplete (#303) ──────────────────────────────
   Backs any <input data-hp-filter-field="NAME"> on the page -- the shared
   filter-bar's autocomplete fields above, and reports.html's scope panel
   (a separate form, same mechanism). One floating box, repositioned and
   refilled by hp-app.js under whichever input is currently focused, rather
   than one per field. Same floating-panel shell language as
   .hp-open-in-menu above (surface-raised/border-strong/shadow-raised). */
/* Panel chrome comes from theme.css's own .dropdown (combined in hp-app.js
   where this box is created) instead of a duplicate copy here. */
.hp-filter-autocomplete {
  position: absolute; z-index: 90;
  max-height: 260px; overflow-y: auto;
}
.hp-filter-autocomplete__row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-md);
  width: 100%; padding: var(--space-sm) var(--space-sm); margin: 0;
  border: none; border-radius: var(--radius-control);
  background: none; font: inherit; text-align: left;
  color: var(--text-000); cursor: pointer;
}
.hp-filter-autocomplete__row:hover, .hp-filter-autocomplete__row.active { background: var(--bg-300); }
.hp-filter-autocomplete__row span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hp-filter-autocomplete__row small { color: var(--text-200); font-size: 11px; flex-shrink: 0; }
pre.code {
  white-space: pre-wrap; word-break: break-word;
  background: var(--bg-sidebar); border: 1px solid var(--border-100);
  border-radius: var(--radius-control);
  padding: var(--space-md); color: var(--text-100);
  font: 12px/1.5 var(--hp-mono);
  max-height: 520px; overflow: auto;
}
/* Add/remove line coloring for a diff/comparison view inside pre.code --
   a static per-line class the template already knows to emit (choosing
   between an Added-range and a Removed-range, not a computed value), so
   no CSP nonce pattern is needed. Reuses the same --success/--danger
   state-color tokens .badge--success/--danger already establish. */
.diff-line { display: block; padding: 0 var(--space-sm); border-radius: 2px; }
.diff-line--add { background: var(--success-soft); color: var(--success-text-on-soft); }
.diff-line--del { background: var(--danger-soft); color: var(--danger-text-on-soft); }

/* ── AI-generated report body + citations panel ───────────────────────────
   The typographic layer for a markdown-parsed AI report: a left accent
   rule + tinted surface reads as "generated report", not plain page copy,
   without needing a second, separate warning-box treatment. Consuming
   apps pair this with their own "AI-generated, unverified" banner above
   it; this only styles the body markdown itself renders. */
.hp-ai-report {
  margin: var(--space-sm) 0; padding: var(--space-lg) var(--space-lg);
  border-left: 3px solid var(--accent);
  background: var(--bg-300);
  border-radius: var(--radius-control);
}
.hp-ai-report__body { color: var(--text-000); font-size: var(--font-size-body); line-height: 1.6; }
.hp-ai-report__body :is(h1, h2, h3, h4, h5, h6) { color: var(--accent-text-on-soft); font-size: 1rem; font-weight: 600; margin: var(--space-lg) 0 var(--space-sm); }
.hp-ai-report__body :is(h1, h2, h3, h4, h5, h6):first-child { margin-top: 0; }
.hp-ai-report__body p { margin: 0 0 var(--space-md); }
.hp-ai-report__body p:last-child { margin-bottom: 0; }
.hp-ai-report__body :is(ul, ol) { margin: 0 0 var(--space-md); padding-left: var(--space-xl); }
.hp-ai-report__body code { font: 12px var(--hp-mono); background: var(--bg-sidebar); border-radius: 3px; padding: 1px var(--space-xs); }
.hp-ai-report__body pre { background: var(--bg-sidebar); border: 1px solid var(--border-100); border-radius: var(--radius-control); padding: var(--space-md); overflow: auto; }
.hp-ai-report__body pre code { background: none; padding: 0; }
.hp-ai-report__body blockquote { margin: 0 0 var(--space-md); padding-left: var(--space-md); border-left: 2px solid var(--border-100); color: var(--text-100); }
.hp-ai-report__body a { color: var(--text-link); }
.hp-ai-report__body strong { color: var(--text-000); font-weight: 700; }

/* Valid vs. invalid citations get distinct pill colors (success vs.
   warning), reusing the same state-color tokens .badge--success/--warning
   already establish. */
.hp-ai-citations { margin-top: var(--space-md); display: flex; flex-direction: column; gap: var(--space-md); }
.hp-ai-citations__label { font: 11px var(--hp-mono); text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 var(--space-xs); }
.hp-ai-citations__label--valid { color: var(--success-text-on-soft); }
.hp-ai-citations__label--invalid { color: var(--warning-text-on-soft); }
.hp-ai-citations__list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--space-sm); }
.hp-ai-citations__item code { font: 12px var(--hp-mono); padding: 2px var(--space-sm); border-radius: 999px; border: 1px solid var(--border-100); }
.hp-ai-citations__item--valid code { background: var(--success-soft); color: var(--success-text-on-soft); border-color: transparent; }
.hp-ai-citations__item--invalid code { background: var(--warning-soft); color: var(--warning-text-on-soft); border-color: transparent; opacity: 0.85; }

/* ── Chat transcript bubbles ────────────────────────────────────────────
   Bubbles by role: user right-aligned/accent-tinted, assistant left-
   aligned/neutral, tool left-aligned/info-tinted and a step smaller
   (it's machine output, not conversation). */
.hp-chat { display: flex; flex-direction: column; gap: var(--space-md); margin-top: var(--space-md); }
.hp-chat-msg { max-width: 88%; padding: var(--space-md) var(--space-lg); border-radius: var(--radius-control); }
.hp-chat-msg--user { align-self: flex-end; background: var(--accent-soft); }
.hp-chat-msg--assistant { align-self: flex-start; background: var(--bg-300); border: 1px solid var(--border-100); }
.hp-chat-msg--tool { align-self: flex-start; background: var(--info-soft); font-size: var(--font-size-caption); }
.hp-chat-msg__meta { display: flex; gap: var(--space-sm); align-items: baseline; margin-bottom: var(--space-xs); }
.hp-chat-msg__role { font: 10px var(--hp-mono); text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-200); }
.hp-chat-msg--user .hp-chat-msg__role { color: var(--accent-text-on-soft); }
.hp-chat-msg--tool .hp-chat-msg__role { color: var(--info-text-on-soft); }
.hp-chat-msg__name { font-size: var(--font-size-caption); color: var(--text-200); }
.hp-chat-msg__content { color: var(--text-000); font-size: var(--font-size-body); line-height: 1.55; }
.hp-chat-msg__content p { margin: 0 0 var(--space-sm); }
.hp-chat-msg__content p:last-child { margin-bottom: 0; }
.hp-chat-msg__content code { font: 12px var(--hp-mono); background: var(--bg-sidebar); border-radius: 3px; padding: 1px var(--space-xs); }
.hp-chat-msg__content pre { background: var(--bg-sidebar); border: 1px solid var(--border-100); border-radius: var(--radius-control); padding: var(--space-sm); overflow: auto; margin: var(--space-sm) 0; }
.hp-chat-msg__content pre code { background: none; padding: 0; }
.hp-chat-msg__toolcalls { margin-top: var(--space-sm); }
.hp-chat-msg__toolcalls summary { cursor: pointer; font-size: var(--font-size-caption); color: var(--text-200); }
.hp-chat-msg__toolcalls pre { margin-top: var(--space-sm); }

footer {
  margin-top: var(--space-xl); padding-top: var(--space-sm);
  font: 10px var(--hp-mono); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--text-200); text-align: center;
}

/* ── Activity-by-sensor heatmap (#191/#193, replacing the single-bar
   24h activity chart) ─────────────────────────────────────────────────
   .heatmap/.heatmap__* chrome is Xore/theme's shared component; this is
   just the gap between the grid and its low->high legend underneath. */
.heatmap__legend { margin-top: var(--space-sm); }

/* ── Card-grid inventories (#213 phase 4) ────────────────────────────────
   .project-card/.project-grid chrome is Xore/theme's shared component; it
   ships as a <div> with cursor:pointer left for a click handler. This page
   wraps each card in a real <a> instead (keyboard nav, open-in-new-tab,
   no JS needed) -- an anchor is inline by default, so it needs its own
   display/color/underline reset to lay out and read exactly like the div
   the component was designed as. */
a.project-card { display: block; color: inherit; text-decoration: none; }

/* Make a whole card open its record, on a card that also has controls.

   Where a card has no interactive children it can simply be an <a>, and
   several already are. A card that carries its own actions -- an overflow
   menu, badge links, a copy button -- cannot: HTML forbids interactive
   content inside an anchor, and nesting it produces a card whose menu
   cannot be opened without also navigating away.

   So the link is a transparent overlay covering the card, and the real
   controls are lifted above it. The result is that clicking the card
   opens its record, clicking a control does what the control says, and
   the markup stays valid.

   The overlay is empty, so it needs its accessible name from an
   aria-label; without one the card is an unlabelled link to a screen
   reader. */
.project-card:has(.hp-card-link) { position: relative; }
.hp-card-link {
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
}
.hp-card-link:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: 2px;
}
/* Everything the reader can act on sits above the overlay. `position:
   relative` is what puts an element on the z-axis at all -- z-index alone
   does nothing to a statically positioned box. */
.project-card:has(.hp-card-link) > *,
.project-card:has(.hp-card-link) .project-card__title,
.project-card:has(.hp-card-link) .project-card__badges a,
.project-card:has(.hp-card-link) .action-menu {
  position: relative;
  z-index: 1;
}
.project-card:has(.hp-card-link) > .hp-card-link { z-index: 0; }
/* A SHA-256 is far longer than any title .project-card was designed to
   hold; truncate with an ellipsis rather than wrapping a hash across
   several lines. */
#ghidra-results .project-card__title,
#sandbox-results .project-card__title,
#wb-select-results .project-card__title,
#github-analysis-results .project-card__title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--font-size-caption); }
/* A payload's SHA-256 title is a real <a> here (not the whole card --
   payload cards have several distinct actions, so they cannot be one big
   link; see the "payloadrow" template comment), but is otherwise the same
   long-hash-in-a-card problem as Ghidra's. The "..." action menu is a
   sibling in the meta row, not part of the title, and stays pinned to the
   row's trailing edge regardless of how long the origin-event text next to
   it runs. Same reasoning as #hp-rp-generated below: the consuming app
   delegates a click anywhere on the card outside the action menu to the
   title link (document-level, not a per-card listener), so the card reads
   and behaves like a single destination even though it isn't literally
   one <a> -- cursor: pointer, the same affordance a real <a>/<button>
   would carry for free, not the "default" arrow a plain non-interactive
   <div> gets. */
#payloads-results .project-card { cursor: pointer; }
#payloads-results .project-card__title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--font-size-caption);
  color: inherit;
  text-decoration: none;
  border-bottom: 1px dotted var(--border-200);
}
#payloads-results .project-card__title:hover { color: var(--accent); border-color: var(--accent); }
#payloads-results .project-card__meta { flex-wrap: wrap; }
#payloads-results .project-card__meta .action-menu { margin-left: auto; }
/* A workbench run's payload hash lives in the meta row, sharing it with the
   created timestamp -- give the hash the shrink-and-ellipsize treatment and
   let the timestamp keep its natural width, so a long hash never pushes the
   timestamp out of the card. */
#workbench-results-list .project-card__meta span.mono { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A generated report's title is an operator-chosen name, not bounded the way
   a hash is -- same overflow risk, same fix. The card is role="button" (see
   hp-reports.js -- it can't be a real <a>, the Download link and Delete
   button inside it are both interactive content), so it also needs the
   pointer cursor a real <a>/<button> would carry for free. */
#hp-rp-generated .project-card { cursor: pointer; }
#hp-rp-generated .project-card__title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#hp-rp-generated .project-card__meta { flex-wrap: wrap; }
#hp-rp-generated .project-card__meta .hp-rp-row-actions { margin-left: auto; }
/* #1567: Delete sat directly beside Download (6px flex gap, same as any
   other two buttons in a row) with no confirm-independent margin for a
   slip -- a real destructive-adjacency risk even though the click itself
   already opens a confirm dialog (hp-reports.js), since the *miss-click*
   is what a wider gap actually guards against, not the confirm step
   after it. Scoped to this specific two-button (Download, Delete) row --
   the report-definitions table row above it (Edit/Generate/Delete) has a
   secondary action between the primary one and Delete already. */
#hp-rp-generated .hp-rp-row-actions .btn-danger { margin-left: var(--space-lg); }

/* ── Leaflet attack map (dark-tile filter scoped to dark mode only) ─────── */
.map-shell {
  position: relative; overflow: hidden;
  border: 1px solid var(--border-100); border-radius: var(--radius-panel);
  background: var(--bg-100);
}
.leaflet-map { display: block; width: 100%; height: clamp(360px, 42vw, 520px); background: var(--bg-000); z-index: 1; }
.map-status {
  position: absolute; z-index: 500; left: 10px; bottom: 28px;
  padding: var(--space-xs) var(--space-sm);
  border: 1px solid var(--border-100); border-radius: 8px;
  background: color-mix(in srgb, var(--bg-100) 88%, transparent);
  color: var(--text-100); font: 10px var(--hp-mono);
  pointer-events: none;
}
.map-fallback[hidden], .leaflet-map[hidden] { display: none; }
.map-fallback { padding-top: var(--space-2xl); }
.map-fallback-note {
  position: absolute; z-index: 2; top: 8px; left: 10px;
  color: var(--danger); font: 11px var(--hp-mono);
}
.leaflet-container { font-family: var(--hp-font); color: var(--text-000); background: var(--bg-000); }
/* Raster basemap tiles ship bright; a dark ground needs them filtered. The
   theme declares that intent (--basemap-filter) rather than this rule
   matching on [data-theme="dark"], which silently did nothing for any theme
   whose attribute value was not literally "dark". */
.leaflet-tile-pane { filter: var(--basemap-filter); }
.leaflet-control-zoom a, .leaflet-control-home {
  background: var(--bg-200) !important;
  color: var(--text-000) !important;
  border-color: var(--border-200) !important;
}
.leaflet-control-home {
  /* Sized to its label ("World") -- a fixed 32px clipped it to "Wor". */
  min-width: 32px; width: auto; height: 32px; padding: 0 var(--space-sm); text-align: center;
  font: 700 11px/30px var(--hp-mono); cursor: pointer;
}
.leaflet-tooltip.attack-tooltip {
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--border-200); border-radius: var(--radius-control);
  background: var(--bg-raised); color: var(--text-000);
  font: 11px/1.45 var(--hp-mono);
  box-shadow: var(--shadow-raised);
}
.leaflet-tooltip.attack-tooltip::before { display: none; }
.leaflet-interactive { transition: fill-opacity 0.12s, stroke-opacity 0.12s; }
.leaflet-interactive:hover { fill-opacity: 0.88 !important; stroke-opacity: 1 !important; }

/* ── Offline SVG world map fallback ─────────────────────────────────────── */
.world { display: block; width: 100%; height: auto; min-height: 260px; touch-action: none; cursor: grab; user-select: none; }
.world.dragging { cursor: grabbing; }
.world .ocean { fill: transparent; }
.world .graticule { stroke: var(--border-100); stroke-width: 1; stroke-dasharray: 3 5; }
.world .land { fill: var(--bg-300); stroke: var(--border-200); stroke-width: 1.2; vector-effect: non-scaling-stroke; }
.world .country:hover { fill: var(--accent-soft); }
.world .country-label {
  fill: var(--text-100);
  font: 600 9px var(--hp-mono); letter-spacing: 0.02em; text-anchor: middle;
  paint-order: stroke; stroke: var(--bg-200); stroke-width: 2.5px; stroke-linejoin: round;
  pointer-events: none;
}
.world .country-label.rank-3, .world .country-label.rank-4,
.world .country-label.rank-5, .world .country-label.rank-6 { display: none; }
.world[data-zoom-level="2"] .country-label.rank-3,
.world[data-zoom-level="3"] .country-label.rank-3,
.world[data-zoom-level="4"] .country-label.rank-3 { display: block; }
.world[data-zoom-level="3"] .country-label.rank-4,
.world[data-zoom-level="4"] .country-label.rank-4 { display: block; }
.world[data-zoom-level="4"] .country-label.rank-5,
.world[data-zoom-level="4"] .country-label.rank-6 { display: block; }
.world circle {
  fill: var(--danger); fill-opacity: 0.62;
  stroke: var(--danger); stroke-width: 1.2;
  filter: drop-shadow(0 0 4px color-mix(in srgb, var(--danger) 55%, transparent));
  transition: r 0.12s, fill-opacity 0.12s;
}
.world circle:hover { fill-opacity: 1; r: 8px; }

/* ── Lazy loading controls (hp-app.js) ──────────────────────────────────── */
.hp-lazy-controls {
  display: flex; align-items: center; justify-content: center; gap: var(--space-md);
  padding: var(--space-lg) 0 var(--space-xs);
}
.hp-lazy-controls > span:first-child { font-size: 12px; color: var(--text-200); }
.hp-lazy-sentinel { width: 1px; height: 1px; }

/* ── Live toast (hp-app.js) ──────────────────────────────────────────────
   theme.css's .toast owns the box (padding, border, radius, background,
   shadow, color, size) -- this used to be a parallel .hp-toast
   reimplementation of the exact same chrome (#191/#193). What's left: the
   fixed stacking container (theme has no concept of "several toasts queued
   in the corner"), and the left accent stripe marking this specific toast
   as a live-update notification, which theme's plain .toast has no color
   variant for. */
.hp-toast-stack {
  position: fixed; top: 44px; right: 16px; z-index: 2000;
  display: flex; flex-direction: column; gap: var(--space-sm);
}
.hp-toast { border-left: 3px solid var(--success); text-decoration: none; animation: hp-panel-in 150ms ease-out; }

/* ── Investigation command palette ───────────────────────────────────────
   theme.css's .modal--palette / .command-palette__field position and frame
   the dialog (#193, closing #183 -- was a permanently-docked .command-bar,
   now the same top-anchored search-everything modal the reference
   screenshots show). This is just the one dashboard-specific adaptation:
   a resizing <textarea> in place of the palette's usual single-line
   <input>, because pasting a multi-line indicator (a cert, a log
   fragment) is a real, existing feature, not a redesign choice. */
.command-palette__field textarea {
  display: block; width: 100%; resize: none;
  border: none; background: transparent;
  color: var(--text-000); font: inherit;
  max-height: 120px; padding: 0;
}
.command-palette__field textarea::placeholder { color: var(--text-200); }

/* Box chrome (padding/border/radius/background/shadow/color/font-size) is
   theme.css's own .toast, combined on the element in dashboard.html
   (class="toast hp-modal-status") instead of duplicated here -- this is
   only the fixed-position placement and fade/slide transition .toast
   deliberately leaves to its consumer (it's normally placed by a stack
   container, which this single persistent status element isn't). */
.hp-modal-status {
  position: fixed;
  z-index: 350;
  top: 44px;
  right: 16px;
  max-width: min(360px, calc(100vw - 32px));
  /* Cancels .toast's own one-shot entrance animation -- this element is a
     single persistent status box toggled via .open repeatedly (not a toast
     freshly inserted per message), driven by the plain transition below. */
  animation: none;
  opacity: 0;
  pointer-events: none;
  transform: translateY(-6px);
  transition: opacity var(--transition), transform var(--transition);
}
.hp-modal-status.open { opacity: 1; transform: none; }
.hp-modal-status[data-state="error"] { border-color: var(--danger); }

@media (prefers-reduced-motion: reduce) {
  .hp-modal-status { transition: none; }
}

/* ── Account menu + settings popup (hp-account.js) ──────────────────────── */
.hp-account { position: relative; }
button.sidebar__profile {
  width: 100%;
  border: 0;
  border-top: 1px solid var(--border-100);
  border-radius: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background var(--transition-fast);
}
button.sidebar__profile:hover { background: var(--bg-300); }
button.sidebar__profile:disabled { cursor: default; }
button.sidebar__profile:disabled:hover { background: transparent; }
.hp-account-menu {
  position: absolute;
  z-index: 80;
  left: 8px;
  right: 8px;
  bottom: calc(100% + 6px);
  min-width: 0;
  animation: hp-panel-in 150ms ease-out;
}
.hp-account-menu .dropdown__item svg { width: 14px; height: 14px; opacity: 0.72; flex: none; }
.hp-account-note { color: var(--text-200); cursor: default; }
.hp-account-note:hover { background: transparent; }

/* Shares its clamped size with .hp-dash-settings (below, this dashboard's
   own settings modal) via one rule instead of two independently
   hand-maintained copies of the same numbers -- that duplication once let
   them drift (#393): this modal fell back to the base .modal's smaller
   960x680, so the auth-backend account-settings modal (loaded edge-to-edge
   into .hp-settings-frame below) rendered visibly smaller than this
   dashboard's own settings, despite both being reached from the same
   account menu. */
.hp-dash-settings, .hp-settings-modal {
  width: min(1120px, calc(100vw - 40px));
  height: min(820px, calc(100dvh - 40px));
}
.hp-settings-modal { flex-direction: column; }
/* Renamed from the original .hp-settings-head: that bare class name
   collided with settings_modal.html's OWN, unrelated .hp-settings-head
   (the internal settings page's h1+description header, below) -- both
   loaded on the same page with no ancestor scoping, so whichever file's
   CSS happened to load last silently won for both, a latent bug this
   migration's duplicate-selector check surfaced. */
.hp-account-settings-head {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  flex: none;
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--border-100);
}
.hp-account-settings-head h2 { flex: 1; margin: 0; font-size: 14px; }
.hp-settings-external { font-size: 12px; text-decoration: none; }
.hp-settings-external:hover { text-decoration: underline; }
.hp-settings-modal .modal__close { position: static; flex: none; }
.hp-settings-frame {
  flex: 1;
  width: 100%;
  min-height: 0;
  border: 0;
  background: var(--bg-000);
}

/* Payload workbench (#155). The grid collapses naturally for narrow screens;
   every interactive control remains a native form element for keyboard use. */
.wb-analyzer-grid,
.wb-child-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-md); }
.wb-analyzer,
.wb-child { min-width: 0; padding: var(--space-lg); border: 1px solid var(--border-100); border-radius: 10px; background: var(--bg-200); }
.wb-analyzer.is-disabled { opacity: .7; }
.wb-analyzer-head,
.wb-child-head,
.wb-run-head { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-sm); }
.wb-analyzer-head label,
.wb-child-head strong,
.wb-run-head > :first-child { flex: 1; min-width: 0; }
.wb-options { margin-top: var(--space-md); }
.wb-options summary { cursor: pointer; color: var(--text-100); font-size: 12px; }
.wb-option-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-md); margin-top: var(--space-md); }
.wb-option-grid label { color: var(--text-200); font-size: 11px; }
.wb-option-grid .form-input { width: 100%; margin-top: var(--space-xs); }
.wb-run { margin-bottom: var(--space-lg); }
.wb-run-head { justify-content: space-between; margin-bottom: var(--space-md); }
.wb-run-head h3 { margin: 0; }
.wb-state--completed { color: var(--green); }
.wb-state--failed,
.wb-state--timed_out { color: var(--red); }
.wb-state--running,
.wb-state--claimed,
.wb-state--queued { color: var(--orange); }
@media (max-width: 820px) {
  .wb-analyzer-grid,
  .wb-child-grid,
  .wb-option-grid { grid-template-columns: 1fr; }
}


/* ── Design refresh components (APIARY design lab, Xore's picks) ────────
   Everything below implements the element/layout selections made in the
   design lab: sidebar view-tabs + investigate action + recents (12B/7D),
   KPI delta+sparkline (3B), master-detail events (EV-B), overview hero
   (OV-B), reports wizard (RP-C), hover row actions (14B), icon empty
   states (8B), content-shaped skeletons (2D), scroll "more" pill (9C),
   and the full settings page (13B). */

/* View tabs nest in the sidebar directly under the ACTIVE nav item
   (pick 7D, refined per Xore): hp-app.js inserts each page's main
   [role=tablist] right after the highlighted .sidebar__item, indented
   like a tree branch. In-page fallback (JS off) keeps theme's stock
   horizontal tabs. */
.app-sidebar .tabs {
  display: flex; flex-direction: column; align-items: stretch;
  gap: 1px; margin: 2px var(--space-sm) var(--space-sm) var(--space-xl); padding: 0;
  background: transparent; border: 0;
  border-left: 1px solid var(--border-100);
}
.app-sidebar .tabs .tab {
  justify-content: flex-start; text-align: left;
  padding: var(--space-sm) var(--space-md); font-size: 13px;
  border: 0; border-left: 2px solid transparent;
  border-radius: 0 var(--radius-xs) var(--radius-xs) 0;
  background: transparent; color: var(--text-100);
}
.app-sidebar .tabs .tab:hover { background: var(--bg-300); }
.app-sidebar .tabs .tab.active {
  border-left-color: var(--accent);
  background: linear-gradient(90deg, var(--accent-soft), transparent 75%);
  color: var(--text-000);
}
.app-sidebar .tabs .tab span { width: 18px; height: 18px; }

/* Attack sources (pick AS-D): profile card grid -- claude's projects
   page applied to attackers. Cards keep every column the table showed. */
.hp-src-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: var(--space-md); }
.hp-src-card { display: flex; flex-direction: column; gap: var(--space-sm); padding: var(--space-md) var(--space-lg); border-radius: 14px; background: var(--bg-300); }
.hp-src-card__head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-sm); }
.hp-src-card__ip { font-family: var(--hp-mono); font-size: 13.5px; font-weight: 600; color: var(--text-000); text-decoration: none; }
.hp-src-card__ip:hover { color: var(--accent); }
.hp-src-card__stats { display: flex; }
.hp-src-card__stats a { flex: 1; min-width: 0; padding: 0 var(--space-md); border-left: 1px solid var(--border-100); color: var(--text-000); text-decoration: none; }
.hp-src-card__stats a:first-child { padding-left: 0; border-left: 0; }
.hp-src-card__stats b { display: block; font-family: var(--font-serif); font-size: 17px; font-weight: 500; }
.hp-src-card__stats span { color: var(--text-200); font-size: 10.5px; }
.hp-src-card__sensors { color: var(--text-100); font-size: 11.5px; text-decoration: none; overflow-wrap: anywhere; }
.hp-src-card__sensors:hover { color: var(--accent); }
.hp-src-card__when { color: var(--text-200); font-size: 10.5px; font-family: var(--hp-mono); }
/* #1566: each timestamp itself never wraps mid-value; the "first -> last"
   line as a whole can still wrap between the two if the card gets narrow
   (flex-wrap on the card body handles that), so this is scoped to the
   [data-hp-utc] spans, not the whole line. */
.hp-src-card__when [data-hp-utc] { white-space: nowrap; }

/* Event explorer (pick EV-D): the table reads as a time-grouped feed --
   quiet minute-break rows between event groups. */
.hp-feed-break td {
  padding: var(--space-md) var(--space-md) var(--space-xs);
  border-bottom: 0;
  color: var(--text-200);
  font-family: var(--hp-mono); font-size: 10.5px; letter-spacing: .05em;
  text-align: center;
}
.hp-feed-break:hover td { background: transparent !important; }

/* Skeleton-first (hard rule per Xore): placeholders for every ES-backed
   surface. .hp-skel-batch rows/cards appear the moment "View more" is
   pressed and are swapped for the fetched entries; KPI tiles show a
   skeleton value while the store warms instead of a placeholder zero. */
.hp-skel-batch td .skeleton-line { display: block; width: 62%; }
tr.hp-skel-batch:nth-child(2n) td .skeleton-line { width: 78%; }
tr.hp-skel-batch:nth-child(3n) td .skeleton-line { width: 47%; }
div.hp-skel-batch {
  display: flex; flex-direction: column; gap: var(--space-sm);
  padding: var(--space-md) var(--space-lg); border-radius: 14px;
  background: var(--bg-300);
}
div.hp-skel-batch .skeleton-line:first-child { width: 55%; }
div.hp-skel-batch .skeleton-line:nth-child(2) { width: 80%; }
div.hp-skel-batch .skeleton-line:nth-child(3) { width: 38%; }
.metric__value .skeleton-line { display: inline-block; width: 74px; height: 18px; vertical-align: middle; }


/* Settings palette picker: the shared .segmented contract plus a swatch
   dot per option; the row wraps instead of forcing the pane wide.

   Kept for consumers still rendering the old picker. The nine hardcoded dot
   colours it used to carry are gone: every one was that theme's *dark* accent,
   so in light mode the swatches showed colours that appeared nowhere in the
   interface being previewed. The dot now takes var(--accent), which is
   correct in both modes for the theme currently applied -- and the gallery
   below is what a theme should actually be previewed with. */
.hp-palette-pick { flex-wrap: wrap; }
.hp-palette-pick button { display: inline-flex; align-items: center; gap: var(--space-sm); }
.hp-palette-pick .dot { width: 11px; height: 11px; border-radius: 50%; flex: none; border: 1px solid var(--border-200); background: var(--accent); }

/* Theme gallery.

   A theme owns the ground, the sidebar, the surface ramp, the borders and the
   text ramp, so an 11px dot previews the one part that matters least. Each
   tile is a miniature of the shell instead.

   The tile carries data-hp-theme and data-theme itself, so everything inside
   it resolves from that theme's real tokens rather than from a copy kept in
   this file. That works because every theme block here is an attribute
   selector rather than a :root rule, and color-scheme is set the same way --
   which is also why there is nothing per-theme to maintain below, and why a
   tile cannot go stale the way the dot colours did. */
.hp-theme-gallery { display: flex; flex-wrap: wrap; gap: var(--space-md); }
.hp-theme-tile {
  display: flex; flex-direction: column; gap: var(--space-sm); width: 148px; padding: var(--space-sm);
  border: 1px solid var(--border-100); border-radius: var(--radius-control);
  background: var(--bg-100); text-align: left; cursor: pointer;
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.hp-theme-tile:hover { border-color: var(--border-200); }
.hp-theme-tile:focus-visible { outline: 2px solid var(--border-focus); outline-offset: 2px; }
.hp-theme-tile[aria-checked="true"] { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.hp-theme-tile__art {
  display: flex; height: 62px; overflow: hidden; border-radius: var(--radius-sm);
  border: 1px solid var(--border-100); background: var(--bg-000);
}
.hp-theme-tile__sidebar { width: 26px; flex: none; background: var(--bg-sidebar); border-right: 1px solid var(--border-100); }
.hp-theme-tile__body { display: flex; flex-direction: column; gap: var(--space-xs); flex: 1; padding: var(--space-sm); background: var(--bg-200); }
.hp-theme-tile__line { height: 5px; width: 60%; border-radius: 3px; background: var(--text-200); }
.hp-theme-tile__line--wide { width: 85%; background: var(--text-000); }
.hp-theme-tile__accent { height: 9px; width: 38%; margin-top: auto; border-radius: 3px; background: var(--accent); }
.hp-theme-tile__label { font-size: var(--font-size-footnote); font-weight: 600; color: var(--text-000); }
.hp-theme-tile__desc { font-size: var(--font-size-xs); line-height: 1.3; color: var(--text-200); }

/* Sidebar search affordance (claude.ai top-row pattern): a quiet icon
   button floated into the brand row, opening the same palette. */
.hp-sidebar-search {
  position: absolute;
  top: 14px;
  right: 10px;
  display: inline-grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border: 0;
  border-radius: var(--radius-xs);
  background: transparent;
  color: var(--text-100);
  cursor: pointer;
}
.hp-sidebar-search:hover { background: var(--bg-300); color: var(--text-000); }
.app-sidebar { position: relative; }

/* Sidebar primary action + recents (pick 12B, claude rail). */
.sidebar__investigate {
  display: flex; align-items: center; justify-content: center; gap: var(--space-sm);
  margin: var(--space-xs) var(--space-sm) var(--space-md);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--accent);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--accent);
  font: 600 13px var(--hp-font);
  cursor: pointer;
  transition: background var(--transition-fast);
}
.sidebar__investigate:hover { background: var(--accent-soft); }
.sidebar__recent { display: flex; flex-direction: column; gap: 1px; padding-right: var(--space-sm); }
.sidebar__recent a {
  display: block; padding: var(--space-xs) var(--space-md);
  border-radius: var(--radius-xs);
  color: var(--text-100);
  font: 12px var(--hp-mono);
  text-decoration: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sidebar__recent a:hover { background: var(--bg-300); color: var(--text-000); }

/* KPI tiles: delta chip + sparkline (pick 3B). */
.metric__delta {
  display: inline-block; margin-left: var(--space-sm); vertical-align: 3px;
  border-radius: var(--radius-pill); padding: 1px var(--space-sm);
  font: 600 11px var(--hp-font);
  background: var(--success-soft); color: var(--success-text-on-soft);
}
.metric__delta--down { background: var(--danger-soft); color: var(--danger-text-on-soft); }
.metric__spark { display: flex; align-items: flex-end; gap: 2px; height: 26px; margin-top: var(--space-sm); }
.metric__spark i {
  --v: 0;
  flex: 1; min-height: 2px;
  /* The bar's height is the datum, so it arrives as a custom property
     rather than as an inline height -- the same shape .mini-chart span and
     .progress > span already use, and the same reason: an inline style is
     invisible to the checks this file guarantees, and a strict CSP blocks
     the alternatives. Consumers set --v to a unitless percentage.

     Defaulting to 0 means a bar with no datum is flat rather than
     full-height, which is the honest rendering of "no events this hour". */
  height: calc(var(--v) * 1%);
  border-radius: 2px 2px 0 0;
  background: var(--accent-soft);
}
.metric__spark i:last-child { background: var(--accent); }

/* Overview hero (pick OV-B): serif greeting, status subline, command bar. */
.hp-hero { grid-column: 1 / -1; text-align: center; padding: var(--space-xl) 0 var(--space-sm); }
.hp-hero h1 {
  margin: 0;
  font: 500 clamp(1.6rem, 2.6vw, 2.2rem)/1.2 var(--font-serif);
  letter-spacing: -0.01em;
  color: var(--text-000);
}
.hp-hero .hp-hero__status { margin: var(--space-sm) 0 0; font-size: 13.5px; color: var(--text-100); }
.hp-hero__search {
  display: flex; align-items: center; gap: var(--space-md);
  max-width: 760px; margin: var(--space-xl) auto var(--space-sm);
  padding: var(--space-md) var(--space-lg);
  border: 1px solid var(--border-200);
  border-radius: var(--radius-panel);
  background: var(--bg-200);
  color: var(--text-200);
  font-size: 14px;
  cursor: pointer;
  text-align: left;
  transition: border-color var(--transition-fast);
  width: 100%;
}
.hp-hero__search:hover { border-color: var(--accent); }
.hp-hero__search kbd {
  margin-left: auto;
  border: 1px solid var(--border-100); border-radius: 5px;
  padding: 1px var(--space-sm); font: 11px var(--hp-mono); color: var(--text-200);
  background: var(--bg-300);
}
.hp-hero__links { margin: var(--space-md) 0 var(--space-xs); font-size: 13px; color: var(--text-200); }
.hp-hero__links a { color: var(--text-link); text-decoration: none; }
.hp-hero__links a:hover { text-decoration: underline; }

/* Master-detail investigate layout (pick EV-B, extended per Xore): the
   table gets the whole width by default; selecting a row opens the sticky
   detail pane on the right (.hp-md--open, added by hp-app.js), and the
   pane's close control returns the full-width table. Falls back to
   stacked below 1100px. */
.hp-md { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-lg); align-items: start; }
.hp-md--open { grid-template-columns: minmax(0, 11fr) minmax(340px, 9fr); }
.hp-md:not(.hp-md--open) > .hp-md__pane { display: none; }
/* The list card carries .wide for the no-JS/stacked fallback; inside the
   two-column grid that span would push the pane below -- neutralize it. */
.hp-md > .card.wide, .hp-md .hp-md__list { grid-column: auto; }
.hp-md__list { min-width: 0; }
/* Wide investigate tables keep their 720px floor (readable columns); when
   the pane narrows the list, the card scrolls sideways instead of
   spilling under the pane.
   #1566: this used to be scoped to .hp-md--open only, so a table whose
   still-visible columns (campaigns' score/network/events/ips/sensors/
   first/last/action -- the nine heavy ones are already data-hp-md-detail,
   pane-only) didn't quite fit at the card's own width had its trailing
   column ("ES ->") silently clipped by .card's base overflow:hidden until
   a row happened to be selected. Always-on scroll means the column is
   reachable (an inline scrollbar, .card__scroll's own convention) whether
   or not the pane is open. */
.hp-md__list > .card { overflow-x: auto; }
.hp-md__pane { position: sticky; top: 14px; min-width: 0; }
.hp-md__pane .card { margin-bottom: 0; }
.hp-md__list tbody tr { cursor: pointer; }
.hp-md__list tbody tr.selected td { background: var(--accent-soft); }
.hp-md__list tbody tr.selected td:first-child { box-shadow: inset 2px 0 0 var(--accent); }
/* Master-detail row-side hiding: only when the controller is active
   (.hp-md--active added by hp-app.js) -- without JS the records stay
   inline in the rows. The overview's live stream opts into the same
   compact rows permanently via .hp-stream-compact (#1565's JSON wall). */
.hp-md--active .hp-md__list td.v article[data-hp-event-detail],
.hp-md--active .hp-md__list td.v .eventmeta,
.hp-stream-compact td.v article[data-hp-event-detail],
.hp-stream-compact td.v .eventmeta { display: none; }
.hp-md__pane [data-hp-md-slot] article { margin: 0 0 var(--space-md); }
.hp-md__pane [data-hp-md-slot] article.card.wide { grid-column: auto; }
.hp-row-actions-cell { width: 1%; }

/* The shapes that had no class, and were being written as inline styles in
   the components instead (Xore/APIARY#1893).

   An inline style is invisible to everything this file guarantees: the
   nine themes are generated and contrast-checked here, and
   check-space-scale.mjs enforces the spacing scale here. A literal value
   in a component looks right in whichever theme it was written under, and
   no check can see it.

   Kept small by checking first. Most of what the components were reaching
   for already existed: .metric-grid is the responsive auto-fit grid,
   .progress is the proportional bar with its fill driven by --v,
   .hp-data-card-list is the vertical stack, .metric__spark is the
   sparkline, .wide is grid-column: 1 / -1, .card carries its own bottom
   margin, .form-input is already width: 100%, and .form-label is already
   display: block -- so forty-odd of those inline styles were restating
   what the class already said and simply come out. */

/* A horizontal run of controls: a label beside its input, a pair of
   buttons, a value with its action.

   Written inline in seven places before this, always as the same three
   declarations with the gap picked by eye. The gap is the scale's, so rows
   in different components line up with each other instead of each being
   nearly right. */
.hp-row { display: flex; align-items: center; gap: var(--space-sm); }
/* The same row, ranged right -- a dialog's confirm/cancel pair. */
.hp-row--end { justify-content: flex-end; }
/* The member of a row that should absorb the leftover width: the value in
   a label/value pair, the input in a search row. */
.hp-grow { flex: 1; }

/* A block that follows another block, at the scale's rhythm.

   Named for the relationship rather than the value, so it stays correct
   when the scale is redefined -- which is the point of the scale, and what
   the density control in Xore/APIARY#1759 depends on. Replaces a marginTop
   on a <details>, a <table>, a <pre> or a <p class="note">, none of which
   carry a top margin of their own. */
.hp-flow { margin-block-start: var(--space-md); }
.hp-flow--tight { margin-block-start: var(--space-sm); }
.hp-flow--loose { margin-block-start: var(--space-xl); }

/* A heading that ranges its row-mates to the far edge -- a card title with
   its actions on the right. The file already does this per component
   (.dropdown__shortcut, .project-card__meta .action-menu); this is the
   same idea where no component owns it. */
.hp-push-end { margin-inline-end: auto; }

/* A label stacked over its control inside a wrapping row.
   The minimum is what stops a field collapsing to its content when the row
   wraps; .form-label and .form-input already supply the rest. */
.hp-field { display: block; min-width: 9rem; }
/* For a control whose value is long -- a URL, a command -- where the field
   should be allowed to grow but not to dominate the row. */
.hp-field--wide { min-width: 15rem; max-width: 30rem; }

/* Text that has its own line breaks and must keep them, without the
   monospace treatment .code gives a captured artifact. */
.hp-pre-wrap { white-space: pre-wrap; }

/* A captured screenshot or rendered artifact.

   The light plate is not a theme colour and must not become one: these are
   images that assume a white page behind them, so the plate belongs to the
   image rather than to the surface it sits on, and stays white in every
   theme on purpose. */
/* Two equal columns that become one when there is no room for two. The
   same shape .wb-child-grid has carried for the workbench, named for anyone.

   The collapse is the point: added for APIARY#1893, where this grid existed
   inline without it and so stayed two columns on a phone. `minmax(0, 1fr)`
   rather than `1fr` so a long unbroken value cannot push a column wider than
   its share.

   Its own media query rather than joining the one further up: this rule sits
   later in the file, and a same-specificity base rule after an override wins
   over it. */
/* A button pinned to the bottom-right of the viewport, out of the page's
   flow -- the "report a problem" affordance and anything like it. Sits below
   menus (70) and modals (101) deliberately: it must never cover the thing it
   opened. Offsets come off the scale rather than the 1rem that was written
   inline for it.

   Added for APIARY#1893. */
.hp-fab {
  position: fixed;
  right: var(--space-lg); bottom: var(--space-lg);
  z-index: 40;
}

.hp-duo { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-md); }
@media (max-width: 820px) {
  .hp-duo { grid-template-columns: 1fr; }
}

.hp-figure { display: block; max-width: 100%; border-radius: var(--radius-control); background: #fff; }

/* Pane close control (the pane is sticky -- a positioned box -- so the
   button anchors to it) and the generic row inspector: pages without a
   dedicated per-row detail render the selected row as labelled fields. */
.hp-md__close {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px;
  border: 1px solid var(--border-100); border-radius: var(--radius-pill);
  background: var(--bg-300); color: var(--text-100);
  font-size: 13px; line-height: 1; cursor: pointer;
}
.hp-md__close:hover { color: var(--text-000); border-color: var(--border-200); background: var(--bg-400); }
/* Investigate consistency (per Xore): columns marked data-hp-md-detail
   are pane-only -- hidden in the list so wide tables (campaigns' 17
   columns) stay scannable, still cloned into the row inspector where the
   full detail lives. Scoped to the LIST so the clones stay visible. */
.hp-md--active .hp-md__list [data-hp-md-detail] { display: none; }
/* Cast preview inside the row inspector: real terminal output, quietly. */
.hp-md__preview {
  max-height: 260px; overflow: auto;
  margin: 0 0 var(--space-md); padding: var(--space-md) var(--space-md);
  border-radius: 10px;
  background: var(--bg-300);
  color: var(--text-100);
  font: 11.5px/1.5 var(--hp-mono);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.hp-md__rowcard dl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--space-sm) var(--space-md); margin: 0; }
.hp-md__rowcard dt { color: var(--text-200); font: 10px/18px var(--hp-mono); text-transform: uppercase; letter-spacing: .04em; }
.hp-md__rowcard dd { margin: 0; min-width: 0; overflow-wrap: anywhere; font-size: 12px; }
/* Cells projected into the inspector lose their table context: neutralize
   the value-cell floor so the narrow pane isn't forced wide. */
.hp-md__rowcard dd td.v, .hp-md__rowcard dd.v { min-width: 0; }

@media (max-width: 1100px) {
  .hp-md--open { grid-template-columns: 1fr; }
  .hp-md__pane { position: static; }
}

/* Hover row actions (pick 14B): quiet icon buttons that fade in per row. */
/* Design refresh 2 (pick 14E): the revealed cluster reads as claude's
   floating toolbar -- one raised pill of icon buttons. */
.hp-row-actions {
  /* Visible at rest, at full strength.
     It used to be opacity: 0 until the row was hovered, which made the
     actions discoverable only by accident -- nothing on screen suggested
     that hovering would reveal anything, so a reader concludes the column
     is empty. Dimming rather than hiding does not fix that: a faint
     control still reads as decoration. What rests here is a real button,
     and the ones that do not fit arrive when the pointer does. */
  display: inline-flex; gap: 2px; opacity: 1;
  padding: 3px var(--space-xs); border-radius: var(--radius-pill);
  background: var(--bg-raised);
  box-shadow: var(--shadow-pill);
  transition: opacity var(--transition-fast);
}

.hp-row-actions a, .hp-row-actions button {
  display: inline-grid; place-items: center;
  width: 24px; height: 24px;
  border: 0; border-radius: 6px;
  background: var(--bg-400);
  color: var(--text-000); /* a11y: secondary measured 4.38:1 on surface-3 */
  font-size: 12px; text-decoration: none; cursor: pointer;
}
.hp-row-actions a:hover, .hp-row-actions button:hover { background: var(--bg-500); color: var(--text-000); }

/* Actions beyond the first, and grouped actions, open on approach.

   The strip has a width budget -- it sits in a table row, and a row of
   twelve icons is not a strip any more. The previous answer was a
   disclosure menu, which solved the width by costing a click to discover
   what was inside, and then the same actions existed twice: once in the
   menu, once in the strip.

   This is the middle. What rests on screen is the action reached for most,
   so the strip is unmistakably there; the rest arrive when the pointer
   does. Nothing sits behind a click, and nothing takes width it has not
   earned.

   Both open on focus-within as well as hover, so they are reachable by
   keyboard. Animating max-width rather than display is what makes that
   possible at all: a display:none child cannot be tabbed to, so it would
   be a mouse-only control wearing an accessible shape. */
.hp-row-actions__more,
.hp-row-actions__items {
  display: inline-flex;
  gap: 2px;
  max-width: 0;
  overflow: hidden;
  opacity: 0;
  transition: max-width var(--transition-fast), opacity var(--transition-fast);
}
tr:hover .hp-row-actions__more,
tr.selected .hp-row-actions__more,
.hp-row-actions:focus-within .hp-row-actions__more,
.hp-row-actions:hover .hp-row-actions__more {
  max-width: 20rem;
  opacity: 1;
}

/* Every action at rest, for surfaces with room for them.

   The collapse above exists because a table row has a width budget: twelve
   icons across a row is not a strip any more, so all but the first wait for
   the pointer. A card has no such constraint -- its actions sit in a footer
   with the whole card width to themselves -- and there the collapse only
   reproduces the problem the disclosure menu had, where what you can do is
   discoverable by accident.

   So this is the same strip with the budget lifted, not a second component.
   `max-width: none` rather than a large value, so the row never wraps at a
   width the number of actions happens to exceed. */
.hp-row-actions--expanded .hp-row-actions__more {
  max-width: none;
  opacity: 1;
  overflow: visible;
}

/* The action in a strip that does something irreversible.

   Everything else in a strip navigates or copies, so the controls are
   deliberately uniform -- which is exactly what makes an action that
   publishes, deletes or resets dangerous to place among them: it looks like
   its neighbours right up until it runs. This keeps the same 24px shape and
   only changes what it says about itself, using the same danger tokens as
   the rest of the interface so it stays contrast-checked across all nine
   themes. */
.hp-row-actions .hp-row-actions__danger {
  background: var(--danger-soft);
  color: var(--danger-text-on-soft);
}
.hp-row-actions .hp-row-actions__danger:hover {
  background: var(--danger);
  color: var(--bg-000);
}

/* A group inside the strip: one icon saying a category exists, its members
   appearing beside it rather than inside a menu. */
.hp-row-actions__group { display: inline-flex; align-items: center; gap: 2px; }
.hp-row-actions__group:hover > .hp-row-actions__items,
.hp-row-actions__group:focus-within > .hp-row-actions__items {
  max-width: 16rem;
  opacity: 1;
}
/* The group's trigger names a category rather than performing an action,
   so it stays quiet until the group is open. */
.hp-row-actions__group > [aria-expanded] { color: var(--text-200); }
.hp-row-actions__group:hover > [aria-expanded],
.hp-row-actions__group:focus-within > [aria-expanded] { color: var(--text-000); }

@media (prefers-reduced-motion: reduce) {
  .hp-row-actions__more,
  .hp-row-actions__items { transition: none; }
}

/* Reports wizard (pick RP-C): step dots + prev/next footer. */
.hp-wizard-progress {
  display: flex; align-items: center; justify-content: center; gap: var(--space-sm);
  margin: var(--space-xs) 0 var(--space-lg);
  font: 11px var(--hp-mono); letter-spacing: 0.08em;
  color: var(--text-200); text-transform: uppercase;
}
.hp-wizard-progress i {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--bg-400); border: 1px solid var(--border-200);
}
.hp-wizard-progress i.done { background: var(--accent-soft); border-color: var(--accent); }
.hp-wizard-progress i.on { background: var(--accent); border-color: var(--accent); }
.hp-wizard-nav { display: flex; justify-content: space-between; gap: var(--space-md); margin-top: var(--space-sm); }

/* Empty states (pick 8E, evolving 8B): a calm serif sentence, a quiet
   muted hint, and one surface-pill action -- with a small inline line
   icon where the action genuinely has one (per Xore: icons only when
   applicable). */
.empty-state { display: grid; place-items: center; text-align: center; padding: var(--space-xl) var(--space-lg); }
.empty-state__icon { font-size: 24px; opacity: 0.6; color: var(--text-200); }
.empty-state__icon svg { width: 26px; height: 26px; }
.empty-state__title { margin: var(--space-sm) 0 2px; font-family: var(--font-serif); font-size: 17px; font-weight: 500; color: var(--text-000); }
.empty-state__hint { margin: 0; max-width: 420px; font-size: 12.5px; color: var(--text-200); }
.empty-state__action {
  display: inline-flex; align-items: center; gap: var(--space-sm); margin-top: var(--space-md);
  border: 0; border-radius: var(--radius-pill);
  padding: var(--space-sm) var(--space-lg);
  background: var(--bg-300);
  font-size: 12.5px; color: var(--text-000); text-decoration: none;
  transition: background var(--transition-fast), color var(--transition-fast);
}
.empty-state__action svg { width: 14px; height: 14px; }
.empty-state__action:hover { background: var(--bg-400); color: var(--accent); }

/* #1575: template gallery -- claude.ai's Scheduled-page empty state, where
   the "No X yet" sentence sits above a dashed divider followed by a grid
   of starter cards that pre-fill whatever creation flow the empty list
   would otherwise have left the operator to start from scratch. Shares the
   empty-state's icon/title/hint sentence above it; the divider and gallery
   are their own elements so a plain empty state (no starters available)
   can render the sentence alone, same markup either way. */
.empty-state__divider { width: 100%; max-width: 460px; margin: var(--space-lg) auto 0; border: none; border-top: 1px dashed var(--border-100); }
.template-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: var(--space-md);
  width: 100%;
  max-width: 720px;
  margin: var(--space-lg) auto 0;
  text-align: left;
}
.template-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-sm);
  padding: var(--space-lg);
  border: 1px solid var(--border-100);
  border-radius: var(--radius-panel);
  background: var(--bg-300);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background var(--transition-fast), border-color var(--transition-fast);
}
.template-card:hover, .template-card:focus-visible { background: var(--bg-500); border-color: var(--border-200); }
.template-card__icon {
  display: grid;
  width: 26px;
  height: 26px;
  place-items: center;
  border-radius: var(--radius-control);
  background: var(--bg-400);
  color: var(--text-200);
}
.template-card__icon svg { width: 15px; height: 15px; }
.template-card__title { font-family: var(--font-serif); font-size: 13.5px; font-weight: 500; color: var(--text-000); }
.template-card__desc { font-size: 11.5px; line-height: 1.45; color: var(--text-200); }
.template-card .chip { min-height: 0; margin-top: 2px; padding: 3px var(--space-md); font-size: 10.5px; pointer-events: none; }

/* #1575: humane 404 -- same empty-state voice, given room to breathe as a
   full page instead of sitting inside a card. */
.hp-notfound { min-height: 56vh; }

/* Content-shaped skeletons (pick 2D): ghosts that mirror the hydrated
   layout (tile / row primitives) with a gentle pulse; .skeleton-line
   keeps working wherever it is already used and now breathes too. */
@keyframes hp-skeleton-pulse { 0%, 100% { opacity: 0.45; } 50% { opacity: 1; } }
.skeleton-line { animation: hp-skeleton-pulse 1.6s var(--ease-out) infinite; }
.skeleton-tile {
  height: 56px; border-radius: 10px;
  background: var(--bg-300);
  animation: hp-skeleton-pulse 1.6s var(--ease-out) infinite;
}
.skeleton-row {
  height: 12px; border-radius: 6px; margin: var(--space-sm) 0;
  background: var(--bg-300);
  animation: hp-skeleton-pulse 1.6s var(--ease-out) infinite;
}
@media (prefers-reduced-motion: reduce) {
  .skeleton-line, .skeleton-tile, .skeleton-row { animation: none; }
}

/* Scroll "more" pill (pick 9C): paired JS in hp-app.js shows it while a
   .card__scroll region still has content below the fold. A real <button>
   now, not a passive affordance: mid-list it pages the region down, and
   near the end of a list with more entries it triggers the region's own
   lazy "Load 25 more", hides, and returns when the reader nears the new
   end -- so the base state must stay click-transparent (pointer-events:
   none) and only the --on state accepts input. */
.hp-scroll-more {
  position: sticky; bottom: 8px; z-index: 3;
  display: grid; place-items: center;
  width: 28px; height: 28px;
  /* Design refresh 2 (pick 9D): claude's circular jump button -- the pill
     becomes a raised round ↓ control; behaviour (page down mid-list, load
     next batch at the end) is unchanged in hp-app.js. */
  /* #135: negating via -var(...) parsed as an unknown function and dropped
     this shorthand whole -- centering and the overlap included. */
  margin: calc(-1 * var(--space-2xl)) auto var(--space-sm);
  border: 1px solid var(--border-100); border-radius: 50%;
  padding: 0;
  box-shadow: var(--shadow-pill);
  background: var(--bg-raised);
  color: var(--text-000); /* a11y: secondary measured 4.38:1 on surface-3 */
  font-size: 11px;
  font-family: inherit;
  line-height: inherit;
  cursor: pointer;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--transition-fast);
}
.hp-scroll-more--on { opacity: 1; pointer-events: auto; }
.hp-scroll-more--on:hover { background: var(--bg-300); }

/* Full settings page (pick 13B): the settings surface rendered as a page
   in the shell -- same rail/pane composition the modal uses, minus the
   overlay chrome. */
#hp-settings.hp-dash-settings--page {
  position: static;
  inset: auto;
  width: 100%;
  max-width: none;
  height: auto;
  max-height: none;
  margin: 0;
  transform: none;
  box-shadow: none;
  border: 1px solid var(--border-100);
}
#hp-settings.hp-dash-settings--page .settings-layout { max-height: none; min-height: 60vh; }


/* Topbar command bar (pick 11C): the global search/jump control sits front
   and center in the toolbar; page titles live in the pages themselves. */
.app-toolbar__search { flex: 1; display: flex; justify-content: center; min-width: 0; }
.hp-topbar-search {
  display: flex; align-items: center; gap: var(--space-sm);
  width: 100%; max-width: 440px;
  padding: var(--space-sm) var(--space-lg);
  border: 1px solid var(--border-100);
  border-radius: var(--radius-pill);
  background: var(--bg-200);
  color: var(--text-200);
  font-size: 13px;
  cursor: pointer;
  transition: border-color var(--transition-fast), background var(--transition-fast);
}
.hp-topbar-search:hover { border-color: var(--border-200); background: var(--bg-300); }
.hp-topbar-search svg { width: 14px; height: 14px; flex: none; }
.hp-topbar-search span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.hp-topbar-search kbd {
  margin-left: auto;
  border: 1px solid var(--border-100); border-radius: 5px;
  padding: 0 var(--space-sm); font: 11px var(--hp-mono); color: var(--text-200);
  background: var(--bg-300);
}
@media (max-width: 700px) { .hp-topbar-search kbd, .hp-topbar-search span { display: none; } .hp-topbar-search { width: auto; } }

/* A11y pass: Leaflet's stock white attribution strip carried near-white
   link text over light tiles (1.8:1). Theme it like every other chrome.
   The one styling pass for this strip -- an earlier block near the map-shell
   rules set bg-100/text-200 against the same selectors with the same
   !important and never rendered after this landed behind it (#147). */
.leaflet-control-attribution {
  background: color-mix(in srgb, var(--bg-200) 90%, transparent) !important;
  color: var(--text-100) !important;
}
.leaflet-control-attribution a { color: var(--text-link) !important; }

/* Theme swap: applied by hp-app.js for the swap frame so every color and
   surface changes atomically -- without this, per-property transition
   timing produced a visible light-ink-on-dark-surface beat on each theme
   change (and on any load where the pre-paint bootstrap and the synced
   server preference disagreed). */
.hp-theme-switching, .hp-theme-switching * { transition: none !important; }

/* #1567: the Report-a-problem dialog -- flat header+form children need a
   column stack and content-driven height; the generic .modal box is a
   fixed-height row flexbox meant for sidebar+content compositions. */
/* A modal sized to a short form rather than to a document. The base .modal
   caps at 1280x840, which is right for a PDF viewer or a log and far too
   much for six fields; height goes to auto so the panel is as tall as its
   content and no taller, with the same viewport margin the base uses.

   flex-direction is the load-bearing one: .modal.open is display:flex, so
   without it a header and a form become side-by-side columns rather than a
   header above a form. .modal--palette sets it for the same reason.

   Two names, one rule. .hp-pr-modal is the problem-report dialog, and had
   carried this shape alone since the Go dashboard; .modal--compact is the
   same shape for anything else that is a short form -- added for
   APIARY#1893, where a second dialog had written it out inline rather than
   find this one. The 680px cap comes from that dialog and is the tighter of
   the two, which suits a form either way. */
.modal--compact,
.hp-pr-modal {
  flex-direction: column;
  width: min(560px, calc(100vw - 40px));
  height: auto;
  max-height: min(680px, calc(100dvh - 40px));
  overflow: auto;
}
.hp-pr-modal form { display: flex; flex-direction: column; gap: var(--space-md); padding: var(--space-lg) var(--space-xl) var(--space-xl); }

/* #1565: the attack map's zoom-2 side gutters read as broken light-gray
   voids in dark mode -- paint the leaflet ground with a theme surface so
   the unfilled world edge looks intentional in both themes, and keep the
   "World" reset control from clipping mid-word at the card edge. */
.leaflet-container { background: var(--bg-300); }
.leaflet-control-home { white-space: nowrap; padding: 3px var(--space-md); }
/* #1565: Leaflet's stock attribution strip sits flush against the map's
   bottom-right corner with no inset -- inside .map-shell's own rounded
   border it read as overlapping/cut off by the curve rather than sitting
   inside it. A small margin off both edges and a matching corner radius. */
.leaflet-bottom.leaflet-right .leaflet-control-attribution { margin: 0 var(--space-sm) var(--space-sm) 0; border-radius: 6px; }

/* space-scale: exempt-start -- down to the matching exempt-end, the layout
   came over verbatim from the honeypot stack (settings modal inline <style>
   plus hp-* geometry, #58), and Xore/APIARY vendors this stylesheet
   byte-for-byte, so snapping these values locally would fork the vendor
   copy. They convert upstream and flow back with the next sync (#138):
   until then they ride outside the scale gate rather than hiding from it. */

/* ── Settings modal layout ─────────────────────────────────────────────
   Was ui/partials/settings_modal.html's own inline <style> (#58): that
   partial is only ever rendered as an injected fragment (/api/settings/
   modal, fetched by hp-app.js and inserted into the already-open
   permanent <dialog>), so an inline <style nonce="..."> there would need
   to carry the PARENT page's CSP nonce, not one generated for its own
   fragment response -- a mismatch that silently drops the rule under CSP.
   Living in the single shared stylesheet, loaded once, sidesteps that
   entirely. */
/* APIARY vendors this stylesheet byte-for-byte and ships no app-specific CSS. */
.hp-ip-filter-menu { min-width: 260px; }
.hp-ip-filter-list { max-height: 260px; overflow-y: auto; margin-bottom: 8px; }
.hp-ip-filter-row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-control);
  font-size: 12.5px;
}
.hp-ip-filter-row:hover { background: var(--bg-300); }
.hp-ip-filter-row input[type="checkbox"] { accent-color: var(--accent); flex: none; }
.hp-ip-filter-ip {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  color: var(--text-000);
  text-overflow: ellipsis;
  white-space: nowrap;
}
.hp-ip-filter-count { flex: none; color: var(--text-200); font-size: var(--font-size-xs); }
.hp-ip-filter-add {
  width: 100%;
  margin: var(--space-xs) 0 var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--border-100);
  border-radius: var(--radius-control);
  background: var(--bg-100);
  color: var(--text-000);
  font-size: 12.5px;
}
.hp-ip-filter-add:focus { border-color: var(--border-focus); }
.hp-ip-filter-actions { display: flex; gap: var(--space-sm); padding: 0 4px; }
.hp-rp-templates {
  display: grid;
  grid-template-columns: repeat(auto-fill,minmax(210px,1fr));
  gap: var(--space-md);
  margin: 4px 0 18px;
}
.hp-rp-template {
  padding: var(--space-md) var(--space-lg);
  border: 1px solid var(--border-100);
  border-radius: var(--radius-panel);
  background: var(--bg-200);
  color: var(--text-000);
  text-align: left;
  cursor: pointer;
  transition: border-color var(--transition-fast),background var(--transition-fast);
}
.hp-rp-template:hover { border-color: var(--border-200); background: var(--bg-500); }
.hp-rp-template[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
.hp-rp-template strong {
  display: block;
  margin-bottom: var(--space-xs);
  font-size: var(--font-size-footnote);
}
.hp-rp-template span { display: block; color: var(--text-200); font-size: 11.5px; line-height: 1.45; }
.hp-rp-grid { display: grid; grid-template-columns: repeat(auto-fill,minmax(220px,1fr)); gap: 12px 16px; }
.hp-rp-field label {
  display: block;
  margin-bottom: var(--space-xs);
  color: var(--text-200);
  font-size: var(--font-size-xs);
  letter-spacing: .06em;
  text-transform: uppercase;
}
.hp-rp-field input, .hp-rp-field select {
  width: 100%;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--border-100);
  border-radius: var(--radius-control);
  background: var(--bg-100);
  color: var(--text-000);
  font-size: var(--font-size-footnote);
}
.hp-rp-field input:focus, .hp-rp-field select:focus { border-color: var(--border-focus); }
.hp-rp-elements { display: grid; grid-template-columns: repeat(auto-fill,minmax(230px,1fr)); gap: 6px 16px; }
.hp-rp-elements label {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  padding: var(--space-xs) 0;
  color: var(--text-100);
  font-size: 12.5px;
  cursor: pointer;
}
.hp-rp-elements input { margin-top: 2px; accent-color: var(--accent); }
.hp-rp-elements small { display: block; color: var(--text-200); font-size: var(--font-size-xs); }
.hp-rp-theme { display: flex; gap: 10px; }
.hp-rp-theme button {
  display: flex;
  max-width: 220px;
  flex: 1;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-md);
  border: 1px solid var(--border-100);
  border-radius: var(--radius-panel);
  background: var(--bg-200);
  color: var(--text-000);
  font-size: var(--font-size-footnote);
  cursor: pointer;
}
.hp-rp-theme button[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
.hp-rp-swatch {
  width: 30px;
  height: 22px;
  flex: none;
  border: 1px solid var(--border-200);
  border-radius: var(--radius-xs);
}
.hp-rp-swatch--dark { background: #20201f; box-shadow: inset 0 -7px 0 #d97757; }
.hp-rp-swatch--light { background: #f7f6f2; box-shadow: inset 0 -7px 0 #c76548; }
.hp-rp-section { margin-top: 0; }
.hp-rp-section[hidden] { display: none; }
.hp-rp-actions {
  position: sticky;
  z-index: 5;
  bottom: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-md);
  margin-top: var(--space-lg);
  padding: var(--space-md) var(--space-lg);
  border: 1px solid var(--border-100);
  border-radius: var(--radius-panel);
  background: var(--bg-raised);
  box-shadow: var(--shadow-raised);
}
[data-hp-reports]:has([data-dashboard-panel="library"]:not([hidden])) .hp-rp-actions { display: none; }
.hp-rp-status { color: var(--text-200); font-size: var(--font-size-caption); }
.hp-rp-status[data-state="error"] { color: var(--danger); }
.hp-rp-status[data-state="ok"] { color: var(--success); }
.hp-rp-tag {
  display: inline-block;
  padding: 2px var(--space-sm);
  border-radius: var(--radius-pill);
  background: var(--bg-300);
  color: var(--text-100);
  font-size: var(--font-size-xs);
}
.hp-rp-payload-results {
  display: flex;
  max-height: 280px;
  flex-direction: column;
  gap: var(--space-xs);
  overflow-y: auto;
  margin: 10px 0;
}
.hp-rp-payload-row {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--border-100);
  border-radius: var(--radius-control);
  background: var(--bg-200);
  color: var(--text-000);
  font-size: 12.5px;
  text-align: left;
  cursor: pointer;
}
.hp-rp-payload-row:hover { border-color: var(--border-200); background: var(--bg-500); }
.hp-rp-payload-row[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
.hp-rp-payload-row code { flex: none; color: var(--text-100); font-size: 11.5px; }
.hp-rp-payload-row>span:nth-child(2) { flex: 1; color: var(--text-200); }
.hp-rp-payload-badges { display: flex; flex: none; gap: 4px; }
.hp-rp-toggle-row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  color: var(--text-100);
  font-size: var(--font-size-footnote);
  letter-spacing: 0;
  text-transform: none;
}
.hp-rp-toggle-row input[type="checkbox"] { accent-color: var(--accent); }
.hp-rp-tag--light { background: var(--info-soft); color: var(--info-text-on-soft,var(--info)); }
.hp-rp-row-actions { display: flex; flex-wrap: wrap; gap: 6px; }

/* Where the studio is in its own sequence.

   The steps also live in the sidebar rail, which lets an operator jump
   straight to one -- worth keeping when editing a saved definition, and no
   help at all when building the first, because five equal tabs say nothing
   about which comes first or how much is left. This states the position in
   words and separates the steps already passed from the ones still ahead,
   so the rail reads as a sequence without losing the ability to skip. */
.hp-rp-progress { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-xs) var(--space-md); margin-block-end: var(--space-md); }
.hp-rp-progress__position { color: var(--text-200); font-size: var(--font-size-xs); letter-spacing: .06em; text-transform: uppercase; }
.hp-rp-progress__steps { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-xs); margin: 0; padding: 0; list-style: none; color: var(--text-200); font-size: var(--font-size-xs); }
.hp-rp-progress__steps li + li::before { content: '\203a'; margin-inline-end: var(--space-xs); color: var(--border-200); }
.hp-rp-progress__steps li[data-state="done"] { color: var(--text-100); }
.hp-rp-progress__steps li[aria-current] { color: var(--accent-text-on-soft); font-weight: 600; }

/* What has been chosen so far, read back before anything is generated.

   Every row carries the step that owns the value, so "why does it say
   that" is one click rather than a hunt through four panels. A value the
   operator never set reads as the word the report will actually act on --
   "everything", "no schedule" -- rather than as an empty cell, which
   leaves it unclear whether the field was skipped or simply not shown. */
.hp-rp-review { display: grid; gap: 0; margin: var(--space-md) 0 0; }
.hp-rp-review__row { display: grid; grid-template-columns: minmax(0, 11rem) minmax(0, 1fr) auto; align-items: baseline; gap: var(--space-sm); padding-block: var(--space-sm); border-block-start: 1px solid var(--border-100); }
.hp-rp-review__row:first-child { border-block-start: 0; }
.hp-rp-review__key { color: var(--text-200); font-size: var(--font-size-xs); letter-spacing: .06em; text-transform: uppercase; }
.hp-rp-review__value { min-width: 0; color: var(--text-000); font-size: var(--font-size-footnote); overflow-wrap: anywhere; }
.hp-rp-review__value[data-unset] { color: var(--text-200); font-style: italic; }
@media (max-width: 640px) {
  .hp-rp-review__row { grid-template-columns: minmax(0, 1fr) auto; }
  .hp-rp-review__key { grid-column: 1 / -1; }
}
.pdf-viewer-modal {
  flex-direction: column;
  width: min(1000px,calc(100vw - 40px));
  height: min(860px,calc(100dvh - 48px));
  padding: 20px 20px 0;
}
.pdf-viewer-title {
  flex: none;
  margin: 0 var(--space-3xl) var(--space-md) 0;
  font-size: var(--font-size-lg);
}
.pdf-viewer-frame {
  flex: 1;
  width: 100%;
  min-height: 0;
  /* #135: as above -- bare -var(...) never parses as a negated length. */
  margin: 0 calc(-1 * var(--space-xl));
  border: 0;
  background: var(--bg-100);
}
@media (max-width:720px) {
  .pdf-viewer-modal { width: 100vw; height: 100dvh; border-radius: 0; }
}
.hp-pl-info-popover, .hp-sb-info-popover { width: 300px; }
.hp-pl-info-row, .hp-sb-info-row {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-sm) var(--space-md);
  font-size: var(--font-size-caption);
}
.hp-pl-info-row .k, .hp-sb-info-row .k {
  color: var(--text-200);
  font-size: 10.5px;
  letter-spacing: .05em;
  text-transform: uppercase;
}
.hp-pl-info-row .v, .hp-sb-info-row .v {
  color: var(--text-000);
  font-family: var(--hp-mono);
  word-break: break-all;
}
.hp-pl-label-trigger.action-menu>summary {
  width: auto;
  height: auto;
  min-height: 32px;
  padding: var(--space-sm) var(--space-lg);
  border-radius: var(--radius-pill);
  font-size: 12.5px;
}
#panel-identity h2 .hp-pl-label-trigger, .hp-sb-label-trigger.action-menu {
  margin-left: var(--space-sm);
  vertical-align: middle;
}
#panel-identity h2 .hp-pl-label-trigger>summary, .hp-sb-label-trigger.action-menu>summary {
  width: auto;
  height: auto;
  min-height: 24px;
  padding: 3px var(--space-md);
  border-radius: var(--radius-pill);
  font-size: var(--font-size-xs);
}
/* min-height reserves the framebuffer's box before noVNC has connected, so
   the panel does not start as a 1px border and jump. It was written inline
   at both call sites instead -- the same literal twice -- while the sibling
   .hp-tty-term below sets its own min-height here in the stylesheet, which
   is the shape this should have had. (APIARY#1893) */
.hp-vnc-canvas-wrap {
  min-height: 260px;
  overflow: hidden;
  border: 1px solid var(--border-100);
  border-radius: var(--radius-panel);
  background: var(--framebuffer-bg);
}
.hp-vnc-canvas-wrap canvas { display: block; width: 100%; height: auto; }
/* The connecting placeholder fills the box it is standing in for. */
.hp-vnc-canvas-wrap .skeleton-line { display: block; min-height: inherit; }
.hp-vnc-status { margin: var(--space-md) 0; color: var(--text-200); font-size: var(--font-size-caption); }
.hp-tty-term {
  min-height: 420px;
  overflow-x: auto;
  padding: var(--space-lg) var(--space-lg);
  border: 1px solid var(--border-100);
  border-radius: var(--radius-panel);
  background: var(--terminal-bg);
  color: var(--terminal-fg);
  font-family: var(--font-mono);
  font-size: var(--font-size-footnote);
  line-height: 1.35;
  white-space: pre;
}
.hp-tty-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-md);
  margin: 12px 0;
}
.hp-tty-controls input[type="range"] { min-width: 160px; flex: 1 1 260px; }
.hp-tty-status { color: var(--text-200); font-size: var(--font-size-caption); }
.hp-dash-settings > .settings-layout{width:100%;height:100%;min-height:0}
.hp-dash-settings .settings-layout__sidebar{overflow-y:auto}
.hp-dash-settings .settings-layout__content{height:100%;overflow-y:auto}
.hp-dash-settings > .modal__close{position:absolute;top:12px;right:12px;z-index:3}
.hp-settings-column{width:min(100%,880px)}
.hp-settings-head{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-xl);margin-bottom:24px}
.hp-settings-head h1{margin-bottom:6px}
.hp-settings-head p{margin:0;color:var(--text-100);font-size:13px}
.hp-settings-status{min-height:18px;margin:0 0 var(--space-lg);font-size:12px;color:var(--text-200)}
.hp-settings-status.is-error{color:var(--danger)}
.hp-settings-status.is-ok{color:var(--success)}
.hp-settings-pane[hidden],.hp-field[hidden]{display:none}
/* hp-dashboard.css zeroes .card's margin-bottom for its own CSS-grid pages
   (gap handles spacing there instead) -- that global rule leaks into this
   modal, which just stacks <section class="card"> in normal block flow with
   no grid/gap of its own, so every card butted against the next with no
   visible seam at all. Restore theme.css's own margin-bottom here, scoped to
   this modal only so the grid pages are untouched. */
.hp-settings-pane .card{margin-bottom:14px}
.hp-settings-pane .card:last-child{margin-bottom:0}
.sidebar__item.is-dirty::after{content:"";width:6px;height:6px;margin-left:auto;border-radius:50%;background:var(--accent)}
.hp-cap-list{display:flex;flex-wrap:wrap;gap:var(--space-sm);margin-top:10px}
.hp-cfg-source{margin:var(--space-sm) 0 0;font-size:11px;color:var(--text-200)}
.hp-cfg-source strong{color:var(--text-100);font-weight:600}
.hp-audit-filter{width:auto;min-width:180px}
.hp-rev-row{display:flex;align-items:center;gap:var(--space-md);padding:var(--space-md) 0;border-bottom:1px solid var(--border-100)}
.hp-rev-row:last-child{border-bottom:0}
.hp-rev-meta{flex:1;min-width:0;font-size:13px;color:var(--text-100)}
.hp-rev-meta small{display:block;color:var(--text-200);font-size:11px;margin-top:2px}
.hp-audit-row{padding:var(--space-sm) 0;border-bottom:1px solid var(--border-100);font-size:12px;color:var(--text-100)}
.hp-audit-row:last-child{border-bottom:0}
.hp-audit-row small{color:var(--text-200)}
.hp-head-actions{display:flex;gap:var(--space-sm);flex-shrink:0}
.hp-services-actions{display:flex;gap:var(--space-sm);flex-wrap:wrap}
@media(prefers-reduced-motion:reduce){
  .hp-dash-settings *{animation:none!important;transition:none!important}
}
/* space-scale: exempt-end (#138) */

/* ═══════════════════════════════════════════════════════════════════════
   Xore/auth-backend — application-specific
   ═══════════════════════════════════════════════════════════════════════
   Not part of the shared design system above this point -- consumed only
   by that app, kept here so it links a single stylesheet. Was app.html's
   own inline <style nonce>; two rules from that block are NOT here because
   they were themselves full duplicates of the system above rather than
   real additions: `.modal.modal--permanent{...}` duplicated
   .modal--permanent's own base rule property-for-property, and
   `a.btn{text-decoration:none}` duplicated .btn's own (unconditional,
   not anchor-specific) text-decoration:none. */
.u-mt8 { margin-top: var(--space-sm); }
.u-mt12 { margin-top: var(--space-md); }
.u-mt14 { margin-top: var(--space-lg); }
/* Removed: was a bare `body { overflow: hidden; }`. That's app-specific
   fixed-viewport-shell behavior, and this file has no app-specific
   exceptions -- every consumer gets the same shared rules this repo's own
   examples demonstrate (.app-shell's min-height:100dvh + .app-main's
   overflow:auto already contain a fixed-viewport shell's scrolling
   correctly, with no help needed from body). The unscoped selector leaked
   into every consumer regardless of whether it used that shell pattern,
   breaking plain document scrolling on this repo's own long-form example
   pages (components.html and others, none of which use .app-shell) --
   theme/examples#22. Any app that still needs body-level scroll locking
   for its own layout owns that in its own stylesheet, not here. */
.pane__h3 { margin-bottom: var(--space-xs); }
.pane__desc { font-size: 13px; color: var(--text-100); margin-bottom: var(--space-lg); }
.filter-row { display: flex; gap: var(--space-md); margin-bottom: var(--space-md); flex-wrap: wrap; }
.filter-row .form-input { flex: 1; min-width: 160px; width: auto; }
.filter-row select.form-input { flex: 0 0 auto; }
.newuser { display: grid; grid-template-columns: 1fr 110px 1fr auto; gap: var(--space-md); align-items: end; }
.row-actions { display: flex; gap: var(--space-sm); flex-wrap: wrap; }
/* theme.css's .metric-grid deliberately has no default margin-bottom (its
   own components.html example adds one per-context) -- this app always
   follows the grid with more content, so it needs one here. */
.metric-grid { margin-bottom: var(--space-xl); }
.user-edit-dialog { width: min(640px, 100%); max-height: min(88vh, 880px); overflow-y: auto; }
.user-edit-section { margin-top: var(--space-lg); padding-top: var(--space-lg); border-top: 1px solid var(--border-100); }
.user-edit-section:first-of-type { margin-top: var(--space-sm); padding-top: 0; border-top: 0; }
.user-edit-section .settings-field { margin-top: var(--space-md); }
.user-edit-section .settings-field:first-child { margin-top: 0; }
.user-edit-inline { display: flex; gap: var(--space-sm); align-items: center; }
.user-edit-inline .form-input { flex: 1; min-width: 0; }
.user-edit-perm-list {
  display: flex; flex-wrap: wrap; gap: var(--space-sm) var(--space-lg);
  margin-top: var(--space-md); max-height: 160px; overflow-y: auto;
  padding: 2px;
}
.user-edit-perm {
  display: flex; align-items: center; gap: var(--space-sm);
  font-size: 12px; font-family: var(--font-mono);
  white-space: nowrap;
}
.user-edit-perm input { margin: 0; }
.user-edit-actions { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-top: var(--space-md); }
.user-edit-actions .btn { font-size: 12px; padding: var(--space-sm) var(--space-md); }
#flash {
  position: fixed; top: 18px; left: 50%; transform: translateX(-50%); z-index: 400;
  display: none;
  background: var(--bg-100); border: 1px solid var(--green); border-radius: var(--radius-md);
  padding: var(--space-lg) var(--space-xl); font-size: 13px; color: var(--text-000); box-shadow: var(--shadow-modal);
  max-width: 90vw; word-break: break-all; text-align: center;
}
#flash.err { border-color: var(--red); }
#flash b { color: var(--green); }
.cell-mono { font-family: var(--font-mono); font-size: 11px; }
.cell-agent { font-size: 11px; color: var(--text-200); max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.empty-row { color: var(--text-200); font-size: 12px; }
.audit-table { table-layout: fixed; min-width: 900px; }
.audit-table th, .audit-table td { box-sizing: border-box; }
.audit-table th:nth-child(1), .audit-table td:nth-child(1) { width: 150px; }
.audit-table th:nth-child(2), .audit-table td:nth-child(2) { width: 160px; }
.audit-table th:nth-child(3), .audit-table td:nth-child(3) { width: 130px; }
.audit-table th:nth-child(4), .audit-table td:nth-child(4) { width: 120px; }
.audit-table th:nth-child(5), .audit-table td:nth-child(5) { width: 170px; }
.audit-table th:nth-child(6), .audit-table td:nth-child(6) { width: auto; }
.audit-table td:nth-child(2) .badge { white-space: normal; overflow-wrap: anywhere; line-height: 1.35; }
.audit-table .cell-agent { max-width: none; white-space: normal; overflow-wrap: anywhere; }
@media (max-width: 600px) {
  .hide-sm { display: none; }
  .newuser { grid-template-columns: 1fr; }
}
