/* Liatrio brand typeface. Space Grotesk is the brand's single family for every
   text role, so one variable-range import covers Light 300 through Bold 700 —
   including the 650 the headings ask for, which a fixed weight list would snap. */
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap");

/* Palette from https://www.liatrio.com/brand-data.json (lastUpdated 2026-04-14).
   Every token that maps to a named brand color carries that name and its official
   hex. Tokens marked "derived" have no named equivalent — they are elevation tints
   or a lightened/darkened brand hue needed to clear the 4.5:1 text contrast the
   brand's own accessibility guideline requires. scripts/check_brand.py re-checks
   both the named hexes and the contrast, so drift fails CI rather than review. */
:root {
  color-scheme: dark;
  --ink: #eeeeee;      /* Grey 100 */
  --muted: #999999;    /* Grey 400 */
  --line: #333333;     /* Grey 600 */
  --paper: #1a1f23;    /* Dark Mode Primary */
  --surface: #1e2327;  /* Dark Mode Secondary */
  --group: #23292f;    /* derived · elevation above Dark Mode Secondary */
  --row-open: #1f2a22; /* derived · Primary Green wash on the open row */
  --drill: #1c2226;    /* derived · recessed answer panel */
  --accent: #89df00;   /* Bright Green — the brand's accent/highlight color */
  --accent-text: #89df00; /* Bright Green — readable as text here at 8.8:1 or better */
  --tip-bg: #f8f9fa;   /* Light Mode Secondary */
  --tip-shadow: rgb(0 0 0 / 0.28); /* derived · a cast shadow, not a brand colour */
  --tip-fg: #111111;   /* Grey 800 */
  --paid: #f77f00;     /* Flame Orange */
  --no: #ef5560;       /* derived · Hot Red #e63946 lightened; the named hex is 3.9:1 here */
  --code-bg: #2a3036;  /* derived · neutral elevation */
  --on-accent: #111111; /* Grey 800 — text on an accent fill, and dark on both accents: white is 2.9:1 on the light one */
  /* The score bar's segments. Aliases rather than new colours: on a dark page
     the vivid half of a status pair is its glyph, on a light page it is its
     disc, so the bar points at whichever one carries against the page. It
     follows the colour-vision palettes for free, since those move --st-*. */
  --meter-yes: var(--st-yes);
  --meter-paid: var(--st-paid);
  --meter-warn: var(--st-warn);
  --meter-no: var(--st-no);
  --meter-unknown: var(--st-unknown);
  --meter-na: var(--st-na);

  /* The theme toggle's glyph, shown through a mask — the black in the data URI
     is never painted, `currentColor` is. A sun on the dark theme and a moon on
     the light one, so the icon shows the mode the button switches *to*, which
     is what the words it replaced said. */
  --theme-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='4.25'/%3E%3Cpath d='M12 2.6v2.4M12 19v2.4M2.6 12H5M19 12h2.4M5.3 5.3L7 7M17 17l1.7 1.7M18.7 5.3L17 7M7 17l-1.7 1.7'/%3E%3C/svg%3E");

  /* Status badges. These are the dark theme's set; the light block overrides
     them, and each colour-vision palette overrides them again, so what a badge
     ends up drawn in resolves across as many as four blocks. Every glyph clears
     4.5:1 on its own disc — scripts/check_brand.py resolves each theme/palette
     combination the way the cascade does and checks the pair that results. */
  --st-yes: #89df00;        /* Bright Green */
  --st-yes-bg: #143a12;     /* derived · deep Primary Green */
  --st-paid: #f77f00;       /* Flame Orange */
  --st-paid-bg: #3a2408;    /* derived · deep Flame Orange */
  --st-no: #ef5560;         /* derived · Hot Red lightened; the named hex is 3.6:1 on its disc */
  --st-no-bg: #3a1418;      /* derived · deep Hot Red */
  --st-warn: #edd52b;       /* derived · Lime rotated off green, so a workaround cannot read as a yes */
  --st-warn-bg: #3a3208;    /* derived · deep yellow */
  --st-unknown: #c068f9;    /* Violet */
  --st-unknown-bg: #2b1440; /* derived · deep Violet */
  --st-na: #00c1db;         /* Lagoon */
  --st-na-bg: #003d5c;      /* Dark Blue */
}

/* Colour-vision palettes for the status badges. The glyph is the primary code —
   every status has its own shape — but colour is the one people read first, and
   the default set does not survive: simulated through a Viénot dichromat model,
   the green check and the yellow bang sit 6.7 ΔE apart under deuteranopia,
   which is to say they are one colour.

   Each set below was chosen by simulating candidates through that model and
   maximising the smallest pairwise distance, with yes, no and n/a anchored so
   the semantics survive the recolour. The result is a worst pair of 31 ΔE
   rather than 6.5. Every glyph still clears 4.5:1 on its own disc, which
   scripts/check_brand.py checks for these blocks as well as the default. */
:root[data-palette="red-green"] {
  --st-yes: #66ccff;        /* derived · blue, because green cannot be told from red here */
  --st-yes-bg: #0e4d6d;     /* derived */
  --st-paid: #597aff;       /* derived */
  --st-paid-bg: #040e39;    /* derived */
  --st-no: #ff5040;         /* derived · red survives as a dark yellow, distinct from the blues */
  --st-no-bg: #470600;      /* derived */
  --st-warn: #ffff19;       /* derived */
  --st-warn-bg: #6e6e00;    /* derived */
  --st-unknown: #8cffa3;    /* derived */
  --st-unknown-bg: #226f32; /* derived */
  --st-na: #c8c8c8;         /* derived · neutral, and neutral under every deficiency */
  --st-na-bg: #4a4a4a;      /* derived */
}

:root[data-palette="blue-yellow"] {
  --st-yes: #5cdb3b;        /* derived · green and red both survive tritanopia, so they stay */
  --st-yes-bg: #125500;     /* derived */
  --st-paid: #ac59ff;       /* derived */
  --st-paid-bg: #1d0437;    /* derived */
  --st-no: #ff4d4d;         /* derived */
  --st-no-bg: #480101;      /* derived */
  --st-warn: #0087e0;       /* derived */
  --st-warn-bg: #00121e;    /* derived */
  --st-unknown: #f4ff8c;    /* derived */
  --st-unknown-bg: #686f22; /* derived */
  --st-na: #c8c8c8;         /* derived */
  --st-na-bg: #4a4a4a;      /* derived */
}

/* Both alternatives invert the same way, and were re-optimised for it rather
   than lightened by eye: a light chip compresses the lightness range the dark
   set was using as a third channel, so the discs were re-picked to hold apart
   under simulation. Red-green: worst pair 28.5 ΔE. Blue-yellow: 32.1. */
:root[data-theme="light"][data-palette="red-green"] {
  --st-yes: #002233;        /* derived */
  --st-yes-bg: #1f92cc;     /* derived */
  --st-paid: #001466;       /* derived */
  --st-paid-bg: #6180fa;    /* derived */
  --st-no: #470600;         /* derived */
  --st-no-bg: #b87974;      /* derived */
  --st-warn: #575700;       /* derived */
  --st-warn-bg: #d1d11f;    /* derived */
  --st-unknown: #002e09;    /* derived */
  --st-unknown-bg: #19a836; /* derived */
  --st-na: #525252;         /* derived · neutral */
  --st-na-bg: #cccccc;      /* Grey 300 */
}

:root[data-theme="light"][data-palette="blue-yellow"] {
  --st-yes: #0f4700;        /* derived */
  --st-yes-bg: #41c71e;     /* derived */
  --st-paid: #2e005c;       /* derived */
  --st-paid-bg: #ae61fa;    /* derived */
  --st-no: #7a0000;         /* derived */
  --st-no-bg: #cc9999;      /* derived */
  --st-warn: #001f33;       /* derived */
  --st-warn-bg: #208ed6;    /* derived */
  --st-unknown: #4e5700;    /* derived */
  --st-unknown-bg: #c7d63a; /* derived */
  --st-na: #292929;         /* derived · neutral */
  --st-na-bg: #949494;      /* derived · neutral */
}

/* Nothing but lightness is left, so the disc carries a ramp and the glyph does
   the telling apart. Yes is inverted out of the ramp entirely — see below —
   and the other five run ordered by how settled the answer is, no furthest
   from the page and n/a nearest it. Both ends of that ramp stay off black and
   white: a ramp is relative to the page under it, and a disc at the page's own
   value is not a disc. */
:root[data-palette="mono"] {
  /* Yes runs the other way round from the ramp below it. With only lightness to
     work with, the answer you see most needs to be the one you can pick out
     without reading the glyph, and inverting it does that where one more step
     along a grey ramp would not. */
  --st-yes: #111111;        /* derived · near Grey 800 */
  --st-yes-bg: #ffffff;     /* White */
  --st-no: #ffffff;         /* White */
  --st-no-bg: #383838;      /* derived */
  --st-warn: #ffffff;       /* White */
  --st-warn-bg: #4a4a4a;    /* derived */
  --st-paid: #ffffff;       /* White */
  --st-paid-bg: #5a5a5a;    /* derived */
  --st-unknown: #ffffff;    /* White */
  --st-unknown-bg: #686868; /* derived */
  --st-na: #ffffff;         /* White */
  --st-na-bg: #757575;      /* derived */
}

:root[data-theme="light"][data-palette="mono"] {
  --st-yes: #ffffff;        /* White */
  --st-yes-bg: #333333;     /* Grey 600 */
  --st-no: #111111;         /* derived */
  --st-no-bg: #9e9e9e;      /* derived */
  --st-warn: #111111;       /* derived */
  --st-warn-bg: #adadad;    /* derived */
  --st-paid: #111111;       /* derived */
  --st-paid-bg: #bcbcbc;    /* derived */
  --st-unknown: #111111;    /* derived */
  --st-unknown-bg: #cacaca; /* derived */
  --st-na: #111111;         /* derived */
  --st-na-bg: #d8d8d8;      /* derived */
}

:root[data-theme="light"] {
  color-scheme: light;
  --ink: #111111;      /* Grey 800 */
  --muted: #666666;    /* Grey 500 */
  --line: #dddddd;     /* Grey 200 */
  --paper: #f8f9fa;    /* Light Mode Secondary */
  --surface: #ffffff;  /* Light Mode Primary */
  --group: #eeeeee;    /* Grey 100 */
  --row-open: #eef8e8; /* derived · Primary Green wash on the open row */
  --drill: #f8f9fa;    /* Light Mode Secondary */
  --accent: #24ae1d;   /* Primary Green — the hero color, decorative use only on light */
  --accent-text: #187a14; /* derived · Primary Green darkened; Bright Green is 1.6:1 here */
  --tip-bg: #1a1f23;   /* Dark Mode Primary */
  --tip-shadow: rgb(0 0 0 / 0.28); /* derived · a cast shadow, not a brand colour */
  --tip-fg: #eeeeee;   /* Grey 100 */
  --paid: #a34e00;     /* derived · Flame Orange darkened for text on light */
  --no: #c1121f;       /* derived · Hot Red darkened for text on light */
  --code-bg: #eeeeee;  /* Grey 100 */
  --on-accent: #111111; /* Grey 800 — text on an accent fill, and dark on both accents: white is 2.9:1 on the light one */
  --meter-yes: var(--st-yes-bg);
  --meter-paid: var(--st-paid-bg);
  --meter-warn: var(--st-warn-bg);
  --meter-no: var(--st-no-bg);
  --meter-unknown: var(--st-unknown-bg);
  --meter-na: var(--st-na-bg);
  --theme-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23000'%3E%3Cpath d='M20.4 14.8A8.6 8.6 0 0 1 9.2 3.6 8.6 8.6 0 1 0 20.4 14.8Z'/%3E%3C/svg%3E");

  /* The badge inverts here. On a dark page a bright glyph on a deep disc is
     the readable way round; on a light one it is the other way, so the disc
     takes the colour and the glyph goes dark. Same hue per status either way,
     so a yes is the same green whichever theme you are in. */
  --st-yes: #2c4700;        /* derived · deep Primary Green */
  --st-yes-bg: #82c21d;     /* derived · Primary Green lightened */
  --st-paid: #663400;       /* derived · deep Flame Orange */
  --st-paid-bg: #ff9626;    /* derived · Flame Orange lightened */
  --st-no: #4c0005;         /* derived · deep Hot Red */
  --st-no-bg: #e0656e;      /* derived · Hot Red lightened */
  --st-warn: #615500;       /* derived · deep yellow */
  --st-warn-bg: #d6cf9a;    /* derived · yellow lightened */
  --st-unknown: #38005c;    /* derived · deep Violet */
  --st-unknown-bg: #a877c7; /* derived · Violet lightened */
  --st-na: #00282e;         /* derived · deep Lagoon */
  --st-na-bg: #1a9cad;      /* derived · Lagoon darkened to hold a dark glyph */
}

* {
  box-sizing: border-box;
}

html {
  /* Everything pinned at the top, measured in theme.js. An anchor that landed
     behind the masthead was already close; the sticky legend would have put a
     family heading fully behind it. The 5.5rem floor covers the moments when
     chrome still measures zero — before measureChrome has run, or on a page
     with no masthead — so an anchor never lands hard against the top edge. */
  scroll-padding-top: max(5.5rem, calc(var(--chrome-h, 0px) + 0.75rem));
}

/* The one call to action on the site. Outlined rather than filled, because it
   is not what the page is for — the research is — and it fills on hover, where
   the accent needs dark text on it to stay readable. Points at Liatrio's own
   contact page; there is nothing to submit here. */
.contact-btn {
  display: inline-flex;
  align-items: center;
  min-height: 2.6rem;
  padding: 0.6rem 1.4rem;
  border: 1px solid var(--accent);
  border-radius: 0.4rem;
  color: var(--ink);
  font-size: 0.84rem;
  font-weight: 600;
  text-decoration: none;
  transition: background 120ms ease, color 120ms ease;
}

.contact-btn:hover,
.contact-btn:focus-visible {
  background: var(--accent);
  color: var(--on-accent);
}

/* Right-aligned above the page's first section, which is where the gateway
   puts it. */
