:root {
  --paper:#f5f5f5; --card:#fff; --rule:#e3e1dc; --rule2:#d3d0c9;
  --ink:#141413; --ink2:#5c5a55; --ink3:#8f8c85;
  --local:#2f6f4e; --cloud:#1f2937; --best:#6d4aff; --warn:#b45309;
  /* Outcome colour for the length chart. Deliberately NOT --local: that green means
     "runs on your GPU" everywhere else, and reusing it for "flat at length" made one
     colour carry two unrelated meanings on the same page. Slate carries neither. */
  --flat:#475569;
  /* Link blue. Safe to add to a palette that already carries five data colours because
     this one is CHROME, never data: nothing on a chart is ever drawn in it, so it
     cannot collide with local/cloud/best/warn/flat the way a sixth data hue would. */
  --link:#0a66c2;
  /* Hover highlight. Chrome like --link, never data: it exists only under the cursor and
     appears in no legend, so it cannot be mistaken for a category. Chosen to sit clear of
     all five data colours - green, navy, violet, orange and slate - so a traced line is
     unmistakably the one you are pointing at. */
  --hover:#d6336c;

  --serif: ui-serif, Georgia, Cambria, "Times New Roman", serif;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono: ui-monospace, "Cascadia Mono", "Segoe UI Mono", Consolas, monospace;

  /* Full width, prose included. The page frame no longer caps at 1400px. */
  --shell: 100%;
  /* Hard cap on the CONTENT column - prose, tables and charts alike. The shell still
     tracks the viewport; this is what keeps a table and the chart beneath it the same
     width on an ultra-wide screen instead of a 2,204 px table above a 1,700 px chart.
     It matches the chart builder's own upper clamp, deliberately: past this a line
     chart gets harder to read, not easier. */
  --column-max: 1200px;
  --rail: 210px;
  --rail-gap: 40px;
  /* Robert's call, made twice: prose runs the full width like everything else. The
     typographic argument for a ~75-character measure is real, but a page where the
     sentences stop at 1,000 px while the cards beneath them span 1,900 px reads as a
     bug rather than as restraint - and that inconsistency is the worse of the two.
     Set this back to 60ch to restore the reading measure. */
  --measure: none;
  --gutter: 24px;
}

*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
body {
  margin:0; min-height:100%; background:var(--paper); color:var(--ink);
  font-family:var(--sans); font-size:16px; line-height:1.5;
  -webkit-font-smoothing:antialiased;
}
h1,h2,h3,p,ul,ol,dl,dd,figure { margin:0; }
ul { padding:0; list-style:none; }
a { color:inherit; text-decoration:none; }
svg { display:block; }

::-webkit-scrollbar { width:10px; height:10px; }
::-webkit-scrollbar-thumb { background:var(--rule2); border-radius:5px; }
::-webkit-scrollbar-track { background:transparent; }

.num { font-variant-numeric: tabular-nums; }
.mono { font-family:var(--mono); }
.sr-only {
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0;
}

/* ── STEP 8, revised: ONE container width. The original split - prose at a reading
   measure, data unbounded - is the textbook call, but it left sentences ending at
   1,000 px above cards spanning 1,900 px, which reads as breakage. --measure is kept
   as the single switch to put the measure back. ── */
.shell { max-width:var(--shell); margin-inline:auto; padding-inline:var(--gutter); }
.prose { max-width:var(--measure); }
.prose-wide { max-width:none; }

/* ── top bar ── */
.topbar { position:sticky; top:0; z-index:30; background:var(--paper); border-bottom:1px solid var(--rule); }
.topbar-in { display:flex; align-items:center; gap:24px; height:56px; }
.brand { font-family:var(--mono); font-size:15px; font-weight:600; letter-spacing:-.01em; }
.topbar-meta { margin-left:auto; display:flex; align-items:center; min-width:0; }
.tagline { font-family:var(--mono); font-size:11.5px; color:var(--ink3); }
/* The bar needs 634px to show both, and only 487px even without the date - so it never
   fitted a phone, before or after the date was added. Below this the tagline drops and
   the date stays: a publication date is the more specific fact, and "independent ·
   non-sponsored · self-funded" is repeated in the byline under the h1 anyway. */
@media (max-width:700px) {
  .tagline { display:none; }
  .pubdate { margin-right:0; padding-right:0; border-right:0; }
}

