/* ---------------------------------------------------------------------------
 * Clear CVE — visual system
 *
 * Read at 3am on a bridge call. That drives every decision here: a dark ramp
 * with real steps between page, panel and raised panel; one accent reserved
 * for things you can interact with; and severity colour kept strictly for
 * status, so nothing coloured is ever ambiguous about whether it's clickable.
 *
 * Every value below is a token. There are no hardcoded colours past :root.
 * ------------------------------------------------------------------------ */

/* ---------------------------------------------------------------------------
 * Tokens
 *
 * Every colour in this file is one of these. There are no literals in the
 * rules below — that was already true except for one overlay shadow, which is
 * now --shadow-overlay.
 *
 * Both themes live in one declaration each, via light-dark(). The alternative
 * — a second palette block repeated for the media query and again for the
 * manual override — states every colour three times and lets the copies
 * drift. Here the pair is the definition, and nothing downstream knows which
 * theme it is in.
 *
 * The switch is color-scheme, nothing else: `light dark` on :root follows the
 * system, and [data-theme] pins it. light-dark() resolves against whatever
 * color-scheme computes to, so the toggle needs to set exactly one property.
 *
 * Ratios in the comments are the worst case measured across every surface the
 * token is actually used on, both themes. AA is 4.5:1 for body text, 3:1 for
 * large text and for UI boundaries.
 * ------------------------------------------------------------------------ */

