/* Perimeter — the three public sites.
 *
 * Every colour, size, space, radius, and duration comes from static/tokens.css.
 * There are no literals in this file: if a value is needed that the scale does
 * not have, the scale is wrong and tokens.css is where to fix it.
 *
 * Register: an instrument console. Monospace for machine-derived data (sources,
 * CVEs, scores, panel votes), a serif for the prose a person actually reads at
 * length, sans for interface text between the two, and colour reserved for
 * exploitation evidence rather than decoration.
 *
 * The typeface rule in one line: mono is the machine, serif is the human.
 */

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--text-md);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  /* A faint engineering grid: at the edge of perception, gone by the time
     you are reading a paragraph. */
  background-color: var(--bg);
  background-image:
    linear-gradient(var(--bg-grid) var(--border), transparent var(--border)),
    linear-gradient(90deg, var(--bg-grid) var(--border), transparent var(--border));
  background-size: 46px 46px, 46px 46px;
}

a { color: inherit; }
a:focus-visible, button:focus-visible, summary:focus-visible {
  outline: var(--rail) solid var(--focus);
  outline-offset: var(--sp-1);
  border-radius: var(--radius-sm);
}

.wrap { max-width: var(--measure); margin-inline: auto; padding-inline: var(--gutter); }

/* ── Typeface ──────────────────────────────────────────────────────────────
   One serif, for prose only. The rule is: mono is the machine — headings,
   metadata, scores, chrome — and serif is the human, meaning the sentences a
   reader actually reads at length. Everything else stays on system stacks.

   Self-hosted rather than fetched from Google Fonts. A publication about
   security should not hand every reader's IP address to a third party to render
   its body text, and doing so is a live GDPR question for EU readers.

   Latin subset, variable weight, roman only: 40KB for the whole 400-700 range.
   No italic file — summaries are model-generated plain text and the templates
   escape HTML, so <em> never reaches the page.

   font-display: swap so text is readable in the system serif immediately and
   reflows when Lora arrives, rather than hiding the page behind a font request. */
@font-face {
  font-family: "Lora";
  /* format("woff2"), not the older format("woff2-variations"): that spelling was
     proposed and never standardised, and a browser that does not recognise a
     format string skips the source entirely rather than guessing. Variable-font
     support needs no announcement here; the weight range below is what declares
     it. */
  src: url("/static/fonts/lora-latin-var.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+2000-206F,
                 U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215;
}

:root {
  --font-serif: "Lora", Georgia, "Times New Roman", serif;
}

/* Announced by screen readers, invisible to everyone else. Used to warn that a
   link opens in a new tab — WCAG 3.2.5 asks that a change of context not be
   sprung on people, and telling them is the accepted mitigation. Not `display:
   none`, which removes it from the accessibility tree along with the screen. */
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

.skip { position: absolute; left: -9999px; }
.skip:focus {
  left: var(--gutter); top: var(--gutter); z-index: 10;
  background: var(--surface); padding: var(--sp-2) var(--sp-3);
  border: var(--border) solid var(--accent);
}

/* Long unbroken tokens — hostnames, hashes, German compounds, CJK runs — must
   wrap rather than push the page sideways. `anywhere` beats `break-word` here
   because it lets the browser break mid-token when there is no other option. */
.item-title, .summary, .why, .lede, .intro p, .archive-title, .url {
  overflow-wrap: anywhere;
}
/* Flex and grid children default to min-width:auto, which refuses to shrink
   below their content. That is what turns one long word into a sideways page. */
.masthead-inner > *, .item-meta > *, .brand-text, .archive-row > * { min-width: 0; }
.brand-text { overflow: hidden; }
/* The name and the tagline want opposite things, and sharing one rule cost the
   name its last few characters on every phone. `min-width: 0` above lets the
   text column shrink under the flex layout, and with `nowrap` the only way to
   fit was an ellipsis — so "Cyber Fortnightly Dispatch" arrived as "Cyber
   Fortnightly Disp…", on a viewport where the tagline is already hidden and the
   name is the only identity left. It wraps instead; the site name is the one
   string on the page that must never be clipped. `anywhere` matches the titles
   and summaries above, and `balance` keeps the two lines close in length rather
   than leaving one word stranded. */
.brand-name { overflow-wrap: anywhere; text-wrap: balance; }
/* The tagline keeps the ellipsis: it is hidden below 32rem, so this only ever
   applies on desktop, where a long tagline genuinely can outrun the measure. */
.brand-tag { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
body { overflow-x: hidden; }

/* ── Masthead ───────────────────────────────────────────────────────────── */

.masthead {
  position: sticky; top: 0; z-index: 5;
  background: var(--surface);
  border-bottom: var(--border) solid var(--rule);
}

.masthead-inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4); min-height: var(--sp-12);
}

.brand { display: inline-flex; align-items: center; gap: var(--sp-2); text-decoration: none; color: var(--ink); }
.brand-mark { width: var(--sp-6); height: var(--sp-6); flex: none; color: var(--accent); }
/* line-height 1.2 alone put the tagline hard against the descenders of the
   wordmark above it. They are two separate pieces of information and need to
   read as such. */
