/**
 * Stylesheet for the generated content pages (`docs/convert/`, `docs/blog/`).
 *
 * The product page keeps its CSS inline because it is one hand-written document; these pages are
 * generated in a set, so they share this file instead of carrying a copy each. The custom properties
 * mirror `docs/index.html` on purpose — same brand blue, same ink ramp, same motion ladder — so a
 * reader moving between the product page and a conversion page stays in one visual language.
 *
 * Bilingual by CSS, not by JavaScript: every string ships in both languages in the HTML and only the
 * one matching `<html lang>` is shown, so a crawler that runs no JavaScript still reads every fact
 * (same rule the product page documents in its head).
 */

:root {
  --brand: #2563eb;
  --brand-dark: #1d4ed8;
  --brand-soft: #eef4ff;
  --brand-line: #d6e4ff;
  --ink: #101828;
  --ink-soft: #475467;
  --ink-faint: #667085;
  --paper: #fff;
  --paper-alt: #f7f9fc;
  --line: #e4e7ec;
  --ok: #067647;
  --warn: #b54708;
  --warn-soft: #fff7ed;
  --radius: 16px;
  --radius-sm: 10px;
  --container: 900px;
  --shadow-sm: 0 1px 2px rgb(16 24 40 / 6%);
  --shadow-md: 0 4px 24px -12px rgb(16 24 40 / 18%);
  --dur-fast: 0.16s;
  --dur-mid: 0.24s;
  --dur-slow: 0.42s;
  --ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-in: cubic-bezier(0.55, 0, 0.55, 0.2);
}

* {
  box-sizing: border-box;
}

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

body {
  margin: 0;
  color: var(--ink);
  background: var(--paper);
  font-family:
    -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Roboto,
    Helvetica, Arial, sans-serif;
  font-size: 16px;
  line-height: 1.72;
}

.wrap {
  max-width: var(--container);
  padding: 0 20px;
  margin: 0 auto;
}

/* One language visible, both present in the DOM. */
html[lang='zh-CN'] [lang='en'] {
  display: none;
}

html[lang='en'] [lang='zh-CN'] {
  display: none;
}

a {
  color: var(--brand);
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

/* ---------------------------------------------------------------- header */

.site-head {
  position: sticky;
  top: 0;
  z-index: 10;
  border-bottom: 1px solid var(--line);
  background: rgb(255 255 255 / 88%);
  backdrop-filter: saturate(1.6) blur(10px);
}

.site-head .wrap {
  display: flex;
  max-width: 1180px;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding-block: 12px;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  color: var(--ink);
  font-weight: 700;
  letter-spacing: -0.01em;
}

.brand img {
  border-radius: 7px;
}

.site-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  font-size: 14px;
}

.site-nav a {
  color: var(--ink-soft);
  transition: color var(--dur-fast) var(--ease-out);
}

.site-nav a:hover {
  color: var(--brand);
  text-decoration: none;
}

/* ------------------------------------------------------------------ body */

.page {
  padding-block: 28px 72px;
}

.crumbs {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  color: var(--ink-faint);
  font-size: 13px;
}

.crumbs a {
  color: var(--ink-faint);
}

.crumbs a:hover {
  color: var(--brand);
}

article {
  margin-top: 18px;
}

.eyebrow {
  margin: 0 0 10px;
  color: var(--brand-dark);
  font-size: 13px;
  font-weight: 650;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

h1 {
  margin: 0 0 14px;
  font-size: clamp(28px, 5vw, 40px);
  line-height: 1.22;
  letter-spacing: -0.02em;
}

.lede {
  margin: 0 0 26px;
  color: var(--ink-soft);
  font-size: 17px;
}

h2 {
  margin: 34px 0 12px;
  font-size: 21px;
  letter-spacing: -0.01em;
}

h3 {
  margin: 26px 0 10px;
  font-size: 17px;
}

p {
  margin: 0 0 14px;
}

ul {
  padding-left: 22px;
  margin: 0 0 16px;
}

li {
  margin-bottom: 9px;
}

.limits li {
  color: var(--ink-soft);
}

.fine {
  color: var(--ink-faint);
  font-size: 13px;
}

.tool-crosslink {
  margin: 16px 0 0;
}

/* --------------------------------------------------------------- article */

/* The byline sits under the h1: what this page is, when it was written, how long it takes. */
.byline {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 18px;
  margin: 0 0 22px;
  color: var(--ink-faint);
  font-size: 13px;
}

.byline time {
  color: var(--ink-soft);
  font-weight: 600;
}

/* Section anchors need clearing the sticky header. */
:target {
  scroll-margin-top: 76px;
}

.toc {
  padding: 16px 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper-alt);
  font-size: 14px;
}