:root {
  color-scheme: light dark;

  /* --- Surfaces.
   *
   * Dark: page < panel < raised, each a visible step up in lightness.
   * Light: the panel steps UP from the page (a card catches the light) while
   * raised and hover step DOWN — a filled chip on paper is an inset, not a
   * highlight. Same roles, opposite directions, which is why these are a pair
   * per token rather than one ramp inverted.
   *
   * Spaced by CIE L*, not by WCAG ratio. Near white and near black the ratio
   * between two adjacent surfaces compresses to nothing — the old ramp put
   * page and panel 1.09:1 apart in BOTH themes, which is why the page read as
   * one flat sheet — while L* stays even along the whole range. Every step
   * below is at least 4 L*, and the page-to-panel step, which is the one that
   * says "this is a card", is 7-8:
   *
   *              L*      step        old L*   old step
   *     light  page  91.2          |  96.0
   *            panel 99.3   +8.1   |  99.3     +3.4
   *            raised 86.7  -12.7  |  92.2     -7.2
   *            hover  82.1   -4.6  |  88.0     -4.1
   *     dark   page   2.6          |   3.8
   *            panel  9.8    +7.2  |   8.2     +4.4
   *            raised 14.1   +4.3  |  12.8     +4.6
   *            hover  17.2   +3.1  |  14.3     +1.5
   *
   * The dark ramp could not move at all before: --text-muted sits on both
   * raised and hover, and at its old value AA capped every surface above the
   * panel at L* 14.5. The muted token is one step lighter now (see below),
   * which is what bought the room. --- */
  --surface-page: light-dark(#ece5db, #0b0907);
  --surface-panel: light-dark(#fffdf8, #201a15);
  --surface-raised: light-dark(#e0d8cc, #29231b);
  /* Still the token the ramp is pinned by: --text-muted measures 4.51:1 light
     and 4.53:1 dark here, and nothing above this is available. */
  --surface-hover: light-dark(#d4cbbe, #312920);

  /* --- The chrome band.
   *
   * The title and the search box sit on this; everything below the rule is
   * content. It is one step off the page in the same direction in both
   * themes — toward the panel, and about half way — which is what keeps it
   * legible as a different surface without letting it read as a card:
   *
   *              L*    step from page   distance left to panel
   *     light   95.6       +4.4                 -3.7
   *     dark     5.9       +3.4                 -3.9
   *
   * Measured in ratios it is 1.10:1 light and 1.07:1 dark against the page,
   * which is the compression the surface note above describes rather than a
   * failure — near white and near black, adjacent surfaces have almost no
   * ratio between them and L* is the only honest spacing. What carries the
   * actual boundary is the rule along the bottom: --rule measures 3.37:1 on
   * this surface in light and 3.54:1 in dark, and a 1px edge owes 3:1.
   *
   * Every token that lands on it clears AA. Worst case in light is
   * --text-muted at 6.49:1 and --border-control, the input's own edge, at
   * 3.45:1; in dark, --text-muted at 5.89:1 and --border-control at 3.91:1.
   * The direction matters for one thing in particular: the example chips fill
   * with --surface-panel and hover to --surface-raised, so a band ON
   * --surface-raised would have swallowed them at the moment they were
   * pointed at. */
  --surface-chrome: light-dark(#f6f2eb, #17120f);

  /* Floating things — the one overlay in the app. Elevation reads as lighter
     on a dark ground and as pure white on a light one, so this cannot be
     --surface-raised in both. */
  --surface-overlay: light-dark(#ffffff, #312920);
  --shadow-overlay: light-dark(rgb(0 0 0 / 14%), rgb(0 0 0 / 45%));

  /* --- How a section says where it ends.
   *
   * The two themes do this by different means, and that is the point rather
   * than an inconsistency. On a dark ground a card is legible because it is
   * lighter than the page: the surface step IS the separation. On paper the
   * same step is 1.23:1 and close to invisible, so what actually bounds a
   * card there is its edge and the shadow under it.
   *
   * So the shadow is a colour token that resolves to transparent in dark,
   * which makes both layers of the cards' box-shadow a no-op there. A single
   * light-dark() cannot hold a comma-separated shadow list — the commas would
   * read as extra arguments — and this way the geometry is stated once.
   *
   * Cards take --border-strong, not --border. --border is a hairline between
   * rows inside a card and wants to stay quiet; the outer edge of the card is
   * the thing carrying the separation. Measured against the two surfaces it
   * runs between — the card's own fill and the page — it is 2.25 / 1.83:1 in
   * light and 1.85 / 2.13:1 in dark. --border on the same pair is 1.47 / 1.20
   * and 1.40 / 1.61, which is the hairline it should stay. */
  --shadow-card: light-dark(rgb(74 60 40 / 10%), transparent);

  /* The left accent rule. A muted neutral, never a hue: the severity ramp and
     the accent are the only colours on this page that mean anything, and a
     coloured rule down the side of every section would either collide with
     them or say nothing. It is 3px, so it needs to clear 3:1 to register at
     all — 3.01:1 light and 3.78:1 dark against the page, 3.70 / 3.27 against
     the panel. Quieter in dark on purpose: a bar this wide at 4:1 on a near
     black ground reads as a highlight rather than as grouping. */
  --rule: light-dark(#8e8272, #786a56);

  --border: light-dark(#dad2c6, #3d3329);
  --border-strong: light-dark(#b5aa9b, #544433);

  /* The boundary of a form control, which is a different job from the two
     above. --border-strong divides surfaces and separates rows: decorative
     weight, and it measures 1.9-2.1:1 against the surfaces it sits on in both
     themes. That is fine for a rule between table rows and not fine for the
     edge of a text field, where WCAG 1.4.11 wants 3:1 — an input with no
     visible boundary is not identifiable as an input at all.

     So the controls whose edge is what identifies them take this instead:
     #cve-input, the collapsed-paste line that replaces it, and .copy-btn.
     Worst case 3.08:1 light (on --surface-page, which is darker than it was)
     and 3.95:1 dark (on --surface-panel, which is lighter). Everything else
     keeps --border-strong. */
  --border-control: light-dark(#8d8070, #857763);

  /* --- Text, four levels.
   *
   * Dark: spaced by relative luminance, 88 / 50 / 28 / 17. The first step is
   * deliberately the largest so primary reads as near-white against
   * everything else. Light mirrors the spacing from the other end. --- */
  --text-primary: light-dark(#1a1611, #f5efe4);
  --text-secondary: light-dark(#4a4136, #c9beac);
  /* This token is what the surface ramp is spaced against, in both
     directions. It sits on --surface-raised and --surface-hover all over the
     page, so AA on those two decides how far the top of the light ramp can
     fall and how far the top of the dark ramp can rise. Both values moved one
     step outward from the old pair to widen the surfaces above them: light
     #655e4f -> #615547, dark #948a76 -> #9e8f77. Worst case is now 4.51:1
     light and 4.53:1 dark, both on --surface-hover. */
  --text-muted: light-dark(#615547, #9e8f77);
  --text-disabled: light-dark(#8f8676, #7b7261); /* inactive controls only */

  /* --- One accent, in two roles.
   *
   * The gold is the brand and does not change: it is the fill on buttons and
   * the tint on callouts in both themes. What cannot survive the swap is gold
   * as TEXT — #e3b341 on white is 1.9:1, nowhere near AA — so the text role
   * is its own token and darkens on light. Splitting the roles is what lets
   * the fill stay identical across themes instead of being dulled to suit a
   * contrast requirement that only applies to the text. --- */
  --accent: #e3b341;
  --accent-hover: #f0c563;
  --accent-pressed: #c99a2e;
  --accent-fg: #100d0a;
  /* Both values sit on the sRGB gamut edge for their lightness, because links
     read as body text otherwise. The two themes get very different answers.

     Light cannot be fixed with saturation, and it is worth writing down why
     rather than trying again. The binding surface is --surface-raised
     (#e0d8cc) — darker than the page, and gold appears on it in .copy-btn and
     the vendor summaries. Solving for the most chromatic gold that still
     clears 4.5:1 there gives C 0.1005, against C 0.0996 for the value this
     replaces: 5% more chroma, which nobody can see. Yellow is simply narrow
     down there. So light-mode links are carried by WEIGHT and a standing
     underline instead — see .related-id — and this value takes the safe end
     of that non-choice at 4.99:1 on raised rather than the 4.55:1 ceiling.

     Dark has real headroom and takes it: C 0.14 -> 0.16, +19%, at 9.8:1. */
  --accent-text: light-dark(#785104, #f5ba15);
  --accent-text-hover: light-dark(#5c3d03, #fec849);
  --accent-soft: color-mix(in srgb, var(--accent) 14%, transparent);
  --accent-edge: color-mix(in srgb, var(--accent) 38%, transparent);
  --focus-ring: light-dark(#7a5709, #e3b341);

  /* --- Semantic status. Never used for interaction.
   *
   * Hues are deliberately pushed off the accent's 42 degrees so a status
   * colour and a link are not confusable: critical 358, high 14, medium 27,
   * low 124. The light values hold those hues and drop in lightness, because
   * all four are used as text somewhere and the dark ones sit at ~2.9:1 on
   * paper. --- */
  --critical: light-dark(#b7242a, #f2696e);
  --high: light-dark(#a5400e, #e8623a);
  --medium: light-dark(#8a5008, #d96a12);
  --low: light-dark(#2c7437, #6cbf72);
  /* Dark value lightened a hair. It is a dot fill and the "quiet" chip tone,
     never text, so it only owes 3:1 — but --surface-panel rose and this fell
     to 4.49:1 against it, which would have made it the one member of the set
     that could not be used as text. Now 4.74, so the rule stays "any semantic
     colour is safe on any surface" with no per-token check. */
  --neutral: light-dark(#6b6355, #8e8579);

  /* --- Emphasis tiers.
   *
   * Everything on this page used to sit at one weight: measured, every chip
   * fill was C 0.01-0.03 and 1.09-1.16:1 against the card, in both themes.
   * Four tones, one wash, nothing to look at first.
   *
   * So emphasis is now tiered to URGENCY, not to severity, and the tiers are
   * separated by construction rather than by a few points of chroma:
   *
   *   LOUD    solid saturated fill      6.05:1 against the card, C 0.20
   *   MEDIUM  20% tint + solid border   1.36:1, border 4.5:1 on the fill
   *   QUIET   no fill, outline only     2.5:1 outline, muted label
   *
   * A tint can never outshout a solid fill, so the ordering holds in both
   * themes without depending on values that later move.
   *
   * LOUD is for what someone is ACTUALLY DOING to this CVE — on CISA's
   * exploited list, used in ransomware, exploit code published. Two hues, not
   * one: "known exploited" and "exploit code is public" are different claims
   * and stay visibly different chips.
   *
   * Light needs a dark fill under a light label and dark needs the reverse,
   * which is why --loud-fg is a pair and the fills are not one ramp inverted.
   */
  --loud-alarm: light-dark(#c30f23, #fd646d);
  --loud-warn: light-dark(#ad4209, #fd693e);
  --loud-fg: light-dark(#fffdf8, #100d0a);

  /* The severity badge is NOT a tier: it answers "how bad", and the tiers
     answer "how urgent". It keeps full semantic chroma in its border and
     label and takes a tint rather than a fill, so it can never be the loudest
     thing on screen — a 9.8 nobody is exploiting must not outshout an
     actively-exploited 7.5, and that inversion is the product's argument.
     Measured: badge 1.31:1 against the card, loud chip 6.05:1.

     The solid --fill-* pills this replaces were mixed toward white in both
     themes to hold a near-black label, and the note they carried is worth
     keeping: at full chroma they outshouted the accent, and the mix was what
     brought them back under it. The tint does that structurally instead, and
     the 4-token ramp is gone with them.

     The label is mixed toward --text-primary, which is itself a light-dark
     pair, so one ratio darkens the tone on paper and lightens it on a dark
     ground. Worst case 4.94:1, on --low in light.

     A tint has to be mixed over the surface it will actually sit on, and the
     severity block stopped being a card — so --sev-ground names that surface
     instead of the badge assuming a panel. Mixed over the panel while sitting
     on the page it came out at 1.02-1.06:1 against its own ground in light,
     i.e. a badge with no tint left in it; over the right ground it is
     1.18-1.28:1 in both themes, which is what was measured in the first
     place. Still nowhere near the 6.05:1 of a loud chip, which is the
     ordering that matters. The default stays the panel so a badge dropped
     into a card later needs nothing. */
  --sev-tint: 16%;
  --sev-ground: var(--surface-panel);

  /* Chips and dots tint from the semantic set, so a new status needs no new
     hand-picked hex — only a --tone- pair. */
  --tone-alarm: var(--critical);
  /* --high, not --medium: as a chip label, medium blended out to 15 degrees
     from the accent, which is too close to read apart at a glance. On --high
     the label lands at 27. Medium still drives the MEDIUM dot and pill. */
  --tone-warn: var(--high);
  --tone-quiet: var(--neutral);
  --tone-unknown: var(--text-disabled);

  /* --- Type --- */
  --font-sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
    Roboto, Helvetica, Arial, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo,
    Consolas, monospace;

  --text-xs: 0.75rem;
  --text-sm: 0.8125rem;
  --text-base: 0.9375rem;
  --text-md: 1.0625rem;
  --text-lg: 1.3125rem;
  --text-xl: 1.75rem;

  --weight-normal: 400;
  --weight-medium: 500;
  --weight-semi: 600;

  /* --- Motion and shape --- */
  --dur: 150ms;
  /* The landing-to-result swap. Longer than --dur because it moves a whole
     screen rather than one control, short enough that it is never a wait:
     this fires on every lookup, dozens of times in a sitting. */
  --dur-swap: 200ms;
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --radius: 6px;
  --radius-sm: 4px;
}

/* The toggle. One property, because light-dark() reads exactly this. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }


/* Honour the OS setting rather than animating anyway. Zeroing the token
   disables every transition in the sheet at once. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --dur: 0ms;
    --dur-swap: 0ms;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

* {
  box-sizing: border-box;
}

/* ---------------------------------------------------------------------------
 * hidden means hidden.
 *
 * The browser's own [hidden] { display: none } lives in the user-agent
 * origin, which loses to EVERY author rule regardless of specificity. So any
 * element this file gives a `display` to could not be hidden from JS, and the
 * fix was a per-element [hidden] override next to each one. That is a rule
 * you have to remember, and it was forgotten four times: the empty result
 * grid rendered before the first lookup, the collapsed input line sat beside
 * the real field as a permanent second box, the reachability tooltip could
 * not be dismissed, and the fix panel's link note would never have hidden.
 *
 * Three of those reached a browser. The utility was the problem, not the call
 * sites, so it is fixed here once: an author rule beats an author rule, and
 * !important beats one carrying a `display` at any specificity. Every
 * per-element override is now deleted, and adding `display` to anything can
 * no longer break its ability to hide.
 *
 * !important is load-bearing rather than lazy. Without it this rule still
 * loses to `#result.layout { display: grid }` on specificity alone, which is
 * exactly the bug. There is no case in this app where an element carrying the
 * hidden attribute should still be on screen — that is what the attribute
 * means — so nothing legitimate is being overridden.
 *
 * The one thing this forecloses is hidden="until-found", where the browser
 * reveals content for find-in-page. Not used here, and it would need
 * `content-visibility: hidden` rather than this attribute anyway; if it is
 * ever wanted, exempt it with [hidden="until-found"] rather than weakening
 * this.
 * ------------------------------------------------------------------------ */
[hidden] {
  display: none !important;
}

/* The one thing the chrome band needs from outside itself.
 *
 * The band escapes the body's padding with `margin-inline: calc(50% - 50vw)`,
 * and vw counts the scrollbar while the layout does not — so on a page long
 * enough to scroll the band is a scrollbar-width too wide and asks for a
 * horizontal scrollbar it has no use for. Clipping it here is the fix that
 * doesn't cost a wrapper element.
 *
 * `clip`, never `hidden`. Overflow on html or body propagates to the
 * viewport, and `hidden` would make the viewport a scroll container — which
 * silently breaks position: sticky and scrollIntoView behaviour for anything
 * added later. `clip` does neither; it only refuses to scroll to what is
 * outside. Nothing on this page needs to be reachable off the right edge:
 * the one thing that can overflow is the reachability tooltip, and that
 * measures itself and picks the edge that keeps it on screen. */
html {
  overflow-x: clip;
}

body {
  margin: 0;
  /* Tokenised because the chrome band has to reach past the top one and put
     it back inside itself; two literals would drift the moment either moved. */
  --page-pad-y: 2rem;
  padding: var(--page-pad-y) clamp(1rem, 3vw, 2.5rem);
  background: var(--surface-page);
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

/* Every link takes the accent. Without this a link added anywhere outside a
   styled block falls back to the user agent's own blue, which is exactly the
   second accent this palette exists to avoid. */
a {
  color: var(--accent-text);
  text-underline-offset: 2px;
  transition: color var(--dur) var(--ease);
}

a:hover {
  color: var(--accent-text-hover);
}

a:active {
  color: var(--accent-pressed);
}

/* One ring definition for everything keyboard reachable, so a new control
   inherits it instead of needing its own rule. */
:where(a, button, summary, input, [tabindex], [role="tab"], [role="tabpanel"]):focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* Results use the whole window. The cap only stops the columns pulling
   apart on an ultrawide display. */
.page {
  position: relative; /* the anchor for .theme-toggle */
  max-width: 1800px;
  margin: 0 auto;
  /* A block formatting context, and it is here for exactly one reason: the
     chrome band's negative top margin. Without it that margin collapses
     THROUGH this element — a parent with no top border or padding takes its
     first child's top margin as its own — and the whole page, theme toggle
     and results included, rides up by the body's top padding instead of just
     the band. `flow-root` is the version of that fix with no side effects:
     no float behaviour, no clearing, nothing overflowing hidden. */
  display: flow-root;
}

/* ---------------------------------------------------------------------------
 * Cards
 *
 * A card marks a DISTINCT UNIT, not merely a section. Everything on the main
 * column was one for a while — urgency, severity, description, tab panel —
 * and four identical rectangles is separation without hierarchy: the eye gets
 * borders everywhere and ranking nowhere, which is the flatness the tiering
 * fixed for chips reappearing at the level of the page.
 *
 * So the test is whether the content is a thing rather than a passage:
 *
 *   #verdict          yes — the primary answer, and the only card above the
 *                     fold. Carries the verdict rule, see below.
 *   [role=tabpanel]   yes — it holds switchable content, and the box is what
 *                     says the tabs above it change what is inside.
 *   .col-side         yes — a separate column with its own subject.
 *   #fix-summary      yes — the one instruction in the batch view.
 *   .triage           yes — a table.
 *   #cvss-block       NO. A heading and whitespace say "different question"
 *                     perfectly well, and boxing it made the severity compete
 *                     with the verdict directly above it.
 *   #result-description NO. It is body text. A box around a paragraph says
 *                     the paragraph is a component, which it is not.
 *
 * Three things do the separating, and which one carries the weight depends on
 * the theme (see --shadow-card):
 *
 *   surface   panel against page. Real in dark, nearly invisible in light.
 *   edge      --border-strong, not --border. Worst case against the two
 *             surfaces it runs between is 1.83:1 light and 1.85:1 dark.
 *   shadow    light only. Two layers: a 1px contact shadow that sits the card
 *             on the page, and a wide soft one that lifts it. In dark
 *             --shadow-card is transparent and both layers are a no-op.
 *
 * The 3px left rule is the fourth thing and it is not separation — it is
 * grouping. Every card starts on the same vertical line, so a column of them
 * reads as one stack of answers rather than as scattered boxes, and the eye
 * has an edge to run down. Neutral by definition: see --rule.
 *
 * There is no .card class because nothing in app.js needs to add one; the
 * sections are all fixed parts of the page and naming them here keeps the
 * markup free of presentational classes.
 * ------------------------------------------------------------------------ */
#verdict,
[role="tabpanel"],
.col-side,
#fix-summary,
.triage {
  background: var(--surface-panel);
  border: 1px solid var(--border-strong);
  border-left: 3px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: 0 1px 1px var(--shadow-card), 0 3px 10px -3px var(--shadow-card);
}

/* Everything except the table, which pads its own cells. Individual sections
   override this where they were already tuned; this is the default so a new
   card needs no padding rule of its own. */
#verdict,
[role="tabpanel"],
.col-side,
#fix-summary {
  padding: 1rem 1.25rem;
}

/* ---------------------------------------------------------------------------
 * Theme toggle
 *
 * Out of the flow, top right. The landing header is a 100vh centred block, so
 * a bar above it would push the fold down and re-centre everything below —
 * paying for a 36px control with the composition of the whole empty state.
 * Absolutely positioned it costs nothing, and the top of the landing is empty
 * anyway because the header centres its content.
 *
 * Anchored to .page rather than .page-head: .page-head is capped at 720px and
 * a control pinned to its right edge would sit in the middle of a results
 * screen, where the page runs to 1800px. On a results view it clears the
 * title, which is centred and short.
 * ------------------------------------------------------------------------ */
.theme-toggle {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text-muted);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}

/* Not the accent. Changing the theme is housekeeping, not one of the things
   this page is for, and the accent is reserved for the primary action. */
.theme-toggle:hover:not(:disabled) {
  background: var(--surface-raised);
  border-color: var(--border-strong);
  color: var(--text-primary);
}

.theme-toggle:active:not(:disabled) {
  background: var(--surface-hover);
}

.theme-toggle:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* Which icon shows is the EFFECTIVE theme, and only app.js knows that: the
   page is either following the system or pinned by the reader, and stating
   that pair in CSS alone means declaring it three times over — once for the
   media query, once for each [data-theme] — which is the duplication the
   token block exists to avoid. So app.js writes the answer onto the button
   and this reads it.

   A CSS-driven swap rather than the hidden attribute, because which icon
   shows is a function of the button's own state and nothing in JS needs to
   toggle either one. (It used to say the hidden attribute would not work
   here; since the global [hidden] rule at the top of this file, that is no
   longer true, and either approach would now work. This one still reads
   better — the state lives in one place.) */
.theme-icon {
  display: none;
  width: 1.05rem;
  height: 1.05rem;
}

.theme-toggle[data-theme="dark"] .theme-icon-sun,
.theme-toggle[data-theme="light"] .theme-icon-moon {
  display: block;
}

/* ---------------------------------------------------------------------------
 * The chrome band
 *
 * The title and the search box are the tool; everything below the rule is
 * what the tool returned. They were on one continuous sheet, which said the
 * search box and the answer were the same kind of thing — so the band gives
 * the controls their own ground and the rule is where the reading starts.
 *
 * It is one element wrapping the header and nothing else. .page-head is
 * capped at 720px and centred, and a band cannot be: it has to reach the
 * window edges while the column inside it stays where it was.
 * ------------------------------------------------------------------------ */
.head-band {
  /* Out to the window and back again. 50% is half this element's containing
     block and 50vw is half the viewport, so the pair cancels exactly whatever
     inset the body's padding and .page's 1800px cap have put this column at —
     without naming either of them, and without a second wrapper to hang the
     background on. The margin takes the box out; the padding puts the content
     back. See the html rule at the top for the scrollbar's share of it. */
  margin-inline: calc(50% - 50vw);
  padding-inline: calc(50vw - 50%);

  /* Up past the body's top padding, then that padding restored on the inside:
     chrome starts at the top of the window, and the title starts where it
     always did. */
  margin-top: calc(var(--page-pad-y) * -1);
  padding-top: var(--page-pad-y);

  background: var(--surface-chrome);
  /* The lower edge, and the only rule on this page that separates two regions
     rather than heading a section. --rule holds 3.37:1 light and 3.54:1 dark
     against the band; a 1px boundary owes 3:1. */
  border-bottom: 1px solid var(--rule);

  /* A stacking context, and it is load-bearing rather than tidiness. The glow
     behind the title is a z-index: -1 pseudo-element, and negative z-index
     descendants paint BEFORE any block background in the same stacking
     context — so without one here the band's own fill would cover the glow
     outright. Inside one the order is: band background, glow, header. The
     theme toggle sits at z-index 1 on .page, above both. */
  position: relative;
  z-index: 0;
}

/* The search box is the exception: a full-bleed text input is worse to use
   than a narrow one, so the header stays centred over the columns. */
.page-head {
  max-width: 720px;
  margin: 0 auto;
}

/* With nothing else on the page, the header takes the screen rather than
   sitting in a band at the top with a void beneath it. It drops back to
   normal flow the moment there is a result to read.
 *
 * The centring lives on .page-head unconditionally and only min-height is
 * toggled, which is what makes the collapse animatable. It used to toggle
 * `display: flex` alongside the height: at the moment of the swap the content
 * stopped being centred and jumped to the top of a box that was still
 * 836px tall, so there was nothing left to animate — the title had already
 * arrived. Centred throughout, the same 703px of height leaving the box
 * carries the title from the middle of the screen to the top of it.
 *
 * min-height: 0 on a flex column with justify-content: center is inert, so
 * the result view is laid out exactly as it was before.
 *
 * See "the swap" below for why this is transitioned and the h1 with it. */
.page-head {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 0;
}

body:not([data-view]) .page-head {
  min-height: calc(100vh - var(--page-pad-y) * 2);
}

/* ---------------------------------------------------------------------------
 * Empty state
 *
 * The page before a search is the only place with any atmosphere. Everything
 * here is static — a fixed gradient and a fixed noise layer, no animation
 * beyond a single fade on load — and all of it is confined to the header.
 * Once a CVE is on screen the reading area is flat and calm, which is the
 * whole point: you are reading this at 3am to make a decision.
 * ------------------------------------------------------------------------ */

/* The glow. Sized and positioned to sit behind the header only; it ends well
   above the results, so no body text is ever read against a gradient. */
.page-head {
  position: relative;
}

.page-head::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  /* Sized against the header, not the viewport. A vw-based width overflowed
     at 380px, and neither html nor body can clip it away: overflow on either
     propagates to the viewport rather than clipping. Bounding it here is the
     only fix that doesn't reach for a wrapper element. */
  width: 100%;
  height: 32rem;
  transform: translate(-50%, -50%);
  background: radial-gradient(
    ellipse 50% 50% at 50% 50%,
    color-mix(in srgb, var(--accent) 8%, transparent) 0%,
    color-mix(in srgb, var(--accent) 3%, transparent) 45%,
    transparent 70%
  );
  pointer-events: none;
  z-index: -1;
}

/* Noise, fixed so it never scrolls against the content and never redraws.
   2.5% over a dark ramp is enough to stop the flat areas banding. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0.025;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

h1 {
  font-family: var(--font-sans);
  font-size: clamp(2.75rem, 7vw, 4.5rem);
  font-weight: 700;
  letter-spacing: -0.045em;
  line-height: 1.05;
  text-align: center;
  color: var(--text-primary);
  margin: 0 0 1rem;
}

/* Gold fading down through the letterforms. The stops keep the bottom of the
   text well clear of invisible — it is decorative weight, not a reason to
   make the product name hard to read.

   --accent-text, not --accent, and this is the one place the distinction is
   load-bearing outside body copy. In dark the two tokens are the same value,
   so nothing here changes. In light they are not: the gradient used to end on
   #e3b341 over a cream page, which measures 1.57:1 — the bottom third of a
   4.5rem display heading, well under the 3:1 that large text needs. On
   --accent-text it ends at 3.90:1 and still reads as gold. */
@supports (-webkit-background-clip: text) or (background-clip: text) {
  h1 {
    background: linear-gradient(
      175deg,
      var(--text-primary) 0%,
      color-mix(in srgb, var(--accent-text) 45%, var(--text-primary)) 55%,
      color-mix(in srgb, var(--accent-text) 80%, transparent) 100%
    );
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}

.tagline {
  max-width: 46ch;
  margin: 0 auto 1.75rem;
  text-align: center;
  font-size: var(--text-md);
  line-height: 1.55;
  color: var(--text-secondary);
  text-wrap: pretty;
}

/* --- Example chips --- */

.examples {
  margin-top: 1.25rem;
  text-align: center;
}

.examples-label {
  display: block;
  margin-bottom: 0.6rem;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.example-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
}

/* The labels are questions now, not two-word captions, so a chip is a column
   in a row of three rather than something sized by its text. Equal basis with
   a cap, so the three read as three of a kind and the label wraps to two
   lines instead of stretching the row. The <li> is the flex item — .example
   is the button inside it. */
.example-list > li {
  display: flex;
  flex: 1 1 13rem;
  max-width: 15rem;
}

/* Straight to one per row, rather than letting it find its own break. Flex
   lines are decided on flex-basis and not on the shrunk width, so three
   13rem items in a 500px row still fit two on the first line — and 2 + 1 is
   the arrangement that reads as a mistake rather than as a layout. The
   breakpoint is where three across stops fitting: the header caps at 720px
   and the body pads it by up to 2.5rem a side. */
@media (max-width: 700px) {
  .example-list > li {
    flex-basis: 100%;
    max-width: none;
  }
}

/* Interactive, so it takes the accent — the severity palette stays out of
   the empty state entirely. */
.example {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.5rem 0.85rem;
  text-align: left;
  background: var(--surface-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease), transform var(--dur) var(--ease);
}

.example:hover {
  background: var(--surface-raised);
  border-color: var(--accent-edge);
  transform: translateY(-1px);
}

.example:active {
  transform: translateY(0);
}

/* The action the chip performs, and the primary line. It replaced a CVE ID
   set in mono — the ID was the checkable half of a claim about one lookup,
   and two of these three chips no longer run one. So it is prose now, in the
   body face, and it carries the accent because the chip is the interactive
   thing in the empty state.

   No underline, unlike the links elsewhere that carry the same token: this
   sits inside a button that is already bordered and hoverable, and underlining
   it would offer a second, narrower target that is not one. Weight separates
   it from the note instead. --accent-text holds 4.5:1 on both surfaces the
   chip uses — panel at rest, raised on hover. */
.example-action {
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  color: var(--accent-text);
}

.example-note {
  font-size: var(--text-xs);
  line-height: 1.4;
  color: var(--text-muted);
  /* Two lines of roughly equal length rather than a full line and one
     orphaned word — these are sentences now, and they wrap. */
  text-wrap: balance;
}

/* --- Retire the empty state once there is an answer --- */

body[data-view] .tagline,
body[data-view] .examples {
  display: none;
}

/* The title steps back rather than disappearing, so the page keeps its
   identity without spending a third of the screen on it. */
body[data-view] h1 {
  font-size: var(--text-xl);
  letter-spacing: -0.02em;
  margin-bottom: 1.75rem;
}

/* And the glow retires with it: nothing decorative sits behind a result. */
body[data-view] .page-head::before {
  display: none;
}

/* --- Load fade. One pass, header only, never on the results view. --- */

@keyframes intro-rise {
  from {
    opacity: 0;
    transform: translateY(0.5rem);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* backwards, not both, and the difference matters. `backwards` is the half
   this needs: it holds the `from` values through the stagger delay below, so
   nothing sits at full opacity for 180ms and then jumps down to rise again.
   `forwards` was doing nothing visible — the `to` values are opacity 1 and
   transform none, which is what these elements compute to anyway — but a
   filling animation outranks normal author declarations in the cascade, so
   it would have gone on overriding them for the life of the page. That is
   not theoretical: the recede in the swap block below sets opacity and
   transform on these exact two elements, and a `both` here silently wins. */
body:not([data-view]) h1,
body:not([data-view]) .tagline,
body:not([data-view]) #search-form,
body:not([data-view]) .examples {
  animation: intro-rise 500ms var(--ease) backwards;
}

body:not([data-view]) .tagline { animation-delay: 60ms; }
body:not([data-view]) #search-form { animation-delay: 120ms; }
body:not([data-view]) .examples { animation-delay: 180ms; }

/* The global reduced-motion block already collapses the duration; clearing
   the delays too means nothing sits invisible waiting for its turn. */
@media (prefers-reduced-motion: reduce) {
  body:not([data-view]) h1,
  body:not([data-view]) .tagline,
  body:not([data-view]) #search-form,
  body:not([data-view]) .examples {
    animation: none;
    animation-delay: 0s;
  }
}

/* ---------------------------------------------------------------------------
 * The swap, landing to result
 *
 * A lookup runs dozens of times in a sitting, so the one rule this obeys is
 * that nothing ever waits on it. The recede is a transition set the moment
 * the request goes out, which means it overlaps the network rather than being
 * spent before it: on a warm cache the answer lands first and the empty state
 * is simply gone, on a cold one the page has already acknowledged the click.
 * The result's own entrance is a keyframe that runs on paint. Neither is
 * awaited by anything in app.js.
 *
 * A transition and not an animation for the outgoing half, because it has to
 * be reversible — an invalid ID or an unreachable server puts the reader back
 * on the landing page, and a keyframe that has already run cannot be undone.
 *
 * Only the landing recedes. Following a related CVE from one result to the
 * next hides the old result outright, which is deliberate and older than this:
 * a rejected input must never leave a stale answer sitting under the error.
 *
 * --- What was actually wrong with it ---
 *
 * Both halves of this always fired. Instrumented: the recede starts on the
 * click, view-in starts on the frame #result is unhidden and runs its full
 * 200ms, and prefers-reduced-motion was not matching. Nothing here was
 * broken, and none of that was visible.
 *
 * What drowned it was the header, which was not part of the effect at all.
 * Setting data-view collapsed .page-head from `min-height: calc(100vh - 4rem)`
 * to normal flow and the h1 from clamp(2.75rem, 7vw, 4.5rem) to --text-xl in
 * ONE FRAME. Measured at 1280x900: 836px of header became 133px, the title
 * went 72px to 28px, and the document grew 900 to 1621 — between two
 * consecutive rAF samples. Against a 703px unanimated jump, a 1.5% scale and
 * a fade on the incoming block is not subtle, it is invisible.
 *
 * So the fix is not more effect on the result. It is animating the thing that
 * was already moving: the height leaves the header over the same 200ms, the
 * title rides it down from the middle of the screen to the top, and the
 * result — which is on screen and opaque from the first frame — surfaces into
 * the space as it closes. One motion, and the answer is not waiting on it.
 * ------------------------------------------------------------------------ */

/* The collapse. min-height is a layout property and animating one is usually
   a bad trade; here the animating subtree is a title, an input and a button
   inside a 720px column, and it is what the reader is actually watching. The
   alternative — a transform on the header — cannot be made to work, because
   the point is that the 703px LEAVES the flow and pulls the result up with
   it, which a transform by definition does not do. */
.page-head {
  transition: min-height var(--dur-swap) var(--ease);
}

/* The title rides the collapse rather than cutting to its small size at the
   top of it. Same duration, so the two read as one movement.

   Not scoped to the swap, because CSS has no way to say "only when data-view
   caused it". The one other thing that changes this font-size is a window
   resize between about 630px and 1030px, where the clamp is on its 7vw leg;
   there the title now follows the drag 200ms behind instead of tracking it
   exactly. Judged the better trade — a resize is not a thing anyone watches
   closely, and the swap is. */
h1 {
  transition: font-size var(--dur-swap) var(--ease),
    margin-bottom var(--dur-swap) var(--ease);
}

.tagline,
.examples {
  transition: opacity var(--dur-swap) var(--ease),
    transform var(--dur-swap) var(--ease);
}

body[data-busy] .tagline,
body[data-busy] .examples {
  opacity: 0;
  /* Small on purpose. Enough to read as stepping back, not enough to read as
     a thing that moved. */
  transform: scale(0.97);
}

@keyframes view-in {
  from {
    opacity: 0;
    transform: scale(0.985) translateY(0.4rem);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Restarts on its own each lookup: both of these are shown and hidden with
   the hidden attribute, and an animation re-runs whenever an element comes
   back from display:none. No JS hook, and nothing to reset between CVEs. */
#result:not([hidden]),
#batch:not([hidden]) {
  /* No delay, so there is nothing for a fill to cover — and see intro-rise
     above for why a `forwards` here would be worse than useless. */
  animation: view-in var(--dur-swap) var(--ease) backwards;
}

/* Straight swap, as asked: no transform anywhere, and the landing holds still
   while the request is out rather than blinking away and back on an error.
   The global reduce block only collapses durations, which would still apply
   the receded state — instantly, which is worse than not at all. */
@media (prefers-reduced-motion: reduce) {
  body[data-busy] .tagline,
  body[data-busy] .examples {
    opacity: 1;
    transform: none;
  }

  #result:not([hidden]),
  #batch:not([hidden]) {
    animation: none;
  }
}

#search-form {
  display: flex;
  align-items: flex-start; /* or the button stretches as the box grows */
  gap: 0.5rem;
}

/* A textarea wearing an input's clothes. It is a textarea so a multi-line
   paste survives (an <input type="text"> welds the lines together), and
   everything below is the cost of that: resize off because JS sizes it to
   its content, a line-height that makes one row the same height the input
   was, and overflow hidden so the scrollbar never appears mid-grow. */
#cve-input {
  flex: 1;
  min-width: 0;
  padding: 0.6rem 0.75rem;
  font-family: var(--font-mono);
  font-size: var(--text-base);
  line-height: 1.4;
  border: 1px solid var(--border-control);
  border-radius: var(--radius);
  background: var(--surface-panel);
  color: var(--text-primary);
  resize: none;
  overflow: hidden;
  transition: border-color var(--dur) var(--ease),
    background-color var(--dur) var(--ease);
}

#cve-input::placeholder {
  color: var(--text-disabled);
}

#cve-input:hover {
  border-color: var(--accent-edge);
}

#cve-input:focus {
  outline: 2px solid var(--focus-ring);
  outline-offset: 1px;
  border-color: transparent;
}

/* --- The collapsed box ---------------------------------------------------
 *
 * What a pasted advisory becomes once it has been triaged. It stands in for
 * the field, so it is shaped like the field — same height, same border, same
 * radius — and not like the button next to it, which is why every one of the
 * global button styles is overridden here. It is a control, though: it opens
 * the paste back up, and it says so.
 * ---------------------------------------------------------------------- */

.input-summary {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.6rem 0.75rem;
  font-family: inherit;
  font-size: var(--text-base);
  font-weight: var(--weight-normal);
  text-align: left;
  border: 1px solid var(--border-control);
  border-radius: var(--radius);
  background: var(--surface-panel);
  color: var(--text-secondary);
}

.input-summary:hover:not(:disabled) {
  background: var(--surface-panel);
  border-color: var(--accent);
}

.input-summary:active:not(:disabled) {
  background: var(--surface-panel);
}

.input-summary:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 1px;
  border-color: transparent;
}

/* The paste can be long — a heading line out of an advisory, say — and the
   affordance next to it must not be what gets pushed off the end. */
.input-summary-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.input-summary-edit {
  flex: none;
  font-size: var(--text-sm);
  color: var(--accent-text);
}

/* Nothing to submit while the box is collapsed: the result of the last
   submit is what is on screen. The button comes back with the field. */
#search-form[data-collapsed="true"] button[type="submit"] {
  display: none;
}

button {
  padding: 0.6rem 1.1rem;
  font-family: inherit;
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  border: none;
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--accent-fg);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease),
    color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

button:hover:not(:disabled) {
  background: var(--accent-hover);
}

button:active:not(:disabled) {
  background: var(--accent-pressed);
}

button:disabled {
  background: var(--surface-raised);
  color: var(--text-disabled);
  cursor: default;
}

#status {
  margin-top: 1rem;
  min-height: 1.25rem;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

#status[data-tone="error"] {
  color: var(--critical);
}

/* --- Breadcrumb trail --------------------------------------------------- */

#breadcrumb {
  margin: 1.5rem 0 0;
}

.crumbs {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}

/* The arrow is drawn between items rather than added as markup, so the trail
   is a clean list for a screen reader. */
.crumbs li + li::before {
  content: "→";
  margin-right: 0.35rem;
  color: var(--text-disabled);
  font-family: var(--font-sans);
}

.crumb {
  padding: 0.15rem 0.4rem;
  font-family: inherit;
  font-size: inherit;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  color: var(--accent-text);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease),
    color var(--dur) var(--ease);
}

.crumb:hover {
  background: var(--surface-raised);
  color: var(--accent-text-hover);
}

.crumb:active,
.related-id:active,
.copy-btn:active {
  color: var(--accent-pressed);
}

/* Where you are now: not a link, and it should not look like one. */
.crumb-current {
  padding: 0.15rem 0.4rem;
  color: var(--text-muted);
  cursor: default;
}

.crumb-ellipsis {
  color: var(--text-disabled);
  cursor: help;
}

/* --- Result heading row --- */

.result-id-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-bottom: 0.25rem;
}

.result-id-row #result-id {
  margin: 0;
}

#result {
  margin-top: 1.5rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--border);
}

#result-id {
  margin: 0 0 0.25rem;
  font-family: var(--font-mono);
  font-size: var(--text-lg);
  font-weight: var(--weight-medium);
  letter-spacing: -0.01em;
  color: var(--text-primary);
}

#result-meta {
  margin: 0 0 1rem;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

/* Body text, and nothing else. It had a box for a while, which said the
   paragraph was a component; 76ch was the measure with the box's padding
   added back, so removing the box returns it to the 70ch every other passage
   on this column uses. */
#result-description {
  margin-bottom: 1.75rem;
  max-width: 70ch;
  color: var(--text-primary);
}

/* A section heading, not another label.
 *
 * This was --text-sm / semi / --text-muted sitting directly above
 * .metric-label at --text-sm / normal / --text-muted: same size, same colour,
 * one weight step between a heading and the rows it introduces. It read as
 * the first item in the list rather than as the thing naming it.
 *
 * Now the same shape as the other section headings at this level —
 * .side-head and .detect-head are both base/semi/primary — so a heading looks
 * like a heading wherever it appears, and the step down to a metric label is
 * a size (15px against 13px), a weight AND a colour rather than a weight
 * alone. (.mech-head is a step above at --text-md: it heads a section inside
 * a panel, where there is a further level to rank.) */
#cvss-block h3 {
  font-size: var(--text-base);
  font-weight: var(--weight-semi);
  letter-spacing: 0;
  color: var(--text-primary);
  margin: 0 0 0.5rem;
}

/* Saturated, and deliberately not loud. See --sev-tint: the chroma is in the
   border and the label, the fill is a tint, and a tint cannot outshout the
   solid fill of an exploitation chip in either theme. */
.severity {
  display: inline-block;
  padding: 0.15rem 0.6rem;
  border-radius: 999px;
  font-weight: var(--weight-semi);
  font-size: var(--text-sm);
  background: color-mix(in srgb, var(--sev-tone) var(--sev-tint), var(--sev-ground));
  border: 1px solid var(--sev-tone);
  color: color-mix(in srgb, var(--sev-tone) 78%, var(--text-primary));
}

.severity-critical { --sev-tone: var(--critical); }
.severity-high { --sev-tone: var(--high); }
.severity-medium { --sev-tone: var(--medium); }
.severity-low { --sev-tone: var(--low); }

/* Unscored is not a severity band, so it gets no fill to compete with the
   real ones — and a softened grey fill couldn't hold the dark label at AA. */
.severity-none {
  background: transparent;
  border: 1px solid var(--border-strong);
  color: var(--text-secondary);
}

.unavailable {
  color: var(--text-muted);
  font-style: italic;
}

/* --- "Am I running this?": version check, verdict, commands --------------
 *
 * The check leads the tab and the verdicts follow it. The affected-products
 * tree that used to close the tab is gone: it restated the verdicts in raw
 * CPE ranges, and the tab ran to three screens for a two-product CVE.
 * ---------------------------------------------------------------------- */

#verdict-block {
  margin-bottom: 1.25rem;
}

.verdict-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.verdict-product + .verdict-product {
  margin-top: 0.85rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--border);
}

/* The answer to "am I affected", so it carries the weight of the band. */
.verdict-line {
  margin: 0;
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  color: var(--text-primary);
  line-height: 1.45;
}

.verdict-line + .verdict-line {
  margin-top: 0.2rem;
}

/* ---------------------------------------------------------------------------
 * One product, its release lines beneath it
 *
 * Six sentences that differed only in the branch became one heading and six
 * rows. The heading carries the product and the claim; each row is a branch
 * welded to its own fix, because the claim is per release line and always was
 * — a single "below 7.4.3" calls a patched 6.2.16 box vulnerable.
 *
 * The grid goes to two columns past four rows. Below that a two-column grid
 * of two rows reads as a mistake, and the whole point of the restructure is
 * that a two-product CVE fits on one screen.
 * ------------------------------------------------------------------------ */
.verdict-head {
  margin: 0 0 0.4rem;
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  color: var(--text-primary);
  line-height: 1.45;
}

.verdict-branches {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.15rem 1.5rem;
}

.verdict-branches[data-columns="2"] {
  grid-template-columns: repeat(2, max-content);
}

/* Single column below the two-column breakpoint: max-content keeps the rows
   from stretching across the card, so the arrows line up. */
@media (max-width: 34rem) {
  .verdict-branches[data-columns="2"] {
    grid-template-columns: max-content;
  }
}

.verdict-branch {
  display: flex;
  align-items: baseline;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  padding: 0.1rem 0.35rem;
  margin-left: -0.35rem;
  border-radius: var(--radius-sm);
}

.branch-line {
  font-family: var(--font-mono);
  color: var(--text-muted);
  /* The labels are short and near-identical in width (6.0.x, 7.2.x), so a
     fixed measure lines every arrow up without a second grid. */
  min-width: 4.5ch;
}

/* --text-muted, not --text-disabled. The arrow is the connective in "6.0.x →
   6.0.18" — it is in the copy-paste and it carries the claim, so it is text
   and owes 4.5:1. --text-disabled measures 3.5:1 on the tab panel and is
   documented as inactive controls only. */
.branch-arrow {
  color: var(--text-muted);
}

.branch-fix,
.branch-range {
  font-family: var(--font-mono);
  color: var(--text-primary);
  font-weight: var(--weight-medium);
}

.branch-range {
  font-family: inherit;
  font-weight: var(--weight-normal);
  color: var(--text-secondary);
}

/* The row a typed version landed on.
 *
 * Tinted with the accent rather than a severity colour, because it marks
 * where you are, not how bad it is — the finding is the version check's own
 * headline directly above. Additive only: an unmarked row means the row could
 * not be tested, never that the branch is clear.
 *
 * The tint alone is worth 1.09:1 on paper, which is not enough to find a row
 * by and is nothing at all to a screen reader, so the state is carried by the
 * "your version" marker in the DOM. This is what makes it scannable. The ring
 * is mixed from --accent-text rather than --accent because --accent-text is
 * the half of the accent pair that darkens on light: --accent-edge measured
 * 1.27:1 against the card and disappeared. At 65% the ring reads 3.24:1
 * against the card in light and 5.35:1 in dark. */
.verdict-branch[data-match="true"] {
  background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-text) 65%, transparent);
}

.verdict-branch[data-match="true"] .branch-line,
.verdict-branch[data-match="true"] .branch-arrow,
.verdict-branch[data-match="true"] .branch-fix {
  color: var(--accent-text);
}

/* The marker itself. Small caps weight rather than a pill, so it annotates
   the row instead of becoming a second thing to read on it. */
.branch-mark {
  margin-left: 0.5rem;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--accent-text);
  white-space: nowrap;
}

/* Muted and italic: it qualifies the lines above rather than competing with
   them, and it must not read as a second verdict. */
.verdict-caveat {
  margin: 0.35rem 0 0;
  font-size: var(--text-xs);
  font-style: italic;
  color: var(--text-muted);
  line-height: 1.5;
}

.verdict-command {
  margin-top: 0.45rem;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem;
}

.verdict-command-label {
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.verdict-command code {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  padding: 0.15rem 0.4rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-raised);
  color: var(--text-secondary);
  /* A long command scrolls inside its own box rather than widening the tab. */
  max-width: 100%;
  overflow-x: auto;
  white-space: pre;
}

.verdict-more {
  margin-top: 0.85rem;
}

.verdict-more summary {
  font-size: var(--text-sm);
  color: var(--accent-text);
  cursor: pointer;
}

.verdict-more summary:hover {
  color: var(--accent-text-hover);
}

.verdict-more .verdict-list {
  margin-top: 0.6rem;
}

.verdict-dropped {
  margin: 0.6rem 0 0;
  font-size: var(--text-xs);
  color: var(--text-muted);
  cursor: help;
}

/* --- Version check ------------------------------------------------------- */

#version-check {
  margin-bottom: 1.25rem;
  padding: 0.9rem 1.1rem;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

#version-label {
  display: block;
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  color: var(--text-secondary);
  margin-bottom: 0.5rem;
}

.version-row {
  display: flex;
  gap: 0.5rem;
  align-items: stretch;
}

#version-input {
  flex: 1;
  min-width: 0;
  padding: 0.5rem 0.7rem;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: 1.4;
  border: 1px solid var(--border-control);
  border-radius: var(--radius);
  background: var(--surface-page);
  color: var(--text-primary);
  transition: border-color var(--dur) var(--ease);
}

#version-input:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 1px;
}

