/* ---------------------------------------------------------------- fonts */
/* Self-hosted, latin subset only. These are Google's own latin subsets,
 * fetched once and served from this origin - see static/fonts/FONTS.md for
 * the source URLs, digests and licenses. The page makes no external
 * request of any kind, which is the whole point: a proxy that blocks
 * fonts.gstatic.com no longer changes how this site reads.
 *
 * Instrument Sans is one variable file covering the whole 400-700 range this
 * UI uses. IBM Plex Mono has no variable build on Google Fonts, so it is four
 * static faces - and it is four rather than three because the mono face
 * genuinely uses all of them: 400 for data, 500 for the "/100" suffix
 * beside a score, 600 for CVE identifiers and table row headers, 700 for
 * the score in a feed row. The Google request only ever asked for three,
 * so 700 was being faux-bolded from 600 until now.
 *
 * unicode-range is kept as published. A character outside it - the
 * latin-ext letters that can turn up in a vendor or product name from a
 * feed - falls back to the next family in the stack rather than pulling a
 * font this origin does not have.
 */
@font-face {
  font-family: "Instrument Sans";
  font-style: normal;
  /* Upstream carries two axes, wdth 75-100 and wght 400-700. Google's
     subset pins wdth at 100 and keeps wght, which is the whole range this
     UI uses: .chip-exigent, .chip-due-soon, .recommend, .hero-score-band

/* The flags column packs up to four chips into one cell. They wrap rather
   than widening the column, and the gap keeps them from reading as one
   long word when three of them fire at once - which on the top rows of the
   merged browser is most of them. */
.flags-cell { display: flex; flex-wrap: wrap; gap: 4px; align-items: flex-start; }
     and .action-button all set 700 on the UI face, and a file capped at
     600 would have had the browser fake every one. */
  font-weight: 400 700;
  font-display: swap;
  src: url("/static/fonts/instrument-sans-latin-variable.woff2") format("woff2");
  /* Instrument Sans's own latin range, which happens to be identical to
     the Plex Sans range this replaced - no U+2074, superscript four, which
     the file does not contain and nothing on the site uses. Claiming
     coverage the file lacks would only send the browser looking for a
     glyph that is not there. */
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/ibm-plex-mono-latin-400.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/static/fonts/ibm-plex-mono-latin-500.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/static/fonts/ibm-plex-mono-latin-600.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/static/fonts/ibm-plex-mono-latin-700.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* eVulnAble component stylesheet.
 *
 * Written against this frontend's own markup. The Streamlit build's
 * BASE_CSS was 1,538 lines with 222 Streamlit-specific selector hits,
 * because every rule had to reach through a DOM it did not control; none
 * of it is ported. Tokens come from /theme.css - there are no literal
 * colours below this line, so a preset is still just its token block, and
 * severity colour still comes from one place for web and both PDFs.
 */

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

html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  /* How much of the top of the window is already occupied by something
     stuck to it. Zero while the rail is a column beside the page; the
     ultrawide bar below sets it. Read by .sectionbar's sticky offset and
     by every anchor's scroll-margin, so the two cannot disagree. */
  --rail-sticky-h: 0px;
  margin: 0;
  /* Breaks only what would otherwise overflow: a bare advisory URL in a
     profile field, a comma-free CVE list. Ordinary words are untouched. */
  overflow-wrap: break-word;
  background: var(--page);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--a1); }
a:focus-visible, button:focus-visible, select:focus-visible,
input:focus-visible, summary:focus-visible {
  outline: 2px solid var(--a1);
  outline-offset: 2px;
}

.mono { font-family: var(--font-data); }
.nowrap { white-space: nowrap; }
.muted { color: var(--muted-strong); font-size: 14.5px; line-height: 1.55; }
/* Prose does not get wider just because the window does. The main column now
   runs to 2100px for the things that earn it - tables, tile rows, card grids
   - but a paragraph set across all of it is one nobody finishes. The site
   already said this in three places (.standfirst at 74ch, .hero-why at 88ch,
   the footer at 90ch); this says it for the panel and section copy that had
   no cap at all, and is the reason raising the column cap does not cost
   legibility. Paragraphs only: .muted also lands on inline spans, where a
   max-width would do nothing anyway, and on captions inside cards already far
   narrower than this, and .note is a bordered box - capping that would
   shrink the box rather than the line, which is not the same thing.
   .disclosure p is plain text inside a box, so the box still spans and only
   the measure is held. */
p.muted, .disclosure p { max-width: 92ch; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--a1); color: var(--on-accent);
  padding: 10px 16px; border-radius: 0 0 var(--radius-input) 0;
}
.skip-link:focus { left: 0; }

/* ------------------------------------------------------ htmx indicator */
/* htmx's own three rules, verbatim. It writes them into an inline <style>
 * unless includeIndicatorStyles is off, which base.html turns off through
 * the htmx-config meta element - an inline stylesheet is exactly what
 * style-src 'self' refuses. Nothing on the site marks an indicator today;
 * these are here so that adding one later works rather than silently
 * doing nothing. */
.htmx-indicator { opacity: 0; }
.htmx-request .htmx-indicator { opacity: 1; transition: opacity 200ms ease-in; }
.htmx-request.htmx-indicator { opacity: 1; transition: opacity 200ms ease-in; }

/* --------------------------------------------------- severity delivery */
/* How a band, an accent, or a proportion reaches an element.
 *
 * Every rule in this section replaces an inline style attribute. There
 * were 62 of them, carrying --risk-color, --section-color, a bar width, a
 * colour-mix percentage; they are the reason 'unsafe-inline' sat in the
 * Content-Security-Policy's style-src, and there is no way to keep them
 * and drop it:
 *
 *   - a nonce attaches to a <style> ELEMENT and does nothing at all for a
 *     style ATTRIBUTE;
 *   - hashing a style attribute requires 'unsafe-hashes', which is its own
 *     weakening rather than a fix;
 *   - style-src-attr would cover them, except Firefox has never
 *     implemented it and falls back to style-src - which would strip every
 *     severity colour on the site in that browser.
 *
 * So the *value* travels as a data attribute and the mapping lives here.
 * config/severity_palette.py is still the single source of the colours
 * themselves: every token below comes from /theme.css, which it generates.
 *
 * The same shape also works inside an HTMX swap, which a nonce would not:
 * a fragment is inserted into a document whose CSP carries the nonce from
 * the ORIGINAL response, so a <style nonce> minted for the fragment would
 * never match and would be blocked.
 */

/* A band, as a colour. The bare selector comes first so an unrecognised
   slug reads as Informational - the value rules below have identical
   specificity and win on document order. */
/* Two tokens per band: the canonical hue for borders and fills, and the
   contrast-corrected one for text that sits on a wash of that same hue.
   See webui/theme.py _contrast_tokens. */
[data-sev]            { --sev-color: var(--sev-info); --sev-on-wash: var(--sev-info-on-wash); }
[data-sev="critical"] { --sev-color: var(--sev-critical); --sev-on-wash: var(--sev-critical-on-wash); }
[data-sev="high"] { --sev-color: var(--sev-high); --sev-on-wash: var(--sev-high-on-wash); }
[data-sev="medium"] { --sev-color: var(--sev-medium); --sev-on-wash: var(--sev-medium-on-wash); }
[data-sev="low"] { --sev-color: var(--sev-low); --sev-on-wash: var(--sev-low-on-wash); }
[data-sev="info"] { --sev-color: var(--sev-info); --sev-on-wash: var(--sev-info-on-wash); }
/* Exigent is a flag layered on Critical, never a sixth band. It reaches
   this map only where a flag legitimately owns the colour - a card's
   escalated hairline, a recommended action - never as a band readout and
   never as a slice of a distribution. */
[data-sev="exigent"]  { --sev-color: var(--exigent); --sev-on-wash: var(--sev-exigent-on-wash); }

/* Text set in the band's colour: a score, a band name, a severity cell. */
/* Text, so it takes the contrast-corrected twin. Falls back to the
   canonical hue where an ancestor set only --sev-color. */
.sev-text { color: var(--sev-on-wash, var(--sev-color)); }

/* A distribution segment's colour. Two families reach it: a severity
   distribution names a band, and the CISA-deadline distribution names a
   surface accent - its buckets are not severities, and colouring them as
   severities would say something untrue about them. One attribute covers
   both so the macro does not have to know which kind it was handed. */
[data-tone="critical"] { --tone-color: var(--sev-critical); }
[data-tone="high"]     { --tone-color: var(--sev-high); }
[data-tone="medium"]   { --tone-color: var(--sev-medium); }
[data-tone="low"]      { --tone-color: var(--sev-low); }
[data-tone="info"]     { --tone-color: var(--sev-info); }
[data-tone="exigent"]  { --tone-color: var(--exigent); }
[data-tone="a1"]       { --tone-color: var(--a1); }
[data-tone="a2"]       { --tone-color: var(--a2); }
[data-tone="a3"]       { --tone-color: var(--a3); }
[data-tone="a4"]       { --tone-color: var(--a4); }
[data-tone="muted"]    { --tone-color: var(--muted); }

/* A section marker's rail, and the accent a sequential chart ramps toward.
   One attribute sets both: a heading never reads --chart-accent and a
   chart never reads --section-color, so there is nothing to collide. */
[data-accent="a1"]       { --section-color: var(--a1); --chart-accent: var(--a1); }
[data-accent="a2"]       { --section-color: var(--a2); --chart-accent: var(--a2); }
[data-accent="a3"]       { --section-color: var(--a3); --chart-accent: var(--a3); }
[data-accent="a4"]       { --section-color: var(--a4); --chart-accent: var(--a4); }
/* The one heading that takes a severity colour rather than a surface
   accent: the Priority feed when it is cut to Exigent. */
[data-accent="critical"] { --section-color: var(--sev-critical); --chart-accent: var(--sev-critical); }

/* The recommended action. --recommend-ink is black or white, whichever
   clears WCAG AA on that fill; it is resolved in
   config/severity_palette.py and emitted by /theme.css, never chosen here
   by eye. Only the hero card's filled button reads it. */
[data-priority="critical"] { --recommend-color: var(--sev-critical); --recommend-ink: var(--ink-critical); --recommend-on-wash: var(--sev-critical-on-wash); }
[data-priority="high"]     { --recommend-color: var(--sev-high);     --recommend-ink: var(--ink-high); --recommend-on-wash: var(--sev-high-on-wash); }
[data-priority="medium"]   { --recommend-color: var(--sev-medium);   --recommend-ink: var(--ink-medium); --recommend-on-wash: var(--sev-medium-on-wash); }
[data-priority="low"]      { --recommend-color: var(--sev-low);      --recommend-ink: var(--ink-low); --recommend-on-wash: var(--sev-low-on-wash); }
[data-priority="info"]     { --recommend-color: var(--sev-info);     --recommend-ink: var(--ink-info); --recommend-on-wash: var(--sev-info-on-wash); }
[data-priority="exigent"]  { --recommend-color: var(--exigent);      --recommend-ink: var(--ink-exigent); }

/* A card whose hairline escalates. --tint-color is deliberately its own
   variable rather than a reuse of --sev-color: the card is an ancestor of
   a score block and a recommended action that carry their own bands, and
   a tint colour inherited into them would be a colour nobody asked for. */
[data-tint="critical"] { --tint-color: var(--sev-critical); }
[data-tint="high"]     { --tint-color: var(--sev-high); }
[data-tint="medium"]   { --tint-color: var(--sev-medium); }
[data-tint="low"]      { --tint-color: var(--sev-low); }
[data-tint="info"]     { --tint-color: var(--sev-info); }
[data-tint="exigent"]  { --tint-color: var(--exigent); }
[data-tint] { --card-border: color-mix(in srgb, var(--tint-color, var(--border)) var(--sev-edge), var(--border)); }

/* A proportion, 0-100, generated below rather than hand-written.
 *
 * One scale does three jobs because all three are the same ratio:
 *   --pct      a width      - a chart bar's fill, a progress bar
 *   --pct-num  the same number, unitless - a stacked segment's flex-grow
 *   --pct-ramp round(44 + 0.56 * pct) - where a sequential chart bar sits
 *              on its accent's ramp, the curve the templates used to
 *              compute inline as (44 + 56 * value / top)%
 *
 * Quantised to whole percent. The widest bar track on the site is a few
 * hundred pixels, so half a percent is under a pixel, and every bar
 * prints its exact value beside it. flex-grow ratios renormalise, so
 * rounding cannot leave a stacked bar short of full width.
 */