.toc ol {
  padding-left: 20px;
  margin: 0;
}

.toc li {
  margin-bottom: 5px;
}

blockquote {
  padding: 2px 0 2px 18px;
  margin: 0 0 18px;
  border-left: 3px solid var(--brand-line);
  color: var(--ink-soft);
  font-size: 17px;
}

code {
  padding: 2px 5px;
  border-radius: 5px;
  background: var(--paper-alt);
  color: var(--brand-dark);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.9em;
}

pre {
  overflow-x: auto;
  padding: 16px 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper-alt);
  font-size: 13.5px;
  line-height: 1.6;
}

pre code {
  padding: 0;
  background: none;
  color: var(--ink);
  font-size: inherit;
}

figure {
  margin: 24px 0;
}

figure img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper-alt);
}

figcaption {
  margin-top: 8px;
  color: var(--ink-faint);
  font-size: 13px;
}

/* The stat strip under the lede: four numbers, both languages, no JS needed. */
.stats {
  display: grid;
  padding: 0;
  gap: 10px;
  margin: 0 0 26px;
  list-style: none;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}

.stats li {
  padding: 12px 14px;
  border: 1px solid var(--brand-line);
  border-radius: var(--radius-sm);
  margin: 0;
  background: var(--brand-soft);
}

.stats b {
  display: block;
  color: var(--brand-dark);
  font-size: 22px;
  line-height: 1.3;
}

.stats span {
  color: var(--ink-soft);
  font-size: 13px;
}

/* ------------------------------------------------------------- route card */

.route-card {
  padding: 18px 20px;
  border: 1px solid var(--brand-line);
  border-radius: var(--radius);
  background: var(--brand-soft);
}

.route-label {
  margin: 0 0 8px;
  color: var(--brand-dark);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.route-steps {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  margin: 0;
  font-weight: 600;
}

.chip {
  padding: 5px 11px;
  border: 1px solid var(--brand-line);
  border-radius: 999px;
  background: var(--paper);
  font-size: 14px;
}

.arrow {
  color: var(--brand-dark);
  font-weight: 700;
}

.route-note {
  margin: 10px 0 0;
  color: var(--ink-soft);
  font-size: 14px;
}

/* --------------------------------------------------------- route finder */
/* The same route-card vocabulary as the pair pages, wrapped in one control surface. The output box
   keeps a floor so choosing a second format does not make the sections below jump. */
.finder {
  display: none;
  gap: 16px;
  padding: 20px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  box-shadow: var(--shadow-sm);
}

/* The finder is an enhancement over the matrix, not a second source of truth. It appears only after
   its script has wired the controls and added `finder-live`; if the script is absent or returns early,
   the dead form stays hidden and the fallback sentence points at the matrix, which is fully static. */
.finder-live .finder {
  display: grid;
}

.finder-fallback {
  margin: 0 0 12px;
  color: var(--ink-soft);
  font-size: 14px;
}

.finder-live .finder-fallback {
  display: none;
}

.finder-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.finder-field {
  display: flex;
  flex: 1 1 220px;
  flex-direction: column;
  gap: 6px;
  color: var(--ink-soft);
  font-size: 13px;
}

.finder-field select {
  width: 100%;
  padding: 10px 12px;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font: inherit;
}

.finder-field select:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

.finder-output {
  min-height: 120px;
  padding: 16px;
  background: var(--paper-alt);
  border: 1px dashed var(--line);
  border-radius: var(--radius-sm);
}

.finder-hint {
  margin: 0;
  color: var(--ink-soft);
  font-size: 14px;
}

.finder-route {
  margin: 0;
}

.finder-link {
  display: inline-block;
  margin-top: 10px;
  color: var(--brand-dark);
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
}

.finder-link:hover {
  text-decoration: underline;
}

.finder-blocked {
  border-color: var(--warn);
  background: var(--warn-soft);
}

.finder-blocked .route-label {
  color: var(--warn);
}

/* -------------------------------------------------------------------- faq */

details {
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper-alt);
}

details + details {
  margin-top: 10px;
}

summary {
  cursor: pointer;
  font-weight: 650;
}

details p {
  margin: 10px 0 0;
  color: var(--ink-soft);
}

/* ---------------------------------------------------------------- cta/foot */