#version-submit {
  padding: 0.5rem 0.9rem;
  font-size: var(--text-sm);
  /* Secondary to "Look up", which is the page's one primary action. */
  background: transparent;
  border: 1px solid var(--border-control);
  color: var(--text-primary);
}

#version-submit:hover {
  background: var(--surface-hover);
}

#version-result:not(:empty) {
  margin-top: 0.75rem;
}

.version-headline {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: 1.5;
  color: var(--text-primary);
}

/* A hit is a finding and is coloured like one. A miss is deliberately NOT
   coloured green or "low": it is an absence of evidence, and the palette must
   not say otherwise. It stays neutral, and the words carry the meaning. */
#version-result[data-hit="true"] .version-headline {
  color: var(--critical);
}

.version-action {
  margin: 0.3rem 0 0;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--text-primary);
}

.version-note {
  margin: 0.3rem 0 0;
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.version-checked {
  margin: 0.4rem 0 0;
  padding-left: 1.1rem;
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.version-checked li + li {
  margin-top: 0.2rem;
}

.version-checked-name {
  color: var(--text-secondary);
}

.version-checked-detail {
  font-family: var(--font-mono);
}

/* --- CVSS decode -------------------------------------------------------- */

#cvss-plain {
  margin-top: 0.75rem;
}

.cvss-plain-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.35rem;
}

