/*
 * Management console — "Cyber Gold" theme.
 *
 * Dark-mode-only, hand-authored to reproduce the sibling project's High-Tech Security SaaS look
 * (larvitar_aio/website_design_rules.md) WITHOUT its delivery mechanism: no Tailwind, no CDN, no
 * Google Fonts, no build step. The system-font stack below is the whole typography dependency.
 * Every colour is a token here so the palette stays consistent and auditable.
 */

:root {
  /* Surfaces */
  --bg: #0f0f0f;
  --card-from: #0f0f0f;
  --card-to: #1a1a1a;
  --panel: #161616;
  --panel-deep: #101012; /* zinc-950-ish */
  --glass: rgba(255, 255, 255, 0.05);

  /* Accents */
  --gold: #dec76b;
  --gold-2: #c9b35e;
  --gold-soft: rgba(222, 199, 107, 0.09);
  --gold-bright: #ffd700; /* reserved for the confirmed-agentic accent */

  /* Text — ticket 032 (console legibility): --ink-dim already clears WCAG 2.2 AA's 4.5:1 normal-
     text minimum against every surface here (~7.5:1 on --bg). The original --ink-muted
     (zinc-500 #71717a) did not (~4.0:1 on --bg, worse on the lighter end of --card-to) — raised
     to a lighter zinc-ish gray that clears 4.5:1 against --bg through --card-to's #1a1a1a (~5.4:1
     at the lightest surface). It happens to land near --st-unknown/--rstatus-unknown (#8b8b93) —
     coincidence, not a merge: those are status-hue tokens always paired with a border + text
     label, --ink-muted is a foreground-only text tone, and nothing here renders one on top of the
     other. --ink-muted is still the console's lowest-contrast text tone — timestamps, evidence
     values, and error/alert copy use --ink-dim (or a dedicated alert color) instead; see the
     per-selector notes below where that promotion was made. */
  --ink: #f4f4f5; /* zinc-100 */
  --ink-body: #d4d4d8; /* zinc-300 */
  --ink-dim: #a1a1aa; /* zinc-400 */
  /* Raised from zinc-500 #71717a for AA contrast — see note above. Cross-note (ticket 039 review
     NIT 2): the base `select` rule's `background-image` (the chevron `data:` URI) hardcodes this
     same hex (#8f8f97) because a data URI is plain text, not evaluated CSS — keep the two in sync
     if this value ever changes. */
  --ink-muted: #8f8f97;

  /* Borders */
  --border: rgba(255, 255, 255, 0.07);
  --border-strong: rgba(255, 255, 255, 0.11);
  --border-gold: rgba(222, 199, 107, 0.28);

  /* Status hues — a generic OK/info/caution/danger/unknown "traffic light" palette used across the
     dashboard for states that are NOT a captured HTTP response code: role/active-state badges,
     bait-coverage hit/untouched, audit severity, scenario runtime state, a validated hackback
     replay. Each is always paired with a text label, never colour alone. */
  --st-2xx: #3fb950;
  --st-3xx: #58a6ff;
  --st-4xx: #e3b341;
  --st-5xx: #f85149;
  --st-unknown: #8b8b93;

  /* Captured HTTP response-status-class hues (ticket 20260831230223) — Live/Detail/War room/
     Sessions row pills and the Overview/War room status-mix breakdown, via dom.js's shared
     STATUS_COLORS/statusPill() and charts.js's statusBreakdown(). Deliberately its OWN palette,
     not --st-* above: a captured status code is protocol telemetry about what the decoy answered
     back, never a verdict on defender/attacker outcome, so 2xx gets a neutral steel tone instead
     of borrowing --st-2xx's green "success" meaning. 3xx/4xx/5xx/unknown keep the same hues as
     --st-* since "redirect"/"client error"/"server error" read as literal, truthful protocol
     descriptions either way — only the 2xx/green pairing was the mislabel.
     Reserve actual green (--health-ok below) for genuine system/collector health and a completed
     defensive-positive event; never derive it from a captured status code alone. */
  --rstatus-2xx: #7c93a8;
  --rstatus-3xx: #58a6ff;
  --rstatus-4xx: #e3b341;
  --rstatus-5xx: #f85149;
  --rstatus-unknown: #8b8b93;

  /* Reserved exclusively for genuine system/collector health (the console API reachable) and a
     completed defensive-positive event (e.g. a validated hackback callback) — never derived from a
     captured HTTP status code alone. */
  --health-ok: #3fb950;
  --health-down: #f85149;

  /* Type */
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, Helvetica, Arial,
    sans-serif;
  --font-mono: "SF Mono", "JetBrains Mono", "Fira Code", ui-monospace, Menlo, Monaco,
    "Cascadia Code", Consolas, monospace;

  /* Type scale (ticket 032 — console legibility). The console shipped with ~25 near-duplicate
     magic font-size values between 0.65rem and 0.95rem, several under 13px — fine on a
     developer's laptop, illegible across a table or on a meeting-room projector. This scale is
     the documented minimum: --fs-2xs (13px) is the floor for every informational text context —
     metadata, hints, table headers/cells, pills, form labels, timestamps, footers. Nothing that
     conveys information should render smaller than that. The one exception is genuinely
     decorative glyphs that carry no information of their own (e.g. the .feed-card__chevron /
     .warroom-journey__node connector arrows, which duplicate what DOM order already conveys, and
     the .hero__mark-text / .brand__mark-text wordmark SVGs) — those stay off this scale, and are
     all marked aria-hidden, which is what ticket 018's type-scale check
     (z/notes/console_verify.mjs) uses to tell a decorative glyph from real informational text
     when it walks the DOM. Fluid clamp()-based hero/KPI/ticker display text is also exempt: its
     minimum edge already sits above --fs-2xs, so snapping it to a fixed step would only reduce
     how it scales. */
  --fs-2xs: 0.8125rem; /* 13px — floor: metadata, hints, table headers/cells, pills, timestamps */
  --fs-xs: 0.875rem; /* 14px — secondary UI text: dense labels, small buttons, form hints */
  --fs-sm: 0.9375rem; /* 15px — body copy default (matches body's base size below) */
  --fs-base: 1rem; /* 16px — emphasized body, card/service identifiers */
  --fs-md: 1.125rem; /* 18px — subheadings, panel/section/dialog titles */
  --fs-lg: 1.375rem; /* 22px — larger headings (login title, War room title) */
  --fs-xl: 2rem; /* 32px — non-fluid executive-display numbers (Overview stat tiles) */

  /* Type roles (ticket 972, design note §4): `font` shorthands mapped onto the scale above, so a
     role never changes a size by accident. New and touched rules use a role; the --fs-* scale stays
     for the rules that have not moved yet. A `font:` shorthand resets weight and line-height, so a
     rule that needs another weight declares it after the role. */
  --type-large-title: 700 var(--fs-xl) / 1.15 var(--font-ui); /* a page title */
  --type-title: 600 var(--fs-md) / 1.3 var(--font-ui); /* a section title */
  --type-headline: 600 var(--fs-base) / 1.35 var(--font-ui); /* a card or group title */
  --type-body: 400 var(--fs-sm) / 1.55 var(--font-ui); /* running text */
  --type-footnote: 400 var(--fs-2xs) / 1.4 var(--font-ui); /* labels, table headers, meta text */

  /* Spacing scale (ticket 972): 4, 8, 12, 16, 20, 24, 32 and 48 px. The page head, the segmented
     control and the Partial badge use it; other rules move when a page ticket touches them. */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.25rem;
  --space-6: 1.5rem;
  --space-7: 2rem;
  --space-8: 3rem;

  /* One 160px tile of film grain (feTurbulence, about 3% once its opacity is applied). css/harbor.css lays it
     over the War room harbor picture; ticket 973 moved the token here from css/depth.css, which no longer
     draws a page backdrop. */
  --depth-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.06'/%3E%3C/svg%3E");

  --shadow-gold: none;
  --radius: 1rem;
  --radius-lg: 1.4rem;
  --maxw: 80rem;
  /* The sticky navbar's height, for sizing what sits under it (.view-root's min-height). It is 63px
     on one row from about 1280px up and 98 to 104px where its row wraps; measured 2026-10-21. */
  --navbar-h: 6.5rem;

  /* Motion system (ticket 033) — one shared duration/easing vocabulary instead of ad-hoc per-rule
     values scattered across this file, particles.js, and view-specific code (live.js/warroom.js).
     Every animation/transition built from these is CSS-only, so the site-wide
     `@media (prefers-reduced-motion: reduce)` rule further down (a universal `*` selector) already
     collapses it to an instant final state — nothing that only uses these tokens needs its own
     reduced-motion override. */
  --dur-fast: 0.12s; /* hover/press micro-feedback */
  --dur-base: 0.18s; /* route/menu/sheet entrances, toast, filter-applied feedback */
  --dur-slow: 0.45s; /* card/panel reveal sequencing */
  --dur-emphasis: 1.2s; /* finite value-change / live-evidence arrival emphasis — never a loop */
  --ease-standard: cubic-bezier(0.4, 0, 0.2, 1); /* general-purpose transitions */
  --ease-emphasized: cubic-bezier(0.16, 1, 0.3, 1); /* decelerated reveal — menus, sheets, routes */
}

* {
  box-sizing: border-box;
}

html {
  color-scheme: dark;
  /* Keep the scrollbar's gutter whether or not the page scrolls, so a classic scrollbar (Windows,
     Linux) appearing when a page grows past the viewport does not shift the page sideways (ticket 926). */
  scrollbar-gutter: stable;
}

body {
  margin: 0;
  min-height: 100vh;
  background: var(--bg);
  color: var(--ink-body);
  font: var(--type-body); /* 15px, unchanged — the scale's documented body tier */
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden; /* the page never scrolls sideways; wide tables scroll inside their own box */
}

a {
  color: var(--gold);
  text-decoration: none;
}

h1,
h2,
h3 {
  color: var(--ink);
  letter-spacing: -0.02em;
  font-weight: 600;
  margin: 0;
}

::selection {
  background: var(--gold-soft);
  color: var(--ink);
}

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

/* ---- Ambient particle field ------------------------------------------------------------- */

#particle-field {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
  opacity: 0.55;
}

/* ---- Navbar ----------------------------------------------------------------------------- */

.navbar {
  position: sticky;
  top: 0;
  z-index: 30;
  background: rgba(15, 15, 15, 0.72);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border-bottom: 1px solid var(--border);
}

.navbar__inner {
  max-width: min(100%, 105rem);
  margin: 0 auto;
  padding: 0.7rem 1.5rem;
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  min-width: 0;
  gap: 1rem;
}

/* ---- Local nav row (ticket 968) ----------------------------------------------------------- */

/* One thin row under the top bar: the section's name at the left (headline weight), its pages at the
   right (body size), the current page marked with the accent underline and nothing else. js/local_nav.js
   fills the <nav> and shows it before the first view paints, so the 44px is in place and the page does
   not jump (ticket 926). The phone tab bar stands in for it at 760px and narrower until ticket 971
   shows the row there. */
.local-nav {
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}

.local-nav__inner {
  max-width: min(100%, 105rem);
  min-height: 44px;
  margin: 0 auto;
  padding-left: max(1.5rem, env(safe-area-inset-left));
  padding-right: max(1.5rem, env(safe-area-inset-right));
  display: flex;
  align-items: center;
  gap: 1.5rem;
  min-width: 0;
}

.local-nav__section {
  margin: 0;
  flex: none;
  font-family: var(--font-ui);
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
}

.local-nav__links {
  display: flex;
  align-items: stretch;
  gap: 0.25rem;
  margin-left: auto;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}

.local-nav__link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 0.75rem;
  border-bottom: 2px solid transparent;
  font-family: var(--font-ui);
  font-size: var(--fs-sm);
  color: var(--ink-dim);
  white-space: nowrap;
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-standard);
}

.local-nav__link:hover {
  color: var(--ink);
}

.local-nav__link:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: -2px;
}

.local-nav__link.is-current {
  color: var(--ink);
  border-bottom-color: var(--gold);
}

@media (max-width: 760px) {
  .local-nav {
    display: none;
  }
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  color: var(--ink);
  font-weight: 600;
  letter-spacing: -0.02em;
  flex: none;
  white-space: nowrap;
}

/* Navbar wordmark: the same viewBox-scaled wordmark + wave + dot markup as .hero__mark, sized to sit
   inline in the slim sticky navbar. A fixed pixel width (not a clamp()) is deliberate here — unlike
   the hero mark, this one never changes size across routes/breakpoints, so there is nothing for a
   clamp to interpolate between. */
.brand__mark {
  display: block;
  width: 120px;
  height: auto;
  flex: none;
  user-select: none;
  pointer-events: none;
}

.brand__mark-text {
  fill: var(--gold);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 44px;
}

.brand__mark-wave {
  stroke: var(--gold-2);
  opacity: 0.55;
}

.brand__mark-dot {
  fill: var(--gold);
}

.brand__tag {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--ink-muted);
  border: 1px solid var(--border);
  border-radius: 9999px;
  padding: 0.1rem 0.5rem;
}

.nav-primary {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  flex: 0 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: visible;
}

/* The CTI link's ⓘ (ticket 1015). The button sits right after the link, so the link gives up most of its right
   padding (0,4,0 outranks console-simple.css's 0.85rem and the 0.5 to 0.6rem tiers below; with two Connectors
   shown the 0,5,0 :has() tiers keep their own 0.5 to 0.6rem, which is as tight). The panel is a child of .navbar
   and not of .nav-primary, which scrolls sideways below 1024px and would clip it; js/app.js (ctiTip) sets its
   `top` and `left` from the button's box when it opens, so these two are only the fallback. It is opaque and
   above the page content (the navbar is z-index 30), wraps its text, and goes away with the rest of the
   operator chrome while signed out. The phone layout hides .nav-primary (css/tabbar.css), and with it the
   button. */
.navbar .nav-primary .nav-link[data-section="cti"] {
  padding-right: 0.3rem;
}

.nav-tip__button {
  margin-right: 0.35rem;
}

.navbar .nav-tip__panel {
  position: absolute;
  top: 100%;
  left: 0.5rem;
  z-index: 5;
  box-sizing: border-box;
  width: max-content;
  max-width: min(22rem, calc(100vw - 1rem));
  margin: 0;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--border-gold);
  background: var(--panel-deep);
  box-shadow: 0 0.6rem 1.6rem rgba(0, 0, 0, 0.45);
  font-size: var(--fs-sm);
}

body.signed-out .nav-tip__panel {
  display: none;
}

.navbar__tools {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-left: auto;
  flex: 0 1 auto;
  min-width: 0;
}

.nav-more {
  position: relative;
}

.nav-trigger {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.8rem;
  border-radius: 9999px;
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  font-weight: 500;
  font-family: inherit;
  background: transparent;
  border: 1px solid transparent;
  cursor: pointer;
  transition:
    color 0.15s ease,
    background 0.15s ease,
    border-color 0.15s ease;
}

.nav-trigger:hover {
  color: var(--ink);
  background: var(--glass);
}

/* The account trigger only — the account disclosure doubles as an identity chip (ticket 003) so it
   reads as "who's signed in" rather than one more nav item, so it does not take the plain
   .nav-trigger look. A dedicated class (specificity 0,1,0, same as .nav-trigger) placed above
   .is-active in source order, so .is-active's higher-specificity border-color still wins when both
   apply, and above the responsive .nav-trigger max-width rules, so their later-in-source, equal-
   specificity max-width still overrides this rule's max-width at each tier.
   The cap is 8rem at every width (tickets 887 and 888; it was 12rem from 1440px up, 8rem below).
   The username is customer data of any length, and the navbar's one row has no spare width for a
   long one: a 20-character name made the chip 192px and the primary links overlapped the search box
   by 4 to 26px at 1440 to 1580px with the picker hidden, and by 9 to 39px from 1610 to 1920px with
   it shown (.navbar__inner stops growing at 105rem, 1680px, so no wider window clears it). The name
   is clipped with an ellipsis inside the chip, never wrapped; the account panel and the trigger's
   aria-label carry it in full. Ticket 970 made the trigger a fixed 44px avatar (see below), so the cap
   no longer binds; it stays, with its tests. */
.nav-trigger--account {
  border-color: var(--border);
  max-width: 8rem;
}

.nav-trigger.is-active {
  color: var(--gold);
  background: var(--gold-soft);
  border-color: var(--border-gold);
}

/* The avatar (ticket 970): the account trigger is a 34px circle of initials in a 44px button at every
   width, with no name or role beside it (the menu's first line carries both). `.navbar
   .nav-trigger--account` (0,2,0) outranks the responsive `.nav-trigger` padding rules of
   console-simple.css, which load later. The one other (0,2,0) padding rule, the 1024-1279px
   `.navbar__tools .nav-trigger` one near the license chip styles, comes later in this file and so
   excludes the account trigger by name; without that, the 0.6rem side padding squeezed the content
   box to about 25px and the circle became an oval. The initials also never shrink as a flex item.
   The accent marks only hover, focus and the open menu. */
.navbar .nav-trigger--account {
  width: 44px;
  height: 44px;
  padding: 0;
  justify-content: center;
  border: 0;
  background: transparent;
}

.nav-trigger__initials {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  background: var(--panel);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.02em;
}

.nav-trigger--account:hover .nav-trigger__initials,
.nav-trigger--account:focus-visible .nav-trigger__initials,
.nav-trigger--account[aria-expanded="true"] .nav-trigger__initials {
  border-color: var(--border-gold);
  color: var(--gold);
}

.nav-trigger__icon {
  width: 16px;
  height: 16px;
}

.nav-trigger__caret {
  width: 12px;
  height: 12px;
  transition: transform 0.15s ease;
}

.nav-trigger[aria-expanded="true"] .nav-trigger__caret {
  transform: rotate(180deg);
}

/* The overflow/navigation menu. A plain disclosure panel — no `role="menu"`, since the links
   inside keep normal tab order rather than implementing the ARIA menu widget's arrow-key contract. */
.nav-panel {
  position: absolute;
  top: calc(100% + 0.6rem);
  right: 0;
  z-index: 40;
  min-width: 15rem;
  max-width: calc(100vw - 2rem);
  max-height: calc(100vh - 5rem);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: 0.6rem;
  box-shadow: 0 20px 45px -15px rgba(0, 0, 0, 0.6);
  /* Same display:none-resets-the-animation mechanism as `.detail-overlay`'s overlay-in above — every
     time app.js clears `hidden` this replays from the start, so opening the menu gets a restrained
     reveal instead of an instant pop-in (ticket 033), with no JS beyond the existing hidden toggle. */
  animation: sheet-in var(--dur-base) var(--ease-emphasized);
}

.nav-panel[hidden] {
  display: none;
}