.cta {
  margin-top: 40px;
  padding: 22px 24px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper-alt);
}

.cta p:first-child {
  margin-top: 0;
}

.cta-links {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
}

.btn {
  display: inline-flex;
  padding: 10px 18px;
  border: 1px solid var(--brand);
  border-radius: 999px;
  background: var(--brand);
  color: #fff;
  font-size: 15px;
  font-weight: 650;
  transition:
    background var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}

.btn:hover {
  border-color: var(--brand-dark);
  background: var(--brand-dark);
  text-decoration: none;
}

.btn.ghost {
  border-color: var(--line);
  background: transparent;
  color: var(--ink);
}

.btn.ghost:hover {
  border-color: var(--brand-line);
  background: var(--brand-soft);
  color: var(--brand-dark);
}

.related-list {
  display: grid;
  padding: 0;
  gap: 8px 20px;
  list-style: none;
}

.index-list {
  padding: 0;
  list-style: none;
}

.index-list li {
  padding-bottom: 12px;
  margin-bottom: 12px;
  border-bottom: 1px dashed var(--line);
}

.index-list a {
  display: block;
  font-weight: 650;
}

/* ============ The format matrix (conversions index) ============ */
/* Fourteen rows by eleven columns cannot fit a 900px column without shrinking the digits below what they
   are, so the grid scrolls sideways instead — a matrix nobody can read is a wall of glyphs. The row
   header stays pinned to the left edge while it scrolls, because a cell that has lost its format name is
   no longer an answer. `overflow-x` also makes the container the sticky root, which is why nothing here
   sticks to the *top*: a header that cannot move vertically would only pretend to. */
.matrix-scroll {
  overflow-x: auto;
  margin: 18px 0 4px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper);
  box-shadow: var(--shadow-sm);
}

table.matrix {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}

.matrix caption {
  padding: 10px 12px 0;
  caption-side: bottom;
  color: var(--ink-faint);
  font-size: 12.5px;
  text-align: left;
}

.matrix th,
.matrix td {
  padding: 6px 8px;
  border-right: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  text-align: center;
  white-space: nowrap;
}

.matrix thead th {
  background: var(--paper-alt);
  color: var(--ink-soft);
  font-weight: 650;
}

.matrix thead th:last-child,
.matrix tbody tr:last-child th,
.matrix tbody tr:last-child td {
  border-bottom: 0;
}

.matrix th:last-child,
.matrix td:last-child {
  border-right: 0;
}

.matrix .mx-corner {
  left: 0;
  z-index: 3;
  color: var(--ink-faint);
  font-weight: 500;
}

.matrix tbody th {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--paper);
  font-weight: 600;
  text-align: left;
}

.matrix td {
  min-width: 48px;
  transition:
    box-shadow var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

.matrix .mx-open {
  background: var(--brand-soft);
}

.matrix .mx-block {
  background: var(--warn-soft);
  color: var(--warn);
}

.matrix .mx-self {
  background: var(--paper-alt);
  color: var(--ink-faint);
}

.matrix .mx-page,
.matrix .mx-hub {
  font-weight: 700;
}

/* Every open cell is a link, and the tint behind a cell means "reachable", not "clickable" — so the
   link cannot rely on colour alone. An underline says it on its own, and it survives both colour-blind
   readers and a screen that renders `--brand-dark` next to `--ink`. The digit stays the loud glyph; the
   underline says the digit is a door. */
.matrix .mx-page,
.matrix .mx-hub {
  display: block;
  color: var(--brand-dark);
  text-decoration: underline;
  text-decoration-color: var(--brand);
  text-decoration-thickness: 1.5px;
  text-underline-offset: 3px;
}

.matrix .mx-page:hover,
.matrix .mx-hub:hover {
  text-decoration-thickness: 2px;
}

/* A cell the reader is looking at, and the two headers that say what it is. Keyboard focus and hover
   arrive at the same state on purpose: the answer a reader is after does not depend on which they used. */
.matrix td.mx-on {
  box-shadow: inset 0 0 0 2px var(--brand);
}

.matrix th.mx-on {
  background: var(--brand-soft);
  color: var(--brand-dark);
}

.matrix td:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
}

.mx-legend {
  margin: 12px 0 0;
  color: var(--ink-soft);
  font-size: 13.5px;
}

/* The readout reserves two lines: it is a live region, and a sentence that changes height while the
   reader steps through the grid moves everything under it. */
.mx-readout {
  min-height: 3.5em;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper-alt);
  color: var(--ink);
  font-size: 14px;
}