/* max-content sizes the label column to the longest label rather than to a
   guessed 13rem, which is what left the dead gap between label and value. */
.cvss-plain-list li {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 1.25rem;
  align-items: baseline;
}

/* Each li is its own grid, so on its own max-content is measured per row and
   the values come out ragged. subgrid hands every row the parent's single
   label column, so they size to the longest label *and* stay aligned. */
@supports (grid-template-columns: subgrid) {
  .cvss-plain-list {
    grid-template-columns: max-content minmax(0, 1fr);
    column-gap: 1.25rem;
  }

  .cvss-plain-list li {
    grid-column: 1 / -1;
    grid-template-columns: subgrid;
  }
}

.metric-label {
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.metric-text {
  max-width: 60ch;
  color: var(--text-primary);
}

@media (max-width: 640px) {
  .cvss-plain-list li {
    grid-template-columns: minmax(0, 1fr);
    gap: 0.1rem;
  }
}

/* --- Verdict strip ------------------------------------------------------ */

/* ---------------------------------------------------------------------------
 * Bands
 *
 * The page answers two questions and used to interleave them. Now: urgency
 * first, as one bounded group — is anyone doing this, how close do they have
 * to get, what is required of me — then severity, then the description.
 *
 * The urgency band is a card because grouping is the whole point; everything
 * inside it is deliberately quieter than the verdict sentence, which is the
 * one thing on this page that should be read first.
 * ------------------------------------------------------------------------ */

#verdict {
  margin: 1rem 0 1.75rem;
  padding: 1rem 1.25rem 1.1rem;
  /* 4px, against the 3px every other card carries. It is the one rule on the
     page that means something, so it is the one rule that is thicker. */
  border-left-width: 4px;
}

/* ---------------------------------------------------------------------------
 * The verdict rule
 *
 * The conclusion, legible from across the room before a word is read. This is
 * the ONLY place the verdict tier drives a colour outside the chips — every
 * other rule on the page is the neutral --rule, which means "this is a
 * section", not "this is the answer".
 *
 * Set from app.js by verdictOf(), which returns the tier and the sentence
 * together so a rule cannot contradict the line beside it.
 *
 * There is no green, and there must not be one. "Low urgency" is still a real
 * vulnerability; green is the one colour a reader takes as "safe", which is a
 * claim about their estate and not one this page can make. Low and unknown
 * therefore share the neutral and separate on line style instead — dashed
 * meaning a lookup that did not answer, which is the vocabulary the chips
 * already use.
 *
 * Against the card and against the page either side of it: --critical 6.26 /
 * 5.09 light and 5.76 / 6.65 dark, --medium 6.39 / 5.20 and 4.93 / 5.70,
 * --rule 3.70 / 3.01 and 3.27 / 3.78. A 4px indicator owes 3:1.
 *
 * "Exploited" is 6px rather than 4px, and that is not decoration. Simulating
 * dichromacy (Viénot 1999) on the two signal colours, red against amber comes
 * out at an OKLab dE of 0.07 under protanopia in light and 0.09 in dark —
 * around two just-noticeable differences, and a reader never sees the two
 * bars side by side to compare. They are recognising one from memory, which
 * needs far more. Every other pair clears 0.10.
 *
 * Lightness cannot fix it in light mode: holding 3:1 against a near-white
 * page caps a signal colour at about L 0.585, and red already sits at 0.51,
 * so there is barely 0.07 of lightness to separate them with. Width is the
 * channel that survives every kind of colour blindness and the distance the
 * rule is meant to be read from, so the pair that matters most — drop
 * everything, versus schedule it — separates on that instead of on hue alone.
 * ------------------------------------------------------------------------ */