@keyframes sheet-in {
  from {
    opacity: 0;
    transform: translateY(-6px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.nav-panel__group {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}

/* Same author-`display`-beats-UA-`[hidden]` gap as `.nav-link[hidden]` below — app.js's
   applyRoleNav() hides the whole "Admin"/"Account" group once every link inside it is hidden
   (signed out, or non-admin), so an empty heading with a divider above it doesn't linger. */
.nav-panel__group[hidden] {
  display: none;
}

.nav-panel__group + .nav-panel__group {
  padding-top: 0.5rem;
  border-top: 1px solid var(--border);
}

/* Signed-in name above the "Account"/"Sign out" controls (ticket 20260910104933) — a non-interactive
   identity line: the name in the UI font and ink, the role under it in footnote size (ticket 970; the
   accent marks only interactive things, and a name is not one). */
.nav-panel__user {
  margin: 0.15rem 0.6rem 0.35rem;
  color: var(--ink);
  font: var(--type-body);
  font-weight: 600;
  overflow-wrap: anywhere;
}

.nav-panel__role {
  display: block;
  color: var(--ink-muted);
  font: var(--type-footnote);
}

.nav-panel .nav-link {
  justify-content: flex-start;
  border-radius: 0.55rem;
  padding: 0.5rem 0.6rem;
}

/* .nav-link's own color/border/hover rules apply to any element carrying the class, but a real
   <button> (the panel's own Sign out control) also needs the minimal appearance reset
   button.auth-status__logout already used for the old navbar control — an <a> never carries a
   browser default background/font/cursor to begin with. */
button.nav-link {
  background: transparent;
  font: inherit;
  cursor: pointer;
}

/* The panel's own "Go to" group is visible — always, at every width, not just below the desktop
   nav breakpoint (ticket 20260910151759: with no dedicated pill for any route, including
   Overview, this group is the only click-away *menu* path to Overview/Live feed/ATT&CK/
   Effectiveness/Briefing at any viewport — Overview alone also stays reachable via the brand
   wordmark's own link; the desktop breakpoint below no longer re-hides this group the way it hid
   the old four-pill duplication). */
.nav-panel [data-group="primary"] {
  display: flex;
}

.nav-link {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.4rem 0.8rem;
  border-radius: 9999px;
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  font-weight: 500;
  border: 1px solid transparent;
  transition:
    color 0.15s ease,
    background 0.15s ease,
    border-color 0.15s ease;
}

.nav-link:hover {
  color: var(--ink);
  background: var(--glass);
}

.nav-link.is-active {
  color: var(--gold);
  background: var(--gold-soft);
  border-color: var(--border-gold);
}

/* The author `display:inline-flex` above beats the UA `[hidden] { display:none }` — a role-hidden
   nav link (ticket 026 — a viewer has no Credentials view) would otherwise stay visible. */
.nav-link[hidden] {
  display: none;
}

.nav-link__icon {
  width: 16px;
  height: 16px;
}

/* The gear (ticket 969): an icon-only .nav-link beside the account menu. Its 20px icon is padded to the
   height of a section link, and it takes .nav-link.is-active's accent when it is the current section, so
   nothing on it is gold otherwise (page rule 3). */
.nav-link--icon {
  flex: none;
  padding: 0.4rem 0.55rem;
}

.nav-link--icon svg {
  display: block;
  width: 20px;
  height: 20px;
}

/* Global search (ticket 014) — the query box js/search_box.js mounts into .navbar__tools, next to
   the account trigger and status pill. Positioned relative so its results dropdown can anchor
   below it. */
.global-search {
  position: relative;
  flex: 0 1 14rem;
  min-width: 9rem;
}

.global-search[hidden] {
  display: none;
}

.global-search__input {
  width: 100%;
  padding: 0.4rem 0.8rem;
  border-radius: 9999px;
  border: 1px solid var(--border);
  background: var(--glass);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-sm);
  transition: border-color 0.15s ease;
}

.global-search__input::placeholder {
  color: var(--ink-muted);
  /* Ticket 927: the 13px floor, so "Search IPs, techniques, groups…" is 198px wide, not 224px. */
  font-size: var(--fs-2xs);
}

.global-search__input:focus-visible {
  outline: none;
  border-color: var(--border-gold);
}

/* Connector picker (ticket 814, sized by ticket 885) — the <select> js/connector_navbar.js shows in
   .navbar__tools beside the search box when the Console manages more than one Connector. Left to
   the base `select` rule it is as wide as its longest option (about 136px for "All Connectors",
   wider for a long Connector name, which is customer data), and in the one-row navbar that width
   pushed the primary links over the search box by 39 to 46px from 1240 to 1536px (0 with the picker
   hidden). A fixed width with an ellipsis keeps it bounded whatever the names; the open list still
   shows them in full. */
.connector-picker {
  flex: 0 1 auto;
  width: 7rem;
  min-width: 0;
  padding-left: 0.55rem;
  padding-right: 1.5rem;
  text-overflow: ellipsis;
}

/* From 1024 to 1680px (ticket 970 lowered this from 1240px; 1680px is the width where .navbar__inner stops growing) the one-row navbar has no spare
   width for the picker, so while it shows the search box starts at the picker's width (the :has()
   sibling test leaves it at its usual size when the picker is hidden: one Connector, or signed out)
   and the primary links keep the 0.6rem padding that ticket 119 gives them below 1280px
   (console-simple.css's 0.85rem would cost 48px). Measured in Chrome with two Connectors, the
   account "rip" and the license chip's short label, the links no longer overlap the search box at any
   width from 1240 to 1920px (39 to 46px of overlap before).
   Ticket 887: a long account name widens the chip to its 8rem cap, which that calibration did not
   include, and .nav-primary shrank below its links (they do not wrap, so they painted over the
   tools row) while the search box could not shrink at all. The primary row now keeps its width while
   the picker shows, so the tools row is the one that gives: the search box and the picker both
   shrink, down to 4rem and 4.5rem. With the 20-character account `bryce.kunz.long.name` and an
   ADMIN tag, the links overlap the search box and the picker by 0px at every width from 1239 to
   1920px (swept in 1px steps), with the picker shown or hidden; the search box is 66px and the
   picker 77px at the tightest widths (1240 to 1290px). Without these rules the row fits from about
   1640px up, and below 1240px the links move to their own row. A browser without :has() or range
   media syntax (Safari before 16.4) drops these rules and keeps the fixed picker width and the
   8rem chip cap above. */
@media (1023px < width <= 1680px) {
  .navbar:has(.connector-picker:not([hidden])) .nav-primary {
    flex-shrink: 0;
  }
  .navbar__tools .global-search:has(~ .connector-picker:not([hidden])) {
    flex-basis: 7rem;
    min-width: 4rem;
  }
  .navbar__tools .connector-picker {
    min-width: 4.5rem;
  }
  .navbar:has(.connector-picker:not([hidden])) .nav-primary .nav-link {
    padding-left: 0.6rem;
    padding-right: 0.6rem;
  }
}

/* Ticket 888: from 1601px up to 105rem (the .navbar__inner cap, 1680px; the row has the same width
   at every size above it) the links take the same 0.6rem padding as in the 1240 to 1680px tier above
   (ticket 887 extended that tier to 1680px; this block keeps ticket 888's explicit guard for the
   1601px to 105rem range). The search box keeps its usual size there. */
@media (1600px < width < 105rem) {
  .navbar:has(.connector-picker:not([hidden])) .nav-primary .nav-link {
    padding-left: 0.6rem;
    padding-right: 0.6rem;
  }
}

/* Below 1400px the picker and the search box start 1rem narrower each (the search box can still
   shrink to 4rem, ticket 887) and the tools row closes its gaps from 0.5rem to 0.3rem (ticket 885
   had this from 1240 to 1279px; ticket 888 raised the top from 80rem to 1400px, because a long
   account name, the chip at its 8rem cap, overlapped the Evidence link by up to 20px at 1280px and
   5px at 1310px). 1400px rather than 1320px leaves a 1366px laptop with a classic scrollbar (about
   1351px usable) the same room. */
@media (1023px < width < 1400px) {
  .navbar__tools:has(.connector-picker:not([hidden])) {
    gap: 0.3rem;
  }
  .navbar__tools .connector-picker {
    width: 6rem;
  }
  .navbar__tools .global-search:has(~ .connector-picker:not([hidden])) {
    flex-basis: 6rem;
    min-width: 4rem;
  }
}

/* Ticket 887: from 1240 to 1300px (1024 to 1300px since ticket 970; the chip is a fixed avatar now, so the cap is inert) the account chip is 7rem, not 8rem. With a long name and the
   picker hidden the tools row ran 4px (1240), 7px (1280) and 3px (1290) past the navbar's inner
   edge, into its padding; with the picker shown the 16px are what the search box and the picker
   need at 1240px. Not scoped to the picker, so the chip does not change width when a second
   Connector arrives. `.navbar .nav-trigger--account` (0,2,0) outranks the `.nav-trigger`
   max-width of the 1439px tier further down, which a later bare class would lose to. */
@media (1023px < width <= 1300px) {
  .navbar .nav-trigger--account {
    max-width: 7rem;
  }
}

/* Ticket 888: from 1240 to 1319px (1024 to 1319px since ticket 970) the long-name row is still short even with the rules above: at
   1280px, before this block, the links ran 36px past their box and the picker was squeezed from
   112 to 75px. So a shown picker also takes 0.5rem links (the ticket 119 padding was 0.6rem), a 7rem
   account chip (the name keeps about 43px) and 0.2rem between the links. Measured in Chrome with the
   account "bryce.kunz.long.name" and two Connectors: 0px of overlap at every width from 1240 to
   1920px (sweeps in 5 to 25px steps); with the short account "rip" the chip is narrower than either
   cap, so nothing here changes it. */
@media (1023px < width < 1320px) {
  .navbar:has(.connector-picker:not([hidden])) .nav-primary {
    gap: 0.2rem;
  }
  .navbar:has(.connector-picker:not([hidden])) .nav-primary .nav-link {
    padding-left: 0.5rem;
    padding-right: 0.5rem;
  }
  .navbar:has(.connector-picker:not([hidden])) .nav-trigger--account {
    max-width: 7rem;
  }
}

.global-search__results {
  position: absolute;
  top: calc(100% + 0.4rem);
  right: 0;
  left: auto;
  z-index: 40;
  min-width: 100%;
  width: max-content;
  max-width: min(24rem, calc(100vw - 2rem));
  max-height: calc(100vh - 6rem);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  margin: 0;
  padding: 0.4rem;
  list-style: none;
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: 0 20px 45px -15px rgba(0, 0, 0, 0.6);
}

.global-search__results[hidden] {
  display: none;
}

.global-search__hit {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  padding: 0.45rem 0.6rem;
  border-radius: 0.55rem;
  color: var(--ink-body);
  font-size: var(--fs-sm);
  font-family: var(--font-mono);
  overflow-wrap: anywhere;
}

.global-search__hit:hover,
.global-search__hit:focus-visible,
.global-search__hit.is-active {
  color: var(--ink);
  background: var(--glass);
}

/* Ticket 927: the arrow keys' active row also gets a gold edge, so it reads without the pointer. */
.global-search__hit.is-active {
  box-shadow: inset 0 0 0 1px var(--border-gold);
}

/* What the hit is (Address, Technique, ...): a small tag before the label, in the UI font so it reads as
   a tag next to the monospace label. */
.global-search__kind {
  flex: none;
  padding: 0.05rem 0.45rem;
  border: 1px solid var(--border);
  border-radius: 9999px;
  color: var(--ink-muted);
  font: var(--type-footnote);
}

.global-search__label {
  min-width: 0;
}

/* The typed part of the label. */
.global-search__mark {
  padding: 0 0.1rem;
  border-radius: 0.2rem;
  background: var(--gold-soft);
  color: var(--ink);
}

/* The shortcut that opens the search from anywhere (Ctrl K, or the Command key on a Mac), shown inside
   the field from 1241px up and gone while the field has focus or text. The field's right padding grows
   by the chip's width at the same breakpoint, and the placeholder ends in an ellipsis rather than
   running under the chip. */
.global-search__hint {
  display: none;
  position: absolute;
  top: 50%;
  right: 0.7rem;
  transform: translateY(-50%);
  padding: 0.05rem 0.4rem;
  border: 1px solid var(--border);
  border-radius: 0.35rem;
  color: var(--ink-muted);
  font-family: var(--font-ui);
  font-size: var(--fs-2xs);
  line-height: 1.3;
  pointer-events: none;
}

@media (width > 1240px) {
  .global-search__input {
    padding-right: 3.6rem;
    text-overflow: ellipsis;
  }
  .global-search__hint {
    display: block;
  }
  .global-search__input:focus ~ .global-search__hint,
  .global-search__input:not(:placeholder-shown) ~ .global-search__hint {
    display: none;
  }
}

.api-status {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--ink-dim);
  border: 1px solid var(--border);
  border-radius: 9999px;
  padding: 0.25rem 0.7rem;
  white-space: nowrap;
}

.api-status__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--ink-muted);
  box-shadow: 0 0 0 0 rgba(63, 185, 80, 0);
}

.api-status.is-ok .api-status__dot {
  background: var(--health-ok);
  animation: pulse-dot 2.4s ease-out infinite;
}

.api-status.is-down .api-status__dot {
  background: var(--health-down);
}

/* Busy (ticket 520): the server answered slowly or asked for time, so it is reachable. Amber, the
   caution tone of .scenario-alert--warn, never the red of offline and never a pulsing green dot. */
.api-status.is-busy .api-status__dot {
  background: var(--st-4xx);
}

.api-status.is-ok {
  color: var(--ink-body);
}

.api-status.is-busy {
  color: var(--st-4xx);
  border-color: rgba(227, 179, 65, 0.4);
}

.api-status.is-down {
  color: #fca5a5;
  border-color: rgba(248, 81, 73, 0.4);
}

/* The status pill (ticket 520) is out of the bar while the console is fine (js/app.js setStatus,
   ticket 970), so when it shows ("Console busy", "Console offline") its label is readable from 1152px
   up. Below that the bar has no room for the label's 100px next to the evaluation pill, a Connector
   picker and the search box (measured at 1024 to 1151px: the tools row ran 67px past the right edge,
   and the search box painted over "Reports"), so the pill clips to its bare dot, phones included. The
   title and the live region still carry the words. */
@media (max-width: 1151px) {
  .api-status__label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
}

/* License status chip (ticket 094): a plain .pill beside the account trigger, amber via
   .status-4xx once the evaluation ends. It shows only for an evaluation or an ended one (ticket 970).
   Below 1532px the full chip label is clipped and the chip paints a short aria-hidden label instead
   ("Eval 12/40", "Eval ended", tickets 116, 119 and 970); at 760px and narrower css/tabbar.css
   paints only the chip's dot, because the phone bar has no room for the label next to the search
   button, the gear and the avatar. The full label stays in the DOM for screen readers, and the chip
   title and aria-label carry the full license message. */

/* A long licensee name ends in an ellipsis rather than widening the navbar. */
.license-status__label {
  max-width: 13rem;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (max-width: 1531px) {
  .license-status__label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
}

.license-status__short {
  display: none;
}

/* Tickets 116 and 119: the short label needs about 84px at the default pill padding (76px at the
   narrower padding set here). In the one-row navbar that means freeing width elsewhere: with only
   the dot, it had about 60 and 89px spare at 1280 and 1440px (ticket 116) and 7px at 1240px
   (ticket 119). From 1240 to 1439px the search box gives up 2rem of its 10rem basis (scoped to
   .navbar__tools so it outranks the navbar geometry tier 2 rule further down). In the two-row
   tier (above 760px up to 1023px) the tools row has at least 160px spare, so nothing else
   changes there. This rule and the two below all use media range syntax, so a browser without it
   (Safari before 16.4) drops all three together and the chip paints only its dot below 1532px,
   as before ticket 116, rather than a short label without the width it needs. */
@media (max-width: 1531px) {
  #license-status {
    padding-inline: 0.5rem;
  }
  .license-status__short {
    display: inline;
  }
}

@media (1023px < width <= 1439px) {
  .navbar__tools .global-search {
    flex-basis: 8rem;
  }
}

/* Ticket 119: from 1024 to 1279px (1240 before ticket 970), below the 80rem rule that narrows the
   triggers, the tools row's triggers take that rule's 0.6rem padding (the avatar keeps its own zero
   padding), and the primary links drop from console-simple.css's 0.85rem to 0.6rem (scoped to .navbar
   so it outranks that later file).
   The px bounds here and in the search rule above are exact complements of tier 3's
   max-width: 1023px, so a fractional viewport (for example 1023.5px at browser zoom) still frees
   the width the short label needs; the upper bound is in rem, like the 80rem rule it meets. */
@media (1023px < width < 80rem) {
  .navbar__tools .nav-trigger:not(.nav-trigger--account),
  .navbar .nav-primary .nav-link {
    padding-left: 0.6rem;
    padding-right: 0.6rem;
  }
}

/* LIVE/PAUSED/STALE chip (ticket 20260831235605) — Live's toolbar and War room's header. Same
   ring-pulse mechanic as .api-status above, recoloured gold per the ticket brief rather than
   reusing --health-ok's green (that hue stays reserved for genuine collector/API reachability,
   not this view-local poll-liveness signal). The pulse is an ordinary CSS animation, so it is
   already neutralized by the site-wide `@media (prefers-reduced-motion: reduce)` rule below —
   no separate JS reduced-motion handling needed, same as .row-new's flash. */
.live-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font: var(--type-footnote);
  font-weight: 600;
  color: var(--ink-dim);
  border: 1px solid var(--border);
  border-radius: 9999px;
  padding: 0.25rem 0.7rem;
  white-space: nowrap;
}

.live-chip__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--ink-muted);
  box-shadow: 0 0 0 0 transparent;
}

.live-chip--live {
  color: var(--gold);
  border-color: var(--border-gold);
}

.live-chip--live .live-chip__dot {
  background: var(--gold);
  animation: live-chip-pulse 2.4s ease-out infinite;
}

.live-chip--paused .live-chip__dot {
  background: var(--ink-muted);
}

.live-chip--stale {
  color: var(--rstatus-4xx);
  border-color: rgba(227, 179, 65, 0.38);
}

.live-chip--stale .live-chip__dot {
  background: var(--rstatus-4xx);
}

@keyframes live-chip-pulse {
  0% {
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--gold) 45%, transparent);
  }
  70% {
    box-shadow: 0 0 0 7px transparent;
  }
  100% {
    box-shadow: 0 0 0 0 transparent;
  }
}

.auth-status {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--ink-dim);
  white-space: nowrap;
}

.auth-status__user {
  color: var(--gold);
}

.auth-status__link {
  color: var(--ink-body);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}

.auth-status__link:hover {
  color: var(--gold);
  border-bottom-color: var(--border-gold);
}

button.auth-status__logout {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  cursor: pointer;
}

.login-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

/* Brand block above the sign-in card (ticket: brand the login screen) — the navbar mark's wave and
   dot (js/views/login.js builds it without the wordmark text, ticket 937), centred instead of
   left-aligned since there is no adjacent nav content here. */
.login-brand {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  max-width: 28rem;
  margin: 2rem auto 0;
}

.login-brand .brand__mark {
  width: 150px;
}

.login-panel {
  max-width: 28rem;
  margin: 1.5rem auto;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}

/* The sign-in page's night cove (ticket 937; js/login_scene.js, loaded after the form). The sign-in
   card sits on the console's second depth plane over it, so the card is the denser, more blurred
   glass here. `.login-panel` is also the class of the MFA step card and of the signed-in profile
   cards, which this ticket leaves alone, so every rule for the card below is scoped to the root that
   holds the brand block, which only the sign-in form draws. That selector also out-ranks
   css/depth.css's plate rule whatever the file order, and the fallbacks below are repeated for the
   same reason. The form is drawn first and the scene fades in after, so nothing here moves the form:
   the scene is a fixed layer. */
.view-root:has(> .login-brand) > .login-panel {
  position: relative;
  z-index: 1;
  background:
    linear-gradient(var(--plane-2), var(--plane-2)) padding-box,
    var(--hud-edge) border-box;
  border: 1px solid transparent;
  -webkit-backdrop-filter: blur(var(--hud-blur-2)) saturate(140%);
  backdrop-filter: blur(var(--hud-blur-2)) saturate(140%);
  box-shadow: var(--elev-3);
}

/* The card is centred, a little below the top on a tall screen. */
.view-root:has(> .login-brand) {
  padding-top: clamp(1rem, 12vh, 7rem);
}

.login-scene {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  isolation: isolate;
  pointer-events: none;
  background: #050914;
}

/* The picture's 16:9 box, covering the viewport and anchored to the bottom centre: a wide screen
   crops the sky, a phone crops the sides. The script writes the size (a whole number of device
   pixels per logical pixel); this is the size before it does. The shimmer, the glow and the
   percentages the script writes are all the picture's own, so they stay on the art at any size. */
.login-scene__frame {
  position: absolute;
  left: 50%;
  bottom: 0;
  width: max(100vw, calc(100dvh * 16 / 9));
  aspect-ratio: 16 / 9;
  transform: translateX(-50%);
}

.login-scene__pixels,
.login-scene__strip {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}

/* The water shimmer: a pre-rendered strip twice as wide as the picture, slid left by half its width
   in a loop. Compositor-only (a transform), no script per frame. */
.login-scene__sea {
  position: absolute;
  left: 0;
  right: 0;
  overflow: hidden;
}

.login-scene__strip {
  width: 200%;
}

/* The lantern's glow: a soft disc over the lamp, screened onto the picture, with a slow flicker. */
.login-scene__glow {
  position: absolute;
  width: 20%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  background: radial-gradient(
    circle,
    rgba(255, 214, 140, 0.6),
    rgba(255, 170, 80, 0.2) 34%,
    transparent 68%
  );
  mix-blend-mode: screen;
}

/* Tilt-shift: at the top and at the bottom of the screen, three stacked backdrop blurs of 2, 4 and 8
   pixels, each masked to a narrower strip toward the edge, so the blur grows toward the frame. */
.login-scene__shift {
  position: absolute;
  left: 0;
  right: 0;
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
}

.login-scene__shift--top {
  top: 0;
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, transparent 100%);
}

.login-scene__shift--bottom {
  bottom: 0;
  -webkit-mask-image: linear-gradient(to top, #000 0%, transparent 100%);
  mask-image: linear-gradient(to top, #000 0%, transparent 100%);
}

.login-scene__shift--a {
  height: 30%;
}

.login-scene__shift--b {
  height: 21%;
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}

.login-scene__shift--c {
  height: 12%;
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
}

/* No backdrop-filter: dark bands at the top and bottom instead. One band per edge is enough. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .login-scene__shift {
    -webkit-mask-image: none;
    mask-image: none;
  }

  .login-scene__shift--b,
  .login-scene__shift--c {
    display: none;
  }

  .login-scene__shift--top {
    background: linear-gradient(to bottom, rgba(3, 6, 20, 0.55), rgba(3, 6, 20, 0));
  }

  .login-scene__shift--bottom {
    background: linear-gradient(to top, rgba(3, 6, 20, 0.55), rgba(3, 6, 20, 0));
  }
}

.login-scene__vignette {
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse at 50% 45%, transparent 50%, rgba(0, 0, 0, 0.6));
}

/* Motion is only in a scene the script marked is-animated; with reduced motion it never is, so the
   still scene shows at once. */
.login-scene.is-animated {
  animation: login-scene-in var(--dur-slow) var(--ease-standard) both;
}

.login-scene.is-animated .login-scene__strip {
  animation: login-shimmer 90s linear infinite;
}

.login-scene.is-animated .login-scene__glow {
  animation: login-flicker 4.3s ease-in-out infinite;
}

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

@keyframes login-shimmer {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-50%);
  }
}

@keyframes login-flicker {
  0%,
  100% {
    opacity: 0.85;
  }
  38% {
    opacity: 1;
  }
  61% {
    opacity: 0.72;
  }
  80% {
    opacity: 0.95;
  }
}

@media (prefers-reduced-motion: reduce) {
  .login-scene.is-animated,
  .login-scene.is-animated .login-scene__strip,
  .login-scene.is-animated .login-scene__glow {
    animation: none;
  }
}