.brand-text { display: flex; flex-direction: column; line-height: 1.2; gap: var(--sp-1); }

.brand-name {
  font-family: var(--font-mono);
  font-size: var(--text-md);
  font-weight: var(--weight-bold);
  letter-spacing: var(--track-tight);
}
/* The blinking caret inside the terminal mark now carries the "this is live"
   signal that a static dot beside the wordmark used to. */
.brand-caret {
  fill: var(--accent);
  animation: brand-caret-blink 1.15s steps(1, end) infinite;
}
/* The window's traffic lights. Muted rather than the usual red/amber/green,
   which would drag three colours into a palette built on one accent. */
.brand-light { fill: currentColor; opacity: 0.4; }
@keyframes brand-caret-blink {
  0%, 55% { opacity: 1; }
  55.01%, 100% { opacity: 0; }
}
/* The global reduced-motion rule collapses animation duration, which would
   leave the caret frozen at whatever frame it reached. Pin it visible instead. */
@media (prefers-reduced-motion: reduce) {
  .brand-caret { animation: none; opacity: 1; }
}

.brand-sub {
  display: inline-block; margin-left: var(--sp-2); padding: 0 var(--sp-1);
  border: var(--border) solid var(--accent); border-radius: var(--radius-sm);
  color: var(--accent);
  font-size: var(--text-3xs); letter-spacing: var(--track-wider);
  text-transform: uppercase; vertical-align: 0.16em;
}

.brand-tag { font-family: var(--font-mono); font-size: var(--text-2xs); color: var(--muted); }
@media (max-width: 32rem) { .brand-tag { display: none; } }

.masthead-nav {
  display: flex; align-items: center; gap: var(--sp-3);
  font-family: var(--font-mono); font-size: var(--text-xs);
}
.masthead-nav a {
  display: inline-flex; align-items: center;
  min-height: var(--tap-floor);
  padding-inline: var(--sp-1);
  text-decoration: none; color: var(--muted);
  border-bottom: var(--border) solid transparent;
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.masthead-nav a:hover { color: var(--accent); border-bottom-color: var(--accent); }
.masthead-nav a[aria-current="page"] { color: var(--accent); }

.nav-sep { width: var(--border); height: var(--sp-4); background: var(--rule); }
.nav-cross { font-size: var(--text-2xs); }
/* `.masthead-nav a.nav-cross`, not `.nav-cross`: the cross-site links are
   anchors, so `.masthead-nav a` (0,1,1) outranked a bare `.nav-cross` (0,1,0)
   and its `display: none` never applied. They have therefore been on every
   phone all along, taking about 120px, squeezing the site name into a column
   too narrow to hold it, and pushing the theme toggle clean off the right edge
   — where `body { overflow-x: hidden }` quietly swallowed it. `.nav-sep` obeyed
   the same rule only because it is a span and nothing outranked it. */
@media (max-width: 46rem) { .nav-sep, .masthead-nav a.nav-cross { display: none; } }

/* At 320px — an SE, the narrowest phone still in use — the remaining nav plus
   the toggle still ran a few pixels past the edge. Tightening the two gaps buys
   back more than enough without touching type or targets. */
@media (max-width: 22rem) {
  .masthead-inner { gap: var(--sp-2); }
  .masthead-nav { gap: var(--sp-2); }
}

.theme-toggle {
  display: inline-grid; place-items: center;
  width: var(--tap-min); height: var(--tap-min);
  padding: 0; border: var(--border) solid var(--rule); border-radius: var(--radius-md);
  background: transparent; color: var(--muted); cursor: pointer;
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.theme-toggle:hover { color: var(--accent); border-color: var(--accent); }

.icon-moon { display: none; }
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) .icon-sun { display: none; }
  :root:not([data-theme="dark"]) .icon-moon { display: inline; }
}
:root[data-theme="light"] .icon-sun { display: none; }
:root[data-theme="light"] .icon-moon { display: inline; }
:root[data-theme="dark"] .icon-sun { display: inline; }
:root[data-theme="dark"] .icon-moon { display: none; }

/* ── Edition header ─────────────────────────────────────────────────────── */

.edition-head { padding-block: var(--sp-10) var(--sp-6); }

.eyebrow {
  /* Was sp-3, which read as one cramped block with the date beneath it. The
     eyebrow labels the heading; it needs to sit near it without touching. */
  margin: 0 0 var(--sp-4);
  font-family: var(--font-mono); font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--track-wider); text-transform: uppercase; color: var(--accent);
}
.eyebrow::before { content: "// "; opacity: 0.55; }

.edition-head h1 {
  margin: 0;
  font-family: var(--font-mono);
  font-weight: var(--weight-bold);
  font-size: var(--display-sm);
  line-height: var(--lh-tight);
  letter-spacing: var(--track-tight);
  text-wrap: balance;
}

.lede {
  /* Serif: this is prose a reader reads at length, not interface text. */
  font-family: var(--font-serif);
  /* Was sp-3. The heading is display-sized, so a gap that looked balanced under
     body copy sat far too tight under it. */
  margin: var(--sp-5) 0 0;
  color: var(--muted); font-size: var(--text-base); max-width: var(--measure-prose);
}

