/* ClearCVE theme colour experiment — Carbon + Electric Mint / Verdigris.
 *
 * Both modes are one declaration per token via light-dark(); the theme switch
 * sets color-scheme and nothing else. Geometry, type, component structure and
 * severity classification are unchanged — this file changes colour only.
 *
 * The page is carbon. The accent is the 5% and is spent deliberately: the one
 * primary action, the selected tab and row, the focus ring, contextual links.
 * Dense lists of identifiers are neutral until interacted with. Severity keeps
 * its own ramp and never borrows the accent.
 */
:root {
  color-scheme: light dark;

  /* --- Surfaces. Carbon, and overwhelmingly neutral: the accent appears on
   * interactive and identity elements only, never as a surface.
   *
   * Dark steps UP from the page; light steps up to the panel (a card catches
   * the light) and DOWN for raised and hover (a filled chip on paper is an
   * inset). Spaced by CIE L*, not by ratio — near white and near black the
   * ratio between adjacent surfaces compresses to nothing:
   *
   *            page  chrome  panel  raised  hover   input
   *   light    96.0   97.8   100.0   93.1    89.4   100.0
   *   dark      2.6    4.4     6.0    10.3    15.0     4.8
   *
   * Chrome is one step off the page toward the panel in both themes, which
   * is what keeps the band legible as a different surface without letting it
   * read as a card. What carries its lower boundary is --rule. --- */
  --surface-page: light-dark(#f3f4ef, #080a0b);
  --surface-panel: light-dark(#ffffff, #101416);
  --surface-raised: light-dark(#e9ece7, #171d20);
  --surface-hover: light-dark(#dee2dc, #1f272b);
  --surface-chrome: light-dark(#f8f9f5, #0c1012);
  --surface-input: light-dark(#ffffff, #0d1113);
  --surface-overlay: light-dark(#ffffff, #1f272b);
  --shadow-overlay: light-dark(rgb(0 0 0 / 14%), rgb(0 0 0 / 50%));
  /* Light only. In dark this resolves to transparent, which makes both layers
     of the cards' box-shadow a no-op there: on a carbon ground the surface
     step is the separation, and a glow would be the thing this palette is
     specifically not. Neutral, never tinted with the accent. */
  --shadow-card: light-dark(rgb(17 22 20 / 8%), transparent);

  /* Three boundary roles, three weights.
     --rule    region boundaries and the grouping rule down the left of every
               card, plus the low / unknown verdict indicator. 3.24 page /
               3.59 panel / 3.39 chrome light, 3.54 / 3.31 / 3.41 dark; a 1px
               region boundary and a 3-4px indicator each owe 3:1.
     --border-strong  the card EDGE. 1.74 / 1.93 light, 1.98 / 1.85 dark.
     --border  the hairline between rows INSIDE a card, and the quietest of
               the three by design.
     --border-control  the edge of a form control, which is what identifies
               it as one: WCAG 1.4.11 wants 3:1 and --border-strong is
               decorative weight. 3.93 on the light input, 5.30 on the dark
               one. Neutral, not the accent — a resting input is not active. */
  --rule: light-dark(#7e8a84, #5c6a70);
  --border: light-dark(#cdd3cf, #283137);
  --border-strong: light-dark(#b4bdb7, #38444b);
  --border-control: light-dark(#78837e, #7a8a90);

  /* --- Text, four levels. Slightly green-shifted greys rather than pure
   * neutrals, so the carbon relates to the accent without carrying it. --- */
  --text-primary: light-dark(#111614, #f2f5f4);
  --text-secondary: light-dark(#56615d, #9aa6a3);
  /* The token the surface ramp is pinned by: it sits on --surface-raised and
     --surface-hover all over the page, so AA on those two decides how far the
     ramp can go. Both values are darker (light) and lighter (dark) than the
     mid-greys this palette was drawn with, which measured 3.07 and 3.17 on
     the hover surface. Worst case is now 4.64 light and 5.22 dark. */
  --text-muted: light-dark(#5c6461, #8e9a97);
  --text-disabled: light-dark(#7f8884, #626d6a); /* inactive controls only */

  /* --- Electric mint / verdigris: the brand, and the one colour that means
   * "you can act on this". It is never a surface, never a heading, never a
   * severity, and it never glows.
   *
   * It is DELIBERATELY SCARCE. The accent marks the one primary action, the
   * selected tab and row, the focus ring, and contextual links — it does not
   * mark everything that happens to be clickable. Three places that were
   * accent by default are now neutral until they are hovered, focused or
   * selected: the Related CVE identifiers, the Copy link button, and the
   * ordered-step counters in "How it works", which were never interactive at
   * all. A sidebar of ten accent-coloured IDs competes with the CVE the
   * reader opened; the same ten in --text-primary do not, and the standing
   * underline is what still says "link" (see .related-id).
   *
   * FILL and TEXT are separate roles and the split is load-bearing in light.
   * #147a67 carries a white label at 5.24:1, which is the button — but as
   * TEXT it measures 4.39 on --surface-raised and 4.00 on --surface-hover,
   * both under AA, and .copy-btn and the vendor summaries sit on exactly
   * those. So the light text role is one step deeper at #0f6656 (5.77 and
   * 5.24). Dark needs no such split: the mint runs 8.52-11.14:1 on every
   * surface, and the button flips its label to near-black at 11.14:1 rather
   * than dulling the mint to suit a requirement only the text role has. --- */
  --accent: light-dark(#147a67, #43d9b0);
  --accent-hover: light-dark(#0f6656, #67e4c2);
  --accent-pressed: light-dark(#0b4f42, #2dbf98);
  --accent-fg: light-dark(#ffffff, #080a0b);
  --accent-text: light-dark(#0f6656, #43d9b0);
  --accent-text-hover: light-dark(#0b4f42, #67e4c2);
  --accent-text-pressed: light-dark(#083b31, #8defd4);
  --accent-soft: color-mix(in srgb, var(--accent) 14%, transparent);
  --accent-edge: color-mix(in srgb, var(--accent) 38%, transparent);
  --focus-ring: light-dark(#147a67, #43d9b0);

  /* --- Semantic status. Never used for interaction, and deliberately kept
   * off the accent's hue: critical 0, high 23-25, medium 35-37, low 214,
   * against the accent at 164-169 degrees. Low is the closest of the four at
   * 46-51 degrees and stays where it is — the accent never appears in a
   * severity role, so the two are never read against each other, and moving
   * a status colour to suit the brand is the thing the separation exists to
   * prevent. Low is a blue rather than the green it was under the warm
   * palette, and with a mint accent that matters more, not less: green would
   * now sit next to the brand, and this page has no success state for green
   * to mean.
   *
   * Light values hold the hues and drop in lightness, because all of these
   * are used as text somewhere and the dark ones sit near 3:1 on paper. --- */
  --critical: light-dark(#bd1c1c, #f87171);
  --high: light-dark(#a84306, #fb8c3c);
  --medium: light-dark(#8a5000, #f0a838);
  --low: light-dark(#1d5fb8, #74a9f0);
  --neutral: light-dark(#57625e, #8a9693);

  /* --- Emphasis tiers, unchanged in construction:
   *   LOUD    solid saturated fill      6.05-7.90:1 against the card
   *   MEDIUM  20% tint + solid border   the tint can never outshout a fill
   *   QUIET   no fill, outline only
   * LOUD is only for what someone is ACTUALLY DOING to this CVE, and stays
   * two hues, because "known exploited" and "exploit code is public" are
   * different claims. The accent is not a tier and never appears here. --- */
  --loud-alarm: light-dark(#bd1c1c, #ff5c5c);
  --loud-warn: light-dark(#a84306, #ff8a3d);
  --loud-fg: light-dark(#ffffff, #080a0b);

  /* The severity badge is NOT a tier: it answers "how bad", the tiers answer
     "how urgent". Full semantic chroma in border and label, a tint instead of
     a fill, so a 9.8 nobody is exploiting cannot outshout an actively
     exploited 7.5. */
  --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);
  --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-text-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 - 4rem);
}

/* ---------------------------------------------------------------------------
 * 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: none;
  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;
}

/* The wordmark keeps --text-primary from the rule above. Splitting it as
   neutral "Clear" + mint "CVE" would need the markup to name the two halves —
   index.html has <h1><a id="home-link">ClearCVE</a></h1>, one text node — and
   there is no colour-only way to reach the second half of a word. A
   hard-stopped background-clip gradient could fake it, but it would land on a
   different letter the moment the webfont fell back, and a decorative
   gradient is the thing this palette is specifically without. So the whole
   wordmark stays neutral: painting it --accent-text would put the interaction
   colour on the largest non-interactive element on the page, directly above
   the one primary button. */

.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;
}

/* The <noscript> notice above the search box. Sized and coloured like the
   tagline it follows, with a left rule so it reads as a notice rather than a
   second tagline. In the stylesheet, not a style attribute: the notice must
   cost the CSP nothing. */
.noscript-note {
  max-width: 46ch;
  margin: 0 auto 1.75rem;
  padding: 0.6rem 0.9rem;
  border-left: 3px solid var(--border-strong);
  background: var(--surface-panel);
  border-radius: 4px;
  text-align: left;
  font-size: var(--text-sm);
  line-height: 1.55;
  color: var(--text-secondary);
  text-wrap: pretty;
}

.noscript-note a {
  color: var(--accent-text);
}

/* --- 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-input);
  color: var(--text-primary);
  resize: none;
  overflow: hidden;
  transition: border-color var(--dur) var(--ease),
    background-color var(--dur) var(--ease);
}

#cve-input::placeholder,
#version-input::placeholder {
  color: var(--text-muted);
}

#cve-input:hover {
  border-color: var(--focus-ring);
}

#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-input);
  color: var(--text-secondary);
}

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

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

.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);
}

/* Keep primary colours from overriding secondary controls' own paint states. */
button:hover:where(:not(:disabled)) {
  background: var(--accent-hover);
}

button:active:where(: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-text-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);
}

/* The page is the CVE.org record because NVD returned nothing. Dashed: the
   lookup that normally answers did not. */
.source-note {
  margin: 0 0 1rem;
  padding: 0.35rem 0.6rem;
  max-width: 70ch;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  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;
}

/* Stacked, not inline with the version box: it is a prior question, not a
   second field alongside it. */
.version-product-row {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin-bottom: 0.5rem;
}

#version-product-label {
  font-size: var(--text-xs);
  color: var(--text-muted);
}

#version-product {
  padding: 0.45rem 0.6rem;
  font-size: var(--text-sm);
  color: var(--text-primary);
  background: var(--surface-input);
  border: 1px solid var(--border-control);
  border-radius: 6px;
}

#version-product-note {
  margin: 0;
}

.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-input);
  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-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);
}

#version-result .verdict-caveat {
  margin-top: 0.4rem;
}

/* These CSS tokens remain available when the application module fails. */
.boot-error {
  margin: 0 0 1rem;
  padding: 0.75rem 1rem;
  border: 1px solid var(--critical);
  border-radius: 6px;
  background: var(--surface-panel);
  color: var(--critical);
  font-size: 0.95rem;
}

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

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

/* Two groups, Exploitability then Impact. A step below the block's own
   "Severity" heading and a step above the metric labels under it: the label
   size, but semibold and secondary, with the space above doing the grouping. */
.cvss-group-head {
  margin: 0 0 0.35rem;
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  color: var(--text-secondary);
}

.cvss-group-head:not(:first-child) {
  margin-top: 0.9rem;
}

.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 container's single
   label column, so they size to the longest label *and* stay aligned — across
   both groups, because the column belongs to #cvss-plain, not to one list. */
@supports (grid-template-columns: subgrid) {
  #cvss-plain {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    column-gap: 1.25rem;
  }

  .cvss-group-head,
  .cvss-plain-list {
    grid-column: 1 / -1;
  }

  .cvss-plain-list {
    grid-template-columns: subgrid;
  }

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

/* A row with a CVSS term behind it. The dotted underline on the label is the
   only hint, kept quiet: the plain sentence is the answer, the term is a
   footnote to it. The row is the anchor for its tooltip. */
.cvss-plain-list li.has-term {
  position: relative;
  border-radius: var(--radius);
}

.cvss-plain-list li.has-term .metric-label {
  text-decoration: underline dotted;
  text-decoration-color: var(--border-strong);
  text-underline-offset: 0.2em;
  cursor: help;
}

/* The focus ring is the global [tabindex] one; only the offset is ours, so
   the ring clears the text instead of cutting through descenders. */
.cvss-plain-list li.has-term:focus-visible {
  outline-offset: 2px;
}

/* The same overlay as the reachability scale, one line of text. */
.metric-tip {
  position: absolute;
  top: calc(100% + 0.35rem);
  left: 0;
  z-index: 20;
  width: max-content;
  max-width: min(26rem, calc(100vw - 2rem));
  padding: 0.35rem 0.6rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-overlay);
  box-shadow: 0 8px 24px var(--shadow-overlay);
  font-size: var(--text-sm);
  font-family: var(--font-mono);
  color: var(--text-primary);
  cursor: default;
}

/* A sentence, not a CVSS term: the body face. */
.metric-tip-prose {
  font-family: inherit;
}

/* Code execution leads the Impact group and reads as its headline. */
.cvss-plain-list li.metric-lead .metric-text {
  font-weight: var(--weight-medium);
}

/* NVD's status where it carries a note: the same quiet hint as a CVSS row. */
.status-term {
  position: relative;
  text-decoration: underline dotted;
  text-decoration-color: var(--border-strong);
  text-underline-offset: 0.2em;
  cursor: help;
}

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

.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 {
    display: block;
  }

  .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.
 *
 * A neutral surface with a neutral edge and an accent LABEL: a solid accent
 * button here would compete with "Look up" directly above it, and an accent
 * fill and an accent edge together were still the second loudest thing on the
 * page. This is the primary thing to click in the BAND, not on the page, and
 * the ranking has to say so at rest. The accent tint and edge it used to
 * carry are now its hover state, so the control gains weight under the
 * pointer instead of starting there.
 *
 * 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(--surface-raised);
  border: 1px solid var(--border-strong);
  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: var(--accent-soft);
  border-color: var(--accent-edge);
}

.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;
}

/* Attribution for a per-CVE write-up. Quiet and last in the body: it is what
   makes the explanation checkable, not part of the explanation. */
.explainer-source {
  margin: 0.6rem 0 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* 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 mint
   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 what the vendor's own record says is affected, 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;
}

/* One line per release branch, which is the default remediation view. The
   full rows sit behind the disclosure under it; see remediationEl. */
.product-remediation {
  margin-top: 0.3rem;
}

.remediation-branches {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.remediation-branches li {
  margin: 0 0 0.15rem;
}

/* The provenance and the caveats. Same construction as every other
   disclosure here — a caret and muted label, nothing that competes with the
   branch lines above it. */
.remediation-evidence {
  margin-top: 0.3rem;
  font-size: var(--text-sm);
}

.remediation-evidence > summary {
  cursor: pointer;
  color: var(--text-muted);
}

.remediation-evidence .product-vendor-versions {
  margin-top: 0.25rem;
}

.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;
  /* overflow-y alone makes x auto too: a long unbroken string would give the
     side panel its own sideways scroll. Text wraps instead (below). */
  overflow-x: hidden;
}

@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);
}

/* Counters, not controls. These were the accent purely because they are small
   and numeric, and nothing here can be clicked — the accent has to keep
   meaning "you can act on this" or it stops meaning anything. */
.mech-steps li::before {
  content: counter(mech);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
  text-align: right;
}

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

/* Layer 3's list of recorded CWEs. The id is the link; the source label is
   provenance, not urgency, so it is the quietest thing on the line — muted,
   small, outlined, never a tone colour. */
.cwe-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 1rem;
  max-width: 68ch;
}

.cwe-head {
  margin: 0;
  overflow-wrap: anywhere;
}

.cwe-id {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}

.cwe-name {
  color: var(--text-primary);
}

.cwe-source {
  display: inline-block;
  padding: 0 0.35rem;
  border: 1px solid var(--border);
  border-radius: 4px;
  font-size: var(--text-xs);
  color: var(--text-muted);
  vertical-align: 0.1em;
  cursor: help;
}

.cwe-desc {
  margin: 0.3rem 0 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.cwe-meta {
  margin: 0.25rem 0 0;
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.cwe-none {
  margin: 0;
  color: var(--text-secondary);
}

.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);
}

/* Every reference the fix state did not use, with the type its sources
   tagged it as. Reference material, so it is set small and quiet. */
.source-refs {
  margin-top: 1.25rem;
}

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

.source-refs-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.3rem;
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

.source-refs-list a {
  color: var(--accent-text);
  text-underline-offset: 2px;
}

.source-ref-kind {
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.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;
}

/* An early EPSS score below the threshold: dashed, like every answer that
   cannot support a stand-down. An outline, not a border, so it takes no
   layout space and the batch column keeps fitting 390px. */
.related-epss-value[data-early],
.triage-epss [data-early] {
  outline: 1px dashed var(--tone-unknown);
  outline-offset: 2px;
  border-radius: 2px;
}

.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);
  /* NEUTRAL until interacted with, and the underline is what carries "link"
     — which is why this can afford to be neutral at all. These are the only
     navigation on the page and they read as body text without weight and an
     underline, and a link distinguishable by hue alone is not
     distinguishable at all to a reader who cannot see the hue. That was
     already true; what changed is that colour is no longer doing any of the
     work at rest.

     The panel holds up to ten of these beside the CVE the reader actually
     opened. In the accent they were ten of the loudest things on the page and
     the sidebar competed with the answer; in --text-primary they read as the
     list of identifiers they are. The accent arrives on hover and focus,
     where it means "this one", and the underline goes to full strength with
     it. The 40% underline is drawn from currentColor so it tracks whichever
     state the row is in rather than naming a colour twice. */
  font-weight: var(--weight-medium);
  background: none;
  color: var(--text-primary);
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
  text-underline-offset: 2px;
  transition: color var(--dur) var(--ease),
    text-decoration-color var(--dur) var(--ease);
}

.related-id:hover,
.related-id:focus-visible {
  background: none;
  color: var(--accent-text);
  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);
  /* NVD descriptions carry URLs and paths with no break point. */
  overflow-wrap: anywhere;
}

.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;
}

/* The fix state sits with the verdict, where someone acting on it will look:
   one line saying which of the four states this CVE is in, then only the
   links that back it. Nothing here is loud — a fix is not something anyone
   is doing to this CVE, and the urgency tiers are for that. */
.fix-block > * + * {
  margin-top: 0.3rem;
}

.fix-state {
  margin: 0;
  max-width: 70ch;
  font-weight: var(--weight-semi);
  color: var(--text-primary);
}

/* Unknown is not an answer and must not read as one. "Couldn't check" goes
   one step further and takes the dashed treatment every other lookup that
   did not answer gets. */
.fix-block[data-fix-state="unknown"] .fix-state {
  font-weight: var(--weight-medium);
  color: var(--text-secondary);
}

.fix-block[data-checked="false"] .fix-state {
  display: inline-block;
  padding: 0.1rem 0.5rem;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-sm);
}

.fix-products {
  margin: 0;
  padding-left: 1.1rem;
  max-width: 70ch;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.fix-record-date {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.fix-workaround,
.fix-notice {
  max-width: 70ch;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.fix-workaround summary,
.fix-notice summary {
  cursor: pointer;
  color: var(--text-muted);
  font-weight: var(--weight-semi);
}

.fix-workaround p,
.fix-notice p {
  margin: 0.3rem 0 0;
  white-space: pre-line;
}

.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-texts {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.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);
}

/* Secondary, and neutral at rest: it sits immediately beside the CVE ID, which
   is the identity of the page, and an accent-coloured control there reads as
   the more important of the two. Its own border already says "button". The
   accent arrives on hover, focus and press. */
.copy-btn {
  flex: none;
  padding: 0.2rem 0.6rem;
  font-family: inherit;
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--text-secondary);
  background: none;
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: color var(--dur) var(--ease),
    background-color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

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

/* 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-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
}

#csv-format {
  max-width: 100%;
  padding: 0.4rem;
  font: inherit;
  color: var(--text-primary);
  background: var(--surface-input);
  border: 1px solid var(--border-control);
  border-radius: 4px;
}

#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);
}

/* The fix state's short label. Secondary, not a tone: a fix is not urgency.
   Unknown steps down, and "couldn't check" is italic like the flaw column's
   own no-answer states. */
.triage-fix {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  white-space: nowrap;
}

.triage-fix[data-fix-state="unknown"] {
  color: var(--text-muted);
}

.triage-fix[data-checked="false"] {
  font-style: italic;
}

.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;
  }
}

/* The table scrolls inside its own box, never the page. On a phone the
   eight columns can still be wider than the card after the rules below
   tighten them, and a page that scrolls sideways moves everything else
   with it. */
.triage-scroll {
  /* Positioned so the visually-hidden header labels, which are absolutely
     positioned, are clipped by this box too; otherwise they escape it and
     hold the page open by themselves. */
  position: relative;
  max-width: 100%;
  overflow-x: auto;
}

/* A phone. Eight columns do not fit 390px on their own widths: the headers
   stop holding the line open and wrap, the Fix label wraps onto a second
   line rather than widening its column, and the cells give up some padding.
   The Year goes, as the flaw type does at 720px: it is context for the
   ordering, not part of it, and the Fix column needs the room. */
@media (max-width: 480px) {
  .triage th:nth-child(7),
  .triage td:nth-child(7) {
    display: none;
  }

  /* Sentence case rather than capitals: the same words, a third narrower,
     which is what lets "Likelihood" and "Exploited" stop setting the width
     of columns whose values are a few characters. */
  .triage thead th {
    white-space: normal;
    letter-spacing: 0;
    text-transform: none;
    font-size: 0.7rem;
  }

  .triage th,
  .triage td {
    padding-left: 0.25rem;
    padding-right: 0.25rem;
  }

  /* Sized for a six-digit 2026 ID (CVE-2026-123456, fifteen characters) —
     2026 passed 90,000 in September — and never truncated: an ID cut short
     is a different CVE. */
  .triage-id {
    font-size: 0.65rem;
  }

  .triage-fix {
    white-space: normal;
    min-width: 4rem;
    font-size: var(--text-xs);
  }
}

/* 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;
}

/* Small public information pages and the shared footer. */
.site-footer {
  padding: 1.25rem 0;
  text-align: center;
  color: var(--text-muted);
  font-size: var(--text-sm);
  line-height: 1.5;
}

.site-footer a {
  display: inline-block;
  padding: 0.25rem 0.15rem;
  color: inherit;
}

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

.info-page .page {
  min-height: calc(100vh - var(--page-pad-y) * 2);
  display: flex;
  flex-direction: column;
}

.info-content {
  width: 100%;
  max-width: 720px;
  margin: 0 auto;
  flex: 1;
  line-height: 1.7;
}

body.info-page .info-content h1 {
  margin: 1.5rem 0;
  font-size: var(--text-xl);
  text-align: left;
  letter-spacing: -0.02em;
  background: none;
  color: var(--text-primary);
  animation: none;
  transition: none;
}

.info-content h2 {
  margin: 1.5rem 0 0.5rem;
  font-size: var(--text-md);
}

.info-content p {
  margin: 0 0 1rem;
  color: var(--text-secondary);
}

.back-to-search {
  display: inline-block;
  font-size: var(--text-sm);
}

/* --- "What do I search for?" sub-tabs ----------------------------------- *
 * Two tabs inside the tab: what the vendor's advisory says to check, and our
 * generated starting queries. Underlined like the side panel's tabs, not
 * filled like the main strip — a filled strip inside a filled strip would
 * read as a second page. One class more specific than the generic [role=tab]
 * rules, which is what makes these win. */
.detect-tabs {
  gap: 0.15rem;
  margin: 0;
  border-bottom: 1px solid var(--border);
}

.detect-tabs .detect-tab {
  padding: 0.35rem 0.6rem;
  margin-bottom: -1px;
  border: 0;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  background: none;
  box-shadow: none;
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--text-muted);
}

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

.detect-tabs .detect-tab[aria-selected="true"] {
  background: none;
  box-shadow: none;
  color: var(--text-primary);
  font-weight: var(--weight-semi);
  border-bottom-color: var(--accent-text);
}

/* Not a card inside the card: the outer tab panel is the box. */
#detect .detect-panel {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

.vendor-check-source p {
  margin: 0;
  max-width: 70ch;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.vendor-check-source .vendor-check-dates {
  color: var(--text-muted);
}

.vendor-check-age {
  margin: 0 0 0.5rem;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.vendor-check-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.vendor-check-item {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 0.5rem;
}

/* Quoted text wraps rather than scrolls, like the queries: a long grep is
   still one thing to copy, and the copy button takes it whole. */
.vendor-check-text {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.35rem 0.5rem;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  background: var(--surface-raised);
  border-radius: var(--radius-sm);
}

.vendor-check-algo,
.vendor-check-link {
  flex: none;
  align-self: center;
  font-size: var(--text-xs);
}

.vendor-check-algo {
  color: var(--text-muted);
}

.vendor-check-item .copy-btn {
  flex: none;
}

/* A group's heading with its "Copy all" at the end of the same line. */
.vendor-check-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  margin: 0 0 0.5rem;
}

.vendor-check-head .detect-head {
  margin: 0;
}

.vendor-check-head .copy-btn {
  flex: none;
}

.vendor-check-links ul {
  margin: 0;
  padding-left: 1.1rem;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* Under the ID, never beside it: the ID column is sized for a six-digit ID
   at phone width, and the flag is shorter than that, so it adds height only. */
.triage-id .triage-vendor-check {
  display: block;
  width: max-content;
  margin-top: 0.2rem;
  font-family: inherit;
}

/* A vendor's placeholder inside a quoted value ("<install>"): marked so it
   cannot be pasted as a literal path. QUIET — a tint, not a highlight. */
.vendor-check-placeholder {
  padding: 0 0.1rem;
  color: inherit;
  background: color-mix(in srgb, var(--medium) 18%, transparent);
  border-radius: 2px;
}

.vendor-check-placeholder-note,
.vendor-check-elsewhere,
.vendor-check-context {
  margin: 0 0 0.5rem;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* The vendor's line that makes a hash an indicator: its own row under the
   hash, in the same muted style as the section notes. */
.vendor-check-context {
  flex: 1 0 100%;
  margin: -0.2rem 0 0;
}

/* A signature ID's line sits once under the whole group, not under an item. */
.vendor-check-group .vendor-check-group-context {
  margin: 0.4rem 0 0;
  max-width: 70ch;
}

/* One label at a time: the long one, or below 480px the short one. */
.detect-tab-short {
  display: none;
}

@media (max-width: 480px) {
  .detect-tab-long {
    display: none;
  }

  .detect-tab-short {
    display: inline;
  }
}

/* Why a young CVE's sources are silent, under its verdict; and the CVEs
 * the same update fixes, under the fix line. */
.verdict-age-note,
.fix-siblings {
  margin: 0.35rem 0 0;
  max-width: 60ch;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* The KEV entry's notes, which "What CISA requires" refers to. */
.kev-notes {
  margin: 0.35rem 0 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* What the vendor says to do after patching, under the fix line. */
.post-patch {
  margin: 0.5rem 0 0;
  font-size: var(--text-sm);
}
.post-patch-lead {
  margin: 0;
}
.post-patch-steps {
  margin: 0.25rem 0 0;
  padding-left: 1.25rem;
}
.post-patch-steps code {
  font-family: var(--font-mono, ui-monospace, monospace);
  overflow-wrap: anywhere;
}
