/*
 * murmtools.com — the Scoreboard system, on the web.
 *
 * This file replaces fifteen pages of duplicated inline <style> blocks, each of
 * which carried the RETIRED family violet (#4A3F8F) and a system font stack.
 * The site was a generic SaaS template describing products that had moved on
 * without it.
 *
 * ── THE RULE THE WHOLE SITE OBEYS ──────────────────────────────────────────
 * These extensions exist to mark every figure with how it came to be known.
 * A site that argues for that and then prints unmarked marketing numbers is
 * arguing against itself. So the vocabulary is real page furniture here:
 *
 *   .fig--counted    solid ink fill      we counted it, and you can check
 *   .fig--computed   outline, no fill    a formula produced it; the rule is printed
 *   .fig--refused    1px dashed + em-dash no such number exists and none can
 *   .fig--withheld   45° hatch           it exists and is not ours to publish
 *
 * Colour carries NONE of that — fill, outline, dash and hatch are shapes, and
 * the display face is reserved for counted figures. Both channels survive a
 * greyscale print and every colour-vision deficiency, which is the same claim
 * the products make about themselves.
 *
 * Blue is the one hue that is never a value: it marks a control you can operate.
 * The three product accents (coral / ink / violet) are identity, not meaning,
 * and each matches that tool's own Web Store tile.
 */

/* ── Faces. Local, subset, never a CDN — a company selling privacy tools does
      not fetch its own typeface from a third party. ── */
@font-face {
  font-family: 'Murm Display';
  font-weight: 700;
  font-display: swap;
  src: url('fonts/space-grotesk-700.woff2') format('woff2');
}
@font-face {
  font-family: 'Murm Sans';
  font-weight: 400;
  font-display: swap;
  src: url('fonts/instrument-sans-400.woff2') format('woff2');
}
@font-face {
  font-family: 'Murm Sans';
  font-weight: 500;
  font-display: swap;
  src: url('fonts/instrument-sans-500.woff2') format('woff2');
}
@font-face {
  font-family: 'Murm Sans';
  font-weight: 600;
  font-display: swap;
  src: url('fonts/instrument-sans-600.woff2') format('woff2');
}
/* ── The metric-matched fallback that stops the swap reflowing the page.
      MEASURED, not guessed: at 100px the same string sets 2641.8px in
      Instrument Sans and 2352.3px in -apple-system — 12.3% narrower, which is
      why /ad-library-tools shifted 0.272 CLS at 146ms when the real face
      landed. Arial is the far closer base at 2562.7px, so the fallback is
      Arial scaled by 2641.8/2562.7 = 103.1%. This face is never chosen when
      the real one loads; it only holds the layout for the first paint.

      The vertical overrides are READ FROM THE FONT, not guessed: Instrument
      Sans is upm=1000, hhea ascent 970, descent -250, lineGap 0 — so 97% / 25%
      / 0%. A first pass used 92%/24% from estimation and left CLS uneven across
      pages, because matching width without matching line box still reflows. ── */
@font-face {
  font-family: 'Murm Sans Fallback';
  src: local('Arial'), local('Helvetica Neue'), local('Liberation Sans');
  size-adjust: 103.1%;
  ascent-override: 97%;
  descent-override: 25%;
  line-gap-override: 0%;
}

@font-face {
  font-family: 'Murm Mono';
  font-weight: 400;
  font-display: swap;
  src: url('fonts/dm-mono-400.woff2') format('woff2');
}

:root {
  /* Scoreboard light. Every ratio measured, not assumed. */
  --paper: #ffffff;
  --card: #f3f4f7;
  --ink: #14161b;    /* 18.10:1 on paper */
  --ink-2: #4c5260;  /* 7.83:1 */
  --rule: #e7e9ef;
  --mark: #8e96a6;   /* 3.06:1 — structural lines only, never text */
  --live: #2c5cff;   /* 5.14:1 — a control you can operate, never a value */
  --live-soft: #edf1ff;
  --on-ink: #b7bdc9; /* 9.60:1 on the ink fill */

  /* Product identity. Each is that tool's own store-tile ground. */
  --stack: #ff5c39;
  --spy: #14161b;
  --ads: #4a3f8f;

  --display: 'Murm Display', 'Space Grotesk', system-ui, sans-serif;
  --sans: 'Murm Sans', 'Instrument Sans', 'Murm Sans Fallback', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  --mono: 'Murm Mono', 'DM Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  --gutter: clamp(20px, 5vw, 34px);
  --r-block: 14px;
  --r-tile: 12px;
  --r-chip: 999px;
  --ease: cubic-bezier(0.2, 0.7, 0.2, 1);
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper: #0f1116;
    --card: #171a21;
    --ink: #f2f4f8;
    --ink-2: #a2a9b8;
    --rule: #242833;
    --mark: #5b6474;
    --live: #7c9bff; /* 7.12:1 on the dark ground */
    --live-soft: #1a2136;
    /* Runs backwards on purpose: the ink fill inverts to near-white, so its
       secondary label must go dark. The light value here measures 1.95:1. */
    --on-ink: #4c5260;
    --stack: #ff7a5c;
    --ads: #9a8cff;
    --spy: #f2f4f8;
  }
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 400 17px/1.6 var(--sans);
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

