/* ---- Operator Mono (self-hosted, licensed — www/fonts/ is .gitignored) */
/* OperatorMonoSSmLig — workhorse for body, tables, search rows.
   All four weights (Light/Book/Medium/Bold) × (Regular/Italic). */
@font-face {
  font-family: "OperatorMonoSSmLig";
  font-weight: 300; font-style: normal; font-display: swap;
  src: url("fonts/OperatorMonoSSmLig-Light.otf") format("opentype");
}
@font-face {
  font-family: "OperatorMonoSSmLig";
  font-weight: 300; font-style: italic; font-display: swap;
  src: url("fonts/OperatorMonoSSmLig-LightItalic.otf") format("opentype");
}
@font-face {
  font-family: "OperatorMonoSSmLig";
  font-weight: 400; font-style: normal; font-display: swap;
  src: url("fonts/OperatorMonoSSmLig-Book.otf") format("opentype");
}
@font-face {
  font-family: "OperatorMonoSSmLig";
  font-weight: 400; font-style: italic; font-display: swap;
  src: url("fonts/OperatorMonoSSmLig-BookItalic.otf") format("opentype");
}
@font-face {
  font-family: "OperatorMonoSSmLig";
  font-weight: 500; font-style: normal; font-display: swap;
  src: url("fonts/OperatorMonoSSmLig-Medium.otf") format("opentype");
}
@font-face {
  font-family: "OperatorMonoSSmLig";
  font-weight: 500; font-style: italic; font-display: swap;
  src: url("fonts/OperatorMonoSSmLig-MediumItalic.otf") format("opentype");
}
@font-face {
  font-family: "OperatorMonoSSmLig";
  font-weight: 700; font-style: normal; font-display: swap;
  src: url("fonts/OperatorMonoSSmLig-Bold.otf") format("opentype");
}
@font-face {
  font-family: "OperatorMonoSSmLig";
  font-weight: 700; font-style: italic; font-display: swap;
  src: url("fonts/OperatorMonoSSmLig-BoldItalic.otf") format("opentype");
}
/* OperatorMonoLig — display variant (looser letterforms) for the titlebar
   / logo only. Light and Book × (Regular/Italic). */
@font-face {
  font-family: "OperatorMonoLig";
  font-weight: 300; font-style: normal; font-display: swap;
  src: url("fonts/OperatorMonoLig-Light.otf") format("opentype");
}
@font-face {
  font-family: "OperatorMonoLig";
  font-weight: 300; font-style: italic; font-display: swap;
  src: url("fonts/OperatorMonoLig-LightItalic.otf") format("opentype");
}
@font-face {
  font-family: "OperatorMonoLig";
  font-weight: 400; font-style: normal; font-display: swap;
  src: url("fonts/OperatorMonoLig-Book.otf") format("opentype");
}
@font-face {
  font-family: "OperatorMonoLig";
  font-weight: 400; font-style: italic; font-display: swap;
  src: url("fonts/OperatorMonoLig-BookItalic.otf") format("opentype");
}

/* ---- Nord palette ---------------------------------------------------- */
:root {
  --nord0:  #2E3440;  --nord1:  #3B4252;  --nord2:  #434C5E;  --nord3:  #4C566A;
  --nord4:  #D8DEE9;  --nord5:  #E5E9F0;  --nord6:  #ECEFF4;
  --nord7:  #8FBCBB;  --nord8:  #88C0D0;  --nord9:  #81A1C1;  --nord10: #5E81AC;
  --nord11: #BF616A;  --nord12: #D08770;  --nord13: #EBCB8B;  --nord14: #A3BE8C;
  --nord15: #B48EAD;
}

/* Text selection — classic terminal inversion: body fg becomes bg, bg becomes
   fg. Overrides the browser's default blue which reads off-palette against
   Nord. Both prefixes so Firefox matches Chromium/Safari. */
::selection     { background: var(--nord4); color: var(--nord0); }
::-moz-selection { background: var(--nord4); color: var(--nord0); }