/* Reduced transparency or forced colours: no scene, the plain dark page with a solid card. */
@media (prefers-reduced-transparency: reduce) {
  .login-scene {
    display: none;
  }

  .view-root:has(> .login-brand) > .login-panel {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

@media (forced-colors: active) {
  .login-scene {
    display: none;
  }

  .view-root:has(> .login-brand) > .login-panel {
    background: Canvas;
    border: 1px solid CanvasText;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    box-shadow: none;
  }
}

/* A phone: the card is the page width minus 16px gutters (css/tabbar.css) and sits in the upper
   third, so the keyboard does not cover the button. */
@media (max-width: 560px) {
  .view-root:has(> .login-brand) {
    padding-top: 0.75rem;
  }

  .login-brand {
    margin-top: 0;
  }

  .view-root:has(> .login-brand) > .login-panel {
    margin-top: 0.75rem;
  }
}

@media print {
  .login-scene {
    display: none;
  }
}

.login-title {
  margin: 0;
  font-size: var(--fs-lg);
  color: var(--ink);
}

.login-lede {
  margin: 0;
  color: var(--ink-body);
  font-size: var(--fs-sm);
  line-height: 1.45;
}

.login-form {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.login-label {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  font: var(--type-footnote);
  color: var(--ink-muted);
}

/* Ticket 039 review round 2 NIT: this class's specificity (0,1,0) outranks the base `select`
   rule (0,0,1) — re-adding `login-input` to a <select> (as the triage select once did) clobbers
   the chevron/fill instead of just losing layout, so don't reuse this class on a select. */
.login-input {
  font-family: var(--font-ui);
  font-size: var(--fs-sm);
  text-transform: none;
  letter-spacing: normal;
  color: var(--ink);
  background: var(--panel-deep);
  border: 1px solid var(--border-strong);
  border-radius: 0.6rem;
  padding: 0.55rem 0.75rem;
  /* Shared with the password field's Show/Hide toggle below (review round 3 NIT) — living here
     rather than scoped to `.login-password-field .login-input` means the username field gets the
     same min-height as the password field, instead of only matching its own natural height. */
  min-height: 2.4rem;
}

.login-input:focus {
  outline: none;
  border-color: var(--border-gold);
  box-shadow: 0 0 0 2px var(--gold-soft);
}

/* Password field + show/hide toggle — flex-wrap so the toggle drops under the input rather than
   forcing horizontal overflow on a narrow phone viewport instead of clipping or scrolling. */
.login-password-field {
  display: flex;
  align-items: stretch;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.login-password-field .login-input {
  flex: 1 1 12rem;
  min-width: 0;
}

/* Pinned to the same min-height as `.login-input` (shared with the password input above) rather
   than relying only on the parent's `align-items: stretch` — that stretch only equalizes the two
   while they share a flex line, and `.login-password-field`'s `flex-wrap: wrap` drops the toggle
   onto its own line (and its own, shorter natural height) on a narrow viewport. Scoped under
   `.login-password-field` (not a bare `.login-toggle-visibility`) so the `font-size` beats
   `.btn--small`'s — equal specificity, later in this file — which would otherwise leave
   `var(--fs-xs)` dead (review round 1). */
.login-password-field .login-toggle-visibility {
  flex: 0 0 auto;
  min-height: 2.4rem;
  font-size: var(--fs-xs);
}

.login-caps-warning {
  margin: 0;
  color: var(--gold);
  font-size: var(--fs-2xs);
}

.login-caps-warning[hidden] {
  display: none;
}

.login-mfa-account {
  margin: 0;
  color: var(--ink-muted);
  font-size: var(--fs-xs);
}

.login-mfa-switch-row {
  display: flex;
}

.login-error {
  color: #fca5a5;
  background: rgba(248, 81, 73, 0.12);
  border: 1px solid rgba(248, 81, 73, 0.35);
  border-radius: 0.6rem;
  padding: 0.55rem 0.75rem;
  font-size: var(--fs-xs);
}

.login-error[hidden],
.login-error:empty {
  display: none;
}

.login-submit {
  align-self: flex-start;
}

.login-status {
  margin: 0;
  min-height: 1.2em;
  color: var(--ink-dim);
  font-size: var(--fs-xs);
}

.login-token-note {
  margin: 0;
  color: var(--ink-muted);
  font-size: var(--fs-2xs);
  line-height: 1.4;
}

/* TOTP MFA enrollment (ticket 028) — the QR is inline SVG geometry, not a bitmap, so it stays
   crisp at any size; the surrounding card is wider than the default .login-panel cap. */
.mfa-card-body,
.mfa-enroll {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}

.mfa-qr {
  background: #ffffff;
  padding: 0.6rem;
  border-radius: 4px;
  align-self: flex-start;
}

.mfa-backup-codes {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: 0.4rem;
  margin: 0;
  padding: 0.75rem 1rem;
  list-style: none;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--ink);
  background: var(--panel-deep);
  border: 1px solid var(--border-gold);
  border-radius: 4px;
}

@keyframes pulse-dot {
  0% {
    box-shadow: 0 0 0 0 rgba(63, 185, 80, 0.45);
  }
  70% {
    box-shadow: 0 0 0 7px rgba(63, 185, 80, 0);
  }
  100% {
    box-shadow: 0 0 0 0 rgba(63, 185, 80, 0);
  }
}

/* ---- Capture context strip (ticket 20260831230239) --------------------------------------- */
/* Always-available evidence-context surface: capture source/instance identity, last captured
   exchange, and freshness (js/capture_context.js). Sits at the bottom of the page, after
   #view-root's content (opens the trailing "evidence and evaluation path" block together with
   .exec-journey below it) — hidden entirely under body.is-kiosk (War
   room renders its own compact .warroom-evidence chip there instead — see the "Kiosk" rule below)
   and while signed out (app.js toggles [hidden] alongside the rest of the operator chrome). Rows
   reuse the generic .pill.status-2xx/3xx/4xx/unknown "traffic light" palette — see dom.js's
   STATUS_LABELS comment for why that palette, not the captured-response rstatus-* one, is the
   right fit for a non-HTTP-status state. The top border is this footer block's own divider,
   separating it from the page content above (#view-root's own generous bottom padding already
   provides the gap above the rule, so no extra top margin is needed here); .exec-journey
   deliberately carries no divider of its own so the pair reads as one block rather than two. The
   bottom margin guarantees breathing room below the strip even when .exec-journey is hidden
   (most routes) and this is the last visible element of the pair. */
.capture-context {
  position: relative;
  z-index: 1;
  max-width: var(--maxw);
  margin: 0 auto 1.6rem;
  padding: 1.4rem 1.5rem 0;
  border-top: 1px solid var(--border);
}

.capture-context[hidden] {
  display: none;
}

.capture-context__rows {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem 1.6rem;
  list-style: none;
  margin: 0;
  padding: 0.6rem 1rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: var(--fs-xs);
}

.capture-context__row {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  min-width: 0;
}

.capture-context__label {
  color: var(--ink-muted);
  font: var(--type-footnote);
  white-space: nowrap;
}

.capture-context__value {
  color: var(--ink-body);
  white-space: nowrap;
}

.capture-context__sub {
  color: var(--ink-dim);
  font-size: var(--fs-2xs);
}

/* ---- Executive workflow breadcrumb (ticket 026) ------------------------------------------ */
/* Orientation strip for the five promoted primary routes (js/exec_journey.js): posture -> evidence
   -> outcome -> board artifact. Sits directly under the capture-context strip at the bottom of the
   page, same width/gutter and card language, closing out the trailing evidence-and-evaluation-path
   block at the very end of the page; hidden entirely while signed out or on any route
   outside the evaluation path (app.js toggles it alongside the rest of the operator chrome), in
   War room's kiosk mode, and in print (see those rules below). */
.exec-journey {
  position: relative;
  z-index: 1;
  max-width: var(--maxw);
  margin: 0.7rem auto 1.6rem;
  padding: 0 1.5rem;
}

.exec-journey[hidden] {
  display: none;
}

.exec-journey__label {
  display: block;
  margin: 0 0 0.35rem;
  font: var(--type-footnote);
  color: var(--ink-muted);
}

.exec-journey__stages {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem 0.9rem;
  list-style: none;
  margin: 0;
  padding: 0.55rem 1rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: var(--fs-xs);
}

.exec-journey__stage {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
}

.exec-journey__stage:not(:last-child)::after {
  content: "\2192";
  margin-left: 0.9rem;
  color: var(--ink-muted);
}

.exec-journey__stage-label {
  color: var(--ink-muted);
  font: var(--type-footnote);
  white-space: nowrap;
}

.exec-journey__stage.is-active .exec-journey__stage-label {
  color: var(--gold);
}

.exec-journey__stage-routes {
  white-space: nowrap;
}

.exec-journey__dot {
  margin: 0 0.3rem;
  color: var(--ink-muted);
}

.exec-journey__link {
  color: var(--ink-dim);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}

.exec-journey__link:hover {
  color: var(--ink-body);
  border-bottom-color: var(--border);
}

.exec-journey__link.is-active {
  color: var(--gold);
  border-bottom-color: var(--border-gold);
}

/* ---- Hero ------------------------------------------------------------------------------- */

.hero {
  position: relative;
  z-index: 1;
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 2.6rem 1.5rem 1.4rem;
}

.hero__mark {
  display: block;
  width: clamp(150px, 26vw, 300px);
  height: auto;
  margin: 0 0 0.6rem;
  user-select: none;
  pointer-events: none;
  filter: drop-shadow(0 0 14px color-mix(in srgb, var(--gold) 25%, transparent));
  animation: hero-mark-glow 5s ease-in-out infinite;
}

.hero__mark-text {
  fill: var(--gold);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 44px;
}

.hero__mark-wave {
  stroke: var(--gold-2);
  opacity: 0.55;
}

.hero__mark-dot {
  fill: var(--gold);
}

@keyframes hero-mark-glow {
  0%,
  100% {
    opacity: 0.85;
  }
  50% {
    opacity: 1;
  }
}

.hero__eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  letter-spacing: 0.22em;
  color: var(--gold-2);
  margin: 0 0 0.5rem;
}

.hero__subtitle {
  margin: 0.6rem 0 0;
  color: var(--ink-dim);
  max-width: 46rem;
  font-size: var(--fs-base);
}

/* Compact chrome (ticket 009): every inner data view (js/app.js's showView() sets this body class
   for every route except #/login) collapses the full-height hero to a slim brand
   strip carrying only the tagline (ticket 20260910104933 — the navbar already carries its own
   wordmark directly above this one, so showing it twice was pure duplication) — so #view-root
   starts within the first ~80px instead of a third of the viewport down. Kiosk mode still fully
   hides `.hero` regardless of this class (`body.is-kiosk .hero` below wins on `display: none`). */
body.chrome-compact .hero {
  display: flex;
  align-items: center;
  padding: 0.6rem 1.5rem;
  border-bottom: 1px solid var(--border);
}

body.chrome-compact .hero__mark {
  display: none;
}

body.chrome-compact .hero__body {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.15rem 0.6rem;
}

body.chrome-compact .hero__eyebrow {
  margin: 0;
}

body.chrome-compact .hero__subtitle {
  display: none;
}

@media (max-width: 560px) {
  body.chrome-compact .hero {
    padding: 0.55rem 1.1rem;
  }
}

/* Signed-out chrome (ticket 20260910110707): a stranger who reaches this console before signing
   in must see an ordinary sign-in screen, not this product's identity or live status — no brand,
   no tagline, no connectivity pill, no ambient motion. `index.html` ships `<body class=
   "signed-out">` so there is no flash of full chrome before app.js's module script runs;
   applyRoleNav() (js/app.js) clears/restores the class on every role change — the same function
   every sign-out path (button, expired token, failed refresh) already re-collapses the operator
   nav from — so it always tracks whether a role is actually known. `!important` mirrors
   `body.is-kiosk` below: `body.chrome-compact .hero { display: flex; }` above is equal
   specificity and later in source, so it would otherwise win over a plain `display: none` here.
   Exception (ticket 017, brand the login screen): the rules below only ever hide the *navbar's*
   brand mark — `.login-brand` is a sibling rendered above `.login-panel`, not the navbar, so it
   is unaffected and paints the wave/dot mark plus the "console" tag on this same signed-out screen.
   The product name stays hidden either way: `.login-brand`'s mark has no `<text>` node at all
   (ticket 937; the navbar's ships empty until `applyRoleNav()` fills it in), so a stranger never
   sees the wordmark. */
body.signed-out .navbar,
body.signed-out .hero,
body.signed-out #particle-field,
body.signed-out #api-status {
  display: none !important;
}

/* ---- View shell ------------------------------------------------------------------------- */

.view-root {
  position: relative;
  z-index: 1;
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0.8rem 1.5rem 4rem;
  outline: none;
  /* A short page keeps most of the screen under the navbar, so a placeholder or a short answer does
     not collapse the page and then grow it back (ticket 926). The 3rem of slack keeps the evidence
     strip's row (about 2.8rem) from making a short page scroll. */
  min-height: calc(100dvh - var(--navbar-h) - 3rem);
}

@media (min-width: 1280px) {
  :root {
    --navbar-h: 3.95rem;
  }
}

/* Route-content entrance (ticket 033: "routes replace the root instantly"). #view-root itself never
   toggles display (only its children are replaced), so unlike .detail-panel/.nav-panel above this
   can't rely on a display:none reset to replay — app.js's showView() re-adds this class after a
   forced reflow on every navigation. Opacity-only (no transform) so it never disturbs the sticky
   table headers a view may render inside #view-root. */
.view-root.view-enter {
  animation: view-enter var(--dur-slow) var(--ease-standard);
}

@keyframes view-enter {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* Card/chart reveal sequencing (ticket 039 — ticket 033's own Scope bullet, left undone there): a
   small, restrained staggered entrance for a grid of cards/charts that appears once at view mount
   (War room's ticker+panel row) — never a grid that gets rebuilt
   on a poll (kpiHost/journeySteps/panelBody already have their own value-change pulse/fade, and
   restaggering them every few seconds would be exactly the "run continuously" ticket 033's own
   Scope ruled out). Opacity-only, same reasoning as #view-root.view-enter above: a transform here
   would create a new containing block for any position:sticky descendant a grid's card might
   render (defensive — none do today, but the constraint is the same one, not a one-off). Plain
   nth-child delays rather than a JS-computed custom property — every grid this targets is small and
   fixed-size, so CSS alone is enough; the reduced-motion rule above zeroes animation-delay too, so
   a reduced-motion viewer never sits looking at an invisible tail item waiting out its delay. */
.stagger-reveal > * {
  animation: stagger-in var(--dur-slow) var(--ease-standard) both;
}

.stagger-reveal > *:nth-child(1) {
  animation-delay: 0s;
}

.stagger-reveal > *:nth-child(2) {
  animation-delay: var(--dur-fast);
}

.stagger-reveal > *:nth-child(3) {
  animation-delay: calc(var(--dur-fast) * 2);
}

.stagger-reveal > *:nth-child(4) {
  animation-delay: calc(var(--dur-fast) * 3);
}

.stagger-reveal > *:nth-child(5) {
  animation-delay: calc(var(--dur-fast) * 4);
}

.stagger-reveal > *:nth-child(n + 6) {
  animation-delay: calc(var(--dur-fast) * 5);
}

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

.section {
  margin-top: 2rem;
}

.section__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 0.9rem;
}

.section__title {
  font: var(--type-title);
}

.section__hint {
  font: var(--type-footnote);
  color: var(--ink-muted);
}

/* Groups a .section__hint with a trailing chip (Live's LIVE/PAUSED/STALE chip) inside
   .section__head without disturbing that flex row's existing two-column layout elsewhere. */
.section__head-meta {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}

/* Day-picker control (ticket 027) — older/newer buttons flanking a <select>, mounted inside
   .section__head-meta on every day-scoped view. */
.day-picker {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.day-picker__nav {
  padding: 0.28rem 0.55rem;
  line-height: 1;
}

.day-picker__nav:disabled {
  opacity: 0.35;
  cursor: default;
  pointer-events: none;
}

/* Dense-row modifier (ticket 039 review NIT 4) — the base `select` rule (ticket 016) sizes for a
   standalone control; this one sits shoulder-to-shoulder with the older/newer nav buttons above,
   so it keeps the mono/`--fs-2xs` type and tighter padding it had before ticket 016, restored
   here rather than by weakening the base rule for every other select. Right padding stays wide
   enough for the base rule's chevron. */
.day-picker__select {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  padding: 0.28rem 1.6rem 0.28rem 0.5rem;
  min-height: 1.8rem;
}

/* Base `select` (ticket 016) now carries the box styling; only the focus treatment stays scoped
   here — restated on `:focus-visible`, not plain `:focus`, and restating the base rule's own gold
   outline rather than silencing it, since this selector's (0,2,0) specificity would otherwise beat
   `select:focus-visible`'s (0,1,1) and drop the ring entirely (ticket 039 review NIT 6). */
.day-picker__select:focus-visible {
  border-color: var(--border-gold);
  outline: 2px solid var(--gold-2);
  outline-offset: 2px;
}

.section__note {
  margin: 0 0 0.9rem;
  color: var(--ink-body);
  font-size: var(--fs-xs);
  line-height: 1.45;
}

.card {
  background: linear-gradient(135deg, var(--card-from), var(--card-to));
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.1rem 1.2rem;
}

/* A flat, opaque panel (ticket 973: no glass in the content layer, design note §5). css/depth.css
   gives every content panel the same fill. */
.glass {
  background: var(--panel);
  border: 1px solid var(--border-strong);
}

/* ---- Stat tiles ------------------------------------------------------------------------- */

.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 0.9rem;
}

.stat-tile {
  background: linear-gradient(135deg, var(--card-from), var(--card-to));
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem 1.1rem;
  position: relative;
  overflow: hidden;
}

.stat-tile::after {
  /* a faint gold rule at the top edge, the sibling system's neon-hint device */
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: linear-gradient(90deg, transparent, var(--gold-soft), transparent);
}

.stat-tile__label {
  font: var(--type-footnote);
  color: var(--ink-muted);
}

.stat-tile__value {
  font-size: var(--fs-xl);
  font-weight: 600;
  letter-spacing: -0.03em;
  color: var(--ink);
  margin-top: 0.25rem;
  font-variant-numeric: tabular-nums;
}

/* A number is not interactive, so it takes no accent (design note rule 3, ticket 972); the
   modifier stays on the tiles that carry it and reads in the value's own ink. */
.stat-tile__value.is-accent {
  color: var(--ink);
}

/* A text-valued tile (ticket 015) — a timestamp or a duration reads wider than a short number at
   --fs-xl and wraps onto two lines, unevening the row's tile heights; drop to --fs-lg, keep it on
   one line, and set it in the mono face used elsewhere for exact/technical values (e.g. hashes,
   method tokens). */
.stat-tile__value--text {
  font-size: var(--fs-lg);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-family: var(--font-mono);
}

.stat-tile__sub {
  font-size: var(--fs-2xs);
  color: var(--ink-dim);
  margin-top: 0.2rem;
}

/* ---- Charts ----------------------------------------------------------------------------- */

/* minmax(0, ...) (not bare 1.4fr/1fr) — same convention .warroom-grid already uses — caps each
   track at its fr-share regardless of a descendant's intrinsic width. Without it, .chart svg's
   own min-width below (nested two levels down, inside a section() wrapper this file's chart-grid
   items actually are) would inflate the *track* itself rather than just needing to scroll inside
   its own card, pushing .chart-grid — and the page — wider than the viewport (ticket 032, a
   doublecheck-adjacent regression this fix's own first pass introduced and this pass corrects). */
.chart-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
  gap: 0.9rem;
}

.chart-grid--even {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}

/* .chart-grid's minmax(0, ...) tracks above already stop .chart svg's min-width from inflating
   the grid track itself; this is what actually scrolls that excess width within the card once the
   track is narrower than it — the same convention .table-scroll uses for wide tables. */
.chart {
  overflow-x: auto;
}

.chart svg {
  display: block;
  width: 100%;
  /* Matches the SVG's own 640-unit viewBox width (hourlyChart() in charts.js). Text sized inside
     an SVG — by attribute or by CSS — is scaled by the same viewBox-to-rendered-box transform as
     everything else in the document, so .chart-axis-label's 13px floor below only actually
     renders at 13px or more once the SVG itself is at least this wide; a narrower card scrolls
     (via the overflow-x above) rather than silently shrinking the axis labels under the floor
     (ticket 032 — the original 10px labels, and this fix's first pass at 13px, both shrank well
     under it once .chart-grid collapsed to one narrow column below the 860px breakpoint). */
  min-width: 40rem;
  height: auto;
  overflow: visible;
}

.chart-axis-label {
  fill: var(--ink-muted);
  font-size: 13px; /* --fs-2xs's px value, pinned literally rather than via var()/rem — see the
                       min-width comment above for why this SVG text needs a real floor guarantee
                       rather than a value that would itself be subject to the same scaling. */
  font-family: var(--font-mono);
}

.bar-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.55rem;
  margin-top: 0.3rem;
}

.bar-item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.6rem 0.8rem;
}

.bar-item__label {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--ink-body);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bar-item__count {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
}

/* Bar widths are drawn as SVG <rect> geometry (not inline CSS `style` attributes carrying data),
   so they render under the strict `style-src 'self'` CSP with no inline-style allowance. */
.bar-item__bar {
  grid-column: 1 / -1;
  display: block;
  width: 100%;
  height: 8px;
  border-radius: 9999px;
  overflow: hidden;
}

/* ---- Status-mix meter ------------------------------------------------------------------- */

.status-meter-svg {
  display: block;
  width: 100%;
  height: 14px;
  border-radius: 9999px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.04);
}

.status-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.8rem;
}

/* The legend pill ("2xx Status Service response · 6,179 · 58%") is 360 px nowrap inside a 321 px
   legend at 390 px, which widens the phone's layout viewport. Let its text wrap (ticket 950). */
.status-legend .pill {
  white-space: normal;
  max-width: 100%;
  min-width: 0;
}

/* ---- Pills ------------------------------------------------------------------------------ */

.pill {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  border-radius: 9999px;
  padding: 0.25rem 0.75rem;
  font-size: var(--fs-2xs);
  font-family: var(--font-mono);
  border: 1px solid var(--border);
  color: var(--ink-dim);
  white-space: nowrap;
}

.pill__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}

.status-2xx {
  color: var(--st-2xx);
  border-color: rgba(63, 185, 80, 0.35);
}
.status-3xx {
  color: var(--st-3xx);
  border-color: rgba(88, 166, 255, 0.35);
}
.status-4xx {
  color: var(--st-4xx);
  border-color: rgba(227, 179, 65, 0.38);
}
.status-5xx {
  color: var(--st-5xx);
  border-color: rgba(248, 81, 73, 0.4);
}
.status-unknown {
  color: var(--st-unknown);
  border-color: var(--border);
}

/* Captured HTTP response-status-class pills only (statusPill()/statusBreakdown() in dom.js /
   charts.js) — see the --rstatus-* comment above for why this is a separate palette from
   .status-2xx etc. above. */
.rstatus-2xx {
  color: var(--rstatus-2xx);
  border-color: rgba(124, 147, 168, 0.35);
}
.rstatus-3xx {
  color: var(--rstatus-3xx);
  border-color: rgba(88, 166, 255, 0.35);
}
.rstatus-4xx {
  color: var(--rstatus-4xx);
  border-color: rgba(227, 179, 65, 0.38);
}
.rstatus-5xx {
  color: var(--rstatus-5xx);
  border-color: rgba(248, 81, 73, 0.4);
}
.rstatus-unknown {
  color: var(--rstatus-unknown);
  border-color: var(--border);
}

/* Per-verb method styling (ticket 20260831235605) — Live's method column and War room's ticker,
   via dom.js's methodToken(). A request method is scannability, not a captured outcome, so it
   gets its own small tier rather than borrowing .rstatus-*'s palette above. */
.method-tok {
  font-family: var(--font-mono);
  font-weight: 600;
  letter-spacing: 0.02em;
}
.method-tok--read {
  color: var(--ink-dim);
}
.method-tok--write {
  color: var(--gold-2);
}
/* The captured method string itself — critical evidence (an unusual/malformed verb is exactly
   the kind of anomaly worth noticing, so it must never render dimmer than .method-tok--read's
   routine GET/HEAD) — never the lowest-contrast token (ticket 032, doublecheck finding). */
.method-tok--other {
  color: var(--ink-dim);
}

/* ---- Data tables ------------------------------------------------------------------------ */

.table-scroll {
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
}

table.data {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  min-width: 720px;
}

/* The one way to ask for the mono face (ticket 972): a table cell, a value or a label that holds an
   address, an id or code. Everything else in the console reads in the UI font. */
.mono {
  font-family: var(--font-mono);
}

table.data thead th {
  text-align: left;
  padding: 0.6rem 0.8rem;
  color: var(--ink-muted);
  font: var(--type-footnote);
  font-weight: 500;
  border-bottom: 1px solid var(--border-strong);
  position: sticky;
  top: 0;
  background: var(--panel-deep);
  white-space: nowrap;
}

/* Sortable column-header buttons (investigations.js's queueTable(), rendered when a caller passes
   `onSort`; Activity's preview passes none and gets plain-text headers) — generalized from a
   selector scoped to `.investigations-table .sort-btn` (ticket 020) to this unscoped `.sort-btn`
   so any future `table.data` outside `.investigations-table` gets the same reset too, not only the
   tables that render it today. */
.sort-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  cursor: pointer;
}

.sort-btn:hover,
.sort-btn:focus-visible {
  color: var(--ink);
}

.sort-btn.is-active {
  color: var(--gold);
}

/* The active column's direction arrow (ticket 510), a span after the label. */
.sort-btn__arrow {
  font-size: 0.85em;
}

table.data tbody tr {
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  transition: background 0.12s ease;
}

table.data tbody tr:hover,
table.data tbody tr:focus-visible {
  background: var(--glass);
  outline: none;
}

table.data tbody tr:focus-visible {
  box-shadow: inset 3px 0 0 var(--gold);
}

table.data td {
  padding: 0.5rem 0.8rem;
  color: var(--ink-body);
  vertical-align: top;
  white-space: nowrap;
}

.col-path {
  max-width: 34ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The desktop-table sibling of .feed-card__ua — same captured, attacker-controlled User-Agent,
   so the same critical-evidence promotion applies (ticket 032, doublecheck finding). */
.col-ua {
  max-width: 28ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink-dim, var(--ink-body));
}

.row-new {
  animation: row-flash 1.6s ease-out;
}

@keyframes row-flash {
  0% {
    background: color-mix(in srgb, var(--gold) 24%, transparent);
  }
  100% {
    background: transparent;
  }
}

/* ---- Live feed controls ----------------------------------------------------------------- */
/* ticket 20260831230339: three always-present rows (primary controls / active-filter chips /
   advanced-filter disclosure) plus a separate exports row, in place of the old single wrapped row
   — a deliberate desktop layout adjustment (still compact, just three short rows instead of one
   long-wrapping one) made so the mobile progressive-disclosure story below has a real seam to hang
   off, rather than fighting one flat row's source order at a breakpoint. */

.feed-controls-wrap {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin-bottom: 0.9rem;
}

.feed-primary-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
}

/* The request filters, inside the page's one Filters dialog (js/views/live.js; ticket 979). */
.feed-filters-adv {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
}

.feed-filters-adv input {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--ink);
  background: var(--panel-deep);
  border: 1px solid var(--border-strong);
  border-radius: 0.6rem;
  padding: 0.4rem 0.6rem;
}

.feed-filters-adv input::placeholder {
  color: var(--ink-muted);
}

.feed-filters-adv input:focus {
  border-color: var(--border-gold);
  outline: none;
}

.feed-exports {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--ink);
  background: var(--glass);
  border: 1px solid var(--border-strong);
  border-radius: 0.6rem;
  padding: 0.42rem 0.85rem;
  cursor: pointer;
  transition:
    transform 0.12s ease,
    box-shadow 0.12s ease,
    background 0.12s ease;
}

/* Same UA-vs-author-display gap as .nav-link[hidden] below — a role-hidden export button
   (ticket 026 — a viewer cannot export) would otherwise stay visible. */
.btn[hidden] {
  display: none;
}

.btn:hover {
  background: rgba(255, 255, 255, 0.08);
}

/* The button kinds (ticket 972, design note §4): `.btn` is the bordered default; `.btn--prominent`
   fills with the accent and marks the one primary action of a view; `.btn--plain` has no fill and no
   border; `.btn--destructive` is bordered with the error color and is never filled. All four share
   .btn's padding and hit area. */
.btn--prominent {
  color: #1a1500;
  background: linear-gradient(135deg, var(--gold), var(--gold-2));
  border-color: transparent;
  font-weight: 600;
}

/* `.btn:hover` (above) would turn the fill translucent; the prominent kind keeps its own. */
.btn--prominent:hover {
  background: linear-gradient(135deg, var(--gold), var(--gold-2));
  box-shadow: var(--shadow-gold);
  transform: translateY(-1px);
}

.btn--plain {
  color: var(--gold);
  background: transparent;
  border-color: transparent;
}

.btn--plain:hover {
  background: var(--gold-soft);
}

.btn--destructive {
  color: var(--st-5xx);
  background: transparent;
}

.btn--destructive:hover {
  background: rgba(248, 81, 73, 0.12);
}

/* The segmented control (js/segmented.js, ticket 972): one row of exclusive choices. The checked item
   is a raised neutral plate with `--ink` text, as Apple's segmented control draws it, not a gold fill;
   the focus ring stays the accent. At 760px and narrower every item is at least 44px tall. */
.segmented {
  display: inline-flex;
  align-items: stretch;
  gap: var(--space-1);
  padding: var(--space-1);
  border: 1px solid var(--border);
  border-radius: 0.7rem;
  background: var(--panel-deep);
}

.segmented__item {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-1) var(--space-3);
  border: 0;
  border-radius: 0.5rem;
  background: transparent;
  color: var(--ink-dim);
  font: var(--type-footnote);
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
}

.segmented__item:hover {
  color: var(--ink);
}

.segmented__item[aria-checked="true"],
.segmented__item[aria-selected="true"] {
  background: rgba(255, 255, 255, 0.14);
  color: var(--ink);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}

.segmented__item:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 1px;
}

@media (max-width: 760px) {
  .segmented__item {
    min-height: 44px;
    padding-inline: var(--space-4);
  }
}