.wrap {
  max-width: 1080px;
  margin: 0 auto;
  padding: 0 var(--gutter);
}
.wrap--narrow {
  max-width: 760px;
}

a {
  color: var(--live);
  text-decoration: none;
}
a:hover {
  text-decoration: underline;
}

:focus-visible {
  outline: 2px solid var(--live);
  outline-offset: 3px;
  border-radius: 4px;
}

/* ── Header ── */
.hdr {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 22px 0;
  position: sticky;
  top: 0;
  z-index: 20;
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid transparent;
  transition: border-color 200ms var(--ease);
}
.hdr.stuck {
  border-bottom-color: var(--rule);
}
.hdr-mark {
  width: 30px;
  height: 30px;
  border-radius: 8px;
  background: var(--ink);
  color: var(--paper);
  display: grid;
  place-items: center;
  font: 700 17px/1 var(--display);
  flex: none;
}
.hdr-name {
  font: 700 19px/1 var(--display);
  letter-spacing: -0.02em;
}
.hdr-nav {
  margin-left: auto;
  display: flex;
  gap: 22px;
  font-size: 15px;
  font-weight: 500;
}
.hdr-nav a {
  color: var(--ink-2);
}
.hdr-nav a:hover {
  color: var(--ink);
  text-decoration: none;
}
@media (max-width: 620px) {
  .hdr-nav {
    display: none;
  }
}

/* ── Hero ── */
.hero {
  padding: clamp(56px, 11vw, 110px) 0 clamp(36px, 6vw, 64px);
}
.hero h1 {
  font: 700 clamp(40px, 8.5vw, 84px) / 0.98 var(--display);
  letter-spacing: -0.04em;
  margin: 0 0 22px;
  max-width: 15ch;
}
.hero .lede {
  font-size: clamp(18px, 2.2vw, 21px);
  line-height: 1.5;
  color: var(--ink-2);
  margin: 0;
  max-width: 56ch;
}
.hero .lede b {
  color: var(--ink);
  font-weight: 500;
}

/* ── Figures: the four provenance states as real furniture ── */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(228px, 1fr));
  gap: 14px;
}

.fig {
  border-radius: var(--r-tile);
  padding: 20px 20px 18px;
  min-height: 168px;
  display: flex;
  flex-direction: column;
}
.fig-n {
  font: 700 clamp(38px, 5vw, 52px) / 0.9 var(--display);
  letter-spacing: -0.04em;
  display: block;
}
.fig-l {
  margin-top: 10px;
  font-size: 15px;
  font-weight: 600;
}
.fig-note {
  margin-top: 7px;
  font-size: 13.5px;
  line-height: 1.45;
  color: var(--ink-2);
}

/* We counted it. */
.fig--counted {
  background: var(--ink);
  color: var(--paper);
}
.fig--counted .fig-note {
  color: var(--on-ink);
}

/* A formula produced it — outlined, never filled, because an estimate that
   fills a block starts arguing at the volume of a fact. */
.fig--computed {
  border: 1px solid var(--ink);
}
.fig--computed .fig-n {
  font-family: var(--sans);
  font-weight: 600;
  letter-spacing: -0.02em;
}

/* No such number exists and none can. Same slot, same scale — refusing to
   guess is a result, so it is drawn at the size of a finding. */
.fig--refused {
  border: 1px dashed var(--mark);
}
.fig--refused .fig-n {
  color: var(--ink-2);
}