.home-contact-row {
  display: flex;
  justify-content: flex-end;
  margin: 0 0 1.25rem;
}

/* A page's opening paragraph, read at body size with a measure — not the
   0.78rem muted aside it used to be while it lived in the masthead. */
.page-lede {
  max-width: 46rem;
  margin: 0 0 1.1rem;
  color: var(--ink);
  font-size: 0.92rem;
  line-height: 1.45;
}

body {
  margin: 0;
  font-family: "Space Grotesk", "Segoe UI", sans-serif;
  color: var(--ink);
  background: var(--paper);
  line-height: 1.45;
}

.shell {
  max-width: 1200px;
  margin: 0 auto;
  padding: 1.5rem 1.25rem 4rem;
}

.masthead {
  position: sticky;
  top: 0;
  z-index: 20;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}

.masthead .shell {
  padding-bottom: 1.25rem;
}

/* The actions column is two rows tall (nav, then the theme toggle), so a
   top-aligned title leaves dead space under it. Centring puts the lockup and
   title on the masthead's optical midline instead. */
.masthead-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

/* Brand lockup. The guideline asks for clear space around the logo equal to the
   height of the "L", which at a 1.55rem wordmark is about 0.9rem — that is the
   gap and the padding either side of the divider, not a decorative choice. */
.masthead-identity {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  min-width: 0;
}

.brand-lockup {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  text-decoration: none;
}

.brand-lockup .logomark {
  display: none;
  height: 1.7rem;
  width: auto;
}

.brand-lockup .wordmark {
  height: 1.55rem;
  width: auto;
}

.brand-divider {
  width: 2px;
  height: 2.35rem;
  background: var(--line);
  flex-shrink: 0;
}

/* Both wordmarks ship on every page and CSS chooses; swapping `src` on theme
   change would flash an unloaded image. Reverse-preferred is the brand's stated
   first choice on backgrounds darker than 50% grey, which is our dark default. */
.brand-lockup .wordmark-dark,
.site-foot-brand .wordmark-dark {
  display: block;
}

.brand-lockup .wordmark-light,
.site-foot-brand .wordmark-light,
:root[data-theme="light"] .brand-lockup .wordmark-dark,
:root[data-theme="light"] .site-foot-brand .wordmark-dark {
  display: none;
}

:root[data-theme="light"] .brand-lockup .wordmark-light,
:root[data-theme="light"] .site-foot-brand .wordmark-light {
  display: block;
}

/* A floor, so that a title and its tagline are never squeezed into a column too
   narrow to read them. Below this the row wraps instead — see the masthead
   breakpoint. */
.masthead-title {
  min-width: 16rem;
}

/* A row, not a column: with the toggle down to one glyph it belongs at the end
   of the nav rather than on a line of its own under it. That is a whole row off
   the masthead on every page. */
.masthead-actions {
  /* Positioned because the tool menu hangs off it. */
  position: relative;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.35rem;
  flex-shrink: 0;
}

.site-nav {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.35rem;
}

.site-nav-link,
.site-map-btn {
  display: inline-flex;
  justify-content: center;
  align-items: center;
  padding: 0.35rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 0.375rem;
  background: var(--paper);
  color: var(--ink);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-decoration: none;
}

/* A fixed min-height keeps the tabs on one baseline whether a label wraps or
   not, so the row does not go ragged when the nav is squeezed. */
.site-nav-link {
  min-height: 2.55rem;
  padding: 0.28rem 0.65rem;
  text-align: center;
  line-height: 1.15;
}

.site-nav-link:hover,
.site-map-btn:hover,
.theme-toggle:hover {
  border-color: var(--accent);
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Focus keeps the ink outline rather than the accent one. Primary Green is
   2.8:1 on the light page, under the 3:1 a focus indicator needs, and focus is
   the state that has to be seen. Hover can afford to be decorative. */
.site-nav-link:focus-visible,
.site-map-btn:focus-visible,
.theme-toggle:focus-visible {
  border-color: var(--accent);
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

/* The current tab is marked by an accent underline rather than an inverted
   fill. aria-current carries the same fact for anyone not seeing the color. */
.site-nav-link.is-current {
  background: var(--group);
  color: var(--ink);
  border-color: var(--line);
  box-shadow: inset 0 -2px 0 var(--accent);
}

/* The wordmark and a page title cannot share a phone-width masthead, so the
   lockup falls back to the logomark, which the brand publishes for exactly this
   and reads on either background. The divider goes with the wordmark.

   The masthead also has to wrap here. The actions column does not shrink, and at
   phone width it is wider than the viewport, so on one line it either pushes the
   nav off-screen or squeezes the title to zero. Giving the identity a full row
   puts the title above the nav and keeps both reachable. */
/* The nav does not shrink, so every pixel the masthead loses comes out of the
   title beside it. Left to itself that ends badly: at 850px the title column
   was 48px wide and the tagline was running to sixteen lines, which took the
   masthead to 479px — most of a laptop screen, above every page. So the two
   stop sharing a row before it gets there, and the nav goes full width
   underneath where it has room to wrap properly.

   1100px is where they stop fitting: the nav is 645px at its natural width and
   the identity needs about 400px beside it. */
@media (max-width: 1100px) {
  .masthead-inner {
    flex-wrap: wrap;
  }

  .masthead-identity {
    gap: 0.55rem;
    flex: 1 1 100%;
  }

  .masthead-actions {
    align-items: flex-start;
    width: 100%;
  }

  .site-nav {
    justify-content: flex-start;
  }
}

/* The wordmark and a page title cannot share a phone-width masthead, which is
   a narrower problem than the one above and keeps its own breakpoint. */
@media (max-width: 780px) {
  .brand-divider {
    display: none;
  }

  .brand-lockup .wordmark,
  :root[data-theme="light"] .brand-lockup .wordmark-light {
    display: none;
  }

  .brand-lockup .logomark {
    display: block;
  }
}

.site-foot {
  border-top: 1px solid var(--line);
  background: var(--surface);
}

.site-foot .shell {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem 1.25rem;
  padding-top: 1.25rem;
  padding-bottom: 1.5rem;
}

.site-foot-brand {
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  text-decoration: none;
}

.site-foot-brand .wordmark {
  height: 1.35rem;
  width: auto;
}

.site-foot-kicker {
  color: var(--muted);
  font-size: 0.78rem;
  letter-spacing: 0.02em;
}

/* The words are the button by default, and the glyph replaces them only where
   masking works. The other way round, a browser without mask support would draw
   the ::before as a plain filled square over hidden labels — a button that says
   nothing. This way that browser keeps the text one. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  margin: 0;
  padding: 0.35rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 0.375rem;
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  cursor: pointer;
}

/* Which word the button shows when the words are the button. The @supports
   block above hides both once the glyph takes over. */
.theme-label-light {
  display: none;
}

.theme-label-dark {
  display: inline;
}

:root[data-theme="light"] .theme-label-light {
  display: inline;
}

:root[data-theme="light"] .theme-label-dark {
  display: none;
}

@supports (mask-image: none) or (-webkit-mask-image: none) {
  /* A square, one glyph wide instead of as wide as "Light", sitting at the end
     of the nav's row rather than on a row of its own. Its accessible name is
     the aria-label theme.js keeps in sync. */
  .theme-toggle[aria-label] {
    width: 2.55rem;
    height: 2.55rem;
    padding: 0;
  }

  .theme-toggle[aria-label]::before {
    content: "";
    flex-shrink: 0;
    width: 1.15rem;
    height: 1.15rem;
    background: currentColor;
    -webkit-mask: var(--theme-icon) center / contain no-repeat;
    mask: var(--theme-icon) center / contain no-repeat;
  }

  .theme-toggle[aria-label] .theme-label-light,
  .theme-toggle[aria-label] .theme-label-dark {
    display: none;
  }
}

/* Space Grotesk is geometric and sets wide, so display sizes need negative
   tracking to hold together — without it the same typeface reads as a looser,
   lighter face. Matches the AI gateway research site so the two read as one. */
h1 {
  font-size: 1.75rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  margin: 0 0 0.35rem;
  text-wrap: balance;
}

/* The masthead title is a label for the page, not its headline, so it sits a
   step below the in-page h1. Kept after the base rule, not folded into the
   responsive block above, because a media query adds no specificity and the
   later declaration would win. */
.masthead h1,
.masthead-label {
  font-size: 1.35rem;
  margin: 0;
}

/* Tool pages label the section here and keep their h1 for the tool's own name,
   so this carries the title's weight without being a heading. */
.masthead-label {
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.2;
}

@media (max-width: 780px) {
  .masthead h1,
  .masthead-label {
    font-size: 1rem;
  }
}

/* The page description is moved out of here by theme.js — see .page-lede. This
   stays for anything else a masthead ever holds. */
.masthead p {
  margin: 0;
  color: var(--muted);
  max-width: 42rem;
}

a {
  color: inherit;
  text-decoration-color: var(--muted);
  text-underline-offset: 0.15em;
}

a:hover {
  text-decoration-color: currentColor;
}

.err {
  color: var(--no);
}

.hint,
.tag {
  color: var(--muted);
  font-size: 0.78rem;
}

.tag {
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin: 0 0 0.75rem;
}

.wrap {
  overflow: auto;
  border: 1px solid var(--line);
  background: var(--surface);
}

#summaries,
#matrix {
  scroll-margin-top: 5.5rem;
}

table {
  border-collapse: collapse;
  width: 100%;
  font-size: 0.85rem;
}

th,
td {
  border-bottom: 1px solid var(--line);
  padding: 0.55rem 0.7rem;
  text-align: left;
  vertical-align: top;
}

thead th {
  background: var(--surface);
  position: sticky;
  top: 0;
  z-index: 2;
  font-weight: 600;
  /* border-collapse: collapse drops borders off sticky cells while they are
     pinned, so draw the rule with a shadow that travels with the header. */
  box-shadow: inset 0 -2px 0 var(--ink);
}

tbody th {
  font-weight: 500;
  /* Wrapping, not nowrap. How much room this column gets depends on how many
     tools its family has: at 1440px a four-tool family gives it 347px and a
     six-tool family 246px, against labels that want 271px — so the six-tool one
     was running its labels out across the badges. Wrapping costs nothing where
     there is room, because there is nothing to wrap. */
  white-space: normal;
  background: var(--surface);
  /* Rows are as tall as their tallest cell — a badge, or a note running to
     three lines — and a title pinned to the top of one reads as though it
     belongs to the row above. */
  vertical-align: middle;
}

/* The badge is the whole of a matrix cell, so it centres with the title it
   answers — across the column as well as down the row, since a badge is a
   fixed-width mark and a left-aligned one drifts away from its heading. On a
   tool page only the status column does; the note and source columns are
   paragraphs and stay at the top. */
.matrix-family td,
.req-row > td:first-of-type {
  vertical-align: middle;
}

.matrix-family thead th:not(:first-child),
.matrix-family tbody td,
.col-status,
.req-row > td:first-of-type {
  text-align: center;
}

/* Column headings take the same green as every other heading on the site, and
   carry the same arrow the summary cards put after a tool name. */
/* A column's answers as one bar, sized by count. The track is the code
   background so an empty stretch reads as "nothing here" rather than as a
   status of its own. */
.col-meter {
  /* Pinned to the foot of the heading rather than flowing under the name, so
     every bar in a row lands on the same line however many lines the name above
     it took. "CodeGraphContext" wraps and "Joern" does not, and a bar that
     followed the name sat lower in one column than the next. The heading cell
     reserves the space with padding-bottom. */
  position: absolute;
  right: 0.7rem;
  bottom: 0.55rem;
  left: 0.7rem;
  display: flex;
  width: auto;
  max-width: 4.75rem;
  height: 0.35rem;
  margin: 0 auto;
  overflow: hidden;
  border-radius: 999px;
  background: var(--code-bg);
  /* The whole bar is the hover target, not each segment. */
  cursor: help;
}

.col-meter-seg {
  min-width: 0;
}

.col-meter-yes {
  background: var(--meter-yes);
}

.col-meter-paid {
  background: var(--meter-paid);
}

.col-meter-warn {
  background: var(--meter-warn);
}

.col-meter-no {
  background: var(--meter-no);
}

.col-meter-unknown {
  background: var(--meter-unknown);
}

.col-meter-na {
  background: var(--meter-na);
}

.candidate-name {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  color: var(--accent-text);
}

.sub {
  display: block;
  color: var(--muted);
  font-weight: 400;
  font-size: 0.75rem;
}

.cell-link {
  display: inline-block;
  text-decoration: none;
}

.inferred {
  position: relative;
  display: inline-block;
  margin-left: 0.15rem;
  color: var(--paid);
  font-weight: 700;
  font-size: 0.95rem;
  line-height: 1;
  cursor: help;
  vertical-align: super;
}

.mark-tip {
  cursor: help;
}

#inferred-float {
  position: fixed;
  z-index: 20;
  width: max-content;
  max-width: 22rem;
  padding: 0.45rem 0.55rem;
  /* Rounder than the rest of the chrome, which is 0.375rem. Floating panels get
     0.5rem so they read as sitting over the page rather than being part of it —
     it is what the gateway does, and #fact-float below matches. */
  border-radius: 0.5rem;
  background: var(--tip-bg);
  color: var(--tip-fg);
  font-size: 0.72rem;
  font-weight: 400;
  line-height: 1.35;
  white-space: pre-line;
  pointer-events: none;
}

tbody tr.row-open th,
tbody tr.row-open td {
  background: var(--row-open);
}

tbody tr.row-open th {
  box-shadow: inset 3px 0 0 var(--accent);
}

.group-row th {
  background: var(--group);
  border-bottom: 1px solid var(--line);
  padding: 0.65rem 0.7rem;
  color: var(--ink);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.035em;
  text-transform: uppercase;
}

/* The whole requirement title is the hover target for its category and full
   text, with a superscript on the end saying so. A marker at the head of the
   row was a second small thing to aim at when the title was already there. */
.row-help {
  margin-left: 0.1rem;
  color: var(--muted);
  font-size: 0.72em;
  font-weight: 700;
}