/* The time control (js/time_control.js, ticket 974): the segmented control, and beside it, while Custom is
   checked, the two date inputs and Apply. It sits in the page head's time slot on every windowed page. */
.time-control {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  max-width: 100%;
}

.time-control__custom,
.time-control .day-picker__range {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.time-control__custom[hidden] {
  display: none;
}

/* Five short segments fit a phone's width only when they share it. */
@media (max-width: 760px) {
  .time-control .segmented {
    max-width: 100%;
  }

  .time-control .segmented__item {
    flex: 1 1 auto;
    padding-inline: var(--space-2);
  }
}

.btn__icon {
  width: 15px;
  height: 15px;
}

/* Base native <select> styling (ticket 016) — resets the UA control face/border so every select
   matches this console's dark theme, with an inline SVG chevron standing in for the browser's own
   dropdown arrow. `--panel`/`--ink-muted` can't be read inside a `data:` URI (it's plain text, not
   evaluated CSS), so the chevron's fill is `--ink-muted`'s hex (#8f8f97) hardcoded — keep the two
   in sync if that token's value ever changes. `img-src 'self' data:` in the console's CSP
   (console/assets.py's csp()) allows this. */
select {
  /* `html`'s `color-scheme: dark` above makes Chrome auto-darken a native select's own
     background/text underneath whatever color we specify — `color-scheme: light` here opts this
     fully custom-painted control back out of that adjustment so `background-color` below renders
     as literally `--panel`, not a UA-mixed tint of it. */
  color-scheme: light;
  appearance: none;
  background-color: var(--panel);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath fill='%238f8f97' d='M1 1l5 5 5-5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.6rem center;
  background-size: 0.7rem 0.7rem;
  border: 1px solid var(--border-strong);
  /* 0.6rem, not `--radius` (1rem) — matches `.btn` above so a select sitting beside a button in
     the day-picker or preset rows reads as the same control family instead of a pill (ticket 039
     review NIT 3). */
  border-radius: 0.6rem;
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-xs);
  padding: 0.35rem 1.8rem 0.35rem 0.65rem;
  min-height: 2rem;
}

/* The open popup is native UA chrome, not this rule's `background-color`/`color` above, on most
   browsers — style it explicitly so it doesn't fall back to a light system popup against this
   dark theme (ticket 039 review NIT 1). */
select option {
  background-color: var(--panel);
  color: var(--ink);
}

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

select:disabled {
  opacity: 0.5;
}

/* Mobile-only affordance (ticket 20260831230339) — hidden by default, so a desktop viewport never
   sees it; the `max-width: 760px` block further down turns it back on. */
.feed-active-filters {
  display: none;
}

.feed-active-filter {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  /* Matches .overview-threat-chip's own padding (the closest existing precedent — a small
     removable/interactive pill, not a full-width row) rather than the tighter .pill default,
     per the ticket's "touch targets ... at least the existing console standard" requirement — a
     /doublecheck codex finding against the original, shorter padding here. */
  padding: 0.4rem 0.85rem;
  border-radius: 9999px;
  border: 1px solid var(--border-strong);
  background: var(--glass);
  color: var(--ink-dim);
  cursor: pointer;
}

.feed-active-filter:hover {
  border-color: var(--border-gold);
  color: var(--ink);
}

/* A small chip follows .overview-threat-chip's own convention — keep the global gold outline —
   rather than the full-width-row convention (.session-row/.feed-card etc.) of substituting a
   border-color change for it; a /doublecheck codex finding that the original border-color-only
   treatment here was weaker than this console's own standard for this specific kind of control. */
.feed-active-filter:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
  color: var(--ink);
}

.feed-active-filter__remove {
  color: var(--ink-muted);
}

.feed-count {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--ink-dim);
}

.feed-count strong {
  color: var(--gold);
  font-variant-numeric: tabular-nums;
}

/* ---- Live feed: mobile event cards ------------------------------------------------------- */
/* ticket 20260831230339 — the purpose-built card/list layout that replaces the eight-column table
   below the `max-width: 760px` breakpoint (see that block, further down, for the actual toggle).
   Hidden by default so a desktop viewport — which keeps the semantic `<table>` — never renders it. */

.feed-cards {
  display: none;
  flex-direction: column;
  gap: 0.6rem;
}

.feed-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.75rem 2.2rem 0.75rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: linear-gradient(135deg, var(--card-from), var(--card-to));
  cursor: pointer;
}

.feed-card:hover,
.feed-card:focus-visible {
  border-color: var(--border-gold);
  outline: none;
}

.feed-card.is-selected {
  box-shadow: inset 3px 0 0 var(--gold);
}

.feed-card--hackback {
  border-color: rgba(227, 179, 65, 0.5);
  box-shadow: 0 0 0 1px rgba(227, 179, 65, 0.18);
}

/* row-flash (see `.row-new` above) animates the `background` *shorthand*, which would null out
   this card's own gradient for the flash's duration and then snap it back — a visible glitch a
   `<tr>` never had (it has no background image to lose). box-shadow flashes instead, leaving the
   gradient untouched throughout. */
.feed-card.row-new {
  animation: feed-card-flash 1.6s ease-out;
}

@keyframes feed-card-flash {
  0% {
    box-shadow: inset 0 0 0 999px color-mix(in srgb, var(--gold) 24%, transparent);
  }
  100% {
    box-shadow: inset 0 0 0 999px transparent;
  }
}

.feed-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
}

/* A captured exchange timestamp — critical evidence, not chrome, so it never uses the console's
   lowest-contrast token (ticket 032). */
.feed-card__time {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--ink-dim);
}

.feed-card__req {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  min-width: 0;
}

.feed-card__req .method-tok {
  flex: none;
}

.feed-card__path {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.feed-card__meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.7rem;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--ink-muted);
}

.feed-card__ip {
  color: var(--ink-dim);
}

/* The captured, attacker-controlled User-Agent — critical evidence, not chrome (ticket 032,
   doublecheck finding). */
.feed-card__ua {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--ink-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.feed-card__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

/* aria-hidden in live.js — a purely decorative disclosure arrow, not informational text, so it
   stays off the --fs-* scale (see the :root type-scale comment's exceptions). */
.feed-card__chevron {
  position: absolute;
  right: 0.8rem;
  top: 50%;
  transform: translateY(-50%);
  color: var(--ink-muted);
  font-size: 1.1rem;
}

/* ---- Services --------------------------------------------------------------------------- */

.service-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 0.9rem;
}

.service-card {
  background: linear-gradient(135deg, var(--card-from), var(--card-to));
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.1rem 1.2rem;
}

.service-card__id {
  font-family: var(--font-mono);
  color: var(--gold);
  font-size: var(--fs-base);
}

.service-card__desc {
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  margin: 0.3rem 0 0.8rem;
}

.service-card__meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.service-card__share {
  margin-top: 0.9rem;
}

/* ---- ATT&CK matrix ------------------------------------------------------------------------ */

.attack-matrix {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
  gap: 0.85rem;
  margin-top: 1rem;
  align-items: start;
}

.attack-tactic {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  min-width: 0;
}

.attack-tactic__head {
  padding: 0.55rem 0.65rem;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: color-mix(in srgb, var(--card-from) 80%, transparent);
}

.attack-tactic__id {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--gold);
}

.attack-tactic__name {
  font-size: var(--fs-sm);
  color: var(--ink);
  font-weight: 600;
}

.attack-tactic__hint {
  margin-top: 0.15rem;
}

.attack-tactic__cells {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.attack-cell {
  display: block;
  text-decoration: none;
  color: inherit;
  background: linear-gradient(135deg, var(--card-from), var(--card-to));
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.75rem 0.85rem;
}

.attack-cell:focus-visible,
.attack-cell.is-selected {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

.attack-cell--hit {
  border-color: color-mix(in srgb, var(--gold) 55%, var(--border));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--gold) 25%, transparent);
}

.attack-cell--untouched {
  opacity: 0.62;
}

.attack-cell__id {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--gold);
}

.attack-cell__name {
  font-size: var(--fs-xs);
  color: var(--ink);
  margin-top: 0.15rem;
}

.attack-cell__meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.55rem;
  margin-top: 0.45rem;
  font-size: var(--fs-2xs);
  color: var(--ink-dim);
}

.attack-cell__when {
  margin-top: 0.25rem;
  font-size: var(--fs-2xs);
}

/* ---- Deployment readiness panel (ticket 022, js/readiness.js) --------------------------- */
/* The compact readiness summary of the Overview's Environment lens, and the one-line "no Status
   Service is listening" gap Effectiveness and ATT&CK show above their content — deliberately styled like
   .capture-context__rows above (same pill palette, same card chrome) so the "is this actually deployed
   and healthy" story reads consistently everywhere it appears, not as a bespoke widget. */
.readiness-panel {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin: 0.9rem 0 0;
  padding: 0.75rem 1rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.readiness-panel__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  font-size: var(--fs-xs);
}

/* A long deployment/service-count pill (e.g. "Deployment: Live — attached scenario") is a flex
   item whose default min-width is its nowrap content width, so the row's own flex-wrap cannot
   help it — it overflows the row, and at narrow viewports the page, instead of shrinking. Scoped
   to this panel only: every other .pill in the app stays a single-line badge. The row's other
   nowrap flex child, .readiness-panel__link (below), gets the identical treatment for the same
   reason — a /doublecheck finding that a longer future link label would hit the same overflow. */
.readiness-panel__row .pill,
.readiness-panel__row .readiness-panel__link {
  white-space: normal;
  min-width: 0;
}

/* The one row that renders only for a real, evidence-based gap (deployment proven live, zero
   services currently listening) — a subtle caution rail so it reads as distinct from the neutral
   deployment-state row above it, without shouting "alert". */
.readiness-panel__row--gap {
  padding-left: 0.6rem;
  border-left: 2px solid rgba(227, 179, 65, 0.5);
}

.readiness-panel__link {
  color: var(--gold-2);
  text-decoration: none;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  white-space: nowrap;
}

.readiness-panel__link:hover {
  color: var(--gold);
}

/* ---- Deception effectiveness ------------------------------------------------------------ */

.effectiveness-surfaces {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.effectiveness-surfaces__line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  font: var(--type-body);
  color: var(--ink);
}

.effectiveness-surfaces__details > summary {
  cursor: pointer;
  user-select: none;
  width: fit-content;
  font: var(--type-footnote);
  color: var(--ink-dim);
}

.effectiveness-surfaces__list {
  display: flex;
  flex-direction: column;
  margin-top: var(--space-2);
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* A row that opens something is one link (design note rule 8). */
.effectiveness-surface {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-4);
  padding: var(--space-3) var(--space-4);
  color: var(--ink);
  text-decoration: none;
  font: var(--type-body);
}

.effectiveness-surface + .effectiveness-surface {
  border-top: 1px solid var(--border);
}

.effectiveness-surface:hover,
.effectiveness-surface:focus-visible {
  background: rgba(255, 255, 255, 0.04);
}

.effectiveness-surface__label {
  flex: 1 1 14rem;
}

.effectiveness-surface__count {
  color: var(--ink-muted);
  font: var(--type-footnote);
  font-variant-numeric: tabular-nums;
}

/* ---- Credential captures ---------------------------------------------------------------- */

.cred-list {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  margin-top: 0.55rem;
}

.cred-row {
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  padding: 0.7rem 0.85rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md, 0.7rem);
  background: linear-gradient(135deg, var(--card-from), var(--card-to));
  cursor: default;
}

.cred-row.is-selected,
.cred-row:focus {
  border-color: var(--border-gold);
  outline: none;
  box-shadow: 0 0 0 1px rgba(212, 175, 55, 0.25);
}

.cred-row__meta,
.cred-row__value {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45rem 0.7rem;
  font-size: var(--fs-2xs);
  color: var(--ink-dim);
}

.cred-row__path,
.cred-value {
  font-family: var(--font-mono);
  color: var(--ink);
}

.cred-value {
  padding: 0.15rem 0.4rem;
  border-radius: 0.35rem;
  background: rgba(0, 0, 0, 0.35);
  max-width: 100%;
  overflow-wrap: anywhere;
}

.cred-label {
  color: var(--ink-muted);
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
}

.cred-type--basic { color: var(--gold); }
.cred-type--bearer { color: #9ecbff; }
.cred-type--form { color: #c5a3ff; }
.cred-type--api-key { color: #ffb86c; }

.btn--small {
  font-size: var(--fs-2xs);
  padding: 0.28rem 0.55rem;
  border-radius: 0.45rem;
}

/* ---- Admin user management (ticket 027) ------------------------------------------------- */

.users-create {
  margin-bottom: 1.2rem;
}

.users-create-form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.75rem;
  margin-top: 0.6rem;
}

.users-create-form .login-label {
  min-width: 10rem;
}

.users-create-form input {
  font-family: var(--font-ui);
  font-size: var(--fs-sm);
  color: var(--ink);
  background: var(--panel-deep);
  border: 1px solid var(--border-strong);
  border-radius: 0.6rem;
  padding: 0.5rem 0.7rem;
}

table.users-table tbody tr {
  cursor: default;
}

/* Dense-row modifier (ticket 039 review NIT 4) — same rationale as `.day-picker__select` above:
   the per-row role select in this tight admin table keeps the mono/`--fs-2xs` type and tighter
   padding it had before the base `select` rule (ticket 016) took over box styling. */
table.users-table select {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  padding: 0.3rem 1.5rem 0.3rem 0.45rem;
  min-height: 1.8rem;
}

.users-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  white-space: nowrap;
}

/* The role cell: the select, and Save / Cancel while the choice differs from the saved role (ticket 986). */
.users-role {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  white-space: nowrap;
}

/* The Add user and Create token sheets: a modal <dialog> holding one form (ticket 986). */
.users-sheet {
  width: min(28rem, calc(100vw - 2rem));
  padding: 1.2rem;
  color: var(--ink);
  background: var(--panel-deep);
  border: 1px solid var(--border-strong);
  border-radius: 0.9rem;
}

.users-sheet::backdrop {
  background: rgb(0 0 0 / 0.55);
}

.users-sheet .users-create-form {
  flex-direction: column;
  align-items: stretch;
  margin-top: 0;
}

.users-sheet__buttons {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
}

/* The API tokens table fits its container (ticket 691, mirroring ticket 659's Breadcrumbs fix).
   Every `table.data td` is `white-space: nowrap`, so each cell's narrowest width was its whole
   text: one revoked fleet token made this table 1609 px wide in its 1230 px container at 1440 px,
   with Expires cut mid-cell and Status/Actions off-screen until the table was scrolled sideways
   (the three full date stamps alone took 245 px each). The cells wrap here instead — a date then
   breaks after its comma — and a long unbroken token name breaks where it must. The selector
   carries the `data` class on purpose: `table.data td` is (0,1,2) and a bare `.users-tokens td`
   (0,1,1) would lose to it. Only the tokens table carries `users-tokens`; the operator accounts
   table keeps the nowrap default. Under about 720 px the table keeps its min-width and scrolls
   sideways inside .table-scroll, as every table.data does. */
table.data.users-tokens td {
  white-space: normal;
}

table.data.users-tokens td:first-child {
  overflow-wrap: anywhere;
}

/* The in-row revoke confirm ("Revoke this token?" plus two buttons) is the widest Actions state —
   366 px of nowrap flex before this rule — so it wraps inside its cell instead of widening the
   fitted table back past its container. */
table.data.users-tokens .users-actions {
  flex-wrap: wrap;
}

/* Review round one, milestone 6 NIT: an expired token's "expires" cell reads amber, same as
   this console's other 4xx/expiring-soon warnings, so it visually stands apart from a live one. */
.users-token-expired {
  color: var(--st-4xx);
}

.users-pager {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.7rem;
}

.users-generated {
  margin: 0 0 1.2rem;
  padding: 0.75rem 0.95rem;
  border: 1px solid rgba(63, 185, 80, 0.4);
  border-radius: var(--radius-md, 0.7rem);
  background: rgba(63, 185, 80, 0.1);
  color: var(--ink);
}

.users-generated__label {
  margin: 0 0 0.4rem;
  font-size: var(--fs-xs);
}

.users-generated__row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
}

.users-generated__value {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  padding: 0.3rem 0.55rem;
  background: var(--panel-deep);
  border: 1px solid var(--border-strong);
  border-radius: 0.5rem;
}

.users-generated__hint {
  margin: 0.5rem 0 0;
  font-size: var(--fs-2xs);
  color: var(--ink-muted);
}

/* ---- Investigation triage panel (tickets 013/012), next to the investigation header (see
   css/investigations.css's .investigation-header rules) — a closed-by-default <details> whose
   <summary> is the one-line status, opening to reveal the same .login-label/.login-input
   select/input/textarea as the admin user-management form above. --------------------------- */

.triage-panel {
  margin-bottom: 1.2rem;
}

.triage-panel__summary {
  cursor: pointer;
  font-size: var(--fs-sm);
  color: var(--ink-muted);
}

.triage-panel__form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.75rem;
  margin-top: 0.6rem;
}

.triage-panel__field {
  min-width: 10rem;
}

.triage-panel__note {
  min-width: 16rem;
  resize: vertical;
}

.triage-panel__status {
  margin: 0;
}

/* ---- Admin audit trail (ticket 029) ------------------------------------------------------ */

.audit-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
}

.audit-filters input {
  font-family: var(--font-ui);
  font-size: var(--fs-sm);
  color: var(--ink);
  background: var(--panel-deep);
  border: 1px solid var(--border-strong);
  border-radius: 0.6rem;
  padding: 0.5rem 0.7rem;
  min-width: 12rem;
}

.audit-pager {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.7rem;
}

.audit-filters__switch {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--fs-sm);
}

/* `.audit-filters input` sizes the text boxes; the switch's checkbox keeps its own size. */
.audit-filters__switch input {
  min-width: 0;
  padding: 0;
}

.audit-toggle {
  margin-left: 0.4rem;
}

/* A row's detail, opened under the row: label / value lines, never JSON (ticket 986). */
.audit-detail-list {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.25rem 1rem;
  margin: 0;
  padding: 0.4rem 0;
}

.audit-detail-list dt {
  color: var(--ink-muted);
}

.audit-detail-list dd {
  margin: 0;
  overflow-wrap: anywhere;
}

table.data .audit-detail-row td {
  white-space: normal;
}

/* ---- Live scenario control (ticket 001b) ------------------------------------------------ */

.scenario-lede {
  margin: 0 0 0.75rem;
  color: var(--ink-body);
  font-size: var(--fs-xs);
  line-height: 1.45;
}

.scenario-watcher {
  margin-top: 0.9rem;
}

.scenario-watcher__title {
  margin: 0 0 0.5rem;
  font-size: var(--fs-sm);
  color: var(--gold-2);
}

.scenario-ref-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
}

.scenario-ref-row select {
  min-width: 18rem;
  max-width: 100%;
}

/* Scenario gallery (ticket 864): one card per bundled Scenario, Recommended first. */
.scenario-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
  gap: 0.9rem;
  margin-bottom: 0.9rem;
}

.scenario-gallery:empty {
  display: none;
}

.scenario-card {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  padding: 1rem;
  border: 1px solid var(--border);
  border-radius: 0.7rem;
  background: var(--panel);
}

.scenario-card.is-selected {
  border-color: var(--gold);
  background: var(--gold-soft);
}

.scenario-card__badges {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.scenario-card__badge {
  padding: 0.1rem 0.5rem;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  font-family: var(--font-ui);
  font-size: var(--fs-2xs);
  color: var(--ink-body);
}

.scenario-card__badge--recommended {
  border-color: var(--gold-2);
  color: var(--gold);
}

.scenario-card__title {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--fs-md);
}

.scenario-card__pick {
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.scenario-card__pick:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

.scenario-card__summary,
.scenario-card__services {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--fs-sm);
  color: var(--ink-dim);
}

.scenario-card__summary {
  flex: 1;
}

.scenario-card__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.scenario-card__tag {
  padding: 0.05rem 0.45rem;
  border-radius: 0.35rem;
  background: var(--panel-deep);
  font-family: var(--font-ui);
  font-size: var(--fs-2xs);
  color: var(--ink-dim);
}

.scenario-card__actions {
  display: flex;
  gap: 0.5rem;
}

.scenario-gallery__selection {
  margin: 0;
}

.scenario-inline-host:empty {
  display: none;
}

.scenario-editor {
  display: block;
  width: 100%;
  min-height: 22rem;
  margin-top: 0.75rem;
  resize: vertical;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  line-height: 1.5;
  color: var(--ink);
  background: var(--panel-deep);
  border: 1px solid var(--border-strong);
  border-radius: 0.6rem;
  padding: 0.75rem 0.9rem;
}

.scenario-editor:focus {
  outline: none;
  border-color: var(--border-gold);
  box-shadow: 0 0 0 2px var(--gold-soft);
}

/* Read-only raw-YAML preview of a not-yet-applied bundled ref (ticket 20260911194020) — sized/
   scrollable like .scenario-editor above, but never focusable/editable. */
.scenario-preview {
  display: block;
  width: 100%;
  max-height: 22rem;
  margin-top: 0.75rem;
  overflow: auto;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  line-height: 1.5;
  color: var(--ink);
  background: var(--panel-deep);
  border: 1px solid var(--border-strong);
  border-radius: 0.6rem;
  padding: 0.75rem 0.9rem;
}

.scenario-editor__caption {
  margin: 0.4rem 0 0;
  font-size: var(--fs-2xs);
  color: var(--ink-muted);
}

.scenario-editor__caption.is-dirty {
  color: var(--st-4xx);
}

.scenario-editor-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: 0.75rem;
}

.scenario-alert {
  margin-top: 0.75rem;
  padding: 0.6rem 0.85rem;
  border-radius: 0.6rem;
  font-size: var(--fs-xs);
  line-height: 1.45;
}

.scenario-alert--ok {
  color: var(--health-ok);
  background: rgba(63, 185, 80, 0.1);
  border: 1px solid rgba(63, 185, 80, 0.35);
}

.scenario-alert--warn {
  color: var(--st-4xx);
  background: rgba(227, 179, 65, 0.1);
  border: 1px solid rgba(227, 179, 65, 0.35);
}

.scenario-alert--error {
  color: #fca5a5;
  background: rgba(248, 81, 73, 0.12);
  border: 1px solid rgba(248, 81, 73, 0.35);
}

.scenario-confirm {
  margin-top: 1.2rem;
  max-width: 34rem;
}

.scenario-confirm__title {
  margin: 0 0 0.6rem;
  font-size: var(--fs-md);
  color: var(--ink);
}

.scenario-confirm__hint {
  margin: 0.5rem 0 0;
  font-size: var(--fs-2xs);
  color: var(--ink-muted);
}

.scenario-confirm__actions {
  display: flex;
  gap: 0.6rem;
  margin-top: 1rem;
}

/* ---- Detail overlay --------------------------------------------------------------------- */

.detail-overlay {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  justify-content: flex-end;
  background: rgba(0, 0, 0, 0.62);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  /* Toggling the `hidden` attribute flips this element's `display` between `none` and `flex` (the
     rule right below), which resets a CSS animation and replays it from the start every time — the
     same mechanism `.detail-panel`'s own slide-in already relies on, so the backdrop now fades in
     alongside the panel with no extra JS (ticket 033: "menu/detail/sheet open-close motion"). */
  animation: overlay-in var(--dur-base) var(--ease-standard);
}

/* The author `display:flex` above beats the UA `[hidden] { display:none }`, so the overlay would
   sit visible over the whole page on load and after close. This scoped rule restores hiding. */
.detail-overlay[hidden] {
  display: none;
}

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