[data-pct="0"] { --pct: 0%; --pct-num: 0; --pct-ramp: 44%; }
[data-pct="1"] { --pct: 1%; --pct-num: 1; --pct-ramp: 45%; }
[data-pct="2"] { --pct: 2%; --pct-num: 2; --pct-ramp: 45%; }
[data-pct="3"] { --pct: 3%; --pct-num: 3; --pct-ramp: 46%; }
[data-pct="4"] { --pct: 4%; --pct-num: 4; --pct-ramp: 46%; }
[data-pct="5"] { --pct: 5%; --pct-num: 5; --pct-ramp: 47%; }
[data-pct="6"] { --pct: 6%; --pct-num: 6; --pct-ramp: 47%; }
[data-pct="7"] { --pct: 7%; --pct-num: 7; --pct-ramp: 48%; }
[data-pct="8"] { --pct: 8%; --pct-num: 8; --pct-ramp: 48%; }
[data-pct="9"] { --pct: 9%; --pct-num: 9; --pct-ramp: 49%; }
[data-pct="10"] { --pct: 10%; --pct-num: 10; --pct-ramp: 50%; }
[data-pct="11"] { --pct: 11%; --pct-num: 11; --pct-ramp: 50%; }
[data-pct="12"] { --pct: 12%; --pct-num: 12; --pct-ramp: 51%; }
[data-pct="13"] { --pct: 13%; --pct-num: 13; --pct-ramp: 51%; }
[data-pct="14"] { --pct: 14%; --pct-num: 14; --pct-ramp: 52%; }
[data-pct="15"] { --pct: 15%; --pct-num: 15; --pct-ramp: 52%; }
[data-pct="16"] { --pct: 16%; --pct-num: 16; --pct-ramp: 53%; }
[data-pct="17"] { --pct: 17%; --pct-num: 17; --pct-ramp: 54%; }
[data-pct="18"] { --pct: 18%; --pct-num: 18; --pct-ramp: 54%; }
[data-pct="19"] { --pct: 19%; --pct-num: 19; --pct-ramp: 55%; }
[data-pct="20"] { --pct: 20%; --pct-num: 20; --pct-ramp: 55%; }
[data-pct="21"] { --pct: 21%; --pct-num: 21; --pct-ramp: 56%; }
[data-pct="22"] { --pct: 22%; --pct-num: 22; --pct-ramp: 56%; }
[data-pct="23"] { --pct: 23%; --pct-num: 23; --pct-ramp: 57%; }
[data-pct="24"] { --pct: 24%; --pct-num: 24; --pct-ramp: 57%; }
[data-pct="25"] { --pct: 25%; --pct-num: 25; --pct-ramp: 58%; }
[data-pct="26"] { --pct: 26%; --pct-num: 26; --pct-ramp: 59%; }
[data-pct="27"] { --pct: 27%; --pct-num: 27; --pct-ramp: 59%; }
[data-pct="28"] { --pct: 28%; --pct-num: 28; --pct-ramp: 60%; }
[data-pct="29"] { --pct: 29%; --pct-num: 29; --pct-ramp: 60%; }
[data-pct="30"] { --pct: 30%; --pct-num: 30; --pct-ramp: 61%; }
[data-pct="31"] { --pct: 31%; --pct-num: 31; --pct-ramp: 61%; }
[data-pct="32"] { --pct: 32%; --pct-num: 32; --pct-ramp: 62%; }
[data-pct="33"] { --pct: 33%; --pct-num: 33; --pct-ramp: 62%; }
[data-pct="34"] { --pct: 34%; --pct-num: 34; --pct-ramp: 63%; }
[data-pct="35"] { --pct: 35%; --pct-num: 35; --pct-ramp: 64%; }
[data-pct="36"] { --pct: 36%; --pct-num: 36; --pct-ramp: 64%; }
[data-pct="37"] { --pct: 37%; --pct-num: 37; --pct-ramp: 65%; }
[data-pct="38"] { --pct: 38%; --pct-num: 38; --pct-ramp: 65%; }
[data-pct="39"] { --pct: 39%; --pct-num: 39; --pct-ramp: 66%; }
[data-pct="40"] { --pct: 40%; --pct-num: 40; --pct-ramp: 66%; }
[data-pct="41"] { --pct: 41%; --pct-num: 41; --pct-ramp: 67%; }
[data-pct="42"] { --pct: 42%; --pct-num: 42; --pct-ramp: 68%; }
[data-pct="43"] { --pct: 43%; --pct-num: 43; --pct-ramp: 68%; }
[data-pct="44"] { --pct: 44%; --pct-num: 44; --pct-ramp: 69%; }
[data-pct="45"] { --pct: 45%; --pct-num: 45; --pct-ramp: 69%; }
[data-pct="46"] { --pct: 46%; --pct-num: 46; --pct-ramp: 70%; }
[data-pct="47"] { --pct: 47%; --pct-num: 47; --pct-ramp: 70%; }
[data-pct="48"] { --pct: 48%; --pct-num: 48; --pct-ramp: 71%; }
[data-pct="49"] { --pct: 49%; --pct-num: 49; --pct-ramp: 71%; }
[data-pct="50"] { --pct: 50%; --pct-num: 50; --pct-ramp: 72%; }
[data-pct="51"] { --pct: 51%; --pct-num: 51; --pct-ramp: 73%; }
[data-pct="52"] { --pct: 52%; --pct-num: 52; --pct-ramp: 73%; }
[data-pct="53"] { --pct: 53%; --pct-num: 53; --pct-ramp: 74%; }
[data-pct="54"] { --pct: 54%; --pct-num: 54; --pct-ramp: 74%; }
[data-pct="55"] { --pct: 55%; --pct-num: 55; --pct-ramp: 75%; }
[data-pct="56"] { --pct: 56%; --pct-num: 56; --pct-ramp: 75%; }
[data-pct="57"] { --pct: 57%; --pct-num: 57; --pct-ramp: 76%; }
[data-pct="58"] { --pct: 58%; --pct-num: 58; --pct-ramp: 76%; }
[data-pct="59"] { --pct: 59%; --pct-num: 59; --pct-ramp: 77%; }
[data-pct="60"] { --pct: 60%; --pct-num: 60; --pct-ramp: 78%; }
[data-pct="61"] { --pct: 61%; --pct-num: 61; --pct-ramp: 78%; }
[data-pct="62"] { --pct: 62%; --pct-num: 62; --pct-ramp: 79%; }
[data-pct="63"] { --pct: 63%; --pct-num: 63; --pct-ramp: 79%; }
[data-pct="64"] { --pct: 64%; --pct-num: 64; --pct-ramp: 80%; }
[data-pct="65"] { --pct: 65%; --pct-num: 65; --pct-ramp: 80%; }
[data-pct="66"] { --pct: 66%; --pct-num: 66; --pct-ramp: 81%; }
[data-pct="67"] { --pct: 67%; --pct-num: 67; --pct-ramp: 82%; }
[data-pct="68"] { --pct: 68%; --pct-num: 68; --pct-ramp: 82%; }
[data-pct="69"] { --pct: 69%; --pct-num: 69; --pct-ramp: 83%; }
[data-pct="70"] { --pct: 70%; --pct-num: 70; --pct-ramp: 83%; }
[data-pct="71"] { --pct: 71%; --pct-num: 71; --pct-ramp: 84%; }
[data-pct="72"] { --pct: 72%; --pct-num: 72; --pct-ramp: 84%; }
[data-pct="73"] { --pct: 73%; --pct-num: 73; --pct-ramp: 85%; }
[data-pct="74"] { --pct: 74%; --pct-num: 74; --pct-ramp: 85%; }
[data-pct="75"] { --pct: 75%; --pct-num: 75; --pct-ramp: 86%; }
[data-pct="76"] { --pct: 76%; --pct-num: 76; --pct-ramp: 87%; }
[data-pct="77"] { --pct: 77%; --pct-num: 77; --pct-ramp: 87%; }
[data-pct="78"] { --pct: 78%; --pct-num: 78; --pct-ramp: 88%; }
[data-pct="79"] { --pct: 79%; --pct-num: 79; --pct-ramp: 88%; }
[data-pct="80"] { --pct: 80%; --pct-num: 80; --pct-ramp: 89%; }
[data-pct="81"] { --pct: 81%; --pct-num: 81; --pct-ramp: 89%; }
[data-pct="82"] { --pct: 82%; --pct-num: 82; --pct-ramp: 90%; }
[data-pct="83"] { --pct: 83%; --pct-num: 83; --pct-ramp: 90%; }
[data-pct="84"] { --pct: 84%; --pct-num: 84; --pct-ramp: 91%; }
[data-pct="85"] { --pct: 85%; --pct-num: 85; --pct-ramp: 92%; }
[data-pct="86"] { --pct: 86%; --pct-num: 86; --pct-ramp: 92%; }
[data-pct="87"] { --pct: 87%; --pct-num: 87; --pct-ramp: 93%; }
[data-pct="88"] { --pct: 88%; --pct-num: 88; --pct-ramp: 93%; }
[data-pct="89"] { --pct: 89%; --pct-num: 89; --pct-ramp: 94%; }
[data-pct="90"] { --pct: 90%; --pct-num: 90; --pct-ramp: 94%; }
[data-pct="91"] { --pct: 91%; --pct-num: 91; --pct-ramp: 95%; }
[data-pct="92"] { --pct: 92%; --pct-num: 92; --pct-ramp: 96%; }
[data-pct="93"] { --pct: 93%; --pct-num: 93; --pct-ramp: 96%; }
[data-pct="94"] { --pct: 94%; --pct-num: 94; --pct-ramp: 97%; }
[data-pct="95"] { --pct: 95%; --pct-num: 95; --pct-ramp: 97%; }
[data-pct="96"] { --pct: 96%; --pct-num: 96; --pct-ramp: 98%; }
[data-pct="97"] { --pct: 97%; --pct-num: 97; --pct-ramp: 98%; }
[data-pct="98"] { --pct: 98%; --pct-num: 98; --pct-ramp: 99%; }
[data-pct="99"] { --pct: 99%; --pct-num: 99; --pct-ramp: 99%; }
[data-pct="100"] { --pct: 100%; --pct-num: 100; --pct-ramp: 100%; }

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

/* flex-start so the sticky rail sits at the top of the row rather than
   stretching to the page height. At narrow widths this becomes a column,
   where the same value would size each child to its own content width -
   see the stretch override in the narrow media query, which is what kept
   a 3,215px comparison table dragging the whole page sideways. */
.shell { display: flex; min-height: 100vh; align-items: flex-start; }

.rail {
  flex: 0 0 var(--rail);
  width: var(--rail);
  position: sticky;
  top: 0;
  /* Review finding M4: the Streamlit sidebar clipped its own contents and
     would not scroll. This one is exactly the viewport tall - so its
     surface never stops halfway down the page - and scrolls internally
     when its own contents outgrow that. */
  height: 100vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 22px;
  padding: 22px 18px 28px;
  background: var(--card);
  border-right: 1px solid var(--border);
}

.brand { display: block; }
.brand img { display: block; width: 100%; height: auto; }

/* Ivory only. The logotype is metallic, and on the one light preset its
   glyphs sit too close to the cream page to read cleanly; a warm plate
   behind them restores the separation the dark presets get for free.
   Deliberately not applied to all four - retro, crimson and emerald do
   not have the problem, and a plate there would be a change to three
   themes to fix one. Tokens rather than literals, so a retuned ivory
   palette carries this with it. */
:root[data-theme="ivory"] .brand {
  background: var(--card-strong);
  border: 1px solid var(--border);
  border-radius: var(--radius-input);
  padding: 11px 13px;
}

.nav ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 3px; }

/* A row is now a label with an optional quiet qualifier under it, so the
   column stacks rather than spreading. justify-content: space-between only
   ever mattered for the "soon" badge, which keeps it below. */
/* The start of a new group, and it has to be seen as one.

   Two earlier attempts failed for the same reason: both stayed inside
   the grey ramp the links already use, so brighter grey read as a more
   important link and dimmer grey read as a link nobody wanted. Grey
   cannot say "different kind of thing" to a row of grey links - only
   hue can.

   --nav-group is its own token per preset (see webui/theme.py) rather
   than a reused accent, because every existing accent fails contrast in
   at least one theme. Bigger than before, too: this is a heading. */
.nav-head {
  font-family: var(--font-data);
  font-size: 11.5px; font-weight: 700;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--nav-group);
  margin: 20px 0 7px; padding-left: 11px;
}
/* First group in the rail, now a <p> before the first <ul> rather than
   the list's first child. */
.nav > .nav-head:first-child { margin-top: 0; }

/* Above 900px the disclosure is machinery, not furniture: no control, no
   box, the panel flowing exactly where the nav used to. */
.railmenu { border: 0; background: none; margin: 0; padding: 0; }
.railmenu > .menu-toggle { display: none; }
.railmenu > .menu-panel { display: block; }
.nav-qual {
  font-size: 11px; font-weight: 400; color: var(--muted);
  line-height: 1.3;
}
.nav-item b { font-weight: 500; }
.nav-item.is-active b { font-weight: 600; }

.nav-item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  padding: 8px 11px;
  border-radius: var(--radius-input);
  border: 1px solid transparent;
  font-size: 13.5px;
  text-decoration: none;
  color: var(--text);
}
.nav-item:hover { background: var(--card-strong); }
.nav-item.is-active {
  background: var(--card-strong);
  border-color: var(--a1);
  color: var(--a1);
  font-weight: 600;
}
.nav-item.is-pending { color: var(--muted); cursor: default; }
.nav-item.is-pending:hover { background: none; }
.nav-item.is-pending { flex-direction: row; align-items: center; justify-content: space-between; gap: 8px; }
.nav-item em {
  font-style: normal;
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 1px 5px;
}

.rail-foot { margin-top: auto; display: grid; gap: 7px; }
/* Below the top-bar breakpoint the theme picker is a labelled select with
   its note, exactly as before - the disclosure around it shows no control
   and no box, the same arrangement .railmenu has above 900px. */
.thememenu { border: 0; background: none; padding: 0; }
.thememenu > .theme-toggle { display: none; }
.thememenu > .theme-panel { display: grid; gap: 7px; }
.rail-note { margin: 0; font-size: 11.5px; color: var(--muted); }

.main {
  flex: 1 1 auto;
  /* Without this a flex item refuses to shrink below its content, and one
     wide table takes the page with it. */
  min-width: 0;
  padding: 30px clamp(18px, 3vw, 44px) 64px;
  /* Measured on a 2560px display: at the old 1500px this left a 792px empty
     gutter to the right of the content while the sidebar stayed pinned left.
     2100px takes that to 192px. The cap does not go away entirely - an
     uncapped column is how a comparison table ends up 2,000px wide with a
     row of pills adrift at either end - and centring the column instead was
     worse than either, because the sidebar stays pinned left and the gap
     opens up between the nav and the page rather than at the edge. */
  max-width: var(--main-cap);
}

/* --------------------------------------------------------------- typography */

h1, h2, h3 { font-weight: 600; letter-spacing: -0.01em; }
h1 { font-size: clamp(26px, 3vw, 34px); margin: 0 0 8px; }
h2 { font-size: 20px; margin: 0 0 10px; }
h3 { font-size: 15.5px; margin: 0 0 8px; }

.hero { margin-bottom: 22px; }
.standfirst { margin: 0 0 6px; max-width: 74ch; color: var(--text); }
.release-meta {
  margin: 0;
  font-family: var(--font-data);
  font-size: 12.5px;
  color: var(--muted);
}

/* Section markers: the coloured rail the Streamlit build drew per section,
   redrawn as a border rather than three nested spans. */
.section { margin: 34px 0; }
.section-head {
  border-left: 3px solid var(--section-color, var(--a1));
  padding-left: 12px;
}