/* The glyph in a cell is not the whole answer — each one carries a phrase only an assistive technology
   or a parser reads. Hidden by clipping, never by `display: none`, which would take it out of the
   accessible name the link is built from. */
.mx-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The filter row is three toggles, not a form: `aria-pressed` carries the state, and only the pressed
   pill is filled so the current view reads at a glance. */
.mx-filter {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 4px;
}

.mx-filter button {
  padding: 7px 13px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--paper);
  color: var(--ink-soft);
  font: inherit;
  font-size: 13.5px;
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

.mx-filter button:hover {
  border-color: var(--brand-line);
  background: var(--brand-soft);
  color: var(--brand-dark);
}

.mx-filter button[aria-pressed='true'] {
  border-color: var(--brand);
  background: var(--brand-soft);
  color: var(--brand-dark);
  font-weight: 700;
}

.mx-filter button:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* Dimming, never hiding: a filtered-out cell is still a true route, and the legend above the grid keeps
   describing it. Opacity rather than `display: none` so the grid keeps its shape and the reader keeps
   their place in it. */
.matrix .mx-filtered {
  opacity: 0.28;
}

.matrix .mx-filtered a {
  text-decoration-color: transparent;
}

/* The route strip under the readout repeats the chain as chips. It starts empty, so the box keeps one
   row of height and the page does not jump the first time a cell is reached. The container is
   `aria-hidden`, so the stagger below can never become a second announcement. */
.mx-trail {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 8px;
  align-items: center;
  min-height: 32px;
  margin: 8px 0 0;
}

.js .mx-trail .chip,
.js .mx-trail .arrow {
  opacity: 0;
  transform: translateY(4px);
  transition:
    opacity var(--dur-mid) var(--ease-out),
    transform var(--dur-mid) var(--ease-out);
}

.js .mx-trail.on .chip,
.js .mx-trail.on .arrow {
  opacity: 1;
  transform: none;
}

/* ============ Source-format hub pages ============ */
/* One card per target: the target, the chain it runs, what survives, and the link to the full page
   when one exists. Cards without a pair page carry the `#to-*` anchor the matrix's hub cells open;
   the shared `:target` rule above scrolls them clear of the sticky header, and this border says which
   one was asked for. */
.hub-routes {
  display: grid;
  gap: 14px;
  padding: 0;
  margin: 18px 0 0;
  list-style: none;
}

.hub-route {
  padding: 16px 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper);
  box-shadow: var(--shadow-sm);
}

.hub-route:target {
  border-color: var(--brand-line);
  box-shadow: 0 0 0 3px var(--brand-soft);
}

.hub-route-head {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  align-items: baseline;
  justify-content: space-between;
  margin: 0 0 4px;
}

.hub-route-target {
  font-size: 16.5px;
  font-weight: 700;
}

.hub-route .route-steps {
  margin: 8px 0 10px;
}

.hub-route > p:last-child {
  margin-bottom: 0;
}

.hub-links {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 14px;
  padding: 0;
  margin: 12px 0 0;
  list-style: none;
}

.hub-links li {
  margin: 0;
}

.hub-links a {
  color: var(--ink-soft);
}

.hub-links a:hover {
  color: var(--brand);
}

/* ============ Index: source-format cards ============ */
.hub-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
  gap: 14px;
  padding: 0;
  margin: 18px 0 0;
  list-style: none;
}

.hub-card {
  padding: 14px 16px;
  margin: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper-alt);
}

.hub-card-title {
  display: block;
  font-size: 16px;
  font-weight: 700;
}

.hub-card p {
  margin: 6px 0 0;
  color: var(--ink-soft);
  font-size: 14px;
}

.hub-card-targets {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 8px;
  align-items: baseline;
  font-size: 13.5px;
}

.site-foot {
  border-top: 1px solid var(--line);
  background: var(--paper-alt);
  padding-block: 26px 34px;
}

.site-foot p {
  margin: 0 0 10px;
  color: var(--ink-soft);
  font-size: 14px;
}

.site-foot .fine {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  font-size: 13px;
}

.site-foot .fine a {
  color: var(--ink-faint);
}

/* ============ Screenshot ============ */

/* Each page shows one real frame of the workbench. The intrinsic size is declared on the <img>, so the
   box is reserved before the file arrives and the text below it never jumps when it loads. */
.shot {
  margin: 32px 0 28px;
}