.detail-panel {
  width: min(760px, 100%);
  height: 100%;
  overflow-y: auto;
  background: linear-gradient(160deg, #141414, var(--panel-deep));
  border-left: 1px solid var(--border-gold);
  box-shadow: -30px 0 60px -30px rgba(0, 0, 0, 0.9);
  padding: 1.4rem 1.5rem 3rem;
  animation: slide-in var(--dur-base) ease-out;
}

@keyframes slide-in {
  from {
    transform: translateX(24px);
    opacity: 0.4;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

/* Sticky close/title region (ticket 20260831230339) — the drawer is a full-height scroll area
   (`.detail-panel`'s own `overflow-y: auto`) holding several forensic <details> blocks that can run
   long, so without this the title and close affordance scroll away entirely, same idiom as
   `table.data thead th`'s own `position: sticky; top: 0;` above. The negative margin cancels the
   ancestor's own top/side padding for this element only, and the matching padding puts it straight
   back — so the unscrolled position is pixel-identical to before, while the scrolled (stuck)
   position sits flush with the panel's edges instead of leaving a gap. Applies at every width, not
   just mobile — a full-width drawer is already what happens under `.detail-panel`'s own
   `width: min(760px, 100%)` on any viewport narrower than 760px. */
.detail-panel__head {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  /* Bottom stays margin-only (unchanged from before this ticket, so it still collapses with
     .detail-block's own margin-top exactly as it always did) — only top/sides get the negative-
     margin-then-repadding trick, since only those two needed cancelling to sit flush once stuck. */
  margin: -1.4rem -1.5rem 1rem;
  padding: 1.4rem 1.5rem 0;
  background: var(--panel-deep);
  border-bottom: 1px solid var(--border-strong);
}

/* /doublecheck (codex) finding: an attacker-controlled title/path with one long unbroken token (no
   spaces) could otherwise force this flex item past the panel's width and push .icon-btn (the close
   button) out past the viewport — min-width: 0 lets it shrink below its content's natural width so
   the children's own overflow handling (below) can actually take effect instead of the flex item's
   default `min-width: auto` winning first. */
.detail-panel__head > div:first-child {
  min-width: 0;
}

.detail-panel__title {
  font-size: var(--fs-md);
}

/* The captured, attacker-controlled request path — critical evidence, not chrome (ticket 032). */
.detail-panel__sub {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--ink-dim);
  margin-top: 0.25rem;
  /* The captured path is attacker-controlled and may be one long unbroken token — wrap it (never
     truncate; the path stays fully readable/copyable) rather than letting it force the flex row
     wider than the panel and push the close button out past the viewport. */
  overflow-wrap: anywhere;
}

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 0.6rem;
  border: 1px solid var(--border-strong);
  background: var(--glass);
  color: var(--ink-dim);
  cursor: pointer;
}

.icon-btn:hover {
  color: var(--ink);
  border-color: var(--border-gold);
}

.icon-btn svg {
  width: 18px;
  height: 18px;
}

.detail-block {
  margin-top: 1.3rem;
}

.detail-block__title {
  font: var(--type-footnote);
  color: var(--gold-2);
  margin-bottom: 0.5rem;
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

/* Recognized-exploit-signature banner (ticket 015) — one bordered row per matched signature, the
   left/box-shadow tint reusing the same .status-2xx/3xx/4xx/5xx/unknown severity tone the pill
   inside it carries (see detail.js's severityTone()), same idiom as .session-row--hackback below. */
.detail-recognized {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.detail-recognized__row {
  padding: 0.7rem 0.9rem;
  border-radius: 0.6rem;
  border: 1px solid var(--border);
  background: var(--panel);
}

.detail-recognized__row.status-5xx {
  border-color: rgba(248, 81, 73, 0.4);
  box-shadow: 0 0 0 1px rgba(248, 81, 73, 0.15);
}
.detail-recognized__row.status-4xx {
  border-color: rgba(227, 179, 65, 0.38);
  box-shadow: 0 0 0 1px rgba(227, 179, 65, 0.12);
}
.detail-recognized__row.status-3xx {
  border-color: rgba(88, 166, 255, 0.35);
  box-shadow: 0 0 0 1px rgba(88, 166, 255, 0.12);
}

.detail-recognized__head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
}

.detail-recognized__label {
  font-size: var(--fs-2xs);
  color: var(--ink-muted);
}

.detail-recognized__desc {
  margin: 0.45rem 0 0;
}

/* Attack story (ticket 018) — the plain-language summary + confidence tier + evidence + next-step
   actions that leads the panel, above the unchanged Summary block and the now-collapsed forensic
   disclosures below it (see .detail-block--forensic further down). */
.attack-story__head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-bottom: 0.6rem;
}

.attack-story__head .detail-block__title {
  margin-bottom: 0;
}

.attack-story__lead {
  font-size: var(--fs-sm);
  color: var(--ink);
  margin: 0 0 0.4rem;
}

.attack-story__why {
  font-style: italic;
}

.attack-story__evidence {
  list-style: none;
  margin: 0.7rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.attack-story__evidence-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  background: var(--panel);
}

/* Ticket 018's explicit "observed fact vs. heuristic interpretation" label — a fixed two-word
   vocabulary, always paired with text (never colour alone), mirroring the shared status-badge
   ".pill" "traffic light" convention's own colour-plus-label rule elsewhere in this file. */
.attack-story__evidence-kind {
  font: var(--type-footnote);
  border-radius: 0.35rem;
  padding: 0.1rem 0.4rem;
  flex-shrink: 0;
}

.attack-story__evidence-kind--observed {
  color: var(--ink);
  background: var(--glass);
  border: 1px solid var(--border-strong);
}

.attack-story__evidence-kind--pattern {
  color: var(--gold);
  background: var(--gold-soft);
  border: 1px solid var(--border-gold);
}

.attack-story__evidence-label {
  color: var(--ink-body);
}

.attack-story__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.9rem;
}

/* Raw-evidence disclosures (ticket 018) — same content/layout as .detail-block, now behind a
   native <details>/<summary> collapsed by default so the Attack Story above is what a reader sees
   first; keyboard/focus/Escape handling is unaffected since <details> needs none of its own. */
.detail-block--forensic > summary {
  cursor: pointer;
  user-select: none;
}

.detail-block--forensic > summary::-webkit-details-marker {
  color: var(--gold-2);
}

.detail-block--forensic[open] > summary {
  margin-bottom: 0.5rem;
}

table.kv {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
}

table.kv td {
  padding: 0.32rem 0.5rem;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
  word-break: break-word;
}

table.kv td.kv-key {
  color: var(--ink-muted);
  width: 34%;
  white-space: nowrap;
}

table.kv td.kv-val {
  color: var(--ink-body);
}

pre.wire {
  margin: 0;
  padding: 0.85rem 1rem;
  background: var(--panel-deep);
  border: 1px solid var(--border);
  border-radius: 0.7rem;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  line-height: 1.5;
  color: var(--ink-body);
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 340px;
  overflow: auto;
}

pre.wire.is-hex {
  white-space: pre;
  word-break: normal;
}

.wire-toolbar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}

.toggle-group {
  display: inline-flex;
  border: 1px solid var(--border-strong);
  border-radius: 0.6rem;
  overflow: hidden;
}

.toggle-group button {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--ink-dim);
  background: transparent;
  border: none;
  padding: 0.3rem 0.7rem;
  cursor: pointer;
}

.toggle-group button.is-active {
  background: var(--gold-soft);
  color: var(--gold);
}

.body-note {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--ink-muted);
  margin: 0.4rem 0 0;
}

/* ---- Empty / loading / error states ----------------------------------------------------- */

.loading-state,
.empty-state,
.error-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 0.8rem;
  padding: 3.5rem 1.5rem;
  color: var(--ink-dim);
}

/* The static placeholder index.html paints before app.js runs. It stands apart from the flow (fixed,
   centred a little above the middle) so that the navbar and the evidence strip opening up above
   #view-root when the account check answers do not move it (ticket 926). */
.loading-state--boot {
  position: fixed;
  inset: 40dvh 0 auto;
  padding: 0;
}

.empty-state {
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-lg);
  background: linear-gradient(135deg, var(--card-from), var(--card-to));
}

.empty-state__badge {
  width: 54px;
  height: 54px;
  border-radius: 16px;
  display: grid;
  place-items: center;
  color: var(--gold);
  background: var(--gold-soft);
  border: 1px solid var(--border-gold);
}

.empty-state__badge svg {
  width: 26px;
  height: 26px;
}

.empty-state h2 {
  font-size: var(--fs-md);
}

.empty-state p,
.error-state p {
  max-width: 34rem;
  margin: 0;
}

/* The page-not-found screen (ticket 927, views/not_found.js; ticket 973): the empty-state card with a pixel
   vignette above one h1, the address that was typed, as plain text, and one button out. */
.empty-state--not-found h1 {
  font: var(--type-title);
}

.not-found__path {
  max-width: 100%;
  padding: 0.25rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--ink-body);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  overflow-wrap: anywhere;
}

/* Compact empty-state variant (ticket 006): a left-aligned, low-height inline notice for spots
   where the full 3.5rem-padded card reads as too heavy — Operations/Investigations keep the
   dashed card border (the base .empty-state rule above), Activity's use sits inside a .section
   and drops it since the section itself already frames the content. A 2x2 grid (badge spanning
   both rows, title/body stacked in the second column) keeps the markup identical to the base
   variant — no wrapper needed around the title/body pair — while sitting the badge beside the
   text instead of stacked above it, which is what keeps the whole thing under 120px tall. */
.empty-state--compact {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  text-align: left;
  column-gap: 0.75rem;
  row-gap: 0.15rem;
  padding: 1.1rem 1.25rem;
}

.empty-state--compact h2,
.empty-state--compact p {
  grid-column: 2;
}

.section > .empty-state--compact {
  border: none;
  background: none;
  border-radius: 0;
  padding: 0;
}

.empty-state--compact .empty-state__badge {
  grid-row: 1 / 3;
  width: 32px;
  height: 32px;
  border-radius: 10px;
}

.empty-state--compact .empty-state__badge svg {
  width: 16px;
  height: 16px;
}

.empty-state--compact h2 {
  font-size: var(--fs-sm);
  font-weight: 600;
}

.empty-state.empty-state--compact p {
  font-size: var(--fs-xs);
  color: var(--ink-dim);
}

/* Quiet-window inventory disclosure (ticket 021) — ATT&CK folds the unchanged technique matrix
   behind a native <details> when hitCount === 0, so the first viewport reads as the .empty-state
   lead-in above ("No technique matched in this window.") rather than a wall of untouched cells.
   Collapsed by default; expanding reveals the identical matrix this view always rendered.
   Same idiom as detail.js's .detail-block--forensic, reused here inside .empty-state's own
   centered layout. */
.quiet-day-inventory {
  align-self: stretch;
  width: 100%;
  margin-top: 1.2rem;
  text-align: left;
}

.quiet-day-inventory > summary {
  cursor: pointer;
  user-select: none;
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--gold-2);
}

.quiet-day-inventory[open] > summary {
  margin-bottom: 1rem;
}

.error-state {
  border: 1px solid rgba(248, 81, 73, 0.35);
  border-radius: var(--radius-lg);
  background: rgba(248, 81, 73, 0.06);
  color: #fca5a5;
}

/* A busy server (ticket 520) is reachable and will answer, so its card is amber, not red. */
.error-state--busy {
  border-color: rgba(227, 179, 65, 0.35);
  background: rgba(227, 179, 65, 0.06);
  color: var(--st-4xx);
}

.error-state code {
  font-family: var(--font-mono);
  color: var(--ink-body);
  background: var(--panel-deep);
  padding: 0.15rem 0.4rem;
  border-radius: 0.4rem;
}

.spinner {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 3px solid color-mix(in srgb, var(--gold) 18%, transparent);
  border-top-color: var(--gold);
  animation: spin 0.8s linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* Sized skeleton placeholders (ticket 033) — dom.js's skeletonState() swaps these in for a major
   view's first paint instead of a bare spinner, so the shape of the eventual content is visible
   immediately and nothing jumps once real data lands. The shimmer sweep is an `infinite` CSS
   animation, same idiom as `.spinner` above — the reduced-motion rule caps its iteration count to 1
   at a near-zero duration, which renders as the settled final frame rather than a moving sweep. */
.skeleton-state {
  padding: 1.5rem 0;
}

.skeleton-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 0.85rem;
}

.skeleton-block {
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: linear-gradient(
    100deg,
    var(--card-from) 30%,
    color-mix(in srgb, var(--gold) 8%, transparent) 50%,
    var(--card-from) 70%
  );
  background-size: 200% 100%;
  animation: skeleton-sweep 1.6s ease-in-out infinite;
}

.skeleton-block--card {
  grid-column: 1 / -1;
  height: 8rem;
}

.skeleton-block--kpi {
  height: 7rem;
}

.skeleton-block--row {
  grid-column: 1 / -1;
  height: 2.2rem;
  border-radius: 0.6rem;
}

/* Page-shaped kinds (ticket 926), sized from the drawn pages at 390px and 1440px so content replaces
   them in place: a page head (title, one meta line, actions), a status banner, a number tile, a
   short titled list, a titled section, one list entry, and a button. */
.skeleton-block--head {
  grid-column: 1 / -1;
  height: 5.5rem;
  border-radius: 0.6rem;
}

.skeleton-block--band {
  grid-column: 1 / -1;
  height: 5.6rem;
}

.skeleton-block--tile {
  height: 5.6rem;
}

.skeleton-block--list {
  grid-column: 1 / -1;
  height: 6.1rem;
}

.skeleton-block--panel {
  grid-column: 1 / -1;
  height: 11rem;
}

.skeleton-block--item {
  grid-column: 1 / -1;
  height: 4.6rem;
  border-radius: 0.8rem;
}

.skeleton-block--button {
  width: 9.1rem;
  height: 2.75rem;
  border-radius: 0.6rem;
}

@media (max-width: 760px) {
  .skeleton-block--head {
    height: 7.8rem;
  }
}

@keyframes skeleton-sweep {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}

/* War room's bare skeleton placeholders (ticket 039 — dom.js's skeletonBlocks()): kpiHost and
   journeySteps provide their own grid/flex layout, so their direct .skeleton-block children need
   just enough sizing to sit in that layout instead of skeletonState()'s own wrapping
   .skeleton-state/.skeleton-grid divs. kpiHost is `display: grid; grid-template-columns: repeat(4,
   minmax(0, 1fr))` — a bare child already auto-places into one of the 4 columns, so only height
   needs setting to roughly match .warroom-kpi's own min-height. journeySteps is `display: flex`,
   where a bare child has no intrinsic width — flex-size it the same way .warroom-journey__node
   itself is sized. Both blocks pass kind "kpi" (dom.js), whose own .skeleton-block--kpi rule sets
   `height: 7rem` — a plain `min-height` here would be inert once that height already exceeds it,
   so journeySteps' own rule below overrides with `height` instead: two classes beat
   .skeleton-block--kpi's one, so the cascade actually applies it. */
.warroom-kpis > .skeleton-block {
  /* The drawn tile: padding, label, margins and the value, whose font is clamp(2.4rem, 5vw, 3.6rem)
     (ticket 926: 132px at 390px, 174px at 1440px, where the line under the value takes its own row). */
  height: calc(5.8rem + 1.05 * clamp(2.4rem, 5vw, 3.6rem));
}

@media (min-width: 641px) {
  .warroom-kpis > .skeleton-block {
    height: calc(7.1rem + 1.05 * clamp(2.4rem, 5vw, 3.6rem));
  }
}

.warroom-journey__steps > .skeleton-block {
  flex: 1 1 0;
  min-width: 0;
  /* The drawn node is 144px on one row (above 1100px) and 121px stacked (ticket 926). */
  height: 7.55rem;
}

@media (min-width: 1101px) {
  .warroom-journey__steps > .skeleton-block {
    height: 9rem;
  }
}

.toast {
  position: fixed;
  bottom: 1.4rem;
  left: 50%;
  transform: translateX(-50%) translateY(12px);
  z-index: 80;
  background: linear-gradient(135deg, var(--gold), var(--gold-2));
  color: #1a1500;
  font-weight: 600;
  font-size: var(--fs-xs);
  padding: 0.55rem 1.1rem;
  border-radius: 9999px;
  box-shadow: var(--shadow-gold);
  opacity: 0;
  pointer-events: none;
  transition:
    opacity var(--dur-base) var(--ease-standard),
    transform var(--dur-base) var(--ease-standard);
}

.toast.is-shown {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* Distinct error/warning toast (ticket 039) — same gold pill by default (success/neutral), red
   when dom.js's toast() is called with { kind: "error" } so a failure reads as one at a glance
   instead of blending into every other confirmation. #dc2626/#b91c1c rather than the
   --rstatus-5xx/.error-state #f85149 red — a doublecheck-review fix: white text over #f85149 is
   only ~3.35:1, below WCAG AA for this 14px text; both stops here hold >=4.5:1 against white
   (#dc2626 ~4.83:1, #b91c1c ~6.47:1). */
.toast.is-error {
  background: linear-gradient(135deg, #dc2626, #b91c1c);
  color: #fff;
  box-shadow: 0 10px 30px rgba(220, 38, 38, 0.35);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  border: 0;
}

/* The inline-SVG icon sprite: kept out of flow (0×0) without an inline `style` attribute, which the
   CSP would block. */
.svg-sprite {
  position: absolute;
  width: 0;
  height: 0;
}

/* ---- Wire console -------------------------------------------------------------------------- */

/* A persistent, non-modal bottom drawer (js/wire_console.js) tailing captured wire exchanges
   across every route while signed in — an always-visible edge tab plus a slide-up panel, in the
   same fixed-bottom family as other drawers but sitting below it (and below .detail-overlay
   so a modal's backdrop still covers the drawer). The outer container itself takes no pointer
   events: it spans the full width even when only the tab is showing, and without this the
   transparent strip would swallow clicks meant for the footer/capture-context row underneath —
   only the tab and the open panel (both `pointer-events: auto`) are ever actually clickable. */
.wire-console {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 45;
  pointer-events: none;
}

.wire-console[hidden] {
  display: none;
}

.wire-console__tab,
.wire-console__panel {
  pointer-events: auto;
}

/* Anchored to the container's own bottom edge (the viewport bottom — the container collapses to
   zero height itself, since both children are taken out of flow / hidden) and lifted fully above
   it by its own height. Closed, that is all that is on screen. Open, the container's box now has
   real height (the panel's, an in-flow child) but `bottom: 0` still means "the containing block's
   bottom edge" — the viewport bottom, not the panel's top — so without the override below the tab
   would sit UNDER the now-taller panel instead of above it. */
.wire-console__tab {
  position: absolute;
  right: 1.2rem;
  bottom: 0;
  transform: translateY(-100%);
  font: var(--type-footnote);
  padding: 0.35rem 0.9rem;
  border: 1px solid var(--border-gold);
  border-bottom: none;
  border-radius: 6px 6px 0 0;
  background: var(--panel);
  color: var(--ink);
  cursor: pointer;
}

body.has-wire-console-open .wire-console__tab {
  bottom: clamp(120px, var(--wire-console-height, 260px), 70vh);
  transform: none;
}

/* Height is a CSS custom property the drag handle below writes directly on the root element (not
   this container) so the has-wire-console-open padding-bottom rule on body itself — which only
   ever reads the property, never sets it — sees the live value too; custom properties inherit
   downward only, so writing it here would leave that rule stuck on the fallback. The same
   clamp(120px, …, 70vh) is repeated on the tab's `bottom` and the body padding above so the three
   never disagree about where the panel's visible edge actually is — a raw var() on all three would
   let a persisted height from a taller window drift past this rule's own min/max once the panel's
   real height is clamped and the others aren't. */
.wire-console__panel {
  height: clamp(120px, var(--wire-console-height, 260px), 70vh);
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  background: linear-gradient(180deg, var(--panel), var(--panel-deep));
  border-top: 1px solid var(--border-gold);
  box-shadow: 0 -18px 40px -20px rgba(0, 0, 0, 0.85);
  animation: wire-console-in var(--dur-base) var(--ease-standard);
}

/* The author `display: flex` above beats the UA `[hidden] { display: none }` — this scoped rule
   restores hiding, same pattern .detail-overlay[hidden] uses above. */
.wire-console__panel[hidden] {
  display: none;
}

.wire-console__section-tabs {
  display: flex;
  flex: 0 0 auto;
  flex-wrap: wrap;
  gap: 0.35rem;
  padding: 0.45rem 0.9rem;
  border-bottom: 1px solid var(--border);
}

.wire-console__section-tabs[hidden] {
  display: none;
}

.wire-console__section-tab {
  flex: 1 1 auto;
  min-width: 0;
  max-width: 100%;
  padding: 0.35rem 0.65rem;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: transparent;
  color: var(--ink-muted);
  font: 600 var(--fs-2xs) var(--font-mono);
  text-align: left;
  white-space: normal;
  overflow-wrap: anywhere;
  cursor: pointer;
}

.wire-console__section-tab[aria-selected="true"] {
  border-color: var(--border-gold);
  background: var(--gold-soft);
  color: var(--ink);
}

.wire-console__section-panel {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
  flex-direction: column;
}

.wire-console__section-panel[hidden] {
  display: none;
}

.wire-console__section-panel--capture {
  overflow-x: hidden;
  overflow-y: auto;
  padding: 0.7rem 0.9rem;
}

.wire-console__capture-controls {
  min-width: 0;
  max-width: 100%;
  overflow-wrap: anywhere;
}

@keyframes wire-console-in {
  from {
    transform: translateY(16px);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}

.wire-console__grip {
  height: 6px;
  flex: 0 0 auto;
  cursor: ns-resize;
  background: var(--border-gold);
  touch-action: none;
}

.wire-console__head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex: 0 0 auto;
  padding: 0.4rem 0.9rem;
  border-bottom: 1px solid var(--border);
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
}

.wire-console__status-label {
  flex: 1 1 auto;
  color: var(--ink-muted);
  font: var(--type-footnote);
}

.wire-console__body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 0.3rem 0;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
}

.wire-console__row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  padding: 0.2rem 0.9rem;
  border: none;
  background: none;
  color: var(--ink-body);
  text-align: left;
  cursor: pointer;
}

.wire-console__row:hover,
.wire-console__row:focus-visible {
  background: var(--panel-deep);
}

.wire-console__cell {
  flex: 0 0 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.wire-console__cell--time {
  width: 20ch; /* "Oct 22, 22:37:11 UTC" */
  color: var(--ink-muted);
}

.wire-console__cell--ip {
  width: 12ch;
}

.wire-console__cell--path {
  flex: 1 1 auto;
  min-width: 0;
}

.wire-console__cell--service {
  width: 10ch;
  color: var(--ink-muted);
}

/* Reserves room for the drawer at the very end of the page's own scrollable flow — not on
   `.view-root`, which #capture-context follows as a later sibling: padding on an
   ancestor that isn't the document's last in-flow content only pushes those siblings down by the
   same amount the page's max-scroll grows, so the true bottom of the page (and the last of the
   page's content) still lands exactly under the fixed drawer at maximum scroll. Padding on `body`
   itself — nothing follows it — is what actually keeps the page's end reachable above the drawer. Kiosk and
   board mode hide the drawer outright (see their own rules further down) and must never also
   reserve space for it. */
body.has-wire-console-open:not(.is-kiosk):not(.is-board) {
  padding-bottom: clamp(120px, var(--wire-console-height, 260px), 70vh);
}

@media (max-width: 560px) {
  .wire-console__panel {
    height: clamp(120px, var(--wire-console-height, 260px), 50vh);
  }

  body.has-wire-console-open .wire-console__tab {
    bottom: clamp(120px, var(--wire-console-height, 260px), 50vh);
  }

  body.has-wire-console-open:not(.is-kiosk):not(.is-board) {
    padding-bottom: clamp(120px, var(--wire-console-height, 260px), 50vh);
  }

  .wire-console__head {
    flex-wrap: wrap;
  }
}

/* ---- Filter presets ----------------------------------------------------------------------- */

.preset-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 0.7rem;
}

.preset-btn {
  font-size: var(--fs-2xs);
  padding: 0.32rem 0.7rem;
}

table.data tbody tr.is-selected {
  background: var(--gold-soft);
  box-shadow: inset 3px 0 0 var(--gold);
}

/* ---- Sessions ----------------------------------------------------------------------------- */

.session-list {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.session-row {
  background: linear-gradient(135deg, var(--card-from), var(--card-to));
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.8rem 1rem;
  cursor: pointer;
}

.session-row:hover,
.session-row:focus-visible {
  border-color: var(--border-gold);
  outline: none;
}

.session-row.is-selected {
  box-shadow: inset 3px 0 0 var(--gold);
}

.session-row--hackback {
  border-color: rgba(227, 179, 65, 0.5);
  box-shadow: 0 0 0 1px rgba(227, 179, 65, 0.18);
}

.agent-row {
  text-decoration: none;
  color: inherit;
  display: block;
}

.agent-row--high {
  border-color: rgba(227, 179, 65, 0.55);
  box-shadow: 0 0 0 1px rgba(227, 179, 65, 0.2);
}

/* One closed <details> per source address on the Evidence Agents tab (ticket 960): the summary is the
   address's row, the body holds its per-session rows. Every part wraps, so a 390 px column never
   scrolls sideways. */
.agent-group {
  min-width: 0;
  background: linear-gradient(135deg, var(--card-from), var(--card-to));
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.agent-group[open] {
  border-color: var(--border-gold);
}

.agent-group__summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem 0.6rem;
  padding: 0.8rem 1rem;
  cursor: pointer;
  list-style: none;
}

.agent-group__summary::-webkit-details-marker {
  display: none;
}

.agent-group__summary::before {
  content: "▸";
  color: var(--ink-dim);
}

.agent-group[open] > .agent-group__summary::before {
  content: "▾";
}

.agent-group__summary:hover,
.agent-group__summary:focus-visible {
  outline: none;
  background: var(--glass);
}

.agent-group__summary .session-row__ip {
  min-width: 0;
  overflow-wrap: anywhere;
}

.agent-group__link {
  margin-left: auto;
}

.agent-group__facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.2rem 0.9rem;
  flex: 1 1 100%;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--ink-dim);
}