.panel {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px 20px;
  margin: 0 0 22px;
}

.note {
  border: 1px solid var(--border);
  border-left: 3px solid var(--muted);
  border-radius: var(--radius-input);
  padding: 10px 14px;
  margin: 12px 0;
  font-size: 13.5px;
}
.note-warn { border-left-color: var(--sev-high); background: color-mix(in srgb, var(--sev-high) var(--sev-wash), var(--card)); }
.note-muted { color: var(--muted); }

.empty-state { text-align: center; padding: 60px 20px; }

/* ------------------------------------------------------------------- kpis */

.kpis {
  list-style: none; margin: 14px 0 0; padding: 0;
  display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}
.kpis-3 { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
.kpis-4 { grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }
.kpis li {
  background: var(--card-strong);
  border: 1px solid var(--border);
  border-radius: var(--radius-input);
  padding: 14px 16px 13px;
  /* A column with the footnote pushed to the bottom, so tiles whose notes are
     different lengths still line their labels up. Grid rows did not: the row
     read as ragged rather than as a set. */
  display: flex; flex-direction: column; gap: 3px;
  position: relative;
}
.kpis li::before {
  /* A short accent rule at the top of each tile - the same device the section
     headings use - so the row reads as deliberate rather than as four boxes. */
  content: ""; position: absolute; top: -1px; left: 16px; width: 26px; height: 2px;
  background: var(--a1); border-radius: 0 0 2px 2px;
}

/* One tile in the row is a call to action rather than a count. A tint alone is
   invisible at 9%, so the signal is carried by the accent rule and the number. */
.kpis li.kpi-alert {
  border-color: color-mix(in srgb, var(--sev-critical) 45%, var(--border));
  background:
    linear-gradient(color-mix(in srgb, var(--sev-critical) 10%, transparent), transparent 72%),
    var(--card-strong);
}
.kpis li.kpi-alert::before { background: var(--sev-critical); width: 44px; height: 3px; }
.kpis li.kpi-alert .kpi-value { color: var(--sev-critical); }
.kpis li .kpi-sub { margin-top: auto; padding-top: 6px; }

/* Each tile is a filter on the feed below it. The <a> wraps only the number
   and the label so the accessible name is "Exigent - 35 entries" rather than
   the footnote as well, and a stretched ::after makes the whole tile the
   click target without nesting the footnote inside a link. */
.kpi-link {
  display: flex; flex-direction: column; gap: 3px;
  text-decoration: none; color: inherit;
}
.kpi-link::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
/* Only a tile that IS a link lifts on hover. Some rows mix the two - the
   vendor half's "Vendors" and "CVSS resolved" are facts about the set with
   no filter to map to - and a tile that brightens under the cursor and then
   does nothing is the reason those rows read as broken. */
.kpis li:has(.kpi-link):hover { border-color: var(--border-strong); }

/* Selected state, reusing .nav-item.is-active's treatment rather than a new
   one: accent border and accent label on a raised surface, no accent fill.
   A fill would repaint the Exigent tile's number and erase the severity
   channel - selection and severity are different axes and both have to stay
   readable when a reader has selected the Exigent tile. */
.kpis li.is-selected {
  border-color: var(--a1);
  background: var(--card-strong);
}
/* Weight as well as hue, completing the treatment .nav-item.is-active uses.
   It matters most in crimson, whose accent IS red: there the border and
   label alone lean on a colour that means Critical everywhere else, and the
   heavier label is a cue that does not depend on hue at all. Severity stays
   readable regardless, because it speaks through the figure and the accent
   rule, which selection never repaints. */
.kpis li.is-selected .kpi-label { color: var(--a1); font-weight: 600; }
/* Severity outranks selection on the tile that carries both: the accent rule
   and the figure stay critical, and only the border and label move. */
.kpis li.kpi-alert.is-selected::before { background: var(--sev-critical); }
.kpis li.kpi-alert.is-selected .kpi-value { color: var(--sev-critical); }

.kpi-value {
  /* The UI face, not --font-data: IBM Plex Mono is monospaced, so its comma
     occupies a full character cell and 1,747 sets as "1 , 747". Plex Sans gives
     the comma half a digit's advance, which is the actual fix. */
  font-family: var(--font-ui);
  font-size: 36px; font-weight: 600; line-height: 1.05;
  letter-spacing: -0.015em;
  color: var(--text);
}

.kpi-label {
  font-size: 14px; font-weight: 600; color: var(--text);
  letter-spacing: 0.01em;
}
/* The tile's own footnote. A headline number on this dataset is easy to
   misread without one - see webui/dashboard_view.py's module docstring. */
.kpi-sub {
  /* Weight, not just size and colour: the UI face is a variable font
     covering 400-700, so 500 costs no extra file and no extra request. Size and
     colour had both been raised already; "too thin" was always about
     stroke weight. --muted-strong rather than --muted - this is copy meant
     to be read, just not read first. */
  font-size: 14.5px; font-weight: 500;
  color: var(--muted-strong);
  line-height: 1.5; margin-top: 4px;
}

/* ---------------------------------------------------------------- toggles */

.toggle-row { display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 12px; }
.toggle {
  display: inline-block;
  padding: 6px 13px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  font-size: 13px;
  text-decoration: none;
  color: var(--muted);
  background: var(--card);
}
.toggle:hover { border-color: var(--border-strong); color: var(--text); }
.toggle.is-on {
  background: var(--a1);
  border-color: var(--a1);
  color: var(--on-accent);
  font-weight: 600;
}

.button {
  display: inline-block;
  padding: 8px 15px;
  border: 1px solid var(--a1);
  border-radius: var(--radius-input);
  background: var(--a1);
  color: var(--on-accent);
  font: inherit;
  font-size: 13.5px;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}
.button:hover { filter: brightness(1.08); }

.control {
  width: 100%;
  padding: 7px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-input);
  background: var(--card-strong);
  color: var(--text);
  font: inherit;
  font-size: 13.5px;
}
.field-label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 4px; }

/* ------------------------------------------------------------------ cards */

/* Two up, one up on narrow screens. Several priority CVEs stay scannable
   at once without excess scrolling, and the card's own two-column body
   (score block beside the reasoning) keeps room to breathe - which a third
   column on a wide monitor takes away. */
.card-grid {
  display: grid; gap: 14px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.card {
  background: var(--card);
  border: 1px solid var(--card-border, var(--border));
  border-radius: var(--radius);
  padding: 15px 17px;
}

.finding-head { display: flex; justify-content: space-between; gap: 10px; align-items: flex-start; }
.finding-id { display: flex; align-items: center; gap: 8px; }
/* Rank marker. Four surfaces show one - the Patch Tuesday and Cross-Vendor
   cards through finding_card(), the dashboard priority feed, and the
   remediation actions on /analysis - and they used to disagree: the cards
   wrote the "#" in the template, the feed row generated it in CSS, and the
   two were different sizes. The disc IS the marker, so the "#" is gone from
   both and the shape is shared, with only the scale differing.

   Not a fixed-diameter circle: ranks on /analysis run past 100, so this is a
   pill whose min-width equals its height. It is circular at one and two
   digits and stretches at three rather than clipping. tabular-nums keeps the
   figures the same width so a column of discs does not jitter.

   The fill is derived from --text rather than named per preset, so it stays a
   step above the card surface in all four themes without a second palette.
   Rank carries no severity meaning and takes no severity colour: that channel
   belongs to the score beside it. */
.rank, .feed-rank {
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--font-ui);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  line-height: 1;
  color: var(--text);
  background: color-mix(in srgb, var(--text) 12%, var(--card-strong));
  border-radius: 999px;
}
.rank { font-size: 12px; min-width: 22px; height: 22px; padding: 0 6px; }
.cve { font-family: var(--font-data); font-size: 14px; font-weight: 600; }
/* The product on a remediation card. Same slot and weight as .cve so the
   card head does not reflow, but the UI face: a product name is a name,
   and monospace is for identifiers a person copies. */
.finding-product { font-family: var(--font-ui); font-size: 14px; font-weight: 600; }
.finding-flags { display: flex; flex-wrap: wrap; gap: 5px; justify-content: flex-end; }
.finding-title { font-size: 14.5px; font-weight: 500; margin: 8px 0 12px; }
.finding-body { display: grid; grid-template-columns: 148px 1fr; gap: 14px; align-items: start; }
.finding-meta { display: grid; gap: 9px; justify-items: start; }
.finding-why { margin: 0; font-size: 13px; }

.chips, .source-chips { display: flex; flex-wrap: wrap; gap: 5px; }

.chip {
  display: inline-block;
  padding: 2px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  font-size: 11.5px;
  color: var(--muted);
  background: var(--card-strong);
}
.chip b { font-family: var(--font-data); color: var(--text); font-weight: 600; }
/* Which search term produced a row. Deliberately not a severity or source
   colour - it is provenance, not a property of the vulnerability. */
.chip-term {
  border-color: color-mix(in srgb, var(--a1) 45%, var(--border));
  color: var(--a1);
}

/* Flag chips carry severity meaning, so they take severity colour - never
   an accent, which changes with the theme. */
.chip-exigent  { color: var(--sev-exigent-on-wash);     border-color: var(--exigent);     background: color-mix(in srgb, var(--exigent) var(--sev-wash), var(--card)); font-weight: 700; }
.chip-exploited { color: var(--sev-critical-on-wash); border-color: var(--sev-critical); background: color-mix(in srgb, var(--sev-critical) var(--sev-wash), var(--card)); font-weight: 600; }
.chip-kev      { color: var(--sev-high-on-wash);    border-color: var(--sev-high);    background: color-mix(in srgb, var(--sev-high) var(--sev-wash), var(--card)); font-weight: 600; }
.chip-disclosed { color: var(--sev-medium-on-wash); border-color: var(--sev-medium);  background: color-mix(in srgb, var(--sev-medium) var(--sev-wash), var(--card)); font-weight: 600; }

/* Deadline chips take the accent ramp, never a severity token: a schedule
   state is not an EVRS band, and painting it in band hues would invite
   reading it as one. "Due soon" gets the primary accent rather than
   "overdue" on purpose - past-deadline is the KEV catalog's baseline
   condition, an imminent deadline is the rare actionable one. */
.chip-deadline { color: var(--muted); border-color: var(--border-strong); }
/* Which half of the feed an item came from - structure, not status. */
.chip-source { color: var(--muted); border-color: var(--border-strong); }
.chip-overdue  { color: var(--a2-on-wash, var(--a2)); border-color: var(--a2); background: color-mix(in srgb, var(--a2) 14%, var(--card)); font-weight: 600; }
.chip-due-soon { color: var(--a1); border-color: var(--a1); background: color-mix(in srgb, var(--a1) 14%, var(--card)); font-weight: 700; }

.source-chip {
  display: inline-block;
  padding: 3px 9px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  font-size: 11.5px;
  text-decoration: none;
  color: var(--text);
  background: var(--card-strong);
}
.source-chip:hover { border-color: var(--a1); color: var(--a1); }

.recommend {
  display: inline-block;
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--recommend-on-wash, var(--recommend-color));
  border: 1px solid var(--recommend-color);
  background: color-mix(in srgb, var(--recommend-color) var(--sev-wash), var(--card));
}

/* ------------------------------------------------------------ score block */

.score {
  --risk-color: var(--sev-color);
  --risk-on-wash: var(--sev-on-wash, var(--sev-color));
  display: grid;
  gap: 1px;
  padding: 10px 12px;
  border: 1px solid var(--risk-color);
  border-left-width: 3px;
  border-radius: var(--radius-input);
  background: color-mix(in srgb, var(--risk-color) var(--sev-wash), var(--card));
}
.score-label { font-size: 10.5px; letter-spacing: 0.05em; text-transform: uppercase; color: var(--muted); }
.score-value { font-family: var(--font-data); font-size: 27px; font-weight: 600; line-height: 1.1; color: var(--risk-on-wash, var(--risk-color)); }
.score-value small { font-size: 12px; font-weight: 500; color: var(--muted); }
.score-band { font-size: 12.5px; font-weight: 600; color: var(--risk-on-wash, var(--risk-color)); }
.score-note { font-size: 10.5px; color: var(--muted); margin-top: 4px; }

/* ----------------------------------------------------------------- charts */

.chart-grid {
  display: grid; gap: 20px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin-top: 14px;
}

.chart {
  margin: 0;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px 16px;
}
.chart figcaption { margin-bottom: 10px; }
.chart figcaption h3 { margin: 0; }
.chart-axis { margin: 10px 0 0; font-size: 11px; color: var(--muted); letter-spacing: 0.03em; }