#verdict[data-tier="exploited"] {
  border-left-color: var(--critical);
  border-left-width: 6px;
}

#verdict[data-tier="likely"] {
  border-left-color: var(--medium);
}

#verdict[data-tier="low"] {
  border-left-color: var(--rule);
}

#verdict[data-tier="unknown"] {
  border-left-color: var(--rule);
  border-left-style: dashed;
}

/* Nothing below the sentence gets a gap wide enough to read as a new
   section — inside the card it is all one answer. */
#verdict > * + * {
  margin-top: 0.7rem;
}

.reach-note {
  margin: 0.35rem 0 0;
  max-width: 60ch;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* Tightened to the row above it: it qualifies the pill, so it must not read
   as a separate remark. Overrides the band's own child spacing. */
#verdict > .reach-note {
  margin-top: 0.35rem;
}

/* ---------------------------------------------------------------------------
 * The SOC line
 *
 * What, if anything, anyone other than the patching team does about this. It
 * is a routing note, not a finding, so it is small and muted and takes none
 * of the chips' emphasis — the verdict rule beside it is what carries
 * urgency, and a second coloured thing at that size would argue with it.
 *
 * The label does take a tone, and its construction is what keeps that safe.
 * It is outline-only with no fill, which is the QUIET tier: by construction a
 * tint can outshout it and a solid fill certainly can, so a red SOC label can
 * never come out louder than the "known exploited" chip below it however the
 * palette moves. Dashed is reserved, here as everywhere: it means the lookup
 * did not answer.
 *
 * The tones clear AA as text on both surfaces this line lands on — the
 * verdict card and, for the batch summary, the page itself. Worst case is
 * --medium in dark on the card at 4.93:1.
 * ------------------------------------------------------------------------ */
.soc-line {
  margin: 0;
  max-width: 60ch;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.soc-label {
  display: inline-block;
  margin-right: 0.15rem;
  padding: 0 0.3rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  letter-spacing: 0.04em;
  color: var(--text-secondary);
}

.soc-line[data-soc="hunt"] .soc-label {
  border-color: var(--critical);
  color: var(--critical);
}

.soc-line[data-soc="monitor"] .soc-label {
  border-color: var(--medium);
  color: var(--medium);
}

/* The one lookup that did not answer, and the one dashed border in this
   block. Nothing else here is allowed to be dashed. */
.soc-line[data-soc="unknown"] .soc-label {
  border-style: dashed;
}

/* Welded to the sentence above it. The verdict says how urgent this is and
   this says who acts on it — one answer in two lines, so the gap between
   them has to be smaller than the gap to the evidence underneath. */
#verdict > .verdict-line {
  margin-bottom: 0.2rem;
}

#verdict > .soc-line {
  margin-top: 0.2rem;
}

/* On the batch view it sits under the one-line summary, on the page rather
   than in a card, and answers the same second question. */
#batch-soc {
  margin-top: 0.4rem;
}

/* The second band, and back to a heading and whitespace after a spell as a
   card. The original reasoning was right and the intervening argument — that
   the odd one out reads as unfinished — was only true while it was the odd
   one out. With four boxes stacked, boxing this one put the severity in the
   same visual class as the verdict directly above it, which is exactly the
   ranking the page exists to invert. Whitespace says "different question" and
   costs no emphasis.

   Its contents now sit on --surface-page rather than on a panel. Text holds
   AA either way — --text-muted is the weakest thing in here and measures
   5.79:1 light, 6.29:1 dark on the page — but a tint is mixed over its
   ground, so the badge is told which ground it is on. */
#cvss-block {
  margin: 0 0 1.75rem;
  --sev-ground: var(--surface-page);
}

.verdict-line {
  margin: 0 0 0.6rem;
  font-size: var(--text-md);
  font-weight: var(--weight-medium);
  max-width: 60ch;
  color: var(--text-primary);
}

/* Plain-English explainer. Sits between the verdict and the chips, closed by
   default: it is the most important thing on the page for one reader and
   noise for another, so it costs one line until asked for. */
.explainer {
  /* #verdict > * + * owns the spacing between band children now that this
     sits inside the run rather than at the end of it. */
  margin: 0;
  max-width: 70ch;
}

/* A button, not a caret link.
 *
 * It sits under the verdict sentence and above the chips, and it is the only
 * thing in that band anyone can click — so it has to look like it. As a small
 * caret it read as a footnote on the chips below it and went unclicked.
 *
 * Tinted and outlined rather than filled: a solid accent button here would
 * compete with "Look up" directly above it, which is the page's one primary
 * action. This is the primary thing to click in the BAND, not on the page.
 *
 * inline-flex also drops the default disclosure triangle, so the marker below
 * is drawn explicitly. */
.explainer summary {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.45rem 0.9rem;
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  color: var(--accent-text);
  background: var(--accent-soft);
  border: 1px solid var(--accent-edge);
  border-radius: var(--radius);
  cursor: pointer;
  list-style: none;
  transition: color var(--dur) var(--ease),
    background-color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

.explainer summary::-webkit-details-marker {
  display: none;
}

.explainer summary::before {
  content: "▸";
  font-size: var(--text-xs);
  line-height: 1;
  transition: transform var(--dur) var(--ease);
}

.explainer[open] summary::before {
  transform: rotate(90deg);
}

.explainer summary:hover {
  color: var(--accent-text-hover);
  background: color-mix(in srgb, var(--accent) 20%, transparent);
  border-color: var(--accent);
}

.explainer summary:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* Open, the button has done its job and the callout below is the thing being
   read. It stays clearly clickable, but stops shouting over its own content. */
.explainer[open] summary {
  background: transparent;
}

/* A callout, not another panel: it carries an accent edge and a tinted
   ground so it reads as the highlighted thing on the page rather than as
   one more grey box among the cards. */
.explainer-body {
  margin-top: 0.6rem;
  padding: 0.9rem 1.1rem;
  background: color-mix(in srgb, var(--accent) 10%, var(--surface-raised));
  border: 1px solid var(--accent-edge);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
}

.explainer-body h4 {
  margin: 0 0 0.3rem;
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  /* Not --text-muted: on the tinted callout ground it measured 3.85:1. */
  color: var(--text-secondary);
}

.explainer-body h4 + p {
  margin: 0 0 1rem;
  color: var(--text-primary);
}

.explainer-body h4 + p:last-child,
.explainer-body p:last-child {
  margin-bottom: 0;
}

/* Stands on its own in the verdict band now, rather than inside the callout,
   so it carries its own width and reads as the quiet note it is. */
.explainer-none {
  margin: 0;
  max-width: 70ch;
  font-size: var(--text-sm);
  font-style: italic;
  color: var(--text-muted);
}

.chips {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
}

/* Status, not interaction: tinted from the semantic set so a chip can never
   be mistaken for something you can click. */
/* Chips are information, not the headline.
 *
 * --chip-tone carries the hue for the fill and border; --chip-fg is the label,
 * blended halfway to --text-secondary. At full saturation the red and amber
 * chips were louder than every interactive element on the page — chroma 138
 * and 154 against the accent's 112 — which inverted the hierarchy the accent
 * is supposed to establish. Blending drops them to 64 and 65: the hue still
 * identifies the status, but the violet is once again the loudest thing. */
/* ---------------------------------------------------------------------------
 * Chips, in three weights
 *
 * data-tone carries the hue — which claim this is. data-weight carries the
 * emphasis — how much it should interrupt you. They are separate because the
 * same hue appears at two weights: "public exploit code exists" and "US
 * federal fix deadline" are both --high, and only one of them is something
 * being done to you right now.
 *
 * The default is QUIET, on purpose. A chip that arrives without a weight is a
 * chip nobody has ranked, and the failure that costs least is the one that
 * does not shout.
 * ------------------------------------------------------------------------ */
.chip {
  --chip-tone: var(--tone-unknown);
  --chip-loud: var(--loud-alarm);
  --chip-fg: color-mix(in srgb, var(--chip-tone) 50%, var(--text-secondary));
  padding: 0.25rem 0.6rem;
  border-radius: 999px;
  font-size: var(--text-sm);
  border: 1px solid color-mix(in srgb, var(--text-muted) 55%, transparent);
  background: transparent;
  color: var(--text-muted);
  transition: background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

.chip[data-tone="alarm"] { --chip-tone: var(--tone-alarm); --chip-loud: var(--loud-alarm); }
.chip[data-tone="warn"] { --chip-tone: var(--tone-warn); --chip-loud: var(--loud-warn); }
.chip[data-tone="quiet"] { --chip-tone: var(--tone-quiet); }

/* An answer we did not get. Dashed, because "couldn't check" and "not listed"
   have to stay apart all the way to the UI, and at this weight there is no
   fill left to tell them apart with — the border is the only thing either one
   has. Not colour: this reads on a monochrome screen and to anyone who cannot
   separate two muted neutrals. */
.chip[data-tone="unknown"] {
  --chip-tone: var(--tone-unknown);
  border-style: dashed;
}

/* MEDIUM — a real tint and a border at full tone. 4.54:1 border on the fill,
   5.88:1 label on it. */
.chip[data-weight="medium"] {
  background: color-mix(in srgb, var(--chip-tone) 20%, var(--surface-panel));
  border-color: var(--chip-tone);
  border-style: solid;
  color: var(--chip-fg);
}

/* LOUD — solid, the most saturated thing the sRGB gamut holds at a lightness
   that still carries its label at AA. Last, so an explicit weight wins over
   the tone rules above whatever the tone is. */
.chip[data-weight="loud"] {
  background: var(--chip-loud);
  border-color: var(--chip-loud);
  border-style: solid;
  color: var(--loud-fg);
  font-weight: var(--weight-semi);
}

/* The citation on a chip that has one. Underlined and inheriting the chip's
   label colour rather than taking the accent: inside a tinted chip the gold
   would fight the tone, and the underline is what says "link" here — the same
   reasoning as .related-id, where colour alone could not carry it either. */
.chip-link {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
  text-underline-offset: 2px;
}

.chip-link:hover,
.chip-link:active {
  color: inherit;
  text-decoration-color: currentColor;
}

.chip[data-weight="medium"]:hover {
  background: color-mix(in srgb, var(--chip-tone) 28%, var(--surface-panel));
}

.chip[data-weight="quiet"]:hover,
.chip:not([data-weight]):hover {
  border-color: color-mix(in srgb, var(--text-muted) 80%, transparent);
}

/* Reachability bubble. Deliberately not a .chip: the chips are verdicts about
 * this CVE's exploitation status, and this is a neutral description of how
 * close an attacker has to get. The dotted underline and help cursor mark it
 * as carrying a caveat worth reading. */
.bubble {
  position: relative; /* the anchor for .reach-scale */
  cursor: help;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.25rem 0.7rem;
  border-radius: 999px;
  font-size: var(--text-sm);
  /* Medium weight, and neutral: how close an attacker has to get is a
     description, not a verdict, so it takes no semantic hue. The border went
     from dashed to solid because dashed now means one thing only — a lookup
     that did not answer. What marks this as carrying a caveat is the dotted
     underline on the label and cursor:help, which it already had. */
  background: color-mix(in srgb, var(--neutral) 20%, var(--surface-panel));
  border: 1px solid var(--neutral);
  color: var(--text-secondary);
  transition: border-color var(--dur) var(--ease),
    background-color var(--dur) var(--ease);
}

/* No hover fill any more. The pill isn't clickable, and a background that
   lights up on hover is the vocabulary of something that is — it was
   promising an interaction that never existed. What hover does now is show
   the scale; cursor:help and the dotted underline below are what say so. */
.bubble:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* Direct child only. The scale lives inside the pill now, and a descendant
   selector put this dotted underline on all four of its rows. */
.bubble > span {
  border-bottom: 1px dotted var(--text-disabled);
}

.bubble::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--neutral);
  flex: none;
}

/* Reachability is a property of the CVE, so it reads on the semantic scale,
   not the accent. */
.bubble[data-reach="n"]::before { background: var(--critical); }
.bubble[data-reach="a"]::before { background: var(--medium); }
.bubble[data-reach="l"]::before { background: var(--low); }
.bubble[data-reach="p"]::before { background: var(--neutral); }

/* margin-left:auto pushes it to the right of the chip row; on a narrow screen
 * the row wraps and that auto margin would leave a lone bubble stranded. */
@media (max-width: 40rem) {
  .bubble {
    margin-left: 0;
  }
}

/* --- The reachability scale ----------------------------------------------
 *
 * Four rows, most exposed at the top, the CVE's own row marked. The order is
 * the information: a position in a list says how bad "same network segment"
 * is in a way the phrase on its own cannot.
 * ---------------------------------------------------------------------- */

.reach-scale {
  position: absolute;
  top: calc(100% + 0.5rem);
  z-index: 20;
  display: block;
  width: max-content;
  max-width: min(26rem, calc(100vw - 2rem));
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  /* One step lighter than the pill it hangs off, because on a dark ground
     raised reads as lighter. The shadow is written out rather than tokenised:
     this is the only overlay in the app, and the design system otherwise
     separates things with borders, not shadows. */
  background: var(--surface-overlay);
  box-shadow: 0 8px 24px var(--shadow-overlay);
  cursor: default;
}