/* It exists and is not ours to publish. */
.fig--withheld {
  border: 1px solid var(--rule);
  background-image: repeating-linear-gradient(
    45deg,
    var(--rule) 0 1.5px,
    transparent 1.5px 6px
  );
}

/* ── Product cards ── */
.tools {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 16px;
}
.tool {
  --accent: var(--ink);
  position: relative;
  border-radius: var(--r-block);
  border: 1px solid var(--rule);
  padding: 26px 24px 24px;
  overflow: hidden;
  transition: transform 200ms var(--ease), border-color 200ms var(--ease);
}
.tool::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 4px;
  background: var(--accent);
}
.tool:hover {
  transform: translateY(-3px);
  border-color: var(--mark);
}
.tool--stack {
  --accent: var(--stack);
}
.tool--spy {
  --accent: var(--spy);
}
.tool--ads {
  --accent: var(--ads);
}
.tool h3 {
  font: 700 23px/1.1 var(--display);
  letter-spacing: -0.02em;
  margin: 4px 0 6px;
}
.tool .role {
  font: 400 12px/1 var(--mono);
  color: var(--ink-2);
  text-transform: lowercase;
}
.tool p {
  color: var(--ink-2);
  font-size: 15px;
  margin: 12px 0 18px;
}
.tool ul {
  list-style: none;
  margin: 0 0 20px;
  padding: 0;
}
.tool li {
  font-size: 14.5px;
  padding: 7px 0;
  border-bottom: 1px solid var(--rule);
  display: flex;
  gap: 10px;
  align-items: baseline;
}
.tool li:last-child {
  border-bottom: 0;
}
.tool li b {
  font: 700 15px/1 var(--display);
  margin-left: auto;
  flex: none;
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 11px 18px;
  border-radius: 10px;
  background: var(--ink);
  color: var(--paper);
  font-weight: 600;
  font-size: 15px;
  transition: transform 140ms var(--ease);
}
.btn:hover {
  text-decoration: none;
  transform: translateY(-1px);
}
.btn--ghost {
  background: none;
  color: var(--ink);
  border: 1px solid var(--mark);
}
/* `--live` INVERTS: #2c5cff in light, #7c9bff in dark. A hardcoded white label
   measured 5.14:1 light but 2.63:1 dark — the same class of bug as `--on-ink`,
   a token whose text colour has to invert with its ground. `--paper` already
   runs backwards with the scheme, so it clears both: 5.14:1 and 7.18:1. */
.btn--free {
  background: var(--live);
  color: var(--paper);
}

/* ── Sections ── */
.sec {
  padding: clamp(48px, 8vw, 88px) 0;
  border-top: 1px solid var(--rule);
}
.sec h2 {
  font: 700 clamp(26px, 4vw, 38px) / 1.1 var(--display);
  letter-spacing: -0.03em;
  margin: 0 0 12px;
  max-width: 20ch;
}
.sec .sub {
  color: var(--ink-2);
  font-size: 17px;
  margin: 0 0 30px;
  max-width: 58ch;
}
.kicker {
  font: 400 12px/1 var(--mono);
  color: var(--ink-2);
  margin: 0 0 14px;
}

/* ── The vocabulary legend ── */
.legend {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 12px;
  margin-top: 26px;
}
.leg {
  padding: 16px;
  border-radius: var(--r-tile);
  border: 1px solid var(--rule);
}
.leg-sw {
  height: 42px;
  border-radius: 8px;
  margin-bottom: 12px;
}
.leg-sw--counted {
  background: var(--ink);
}
.leg-sw--computed {
  border: 1px solid var(--ink);
}
.leg-sw--refused {
  border: 1px dashed var(--mark);
}
.leg-sw--withheld {
  border: 1px solid var(--rule);
  background-image: repeating-linear-gradient(
    45deg,
    var(--rule) 0 1.5px,
    transparent 1.5px 6px
  );
}
.leg b {
  display: block;
  font-size: 14.5px;
  margin-bottom: 4px;
}
.leg span {
  font-size: 13.5px;
  line-height: 1.45;
  color: var(--ink-2);
}