.bars { list-style: none; margin: 0; padding: 0; display: grid; gap: 5px; }
.bar-row {
  display: grid;
  grid-template-columns: minmax(90px, 34%) 1fr auto;
  align-items: center;
  gap: 10px;
  padding: 3px 5px;
  border-radius: var(--radius-pill);
  text-decoration: none;
  color: var(--text);
}
.bar-row:hover { background: var(--card-strong); }
.bar-label {
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bar-track { height: 11px; background: var(--card-strong); border-radius: var(--radius-token); overflow: hidden; }
/* Width from data-pct. Colour does one of two jobs and never decoration:
   a severity chart gives each bar its own band via data-sev, and every
   other chart ramps one accent by magnitude - blended toward the card so
   it stays opaque and monotonic. */
.bar-fill {
  display: block;
  height: 100%;
  width: var(--pct, 0%);
  border-radius: var(--radius-token);
  background: color-mix(in srgb, var(--chart-accent, var(--a1)) var(--pct-ramp, 100%), var(--card));
}
.bar-fill[data-sev] { background: var(--sev-color); }
.bar-value { font-family: var(--font-data); font-size: 12px; color: var(--muted); }

/* -------------------------------------------------------------- feed hero */

/* #1 gets the room its rank claims; everything under it is a row. The
   previous version rendered all ten as identical cards in a 2-up grid,
   where the only thing separating first from sixth was a two-digit
   number - so nothing separated them. */
.hero-card {
  display: grid;
  grid-template-columns: 108px 190px 1fr;
  gap: 20px;
  align-items: start;
  padding: 20px 22px;
  margin-bottom: 18px;
}

.hero-rank { display: grid; gap: 2px; }
.hero-rank-num {
  font-family: var(--font-data);
  font-size: 46px;
  font-weight: 600;
  line-height: 1;
  color: var(--muted);
}
.hero-rank-num::before { content: "#"; font-size: 26px; }
.hero-rank-label {
  font-size: 10.5px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
}

.hero-score {
  --risk-color: var(--sev-color);
  --risk-on-wash: var(--sev-on-wash, var(--sev-color));
  display: grid;
  gap: 1px;
  padding: 13px 15px;
  border: 1px solid var(--risk-color);
  border-left-width: 4px;
  border-radius: var(--radius-input);
  background: color-mix(in srgb, var(--risk-color) var(--sev-wash), var(--card));
}
.hero-score-value {
  font-family: var(--font-data);
  font-size: 52px;
  font-weight: 600;
  line-height: 1.05;
  color: var(--risk-on-wash, var(--risk-color));
}
.hero-score-value small { font-size: 17px; font-weight: 500; color: var(--muted); }
.hero-score-band {
  font-size: 15px;
  font-weight: 700;
  color: var(--risk-on-wash, var(--risk-color));
  margin-bottom: 3px;
}

.hero-main { display: grid; gap: 10px; justify-items: start; }
.hero-head {
  display: flex; flex-wrap: wrap; gap: 10px;
  align-items: center; justify-content: space-between;
  width: 100%;
}
.hero-cve { font-size: 17px; }
.hero-title { font-size: 18px; font-weight: 600; margin: 0; line-height: 1.35; }
.hero-why { margin: 0; font-size: 14px; max-width: 88ch; }

.hero-actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: 14px;
  margin-top: 2px;
}
/* A button rather than a pill: on the top item this is the one thing the
   reader is meant to do next. */
.action-button {
  display: inline-block;
  padding: 9px 20px;
  border-radius: var(--radius-input);
  border: 1px solid var(--recommend-color);
  background: var(--recommend-color);
  /* Black or white, whichever clears AA on this band's fill - resolved in
     webui/evrs_view.py from the severity palette, not chosen by eye. */
  color: var(--recommend-ink);
  font-size: 13.5px;
  font-weight: 700;
  letter-spacing: 0.02em;
}

/* -------------------------------------------------------------- feed list */


/* ------------------------------------------------------------ search page */

.search-form {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px 16px;
  margin: 16px 0;
}
.search-row { display: flex; flex-wrap: wrap; gap: 10px; }
.search-row .control { flex: 1 1 320px; }

.search-result { margin: 0 0 14px; }
.search-body {
  display: grid;
  grid-template-columns: 160px 1fr;
  gap: 16px;
  align-items: start;
  margin-top: 12px;
}
.search-main { min-width: 0; }
.search-main p { margin: 0 0 8px; font-size: 13.5px; }
.search-actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin: 12px 0;
}
.research-links { margin: 0; padding-left: 18px; font-size: 13px; }
.research-links li { margin-bottom: 4px; }

/* --------------------------------------------------------------- watchlist */

.watchlist-head {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between; gap: 10px;
}
.watchlist-head h2 { margin: 0; }
.watchlist-actions { display: flex; flex-wrap: wrap; gap: 8px; }

.wl-export-btn, .wl-icon-btn, .wl-save-btn {
  background: var(--card-strong);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-input);
  font: inherit;
  cursor: pointer;
}
.wl-export-btn { padding: 5px 12px; font-size: 12.5px; color: var(--muted); }
.wl-export-btn:hover:not(:disabled) { color: var(--a1); border-color: var(--a1); }
.wl-export-btn:disabled { opacity: 0.45; cursor: default; }

.wl-save-btn { padding: 7px 14px; font-size: 13px; }
.wl-save-btn:hover { border-color: var(--a1); }
.wl-save-btn.is-saved { color: var(--a1); border-color: var(--a1); font-weight: 600; }

.wl-list { list-style: none; margin: 10px 0 0; padding: 0; max-height: 260px; overflow-y: auto; }
.wl-item {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 7px 0; border-top: 1px solid var(--border);
}
.wl-item:first-child { border-top: none; }
.wl-item-main { display: flex; flex-direction: column; min-width: 0; }
.wl-item-cve { font-weight: 600; font-size: 13px; }
.wl-item-meta {
  font-size: 11.5px; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 46ch;
}
.wl-item-actions { display: flex; gap: 6px; flex-shrink: 0; }
.wl-icon-btn { padding: 3px 9px; font-size: 11.5px; }
.wl-icon-btn:hover { border-color: var(--a1); color: var(--a1); }

/* ------------------------------------------------------- long filter lists */

/* Sixty-odd KEV vendors will not fit in a filter bar, so the list is a
   disclosure that says how many are selected while closed. */
.vendor-picker { flex: 1 1 100%; margin: 0; }
.check-grid {
  display: grid; gap: 4px 14px;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  max-height: 260px; overflow-y: auto;
  padding-bottom: 12px;
}

.clear-form { margin: 22px 0 8px; }

/* ------------------------------------------------------------- dns checker */

.dns-form { margin-bottom: 22px; }
.dns-form-grid {
  display: grid;
  grid-template-columns: 1.4fr 1fr;
  gap: 22px;
  align-items: start;
}
.dns-input { font-family: var(--font-data); font-size: 13px; resize: vertical; }

/* The Subnet Calculator's own two fields. The block goes in a column wide
   enough for an expanded IPv6 address without wrapping, and the divide
   control takes what is left rather than a share: a select holding
   "/26 - 4 blocks" needs about a third of what an address needs, and an
   even split would leave the address field short at the width where
   people actually use this page. */
.subnet-form-grid {
  display: grid;
  /* 3:1 rather than 2:1. The divide control is secondary to the field it
     depends on - there is nothing to divide until something is typed - and
     at 2fr/1fr it read as a peer. */
  grid-template-columns: minmax(0, 3fr) minmax(180px, 1fr);
  gap: 18px;
  align-items: start;
}
.subnet-ranges-field { margin-top: 18px; }

/* The panel that is an instrument rather than a container gets the accent
   rule the KPI tiles and section headings already use, so the page reads as
   one system instead of a form floating above some cards. */
.panel-tool { position: relative; }
.panel-tool::before {
  content: ""; position: absolute; top: -1px; left: 18px;
  width: 34px; height: 2px; background: var(--a1);
  border-radius: 0 0 2px 2px;
}

/* Values, not prose. Mono earns its place on an address and loses it on a
   sentence: the help line under this field used to be a paragraph set in a
   monospace, which is most of what made this panel read as a terminal. */
.subnet-input { font-family: var(--font-data); font-size: 14px; resize: vertical; }
#subnet-q { font-size: 16px; padding: 10px 12px; letter-spacing: 0.01em; }

/* The field and its prefix read as one control: a shared border, one focus
   ring, the divider between them rather than around them. Two separate boxes
   would say they were two separate questions, and they are one. */
.input-prefix {
  display: flex; align-items: stretch;
  border: 1px solid var(--border); border-radius: var(--radius-input);
  background: var(--card-strong);
  overflow: hidden;
}
.input-prefix:focus-within { border-color: var(--a1); }
.input-prefix .subnet-input {
  flex: 1 1 auto; min-width: 0;
  border: 0; border-radius: 0; background: transparent;
}
.input-prefix .subnet-input:focus-visible { outline: none; }
.prefix-select {
  flex: 0 0 auto; width: auto;
  border: 0; border-left: 1px solid var(--border); border-radius: 0;
  background: color-mix(in srgb, var(--text) 7%, var(--card-strong));
  font-family: var(--font-data); font-size: 14px;
  padding-right: 26px;
}
.prefix-select:focus-visible { outline: none; }

/* Subnet facts, upfront. Every value the calculator knows is on the page
   at full size rather than split between a headline row and a disclosure:
   a reader is here to take a value away - into a firewall rule, a ticket,
   a WHERE clause - and a value behind a toggle is a value they have to go
   looking for. Each sits alone on its own line in the mono face so a
   double-click selects the whole thing and nothing else.

   One flat grid rather than four labelled bands. The bands cost four
   headings and four forced row breaks; the grouping they carried is now
   the left rule's colour, on the same data-accent mechanism the section
   headings use, which costs no height at all. */
.fact-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(196px, 1fr));
  gap: 7px;
  margin-top: 14px;
}
.fact {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 7px 11px;
  border: 1px solid var(--border);
  border-left: 3px solid var(--section-color, var(--border));
  border-radius: var(--radius-input);
  background: var(--card);
}
.fact-label {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}
.fact-value {
  font-size: 14px;
  line-height: 1.35;
  color: var(--text);
  /* A range or a reverse-DNS zone is one token that must not push the
     grid wider than the panel. */
  overflow-wrap: anywhere;
}
.fact-value small { font-size: 0.72em; }
.fact-note {
  margin-top: auto;
  padding-top: 2px;
  font-size: 11px;
  line-height: 1.4;
  color: var(--muted-strong);
}
.fact-foot { margin: 9px 0 0; font-size: 12.5px; }
.range-counts { margin: 12px 0 0; font-size: 13.5px; color: var(--muted-strong); }
.range-counts b {
  font-family: var(--font-data); font-size: 15px; font-weight: 600; color: var(--text);
}

/* The pasted range list's answer and the split table are independent of
   one another, so they sit side by side wherever the column is wide
   enough for both. Stacked they cost 550px for two things read at a
   glance. Below that they fall back to one column on their own. */
.subnet-secondary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(430px, 1fr));
  gap: 0 26px;
  align-items: start;
}
/* Each half opens its own section, so the first one must not carry the
   top margin that separated them when they were stacked. */
.subnet-secondary > * > .section:first-child,
.subnet-secondary > .section:first-child { margin-top: 18px; }
.machine-formats p { margin: 12px 0 0; }

.field-examples {
  display: flex; flex-wrap: wrap; align-items: center; gap: 7px;
  margin-top: 9px;
}
.field-examples-label {
  font-size: 11px; font-weight: 600; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--muted); margin-right: 2px;
}
.example-chip {
  display: inline-flex; align-items: baseline; gap: 7px;
  padding: 5px 10px; text-decoration: none; color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-pill);
  background: var(--card-strong);
  font-size: 12.5px;
}
.example-chip:hover { border-color: var(--a1); }
.example-chip .mono { font-size: 12.5px; }
.example-chip em {
  font-style: normal; font-size: 11px; color: var(--muted);
}

/* Hierarchy through the labels too: the field is the instrument, the
   divide control is an option on it. */
.subnet-field .field-label { color: var(--text); font-weight: 600; }
.control-sm { font-size: 13px; padding: 6px 9px; }
.control-sm:disabled { opacity: 0.55; cursor: not-allowed; }

@media (max-width: 900px) {
  .subnet-form-grid { grid-template-columns: 1fr; }
}
.dns-settings { display: grid; gap: 14px; align-content: start; justify-items: start; }
.dns-settings .filter-group { gap: 8px; }

.progress {
  height: 8px;
  border-radius: var(--radius-token);
  background: var(--card-strong);
  overflow: hidden;
  margin: 12px 0 8px;
}
.progress-fill {
  display: block;
  height: 100%;
  /* 0 until a status partial supplies data-pct, or until upload.js sets
     the width through the CSSOM - which CSP does not govern. */
  width: var(--pct, 0%);
  background: var(--a1);
  border-radius: var(--radius-token);
  transition: width 0.3s ease;
}
@media (prefers-reduced-motion: reduce) { .progress-fill { transition: none; } }

/* -------------------------------------------------- vendor comparison pages */

.disclaimer > summary { color: var(--a2); }

.vendor-picker-row {
  display: flex; flex-wrap: wrap; gap: 6px;
  margin: 12px 0 16px;
}

/* The executive comparison tables carry paragraph-length cells - "best
   organizational fit" is a sentence, not a value - and the browser's auto
   layout answers that by making one column enormous and squeezing the
   prose into a ribbon. A minimum table width keeps the columns honest
   inside the scroller, a per-cell cap stops any one of them running away,
   and the prose cells clamp to five lines. The full text is never lost:
   it is in the profile below, which the row header links to. */
.compare-table { min-width: 1200px; }
.compare-table td, .compare-table th[scope="row"] { max-width: 270px; }
/* Row headers elsewhere are CVE ids and must not wrap; here they are
   product names and domains long enough to collide with the next column
   if they do not.
   `anywhere` rather than `break-word`: only `anywhere` also shrinks the
   cell's min-content width, which is what the table layout algorithm
   measures. With `break-word` a 41-character domain with no spaces in it
   kept its column at full width and simply spilled over the next one. */
/* .data-table.compare-table, not .compare-table: the base rule
   `.data-table th[scope="row"] { white-space: nowrap }` has the same
   specificity and sits later in this file, so a single class here loses on
   source order and the domain column keeps spilling over the next one. */