.agent-group__kinds {
  flex: 1 1 100%;
  min-width: 0;
  overflow-wrap: anywhere;
  font-size: var(--fs-xs);
  color: var(--ink-dim);
}

.agent-group__kinds:empty {
  display: none;
}

.agent-group__body {
  padding: 0 1rem 0.9rem;
}

.agent-score {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-weight: 700;
  color: var(--gold);
  letter-spacing: 0.02em;
}

.agent-signal {
  font-size: var(--fs-2xs);
  padding: 0.15rem 0.55rem;
  max-width: 22rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.session-row__head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
}

.session-row__ip {
  font-family: var(--font-mono);
  font-weight: 600;
  color: var(--ink);
}

.session-row__char {
  font-size: var(--fs-xs);
  color: var(--ink-dim);
}

.session-tag {
  font-size: var(--fs-2xs);
  padding: 0.15rem 0.55rem;
  color: var(--ink-muted);
}

/* first_ts_utc → last_ts_utc plus duration/count — critical timestamp evidence, not chrome
   (ticket 032, doublecheck finding). */
.session-row__meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem;
  margin-top: 0.45rem;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--ink-dim);
}

.session-row__dossier {
  margin: 0.4rem 0 0;
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  color: var(--ink-dim);
}

.session-row__expanded {
  margin: -0.2rem 0 0.4rem;
  padding: 0.9rem 1rem;
  border: 1px solid var(--border);
  border-top: none;
  border-radius: 0 0 var(--radius) var(--radius);
  background: var(--panel-deep);
}

.hackback-phases {
  margin-bottom: 0.9rem;
}

.hackback-phases__list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

.timeline {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
}

.timeline-row {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.4rem 0.6rem;
  border-radius: 0.5rem;
  cursor: pointer;
}

.timeline-row:hover,
.timeline-row:focus-visible {
  background: var(--glass);
  outline: none;
}

/* A session-timeline timestamp — critical evidence, not chrome (ticket 032). */
.timeline-row__time {
  color: var(--ink-dim);
  min-width: 6.5ch;
}

/* ---- Agent detection evidence (ticket 020-20260831230419) --------------------------------- */
/* The confidence pill in a collapsed row and the expanded evidence card share the same
   .status-2xx/3xx/4xx/5xx "traffic light" tone convention as detail.js's attack-story badge (see
   agents.js's confidenceTone()) — high/moderate/low here, not attack-story's four-tier scale. */

.agent-confidence {
  font-size: var(--fs-2xs);
}

.session-row__expanded-body.agent-evidence-panel {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

/* The evidence panel's lazily-loaded request-cadence timeline — same .timeline/.timeline-row
   primitives Sessions' own expanded view uses (generic, not scoped to that file), just with a
   title + spacing matching .hackback-phases above it. */
.agent-timeline {
  margin-bottom: 0.9rem;
}

.agent-timeline .timeline {
  margin-top: 0.5rem;
}

.agent-evidence__head-row {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  flex-wrap: wrap;
}

.agent-evidence {
  list-style: none;
  margin: 0.3rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}

.agent-evidence__row {
  padding: 0.65rem 0.85rem;
  border: 1px solid var(--border);
  border-radius: 0.6rem;
  background: var(--panel);
}

.agent-evidence__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.6rem;
  flex-wrap: wrap;
}

.agent-evidence__label {
  font-weight: 600;
  color: var(--ink);
}

.agent-evidence__weight {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--gold);
  white-space: nowrap;
}

/* The evidence pointer (timestamp/path/exchange id) backing an agent-detection signal — critical
   evidence, not chrome (ticket 032). */
.agent-evidence__detail {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--ink-dim);
  margin: 0.35rem 0 0;
}

.agent-evidence__reference {
  font-family: var(--font-mono);
}

.agent-evidence__limitation {
  font-style: italic;
}

/* The no-candidate/low-data empty state's signal catalog — left-aligned rows inside the otherwise
   centered .empty-state, so a multi-sentence explanation stays readable. */
.agent-signal-catalog {
  list-style: none;
  margin: 0.8rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  max-width: 34rem;
  text-align: left;
}

.agent-signal-catalog__row {
  padding: 0.6rem 0.85rem;
  border: 1px solid var(--border);
  border-radius: 0.6rem;
  background: var(--panel);
}

/* ---- Responsive ------------------------------------------------------------------------- */

/* The breakpoint is `80rem` — the exact value of `--maxw`, the content column width used
   elsewhere on the page (`.navbar__inner` itself caps at `min(100%, 105rem)`, a different value).
   Media queries cannot read a custom property, so `--maxw`, this rule, and the upper bound of the
   ticket 119 rule `@media (1023px < width < 80rem)` near the license chip styles are declared
   independently and must be kept equal by hand if `--maxw` ever changes. */
@media (min-width: 80rem) {
  .nav-link,
  .nav-trigger {
    padding-left: 0.6rem;
    padding-right: 0.6rem;
  }
}

/* Navbar geometry tier 2 (ticket 001): 1439px and narrower shrinks the search box and caps the
   account trigger so the row keeps fitting one line down to 1024px (ticket 970; 1240px before). */
@media (max-width: 1439px) {
  .global-search {
    flex-basis: 10rem;
    min-width: 8rem;
  }
  .nav-trigger {
    max-width: 8rem;
  }
}

/* Navbar geometry tier 3 (ticket 001): below 1024px the primary links no longer fit next to the
   tools cluster, so they drop to their own horizontally scrollable row instead of colliding. Ticket 970
   lowered this from 1240px: the tools cluster shrank to the gear, the pill and the avatar. */
@media (max-width: 1023px) {
  .navbar__inner {
    flex-wrap: wrap;
    row-gap: 0.5rem;
  }
  .nav-primary {
    order: 3;
    flex-basis: 100%;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .nav-primary::-webkit-scrollbar {
    display: none;
  }
}

@media (max-width: 860px) {
  .chart-grid,
  .chart-grid--even {
    grid-template-columns: minmax(0, 1fr);
  }
}

@media (max-width: 560px) {
  .hero {
    padding-top: 1.8rem;
  }
}

/* ---- Live feed: mobile breakpoint ---------------------------------------------------------- */
/* ticket 20260831230339 — below 760px (a real phone; 768 CSS px, tablet portrait, keeps the desktop
   table exactly as before): the semantic table hides in favor of the card list built alongside it,
   and the six advanced-filter inputs collapse behind a toggle so the evidence list sits closer to
   the top of the screen. Chosen below 768 (not e.g. at it) deliberately, so a check at exactly
   768px still exercises "keep the semantic table at larger widths". */
@media (max-width: 760px) {
  /* The shared capture-context rows carry long, user-visible provenance explanations (see
     capture_context.js's buildRow()). Keep each row inside the card on phones: the desktop
     baseline flex row lets a no-break label/value pair reserve its max-content width, while a
     long live-attachment or masking explanation can then push the page wider than the viewport.
     Stacking the three pieces and allowing the value/explanation text to wrap preserves every
     evidence fact without hiding it or relying on a hover-only title. */
  .capture-context__rows {
    flex-direction: column;
    align-items: stretch;
    gap: 0.7rem;
  }

  .capture-context__row {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.2rem;
    width: 100%;
  }

  .capture-context__value,
  .capture-context__sub {
    min-width: 0;
    max-width: 100%;
    white-space: normal;
    overflow-wrap: anywhere;
  }

  .live-table-scroll {
    display: none;
  }

  .feed-cards {
    display: flex;
  }

  .feed-active-filters:not([hidden]) {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
  }

  .feed-filters-adv {
    flex-direction: column;
    align-items: stretch;
  }

  .feed-exports {
    justify-content: flex-end;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    animation-delay: 0s !important;
    transition-duration: 0.001ms !important;
  }
}

/* Forced-colors / high-contrast mode (ticket 032): every status/selection cue in this file already
   pairs a colour with a border and a text label (never colour alone), which forced-colors mode
   generally preserves on its own. The one place that leans on `background` + `border-color` alone
   to mark "current" — the active nav pill, since its label text is identical whether active or
   not — gets an explicit outline here so it survives even where a UA flattens both to the same
   system colour pair. */
@media (forced-colors: active) {
  .nav-link.is-active {
    outline: 2px solid CanvasText;
    outline-offset: -3px;
  }
}

/* ---- War room / kiosk ---------------------------------------------------------------------- */

.warroom-toolbar {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1rem;
  flex-wrap: wrap;
}

.warroom-toolbar__left,
.warroom-toolbar__right {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.warroom-clock {
  font-family: var(--font-mono);
  font-size: var(--fs-md);
  color: var(--gold);
  letter-spacing: 0.04em;
  min-width: 11ch;
}


.warroom-kpis {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0.85rem;
  margin-bottom: 1rem;
}

.warroom-kpi {
  background: linear-gradient(180deg, var(--card-from), var(--card-to));
  border: 1px solid var(--border-gold);
  border-radius: var(--radius-lg);
  padding: 1.1rem 1.25rem;
  box-shadow: var(--shadow-gold);
  min-height: 7.5rem;
}

/* KPI value-change emphasis (ticket 033: "KPI updates snap") — warroom.js's kpiTile() adds this
   class only on a poll where a value actually changed from the previous paint (never on first
   paint), and only a fresh DOM node is ever born with it (kpiHost.replaceChildren() rebuilds every
   tile each poll), so the animation always plays on insertion with no reflow trick needed. A
   background flash on the whole tile, the same highlight-then-settle idiom as .row-new/
   .feed-card-flash elsewhere in this file, rather than a text-color flash — .is-accent already
   colors the value gold permanently, so a color-based pulse on an accent tile would have nothing
   to visibly change. */
.warroom-kpi.is-updated {
  animation: kpi-pulse var(--dur-emphasis) ease-out;
}

@keyframes kpi-pulse {
  0% {
    background: color-mix(in srgb, var(--gold) 22%, transparent);
  }
  100% {
    background: linear-gradient(180deg, var(--card-from), var(--card-to));
  }
}

.warroom-kpi__label {
  font: var(--type-footnote);
  color: var(--ink-muted);
  margin-bottom: 0.35rem;
}

.warroom-kpi__value {
  font-size: clamp(2.4rem, 5vw, 3.6rem);
  font-weight: 700;
  line-height: 1.05;
  color: var(--ink);
  letter-spacing: -0.03em;
}

.warroom-kpi__value.is-accent {
  color: var(--gold);
}

.warroom-kpi__sub {
  margin-top: 0.35rem;
  font-size: var(--fs-xs);
  color: var(--ink-dim);
}

/* ---- War room — Threat journey (ticket 025) ------------------------------------------------ */

.warroom-journey {
  background: linear-gradient(180deg, var(--card-from), var(--card-to));
  border: 1px solid var(--border-gold);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-gold);
  padding: 1rem 1.1rem;
  margin-bottom: 1rem;
}

.warroom-journey__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin-bottom: 0.75rem;
}

.warroom-journey__title {
  font-size: var(--fs-md);
  margin: 0;
}

.warroom-journey__steps {
  display: flex;
  gap: 1.4rem;
  align-items: stretch;
}

.warroom-journey__node {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: 0.75rem 0.9rem;
  border-radius: 0.6rem;
  border: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.03);
  text-decoration: none;
  color: inherit;
}

.warroom-journey__node:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

/* A chevron connector between adjacent phases — purely decorative (the fixed left-to-right DOM/
   tab order already carries the sequence for assistive tech), suppressed once the strip wraps
   (see the 1100px breakpoint below). */
.warroom-journey__node:not(:last-child)::after {
  content: "\2192";
  position: absolute;
  top: 50%;
  right: -1.1rem;
  transform: translateY(-50%);
  color: var(--ink-muted);
  font-size: 1.1rem;
}

.warroom-journey__node.is-observed {
  border-color: color-mix(in srgb, var(--gold) 55%, var(--border));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--gold) 25%, transparent);
}

.warroom-journey__node.is-unobserved {
  opacity: 0.6;
}

.warroom-journey__label {
  font-weight: 700;
  font-size: var(--fs-sm);
  color: var(--ink);
}

.warroom-journey__meta {
  font-size: var(--fs-xs);
  color: var(--gold-2);
  font-family: var(--font-mono);
}

.warroom-journey__node.is-unobserved .warroom-journey__meta {
  color: var(--ink-muted);
  font-family: inherit;
}


.warroom-journey__node.is-pulse {
  animation: warroom-journey-pulse 1.2s ease;
}

@keyframes warroom-journey-pulse {
  0% {
    background: color-mix(in srgb, var(--gold) 32%, transparent);
    border-color: color-mix(in srgb, var(--gold) 65%, transparent);
    transform: scale(1.02);
  }
  100% {
    background: rgba(255, 255, 255, 0.03);
    transform: scale(1);
  }
}


body.is-kiosk .warroom-journey__node {
  padding: 0.65rem 0.8rem;
}

@media (max-width: 1100px) {
  .warroom-journey__steps {
    flex-wrap: wrap;
  }
  .warroom-journey__node,
  .warroom-journey__steps > .skeleton-block {
    flex: 1 1 calc(50% - 0.7rem);
  }
  /* The selector repeats :not(:last-child) so it outranks the connector rule above. Without it the arrow
     stayed (it is not hidden "once the strip wraps", as that comment says) and stood 1.1rem outside every
     node, which made the page scroll sideways on a phone (ticket 932). */
  .warroom-journey__node:not(:last-child)::after {
    content: none;
  }
}

@media (max-width: 640px) {
  .warroom-journey__node,
  .warroom-journey__steps > .skeleton-block {
    flex: 1 1 100%;
  }
}

.warroom-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  gap: 1rem;
  align-items: stretch;
}

.warroom-ticker,
.warroom-panel {
  min-height: 22rem;
  display: flex;
  flex-direction: column;
  padding: 1rem 1.1rem;
}

.warroom-ticker__head,
.warroom-panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.75rem;
}

.warroom-ticker__title {
  font-size: var(--fs-md);
  margin: 0;
}




.warroom-panel__body {
  flex: 1;
}

.warroom-panel__body.is-fading {
  animation: warroom-panel-in 0.45s ease;
}

@keyframes warroom-panel-in {
  from {
    opacity: 0.35;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.warroom-ticker__body {
  flex: 1;
  overflow: hidden;
}

.warroom-ticker__list {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  font-family: var(--font-mono);
  font-size: clamp(0.95rem, 1.4vw, 1.15rem);
}

.warroom-ticker__row {
  display: grid;
  /* IP track is 16ch — wide enough for a full IPv4 address ("255.255.255.255" is 15 chars,
     e.g. RFC 5737 addresses) so a long client IP never overflows into the
     method column (ticket 030-20260902094151). The trailing `auto` is the optional LLM chip
     (ticket 009's `llmBadge()`, appended only when `row.meta.generator === "llm"`) — without its
     own track it fell into an implicit seventh column sized by the grid's default `auto-columns`
     (review NIT, deferred to ticket 017). */
  grid-template-columns: 18ch 16ch 5.5ch minmax(0, 1fr) auto 10ch auto;
  gap: 0.65rem;
  align-items: center;
  padding: 0.45rem 0.55rem;
  border-radius: 0.45rem;
  border: 1px solid transparent;
  background: rgba(255, 255, 255, 0.03);
}

.warroom-ticker__row.is-new {
  animation: warroom-row-flash 1.1s ease;
}

@keyframes warroom-row-flash {
  0% {
    background: color-mix(in srgb, var(--gold) 28%, transparent);
    border-color: color-mix(in srgb, var(--gold) 55%, transparent);
  }
  100% {
    background: rgba(255, 255, 255, 0.03);
    border-color: transparent;
  }
}

/* A ticker-row timestamp, read on a projector — critical evidence, not chrome (ticket 032). It is the
   absolute "Oct 22, 22:37 UTC" (ticket 984), so its track is 18ch rather than a relative label's 7. */
.warroom-ticker__time {
  color: var(--ink-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.warroom-ticker__ip {
  color: var(--ink);
  /* Belt-and-suspenders alongside the 16ch track above: a full IPv6 address (client_ip.py
     resolves those too, e.g. via a trusted X-Forwarded-For) can still exceed even a widened
     column, so truncate rather than collide — same pattern as .warroom-ticker__path/__service. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.warroom-ticker__method {
  color: var(--gold-2);
  font-weight: 600;
}

.warroom-ticker__path {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink-body);
}

.warroom-ticker__service {
  color: var(--ink-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.warroom-ticker__empty {
  margin: 2rem 0 0;
  color: var(--ink-muted);
}

/* "Last event HH:MM:SS UTC" line under the ticker (ticket 20260831235605). */
/* A timestamp — critical evidence, not chrome (ticket 032, doublecheck finding). */
.warroom-ticker__last-event {
  margin: 0.6rem 0 0;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--ink-dim);
}

/* Kiosk: hide laptop chrome. The full capture-context strip is hidden too — War room's own
   .warroom-evidence chip in the toolbar (which stays visible in kiosk) carries the same
   provenance/freshness facts instead. */
body.is-kiosk .navbar,
body.is-kiosk .hero,
body.is-kiosk #particle-field,
body.is-kiosk .capture-context,
body.is-kiosk .exec-journey,
/* The drawer stays mounted (its own polling keeps running) but must not compete with the SOC-wall
   layout — it reappears on exit. */
body.is-kiosk .wire-console {
  display: none !important;
}

.warroom-evidence {
  margin-left: 0.4rem;
}

body.is-kiosk .view-root {
  max-width: none;
  padding: 1.25rem 1.5rem 1.75rem;
}

body.is-kiosk .warroom-kpis {
  margin-top: 0.25rem;
}

body.is-kiosk .warroom-kpi__value {
  font-size: clamp(3rem, 6vw, 4.5rem);
}

/* +12rem over the pre-ticket-025 18rem budget for the Threat journey strip now sitting between
   the KPI row and this grid (a compact single row of nodes, so that is all it adds, but that row plus its own card padding and
   margin measures ~183px/11.4rem in a real browser, not the ~5rem first assumed — a
   /doublecheck-adjacent fix caught by actually resizing a real window rather than trusting the
   arithmetic).
   Ticket 031: min-height alone is a floor, not a ceiling — a flex item with no explicit height
   grows past it to fit its content, so a full ~40-row ticker pushed the whole kiosk page to
   roughly 3x the viewport height under real traffic (pre-existing since ticket 012, independent
   of the journey strip above — confirmed by hiding the strip and seeing the bulk of the overflow
   persist). max-height at the same value turns that floor into a hard ceiling; the nested flex
   chain below (.warroom-ticker__body) is what makes overflow: hidden actually clip the excess
   instead of the card just growing past it anyway. A sparse capture is unaffected — its content
   was already shorter than this budget, so the ceiling never binds and nothing new gets clipped. */
body.is-kiosk .warroom-ticker,
body.is-kiosk .warroom-panel {
  min-height: calc(100vh - 30rem);
  max-height: calc(100vh - 30rem);
}

/* .warroom-ticker__body already has flex: 1; overflow: hidden (above), but a flex item's default
   min-height is auto — its content's min-content size — which ignores overflow: hidden and keeps
   growing regardless of the ancestor max-height just added. min-height: 0 lets it actually shrink
   to the space left after .warroom-ticker__head/__last-event, so overflow: hidden clips whatever
   ticker rows don't fit instead of the card growing past one screen (ticket 031).
   .warroom-panel__body needs the identical override — its content (top_client_ips/top_paths, both
   capped server-side at 10 rows by store.py's _TOP_N, or the small fixed status-class breakdown)
   was assumed short enough to never approach this ceiling, but browser verification with the
   bundled scenario dataset showed a full 10-row ranked-bars panel is taller than the
   ~20.4rem/326px kiosk budget at 1280x720, overflowing the card exactly like the ticker did. */
body.is-kiosk .warroom-ticker__body,
body.is-kiosk .warroom-panel__body {
  min-height: 0;
  overflow: hidden;
}

@media (max-width: 1100px) {
  .warroom-kpis {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  /* minmax(0, 1fr), not a bare 1fr: a bare 1fr cannot go below its content's minimum width, and the
     ticker's rows pushed the single column to 417px, so War room scrolled sideways at 390px (ticket 932). */
  .warroom-grid {
    grid-template-columns: minmax(0, 1fr);
  }
  .warroom-ticker__row {
    /* Same 16ch IP track as the base rule — see the comment there. */
    grid-template-columns: 18ch 16ch 5ch minmax(0, 1fr) auto;
  }
  .warroom-ticker__service {
    display: none;
  }
}

@media (max-width: 640px) {
  .warroom-kpis {
    grid-template-columns: 1fr;
  }
  /* The five tracks above (time, a 16ch address, the method, the path, the status) need about 330px and a
     phone card has about 300: the status pill stood 14px outside the card at 390px (ticket 932). A compact
     row: no method column, a 10ch address. The 18ch time track (an absolute time since ticket 984) leaves
     the path about 20px; the address and the status stay whole. */
  .warroom-ticker__row {
    grid-template-columns: 18ch 10ch minmax(0, 1fr) auto;
    gap: 0.3rem;
    padding: 0.45rem 0.3rem;
  }
  .warroom-ticker__method {
    display: none;
  }
}

/* ---- Overview — executive decision layer (ticket 014) ------------------------------------- */

/* A KPI band of .warroom-kpi tiles (Operations, Intrusion Groups and the Entities AI-agent block use it):
   a variable-width auto-fit grid (same idiom as .stat-grid above) rather than War room's fixed 4-column
   kiosk layout, so the row reflows cleanly at narrow widths with no extra media query. The Overview hero
   that first needed it went with ticket 984. */
.command-hero-kpis {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 0.85rem;
  margin-bottom: 1rem;
}

.overview-posture {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.6rem 0.8rem;
}

.overview-posture__headline {
  margin: 0;
  font-size: var(--fs-md);
  line-height: 1.5;
  color: var(--ink);
  max-width: 54rem;
}

.overview-unavailable-note {
  margin-top: 0.6rem;
}

.overview-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

/* Reuses .stat-tile for layout — this modifier only turns the tile into a clickable drill-down
   (the outcomes band is `<a>` elements, unlike the plain-`<div>` supporting-telemetry tiles). */
.overview-outcome {
  display: block;
  text-decoration: none;
  color: inherit;
}

.overview-outcome:hover,
.overview-outcome:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

/* Reuses .briefing-event/.briefing-events (Executive briefing's notable-engagement list) for the
   "what changed" list — each row wraps its title/detail in this link instead of being static. */
.overview-changed-link {
  display: block;
  text-decoration: none;
  color: inherit;
}

.overview-changed-link:hover .briefing-event__title,
.overview-changed-link:focus-visible .briefing-event__title {
  color: var(--gold);
}



@media (max-width: 560px) {
  .overview-posture {
    flex-direction: column;
    align-items: flex-start;
  }
}

/* Command keeps real session evidence dominant; all columns can shrink at zoom/narrow widths. */
.command-page {
  overflow-wrap: anywhere;
}




.command-page > .section {
  margin-top: 1.2rem;
}

.command-page > .section:first-of-type {
  margin-top: 0;
  margin-bottom: 0.75rem;
}

/* Tighter head spacing than the app-wide .section__head default, scoped to the command page's own
   top-level sections only (the assessment leading it, plus outcomes/threats when top-level) — a
   real-browser-measured fit for the mandated 1440x900/1280x800 first-viewport acceptance (ticket
   004's repair), never applied to .section usage elsewhere in the console. `:not(.command-board)`
   keeps Board's own denser `.command-board .section__head { margin: 0 0 4px; }` (below) in force —
   without it, this rule's higher specificity (three classes vs. two) would silently override
   Board's outcomesBlock head margin at wide viewports where this media block isn't in effect. */
.command-page:not(.command-board) > .section > .section__head {
  margin-bottom: 0.6rem;
}

@media (max-height: 820px) {
  /* A shorter viewport (the mandated 1280×800 first-viewport check) gets a denser heading/
     assessment rhythm so the rail's "Next review action" bottom edge stays inside the first
     screen without touching content, order, or any of it below the fold — ticket 004's repair.
     Taller viewports (1440×900 and up) keep the roomier defaults above. */
  .command-page > .section:first-of-type {
    margin-bottom: 0.35rem;
  }

  .command-page:not(.command-board) > .section > .section__head {
    margin-bottom: 0.25rem;
  }
}

.command-page .overview-posture__headline {
  font-size: var(--fs-base);
  line-height: 1.45;
  max-width: none;
  flex: 1 1 30rem;
}

.command-finding {
  display: block;
  color: var(--gold-2);
  font-size: var(--fs-sm);
  margin-top: 0.5rem;
  text-decoration: none;
}

.command-finding:hover {
  text-decoration: underline;
}

.command-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.8fr) minmax(0, 1fr);
  gap: 1.2rem;
  align-items: start;
}

.command-activity {
  min-width: 0;
  padding: 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: linear-gradient(145deg, var(--card-from), var(--card-to));
}

.command-activity__head .body-note {
  margin: 0.25rem 0 0.8rem;
}

/* Evidence-linked Command Journey (ticket 004). This is a selected-session narrative over the
   shared bounded event snapshot, not the fixed aggregate War Room journey. Every stage carries a
   text state so unresolved evidence remains visible in reduced-motion, print, and text-only use. */
.command-journey {
  min-width: 0;
  padding: 1rem;
  border: 1px solid var(--border-gold);
  border-radius: var(--radius);
  background: linear-gradient(145deg, var(--card-from), var(--card-to));
  box-shadow: var(--shadow-gold);
  overflow-wrap: anywhere;
}

.command-journey__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.8rem;
  flex-wrap: wrap;
  margin-bottom: 0.7rem;
}

.command-journey__eyebrow {
  color: var(--gold-2);
  font: 600 var(--fs-2xs) var(--font-mono);
  letter-spacing: 0.16em;
  margin: 0;
}

.command-journey__title {
  font-size: var(--fs-md);
  margin: 0.2rem 0 0;
}

.command-journey__head > .body-note {
  margin: 0;
  color: var(--ink-dim);
}

.command-journey__picker {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-bottom: 0.8rem;
  color: var(--ink-dim);
  font-size: var(--fs-2xs);
}

.command-journey__picker select {
  min-width: min(28rem, 100%);
  max-width: 100%;
}

.command-journey__dossier {
  margin: 0 0 0.8rem;
  color: var(--ink-dim);
  font-size: var(--fs-sm);
}

.command-journey__steps {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1.2rem;
  align-items: stretch;
  margin-bottom: 0.9rem;
}

.command-journey__step {
  position: relative;
  display: flex;
  min-width: 0;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.3rem;
  padding: 0.75rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: 0.65rem;
  background: rgba(255, 255, 255, 0.025);
  color: var(--ink);
  text-align: left;
  font: inherit;
  cursor: pointer;
  overflow-wrap: anywhere;
  transition: border-color var(--dur-fast) var(--ease-standard), background var(--dur-fast) var(--ease-standard);
}

.command-journey__step:not(:last-child)::after {
  content: "\2192";
  position: absolute;
  top: 50%;
  right: -0.95rem;
  color: var(--ink-muted);
  font-size: 1.1rem;
  transform: translateY(-50%);
}

.command-journey__step:hover,
.command-journey__step:focus-visible,
.command-journey__step.is-active {
  border-color: var(--gold);
  background: var(--gold-soft);
}

.command-journey__step:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

.command-journey__step.is-observed {
  border-color: color-mix(in srgb, var(--gold) 55%, var(--border));
}

.command-journey__step.is-validated {
  border-color: var(--health-ok);
}

.command-journey__step.is-caution {
  border-color: var(--st-4xx);
}

.command-journey__step.is-unresolved {
  border-style: dashed;
  border-color: var(--border-strong);
}

.command-journey__step.is-new,
.command-journey__event.is-new {
  animation: command-journey-arrival var(--dur-emphasis) var(--ease-emphasized);
}

@keyframes command-journey-arrival {
  0% {
    background: color-mix(in srgb, var(--gold) 28%, transparent);
    border-color: var(--gold);
  }
  100% {
    background: rgba(255, 255, 255, 0.025);
  }
}

.command-journey__step-icon {
  color: var(--gold-2);
}

.command-journey__step-icon svg {
  width: 16px;
  height: 16px;
}

.command-journey__step-label {
  font-size: var(--fs-sm);
  font-weight: 700;
}

.command-journey__step-detail {
  min-width: 0;
  color: var(--ink-dim);
  font-size: var(--fs-2xs);
  line-height: 1.4;
}

.command-journey__step-state {
  margin-top: auto;
  color: var(--gold-2);
  font: 600 var(--fs-2xs) var(--font-mono);
}

.command-journey__step.is-unresolved .command-journey__step-state {
  color: var(--ink-muted);
}

.command-journey__step.is-caution .command-journey__step-state {
  color: var(--st-4xx);
}

.command-journey__step.is-validated .command-journey__step-state {
  color: var(--health-ok);
}

.command-journey__sequence {
  min-width: 0;
  padding: 0.8rem;
  border: 1px solid var(--border);
  border-radius: 0.65rem;
  background: rgba(0, 0, 0, 0.18);
}

.command-journey__sequence-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-bottom: 0.5rem;
}

.command-journey__sequence-head h3 {
  margin: 0;
  font-size: var(--fs-sm);
}

.command-journey__event-list {
  display: grid;
  gap: 0.45rem;
  list-style: none;
  padding: 0;
  margin: 0;
}

.command-journey__event {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 0.5rem;
  min-width: 0;
  align-items: center;
  padding: 0.45rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: 0.45rem;
}

.command-journey__event-select {
  display: grid;
  grid-template-columns: auto minmax(8rem, 0.85fr) minmax(0, 1.5fr) auto minmax(0, 1fr);
  gap: 0.35rem 0.6rem;
  align-items: center;
  min-width: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--ink);
  text-align: left;
  font: inherit;
  cursor: pointer;
}