/* ── Pager ──────────────────────────────────────────────────────────────── */

.pager {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--sp-3); margin-top: var(--sp-6); padding-top: var(--sp-3);
  border-top: var(--border) solid var(--rule-soft);
  font-family: var(--font-mono); font-size: var(--text-xs);
}
.pager-foot {
  margin-top: var(--sp-12); padding-top: var(--sp-5);
  border-top: var(--border) solid var(--rule);
  justify-content: center; gap: var(--sp-8);
}
.pager-link {
  display: inline-flex; align-items: center;
  min-height: var(--tap-floor);          /* was 18.9px — below WCAG 2.5.8 */
  padding-inline: var(--sp-1);
  color: var(--muted); text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}
.pager-link:hover { color: var(--accent); }
.pager-link.is-off { opacity: 0.3; }

/* ── Filters ────────────────────────────────────────────────────────────── */

.controls { display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--sp-3) 0 var(--sp-5); }
.control-group { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1); }

.control-label {
  font-family: var(--font-mono); font-size: var(--text-2xs);
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--muted);
  margin-right: var(--sp-1); min-width: 4rem;
}

.chip {
  display: inline-flex; align-items: center;
  min-height: var(--tap-floor);
  font-family: var(--font-mono); font-size: var(--text-xs); line-height: 1;
  padding: var(--sp-2) var(--sp-3);
  border: var(--border) solid var(--rule); border-radius: var(--radius-sm);
  background: transparent; color: var(--muted); cursor: pointer;
  transition: color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              background var(--dur-fast) var(--ease);
}
.chip:hover { color: var(--ink); border-color: var(--muted); }
.chip.is-on, .chip[aria-pressed="true"] {
  color: var(--accent); border-color: var(--accent); background: var(--accent-dim);
}

/* ── Sections ───────────────────────────────────────────────────────────── */

/* Each section carries its own hue via one local variable, so every rule below
   reads `var(--sec)` and the mapping lives in exactly one place. `data-section`
   was already on the element for the filters — no markup change needed.
   The fallback keeps anything unmapped on the accent rather than colourless. */
.section {
  --sec: var(--accent);
  margin-block: var(--sp-10);
}
.section[data-section="vulns"]   { --sec: var(--sec-vulns); }
.section[data-section="threat"]  { --sec: var(--sec-threat); }
.section[data-section="offense"] { --sec: var(--sec-offense); }
.section[data-section="aisec"]   { --sec: var(--sec-aisec); }
.section[data-section="defense"] { --sec: var(--sec-defense); }
.section[data-section="grc"]     { --sec: var(--sec-grc); }
.section[data-section="india"]   { --sec: var(--sec-india); }
.section[hidden] { display: none; }

/* The India block sits above the topic sections and holds items lifted out of
   them, so it needs to read as a different kind of grouping without shouting.
   A rail and a faint tint do that; a heavier treatment would make the page look
   like it leads with a sponsor. */
.section-india {
  padding: var(--sp-4) var(--sp-4) var(--sp-2);
  border-left: var(--rail) solid var(--sec);
  background: linear-gradient(90deg, var(--surface), transparent 70%);
}
.section-note {
  margin: var(--sp-2) 0 0;
  color: var(--muted);
  font-family: var(--font-serif);
  font-size: var(--text-sm);
}
/* The section an item was lifted out of. Mono, like the rest of the machine
   -derived metadata on that line, and tinted so it is scannable down the block. */
.item-section {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--sec, var(--muted));
}

.section-head {
  display: flex; align-items: center; gap: var(--sp-3);
  padding-bottom: var(--sp-2);
  border-bottom: var(--border) solid var(--sec);
}

.section-head h2 {
  margin: 0;
  font-family: var(--font-mono);
  font-weight: var(--weight-bold); font-size: var(--text-sm);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  color: var(--sec);
}
.section-head h2 a { text-decoration: none; }
.section-head h2 a::before { content: "▚ "; opacity: 0.6; }
.section-head h2 a:hover { text-decoration: underline; text-underline-offset: var(--sp-1); }