/* ── Prose (privacy pages, articles) ── */
.prose h1 {
  font: 700 clamp(30px, 5vw, 44px) / 1.08 var(--display);
  letter-spacing: -0.03em;
  margin: 44px 0 10px;
}
.prose h2 {
  font: 600 20px/1.3 var(--sans);
  margin: 34px 0 10px;
}
.prose p,
.prose li {
  color: var(--ink-2);
  font-size: 16.5px;
}
.prose b {
  color: var(--ink);
  font-weight: 600;
}
.prose code,
.mono {
  font: 400 14px var(--mono);
  background: var(--card);
  border-radius: 5px;
  padding: 1px 6px;
}
.prose table {
  border-collapse: collapse;
  width: 100%;
  font-size: 15px;
}
.prose th,
.prose td {
  text-align: left;
  padding: 10px 12px;
  border: 1px solid var(--rule);
  vertical-align: top;
}
.prose th {
  background: var(--card);
  font-weight: 600;
}
.prose td {
  color: var(--ink-2);
}

/* ── Footer ── */
.ftr {
  border-top: 1px solid var(--rule);
  padding: 34px 0 56px;
  color: var(--ink-2);
  font-size: 14.5px;
}
.ftr-cols {
  display: flex;
  flex-wrap: wrap;
  gap: 30px 56px;
  margin-bottom: 26px;
}
.ftr-col b {
  display: block;
  color: var(--ink);
  font-size: 13px;
  margin-bottom: 10px;
}
.ftr-col a {
  display: block;
  color: var(--ink-2);
  padding: 4px 0;
}
.ftr-col a:hover {
  color: var(--live);
}
.ftr-legal {
  border-top: 1px solid var(--rule);
  padding-top: 20px;
  font-size: 13px;
  display: flex;
  flex-wrap: wrap;
  gap: 8px 18px;
  align-items: center;
}
.ftr-legal .sp {
  flex: 1;
}

/* ── Motion. One rise, staggered, once. ── */
@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
.rise {
  animation: rise 620ms var(--ease) both;
}
.rise:nth-child(2) {
  animation-delay: 70ms;
}
.rise:nth-child(3) {
  animation-delay: 140ms;
}
.rise:nth-child(4) {
  animation-delay: 210ms;
}
.rise:nth-child(5) {
  animation-delay: 280ms;
}
.rise:nth-child(6) {
  animation-delay: 350ms;
}

/* Ship it. The whole sequence collapses and nothing is left invisible. */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   CONTENT PAGES
   Fourteen pages carried their own inline <style>, each a near-copy of the
   next and each declaring the retired violet. The class names below are the
   ones those pages already use, so the markup did not have to be rewritten —
   only the stylesheet they point at. Old tokens map as:
     --text → --ink · --text2 → --ink-2 · --text3 → --ink-2 (never --mark,
     which is 3.06:1 and must not carry text) · --border → --rule
     --subtle → --card · --primary/--accent → --live
   ═══════════════════════════════════════════════════════════════════════════ */

.crumb {
  font: 400 13px/1 var(--mono);
  color: var(--ink-2);
  padding-top: 20px;
}
.crumb a {
  color: var(--ink-2);
}
.crumb a:hover {
  color: var(--live);
}

.eyebrow,
.kicker {
  font: 400 12px/1 var(--mono);
  color: var(--ink-2);
  margin: 0 0 14px;
  /* The old rule set `letter-spacing:.08em; text-transform:uppercase` on a
     700 weight in the accent hue — four devices shouting one label. A mono
     face already reads as machine furniture; it needs none of them. */
}

h1 {
  font: 700 clamp(32px, 5.5vw, 52px) / 1.06 var(--display);
  letter-spacing: -0.035em;
  margin: 26px 0 14px;
  max-width: 20ch;
}
h2 {
  font: 700 clamp(22px, 3vw, 30px) / 1.15 var(--display);
  letter-spacing: -0.025em;
  margin: 40px 0 12px;
}
h3 {
  font: 600 18px/1.3 var(--sans);
  margin: 0 0 8px;
}

.standfirst,
.lede {
  font-size: 19px;
  line-height: 1.55;
  color: var(--ink-2);
  margin: 0 0 10px;
  max-width: 62ch;
}
.byline,
.meta,
.updated,
.note {
  font: 400 13px/1.5 var(--mono);
  color: var(--ink-2);
  margin: 16px 0 0;
}

p,
li {
  max-width: 68ch;
}

/* Cards, callouts, findings — one construction, three names, because the
   pages already use all three and the markup is not being rewritten. */