.data-table.compare-table th[scope="row"] {
  white-space: normal;
  /* `anywhere` stops a long unbroken identifier - a 41-character domain -
     spilling into the next column, but on its own it also drops the
     column's min-content width to one character and the layout algorithm
     duly collapses it. The floor is what keeps the break readable. */
  overflow-wrap: anywhere;
  min-width: 170px;
}
.cell-prose {
  display: -webkit-box;
  -webkit-line-clamp: 5;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.profile { padding: 20px 22px; }
.profile-name { font-size: 20px; margin: 0 0 2px; }

/* A definition list rather than a run of <p><b>Label:</b> …</p>. The label
   column stays a column, which is what makes forty fields scannable. */
.profile-fields {
  display: grid;
  grid-template-columns: minmax(160px, 240px) 1fr;
  gap: 8px 20px;
  margin: 16px 0;
  font-size: 13.5px;
}
.profile-fields dt { font-weight: 600; color: var(--muted); }
.profile-fields dd { margin: 0; }
.profile-note { display: block; margin-top: 3px; }

.kpi-value-sm { font-size: 16px; line-height: 1.3; }

.guidance-list { margin: 0 0 10px; padding-left: 20px; font-size: 13.5px; }
.guidance-list li { margin-bottom: 5px; }
.guidance-list > li > p { margin: 2px 0 0; }

.export-form .check-grid { max-height: none; }

/* ---------------------------------------------------------- distributions */

.dist {
  margin: 0;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px 16px;
}
.dist figcaption { margin-bottom: 10px; }
.dist figcaption h3 { margin: 0; }
.dist-bar {
  display: flex;
  height: 9px;
  border-radius: var(--radius-token);
  overflow: hidden;
  background: var(--card-strong);
}
/* flex-basis 0 so the grow ratios ARE the proportions. A band that
   rounds to a 0% share keeps min-width, exactly as a share of one in a
   hundred thousand did when the ratio was the raw count. */
.dist-seg {
  min-width: 2px;
  flex: 0 1 0%;
  flex-grow: var(--pct-num, 0);
  background: var(--tone-color, var(--muted));
}
.dist-key {
  display: flex; flex-wrap: wrap; gap: 6px 16px;
  margin-top: 11px;
  font-size: 12px;
}
.dist-key-item { display: inline-flex; align-items: baseline; gap: 5px; }
.dist-key-item b { font-family: var(--font-data); }
.dist-key-item > span:last-child { color: var(--muted); font-size: 11px; }
.dist-swatch {
  display: inline-block; width: 9px; height: 9px; border-radius: 2px;
  align-self: center;
  background: var(--tone-color, var(--muted));
}
.dist .muted { margin-bottom: 0; }

/* --------------------------------------------------------------- filters */

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  /* Tops, not bottoms.
     
     This was flex-end, with .filters-top opting the comparison pages out
     because "bottom-aligning short selects beside a five-checkbox
     fieldset leaves a hole above the selects". That was the right
     diagnosis and half the fix: the advisories filters have the same
     shape - a five-band Risk fieldset next to a two-item Source one - and
     never opted in, so Source sank 72px and its legend floated level with
     the middle of its neighbour's checkbox list. On a 390px screen, where
     the groups wrap into pairs, every row was ragged.
     
     A legend is the label for what is under it, so legends line up.
     The submit button is the one thing that genuinely wants the bottom -
     it is a control, not a labelled field, and it should sit level with
     the inputs it applies - so it asks for it by itself below. */
  align-items: flex-start;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px 16px;
  margin: 14px 0 18px;
}
/* Kept as a no-op alias: the comparison pages carry it in their markup and
   it now says the same thing the default does. Removing it would mean
   editing four templates to change nothing. */
.filters-top { align-items: flex-start; }
/* :has() so no template has to grow a class for this. A browser without it
   puts the button level with the legends, which is tidy rather than
   broken. */
.filters .filter-group:has(> .button) { align-self: flex-end; }
.filter-group { border: 0; margin: 0; padding: 0; display: grid; gap: 4px; align-content: start; }
.filter-group.grow { flex: 1 1 280px; }
.filter-group legend { padding: 0; font-size: 12px; color: var(--muted); }
.check { display: flex; align-items: center; gap: 6px; font-size: 13px; cursor: pointer; }
.check input { accent-color: var(--a1); }

.result-count { font-size: 13px; color: var(--muted); margin: 0 0 10px; }

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

/* Every wide table lives in one of these. max-width pins it to the
   column it sits in, so the table scrolls sideways inside the box and the
   page body never does. tests/test_layout_overflow.py fails the build if a
   .data-table ever appears without one. */
.table-scroll {
  overflow-x: auto;
  max-width: 100%;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.data-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.data-table th, .data-table td {
  text-align: left;
  padding: 9px 12px;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
.data-table thead th {
  position: sticky; top: 0;
  background: var(--card-strong);
  font-size: 11.5px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 600;
  white-space: nowrap;
}
.data-table tbody tr:last-child th, .data-table tbody tr:last-child td { border-bottom: 0; }
.data-table tbody tr:hover { background: var(--card); }
.data-table th[scope="row"] { font-weight: 600; white-space: nowrap; }
.products { max-width: 320px; color: var(--muted); font-size: 12px; }

/* Affected products on a search result, one line per product. The name
   carries the mono face because it is an identifier - "next.js", not a
   sentence - and the versions sit after it in the muted tone so the two
   read as label and detail rather than as one run-on string. Grouping is
   done in services/nvd.py; this only styles it. */
.impact-head { margin-bottom: 2px; }
.impact-list { list-style: none; margin: 0 0 10px; padding: 0; display: grid; gap: 5px; }
.impact-list li { font-size: 13px; line-height: 1.5; }
.impact-list li b { font-family: var(--font-data); font-weight: 600; }
.impact-kind {
  font-family: var(--font-data);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.impact-versions { color: var(--muted-strong); }

.band-dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-right: 6px; vertical-align: baseline;
  background: var(--sev-color);
}

.badge {
  display: inline-block;
  padding: 1px 8px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
  font-size: 11.5px;
  color: var(--muted);
}
/* badge-on marks a risk flag - KEV listed, actively exploited - so it
   takes the Critical hue. */
.badge-on {
  color: var(--sev-critical-on-wash);
  border-color: var(--sev-critical);
  background: color-mix(in srgb, var(--sev-critical) var(--sev-wash), var(--card));
  font-weight: 600;
}
/* badge-yes marks a capability a buyer wants: CREST accreditation, a
   FedRAMP authorisation, built-in patching. Painting those in the Critical
   hue said the opposite of what they mean, so they take the accent - which
   is structure, not severity. */
.badge-yes {
  color: var(--a1-on-wash, var(--a1));
  border-color: var(--a1);
  background: color-mix(in srgb, var(--a1) 14%, var(--card));
  font-weight: 600;
}

.bands { border-collapse: collapse; font-size: 13px; margin-top: 10px; }
.bands caption { text-align: left; font-size: 12px; color: var(--muted); padding-bottom: 6px; }
.bands th, .bands td { text-align: left; padding: 5px 14px 5px 0; }
.swatch {
  display: inline-block; width: 10px; height: 10px; border-radius: 2px;
  margin-right: 8px;
  background: var(--sev-color);
}

/* ----------------------------------------------------------------- pager */

.pager { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; margin: 14px 0; }
.pager-summary { font-size: 12.5px; color: var(--muted); }
.pager ul { list-style: none; display: flex; flex-wrap: wrap; gap: 4px; margin: 0; padding: 0; }
.pager-link {
  display: inline-block;
  min-width: 32px;
  text-align: center;
  padding: 4px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-input);
  font-family: var(--font-data);
  font-size: 12.5px;
  text-decoration: none;
  color: var(--text);
}
.pager-link:hover { border-color: var(--a1); }
.pager-link.is-current { background: var(--a1); border-color: var(--a1); color: var(--on-accent); font-weight: 600; }
.pager-gap { color: var(--muted); padding: 0 2px; }

.downloads { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin: 18px 0; }
.downloads .muted { flex: 1 1 340px; margin: 0; }

/* ------------------------------------------------------------ disclosures */

.disclosure {
  border: 1px solid var(--border);
  border-radius: var(--radius-input);
  background: var(--card);
  padding: 0;
  margin: 10px 0;
}
.disclosure > summary {
  cursor: pointer;
  padding: 10px 14px;
  font-size: 13.5px;
  font-weight: 500;
  list-style-position: inside;
}
.disclosure > summary:hover { color: var(--a1); }
.disclosure > *:not(summary) { padding: 0 14px; }
.disclosure > *:not(summary):last-child { padding-bottom: 12px; }
.disclosure p { font-size: 13.5px; }

/* Arriving from a chart bar: make it obvious which record you landed on,
   and leave room above it so it is not jammed against the viewport top. */
.cve-detail { scroll-margin-top: 18px; }
.cve-detail:target {
  border-color: var(--a1);
  box-shadow: 0 0 0 1px var(--a1);
  background: color-mix(in srgb, var(--a1) 7%, var(--card));
}
.cve-detail:target > summary { color: var(--a1); }
.cve-detail-body { padding: 0 14px 14px; }
.cve-detail-grid {
  display: grid; gap: 16px;
  grid-template-columns: 160px repeat(auto-fit, minmax(240px, 1fr));
  align-items: start;
}
.cve-detail-grid p { margin: 0 0 6px; font-size: 13px; }
.detail-head { margin-top: 26px; }

/* ---------------------------------------------------------------- footer */

.site-footer {
  margin-top: 46px;
  padding-top: 18px;
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-size: 12.5px;
}
.site-footer h2 { font-size: 13px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); }
.site-footer p { max-width: 90ch; }

/* ------------------------------------------------------------ responsive */

@media (max-width: 1180px) {
  .card-grid, .chart-grid { grid-template-columns: 1fr; }
  .hero-card { grid-template-columns: 96px 1fr; }
  .hero-main { grid-column: 1 / -1; }
}

@media (max-width: 900px) {
  /* stretch, not flex-start: in a column flex container flex-start sizes
     each child to its own content width, which is how .main ended up
     1,245px wide inside a 390px viewport. */
  .shell { flex-direction: column; align-items: stretch; }
  .main { width: 100%; max-width: 100%; }
  .rail {
    position: static;
    width: 100%;
    flex-basis: auto;
    height: auto;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px 14px;
    padding: 12px 16px;
    border-right: 0;
    border-bottom: 1px solid var(--border);
  }
  .brand { flex: 0 0 auto; }
  .brand img { width: 150px; }

  /* The strip this replaces was one scrolling row of chips. It scrolled,
     but nothing said so: three items were visible, seven were a swipe away
     with no affordance, and the review that found it was right to call it
     the site's one bug. A closed disclosure costs one tap and shows all ten
     with their headings and qualifiers - the same rail a desktop gets. */
  .railmenu { flex: 0 0 auto; order: 2; margin-left: auto; }
  .railmenu > .menu-toggle {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 8px 13px; cursor: pointer; list-style: none;
    border: 1px solid var(--border); border-radius: var(--radius-input);
    background: var(--card-strong);
    font-size: 13.5px; font-weight: 500;
  }
  .railmenu > .menu-toggle::-webkit-details-marker { display: none; }
  .railmenu[open] > .menu-toggle { border-color: var(--a1); color: var(--a1); }
  .menu-lines {
    width: 15px; height: 11px; position: relative;
    border-top: 2px solid currentColor; border-bottom: 2px solid currentColor;
  }
  .menu-lines::after {
    content: ""; position: absolute; left: 0; right: 0; top: 3.5px;
    border-top: 2px solid currentColor;
  }
  /* The panel is the third row of the rail, full width, below the brand and
     the button that opens it. */
  /* Blocks, not flex. The first attempt made the open disclosure a wrapping
     flex container so the button could stay on its row and the panel wrap
     below; the panel shared the line with the button and the button
     stretched to the panel's height. A block container stacks by definition,
     and the button right-aligns with an auto margin on a max-content width -
     no wrapping rules to get wrong. */
  .railmenu[open] {
    flex: 1 1 100%;
    display: block;
    margin-left: 0;
  }
  .railmenu[open] > .menu-toggle {
    margin-left: auto;
    width: max-content;
  }
  .railmenu > .menu-panel {
    margin-top: 12px; padding-top: 12px;
    border-top: 1px solid var(--border);
  }

  .nav { flex: 1 1 100%; min-width: 0; }
  .nav ul { display: block; }
  .nav li { display: block; }
  .nav-item { white-space: nowrap; padding: 6px 12px; }
  .nav-item em { display: none; }
  .nav-item.is-pending { opacity: 0.55; }

  /* Headings and qualifiers were suppressed while the nav was a one-line
     strip. The panel is a vertical list, so they come back - a phone gets
     the same rail a desktop does. */

  /* Review finding: Theme sat above the nav on a phone, spending the top of
     the screen on a cosmetic control. order puts it last without moving it
     in the document, so the desktop rail keeps it pinned to the bottom.

     It then still sat there with the menu shut, which is a whole row of a
     phone screen spent on a control almost nobody opens twice. It belongs
     with the menu, so it opens and closes with it. Done as a sibling
     selector rather than by moving the element into .menu-panel: the
     desktop rail pins this block to the bottom with margin-top:auto as a
     flex child of .rail, and nesting it inside the disclosure would take
     that away to fix a phone.
     The rule is inside this media query, so a desktop never sees it. */
  .railmenu:not([open]) ~ .rail-foot { display: none; }
  .rail-foot {
    margin-top: 0;
    order: 4;
    flex: 1 1 100%;
    display: flex;
    align-items: center;
    gap: 8px;
    padding-top: 12px;
    border-top: 1px solid var(--border);
  }
  .rail-foot .field-label { margin-bottom: 0; }
  .rail-foot .control { width: auto; }
  .rail-note { display: none; }
  .finding-body { grid-template-columns: 1fr; }
  .hero-card { grid-template-columns: 1fr; }
  .hero-rank { display: flex; align-items: baseline; gap: 10px; }
  .hero-rank-num { font-size: 32px; }
  .cve-detail-grid { grid-template-columns: 1fr; }
  .search-body { grid-template-columns: 1fr; }
  .dns-form-grid { grid-template-columns: 1fr; }
  .profile-fields { grid-template-columns: 1fr; gap: 2px 0; }
  .profile-fields dd { margin-bottom: 10px; }
}


/* A disclosure that carries a warning: same amber the note-warn banner used,
   so the colour stays attached to the thing being warned about rather than
   sitting in a separate box above it. */
.disclosure-warn {
  /* Same treatment .note-warn had - the 3px left rule is what actually reads
     as "warning" at a glance; a tinted background alone disappears. */
  border-left: 3px solid var(--sev-high);
  border-color: color-mix(in srgb, var(--sev-high) 38%, var(--border));
  border-left-color: var(--sev-high);
  background: color-mix(in srgb, var(--sev-high) var(--sev-wash), var(--card));
}
.disclosure-warn > summary { color: var(--text); }
.disclosure-warn > summary b { color: var(--sev-high); }


/* ------------------------------------------------- active filter state */
/* Arriving from a chart bar used to just make the table shorter. When any
   filter is on, the result block takes a tinted surface and an accent rule
   so the change is visible, and every filter is listed with a way out. */