/* Dashed run-out to the count, so the header reads as a console rule. */
.section-head::after {
  content: ""; flex: 1; height: var(--border);
  background-image: linear-gradient(90deg, var(--rule) 50%, transparent 50%);
  background-size: 6px var(--border);
}
.section-count {
  order: 3;
  font-family: var(--font-mono); font-size: var(--text-xs); color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.section-count::before { content: "["; opacity: 0.5; }
.section-count::after  { content: "]"; opacity: 0.5; }

/* ── Items ──────────────────────────────────────────────────────────────── */

.items { list-style: none; margin: 0; padding: 0; counter-reset: item; }

.item {
  counter-increment: item;
  position: relative;
  padding: var(--sp-5) 0 var(--sp-5) var(--sp-10);
  border-bottom: var(--border) solid var(--rule-soft);
}
.item[hidden] { display: none; }
.item:last-child { border-bottom: 0; }

/* The severity rail: exploitation evidence, readable before any text. */
.item::after {
  content: "";
  position: absolute; left: 0; top: var(--sp-5); bottom: var(--sp-5);
  width: var(--rail); background: var(--rule);
}
.item:has(.sev-kev)::after  { background: var(--sev-crit); }
.item:has(.sev-high)::after { background: var(--sev-high); }
.item[data-tag="must_read"]::after { background: var(--accent); }

.item::before {
  content: "[" counter(item, decimal-leading-zero) "]";
  position: absolute; left: var(--sp-2); top: calc(var(--sp-5) + 0.12rem);
  font-family: var(--font-mono); font-size: var(--text-2xs);
  color: var(--muted);                    /* was --rule at 1.31:1 — invisible */
  font-variant-numeric: tabular-nums;
}
/* Tighten the gutter on small phones rather than dropping the index entirely. */
@media (max-width: 30rem) {
  .item { padding-left: var(--sp-8); }
  .item::before { left: var(--sp-1); font-size: var(--text-3xs); }
}

.item-title {
  margin: 0 0 var(--sp-2);
  font-size: var(--text-lg); font-weight: var(--weight-bold); line-height: var(--lh-snug);
  letter-spacing: -0.008em; text-wrap: pretty;
}
.item-title a { text-decoration: none; transition: color var(--dur-fast) var(--ease); }
.item-title a:hover { color: var(--accent); }

.item-meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1) var(--sp-2);
  margin: 0 0 var(--sp-2);
  font-family: var(--font-mono); font-size: var(--text-2xs); color: var(--muted);
}
.item-meta .src { color: var(--ink-soft); }
.item-meta .src::before { content: "⟩ "; color: var(--accent); opacity: 0.7; }
.item-meta .sep { opacity: 0.35; }
.item-meta .stat { font-variant-numeric: tabular-nums; }

.badge {
  display: inline-block; padding: 0 var(--sp-1);
  border: var(--border) solid var(--rule); border-radius: var(--radius-sm);
  font-size: var(--text-2xs); white-space: nowrap;
}
.badge-must_read {
  color: var(--accent); background: var(--accent-dim);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  text-transform: uppercase; letter-spacing: var(--track-wide);
  font-size: var(--text-3xs);
}
.badge-research {
  color: var(--accent); border-color: color-mix(in srgb, var(--accent) 35%, transparent);
  text-transform: uppercase; letter-spacing: var(--track-wide); font-size: var(--text-3xs);
}
.badge-kev {
  color: var(--sev-crit); background: var(--sev-crit-bg);
  border-color: color-mix(in srgb, var(--sev-crit) 45%, transparent);
  /* Every other badge is a word or two, where nowrap stops an ugly break. This
     one carries a sentence — "Exploited in the wild · patch by <date>" — which
     at 320px measures 336px and was the last thing dragging the page sideways.
     It is the one badge that has to be allowed to wrap. */
  white-space: normal;
}

.cve.sev-kev  { color: var(--sev-crit); background: var(--sev-crit-bg); border-color: color-mix(in srgb, var(--sev-crit) 45%, transparent); }
.cve.sev-high { color: var(--sev-high); background: var(--sev-high-bg); border-color: color-mix(in srgb, var(--sev-high) 35%, transparent); }
.cve.sev-mid  { color: var(--sev-mid); }
.cve.sev-low  { color: var(--muted); }

/* Panel agreement — the number that says how far to trust the ranking. The
   word is not decoration: "3/3" alone reads as a score or a rank. */
.panel-key {
  text-transform: uppercase; letter-spacing: var(--track-wide);
  font-size: var(--text-3xs); opacity: 0.75; margin-right: var(--sp-1);
}
.panel {
  font-family: var(--font-mono); font-size: var(--text-2xs);
  padding: 0 var(--sp-1); border: var(--border) solid var(--rule); border-radius: var(--radius-sm);
  color: var(--muted); font-variant-numeric: tabular-nums; cursor: help;
}
.panel.is-unanimous { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, transparent); }

/* Copy. Prose stays in the sans — this is the part a human reads. */
.why {
  /* Serif: this is prose a reader reads at length, not interface text. */
  font-family: var(--font-serif);
  margin: 0 0 var(--sp-2); padding-left: var(--sp-3);
  border-left: var(--border) solid var(--accent);
  font-size: var(--text-base); color: var(--ink);
}
.why-key {
  display: block;
  font-family: var(--font-mono); font-size: var(--text-3xs);
  letter-spacing: var(--track-wider); text-transform: uppercase;
  color: var(--accent); margin-bottom: var(--sp-1);
}
.summary {
  /* Serif: this is prose a reader reads at length, not interface text. */
  font-family: var(--font-serif);
  margin: 0 0 var(--sp-2); color: var(--ink-soft);
  font-size: var(--text-base); max-width: var(--measure-prose);
}

/* Indicators, collapsed. Open by choice: most readers want the story, and the
   handful who want to search their telemetry want the values without leaving.
   Everything inside is defanged at render time, so nothing here is clickable and
   a stray copy-paste cannot resolve. */