/* ── page grid: sticky section nav + article ── */
/* Below the rail breakpoint: one column, capped, centred. */
.layout { display:grid; grid-template-columns:minmax(0,var(--column-max)); gap:var(--rail-gap);
          justify-content:center; }
.rail { display:none; }
.toc { position:sticky; top:96px; border-left:1px solid var(--rule2); padding:4px 0; font-size:13px; line-height:1.35; }
.toc a {
  position:relative; display:block; padding:8px 0 8px 16px; color:var(--ink3); transition:color .15s;
}
.toc a::before { content:""; position:absolute; left:-1px; top:0; height:100%; width:2px; background:transparent; }
.toc a:hover { color:var(--ink); }
.toc a.on { color:var(--ink); font-weight:600; }
.toc a.on::before { background:var(--ink); }

main { min-width:0; padding:40px 0 96px; }

/* ── hero ── */
.crumbs { font-family:var(--mono); font-size:13px; text-transform:uppercase; letter-spacing:.14em; color:var(--ink3); }
.crumbs .sep { margin:0 6px; color:var(--rule2); }
.crumbs .here { color:var(--ink2); }
h1 {
  /* Breathing room after the About block, which now ends in a full paragraph rather than
     a rule. At 20px the "...GPU or an API key." line sat right on top of the title. */
  margin-top:72px; max-width:24ch; font-family:var(--serif); font-weight:700;
  font-size:clamp(2.5rem, 5vw, 4.5rem); line-height:1.02; letter-spacing:-.02em;
}
.standfirst { margin-top:24px; max-width:none; font-family:var(--serif); font-size:21px; line-height:1.6; color:var(--ink2); }
.standfirst strong { font-weight:inherit; color:var(--ink); }
.byline { margin-top:24px; display:flex; flex-wrap:wrap; align-items:center; gap:4px 20px; font-family:var(--mono); font-size:14px; color:var(--ink3); }
.byline .lede { font-weight:500; color:var(--ink2); }
/* Full width like the rest now. Worth knowing if it ever reads badly: this is the
   smallest type on the page, so it hits an uncomfortable line length sooner than the
   serif prose does. */

/* ── key takeaways ── */
.takeaways { margin-top:18px; display:grid; gap:14px; counter-reset:tk; max-width:none;
             list-style:none; padding:0; }   /* the numerals are the ::before counters */
.takeaways li {
  position:relative; padding-left:34px; font-family:var(--serif); font-size:18px;
  line-height:1.55; color:var(--ink2);
}
.takeaways li::before {
  counter-increment:tk; content:counter(tk);
  position:absolute; left:0; top:2px; width:22px; height:22px; border-radius:50%;
  background:var(--ink); color:var(--paper);
  font-family:var(--mono); font-size:11px; font-weight:600;
  display:flex; align-items:center; justify-content:center;
}
.takeaways b { color:var(--ink); font-weight:600; }
.takeaways a { border-bottom:1px solid var(--rule2); }
.takeaways a:hover { border-bottom-color:var(--ink); }

/* ── sections ── */
section { scroll-margin-top:96px; padding-top:56px; }
.sec-head { display:flex; flex-wrap:wrap; align-items:flex-end; justify-content:space-between; gap:12px; border-bottom:1px solid var(--rule); padding-bottom:12px; }
h2 { font-family:var(--serif); font-size:30px; font-weight:600; letter-spacing:-.01em; }
h2.with-mark { display:flex; align-items:center; gap:10px; }
h2.with-mark::before { content:""; width:14px; height:14px; background:var(--ink); flex:none; }
.sec-note { margin-top:4px; font-family:var(--mono); font-size:13px; color:var(--ink3); }
.sec-note em { font-style:normal; color:var(--ink2); }
.sec-intro { margin-top:8px; font-family:var(--serif); font-size:18px; line-height:1.6; color:var(--ink2); }
.sub-h { margin-top:44px; font-family:var(--serif); font-size:22px; font-weight:600; letter-spacing:-.01em; }
.chart-svg .bumpline { fill:none; stroke-width:1.75; opacity:.5; }
.chart-svg .bumpdot { stroke:var(--card); stroke-width:1.25; }
.chart-svg .bumplab { font-size:11.5px; }
.chart-svg .rankno { fill:var(--ink3); font-size:11px; }
/* Tracing one engine through 18 crossing lines is the whole job of this chart, so the
   hovered line doubles its weight (1.75 -> 3.5) AND changes hue. Weight alone is not
   enough where six lines converge; these rules beat the inline stroke/fill because CSS
   outranks an SVG presentation attribute. The dots and the end label follow the line, so
   the eye can land anywhere along it. */