.result-head { margin: 14px 0 10px; }
.result-head.is-filtered {
  border-left: 3px solid var(--a1);
  background: color-mix(in srgb, var(--a1) 7%, var(--card));
  border-radius: 0 var(--radius-input) var(--radius-input) 0;
  padding: 10px 14px;
}
.result-head .result-count { margin: 0; }
.result-head.is-filtered .result-count { font-weight: 600; color: var(--text); }
.filter-chips {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 8px;
}
.filter-chips-label {
  font-family: var(--font-data); font-size: 10px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--muted); margin-right: 2px;
}
.filter-chip {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 12px; line-height: 1;
  padding: 5px 9px;
  border: 1px solid color-mix(in srgb, var(--a1) 45%, var(--border));
  border-radius: 999px;
  background: var(--card-strong);
  color: var(--text); text-decoration: none;
}
.filter-chip span[aria-hidden] { color: var(--muted); font-size: 13px; }
.filter-chip:hover { border-color: var(--a1); }
.filter-chip:hover span[aria-hidden] { color: var(--a1); }
.filter-chip-all { border-style: dashed; color: var(--muted); }


/* --------------------------------------------------- search: start here */
.start-list { list-style: none; margin: 12px 0 0; padding: 0; display: grid; gap: 8px; }
.start-item {
  display: grid; grid-template-columns: 46px 1fr auto; align-items: center; gap: 14px;
  padding: 11px 14px; text-decoration: none;
  background: var(--card-strong); border: 1px solid var(--border);
  border-radius: var(--radius-input); color: var(--text);
}
.start-item:hover { border-color: var(--a1); }
.start-score {
  font-family: var(--font-data); font-size: 21px; font-weight: 600;
  font-variant-numeric: tabular-nums; text-align: right;
}
.start-body { display: grid; gap: 2px; min-width: 0; }
.start-cve { font-size: 12px; color: var(--muted); }
.start-title { font-size: 13.5px; }
.start-band {
  font-family: var(--font-data); font-size: 10px; font-weight: 600;
  letter-spacing: 0.07em; text-transform: uppercase; white-space: nowrap;
}
@media (max-width: 560px) {
  .start-item { grid-template-columns: 40px 1fr; }
  .start-band { grid-column: 2; }
}

/* ----------------------------------------------- remediation CVE dump */
/* One remediation action can clear several hundred CVEs. Collapsed by
   default (see analysis.html); open, it is a block of identifiers nobody
   reads in sequence, so it scrolls in place rather than pushing the rest
   of the card down the page. */
.cve-dump { margin-top: 8px; }
.cve-dump-list {
  margin: 6px 0 0;
  max-height: 9.5em;
  overflow-y: auto;
  font-family: var(--font-data);
  font-size: 12px;
  line-height: 1.6;
  color: var(--muted-strong);
  overflow-wrap: anywhere;
}


/* ------------------------------------------------------- subnet drawings */

/* Widths and offsets ride on [data-pct], the same attribute the chart bars
   and stacked segments use. This page originally carried its own .w-N/.at-N
   scale, written before the CSP work landed and described there as a stopgap
   until a nonced <style> block could replace it. That was wrong on the facts:
   the results region is swapped by HTMX, and a fragment is inserted into a
   document whose CSP carries the nonce from the ORIGINAL response, so a
   <style nonce> minted for the fragment would be refused outright. The
   attribute is not a workaround for the nonce - it is the thing that works
   where a nonce cannot. */
.subnet-head {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 10px;
  margin-bottom: 4px;
}
.subnet-cidr { margin: 0; font-family: var(--font-data); font-size: 22px; letter-spacing: -0.01em; }
/* Scope reads as part of the block's name, so it sits on the same line at
   label weight rather than shouting from a tile. It is never severity
   colour: routability is not risk, and 203.0.113.0/24 is neither. */
.subnet-scope {
  font-size: 11px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text);
  background: color-mix(in srgb, var(--text) 12%, var(--card-strong));
  border-radius: var(--radius-pill); padding: 4px 9px;
}
.subnet-scope[data-scope="global"] {
  color: var(--on-accent); background: var(--a1);
}
.subnet-family {
  font-family: var(--font-data); font-size: 11px; color: var(--muted);
  letter-spacing: 0.06em;
}
.subnet-privacy {
  margin-left: auto; font-size: 11.5px; color: var(--muted); text-align: right;
}
.subnet-scope-note { margin: 0 0 14px; max-width: 78ch; }

.draw { margin: 18px 0 0; }
.draw-head {
  display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
  margin-bottom: 9px;
}
.draw-title {
  font-family: var(--font-data); font-size: 10px; font-weight: 600;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--a1);
}
.draw-note { font-family: var(--font-data); font-size: 11px; color: var(--muted); text-align: right; }
.draw-caption { margin: 8px 0 0; font-size: 13px; }

.bounds-bar {
  position: relative; display: flex; height: 34px;
  border-radius: var(--radius-token); overflow: hidden;
  background: var(--card-strong);
}
.bounds-seg { display: block; height: 100%; width: var(--pct, 0%); }
/* Reserved ends read as unavailable rather than as dangerous - this is the
   one place a red would be misread as severity. */
.bounds-seg.is-reserved { background: color-mix(in srgb, var(--muted) 30%, var(--card-strong)); }
.bounds-seg.is-usable { background: color-mix(in srgb, var(--a1) 34%, var(--card-strong)); }
.bounds-host {
  position: absolute; top: -3px; bottom: -3px; width: 2px;
  left: var(--pct, 0%);
  background: var(--text); border-radius: 1px;
}
.bounds-tick { position: relative; height: 18px; }
.bounds-tick-label {
  position: absolute; left: var(--pct, 0%); transform: translateX(-50%);
  white-space: nowrap;
  font-family: var(--font-data); font-size: 11px; color: var(--text);
}
.bounds-legend {
  display: flex; justify-content: space-between; gap: 10px; margin-top: 7px;
}
.bounds-legend span { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.bounds-legend b { font-weight: 600; font-size: 12.5px; color: var(--text); }
.bounds-legend em {
  font-style: normal; font-size: 10px; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--muted);
}
.bounds-right { text-align: right; align-items: flex-end; }

.derived {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 10px; margin-top: 20px;
  padding-top: 16px; border-top: 1px solid var(--border);
}
.derived-item { display: flex; flex-direction: column; gap: 3px; }
.derived-label {
  font-size: 10px; font-weight: 600; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--muted);
}
.derived-value { font-size: 13.5px; }

/* ------------------------------------------------------- subnet openers */
.openers { display: grid; gap: 10px; }
.opener-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.opener-chip {
  display: inline-flex; align-items: baseline; gap: 8px;
  padding: 8px 12px; text-decoration: none;
  border: 1px solid var(--border); border-radius: var(--radius-pill);
  background: var(--card-strong); color: var(--text);
  font-family: var(--font-data); font-size: 13px;
}
.opener-chip:hover { border-color: var(--border-strong); }
.opener-chip small {
  font-size: 11px; color: var(--muted); font-family: var(--font-ui);
}
.opener-chip .opener-flag {
  font-size: 9.5px; font-weight: 600; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--muted);
}
.opener-chip .opener-flag.is-routable { color: var(--a1); }
.opener-questions {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 10px;
}
.opener-question {
  display: flex; flex-direction: column; gap: 6px; padding: 12px 14px;
  border: 1px solid var(--border); border-radius: var(--radius-input);
  background: var(--card-strong); color: inherit; text-decoration: none;
}
.opener-question:hover { border-color: var(--border-strong); }
.opener-question b { font-weight: 500; font-size: 13.5px; }
.opener-question span { font-family: var(--font-data); font-size: 12px; color: var(--a1); }

@media (max-width: 700px) {
  .bounds-legend { flex-wrap: wrap; }
}

/* The site-wide footer: About / Methodology / Privacy / Contact / Security
   policy, on every page. Quiet on purpose - it is reassurance a reader
   looks for deliberately, not something that should compete with the
   page. Wraps to as many rows as it needs rather than scrolling, because
   a link a phone hides is a link that is not there. */
.site-nav-foot {
  margin-top: 32px; padding-top: 16px;
  border-top: 1px solid var(--border);
}
.site-nav-foot ul {
  list-style: none; margin: 0 0 8px; padding: 0;
  display: flex; flex-wrap: wrap; gap: 6px 18px;
}
.site-nav-foot a { color: var(--muted); text-decoration: none; font-size: 13px; }
.site-nav-foot a:hover { color: var(--text); text-decoration: underline; }
.site-nav-foot .rail-note { margin: 0; }

/* A plain bulleted list that keeps the prose measure, for the About and
   Privacy pages. The data tables have their own treatment; these are
   sentences, and they read as sentences. */
.plain-list { margin: 10px 0 0; padding-left: 20px; max-width: 92ch; }
.plain-list li { margin-bottom: 8px; }

/* The homepage toolkit. Six cards saying what the site does, because the
   rail is navigation and a first-time visitor needs an answer before they
   need a menu - and on a phone the rail is behind one. Two columns from
   540px, three from 1000px: the labels are one word and the descriptions
   are one line, so more columns than that would leave a row of stubs. */
.tool-grid {
  list-style: none; margin: 14px 0 0; padding: 0;
  display: grid; gap: 10px;
  grid-template-columns: 1fr;
}
@media (min-width: 540px) { .tool-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .tool-grid { grid-template-columns: repeat(3, 1fr); } }

/* The primary tier is an ordered list - the five steps are a sequence, not
   a set - and the numbers are drawn rather than left to the list marker,
   which a grid moves outside the card. */
.tool-grid-primary { counter-reset: toolstep; }
.tool-grid-primary li { counter-increment: toolstep; }
.tool-grid-primary b::before {
  content: counter(toolstep);
  display: inline-block;
  min-width: 17px;
  margin-right: 8px;
  font-family: var(--font-data);
  font-size: 11px;
  color: var(--a2);
}

/* Secondary tier: still real destinations, visibly not the product. A row
   of quiet links rather than four more cards competing with the five
   above - which is the whole point of splitting them out. */
.tool-aside-head {
  margin: 22px 0 0;
  font-family: var(--font-data);
  font-size: 10px; font-weight: 600;
  letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--muted);
}
.tool-aside {
  list-style: none; margin: 10px 0 0; padding: 0;
  display: grid; gap: 8px; grid-template-columns: 1fr;
}
.tool-aside a {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px;
  padding: 8px 0;
  text-decoration: none;
  color: var(--muted-strong);
  font-size: 13.5px;
  border-bottom: 1px solid var(--border);
}
.tool-aside li:last-child a { border-bottom: 0; }
.tool-aside a:hover { color: var(--a1); }
.tool-aside span { color: var(--muted); font-size: 12.5px; }
@media (min-width: 700px) { .tool-aside { grid-template-columns: repeat(2, 1fr); column-gap: 26px; } }
@media (min-width: 1000px) { .tool-aside { grid-template-columns: repeat(4, 1fr); } .tool-aside a { border-bottom: 0; } }
.tool-grid a {
  display: flex; flex-direction: column; gap: 4px; height: 100%;
  padding: 13px 15px; text-decoration: none;
  background: var(--card-strong);
  border: 1px solid var(--border);
  border-radius: var(--radius-input);
}
.tool-grid a:hover { border-color: var(--a1); }
.tool-grid b { color: var(--text); font-size: 15px; }
.tool-grid span { color: var(--muted); font-size: 13px; line-height: 1.45; }

/* The one thing a finder came to this page for. Sized up because a
   footer link marked "Security policy" should resolve to an address, not
   to a paragraph an address is buried in. */
.contact-line {
  margin: 0 0 10px;
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* "Start here" on the vendor comparisons. Cards rather than a list
   because nine scenarios as a list is a wall, and the thing a reader is
   doing here is scanning headings for the one that describes them - which
   is what a grid of headings is for and what a list of paragraphs is not.

   Two columns at tablet, three at desktop, matching .tool-grid so the two
   card grids on this site are one shape. Auto-fit rather than a fixed
   count: the last row of a nine-card grid should not stretch one card
   across three columns. */
.scenario-grid {
  display: grid; gap: 10px; margin: 14px 0 0;
  grid-template-columns: 1fr;
}
@media (min-width: 620px) { .scenario-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1080px) { .scenario-grid { grid-template-columns: repeat(3, 1fr); } }
.scenario-card { padding: 13px 15px; height: 100%; }
.scenario-card h3 {
  margin: 0 0 7px; font-size: 14.5px; line-height: 1.35;
  color: var(--text);
}
.scenario-card .plain-list { margin: 0; }
.scenario-card .plain-list li {
  font-size: 13px; line-height: 1.5; color: var(--muted-strong);
}

/* ------------------------------------------------------------- hero split */

/* The dashboard hero, two columns. Measured before: the standfirst wraps
   at its own measure and used 706px of a 1752px band, leaving 60% of the
   widest element on the page empty - which reads as unfinished rather
   than as room to breathe.

   The fix is not a longer line. 74ch is the measure prose is readable at
   and stretching it to 1752px would trade one problem for a worse one.
   The fix is to put something in the space, and the thing a reader wants
   next is how current the data is.

   One column below 1100px, where there is no spare width to split. */
.hero-split { display: grid; gap: 18px; align-items: start; }
.hero-lede { min-width: 0; }

/* One column below 1100px, where there is no spare width to split. The
   panel keeps a rule as a top border there, so it still reads as its own
   statement rather than as more standfirst. */
.hero-side { min-width: 0; border-top: 1px solid var(--border); padding-top: 16px; }
/* ---- One advisory as a tile ------------------------------------------
   Shown when a browser narrows to a single entry, which is where every
   click on the risk chart lands. Same furniture as the Microsoft half's
   detail panel, so a CVE looks the same whichever way you reached it. */
.advisory-tile { margin: 4px 0 14px; }
.advisory-tile-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 12px;
}
.advisory-tile-head .cve { font-size: 15px; font-weight: 600; }
.advisory-tile-title {
  margin: 10px 0 14px;
  font-size: 17px;
  line-height: 1.35;
  color: var(--text);
}
.advisory-tile-body {
  display: grid;
  gap: 16px;
  /* Score block beside the detail from 720px up, stacked below it - the
     score is the first thing to read either way, so it leads in both. */
  grid-template-columns: 1fr;
  align-items: start;
}
.advisory-tile-detail { display: grid; gap: 10px; min-width: 0; }
.advisory-tile-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px 14px;
  margin-top: 2px;
}
@media (min-width: 720px) {
  .advisory-tile-body { grid-template-columns: 210px minmax(0, 1fr); gap: 20px; }
}

