
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/ibm-plex-mono-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/static/fonts/ibm-plex-mono-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/static/fonts/ibm-plex-mono-600-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/ibm-plex-mono-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/static/fonts/ibm-plex-mono-500-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/static/fonts/ibm-plex-mono-600-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/static/fonts/inter-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/static/fonts/inter-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('/static/fonts/space-grotesk-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('/static/fonts/space-grotesk-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ==========================================================================
   Abuse Radar design system
   Tokens -> primitives -> components -> layout -> responsive.
   ========================================================================== */
:root {
  color-scheme: dark;

  /* Surfaces. Steps between them are deliberately tiny (1.05-1.09:1) - panel
     separation is carried by the 1px border, not a luminance jump. That is
     what keeps the surface reading as thin technical structure, and it avoids
     the gradient banding a large soft fill produces on a dark ground. */
  --bg: #05080c;
  --bg-deep: #03060a;          /* status rail, footer: recedes below the ground */
  --surface: #0b1017;          /* panel body */
  --surface-2: #101823;        /* raised: inputs, rows, tiles, chips */
  --surface-3: #16202c;        /* hover / active row, meter tracks */
  --plot: #070c12;             /* chart wells and the map well */

  /* Lines. --line/-2/-3 are decorative separators and are exempt from WCAG
     1.4.11; every boundary a user must see to identify a CONTROL uses
     --line-ui, which clears 3:1 on all five surfaces. */
  --line: rgba(150, 180, 210, .10);
  --line-2: rgba(150, 180, 210, .18);
  --line-3: rgba(150, 180, 210, .30);
  --line-strong: rgba(150, 180, 210, .30);
  --line-ui: #5c7080;

  /* Ink. --text-4 is the dimmest value permitted anywhere and still clears
     4.5:1 on every surface (4.65:1 on --surface-3). */
  --text: #e8eef6;
  --text-2: #a9b8c8;
  --text-3: #8494a6;
  --text-4: #7a8a9c;

  /* Brand */
  --accent: #4fd1c5;
  --accent-dim: #2c9d95;
  --accent-ink: #04191c;
  --gold: #d7a64a;

  /* Status (always paired with a text label, never colour alone) */
  --sev-critical: #ff6b6b;
  --sev-high: #ff9f5a;
  --sev-medium: #ddb050;
  --sev-low: #8fa3b6;
  --ok: #43d6a5;
  --warn: #e3b552;
  --danger: #ff7a70;
  --neutral: #9aabbd;

  /* Categorical series - fixed order, never cycled */
  --cat-ssh: #7087d5;
  --cat-mail: #05a388;
  --cat-leak: #b68028;
  --cat-other: #b570af;

  /* Spacing scale (4px base) */
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 20px; --s6: 24px; --s7: 32px; --s8: 44px;

  --radius: 3px;
  --radius-sm: 2px;
  --radius-pill: 999px;
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  --sans: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --display: "Space Grotesk", Inter, ui-sans-serif, system-ui, sans-serif;

  /* Type scale.  Fluid between a 380px phone and a 1440px desktop: the old
     scale was fixed, so a heading sized for a phone had to carry a 1440px
     hero as well and the page read as a console rather than a publication.
     Every step is clamped at both ends so nothing wraps on narrow screens. */
  --t-hero:    clamp(27px, 1.55vw + 21px, 44px);
  --t-page:    clamp(21px, 0.75vw + 18px, 29px);
  --t-section: clamp(17px, 0.38vw + 15.5px, 21px);
  --t-stat:    clamp(26px, 1.10vw + 21px, 38px);
  --maxw: 1360px;
  --maxw-wide: 1760px;

  /* ---- Console layer -------------------------------------------------
     The operations surface sits a step deeper than the document surfaces
     above so panels read as instruments mounted on a chassis rather than
     cards floating on a page. Borders are hairlines: at 1px they already
     carry the structure, so anything heavier just adds noise at density. */
  --tick: rgba(79, 209, 197, .34);         /* corner ticks on a panel */
  --grid-line: rgba(150, 180, 210, .045);  /* background grid texture */
  --grid-step: 32px;

  /* Signal roles. Deliberately reusing the already-validated hues rather
     than minting new ones - green/cyan/amber/red are all present above. */
  --sig-live: var(--ok);
  --sig-watch: var(--warn);
  --sig-alert: var(--danger);
  --sig-idle: var(--text-3);

  /* Telemetry type: mono, tight, tabular. Used for every machine value. */
  --t-micro: 10px;             /* eyebrow / micro-label */
  --t-tele: 12px;              /* inline telemetry value */
  --t-readout: clamp(15px, .45vw + 13.5px, 19px);  /* panel readout */

  /* ---- Motion tokens --------------------------------------------------
     One place to retune the whole system. Durations are deliberately short:
     instrumentation should acknowledge, not perform. */
  --dur-tap: 90ms;             /* press / focus acknowledgement */
  --dur-ui: 160ms;             /* hover, chip, tab */
  --dur-panel: 260ms;          /* panel reveal, counter roll */
  --dur-draw: 620ms;           /* sparkline / path draw-in */
  --dur-arc: 1700ms;           /* source -> hub arc travel */
  --dur-pulse: 2400ms;         /* ambient beacon cycle */
  --ease-out: cubic-bezier(.22, .61, .36, 1);
  --ease-inout: cubic-bezier(.65, 0, .35, 1);
  --stagger: 45ms;
}

/* A single global switch. Every animation in this sheet is written to be
   cancelled here, so reduced-motion support cannot drift out of date as
   components are added. State changes still happen - they just arrive
   instantly instead of being animated. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
}

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font: 15px/1.55 var(--sans);
  color: var(--text);
  background:
    /* Structural grid. Two 1px lines repeated by background-size cost one
       paint on a fixed layer - far cheaper than a tiled image, and it gives
       the surface its instrument-panel structure. */
    linear-gradient(var(--grid-line) 1px, transparent 1px) 0 0 / 100% var(--grid-step),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px) 0 0 / var(--grid-step) 100%,
    radial-gradient(1100px 520px at 15% -8%, rgba(79, 209, 197, .07), transparent 62%),
    radial-gradient(900px 460px at 88% -4%, rgba(215, 166, 74, .06), transparent 58%),
    var(--bg);
  background-attachment: fixed, fixed, scroll, scroll, scroll;
  min-height: 100vh;
  overflow-x: hidden;
}