.iocs { margin: var(--sp-3) 0 0; font-size: var(--text-2xs); }
.iocs summary {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  min-height: var(--tap-floor); cursor: pointer;
  font-family: var(--font-mono); color: var(--muted);
  letter-spacing: var(--track-wide); text-transform: uppercase;
}
.iocs summary:hover { color: var(--accent); }
.ioc-count {
  padding: 0 var(--sp-1); border: var(--border) solid var(--rule);
  border-radius: var(--radius-sm); color: var(--ink-soft);
}
.iocs[open] summary { color: var(--accent); margin-bottom: var(--sp-2); }
.iocs dl {
  display: grid; grid-template-columns: max-content 1fr;
  gap: var(--sp-1) var(--sp-3); margin: 0;
  padding: var(--sp-3); border-left: var(--rail) solid var(--rule);
  background: var(--surface);
}
.iocs dt {
  font-family: var(--font-mono); color: var(--muted);
  text-transform: uppercase; letter-spacing: var(--track-wide);
}
.iocs dd { margin: 0; min-width: 0; }
/* Values wrap anywhere: a 64-character hash has no break opportunity of its own
   and would otherwise set the width of the whole page. */
.iocs code {
  font-family: var(--font-mono); color: var(--ink-soft);
  overflow-wrap: anywhere; margin-right: var(--sp-2);
}

/* Credit for the accounts folded into this one. Quiet: it is attribution,
   not content, and it should not compete with the summary above it. */
.also-covered {
  margin: var(--sp-2) 0 0;
  font-family: var(--font-mono); font-size: var(--text-2xs);
  color: var(--muted); overflow-wrap: anywhere;
}
.also-covered a { color: var(--ink-soft); }
.also-covered a:hover { color: var(--accent); }

/* The sources page. A plain list, deliberately: it is a disclosure, not a
   showcase, and dressing it up would misread what it is for. */
.source-list, .source-list ul { list-style: none; margin: 0; padding: 0; }
.source-group { margin-bottom: var(--sp-6); }
.source-group h3 {
  font-family: var(--font-mono); font-size: var(--text-2xs);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  color: var(--muted); margin: 0 0 var(--sp-2);
}
.source-group li {
  padding: var(--sp-1) 0;
  border-bottom: var(--border) solid var(--rule-soft);
  display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: baseline;
}
.source-group li .url { margin-left: auto; }

/* Shown only on items whose source published in another language: the
   publisher's own headline, under the English one, so nothing is silently
   replaced. Quiet by design, it is provenance rather than content. */
.item-original {
  margin: calc(var(--sp-1) * -1) 0 var(--sp-2);
  font-size: var(--text-2xs); color: var(--muted);
  font-family: var(--font-mono); overflow-wrap: anywhere;
}
.item-original .pill { margin-right: var(--sp-1); }

.item-links { display: flex; flex-wrap: wrap; gap: var(--sp-4); margin: 0; font-family: var(--font-mono); font-size: var(--text-2xs); }
.item-links a {
  display: inline-flex; align-items: center;
  min-height: var(--tap-floor);          /* was 17.7px — below WCAG 2.5.8 */
  color: var(--muted); text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}
.item-links a:hover { color: var(--accent); }

/* ── Issue front matter ─────────────────────────────────────────────────── */

.issue-head h1 { font-size: var(--display-md); max-width: 22ch; }

.intro {
  margin: var(--sp-6) 0 0; padding: var(--sp-4) 0 0 var(--sp-4);
  border-left: var(--rail) solid var(--accent);
  font-size: var(--text-md); line-height: var(--lh-loose); color: var(--ink);
}
.intro p { font-family: var(--font-serif); margin: 0 0 var(--sp-3); max-width: var(--measure-prose); }
.intro-sig { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--muted); }

.cross-promo {
  margin-top: var(--sp-10); padding: var(--sp-4) var(--sp-5);
  border: var(--border) solid var(--rule);
  background: var(--surface); font-size: var(--text-sm); color: var(--ink-soft);
}
.cross-promo p { margin: 0; }
.cross-promo a { color: var(--accent); }

.covers {
  display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-3); margin: var(--sp-4) 0 0;
  font-family: var(--font-mono); font-size: var(--text-2xs);
}
.covers a { color: var(--muted); text-decoration: none; }
.covers a:hover { color: var(--accent); }

/* ── Archive ────────────────────────────────────────────────────────────── */

.archive { list-style: none; margin: 0; padding: 0; }
.archive-row {
  display: grid; gap: var(--sp-2) var(--sp-8); padding-block: var(--sp-5);
  border-bottom: var(--border) solid var(--rule-soft);
}
@media (min-width: 46rem) { .archive-row { grid-template-columns: 13rem 1fr; } }

.archive-date { display: flex; flex-direction: column; gap: var(--sp-1); }
.archive-date a {
  font-family: var(--font-mono); font-size: var(--text-base);
  text-decoration: none; letter-spacing: var(--track-tight);
}
.archive-date a:hover { color: var(--accent); }
.archive-count { font-family: var(--font-mono); font-size: var(--text-2xs); color: var(--muted); }

.archive-title { margin: 0 0 var(--sp-1); font-size: var(--text-md); font-weight: var(--weight-bold); }
.archive-title a { text-decoration: none; }
.archive-title a:hover { color: var(--accent); }