.command-journey__event-select:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

.command-journey__event-select.is-selected {
  color: var(--gold-2);
}

.command-journey__event-index,
.command-journey__event-time {
  color: var(--ink-muted);
  font: var(--fs-2xs) var(--font-mono);
}

.command-journey__event-request,
.command-journey__event-response,
.command-journey__event-evidence {
  min-width: 0;
  font-size: var(--fs-2xs);
  overflow-wrap: anywhere;
}

.command-journey__event-request {
  line-height: 1.35;
}

.command-journey__event-service {
  color: var(--ink-dim);
}

.command-journey__event-response {
  white-space: nowrap;
}

.command-journey__event-evidence {
  color: var(--ink-dim);
}

.command-journey__event-evidence.is-validated {
  color: var(--health-ok);
}

.command-journey__event-evidence.is-caution {
  color: var(--st-4xx);
}

.command-journey__unresolved {
  color: var(--ink-muted);
}

.command-journey__event-link,
.command-journey__session-link {
  color: var(--gold-2);
  font-size: var(--fs-2xs);
  white-space: nowrap;
}

.command-journey__event-link:hover,
.command-journey__event-link:focus-visible,
.command-journey__session-link:hover,
.command-journey__session-link:focus-visible {
  text-decoration: underline;
}

.command-journey__text-alternative {
  margin-top: 0.7rem;
  color: var(--ink-dim);
  font-size: var(--fs-2xs);
}

.command-journey__text-alternative summary {
  cursor: pointer;
}

.command-journey__text-alternative .body-note {
  margin: 0.6rem 0;
}

.command-journey__links {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.6rem 1rem;
  margin-top: 0.7rem;
}

@media (max-width: 1050px) {
  .command-journey__steps {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .command-journey__step:nth-child(2)::after {
    content: none;
  }
}

@media (max-width: 640px) {
  .command-journey__steps {
    grid-template-columns: minmax(0, 1fr);
    gap: 0.55rem;
  }

  .command-journey__step::after {
    content: none !important;
  }

  .command-journey__event {
    grid-template-columns: minmax(0, 1fr);
  }

  .command-journey__event-select {
    grid-template-columns: auto minmax(0, 1fr) auto;
  }

  .command-journey__event-time {
    grid-column: 2 / -1;
  }

  .command-journey__event-request {
    grid-column: 1 / -1;
  }

  .command-journey__event-response {
    grid-column: 1 / 2;
  }

  .command-journey__event-evidence {
    grid-column: 2 / -1;
  }

  .command-journey__event-link {
    justify-self: start;
  }
}

/* Command lens selector + Environment (ticket 005). The graph is an evidence diagram, not a
   network map: source-to-contact lines exist only for returned command events. The inventory and
   capture columns stay separate so a configured-but-unobserved service cannot read as unhealthy or
   protected, and a recorded label cannot silently acquire a runtime-slot identity. */
.command-visualization {
  display: grid;
  gap: 0.8rem;
  min-width: 0;
}

.command-location-context {
  border-top: 1px solid var(--border);
  padding: 1rem;
  min-width: 0;
  overflow-wrap: anywhere;
}

.command-location-unknown > summary {
  cursor: pointer;
  padding-block: 0.5rem;
}

.command-location-unknown button {
  white-space: normal;
  text-align: left;
  max-width: 100%;
}

.command-lens-selector {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  width: fit-content;
  max-width: 100%;
  padding: 0.25rem;
  border: 1px solid var(--border);
  border-radius: 0.65rem;
  background: rgba(0, 0, 0, 0.18);
}

.command-lens-tab {
  min-width: 7rem;
  padding: 0.45rem 0.75rem;
  border: 1px solid transparent;
  border-radius: 0.45rem;
  background: transparent;
  color: var(--ink-dim);
  font: 600 var(--fs-xs) var(--font-mono);
  cursor: pointer;
}

.command-lens-tab:hover,
.command-lens-tab:focus-visible,
.command-lens-tab.is-active {
  border-color: var(--gold);
  background: var(--gold-soft);
  color: var(--gold-2);
}

.command-lens-tab:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

.command-lens-host {
  min-width: 0;
}

.environment-lens {
  min-width: 0;
  padding: 1rem;
  border: 1px solid var(--border-gold);
  border-radius: var(--radius);
  background: linear-gradient(145deg, var(--card-from), var(--card-to));
  box-shadow: var(--shadow-gold);
  overflow-wrap: anywhere;
}

.environment-lens__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.8rem;
  flex-wrap: wrap;
  margin-bottom: 0.65rem;
}

.environment-lens__eyebrow {
  margin: 0;
  color: var(--gold-2);
  font: 600 var(--fs-2xs) var(--font-mono);
  letter-spacing: 0.16em;
}

.environment-lens__title {
  margin: 0.2rem 0 0;
  font-size: var(--fs-md);
}

.environment-lens__head > .body-note {
  margin: 0;
  color: var(--ink-dim);
}

.environment-summary {
  margin: 0 0 0.75rem;
  color: var(--ink-dim);
  font: var(--fs-2xs) var(--font-mono);
  line-height: 1.5;
}

.environment-business-context,
.business-context-summary {
  display: grid;
  gap: 0.35rem;
  margin: 0.65rem 0;
  padding: 0.65rem 0.75rem;
  border: 1px solid var(--border-gold);
  border-radius: 0.5rem;
  background: color-mix(in srgb, var(--gold) 4%, transparent);
  overflow-wrap: anywhere;
}

.environment-business-context strong { color: var(--gold-2); }
.environment-business-context span,
.business-context-summary p { margin: 0; color: var(--ink-dim); font-size: var(--fs-2xs); line-height: 1.45; }

/* Overview's own collapsed disclosure for the optional business-context overlay (ticket 004's
   repair) — a direct .command-page child that is not itself .section, so it needs its own
   top margin to match the 1.2rem rhythm of .command-page > .section below. Board instead nests
   the same section markup inside the existing .board-assessment details (see console.css's
   .command-board ... .board-assessment rule) rather than adding a second top-level block. */
.command-business-overlay {
  margin-top: 1.2rem;
}

.command-business-overlay > summary {
  cursor: pointer;
  color: var(--gold-2);
  font: 600 var(--fs-2xs) var(--font-mono);
  letter-spacing: 0.05em;
}

/* The nested .section (the disclosure body) would otherwise fall back to the app-wide
   `.section { margin-top: 2rem }` — a needlessly large gap directly under a <summary> line. */
.command-business-overlay > summary + .section {
  margin-top: 0.6rem;
}

.environment-legend {
  display: flex;
  align-items: center;
  gap: 0.8rem 1.2rem;
  flex-wrap: wrap;
  margin: 0.75rem 0 0.55rem;
  color: var(--ink-dim);
  font-size: var(--fs-2xs);
}

.environment-legend > span:not(.environment-legend__note) {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
}

.environment-legend svg {
  width: 15px;
  height: 15px;
  color: var(--gold-2);
}

.environment-legend__note {
  color: var(--ink-muted);
}

.environment-graph {
  min-width: 0;
  max-width: 100%;
  padding: 0.55rem;
  border: 1px solid var(--border);
  border-radius: 0.65rem;
  background: rgba(0, 0, 0, 0.22);
  overflow-x: auto;
}

.environment-graph__svg {
  display: block;
  width: 100%;
  min-width: 680px;
  height: auto;
}

.environment-svg__column-label {
  fill: var(--ink-muted);
  font: 600 11px var(--font-mono);
  letter-spacing: 0.12em;
}

.environment-svg__link {
  fill: none;
  stroke: var(--gold-2);
  stroke-dasharray: 5 4;
  stroke-linecap: round;
  stroke-width: 1.5;
  opacity: 0.5;
  cursor: pointer;
}

.environment-svg__link:hover,
.environment-svg__link:focus {
  stroke: var(--gold);
  opacity: 0.95;
  stroke-width: 2.5;
}

.environment-svg__link:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 3px;
}

.environment-svg__node {
  outline: none;
}

.environment-svg__source,
.environment-svg__contact {
  cursor: pointer;
}

.environment-svg__source > circle:first-child {
  fill: var(--bg);
  stroke: var(--gold-2);
  stroke-width: 2;
}

.environment-svg__source-core {
  fill: var(--gold-2);
}

.environment-svg__source:hover > circle:first-child,
.environment-svg__source:focus > circle:first-child,
.environment-svg__source.is-selected > circle:first-child {
  fill: var(--gold-soft);
  stroke: var(--gold);
}

.environment-svg__contact rect {
  fill: color-mix(in srgb, var(--gold) 8%, transparent);
  stroke: var(--gold);
  stroke-width: 1.5;
}

.environment-svg__contact:hover rect,
.environment-svg__contact:focus rect {
  fill: var(--gold-soft);
  stroke-width: 2;
}

.environment-svg__service-lines {
  fill: none;
  stroke: var(--gold-2);
  stroke-linecap: round;
  stroke-width: 1.5;
}

.environment-svg__label {
  fill: var(--ink);
  font: 13px var(--font-mono);
}

.environment-svg__meta {
  fill: var(--ink-muted);
  font: 11px var(--font-mono);
}

.environment-svg__source:focus-visible,
.environment-svg__contact:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 4px;
}

.environment-graph__empty {
  margin: 0.5rem 0 0.1rem;
  color: var(--ink-muted);
  font-size: var(--fs-sm);
}

.environment-graph__bounds {
  margin: 0.45rem 0 0.1rem;
  color: var(--ink-muted);
  font: var(--fs-2xs) var(--font-mono);
  line-height: 1.45;
}

.environment-inventory {
  min-width: 0;
  margin-top: 0.8rem;
  padding-top: 0.8rem;
  border-top: 1px solid var(--border);
}

.environment-subhead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6rem;
  flex-wrap: wrap;
}

.environment-subhead h3 {
  margin: 0;
  font-size: var(--fs-sm);
}

.environment-inventory > .body-note {
  margin: 0.35rem 0 0.65rem;
}

.environment-inventory__list {
  display: grid;
  gap: 0.45rem;
  list-style: none;
  padding: 0;
  margin: 0;
}

.environment-inventory__entry {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: 0.7rem;
  width: 100%;
  padding: 0.65rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  background: rgba(255, 255, 255, 0.025);
  color: var(--ink);
  text-align: left;
  font: inherit;
  cursor: pointer;
}

.environment-inventory__entry:hover,
.environment-inventory__entry:focus-visible,
.environment-inventory__entry.is-selected {
  border-color: var(--gold);
  background: var(--gold-soft);
}

.environment-inventory__entry:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

.environment-inventory__icon {
  display: grid;
  place-items: center;
  width: 1.7rem;
  height: 1.7rem;
  color: var(--gold-2);
}

.environment-inventory__icon svg {
  width: 20px;
  height: 20px;
}

.environment-inventory__copy {
  display: grid;
  min-width: 0;
  gap: 0.2rem;
}

.environment-inventory__copy strong {
  font-size: var(--fs-sm);
}

.environment-inventory__description,
.environment-inventory__slot,
.environment-inventory__address {
  color: var(--ink-dim);
  font-size: var(--fs-2xs);
  line-height: 1.4;
}

.environment-inventory__slot {
  color: var(--gold-2);
}

.environment-inventory__address {
  overflow-wrap: anywhere;
}

.environment-list {
  margin-top: 0.8rem;
  padding-top: 0.8rem;
  border-top: 1px solid var(--border);
}

.environment-list > summary {
  color: var(--gold-2);
  font-size: var(--fs-sm);
  cursor: pointer;
}

.environment-list > .body-note {
  margin: 0.55rem 0;
}

.environment-list__rows {
  display: grid;
  gap: 0.45rem;
  list-style: none;
  padding: 0;
  margin: 0;
}

.environment-list__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 0.45rem 0.7rem;
  min-width: 0;
  padding: 0.5rem;
  border: 1px solid var(--border);
  border-radius: 0.45rem;
}

.environment-list__contact {
  display: grid;
  grid-template-columns: auto minmax(6rem, 0.7fr) auto minmax(8rem, 1fr) minmax(0, 1.3fr) auto;
  align-items: center;
  gap: 0.35rem 0.55rem;
  min-width: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--ink);
  text-align: left;
  font: inherit;
  cursor: pointer;
}

.environment-list__contact:hover,
.environment-list__contact:focus-visible {
  color: var(--gold-2);
}

.environment-list__contact:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

.environment-list__time,
.environment-list__id {
  color: var(--ink-muted);
  font: var(--fs-2xs) var(--font-mono);
}

.environment-list__source,
.environment-list__service,
.environment-list__request {
  min-width: 0;
  overflow-wrap: anywhere;
  font-size: var(--fs-2xs);
}

.environment-list__arrow {
  color: var(--gold-2);
}

.environment-list__service {
  display: grid;
  gap: 0.1rem;
}

.environment-list__identity {
  color: var(--ink-muted);
  font-size: var(--fs-2xs);
}

.environment-list__business { color: var(--gold-2); font-size: var(--fs-2xs); }
.environment-list__business-note { color: var(--ink-muted); font-size: var(--fs-2xs); }

.environment-list__link {
  align-self: center;
  color: var(--gold-2);
  font-size: var(--fs-2xs);
  white-space: nowrap;
}

.environment-list__link:hover,
.environment-list__link:focus-visible {
  text-decoration: underline;
}

.environment-list__omitted {
  color: var(--ink-muted);
  font-size: var(--fs-2xs);
}

.constellation-lens {
  min-width: 0;
  padding: 1rem;
  border: 1px solid var(--border-gold);
  border-radius: var(--radius);
  background: linear-gradient(145deg, var(--card-from), var(--card-to));
  box-shadow: var(--shadow-gold);
  overflow-wrap: anywhere;
}

.constellation-lens__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.8rem;
  flex-wrap: wrap;
  margin-bottom: 0.65rem;
}

.constellation-lens__eyebrow {
  margin: 0;
  color: var(--gold-2);
  font: 600 var(--fs-2xs) var(--font-mono);
  letter-spacing: 0.16em;
}

.constellation-lens__title {
  margin: 0.2rem 0 0;
  font-size: var(--fs-md);
}

.constellation-lens__head > .body-note {
  margin: 0;
  color: var(--ink-dim);
}

.constellation-summary {
  margin: 0 0 0.75rem;
  color: var(--ink-dim);
  font: var(--fs-2xs) var(--font-mono);
  line-height: 1.5;
}

.constellation-graph {
  min-width: 0;
  max-width: 100%;
  margin: 0.75rem 0;
  padding: 0.55rem;
  border: 1px solid var(--border);
  border-radius: 0.65rem;
  background: rgba(0, 0, 0, 0.22);
  overflow-x: auto;
}

.constellation-graph__svg {
  display: block;
  width: 100%;
  min-width: 680px;
  height: auto;
}

.constellation-svg__column-label {
  fill: var(--ink-muted);
  font: 600 11px var(--font-mono);
  letter-spacing: 0.12em;
}

.constellation-svg__link {
  fill: none;
  stroke: var(--gold-2);
  stroke-dasharray: 5 4;
  stroke-linecap: round;
  stroke-width: 1.5;
  opacity: 0.5;
}

.constellation-svg__link.is-selected {
  stroke: var(--gold);
  stroke-width: 2.5;
  opacity: 0.95;
}

.constellation-svg__node {
  outline: none;
  cursor: pointer;
}

.constellation-svg__group > rect {
  fill: color-mix(in srgb, var(--gold) 8%, transparent);
  stroke: var(--gold);
  stroke-width: 1.5;
}

.constellation-svg__group:hover > rect,
.constellation-svg__group:focus > rect,
.constellation-svg__group.is-selected > rect {
  fill: var(--gold-soft);
  stroke-width: 2;
}

.constellation-svg__group-core,
.constellation-svg__member-core {
  fill: var(--gold-2);
}

.constellation-svg__member > circle:first-child {
  fill: var(--bg);
  stroke: var(--ink-dim);
  stroke-width: 2;
}

.constellation-svg__member:hover > circle:first-child,
.constellation-svg__member:focus > circle:first-child,
.constellation-svg__member.is-group-selected > circle:first-child,
.constellation-svg__member.is-selected > circle:first-child {
  fill: var(--gold-soft);
  stroke: var(--gold);
}

.constellation-svg__group:focus-visible,
.constellation-svg__member:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 4px;
}

.constellation-svg__label {
  fill: var(--ink);
  font: 13px var(--font-mono);
}

.constellation-svg__meta {
  fill: var(--ink-muted);
  font: 11px var(--font-mono);
}

.constellation__bounds {
  margin: 0.45rem 0 0.8rem;
  color: var(--ink-muted);
  font: var(--fs-2xs) var(--font-mono);
  line-height: 1.45;
}

.constellation-groups {
  display: grid;
  gap: 0.7rem;
}

.constellation-group {
  min-width: 0;
  padding: 0.8rem;
  border: 1px solid var(--border);
  border-radius: 0.65rem;
  background: rgba(0, 0, 0, 0.15);
}

.constellation-group.is-selected {
  border-color: var(--gold);
  background: var(--gold-soft);
}

.constellation-group__select {
  display: grid;
  gap: 0.2rem;
  width: 100%;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--ink);
  text-align: left;
  font: inherit;
  cursor: pointer;
}

.constellation-group__select:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 3px;
}

.constellation-group__title {
  color: var(--gold-2);
  font-weight: 700;
}

.constellation-group__summary,
.constellation-group__bound {
  color: var(--ink-dim);
  font: var(--fs-2xs) var(--font-mono);
}

.constellation-group__bound {
  color: var(--gold-2);
}

.constellation-group > .body-note {
  margin: 0.55rem 0;
}

.constellation-group__contacts,
.constellation-group__member-list {
  margin-top: 0.7rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--border);
}

.constellation-group__contacts ul,
.constellation-group__members,
.constellation-group__relationships ul {
  display: grid;
  gap: 0.35rem;
  padding-left: 1.1rem;
}

.constellation-group__contacts li,
.constellation-relationship {
  color: var(--ink-dim);
  font-size: var(--fs-2xs);
  line-height: 1.45;
}

.constellation-group__contacts li.is-selected {
  color: var(--gold-2);
  font-weight: 600;
}

.constellation-group__members {
  list-style: none;
  padding: 0;
  margin: 0.45rem 0 0;
}