h1, h2, h3, h4 {
  color: #fff; margin: 0; font-weight: 700;
  font-family: var(--display); letter-spacing: -.015em;
}
h1 { font-size: var(--t-page); line-height: 1.15; letter-spacing: -.022em; }
h2 { font-size: var(--t-section); line-height: 1.22; }
h3 { font-size: 15px; line-height: 1.3; letter-spacing: -.01em; }
p  { margin: 0; color: var(--text-2); }
a  { color: var(--accent); text-underline-offset: 3px; }
a:hover { color: #7ce7dc; }
code, .mono { font-family: var(--mono); font-size: .92em; }

/* Accessibility -------------------------------------------------------- */
.skip-link {
  position: absolute; left: var(--s3); top: -60px; z-index: 100;
  padding: var(--s2) var(--s4); border-radius: var(--radius-sm);
  background: var(--accent); color: var(--accent-ink); font-weight: 700;
  transition: top .15s ease;
}
.skip-link:focus { top: var(--s3); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
/* A <table> ignores width:1px and lays out at min-content, so it must be
   wrapped rather than carry this class itself; clip-path contains anything
   that still overflows. */
.visually-hidden {
  position: absolute !important; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}
.visually-hidden table { position: absolute; }
/* Captions stay in the table's flow but are not painted. */
.visually-hidden-caption {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* Layout --------------------------------------------------------------- */
.wrap { width: 100%; max-width: var(--maxw); margin: 0 auto; padding: 0 var(--s6); }
/* Data-dense pages use the viewport instead of the reading-width cap, while
   keeping comfortable gutters. min() keeps it fluid rather than stepped. */
.wrap-wide { max-width: min(var(--maxw-wide), 100% - 2 * var(--s6)); }
main.wrap { padding-top: var(--s6); padding-bottom: var(--s8); }
.stack > * + * { margin-top: var(--s6); }

/* Header --------------------------------------------------------------- */
.site-header {
  position: sticky; top: 0; z-index: 30;
  background: rgba(8, 13, 19, .88);
  border-bottom: 1px solid var(--line);
  backdrop-filter: blur(14px);
}
.header-inner {
  display: flex; align-items: center; gap: var(--s5);
  min-height: 62px; padding-top: var(--s2); padding-bottom: var(--s2);
}
.brand { display: flex; align-items: center; gap: var(--s3); text-decoration: none; flex: 0 0 auto; }
.brand:hover { color: inherit; }
.mark {
  width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center;
  background: linear-gradient(140deg, var(--accent), var(--gold));
  color: var(--accent-ink); font-weight: 900; font-size: 14px; letter-spacing: -.03em;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .4);
}
.brand-text { display: flex; flex-direction: column; line-height: 1.15; }
.brand-text strong { color: #fff; font-size: 15px; font-weight: 750; }
.brand-text span { color: var(--text-3); font-size: 11px; letter-spacing: .06em; text-transform: uppercase; }

.site-nav { display: flex; align-items: center; gap: var(--s1); margin-left: auto; flex-wrap: wrap; }
.site-nav a, .site-nav button {
  display: inline-flex; align-items: center; gap: var(--s2);
  padding: 8px 11px; border-radius: var(--radius-sm);
  color: var(--text-2); text-decoration: none; font-size: 13.5px; font-weight: 650;
  border: 1px solid transparent; background: transparent; cursor: pointer; font-family: inherit;
}
.site-nav a:hover, .site-nav button:hover { color: #fff; background: var(--surface-2); }
.site-nav a[aria-current="page"] {
  color: #fff; background: var(--surface-2); border-color: var(--line-strong);
}
.nav-toggle { display: none; }

/* Buttons -------------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  min-height: 40px; padding: 0 var(--s4);
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  background: var(--surface-2); color: var(--text);
  font: 650 13.5px/1 var(--sans); text-decoration: none; cursor: pointer;
  transition: background .14s ease, border-color .14s ease;
}
.btn:hover { background: var(--surface-3); border-color: rgba(79, 209, 197, .5); color: #fff; }
.btn[disabled], .btn[aria-disabled="true"] { opacity: .45; pointer-events: none; }
.btn-primary {
  background: linear-gradient(180deg, #5ee1d6, #33bbb0);
  border-color: #5ee1d6; color: var(--accent-ink); font-weight: 750;
}
.btn-primary:hover { background: linear-gradient(180deg, #6ee9de, #3ac6ba); color: var(--accent-ink); }
.btn-sm { min-height: 32px; padding: 0 var(--s3); font-size: 12.5px; }
.btn-ghost { background: transparent; }

/* Forms ---------------------------------------------------------------- */
label { display: block; }
.field-label {
  display: block; margin-bottom: 6px;
  color: var(--text-2); font-size: 11.5px; font-weight: 700;
  letter-spacing: .07em; text-transform: uppercase;
}
input, select, textarea {
  width: 100%; min-height: 40px; padding: 9px 11px;
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  background: #0a1119; color: var(--text); font: 400 14px/1.4 var(--sans);
}
input::placeholder, textarea::placeholder { color: var(--text-3); }
input:focus, select:focus, textarea:focus { border-color: var(--accent-dim); outline-offset: 1px; }
input[aria-invalid="true"] { border-color: var(--danger); }
textarea { min-height: 130px; padding: 11px; resize: vertical; line-height: 1.5; }
select {
  appearance: none; padding-right: 30px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' stroke='%237d8fa1' stroke-width='1.6' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 11px center; background-size: 11px;
}
.field-hint { margin-top: 5px; color: var(--text-3); font-size: 12.5px; }
.field-error { margin-top: 5px; color: #ff9d9d; font-size: 12.5px; font-weight: 600; }

/* Cards / panels ------------------------------------------------------- */
.card {
  min-width: 0;
  /* Hairline + near-square corners + no drop shadow. The old treatment
     (10px radius, 12px/32px shadow on every card) read as SaaS and cost a
     full-size blur per card; structure now comes from the border alone. */
  border: 1px solid var(--line-2); border-radius: var(--radius);
  background: linear-gradient(180deg, var(--surface-2), var(--surface));
  position: relative;
}
.card-pad { padding: var(--s5); }
.card-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s4); flex-wrap: wrap; margin-bottom: var(--s4);
}
.card-head p { font-size: 13px; }
.section-title { display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap; }
.section-title .count { color: var(--text-3); font-size: 13px; font-weight: 600; }

/* Stat tiles ----------------------------------------------------------- */
.stat-row {
  min-width: 0;
  display: grid; gap: var(--s3);
  grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
}
.stat {
  position: relative; overflow: hidden; padding: var(--s4);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: linear-gradient(180deg, var(--surface-2), var(--surface));
}
.stat::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 2px;
  background: linear-gradient(90deg, var(--accent), rgba(215, 166, 74, .8));
}
.stat .stat-label {
  display: block; color: var(--text-3); font-size: 11.5px;
  font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
}
.stat .stat-value {
  display: block; margin-top: 7px; color: #fff;
  font-family: var(--display); font-size: var(--t-stat);
  line-height: 1.05; font-weight: 600; letter-spacing: -.028em;
  font-variant-numeric: tabular-nums;
}
.stat .stat-sub { display: block; margin-top: 5px; color: var(--text-3); font-size: 12.5px; }
.stat.is-alert::before { background: linear-gradient(90deg, var(--sev-high), var(--sev-critical)); }

/* Chips / badges ------------------------------------------------------- */
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 9px; border-radius: var(--radius-pill);
  font-size: 11.5px; font-weight: 700; letter-spacing: .02em; white-space: nowrap;
  border: 1px solid var(--line-strong); background: var(--surface-3); color: var(--text-2);
}
.chip .dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: 0 0 auto; }
.chip-critical { color: #ffb0b0; background: rgba(255, 107, 107, .14); border-color: rgba(255, 107, 107, .42); }
.chip-high     { color: #ffc79c; background: rgba(255, 159, 90, .13); border-color: rgba(255, 159, 90, .40); }
.chip-medium   { color: #f3d38a; background: rgba(221, 176, 80, .13); border-color: rgba(221, 176, 80, .40); }
.chip-low      { color: #c3d2e0; background: rgba(143, 163, 182, .14); border-color: rgba(143, 163, 182, .36); }
.chip-ok       { color: #7fe6d3; background: rgba(63, 191, 159, .13); border-color: rgba(63, 191, 159, .38); }
.chip-warn     { color: #f3d38a; background: rgba(221, 176, 80, .13); border-color: rgba(221, 176, 80, .40); }
.chip-muted    { color: var(--text-3); }
.chip-unlisted { color: #9fe8d9; background: rgba(63, 191, 159, .10); border-color: rgba(63, 191, 159, .30); }

/* Copyable IP ---------------------------------------------------------- */
.ip-cell { display: inline-flex; align-items: center; gap: 5px; }
.ip-value {
  font-family: var(--mono); font-size: 13.5px; font-weight: 650; color: #fff;
  text-decoration: none; border-bottom: 1px solid rgba(79, 209, 197, .45); padding-bottom: 1px;
}
.ip-value:hover { color: var(--accent); border-bottom-color: var(--accent); }
.copy-btn {
  display: inline-grid; place-items: center; width: 24px; height: 24px; padding: 0;
  border: 1px solid transparent; border-radius: 5px; background: transparent;
  color: var(--text-3); cursor: pointer; flex: 0 0 auto;
}
.copy-btn:hover { color: var(--accent); background: var(--surface-3); border-color: var(--line); }
.copy-btn svg { width: 13px; height: 13px; }
.copy-btn.is-done { color: var(--ok); }
/* Text variant. The base .copy-btn is a 24px icon square, so a labelled copy
   button has to opt back out of the fixed box. Same delegate, same behaviour. */
.btn.copy-btn {
  display: inline-flex; align-items: center; width: auto; height: auto;
  padding: 5px var(--s3); border-radius: var(--radius-sm);
  border: 1px solid var(--line-ui); background: var(--surface-2);
  color: var(--text); white-space: nowrap;
}
.btn.copy-btn:hover { color: var(--accent); border-color: var(--accent); background: var(--surface-3); }
.btn.copy-btn.is-done { color: var(--ok); border-color: var(--ok); }

/* Tables --------------------------------------------------------------- */
/* The scroll container must be the element that also carries the rounded
   clip.  Previously .table-wrap had overflow:hidden, so content wider than
   the wrapper was cut off instead of scrolling inside .table-scroll. */
.table-wrap {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface);
}
.table-scroll {
  max-width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  border-radius: inherit;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
  overscroll-behavior-x: contain;
}
.table-scroll::-webkit-scrollbar,
.code-block::-webkit-scrollbar { height: 9px; }
.table-scroll::-webkit-scrollbar-track,
.code-block::-webkit-scrollbar-track { background: transparent; }
.table-scroll::-webkit-scrollbar-thumb,
.code-block::-webkit-scrollbar-thumb {
  background: var(--line-strong); border-radius: var(--radius-pill);
}
.table-scroll::-webkit-scrollbar-thumb:hover,
.code-block::-webkit-scrollbar-thumb:hover { background: var(--text-3); }
table.data { width: 100%; border-collapse: collapse; font-size: 13.5px; }
table.data { table-layout: auto; }
table.data th, table.data td {
  padding: 11px var(--s3); text-align: left; vertical-align: middle;
  border-bottom: 1px solid var(--line); white-space: nowrap;
}
table.data th:first-child, table.data td:first-child { padding-left: var(--s4); }
table.data th:last-child, table.data td:last-child { padding-right: var(--s4); }
table.data th {
  position: sticky; top: 0; z-index: 2;
  background: #0a1017; color: var(--text-2);
  font-size: 11px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
}
table.data tbody tr:last-child td { border-bottom: 0; }
table.data tbody tr:hover { background: rgba(79, 209, 197, .045); }
table.data td.wrap-cell { white-space: normal; min-width: 168px; max-width: 260px; }
/* The timezone hint must not contribute to the column's intrinsic width:
   it is supplementary, and the exact UTC value lives in each cell's title. */
table.data th .tz {
  position: absolute; margin-top: 13px; color: var(--text-3);
  font-size: 9.5px; letter-spacing: .06em; font-weight: 600; pointer-events: none;
}
table.data th { position: sticky; }
table.data th.has-tz { padding-bottom: 18px; }
table.data td.num { text-align: right; font-variant-numeric: tabular-nums; }
table.data th.num { text-align: right; }
.col-sort { display: inline-flex; align-items: center; gap: 5px; color: inherit; text-decoration: none; }
.col-sort:hover { color: #fff; }
.col-sort .arrow { opacity: .35; font-size: 9px; }
.col-sort[aria-sort] .arrow, .col-sort.is-active .arrow { opacity: 1; color: var(--accent); }
.time-cell { display: block; line-height: 1.3; min-width: 108px; }
.time-cell .exact { color: var(--text); font-variant-numeric: tabular-nums; }
.time-cell .ago {
  display: block; color: var(--text-3); font-size: 11.5px; margin-top: 2px;
  max-width: 118px; overflow: hidden; text-overflow: ellipsis;
}
.net-cell { display: block; line-height: 1.3; max-width: 200px; white-space: normal; }
.net-cell .place { color: var(--text); font-weight: 600; }
.net-cell .org {
  display: block; color: var(--text-3); font-size: 11.5px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
table.data td:has(.net-cell) { white-space: normal; }

/* Empty / loading / error states --------------------------------------- */
.state {
  display: grid; place-items: center; gap: var(--s3);
  padding: var(--s8) var(--s5); text-align: center; color: var(--text-3);
}
.state h3 { color: var(--text-2); font-size: 15px; }
.state p { font-size: 13.5px; max-width: 44ch; }
.state .state-icon {
  width: 38px; height: 38px; border-radius: 50%; display: grid; place-items: center;
  background: var(--surface-3); border: 1px solid var(--line); color: var(--text-3);
}
.skeleton {
  border-radius: 6px; background: linear-gradient(90deg, var(--surface-2) 25%, var(--surface-3) 37%, var(--surface-2) 63%);
  background-size: 400% 100%; animation: shimmer 1.4s ease infinite;
}
@keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }

.notice {
  display: flex; gap: var(--s3); align-items: flex-start;
  padding: var(--s4); border-radius: var(--radius);
  border: 1px solid var(--line-strong); background: var(--surface-2); color: var(--text-2);
}
.notice strong { color: #fff; }
.notice-ok { border-color: rgba(63, 191, 159, .4); background: rgba(63, 191, 159, .08); }
.notice-error { border-color: rgba(255, 107, 107, .4); background: rgba(255, 107, 107, .07); }

/* Pagination ----------------------------------------------------------- */
.pager {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s4); flex-wrap: wrap; margin-top: var(--s4);
  color: var(--text-3); font-size: 13px;
}
.pager-controls { display: flex; align-items: center; gap: var(--s2); }

/* ==========================================================================
   Charts
   ========================================================================== */
.chart-figure { margin: 0; }
.chart-caption { margin-top: var(--s3); color: var(--text-3); font-size: 12.5px; }

/* Category mix: one 100% stacked bar + direct-labelled legend */
.mix-bar {
  display: flex; gap: 2px; height: 13px; margin-bottom: var(--s4);
  border-radius: var(--radius-pill); overflow: hidden; background: var(--surface-3);
}
.mix-seg { height: 100%; min-width: 3px; }
.mix-seg:first-child { border-radius: var(--radius-pill) 0 0 var(--radius-pill); }
.mix-seg:last-child { border-radius: 0 var(--radius-pill) var(--radius-pill) 0; }
.mix-legend { display: grid; gap: var(--s2); grid-template-columns: repeat(auto-fit, minmax(158px, 1fr)); }
.mix-item {
  padding: var(--s3); border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); border-left: 3px solid var(--c, var(--accent));
}
.mix-item .mix-name {
  display: block; color: var(--text-2); font-size: 11.5px;
  font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
}
.mix-item .mix-value {
  display: block; margin-top: 5px; color: #fff; font-size: 20px;
  font-weight: 700; font-variant-numeric: tabular-nums;
}
.mix-item .mix-share { display: block; margin-top: 2px; color: var(--text-3); font-size: 12px; }

/* Activity bars: single series, one hue, 4px rounded data ends */
.bars { display: flex; align-items: flex-end; gap: 2px; height: 122px; }
.bar-col {
  flex: 1 1 0; min-width: 0; display: flex; flex-direction: column;
  justify-content: flex-end; height: 100%; position: relative; cursor: default;
}
.bar-fill {
  width: 100%; min-height: 2px; border-radius: 4px 4px 0 0;
  background: linear-gradient(180deg, var(--accent), var(--accent-dim));
  transition: filter .12s ease;
}
.bar-col:hover .bar-fill { filter: brightness(1.25); }
.bar-col.is-empty .bar-fill { background: var(--surface-3); }
.bar-axis {
  display: flex; justify-content: space-between; margin-top: var(--s2);
  color: var(--text-3); font-size: 11px; font-variant-numeric: tabular-nums;
}

/* Ranked bar rows: countries, networks */
.rank-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 5px; }
.rank-row {
  position: relative; display: grid; grid-template-columns: 1fr auto;
  align-items: center; gap: var(--s3); padding: 8px 11px; overflow: hidden;
  border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface);
}
.rank-row .track {
  position: absolute; left: 0; top: 0; bottom: 0; width: var(--w, 0%);
  background: linear-gradient(90deg, rgba(79, 209, 197, .16), rgba(79, 209, 197, .04));
  border-right: 1px solid rgba(79, 209, 197, .3);
}
.rank-row > * { position: relative; }
.rank-name { display: flex; align-items: baseline; gap: var(--s2); min-width: 0; }
.rank-name .primary {
  color: #fff; font-weight: 650; font-size: 13.5px; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The link, not just its wrapper, must be the truncating box - otherwise the
   inline anchor still reports (and can lay out at) its full text width. */
.rank-name .primary a {
  display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rank-name .secondary { color: var(--text-3); font-size: 11.5px; font-family: var(--mono); flex: 0 0 auto; }
.rank-value { color: var(--text); font-weight: 700; font-size: 13.5px; font-variant-numeric: tabular-nums; }

/* World map ------------------------------------------------------------ */
.world-map {
  /* Matches world-map.svg exactly (1000 x 426.12 Robinson, cropped 58S-84N).
     The flow overlay now shares this viewBox and no longer overrides
     preserveAspectRatio, so land and markers cannot drift apart at any
     container size - previously they agreed only while the box stayed 2:1. */
  position: relative; width: 100%; aspect-ratio: 1000 / 426.12;
  border: 1px solid var(--line-2); border-radius: var(--radius); overflow: hidden;
  background:
    repeating-linear-gradient(0deg, transparent 0 34px, rgba(150, 180, 210, .045) 34px 35px),
    repeating-linear-gradient(90deg, transparent 0 46px, rgba(150, 180, 210, .045) 46px 47px),
    url("/world-map.svg") center / contain no-repeat,
    var(--plot);
  background-size: auto, auto, 100% 100%, auto;
}
/* Density field: the real located population, under the flow layer. Static -
   it is the resting state of the map, not an animation. */
.map-dust { pointer-events: none; }
/* ---- map foot ------------------------------------------------------------
   The legend, the live-arc status, the density caption and the "not plotted"
   note all belong to the foot of the field. They used to be four independent
   absolutely-positioned overlays, each carrying its own hardcoded offset,
   which is only ever one layout change away from a collision - and on Global
   Ops they did collide: the control dock landed on the legend and the caption
   at every width from 1024px to 1600px, and the legend and caption overlapped
   each other even at 1920px.

   They are one dock now. The dock itself is still an overlay - making these
   static printed the caption across the northern hemisphere, see the
   .cmd-primary note below - but the items inside it are in normal flow, so
   they stack. Two of them cannot overlap without the box model breaking, and
   there is no offset left to keep in sync with anything. */
.map-foot {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 6;
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 6px; padding: 0 10px 10px; pointer-events: none;
}
.map-foot > * { pointer-events: auto; max-width: 100%; }

/* The honesty text is long because it is precise, and it was being printed
   across the map to prove it was there. It is one control now: collapsed by
   default, native <details> so it is keyboard-operable with no script, and
   the panel is bounded and floats clear rather than reflowing the field. */
.map-details { position: relative; }
.map-details > summary {
  list-style: none; cursor: pointer;
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 9px; border-radius: var(--radius-sm);
  background: rgba(6, 12, 18, .82); border: 1px solid var(--line);
  color: var(--text-2); font-size: 10.5px; font-weight: 700;
  letter-spacing: .05em; text-transform: uppercase; white-space: nowrap;
}
.map-details > summary::-webkit-details-marker { display: none; }
.map-details > summary::before {
  content: "i"; flex: 0 0 auto;
  width: 12px; height: 12px; border-radius: 50%;
  border: 1px solid currentColor; font-size: 8px; line-height: 10px;
  text-align: center; font-style: italic; font-weight: 700;
}
.map-details > summary:hover { color: var(--text); border-color: var(--line-2); }
.map-details > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.map-details[open] > summary { color: var(--text); border-color: var(--line-2); }
/* display must be stated for both states. Setting `display: flex` here alone
   overrides the UA rule that hides a closed <details>, and the panel is laid
   out and painted permanently - which is the wall again, with an extra step. */
.map-details-body { display: none; }
.map-details[open] > .map-details-body {
  position: absolute; bottom: calc(100% + 6px); left: 0;
  width: max-content; max-width: min(460px, 70vw);
  display: flex; flex-direction: column; gap: 6px;
  padding: 10px 12px; border-radius: var(--radius-sm);
  background: rgba(6, 12, 18, .95); border: 1px solid var(--line-2);
  backdrop-filter: blur(10px);
  box-shadow: 0 18px 40px -20px rgba(0, 0, 0, .9);
}
.map-details-body p { margin: 0; }
.map-anchor-note { font-size: 9.5px; line-height: 1.45; color: var(--text-3); }

.map-field-note {
  max-width: 100%; margin: 0; text-align: left; font-size: 9.5px;
  line-height: 1.4; color: var(--text-3);
}
@media (max-width: 700px) { .map-field-note { display: none; } }


.map-flows { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 2; }
/* Aggregate flow: a STATIC layer. The previous perpetual dash animation ran
   on 12 paths forever, implied continuous traffic the backend does not
   observe, and measured 30fps vs 60fps at 4x CPU throttle. Motion on this map
   now means one thing only: a real, newly observed indicator (see .map-fx). */
.map-route {
  fill: none; stroke: rgba(79, 209, 197, .34); stroke-width: .22;
  stroke-linecap: round; stroke-dasharray: var(--dash, .7 2.4); opacity: .7;
}

/* The disc carries two claims: how much was observed, and where. The second
   one wins when they conflict. --s is the weight the data earns; --cw is the
   land actually under the anchor, expressed as a share of the map's width so
   it scales with the map instead of being a pixel guess. Whichever is smaller
   is drawn, above a floor that keeps the marker readable and clickable.

   Taiwan used to carry a disc of radius 3.6 map units on 1.88 units of land -
   a ring in the Philippine Sea with an island under the middle of it. This is
   why centre-on-land was not enough.

   Centring is by transform, not negative margins: the computed width is a
   min() now, and a margin cannot refer to it. */
.map-node {
  position: absolute;
  /* --ring is the border and halo the disc carries, which the reader also sees
     sitting on the water. It is charged against the cap only where the land is
     tight, below: Korea's disc exactly matched the peninsula's inscribed
     circle and still had five pixels of glow in the Yellow Sea either side.
     Charging it everywhere would collapse every marker to the floor on a
     phone, where the whole map is 378px wide, and lose the weight the disc is
     there to carry. The clearance behind --cw is measured to the country's own
     outline, so for an island or a peninsula it is the coast; for a
     continental country it is a land border, and it never binds anyway. */
  --ring: 0px;
  width: max(var(--smin, 7px), min(var(--s, 10px), calc(var(--cw, 100%) - var(--ring))));
  height: auto; aspect-ratio: 1; margin: 0;
  transform: translate(-50%, -50%);
  border-radius: 50%; background: var(--gold); border: 2px solid rgba(255, 255, 255, .85);
  box-shadow: 0 0 0 3px rgba(8, 17, 26, .75), 0 0 0 5px rgba(215, 166, 74, .16);
  z-index: 3; cursor: help;
}
/* Where the land cannot support the normal weight, the rings go too - they add
   five more pixels of footprint around a marker that is already at its floor.
   A small island gets a precise point, not a blob with a halo. */
.map-node[data-fit="tight"] {
  --smin: 6px; --ring: 8px;
  border-width: 1px;
  box-shadow: 0 0 0 2px rgba(8, 17, 26, .88), 0 0 0 3px rgba(215, 166, 74, .22);
}
.map-node:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }

/* The hub is a square rotated 45 degrees, so it is distinguishable from the
   round source markers by shape as well as colour. */
.map-hub {
  position: absolute; width: 15px; height: 15px; margin: -7.5px 0 0 -7.5px;
  background: var(--accent); border: 2px solid #eafffb; border-radius: 3px;
  transform: rotate(45deg);
  box-shadow: 0 0 0 3px rgba(8, 17, 26, .8), 0 0 0 6px rgba(79, 209, 197, .18);
  z-index: 4; cursor: help;
}
.map-hub::after {
  content: ""; position: absolute; inset: -9px; border-radius: 4px;
  border: 1px solid rgba(79, 209, 197, .35); animation: ping 2.6s ease-out infinite;
}
.map-hub:focus-visible { outline: 2px solid #fff; outline-offset: 4px; }
@keyframes ping { 0% { transform: scale(.5); opacity: .8; } 100% { transform: scale(1.7); opacity: 0; } }

.map-legend {
  display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap;
  padding: 5px 9px; border-radius: var(--radius-sm);
  background: rgba(6, 12, 18, .82); border: 1px solid var(--line);
  color: var(--text-2); font-size: 10.5px; font-weight: 700;
  letter-spacing: .05em; text-transform: uppercase;
}
.map-legend span { display: inline-flex; align-items: center; gap: 5px; }
.map-legend i { flex: 0 0 auto; }
.map-legend .key-dust {
  width: 5px; height: 5px; border-radius: 1px; background: var(--cat-leak);
}
.map-legend .key-source {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--gold); border: 1px solid rgba(255, 255, 255, .8);
}
.map-legend .key-hub {
  width: 8px; height: 8px; border-radius: 2px; transform: rotate(45deg);
  background: var(--accent); border: 1px solid #eafffb;
}
.map-legend .key-route {
  width: 14px; height: 0; border-top: 2px dotted rgba(79, 209, 197, .85);
}
.map-note {
  padding: 4px 8px; border-radius: var(--radius-sm);
  background: rgba(6, 12, 18, .82); border: 1px solid var(--line);
  color: var(--text-3); font-size: 10.5px;
}
.map-empty { display: grid; place-items: center; height: 100%; color: var(--text-3); font-size: 13.5px; }

/* Attack source matrix -------------------------------------------------- */
.matrix { position: relative; min-width: 0; margin-top: var(--s4); }
.matrix-rows { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.matrix-row {
  display: grid; grid-template-columns: minmax(96px, 172px) minmax(0, 1fr) auto;
  align-items: center; gap: var(--s3); min-width: 0; border-radius: 5px;
  padding: 3px 4px; margin: 0 -4px; cursor: default;
}
.matrix-row:hover, .matrix-row:focus-visible { background: rgba(148, 176, 205, .07); outline: none; }
.matrix-row:focus-visible { box-shadow: inset 0 0 0 1px var(--accent); }
.matrix-label { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.matrix-country {
  color: var(--text); font-size: 12.5px; font-weight: 650;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.matrix-label em {
  flex: 0 0 auto; color: var(--text-3); font-style: normal;
  font-size: 10.5px; font-family: var(--mono);
}
.matrix-track {
  display: flex; gap: 2px; height: 12px; min-width: 3px;
  border-radius: 3px; overflow: hidden; background: var(--surface-3);
}
.matrix-seg { min-width: 2px; height: 100%; }
.matrix-total {
  color: var(--text); font-size: 12px; font-weight: 700;
  font-variant-numeric: tabular-nums; min-width: 34px; text-align: right;
}
.matrix-legend {
  display: flex; gap: var(--s3); flex-wrap: wrap; margin-top: var(--s3);
  color: var(--text-2); font-size: 10.5px;
}
.matrix-legend span { display: inline-flex; align-items: center; gap: 5px; }
.matrix-legend i { width: 9px; height: 3px; border-radius: 2px; background: var(--c); flex: 0 0 auto; }
.matrix-state {
  display: grid; place-items: center; gap: 5px; min-height: 92px;
  color: var(--text-3); font-size: 12.5px; text-align: center;
}
.matrix-tip {
  position: absolute; z-index: 7; pointer-events: none; opacity: 0;
  transform: translate(-50%, -100%); transition: opacity .1s ease;
  padding: 8px 10px; border-radius: var(--radius-sm); min-width: 178px;
  background: rgba(6, 12, 18, .97); border: 1px solid var(--line-strong);
  box-shadow: 0 8px 20px rgba(0, 0, 0, .5); font-size: 11.5px; color: var(--text);
}
.matrix-tip.is-visible { opacity: 1; }
.matrix-tip b { display: block; color: #fff; margin-bottom: 4px; font-size: 11.5px; }
.matrix-tip .row { display: flex; gap: var(--s3); justify-content: space-between; white-space: nowrap; }
.matrix-tip .row em { font-style: normal; color: var(--text-3); display: inline-flex; align-items: center; gap: 5px; }
.matrix-tip .row i { width: 8px; height: 8px; border-radius: 2px; background: var(--c); }
.matrix-tip .row strong { font-variant-numeric: tabular-nums; }
.matrix-tip .sep { display: block; margin: 5px 0; border-top: 1px solid var(--line); }
.matrix-tip .asn { display: block; color: var(--text-3); font-size: 10.5px; white-space: normal; }

/* Cards in this row size to their content instead of stretching to the
   tallest sibling, which is what left the Attack mix card ~294px blank. */

/* Threat activity timeline --------------------------------------------- */

.activity {
  min-width: 0; padding: var(--s4);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: linear-gradient(180deg, var(--surface-2), var(--surface));
}
.activity-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--s3); flex-wrap: wrap; margin-bottom: var(--s3);
}
.activity-head h3 { font-size: 14px; }
.activity-sub { margin-top: 3px; color: var(--text-3); font-size: 11.5px; }

.range-tabs {
  display: inline-flex; gap: 2px; padding: 2px; flex: 0 0 auto;
  border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface);
}
.range-tabs button {
  min-height: 26px; padding: 0 10px; border: 0; border-radius: 5px;
  background: transparent; color: var(--text-3); cursor: pointer;
  font: 700 11.5px/1 var(--sans); letter-spacing: .04em;
}
.range-tabs button:hover { color: var(--text); background: var(--surface-3); }
.range-tabs button[aria-selected="true"] {
  background: var(--surface-3); color: #fff; box-shadow: inset 0 0 0 1px var(--line-strong);
}

.activity-kpis {
  display: grid; gap: var(--s2); margin-bottom: var(--s3);
  grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
}
.activity-kpi {
  min-width: 0; padding: 7px 10px; border-radius: var(--radius-sm);
  border: 1px solid var(--line); background: var(--surface);
  border-left: 3px solid var(--c, var(--line-strong));
}
.activity-kpi .k {
  display: block; color: var(--text-3); font-size: 9.5px;
  font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.activity-kpi .v {
  display: block; margin-top: 3px; color: #fff; font-size: 17px;
  font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums;
}

.activity-plot { position: relative; min-width: 0; }
.activity-plot svg { display: block; max-width: 100%; height: auto; }
.act-grid { stroke: var(--line); stroke-width: .5; }
.act-bar { fill: var(--cat-mail); }
.act-bar-hit { fill: transparent; }
.activity-plot .act-col:hover .act-bar { fill: #0abfa0; }
.activity-plot .act-col:hover .act-bar-hit { fill: rgba(148, 176, 205, .07); }
.act-line { fill: none; stroke: var(--cat-leak); stroke-width: 1.6; stroke-linejoin: round; stroke-linecap: round; }
.act-area { fill: var(--cat-leak); opacity: .1; }
.act-axis { fill: var(--text-3); font-size: 9px; font-family: var(--sans); }
.act-ymax { fill: var(--text-3); font-size: 9px; font-family: var(--sans); }

.activity-legend {
  display: flex; gap: var(--s4); flex-wrap: wrap; margin-top: var(--s2);
  color: var(--text-2); font-size: 11px;
}
.activity-legend span { display: inline-flex; align-items: center; gap: 6px; }
.activity-legend i { width: 10px; height: 3px; border-radius: 2px; background: var(--c); }
.activity-legend i.dot-mark { width: 8px; height: 8px; border-radius: 50%; }

.activity-tip {
  position: absolute; z-index: 6; pointer-events: none; opacity: 0;
  transform: translate(-50%, -100%); transition: opacity .1s ease;
  padding: 7px 9px; border-radius: var(--radius-sm); white-space: nowrap;
  background: rgba(6, 12, 18, .96); border: 1px solid var(--line-strong);
  box-shadow: 0 8px 20px rgba(0, 0, 0, .5); font-size: 11.5px; color: var(--text);
}
.activity-tip.is-visible { opacity: 1; }
.activity-tip b { display: block; color: #fff; margin-bottom: 3px; font-size: 11px; }
.activity-tip .row { display: flex; gap: var(--s3); justify-content: space-between; }
.activity-tip .row em { font-style: normal; color: var(--text-3); }
.activity-tip .row strong { font-variant-numeric: tabular-nums; }

.activity-note { margin-top: var(--s2); color: var(--text-3); font-size: 10.5px; }
.activity-state {
  display: grid; place-items: center; gap: 6px; min-height: 150px;
  color: var(--text-3); font-size: 12.5px; text-align: center;
}
.activity.is-busy .activity-plot { opacity: .45; transition: opacity .12s ease; }

/* Collector health ----------------------------------------------------- */
.collector-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 5px; }
.collector-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  padding: 9px 11px; border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface);
}
.collector-main { min-width: 0; flex: 1 1 auto; }
.collector-trend { flex: 0 0 auto; display: flex; align-items: center; }
.collector-row .name { display: block; color: #fff; font-size: 13px; font-weight: 650; }
.collector-row .meta { display: block; color: var(--text-3); font-size: 11.5px; margin-top: 2px; }
.state-pill {
  flex: 0 0 auto; min-width: 56px; text-align: center;
  padding: 3px 8px; border-radius: var(--radius-pill);
  font-size: 11px; font-weight: 700; border: 1px solid var(--line-strong);
  background: var(--surface-3); color: var(--text-2);
}
.state-live,
.state-reporting,
.state-healthy { color: #7fe6d3; background: rgba(63, 191, 159, .14); border-color: rgba(63, 191, 159, .4); }
.state-idle { color: var(--text-2); background: var(--surface-3); }
/* Awaiting heartbeat rollout: informative, not a fault. */
.state-unmonitored { color: var(--text-2); background: var(--surface-3); border-style: dashed; }
/* Overdue is the only actionable state: silent beyond this sensor's own pattern. */
.state-overdue { color: #ffc79c; background: rgba(255, 159, 90, .14); border-color: rgba(255, 159, 90, .42); }
.state-pending { color: var(--text-3); }
/* Retired is deliberate, not a fault - muted rather than red. */
.state-retired { color: var(--text-3); background: var(--surface-3); border-style: dashed; }
.state-off { color: #ffb0b0; background: rgba(255, 107, 107, .12); border-color: rgba(255, 107, 107, .35); }
.collector-row .meta { line-height: 1.5; }
.health-note {
  margin-top: var(--s3); padding-top: var(--s3); border-top: 1px solid var(--line);
  color: var(--text-3); font-size: 10.5px; line-height: 1.45;
}

/* ==========================================================================
   Page furniture
   ========================================================================== */
.hero { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, 360px); gap: var(--s6); align-items: start; }
.hero h1, .hero h2 {
  font-size: var(--t-hero); line-height: 1.08; max-width: 18ch;
  letter-spacing: -.03em; font-weight: 700;
}
.hero .lede {
  margin-top: var(--s4); font-size: clamp(14.5px, .28vw + 13.6px, 17px);
  line-height: 1.6; max-width: 58ch;
}

.search-card { padding: var(--s5); }
.search-form { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--s2); align-items: end; }
.search-form .field { grid-column: 1 / 2; }
.search-form input { min-height: 44px; font-family: var(--mono); font-size: 15px; }
.search-result { grid-column: 1 / -1; margin-top: var(--s3); }
.result-card { padding: var(--s4); border: 1px solid var(--line-strong); border-radius: var(--radius); background: var(--surface); }
.result-head { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
.result-head .result-ip { font-family: var(--mono); font-size: 16px; font-weight: 700; color: #fff; }
.result-grid { display: grid; gap: var(--s2); grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); margin-top: var(--s3); }
.result-grid div { padding: 9px 11px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-2); }
.result-grid dt, .result-grid .k {
  display: block; color: var(--text-3); font-size: 10.5px;
  font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
}
.result-grid dd, .result-grid .v { display: block; margin: 4px 0 0; color: #fff; font-size: 13.5px; overflow-wrap: anywhere; }
.result-actions { display: flex; gap: var(--s2); flex-wrap: wrap; margin-top: var(--s3); }

/* IP detail page ------------------------------------------------------- */
.ip-hero { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--s5); align-items: start; padding: var(--s6); }
.ip-hero .ip-title { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
.ip-hero h1 { font-family: var(--mono); font-size: 26px; letter-spacing: -.02em; }
.ip-hero .ip-sub { margin-top: var(--s2); font-size: 14px; }
.risk-gauge { display: grid; gap: var(--s2); min-width: 210px; }
.risk-track { height: 8px; border-radius: var(--radius-pill); background: var(--surface-3); overflow: hidden; }
.risk-fill { height: 100%; width: var(--w, 0%); border-radius: var(--radius-pill); background: var(--sev-low); }
.risk-fill.sev-critical { background: var(--sev-critical); }
.risk-fill.sev-high { background: var(--sev-high); }
.risk-fill.sev-medium { background: var(--sev-medium); }
.risk-meta { display: flex; justify-content: space-between; color: var(--text-3); font-size: 12px; }
.risk-meta strong { color: #fff; font-variant-numeric: tabular-nums; }
.score-parts { display: grid; gap: 4px; margin-top: var(--s2); }
.score-part { display: grid; grid-template-columns: 1fr auto; gap: var(--s2); font-size: 12px; color: var(--text-3); }
.score-part b { color: var(--text-2); font-weight: 650; font-variant-numeric: tabular-nums; }

.kv-grid { display: grid; gap: var(--s2); grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
.kv {
  padding: var(--s3); border: 1px solid var(--line);
  border-radius: var(--radius-sm); background: var(--surface); min-height: 66px;
}
.kv dt, .kv .k {
  display: block;
  color: var(--text-3); font-size: 10.5px; font-weight: 700;
  letter-spacing: .07em; text-transform: uppercase;
}
.kv dd, .kv .v {
  display: block; margin: 5px 0 0; color: #fff; font-size: 13.5px;
  overflow-wrap: anywhere;
}
.kv .v small { display: block; margin-top: 2px; color: var(--text-3); font-size: 11.5px; font-weight: 400; }

.timeline { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s2); }
.timeline li {
  position: relative; padding: var(--s3) var(--s3) var(--s3) var(--s6);
  border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface);
}
.timeline li::before {
  content: ""; position: absolute; left: 14px; top: 18px;
  width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
}
.timeline .t-head { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.timeline .t-head strong { color: #fff; font-size: 13.5px; }
.timeline .t-meta { margin-top: 5px; color: var(--text-3); font-size: 12px; }

/* Filter bar ----------------------------------------------------------- */
/* auto-fit lets the toolbar reflow to as many columns as genuinely fit,
   instead of a fixed 6-column template that eventually overflows. */
.filter-bar {
  display: grid; gap: var(--s3); align-items: end;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  padding: var(--s4); border: 1px solid var(--line);
  border-radius: var(--radius); background: var(--surface);
}
.filter-bar > .field-search { grid-column: span 2; min-width: 0; }
.filter-bar > * { min-width: 0; }
.filter-actions {
  display: flex; gap: var(--s2); align-items: end; justify-content: flex-end;
  grid-column: 1 / -1;
}
.filter-actions .btn { min-width: 84px; }
@media (min-width: 1100px) {
  .filter-actions { grid-column: span 2; justify-content: flex-start; }
}
.active-filters { display: flex; gap: var(--s2); flex-wrap: wrap; margin-top: var(--s3); }
.filter-tag {
  display: inline-flex; align-items: center; gap: 6px; padding: 4px 9px;
  border-radius: var(--radius-pill); border: 1px solid var(--line-strong);
  background: var(--surface-2); color: var(--text-2); font-size: 12px; text-decoration: none;
}
.filter-tag:hover { color: #fff; border-color: rgba(255, 107, 107, .5); }

/* Blocklists ----------------------------------------------------------- */
.list-grid { display: grid; gap: var(--s3); grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.list-card {
  display: block; padding: var(--s4); text-decoration: none; color: var(--text);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: linear-gradient(180deg, var(--surface-2), var(--surface));
}
.list-card:hover { border-color: rgba(79, 209, 197, .55); color: var(--text); }
.list-card .list-name { display: block; color: #fff; font-size: 17px; font-weight: 700; }
.list-card .list-desc { display: block; margin-top: 4px; color: var(--text-3); font-size: 13px; }
.list-card .list-meta {
  display: flex; gap: var(--s3); margin-top: var(--s3);
  color: var(--text-2); font-size: 12px; font-weight: 650;
}
.list-card .list-meta b { color: #fff; font-variant-numeric: tabular-nums; }

/* Prose / docs --------------------------------------------------------- */
.prose { max-width: 74ch; overflow-wrap: break-word; }
.prose code, .card p code, td code, .field-hint code { overflow-wrap: anywhere; }
.card p, .card h2, .card h3 { overflow-wrap: break-word; }
.prose p + p { margin-top: var(--s3); }
.prose ul, .prose ol { margin: var(--s3) 0; padding-left: var(--s5); color: var(--text-2); }
.prose li + li { margin-top: 6px; }
.code-block {
  margin: 0; padding: var(--s4); overflow-x: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
  overscroll-behavior-x: contain;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: #070d14; color: #cfe9e5; font-family: var(--mono); font-size: 12.5px; line-height: 1.6;
}
.endpoint {
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  margin-bottom: var(--s2); font-family: var(--mono); font-size: 13px;
}
.method {
  padding: 2px 8px; border-radius: 5px; font-size: 11px; font-weight: 700;
  background: rgba(63, 191, 159, .15); color: #7fe6d3; border: 1px solid rgba(63, 191, 159, .4);
}
.method-post { background: rgba(221, 176, 80, .14); color: #f3d38a; border-color: rgba(221, 176, 80, .4); }
.grid-2 {
  display: grid; gap: var(--s3);
  grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
}
.grid-2 > * { min-width: 0; }
/* Two-up only when each column can actually hold its table; otherwise the
   cards stack and each gets the full width instead of a cramped scroller. */
.grid-2-tables > * { min-width: 0; }
.grid-2-tables .net-cell { max-width: 168px; }
.grid-2-tables table.data td.wrap-cell { min-width: 140px; max-width: 200px; }

/* Modal ---------------------------------------------------------------- */
.modal {
  position: fixed; inset: 0; z-index: 60; display: none;
  align-items: center; justify-content: center; padding: var(--s5);
  background: rgba(3, 7, 11, .74); backdrop-filter: blur(6px);
}
.modal.is-open { display: flex; }
.modal-panel {
  width: min(660px, 100%); max-height: min(82vh, 780px); overflow: auto;
  border: 1px solid var(--line-strong); border-radius: var(--radius);
  background: var(--surface); box-shadow: 0 24px 64px rgba(0, 0, 0, .6);
}
.modal-head {
  position: sticky; top: 0; display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--s4); padding: var(--s4) var(--s5); border-bottom: 1px solid var(--line); background: #0a1017;
}
.modal-head h2 { font-family: var(--mono); font-size: 18px; }
.modal-head p { margin-top: 3px; font-size: 13px; }
.modal-close { width: 34px; height: 34px; min-height: 0; padding: 0; font-size: 20px; line-height: 1; }
.modal-body { padding: var(--s5); }

/* Footer --------------------------------------------------------------- */
.site-footer { border-top: 1px solid var(--line); margin-top: var(--s8); background: rgba(8, 13, 19, .5); }
.footer-inner {
  display: flex; gap: var(--s5); flex-wrap: wrap; justify-content: space-between;
  padding-top: var(--s6); padding-bottom: var(--s6); color: var(--text-3); font-size: 13px;
}
.footer-links { display: flex; gap: var(--s4); flex-wrap: wrap; }
.footer-links a { color: var(--text-2); text-decoration: none; }
.footer-links a:hover { color: var(--accent); }

/* Toast ---------------------------------------------------------------- */
.toast {
  position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%) translateY(12px);
  z-index: 80; padding: 9px var(--s4); border-radius: var(--radius-pill);
  background: var(--surface-3); border: 1px solid var(--line-strong); color: #fff;
  font-size: 13px; font-weight: 650; opacity: 0; pointer-events: none;
  transition: opacity .16s ease, transform .16s ease;
}
.toast.is-visible { opacity: 1; transform: translateX(-50%) translateY(0); }

/* ==========================================================================
   Console layer
   Instrument primitives used by the operations dashboard. Everything here is
   additive: the document primitives above still drive the content pages.
   ========================================================================== */

/* ---- Status rail --------------------------------------------------------
   A thin band of live telemetry directly under the masthead. Horizontally
   scrollable on narrow screens rather than wrapping to three lines or
   dropping values - no reading is hidden on mobile. */
.wrap-console { max-width: var(--maxw-wide); }

/* ---- Console grid -------------------------------------------------------
   One 12-column grid for the whole dashboard. Panels are placed with
   grid-column only - no `order` between panels - so DOM order, visual order
   and screen-reader order stay identical at every breakpoint. The single
   `order` in the system lives inside the geography panel and is documented
   there. `align-items:start` keeps a panel at its natural height rather than
   stretching it into dead space when its data shrinks. */
.console-main {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--s4);
  align-items: start;
  padding-block: var(--s4) var(--s7);
}
.console-main > * { min-width: 0; }   /* every panel holds a scroller or a table */

/* Fail safe, and it must stay ABOVE the .p-* spans so they still win: a panel
   that declares no span gets the whole row rather than a single 1/12 track.
   The Wave 3 brief and query panels were added without one and each collapsed
   into a 128px column at 1920px - 1532px and 3986px tall - beside a completely
   empty row. A missing span should degrade to "full width", never to a
   sliver. */
.console-main > * { grid-column: 1 / -1; }

.p-masthead { grid-column: 1 / 9; }
.p-ipcheck  { grid-column: 9 / -1; }
.p-metrics  { grid-column: 1 / -1; }
.p-geo      { grid-column: 1 / -1; }
.p-activity { grid-column: 1 / 9; }
.p-window   { grid-column: 9 / -1; }
.p-classes  { grid-column: 1 / 8; }
.p-fleet    { grid-column: 8 / -1; }
.p-topips   { grid-column: 1 / 7; }
.p-newest   { grid-column: 7 / -1; }
.p-dispatch { grid-column: 1 / -1; }
/* Wave 3 console panels. Declared explicitly rather than relying on the
   fail-safe above, so their placement is a stated decision like every other
   panel's: the situation brief and the analyst query both read across the
   full width. */
.intel-panel { grid-column: 1 / -1; }
.query-panel { grid-column: 1 / -1; }

/* Metric rail: 6 across, then 3, then 2. */
.p-metrics { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 1px; background: var(--line-2); }

/* Geography: the map is the focal cell and keeps the largest track at every
   width. The two ranked lists share one side rail rather than being separate
   grid columns - as columns they collapsed one-at-a-time and left a
   full-width stretched list under the map. */
.geo-grid {
  display: grid; grid-template-columns: minmax(0, 1fr) 300px;
  gap: var(--s4); align-items: start;
}
.geo-lists { display: grid; gap: var(--s5); align-content: start; }

/* ---- Console map -> Global Operations --------------------------------
   The console map is a summary; Global Ops is the same data at operations
   scale. Nothing on the console said the second surface existed.

   It is a real anchor, so it is keyboard reachable, focusable, openable in a
   new tab and readable by assistive technology without a click handler. It
   sits in the map's top-right inset rather than making the canvas itself a
   link: markers are buttons with their own hover, focus and selection
   behaviour, and a blanket link over them would make every marker click
   ambiguous. Its footprint is its own box and nothing else. */
.geo-map-cell { position: relative; min-width: 0; }
.map-cta {
  position: absolute; top: 10px; right: 10px; z-index: 7;
  display: grid; gap: 1px; padding: 7px 11px;
  border: 1px solid rgba(126, 170, 205, .22); border-radius: 8px;
  background: linear-gradient(180deg, rgba(16, 30, 44, .92), rgba(8, 15, 23, .88));
  backdrop-filter: blur(3px);
  color: var(--text); text-decoration: none; text-align: right;
  transition: border-color .15s ease, color .15s ease, transform .15s ease;
}
.map-cta-eyebrow {
  font-size: 9px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  color: var(--text-3);
}
.map-cta-action {
  font-size: 11.5px; font-weight: 650; letter-spacing: .01em; white-space: nowrap;
}
.map-cta:hover { border-color: var(--accent); color: var(--accent); }
.map-cta:hover .map-cta-eyebrow { color: var(--accent); }
.map-cta:focus-visible {
  outline: 2px solid var(--accent, #6ea8fe); outline-offset: 2px;
  border-color: var(--accent); color: var(--accent);
}
/* Hover is not the only way to discover it: the label is always visible. */
@media (max-width: 560px) {
  .map-cta { top: 8px; right: 8px; padding: 6px 9px; }
  .map-cta-eyebrow { display: none; }
  .map-cta-action { font-size: 11px; }
}
@media (prefers-reduced-motion: no-preference) {
  .map-cta:hover { transform: translateY(-1px); }
}

@media (min-width: 1600px) {
  .geo-grid { grid-template-columns: minmax(0, 1fr) 640px; }
  .geo-lists { grid-template-columns: 1fr 1fr; gap: var(--s4); }
}
@media (max-width: 1279px) {
  .console-main { grid-template-columns: repeat(8, minmax(0, 1fr)); }
  .p-masthead { grid-column: 1 / -1; }
  .p-ipcheck  { grid-column: 1 / -1; }
  .p-activity { grid-column: 1 / -1; }
  .p-window   { grid-column: 1 / -1; }
  .p-classes  { grid-column: 1 / 5; }
  .p-fleet    { grid-column: 5 / -1; }
  .p-topips   { grid-column: 1 / -1; }
  .p-newest   { grid-column: 1 / -1; }
  .p-metrics  { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 1023px) {
  .console-main { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .p-classes, .p-fleet { grid-column: 1 / -1; }
  .geo-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 700px) {
  .console-main { grid-template-columns: 1fr; gap: var(--s3); }
  .console-main > * { grid-column: 1 / -1 !important; }
  .p-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .geo-grid { grid-template-columns: 1fr; }

  /* On a phone the map is honestly a supporting overview, so the ranked
     lists lead and the map follows. This is the ONLY `order` in the system:
     three peer children of one panel, all carrying the same data, so reading
     order stays meaningful.
     It goes full-bleed rather than being boxed or put in a scroller. A taller
     aspect was tried and does nothing - the land is `background-size: contain`,
     so extra height just letterboxes the same map. Cropping was rejected
     separately: it buys height only by pushing real sources off the map. */
  .geo-countries { order: 1; }
  .geo-networks  { order: 2; }
  .geo-map-cell  { order: 3; margin-inline: calc(var(--s4) * -1); }
  .geo-map-cell .world-map { border-inline: 0; border-radius: 0; }
  .map-legend { font-size: 9px; gap: var(--s2); }
}

.statusbar {
  border-block: 1px solid var(--line-2);
  background: var(--bg-deep);
}
.statusbar-inner {
  display: flex; align-items: stretch; gap: 0;
  overflow-x: auto; scrollbar-width: none;
  -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain;
}
.statusbar-inner::-webkit-scrollbar { display: none; }
.sb-item {
  flex: 0 0 auto; display: flex; flex-direction: column; gap: 2px;
  padding: 7px var(--s4); border-right: 1px solid var(--line-2); min-width: 0;
}
.sb-item:last-child { border-right: 0; }
.sb-k {
  font-size: var(--t-micro); font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-3); white-space: nowrap;
}
.sb-v {
  font-family: var(--mono); font-size: var(--t-tele); font-weight: 500;
  color: var(--text); font-variant-numeric: tabular-nums; white-space: nowrap;
}
.sb-v .unit { color: var(--text-3); font-size: 11px; }
.sb-item.is-live .sb-v { color: var(--sig-live); }
.sb-item.is-watch .sb-v { color: var(--sig-watch); }
.sb-item.is-alert .sb-v { color: var(--sig-alert); }
.sb-spacer { flex: 1 1 auto; border-right: 0; }

/* ---- Status LED ---------------------------------------------------------
   Breathing is opacity-only so it composites; it never runs alone as a
   meaning-carrier - every LED sits beside a text label. */
.led {
  width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto;
  background: var(--sig-idle); display: inline-block;
  box-shadow: 0 0 0 2px rgba(0, 0, 0, .5);
}
.led-live { background: var(--sig-live); }
.statusbar .led-live { animation: ledbreathe var(--dur-pulse) var(--ease-inout) infinite; }
@keyframes ledbreathe { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* ---- Panel --------------------------------------------------------------
   The console unit. Header is one compact line: glyph, title, then dim
   right-aligned metadata. */
.pnl {
  min-width: 0; position: relative; display: flex; flex-direction: column;
  border: 1px solid var(--line-2); border-radius: var(--radius);
  background: linear-gradient(180deg, var(--surface-2), var(--surface));
}
.pnl-hd {
  display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap;
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--line-2);
}
.pnl-glyph { color: var(--accent); font-size: 9px; line-height: 1; flex: 0 0 auto; }
.pnl-eyebrow {
  font-size: var(--t-micro); font-weight: 700; letter-spacing: .12em;
  text-transform: uppercase; color: var(--text-3); flex: 0 0 auto;
}
.pnl-ttl { font-size: var(--t-section); line-height: 1.2; margin: 0; }
.pnl-meta {
  margin-left: auto; font-family: var(--mono); font-size: 11px;
  color: var(--text-3); font-variant-numeric: tabular-nums;
}
.pnl-bd { padding: var(--s4); min-width: 0; }
.pnl-ft {
  margin-top: auto; padding: var(--s2) var(--s4);
  border-top: 1px solid var(--line-2);
  font-size: 11px; color: var(--text-3); line-height: 1.45;
}

/* Corner ticks. Purely structural; hidden from assistive tech. */
.pnl-tick { position: absolute; width: 5px; height: 5px; pointer-events: none; }
.pnl-tick-tr { top: -1px; right: -1px; border-top: 1px solid var(--tick); border-right: 1px solid var(--tick); }
.pnl-tick-bl { bottom: -1px; left: -1px; border-bottom: 1px solid var(--tick); border-left: 1px solid var(--tick); }

/* ---- Page masthead ------------------------------------------------------
   Content pages get the console's header grammar without its instrument
   chrome: same eyebrow, same right-aligned qualifier, but the lede stays a
   readable paragraph rather than being boxed. */
.page-head { display: flex; flex-direction: column; gap: var(--s3); margin-bottom: var(--s2); }
.ph-top { display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap; }
.ph-top h1 { margin: 0; }
.ph-meta {
  margin-left: auto; font-family: var(--mono); font-size: 11.5px;
  color: var(--text-3); font-variant-numeric: tabular-nums;
}
.ph-actions { margin-left: var(--s3); display: flex; gap: var(--s2); flex-wrap: wrap; }
.ph-meta + .ph-actions { margin-left: var(--s3); }
.ph-lede { max-width: 78ch; color: var(--text-2); font-size: clamp(14px, .2vw + 13.4px, 15.5px); }
@media (max-width: 700px) {
  .ph-meta { margin-left: 0; flex-basis: 100%; }
  .ph-actions { margin-left: 0; }
}

/* ---- Readout tile -------------------------------------------------------
   Denser sibling of .stat for the metric rail. */
.readout { display: grid; gap: 1px; background: var(--line-2); }
.ro {
  background: var(--surface); padding: var(--s3) var(--s4);
  display: flex; flex-direction: column; gap: 3px; min-width: 0;
}
.ro-k {
  font-size: var(--t-micro); font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-3);
}
.ro-v {
  font-family: var(--display); font-size: var(--t-readout); font-weight: 600;
  letter-spacing: -.02em; color: #fff; font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
.ro-sub { font-size: 11px; color: var(--text-3); line-height: 1.35; }

/* ---- Recomposed dashboard pieces ---------------------------------------- */
.masthead { display: flex; flex-direction: column; gap: var(--s3); }
.masthead h1 {
  font-size: var(--t-hero); line-height: 1.06; letter-spacing: -.03em;
  max-width: 20ch;
}
.masthead .lede { max-width: 78ch; }
.masthead-sig { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.masthead-sig strong { color: #fff; font-family: var(--mono); font-size: 13px; font-weight: 500; }

.sub-ttl {
  display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap;
  margin: 0 0 var(--s3); font-size: 13px; letter-spacing: -.005em;
}
.sub-ttl .count { font-size: 11px; font-weight: 500; color: var(--text-3); font-family: var(--sans); }
.sub-ttl-gap { margin-top: var(--s6); }

.pnl-actions { margin-left: var(--s3); flex: 0 0 auto; }
.pnl-meta + .pnl-actions { margin-left: var(--s3); }

.readout-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 700px) { .readout-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* ---- Reason breakdown ---------------------------------------------------
   A compact technical grid rather than another set of cards: five aligned
   columns, one hairline per row, the bar sitting behind the label as a
   magnitude cue rather than a separate chart. */
.reason-grid { list-style: none; margin: 0; padding: 0; }
.reason-row {
  position: relative; display: grid; align-items: center;
  grid-template-columns: minmax(0, 1fr) auto 84px 76px; gap: var(--s3);
  padding: 7px var(--s2); border-bottom: 1px solid var(--line);
}
.reason-row:last-child { border-bottom: 0; }
.reason-head {
  font-size: var(--t-micro); font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--text-3); border-bottom: 1px solid var(--line-2);
}
.reason-bar {
  position: absolute; inset: 0 auto 0 0; width: var(--w, 0);
  background: var(--c, var(--accent)); opacity: .12; pointer-events: none;
}
.reason-name { position: relative; font-size: 13px; color: var(--text); min-width: 0; }
.reason-code { position: relative; font-family: var(--mono); font-size: 11px; color: var(--text-3); }
.reason-n {
  position: relative; text-align: right; font-family: var(--mono); font-size: 12px;
  color: var(--text); font-variant-numeric: tabular-nums;
}
.reason-n .unit { color: var(--text-3); font-size: 10px; }
@media (max-width: 700px) {
  .reason-row { grid-template-columns: minmax(0, 1fr) 70px 64px; }
  .reason-code { display: none; }
}

/* ---- Live map layer -----------------------------------------------------
   Ambient motion is deliberately small and non-travelling, so it can never be
   mistaken for an event arriving: the hub breathes, recently-hit markers keep
   a ring for a few seconds, and that is all. Everything that travels is a
   real, newly observed indicator. */
.map-fx {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 3;
}
.map-status {
  display: flex; align-items: center; gap: var(--s2);
  padding: 5px var(--s3); border: 1px solid var(--line-2); border-radius: var(--radius-sm);
  background: rgba(3, 6, 10, .82);
  font-size: 11px; color: var(--text-2); line-height: 1.35;
}
.map-node { z-index: 5; }
.map-node.is-hit::after {
  content: ""; position: absolute; inset: 50% auto auto 50%;
  width: 10px; height: 10px; margin: -5px 0 0 -5px; border-radius: 50%;
  border: 1.5px solid var(--accent);
  animation: hitring 1.1s var(--ease-out) 3;
}
@keyframes hitring {
  from { opacity: .9; transform: scale(.6); }
  to   { opacity: 0;  transform: scale(3.4); }
}

/* ---- Phone: the map is 164px tall, so its chrome cannot be a stack -------
   At 412px the map is 386x164 (its own 2.35:1 ratio at full width), and the
   desktop foot - legend on two lines, replay status, details - was 386x122
   of that: 74% of the field, with the legend and status panels opaque over
   Europe, Asia, Africa and South America. Markers sat underneath them.

   The disclosures are load-bearing and none are removed: the foot becomes one
   compact scrolling row pinned to the bottom edge, which is the same
   treatment Global Ops already uses at this width. ~30px instead of 122px. */
@media (max-width: 700px) {
  .map-foot {
    left: 4px; right: 4px; bottom: 4px; padding: 0;
    gap: 4px; justify-content: flex-start;
    flex-wrap: nowrap; overflow-x: auto; overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .map-foot::-webkit-scrollbar { display: none; }
  .map-foot > * { flex: none; }
  .map-legend {
    display: flex; flex-wrap: nowrap; gap: 10px; justify-content: flex-start;
    overflow-x: auto; overscroll-behavior-x: contain;
    background: rgba(6, 12, 18, .86); border-radius: 6px; padding: 3px 6px;
    font-size: .58rem; letter-spacing: .06em; white-space: nowrap;
    backdrop-filter: blur(4px); scrollbar-width: none;
  }
  .map-legend::-webkit-scrollbar { display: none; }
  .map-legend > * { flex: none; }
  .map-status { white-space: nowrap; font-size: .6rem; padding: 3px 7px; }
  .map-details > summary { padding: 3px 7px; font-size: 9.5px; }
  /* The foot is a horizontal scroller, so the summary this panel is anchored
     to can sit far outside the visible strip - measured opening at x=662 in a
     412px viewport, entirely off-screen. Anchoring it to the MAP instead of to
     the summary fixes that: .world-map carries a filter, which makes it the
     containing block for a fixed-position descendant, and it is also the box
     the panel should be bounded by. If that filter ever goes away the panel
     becomes viewport-anchored, which is still on-screen and still correct. */
  .map-details[open] > .map-details-body {
    position: fixed; left: 8px; right: 8px; bottom: 40px;
    width: auto; max-width: none;
  }
  /* Out of the field entirely. The map's top-right corner at this projection
     is Siberia, and the aggregate marker for RU sat 100% underneath this
     control at 320, 360 and 390px. Below the map it is still attached to the
     map, still a one-tap control, and occludes nothing. */
  .geo-map-cell { display: flex; flex-direction: column; }
  /* Positioning only. The <=560px block already compacts the padding and
     drops the eyebrow, and it is authored EARLIER in the file - setting
     padding here would win at 560px and undo it. */
  .map-cta {
    position: static; align-self: flex-end; margin-top: 6px; text-align: right;
  }
}

/* Sparkline reveal. Compositor-only by construction. */
.spark svg { transition: opacity var(--dur-panel) var(--ease-out), transform var(--dur-panel) var(--ease-out); }
.will-spark svg { opacity: 0; transform: translate3d(0, 4px, 0); }
.spark-in svg { opacity: 1; transform: none; }

/* Containment. A panel's internals cannot affect layout outside it, so a
   rolling counter or a revealed panel cannot invalidate the whole document.
   content-visibility lets the browser skip work for panels below the fold;
   the intrinsic size keeps the scrollbar honest so skipping cannot shift. */
.pnl, .card { contain: content; }
.p-topips, .p-newest, .p-dispatch, .p-classes, .p-fleet {
  content-visibility: auto;
  contain-intrinsic-size: auto 480px;
}

/* ---- Motion states ------------------------------------------------------
   All compositor-only: transform + opacity. Nothing here animates a property
   that triggers layout, so a reveal cannot shift the page (no CLS) and a
   rolling counter cannot reflow its neighbours. */
.will-reveal {
  opacity: 0; transform: translate3d(0, 10px, 0);
  transition: opacity var(--dur-panel) var(--ease-out) var(--reveal-delay, 0ms),
              transform var(--dur-panel) var(--ease-out) var(--reveal-delay, 0ms);
}
.is-revealed { opacity: 1; transform: none; }

/* A rolling counter reserves its width in advance so digits changing width
   cannot nudge the layout mid-roll. */
.ro-v, .sb-v, .stat-value { font-variant-numeric: tabular-nums; }
.is-rolling { color: var(--accent); }
.just-changed {
  animation: valueflash 1.2s var(--ease-out) 1;
}
@keyframes valueflash {
  0% { color: var(--accent); }
  100% { color: inherit; }
}

/* Readout state. These classes were emitted before they were styled, so the
   tile that was out of bounds looked identical to every other tile. */
.ro.is-alert { box-shadow: inset 2px 0 0 var(--sig-alert); }
.ro.is-alert .ro-v { color: var(--sig-alert); }
.ro.is-watch { box-shadow: inset 2px 0 0 var(--sig-watch); }
.ro.is-watch .ro-v { color: var(--sig-watch); }

/* ---- Content-page composition -------------------------------------------
   Shared by the non-dashboard pages. These carry the console's grammar
   without its instrument chrome. */
.readout-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 1023px) { .readout-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 700px) {
  .readout-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* A second line under a table cell: the qualifier that stops a bare value
   being ambiguous, without spending another column on it. */
.cell-sub { display: block; margin-top: 2px; font-size: 11px; color: var(--text-3); }

/* Sensor roster. Rows needing attention carry a left edge as well as their
   pill, so the anomaly is findable without reading every row - and the pill
   still carries the word, so it never depends on colour alone. */
.sensor-table tr[data-state="overdue"] td:first-child { box-shadow: inset 2px 0 0 var(--danger); }
.sensor-table tr[data-state="pending"] td:first-child { box-shadow: inset 2px 0 0 var(--warn); }
.sensor-table tr[data-state="retired"] td { color: var(--text-3); }
.sensor-table tr[data-state="retired"] strong { color: var(--text-2); font-weight: 600; }
.sensor-table .spark svg { width: 72px; }

.state-legend { margin-top: var(--s4); }
.state-legend .kv { min-height: 0; }

/* ---- Legacy card headers -------------------------------------------------
   Any .card still composed with a .card-head picks up the panel header
   grammar - rule beneath, tighter padding, the same leading glyph - so a page
   not yet restructured still reads as part of the console rather than as a
   leftover. Applied by CSS rather than by rewriting eight nested f-strings,
   which would be churn for no functional gain. */
.card > .card-head,
.card-pad > .card-head:first-child {
  margin: calc(var(--s5) * -1) calc(var(--s5) * -1) var(--s4);
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--line-2);
}
.card-head h2, .card-head h3 { font-size: var(--t-section); }
.card-head .count { font-family: var(--mono); font-size: 11px; color: var(--text-3); }

/* ---- API reference -------------------------------------------------------
   The verb and path are the identifier a reader scans for, so they lead the
   panel header instead of sitting inside the body. */
.endpoint-pnl { margin-bottom: var(--s3); }
.endpoint-hd { gap: var(--s3); align-items: center; }
.endpoint-path {
  font-family: var(--mono); font-size: 13px; color: var(--text);
  overflow-wrap: anywhere; min-width: 0;
}
.endpoint-title { margin-bottom: var(--s2); }
.endpoint-desc { max-width: 78ch; font-size: 13.5px; line-height: 1.6; }
.ex-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s3); margin-top: var(--s4); align-items: start; }
@media (max-width: 900px) { .ex-grid { grid-template-columns: minmax(0, 1fr); } }
.ex .snip-head h4 { font-size: var(--t-micro); font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: var(--text-3); margin: 0; }
.api-intro { margin-bottom: var(--s5); }
/* Code blocks must never widen the page on a phone. */
.code-block { max-width: 100%; }

/* A framed, tinted callout only reads as deliberate if it fits its content;
   at >=1100px it was 1312px wide around 664px of text. */
#limits { max-width: 82ch; }
#limits .caveats { max-width: none; }

/* ---- Long-form document -------------------------------------------------
   Methodology is read, not scanned, so it is a document with a measure and a
   rule between sections - not six equal cards, which flattened a core rule and
   a footnote to the same weight. */
.toc {
  border: 1px solid var(--line-2); border-radius: var(--radius);
  background: var(--surface); padding: var(--s4) var(--s5); margin-bottom: var(--s5);
}
.toc-h {
  font-size: var(--t-micro); font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-3); margin-bottom: var(--s3);
}
.toc ol {
  margin: 0; padding: 0; list-style: none;
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s2) var(--s4);
  counter-reset: toc;
}
.toc li { counter-increment: toc; font-size: 13px; }
.toc li::before {
  content: counter(toc, decimal-leading-zero) " ";
  font-family: var(--mono); font-size: 10.5px; color: var(--text-3);
}
@media (max-width: 900px) { .toc ol { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px) { .toc ol { grid-template-columns: 1fr; } }

.doc { display: grid; gap: 0; }
.doc-sec { padding: var(--s6) 0; border-top: 1px solid var(--line); }
.doc-sec:first-child { border-top: 0; padding-top: 0; }
.doc-sec h2 { margin-bottom: var(--s3); scroll-margin-top: var(--s7); }
.doc-sec p { max-width: 76ch; line-height: 1.65; font-size: clamp(13.5px, .12vw + 13.1px, 15px); }
.doc-sec p + p { margin-top: var(--s3); }

/* Limitations read differently on purpose: this is the section that says what
   the data cannot support, and it should not look like another rule block. */
.doc-caveat {
  border-left: 2px solid var(--warn); padding-left: var(--s5); margin: var(--s4) 0;
  background: linear-gradient(90deg, rgba(227, 181, 82, .05), transparent 40%);
}
.caveats { margin: var(--s4) 0 0; padding-left: var(--s5); display: grid; gap: var(--s3); max-width: 76ch; }
.caveats li { font-size: 13.5px; line-height: 1.6; color: var(--text-2); }
.caveats strong { color: var(--text); }

.score-breakdown {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--s2); margin-top: var(--s4);
}
@media (max-width: 900px) { .score-breakdown { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.signal-grid { margin-top: var(--s4); grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.reason-help { font-size: 13px; font-weight: 400; color: var(--text-2); }

/* A row header keeps table semantics without looking like a column head. */
.row-head { font-weight: inherit; text-align: left; }

/* Sort affordance. The inactive arrow was rgb(66,75,85) on rgb(10,16,23) =
   2.16:1 at 9px, below the 3:1 that WCAG 1.4.11 requires of a control that
   has to be identifiable; and the link box was 22x17, under the 24x24 target
   minimum of 2.5.8. */
th a.col-sort { display: inline-flex; align-items: center; gap: 4px; min-height: 24px; padding: 4px 2px; }
th a.col-sort .arrow { color: var(--text-3); font-size: 10px; }
th a.col-sort:hover .arrow, th a.col-sort:focus-visible .arrow { color: var(--accent); }

/* Content-bearing text below 11px. The dashboard's 10px eyebrows are a
   deliberate house style for labels, but these carry meaning: a section
   heading, and the URL a reader verifies before copying it. */
.snip-head h4 { font-size: 11px; }
.toc-h { font-size: 11px; }
.bl-url { font-size: 12px; }
.pnl-ft code { font-size: 11px; }

/* ---- Stacked-table repairs (<=719px) -------------------------------------
   The stacked table turns each cell into a label/value pair. Three cases it
   did not account for, all measured on a 320-390px viewport. */
@media (max-width: 780px) {
  /* A full-span cell (empty state, "no results") has no data-label, so it was
     still reserving the 80px label column - leaving ~6 characters per line. */
  table.data td[colspan] { display: block; padding-left: 0; }
  table.data td[colspan]::before { content: none; display: none; }

  /* Labels are generated content and inherited overflow-wrap:anywhere from the
     cell, breaking mid-word: "OBSERVATIO / NS". */
  table.data td::before { overflow-wrap: normal; word-break: keep-all; }

  /* A cell with more than one child pushed the third item into the label
     column, orphaning sub-text under the labels. */
  table.data td > * { grid-column: 2; }

  /* A pill stretched to the full value column reads as a bar. */
  table.data td .state-pill, table.data td .chip { justify-self: start; }

  /* The fleet roster lost 82px of width to the panel padding, which forced the
     label track to its 80px floor and broke timestamps onto four lines. */
  .p-fleet-table > .pnl-bd { padding-inline: 0; }
}

/* The filter bar drew its own frame inside a panel body - the only box-in-box
   in the phase. */
.pnl-bd > .filter-bar { border: 0; border-radius: 0; padding: 0; background: none; }

/* ---- Delist workflow ------------------------------------------------------
   A form plus the context needed to fill it in honestly, rather than a bare
   form on a page. The side rail is after the form in DOM order, so keyboard
   and screen-reader users reach the task first. */
.delist-grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); gap: var(--s3); align-items: start; }
@media (max-width: 900px) { .delist-grid { grid-template-columns: 1fr; } }
.delist-side { display: grid; gap: var(--s3); align-content: start; }
.delist-form > .field + .field,
.delist-form > .field-row + .field { margin-top: var(--s4); }
.field-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s4); align-items: start; }
@media (max-width: 560px) { .field-row { grid-template-columns: 1fr; } }
.form-actions { margin-top: var(--s5); }

/* Honeypot: off-screen, never announced, never focusable. */
.hp-field { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.steps { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s4); counter-reset: step; }
.step { display: grid; grid-template-columns: 22px minmax(0, 1fr); gap: var(--s3); align-items: start; }
.step-n {
  display: grid; place-items: center; width: 22px; height: 22px; border-radius: 50%;
  border: 1px solid var(--line-ui); font-family: var(--mono); font-size: 11px;
  color: var(--accent); font-variant-numeric: tabular-nums;
}
.step-t { font-size: 13px; line-height: 1.55; color: var(--text-2); }
.step-t strong { color: var(--text); }

.checklist { margin: 0; padding-left: var(--s5); display: grid; gap: var(--s3); }
.checklist li { font-size: 13px; line-height: 1.55; color: var(--text-2); }

/* ---- Address report -----------------------------------------------------
   The address is the page's identity, so it leads in mono at display size.
   Risk and enrichment sit side by side because a reader checks the score and
   then immediately asks what the address is. */
.crumbs { font-size: 12.5px; color: var(--text-3); margin-bottom: var(--s2); }
.crumbs a { color: var(--text-2); }
.crumbs span[aria-hidden] { opacity: .5; margin: 0 4px; }

.ip-head .ph-top { align-items: center; }
.ip-h1 {
  font-family: var(--mono); font-size: clamp(20px, 1.1vw + 16px, 30px);
  font-weight: 600; letter-spacing: -.01em; overflow-wrap: anywhere; min-width: 0;
}
.ip-head .ph-actions { margin-left: 0; margin-top: var(--s2); }

.ip-grid { display: grid; grid-template-columns: minmax(0, 320px) minmax(0, 1fr); gap: var(--s3); align-items: start; }
@media (max-width: 900px) { .ip-grid { grid-template-columns: minmax(0, 1fr); } }

.ip-note {
  margin-top: var(--s3); padding: var(--s3) var(--s4);
  border-left: 2px solid var(--line-2); max-width: 88ch;
  font-size: 12.5px; line-height: 1.6; color: var(--text-3);
}

/* ---- Blocklist dispatch -------------------------------------------------- */
.bl-grid { display: grid; gap: var(--s3); grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 1023px) { .bl-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 700px) { .bl-grid { grid-template-columns: 1fr; } }
.bl-figures { display: flex; gap: var(--s6); margin-bottom: var(--s3); }
.bl-n {
  display: block; font-family: var(--display); font-size: var(--t-readout);
  font-weight: 600; letter-spacing: -.02em; color: #fff; font-variant-numeric: tabular-nums;
}
.bl-k {
  display: block; font-size: var(--t-micro); font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--text-3);
}
.bl-desc { font-size: 13px; color: var(--text-2); line-height: 1.5; }
.bl-actions { display: flex; gap: var(--s2); flex-wrap: wrap; margin: var(--s4) 0 var(--s3); }
.bl-url {
  display: block; font-family: var(--mono); font-size: 10.5px; color: var(--text-3);
  overflow-wrap: anywhere;
}

/* Copyable snippets. The copy button reuses the existing .copy-btn delegate,
   so this adds no JavaScript. */
.snip-grid { display: grid; gap: var(--s3); grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: var(--s4); }
@media (max-width: 900px) { .snip-grid { grid-template-columns: minmax(0, 1fr); } }
.snip-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); margin-bottom: var(--s2); }
.snip-head h3 { font-size: 12.5px; }

/* ---- Sparkline ---------------------------------------------------------- */
.spark { display: inline-flex; align-items: center; gap: var(--s2); min-width: 0; }
.spark svg { width: 64px; height: 20px; overflow: visible; display: block; }
.spark-area { fill: rgba(79, 209, 197, .14); }
.spark-line {
  fill: none; stroke: var(--accent); stroke-width: 1.4;
  stroke-linejoin: round; stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.spark-peak {
  font-family: var(--mono); font-size: 10px; color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.spark-empty { display: inline-block; width: 64px; height: 20px; }

/* ==========================================================================
   Responsive
   ========================================================================== */
@media (max-width: 1400px) {
  .wrap-wide { padding-left: var(--s5); padding-right: var(--s5); }
  .wrap-wide table.data th, .wrap-wide table.data td { padding-left: 10px; padding-right: 10px; }
  .wrap-wide table.data th:first-child, .wrap-wide table.data td:first-child { padding-left: var(--s3); }
  .wrap-wide table.data th:last-child, .wrap-wide table.data td:last-child { padding-right: var(--s3); }
  .wrap-wide .net-cell { max-width: 176px; }
  .wrap-wide table.data td.wrap-cell { min-width: 150px; }
}

@media (max-width: 1180px) {
  .hero { grid-template-columns: 1fr; }
  }

@media (max-width: 900px) {
  .wrap { padding-left: var(--s4); padding-right: var(--s4); }
  .ip-hero { grid-template-columns: 1fr; }
  .hero h1, .hero h2 { font-size: 24px; }
}

@media (max-width: 780px) {
  .nav-toggle {
    display: inline-flex; margin-left: auto; align-items: center; gap: var(--s2);
    min-height: 38px; padding: 0 var(--s3); border: 1px solid var(--line-strong);
    border-radius: var(--radius-sm); background: var(--surface-2);
    color: var(--text); font: 650 13px/1 var(--sans); cursor: pointer;
  }
  .site-nav {
    display: none; width: 100%; margin: 0; padding-bottom: var(--s3);
    flex-direction: column; align-items: stretch; gap: 2px;
  }
  .site-nav.is-open { display: flex; }
  .site-nav a, .site-nav button { width: 100%; justify-content: flex-start; min-height: 42px; }
  .header-inner { flex-wrap: wrap; }

  /* Tables collapse into readable cards */
  .table-wrap { border: 0; background: transparent; }
  /* Card mode still keeps a scroller: it must never be the page that scrolls. */
  .table-scroll { overflow-x: auto; }
  table.data { font-size: 14px; }
  table.data thead { display: none; }
  table.data tbody, table.data tr, table.data td { display: block; width: 100%; }
  table.data tbody tr {
    margin-bottom: var(--s3); padding: var(--s3) var(--s4);
    border: 1px solid var(--line); border-radius: var(--radius);
    background: var(--surface-2);
  }
  table.data tbody tr:hover { background: var(--surface-2); }
  table.data td {
    display: grid; grid-template-columns: minmax(80px, 34%) minmax(0, 1fr); gap: var(--s3);
    align-items: baseline; padding: 7px 0; white-space: normal; text-align: left;
    overflow-wrap: anywhere;
  }
  table.data td > * { min-width: 0; }
  table.data td::before {
    content: attr(data-label); color: var(--text-3); font-size: 10.5px;
    font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  }
  table.data td.num { text-align: left; }
  table.data td:last-child { border-bottom: 0; padding-bottom: 0; }
  table.data td:first-child { padding-top: 0; }
  .net-cell { max-width: none; }
  .hide-sm { display: none; }
  .pager { flex-direction: column; align-items: stretch; gap: var(--s3); }
  .pager-controls { justify-content: space-between; }
  .pager-controls .btn { flex: 1 1 0; }
}

/* Below ~420px stack the label above the value: two columns cannot hold a
   timestamp and its label at that width.

   This block MUST stay after the max-width:780px card rules above. It used to
   sit between the two 780px blocks, so at 390px both matched and, at equal
   specificity, the later 780px rule won: the grid kept two columns while
   `td > *` still moved every child into column one. Every value was squeezed
   into the 80px minmax floor beside a permanently empty second column, which
   is what turned the feed into a 37,000px page of tall, mostly-blank cards.
   Ordering is the whole fix; nothing here is a new visual decision. */
@media (max-width: 420px) {
  table.data td {
    grid-template-columns: minmax(0, 1fr);
    /* The 12px gap separated the label and value COLUMNS. Once they stack it
       becomes vertical dead space in every cell of every row, so the stacked
       form gets a row gap sized for stacked text instead. */
    row-gap: 2px;
  }
  table.data td > * { grid-column: 1; }
  table.data td::before { padding-bottom: 2px; }
}

@media (max-width: 700px) {
  .matrix-row { grid-template-columns: minmax(72px, 96px) minmax(0, 1fr) auto; gap: var(--s2); }
  .matrix-country { font-size: 12px; }
  .matrix-legend { gap: var(--s2); font-size: 10px; }
  .activity-head { flex-direction: column; align-items: stretch; }
  .range-tabs { align-self: flex-start; }
  .activity-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* Two short lines beat an ellipsised label. */
  .activity-kpi .k { white-space: normal; min-height: 2em; }
}

@media (max-width: 560px) {
  .wrap { padding-left: var(--s3); padding-right: var(--s3); }
  main.wrap { padding-top: var(--s4); }
  .card-pad { padding: var(--s4); }
  .stat-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .stat .stat-value { font-size: 22px; }
  .hero h1, .hero h2 { font-size: 21px; }
  .ip-hero { padding: var(--s4); }
  .ip-hero h1 { font-size: 20px; }
  .search-form { grid-template-columns: 1fr; }
  .search-form .btn { width: 100%; }
  .filter-bar { grid-template-columns: 1fr; padding: var(--s3); }
  .filter-bar > .field-search { grid-column: span 1; }
  .mix-legend { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .bars { height: 96px; }
  .modal { padding: 0; }
  .modal-panel { max-height: 100vh; height: 100%; border-radius: 0; width: 100%; }
  .footer-inner { flex-direction: column; gap: var(--s3); }
}

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

@media (prefers-contrast: more) {
  :root { --line: rgba(190, 214, 235, .34); --line-strong: rgba(190, 214, 235, .5); --text-3: #93a5b6; }
}

@media print {
  .site-header, .site-footer, .filter-bar, .pager, .modal, .copy-btn { display: none !important; }
  body { background: #fff; color: #000; }
}

/* ---- Live data ----------------------------------------------------------
   Motion here is only ever the consequence of a value that actually changed.
   Nothing pulses on a timer and no element animates while the data behind it
   is still. Reduced motion removes the transitions and keeps every value. */
.live-line {
  display: inline-flex; align-items: center; gap: 6px;
  font-variant-numeric: tabular-nums; white-space: nowrap;
  /* Reserved width: the age text is the only thing on the page that changes
     length on a timer, and letting it reflow its container was the entire
     measured steady-state layout shift. */
  min-width: 15ch; justify-content: flex-start;
}
.live-line::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--muted); flex: none;
}
.live-line[data-live-state="live"]::before { background: var(--ok, #35d07f); }
.live-line[data-live-state="stale"]::before { background: var(--warn, #e0a33e); }
.live-line[data-live-state="stale"] { color: var(--warn, #e0a33e); }

/* A value that just changed is briefly tinted. Colour is never the only
   signal: the number itself has already changed, and the feed announces
   arrivals as text to the live region. */
.just-changed {
  animation: liveTint 1.2s ease-out;
}
@keyframes liveTint {
  0%   { color: var(--accent, #6ea8fe); }
  100% { color: inherit; }
}

.feed-live { display: flex; justify-content: flex-end; min-height: 0; }
.feed-new {
  appearance: none; border: 1px solid var(--line); background: var(--panel);
  color: var(--text); border-radius: 999px; padding: 6px 14px;
  font: inherit; font-size: 13px; cursor: pointer;
  margin: 0 0 10px; transition: background 160ms ease, border-color 160ms ease;
}
.feed-new:hover, .feed-new:focus-visible { border-color: var(--accent, #6ea8fe); }
.feed-new[hidden] { display: none; }

/* Newly inserted rows settle in rather than snapping. The table is trimmed
   from the bottom as rows arrive at the top, so the page height does not
   change and nothing below the table moves. */
.row-new > * { animation: rowIn 520ms ease-out; }
@keyframes rowIn {
  from { background: color-mix(in srgb, var(--accent, #6ea8fe) 14%, transparent); }
  to   { background: transparent; }
}

@media (prefers-reduced-motion: reduce) {
  .just-changed { animation: none; }
  .row-new > * { animation: none; }
  .feed-new { transition: none; }
}

/* ---- Command Center ------------------------------------------------------
   The console reads as one instrument, not a row of widgets: the layer
   controls, the investigation target and the wallboard entry all sit on one
   bar directly above the map they govern. Motion here only ever follows a
   real state change - a target being selected, or a genuine observation
   arriving for it. */
.console-bar {
  display: flex; flex-wrap: wrap; gap: 10px 14px;
  align-items: center; justify-content: space-between;
  margin: 0 0 12px; padding-bottom: 10px;
  border-bottom: 1px solid var(--line);
}
.console-bar-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

.layer-bar { display: flex; flex-wrap: wrap; gap: 6px; }
.layer-btn {
  appearance: none; font: inherit; font-size: 12px; letter-spacing: .02em;
  padding: 5px 11px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--line); background: transparent; color: var(--muted);
  transition: color 140ms ease, border-color 140ms ease, background 140ms ease;
}
.layer-btn:hover { color: var(--text); }
.layer-btn.is-on { color: var(--text); border-color: var(--accent, #6ea8fe); background: rgba(110,168,254,.10); }
.layer-btn:focus-visible { outline: 2px solid var(--accent, #6ea8fe); outline-offset: 2px; }

.tracked-chip {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 12px; padding: 4px 6px 4px 10px; border-radius: 999px;
  border: 1px solid var(--accent, #6ea8fe); color: var(--text);
  font-variant-numeric: tabular-nums;
  /* A full-length IPv6 made this pill 394px wide inside a 390px viewport, so
     it ran off the right edge and took the end of the address with it. The
     pill wraps its own contents instead of growing past its container. */
  flex-wrap: wrap; max-width: 100%; min-width: 0;
}
.tracked-chip [data-tracked-ip] {
  font-family: var(--mono, ui-monospace, monospace);
  overflow-wrap: anywhere; min-width: 0;
}
.tracked-chip[hidden] { display: none; }
.tracked-chip .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent, #6ea8fe); }
.tracked-stop, .drawer-close {
  appearance: none; border: 0; background: transparent; color: var(--muted);
  font: inherit; cursor: pointer; padding: 2px 6px; border-radius: 6px; line-height: 1;
}
.tracked-stop:hover, .drawer-close:hover { color: var(--text); }
.tracked-stop:focus-visible, .drawer-close:focus-visible { outline: 2px solid var(--accent, #6ea8fe); }

.track-btn {
  appearance: none; border: 0; background: transparent; color: var(--muted);
  cursor: pointer; padding: 0 4px; font-size: 12px; line-height: 1; border-radius: 4px;
}
.track-btn:hover { color: var(--text); }
.track-btn:focus-visible { outline: 2px solid var(--accent, #6ea8fe); outline-offset: 1px; }
.track-btn.is-tracked, .track-btn[aria-pressed="true"] { color: var(--accent, #6ea8fe); }
tr.is-tracked > * { background: rgba(110,168,254,.07); }
tr.is-tracked > :first-child { box-shadow: inset 2px 0 0 var(--accent, #6ea8fe); }

/* The tracked country is emphasised, never animated: an arc means an
   observation occurred, and selecting a target is not an observation. */
.map-node.is-target {
  outline: 2px solid var(--accent, #6ea8fe); outline-offset: 3px; z-index: 7;
}

/* ---- Intelligence drawer ------------------------------------------------- */
.drawer {
  position: fixed; top: 0; right: 0; bottom: 0; width: min(380px, 92vw);
  z-index: 40; display: flex; flex-direction: column; gap: 12px;
  padding: 18px 18px 24px; overflow-y: auto; overscroll-behavior: contain;
  background: var(--panel); border-left: 1px solid var(--line);
  box-shadow: -18px 0 48px rgba(0,0,0,.34);
}
.drawer[hidden] { display: none; }
.drawer-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.drawer-eyebrow { display: block; font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--muted); }
.drawer-ip { font-size: 19px; font-variant-numeric: tabular-nums; word-break: break-all; }
.drawer-meta { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.drawer-empty { color: var(--muted); font-size: 13px; }
.drawer .kv-grid { display: grid; gap: 2px; }
/* Fixed columns and a reserved line height so a field going from the em-dash
   placeholder to a real value cannot reflow its neighbours. Populating the
   drawer measured CLS 0.0485 as a flex row; as a grid it is ~0. */
.drawer .kv {
  display: grid; grid-template-columns: minmax(92px, 42%) 1fr; gap: 12px;
  align-items: baseline; padding: 7px 0; border-bottom: 1px solid var(--line);
}
.drawer .kv .k { color: var(--muted); font-size: 12px; }
.drawer .kv .v {
  text-align: right; font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere; min-height: 1.35em;
}
.drawer.has-activity { box-shadow: -18px 0 48px rgba(0,0,0,.34), inset 3px 0 0 var(--accent, #6ea8fe); }

@media (max-width: 719px) {
  /* A bottom sheet on small screens: a full-height side panel would bury the
     map the operator is still working from. */
  .drawer {
    top: auto; left: 0; width: auto; max-height: 72vh;
    border-left: 0; border-top: 1px solid var(--line);
    border-radius: 14px 14px 0 0; box-shadow: 0 -18px 48px rgba(0,0,0,.4);
  }
}

/* ---- Wallboard ----------------------------------------------------------- */
.wallboard-exit {
  position: fixed; top: 14px; right: 14px; z-index: 60;
  appearance: none; font: inherit; font-size: 12px; padding: 7px 13px;
  border-radius: 999px; cursor: pointer;
  border: 1px solid var(--line); background: var(--panel); color: var(--text);
}
.wallboard-exit[hidden] { display: none; }
.wallboard-exit:focus-visible { outline: 2px solid var(--accent, #6ea8fe); outline-offset: 2px; }

/* Chrome is removed; every live surface keeps running underneath. */
html.is-wallboard .site-header,
html.is-wallboard .site-footer,
html.is-wallboard .p-filters,
html.is-wallboard .wallboard-enter,
html.is-wallboard .statusbar .sb-spacer { display: none !important; }
html.is-wallboard body { padding-top: 0; }
html.is-wallboard .console-main { max-width: none; padding: 14px 20px 20px; }
html.is-wallboard .pnl { border-radius: 10px; }
html.is-wallboard .stat-value { font-size: clamp(28px, 3.2vw, 56px); }
html.is-wallboard .stat-label { font-size: clamp(10px, .74vw, 13px); }
html.is-wallboard .world-map { min-height: clamp(360px, 52vh, 900px); }

@media (min-width: 2400px) {
  html.is-wallboard .console-main { padding: 26px 40px; }
  html.is-wallboard .stat-value { font-size: clamp(44px, 2.8vw, 76px); }
}

@media (prefers-reduced-motion: reduce) {
  .layer-btn, .track-btn { transition: none; }
}

/* ---- Command Center Wave 2 ----------------------------------------------
   The console bar now states three things: the perspective being used, the
   investigation in progress, and how trustworthy the data is. Nothing here
   animates on a timer - every transition follows an operator action or a real
   change in data health. */
.scene-bar { display: flex; flex-wrap: wrap; gap: 6px; }
.scene-btn {
  appearance: none; font: inherit; font-size: 12px; letter-spacing: .01em;
  padding: 5px 12px; border-radius: 6px; cursor: pointer;
  border: 1px solid var(--line); background: transparent; color: var(--muted);
  transition: color 140ms ease, border-color 140ms ease, background 140ms ease;
}
.scene-btn:hover { color: var(--text); }
.scene-btn.is-on {
  color: var(--text); border-color: var(--accent, #6ea8fe);
  background: rgba(110,168,254,.12);
}
.scene-btn:focus-visible { outline: 2px solid var(--accent, #6ea8fe); outline-offset: 2px; }

.inv-chip {
  display: inline-flex; align-items: center; gap: 7px; flex-wrap: wrap;
  font-size: 12px; padding: 4px 6px 4px 10px; border-radius: 999px;
  border: 1px solid var(--accent, #6ea8fe); color: var(--text);
  font-variant-numeric: tabular-nums;
}
.inv-chip[hidden] { display: none; }
.inv-label { color: var(--muted); font-size: 10px; letter-spacing: .12em; text-transform: uppercase; }
.inv-facet { padding: 1px 7px; border-radius: 999px; background: rgba(110,168,254,.14); }
.inv-facet[hidden] { display: none; }
/* Geography and severity read differently from the address on purpose: they
   are separate facets of one investigation, not the same selection. */
.inv-geo { background: rgba(255,255,255,.08); letter-spacing: .06em; }
.inv-sev { background: rgba(224,163,62,.16); text-transform: capitalize; }
.inv-clear {
  appearance: none; border: 0; background: transparent; color: var(--muted);
  font: inherit; cursor: pointer; padding: 2px 6px; border-radius: 6px; line-height: 1;
}
.inv-clear:hover { color: var(--text); }
.inv-clear:focus-visible { outline: 2px solid var(--accent, #6ea8fe); }

.share-btn.is-done { border-color: var(--ok, #35d07f); color: var(--ok, #35d07f); }

/* A selected country is emphasised, not animated: selecting a geography is
   not an observation. */
.map-node[aria-pressed="true"], .map-node.is-selected {
  outline: 2px dashed var(--text); outline-offset: 3px; z-index: 6;
}
.map-node { cursor: pointer; }

/* Data health, one visual language everywhere it appears. Colour is never the
   only signal - the state word is always present in the text. */
.live-line[data-live-state="degraded"]::before { background: var(--warn, #e0a33e); }
.live-line[data-live-state="degraded"] { color: var(--warn, #e0a33e); }
.live-line[data-live-state="unavailable"]::before { background: var(--bad, #ff6b6b); }
.live-line[data-live-state="unavailable"] { color: var(--bad, #ff6b6b); }

/* Scene emphasis: presentation only, derived from state already on the page. */
html[data-scene="severity"] .map-node:not(.is-target):not(.is-selected) { opacity: .55; }
html[data-scene="sources"] .map-node { opacity: 1; }

html.is-wallboard .scene-bar { gap: 8px; }
html.is-wallboard .scene-btn { font-size: clamp(11px, .8vw, 15px); }
html.is-wallboard .share-btn { display: none; }

@media (max-width: 719px) {
  .console-bar { gap: 8px; }
  .scene-btn, .layer-btn { font-size: 11px; padding: 5px 9px; }
  .inv-chip { font-size: 11px; }
}

@media (prefers-reduced-motion: reduce) {
  .scene-btn { transition: none; }
}

/* ---- Intelligence layer (Wave 3) ----------------------------------------
   The evidence is the point, so the treatment is deliberately plain: no
   gauges, no glow, no animated confidence. A finding is a label, a sentence,
   and the numbers behind it one keystroke away. */
.intel-panel .pnl-head { align-items: baseline; }
.intel-meta {
  display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap;
  margin-left: auto; font-size: 11px; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.intel-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.intel-item { padding: 10px 0; border-bottom: 1px solid var(--line); }
.intel-item:last-child { border-bottom: 0; }
.intel-head { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }

/* The label is a word, never a score. Colour reinforces it; the word carries
   it, so nothing is conveyed by colour alone. */
.intel-label {
  font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  padding: 2px 8px; border-radius: 999px;
  border: 1px solid var(--line); color: var(--muted);
}
.intel-notable { border-color: var(--bad, #ff6b6b); color: var(--bad, #ff6b6b); }
.intel-elevated { border-color: var(--warn, #e0a33e); color: var(--warn, #e0a33e); }
.intel-normal { border-color: var(--line); color: var(--muted); }
.intel-reduced { border-color: var(--line); color: var(--muted); }
.intel-insufficient { border-color: var(--line); color: var(--muted); font-style: italic; }
.intel-kind {
  font-size: 10px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted);
}
.intel-text { margin: 0; font-size: 14px; line-height: 1.5; }

.intel-save { margin: 8px 0 0; }
.drawer-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
  padding: 8px 0; border-bottom: 1px solid var(--line); }
.wb-select { padding: 4px 8px; border-radius: 8px; border: 1px solid var(--line);
  background: var(--bg-elev); color: var(--fg); font: inherit; max-width: 100%; }
/* ---------------------------------------------- investigation workbench */
.wb-new-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.wb-input { flex: 1 1 260px; min-width: 0; padding: 8px 10px; border-radius: 8px;
  border: 1px solid var(--line); background: var(--bg-elev); color: var(--fg); font: inherit; }
.wb-textarea { width: 100%; box-sizing: border-box; padding: 8px 10px; border-radius: 8px;
  border: 1px solid var(--line); background: var(--bg-elev); color: var(--fg);
  font: inherit; resize: vertical; margin-bottom: 8px; }
.wb-list, .wb-notes, .wb-timeline { list-style: none; margin: 0; padding: 0;
  display: grid; gap: 10px; }
.wb-item, .wb-note, .wb-tl-item { padding: 10px 0; border-bottom: 1px solid var(--line); }
.wb-item:last-child, .wb-note:last-child, .wb-tl-item:last-child { border-bottom: 0; }
.wb-item-head, .wb-tl-head, .wb-case-head, .wb-watch-head {
  display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.wb-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.wb-chip { display: inline-block; padding: 2px 8px; border-radius: 999px;
  border: 1px solid var(--line); font-size: 12px; line-height: 1.6; }
.wb-chip-quiet { opacity: 0.72; }
.wb-chip-change { border-color: var(--accent, #6ea8fe); }
/* An analyst note must never be mistaken for a measured fact, so it is marked
   in its own colour as well as its own word. */
.wb-chip-note { border-style: dashed; opacity: 0.9; }
.wb-note-body { margin: 6px 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.wb-digest { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px; overflow-wrap: anywhere; margin: 4px 0 0; opacity: 0.85; }
.wb-changes { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
.wb-meta { margin: 4px 0 0; font-size: 13px; opacity: 0.8; }
.wb-error { margin: 8px 0 0; color: var(--danger, #f2777a); }
.wb-empty { opacity: 0.75; margin: 6px 0 0; }
.wb-ip, .wb-case-link { font-weight: 600; }
.wb-tl-text { margin: 4px 0 0; overflow-wrap: anywhere; }
.btn-small { padding: 3px 10px; font-size: 12px; }
@media (max-width: 640px) {
  .wb-new-row { flex-direction: column; align-items: stretch; }
  .wb-actions .btn { flex: 1 1 auto; text-align: center; }
}
.intel-spark { display: block; margin: 6px 0 2px; width: 100%; max-width: 260px; height: 30px; }
.intel-spark .spark-line { fill: none; stroke: var(--accent, #6ea8fe); stroke-width: 1.5; }
.intel-spark .spark-band { fill: var(--line); opacity: 0.35; }
.intel-spark .spark-today { fill: var(--accent, #6ea8fe); }
html.is-wallboard .intel-spark { max-width: 420px; height: 44px; }
.intel-evidence { margin-top: 6px; }
.intel-evidence > summary {
  cursor: pointer; font-size: 12px; color: var(--muted);
  list-style: none; display: inline-flex; align-items: center; gap: 5px;
}
.intel-evidence > summary::-webkit-details-marker { display: none; }
.intel-evidence > summary::before { content: "+"; font-variant-numeric: tabular-nums; }
.intel-evidence[open] > summary::before { content: "2"; }
.intel-evidence > summary:hover { color: var(--text); }
.intel-evidence > summary:focus-visible { outline: 2px solid var(--accent, #6ea8fe); outline-offset: 2px; }
.intel-evidence dl {
  display: grid; grid-template-columns: minmax(120px, 38%) 1fr;
  gap: 2px 12px; margin: 8px 0 0; font-size: 12px;
  font-variant-numeric: tabular-nums;
}
/* dd could always wrap; dt could not, so an address used as the term - which
   is how the evidence list names a source - overflowed its 38% column. */
.intel-evidence dt { color: var(--muted); overflow-wrap: anywhere; min-width: 0; }
.intel-evidence dd { margin: 0; overflow-wrap: anywhere; min-width: 0; }
.intel-note { margin: 12px 0 0; font-size: 11px; color: var(--muted); line-height: 1.5; }

/* ---- Analyst query ------------------------------------------------------- */
.q-form { display: flex; gap: 8px; align-items: center; }
.q-input {
  flex: 1 1 auto; min-width: 0; font: inherit; font-size: 13px;
  padding: 7px 11px; border-radius: 8px;
  border: 1px solid var(--line); background: var(--panel); color: var(--text);
  font-variant-numeric: tabular-nums;
}
.q-input:focus-visible { outline: 2px solid var(--accent, #6ea8fe); outline-offset: 1px; }
.q-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.q-chip {
  appearance: none; font: inherit; font-size: 11px; cursor: pointer;
  padding: 4px 10px; border-radius: 999px;
  border: 1px solid var(--line); background: transparent; color: var(--muted);
}
.q-chip:hover { color: var(--text); }
.q-chip:focus-visible { outline: 2px solid var(--accent, #6ea8fe); outline-offset: 2px; }
.q-out { margin-top: 12px; font-size: 13px; }
.q-line { margin: 0 0 6px; line-height: 1.5; }
.q-table {
  display: grid; grid-template-columns: minmax(110px, 34%) 1fr; gap: 2px 12px;
  margin: 6px 0 0; font-size: 12px; font-variant-numeric: tabular-nums;
}
/* Same shape and same 34% cap as the evidence list, and "top ips" puts an
   address in the term - so the term needs to be able to wrap too. */
.q-table dt { color: var(--muted); overflow-wrap: anywhere; min-width: 0; }
.q-table dd { margin: 0; overflow-wrap: anywhere; min-width: 0; }
.q-action { display: inline-block; margin-top: 8px; font-size: 12px; }

/* ---- Entity assessment in the drawer ------------------------------------- */
.entity-intel { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); }
.entity-intel[hidden] { display: none; }
.entity-h { font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); margin: 0 0 8px; }
.entity-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.entity-finding { display: grid; grid-template-columns: auto 1fr; gap: 8px; align-items: baseline; font-size: 12px; }
.entity-tag {
  font-size: 9px; letter-spacing: .1em; text-transform: uppercase;
  padding: 2px 7px; border-radius: 999px; border: 1px solid var(--line); color: var(--muted);
  white-space: nowrap;
}
.entity-text { line-height: 1.5; }

/* Wallboard shows the assessment, not the query console. */
html.is-wallboard .query-panel { display: none; }
html.is-wallboard .intel-note { display: none; }
html.is-wallboard .intel-text { font-size: clamp(14px, 1.15vw, 22px); }
html.is-wallboard .intel-list > li:nth-child(n+4) { display: none; }

@media (max-width: 719px) {
  .intel-meta { margin-left: 0; }
  .intel-evidence dl, .q-table { grid-template-columns: 1fr; }
  .intel-evidence dd, .q-table dd { margin-bottom: 6px; }
}

/* ==========================================================================
   PREMIUM REFINEMENT LAYER
   --------------------------------------------------------------------------
   Deliberately the last section of this stylesheet. Every rule below refines
   a decision made above rather than introducing a new component, and the
   cascade-order bug repaired at a8eee8d is the reason this is a marked block
   at the end instead of edits sprinkled through the file: at equal
   specificity the later rule wins, so "later" is where the refinement has to
   live for it to be predictable.

   The console did not feel cluttered because it lacked decoration. It felt
   cluttered because it repeated itself, gave identical weight to unequal
   things, and opened every drawer at once. Nothing here adds ornament.
   ========================================================================== */

/* ---- 1. Softer geometry -------------------------------------------------
   3px reads as a wireframe. 6px reads as a considered surface without
   becoming a consumer app. */
:root {
  --radius: 6px;
  --radius-sm: 4px;
  --s9: 56px;                     /* section separation, above the old --s8 */
}

/* ---- 2. Panels breathe, and stop shouting -------------------------------
   A flat surface with a hairline reads calmer than a gradient inside a
   brighter outline; the panel should be felt, not outlined. */
.pnl {
  border-color: var(--line);
  background: var(--surface);
}
.pnl-hd {
  padding: var(--s4) var(--s5) var(--s3);
  border-bottom-color: var(--line);
  gap: var(--s2) var(--s3);
}
.pnl-bd { padding: var(--s5); }
.pnl-ft {
  padding: var(--s3) var(--s5);
  border-top-color: var(--line);
  color: var(--text-4);
  line-height: 1.6;
}
/* Corner ticks were instrument chrome on every panel at once. Keep them for
   the console's focal surfaces only. */
.pnl .pnl-tick { opacity: .5; }

/* Console rhythm: the grid gap was the same size as the padding inside each
   panel, so panels ran together into one field of boxes. */
.console-main { gap: var(--s6); padding-block: var(--s6) var(--s9); }

/* ---- 3. Type hierarchy --------------------------------------------------
   The eyebrow was competing with the title it introduces. */
.pnl-eyebrow {
  font-size: 9.5px; letter-spacing: .16em; color: var(--text-4); font-weight: 650;
}
.pnl-ttl { letter-spacing: -.015em; font-weight: 600; }
.pnl-meta { color: var(--text-4); font-size: 10.5px; letter-spacing: .01em; }
.pnl-note, .intel-note, .activity-note, .health-note, .map-field-note {
  line-height: 1.6; color: var(--text-4);
}

/* ---- 4. One metric treatment --------------------------------------------
   There were three: a plain readout, and two variants wearing a 3px coloured
   left bar. The colour was decorative - it repeated a hue already shown in
   the chart beside it - while the bar made every metric look like an alert.
   The number carries the emphasis now, the label recedes, and the accent
   survives as a small mark rather than a slab. */
.mix-item, .activity-kpi {
  border-left-width: 1px;
  border-left-color: var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  position: relative;
  padding-left: var(--s3);
}
.mix-item::before, .activity-kpi::before {
  content: ""; position: absolute; left: 0; top: var(--s3); bottom: var(--s3);
  width: 2px; border-radius: 0 2px 2px 0;
  background: var(--c, var(--line-3));
}
.mix-item .mix-name, .activity-kpi .k {
  color: var(--text-4); font-size: 9.5px; letter-spacing: .12em; font-weight: 650;
}
.mix-item .mix-value { font-size: 22px; letter-spacing: -.02em; }
.activity-kpi .v { font-size: 18px; letter-spacing: -.02em; }

/* Readout: quieter label, louder value - the metric is the number. */
.ro { padding: var(--s4) var(--s5); gap: 5px; }
.ro-k { color: var(--text-4); font-size: 9.5px; letter-spacing: .13em; font-weight: 650; }
.ro-v { letter-spacing: -.03em; }
.ro-sub { color: var(--text-4); line-height: 1.5; }
.readout { background: var(--line); }

/* ---- 5. The status rail is a status bar, not a dashboard ---------------- */
.statusbar-inner { gap: 0; }
.sb-item { padding-block: var(--s3); }
.sb-k { color: var(--text-4); font-size: 9.5px; letter-spacing: .14em; }

/* ---- 6. Masthead: a console header, not a landing page ------------------ */
.masthead h1 { max-width: 17ch; letter-spacing: -.035em; }
.masthead .lede { max-width: 62ch; color: var(--text-3); line-height: 1.62; }

/* ---- 7. Platform telemetry is not threat data ---------------------------
   System status was styled exactly like the panels reporting observed abuse
   and sat in a prime column. It keeps its place and its precision, at the
   weight of supporting information. */
.p-window .ro-v { font-size: clamp(14px, .3vw + 13px, 16px); color: var(--text); }
.p-window .pnl-ttl { font-size: clamp(15px, .2vw + 14px, 17px); }

/* ---- 8. Tables: rhythm over ruled boxes --------------------------------- */
table.data th {
  font-size: 9.5px; letter-spacing: .13em; color: var(--text-4); font-weight: 650;
}
/* Row air is worth paying for where rows are dense single lines. In card
   mode each cell is already a stacked label-and-value with its own spacing,
   so the same increase there bought nothing and added 2,800px to a 28,000px
   phone page - measured, and reverted to the card treatment's own rhythm. */
@media (min-width: 781px) { table.data td { padding-block: 11px; } }
table.data tbody tr:last-child td { border-bottom: 0; }

/* ---- 9. Restraint in motion --------------------------------------------- */
.pnl { transition: border-color var(--dur-ui) var(--ease-out); }

/* ---- 10. The assessment leads with one finding, not six -----------------
   Every finding was rendered at identical weight with its own evidence
   toggle, so the panel read as a list of six equal announcements and became
   the longest thing on the page. The leading finding is the assessment; the
   rest are the facts supporting it. Nothing is hidden - the evidence toggle
   is still on every row - but the eye is told where to start. */
.intel-item:first-child { padding-bottom: var(--s4); }
.intel-item:first-child .intel-text {
  font-size: 15.5px; line-height: 1.55; color: var(--text);
}
.intel-item + .intel-item { padding: 9px 0; }
.intel-item + .intel-item .intel-head { margin-bottom: 2px; }
.intel-item + .intel-item .intel-text {
  font-size: 13px; line-height: 1.5; color: var(--text-2);
}
.intel-item + .intel-item .intel-kind { opacity: .55; }
.intel-item + .intel-item .intel-evidence > summary { font-size: 11px; }
.intel-scope, .intel-generated { color: var(--text-4); }

/* ---- 11. Right-align the freshness marker ------------------------------- */
.sb-live { margin-left: auto; }
.sb-spacer { border-right: 0; }

/* ---- 12. Mobile: a narrative, not a stack -------------------------------
   At phone width the assessment is what an analyst opened the page for, so
   it comes before the lookup form rather than after it. Order only - the DOM
   sequence, and therefore reading and tab order on every other width, is
   untouched. */
@media (max-width: 700px) {
  .console-main { gap: var(--s5); }
  .console-main > .p-masthead { order: 1; }
  .console-main > .p-metrics  { order: 2; }
  .console-main > .intel-panel { order: 3; }
  .console-main > .p-ipcheck  { order: 4; }
  .console-main > *           { order: 5; }
  .pnl-bd { padding: var(--s4); }
  .pnl-hd { padding: var(--s4) var(--s4) var(--s3); }
  .masthead h1 { font-size: clamp(25px, 6.5vw, 32px); }
  .masthead .lede { font-size: 14px; }
  /* Section headings need room to read as headings on a narrow column. */
  .pnl + .pnl { margin-top: 2px; }
}

/* ---- 13. Flatten the box inside the box ---------------------------------
   The activity section drew its own framed card inside an already-framed
   panel, so the chart sat two borders deep. The panel is the frame. */
.activity { border: 0; background: none; padding: 0; }
.activity-head { padding-bottom: var(--s2); }
.activity-sub { margin-top: 0; color: var(--text-4); }
.activity-kpis { gap: var(--s3); margin-bottom: var(--s4); }

/* ---- 14. The assessment must not shove the page when it arrives ---------
   The panel ships empty by design - Wave 3 renders no server-side claim that
   could drift from the analysis - so the findings appear a moment after paint
   and everything below jumped. Measured: 788px of growth at 390px, and a
   cumulative layout shift of 0.181 before this change.

   The reserve applies ONLY to the empty list, is sized to a single finding
   rather than the whole set, and releases itself the instant the first row is
   inserted. It is not a fixed panel height and it never leaves a blank block
   behind: :empty stops matching as soon as there is content. */
.intel-list:empty { min-height: 148px; }
.intel-list:empty::after {
  content: "Reading the current window\2026";
  display: block; padding-top: var(--s3);
  color: var(--text-4); font-size: 13px;
}

/* ---- 15. The workbench is an instrument, not a form ---------------------
   Every control was a .btn of identical weight, the note field opened at the
   size of a document, and the case status was grey body text beside the
   title. */
.btn-quiet {
  background: none; border-color: transparent; color: var(--text-3);
  padding-inline: var(--s2);
}
.btn-quiet:hover { color: var(--text); border-color: var(--line-2); }
.wb-exports {
  display: inline-flex; gap: 2px; align-items: center; margin-left: auto;
  padding-left: var(--s3); border-left: 1px solid var(--line);
}
.wb-actions { gap: var(--s2); align-items: center; }
.wb-case-head { gap: var(--s3); align-items: center; margin-bottom: 2px; }
.wb-status {
  display: inline-flex; align-items: center; padding: 2px 10px;
  border: 1px solid var(--line-2); border-radius: var(--radius-pill);
  font-size: 10.5px; font-weight: 650; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-3);
}
.wb-status-active { border-color: var(--accent-dim); color: var(--accent); }
.wb-status-closed { opacity: .7; }
/* The note field starts at the size of a note and grows when it is being
   used, instead of reserving a document-sized block on every page load. */
.wb-textarea {
  min-height: 42px; transition: min-height var(--dur-ui) var(--ease-out);
}
.wb-textarea:focus { min-height: 96px; }
.wb-item { padding: var(--s3) 0; }
.wb-item-head { gap: var(--s2) var(--s3); }
/* margin-left:auto cannot right-align a block child, so the row action sat on
   its own line under the address. The row is a two-column grid: content left,
   action right, stacking again on a phone where there is no room to pair. */
.wb-list .wb-item {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  column-gap: var(--s3); align-items: start;
}
.wb-list .wb-item-head, .wb-list .wb-changes { grid-column: 1; }
.wb-list [data-wb-remove], .wb-list [data-wb-unwatch] {
  grid-column: 2; grid-row: 1 / span 2; align-self: center;
}
@media (max-width: 560px) {
  .wb-list .wb-item { grid-template-columns: minmax(0, 1fr); }
  .wb-list [data-wb-remove], .wb-list [data-wb-unwatch] {
    grid-column: 1; grid-row: auto; justify-self: start; margin-top: var(--s2);
  }
}
.wb-note-body { font-size: 14px; line-height: 1.6; }
.wb-meta { color: var(--text-4); }
.wb-timeline .wb-tl-item { padding: var(--s3) 0; }
.wb-digest { font-size: 11px; color: var(--text-4); }

/* ---- 16. A flex basis written for a row must not survive into a column ---
   .wb-input carries flex: 1 1 260px so it fills the row beside its button.
   The phone breakpoint turns .wb-new-row into a column, where that 260px is
   a HEIGHT: the "add an address" field rendered 260px tall on every phone.
   Pre-existing since the workbench shipped; found by looking at the mobile
   screenshot rather than the desktop one. */
@media (max-width: 640px) {
  .wb-new-row > .wb-input { flex: 0 0 auto; width: 100%; }
}

/* ==========================================================================
   RECOMPOSITION
   --------------------------------------------------------------------------
   The previous pass corrected duplication and hierarchy but preserved the
   composition: thirteen full-width panels down one column, the map with zero
   pixels in the first viewport at every width, seventy table rows on a page
   whose job is situational awareness. The owner's verdict on a real phone was
   that it looked basically the same, and the measurements agreed.

   This changes the composition itself. One dominant operational surface owns
   the fold; everything else is reference material in two named regions.
   ========================================================================== */

/* ---- The command surface ------------------------------------------------ */
.cmd {
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
  margin-bottom: var(--s9);
}
.cmd-head {
  display: flex; align-items: flex-end; gap: var(--s5) var(--s6); flex-wrap: wrap;
  padding: var(--s6) var(--s6) var(--s5);
  border-bottom: 1px solid var(--line);
}
.cmd-id { min-width: 0; flex: 1 1 320px; }
.cmd-head h1 {
  font-size: clamp(24px, 1.5vw + 18px, 38px); line-height: 1.04;
  letter-spacing: -.035em; margin: 0;
}
.cmd-sub {
  margin: var(--s2) 0 0; max-width: 56ch; color: var(--text-3);
  font-size: clamp(13px, .18vw + 12.4px, 15px); line-height: 1.55;
}
/* The lookup was a full panel competing with the hero. It is one control. */
.cmd-lookup {
  display: flex; gap: var(--s2); align-items: center;
  flex: 0 1 380px; min-width: 0;
}
.cmd-lookup input {
  flex: 1 1 auto; min-width: 0; padding: 9px 12px;
  border: 1px solid var(--line-2); border-radius: var(--radius-sm);
  background: var(--surface-2); color: var(--text); font: inherit;
}
.cmd-lookup .search-result { flex-basis: 100%; }

/* Map dominant, situation and metrics as the rail beside it. */
.cmd-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.62fr) minmax(300px, .95fr);
  align-items: stretch;
}
.cmd-primary { min-width: 0; border-right: 1px solid var(--line); }
.cmd-side {
  min-width: 0; display: flex; flex-direction: column;
  background: linear-gradient(180deg, rgba(150,180,210,.022), transparent 40%);
}
/* Inside one surface the children stop being cards. This is where most of the
   "box after box" feeling actually went. */
.cmd .pnl, .cmd .intel-panel {
  border: 0; border-radius: 0; background: none; margin: 0;
}
.cmd .pnl-tick { display: none; }
.cmd-side .intel-panel { flex: 1 1 auto; }
.cmd-metrics {
  border-top: 1px solid var(--line);
  grid-template-columns: repeat(3, minmax(0, 1fr));
  background: var(--line);
}
.cmd-metrics .ro { padding: var(--s4) var(--s5); }
.cmd-metrics .ro-v { font-size: clamp(20px, .8vw + 16px, 30px); }

/* The map earns the fold. */
.cmd-primary .map-field, .cmd-primary [data-map] { min-height: clamp(260px, 34vh, 420px); }

/* ---- Reference regions --------------------------------------------------
   Nine peer panels became two named groups. The heading and its rule carry
   the grouping that nine borders were failing to. */
.region { margin-bottom: var(--s9); }
.region-h {
  font-size: 11px; font-weight: 650; letter-spacing: .18em; text-transform: uppercase;
  color: var(--text-4); margin: 0 0 var(--s4);
  padding-bottom: var(--s3); border-bottom: 1px solid var(--line);
}
.region-grid {
  display: grid; gap: var(--s5);
  grid-template-columns: repeat(12, minmax(0, 1fr));
  align-items: start;
}
.region-grid > * { grid-column: 1 / -1; }
.region-grid > .p-activity { grid-column: 1 / 8; }
.region-grid > .p-window   { grid-column: 8 / -1; }
.region-grid > .p-classes  { grid-column: 1 / -1; }
.region-grid > .p-fleet    { grid-column: 1 / 7; }
.region-grid > .p-topips   { grid-column: 7 / -1; }
.region-grid > .p-newest   { grid-column: 1 / 7; }
.region-grid > .p-dispatch { grid-column: 7 / -1; }

/* Reference panels are quieter than the command surface by design. */
.region .pnl { background: none; border-color: var(--line); }

/* The region spans MUST collapse before the table card mode starts, or a
   half-width panel at 430px leaves the value column 19px wide - precisely the
   squeeze repaired at a8eee8d. Caught by that suite, not by eye. The
   breakpoint is 1023px so a panel is never narrower than the card treatment
   assumes. */
@media (max-width: 1023px) {
  .region-grid > .p-activity,
  .region-grid > .p-window,
  .region-grid > .p-classes,
  .region-grid > .p-fleet,
  .region-grid > .p-topips,
  .region-grid > .p-newest,
  .region-grid > .p-dispatch { grid-column: 1 / -1; }
}

/* ---- Mobile: designed as mobile ----------------------------------------
   Not the desktop grid collapsed. The surface reads as one screen - status,
   situation, metrics, map - and the reference regions follow as clearly
   separated groups rather than a continuing stack of peers. */
@media (max-width: 900px) {
  .cmd-grid { grid-template-columns: minmax(0, 1fr); }
  .cmd-primary { border-right: 0; border-bottom: 1px solid var(--line); }
}
@media (max-width: 700px) {
  .cmd { margin-bottom: var(--s7); border-radius: var(--radius); }
  .cmd-head { padding: var(--s5) var(--s4) var(--s4); gap: var(--s4); }
  .cmd-head h1 { font-size: clamp(23px, 6.4vw, 30px); }
  .cmd-sub { font-size: 13.5px; }
  .cmd-lookup { flex: 1 1 100%; }
  /* Situation before the map on a phone: the sentence is readable at 390px,
     the map is an overview. DOM order already matches, so reading and tab
     order need no override. */
  .cmd-side { order: 1; }
  .cmd-primary { order: 2; border-bottom: 0; border-top: 1px solid var(--line); }
  .cmd-grid { display: flex; flex-direction: column; }
  .cmd-metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .cmd-metrics .ro { padding: var(--s3) var(--s3); }
  .cmd-metrics .ro-v { font-size: clamp(18px, 5.4vw, 24px); }
  .cmd-metrics .ro-sub { display: none; }
  .region { margin-bottom: var(--s7); }
  .region-grid { gap: var(--s4); }
  .cmd-primary .map-field, .cmd-primary [data-map] { min-height: clamp(200px, 30vh, 300px); }
}

/* ---- Command surface corrections ---------------------------------------
   Measured on the first prototype rather than assumed. */

/* The geo panel pairs the map with a 300px ranking rail. Inside the command
   surface's primary cell that pairing squeezed the map to roughly 400x180 -
   smaller than it was before the recomposition, which defeats the point. In
   here the map takes the full width and the rankings sit beneath it. */
.cmd-primary .geo-grid { grid-template-columns: minmax(0, 1fr); }
.cmd-primary .map-field,
.cmd-primary .map-wrap,
.cmd-primary [data-map] { min-height: clamp(300px, 40vh, 520px); }
/* The ranked lists become a compact pair under the map instead of a tall rail. */
.cmd-primary .geo-lists { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s4); }
@media (max-width: 700px) {
  .cmd-primary .geo-lists { grid-template-columns: minmax(0, 1fr); }
}

/* The map caption is an overlay by design. Making it static put it into flow
   at the TOP of the plot, printing a paragraph across the northern hemisphere
   - my own doing, corrected here rather than papered over. It keeps its
   overlay position and simply sits at the foot of the field. */
.cmd-primary .map-foot { padding: 0 var(--s3) var(--s2); }
.cmd-primary .map-field-note { max-width: none; font-size: 10px; line-height: 1.45; }
/* The three overlays used to be given three hand-tuned bands here. The foot
   stacks them instead, so the bands - and the arithmetic holding them apart -
   are gone. */

/* Metrics lead the rail, so they need to read as the headline numbers. */
.cmd-side .cmd-metrics { border-top: 0; border-bottom: 1px solid var(--line); }

/* The lookup input was clipping its own placeholder. */
.cmd-lookup { flex: 0 1 440px; }
.cmd-lookup input { min-width: 220px; }

/* ---- Alert triage (Wave 5) ---------------------------------------------
   Priority is a word first; the colour is a second channel, never the only
   one. */
.alert-filters { display: flex; gap: var(--s2); flex-wrap: wrap; margin-bottom: var(--s4); }
.alert-filters .btn[aria-pressed="true"] { border-color: var(--accent-dim); color: var(--accent); }
.alert-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s3); }
.alert-item {
  padding: var(--s4); border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface-2); border-left: 2px solid var(--line-3);
}
.alert-p-attention { border-left-color: var(--sig-watch, #d7a64a); }
.alert-p-high { border-left-color: var(--sig-alert, #f2777a); }
.alert-head { display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap; }
.alert-when { margin-left: auto; font-size: 11px; color: var(--text-4); }
.alert-title { margin: var(--s2) 0 2px; font-size: 15px; line-height: 1.45; }
.alert-meta { margin: 0; font-size: 11.5px; color: var(--text-4);
  font-family: var(--mono); overflow-wrap: anywhere; }
.alert-related { margin: var(--s2) 0 0; font-size: 12.5px; display: flex;
  gap: var(--s2); align-items: center; flex-wrap: wrap; }
.alert-closed-note { color: var(--text-4); }
.alert-evidence { margin-top: var(--s3); }
.alert-actions { display: flex; gap: var(--s2); flex-wrap: wrap; margin-top: var(--s3); }
.alert-chip-high { border-color: var(--sig-alert, #f2777a); color: var(--sig-alert, #f2777a); }
.alert-chip-attention { border-color: var(--sig-watch, #d7a64a); color: var(--sig-watch, #d7a64a); }
.alert-state-new { border-color: var(--accent-dim); color: var(--accent); }
.alert-state-resolved { opacity: .7; }
@media (max-width: 560px) {
  .alert-when { margin-left: 0; flex-basis: 100%; }
  .alert-actions .btn { flex: 1 1 auto; text-align: center; }
}

/* ----------------------------------------------------------------------
   Wave 7 - related activity.
   Appended last on purpose: at equal specificity the later rule wins, and
   an earlier wave's narrow-width block sitting after this one is exactly
   the cascade-order defect that produced 128px panels before.
   ---------------------------------------------------------------------- */
.ip-related .rel-lede,
.ip-related .rel-trunc,
.ip-related .rel-limit { color: var(--muted); font-size: .84rem; line-height: 1.5; margin: 0 0 12px; }
.ip-related .rel-limit { margin: 14px 0 0; padding-top: 12px; border-top: 1px solid var(--line); }
.ip-related .rel-empty { color: var(--muted); margin: 0; line-height: 1.55; }

.rel-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }

.rel-card {
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 12px 14px;
  background: color-mix(in srgb, var(--panel) 60%, transparent);
}
.rel-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.rel-ip {
  font-family: var(--mono, ui-monospace, monospace);
  font-size: .95rem; font-weight: 600; letter-spacing: .01em;
  /* Long IPv6 must wrap rather than push the page sideways. */
  overflow-wrap: anywhere; word-break: break-word; min-width: 0;
}
.rel-class {
  font-size: .68rem; text-transform: uppercase; letter-spacing: .08em;
  padding: 3px 8px; border-radius: 999px; border: 1px solid var(--line);
  white-space: nowrap; font-weight: 600;
}
/* The class is carried by the text itself, never by colour alone. */
.rel-class-multi-signal { border-color: var(--accent); color: var(--accent); }
.rel-class-single-signal { color: var(--muted); }

.rel-meta { color: var(--muted); font-size: .8rem; margin: 6px 0 10px; line-height: 1.5; }

.rel-ev-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.rel-ev {
  display: grid; gap: 2px 8px; font-size: .82rem; line-height: 1.45;
  padding-left: 11px; border-left: 2px solid var(--line);
}
.rel-ev-fact { font-weight: 600; }
.rel-ev-val { font-family: var(--mono, ui-monospace, monospace); overflow-wrap: anywhere; }
.rel-ev-detail,
.rel-ev-why { color: var(--muted); font-size: .78rem; overflow-wrap: anywhere; }
.rel-weight {
  justify-self: start; font-size: .64rem; text-transform: uppercase;
  letter-spacing: .07em; color: var(--muted);
}
.rel-weight-distinguishing { color: var(--accent); }

.rel-actions { margin-top: 10px; display: flex; gap: 8px; flex-wrap: wrap; }
.rel-skip { margin-top: 10px; font-size: .8rem; color: var(--muted); }
.rel-skip summary { cursor: pointer; }
.rel-skip-list { margin: 8px 0 0; padding-left: 18px; line-height: 1.5; }

/* Wide viewports: two evidence columns read faster without shrinking text. */
@media (min-width: 900px) {
  .rel-ev { grid-template-columns: minmax(0, auto) minmax(0, 1fr); align-items: baseline; }
  .rel-ev-why { grid-column: 1 / -1; }
}
/* Narrow viewports: single column, full-width actions, nothing clipped. */
@media (max-width: 560px) {
  .rel-card { padding: 11px 12px; }
  .rel-head { gap: 8px; }
  .rel-actions .btn { flex: 1 1 auto; text-align: center; }
}
@media (prefers-reduced-motion: reduce) {
  .rel-card { transition: none; }
}

/* ----------------------------------------------------------------------
   Wave 8 - analyst panel. Appended last: at equal specificity the later
   rule wins, and an earlier wave's narrow-width block sitting after this
   one is the cascade-order defect that produced 128px panels before.
   ---------------------------------------------------------------------- */
.ai-panel .ai-ask { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap;
  margin: 0 0 10px; }
.ai-ask-label { font-size: .64rem; text-transform: uppercase; letter-spacing: .09em;
  color: var(--muted); }
.ai-ask-text { font-size: .95rem; font-weight: 600; }
.ai-mode-note, .ai-disclaimer { color: var(--muted); font-size: .78rem; line-height: 1.5;
  margin: 0 0 12px; }
.ai-disclaimer { margin: 14px 0 0; padding-top: 12px; border-top: 1px solid var(--line); }
.ai-answer { line-height: 1.6; margin: 0 0 12px; }
.ai-empty { color: var(--muted); line-height: 1.55; margin: 0; }

.ai-claims { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; }
.ai-claim { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap;
  padding-left: 11px; border-left: 2px solid var(--line); line-height: 1.5;
  font-size: .86rem; }
.ai-claim-text { flex: 1 1 240px; min-width: 0; overflow-wrap: anywhere; }
.ai-refs { display: flex; gap: 5px; flex-wrap: wrap; }
/* A citation is a real control, not decoration: it is a button so it is
   keyboard reachable and announced. */
.ai-ref { font-family: var(--mono, ui-monospace, monospace); font-size: .68rem;
  padding: 2px 7px; border-radius: 999px; border: 1px solid var(--line);
  background: transparent; color: var(--accent); cursor: pointer; }
.ai-ref:hover, .ai-ref:focus-visible { border-color: var(--accent); }
.ai-issues { margin: 4px 0 0; padding-left: 16px; flex-basis: 100%; }
.ai-issue { color: var(--warn, #d7a64a); font-size: .76rem; line-height: 1.45; }

.ai-limits, .ai-evidence, .ai-notes { margin-top: 12px; font-size: .8rem;
  color: var(--muted); }
.ai-limits summary, .ai-evidence summary, .ai-notes summary { cursor: pointer; }
.ai-limits ul, .ai-notes ul { margin: 8px 0 0; padding-left: 18px; line-height: 1.5; }

.ai-atoms { list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 8px; }
.ai-atom { border: 1px solid var(--line); border-radius: 8px; padding: 9px 11px; }
.ai-atom:target { border-color: var(--accent); }
.ai-atom-head { display: flex; gap: 9px; align-items: baseline; flex-wrap: wrap; }
.ai-atom-id { color: var(--accent); font-size: .72rem; font-weight: 600; }
.ai-atom-type { font-size: .76rem; font-weight: 600; overflow-wrap: anywhere; }
.ai-atom-src { font-size: .7rem; color: var(--muted); overflow-wrap: anywhere; }
/* Evidence values are JSON and can be wide: they scroll inside their own
   box so the page body never scrolls sideways. */
.ai-atom-value { margin: 7px 0 0; font-size: .72rem; line-height: 1.45;
  white-space: pre-wrap; overflow-wrap: anywhere; overflow-x: auto; max-height: 220px;
  overflow-y: auto; }
.ai-atom-limit { margin: 6px 0 0; font-size: .72rem; color: var(--muted); line-height: 1.45; }
.ai-trunc { margin: 9px 0 0; padding-left: 18px; font-size: .76rem; line-height: 1.45; }

@media (max-width: 560px) {
  .ai-claim { font-size: .84rem; }
  .ai-atom-value { max-height: 170px; }
}
@media (prefers-reduced-motion: reduce) {
  .ai-ref { transition: none; }
}

/* ======================================================================
   v1.1 GLOBAL OPERATIONS
   Appended last on purpose: at equal specificity the later rule wins, and an
   earlier wave's narrow-width block sitting after this one is the exact
   cascade-order defect that once produced 128px panels.

   The composition rule that separates this from the dashboard: the map is the
   BACKGROUND LAYER of the stage and every panel is an overlay on top of it.
   The dashboard puts a map inside a card inside a grid; here the map is the
   canvas and the interface floats over it. That single structural decision is
   what makes the two surfaces unmistakable at a glance.
   ====================================================================== */
.gops-body { overflow: hidden; }
.gops-main { padding: 0; margin: 0; max-width: none; width: 100%; }

.gops { max-width: 100%; }
.gops-stage, .gops-bar, .gops-sheet-body { min-width: 0; }
.gops-bar-left, .gops-bar-mid, .gops-bar-right { min-width: 0; }
.gops-title { overflow: hidden; }
.gops-title strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gops {
  /* svh, not vh: on iOS vh includes browser chrome that is not actually
     there, which pushes the activity strip off-screen. */
  height: 100svh;
  min-height: 100svh;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  background:
    radial-gradient(120% 80% at 50% -10%, #0d1a26 0%, transparent 60%),
    radial-gradient(100% 70% at 50% 110%, #0a1420 0%, transparent 55%),
    #060b11;
  color: var(--text);
  overflow: hidden;
}

/* ---- command bar ---------------------------------------------------- */
.gops-bar {
  display: flex; align-items: center; gap: var(--s4);
  padding: 0 16px; height: 52px; min-height: 52px;
  border-bottom: 1px solid rgba(120, 160, 190, .16);
  background: linear-gradient(180deg, rgba(10,20,30,.92), rgba(10,20,30,.72));
  backdrop-filter: blur(8px);
  position: relative; z-index: 30;
}
.gops-bar-left { display: flex; align-items: center; gap: 10px; min-width: 0; }
.gops-mark {
  width: 26px; height: 26px; display: grid; place-items: center; flex: none;
  border-radius: 7px; background: var(--accent); color: #04121a;
  font-weight: 800; font-size: .7rem; letter-spacing: .04em;
}
.gops-title { display: flex; flex-direction: column; line-height: 1.15; min-width: 0; }
.gops-title strong { font-size: .84rem; letter-spacing: .01em; }
.gops-title span {
  font-size: .6rem; text-transform: uppercase; letter-spacing: .16em;
  color: var(--accent);
}
.gops-bar-mid { display: flex; align-items: center; gap: 12px; margin-left: auto; min-width: 0; }
.gops-health { display: inline-flex; align-items: center; gap: 7px; font-size: .74rem; }
.gops-health-dot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--muted); flex: none;
}
/* State is carried by the TEXT as well as the colour - never colour alone. */
.gops-health[data-health-state="live"]     .gops-health-dot { background: #45d6a0; box-shadow: 0 0 0 3px rgba(69,214,160,.16); }
.gops-health[data-health-state="degraded"] .gops-health-dot { background: #d7a64a; box-shadow: 0 0 0 3px rgba(215,166,74,.16); }
.gops-health[data-health-state="stale"]    .gops-health-dot { background: #d76a5a; box-shadow: 0 0 0 3px rgba(215,106,90,.16); }
.gops-age { font-size: .7rem; color: var(--muted); white-space: nowrap; }
.gops-bar-right { display: flex; align-items: center; gap: 7px; }
.gops-btn {
  display: inline-flex; align-items: center; gap: 6px;
  font: inherit; font-size: .74rem; padding: 6px 11px; border-radius: 8px;
  border: 1px solid rgba(120,160,190,.22); background: rgba(255,255,255,.03);
  color: var(--text); cursor: pointer; text-decoration: none; white-space: nowrap;
}
.gops-btn:hover, .gops-btn:focus-visible { border-color: var(--accent); color: var(--accent); }
.gops-btn[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); background: rgba(79,209,197,.09); }

/* ---- stage: map is the canvas, panels overlay it --------------------- */
.gops-stage { position: relative; min-height: 0; overflow: hidden; }
/* One DOM, two behaviours. On desktop the sheet is a pass-through so the
   rails keep their absolute overlay positions over the map canvas; on mobile
   it becomes a real tabbed bottom sheet. Duplicating the markup per breakpoint
   would guarantee the two copies drift apart. */
.gops-sheet { display: contents; }
.gops-tabs { display: none; }
.gops-sheet-body { display: contents; }
.gops-sheet-body > [data-gops-sec] { display: contents; }
.gops-canvas {
  position: absolute; inset: 0 0 92px 0;
  display: grid; place-items: center;
  padding: 8px;
}
/* Neutralise the dashboard CARD chrome so the map reads as the canvas.
   Critically NOT `background: none`: the land silhouette is a CSS
   background-image (url(/world-map.svg)) on .world-map, so resetting the
   background erases the entire map and leaves only the flow/dust overlay
   floating in the dark. Reset the card's box, never its paint. */
.gops-canvas .geo-map-cell {
  width: 100%; height: 100%; margin: 0; border: 0; padding: 0; background: none;
}
.gops-canvas .world-map {
  /* NEVER override the height or the aspect-ratio here. Markers are placed
     with percentage left/top against this box while the land is painted as a
     `contain` background image. The two only agree while the box keeps the
     map's own 1000:426.12 ratio - force height:100% and the land letterboxes
     inside a taller box while the markers keep using the full height, so
     every marker silently drifts off its country. That is a data-honesty
     failure wearing a layout bug's clothes. Set the width and let
     aspect-ratio derive the height.

     max-height did the same damage by the back door. On a short, wide window -
     an ultrawide, or any laptop with devtools open - it clamped the box below
     the map's ratio, and because world-map.svg carries no preserveAspectRatio
     the SVG letterboxes its own content inside whatever box it is given,
     background-size:100% 100% or not. Measured at 2000x690: the box was
     1972x532 while the land was painted 1248 wide and centred, and 7 of 12
     markers - the Malaysia hub among them - stood in open water, Korea 16
     points of map width from where it belonged.

     So the width is derived from the height that is actually available, and
     the box can no longer be a different shape from the map inside it. The
     old declarations stay as the fallback for anything without container
     query units. */
  width: 100%; height: auto; max-height: 100%;
  margin: 0; border: 0; padding: 0; border-radius: 0; box-shadow: none;
}
@supports (height: 1cqh) {
  .gops-canvas { container-type: size; }
  .gops-canvas .world-map {
    width: min(100%, calc(100cqh * 1000 / 426.12));
    max-height: none;
  }
}
.gops-canvas .world-map > svg,
.gops-canvas .world-map > canvas { width: 100%; height: 100%; }
/* The map carries its own disclosures - the hub/flow legend and the
   "placed at reported coordinates, never snapped onto land" note. They are
   load-bearing honesty text, so they are inset clear of the floating rails
   rather than hidden or truncated. */
.gops-canvas .map-legend,
.gops-canvas .map-caption,
.gops-canvas .map-field-note { font-size: .66rem; }
/* Global Ops is the one surface that also floats a control dock over the
   field, and the foot and the dock were fighting for the same strip. They get
   separate bands instead of a shared one with a reserved gap: the disclosures
   take the head of the field, the dock keeps the foot. A reserved gap would
   have to be a number, and that number would be wrong the moment the dock
   wrapped to another row - which is precisely how the collision arrived. The
   phone layout already put the legend at the top for its own reasons, so this
   makes one rule out of two.

   The insets clear the two floating rails; nothing is hidden or truncated. */
/* A compact legend dock at the head of the field, inset clear of both rails.
   It used to be a stack of full-width paragraphs here, which is how three
   sentences ended up printed across Russia and Europe. One slim row now:
   what the marks mean, the live/replay state, and a control for the rest.
   The field is left to be a field. */
/* The inset reads the same variable the rail is sized from. They were two
   hardcoded numbers before: the rail grows to 330px at 1600px and 380px at
   2200px while the dock stayed inset at 320px, so on every large monitor the
   legend was printed underneath the situation panel. */
.gops-canvas .map-foot {
  top: 10px; bottom: auto;
  left: calc(var(--rail-w, 292px) + 28px); right: calc(var(--rail-w, 292px) + 28px);
  justify-content: flex-start; padding: 0;
}
/* At the head of the field the panel has to open downward - there is no room
   above it. */
.gops-canvas .map-details[open] > .map-details-body { bottom: auto; top: calc(100% + 6px); }

.gops-rail {
  /* Anchored to the top and sized by content, with a ceiling. Stretching to
     the floor left a tall empty box under the brief, which reads as an
     unfinished panel rather than a deliberate one. */
  position: absolute; top: 12px; width: var(--rail-w, 292px);
  max-height: calc(100% - 116px);
  display: flex; flex-direction: column; gap: 10px;
  z-index: 20; pointer-events: none;
}
.gops-rail-left  { left: 12px; }
.gops-rail-right { right: 12px; }
.gops-panel {
  pointer-events: auto;
  border: 1px solid rgba(120,160,190,.18);
  border-radius: 12px;
  background: linear-gradient(180deg, rgba(9,17,25,.90), rgba(9,17,25,.78));
  backdrop-filter: blur(10px);
  padding: 13px 14px;
  min-height: 0;
}
.gops-panel-grow { flex: 0 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.gops-h {
  margin: 0 0 10px; font-size: .62rem; text-transform: uppercase;
  letter-spacing: .17em; color: var(--muted); font-weight: 700;
}
.gops-muted { color: var(--muted); font-size: .78rem; line-height: 1.55; margin: 0; }
.gops-note { margin: 10px 0 0; font-size: .66rem; color: var(--muted); line-height: 1.45; }

.gops-metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; }
.gops-metric {
  border: 1px solid rgba(120,160,190,.13); border-radius: 9px;
  padding: 8px 9px; background: rgba(255,255,255,.015); min-width: 0;
}
.gops-metric-label {
  display: block; font-size: .58rem; text-transform: uppercase;
  letter-spacing: .12em; color: var(--muted);
}
.gops-metric-value {
  display: block; font-size: 1.32rem; font-weight: 700; line-height: 1.15;
  margin-top: 3px; font-variant-numeric: tabular-nums;
}
.gops-metric-sub {
  display: block; font-size: .63rem; color: var(--muted); margin-top: 2px;
  overflow-wrap: anywhere;
}

.gops-brief p { margin: 0 0 9px; font-size: .8rem; line-height: 1.55; }
.gops-brief p:last-child { margin-bottom: 0; }
.gops-brief .gops-brief-label {
  font-size: .58rem; text-transform: uppercase; letter-spacing: .13em;
  color: var(--accent); display: block; margin-bottom: 3px;
}

.gops-dock-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.gops-x {
  background: none; border: 0; color: var(--muted); font-size: 1.1rem;
  cursor: pointer; line-height: 1; padding: 2px 4px;
}
.gops-x:hover, .gops-x:focus-visible { color: var(--accent); }
.gops-dock-body { font-size: .8rem; line-height: 1.55; }
.gops-dock-body dl { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 5px 10px; }
.gops-dock-body dt { color: var(--muted); font-size: .68rem; text-transform: uppercase; letter-spacing: .08em; }
.gops-dock-body dd { margin: 0; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
.gops-rel { list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 7px; }
.gops-rel li {
  border-left: 2px solid rgba(120,160,190,.25); padding-left: 9px; font-size: .74rem;
  line-height: 1.45;
}
.gops-rel-ip { font-family: var(--mono, ui-monospace, monospace); overflow-wrap: anywhere; }
.gops-rel-why { color: var(--muted); font-size: .68rem; display: block; }

/* ---- floating controls ---------------------------------------------- */
/* Two control families - scene and layers - so two rows, always, rather than
   one row that silently becomes two when it runs out of width. A dock whose
   height depends on the viewport is a dock whose clearance has to be guessed;
   this one has the same shape at every desktop width. */
.gops-controls {
  position: absolute; left: 50%; transform: translateX(-50%); bottom: 104px;
  z-index: 22; display: flex; flex-direction: column; align-items: center;
  gap: 8px; pointer-events: none; max-width: calc(100% - 640px);
}
.gops-controls > * { pointer-events: auto; }
.gops-controls .scene-bar, .gops-controls .layer-bar {
  border: 1px solid rgba(120,160,190,.18); border-radius: 999px;
  background: rgba(9,17,25,.88); backdrop-filter: blur(10px);
  padding: 5px 7px; margin: 0;
}

/* ---- activity strip -------------------------------------------------- */
.gops-strip {
  /* Pinned, not flowed. The strip is a child of the sheet (so the mobile tab
     structure can own it), and the sheet is display:contents on desktop -
     which made the strip flow to the TOP of the stage, straight over the
     situation rail. Position it explicitly instead of relying on source
     order. */
  position: absolute; left: 0; right: 0; bottom: 0;
  border-top: 1px solid rgba(120,160,190,.16);
  background: linear-gradient(180deg, rgba(10,20,30,.72), rgba(10,20,30,.94));
  backdrop-filter: blur(8px);
  padding: 8px 16px 10px; z-index: 25;
}
.gops-strip-head { display: flex; align-items: center; gap: 12px; margin-bottom: 6px; }
.gops-strip-head .gops-h { margin: 0; }
.gops-strip-legend { display: flex; align-items: center; gap: 6px; font-size: .63rem; color: var(--muted); }
.gops-key { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
.gops-key-live { background: var(--accent); box-shadow: 0 0 0 3px rgba(79,209,197,.15); }
.gops-key-replay { background: rgba(120,160,190,.5); }
.gops-strip-more { margin-left: auto; font-size: .7rem; color: var(--accent); text-decoration: none; white-space: nowrap; }
/* Columns sized to hold an address, not to divide the strip evenly.
   grid-auto-flow:column with minmax(0, 1fr) gave every row the same slice of
   the strip regardless of how much text it had, so a full-length IPv6 got
   ~106px of a 317px need and rendered as "2606:4700:47…". The row is
   identified by its address; an ellipsis there removes the identity. */
.gops-strip-list {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-auto-flow: row;
  grid-template-columns: repeat(auto-fit, minmax(232px, 1fr));
  gap: 10px 14px; overscroll-behavior-x: contain;
}
.gops-strip-list > li {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; min-width: 0;
  font-size: .73rem; padding: 4px 0 4px 8px;
  border-left: 2px solid transparent;
}
.gops-strip-list > li.is-new { border-left-color: var(--accent); }
.gops-strip-list > li.is-replay { border-left-color: rgba(120,160,190,.4); }
/* The complete address, always. Wrapping a long IPv6 onto a second line is
   worse-looking than one line and far better than hiding half of it: the
   reader can still tell which address this is, which is the entire purpose of
   the row. IPv4 is short enough that it never wraps at these column widths. */
.gops-strip-ip {
  font-family: var(--mono, ui-monospace, monospace);
  white-space: normal; overflow-wrap: anywhere;
  flex: 0 1 auto; min-width: 0; max-width: 100%;
}
.gops-strip-meta { color: var(--muted); font-size: .66rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gops-strip-empty { color: var(--muted); font-size: .74rem; }

/* ---- focus / wallboard ----------------------------------------------- */
.gops.is-focus .gops-bar-right .gops-btn:not([data-gops-focus]),
.gops.is-focus .gops-controls { opacity: 0; pointer-events: none; }
.gops.is-focus .gops-rail-right { opacity: 0; pointer-events: none; }

/* ---- large monitors: become more spatial, not stretched --------------- */
@media (min-width: 1600px) {
  .gops { --rail-w: 330px; }
  .gops-canvas { padding: 14px 14px 0; }
  .gops-metric-value { font-size: 1.5rem; }
}
@media (min-width: 2200px) {
  .gops { --rail-w: 380px; }
  .gops-bar { height: 60px; min-height: 60px; }
  .gops-metric-value { font-size: 1.7rem; }
  .gops-strip-list > li { font-size: .8rem; }
}

/* ---- tablet: dock collapses first, map keeps the space --------------- */
@media (max-width: 1180px) {
  .gops { --rail-w: 250px; }
  .gops-controls { max-width: calc(100% - 540px); }
}

/* ======================================================================
   MOBILE - a dedicated spatial application, not a stacked desktop page.
   The map stays a persistent canvas in the upper half; everything else
   becomes a bottom sheet with segmented sections. The page never scrolls:
   the SHEET scrolls.
   ====================================================================== */
@media (max-width: 900px) {
  .gops { grid-template-rows: auto minmax(0, 1fr); }
  /* The canvas is sized by the map itself (auto), not by a 1fr share: the
     map's 2.35:1 ratio makes it ~164px tall at 390px wide, so giving it half
     the viewport just produced a large empty box above and below it. The
     sheet takes everything the map does not need. */
  .gops-stage { display: grid; grid-template-rows: auto minmax(0, 1fr); }
  /* Global Ops is explicitly map-centric, so the canvas takes the space the
     sheet does not - never a thin strip above a wall of panels. */
  .gops-canvas {
    position: relative; inset: auto; padding: 6px; min-height: 0;
    align-content: center;
    /* container-type: size is set for the DESKTOP layout, where this element
       is absolutely positioned and therefore has a definite height. Here the
       grid row is `auto`, so size containment means the contents contribute
       NOTHING to the height: the canvas collapsed to its 12px of padding,
       100cqh resolved to ~0, and the map's width - min(100%, 100cqh * ratio) -
       computed to 0px. The map was not hidden, off-screen or covered. It was
       zero by zero. Measured: canvas 412x12, map 0x0; with containment off,
       canvas 412x182 and map 400x170.

       Turning containment off restores the aspect-ratio fallback the rule
       above already documents as the path for browsers without cq units. */
    container-type: normal;
  }
  /* At phone width the in-map legend overlaps the map itself. Move it below
     the map rather than hiding it: it names what the marks and the hub MEAN,
     which is not decoration. */
  .gops-canvas { position: relative; }
  /* Flowed under the header rather than floating over the map, with its own
     backdrop so it stays legible against whatever land sits behind it. It
     names what the marks and the hub MEAN, so it is not decoration that can
     simply be dropped at phone width. */
  /* The legend names what the marks and the hub MEAN, so it stays - but at
     390px it was covering roughly a quarter of an already small map. Compact
     it to a single scrollable line pinned to the top edge. */
  /* One scrolling row on a phone. The map is only about 160px tall here, so a
     second row of chrome costs a sixth of the field. */
  .gops-canvas .map-foot {
    left: 4px; right: 4px; bottom: 4px; top: auto; padding: 0;
    justify-content: flex-start; gap: 4px;
    flex-wrap: nowrap; overflow-x: auto; overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .gops-canvas .map-foot::-webkit-scrollbar { display: none; }
  .gops-canvas .map-foot > * { flex: none; }
  /* Same anchor problem as the console map, measured opening at x=681 in a
     412px viewport. Bounded to the map for the same reason. */
  .gops-canvas .map-details[open] > .map-details-body {
    position: fixed; top: auto; left: 8px; right: 8px; bottom: 40px;
    width: auto; max-width: none;
  }
  .gops-canvas .map-legend {
    display: flex; flex-wrap: nowrap; gap: 10px; justify-content: flex-start;
    overflow-x: auto; overscroll-behavior-x: contain;
    background: rgba(6,11,17,.86); border-radius: 6px; padding: 3px 6px;
    font-size: .58rem; letter-spacing: .06em; white-space: nowrap;
    backdrop-filter: blur(4px);
    scrollbar-width: none;
  }
  .gops-canvas .map-legend::-webkit-scrollbar { display: none; }
  .gops-canvas .map-legend > * { flex: none; }
  .gops-canvas .map-field-note { display: none; }

  .gops-rail, .gops-strip { position: static; width: auto; inset: auto; }
  .gops-canvas { inset: auto; }
  .gops-strip { backdrop-filter: none; background: none; border-top: 0; padding: 0; }
  .gops-sheet { display: flex; }
  .gops-tabs { display: flex; }
  .gops-sheet-body { display: block; }
  .gops-sheet-body > [data-gops-sec] { display: block; }
  .gops-rail { display: flex; }

  /* Sheet: rails and strip collapse into one scrolling surface with tabs. */
  .gops-sheet {
    grid-row: 2; z-index: 26;
    border-top: 1px solid rgba(120,160,190,.2);
    background: linear-gradient(180deg, rgba(9,17,25,.94), rgba(6,11,17,.98));
    backdrop-filter: blur(10px);
    display: flex; flex-direction: column; min-height: 0;
    max-height: none;
  }
  .gops-tabs {
    display: flex; gap: 4px; padding: 7px 10px 0; overflow-x: auto;
    border-bottom: 1px solid rgba(120,160,190,.14);
  }
  .gops-tab {
    font: inherit; font-size: .68rem; text-transform: uppercase; letter-spacing: .1em;
    padding: 7px 11px; border: 0; border-bottom: 2px solid transparent;
    background: none; color: var(--muted); cursor: pointer; white-space: nowrap;
    min-height: 38px;
  }
  .gops-tab[aria-selected="true"] { color: var(--accent); border-bottom-color: var(--accent); }
  .gops-sheet-body { overflow-y: auto; overscroll-behavior: contain; padding: 10px; min-height: 0; }
  .gops-sheet-body > [hidden] { display: none; }

  .gops-panel { border: 0; background: none; backdrop-filter: none; padding: 0 0 12px; }
  .gops-panel + .gops-panel { border-top: 1px solid rgba(120,160,190,.12); padding-top: 12px; }
  .gops-metrics { grid-template-columns: 1fr 1fr; }
  .gops-controls {
    position: static; transform: none; max-width: none; margin: 0;
    justify-content: flex-start;
  }
  .gops-strip-list { grid-auto-flow: row; grid-auto-columns: auto; }
  .gops-bar { padding: 0 10px; gap: 8px; }
  /* Icon-only controls at phone width. The accessible name lives on
     aria-label, so hiding the visible label costs nothing to a screen
     reader. */
  .gops-btn-label { display: none; }
  .gops-bar-right { flex: none; gap: 5px; }
  .gops-btn { padding: 7px 9px; min-height: 38px; }
  .gops-title span { display: none; }
  .gops-title strong { white-space: nowrap; }
  .gops-bar-left { flex: none; }
}
@media (max-width: 400px) {
  .gops-bar-mid .gops-age { display: none; }
  /* .gops-metric-value deliberately NOT set here. The cinematic layer is
     authored later in this sheet and carries a max-width:900px block that
     also sets it, so a rule here would match on a 390px screen, lose to the
     wider block at identical specificity, and be dead. The narrow size lives
     with that layer instead - see the a8eee8d note there. */
}

/* ---- motion ---------------------------------------------------------- */
/* Grammar: selection focus is smooth and short; nothing loops forever. */
.gops-canvas .map-stack { transition: transform 620ms cubic-bezier(.22,.61,.36,1); }
.gops-panel, .gops-btn { transition: border-color 160ms ease, color 160ms ease, opacity 220ms ease; }

@media (prefers-reduced-motion: reduce) {
  .gops-canvas .map-stack { transition: none; }
  .gops-panel, .gops-btn { transition: none; }
  .gops.is-focus .gops-controls,
  .gops.is-focus .gops-rail-right { transition: none; }
}

/* ======================================================================
   GLOBAL OPS - CINEMATIC PASS
   ======================================================================
   The first cut was functionally right and visually ordinary, and the audit
   said why in three numbers: twelve type sizes with ten of them between
   0.58 and 0.84rem (so nothing dominates), zero vignette or atmosphere
   layers (so no depth), and every surface inside a ~10% luminance band (so
   background, midground and foreground share one plane). That is the
   definition of "a fullscreen map with some panels".

   This layer fixes those three things and nothing else. It adds no data, no
   marks, and no motion that could be mistaken for activity.

   HONESTY RULE FOR EVERY EFFECT BELOW: atmosphere is painted on the STAGE,
   never on a data mark, is always `pointer-events: none`, and is always a
   neutral blue-grey - never the accent, which belongs exclusively to real
   activity, selection and alerts. A decorative layer must never be
   mistakable for an observation.
   ====================================================================== */

/* ---- 1. THE STAGE IS LIT ---------------------------------------------
   A key light above and behind the map, and a falloff at the edges of the
   frame. This is what turns a flat rectangle into a surface with a
   viewpoint. Both layers are behind everything and inert. */
.gops-stage::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background:
    radial-gradient(120% 90% at 50% -20%, rgba(64, 132, 178, .20) 0%,
                    rgba(30, 70, 100, .07) 38%, transparent 66%),
    radial-gradient(80% 60% at 50% 118%, rgba(24, 60, 92, .16) 0%, transparent 60%);
}
/* Edge falloff, in FRONT of the map but behind every panel. The map fades
   into the dark instead of ending at a hard rectangle. */
.gops-stage::after {
  content: ""; position: absolute; inset: 0; z-index: 6; pointer-events: none;
  background:
    radial-gradient(115% 105% at 50% 46%, transparent 52%,
                    rgba(3, 7, 12, .34) 78%, rgba(2, 5, 9, .72) 100%);
}
.gops-canvas { z-index: 1; }

/* ---- 2. THE MAP READS AS A SURFACE, NOT AN IMAGE ----------------------
   An inset falloff at its own edges plus a faint cool rim. The background
   image and the aspect-ratio are deliberately untouched - only paint is
   added around them. */
.gops-canvas .world-map {
  box-shadow:
    inset 0 0 170px 46px rgba(3, 8, 14, .60),
    inset 0 1px 0 rgba(150, 195, 225, .07),
    0 0 90px rgba(38, 96, 140, .16);
}

/* ---- 3. FOCAL HIERARCHY ----------------------------------------------
   The single biggest cause of "ordinary": ten sizes crammed between .58 and
   .84rem. The primary figure now leads by a wide margin, and the labels get
   quieter and wider-tracked so the number carries the panel. */
.gops-metric-value {
  font-size: 1.82rem; font-weight: 650; letter-spacing: -.022em;
  line-height: 1.05; margin-top: 5px;
  color: #eaf3fa;
  text-shadow: 0 1px 14px rgba(90, 160, 205, .18);
}
.gops-metric-label {
  font-size: .53rem; letter-spacing: .15em; color: #6f8ba3;
  /* A wrapped label pushes the value down and makes the rail top-heavy at
     1280, where the rail is only 292px wide. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.gops-metric-sub { font-size: .62rem; color: #7e97ad; margin-top: 4px; }
.gops-metric {
  padding: 11px 12px 12px;
  background:
    linear-gradient(180deg, rgba(120,170,210,.045), transparent 62%),
    rgba(255,255,255,.012);
  border-color: rgba(120,160,190,.16);
}
.gops-h {
  font-size: .57rem; letter-spacing: .22em; color: #6c869d;
}

/* ---- 4. PANELS SIT ABOVE THE STAGE -----------------------------------
   Lifted with a real shadow and a lit top edge, so the foreground plane is
   unmistakably in front of the map rather than pasted onto it. */
.gops-panel {
  border-color: rgba(126,170,205,.20);
  background:
    linear-gradient(180deg, rgba(16,30,44,.90), rgba(8,15,23,.86));
  box-shadow:
    0 1px 0 rgba(160,205,235,.09) inset,
    0 18px 44px -20px rgba(0,0,0,.85),
    0 2px 10px rgba(0,0,0,.35);
  backdrop-filter: blur(14px) saturate(1.15);
}
.gops-bar {
  background: linear-gradient(180deg, rgba(11,21,32,.96), rgba(8,16,25,.80));
  border-bottom-color: rgba(126,170,205,.18);
  box-shadow: 0 1px 0 rgba(160,205,235,.07) inset, 0 10px 30px -18px rgba(0,0,0,.9);
  backdrop-filter: blur(14px) saturate(1.15);
}
.gops-strip {
  background: linear-gradient(180deg, rgba(8,16,25,.72), rgba(6,12,19,.97));
  border-top-color: rgba(126,170,205,.18);
  box-shadow: 0 -1px 0 rgba(160,205,235,.06) inset;
  backdrop-filter: blur(14px) saturate(1.1);
}

/* ---- 5. IDENTITY -----------------------------------------------------
   Instrumentation, not website navigation. */
.gops-title strong { font-size: .9rem; letter-spacing: .005em; color: #eaf3fa; }
.gops-title span { font-size: .55rem; letter-spacing: .26em; }
.gops-mark {
  box-shadow: 0 0 0 1px rgba(79,209,197,.30), 0 6px 18px -6px rgba(79,209,197,.55);
}
.gops-health { font-size: .7rem; letter-spacing: .04em; }
.gops-health-text { text-transform: uppercase; letter-spacing: .14em; font-size: .62rem; }
.gops-age { font-size: .64rem; letter-spacing: .04em; }
.gops-btn {
  border-color: rgba(126,170,205,.20);
  background: linear-gradient(180deg, rgba(255,255,255,.045), rgba(255,255,255,.012));
  letter-spacing: .05em; font-size: .7rem;
}
.gops-btn:hover, .gops-btn:focus-visible {
  background: linear-gradient(180deg, rgba(79,209,197,.14), rgba(79,209,197,.05));
  box-shadow: 0 0 0 1px rgba(79,209,197,.30);
}

/* ---- 6. THE DOCK IS A CONSOLE, NOT A CARD STACK -----------------------
   Hierarchy from spacing, rules and type - no nested boxes. */
.gops-dock-body dl { gap: 7px 14px; }
.gops-dock-body dt { font-size: .58rem; letter-spacing: .14em; color: #6c869d; }
.gops-dock-body dd { font-size: .86rem; color: #e2eef7; }
.gops-brief p { font-size: .84rem; line-height: 1.6; color: #dce8f2; }
.gops-brief-label {
  font-size: .55rem; letter-spacing: .2em;
}
.gops-rel li { border-left-color: rgba(126,170,205,.30); padding-left: 11px; }
.gops-rel-ip { font-size: .8rem; color: #e2eef7; }

/* ---- 7. SELECTION IS A MOMENT ----------------------------------------
   Applied to a REAL marker only, by the controller, when something is
   actually selected or tracked. Never ambient. */
.gops .map-node.is-sel {
  z-index: 9;
  filter: drop-shadow(0 0 10px rgba(79,209,197,.85))
          drop-shadow(0 0 26px rgba(79,209,197,.45));
}
.gops .map-node.is-sel::after {
  content: ""; position: absolute; inset: -13px; border-radius: 50%;
  border: 1px solid rgba(79,209,197,.65); pointer-events: none;
  animation: gops-lock 620ms cubic-bezier(.22,.61,.36,1) both;
}
@keyframes gops-lock {
  from { transform: scale(2.1); opacity: 0; }
  to   { transform: scale(1);   opacity: 1; }
}
/* While something is tracked the rest of the map recedes slightly, so the
   target dominates without anything being hidden. */
.gops.is-tracking .gops-canvas .world-map { filter: saturate(.72) brightness(.80); }
.gops.is-tracking .gops-canvas .map-node.is-sel { filter: none; }

/* ---- 8. NO AMBIENT ALERT STATE ---------------------------------------
   An alert perimeter treatment was written and then deliberately removed.
   Alerts are private, this surface calls only public endpoints, and
   production holds zero alerts - so no authoritative source could ever set
   it. A visual alert state nothing can legitimately trigger is precisely the
   alert theatre this product refuses, and dead styling that implies a
   capability is worse than no styling at all. */

/* ---- 9. ENTRY ---------------------------------------------------------
   ~700ms: the stage establishes, then the instrumentation settles. Nothing
   is delayed or blocked - this is opacity and a few pixels of travel on
   elements that are already interactive. */
@keyframes gops-rise { from { opacity: 0; transform: translateY(9px); } to { opacity: 1; transform: none; } }
@keyframes gops-fade { from { opacity: 0; } to { opacity: 1; } }
.gops-canvas { animation: gops-fade 700ms ease both; }
.gops-bar    { animation: gops-rise 420ms cubic-bezier(.22,.61,.36,1) 60ms both; }
.gops-rail-left  { animation: gops-rise 520ms cubic-bezier(.22,.61,.36,1) 190ms both; }
.gops-rail-right { animation: gops-rise 520ms cubic-bezier(.22,.61,.36,1) 260ms both; }
.gops-strip      { animation: gops-rise 520ms cubic-bezier(.22,.61,.36,1) 330ms both; }
.gops-controls   { animation: gops-fade 620ms ease 400ms both; }

/* ---- 10. AMBIENT ------------------------------------------------------
   A single, very slow luminance drift on the stage light. Decorative and
   non-semantic by construction: it is a full-frame gradient, it never moves
   position, it carries no accent colour, and it cannot be confused with a
   mark, a pulse or a route. */
@keyframes gops-breathe { 0%,100% { opacity: .85; } 50% { opacity: 1; } }
.gops-stage::before { animation: gops-breathe 14s ease-in-out infinite; }

/* ---- 11. LARGE DISPLAYS ----------------------------------------------
   More scale and air, not more panels. */
@media (min-width: 1600px) {
  .gops-metric-value { font-size: 2.35rem; }
  .gops-brief p { font-size: .9rem; }
  .gops-dock-body dd { font-size: .92rem; }
  .gops-title strong { font-size: .98rem; }
}
@media (min-width: 2200px) {
  .gops-metric-value { font-size: 2.9rem; }
  .gops-metrics { gap: 12px; }
  .gops-panel { padding: 17px 19px; }
  .gops-brief p { font-size: 1rem; line-height: 1.65; }
  .gops-h { font-size: .62rem; }
  .gops-strip-list > li { font-size: .84rem; }
}

/* ---- 12. PHONE -------------------------------------------------------
   Premium presence without becoming a miniature desktop HUD. */
@media (max-width: 900px) {
  .gops-metric-value { font-size: 1.72rem; }
  .gops-panel { box-shadow: none; backdrop-filter: none; background: none; }
  .gops-stage::after {
    background: radial-gradient(130% 60% at 50% 24%, transparent 46%,
                                rgba(2,5,9,.55) 88%);
  }
  .gops-tab { letter-spacing: .14em; }
  .gops-tab[aria-selected="true"] { text-shadow: 0 0 14px rgba(79,209,197,.35); }
}

/* The narrowest block must come LAST. This one exists because the guard
   caught the a8eee8d bug being reintroduced: the base sheet sets
   .gops-metric-value inside max-width:400px, and the cinematic
   max-width:900px block above is authored later in the file. On a 390px
   screen both queries match at identical specificity, so the WIDER rule won
   and the 400px size silently never applied. Restating it here, after the
   900px block, is what makes the narrow rule actually take effect. */
@media (max-width: 400px) {
  .gops-metric-value { font-size: 1.55rem; }
  .gops-metric-label { letter-spacing: .14em; }
  .gops-metric { padding: 9px 10px 10px; }
}


/* ---- 14. THE MAP SURFACE ITSELF --------------------------------------
   The remaining reason it read ordinary: the land is one flat fill sitting
   on empty black, so the map looked like a picture rather than an
   instrument. Two layers fix that, both BELOW every data mark (marks live
   at z-index 2-5) and both inert.

   ::before is a proportional instrument grid plus a limb shade. The grid is
   in PERCENTAGES so it scales with the map box instead of drifting against
   it the way the fixed 34/46px screen grid did. It is deliberately NOT
   labelled and NOT claimed to be a graticule: on a Robinson projection true
   meridians curve, so calling a straight grid a coordinate system would be
   a false geographic claim. It is texture, and it is described as texture.

   The limb shade darkens toward the edges so the surface reads as curved
   rather than as a flat rectangle - the single cheapest depth cue available
   without a 3D dependency. */
.gops-canvas .world-map::before {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    repeating-linear-gradient(0deg,  transparent 0 calc(100% / 12 - 1px),
                              rgba(140, 185, 220, .075) calc(100% / 12 - 1px) calc(100% / 12)),
    repeating-linear-gradient(90deg, transparent 0 calc(100% / 24 - 1px),
                              rgba(140, 185, 220, .065) calc(100% / 24 - 1px) calc(100% / 24)),
    radial-gradient(115% 128% at 50% 46%,
                    rgba(38, 96, 140, .16) 0%,
                    rgba(10, 26, 42, .10) 44%,
                    rgba(2, 6, 11, .50) 82%,
                    rgba(1, 4, 8, .78) 100%);
}
/* ::after is a faint horizon band across the equator region. It gives the
   eye a horizontal axis to read the sphere against. Neutral, never accent. */
.gops-canvas .world-map::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg,
    transparent 0%, rgba(120, 175, 215, .05) 47%,
    rgba(120, 175, 215, .05) 53%, transparent 100%);
}

/* ---- 15. ACTIVITY STRIP LEGIBILITY -----------------------------------
   The rows were the least readable thing on screen: dim, cramped, and
   truncating the address that identifies the row. */
.gops-strip-list > li {
  gap: 10px; padding: 5px 0 5px 10px;
  border-left-width: 2px;
}
.gops-strip-ip { font-size: .8rem; color: #dfeaf4; letter-spacing: .01em; }
.gops-strip-meta { font-size: .68rem; color: #7e97ad; }
.gops-strip-list > li.is-new { border-left-color: var(--accent); }
.gops-strip-list > li.is-new .gops-strip-ip { color: #f2f8fc; }
.gops-strip-head .gops-h { font-size: .57rem; }
.gops-strip-legend { font-size: .6rem; letter-spacing: .06em; }
.gops-strip-more { font-size: .68rem; letter-spacing: .05em; }

/* ---- 16. CONTROLS READ AS INSTRUMENTATION ---------------------------- */
.gops-controls .scene-bar, .gops-controls .layer-bar {
  border-color: rgba(126,170,205,.20);
  background: linear-gradient(180deg, rgba(16,30,44,.92), rgba(8,15,23,.88));
  box-shadow: 0 1px 0 rgba(160,205,235,.08) inset, 0 14px 34px -18px rgba(0,0,0,.9);
  backdrop-filter: blur(14px) saturate(1.15);
}

@media (prefers-reduced-motion: reduce) {
  .gops-canvas .world-map::before, .gops-canvas .world-map::after { animation: none; }
}

/* ---- 13. REDUCED MOTION ----------------------------------------------
   Every animation above is presentation. Under reduced motion the surface
   renders immediately and completely - no entry sequence, no drift, no
   selection sweep - and loses no information. */
@media (prefers-reduced-motion: reduce) {
  .gops-canvas, .gops-bar, .gops-rail-left, .gops-rail-right,
  .gops-strip, .gops-controls, .gops-stage::before { animation: none; }
  .gops .map-node.is-sel::after { animation: none; }
  .gops.is-tracking .gops-canvas .world-map { filter: none; }
}