/* Whichever edge keeps it on screen; attachTooltip measures and sets this,
   because the pill is the last item of a wrapping row and can end up
   anywhere across the width. */
.reach-scale[data-align="left"] { left: 0; }
.reach-scale[data-align="right"] { right: 0; }

.reach-scale ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.3rem;
}

.reach-scale li {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* The same dot and the same semantic colours as the pill, so the row a reader
   is looking for is the one wearing the colour they just saw. */
.reach-scale li::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--neutral);
  flex: none;
  transform: translateY(-0.1em);
}

.reach-scale li[data-reach="n"]::before { background: var(--critical); }
.reach-scale li[data-reach="a"]::before { background: var(--medium); }
.reach-scale li[data-reach="l"]::before { background: var(--low); }
.reach-scale li[data-reach="p"]::before { background: var(--neutral); }

/* The other three are context. This one is the answer. */
.reach-scale li[data-current="true"] {
  color: var(--text-primary);
  font-weight: var(--weight-medium);
}

.reach-scale-mark {
  margin-left: auto;
  padding-left: 0.75rem;
  font-size: var(--text-xs);
  font-weight: var(--weight-normal);
  color: var(--text-muted);
  white-space: nowrap;
}

@media (prefers-reduced-motion: no-preference) {
  .reach-scale {
    animation: tooltip-in 120ms var(--ease) both;
  }
}

@keyframes tooltip-in {
  from { opacity: 0; transform: translateY(-2px); }
  to { opacity: 1; transform: none; }
}

/* --- Products the vendor named, and fixes only the vendor states ---------
 *
 * NVD's analysed CPE data is the norm, so only the exceptions are marked.
 * Both of these read as annotations on a verdict, not as another product.
 * They used to annotate a row in the affected-products tree; that tree is
 * gone and the two supplements it carried now arrive as verdict items, so
 * these moved with them rather than being deleted alongside it.
 * ---------------------------------------------------------------------- */

/* Sits inline at the end of the verdict heading it qualifies, so "from the
   vendor" reads as a note on that product's line and not as a chip in its
   own right. */
.product-source {
  margin-left: 0.4rem;
  padding: 0.05rem 0.4rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: var(--text-xs);
  font-weight: var(--weight-normal);
  color: var(--text-muted);
  white-space: nowrap;
  cursor: help;
}

/* Its own line: it is the answer to "what do I upgrade to", and inline after
   fifty enumerated affected versions is where it would never be read. */
.product-vendor-versions {
  display: block;
  margin-top: 0.2rem;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  cursor: help;
}

.related-year {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.kev-action {
  margin: 0.6rem 0 0;
  font-size: var(--text-sm);
  max-width: 65ch;
}

.kev-action summary {
  cursor: pointer;
  color: var(--text-muted);
  border-radius: var(--radius-sm);
  transition: color var(--dur) var(--ease);
}

.kev-action summary:hover {
  color: var(--accent-text);
}

.kev-action p {
  margin: 0.4rem 0 0;
  color: var(--text-secondary);
}

/* ---------------------------------------------------------------------------
 * Two-column results
 *
 * Main column carries the answer, side column carries the neighbours. Both
 * use the full window; the ~70ch reading measure is applied to the prose
 * rather than to the column, so the panels get the room they need without
 * the body copy becoming a wall of text.
 * ------------------------------------------------------------------------ */

/* Both tracks are minmax(0, …): a min-content floor on either one lets the
   grid compute wider than its container and push the left column off-screen.
   The side column gets a preferred width via clamp instead of a hard floor,
   so it shrinks with the viewport rather than forcing overflow. */
.layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, clamp(17rem, 28%, 24rem));
  gap: clamp(1.25rem, 3vw, 3rem);
  align-items: start;
}

/* The column itself is free to fill the track — tables, product trees and
   CPE lists want the room. The ~70ch measure is applied to the prose
   inside it instead, so body copy never becomes a wall of text. */
.col-main {
  min-width: 0;
}

.verdict-line,
.verdict-head,
.kev-action,
#result-description,
.source-gives,
.metric-text {
  max-width: 70ch;
}

/* A card, not a column with a rule down its side.
 *
 * As a bare column it floated on the page background with nothing bounding
 * it, so once the list ended the track below read as a void rather than as
 * the end of a panel. It takes the shared card treatment above.
 *
 * It was already sticky and already worked — it pins at 1.5rem — but a 440px
 * panel beside a 2900px main column leaves empty track no amount of sticky
 * can fill. Bounding it is what makes that space read as deliberate. */
.col-side {
  min-width: 0;
  position: sticky;
  top: 1.5rem;
  max-height: calc(100vh - 3rem);
  overflow-y: auto;
}

@media (max-width: 1024px) {
  .layout {
    grid-template-columns: minmax(0, 1fr);
    gap: 2rem;
  }

  /* Stacked: no sticky and no scroll box. It keeps the card treatment, which
     is what separates it from the main column now that they share one track. */
  .col-side {
    position: static;
    max-height: none;
    overflow-y: visible;
  }
}

/* --- Tabs --------------------------------------------------------------- */

.tabs {
  margin-top: 2rem;
}

/* No rule under the tabs any more. The panel below is a card with its own
   top edge, and a hairline sitting 4px above a 1px border was two lines doing
   one job. The tabs read as controls above the card, which is what they
   are — the selected one is already filled and ringed, so nothing depended on
   it merging into the panel. */
[role="tablist"] {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin-bottom: 0.4rem;
}

[role="tab"] {
  padding: 0.55rem 0.9rem;
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  background: none;
  color: var(--text-muted);
  border: none;
  border-radius: var(--radius);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease),
    color var(--dur) var(--ease);
}

/* A real surface change, not just a colour shift — the tab should feel like
   it lights up under the pointer. It lands on --surface-raised, which is a
   step the eye reads as a panel rather than as a tint. */
[role="tab"]:hover:not([aria-selected="true"]) {
  background: var(--surface-raised);
  color: var(--text-primary);
  box-shadow: inset 0 0 0 1px var(--border-strong);
}

/* Filled and accented, so a hovered inactive tab can never be mistaken for
   the selected one: hover is a neutral grey panel, selected is violet. */
[role="tab"][aria-selected="true"] {
  background: var(--accent-soft);
  color: var(--accent-text);
  box-shadow: inset 0 0 0 1px var(--accent-edge);
}

/* Box comes from the shared card rule; this is only the padding, which is a
   touch deeper than the default because a tab panel holds a whole screen of
   content rather than a paragraph. */
[role="tabpanel"] {
  padding: 1.25rem;
}

/* --- "How it works" ----------------------------------------------------- */

.field-list {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 0.35rem 1.25rem;
  margin: 0 0 1.5rem;
  padding: 0.85rem 1rem;
  font-size: var(--text-sm);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.field-list dt {
  color: var(--text-muted);
}

.field-list dd {
  margin: 0;
  overflow-wrap: anywhere;
  color: var(--text-secondary);
}

.field-list code {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  overflow-wrap: anywhere;
  color: var(--text-secondary);
}

/* --- "How it works" ------------------------------------------------------
 *
 * Prose, not a field table. The section headings read as headings rather than
 * as the muted field labels they used to sit among, because there is now
 * something under them worth reading.
 * ---------------------------------------------------------------------- */

#mechanics .mech-head {
  font-size: var(--text-md);
  font-weight: var(--weight-semi);
  color: var(--text-primary);
  margin: 0 0 0.5rem;
}

#mechanics .mech-head:not(:first-child) {
  margin-top: 1.6rem;
}

.mech-what {
  margin: 0;
  max-width: 68ch;
  color: var(--text-secondary);
}

/* Numbered, and the numbers matter: each step is what makes the next one
   possible. That ordering is the thing the CVSS decode above the tabs cannot
   express, and the whole reason this section is a list and not a paragraph. */
.mech-steps {
  margin: 0.8rem 0 0;
  padding: 0;
  list-style: none;
  counter-reset: mech;
  display: grid;
  gap: 0.5rem;
  max-width: 68ch;
}

.mech-steps li {
  counter-increment: mech;
  display: grid;
  grid-template-columns: 1.5rem 1fr;
  gap: 0.6rem;
  align-items: baseline;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.mech-steps li::before {
  content: counter(mech);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--accent-text);
  text-align: right;
}