/* ---- Section navigator (/advisories) -------------------------------------
   The page is an application in one document. This is the persistent way
   between its parts.

   Sticky rather than fixed: it belongs to the page, not to the window, so
   it scrolls up with the hero and then stops - a bar pinned before the
   reader has seen what page they are on is chrome. */
.sectionbar {
  position: sticky;
  /* Below whatever is already stuck to the top. In the sidebar layout
     that is nothing. Above 2400px the rail becomes a sticky bar of its
     own and this has to clear it, or the two share one strip and the
     section bar covers the site navigation. */
  top: var(--rail-sticky-h);
  z-index: 20;
  margin: 0 0 18px;
  padding: 2px 0;
  background: var(--page);
  border-bottom: 1px solid var(--border);
}
.sectionbar ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: 2px;
  /* Seven labels do not fit a phone. They scroll sideways here rather
     than wrapping to three rows and pushing the page down every time the
     bar sticks. */
  overflow-x: auto;
  scrollbar-width: none;
}
.sectionbar ul::-webkit-scrollbar { display: none; }
.sectionbar a {
  display: block;
  padding: 9px 12px;
  white-space: nowrap;
  text-decoration: none;
  font-size: 13px;
  color: var(--muted);
  border-bottom: 2px solid transparent;
}
.sectionbar a:hover { color: var(--text); }
/* The section currently in view. Set by the scroll-spy in app.js; the bar
   works without it, which is why the underline is the only thing it
   changes. */
.sectionbar a.is-current {
  color: var(--text);
  border-bottom-color: var(--a2);
  font-weight: 600;
}

/* Every anchor target on the site, not only the ones this bar links to.
   Without it a jump on /advisories lands the heading under the sticky
   bar - the classic way an anchor navigator arrives one line short of
   where it said it would. Left global rather than scoped to that page
   because the site has anchor links elsewhere (the KPI tiles, the chart
   bars, "see the two due this week") and a jump that stops a few pixels
   above its heading is better everywhere, not just here. */
[id] { scroll-margin-top: calc(var(--rail-sticky-h) + 58px); }

/* ---- Third-party notices ------------------------------------------------
   A license page is a reference document, not a read-through: people
   arrive looking for one source and want its terms without reading the
   other nine. So each source is its own panel with a fixed field order,
   and the fields are a definition list because that is what they are. */
.notice-entry { margin-top: 14px; }
.notice-entry h3 {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  margin: 0 0 10px;
  font-size: 15px;
}

.notice-fields {
  display: grid;
  /* Labels get a column of their own from 640px up, where there is room
     for two. Below that they stack, because a 90px label column beside a
     three-line sentence is a column of orphaned words. */
  grid-template-columns: 1fr;
  gap: 2px 18px;
  margin: 0;
}
.notice-fields dt {
  font-family: var(--font-data);
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  padding-top: 2px;
}
.notice-fields dd {
  margin: 0 0 10px;
  font-size: 14px;
  line-height: 1.55;
  color: var(--muted-strong);
}
.notice-fields dd:last-child { margin-bottom: 0; }

/* Verbatim license text. Monospaced and whitespace-preserving because it
   is quoted, not written - reflowing an MIT notice into the body face
   would make it look like the site's own prose, which is the one thing a
   reproduced notice must not look like. */
.license-notice {
  margin: 12px 0 0;
  padding: 12px 14px;
  border-left: 3px solid var(--a3);
  background: var(--card-strong);
  font-family: var(--font-data);
  font-size: 12px;
  line-height: 1.65;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  color: var(--muted-strong);
}

/* "Under review" - amber, deliberately not a severity colour. Severity
   hues mean one thing on this site and an open license question is not a
   vulnerability. */
.pill {
  display: inline-block;
  padding: 2px 9px;
  border-radius: 999px;
  border: 1px solid var(--border);
  font-family: var(--font-data);
  font-size: 10.5px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}
.pill-warn { border-color: var(--a3); color: var(--a3); }

@media (min-width: 640px) {
  .notice-fields { grid-template-columns: 108px minmax(0, 1fr); }
  .notice-fields dd { margin-bottom: 8px; }
}

@media (min-width: 1100px) {
  .hero-split { grid-template-columns: minmax(0, 1fr) minmax(320px, 400px); gap: 40px; }
  .hero-side {
    border-top: 0; padding-top: 2px;
    border-left: 1px solid var(--border); padding-left: 40px;
  }
}

/* Above 1500px the panel gets the width its numbers were designed at, and
   the headline gets the size. Measured at 2559: the lede column is still
   over 900px there, so the standfirst is not being squeezed to buy it. */
@media (min-width: 1500px) {
  .hero-split { grid-template-columns: minmax(0, 1fr) minmax(520px, 620px); }
  .hero-split h1 { font-size: 60px; line-height: 1.02; letter-spacing: -0.022em; }
  .hero-lede .standfirst { max-width: 56ch; font-size: 17.5px; line-height: 1.5; }
}

/* The number, in --sev-critical.

   The DEADLINE_BUCKETS note in webui/dashboard_view.py says schedule
   state is painted from the accent ramp and never from a severity token,
   and that rule still holds where it was written: inside a five-segment
   distribution, a red slice would be read as a band. This is not that
   chart. It is one figure carrying the page's only argument, and in the
   accent ramp's rust it read as decoration - a number nobody has to look
   at twice. Red is doing the job a hero number exists for.

   The cost is real and is accepted deliberately: the distribution below
   still paints "Past CISA deadline" in --a2, so the same fact appears in
   two colours one screen apart. Recolouring that segment to match would
   put a severity hue back inside the band chart, which is the thing the
   rule is actually protecting against. */
.hero-overdue { display: flex; align-items: flex-start; gap: 16px; margin: 0 0 16px; }
.hero-overdue b {
  font-size: 54px; line-height: 0.86; font-weight: 600;
  letter-spacing: -0.03em; color: var(--sev-critical);
}
.hero-overdue span {
  padding-top: 5px; max-width: 20ch; text-wrap: balance;
  font-size: 15px; line-height: 1.3; color: var(--text);
}

.yr {
  display: grid; grid-template-columns: 44px minmax(0, 1fr) 48px;
  gap: 12px; align-items: center; margin-bottom: 7px;
}
.yr i {
  font-style: normal; font-family: var(--font-data);
  font-size: 12px; color: var(--muted-strong);
}
.yr b {
  font-family: var(--font-data); font-size: 13px; font-weight: 500;
  text-align: right; color: var(--text);
}
.yr-track {
  height: 14px; border-radius: 3px;
  background: var(--card-strong); overflow: hidden;
}
/* The width arrives as data-pct, resolved by the [data-pct] ramp near the
   top of this file. style-src is 'self' with no 'unsafe-inline', so a
   width in a style attribute would be dropped by the browser. */
.yr-fill {
  height: 100%; width: var(--pct, 0%); border-radius: 3px;
  background: var(--sev-critical); opacity: 0.55;
}
.yr-fill.is-peak { opacity: 1; }

.hero-years-note {
  margin: 12px 0 14px; max-width: 62ch;
  font-size: 13px; line-height: 1.5; color: var(--muted-strong);
}

/* Inherits colour, radius and hover from .button; this adds only the
   treatment that makes it read as the hero's one action. */
.hero-cta {
  padding: 10px 19px;
  font-family: var(--font-data); font-size: 12.5px;
  letter-spacing: 0.1em; text-transform: uppercase;
}

/* ------------------------------------------------------- hero provenance */

/* Was a card in the hero's right half. It answers "how current is this",
   which is a fair question but not the first one, and holding the hero's
   whole right side meant the page led with no claim at all. As a strip it
   still answers it, under the thing it is evidence for.

   Flex rather than the fixed column grid it was mocked with: a source that
   is down loses its line (see priority_feed.provenance), so the number of
   entries here is not a constant. */
.hero-foot {
  display: flex; flex-wrap: wrap; align-items: flex-start;
  gap: 12px 34px;
  margin: 20px 0 24px; padding-top: 16px;
  border-top: 1px solid var(--border);
}
.hero-foot-k {
  font-family: var(--font-data); font-size: 10px;
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--muted);
}
.hero-foot-k small, .hero-feeds small {
  display: block; margin-top: 3px;
  font-family: var(--font-data); font-size: 11px;
  letter-spacing: 0; text-transform: none; color: var(--muted);
}
.hero-foot dl { margin: 0; }
.hero-foot dt { font-size: 12.5px; color: var(--text); }
.hero-foot dd {
  margin: 2px 0 0;
  font-family: var(--font-data); font-size: 11.5px; color: var(--muted-strong);
}
.hero-feeds {
  margin: 0 0 0 auto; text-align: right;
  font-size: 12.5px; color: var(--text);
}

/* ------------------------------------------------------------- ultrawide */

/* Above this width the sidebar layout stops paying for itself.

   The main column caps at 2100px - uncapped, a comparison table reaches
   2,000px with a row of pills adrift at either end - and the rail is
   pinned left at 268px. Measured: at 1920 and 1440 the column has not
   reached its cap and nothing is wasted; at 2560 the leftover is 192px,
   which is the margin the cap was chosen to accept. At 3440 it is
   1,072px of empty page, all of it on one side, because the column is
   left-aligned against a rail that will not move.

   Centring the column alone was tried and was worse: the rail stays
   pinned left, so the gap opens up BETWEEN the nav and the page instead
   of at the edge. Centring only works once the rail is out of the way,
   which is what this does - the rail becomes a bar across the top, the
   way it already does below 900px, and the column centres under it.

   2400px, and the arithmetic rather than a round number. The sidebar
   layout is fine exactly while the column has not hit its cap: below
   268 + 2100 = 2368px every pixel goes into the column, and above it
   every pixel goes into a gutter on one side. So 2368 is where the
   layout stops working, and 2400 is the next round number above it.

   This was 2600, chosen as "just past what 2560 already accepts" - but
   that acceptance was the thing being complained about, only smaller.
   Marc's own window is 2559px: 176px of dead gutter, and 41px short of
   the breakpoint meant to fix it. A threshold that misses the person who
   reported the problem is the wrong threshold.

   Below 2400 nothing changes. The nav does not get tighter either: the
   column is a fixed 1840px at every width above the breakpoint, so the
   bar has the same room at 2400 as at 3440. */

/* ------------------------------------------------- the priority feed rows
   T7: one line per entry, a <details> per row. The summary is the grid the
   row used to be; the detail opens underneath, indented past the rank and
   score columns so the text lines up with the title above it. No JS: the
   element does the toggling, and a keyboard reaches it as a summary. */