.chart-svg .bumpgroup:hover .bumpline,
.chart-svg .bumpgroup:focus-visible .bumpline {
  opacity:1; stroke-width:3.5; stroke:var(--hover);
}
.chart-svg .bumpgroup:hover .bumpdot,
.chart-svg .bumpgroup:focus-visible .bumpdot { fill:var(--hover); r:4.5; }
.chart-svg .bumpgroup:hover .bumplab,
.chart-svg .bumpgroup:focus-visible .bumplab { fill:var(--hover); font-weight:700; }
.chart-svg .bumpgroup:focus-visible { outline:none; }

/* ── pill toggles (class filter, lane tabs) ── */
.pillset { display:inline-flex; border:1px solid var(--rule2); border-radius:999px; background:var(--card); padding:2px; font-size:12px; }
.pill { border:0; border-radius:999px; padding:4px 12px; font:inherit; cursor:pointer; background:transparent; color:var(--ink2); transition:color .15s,background .15s; }
.pill:hover { color:var(--ink); }
.pill[aria-pressed="true"], .pill.on { background:var(--ink); color:var(--paper); font-weight:500; }

/* ── highlight cards (STEP 9: 3 up, 1 down) ── */
.cards { margin-top:20px; display:grid; grid-template-columns:1fr; gap:16px; }
.card { border:1px solid var(--rule); background:var(--card); padding:16px; }
.card h3 { display:flex; align-items:center; gap:8px; font-family:var(--serif); font-size:21px; font-weight:600; letter-spacing:-.01em; }
.card h3 .sw { width:14px; height:14px; flex:none; }
.card-note { margin-top:6px; min-height:48px; font-size:13px; line-height:1.55; color:var(--ink3); }
.bars { margin-top:12px; display:grid; gap:6px; }
.bar-row { display:flex; align-items:center; gap:8px; }
/* Marks the elided middle of a both-ends card. Without it the two groups read as one
   contiguous ranking and the card lies in a new way instead of the old one. */
.bar-gap { display:flex; align-items:center; justify-content:center; margin:2px 0 2px 112px; }
.bar-gap span { font-family:var(--mono); font-size:11px; color:var(--ink3);
                letter-spacing:.04em; }