.mech-source {
  margin: 0.8rem 0 0;
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.mech-score-lead {
  margin: 0;
  max-width: 68ch;
  color: var(--text-secondary);
}

.mech-drivers {
  list-style: none;
  margin: 0.7rem 0 0;
  padding: 0;
  display: grid;
  gap: 0.5rem;
  max-width: 68ch;
}

.mech-drivers li {
  display: grid;
  grid-template-columns: 8rem 1fr;
  gap: 0.6rem;
  align-items: baseline;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

@media (max-width: 40rem) {
  .mech-drivers li {
    grid-template-columns: 1fr;
    gap: 0.15rem;
  }
}

.mech-drivers li[data-direction="none"] {
  grid-template-columns: 1fr;
  font-style: italic;
  color: var(--text-muted);
}

.mech-driver-label {
  font-weight: var(--weight-medium);
  color: var(--text-primary);
}

/* The two directions read as opposites at a glance — the same semantic pair
   the dots and chips already use, so no new colour vocabulary. */
.mech-drivers li[data-direction="up"] .mech-driver-label {
  color: var(--critical);
}

.mech-drivers li[data-direction="down"] .mech-driver-label {
  color: var(--low);
}

/* Last, collapsed, and quiet. The values behind everything above, for anyone
   checking the working. */
.raw-fields {
  margin-top: 1.8rem;
  border-top: 1px solid var(--border);
  padding-top: 0.9rem;
}

.raw-fields summary {
  font-size: var(--text-sm);
  color: var(--text-muted);
  cursor: pointer;
  list-style: none;
}

.raw-fields summary::-webkit-details-marker {
  display: none;
}

.raw-fields summary::before {
  content: "▸ ";
  font-size: var(--text-xs);
  color: var(--text-disabled);
}

.raw-fields[open] summary::before {
  content: "▾ ";
}

.raw-fields summary:hover {
  color: var(--text-secondary);
}

.raw-fields .field-list {
  margin-top: 0.8rem;
}

/* --- "Where this comes from" -------------------------------------------- */

.source-row {
  padding: 0.75rem 0;
  border-top: 1px solid var(--border);
}

.source-row:first-child {
  border-top: none;
  padding-top: 0;
}

.source-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
  margin: 0 0 0.25rem;
}

.source-name {
  font-weight: var(--weight-semi);
  color: var(--text-primary);
}

.source-host {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.source-gives {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
  max-width: 65ch;
}

/* A choice this page made about THIS CVE, as opposed to what the source does
   in general. Indented under the row's own description and given the rule
   down its side so it reads as a footnote on that source, not a new row. */
.source-choice {
  margin: 0.4rem 0 0;
  padding-left: 0.7rem;
  max-width: 70ch;
  border-left: 2px solid var(--rule);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.source-note {
  margin: 1rem 0 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* --- Related CVEs ------------------------------------------------------- */

.side-head {
  font-size: var(--text-base);
  letter-spacing: 0;
  font-weight: var(--weight-semi);
  color: var(--text-primary);
  margin: 0 0 0.35rem;
}

#related-summary {
  margin: 0 0 0.75rem;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* The batch summary is the same kind of line as the related one — what this
   list is and how it is ordered — so it takes the same treatment rather than
   a second style saying the same thing. */
#batch-panel-summary {
  margin: 0 0 0.75rem;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* --- The side panel's two lists ------------------------------------------

   A tablist, shown only while a triage session is stored. It replaces the
   heading rather than sitting under it, because with two panels a heading
   naming one of them is wrong.

   Underline-marked rather than boxed: this is a switch between two lists in a
   340px column, and a pair of filled tabs there reads as heavier than the
   verdict card it sits beside. The selected tab is carried by weight, colour
   AND the rule beneath it, so it is never colour alone. */
.side-tabs {
  display: flex;
  gap: 0.15rem;
  margin: 0 0 0.6rem;
  border-bottom: 1px solid var(--border);
}

/* These carry role=tab, so the main tab strip's rules reach them and have to
   be turned off one by one — that strip is filled, rounded and full-size,
   which is right for switching a whole screen of content and wrong for
   switching a list inside a 340px column. The selectors below are one class
   more specific than the generic ones, which is what makes them win. */
.side-tabs .side-tab {
  padding: 0.3rem 0.5rem;
  border: 0;
  border-radius: 0;
  /* Sits on top of the container's own rule, so the selected tab's marker
     covers it rather than doubling it. */
  margin-bottom: -1px;
  border-bottom: 2px solid transparent;
  background: none;
  box-shadow: none;
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--text-muted);
  cursor: pointer;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

.side-tabs .side-tab:hover {
  background: none;
  box-shadow: none;
  color: var(--text-primary);
}

/* Underline, not a fill. A solid accent here would be the LOUD construction,
   which the emphasis tiers reserve for what someone is actually doing to a
   CVE — and it would sit directly above a column of KEV chips and outshout
   them. Weight and colour carry the state as well as the rule beneath it, so
   it is never colour alone. */
.side-tabs .side-tab[aria-selected="true"] {
  background: none;
  box-shadow: none;
  color: var(--text-primary);
  font-weight: var(--weight-semi);
  border-bottom-color: var(--accent-text);
}

/* The side panels are NOT cards. The shared card rule and the tab-panel
   padding both match [role="tabpanel"], which would put a bordered, padded
   box inside .col-side — a card within a card, which is exactly the
   "separation without hierarchy" the card doctrine above rules out. The
   column is the unit here; what is inside it is a list. */
.col-side [role="tabpanel"] {
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

/* The row you are looking at. Marked, never removed — position within the set
   is the whole point of this list, and dropping the current row is exactly
   the information a reader came here for. Not colour alone: the word
   "viewing" is in the DOM and aria-current is on the row. */
.related-row[data-current] {
  background: var(--surface-raised);
  border-radius: var(--radius-sm);
  margin-inline: -0.4rem;
  padding-inline: 0.4rem;
  box-shadow: inset 3px 0 0 var(--accent-text);
}

/* Not a button: this is the page you are already on, so it is neither
   focusable nor clickable. Styled as text at the same size and weight so the
   column of IDs stays aligned.

   It refuses to shrink, unlike the ID buttons around it. Those may wrap when
   the row is tight; this row is tighter than any other because it carries the
   "viewing" marker as well, and letting it shrink broke the one ID the reader
   is looking for across three lines. The marker wraps instead — it is the
   part that can afford to. */
.related-id-current {
  flex: 0 1 auto;
  min-width: 0;
  padding: 0.35rem 0;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  color: var(--text-primary);
}

/* Sits with the flaw type and the year, and is separated from them by the
   same middot the meta line already uses between its own items. */
.batch-here {
  font-size: var(--text-xs);
  color: var(--accent-text);
  font-weight: var(--weight-semi);
  text-transform: lowercase;
}

#related-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--border);
}

.related-row {
  border-bottom: 1px solid var(--border);
  border-radius: var(--radius-sm);
  transition: background-color var(--dur) var(--ease);
}

/* The whole row lights up, not just the link — at 3am the target is the row.
   The row sits on --surface-panel now, so hover steps up from it. */
.related-row:hover {
  background: var(--surface-raised);
}

/* Six fields in a ~340px column. Two fixed lines — identity and score on top,
   metadata beneath — rather than one wrapping line that broke in a different
   place on every row and made the list unscannable. */
.related-head {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.35rem 0.4rem;
}

.related-head > .dot {
  margin-top: 0.45rem;
}

.related-body {
  flex: 1;
  min-width: 0;
}

.related-line {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.related-meta {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  font-size: var(--text-xs);
  color: var(--text-muted);
}

/* Separates the two metadata fields without spending a character on a bullet
   when only one of them is present. */
.related-meta > * + *::before {
  content: "·";
  margin-right: 0.4rem;
  color: var(--text-disabled);
}

.related-flaw {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The honest fallbacks read as weaker than a real classification, so a
   CWE number is never mistaken for a plain-English label. */
/* --text-muted, not --text-disabled: these carry information the reader is
   meant to read. --text-disabled is for inactive controls and measured
   3.75:1 here, under AA. The weaker treatment comes from the mono face and
   the smaller size instead of from the colour. */
.related-flaw[data-kind="named"],
.related-flaw[data-kind="placeholder"],
.related-flaw[data-kind="unnamed"],
.related-flaw[data-kind="none"] {
  color: var(--text-muted);
  cursor: help;
}

/* Italic for the three that name no weakness — NVD having nothing to record,
   an id newer than our catalogue, and nothing recorded at all. A real name
   from MITRE is not one of those: it is a classification, just not ours. */
.related-flaw[data-kind="placeholder"],
.related-flaw[data-kind="unnamed"],
.related-flaw[data-kind="none"] {
  font-style: italic;
}

/* EPSS is the sort key, so it reads as a number rather than as another label:
   tabular figures keep the column scannable down the list. */
.related-epss-value {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
  white-space: nowrap;
}

.related-epss-value[data-unscored] {
  color: var(--text-disabled);
}

.related-reach {
  margin: 0.35rem 0 0;
  font-size: var(--text-xs);
  color: var(--text-muted);
  cursor: help;
}

/* --- Older known-exploited entries --- */

.older-kev {
  margin-top: 0.75rem;
  border-top: 1px solid var(--border);
  padding-top: 0.6rem;
}

.older-kev summary {
  cursor: pointer;
  font-size: var(--text-sm);
  color: var(--text-muted);
  border-radius: var(--radius-sm);
  transition: color var(--dur) var(--ease);
}

.older-kev summary:hover {
  color: var(--accent-text);
}

.older-kev-list {
  list-style: none;
  margin: 0.4rem 0 0;
  padding: 0;
}

.dot {
  flex: none;
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 50%;
  background: var(--neutral);
}

.dot[data-severity="critical"] { background: var(--critical); }
.dot[data-severity="high"] { background: var(--high); }
.dot[data-severity="medium"] { background: var(--medium); }
.dot[data-severity="low"] { background: var(--low); }

.related-id {
  flex: 0 1 auto;
  min-width: 0;
  padding: 0.35rem 0;
  text-align: left;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  /* Weight and a standing underline, not just colour. These are the only
     navigation on the page and they read as body text without them — and in
     light mode colour cannot fix that on its own, because AA on the page
     ground caps gold at C 0.11 (see --accent-text). The underline is drawn at
     40% so a column of ten IDs does not turn into a fence; hover takes it to
     full strength. */
  font-weight: var(--weight-medium);
  background: none;
  color: var(--accent-text);
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--accent-text) 40%, transparent);
  text-underline-offset: 2px;
  transition: color var(--dur) var(--ease),
    text-decoration-color var(--dur) var(--ease);
}

.related-id:hover {
  background: none;
  color: var(--accent-text-hover);
  text-decoration-color: currentColor;
}

.chip-sm {
  padding: 0.1rem 0.4rem;
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  letter-spacing: 0.03em;
}

.related-toggle {
  flex: none;
  align-self: center;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  background: none;
  color: var(--text-muted);
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease),
    color var(--dur) var(--ease);
}

.related-toggle:hover {
  background: var(--surface-raised);
  color: var(--text-primary);
}

.related-toggle::before {
  content: "▾";
  display: block;
  font-size: var(--text-sm);
  line-height: 1;
  transition: transform var(--dur) var(--ease);
}

.related-toggle[aria-expanded="true"]::before {
  transform: rotate(180deg);
}

.related-detail {
  padding: 0 0.4rem 0.75rem 1.5rem;
}

.related-summary-text {
  margin: 0 0 0.4rem;
  font-size: var(--text-sm);
  color: var(--text-primary);
}

.related-epss {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--text-muted);
}

/* ---------------------------------------------------------------------------
 * "What do I search for?"
 *
 * Full width of the main column. Queries soft-wrap rather than scroll: a
 * clause that ORs a dozen values together runs wide in every one of the four
 * languages, and a horizontal drag inside a code block is unreadable.
 * ------------------------------------------------------------------------ */

#detect {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

.detect-head {
  margin: 0 0 0.5rem;
  font-size: var(--text-base);
  font-weight: var(--weight-semi);
  color: var(--text-primary);
}

/* Stated before anything actionable, so it can't be scrolled past. */
.detect-caveat {
  margin: 0;
  padding: 0.6rem 0.75rem;
  max-width: 70ch;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  border-left: 3px solid var(--medium);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: color-mix(in srgb, var(--medium) 10%, var(--surface-raised));
}

.detect-cwe,
.detect-note {
  margin: 0;
  max-width: 70ch;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.detect-note {
  color: var(--text-muted);
}

.detect-exposure p {
  margin: 0;
  max-width: 70ch;
  color: var(--text-primary);
}

.detect-scope {
  margin: 0;
  max-width: 70ch;
  padding: 0.6rem 0.75rem;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  /* --rule, not --border-strong: this is the same 3px left-rule shape the
     cards use, and --border-strong is now the card EDGE colour, which reads
     at 1.62:1 on the surface this block sits on. */
  border-left: 3px solid var(--rule);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}

/* Fix links sit with the verdict, where someone acting on it will look. */
/* Two runs, stacked: fixes then workarounds. They come from different
   upstreams and mean different things, so they get separate labelled lines
   rather than one merged list of links. */
.fix-block > * + * {
  margin-top: 0.3rem;
}

/* A workaround is not a fix and must not read as one. Quieter than the fix
   run above it — same shape, one step down in weight. */
.fix-mitigations {
  font-size: var(--text-sm);
}

.fix-mitigations .fix-label {
  color: var(--text-muted);
}

.fix-links {
  margin: 0 0 0.85rem;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
  font-size: var(--text-sm);
}

.fix-label {
  font-weight: var(--weight-semi);
  color: var(--text-muted);
}

.fix-links a {
  color: var(--accent-text);
  text-underline-offset: 2px;
  transition: color var(--dur) var(--ease);
}

.fix-links a:hover {
  color: var(--accent-text-hover);
}

/* The overflow toggle on a link run.
   Red Hat files one erratum per affected product, so a CVE can carry 38 fix
   links on one host. Two sit on the line and the rest go behind this — the
   same trade the fix panel makes for release lines and vendor editions, at
   the smallest scale the page has. Styled off .fix-branches deliberately:
   a reader who has learned one of these toggles has learned all of them. */
.link-more {
  font-size: var(--text-xs);
}

.link-more summary {
  cursor: pointer;
  color: var(--text-muted);
  border-radius: var(--radius-sm);
  transition: color var(--dur) var(--ease);
}

.link-more summary:hover {
  color: var(--accent-text);
}

/* Column, not the parent's wrapped row: past two links the labels are
   advisory ids that differ only in their last digits, and reading them side
   by side is what made twenty of them unscannable in the first place. */
.link-more-list {
  margin: 0.35rem 0 0;
  padding-left: 1rem;
  border-left: 2px solid var(--border);
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

/* What the server cut. Said out loud, never a silent truncation. */
.link-capped {
  color: var(--text-muted);
  font-size: var(--text-xs);
}

.lookfor-list {
  margin: 0;
  padding: 0.85rem 1rem;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.lookfor-list li {
  display: grid;
  grid-template-columns: minmax(9rem, 12rem) minmax(0, 1fr);
  gap: 1.25rem;
  align-items: baseline;
}

.lookfor-source {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  letter-spacing: 0;
  color: var(--text-muted);
}

.lookfor-text {
  max-width: 70ch;
  font-size: var(--text-sm);
  color: var(--text-primary);
}

.query {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding-top: 1rem;
  border-top: 1px solid var(--border);
}

.query-head {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.query-head h5 {
  margin: 0;
  font-size: var(--text-base);
  font-weight: var(--weight-semi);
  color: var(--text-primary);
}

.query-source {
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: 0;
  color: var(--text-muted);
}

.query-why,
.query-note {
  margin: 0;
  max-width: 70ch;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.query-lang {
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  transition: border-color var(--dur) var(--ease);
}

.query-lang:hover {
  border-color: var(--border-strong);
}

.query-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.35rem 0.5rem;
  background: var(--surface-raised);
  border-bottom: 1px solid var(--border);
}

.query-lang-label {
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--text-muted);
}

.copy-btn {
  flex: none;
  padding: 0.2rem 0.6rem;
  font-family: inherit;
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--accent-text);
  background: none;
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.copy-btn:hover {
  background: var(--accent-soft);
  border-color: var(--accent-edge);
  color: var(--accent-text-hover);
}

/* The code body is one step further from the card than the bar above it, so
   the two still separate now that the bar has taken --surface-raised. That
   step is --surface-hover, which is the fourth surface on the ramp and not
   only a hover state — it is darker than raised on paper and lighter than it
   on a dark ground, which is what "further in" means in each theme. */
.query-code {
  margin: 0;
  padding: 0.6rem 0.75rem;
  background: var(--surface-hover);
  font-size: var(--text-sm);
  line-height: 1.6;
  tab-size: 2;
}

/* Soft-wrap with a hanging indent: the table name sits flush and every
   pipe clause is inset, so a wrapped line can't be mistaken for a new
   clause. `each-line` does this per source line where supported; without
   it the same padding/indent pair still insets the clause list. */
.query-code code {
  display: block;
  font-family: var(--font-mono);
  color: var(--text-secondary);
  white-space: pre-wrap;
  overflow-wrap: break-word;
  padding-left: 1.75rem;
  text-indent: -1.75rem;
}

@supports (text-indent: -1.75rem each-line) {
  .query-code code {
    text-indent: -1.75rem each-line;
  }
}

.detect-assumptions ul {
  margin: 0;
  padding: 0.85rem 1rem 0.85rem 2.1rem;
  max-width: 70ch;
  font-size: var(--text-sm);
  color: var(--text-muted);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.detect-assumptions li + li {
  margin-top: 0.35rem;
}

/* ---------------------------------------------------------------------------
 * Batch triage
 *
 * One screen answering one question: of the eleven CVEs in this bulletin,
 * which ones are anyone actually exploiting? So it is a table, not a stack of
 * cards — the whole value is being able to run an eye down two columns and
 * stop at the top two rows.
 *
 * The severity dot is first because it is the only thing here that is a
 * colour, and last in importance: the ordering is the argument, and the dot
 * is context for a row you have already been sent to by the sort.
 * ------------------------------------------------------------------------ */

#batch {
  margin-top: 2rem;
}

.batch-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}

#batch-heading {
  margin: 0;
}

#batch-summary {
  max-width: 70ch;
  margin: 0.5rem 0 0;
  color: var(--text-secondary);
}

#batch-notes:not(:empty) {
  margin-top: 0.75rem;
}

#batch-notes p {
  max-width: 70ch;
  margin: 0.35rem 0 0;
  font-size: var(--text-sm);
}

.triage {
  width: 100%;
  margin-top: 1.25rem;
  border-collapse: collapse;
  /* border-radius does nothing on a collapsed table without this. The card's
     3px left rule survives the collapse because a collapsing border resolves
     to the widest of the pair, and the row cells only carry 1px. */
  overflow: hidden;
}

.triage thead th {
  padding: 0.5rem 0.6rem;
  text-align: left;
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border-strong);
  white-space: nowrap;
}

.triage th.num,
.triage td.num {
  text-align: right;
}

/* The flaw type is the one elastic column; everything else is a fixed shape
   and should not move as the batch changes. */
.triage-dot-cell { width: 1.6rem; }
.triage-toggle-cell { width: 2rem; }