.card,
.finding,
.cta {
  border: 1px solid var(--rule);
  border-radius: var(--r-block);
  padding: 20px 22px;
  margin: 22px 0;
  background: var(--card);
}
.cta {
  margin: 40px 0 12px;
  padding: 26px;
}
.cta h2 {
  margin: 0 0 8px;
  font-size: 22px;
}
.cta p {
  margin: 0 0 14px;
  color: var(--ink-2);
}
.card p:last-child,
.finding p:last-child,
.cta p:last-child {
  margin-bottom: 0;
}

/* A callout is a card that leans on one edge. The edge is --live because it
   is chrome; it says "read this bit", not "this figure is different". */
.callout {
  border: 1px solid var(--rule);
  border-left: 3px solid var(--live);
  border-radius: var(--r-tile);
  padding: 16px 18px;
  margin: 22px 0;
  background: var(--card);
}
.callout p {
  margin: 0;
  font-size: 15.5px;
}

.tag {
  display: inline-block;
  font: 400 11.5px/1 var(--mono);
  color: var(--ink-2);
  border: 1px solid var(--rule);
  border-radius: var(--r-chip);
  padding: 5px 10px;
  margin-bottom: 10px;
}

.verdict {
  display: grid;
  gap: 14px;
  margin: 26px 0;
}

/* Comparison tables. These carry yes/no cells across five vs/ pages, and the
   glyph does the work — a green tick on a red cross is the one encoding that
   fails for the readers most likely to need it. */
.tablewrap {
  overflow-x: auto;
  margin: 24px 0;
  border-radius: var(--r-tile);
}
table {
  border-collapse: collapse;
  width: 100%;
  font-size: 15px;
  min-width: 520px;
}
th,
td {
  text-align: left;
  padding: 11px 13px;
  border: 1px solid var(--rule);
  vertical-align: top;
}
th {
  background: var(--card);
  font-weight: 600;
  font-size: 14px;
}
td {
  color: var(--ink-2);
}
.yes,
.no,
.ok,
.warn {
  font-weight: 600;
  color: var(--ink);
}
.yes::before,
.ok::before {
  content: '✓ ';
}
.no::before,
.warn::before {
  content: '✗ ';
  color: var(--ink-2);
}

/* Numbered steps — welcome and uninstall both use them, at two densities. */
.steps {
  counter-reset: s;
  list-style: none;
  padding: 0;
  margin: 30px 0 8px;
  display: grid;
  gap: 12px;
}
.steps li {
  counter-increment: s;
  position: relative;
  padding: 17px 20px 17px 60px;
  border: 1px solid var(--rule);
  border-radius: var(--r-tile);
  max-width: none;
}
.steps li::before {
  content: counter(s);
  position: absolute;
  left: 18px;
  top: 15px;
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background: var(--ink);
  color: var(--paper);
  display: grid;
  place-items: center;
  font: 700 14px/1 var(--display);
}
.steps h3 {
  margin: 0 0 3px;
  font-size: 16px;
}
.steps p {
  margin: 0;
  color: var(--ink-2);
  font-size: 14.5px;
}

.reasons {
  display: grid;
  gap: 10px;
  margin: 30px 0 8px;
}
.reasons a {
  display: block;
  padding: 15px 18px;
  border: 1px solid var(--rule);
  border-radius: var(--r-tile);
  color: var(--ink);
  font-weight: 600;
  transition: border-color 160ms var(--ease), background 160ms var(--ease);
}
.reasons a:hover {
  border-color: var(--live);
  background: var(--live-soft);
  text-decoration: none;
}
.reasons a span {
  display: block;
  font-size: 13.5px;
  font-weight: 400;
  color: var(--ink-2);
  margin-top: 3px;
}

.kbd {
  font: 400 13px var(--mono);
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: 5px;
  padding: 2px 7px;
}

.faq {
  margin-top: 44px;
}
.faq details {
  border-bottom: 1px solid var(--rule);
  padding: 15px 0;
}
.faq summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 16.5px;
}
.faq summary::marker {
  color: var(--ink-2);
}
.faq p {
  margin: 10px 0 2px;
  color: var(--ink-2);
  font-size: 15.5px;
}

/* Legacy chrome from the old pages. `.mark` and `.brand` are the header the
   new .hdr replaces; they stay so an unconverted page still renders. */
.mark {
  display: inline-grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: 8px;
  background: var(--ink);
  color: var(--paper);
  font: 700 17px/1 var(--display);
}
.brand {
  font: 700 19px/1 var(--display);
  letter-spacing: -0.02em;
}
.sp {
  flex: 1;
}
.foot {
  border-top: 1px solid var(--rule);
  margin-top: 48px;
  padding: 26px 0 56px;
  color: var(--ink-2);
  font-size: 14px;
}