.archive-sections {
  display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-3); margin: 0 0 var(--sp-2);
  font-family: var(--font-mono); font-size: var(--text-2xs); color: var(--muted);
}
.tally { --sec: var(--muted); }
.tally[data-sec="vulns"]   { --sec: var(--sec-vulns); }
.tally[data-sec="threat"]  { --sec: var(--sec-threat); }
.tally[data-sec="offense"] { --sec: var(--sec-offense); }
.tally[data-sec="aisec"]   { --sec: var(--sec-aisec); }
.tally[data-sec="defense"] { --sec: var(--sec-defense); }
.tally[data-sec="grc"]     { --sec: var(--sec-grc); }
.tally b { color: var(--sec); font-weight: var(--weight-bold); }

.archive-highlights { margin: 0; padding-left: var(--sp-4); font-size: var(--text-sm); color: var(--ink-soft); }
.archive-highlights li { margin-bottom: var(--sp-1); }
.archive-highlights a:hover { color: var(--accent); }

/* ── Research index ─────────────────────────────────────────────────────── */

.lede-wide { font-family: var(--font-serif); max-width: var(--measure-prose); font-size: var(--text-base); color: var(--ink-soft); }
.lede-wide a { color: var(--accent); }

.research-stats {
  display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-6); margin: var(--sp-5) 0 0;
  font-family: var(--font-mono); font-size: var(--text-xs); color: var(--muted);
}
.research-stats b { color: var(--accent); font-weight: var(--weight-bold); margin-right: var(--sp-1); }

.jump {
  display: flex; flex-wrap: wrap; gap: var(--sp-1);
  margin: var(--sp-6) 0 0; padding-top: var(--sp-5);
  border-top: var(--border) solid var(--rule-soft);
}
.jump a {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  min-height: var(--tap-floor);
  padding: var(--sp-1) var(--sp-3);
  border: var(--border) solid var(--rule); border-radius: var(--radius-sm);
  font-family: var(--font-mono); font-size: var(--text-xs);
  text-decoration: none; color: var(--muted);
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.jump a:hover { color: var(--sec, var(--accent)); border-color: var(--sec, var(--accent)); }
.jump a[data-sec="vulns"]   { --sec: var(--sec-vulns); }
.jump a[data-sec="threat"]  { --sec: var(--sec-threat); }
.jump a[data-sec="offense"] { --sec: var(--sec-offense); }
.jump a[data-sec="aisec"]   { --sec: var(--sec-aisec); }
.jump a[data-sec="defense"] { --sec: var(--sec-defense); }
.jump a[data-sec="grc"]     { --sec: var(--sec-grc); }
/* A hue swatch, so the chip is identifiable before you read its label. */
.jump a::before {
  content: ""; width: var(--sp-2); height: var(--sp-2);
  border-radius: var(--radius-pill); background: var(--sec, var(--accent));
  flex: none;
}
.jump a span { font-size: var(--text-2xs); color: var(--ink-soft); font-variant-numeric: tabular-nums; }

.month-index {
  list-style: none; margin: var(--sp-5) 0 0; padding: 0;
  columns: 1; column-gap: var(--sp-8);
  font-family: var(--font-mono); font-size: var(--text-xs);
}
@media (min-width: 34rem) { .month-index { columns: 2; } }
.month-index li {
  display: flex; justify-content: space-between; gap: var(--sp-4);
  padding-block: var(--sp-1); border-bottom: var(--border) solid var(--rule-soft);
  break-inside: avoid;
}
.month-index span { color: var(--muted); }

/* ── Colophon, empty state, footer ──────────────────────────────────────── */

/* A labelled fact table, outside the colophon's <details>. Same treatment,
   because these read as the same kind of information. */
.facts { display: grid; grid-template-columns: auto 1fr; gap: var(--sp-1) var(--sp-5); margin: 0 0 var(--sp-5); }
.facts dt { color: var(--muted); font-size: var(--text-sm); }
.facts dd { margin: 0; font-family: var(--font-mono); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }

.colophon {
  margin-top: var(--sp-10); padding: var(--sp-3) var(--sp-4);
  border: var(--border) solid var(--rule); border-radius: var(--radius-md);
  background: var(--surface); font-size: var(--text-sm);
}
.colophon summary {
  cursor: pointer; font-family: var(--font-mono); font-size: var(--text-2xs);
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--muted);
}
.colophon summary:hover { color: var(--accent); }
.colophon dl { display: grid; grid-template-columns: auto 1fr; gap: var(--sp-1) var(--sp-5); margin: var(--sp-4) 0 0; }
.colophon dt { color: var(--muted); font-size: var(--text-sm); }
.colophon dd { margin: 0; font-family: var(--font-mono); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }

.status-live {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

.empty-state {
  padding: var(--sp-8) 0; color: var(--muted);
  font-size: var(--text-base); text-align: center; font-family: var(--font-mono);
  text-wrap: balance;
}
.empty-state a { color: var(--accent); }

/* A recovery action inside an empty state — the point of saying "nothing here"
   is to offer the way out. */
.btn-inline {
  display: inline-block; margin-left: var(--sp-2);
  min-height: var(--tap-floor);
  padding: var(--sp-1) var(--sp-3);
  border: var(--border) solid var(--accent); border-radius: var(--radius-sm);
  background: var(--accent-dim); color: var(--accent);
  font-family: var(--font-mono); font-size: var(--text-xs); cursor: pointer;
  transition: background var(--dur-fast) var(--ease);
}
.btn-inline:hover { background: transparent; }

/* ── Footer ─────────────────────────────────────────────────────────────────
   Provenance as a readout: named fields in a grid, not a stack of fine print.
   The labels are the signature — mono, accent, hairline-ruled — and they are the
   only ornament here, because the page's boldness is spent on the masthead and
   the section rules. */
/* ── Support ───────────────────────────────────────────────────────────────
   Sits between the last section and the footer: the end of the reading, not an
   interruption of it. Styled as a quiet panel rather than a banner — it should
   read as a note from the person who runs this, which is what it is, and should
   not be the loudest thing on a page whose job is the news above it. */
.support {
  margin-top: var(--sp-16);
  border-top: var(--border) solid var(--rule);
  background: var(--surface);
  padding-block: var(--sp-8);
  /* The masthead is sticky, so the anchor jump from the nav link would otherwise
     land with the heading tucked underneath it. */
  scroll-margin-top: var(--sp-16);
}
/* The panel and the footer share a background and each carried a 4rem top
   margin, so together they drew two rules with a band of dead surface between
   them. Butted up, they read as one closing region: the ask, a rule, the
   colophon. */
.support + .footer { margin-top: 0; }
.support-inner {
  display: flex; flex-direction: column; align-items: center;
  gap: var(--sp-4); text-align: center;
}
/* Takes the leftover width but may shrink to nothing before the button wraps:
   a flex child will not go below its content otherwise, which is what pushes a
   long paragraph and a button onto one overflowing line. */
.support-copy { max-width: var(--measure-prose); }
.support-head {
  margin: 0 0 var(--sp-2);
  font-family: var(--font-mono); font-size: var(--text-xs);
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--muted); font-weight: 600;
}
.support-prose {
  margin: 0; font-family: var(--font-serif);
  font-size: var(--text-sm); line-height: var(--lh-body); color: var(--ink-soft);
}
.support-rails {
  display: flex; flex-wrap: wrap; justify-content: center; gap: var(--sp-3);
  margin: 0; padding: 0; list-style: none;
}
/* Full width on its own row: it is a footnote to both rails, not a third one.
   Quiet, because it is fine print, and fine print that shouts reads as a pitch. */