/* Shiny injects bootstrap; override its base styles. */
* { box-sizing: border-box; }
html, body {
  margin: 0; padding: 0;
  background: var(--nord0);
  color: var(--nord4);
  /* Workhorse first (self-hosted via @font-face above). System fallbacks in
     case an ad-blocker / network issue blocks the .otf download or the file
     is missing on a dev environment without the licensed fonts. */
  font-family: "OperatorMonoSSmLig", "Operator Mono SSm", "Operator Mono",
               "JetBrains Mono", "Fira Code", "IBM Plex Mono",
               ui-monospace, monospace;
  /* Medium (500) default: Operator Mono Book (400) reads too thin at 13px
     against the nord0 background. Bold emphasis still jumps to 700 where
     declared on chips / headlines. */
  font-size: 13px; line-height: 1.55; font-weight: 500;
  font-feature-settings: "calt" on, "liga" on;
}
body { min-height: 100vh; }
.container-fluid { padding: 0 !important; }
a { color: var(--nord9); text-decoration: none; }
a:hover { color: var(--nord8); }
input, button, select, textarea {
  font-family: inherit; font-size: inherit; color: inherit;
  background: transparent; border: 0; outline: none;
}

/* Scrollbars — thin, Nord-coloured, only visible when actually scrolling.
   Firefox uses two properties; WebKit needs ::-webkit-scrollbar pseudos.
   Tracks are transparent so the scrollbar doesn't cut a visible gutter
   next to panes that aren't scrolling. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--nord2) transparent;
}
::-webkit-scrollbar         { width: 6px; height: 6px; }
::-webkit-scrollbar-track   { background: transparent; }
::-webkit-scrollbar-thumb   { background: var(--nord2); border-radius: 0; }
::-webkit-scrollbar-thumb:hover { background: var(--nord3); }
::-webkit-scrollbar-corner  { background: transparent; }

/* ---- TUI shell ------------------------------------------------------- */
.shell {
  max-width: none; margin: 12px 16px;
  border: 1px solid var(--nord2);
  display: grid;
  /* rows: titlebar · prompt · body · status. Fixed height so the body row
     gets a real 1fr allocation — mod_search's JS reads .pane.results'
     clientHeight to compute how many CVEs fit, so the pane must be bounded. */
  grid-template-rows: auto auto 1fr auto;
  height: calc(100vh - 80px);
}

.banner {
  padding: 5px 12px;
  border-bottom: 1px solid var(--nord2);
  background: var(--nord1);
  color: var(--nord13);
  text-align: center;
  font-size: 12px;
  letter-spacing: 0.05em;
}
.banner a { color: var(--nord13); text-decoration: underline; text-decoration-color: var(--nord3); }
.banner .sep { color: var(--nord3); padding: 0 6px; }

.titlebar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 4px 12px;
  border-bottom: 1px solid var(--nord2);
  color: var(--nord6);
  /* Display variant — looser letterforms than SSm, reserved for the header
     per CLAUDE.md's font usage guidance. */
  font-family: "OperatorMonoLig", "OperatorMonoSSmLig", "Operator Mono",
               ui-monospace, monospace;
}
.titlebar .dim { color: var(--nord3); }

.prompt {
  padding: 6px 12px;
  border-bottom: 1px solid var(--nord2);
  color: var(--nord5);
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  /* JS positions .cursor absolutely relative to this container. */
  position: relative;
}
.prompt .sigil { color: var(--nord8); }
.prompt .label { color: var(--nord3); }
.prompt input.search {
  flex: 1; min-width: 260px;
  color: var(--nord5);
  /* Hide the native thin caret — the decorative block cursor (www/app.js)
     tracks selectionStart and renders the visible caret instead. */
  caret-color: transparent;
}
.prompt input.search::placeholder { color: var(--nord3); }
/* 12px vs the 13px prompt input because the severity chips inside each
   label ride at font-weight 700; bold bumps the apparent cap-height up a
   notch, so a one-point smaller type lands the `[ ] critical` line at the
   same optical height as the `/ search >` prompt text. */
.prompt .filters { color: var(--nord3); display: inline-flex; gap: 10px; flex-wrap: wrap; font-size: 12px; }
.prompt .filters label { color: var(--nord3); cursor: pointer; user-select: none;
                         display: inline-flex; align-items: baseline; gap: 6px; }
.prompt .filters label.on { color: var(--nord5); }

/* TUI-style checkboxes: hide the native input and paint `[ ]` / `[x]` as
   a ::before pseudo on the label. Monospace means the glyph width is
   predictable and white-space: pre keeps the interior space from being
   collapsed — earlier attempts set a width on the input itself and
   the `[ ]` pseudo-content wrapped onto two lines. */
.prompt .filters input[type="checkbox"] {
  position: absolute;
  width: 1px; height: 1px;
  opacity: 0;
  pointer-events: none;
}
.prompt .filters label::before {
  content: "[ ]";
  color: var(--nord8);
  white-space: pre;
  font: inherit;
}
.prompt .filters label:has(input:checked) { color: var(--nord5); }
.prompt .filters label:has(input:checked)::before {
  content: "[x]";
}