.feed-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.feed-row {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.feed-row:hover { border-color: var(--border-strong); }
.feed-entry[open] { border-color: var(--a1); }
.feed-summary {
  display: grid;
  grid-template-columns: 40px 68px minmax(0, 1fr) auto;
  gap: 14px;
  align-items: center;
  padding: 9px 14px;
  cursor: pointer;
  list-style: none;
}
.feed-summary::-webkit-details-marker { display: none; }
.feed-summary:hover .feed-title { color: var(--a1); }
.feed-rank {
  font-size: 14px;
  min-width: 30px; height: 30px; padding: 0 7px;
  justify-self: end;
}
.feed-score {
  --risk-color: var(--sev-color);
  display: grid;
  gap: 0;
  padding: 3px 8px;
  border-left: 3px solid var(--risk-color);
  border-radius: 0 var(--radius-pill) var(--radius-pill) 0;
  background: color-mix(in srgb, var(--risk-color) var(--sev-wash), var(--card));
}
.feed-score b {
  font-family: var(--font-data);
  font-size: 19px;
  line-height: 1.15;
  color: var(--risk-color);
}
.feed-score span { font-size: 10px; font-weight: 600; color: var(--risk-color); }
.feed-main { display: grid; gap: 2px; min-width: 0; }
.feed-head { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.feed-head .cve { font-size: 13.5px; }
.feed-head a.cve { text-decoration: none; }
.feed-head a.cve:hover { text-decoration: underline; }
.feed-head .finding-flags { display: inline-flex; flex-wrap: wrap; gap: 6px; }
.feed-title {
  font-size: 13.5px; font-weight: 500; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.feed-entry[open] .feed-title { white-space: normal; }
.feed-side { display: inline-flex; align-items: center; gap: 10px; justify-self: end; }
.feed-expand {
  width: 0; height: 0;
  border-left: 5px solid transparent; border-right: 5px solid transparent;
  border-top: 6px solid var(--muted);
  transition: transform 120ms;
}
.feed-entry[open] .feed-expand { transform: rotate(180deg); }
.feed-detail {
  display: grid; gap: 8px; justify-items: start;
  padding: 2px 14px 12px calc(14px + 40px + 14px + 68px + 14px);
}
.feed-why { margin: 0; font-size: 13px; color: var(--muted); max-width: 88ch; }
@media (max-width: 720px) {
  .feed-summary { grid-template-columns: 34px 62px minmax(0, 1fr); gap: 10px; padding: 9px 10px; }
  .feed-side { grid-column: 3; justify-self: start; }
  .feed-detail { padding-left: 10px; }
}

/* --------------------------------------------------- search result rows
   T10: the same one-line-per-entry shape as the home feed, with the full
   card underneath. Score column, then the CVE and first sentence, then
   the CVSS chip and the marker at the right. */
.search-rows { list-style: none; margin: 0 0 14px; padding: 0; display: grid; gap: 6px; }
.search-row { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); }
.search-row:hover { border-color: var(--border-strong); }
.search-row.is-kev { border-left: 3px solid var(--sev-critical, var(--a1)); }
.search-entry[open] { border-color: var(--a1); }
.search-summary {
  display: grid;
  grid-template-columns: 68px minmax(0, 1fr) auto;
  gap: 14px;
  align-items: center;
  padding: 9px 14px;
  cursor: pointer;
  list-style: none;
}
.search-summary::-webkit-details-marker { display: none; }
.search-summary:hover .feed-title { color: var(--a1); }
.search-summary-main { display: grid; gap: 2px; min-width: 0; }
.search-summary-side { display: inline-flex; align-items: center; gap: 10px; justify-self: end; }
.search-entry[open] .feed-expand { transform: rotate(180deg); }
.score-unscored-row { --risk-color: var(--muted); }
.search-entry > .search-result { margin: 0; border: 0; border-top: 1px solid var(--border); border-radius: 0; padding: 12px 14px 14px; }
.search-pager { margin-bottom: 18px; }
@media (max-width: 720px) {
  .search-summary { grid-template-columns: 62px minmax(0, 1fr); gap: 10px; padding: 9px 10px; }
  .search-summary-side { grid-column: 2; justify-self: start; }
}
/* The watchlist as a disclosure below the results. */
.watchlist > summary { font-size: 15px; font-weight: 600; }
.watchlist .watchlist-head { margin: 8px 0 4px; }

/* T6: the advisories section bar is a tab strip - one tab renders at a
   time, so the current entry is a state, not a scroll position. */
.tabbar a[aria-current] { color: var(--a1); border-bottom: 2px solid var(--a1); }

/* T8: the comparison tables show that they scroll. The shell carries a
   right-edge fade while there is more to the right and a one-line hint
   under the table; app.js sets is-overflowing / is-at-end from the real
   scroll metrics, and without it the hint shows below 1240px, which is
   where a 1200px-minimum table cannot fit. The first column stays put
   so a row still says which vendor it is six columns in. */
.table-shell { position: relative; }
.table-shell::after {
  content: ""; position: absolute; top: 0; right: 0; bottom: 0; width: 56px;
  pointer-events: none; opacity: 0; transition: opacity 160ms;
  border-radius: 0 var(--radius) var(--radius) 0;
  background: linear-gradient(to left, var(--card) 10%, transparent);
}
.table-shell.is-overflowing:not(.is-at-end)::after { opacity: 0.9; }
.scroll-hint { display: none; margin: 6px 0 0; font-size: 12.5px; color: var(--muted); }
.table-shell.is-overflowing > .scroll-hint { display: block; }
html:not(.js-scroll) .table-shell > .scroll-hint { display: none; }
@media (max-width: 1240px) {
  html:not(.js-scroll) .table-shell > .scroll-hint { display: block; }
}
.compare-table th[scope="row"], .compare-table thead th:first-child {
  position: sticky; left: 0; z-index: 1;
  background: var(--card);
  box-shadow: 1px 0 0 var(--border);
}
.compare-table thead th:first-child { z-index: 2; background: var(--card-strong); }

/* T9: per-domain cards on the DNS results, in place of the 43-column
   table. Same details-row idiom as the feed and the search results. */
.domain-cards { list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 6px; }
.domain-card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); }
.domain-card.tier-attention { border-left: 3px solid var(--sev-critical); }
.domain-card.tier-look { border-left: 3px solid var(--sev-medium); }
.domain-entry[open] { border-color: var(--a1); }
.domain-summary {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  padding: 9px 14px; cursor: pointer; list-style: none;
}
.domain-summary::-webkit-details-marker { display: none; }
.domain-name { font-size: 14px; font-weight: 600; }
.domain-badges { display: inline-flex; flex-wrap: wrap; gap: 6px; margin-left: auto; }
.domain-summary .feed-expand { margin-left: 4px; }
.domain-entry[open] .feed-expand { transform: rotate(180deg); }
.chip-tier.tier-attention { border-color: var(--sev-critical); color: var(--sev-critical); }
.chip-tier.tier-look { border-color: var(--sev-medium); color: var(--sev-medium); }
.domain-body { border-top: 1px solid var(--border); padding: 12px 14px 14px; display: grid; gap: 12px; }
.domain-findings { margin: 0; padding-left: 18px; font-size: 13.5px; }
.domain-findings .level-high { color: var(--sev-critical); }
.domain-findings .level-medium { color: var(--sev-medium); }
.domain-groups { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; }
.domain-group h4 { margin: 0 0 6px; font-size: 11.5px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); }
.domain-group dl { margin: 0; display: grid; grid-template-columns: minmax(110px, auto) 1fr; gap: 3px 10px; font-size: 13px; }
.domain-group dt { color: var(--muted); }
.domain-group dd { margin: 0; overflow-wrap: anywhere; }

/* "Data as of" under the At a glance heading. */
.data-asof { margin: -4px 0 12px; font-size: 12.5px; }
.data-asof b { font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }

@media (min-width: 2400px) {
  /* Narrower than the 2100px the sidebar layout uses, and deliberately.
     Beside a rail, 2100 is balanced - the rail absorbs 268px and the page
     has a left edge. Centred with nothing beside it, 2100 is a very long
     measure floating in the middle of a 3440px screen. Measured off the
     reference Marc pointed at: 1,843px on a 3440 display, 53.6% of the
     width. Redeclared here rather than changed at the root, because at
     2560 the sidebar layout does reach 2100 and lowering it there would
     newly waste 452px to save nothing. */
  :root { --main-cap: 1840px; }
  /* 10px of padding above and below a 46px row. Hardcoded because CSS
     cannot measure it, and asserted in the browser rather than trusted:
     if the bar grows, the section bar starts overlapping it and every
     anchor jump lands short. */
  body { --rail-sticky-h: 66px; }

  .shell { flex-direction: column; align-items: stretch; }

  .rail {
    flex: 0 0 auto;
    width: 100%;
    /* Was 100vh with its own scrollbar, which is right for a column and
       absurd for a bar. */
    height: auto;
    overflow-y: visible;
    position: sticky;
    top: 0;
    z-index: 5;
    flex-direction: row;
    align-items: center;
    gap: 16px;
    /* The bar's SURFACE spans the window - a rule that stops short of the
       edge reads as a floating panel rather than a header - but its
       CONTENTS line up with the column beneath it. The wordmark sits
       above the H1 and the theme control above the right edge of the
       cards, instead of both hugging the window edges a third of a screen
       away from anything they belong to.
       
       Half the leftover, plus the column's own padding, which is what
       .main adds inside its box. max() with 0 so this collapses to the
       plain gutter the moment the column stops being capped. */
    padding-block: 10px;
    padding-inline: calc(
      max(0px, (100% - var(--main-cap)) / 2) + clamp(18px, 3vw, 44px));
    border-right: 0;
    border-bottom: 1px solid var(--border);
  }

  .brand { flex: 0 0 auto; }
  .brand img { width: 168px; }

  .railmenu { flex: 1 1 auto; min-width: 0; }
  /* The bar is one row: heading, its links, next heading, its links.
     .nav itself has to be the flex container for that, because each group
     is now its own <ul> - a heading and a list per group, so a screen
     reader can tell which links a group name covers. Left as blocks they
     stack into three rows bunched beside the logo, which is exactly what
     happened when the grouping was fixed for accessibility and this
     breakpoint was not looked at. */
  .nav, .nav ul {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 5px;
  }
  /* The group headings stay, paid for by the theme picker.

     They cost about 257px and the bar had 40px spare, so the first pass
     dropped them. Marc's call was that the grouping earns its place and
     the theme control does not: collapsed to a button it gives back
     roughly 220px, and trimming the gaps and padding below finds the
     rest. Ten links in a row is a list; three named groups is a map.

     Set as separators rather than headings here - before their group on
     one line instead of above it. The markup does not move, so a screen
     reader reads them in the same order it always did. */
  .nav-head {
    /* Asymmetric, and that asymmetry is most of the grouping. In the
       column a label sits above its links and position says which links
       it covers; on one row nothing says it, so the gap before the label
       is made much larger than the gap after it. The label is then
       visibly attached to what follows rather than floating between two
       runs of links - which is the thing that made these read as more
       options. Costs no contrast, which is what went wrong last time. */
    margin: 0 3px 0 12px;
    padding-left: 15px;
    font-size: 11px;
    align-self: center;
    /* And a rule before each group, since the vertical gap that separated
       them in the column is gone. */
    border-left: 1px solid var(--border);
  }
  .nav > .nav-head:first-child {
    margin-left: 0; padding-left: 0; border-left: 0;
  }

  .nav-item {
    flex-direction: row;
    align-items: baseline;
    gap: 7px;
    padding: 7px 7px;
    white-space: nowrap;
  }

  /* No qualifiers. This reverses a call made an hour earlier, when the
     column was 2100px and there was room to keep the two on /compare -
     "Pen Testing" and "Vuln Management" are the site's subject before
     they are two page names, and the qualifiers said those links led to
     research rather than to a tool. That argument did not change; the
     space did. At 1840px the bar has 1,266px for a nav that wants 1,484,
     and a second row costs more legibility than the two subtitles buy.

     Nine plain labels, which is also the shape of the reference this
     layout is following. Hidden, not deleted: the markup is unchanged,
     so a screen reader still reads every qualifier and every layout
     below 2600px still shows them under their labels, where there is
     room for a second line. */
  .nav-qual { display: none; }

  /* Theme, collapsed to a button at the far end. Open, it is a labelled
     select and a sentence of explanation - a column's worth of thing in
     a row that has no room for it. app.js closes it at this width. */
  .rail-foot {
    flex: 0 0 auto;
    margin-top: 0;
    position: relative;
  }
  .thememenu > .theme-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px; height: 32px;
    cursor: pointer;
    list-style: none;
    border: 1px solid var(--border);
    border-radius: var(--radius-input);
    background: var(--card-strong);
  }
  .thememenu > .theme-toggle::-webkit-details-marker { display: none; }
  .thememenu[open] > .theme-toggle { border-color: var(--a1); }
  /* Three bands, because the control changes surface colour and that is
     what it looks like. Drawn rather than lettered so it costs 32px
     instead of the word plus the select. */
  .theme-toggle-mark {
    width: 14px; height: 12px;
    border-top: 3px solid var(--a1);
    border-bottom: 3px solid currentColor;
    box-sizing: border-box;
  }
  .thememenu > .theme-panel {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    z-index: 6;
    width: max-content;
    min-width: 232px;
    padding: 12px 13px;
    background: var(--card-strong);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: 0 10px 28px rgba(0, 0, 0, 0.32);
  }
  .thememenu > .theme-panel .control { width: 100%; }

  /* The point of all of the above.
  
     width:100% is not redundant. An auto inline margin on a flex item
     overrides align-items: stretch, so without it the column falls back
     to its own content width - the Dashboard reached the cap and a CVE
     page came out 970px, which meant the bar above could not line up with
     both. One column width on every page, or the nav appears to move as
     you navigate. */
  .main { width: 100%; margin-inline: auto; }
}


/* The score panel when nothing was published to score. Muted rather than
   banded: there is no band, and colouring it would assert the severity
   the phrase exists to withhold. Sized to keep the panel from reflowing
   between a scored and an unscored finding. */
.score-unscored { color: var(--muted); font-size: 0.60em; letter-spacing: 0.01em; }

/* A chart bar that is not a link: same geometry, no hover invitation. */
.bar-row.bar-static { cursor: default; }
.bar-row.bar-static:hover { background: transparent; }

/* A chart reduced to its one fact. Same frame as its neighbours so the grid
   stays aligned; no bars, because there is nothing to compare. */
.chart-flat .chart-flat-line { margin: 4px 0 0; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 13px; }

/* Remediation card, tightened. The fix line is the second most important
   thing on the card after the product; the vendor paragraph is behind a
   disclosure. */
.finding-fix { margin: -2px 0 6px; font-size: 14px; font-weight: 500; color: var(--text); }
.finding-detail { margin-top: 6px; }
.finding-detail > summary { font-size: 12.5px; }
.finding-detail .finding-why { margin-top: 6px; }
.score-note-compact { cursor: help; text-decoration: underline dotted; text-underline-offset: 2px; }

/* Severity toggles: a row of pills, each naming its band and its count.
   Checked is the normal state - the pill reads as on; unchecked dims. */
.sev-toggles { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.sev-toggles legend { width: 100%; margin-bottom: 4px; }
.toggle { display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px; border: 1px solid var(--border); border-radius: 999px; background: var(--card); cursor: pointer; font-size: 13px; user-select: none; }
.toggle b { font-weight: 600; color: var(--muted); font-variant-numeric: tabular-nums; }
.toggle:has(input:checked) { border-color: var(--a1); background: var(--card-strong); }
.toggle:has(input:not(:checked)) { color: var(--muted); }
.toggle:has(input:not(:checked)) span { text-decoration: line-through; text-decoration-color: var(--border-strong); }
.toggle input { margin: 0; accent-color: var(--a1); }
.toggle:focus-within { outline: 2px solid var(--a1); outline-offset: 2px; }
html.js-filters .filters-apply { display: none; }

/* B1: the remediation list's controls. The shared .filters box, with the
   search box given room and the selects kept to their content so four
   groups fit on one row above ~900px and wrap in pairs below. The Apply
   button stays visible even with JS: the search box needs it. */
.card-filters .filter-group.grow { flex: 2 1 260px; }
.card-filters select.control { min-width: 9em; }

/* B3: the tagging panel. The section head is the summary, so it keeps
   its accent bar and size; the marker sits before it. The table is
   short by construction (tagged assets plus twenty matches), so it
   needs no max-height of its own. */
.tags-panel > summary { list-style: none; cursor: pointer; }
.tags-panel > summary::-webkit-details-marker { display: none; }
.tags-panel > summary .section-head { display: inline-block; margin: 0; }
.tags-panel > summary::before { content: "▸"; display: inline-block; width: 1em; color: var(--muted); transition: transform 120ms; }
.tags-panel[open] > summary::before { transform: rotate(90deg); }
.tags-panel { margin: 0; }
.tags-panel > summary:hover .section-head { color: var(--a1); }
.tag-table th[scope="row"] { font-family: var(--font-data); font-weight: 500; font-size: 13px; }
.tag-table td .check { justify-content: center; }
.tag-note { flex: 1 1 100%; margin: 0; }