.constellation-group__members > li {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  padding: 0.45rem 0;
  border-bottom: 1px solid var(--border);
}

.constellation-member {
  display: grid;
  gap: 0.15rem;
  min-width: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--ink);
  text-align: left;
  font: inherit;
  cursor: pointer;
}

.constellation-member:hover,
.constellation-member:focus-visible,
.constellation-member.is-selected {
  color: var(--gold-2);
}

.constellation-member:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

.constellation-member__source,
.constellation-member__detail,
.constellation-member__dossier {
  min-width: 0;
  overflow-wrap: anywhere;
  font-size: var(--fs-2xs);
}

.constellation-member__detail,
.constellation-member__dossier,
.constellation-relationship__evidence {
  color: var(--ink-muted);
}

.constellation-member__dossier {
  color: var(--gold-2);
  white-space: nowrap;
}

.constellation-member__dossier:hover,
.constellation-member__dossier:focus-visible {
  text-decoration: underline;
}

.constellation-group__relationships {
  margin-top: 0.7rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--border);
}

.constellation-group__relationships > summary,
.constellation-limitations > summary {
  color: var(--gold-2);
  cursor: pointer;
}

.constellation-relationship__title {
  margin: 0;
  color: var(--ink);
  font-weight: 600;
}

.constellation-relationship__features {
  margin-block: 0.3rem;
}

.constellation-relationship__evidence ul {
  margin: 0.2rem 0 0;
}

.constellation-relationship__evidence a {
  color: var(--gold-2);
}

.constellation-relationship__evidence a:hover,
.constellation-relationship__evidence a:focus-visible {
  text-decoration: underline;
}

.constellation-limitations {
  margin-top: 0.8rem;
  padding-top: 0.8rem;
  border-top: 1px solid var(--border);
}

@media (max-width: 640px) {
  .command-lens-selector {
    width: 100%;
  }

  .command-lens-tab {
    flex: 1 1 0;
    min-width: 0;
  }

  .environment-lens__head > .body-note {
    width: 100%;
  }

  .constellation-lens__head > .body-note {
    width: 100%;
  }

  .environment-list__row {
    grid-template-columns: minmax(0, 1fr);
  }

  .environment-list__contact {
    grid-template-columns: auto minmax(0, 1fr) auto;
  }

  .environment-list__time {
    grid-column: 1 / -1;
  }

  .environment-list__source {
    grid-column: 1 / 2;
  }

  .environment-list__arrow {
    grid-column: 2 / 3;
  }

  .environment-list__service {
    grid-column: 1 / -1;
  }

  .environment-list__request,
  .environment-list__id {
    grid-column: 1 / -1;
  }

  .environment-list__link {
    justify-self: start;
  }

  .constellation-group__members > li {
    grid-template-columns: minmax(0, 1fr);
  }

  .constellation-member__dossier {
    justify-self: start;
  }
}

.command-sessions {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: 0.45rem;
}

.command-session {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.2fr) minmax(0, 1fr);
  gap: 0.3rem 0.7rem;
  width: 100%;
  padding: 0.7rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  color: var(--ink);
  text-align: left;
  font: inherit;
  cursor: pointer;
  overflow-wrap: anywhere;
}

.command-session:hover {
  border-color: var(--gold-2);
}

.command-session[aria-pressed="true"] {
  border-color: var(--gold);
  background: color-mix(in srgb, var(--gold) 3.5%, transparent);
}

.command-session:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

.command-cell {
  min-width: 0;
}

.command-cell__label {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  color: var(--ink-muted);
  font-size: var(--fs-2xs);
}

.command-cell__label svg {
  width: 13px;
  height: 13px;
  flex: none;
}

.command-cell__value {
  display: block;
  font-size: var(--fs-sm);
  line-height: 1.4;
}

.command-session__signal {
  grid-column: 1 / -1;
  color: var(--ink-dim);
  font-size: var(--fs-2xs);
}

.command-all-sessions {
  display: inline-block;
  margin-top: 0.8rem;
  font-size: var(--fs-sm);
  color: var(--gold-2);
}

.command-rail {
  grid-template-columns: minmax(0, 1fr);
  min-width: 0;
  display: grid;
  gap: 0.6rem;
}

.command-rail .section {
  margin: 0;
}

.command-rail .section__head {
  margin-bottom: 0.25rem;
}

@media (max-height: 820px) {
  /* A shorter viewport (the mandated 1280×800 first-viewport check) gets a denser rail so the
     "Next review action" card's bottom edge stays inside the first screen without touching its
     content or reordering it — ticket 004's repair. Taller viewports keep the roomier defaults
     above (and the matching block on .command-heading/.command-page > .section rules). */
  .command-rail {
    gap: 0.2rem;
  }

  .command-rail .section__head {
    margin-bottom: 0.1rem;
  }
}

.command-rail .card {
  background: none;
  border: 0;
  padding: 0;
  box-shadow: none;
}

.command-rail .briefing-prose, .command-rail .briefing-event__detail {
  font-size: var(--fs-sm);
  line-height: 1.45;
  color: var(--ink-dim);
}

.command-rail .briefing-event {
  padding: 0;
  border: 0;
}

.command-rail .overview-action {
  white-space: normal;
  text-align: left;
  justify-content: flex-start;
  border-color: var(--gold-2);
}

.command-evidence__label {
  font-size: var(--fs-sm);
  color: var(--ink);
  margin: 0;
}

.command-evidence-link {
  color: var(--gold-2);
  font-size: var(--fs-sm);
}

.command-rail .body-note {
  margin: 0.25rem 0;
}

.command-page .overview-outcomes {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.command-page .overview-outcome {
  padding: 0.8rem 1rem;
}

.command-page .overview-outcome .stat-tile__value {
  font-size: var(--fs-xl);
}

.command-definitions {
  margin-top: 0.6rem;
  font-size: var(--fs-sm);
  color: var(--ink-dim);
}

.command-definitions summary {
  cursor: pointer;
}

@media (max-width: 860px) {
  .command-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .command-page .overview-outcomes {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

}
@media (max-width: 560px) {
  .command-session {
    grid-template-columns: minmax(0, 1fr);
  }

  .command-page .overview-posture__headline {
    flex-basis: auto;
  }

  .command-page .overview-outcomes {
    grid-template-columns: minmax(0, 1fr);
  }

  .command-page .section__head {
    flex-wrap: wrap;
  }

}

/* ---- Executive briefing -------------------------------------------------------------------- */

.briefing-page {
  display: flex;
  flex-direction: column;
}

.briefing-report-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 1.2rem;
  margin-bottom: 0.9rem;
}

.briefing-report-mark {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gold);
  font-weight: 700;
}

.briefing-report-title {
  margin: 0.2rem 0 0.15rem;
}

.briefing-report-sub {
  margin: 0;
  font-size: var(--fs-xs);
  color: var(--ink-muted);
}

.briefing-report-meta {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: auto auto;
  gap: 0.2rem 0.9rem;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
}

.briefing-report-meta__row {
  display: contents;
}

.briefing-report-meta__label {
  color: var(--ink-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: var(--fs-2xs);
  align-self: baseline;
}

.briefing-report-meta__value {
  color: var(--ink);
}

.briefing-actions {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin: 0.9rem 0;
}

.briefing-purpose {
  margin: 0 0 0.4rem;
  max-width: 62rem;
}

.briefing-headline {
  font-size: var(--fs-md);
  line-height: 1.45;
  color: var(--ink);
  margin: 0 0 1rem;
  max-width: 54rem;
}

.briefing-prose {
  margin: 0;
  line-height: 1.55;
  color: var(--ink);
}

.briefing-prose--risk {
  font-weight: 500;
}

.briefing-rating {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.6rem;
}

.briefing-rating__badge {
  font-size: var(--fs-xs);
  padding: 0.3rem 0.9rem;
}

.briefing-confidence {
  margin: 0;
}

/* Reuses .stat-tile for layout (same pattern as overview.js's .overview-outcome) — the ATT&CK/
   bait-coverage outcome tiles are deep links, unlike the plain-`<div>` KPI tiles above them. */
.briefing-kpi-link {
  display: block;
  text-decoration: none;
  color: inherit;
}

.briefing-kpi-link:hover,
.briefing-kpi-link:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

.briefing-events {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
}

.briefing-event {
  padding: 0;
  border-radius: 0.55rem;
  border: 1px solid var(--border-strong);
  background: var(--glass);
  overflow: hidden;
}

.briefing-event__link {
  display: block;
  padding: 0.55rem 0.7rem;
  text-decoration: none;
  color: inherit;
}

.briefing-event__link:hover,
.briefing-event__link:focus-visible {
  background: var(--gold-soft);
  outline: none;
}

.briefing-event__title {
  font-weight: 600;
  font-size: var(--fs-sm);
  margin-bottom: 0.2rem;
}

/* The notable-engagement evidence line (method/path/timestamp) in the board-ready Briefing —
   critical evidence, not chrome (ticket 032); also added to the print override list below, which
   had missed it (--ink-muted's light gray was unreadable on printed white paper). */
.briefing-event__detail {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--ink-dim);
}

.briefing-event--hackback {
  border-color: color-mix(in srgb, var(--gold) 45%, transparent);
}

.briefing-event--errors {
  border-color: rgba(255, 120, 90, 0.4);
}

.briefing-followups {
  margin: 0;
  padding-left: 1.3rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.briefing-followup {
  line-height: 1.5;
}

.briefing-followup__link {
  color: var(--ink);
}

.briefing-followup__link:hover,
.briefing-followup__link:focus-visible {
  color: var(--gold);
}

/* A print-safe list of stable identifiers (session id / client IP / UTC timestamp) for the items
   in "Notable observations" above — for a paper/PDF copy where a deep link can't be clicked. */
.briefing-appendix {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
}

.briefing-appendix__row {
  display: flex;
  gap: 0.6rem;
  align-items: baseline;
}

.briefing-appendix__ref {
  color: var(--ink-muted);
  flex: 0 0 auto;
}

.briefing-appendix__detail {
  color: var(--ink);
}

.briefing-footer {
  margin-top: 2rem;
  padding-top: 1rem;
  border-top: 1px solid var(--border);
}

.briefing-footer__text {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--ink-muted);
}

/* 390px: the report head's brand/title and its meta list used to share one baseline flex row and
   the print button used to compete with the purpose paragraph in the same row — both now stack. */
@media (max-width: 560px) {
  .briefing-report-head {
    flex-direction: column;
  }

  .briefing-report-meta {
    grid-template-columns: auto 1fr;
    width: 100%;
  }

  .briefing-print-btn {
    width: 100%;
  }
}

/* Print-only content (ticket 984): the briefing's report title and identity block exist for the printed
   copy; on screen the page head's title stands alone. Print media leaves them as their own rules draw them. */
@media screen {
  .print-only {
    display: none !important;
  }
}

/* Print / Save as PDF — hide chrome, keep the report legible, identity-marked, and evidence-backed
   on white paper. `size: auto` (never a forced Letter or A4) plus mm margins is what keeps one
   stylesheet safe on both page sizes. */
@media print {
  @page {
    size: auto;
    margin: 14mm;
  }

  body {
    background: #fff !important;
    color: #111 !important;
    /* The drawer is hidden below, but body.has-wire-console-open can still be set (the operator
       printed while it was open) — its padding-bottom rule targets body directly now, so print
       must reset it explicitly rather than relying on the drawer's own display:none. */
    padding-bottom: 0 !important;
  }

  #particle-field,
  .navbar,
  .hero,
  .capture-context,
  .exec-journey,
  .no-print,
  .toast,
  #shortcuts-overlay,
  #detail-overlay,
  .wire-console {
    display: none !important;
  }

  .view-root {
    padding: 0 !important;
    max-width: none !important;
  }

  /* Reserve room at the bottom of every page for the fixed, repeating footer below. */
  .briefing-page {
    padding-bottom: 2.2rem;
  }

  .briefing-page .section__title,
  .briefing-report-mark,
  .briefing-report-sub,
  .briefing-report-meta__label,
  .briefing-report-meta__value,
  .briefing-headline,
  .briefing-prose,
  .briefing-confidence,
  .briefing-event__title,
  .briefing-event__detail,
  .briefing-followup__link,
  .briefing-appendix__ref,
  .briefing-appendix__detail,
  .stat-tile__label,
  .stat-tile__value,
  .stat-tile__sub,
  .section__hint {
    color: #111 !important;
  }

  .stat-tile,
  .briefing-card,
  .briefing-event,
  .card {
    background: #fff !important;
    border: 1px solid #ccc !important;
    box-shadow: none !important;
    break-inside: avoid;
    page-break-inside: avoid;
  }

  .briefing-section,
  .briefing-hero-block,
  .briefing-kpi-band,
  .briefing-rating,
  .briefing-report-head {
    break-inside: avoid;
    page-break-inside: avoid;
  }

  /* A section heading must never be stranded alone at the bottom of a page. */
  .briefing-section .section__head,
  .briefing-report-title {
    break-after: avoid;
    page-break-after: avoid;
  }

  .stat-tile__value.is-accent {
    color: #000 !important;
  }

  .briefing-event--hackback,
  .briefing-event--errors,
  .briefing-event--probe {
    border-color: #888 !important;
  }

  .briefing-event__link,
  .briefing-followup__link,
  .briefing-kpi-link {
    text-decoration: none !important;
  }

  .briefing-footer {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    margin: 0;
    padding: 0.3rem 0;
    border-top: 1px solid #ccc;
    background: #fff;
  }

  .briefing-footer__text {
    color: #555 !important;
  }
}

/* Command rail: the exchange chooser stays inside its column. */
.command-rail select { max-width: 100%; min-width: 0; }

/* Geographic Command lenses: bounded canvas, wrapping controls and inert evidence text. */
.geography { min-width: 0; overflow-wrap: anywhere; }
.geography h3 { color: var(--gold-2); margin: 0 0 0.5rem; }
.geography-stage {
  width: 100%; min-width: 0; aspect-ratio: 1.65;
  border: 1px solid var(--border); border-radius: 0.8rem; overflow: hidden;
  background: radial-gradient(ellipse at 50% 48%, #173544 0, #07141f 43%, #050d15 73%);
}
.geography-canvas { display: block; width: 100%; height: 100%; max-width: 100%; }
/* Ticket 929: contacts were recorded but none can be placed. The map stays, dimmed, under a centred
   opaque note (ticket 973: text is never on glass) that says why; the note itself takes no pointer events beyond its own text. */
.geography-map { position: relative; min-width: 0; }
.geography-stage--empty .geography-canvas { opacity: 0.35; filter: saturate(0.6); }
.geography-empty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: 0.6rem; pointer-events: none; }
.geography-empty__note {
  box-sizing: border-box; max-width: min(36rem, 100%); max-height: 100%; overflow: auto; pointer-events: auto;
  padding: 0.7rem 1rem; text-align: center; color: var(--ink); font-size: var(--fs-xs);
  background: var(--panel); border: 1px solid var(--border); border-radius: 0.6rem;
}
.geography-empty__note p { margin: 0.25rem 0; }
.geography-empty__title { color: var(--gold-2); font-weight: 600; }
.geography-empty__cause { color: var(--ink-dim); }
.geography-controls { display: flex; flex-wrap: wrap; gap: 0.45rem; align-items: center; margin: 0.65rem 0; min-width: 0; }
.geography-controls[hidden] { display: none; }
.geography-controls .btn { white-space: normal; max-width: 100%; }
.geography-counts, .geography-legend, .geography-selection { margin: 0.65rem 0; }
.geography-evidence { border-top: 1px solid var(--border); margin-top: 1rem; padding-top: 0.8rem; }
.geography-evidence summary { cursor: pointer; color: var(--gold-2); }
.geography-evidence ol { padding-left: 1.5rem; }
.geography-evidence li { margin: 0.8rem 0; }
.geography-contact { display: block; width: 100%; padding: 0.6rem; text-align: left; color: var(--ink); background: #10212d; border: 1px solid var(--border); border-radius: 0.4rem; overflow-wrap: anywhere; }
.geography-contact[aria-pressed="true"] { border-color: var(--gold); }
.geography button:focus-visible, .geography summary:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }
@media (max-width: 760px) {
  .command-lens-selector { flex-wrap: wrap; }
  .geography-controls .btn { flex: 1 1 auto; }
  .geography-stage { border-radius: 0.4rem; }
}

/* Shared Command replay controls stay usable beneath every visualization at mobile widths. */
.command-timeline {
  padding: 16px;
  margin-top: 16px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel);
  min-width: 0;
  overflow-wrap: anywhere;
}
.command-timeline__mode,
.command-replay-label { color: var(--gold); font-weight: 700; overflow-wrap: anywhere; }
.command-timeline__actions,
.command-timeline__playback { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; }
.command-timeline button { white-space: normal; max-width: 100%; }
.command-timeline select { width: 100%; min-width: 0; max-width: 100%; }
.command-timeline__playback select { width: auto; }
.command-timeline__scrub { display: block; width: 100%; margin: 16px 0; }
.command-timeline__time,
.command-timeline__note { overflow-wrap: anywhere; }
.command-leaves-cursor { font-size: var(--fs-2xs); font-weight: 400; }

/* Board reuses Command renderers, with details expanded on demand. */
.presentation-toolbar {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: 0 0 8px;
}
.presentation-toolbar > * { min-width: 0; max-width: 100%; white-space: normal; }
.presentation-content, .has-board, .command-board { min-width: 0; }
.command-print-summary { display: none; }
body.is-board .hero, body.is-board .capture-context, body.is-board .exec-journey, body.is-board .api-status, body.is-board .wire-console { display: none; }
body.is-board .view-root { padding-top: 8px; padding-bottom: 12px; }
.command-board > .section { margin: 8px 0; }
.command-board .section__head { margin: 0 0 4px; }
.command-board .overview-outcomes { gap: 10px; }
.command-board .overview-outcomes .stat-tile { padding: 6px 12px; display: grid; grid-template-columns: 1fr auto; align-items: center; }
.command-board .overview-outcomes .stat-tile__value { font-size: 2rem; line-height: 1.2; margin: 0; }
.command-board .overview-outcomes .stat-tile__sub { display: none; }
.command-board .overview-outcomes .command-leaves-cursor { font-size: var(--fs-2xs); grid-column: 1 / -1; }
.command-board .command-definitions { margin: 4px 0; }
.command-board .command-grid { grid-template-columns: minmax(0, 1.8fr) minmax(300px, 1fr); gap: 18px; }
.command-board .command-visualization > .command-replay-label,
.command-board .command-rail > .command-replay-label { display: none; }
.command-board .command-lens-selector { margin: 0 0 8px; }
.command-board .command-rail { display: block; }
.command-board .command-rail > .section { margin: 0 0 12px; }
.command-board .command-rail .card { padding: 0; }
.command-board .command-rail p { margin: 4px 0; }
.command-board .geography h3 { margin: 0 0 4px; }
.command-board .geography-counts { margin: 4px 0; }
.command-board .geography-stage { height: clamp(160px, 22vh, 240px); aspect-ratio: auto; }
.command-board .geography-selection { margin: 4px 0; }
.command-board .command-timeline { padding: 10px; }
.command-board .command-timeline__playback { margin: 8px 0; }
.command-board .command-timeline__scrub { margin: 8px 0; }
.command-board .command-timeline__more, .board-playback, .board-evidence, .board-assessment { margin-top: 8px; overflow-wrap: anywhere; }
body.is-kiosk .presentation-toolbar { position: sticky; top: 0; z-index: 5; background: var(--bg); padding: 4px 0; }
@media (max-width: 760px) {
  .command-board .command-grid { grid-template-columns: minmax(0, 1fr); }
  .command-board .geography-stage { height: 220px; }
}
@media print {
  .command-visualization, .command-rail select, .board-playback { display: none !important; }
  .command-print-summary { display: block; }
  .command-grid { display: block !important; }
  .command-page, .command-page * { color: #111 !important; background: #fff !important; box-shadow: none !important; }
  .command-page a { text-decoration: underline; }
  .command-page .section, .command-print-summary { break-inside: avoid; }
  .command-page details > *, .command-board .overview-outcomes .stat-tile__sub { display: block; }
}

/* Selected captured reconstruction: source is inert text, bounded and never a live preview. */
.attacker-view-host { min-width: 0; max-width: 100%; margin-top: 1rem; }
.attacker-view { padding: 1rem; border: 1px solid var(--border); border-radius: 8px; min-width: 0; overflow-wrap: anywhere; }
.attacker-view h3 { margin: 0 0 .75rem; font-size: 1rem; }
.attacker-view h4 { margin: 0 0 .75rem; font-size: .95rem; }
.attacker-view__pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); gap: 1rem; }
.attacker-view__column { min-width: 0; padding-top: 1rem; }
.attacker-view__fact { border-top: 1px solid var(--border); padding: .65rem 0; }
.attacker-view__fact strong { font-size: .82rem; }
.attacker-view__fact p { margin: .35rem 0; }
.attacker-view__ref { display: inline-block; font-size: var(--fs-2xs); overflow-wrap: anywhere; }
.attacker-view__source { white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; max-height: 16rem; overflow: auto; margin: .5rem 0; font-size: var(--fs-2xs); }
.attacker-view__controls { margin: .75rem 0; }
@media print {
  .attacker-view--revealed .attacker-view__pair { display: none; }
  .attacker-view--revealed::after { content: "Captured content hidden in print; inspect the exact exchange in the authenticated console."; }
}

/* ---- Investigation graph (ticket 015-20260916195518, review round 1) --------------------- */
.investigation-graph { min-width: 0; max-width: 100%; overflow-x: auto; }
.investigation-graph__svg { display: block; width: 100%; min-width: 680px; height: auto; }
.investigation-graph__edge-line { stroke: var(--gold-2); stroke-width: 1.5; opacity: 0.55; }
/* Ticket 929: the edge label and the column heading take their text-anchor from the SVG attribute
   (views/graph_pane.js sets start, middle or end per label); a text-anchor property here would beat it.
   The halo (a stroke painted under the fill, in the page background) keeps a line from reading as
   crossing the label. */
.investigation-graph__edge-label { fill: var(--ink-muted); font-family: var(--font-mono); font-size: var(--fs-2xs); paint-order: stroke; stroke: var(--bg); stroke-width: 4px; stroke-linejoin: round; }
.investigation-graph__node-label { fill: var(--ink); font-family: var(--font-mono); font-size: var(--fs-xs); text-anchor: middle; }
.investigation-graph__node-marker { fill: var(--gold); font-family: var(--font-mono); font-weight: 600; font-size: var(--fs-2xs); letter-spacing: 0.08em; text-anchor: middle; }
.investigation-graph__column-label { fill: var(--ink-muted); font-family: var(--font-mono); font-weight: 600; font-size: var(--fs-2xs); letter-spacing: 0.12em; }
.investigation-graph__legend { margin-top: 0.6rem; }
.investigation-graph__legend-list { display: flex; flex-wrap: wrap; gap: 0.6rem 1.1rem; margin: 0; padding: 0; list-style: none; }
.investigation-graph__legend-item { display: inline-flex; align-items: center; gap: 0.35rem; color: var(--ink-dim); font-size: var(--fs-2xs); }
.investigation-graph__legend-swatch { width: 14px; height: 14px; flex: none; }
.investigation-graph__legend-marker { color: var(--gold); font-family: var(--font-mono); font-weight: 600; font-size: var(--fs-2xs); letter-spacing: 0.08em; }
.investigation-graph-pane { min-height: 220px; padding: 0.4rem 0; }
.investigation-graph-pane > .body-note { color: var(--ink-muted); }
.investigation-graph-pane > .error-state { color: var(--ink-dim); }
.investigation-graph__edge { opacity: 0.85; transition: opacity 0.15s ease; }
.investigation-graph__edge:hover { opacity: 1; }
.investigation-graph__node { cursor: default; }
.investigation-graph__node--linked { cursor: pointer; }
.investigation-graph__node-circle { stroke: var(--bg); stroke-width: 1.5px; transition: stroke 0.15s ease; }
.investigation-graph__node:hover .investigation-graph__node-circle { stroke: var(--gold); }
/* ---- Cross-session induced edges/peer nodes (ticket 003-20260917100059) ------------------ */
.graph-edge--cross-session { stroke-dasharray: 4 3; }
.graph-node--peer .investigation-graph__node-circle { stroke: var(--ink-dim); stroke-dasharray: 2 1.5; }
@media (prefers-reduced-motion: reduce) {
  .investigation-graph__edge,
  .investigation-graph__node-circle { transition: none; }
}