/* The fourteen content pages carry `<header><span class="mark">…` rather than
   the homepage's `.hdr`. Their flex layout lived in the inline <style> that was
   deleted, so without this rule the mark and the wordmark butt together and the
   page's first heading collapses under them. Styling the bare element means the
   existing markup did not have to be touched on any of them. */
header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 22px 0 6px;
}
header .brand {
  line-height: 1;
}

/* An eyebrow directly after the header was inheriting the header's own bottom
   spacing and sitting on top of it. */
header + article > .eyebrow,
header + .eyebrow {
  margin-top: 22px;
}

/* `.byline` sat 16px under a 19px standfirst and 0px above the next paragraph,
   so it read as a caption for the body text rather than for the headline. */
.byline {
  margin: 14px 0 26px;
}

/* Content-page headlines run long — a book title, a comparison, a question.
   20ch orphaned the last word on nearly all of them. */
h1 {
  max-width: 26ch;
}
.hero h1 {
  max-width: 15ch;
}

/* ════════════════════════════════════════════════════════════════════════════
   MOTION LAYER
   Built from ~/dev-library/animations.md, by name, with its caveats honoured.
   Every technique here is named in the comment above it so it can be looked up.

   THE ONE RULE THAT OVERRIDES ALL OF THEM: a counted figure counts up, and a
   REFUSAL DOES NOT. The count-up is not decoration here — it is the page
   saying "this number was arrived at by counting, and you are watching it be
   counted." Animating the em-dash in the refused tile would be a lie told in
   motion, so `.fig--refused .fig-n` is excluded from the counter by selector
   and asserted excluded in the audit.
   ════════════════════════════════════════════════════════════════════════════ */

/* ── "Font-load settle guard" (S). Five bundled faces with font-display:swap
      means the fallback paints first. Any per-line or per-character reveal that
      starts before the swap re-breaks its lines mid-flight. `motion.js` adds
      .fonts-in after `document.fonts.ready`; nothing animates until it lands,
      and the no-JS state is fully visible. ── */
.js .anim {
  opacity: 0;
}
.js.fonts-in .anim {
  opacity: 1;
}

/* ── "Line-mask headline rise" (M). Each line in its own clipped box, pushed up
      from behind a physical edge rather than faded. 90ms apart, expo-out. The
      real string stays in the DOM as one readable node for AT — motion.js keeps
      an .sr-only copy and marks the split version aria-hidden. ── */
.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;
}
.ln {
  display: block;
  overflow: clip;
}
.ln > i {
  display: block;
  font-style: normal;
  transform: translateY(110%) rotate(1.5deg);
  transition:
    transform 900ms cubic-bezier(0.16, 1, 0.3, 1),
    opacity 900ms linear;
  opacity: 0;
}
.fonts-in .ln > i {
  transform: none;
  opacity: 1;
}
.fonts-in .ln:nth-child(2) > i { transition-delay: 90ms; }
.fonts-in .ln:nth-child(3) > i { transition-delay: 180ms; }

/* ── "Blur-to-focus word stagger" (S) for the lede, one beat behind the head. ── */
.js .lede {
  filter: blur(6px);
  opacity: 0;
  transform: translateY(8px);
  transition: filter 700ms var(--ease), opacity 700ms var(--ease), transform 700ms var(--ease);
  transition-delay: 260ms;
}
.js.fonts-in .lede {
  filter: none;
  opacity: 1;
  transform: none;
}

/* ── "Card-grid stagger on enter" (S). IntersectionObserver adds .in; the
      stagger is transition-delay, NOT a view() timeline — animation-delay is
      ignored on scroll-driven animations, so a view() stagger would fire each
      card at its own scroll position instead of in sequence. ── */
.js .stag > * {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 620ms var(--ease), transform 620ms var(--ease);
  transition-delay: calc(var(--i, 0) * 60ms);
}
.js .stag.in > * {
  opacity: 1;
  transform: none;
}