.row-title:hover .row-help,
.row-title:focus-visible .row-help {
  color: var(--accent-text);
}

/* On the tool page the title is the disclosure button, which is already a
   pointer and already focusable. Only the plain matrix title needs either. */
span.row-title {
  cursor: help;
}

span.row-title:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.hard-stop {
  display: inline-block;
  margin-left: 0.5rem;
  color: var(--no);
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.source {
  color: var(--muted);
  font-size: 0.75rem;
  max-width: 22rem;
}

.drill-toggle {
  display: inline;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  white-space: normal;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: var(--muted);
  text-underline-offset: 0.15em;
}

.drill-toggle:hover,
.drill-toggle:focus-visible {
  text-decoration-color: currentColor;
}

.drill-toggle[aria-expanded="true"] {
  font-weight: 650;
}

.drill-toggle::after {
  content: "▸";
  margin-left: 0.35rem;
  color: var(--muted);
  font-size: 0.7rem;
  text-decoration: none;
}

.drill-toggle[aria-expanded="true"]::after {
  content: "▾";
}

.drill-row > td {
  background: var(--surface);
  padding: 0;
}

.drill {
  padding: 0.9rem 1rem 1rem;
  border-left: 3px solid var(--accent);
  background: var(--drill);
}

.drill h3 {
  margin: 0;
  font-size: 0.95rem;
}

.drill-meta {
  margin: 0.15rem 0 0.75rem;
  color: var(--muted);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.drill dl {
  display: grid;
  grid-template-columns: 9rem minmax(0, 1fr);
  gap: 0.4rem 1rem;
  margin: 0;
}

.drill dt {
  color: var(--muted);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.drill dd {
  margin: 0;
  max-width: 52rem;
}

.drill .source-list {
  padding-left: 1.1rem;
}

.drill-links {
  margin: 0.85rem 0 0;
  font-size: 0.78rem;
  color: var(--muted);
}

.local-copy,
.source-third-party {
  font-size: 0.72rem;
  color: var(--muted);
}

.source-more {
  display: block;
  margin-top: 0.2rem;
  font-size: 0.7rem;
  color: var(--muted);
}

@media (max-width: 780px) {
  .drill dl {
    grid-template-columns: minmax(0, 1fr);
    gap: 0.15rem;
  }

  .drill dt {
    margin-top: 0.5rem;
  }
}

.sources p {
  max-width: 46rem;
  color: var(--muted);
  font-size: 0.9rem;
}

.sources h3 {
  font-size: 0.85rem;
  margin: 1rem 0 0.35rem;
}

.source-list {
  margin: 0;
  padding-left: 1.2rem;
}

.source-list a {
  word-break: break-all;
}

/* A result is a coloured disc with a glyph rather than a word. Six statuses
   read faster as shape and colour than as six words to parse, and the status
   column stops being as wide as "unknown". The word rides along as the
   accessible name and as the hover title, because a disc says neither.

   Class names are prefixed: `.no` and `.yes` on their own are the kind of name
   something else in a stylesheet this size will eventually want. */
/* The ring is the status' own colour at 42%, which is what keeps the disc a
   disc where its fill sits close to the page — on the dark theme the deep fills
   are 1.0-1.4:1 against it. Each .status-* sets the colour; the width lives
   here so there is one place it can change. */
.status {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  border: 1px solid transparent;
  border-radius: 50%;
  vertical-align: middle;
}

/* The glyph carries the badge, so it takes most of the disc rather than
   sitting in the middle of it with a ring around the outside. */
.status svg {
  width: 1.44rem;
  height: 1.44rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.status-yes {
  color: var(--st-yes);
  background: var(--st-yes-bg);
  border-color: color-mix(in srgb, var(--st-yes) 42%, transparent);
}

.status-paid {
  color: var(--st-paid);
  background: var(--st-paid-bg);
  border-color: color-mix(in srgb, var(--st-paid) 42%, transparent);
}

.status-no {
  color: var(--st-no);
  background: var(--st-no-bg);
  border-color: color-mix(in srgb, var(--st-no) 42%, transparent);
}

.status-no-plus {
  color: var(--st-warn);
  background: var(--st-warn-bg);
  border-color: color-mix(in srgb, var(--st-warn) 42%, transparent);
}

.status-unknown {
  color: var(--st-unknown);
  background: var(--st-unknown-bg);
  border-color: color-mix(in srgb, var(--st-unknown) 42%, transparent);
}

.status-na {
  color: var(--st-na);
  background: var(--st-na-bg);
  border-color: color-mix(in srgb, var(--st-na) 42%, transparent);
}

/* The legend names what every badge in the tables below means, so it has to
   stay reachable while those tables are being read. It pins under the masthead
   rather than at the top of the window: the masthead is sticky too, and a bar
   that covered the header would be worse than one that scrolled away.
   --masthead-h comes from theme.js; until it lands the legend simply does not
   pin, which is the safe way round. */
.legend {
  position: sticky;
  /* No fallback on purpose: with one, a missing --masthead-h would pin the
     legend at the top of the viewport and lay it over the sticky masthead. With
     none the declaration is invalid until theme.js sets it, `top` stays auto,
     and the legend simply does not pin. */
  top: var(--masthead-h);
  z-index: 10;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1rem;
  margin: 0 0 1rem;
  padding: 0.6rem 0;
  background: var(--paper);
  border-bottom: 1px solid var(--line);
  font-size: 0.8rem;
  color: var(--muted);
}

/* Pushed to the far end of the legend, which is the one row on the page that is
   already about how a result is drawn. */
.legend-palette {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-left: auto;
}

.legend-palette select {
  padding: 0.2rem 0.35rem;
  border: 1px solid var(--line);
  border-radius: 0.3rem;
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 0.75rem;
}

/* The legend used to go static here, because the masthead was 363px tall at
   781px and pinning anything under it left over half a phone screen as chrome.
   The masthead wraps properly now, so the legend stays pinned at every width —
   it is what says the six glyphs below it mean anything, which is worth more
   while a table is being scrolled than the height it costs. It does get tighter
   on a narrow screen, where it wraps to four rows. */
@media (max-width: 900px) {
  .legend {
    gap: 0.35rem 0.7rem;
    padding: 0.45rem 0;
    font-size: 0.75rem;
  }

  /* margin-left: auto pins it to the right of whatever line it wraps onto,
     which on a narrow screen is a line of its own with nothing to sit against. */
  .legend-palette {
    margin-left: 0;
  }
}

section {
  margin-top: 2.5rem;
}

section h2 {
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  margin: 0 0 0.75rem;
}

.cards {
  display: grid;
  gap: 0.75rem;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}

article.prose,
.cards article {
  border: 1px solid var(--line);
  background: var(--surface);
  padding: 0.85rem 1rem;
}

.cards article h3 {
  margin: 0;
  font-size: 0.95rem;
}

.cards article p {
  margin: 0.35rem 0 0;
  color: var(--muted);
  font-size: 0.85rem;
}

.crumbs {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  font-size: 0.8rem;
  color: var(--muted);
  margin-bottom: 1rem;
}

.page-head {
  margin-bottom: 1.5rem;
}

.page-head .sub {
  margin: 0 0 0.5rem;
  font-size: 0.9rem;
}

.conclusion {
  margin: 0 0 0.75rem;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--line);
  border-left: 3px solid var(--ink);
  background: var(--surface);
  font-size: 0.92rem;
  max-width: 52rem;
}

.conclusion-label {
  display: block;
  margin-bottom: 0.2rem;
  color: var(--muted);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.headlines {
  margin: 1.25rem 0 1.5rem;
}

.headlines h2 {
  font-size: 1rem;
  margin: 0 0 0.2rem;
}

.headline-strip {
  display: grid;
  gap: 0.75rem;
  grid-template-columns: repeat(auto-fill, minmax(17.5rem, 1fr));
  margin-top: 0.75rem;
}

.fit-cards {
  display: grid;
  gap: 0.6rem;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  margin-top: 0.75rem;
}

.fit-card {
  border: 1px solid var(--line);
  background: var(--surface);
  padding: 0.65rem 0.8rem 0.75rem;
}

.fit-card h3 {
  margin: 0 0 0.3rem;
  font-size: 0.88rem;
}

.fit-card p {
  margin: 0;
  font-size: 0.8rem;
  color: var(--muted);
}

.fit-card-empty {
  font-style: italic;
}

.headline {
  border: 1px solid var(--line);
  background: var(--surface);
  padding: 0.85rem 1rem 0.95rem;
}

.headline h3 {
  margin: 0 0 0.65rem;
  font-size: 0.95rem;
}

/* A rule between every segment. Only the TL;DR had one before, which read as
   "the TL;DR is separate" rather than "these are four separate answers".
   Padding on both sides of the border rather than a margin, so each rule sits
   midway between the two answers it divides. */
.headline-item {
  margin: 0;
  padding: 0.55rem 0;
  border-top: 1px solid var(--line);
}

.headline-item:first-child {
  padding-top: 0;
  border-top: 0;
}

.headline-item:last-child {
  padding-bottom: 0;
}

.headline-label {
  display: block;
  margin-bottom: 0.12rem;
  color: var(--muted);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.headline-item p {
  margin: 0;
  font-size: 0.85rem;
}


.headline-tldr p {
  font-size: 0.9rem;
}

.page-head .headline {
  margin: 0.65rem 0 0.75rem;
  max-width: 52rem;
  border-left: 3px solid var(--ink);
}

/* The tool menu, hung off its nav button. Fourteen tool pages will not fit in
   a nav bar and did not belong stacked on the tool pages themselves, so they
   sit behind one button as a list grouped by family — the same grouping the
   matrix and the summaries use. Absolutely positioned inside the masthead
   rather than fixed to the viewport, so it travels with a sticky header
   instead of needing to be re-placed on every scroll. */
#tool-menu {
  position: absolute;
  top: 100%;
  right: 0;
  z-index: 40;
  min-width: 14rem;
  max-height: min(32rem, 72vh);
  margin-top: 0.35rem;
  padding: 0.5rem 0;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: 0.375rem;
  background: var(--surface);
  box-shadow: 0 10px 28px rgb(0 0 0 / 0.35);
}

.tool-menu-table {
  width: 100%;
  font-size: 0.8rem;
}

/* No rules between rows: a menu is a list, and the family headers are already
   doing the dividing. */
.tool-menu-table th,
.tool-menu-table td {
  border: 0;
  padding: 0.18rem 0.85rem;
  text-align: left;
  white-space: nowrap;
}

.tool-menu-family th {
  padding-top: 0.55rem;
  color: var(--muted);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.tool-menu-table tbody:first-child .tool-menu-family th {
  padding-top: 0;
}

.tool-menu-table a {
  display: block;
  text-decoration: none;
}

.tool-menu-table a:hover,
.tool-menu-table a:focus-visible {
  color: var(--accent-text);
  text-decoration: underline;
}

.tool-menu-current {
  display: block;
  color: var(--accent-text);
  font-weight: 650;
}

.tool-menu-loading,
.tool-menu-error {
  margin: 0;
  padding: 0.25rem 0.85rem;
  color: var(--muted);
  font-size: 0.8rem;
}

article.prose {
  margin-top: 1.5rem;
  padding: 1rem 1.25rem;
}

.prose h1,
.prose h2,
.prose h3 {
  font-size: 1rem;
  margin: 1.25rem 0 0.5rem;
}

.prose h1:first-child,
.prose h2:first-child {
  margin-top: 0;
}

.prose p,
.prose ul,
.prose ol {
  font-size: 0.9rem;
}

.prose pre {
  background: var(--code-bg);
  padding: 0.75rem;
  overflow-x: auto;
  font-size: 0.8rem;
}

.prose code {
  background: var(--code-bg);
  padding: 0.05rem 0.25rem;
  font-size: 0.85em;
}

.prose pre code {
  background: none;
  padding: 0;
}

.prose blockquote {
  margin: 0.75rem 0;
  padding-left: 0.9rem;
  border-left: 3px solid var(--line);
  color: var(--muted);
}

.prose table {
  margin: 0.75rem 0;
}

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

.guide-steps {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 0.75rem;
  margin: 0 0 1.25rem;
  padding: 0;
  list-style: none;
  font-size: 0.78rem;
  color: var(--muted);
}

.guide-steps li {
  padding: 0.15rem 0.45rem;
  border: 1px solid var(--line);
  background: var(--surface);
}

.guide-steps li.current {
  border-color: var(--ink);
  color: var(--ink);
  font-weight: 650;
}

.guide-steps li.done {
  color: var(--ink);
}

.guide-panel {
  max-width: 46rem;
}

.guide-panel h2 {
  font-size: 1.1rem;
  margin: 0 0 0.5rem;
}

.guide-panel > p {
  color: var(--muted);
  font-size: 0.92rem;
  max-width: 42rem;
}

.guide-list {
  margin: 0.75rem 0 1rem;
  padding-left: 1.2rem;
  font-size: 0.9rem;
}

.guide-fieldset {
  margin: 1rem 0;
  padding: 0;
  border: 0;
}

.guide-fieldset legend {
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.45rem;
}

.guide-option,
.guide-check {
  display: flex;
  gap: 0.65rem;
  align-items: flex-start;
  margin: 0 0 0.55rem;
  padding: 0.7rem 0.85rem;
  border: 1px solid var(--line);
  background: var(--surface);
  cursor: pointer;
}

.guide-option:has(input:checked),
.guide-check:has(input:checked) {
  border-color: var(--ink);
  box-shadow: inset 3px 0 0 var(--accent);
}

.guide-option input,
.guide-check input {
  margin-top: 0.2rem;
  flex-shrink: 0;
}

.guide-option-body,
.guide-check-body {
  display: block;
}

.guide-option-label,
.guide-check-label {
  display: block;
  font-weight: 600;
  font-size: 0.92rem;
}

.guide-option-detail,
.guide-check-detail {
  display: block;
  margin-top: 0.15rem;
  color: var(--muted);
  font-size: 0.85rem;
}

.guide-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 1.25rem;
}

.guide-actions button,
.guide-req-actions button {
  margin: 0;
  padding: 0.45rem 0.85rem;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
}

.guide-next {
  border-color: var(--ink);
}

.guide-next:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.guide-actions button:hover,
.guide-actions button:focus-visible,
.guide-req-actions button:hover,
.guide-req-actions button:focus-visible {
  border-color: var(--ink);
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

.guide-req-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
  margin: 0.75rem 0 0.35rem;
}

.guide-recap {
  display: grid;
  grid-template-columns: 10rem minmax(0, 1fr);
  gap: 0.35rem 1rem;
  margin: 0 0 1rem;
  font-size: 0.88rem;
}

.guide-recap dt {
  color: var(--muted);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.guide-recap dd {
  margin: 0;
}

.guide-results {
  display: grid;
  gap: 0.75rem;
  margin: 1rem 0;
}

.guide-result {
  border: 1px solid var(--line);
  background: var(--surface);
  padding: 0.85rem 1rem 0.95rem;
}

.guide-result-blocked {
  opacity: 0.85;
}

.guide-result-head h3 {
  margin: 0;
  font-size: 0.98rem;
}

.guide-result p {
  margin: 0.45rem 0 0;
  font-size: 0.88rem;
}

.guide-score {
  font-weight: 650;
}

.guide-blocked {
  color: var(--no);
  font-weight: 600;
}

.guide-result details {
  margin-top: 0.65rem;
  font-size: 0.85rem;
}

.guide-gap-list {
  margin: 0.35rem 0 0;
  padding-left: 1.1rem;
}

.guide-gap-list li {
  margin: 0.35rem 0;
}

.guide-workaround {
  display: block;
  margin-top: 0.1rem;
  color: var(--muted);
  font-size: 0.8rem;
}

.guide-links,
.guide-footer-links {
  margin-top: 0.65rem;
  font-size: 0.82rem;
  color: var(--muted);
}

.guide-subsection {
  margin-top: 1.75rem;
}

.guide-subsection h3 {
  font-size: 0.95rem;
  margin: 0 0 0.5rem;
}

.guide-ruled-out {
  margin: 0;
  padding-left: 1.1rem;
  font-size: 0.88rem;
}

.guide-empty {
  padding: 0.85rem 1rem;
  border: 1px solid var(--line);
  background: var(--surface);
  font-size: 0.9rem;
}

@media (max-width: 780px) {
  .guide-recap {
    grid-template-columns: minmax(0, 1fr);
    gap: 0.15rem;
  }

  .guide-recap dt {
    margin-top: 0.45rem;
  }
}

/* Tools Summary: one section per family, each explaining what the family gives
   an agent, then the tools we are investigating in it. */
.tool-family {
  margin: 2rem 0 0;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
}

.tool-family:first-of-type {
  border-top: 0;
  padding-top: 0;
}

/* Both the family and each tool are <details>. Native disclosure means keyboard
   operation, find-in-page expansion, and correct semantics come for free, with
   no JS on a site that has none. */
.tool-family > summary,
.matrix-family > summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
}

.tool-family > summary::-webkit-details-marker,
.matrix-family > summary::-webkit-details-marker {
  display: none;
}

/* The triangle is drawn rather than inherited so it can sit on the baseline of
   the label and turn with the same easing in both places. */
.tool-family > summary::before,
.matrix-family > summary::before {
  content: "";
  flex-shrink: 0;
  width: 0;
  height: 0;
  border-left: 0.34rem solid currentColor;
  border-top: 0.28rem solid transparent;
  border-bottom: 0.28rem solid transparent;
  color: var(--muted);
  transform: translateY(-0.1em);
  transition: transform 0.15s ease;
}

.tool-family[open] > summary::before,
.matrix-family[open] > summary::before {
  transform: translateY(-0.1em) rotate(90deg);
}

.tool-family > summary:hover::before,
.matrix-family > summary:hover::before {
  color: var(--accent);
}

.tool-family > summary:focus-visible,
.matrix-family > summary:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
  border-radius: 0.25rem;
}

.tool-family > summary .tool-family-label,
.matrix-family > summary .matrix-family-label {
  display: inline;
  margin: 0;
  min-width: 0;
}

/* Kept on one line: a wrapped "3 / tools" reads as two facts, not one. */
.tool-count {
  color: var(--muted);
  font-size: 0.78rem;
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.tool-family-body {
  padding-top: 0.35rem;
}

.tool-name {
  font-size: 1rem;
  font-weight: 650;
}

.tool-body {
  padding-top: 0.5rem;
}

.tool-links {
  margin: 0.75rem 0 0;
  font-size: 0.85rem;
}

.tool-family-blurb {
  max-width: 46rem;
  margin: 0 0 0.35rem;
}

/* No margin: every card is a grid child now, and a top margin on top of the
   grid's row gap made the rows 1.75rem apart while the columns stayed 0.75rem.
   Spacing comes from the gap alone. */
.tool-card {
  padding: 0.9rem 1rem 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius, 0.375rem);
  background: var(--surface);
}


.tool-card-pending {
  background: var(--paper);
}

.tool-pending,
.tool-example {
  margin: 0;
  color: var(--muted);
  font-size: 0.85rem;
}

/* An invented entry is dashed rather than solid, so it reads as a placeholder
   at a glance and not as one more researched tool. */
.tool-card-example {
  background: var(--paper);
  border-style: dashed;
}

.tool-example-tag {
  display: inline-block;
  margin-left: 0.4rem;
  padding: 0.1rem 0.4rem;
  border: 1px solid var(--accent);
  border-radius: 0.25rem;
  color: var(--muted);
  font-size: 0.68rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  vertical-align: 0.1em;
}

/* Flagged rather than styled as an error: it is correct content that is meant
   to be deleted, so it should read as a note, not as a fault. */
.tool-example {
  margin: 0 0 0.5rem;
  padding-left: 0.6rem;
  border-left: 3px solid var(--accent);
  font-style: italic;
}

.tool-description {
  max-width: 46rem;
  margin: 0 0 0.75rem;
}

/* Label and value sit side by side on a wide screen and stack on a narrow one,
   which is what the drill-down definition lists already do. */
.tool-facts {
  margin: 0;
  font-size: 0.9rem;
}

.tool-facts > div {
  display: grid;
  grid-template-columns: 7.5rem minmax(0, 1fr);
  gap: 0.15rem 0.75rem;
  padding: 0.2rem 0;
}

.tool-facts dt {
  color: var(--muted);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.tool-facts dd {
  margin: 0;
  min-width: 0;
  overflow-wrap: anywhere;
}

.tool-deps {
  margin: 0;
  padding-left: 1.1rem;
}

.tool-deps li {
  padding: 0.05rem 0;
}

@media (max-width: 780px) {
  .tool-facts > div {
    grid-template-columns: minmax(0, 1fr);
  }

  .tool-facts dt {
    margin-top: 0.4rem;
  }
}

/* Home. A splash rather than a summary: what these tools are, what an agent
   cannot answer without one, and what this comparison claims. The copy is
   static in index.html; home.js fills the families, the counts, and the
   destinations so none of the three can drift from the research.

   Headings here are --ink with a green eyebrow above them, which is the
   opposite of the rest of the site — see the `h2` override below. */
.splash-hero {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(18rem, 0.95fr);
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
  min-height: min(47rem, calc(100vh - var(--chrome-h, 5.5rem)));
  padding: clamp(2rem, 5vw, 4rem) 0 clamp(3.5rem, 8vw, 7rem);
}

/* The section heading is the ink-coloured line; the green is the eyebrow above
   it. Everywhere else on the site an h2 is green, so this undoes that for the
   splash only. */
.splash-hero h2,
.splash-definition > h2,
.splash-problem > h2,
.splash-purpose > h2,
.splash-start > h2 {
  color: var(--ink);
}

.splash-prompt {
  display: inline-flex;
  align-items: center;
  max-width: 100%;
  gap: 0.55rem;
  margin-bottom: 1.5rem;
  padding: 0.45rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 0.375rem;
  background: var(--surface);
  color: var(--muted);
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 0.68rem;
}

.splash-prompt > span {
  flex: none;
  color: var(--accent-text);
}

.splash-prompt code {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.splash-prompt i {
  width: 0.4rem;
  height: 0.8rem;
  flex: none;
  background: var(--accent);
  animation: splash-caret 1.1s steps(1) infinite;
}

@keyframes splash-caret {
  50% {
    opacity: 0;
  }
}

.splash-eyebrow {
  margin: 0 0 0.8rem;
  color: var(--accent-text);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.splash-title {
  max-width: 24ch;
  margin: 0 0 0.45rem;
  font-size: clamp(1.85rem, 4vw, 3rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.1;
}

.splash-revision {
  margin: 0 0 1rem;
  color: var(--muted);
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.04em;
}

.splash-lede {
  max-width: 42rem;
  margin: 0;
  color: var(--muted);
  font-size: clamp(1rem, 1.6vw, 1.2rem);
  font-weight: 500;
  letter-spacing: -0.01em;
  line-height: 1.5;
}

/* The lede runs to more than one paragraph. Spacing goes between them rather
   than on each, so the last one still sits flush against .splash-actions and
   its own margin-top governs that gap. */
.splash-lede + .splash-lede {
  margin-top: 0.9rem;
}

.splash-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: 1.6rem;
}

.splash-action {
  display: inline-flex;
  align-items: center;
  min-height: 2.8rem;
  padding: 0.7rem 1.25rem;
  border: 1px solid transparent;
  border-radius: 0.375rem;
  color: var(--ink);
  font-size: 0.86rem;
  font-weight: 600;
  text-decoration: none;
  transition: border-color 120ms ease, transform 120ms ease;
}

.splash-action:hover {
  transform: translateY(-1px);
}

.splash-action-primary {
  background: var(--accent);
  color: var(--on-accent);
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.splash-action-secondary {
  border-color: var(--line);
}

.splash-action-secondary:hover {
  border-color: var(--muted);
}

/* ---- the same schedule without movement ----

   prefers-reduced-motion is about things that move: travel, parallax, zoom.
   A cross-fade is what you replace motion *with*, not something to strip as
   well — so under that preference the sequence still runs and still means what
   it meant, it just stops sliding and scaling. These are the fade-only twins
   of the sets above, on identical percentages. Generated from the same table,
   so the two cannot disagree about when a stage lights. */

@keyframes pipe-spark-1-still {
  0%,
  3% {
    opacity: 0.3;
  }
  6%,
  88% {
    opacity: 1;
  }
  94%,
  100% {
    opacity: 0.3;
  }
}

@keyframes pipe-spark-2-still {
  0%,
  22% {
    opacity: 0.3;
  }
  25%,
  88% {
    opacity: 1;
  }
  94%,
  100% {
    opacity: 0.3;
  }
}

@keyframes pipe-spark-3-still {
  0%,
  76% {
    opacity: 0.3;
  }
  79%,
  88% {
    opacity: 1;
  }
  94%,
  100% {
    opacity: 0.3;
  }
}

@keyframes pipe-wire-1-still {
  0%,
  9% {
    opacity: 0;
  }
  19%,
  88% {
    opacity: 1;
  }
  94%,
  100% {
    opacity: 0;
  }
}

@keyframes pipe-wire-2-still {
  0%,
  28% {
    opacity: 0;
  }
  38%,
  88% {
    opacity: 1;
  }
  94%,
  100% {
    opacity: 0;
  }
}

@keyframes pipe-wire-3-still {
  0%,
  63% {
    opacity: 0;
  }
  73%,
  88% {
    opacity: 1;
  }
  94%,
  100% {
    opacity: 0;
  }
}

/* The reach diagram. Four depths one question can be answered at, widening
   down the panel, with only a handful of marks lit at any of them — reach
   grows, what comes back does not. HTML rather than SVG so the labels are real
   text at rem sizes; an SVG <text> at a fixed viewBox size shrinks with the
   panel and stops being readable.

   The field of files is a repeating gradient, so a track is one element however
   wide it gets. --span (how far the depth reaches) and --at (where a lit mark
   sits) are set inline: geometry, not colour. */
.reach {
  position: relative;
  padding: clamp(1.25rem, 3vw, 2rem);
  border: 1px solid var(--line);
  border-radius: 0.5rem;
  background: var(--surface);
  overflow: hidden;
}

.reach-head,
.reach-row,
.reach-key {
  position: relative;
  z-index: 1;
}

.reach-head {
  display: flex;
  justify-content: space-between;
  margin-bottom: 1.1rem;
  color: var(--muted);
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 0.62rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.reach-head span:last-child {
  color: var(--accent-text);
}

.reach-row {
  display: grid;
  grid-template-columns: minmax(0, 8.5rem) minmax(0, 1fr);
  gap: 0.75rem;
  align-items: center;
  margin-bottom: 0.7rem;
}

.reach-label {
  overflow: hidden;
  color: var(--muted);
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 0.68rem;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The unlit field: a tick every 7px, none of it ever read. --span is how far
   this depth reaches, so the tracks form a widening staircase down the panel. */
/* --reach-pitch and --reach-bar are the field's geometry, and they are read
   back by home.js when it places a mark. That is the whole reason they are
   custom properties: a mark is not a decoration floating over the field, it is
   one of those bars lit up, so it has to land exactly on one. Percentages
   cannot do that — the field is a pixel grid and a percentage of the track
   lands wherever it lands. */
.reach-track {
  --reach-pitch: 7px;
  --reach-bar: 2px;
  position: relative;
  display: block;
  width: var(--span, 100%);
  height: 1.15rem;
  border-radius: 0.15rem;
  background-image: repeating-linear-gradient(
    90deg,
    color-mix(in srgb, var(--muted) 40%, transparent) 0 var(--reach-bar),
    transparent var(--reach-bar) var(--reach-pitch)
  );
}

/* A mark is one location that came back. Which ones, and how many, are picked
   fresh every pass by home.js — a fixed set glowing on a loop reads as a
   decoration, and the point of this figure is that a query returns a different
   handful each time. So position comes in on --at, and the lit state is a
   class with a transition rather than keyframes, the same way the graph in the
   pipeline below works. */
.reach-track i {
  position: absolute;
  top: -0.15rem;
  left: var(--at, 0px);
  width: var(--reach-bar);
  height: calc(100% + 0.3rem);
  border-radius: 0.15rem;
  background: var(--accent-text);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 22%, transparent);
  opacity: 0.18;
  transform: scaleY(0.55);
  transition: opacity 320ms ease, transform 320ms ease;
}

.reach-track i.is-lit {
  opacity: 1;
  transform: scaleY(1);
}

.reach-key {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 1.1rem 0 0;
  color: var(--muted);
  font-size: 0.7rem;
}

.reach-key i {
  width: 2px;
  height: 0.8rem;
  flex: none;
  border-radius: 0.15rem;
  background: var(--accent-text);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 22%, transparent);
}

/* The hero's right column holds two figures now, stacked. */
.hero-figures {
  display: grid;
  gap: clamp(0.75rem, 2vw, 1.1rem);
  min-width: 0;
}

/* The pipeline. One pass of the thing this site is about: a prompt goes in,
   code intelligence answers it out of the repository and its dependencies,
   and the answer goes on to the agent.

   Every animation on it runs the same --pipe-cycle with a different delay, so
   they are phase-locked by construction — one duration, so they cannot drift
   apart the way independently-timed animations do. The delays are the running
   order, read top to bottom below. */
.pipeline {
  --pipe-cycle: 10s;
  position: relative;
  padding: clamp(1rem, 2.5vw, 1.5rem);
  border: 1px solid var(--line);
  border-radius: 0.5rem;
  background: var(--surface);
}

.pipeline-head {
  display: flex;
  justify-content: space-between;
  margin-bottom: 0.9rem;
  color: var(--muted);
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 0.62rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.pipeline-head span:last-child {
  color: var(--accent-text);
}

.pipeline-row {
  display: grid;
  grid-template-columns:
    auto minmax(1rem, 1fr) auto minmax(1rem, 1fr)
    auto minmax(1rem, 1fr) auto;
  gap: 0.4rem;
  align-items: center;
}

.pipe-step {
  display: grid;
  justify-items: center;
  gap: 0.45rem;
  min-width: 0;
}

/* currentColor is what the drawings stroke with, so the step's colour is the
   drawing's colour. Unlit is --muted; the lit phase takes it to the green. */
/* The line art stays muted for the whole pass. Only the sparks light on the
   prompt and the agent, and the graph lights node by node — turning a whole
   drawing green read as a state change rather than as something happening
   inside it. currentColor is what the strokes follow. */
.pipe-art {
  display: block;
  color: var(--muted);
}

.pipe-art svg {
  display: block;
  width: 100%;
  height: auto;
}

/* Sized on the height of the ink, not on the CSS width. Each drawing has a
   different aspect ratio and a different amount of padding inside its own
   viewBox: the ink fills 90% of the prompt's box, 81% of the agent's, 94% of
   the repository graph's and 79% of the gears'. Setting them all to one width
   would leave them looking unmatched.

   The repository graph is drawn at 78px of ink and the other three at 62, four
   fifths of it. Matched exactly they were too heavy: the graph is thin strokes
   around a lot of empty space while the terminal, the head and the gears are
   dense, and a dense shape reads larger than a sparse one of the same measured
   size. The eye wants them a little smaller, not equal.

   The 78 is itself down from 84, where four drawings needed more row than the
   panel has. */
.pipe-step-prompt .pipe-art {
  width: clamp(3.76rem, 11.2vw, 5.28rem);
}

.pipe-step-agent .pipe-art {
  width: clamp(3.44rem, 10.16vw, 4.8rem);
}

.pipe-step-ci .pipe-art {
  width: clamp(3.7rem, 11vw, 5.2rem);
}

.pipe-step-work .pipe-art {
  width: clamp(3.52rem, 10.4vw, 4.92rem);
}

.pipe-name {
  color: var(--muted);
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 0.6rem;
  letter-spacing: 0.04em;
  text-align: center;
}

/* The wire. A dim rail, a fill that grows left to right, and a dot riding the
   leading edge. */
.pipe-wire {
  position: relative;
  display: block;
  height: 2px;
  min-width: 0;
  border-radius: 1px;
  background: var(--line);
}

.pipe-wire i {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0;
  border-radius: 1px;
  background: var(--accent-text);
  animation-duration: var(--pipe-cycle);
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

.pipe-wire b {
  position: absolute;
  top: 50%;
  left: 0;
  width: 0.3rem;
  height: 0.3rem;
  margin: -0.15rem 0 0 -0.15rem;
  border-radius: 50%;
  background: var(--accent-text);
  opacity: 0;
  animation-duration: var(--pipe-cycle);
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* ---- what the classed parts do ----

   Base properties only. The animation-names and the one delay are in the
   running order below, so this block must not set animation-name. */

/* Lit by class rather than by keyframes: the set is randomised each pass, and
   a transition is what makes an arbitrary set fade in and out cleanly. */
.pipeline .ci-node,
.pipeline .ci-link,
.pipeline .ci-file {
  stroke: var(--muted);
  transition: stroke 280ms ease, opacity 280ms ease;
}

.pipeline .ci-link,
.pipeline .ci-file {
  opacity: 0.55;
}

.pipeline .ci-node.is-lit,
.pipeline .ci-link.is-lit,
.pipeline .ci-file.is-lit {
  stroke: var(--accent-text);
  opacity: 1;
}

/* The spark inside the repository hexagon. It is a fill rather than a stroke,
   so it dims and lights on opacity while everything around it moves stroke. */
.pipeline .ci-core {
  opacity: 0.22;
  transition: opacity 280ms ease;
}

.pipeline .ci-core.is-lit {
  opacity: 1;
}

.pipeline .ci-spark {
  transform-box: fill-box;
  transform-origin: center;
  opacity: 0.3;
  animation-duration: var(--pipe-cycle);
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* The terminal waiting for input, before the prompt is sent. */
.pipeline .ci-caret {
  animation-name: pipe-caret;
  animation-duration: var(--pipe-cycle);
  animation-timing-function: steps(1);
  animation-iteration-count: infinite;
}

@keyframes pipe-caret {
  0%,
  4%,
  12%,
  20% {
    opacity: 1;
  }
  8%,
  16%,
  24%,
  100% {
    opacity: 0.15;
  }
}

/* ---- the running order ----

   Four stages: the prompt goes to the agent, the agent asks code
   intelligence, and then the work happens. The pass accumulates — nothing
   switches off until the work is done, and then the whole chain clears
   together and starts again.

   That is why there are no animation-delays. A delay shifts an element's
   whole timeline including when it ends, so elements sharing one keyframe set
   could never go dark at the same moment. Each start is written into its own
   percentages against one 10s cycle:

     0.3s  prompt spark lights, and stays lit
     0.9s  first wire starts filling
     1.9s  first wire full, its dot has arrived
     2.2s  agent sparks light, and stay lit
     2.8s  second wire starts filling
     3.8s  second wire full
     4s  the graph starts lighting — home.js, GRAPH_IN
     6.3s  third wire starts filling
     7.3s  third wire full
     7.6s  the gears' sparks light, and stay lit
     8.8s  the whole chain fades — GRAPH_OUT matches
     9.4s  dark, and round again

   Longhands rather than the animation shorthand throughout: a shorthand here
   would reset the others and collapse the sequence. */

.pipe-step-prompt .ci-spark {
  animation-name: pipe-spark-1;
}

.pipe-step-agent .ci-spark {
  animation-name: pipe-spark-2;
}

.pipe-step-work .ci-spark {
  animation-name: pipe-spark-3;
}

/* Barely apart, so two sparks in one drawing do not pop as one shape. */
.pipeline .ci-spark-sm {
  animation-delay: 0.12s;
}

.pipe-wire-1 i {
  animation-name: pipe-fill-1;
}

.pipe-wire-1 b {
  animation-name: pipe-travel-1;
}

.pipe-wire-2 i {
  animation-name: pipe-fill-2;
}

.pipe-wire-2 b {
  animation-name: pipe-travel-2;
}

.pipe-wire-3 i {
  animation-name: pipe-fill-3;
}

.pipe-wire-3 b {
  animation-name: pipe-travel-3;
}

@keyframes pipe-spark-1 {
  0%,
  3% {
    opacity: 0.3;
    transform: scale(0.88);
  }
  6%,
  88% {
    opacity: 1;
    transform: scale(1);
  }
  94%,
  100% {
    opacity: 0.3;
    transform: scale(0.88);
  }
}

@keyframes pipe-spark-2 {
  0%,
  22% {
    opacity: 0.3;
    transform: scale(0.88);
  }
  25%,
  88% {
    opacity: 1;
    transform: scale(1);
  }
  94%,
  100% {
    opacity: 0.3;
    transform: scale(0.88);
  }
}

@keyframes pipe-spark-3 {
  0%,
  76% {
    opacity: 0.3;
    transform: scale(0.88);
  }
  79%,
  88% {
    opacity: 1;
    transform: scale(1);
  }
  94%,
  100% {
    opacity: 0.3;
    transform: scale(0.88);
  }
}

@keyframes pipe-fill-1 {
  0%,
  9% {
    width: 0;
  }
  19%,
  88% {
    width: 100%;
  }
  94%,
  100% {
    width: 0;
  }
}

@keyframes pipe-fill-2 {
  0%,
  28% {
    width: 0;
  }
  38%,
  88% {
    width: 100%;
  }
  94%,
  100% {
    width: 0;
  }
}

@keyframes pipe-fill-3 {
  0%,
  63% {
    width: 0;
  }
  73%,
  88% {
    width: 100%;
  }
  94%,
  100% {
    width: 0;
  }
}

@keyframes pipe-fill-1-down {
  0%,
  9% {
    height: 0;
  }
  19%,
  88% {
    height: 100%;
  }
  94%,
  100% {
    height: 0;
  }
}

@keyframes pipe-fill-2-down {
  0%,
  28% {
    height: 0;
  }
  38%,
  88% {
    height: 100%;
  }
  94%,
  100% {
    height: 0;
  }
}

@keyframes pipe-fill-3-down {
  0%,
  63% {
    height: 0;
  }
  73%,
  88% {
    height: 100%;
  }
  94%,
  100% {
    height: 0;
  }
}

@keyframes pipe-travel-1 {
  0%,
  9% {
    left: 0;
    opacity: 0;
  }
  10.5% {
    opacity: 1;
  }
  19% {
    left: 100%;
    opacity: 1;
  }
  21%,
  100% {
    left: 100%;
    opacity: 0;
  }
}

@keyframes pipe-travel-2 {
  0%,
  28% {
    left: 0;
    opacity: 0;
  }
  29.5% {
    opacity: 1;
  }
  38% {
    left: 100%;
    opacity: 1;
  }
  40%,
  100% {
    left: 100%;
    opacity: 0;
  }
}

@keyframes pipe-travel-3 {
  0%,
  63% {
    left: 0;
    opacity: 0;
  }
  64.5% {
    opacity: 1;
  }
  73% {
    left: 100%;
    opacity: 1;
  }
  75%,
  100% {
    left: 100%;
    opacity: 0;
  }
}

@keyframes pipe-travel-1-down {
  0%,
  9% {
    top: 0;
    opacity: 0;
  }
  10.5% {
    opacity: 1;
  }
  19% {
    top: 100%;
    opacity: 1;
  }
  21%,
  100% {
    top: 100%;
    opacity: 0;
  }
}

@keyframes pipe-travel-2-down {
  0%,
  28% {
    top: 0;
    opacity: 0;
  }
  29.5% {
    opacity: 1;
  }
  38% {
    top: 100%;
    opacity: 1;
  }
  40%,
  100% {
    top: 100%;
    opacity: 0;
  }
}

@keyframes pipe-travel-3-down {
  0%,
  63% {
    top: 0;
    opacity: 0;
  }
  64.5% {
    opacity: 1;
  }
  73% {
    top: 100%;
    opacity: 1;
  }
  75%,
  100% {
    top: 100%;
    opacity: 0;
  }
}

.splash-definition,
.splash-problem,
.splash-purpose,
.splash-start {
  padding: clamp(3rem, 7vw, 5.5rem) 0;
  border-top: 1px solid var(--line);
}

.splash-definition > h2,
.splash-problem > h2,
.splash-purpose > h2,
.splash-start > h2 {
  max-width: 22ch;
  margin: 0 0 1rem;
  font-size: clamp(1.65rem, 3vw, 2.35rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.1;
}

.splash-definition > p:not(.splash-eyebrow),
.splash-problem > p:not(.splash-eyebrow),
.splash-purpose > p:not(.splash-eyebrow) {
  max-width: 46rem;
  margin: 0 0 1.2rem;
  color: var(--muted);
}

.splash-definition-grid {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(21rem, 1.1fr);
  gap: clamp(2rem, 6vw, 5rem);
  align-items: start;
  margin-top: 2rem;
}

.splash-definition-grid p {
  max-width: 42rem;
  margin: 0 0 1rem;
  color: var(--muted);
}

.splash-code {
  min-width: 0;
  border: 1px solid var(--line);
  border-radius: 0.375rem;
  background: var(--surface);
  overflow: hidden;
}

.splash-code-bar {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.6rem 0.75rem;
  border-bottom: 1px solid var(--line);
  background: var(--group);
}

.splash-code-bar span {
  width: 0.42rem;
  height: 0.42rem;
  border-radius: 50%;
  background: var(--muted);
  opacity: 0.45;
}

.splash-code-bar b {
  margin-left: 0.35rem;
  color: var(--muted);
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 0.62rem;
  font-weight: 400;
}

.splash-code pre {
  margin: 0;
  padding: clamp(1rem, 3vw, 1.5rem);
  overflow-x: auto;
  color: var(--ink);
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: clamp(0.66rem, 1.3vw, 0.78rem);
  line-height: 1.85;
}

.code-comment {
  color: var(--muted);
}

.code-key {
  color: var(--accent-text);
}

/* --ink rather than a status colour: the colour-vision palettes move --st-*,
   and nothing here is a status. Three levels, all tokens check_brand.py
   already holds to AA on --surface. */
.code-value {
  color: var(--ink);
  font-weight: 600;
}

/* The four families, one line each. The long blurb is on /summaries/, which is
   where each cell links. Cells are read from research/index.json, so the grid
   follows the family count rather than a hard-coded four. */
/* The dividers are the 1px gaps, with the container's own colour showing
   through, rather than a border on each cell. The family count is data — a
   fifth family would reflow the columns — and nth-child rules for "last in
   the row" cannot survive a column count CSS does not know. This can. */
.splash-owns {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
  gap: 1px;
  margin-top: clamp(2.5rem, 6vw, 4rem);
  border: 1px solid var(--line);
  border-radius: 0.375rem;
  background: var(--line);
  overflow: hidden;
}

.splash-owns > a {
  min-width: 0;
  padding: 1.25rem;
  background: var(--surface);
  color: var(--ink);
  text-decoration: none;
  transition: background 120ms ease;
}

.splash-owns > a:hover {
  background: var(--group);
}

.splash-owns strong,
.splash-owns span,
.splash-owns em {
  display: block;
}

.splash-owns strong {
  margin-bottom: 0.35rem;
  color: var(--accent-text);
  font-size: 0.92rem;
}

.splash-owns span {
  color: var(--muted);
  font-size: 0.8rem;
}

.splash-owns em {
  margin-top: 0.5rem;
  color: var(--muted);
  font-size: 0.72rem;
  font-style: normal;
  letter-spacing: 0.04em;
}

.splash-ledger {
  margin: 2rem 0 1.5rem;
  border-top: 1px solid var(--line);
}

.splash-ledger-head,
.splash-ledger-row {
  display: grid;
  grid-template-columns: minmax(10rem, 0.7fr) minmax(0, 1.15fr) minmax(0, 1.15fr);
  gap: clamp(1rem, 4vw, 3rem);
}

.splash-ledger-head {
  padding: 0.85rem 0;
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 0.64rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.splash-ledger-row {
  align-items: start;
  padding: 1.35rem 0;
  border-bottom: 1px solid var(--line);
}

.splash-ledger-who strong,
.splash-ledger-who span {
  display: block;
}

.splash-ledger-who strong {
  font-size: 0.92rem;
}

.splash-ledger-who span {
  margin-top: 0.15rem;
  color: var(--muted);
  font-size: 0.74rem;
}

.splash-ledger-row p {
  margin: 0;
  color: var(--muted);
  font-size: 0.86rem;
}

.splash-ledger-row p:last-child {
  color: var(--ink);
}

.splash-problem-note {
  margin-top: 2rem !important;
  padding-left: 1rem;
  border-left: 3px solid var(--accent);
}

.splash-limits {
  padding: 0.9rem 1rem;
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: 0.375rem;
  background: var(--surface);
  font-size: 0.9rem;
}

/* Counts come from the research files, so home cannot claim a tool, a
   requirement, or a family the data does not contain. Columns follow the tile
   count rather than a fixed number. */
.splash-stats {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  margin-top: 2rem;
  border: 1px solid var(--line);
  border-radius: 0.375rem;
  overflow: hidden;
}

.splash-stat {
  display: block;
  min-width: 0;
  padding: 1.1rem;
  border-right: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  text-decoration: none;
  transition: background 120ms ease;
}

.splash-stat:last-child {
  border-right: 0;
}

.splash-stat:hover {
  background: var(--group);
}

.splash-stat-value {
  display: block;
  color: var(--accent-text);
  font-size: 1.55rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.1;
}

.splash-stat-label {
  display: block;
  margin-top: 0.25rem;
  color: var(--muted);
  font-size: 0.74rem;
}

.splash-start .fit-cards {
  grid-template-columns: repeat(auto-fit, minmax(min(21rem, 100%), 1fr));
}

.splash-card p {
  font-size: 0.84rem;
}

/* The hero stacks well before the rest of the page does.

   Two columns give the figures 0.95 of half the shell, and four drawings in a
   row need about 410px of it. That is not there until roughly 1060px wide: at
   900px the column is 342px and the row hangs 67px out of its panel, at 861px
   it is 83px out. The drawings cannot shrink to fit either, because their vw
   term does not bite until about 760px, by which point this has long since
   stacked.

   Stacking here rather than shrinking them, because the copy wants it too: at
   900px a two-column hero sets the lede in a 408px measure, and one column
   gives it a proper one. The figures are capped so they do not stretch across
   the full width once they have it. */
@media (max-width: 1100px) {
  .splash-hero {
    grid-template-columns: 1fr;
    min-height: 0;
  }

  .hero-figures {
    max-width: 36rem;
  }
}

@media (max-width: 860px) {
  .splash-definition-grid {
    grid-template-columns: 1fr;
  }

  .splash-stats {
    grid-auto-flow: row;
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  /* An odd tile count would otherwise leave a gap in the last row. */
  .splash-stat:last-child:nth-child(odd) {
    grid-column: 1 / -1;
  }

  .splash-stat:nth-child(2) {
    border-right: 0;
  }

  .splash-stat:nth-child(-n + 2) {
    border-bottom: 1px solid var(--line);
  }

  .splash-ledger-head {
    display: none;
  }

  .splash-ledger-row {
    grid-template-columns: minmax(9rem, 0.65fr) minmax(0, 1.35fr);
    gap: 0.6rem 1.5rem;
  }

  .splash-ledger-row p:last-child {
    grid-column: 2;
  }
}

@media (max-width: 520px) {
  .splash-action {
    justify-content: center;
    width: 100%;
  }

  .splash-stats {
    grid-auto-flow: row;
    grid-template-columns: 1fr;
  }

  .splash-stat,
  .splash-stat:nth-child(2) {
    border-right: 0;
    border-bottom: 1px solid var(--line);
  }

  .splash-stat:last-child {
    border-bottom: 0;
  }

  .splash-ledger-row {
    grid-template-columns: 1fr;
    gap: 0.45rem;
    padding: 1.15rem 0;
  }

  .splash-ledger-row p:last-child {
    grid-column: 1;
    margin-top: 0.3rem;
  }

  /* Four drawings and three wires do not fit across a phone, so the row
     becomes a column and the wires run downward.

     Everything grows on the way. Stacked, the figure has the whole page width
     instead of a share of the hero's right-hand column, and at these viewports
     the vw term in each clamp bottoms out — so left alone the drawings sit at
     their smallest while the most room is available.

     The wires matter more than they look. The schedule is the same either way,
     so a wire has one second to fill however long it is: across the page that
     is a visible sweep, but down a 1.6rem stub it was 26px a second, which
     reads as nothing happening and then the next stage popping. That is what
     made the vertical version feel mistimed when the timing was identical. */
  .pipeline-row {
    grid-template-columns: 1fr;
    justify-items: center;
    gap: 0.35rem;
  }

  .pipe-name {
    font-size: 0.66rem;
  }

  .pipe-wire {
    width: 2px;
    height: clamp(2.6rem, 9vw, 3.4rem);
  }

  .pipe-wire i {
    inset: 0 0 auto 0;
    width: 100%;
    height: 0;
  }

  .pipe-wire b {
    top: 0;
    left: 50%;
    width: 0.36rem;
    height: 0.36rem;
    margin: -0.18rem 0 0 -0.18rem;
  }

  /* Same schedule as the wide layout, filling downward instead. */
  .pipe-wire-1 i {
    animation-name: pipe-fill-1-down;
  }

  .pipe-wire-1 b {
    animation-name: pipe-travel-1-down;
  }

  .pipe-wire-2 i {
    animation-name: pipe-fill-2-down;
  }

  .pipe-wire-2 b {
    animation-name: pipe-travel-2-down;
  }

  .pipe-wire-3 i {
    animation-name: pipe-fill-3-down;
  }

  .pipe-wire-3 b {
    animation-name: pipe-travel-3-down;
  }
}

/* The travelling dots are the one piece of motion on the page. Reduced-motion
   readers lose them and keep the diagram. */
@media (prefers-reduced-motion: reduce) {
  /* Marks still arrive and leave, they just do not stretch to do it. */
  .reach-track i {
    transform: none;
    transition: opacity 320ms ease;
  }

  .splash-prompt i {
    animation: none;
  }

  /* The dot is nothing but travel, so it is the one thing that goes. */
  .pipe-wire b {
    animation: none;
    opacity: 0;
  }

  /* The wire is already its full length and fades in on its own beat, rather
     than growing across the panel. */
  .pipe-wire i {
    width: 100%;
    height: 100%;
    opacity: 0;
  }

  .pipe-wire-1 i {
    animation-name: pipe-wire-1-still;
  }

  .pipe-wire-2 i {
    animation-name: pipe-wire-2-still;
  }

  .pipe-wire-3 i {
    animation-name: pipe-wire-3-still;
  }

  /* Sparks brighten in place instead of swelling. */
  .pipeline .ci-spark {
    transform: none;
  }

  .pipe-step-prompt .ci-spark {
    animation-name: pipe-spark-1-still;
  }

  .pipe-step-agent .ci-spark {
    animation-name: pipe-spark-2-still;
  }

  .pipe-step-work .ci-spark {
    animation-name: pipe-spark-3-still;
  }

  /* The graph is a colour change and nothing else, so it runs untouched — its
     transition is the cross-fade this preference asks for. */
}


/* Analysis listings for local findings and external publications. */
.post {
  margin: 1rem 0 0;
  padding: 0.9rem 1rem 1rem;
  border: 1px solid var(--line);
  border-radius: 0.375rem;
  background: var(--surface);
}

.post h2 {
  font-size: 1.05rem;
  margin: 0 0 0.2rem;
}

.post-example {
  background: var(--paper);
  border-style: dashed;
}

.post-meta {
  margin: 0 0 0.5rem;
  color: var(--muted);
  font-size: 0.8rem;
}

.post-brief {
  max-width: 46rem;
  margin: 0;
}

.post-links {
  margin: 0.7rem 0 0;
  font-size: 0.85rem;
}

.post-updated {
  margin-top: 1.5rem;
}

/* Matrix split by family. Each table is a handful of columns rather than every
   tool at once, so it fits without the vertical cap the combined grid needed —
   .wrap still handles horizontal overflow on a narrow screen. */
.matrix-family {
  margin: 1.25rem 0 0;
  padding-top: 1rem;
  border-top: 1px solid var(--line);
}

.matrix-family:first-of-type {
  border-top: 0;
  padding-top: 0;
}

.matrix-family-body {
  padding-top: 0.6rem;
}

.matrix-family-wrap {
  overscroll-behavior-x: contain;
}

/* Both family headings are spans carrying role="heading" aria-level="2", so
   they get no h2 typography from the element selector and have to be styled
   here. One rule for both — the summaries one was missed when it stopped being
   an <h2>, and rendered as 16px body text until this. */
.matrix-family-label,
.tool-family-label {
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  min-width: 0;
}

/* Fixed layout, which is what makes every tool column the width it is told to
   be. It also means a column with no width set gets none at all — min-content
   is not a floor here — so the requirement column below is sized explicitly
   rather than left to the browser. */
.matrix-family table {
  table-layout: fixed;
}

/* The requirement column takes a fixed share and the tool columns split what is
   left. Pinning the tool columns to a constant width instead pushes them to the
   right edge in a family with one tool, leaving its results a page away from the
   requirement they answer. */
/* Tool columns are capped and packed to the left rather than splitting the row
   between them. Sharing it equally meant a family's column width depended on
   how many tools happened to be in it — LSP, with only Serena in it so far, gave
   one badge two thirds of the page to sit in the middle of. A column is the same
   width in every family now, and the slack goes to a trailing spacer column so
   the table still spans its panel. */
/* Every heading cell reserves the bar's line — the whole row is one height, so
   the space has to be there whether or not that column draws a bar. */
.matrix-family thead th {
  position: relative;
  padding-bottom: 1.5rem;
}

.matrix-family thead th:first-child {
  width: 30%;
}

.matrix-family th:not(:first-child):not(.col-spacer) {
  width: 9.5rem;
}

/* No width at all, deliberately. Under fixed layout the sized columns are
   honoured first and whatever is left goes to the auto ones — and this is the
   only auto column, so it takes exactly the slack. Asking for 100% instead made
   it compete with the others and squeeze the requirement column to 92px. */
.col-spacer {
  padding: 0;
}

/* Under about 1140px the widest family — five tools — cannot fit five capped
   columns beside a 30% requirement column without the panel scrolling
   sideways, and a table you have to scroll is worse than one whose columns
   stretch. So the cap comes off and the slack column collapses, which is the
   behaviour the whole table had before. */
@media (max-width: 1140px) {
  .matrix-family th:not(:first-child):not(.col-spacer) {
    width: auto;
  }

  .col-spacer {
    width: 0;
  }
}

/* Below about 950px the requirement column stops being wide enough for a label
   that will not wrap, and the labels start running out from under themselves —
   271px of text in a 227px column at 800px, straight across the badges. It is
   not about how many tools a family has: LSP, with one, overflows by exactly as
   much as Indexing with four.

   So the grid stops being squeezed to fit and starts being scrolled. Every
   column gets a width it can be read at, labels wrap rather than overflow, and
   the panel scrolls when the sum does not fit — which for a single-tool family
   is never, even on a phone. The label column is pinned to the left while that
   happens, because a matrix you scroll without its row headings is a grid of
   unlabelled dots. */
@media (max-width: 950px) {
  .matrix-family thead th:first-child {
    width: 12rem;
  }

  .matrix-family th:not(:first-child):not(.col-spacer) {
    width: 7rem;
  }

  .matrix-family thead th:first-child,
  .matrix-family tbody tr:not(.group-row) th {
    position: sticky;
    left: 0;
    /* Above the badge cells it slides over, and above the sticky header row's
       own z-index where the two meet in the corner. */
    z-index: 3;
  }

  /* Scrolling right would otherwise leave a band of colour with the section
     name somewhere off to the left. The cell cannot be pinned — it spans the
     row, so it has no slack to move within — so the label inside it is. */
  .matrix-family .group-row .group-label {
    position: sticky;
    left: 0.7rem;
    display: inline-block;
  }
}

/* The cap only holds if a long name can break inside it. */
/* No arrow in here any more, so it is text rather than a row — it wraps on word
   boundaries like text, and only breaks a word when a single one cannot fit. */
.matrix-family thead th .candidate-name {
  display: inline;
  overflow-wrap: anywhere;
}


/* The family's own description, above its table. Same copy as /summaries/ and
   the home page, read from research/index.json so the three cannot disagree. */
.matrix-family-blurb {
  max-width: 52rem;
  margin: 0 0 0.75rem;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: 0.375rem;
  background: var(--surface);
  font-size: 0.9rem;
}

/* Headings in the accent green so a long page has visible structure. Uses
   --accent-text rather than --accent: these are text, and Bright Green is
   1.6:1 on a light page. The token resolves to a darkened green there. */
h2 {
  color: var(--accent-text);
}

/* An open disclosure turns its marker and its title green, so open and closed
   differ by more than the angle of a small triangle. */
.tool-family[open] > summary > .tool-family-label,
.matrix-family[open] > summary > .matrix-family-label,
.tool-family[open] > summary::before,
.matrix-family[open] > summary::before {
  color: var(--accent-text);
}

/* The profile block on a tool's own page. /summaries/ shows it inside a card;
   here it sits in the page head, so it needs the separation the card gave it. */
.page-head .tool-facts {
  margin: 0.85rem 0 0.75rem;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: 0.375rem;
  background: var(--surface);
  max-width: 52rem;
}


/* The headline already draws itself as a panel for the tool page. Inside a card
   that is a box in a box, so flatten it and let the card be the container. */
.tool-card .headline {
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
}

.tool-card .tool-facts {
  margin-top: 0.75rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--line);
}

/* One scale for everything on a card — the fit summary is the whole of it now
   that the description reads on the tool page instead. */
.tool-card .headline-item p {
  font-size: 0.88rem;
}

/* Three to a row, each locked to a third, rather than a track size the browser
   grows to fill: a card is the unit of comparison here, so every card being the
   same width matters more than any one of them using the space well. A family
   with one tool gets a third and empty space beside it, which is the point. */
.tool-cards {
  /* On the container, not on each card: a per-card top margin would land
     between the grid rows as well and make them 1.75rem apart. */
  margin-top: 0.85rem;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.75rem;
  align-items: start;
}

/* Each of the four answers gets the same height in every card of a row, so ICP
   reads across against ICP and the rules between them run straight across the
   row instead of stepping. The family grid owns five rows per band — the title
   and the four answers — and the card subgrids them, then the fit summary
   subgrids the card. Two levels, because the answers are a level below the
   card. Rows are placed by name rather than by order, so a tool missing an
   answer leaves that band empty instead of shunting the rest out of line.

   Guarded: without subgrid the cards keep the flow layout above, which is
   ragged but correct. */
@supports (grid-template-rows: subgrid) {
  .tool-cards {
    align-items: stretch;
  }

  .tool-card {
    grid-row: span 5;
    display: grid;
    grid-template-rows: subgrid;
    row-gap: 0;
  }

  .tool-card > h3 {
    grid-row: 1;
  }

  .tool-card .headline {
    grid-row: 2 / span 4;
    display: grid;
    grid-template-rows: subgrid;
    row-gap: 0;
  }

  .tool-card .headline-icp {
    grid-row: 1;
  }

  .tool-card .headline-bestFor {
    grid-row: 2;
  }

  .tool-card .headline-poorFit {
    grid-row: 3;
  }

  .tool-card .headline-tldr {
    grid-row: 4;
  }

  /* An unscored card has no four answers to line up, so it keeps the band's
     height — the row still has to be square — but drops the subgrid and packs
     its note and link at the top. Left subgridded, the link landed in the row
     a neighbour's BEST FOR had stretched, stranded halfway down an empty card. */
  .tool-card-pending {
    grid-template-rows: none;
    grid-auto-rows: min-content;
    align-content: start;
    row-gap: 0.4rem;
  }
}

/* Below these a third is narrower than the fit summary can be read in, and a
   column that cannot be read is worse than an uneven row. */
@media (max-width: 62rem) {
  .tool-cards {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 40rem) {
  .tool-cards {
    grid-template-columns: 1fr;
  }
}

/* Tool cards on /summaries/. Nothing collapses — the family section above is
   the control for page length, and a second layer would hide the comparison
   this page exists to make.

   One definition of the title row, not two. An earlier block set
   `flex-wrap: wrap` for a title that was name-plus-licence-chip; left alongside
   this one it still applied, and a long name could push the fact icons onto a
   line of their own. The row is the name at one end and the icons at the other,
   and it does not wrap. */
.tool-card > h3 {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin: 0 0 0.5rem;
  font-size: 1rem;
}

.tool-card > h3 > a {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  color: var(--accent-text);
}

/* Reads as "there is more of this through here". Sized off the title so it
   tracks it, and nudged down to sit on the same optical line as the caps.
   Shared: the summary card titles and the matrix column headings both use it. */
.more-icon {
  width: 0.8em;
  height: 0.8em;
  flex-shrink: 0;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  transform: translateY(0.04em);
}

/* Pushed to the right of the title. Links go somewhere, buttons open a panel. */
.fact-icons {
  display: inline-flex;
  align-items: center;
  gap: 0.15rem;
  margin-left: auto;
  flex-shrink: 0;
}

.fact-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.65rem;
  height: 1.65rem;
  padding: 0;
  margin: 0;
  border: 1px solid transparent;
  border-radius: 0.3rem;
  background: none;
  color: var(--muted);
  text-decoration: none;
  cursor: pointer;
}

.fact-icon svg {
  width: 1rem;
  height: 1rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.3;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.fact-icon:hover {
  color: var(--accent-text);
  border-color: var(--line);
}

.fact-icon:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 1px;
  color: var(--accent-text);
}

.fact-icon[aria-expanded="true"] {
  color: var(--accent-text);
  border-color: var(--accent);
}

/* The panel behind the licence and dependency icons. pre-line so a dependency
   list keeps one item per line without needing markup inside a tooltip. */
#fact-float {
  position: fixed;
  z-index: 60;
  max-width: 22rem;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 0.5rem;
  background: var(--tip-bg);
  color: var(--tip-fg);
  font-size: 0.82rem;
  line-height: 1.4;
  white-space: pre-line;
  box-shadow: 0 6px 20px var(--tip-shadow);
}

#fact-float[hidden] {
  display: none;
}

/* Analysis keeps prose and scrollable tables within the same reading width. */
.analysis-list {
  max-width: 60rem;
  margin: 0 auto;
}

.post-featured {
  border-left: 4px solid var(--accent);
  padding: 1.5rem;
}

.analysis-eyebrow {
  margin: 0 0 0.5rem;
  color: var(--muted);
  font-size: 0.8rem;
  font-weight: 700;
}

.analysis-article {
  max-width: 76rem;
  margin: 0 auto;
}

.analysis-article .page-head h1 {
  max-width: 30ch;
  font-size: clamp(1.7rem, 4vw, 2.5rem);
  line-height: 1.2;
}

.analysis-related {
  font-size: 0.9rem;
}

.analysis-article .prose > * {
  max-width: 48rem;
  margin-left: auto;
  margin-right: auto;
}

.analysis-article .prose p,
.analysis-article .prose ul,
.analysis-article .prose ol {
  font-size: 1rem;
  line-height: 1.7;
}

.analysis-article .prose h2 {
  margin-top: 2rem;
  font-size: 1.25rem;
}

.analysis-table {
  overflow-x: auto;
  margin-top: 1.5rem;
  margin-bottom: 1.5rem;
}

.analysis-table:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.analysis-table table {
  min-width: 42rem;
}

.analysis-article .crumbs {
  justify-content: flex-start;
  gap: 0.4rem;
}

/* ======================================================================
   Home, the explainer. The term "code intelligence" is not one most visitors
   arrive knowing, so the page now carries the definition, the scale argument
   an enterprise needs, and a drawing for each. Everything below extends the
   splash rules above rather than replacing them; the hero, the ledger, the
   stats and the destination cards are unchanged.
   ====================================================================== */

/* With a single figure on the right the hero no longer needs to be a screen
   tall, and the pipeline gets the room the two stacked figures used to share. */
.splash-hero {
  min-height: min(38rem, calc(100vh - var(--chrome-h, 5.5rem)));
}

.pipeline-hero {
  padding: clamp(1.25rem, 3vw, 2rem);
}

.pipeline-hero .pipeline-head {
  margin-bottom: 1.6rem;
}

.pipeline-hero .pipe-step-prompt .pipe-art,
.pipeline-hero .pipe-step-work .pipe-art {
  width: clamp(2.8rem, 6.5vw, 4rem);
}

.pipeline-hero .pipe-step-agent .pipe-art {
  width: clamp(2.6rem, 6vw, 3.7rem);
}

.pipeline-hero .pipe-step-ci .pipe-art {
  width: clamp(6rem, 14vw, 9rem);
}

.pipeline-hero .pipe-name {
  font-size: 0.66rem;
}

.pipeline-caption {
  max-width: 34rem;
  margin: 1.5rem 0 0;
  padding-top: 1rem;
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.8rem;
  line-height: 1.5;
}

/* A sub-heading inside a splash section, between the definition and the
   family cells. Smaller than the section's h2, and in ink like it. */
.splash-subhead {
  max-width: 22ch;
  margin: clamp(2.5rem, 6vw, 4rem) 0 0.5rem;
  color: var(--ink);
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.splash-subhead-note {
  max-width: 46rem;
  margin: 0;
  color: var(--muted);
  font-size: 0.92rem;
}

.splash-definition .splash-owns {
  margin-top: 1.5rem;
}

/* The family cell's drawing. Sized like an icon, coloured like the muted
   strokes everywhere else, with one accent part per drawing. */
.splash-owns-art {
  display: block;
  width: 2rem;
  height: 2rem;
  margin-bottom: 0.9rem;
  color: var(--muted);
}

.splash-owns-art svg {
  width: 100%;
  height: 100%;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.splash-owns-art .fam-accent {
  stroke: var(--accent-text);
}

.splash-owns > a:hover .splash-owns-art {
  color: var(--ink);
}

/* The why and the costs sections share the splash sections' rhythm. */
.splash-why,
.splash-costs {
  padding: clamp(3rem, 7vw, 5.5rem) 0;
  border-top: 1px solid var(--line);
}

.splash-why > h2,
.splash-costs > h2 {
  max-width: 24ch;
  margin: 0 0 1rem;
  color: var(--ink);
  font-size: clamp(1.65rem, 3vw, 2.35rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.1;
}

.splash-why > p:not(.splash-eyebrow),
.splash-costs > p:not(.splash-eyebrow) {
  max-width: 46rem;
  margin: 0 0 1.2rem;
  color: var(--muted);
}

/* ---- the field ----
   A panel of dots, one per slice of the repository. The dots are a repeating
   gradient on --field-pitch, and home.js reads that pitch back to place the
   window and the marks on whole cells, the same way the reach tracks work. */
.field {
  margin-top: 2rem;
  padding: clamp(1.25rem, 3vw, 2rem);
  border: 1px solid var(--line);
  border-radius: 0.5rem;
  background: var(--surface);
}

.field-head {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.1rem;
  color: var(--muted);
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 0.62rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.field-head span:last-child {
  flex: none;
  color: var(--accent-text);
}

.field-grid {
  --field-pitch: 12px;
  position: relative;
  height: clamp(11rem, 26vw, 17rem);
  border-radius: 0.25rem;
  background-image: radial-gradient(
    circle,
    color-mix(in srgb, var(--muted) 42%, transparent) 1.4px,
    transparent 1.9px
  );
  background-size: var(--field-pitch) var(--field-pitch);
  overflow: hidden;
}

/* The context window, sized by home.js to one percent of the cells on
   screen. The label hangs to its right so the box itself stays the measure. */
.field-window {
  position: absolute;
  top: var(--field-pitch);
  left: var(--field-pitch);
  width: var(--w, calc(var(--field-pitch) * 6));
  height: var(--h, calc(var(--field-pitch) * 3));
  border: 1.5px solid var(--accent-text);
  border-radius: 0.2rem;
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  font-weight: 400;
}

.field-window span {
  position: absolute;
  top: 50%;
  left: calc(100% + 0.6rem);
  transform: translateY(-50%);
  color: var(--accent-text);
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 0.62rem;
  letter-spacing: 0.04em;
  white-space: nowrap;
}

/* A mark is one location that came back: a cell, lit. Position comes in on
   --x and --y from home.js, and the lit state is a class with a transition
   so an arbitrary handful fades in and out cleanly. */
.field-grid i {
  position: absolute;
  top: calc(var(--y, 0px) + 1.5px);
  left: calc(var(--x, 0px) + 1.5px);
  width: calc(var(--field-pitch) - 3px);
  height: calc(var(--field-pitch) - 3px);
  border-radius: 0.15rem;
  background: var(--accent-text);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
  opacity: 0;
  transform: scale(0.4);
  transition: opacity 320ms ease, transform 320ms ease;
}

.field-grid i.is-lit {
  opacity: 1;
  transform: scale(1);
}

.field-key {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
  gap: 0.5rem 2rem;
  margin: 1.1rem 0 0;
  color: var(--muted);
  font-size: 0.74rem;
  line-height: 1.45;
}

.field-key i {
  display: inline-block;
  width: 0.7rem;
  height: 0.7rem;
  margin-right: 0.35rem;
  border-radius: 0.15rem;
  vertical-align: -0.05em;
}

.field-key-window {
  border: 1.5px solid var(--accent-text);
  background: color-mix(in srgb, var(--accent) 18%, transparent);
}

.field-key-mark {
  background: var(--accent-text);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 22%, transparent);
}

/* ---- the three estates ----
   One card each for a legacy monolith, a poly-repo estate, and a dependency
   tree. The drawing sits on top at a fixed aspect so the three line up
   whatever the copy length. */
.splash-estates {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1rem, 2.5vw, 2rem);
  margin-top: clamp(2rem, 5vw, 3.5rem);
}

.estate {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.estate h3 {
  margin: 1.4rem 0 0.6rem;
  color: var(--ink);
  font-size: 1.1rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.25;
}

.estate p {
  margin: 0 0 0.9rem;
  color: var(--muted);
  font-size: 0.9rem;
  line-height: 1.55;
}

.estate-ask {
  margin-top: auto !important;
  padding: 0.75rem 0.9rem;
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: 0.375rem;
  background: var(--surface);
  color: var(--muted);
  font-size: 0.84rem !important;
}

.estate-ask strong {
  color: var(--ink);
  font-weight: 600;
}

.estate-art {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 16 / 10;
  padding: clamp(1rem, 2.5vw, 1.5rem);
  border: 1px solid var(--line);
  border-radius: 0.5rem;
  background: var(--surface);
  overflow: hidden;
}

/* The reach panel reused as an estate drawing: same component, told by era.
   Flex column so the rows fill the fixed-aspect card without stretching. */
.reach-estate {
  flex-direction: column;
  align-items: stretch;
  justify-content: center;
  gap: 0;
}

.reach-estate .reach-row {
  grid-template-columns: minmax(0, 6.5rem) minmax(0, 1fr);
}

.reach-estate .reach-row:last-child {
  margin-bottom: 0;
}

.estate-svg {
  display: block;
  width: 100%;
  color: var(--muted);
}

.estate-svg svg {
  display: block;
  width: 100%;
  height: auto;
}

/* The poly-repo estate. Callers light one after another along their lines,
   carried by --i from the drawing, then the whole set fades and starts over. */
.estate-svg .est-repo {
  opacity: 0.55;
}

.estate-svg .est-src {
  opacity: 1;
}

.estate-svg .est-hit,
.estate-svg .est-link {
  opacity: 0;
  animation: est-find 9s ease-in-out infinite;
  animation-delay: calc(0.6s + var(--i, 0) * 0.45s);
}

@keyframes est-find {
  0%,
  3% {
    opacity: 0;
  }
  8%,
  80% {
    opacity: 1;
  }
  90%,
  100% {
    opacity: 0;
  }
}

/* The dependency tree. The declared ring lights first, then what it pulls
   in; dashed circles are the ones with no source to read, and they stay
   dim so the eye reads them as gaps. */
.estate-svg .dep-link,
.estate-svg .dep-node {
  opacity: 0.5;
  transition: none;
}

.estate-svg .dep-root {
  opacity: 1;
  stroke: var(--accent-text);
}

.estate-svg .dep-link[data-hop="1"],
.estate-svg .dep-node[data-hop="1"] {
  animation: dep-reach 9s ease-in-out infinite;
  animation-delay: 1s;
}

.estate-svg .dep-link[data-hop="2"],
.estate-svg .dep-node[data-hop="2"] {
  animation: dep-reach 9s ease-in-out infinite;
  animation-delay: 2.2s;
}

.estate-svg .dep-node.dep-closed {
  animation: none;
  opacity: 0.4;
}

@keyframes dep-reach {
  0%,
  4% {
    stroke: var(--muted);
    opacity: 0.5;
  }
  12%,
  78% {
    stroke: var(--accent-text);
    opacity: 1;
  }
  90%,
  100% {
    stroke: var(--muted);
    opacity: 0.5;
  }
}

/* ---- the four bills ---- */
.splash-cost-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: 1px;
  margin-top: 2rem;
  border: 1px solid var(--line);
  border-radius: 0.375rem;
  background: var(--line);
  overflow: hidden;
}

.splash-cost {
  min-width: 0;
  padding: 1.5rem 1.35rem;
  background: var(--surface);
}

.splash-cost svg {
  display: block;
  width: 2.2rem;
  height: 2.2rem;
  margin-bottom: 1rem;
  color: var(--muted);
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.splash-cost .cost-accent {
  stroke: var(--accent-text);
}

.splash-cost h3 {
  margin: 0 0 0.5rem;
  color: var(--ink);
  font-size: 0.98rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.3;
}

.splash-cost p {
  margin: 0;
  color: var(--muted);
  font-size: 0.84rem;
  line-height: 1.5;
}

@media (max-width: 1100px) {
  /* The hero stacks here rather than at 860px. Between the two, the right
     column is about 350px inside the panel and the four drawings plus their
     wires need over 400px, so the artwork overlapped before the column gave
     way. Stacked, the figure gets the page width and a cap. */
  .splash-hero {
    grid-template-columns: 1fr;
    min-height: 0;
  }

  .hero-figures {
    max-width: 40rem;
  }

  .splash-estates {
    grid-template-columns: 1fr;
    max-width: 40rem;
  }

  .estate-art {
    aspect-ratio: 16 / 9;
  }
}

@media (max-width: 860px) {
  .field-head {
    flex-direction: column;
    gap: 0.25rem;
  }

  .field-window span {
    display: none;
  }
}

/* The new figures follow the same rule as the reach marks: they fade rather
   than move for a reduced-motion reader, and the sequence stays. */
@media (prefers-reduced-motion: reduce) {
  .field-grid i {
    transform: none;
    transition: opacity 320ms ease;
  }
}

/* ======================================================================
   Recommendations. One pick per family, why, and the guides for standing it
   up. The pick's facts are read from its own data.json; this is only layout.
   ====================================================================== */
.rec-intro > p,
.rec-stack-setup > p {
  max-width: 46rem;
  color: var(--muted);
}

.rec-intro > p:first-child {
  color: var(--ink);
  font-size: 1.05rem;
}

/* The stack at a glance: the agent, a fan of wires, and a tile per family. */
.rec-stack {
  display: grid;
  grid-template-columns: auto 3.5rem minmax(0, 1fr);
  align-items: center;
  gap: 0;
  margin: 2rem 0 0;
  padding: clamp(1.25rem, 3vw, 2rem);
  border: 1px solid var(--line);
  border-radius: 0.5rem;
  background: var(--surface);
}

.rec-stack-agent {
  display: grid;
  justify-items: center;
  gap: 0.5rem;
  color: var(--muted);
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 0.62rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: center;
}

.rec-stack-agent-art {
  display: block;
  width: 3.4rem;
  height: 3.4rem;
}

.rec-stack-agent-art svg {
  width: 100%;
  height: 100%;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.rec-stack-agent-art .rec-spark {
  fill: var(--accent);
  stroke: none;
}

/* Four wires from one point. Drawn as a single element: the trunk is the
   border, and the fan is a conic gradient trimmed to a thin rail. */
.rec-stack-wires {
  position: relative;
  align-self: stretch;
  min-height: 7rem;
}

.rec-stack-wires::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  width: 50%;
  height: 2px;
  background: var(--accent-text);
}

.rec-stack-wires::after {
  content: "";
  position: absolute;
  top: 12%;
  bottom: 12%;
  left: 50%;
  width: 50%;
  border: 2px solid var(--accent-text);
  border-right: 0;
  border-radius: 0.5rem 0 0 0.5rem;
  opacity: 0.7;
}

.rec-stack-tiles {
  display: grid;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.rec-stack-tiles a {
  display: grid;
  grid-template-columns: minmax(8rem, 0.6fr) minmax(0, 0.5fr) minmax(0, 1.4fr);
  gap: 0 1rem;
  align-items: baseline;
  padding: 0.65rem 0.9rem;
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: 0.375rem;
  background: var(--paper);
  color: var(--ink);
  text-decoration: none;
  transition: background 120ms ease;
}

.rec-stack-tiles a:hover {
  background: var(--group);
}

.rec-stack-family {
  color: var(--muted);
  font-size: 0.74rem;
}

.rec-stack-tiles strong {
  color: var(--accent-text);
  font-size: 0.98rem;
}

.rec-stack-tag {
  color: var(--muted);
  font-size: 0.78rem;
}

.rec-stack-setup {
  margin-top: 2.5rem;
}

/* Guide links. A planned guide is dashed so it reads as a place being held. */
.rec-guides {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
  gap: 0.75rem;
}

.rec-guide {
  display: grid;
  align-content: start;
  gap: 0.3rem;
  padding: 0.9rem 1rem;
  border: 1px solid var(--line);
  border-radius: 0.375rem;
  background: var(--surface);
  color: var(--ink);
  text-decoration: none;
  transition: border-color 120ms ease, background 120ms ease;
}

a.rec-guide:hover {
  border-color: var(--muted);
  background: var(--group);
}

.rec-guide-planned {
  border-style: dashed;
}

.rec-guide-large {
  padding: 1.25rem 1.35rem;
}

.rec-guide-kind {
  font-weight: 600;
  font-size: 0.95rem;
}

.rec-guide-large .rec-guide-kind {
  font-size: 1.05rem;
}

.rec-guide-hint {
  color: var(--muted);
  font-size: 0.84rem;
  line-height: 1.45;
}

.rec-guide-state {
  margin-top: 0.4rem;
  color: var(--accent-text);
  font-size: 0.74rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.rec-guide-planned .rec-guide-state,
.rec-guide-missing .rec-guide-state {
  color: var(--muted);
}

/* One section per family. */
.rec-family {
  margin-top: clamp(2.5rem, 6vw, 4rem);
  padding-top: clamp(2rem, 4vw, 3rem);
  border-top: 1px solid var(--line);
}

.rec-eyebrow {
  margin: 0 0 0.3rem;
  color: var(--accent-text);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.rec-family-tag {
  max-width: 46rem;
  margin: 0 0 1.5rem;
  color: var(--muted);
}

.rec-none {
  color: var(--muted);
}

.rec-pick {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(20rem, 1fr);
  gap: clamp(1.5rem, 4vw, 3rem);
  align-items: start;
}

.rec-pick-label {
  margin: 0 0 0.25rem;
  color: var(--muted);
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 0.62rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* Readiness caveat on a pick. Warn tokens: it qualifies the pick, it does not
   withdraw it, so it must not read as a "no". */
.rec-readiness {
  display: inline-block;
  margin-left: 0.6rem;
  padding: 0.1rem 0.5rem;
  border: 1px solid var(--st-warn);
  border-radius: 999px;
  background: var(--st-warn-bg);
  color: var(--st-warn);
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 0.62rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  vertical-align: middle;
}

/* In a stack tile the name and badge share one grid cell, so the tagline keeps
   its column and the tiles stay aligned. */
.rec-stack-pick {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.35rem 0.5rem;
  min-width: 0;
}

.rec-readiness-small {
  margin-left: 0;
}

/* The readiness note takes the full width under both columns: it is a long
   paragraph of findings, and in the side column it ran to a narrow tower under
   "Why this one". The heading is set in the warn pair, foreground on its own
   background like the badge beside the pick's name, so it reads as the caveat
   it is. The pair matters: --st-warn alone over --paper fails 4.5:1 in the
   dark blue-yellow palette, and the brand checker only verifies warn text
   against --st-warn-bg, so the heading must not sit on the page background. */
.rec-pick-readiness {
  grid-column: 1 / -1;
}

/* Both classes, so this outranks the later .rec-why-title margin reset. */
.rec-why-title.rec-readiness-title {
  display: inline-block;
  margin: 0 0 0.6rem;
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--st-warn);
  border-radius: 999px;
  background: var(--st-warn-bg);
  color: var(--st-warn);
}

.rec-pick-name {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  margin: 0 0 0.35rem;
  color: var(--ink);
  font-size: clamp(1.6rem, 3vw, 2.2rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.1;
}

.rec-pick-name a {
  color: inherit;
  text-decoration-color: var(--line);
}

.rec-pick-name a:hover {
  text-decoration-color: var(--accent);
}

.rec-pick-icons {
  display: inline-flex;
  gap: 0.3rem;
}

.rec-licence {
  display: block;
  margin-bottom: 0.9rem;
  color: var(--muted);
  font-size: 0.82rem;
}

.rec-pick-description {
  margin: 0 0 1.4rem;
  color: var(--muted);
  line-height: 1.55;
}

.rec-why-title,
.rec-guides-title {
  margin: 0 0 0.5rem;
  color: var(--ink);
  font-size: 1rem;
  font-weight: 600;
}

.rec-guides-title {
  margin-top: 2rem;
}

.rec-why {
  margin: 0;
  padding-left: 1rem;
  border-left: 3px solid var(--accent);
  color: var(--ink);
  line-height: 1.55;
}

.rec-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  margin: 0;
  font-size: 0.86rem;
}

.rec-pick-side {
  padding-top: 1.6rem;
}

.rec-updated {
  margin-top: 3rem;
}

@media (max-width: 1000px) {
  .rec-pick {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 780px) {
  .rec-stack {
    grid-template-columns: 1fr;
    gap: 1rem;
  }

  .rec-stack-wires {
    display: none;
  }

  .rec-stack-tiles a {
    grid-template-columns: 1fr;
    gap: 0.15rem;
  }
}

/* Training: lesson list beside the rendered page. */
.training {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr);
  gap: 2rem;
  align-items: start;
}

.training-nav {
  position: sticky;
  /* The masthead is sticky too, and it wraps taller on a narrow screen, so the
     lesson list pins under its measured height rather than at the top of the
     window. No fallback on purpose, as with .legend: until theme.js publishes
     --masthead-h the calc() is invalid, `top` stays auto, and the list simply
     does not pin instead of sliding behind the header. */
  top: calc(var(--masthead-h) + 1rem);
  font-size: 0.85rem;
}

.training-nav h2 {
  margin: 0;
  font-size: 0.95rem;
}

.training-course,
.training-source {
  margin: 0.25rem 0 0;
  color: var(--muted);
  font-size: 0.8rem;
}

.training-nav ol {
  list-style: none;
  margin: 0.75rem 0 0;
  padding: 0;
  border-left: 1px solid var(--line);
}

.training-nav li a {
  display: block;
  padding: 0.3rem 0 0.3rem 0.75rem;
  margin-left: -1px;
  border-left: 2px solid transparent;
  color: inherit;
  text-decoration: none;
}

.training-nav li a[aria-current="page"] {
  border-left-color: var(--accent);
  font-weight: 600;
}

.training-body article.prose {
  margin-top: 0;
}

.training-body .prose table {
  display: block;
  overflow-x: auto;
}

@media (max-width: 780px) {
  .training {
    grid-template-columns: 1fr;
    gap: 1rem;
  }

  .training-nav {
    position: static;
  }
}