.body {
  display: grid;
  grid-template-columns: 200px 1fr 344px;
  /* Row 1 = results + (groups/graphs tops), row 2 = detail-host under
     the results column only. Groups and graphs span both rows so they
     keep full height when a CVE is selected — otherwise the graphs pane
     got squeezed as the detail strip grew, hiding the stacked-area chart.
     1fr on the pane row lets the results pane fill whatever height remains
     after the detail strip — the dynamic page-size JS reads that height. */
  grid-template-rows: 1fr auto;
  min-height: 0;
}
/* Explicit placement on every .body grid child. Auto-placement with a mix
   of row-spanning siblings and a column-pinned .detail-host reordered the
   middle/right columns the first time around — pin each position and the
   auto-flow can't get creative. Groups (left) and graphs (right) span both
   rows so the stacked-area graphs pane keeps full height when the detail
   strip opens under the results column. */
.pane.groups  { grid-column: 1 / 2; grid-row: 1 / -1; }
.pane.results { grid-column: 2 / 3; grid-row: 1 / 2; }
.pane.graphs  { grid-column: 3 / 4; grid-row: 1 / -1; }
/* min-width: 0 is load-bearing — CSS grid items default to min-width: auto
   which refuses to shrink below intrinsic content width. Without this a wide
   table inside the pane expands the entire grid column and collapses siblings. */
.pane       { border-right: 1px solid var(--nord2); overflow: hidden; min-width: 0; }
.pane:last-child { border-right: 0; }
.pane h3    {
  margin: 0; padding: 6px 12px;
  font-size: 12px; font-weight: 500;
  color: var(--nord3); text-transform: lowercase;
  border-bottom: 1px solid var(--nord2);
  letter-spacing: 0.04em;
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 12px;
}
.pane h3 .count { color: var(--nord5); margin-left: 8px; font-variant-numeric: tabular-nums; }
.pane h3 .pager { display: inline-flex; align-items: baseline; gap: 10px; }
.pane h3 .pager .pg    { color: var(--nord9); cursor: pointer; user-select: none; }
.pane h3 .pager .pg:hover { color: var(--nord8); }
.pane h3 .pager .pgnum { color: var(--nord3); font-variant-numeric: tabular-nums; }

/* ---- groups pane ----------------------------------------------------- */
.groups ul { list-style: none; margin: 0; padding: 6px 0; }
.groups li {
  padding: 2px 12px; color: var(--nord4); cursor: pointer;
  display: flex; justify-content: space-between;
}
.groups li:hover { color: var(--nord6); }
.groups li.active { background: var(--nord2); color: var(--nord6); }
.groups li .count { color: var(--nord3); font-variant-numeric: tabular-nums; }
.groups li.active .count { color: var(--nord5); }
.groups .new {
  padding: 8px 12px; color: var(--nord3);
  border-top: 1px solid var(--nord2); margin-top: 6px;
}

/* ---- saved views (localStorage-backed, client-only) -----------------
   Rendered by www/app.js into <ul id="saved-views">. Visually the same
   grid as .groups li (label on left, controls on right) so canonical
   and personal shortcuts read as one list; only the subhead and the
   numeric index hint give them away. */
.groups .subhead {
  color: var(--nord3); font-weight: 700;
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
  padding: 10px 12px 4px; margin-top: 6px;
  border-top: 1px solid var(--nord2);
}
.saved-views { list-style: none; margin: 0; padding: 2px 0; }
.saved-views li {
  padding: 2px 12px; color: var(--nord4); cursor: pointer;
  display: grid; grid-template-columns: 20px 1fr auto;
  align-items: baseline; gap: 8px;
}
.saved-views li:hover { color: var(--nord6); }
.saved-views li.active { background: var(--nord2); color: var(--nord6); }
.saved-views li.empty {
  color: var(--nord3); cursor: default;
  grid-template-columns: 1fr; padding: 4px 12px;
}
.saved-views li.empty:hover { color: var(--nord3); }
.saved-views .idx { color: var(--nord3); font-variant-numeric: tabular-nums; }
.saved-views .vname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.saved-views .del {
  color: var(--nord3); padding: 0 4px;
  opacity: 0; transition: opacity 80ms;
}
.saved-views li:hover .del { opacity: 1; }
.saved-views .del:hover { color: var(--nord11); }

/* Transient toast for cap / no-view-active feedback. Lives inside
   .shell .status so it inherits the status-bar typography; absolutely
   positioned over the keys so it doesn't reflow the layout on each
   press. Fades in via opacity; www/app.js removes it after 2.5s. */