.triage td {
  padding: 0.5rem 0.6rem;
  vertical-align: middle;
  border-bottom: 1px solid var(--border);
}

.triage tbody tr:last-child td {
  border-bottom: none;
}

.triage-row {
  cursor: pointer;
  transition: background-color var(--dur) var(--ease);
}

.triage-row:hover {
  background: var(--surface-raised);
}

.triage-row:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: -2px;
}

/* Open rows keep the raised surface so the detail beneath them reads as
   attached to a row rather than floating between two. */
.triage-row[aria-expanded="true"],
.triage-row[aria-expanded="true"] + .triage-detail {
  background: var(--surface-raised);
}

.triage-row[aria-expanded="true"] td {
  border-bottom-color: transparent;
}

.triage-id {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--text-primary);
  white-space: nowrap;
}

.triage-flaw {
  max-width: 22rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* Same honest fallbacks as the related list, treated the same way, so a
   CWE number is never mistaken for a plain-English label. */
.triage-flaw[data-kind="named"],
.triage-flaw[data-kind="placeholder"],
.triage-flaw[data-kind="unnamed"],
.triage-flaw[data-kind="none"] {
  color: var(--text-muted);
  cursor: help;
}

/* Italic for the three that name no weakness — NVD having nothing to record,
   an id newer than our catalogue, and nothing recorded at all. A real name
   from MITRE is not one of those: it is a classification, just not ours. */
.triage-flaw[data-kind="placeholder"],
.triage-flaw[data-kind="unnamed"],
.triage-flaw[data-kind="none"] {
  font-style: italic;
}

/* The sort key. Tabular figures because the entire point is comparing this
   column down the page. */
.triage-epss {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
  white-space: nowrap;
}

.triage-epss[data-unscored] {
  color: var(--text-disabled);
  cursor: help;
}

.triage-kev {
  white-space: nowrap;
  color: var(--text-disabled);
}

.triage-year {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

.triage-toggle {
  display: block;
  color: var(--text-muted);
  transition: transform var(--dur) var(--ease);
}

.triage-toggle::before {
  content: "▾";
  display: block;
  font-size: var(--text-sm);
  line-height: 1;
}

.triage-row[aria-expanded="true"] .triage-toggle {
  transform: rotate(180deg);
}

.triage-detail > td {
  padding: 0 0.6rem 0.85rem 2.2rem;
}

.triage-verdict {
  max-width: 70ch;
  margin: 0 0 0.4rem;
  font-weight: var(--weight-medium);
  color: var(--text-primary);
}

.triage-summary-text {
  max-width: 70ch;
  margin: 0 0 0.5rem;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.triage-facts {
  list-style: none;
  margin: 0 0 0.5rem;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 0.9rem;
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.triage-open {
  font-size: var(--text-sm);
}

/* Below this the seven columns stop fitting. The flaw type is the first to
   go: it is the only column here that isn't part of the ranking argument. */
@media (max-width: 720px) {
  .triage th:nth-child(3),
  .triage td:nth-child(3) {
    display: none;
  }

  .triage-detail > td {
    padding-left: 1rem;
  }
}

/* Available to screen readers, not to the eye. The triage table's caption and
   its two unlabelled columns need names that would only be noise on screen. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------------------
 * Fixed versions
 *
 * Above the table because it is the only part of the triage view that is an
 * instruction rather than a finding. A card, so it reads as the answer and
 * not as another note stacked under the summary.
 * ------------------------------------------------------------------------ */

#fix-summary {
  margin-top: 1.25rem;
  padding: 0.9rem 1.1rem;
}

#fix-summary h3 {
  margin: 0 0 0.6rem;
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
  cursor: help;
}

/* ---------------------------------------------------------------------------
 * Two sections, and a row per vendor
 *
 * "Upgrade to 7.4.3" and "no fixed version published" are different
 * instructions and now sit apart; a vendor with more than one product on the
 * batch is one row with the rest behind a toggle. Both are in app.js — see
 * the note above FIX_SECTION_FIXED for why.
 * ------------------------------------------------------------------------ */
.fix-section + .fix-section {
  margin-top: 0.9rem;
  padding-top: 0.8rem;
  border-top: 1px solid var(--border);
}

/* A step below the panel's own h3, which is the uppercase label above both of
   these. Sentence case and full weight, because unlike that label this is
   something to read rather than a name for the block. */
.fix-section-head {
  margin: 0 0 0.5rem;
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  color: var(--text-secondary);
}

.fix-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.fix-product + .fix-product {
  margin-top: 0.5rem;
  padding-top: 0.5rem;
  border-top: 1px solid var(--border);
}

/* A button now, because clicking a product marks its CVEs in the table above
   and that has to be reachable by keyboard. Everything the UA puts on a
   button is stripped so it still reads as the row's name — the affordance is
   the hover and focus state below, not a box around the words. */
.fix-name {
  font-weight: var(--weight-medium);
  color: var(--text-primary);
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  text-align: left;
  cursor: pointer;
  border-radius: var(--radius-sm);
  transition: color var(--dur) var(--ease);
}

.fix-name:hover {
  color: var(--accent-text);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* The instruction itself. Mono because it carries a version number the
   reader is going to type or search for. */
.fix-action {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.fix-name + .fix-action::before,
.fix-name + .fix-none::before,
.fix-name + .fix-count::before,
.fix-count + .fix-action::before,
.fix-count + .fix-differ::before,
.fix-count + .fix-none::before,
.fix-action + .fix-none::before,
.fix-differ + .fix-none::before {
  content: " — ";
  font-family: var(--font-sans);
  color: var(--text-muted);
}

/* How many products the group row stands for. This is the number the row
   exists to give: nobody runs all fourteen editions, they run one, and the
   count is what says how far down the toggle to look. */
.fix-count {
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* Said instead of a version, never alongside one. Where the members do not
   share a fix there is no single answer to print on this line, and the
   highest of them would be a version that fixes exactly one of the products
   the row names. */
.fix-differ {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* The CVEs a product accounts for, named rather than counted.
   Under the instruction, not beside it: the version to upgrade to is the
   answer and these are the evidence for it, so they take the quieter row. */
.fix-cves {
  margin-top: 0.3rem;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem;
}

/* Each ID is a button both ways: it marks that CVE in the table above, and
   the table marks it back. Outline-only — the QUIET construction — because a
   navigation affordance must never come out louder than the exploitation
   chips it sits under. */
.fix-cve {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: 1.5;
  padding: 0.05rem 0.35rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-secondary);
  cursor: pointer;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

.fix-cve:hover {
  color: var(--accent-text);
  border-color: var(--accent-text);
}

.fix-cve[aria-pressed="true"] {
  color: var(--accent-text);
  border-color: var(--accent-text);
  font-weight: var(--weight-semi);
}

.fix-cve-rest {
  font-size: var(--text-xs);
  color: var(--text-muted);
  cursor: help;
}

/* --- Linking the panel to the table --------------------------------------

   One selection at a time, marked in both views. Additive only: nothing is
   hidden and an unmarked row means "not part of what you clicked", never
   "not affected" — the same rule the version check's matched branch follows.

   Not carried by colour. A marked row gets a solid left bar and a weight
   change as well as the tint, and aria-current in the DOM, so the mark
   survives a monochrome screen and reaches a screen reader. The unmarked
   rows lose a little contrast rather than being greyed out, because they are
   still the answer to every other question on the page. */

.fix-link-note {
  margin: 0 0 0.7rem;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.fix-link-clear {
  font: inherit;
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent-text);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}

[data-linking="true"] .fix-product:not([data-linked]),
[data-linking="true"] .triage-row:not([data-linked]) {
  opacity: 0.55;
}

.fix-product[data-linked] {
  border-left: 3px solid var(--accent-text);
  margin-left: -0.75rem;
  padding-left: calc(0.75rem - 3px);
  background: var(--surface-raised);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}

.fix-product[data-linked] > .fix-line > .fix-name {
  font-weight: var(--weight-semi);
}

.triage-row[data-linked] > td {
  background: var(--surface-raised);
}

/* The bar goes on the first cell so it reads as an edge on the row, which a
   border on a <tr> cannot do once the cells have their own backgrounds. */
.triage-row[data-linked] > td:first-child {
  box-shadow: inset 3px 0 0 var(--accent-text);
}

.triage-row[data-linked] > .triage-id {
  font-weight: var(--weight-semi);
}

/* An answer, not a gap — so it reads as stated rather than missing. */
.fix-none {
  font-size: var(--text-sm);
  color: var(--text-muted);
  cursor: help;
}

/* Several release lines, behind a toggle. Indented under the product because
   the choice between them is the reader's — which one they are running. */
.fix-trains {
  list-style: none;
  margin: 0.35rem 0 0;
  padding: 0 0 0 1rem;
  border-left: 2px solid var(--border);
}

.fix-trains li {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: 0.12rem 0;
}

.fix-train-label {
  min-width: 5.5rem;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.fix-more {
  margin-top: 0.6rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--border);
}

.fix-more summary {
  cursor: pointer;
  font-size: var(--text-sm);
  color: var(--text-muted);
  border-radius: var(--radius-sm);
  transition: color var(--dur) var(--ease);
}

.fix-more summary:hover {
  color: var(--accent-text);
}

.fix-more .fix-list {
  margin-top: 0.5rem;
}

/* --- Verdict sections ---------------------------------------------------
 *
 * The reader is meant to be able to stop after the first one, so the break
 * between sections is a real one: a ruled band, not just a bolder row.
 * ---------------------------------------------------------------------- */

.triage-group th {
  padding: 0.65rem 0.6rem 0.35rem;
  text-align: left;
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
  background: var(--surface-raised);
  border-bottom: 1px solid var(--border-strong);
  cursor: help;
}

/* The first section needs no rule above it — the table head is already there. */
.triage tbody tr.triage-group:not(:first-child) th {
  border-top: 1px solid var(--border-strong);
}

/* Confirmed exploited already, so the prediction beside it stops competing
   with the badge. Muted, not hidden: it is still information. */
.triage-epss[data-secondary] {
  color: var(--text-muted);
  font-weight: var(--weight-normal);
}

/* --- Not found ----------------------------------------------------------
 *
 * A row, not a footnote. The count above the table has to be the count that
 * was pasted, and a triage view that quietly drops one is worse than useless.
 * ---------------------------------------------------------------------- */

.triage-missing {
  cursor: default;
}

.triage-missing:hover {
  background: none;
}

.triage-missing .triage-id {
  color: var(--text-muted);
}

.triage-missing-note {
  font-size: var(--text-sm);
  font-style: italic;
  color: var(--text-muted);
  cursor: help;
}

/* --- Home link -----------------------------------------------------------
 *
 * The title is the way back. Accent on hover rather than by default: at rest
 * it is a heading and should read as one, but it has to answer the pointer
 * like every other site's masthead does.
 * ---------------------------------------------------------------------- */

#home-link {
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  border-radius: var(--radius-sm);
  transition: color var(--dur) var(--ease);
}

#home-link:hover {
  color: var(--accent-text);
}

#home-link:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 3px;
}

/* --- Fixed versions, collapsed ------------------------------------------
 *
 * One line per product by default. This block sits above the table and was
 * the longest thing on the page when it listed every line of every product.
 * ---------------------------------------------------------------------- */

.fix-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 0.5rem;
}

.fix-branches {
  margin-top: 0.2rem;
}

.fix-branches summary {
  cursor: pointer;
  font-size: var(--text-xs);
  color: var(--text-muted);
  border-radius: var(--radius-sm);
  transition: color var(--dur) var(--ease);
}

.fix-branches summary:hover {
  color: var(--accent-text);
}

.fix-branches .fix-trains {
  margin-top: 0.3rem;
}

/* The editions inside a group toggle. Indented and ruled off the same way the
   release lines are, one level up, because the choice between them is the
   same kind of choice: which one are you running. */
.fix-group > .fix-branches > .fix-list {
  margin-top: 0.4rem;
  padding-left: 1rem;
  border-left: 2px solid var(--border);
}

/* Stated, not silently applied: NVD lists every vendor that ships an
   affected component, and the reader should know some were filtered. */
.fix-dropped {
  margin: 0.6rem 0 0;
  padding-top: 0.6rem;
  border-top: 1px solid var(--border);
  font-size: var(--text-xs);
  color: var(--text-muted);
  cursor: help;
}

/* The standing caveat closing the panel. Same weight as .fix-dropped — both
 * are notes about the list rather than part of it — but no cursor: help,
 * because it carries no title to reveal. */
.fix-caveat {
  margin: 0.6rem 0 0;
  padding-top: 0.6rem;
  border-top: 1px solid var(--border);
  font-size: var(--text-xs);
  color: var(--text-muted);
}

/* One rule under both notes, not two stacked rules. */
.fix-dropped + .fix-caveat {
  margin-top: 0.35rem;
  padding-top: 0;
  border-top: none;
}

/* --- All clear -----------------------------------------------------------
 *
 * The most common outcome for a vendor monthly bundle, and the one the flat
 * table served worst. The conclusion carries the weight here; the table is
 * still one click away for anyone who wants to check the working.
 * ---------------------------------------------------------------------- */

#batch-summary[data-all-clear="true"] {
  font-size: var(--text-lg);
  color: var(--text-primary);
}

/* Every case except the all-clear: the table is simply the table, so the
   disclosure it lives in has to leave no trace of itself. */
#batch:not([data-all-clear="true"]) #triage-toggle {
  display: none;
}

#triage-toggle {
  margin-top: 1.25rem;
  padding: 0.5rem 0;
  cursor: pointer;
  font-size: var(--text-sm);
  color: var(--text-muted);
  border-radius: var(--radius-sm);
  transition: color var(--dur) var(--ease);
}

#triage-toggle:hover {
  color: var(--accent-text);
}

#triage-toggle:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}