/* Everything a framed screenshot needs beyond what `figure img` already gives it: the figure is a
   `figure`, so block display, full width, the hairline and the paper-alt backing are already in place.
   Only the lift is specific to this one. */
.shot img {
  box-shadow: var(--shadow-md);
}

.shot figcaption {
  margin-top: 10px;
  color: var(--ink-faint);
  font-size: 13px;
  text-align: center;
}

/* ============ Motion ============ */
/* One movement for the whole set of content pages, using transform and opacity only — the same ladder
   `docs/index.html` sets in its own Motion section, so a reader moving between the two stays in one
   visual language. Nothing longer than half a second, nothing that moves layout.
   The `opacity: 0` lives under `.js`, a class the head script adds before first paint. A client that
   renders CSS but never runs the observer would otherwise be shown blank sections, and search and AI
   crawlers are exactly that kind of client. */
.js .reveal {
  opacity: 0;
  transform: translateY(14px);
  transition:
    opacity var(--dur-slow) var(--ease-out),
    transform var(--dur-slow) var(--ease-out);
}

.js .reveal.in {
  opacity: 1;
  transform: none;
}

/* The route card is the one thing these pages exist to say, so its chain arrives in step order instead of
   all at once: the eye walks from the first format to the last rather than reading a row of chips. Delay is
   the only lever — the marks are already styled — and seven of them at 60ms still finish inside two-thirds
   of a second. Behind `.js` because a client that never runs the reveal script must not be shown a route it
   cannot see; chains shorter than the last rule below simply leave those delays unused. */
.js .route-steps > * {
  opacity: 0;
  transform: translateX(-8px);
  transition:
    opacity var(--dur-mid) var(--ease-out),
    transform var(--dur-mid) var(--ease-out);
}

/* The route chips are stepped in for any route block that has been revealed — the pair page's single
   route card and the hub page's list of routes both sit inside a `.reveal` section, and keying on that
   section means a hub stops shipping invisible chains. */
.js .reveal.in .route-steps > * {
  opacity: 1;
  transform: none;
}

.js .route-steps > :nth-child(2) {
  transition-delay: 60ms;
}

.js .route-steps > :nth-child(3) {
  transition-delay: 120ms;
}

.js .route-steps > :nth-child(4) {
  transition-delay: 180ms;
}

.js .route-steps > :nth-child(5) {
  transition-delay: 240ms;
}

.js .route-steps > :nth-child(6) {
  transition-delay: 300ms;
}

.js .route-steps > :nth-child(7) {
  transition-delay: 360ms;
}

/* The nav labels already change colour on hover; the rule that sweeps in beneath them says the same thing
   with geometry, and arrives on the curve for entering, leaves on the one for leaving. Gated to a mouse
   because a touch device has no hover state to earn, and a line left behind after a tap reads as a bug
   rather than as feedback. */
@media (hover: hover) and (pointer: fine) {
  .site-nav a {
    position: relative;
  }

  .site-nav a::after {
    content: '';
    position: absolute;
    right: 0;
    bottom: -1px;
    left: 0;
    height: 2px;
    border-radius: 2px;
    background-color: var(--brand);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform var(--dur-mid) var(--ease-in);
  }

  .site-nav a:hover::after,
  .site-nav a:focus-visible::after {
    transform: scaleX(1);
    transition-timing-function: var(--ease-out);
  }
}

/* A hairline across the top that fills as you read answers "how far into this page" — on a 900px column of
   dense bilingual prose that question is real. It is driven by the scroll offset rather than a timer, so it
   can never run ahead of the reader and needs no script. The blanket collapse in the block below cannot stop
   a progress-driven animation, so this one is switched off by name there. */
@supports (animation-timeline: scroll()) {
  body::after {
    content: '';
    position: fixed;
    top: 0;
    right: 0;
    left: 0;
    z-index: 20;
    height: 2px;
    background: linear-gradient(90deg, var(--brand-line), var(--brand));
    transform: scaleX(0);
    transform-origin: left;
    pointer-events: none;
    animation-name: read-progress;
    animation-duration: auto;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: scroll(root block);
  }

  @keyframes read-progress {
    to {
      transform: scaleX(1);
    }
  }
}

/* Engines that have both `interpolate-size` and `::details-content` can animate the panel a `<details>`
   opens, and every one of these pages carries a FAQ, so an answer should grow out of its question instead of
   landing mid-scroll. Doubly gated — without
   `interpolate-size`, `auto` is not an animatable length; without `::details-content`, there is no panel to
   address — and either absence leaves today's instant snap. The chevron below lives inside the same gate on
   purpose: an engine that cannot grow the panel keeps its own disclosure triangle, which is at least
   honest about snapping. */