/* ── "Perimeter stroke draw-on" (S), repurposed from buttons to the two
      OUTLINED provenance states. The computed and refused tiles are defined by
      an edge that refuses to fill, so that edge draws itself into being. A
      border cannot be drawn, so an inset SVG rect with pathLength="1" carries
      it and the CSS border goes transparent while the stroke runs.

      The catalogue's caveat is that the rect does not reflow with its box —
      here it is `position:absolute; inset:0` with `vector-effect` and
      preserveAspectRatio="none", so it tracks the tile at any size with no
      ResizeObserver. And per the same entry, this never doubles as the focus
      indicator: :focus-visible keeps the real outline. ── */
.fig {
  position: relative;
}
.edge {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: visible;
}
.edge rect {
  fill: none;
  stroke-width: 2;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}
.js .fig--computed,
.js .fig--refused {
  border-color: transparent;
}
.fig--computed .edge rect {
  stroke: var(--ink);
}
.fig--refused .edge rect {
  stroke: var(--mark);
  stroke-dasharray: 0.014 0.008;
  stroke-dashoffset: 1;
}
.stag.in .fig--computed .edge rect,
.stag.in .fig--refused .edge rect {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 1100ms var(--ease);
  transition-delay: calc(var(--i, 0) * 60ms + 200ms);
}
/* The dashed refusal keeps its dash rhythm; only the offset runs. */
.stag.in .fig--refused .edge rect {
  stroke-dashoffset: 0;
}

/* ── "Card hover lift with shadow crossfade" (S). Two pre-baked shadows on
      stacked pseudo-elements, crossfaded by opacity. box-shadow is NEVER
      transitioned — that repaints the blur every frame. Mirrored on
      :focus-visible per the caveat. ── */
.tool {
  isolation: isolate;
}
.tool::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: 0 18px 44px -18px rgba(20, 22, 27, 0.34);
  opacity: 0;
  transition: opacity 220ms var(--ease);
  pointer-events: none;
  z-index: -1;
}
.tool:hover::after,
.tool:focus-within::after {
  opacity: 1;
}

/* ── "Arrow launch-and-return" (S). The arrow leaves through a clipping edge
      while a duplicate enters from the opposite corner, so it loops through the
      button rather than nudging. Masked to stop the 1px leak the entry warns
      about. ── */
.btn .arw {
  display: inline-block;
  overflow: hidden;
  width: 1em;
  height: 1em;
  position: relative;
  vertical-align: -0.05em;
}
.btn .arw i {
  position: absolute;
  inset: 0;
  font-style: normal;
  transition: transform 420ms var(--ease);
}
.btn .arw i + i {
  transform: translate(-120%, 120%);
}
.tool:hover .btn .arw i,
.btn:hover .arw i {
  transform: translate(120%, -120%);
}
.tool:hover .btn .arw i + i,
.btn:hover .arw i + i {
  transform: none;
}

/* ── "Press squash-and-spring" (S), trimmed to a press scale. ── */
.btn:active {
  transform: scale(0.975);
  transition-duration: 90ms;
}

/* ── "Underline wipe, in left out right" (S) on the nav. ── */
.hdr-nav a {
  position: relative;
}
.hdr-nav a::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -4px;
  height: 1.5px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 260ms var(--ease);
}
.hdr-nav a:hover::after {
  transform: scaleX(1);
}
.hdr-nav a:not(:hover)::after {
  transform-origin: right;
}

/* ── "Scroll-timeline header condense" (S). Pure CSS, off the main thread, and
      it unwinds exactly when you drag back up. Per the caveat, backdrop-filter
      is NOT in the keyframes — animating a blur radius is the expensive part —
      and the un-scrolled look is the default so unsupported browsers are fine. ── */
@supports (animation-timeline: scroll()) {
  @keyframes condense {
    to {
      padding-top: 12px;
      padding-bottom: 12px;
    }
  }
  @keyframes markshrink {
    to {
      scale: 0.86;
    }
  }
  .hdr {
    animation: condense linear both;
    animation-timeline: scroll(root block);
    animation-range: 0 160px;
  }
  .hdr-mark {
    animation: markshrink linear both;
    animation-timeline: scroll(root block);
    animation-range: 0 160px;
  }
}

/* ── "CSS-only reading progress bar" (S). Zero JS, scroll-driven. ── */
.progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 2px;
  width: 100%;
  background: var(--live);
  transform-origin: left;
  transform: scaleX(0);
  z-index: 40;
}
@supports (animation-timeline: scroll()) {
  @keyframes grow {
    to {
      transform: scaleX(1);
    }
  }
  .progress {
    animation: grow linear both;
    animation-timeline: scroll(root block);
  }
}