.support-note {
  /* Full width of the wrap, so its rule ends where the footer's columns end.
     Capped at the prose measure it stopped short of every other block on the
     page, which read as the panel being indented rather than as a measure. */
  align-self: stretch;
  margin: var(--sp-2) 0 0; padding-top: var(--sp-5);
  border-top: var(--border) solid var(--rule);
  font-family: var(--font-serif); font-size: var(--text-xs);
  line-height: var(--lh-body); color: var(--muted);
}
.support-note .contact { font-family: var(--font-mono); font-weight: 600; color: var(--ink-soft); }
.support-button {
  display: inline-flex; align-items: center; justify-content: center;
  /* Equal width, so the pair reads as one deliberate choice rather than two
     buttons that happened to have labels of different lengths. */
  min-width: var(--action-width);
  min-height: var(--tap-min); padding: var(--sp-2) var(--sp-5);
  border: var(--border) solid var(--accent); border-radius: var(--radius-md);
  color: var(--accent); font-family: var(--font-mono); font-size: var(--text-xs);
  text-decoration: none; white-space: nowrap;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.support-button:hover, .support-button:focus-visible {
  background: var(--accent); color: var(--surface);
}
/* The first rail is filled. Not a judgement about which is better: most of this
   readership is outside India and can only use that one, so the eye should land
   there first and the other should still be obviously available. */
.support-button.is-primary { background: var(--accent); color: var(--surface); }
.support-button.is-primary:hover, .support-button.is-primary:focus-visible {
  background: transparent; color: var(--accent);
}
/* On a phone the button goes full width: it is the one thing on this panel that
   is meant to be tapped, and a 40%-width target beside wrapped prose reads as
   decoration. */
@media (max-width: 34rem) {
  .support-rails { width: 100%; }
  .support-rails li { flex: 1 1 100%; }
  .support-button { width: 100%; }
}

/* Matches the cross-site links: same size, and hidden on the same breakpoint.
   The masthead has no room for it at 46rem and below — that is what pushed the
   theme toggle off the right edge once already — and the panel above the footer
   carries the ask on those screens anyway. */
.nav-support { font-size: var(--text-2xs); }
@media (max-width: 46rem) { .masthead-nav a.nav-support { display: none; } }

.footer {
  margin-top: var(--sp-16); padding-block: var(--sp-10) var(--sp-8);
  border-top: var(--border) solid var(--rule); background: var(--surface);
  color: var(--muted); font-size: var(--text-sm);
}

.footer-grid {
  display: grid;
  /* The identity column earns twice the width: it carries the only paragraph
     anyone reads. The other two are reference, and reference wants to be scanned
     rather than read. */
  grid-template-columns: 2fr 1fr 1fr;
  gap: var(--sp-8) var(--sp-6);
}
@media (max-width: 46rem) {
  .footer-grid { grid-template-columns: 1fr; gap: var(--sp-6); }
}

.footer-block { min-width: 0; }
.footer-block > :last-child { margin-bottom: 0; }

.footer-label {
  margin: 0 0 var(--sp-3);
  padding-bottom: var(--sp-1);
  border-bottom: var(--border) solid var(--rule);
  font-family: var(--font-mono); font-size: var(--text-3xs);
  font-weight: var(--weight-bold); letter-spacing: var(--track-wider);
  text-transform: uppercase; color: var(--accent);
}

.footer-lead {
  margin: 0 0 var(--sp-3);
  color: var(--ink); font-size: var(--text-md);
  font-family: var(--font-mono); letter-spacing: var(--track-tight);
}
.footer-lead strong { font-weight: var(--weight-bold); }

/* Prose, so serif — the same rule the rest of the page follows. */
.footer-prose, .footer-rights {
  margin: 0; font-family: var(--font-serif);
  max-width: var(--measure-prose); line-height: var(--lh-body);
}

.footer-links { margin: 0; padding: 0; list-style: none; font-family: var(--font-mono); }
.footer-links li + li { margin-top: var(--sp-2); }
.footer-links a { text-decoration: none; }
.footer-links a::before { content: "→ "; color: var(--accent); opacity: 0.7; }
.footer-links a.is-here { color: var(--accent); }

/* Centred and a size up. This is a signature rather than another field, and it
   is the last thing on the page — left-aligning it under a three-column grid
   made it read as a fourth, orphaned column. */
.footer-credit {
  margin: var(--sp-8) 0 0; padding-top: var(--sp-5);
  border-top: var(--border) solid var(--rule-soft);
  font-family: var(--font-mono); font-size: var(--text-sm);
  text-align: center; color: var(--ink-soft);
}
.footer-credit a { text-decoration: underline; text-underline-offset: 0.2em; }
/* Stop the emoji inheriting the surrounding treatment. */
.footer-credit .heart { font-style: normal; color: var(--sev-crit); }

.footer a:hover { color: var(--accent); }
.footer-credit { color: var(--ink-soft); }
.footer-credit a { text-decoration: underline; text-underline-offset: 0.2em; }
/* The emoji keeps its own colours regardless of theme, so it needs no styling
   beyond stopping it inheriting the surrounding italic/monospace treatment. */
.footer-credit .heart { font-style: normal; }

/* ── Touch targets ──────────────────────────────────────────────────────────
   Several links were sized to --tap-floor (24px, the WCAG 2.5.8 minimum) to
   keep dense rows dense. That is defensible under a cursor and mean under a
   thumb, so on a coarse pointer they go to --tap-min (44px, the 2.5.5 enhanced
   target). Keyed on pointer type rather than viewport width because the
   question is what is doing the pointing, not how wide the screen is — a
   touchscreen laptop wants the big targets and a narrow desktop window does
   not. The floors on the individual rules stay; this overrides them on touch. */
@media (pointer: coarse) {
  .pager-link,
  .item-links a,
  .masthead-nav a,
  .chip {
    min-height: var(--tap-min);
    padding-block: var(--sp-2);
  }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

@media print {
  body { background: var(--surface); color: var(--ink); background-image: none; }
  .masthead, .controls, .pager, .footer, .theme-toggle { display: none; }
  .item { break-inside: avoid; }
}

/* ── Ransomware tracker ─────────────────────────────────────────────────────
   A data panel, not a story list, and styled to read as one: dense, monospace,
   no scores or badges. Nothing here went through the model panel. */
.tracker-note {
  /* Full width, matching the table it introduces. It was capped at the prose
     measure, which stopped it 185px short of the table's right edge and read as
     a block drifting left rather than a caption belonging to what follows. */
  margin: 0 0 var(--sp-4);
  padding: var(--sp-3);
  border-left: var(--rail) solid var(--sev-crit);
  background: var(--surface);
  color: var(--ink-soft);
  font-family: var(--font-serif);
  font-size: var(--text-sm);
}
/* Five columns of names and sectors will not fit a phone. Scroll the table
   rather than the page — a document that scrolls sideways is broken. */
.tracker-scroll { overflow-x: auto; }
.tracker-table {
  width: 100%; border-collapse: collapse;
  font-family: var(--font-mono); font-size: var(--text-2xs);
}
.tracker-table th {
  text-align: left; padding: var(--sp-2) var(--sp-3);
  border-bottom: var(--border) solid var(--rule);
  color: var(--muted); font-weight: var(--weight-bold);
  letter-spacing: var(--track-wider); text-transform: uppercase;
  white-space: nowrap;
}
.tracker-table td {
  padding: var(--sp-2) var(--sp-3);
  border-bottom: var(--border) solid var(--rule-soft);
  vertical-align: top;
}
.tracker-table tr:hover td { background: var(--surface); }
.tracker-victim { color: var(--ink); max-width: 22rem; }
.tracker-group {
  display: inline-block; padding: 0 var(--sp-1);
  border: var(--border) solid var(--rule); border-radius: var(--radius-sm);
  color: var(--accent);
}
.tracker-sector, .tracker-when { color: var(--muted); }
.tracker-when a { color: var(--accent); margin-left: var(--sp-1); }