@supports (interpolate-size: allow-keywords) and (selector(::details-content)) {
  html {
    interpolate-size: allow-keywords;
  }

  details::details-content {
    block-size: 0;
    opacity: 0;
    overflow: clip;
    transition:
      block-size var(--dur-slow) var(--ease-out),
      opacity var(--dur-mid) var(--ease-out),
      content-visibility var(--dur-slow) allow-discrete;
  }

  details[open]::details-content {
    block-size: auto;
    opacity: 1;
  }

  /* Closing is the short end of the pair: a panel that collapses at opening speed feels like it is waiting
     for you. */
  details:not([open])::details-content {
    transition-duration: var(--dur-fast);
    transition-timing-function: var(--ease-in);
  }

  summary {
    display: flex;
    gap: 12px;
    align-items: center;
    justify-content: space-between;
    list-style: none;
  }

  summary::-webkit-details-marker {
    display: none;
  }

  /* Two borders and a rotation, so the mark is drawn rather than pasted: it inherits nothing from the font
     and cannot render as a box glyph on a machine without the right fallback. One half-turn on open, and
     its colour follows the question it belongs to. */
  summary::after {
    content: '';
    flex: none;
    width: 7px;
    height: 7px;
    border-color: var(--ink-faint);
    border-style: solid;
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
    transition:
      transform var(--dur-mid) var(--ease-out),
      border-color var(--dur-fast) var(--ease-out);
  }

  details[open] summary::after {
    transform: rotate(-135deg);
    transition-timing-function: var(--ease-in);
  }

  summary:hover::after,
  summary:focus-visible::after {
    border-color: var(--brand);
  }
}

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

  /* A blanket stop rather than a per-component opt-out, so motion added later cannot slip past it.
     Durations collapse instead of the properties being removed, which keeps every element at the
     state it was animating *to*. The iteration count goes with them because a collapsed duration
     does not stop a loop, it speeds it up: `infinite` at 0.01ms samples a different phase every
     frame, which is flicker rather than stillness. Nothing on these pages loops today — the line is
     the backstop for the day something does. A loop whose frames displace the element still has to be
     named and switched off, the way `docs/index.html` switches off its three: one 0.01ms pass paints
     those frames for a frame, and a one-frame jump is the artifact this media query exists to prevent. */
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    animation-delay: 0ms !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
  }

  /* And the state that begins invisible is pinned visible, so nothing here depends on the observer
     above having run. */
  .js .reveal {
    opacity: 1;
    transform: none;
  }

  /* The route chips sit inside a `.reveal` section, but their own start state is a separate `opacity: 0`,
     and the rule above reaches only the section. The delay is gone, so they simply appear with it. */
  .js .route-steps > * {
    opacity: 1;
    transform: none;
  }

  /* A progress-driven animation cannot be reached by collapsing a duration — its clock is the scroll
     offset, not time — so the reading rail is removed rather than shortened. */
  body::after {
    content: none;
  }

  /* `::details-content` is not covered by the blanket above (it is neither `*` nor one of the two marker
     pseudos), and its growth is the only motion here that moves layout. Named on its own so an engine that
     cannot parse the pseudo-element drops this rule and nothing else. */
  details::details-content {
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
  }
}

/* ============ Print ============ */
/* Printing is exactly the client the motion rules above keep worrying about: it renders CSS and never
   runs the reveal observer. Without this block a page printed before it was scrolled through comes out
   as blank bands, and the route card — the one thing these pages exist to say — prints as nothing at
   all. The reading rail is scroll-driven, so it is removed rather than frozen wherever the paper
   happens to land. Screen behaviour is untouched: these declarations are print-only. */
@media print {
  .js .reveal,
  .js .route-steps > * {
    opacity: 1;
    transform: none;
    transition: none;
  }

  /* A scroll container on paper clips to its box and prints the columns that happened to be in view.
     Printing shows the whole grid, which is the one thing a reader might actually want on paper. The
     filter's dimming is undone as well: the paper carries every route, not whichever the screen was
     narrowed to at the moment of printing. */
  .matrix-scroll {
    overflow: visible;
    box-shadow: none;
  }

  .matrix .mx-filtered {
    opacity: 1;
  }

  .mx-filter,
  .mx-trail {
    display: none;
  }

  body::after {
    content: none;
  }
}