.bar-name { width:104px; flex:none; text-align:right; font-family:var(--mono); font-size:12px; color:var(--ink2); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.bar-track { position:relative; height:22px; flex:1; background:var(--paper); min-width:0; }
.bar-fill { position:absolute; inset-block:0; left:0; display:flex; align-items:center; justify-content:flex-end; padding-right:6px; }
.bar-val { font-family:var(--mono); font-size:12px; font-weight:600; color:#fff; }
.bar-val.out { position:absolute; inset-block:0; display:flex; align-items:center; padding-left:6px; color:var(--ink); }
.card-foot { margin-top:12px; border-top:1px solid var(--rule); padding-top:8px; font-family:var(--mono); font-size:12px; color:var(--ink3); }

/* ── chart ── */
.panel { margin-top:20px; border:1px solid var(--rule); background:var(--card); }
/* Tinted with the page ground rather than a hardcoded rgba() copy of it - the literal
   here was the OLD --paper value and would have stayed warm after the variable changed. */
.panel-bar { display:flex; flex-wrap:wrap; gap:4px; border-bottom:1px solid var(--rule); background:color-mix(in srgb, var(--paper) 60%, var(--card)); padding:8px 12px; font-size:12.5px; }
.panel-legend { display:flex; flex-wrap:wrap; align-items:center; gap:4px 20px; border-bottom:1px solid var(--rule); padding:10px 16px; }
/* ── listen: five playable clips ── */
.listen-grid { margin-top:20px; display:grid; grid-template-columns:1fr; gap:16px; }
@media (min-width:820px) { .listen-grid { grid-template-columns:1fr 1fr; } }
/* A contrast card carries two transcripts, so it gets the full width - side by side the
   two hypotheses wrap to three lines each and the comparison stops being readable. */
.listen-card.wide { grid-column:1 / -1; }
.listen-row.weak { border-top:1px dashed var(--rule2); padding-top:10px; }
.listen-err { margin-left:6px; padding:1px 6px; border-radius:999px; font-size:10px; }
.listen-err.good { background:rgba(47,111,78,.12); color:var(--local); }
.listen-err.bad { background:rgba(180,83,9,.12); color:var(--warn); }
/* Both rates for THIS clip. On a card whose point is that a boundary move is charged
   like a mishearing, the pair is the evidence, so it sits on the card not in a tooltip. */
.listen-rates { margin-left:8px; font-size:10.5px; color:var(--ink3); }
.listen-card { background:var(--card); border:1px solid var(--rule); border-radius:4px;
               padding:16px 18px; min-width:0; }
.listen-head { display:flex; align-items:center; justify-content:space-between; gap:12px;
               padding-bottom:10px; border-bottom:1px solid var(--rule); }
.listen-lang { display:flex; align-items:center; gap:8px; font-size:18px; }
.listen-eng { font-size:13px; color:var(--ink2); }
.listen-card audio { width:100%; margin:12px 0 8px; height:36px; }
.listen-rank { font-size:11.5px; color:var(--ink3); }
.listen-row { margin-top:12px; }
.listen-k { display:block; font-size:10.5px; text-transform:uppercase; letter-spacing:.1em;
            color:var(--ink3); }
.listen-ref, .listen-hyp { margin-top:4px; font-family:var(--serif); font-size:16px;
                           line-height:1.5; color:var(--ink2); }
.listen-hyp { color:var(--ink); }
/* Marked, not colour-only: the diff has to survive a colour-blind reader and a
   greyscale print, so deletions are struck and insertions underlined as well. */
.ddel { color:var(--warn); text-decoration:line-through; text-decoration-thickness:1px; }
.dins { color:var(--local); text-decoration:underline; text-underline-offset:2px; font-weight:600; }
/* Folded by the normalizer: visibly different, scored identical. Muted rather than
   alarming, because it is NOT an error - marking it like one would contradict the score. */
.dfold { color:var(--ink3); text-decoration:underline dotted; text-underline-offset:2px; }

/* Engine picker for the conditions chart. */
.panel-pick { display:flex; align-items:baseline; gap:12px; flex-wrap:wrap;
              padding:10px 16px; border-bottom:1px solid var(--rule); }
.pick-label { font-family:var(--mono); font-size:11px; text-transform:uppercase;
              letter-spacing:.1em; color:var(--ink3); flex:none; }
.chips { display:flex; flex-wrap:wrap; gap:6px; }
.chip { display:inline-flex; align-items:center; gap:6px; cursor:pointer;
        border:1px solid var(--rule2); border-radius:999px; background:var(--card);
        padding:3px 10px; font-family:var(--mono); font-size:12px; color:var(--ink3);
        transition:color .15s, border-color .15s; }
.chip:hover:not(:disabled) { color:var(--ink); border-color:var(--ink3); }
.chip.on { color:var(--ink); border-color:var(--ink3); }
.chip:disabled { opacity:.4; cursor:not-allowed; }
.chip .sw { width:9px; height:9px; border-radius:2px; flex:none; }

/* Drawn flags. The hairline matters: the French and Italian flags are part white, so
   without an edge they bleed into the page ground and read as a two-stripe flag. */
.flag { vertical-align:-2px; border:.5px solid rgba(0,0,0,.22); border-radius:1px; }
.legend { display:flex; flex-wrap:wrap; gap:4px 20px; font-family:var(--mono); font-size:13px; color:var(--ink2); }
.legend li { display:flex; min-width:0; align-items:center; gap:6px; }
.legend .sw { width:9px; height:9px; flex:none; }
.legend .lm { max-width:18ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--ink3); }
.axis-note { margin-left:auto; font-family:var(--mono); font-size:12.5px; color:var(--ink3); }
/* STEP 10: the chart is drawn at its intrinsic size and SCROLLS below that, never
   shrinks. Stretching a narrow viewBox into a wide column scales the type with it -
   v3 rendered 10.5 px labels at 17.3 px that way, dwarfing every neighbouring chart. */
.chart-wrap { position:relative; margin:0; padding:16px 12px; }
.chart-scroll { overflow-x:auto; overflow-y:hidden; }
/* Width is set in JS to match the viewBox 1:1, so one SVG unit is one CSS pixel and
   type renders at exactly the size it was specified at. Never `width:100%` over a
   fixed viewBox - that is the v3 bug, where 10.5 px labels rendered at 17.3 px. */
.chart-svg { max-width:none; height:auto; margin-inline:auto; }

/* ── chart internals. State lives in ONE class on the <svg> root plus a class per
   affected node, so focusing a lane re-paints rather than regenerating the SVG. ── */
.chart-svg .grid { stroke:var(--rule); stroke-width:1; }
.chart-svg .tick { fill:var(--ink3); font-size:12px; }
/* .lane-l1/.lane-l2 are SHARED by three charts whose label geometry is hard-coded
   (conditions, bump, per-language). Enlarging them globally for the per-language chart
   pushed the bump chart's top row off the viewBox - so the bigger type is scoped to the
   per-language chart's own classes and these keep their original size. */
.chart-svg .lane-l1 { fill:var(--ink2); font-size:14px; font-weight:500; }
.chart-svg .lane-l2 { fill:var(--ink3); font-size:12px; }
/* Per-language panels only. l1 carries the flag and takes the full +60%; l2 stops at
   16px because "best: elevenlabs" at 19px measures 133px against a 126px panel - the
   labels are panel-centred, so two neighbours would overlap by ~7px. */
.chart-svg .lang-l1 { fill:var(--ink2); font-size:22px; font-weight:500; }
.chart-svg .lang-l2 { fill:var(--ink3); font-size:16px; }
.chart-svg .lane.off .lane-l1 { fill:#b8b5ae; }
.chart-svg .lane.off .lane-l2 { fill:#c8c5be; }
.chart-svg .focusband { fill:var(--ink); opacity:0; }
.chart-svg .crosshair { stroke:var(--ink3); stroke-width:1; stroke-dasharray:4 4; opacity:0; }
.chart-svg .crosshair.show { opacity:1; }
.chart-svg .series-line { fill:none; stroke-width:2; stroke-linejoin:round; stroke-linecap:round; }
.chart-svg .dot { fill:#fff; stroke-width:2; }
.chart-svg .endlab-v { font-size:13px; font-weight:600; }
.chart-svg .endlab-n { fill:var(--ink3); font-size:11.5px; }
.chart-svg .leader { fill:none; stroke-width:1; opacity:.45; }
.chart-svg .vlab { font-size:12px; font-weight:600; opacity:0; }
.chart-svg .vlab.show { opacity:1; }
.chart-svg .axlab { fill:var(--ink3); font-size:13px; }
.chart-svg .frontier { fill:none; stroke:var(--best); stroke-width:1.5; stroke-dasharray:5 4; opacity:.65; }
.chart-svg .halo { fill:var(--card); }
.chart-svg .mark .pt { stroke:var(--card); stroke-width:1.5; }
.chart-svg .mark .hit { fill:transparent; }
.chart-svg .mark:focus-visible { outline:none; }
.chart-svg .mark:focus-visible .halo { stroke:var(--best); stroke-width:2.5; }
.chart-svg .ptlab { font-size:11.5px; fill:var(--ink2); }
.chart-foot { margin-top:10px; font-family:var(--mono); font-size:13px; line-height:1.55; color:var(--ink3); }
.chart-foot b { color:var(--ink2); font-weight:600; }

/* ── a correction the page has to carry, not bury ── */
.callout { margin-top:18px; border-left:2px solid var(--best); background:rgba(109,74,255,.05); padding:12px 16px; }
.callout .k { font-family:var(--mono); font-size:10.5px; text-transform:uppercase; letter-spacing:.1em; color:var(--best); }
.callout p { margin-top:5px; max-width:none; font-family:var(--serif); font-size:15.5px; line-height:1.55; color:var(--ink2); }
.callout b { color:var(--ink); font-weight:600; }

.split { margin-top:20px; display:grid; grid-template-columns:1fr; gap:16px; }
.panel.pad { padding:16px; margin-top:0; }
.panel-h { font-family:var(--serif); font-size:17px; font-weight:600; letter-spacing:-.01em; }
.panel.pad .bars { margin-top:12px; }

/* verbatim exhibits - what an engine actually returned */
.quotes { margin-top:16px; display:grid; grid-template-columns:1fr; gap:12px; }
.quote { border:1px solid var(--rule); background:var(--card); padding:12px 14px; }
.quote .who { display:flex; align-items:center; justify-content:space-between; gap:10px; font-family:var(--mono); font-size:11px; color:var(--ink2); }
.quote .tag { font-size:10px; text-transform:uppercase; letter-spacing:.08em; padding:2px 6px; }
.quote .tag.fab { background:rgba(180,83,9,.12); color:var(--warn); }
.quote .tag.ann { background:rgba(47,111,78,.12); color:var(--local); }
.quote blockquote { margin:8px 0 0; font-family:var(--serif); font-size:15px; line-height:1.5; color:var(--ink); }
.quote blockquote::before { content:'\201C'; } .quote blockquote::after { content:'\201D'; }
.quote .ctx { margin-top:6px; font-family:var(--mono); font-size:10.5px; color:var(--ink3); }

/* CI text inside a data cell */
.ci { color:var(--ink3); font-size:10.5px; }
.tie { color:var(--best); font-weight:600; }
td.hi { background:rgba(109,74,255,.07); }

/* limits matrix */
/* Coverage, as two columns. 18px is the old 12px + 50%. */
.limits-grid { margin-top:16px; display:grid; grid-template-columns:1fr; gap:24px 40px; }
@media (min-width:760px) { .limits-grid { grid-template-columns:1fr 1fr; } }
.limits-col ul { list-style:none; margin:10px 0 0; padding:0; }
.limits-col li { display:flex; gap:10px; align-items:baseline; font-size:18px;
                 line-height:1.5; padding:7px 0; border-top:1px solid var(--rule); }
.limits-h { font-family:var(--mono); font-size:14px; font-weight:600; text-transform:uppercase;
            letter-spacing:.08em; }
.limits .yes, .limits-col .yes, .limits-h.yes { color:var(--local); }
.limits .no,  .limits-col .no,  .limits-h.no  { color:var(--warn); }

/* Definition tables are prose-shaped: they need to wrap, unlike the data tables. */
td.wrap { white-space:normal; max-width:38ch; font-family:var(--sans); font-size:12px;
          line-height:1.5; color:var(--ink2); }
#metricTable, #datasetTable { min-width:720px; }
#metricTable th[scope="row"], #datasetTable th[scope="row"] { white-space:normal; max-width:20ch; vertical-align:top; }
#metricTable td, #datasetTable td { vertical-align:top; }
#langTable { min-width:640px; }
#pinsTable { min-width:760px; }
#benchTable { min-width:820px; }
#benchTable tr.mine { background:rgba(109,74,255,.06); }
#benchTable .yes { color:var(--local); font-weight:600; }
#benchTable .no { color:var(--ink3); }
.caveats { display:grid; gap:10px; }
.caveats li { position:relative; padding-left:18px; }
.caveats li::before { content:"-"; position:absolute; left:0; color:var(--ink3); }
.caveats b { color:var(--ink); font-weight:600; }
#langTable td { line-height:1.3; }
#digitTable { min-width:520px; }
.ctx-line { font-family:var(--mono); font-size:10px; color:var(--ink3); padding-left:94px; }

@media (min-width:820px) {
  .split { grid-template-columns:1fr 1fr; }
  .quotes { grid-template-columns:1fr 1fr; }
}
.chart-svg.focused .series-line,
.chart-svg.focused .dot { opacity:.35; }
.chart-svg.focused .endlab-v,
.chart-svg.focused .endlab-n,
.chart-svg.focused .leader { opacity:.5; }
.chart-svg.focused .dot.hot { opacity:1; }
.chart-svg .series-line, .chart-svg .dot, .chart-svg .endlab-v,
.chart-svg .endlab-n, .chart-svg .leader, .chart-svg .vlab { transition:opacity .15s; }

/* ── STEP 19: every chart ships a table twin, built alongside it rather than
   retrofitted. Nothing on this page is hover-only. ── */
.twin { margin-top:12px; border-top:1px solid var(--rule); padding-top:10px; }
.twin summary { cursor:pointer; font-family:var(--mono); font-size:11px; color:var(--ink3); }
.twin summary:hover { color:var(--ink); }
/* The twin has to scroll inside itself, like every other wide table here. Left
   unscrolled it fits while CLOSED and blows the page out the moment a reader opens
   it - 685 px of table in a 365 px viewport, which no amount of testing the closed
   state would ever have caught. */
.twin[open] { overflow-x:auto; }
.twin table { min-width:520px; margin-top:10px; font-size:11.5px; }
.twin th, .twin td { padding:4px 8px; }
.twin thead th { text-align:right; }
.twin thead th:first-child { text-align:left; }
.twin tbody th { text-align:left; font-weight:500; color:var(--ink); }
.tip {
  pointer-events:none; position:absolute; z-index:10; display:none;
  border:1px solid var(--rule2); border-radius:3px; background:var(--card);
  padding:8px 10px; font-family:var(--mono); font-size:12.5px;
  box-shadow:0 1px 3px rgba(0,0,0,.08);
}
.tip.show { display:block; }
.tip-title { margin-bottom:4px; font-weight:600; }
.tip-basis { margin-bottom:6px; max-width:34ch; line-height:1.4; color:var(--ink3); }
.tip-row { display:flex; align-items:center; gap:8px; }
.tip-row .sw { width:9px; height:9px; flex:none; }
.tip-row .nm { flex:1; color:var(--ink2); }
.tip-row .vl { font-weight:600; }

/* ── the 18-engine table (STEP 9: its own horizontal scroll) ── */
.search {
  width:224px; border:1px solid var(--rule2); border-radius:999px; background:var(--card);
  padding:6px 14px; font-family:var(--mono); font-size:12px; color:inherit; outline:none; transition:border-color .15s;
}
.search::placeholder { color:var(--ink3); }
.search:focus { border-color:var(--ink2); }
.table-scroll { margin-top:16px; overflow-x:auto; }
table { width:100%; min-width:880px; border-collapse:collapse; font-family:var(--mono); font-size:14px; }
thead tr { border-bottom:1px solid var(--rule2); }
th {
  cursor:pointer; user-select:none; white-space:nowrap; padding:8px 10px;
  font-size:13px; font-weight:500; text-transform:uppercase; letter-spacing:.06em;
  color:var(--ink3); transition:color .15s;
}
th:hover { color:var(--ink); }
th.l { text-align:left; } th.r { text-align:right; }
th .dir { margin-left:4px; color:var(--ink); }
tbody tr { border-bottom:1px solid var(--rule); transition:background .15s; }
tbody tr:hover { background:var(--card); }
td { padding:6px 10px; }
td.r { text-align:right; color:var(--ink2); white-space:nowrap; }
td.name { white-space:nowrap; font-weight:500; }
td .cls { display:inline-flex; align-items:center; gap:6px; font-size:10.5px; color:var(--ink2); }
td .cls .sw { width:7px; height:7px; }
td .rank { color:var(--ink3); }
.none { color:var(--ink3); }
.rowcount { margin-top:12px; font-family:var(--mono); font-size:11px; color:var(--ink3); }

/* ── method ── */
.method-grid { margin-top:16px; display:grid; grid-template-columns:1fr; gap:32px; }
.method-prose { display:grid; gap:16px; font-family:var(--serif); font-size:18px; line-height:1.6; color:var(--ink2); }
.method-prose .lead { color:var(--ink); }
.specs { align-self:start; border:1px solid var(--rule); background:var(--card); }
.spec { display:flex; justify-content:space-between; gap:12px; padding:8px 14px; }
.spec + .spec { border-top:1px solid var(--rule); }
.spec dt { flex:none; font-family:var(--mono); font-size:11px; color:var(--ink3); }
.spec dd { text-align:right; font-family:var(--mono); font-size:11px; color:var(--ink); }

.foot { margin-top:64px; border-top:1px solid var(--rule); padding-top:16px; font-family:var(--mono); font-size:11px; color:var(--ink3); }

/* ══ STEP 9: breakpoints ══════════════════════════════════════════════════ */
/* The 640px rule that used to sit here set --gutter:24px, which is the same value the
   :root default already has - so it did nothing except silently OVERRIDE the 48px rule
   declared above it. Two rules matched above 1200px, equal specificity, and the later
   one won. Measured at 1600 and 2560: the gutter was still 24px. Removed; the widening
   now lives here, in source order, where it actually applies. */
@media (min-width:1200px) {
  :root { --gutter:48px; }
}
@media (min-width:900px) {
  .method-grid { grid-template-columns:1fr 280px; }
}
@media (min-width:1024px) {           /* the section rail appears */
  /* main = viewport − scrollbar − 2×gutter − rail − gap, capped at --column-max.
     minmax(0, …) rather than 1fr so the track can shrink below its content (the wide
     tables scroll inside themselves) and stop growing at the cap. justify-content
     centres rail+main together once the cap binds, so an ultra-wide screen gets even
     margins instead of the whole page hugging the left edge. */
  .layout { grid-template-columns:var(--rail) minmax(0,var(--column-max)); }
  .rail { display:block; }
  .cards { grid-template-columns:repeat(3,1fr); }
}

/* ══ accessibility ════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion:reduce) {
  html { scroll-behavior:auto; }
  *, *::before, *::after { transition:none !important; animation:none !important; }
}
:where(a,button,input,th):focus-visible { outline:2px solid var(--best); outline-offset:2px; }
@media (forced-colors:active) {
  .card, .panel, .specs, .topbar { border-color:CanvasText; }
  .pill[aria-pressed="true"], .pill.on { forced-color-adjust:none; background:Highlight; color:HighlightText; }
}

/* ── who made this ── */
.about-grid { margin-top:26px; display:grid; grid-template-columns:1fr; gap:22px; }
.about-q {
  font-family:var(--mono); font-size:22px; font-weight:600; text-transform:uppercase;
  letter-spacing:.1em; color:var(--ink3);
}
.about-a { margin-top:8px; font-family:var(--serif); font-size:18px; line-height:1.55; color:var(--ink2); }
.about-a b { color:var(--ink); font-weight:600; }
/* overflow-wrap is load-bearing: at 19px mono the LinkedIn URL is one 379px unbreakable
   token, which pushed the whole page into a horizontal scroll on a 365px viewport. */
.about-foot { margin-top:16px; font-family:var(--mono); font-size:19px; color:var(--ink3);
              overflow-wrap:anywhere; }
.about-foot a { color:var(--link); font-weight:600; border-bottom:1px solid var(--link); }
.about-foot a:hover { color:var(--ink); border-bottom-color:var(--ink); }

/* ── author photo ── */
/* align-items:flex-start, not center: the body beside the photo is now a heading plus a
   paragraph plus the link, so centring it floats the photo against a wall of text. */
.about-head { margin-top:20px; display:flex; align-items:flex-start; gap:28px; flex-wrap:wrap; }
.about-head-body { flex:1 1 32ch; min-width:0; }
.author {
  position:relative; flex:none; width:120px; height:120px; margin:0;
  border-radius:50%; overflow:hidden; background:var(--rule);
  box-shadow:0 0 0 1px var(--rule2);
}
.author img { width:100%; height:100%; object-fit:cover; display:block; }
/* Shown only when the image fails or is absent - see the onerror in index.html. */
.author-fallback {
  position:absolute; inset:0; display:none; align-items:center; justify-content:center;
  font-family:var(--mono); font-size:34px; font-weight:600; color:var(--ink3);
  letter-spacing:.04em;
}
.author.no-photo .author-fallback { display:flex; }
/* The independence line now sits under the h1 rather than beside the photo - it is a
   claim about the benchmark, not about the author. */
.title-lede {
  margin-top:14px; font-family:var(--serif); font-size:19px;
  line-height:1.5; color:var(--ink2);
}
@media (min-width:900px) { .author { width:160px; height:160px; } }

/* Inline colour keys for prose that has to name a chart colour. Each carries a dot as
   well as the colour word, so the sentence still parses in greyscale. */
.key-local, .key-cloud, .key-best { font-weight:600; white-space:nowrap; }
.key-local { color:var(--local); }
.key-cloud { color:var(--cloud); }
.key-best  { color:var(--best); }
.key-local::before, .key-cloud::before, .key-best::before {
  content:""; display:inline-block; width:8px; height:8px; margin-right:4px;
  vertical-align:baseline; background:currentColor;
}

/* Marks a cell whose figure comes from fewer languages than the column implies. Small and
   muted: it is a scope note, not a warning - the number itself is sound. */
.partial {
  margin-left:4px; padding:0 4px; border-radius:3px; font-size:9.5px;
  background:var(--rule); color:var(--ink3); text-transform:uppercase; letter-spacing:.06em;
}

/* Publish date, ahead of the tagline in the top bar. Separated by a rule rather than a
   dot so it does not read as a fourth item in the "independent · non-sponsored ·
   self-funded" list - it is a different kind of fact. */
.pubdate { flex:none; margin-right:12px; padding-right:12px;
           border-right:1px solid var(--rule2); color:var(--ink2); white-space:nowrap; }
.pubdate:empty { display:none; }