.shell .status .toast {
  margin-left: 16px; padding: 0 8px;
  color: var(--nord0); background: var(--nord13);
  border-radius: 2px;
  animation: toast-in 120ms ease-out;
}
@keyframes toast-in { from { opacity: 0; } to { opacity: 1; } }

/* ---- results pane (htop-style table) --------------------------------
   Each CVE is one <tbody class="cve"> containing two rows:
     tr.main — tight monospaced columns (id / cvss / sev / epss / exp / kev / adp / src)
     tr.desc — full-width description (colspan=8) on a second line, ellipsis-clipped
   Hover / selection highlight the whole tbody so the pair stays visually tied.  */
.results table { width: 100%; border-collapse: collapse; table-layout: fixed; }
.results th, .results td {
  text-align: left; padding: 0 12px;
  color: var(--nord4);
  line-height: 1.35;
}
.results thead th {
  color: var(--nord3); font-weight: 700;
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
  padding: 4px 12px 2px;
  border-bottom: 1px solid var(--nord2);
  white-space: nowrap;
}
.results td.num,
.results th.num { text-align: right; font-variant-numeric: tabular-nums; }
/* Match the header's font metrics to the value row (13px regular, no
   letter-spacing) so right-aligned "cvss"/"epss" end at the same x-pixel as
   "10.0"/"0.21". At 11px bold + 0.06em spacing the header chars rendered
   narrower than the tabular-num values below, leaving a ~2-char visual gap. */
.results th.num { font-size: 13px; font-weight: 500; letter-spacing: 0; }

.results tbody.cve { cursor: pointer; }
.results tbody.cve:hover { background: var(--nord1); }
.results tbody.cve.sel   { background: var(--nord2); color: var(--nord6); }