/* ── "Bento focus dim" (S). Hovering one tool recedes the others, so the grid
      answers the pointer as one object instead of three. ── */
@media (hover: hover) and (pointer: fine) {
  .tools:has(.tool:hover) .tool:not(:hover) {
    opacity: 0.55;
    transition: opacity 260ms var(--ease);
  }
}

/* ── "Magnetic button with lagging label" (M). Gated on a fine pointer per the
      caveat; motion.js caches the rect and zeroes travel under reduced motion. ── */
@media (hover: hover) and (pointer: fine) {
  .mag {
    transition: transform 420ms var(--ease);
  }
  .mag.pull {
    transition: transform 90ms linear;
  }
}

/* ── "Element-anchored" murmuration. The brand mark IS a murmuration, so the
      hero carries a small one: drifting points that link when they come close.
      The catalogue flags the constellation field as heavily overused as a stock
      tech background AND as a contrast hazard — so this one is deliberately
      NOT a background: it is a contained 300×200 element in the hero's dead
      right-hand space, sits behind no body copy at any breakpoint, and is
      removed entirely below 900px. IntersectionObserver gates the rAF. ── */
.murmur {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-42%);
  width: 300px;
  height: 200px;
  pointer-events: none;
  opacity: 0.5;
}
.hero {
  position: relative;
}
@media (max-width: 900px) {
  .murmur {
    display: none;
  }
}

/* ── Reduced motion. Everything collapses, nothing is left invisible, and the
      counters print their true value instantly. ── */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
  }
  .js .anim,
  .js .lede,
  .js .stag > * {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
  }
  .ln > i {
    transform: none !important;
    opacity: 1 !important;
  }
  /* The drawn edge must NOT depend on a transition having run: a refusal that
     only appears via animation is invisible to exactly the users least able to
     notice it went. Same argument the extensions make about the wipe. */
  .js .fig--computed,
  .js .fig--refused {
    border-color: var(--mark);
  }
  .edge rect {
    stroke-dashoffset: 0 !important;
  }
  .murmur {
    display: none;
  }
}

/* ── FAQ. Natural-language questions in <details> so the answer is in the DOM
      for extraction whether or not it is open — content gated behind an
      interaction is content an AI cannot read. ── */
.faq-q {
  border-bottom: 1px solid var(--rule);
  padding: 16px 0;
}
.faq-q summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 16.5px;
  letter-spacing: -0.01em;
  list-style: none;
  display: flex;
  gap: 12px;
  align-items: baseline;
}
.faq-q summary::-webkit-details-marker { display: none; }
.faq-q summary::after {
  content: '+';
  margin-left: auto;
  font: 400 20px/1 var(--mono);
  color: var(--ink-2);
  transition: transform 240ms var(--ease);
}
.faq-q[open] summary::after { transform: rotate(45deg); }
.faq-q p {
  margin: 10px 0 2px;
  color: var(--ink-2);
  font-size: 15.5px;
  max-width: 68ch;
}
.note-updated {
  margin-top: 26px;
  font: 400 13px var(--mono);
  color: var(--ink-2);
}


/* ── Links inside prose are underlined, not merely coloured.
      axe flagged `link-in-text-block` on nine pages: a link distinguishable
      from its surrounding text by hue alone. That is precisely the failure
      this site's own thesis calls out — "no colour carries any of that" — so
      leaving it would have the homepage arguing against the rest of the site.
      Navigation, buttons and cards are excluded: they are not in a text block,
      and each already carries its own non-colour affordance. ── */
.prose a:not(.btn),
.fig-note a,
.tool p a,
.sub a,
.faq-q p a,
.ftr-legal a,
.note-updated a {
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}

/* ── Tap targets. axe/Lighthouse want 24px minimum; footer link rows and
      inline chrome links were under it. Padding rather than font-size so the
      type scale is untouched. ── */
.ftr-col a {
  padding: 6px 0;
  display: block;
  min-height: 24px;
}
.hdr-nav a {
  padding: 4px 0;
  display: inline-block;
  min-height: 24px;
}

/* ── Skip link. First focusable thing on the page, invisible until focused. ── */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 12px 18px;
  background: var(--ink);
  color: var(--paper);
  border-radius: 0 0 var(--r-tile) 0;
  font-weight: 600;
}
.skip-link:focus {
  left: 0;
  text-decoration: none;
}