.results tr.main td   {
  padding-top: 2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.results tr.desc td   {
  padding: 0 12px 3px 12px;
  border-bottom: 1px solid var(--nord2);
  /* With table-layout:fixed the td width is bounded by the colgroup sum.
     Inner div clips the one-liner. */
}
.results tr.desc .sum {
  color: var(--nord3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 12px;
}
.results tbody.cve.sel .sum { color: var(--nord5); }

.results .empty {
  padding: 24px 12px; color: var(--nord3); text-align: center;
}

/* Keyword highlights — matched substrings from $q and $q_any. Subtle so the
   row is still readable at a glance; yellow-ish nord13 lifts out against
   nord3 description text. No background so selected/hover states stay clean. */
.results mark {
  color: var(--nord13);
  background: transparent;
  font-weight: 500;
  padding: 0;
}
.results tr.main mark { color: var(--nord13); }

/* Terms display after the hit range: "1–10 of 2,340 · kvm | qemu | vmware".
   Dim the separator and list to keep count the primary read. */
.pane h3 .terms { color: var(--nord3); margin-left: 8px; }
.pane h3 .terms b { color: var(--nord5); font-weight: 500; }

/* ---- detail strip ----------------------------------------------------
   The grid child of .body is .detail-host (see mod_detail.R). Shiny's
   uiOutput wraps its content in a <div class="shiny-html-output">, so
   putting the grid-column rule directly on .detail would miss — that div
   would occupy only the first grid column. .detail-host spans all three;
   .detail (inside it) fills naturally.                                    */
.detail-host {
  /* Column 2 only — below the results table. Groups and graphs span
     both rows (see .body), so the detail strip sits in the middle column
     without eating into the side panes. border-right continues the col2↔col3
     divider that .pane.results draws in row 1; without it, the vertical
     line between the detail strip and the graphs pane disappears. */
  grid-column: 2 / 3;
  grid-row: 2 / 3;
  border-top: 1px solid var(--nord2);
  border-right: 1px solid var(--nord2);
}
.detail {
  padding: 4px 12px 6px;
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: 18px; row-gap: 1px;
  color: var(--nord4);
}
.detail .k { color: var(--nord3); }
.detail .v { color: var(--nord5); word-break: break-word; }
.detail .title { grid-column: 1 / -1; color: var(--nord6); margin-bottom: 2px; }
.detail .title .id { color: var(--nord8); }
/* Clamp to 5 lines — short descriptions reserve the same vertical space,
   long ones truncate with ellipsis. Dominant variance in the detail strip
   is the description; everything above it is single-line and steady.
   Fixing this kills the j/k layout jump without a scrollbar. 5 lines fits
   most NVD descriptions without trailing "…" while still bounding very
   long ones so a single CVE can't push the results pane down to nothing. */
.detail .summary {
  grid-column: 1 / -1; color: var(--nord4); margin-top: 4px;
  display: -webkit-box;
  -webkit-line-clamp: 5;
  -webkit-box-orient: vertical;
  overflow: hidden;
  min-height: calc(5 * 1.35em);
  line-height: 1.35em;
}
.detail .empty {
  grid-column: 1 / -1; color: var(--nord3); text-align: center;
  padding: 12px 0;
}

/* Source selector — inline <select> in the detail title. Strip the browser's
   native chrome so it sits in the same visual register as the surrounding TUI:
   no rounded corners, no drop shadow, monospace font, nord palette. The
   hover/focus outline uses nord8 (primary accent) to match the cursor. */
.detail .src-select {
  background: var(--nord1); color: var(--nord5);
  border: 1px solid var(--nord2);
  font: inherit; font-size: 12px;
  padding: 1px 6px; margin-left: 4px;
  border-radius: 0;
  appearance: none; -webkit-appearance: none;
  cursor: pointer;
}
.detail .src-select:hover,
.detail .src-select:focus { border-color: var(--nord8); outline: none; }

/* Divergence strip — one chip per source showing its v3 score. Spans both
   grid columns so it sits as a full-width line between title and the
   key/value rows. Ruler children are positioned via left:%, so text and
   dots render at native pixel size and the 0–10 axis reflows with the
   pane — no SVG viewBox stretching that would squash text glyphs. */
.detail .cvss-map {
  grid-column: 1 / -1;
  display: flex; align-items: center;
  gap: 12px;
  margin: 2px 0 2px;
}
.detail .cvss-map .k { flex: 0 0 auto; }
.detail .cvss-map .cvss-ruler {
  /* Cap at 420px: wider than that just dilutes the signal. Shrinks down
     to 200px on narrow panes so the key+ruler+Δ line never wraps. */
  flex: 0 1 420px;
  position: relative;
  height: 44px;
  min-width: 200px;
}
.detail .cvss-map .delta-chip {
  flex: 0 0 auto;
  color: var(--nord13);
  font-weight: 700;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

/* Severity bands: thin horizontal stripe centred on the axis. Low alpha so
   the dot is the focal point; higher contrast would read as chartjunk. */
.cvss-ruler .band {
  position: absolute;
  top: 12px; height: 8px;
  opacity: 0.22;
  pointer-events: none;
}
.cvss-ruler .band.low  { background: var(--nord14); }
.cvss-ruler .band.med  { background: var(--nord13); }
.cvss-ruler .band.high { background: var(--nord12); }
.cvss-ruler .band.crit { background: var(--nord11); }
.cvss-ruler .axis-line {
  position: absolute;
  top: 16px; left: 0; right: 0;
  height: 1px; background: var(--nord2);
}
/* Dot: small disc marking the source's v3 score position. margin-left:-3.5
   centres it on the computed left:% (no transform needed). */
.cvss-ruler .dot {
  position: absolute;
  top: 13px;
  width: 7px; height: 7px;
  margin-left: -3.5px;
  border-radius: 50%;
  background: var(--nord5);
  border: 1px solid var(--nord0);
}
/* v4 tick: short vertical line above the dot when the source also carries
   a v4 score. nord7 teal separates it from the primary nord5 dot. */
.cvss-ruler .v4-tick {
  position: absolute;
  top: 4px;
  width: 2px; height: 8px;
  margin-left: -1px;
  background: var(--nord7);
}
/* Labels live below the axis. Two lanes alternate by source order so
   adjacent dots at similar scores don't overprint their labels.
   line-height:1 keeps the label box tight so lane-b doesn't overflow the
   44px ruler into the next detail row. */
.cvss-ruler .lbl {
  position: absolute;
  transform: translateX(-50%);
  font-family: "OperatorMonoSSmLig", "Operator Mono SSm",
               ui-monospace, monospace;
  font-size: 11px; line-height: 1;
  color: var(--nord4);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.cvss-ruler .lbl.lane-a { top: 22px; }
.cvss-ruler .lbl.lane-b { top: 33px; }

/* Affected-products preview for per-source views. Raw JSON fallback keeps
   the pre-wrap box; per-source structured rows render as stacked aff-rows
   with inline columns (state | product | package, or cpe | range, or
   ecosystem | package | range). Keep density high — htop-like. */
.detail .src-affected {
  font-size: 11px; color: var(--nord4);
  white-space: pre-wrap; word-break: break-all;
  max-height: 180px; overflow-y: auto;
}
.detail .src-affected .aff-row {
  display: flex; gap: 10px; align-items: baseline;
  white-space: normal; word-break: normal;
  line-height: 1.35;
}
.detail .src-affected .aff-cpe,
.detail .src-affected .aff-prod,
.detail .src-affected .aff-pkg,
.detail .src-affected .aff-eco { color: var(--nord5); }
.detail .src-affected .aff-eco { min-width: 72px; color: var(--nord7); }
.detail .src-affected .aff-state { min-width: 88px; text-transform: lowercase; }
.detail .src-affected .aff-state.ok   { color: var(--nord14); }
.detail .src-affected .aff-state.crit { color: var(--nord11); }
.detail .src-affected .aff-state.med  { color: var(--nord13); }
.detail .src-affected .aff-range { color: var(--nord3); font-variant-numeric: tabular-nums; }
.detail .src-affected .aff-more { margin-top: 2px; font-size: 10px; }

/* Distro fix-status block. One row per (distro, release) with package chips.
   Same visual register as affected-rows — dense, htop-aligned. Colour tracks
   the nord severity palette used everywhere else: ok (fixed / n/a), crit
   (needed), high (pending), med (deferred), dim (ignored). */
.detail .distro-list { font-size: 11px; color: var(--nord4); }
.detail .distro-list .distro-row {
  display: flex; gap: 10px; align-items: baseline;
  line-height: 1.35;
}
.detail .distro-list .distro-release {
  min-width: 120px; color: var(--nord7);
  font-variant-numeric: tabular-nums;
}
.detail .distro-list .distro-chip {
  padding: 0 6px;
  border-left: 1px solid var(--nord2);
}
.detail .distro-list .distro-chip:first-of-type {
  border-left: 0; padding-left: 0;
}
.detail .distro-list .distro-chip.ok   { color: var(--nord14); }
.detail .distro-list .distro-chip.high { color: var(--nord12); }
.detail .distro-list .distro-chip.med  { color: var(--nord13); }
.detail .distro-list .distro-chip.crit { color: var(--nord11); }
.detail .distro-list .distro-chip.dim  { color: var(--nord3); }

/* Change-history timeline. One row per observed field flip, most recent
   first. Column widths line up so date/field/arrow form a scannable strip;
   old value in dim, new value in body colour so the eye tracks the current
   state. Matches distro-list visual density. */
.detail .hist-list { font-size: 11px; color: var(--nord4); }
.detail .hist-list .hist-row {
  display: flex; gap: 10px; align-items: baseline;
  line-height: 1.35;
  white-space: nowrap;
}
.detail .hist-list .hist-when {
  min-width: 80px; color: var(--nord7);
  font-variant-numeric: tabular-nums;
}
.detail .hist-list .hist-field { min-width: 132px; color: var(--nord5); }
.detail .hist-list .hist-old,
.detail .hist-list .hist-new,
.detail .hist-list .hist-arrow {
  overflow: hidden; text-overflow: ellipsis;
}
.detail .hist-list .hist-new { color: var(--nord5); }

/* ---- graphs pane ----------------------------------------------------- */
/* Sections are delimited by their h4 header + padding alone — no
   border-bottom. The pane has overflow: hidden, so when content spills
   past the pane bottom the trailing section's border used to sit right
   above .detail-host's border-top and read as a double line. Tufte-light
   anyway: less ink, the headers carry the separation. */
.graphs .section { padding: 8px 12px; }
.graphs .section h4 {
  margin: 0 0 6px 0;
  font-size: 11px; font-weight: 500; color: var(--nord3);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.graphs .section .hint { color: var(--nord3); font-size: 11px; margin-top: 4px; }

.bar {
  display: grid;
  grid-template-columns: 94px 1fr 44px;
  align-items: center;
  column-gap: 8px;
  margin: 1px 0;
  font-size: 12px;
}
.bar .l { color: var(--nord4); }
.bar .t { text-align: right; color: var(--nord4); font-variant-numeric: tabular-nums; }
.bar .g { height: 10px; background: var(--nord2); position: relative; }
.bar .g > i { position: absolute; left: 0; top: 0; bottom: 0; display: block; }

/* year-bar sparklines — bars fill the pane width via preserveAspectRatio
   "none" on the SVG; no axes, no gridlines. Range labels sit under the figure
   as a 3-column flex row (min year · peak · max year). `.stacked` variants
   carry their render height inline (style="height:Npx") so hero + secondary
   panels can declare different sizes without new CSS classes. */
.spark-wrap { width: 100%; }
.spark      { display: block; width: 100%; height: 60px; }
.spark rect { fill: var(--nord9); }
.spark-axis {
  display: flex; justify-content: space-between;
  color: var(--nord3); font-size: 11px; margin-top: 4px;
  font-variant-numeric: tabular-nums;
}
.spark-axis .m { color: var(--nord4); }

/* Direct-labelled legend under each stacked sparkline. Small color square +
   stratum name, flex-wrapped so narrow panes just re-flow. Tufte-light — no
   box, no separator line. */
.spark-legend {
  display: flex; flex-wrap: wrap; gap: 4px 10px;
  margin-top: 4px; font-size: 11px; color: var(--nord4);
}
.spark-legend .leg-item   { display: inline-flex; align-items: center; gap: 4px; }
.spark-legend .leg-item i {
  display: inline-block; width: 8px; height: 8px;
}

/* The hero (enrichment) panel gets a slightly heavier visual weight than
   the following stacked panel — thicker left-margin tick so the eye lands
   on it first. Still no frames / backgrounds — just a nord2 hairline. */
.graphs .section.hero { border-left: 2px solid var(--nord2); padding-left: 10px; }

/* ---- severity + flag chips ------------------------------------------ */
.crit { color: var(--nord11); font-weight: 700; }
.high { color: var(--nord12); font-weight: 700; }
.med  { color: var(--nord13); }
.low  { color: var(--nord14); }
.kev  { color: var(--nord11); font-weight: 700; }
.exp  { color: var(--nord12); font-weight: 700; }
.adp  { color: var(--nord7);  font-weight: 700; }
.dim  { color: var(--nord3); }
.tag  { color: var(--nord15); }
.link { color: var(--nord9); }
.ok   { color: var(--nord14); }

/* ---- cursor ----------------------------------------------------------
   Absolutely positioned inside .prompt (which is position: relative).
   www/app.js measures text width up to input.selectionStart and updates
   `left` / `top` / `height` on every keystroke, caret move, and resize.
   Hidden by default — app boots in vim "normal mode" with no input
   focused. Revealed via :has(input:focus) so the block cursor only
   blinks while the search bar is active. */
.cursor {
  position: absolute;
  width: 0.55em; height: 1.05em;
  background: var(--nord8);
  pointer-events: none;
  animation: blink 1.05s steps(1) infinite;
  display: none;
}
.prompt:has(input.search:focus) .cursor { display: block; }
@keyframes blink { 50% { background: transparent; } }

/* ---- status bar ------------------------------------------------------ */
/* Single persistent horizontal separator above the keys row. Lives here
   (not on .detail-host) so an empty detail strip doesn't double-line
   against the status border — the line sits immediately above the status
   row regardless of detail state. */
.status {
  display: flex; justify-content: space-between; align-items: flex-start;
  padding: 4px 12px;
  color: var(--nord3); font-size: 12px;
  border-top: 1px solid var(--nord2);
}
.status .keys-col { display: flex; flex-direction: column; gap: 2px; }
.status .keys     { display: block; }
.status .keys b   { color: var(--nord4); font-weight: 500; }
/* Second line: query-syntax advertising. Dimmer than the keybind line so
   the eye reads keys first, syntax as secondary reference. `source:` /
   operator tokens pick up nord9 to echo the detail-pane link accent. */
.status .keys.syntax     { color: var(--nord3); }
.status .keys.syntax b   { color: var(--nord9); }

/* ---- footer attribution --------------------------------------------- */
.attr {
  color: var(--nord3); font-size: 11px;
  text-align: center; margin: 12px 0 24px 0;
}
.attr a { color: var(--nord9); }

/* Hide Shiny's error placeholders by default (we render our own empty states). */
.shiny-output-error, .shiny-output-error-validation { color: var(--nord11); font-size: 12px; }

/* ---- help overlay (? key / Esc) ------------------------------------- */
/* Fixed-position full-viewport backdrop, hidden by default. www/app.js
   toggles .show; clicking the backdrop (but not the card) closes it too. */
.help-overlay {
  display: none; position: fixed; inset: 0; z-index: 100;
  background: rgba(46, 52, 64, 0.88);
  align-items: center; justify-content: center;
}
.help-overlay.show { display: flex; }
.help-card {
  background: var(--nord1);
  border: 1px solid var(--nord3);
  padding: 20px 28px;
  /* Three columns side-by-side fit much better on a typical landscape
     display than a single tall stack. Cap at 92vw so the card still
     breathes on narrow windows; the column grid below reflows to 1 or 2
     columns via its own minmax() clamp. */
  width: min(1180px, 92vw);
  max-height: 88vh;
  overflow-y: auto;
  box-shadow: 0 0 0 1px var(--nord0);
}
/* Three-column layout for the help sections. `minmax(320px, 1fr)` keeps
   each column readable and reflows to fewer columns on narrow viewports
   (the grid auto-flows, so a phone-width card shows one column stacked). */
.help-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 8px 36px;
  align-items: start;
}
.help-col h2 {
  color: var(--nord6);
  margin: 0 0 10px 0;
  font-size: 14px; font-weight: 500;
  letter-spacing: 0.04em;
}
.help-card h2 {
  color: var(--nord6);
  font-size: 14px; font-weight: 500;
  letter-spacing: 0.04em;
}
.help-card dl {
  /* `auto` on the dt column lets each section size its label width to
     its own widest dt — the data-sources column has long names like
     "CISA Vulnrichment" while keybindings has tight "j / ↓". */
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 4px 14px;
  margin: 0;
  font-size: 13px;
}
.help-card dt { color: var(--nord8); font-weight: 500; }
.help-card dd { margin: 0; color: var(--nord4); }
.help-card .hint {
  color: var(--nord3); font-size: 11px;
  margin: 14px 0 0 0; padding-top: 10px;
  border-top: 1px solid var(--nord2);
}

/* ---- focus mode (F key) --------------------------------------------- */
/* .focus lives inside .body as one grid cell, display:none by default.
   Shiny's suspendWhenHidden defers rendering the 6 FOCUS plots until the
   user actually presses F — otherwise they all fire at session start,
   blocking page paint for seconds on a cold worker. First-paint-at-0x0
   is prevented by the explicit width/height fallbacks in mod_focus.R
   (every renderPlot has a safe floor). */
.focus {
  display: none;
  padding: 16px 20px;
  color: var(--nord4);
  grid-column: 1 / -1;
  grid-row: 1 / -1;
  overflow: auto;
}
.shell.focus-mode .body > .pane  { display: none; }
.shell.focus-mode .body > .focus { display: block; }

.focus h4 {
  margin: 0 0 8px 0;
  font-size: 11px; font-weight: 500; color: var(--nord3);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.focus .hint { color: var(--nord3); font-size: 11px; margin-top: 4px; }

/* Hero: full-width, card-less. No border, no background swatch — the plot
   itself carries the nord0 background via the ggplot theme. */
.focus-hero { margin-bottom: 24px; }

/* 2x3 grid of small multiples. Identical scale / type / padding across
   cells so the eye can compare without retraining, per Tufte. */
.focus-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}
.focus-cell { min-width: 0; }

/* Summary cell uses <dl> key/value pairs instead of a chart. */
.focus-dl {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: 16px; row-gap: 4px;
  margin: 0;
  font-size: 12px;
}
.focus-dl dt { color: var(--nord3); }
.focus-dl dd { color: var(--nord5); margin: 0; font-variant-numeric: tabular-nums; }

/* ---- network mode (N key) ------------------------------------------- */
/* .network mirrors .focus: grid-child of .body, display:none by default
   so Shiny suspends its plot outputs until N is pressed. Layout is a
   two-column split — circular vendor graph on the left, top-N companion
   table on the right. When shown, display:grid activates the two-column
   layout. */
.network {
  display: none;
  padding: 16px 20px;
  color: var(--nord4);
  grid-column: 1 / -1;
  grid-row: 1 / -1;
  overflow: auto;

  grid-template-columns: minmax(0, 1fr) 300px;
  gap: 24px;
  align-items: start;
}
.shell.network-mode .body > .pane    { display: none; }
.shell.network-mode .body > .focus   { display: none; }
.shell.network-mode .body > .network { display: grid; }

.network h4 {
  margin: 0 0 8px 0;
  font-size: 11px; font-weight: 500; color: var(--nord3);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.network .hint { color: var(--nord3); font-size: 11px; margin-top: 4px; }
.network-hero { min-width: 0; }
.network-side { min-width: 0; }

/* Companion table — dense monospace, no chrome. Matches the table treatment
   on the results pane so the two views sit in the same visual register. */
.network-side table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.network-side table th,
.network-side table td {
  padding: 2px 6px;
  border: 0;
  color: var(--nord4);
}
.network-side table thead th {
  color: var(--nord3);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  border-bottom: 1px solid var(--nord2);
  text-align: left;
}
.network-side table tbody td:first-child { color: var(--nord5); }
