/**
 * @file tokens.css
 * @description GENERATED, and not by hand. Run
 * `node scripts/wiki-tokens.mjs` from site/ to rewrite it, and
 * `node scripts/wiki-tokens.mjs --check` to find out that you have not.
 *
 * The docs are one directory below a site whose stylesheets they cannot link:
 * styles/ never ships, and the built bundle's filename carries a content hash
 * that moves on every build. This file is the seam between the two. It is the
 * site's token layer, copied, so that wiki/style.css writes no colour of its own
 * and the docs answer the reader's skin the way every other page does.
 *
 * Read styles/tokens.css for the reasoning behind any value here. Corrections
 * go there. Anything written into this file is lost on the next generation.
 *
 * @author Byte <bhumika@bytevion.com>
 */

/* ==========================================================================
 * styles/tokens.css, verbatim. Colour, grid, duration, easing.
 * ========================================================================== */

/**
 * @file tokens.css
 * @description The two skins. Light is a printed engineering plate: a warm grey
 * ground with the panels laid *lighter* than it, indigo ink, hairlines mixed
 * from the ink rather than from black. Dark is an obsidian console: one near
 * black ground, rules at twelve percent white, and nothing else raised.
 *
 * Nobody else writes a colour. If a value is missing, ask for it here rather
 * than inlining a hex, because a hex in a component is a colour that cannot
 * follow the theme.
 *
 * ## Why light is declared bare and dark is declared twice
 *
 * The bare `:root` is light, so a visitor with no preference and no stored
 * choice gets the plate. Dark then has to be written twice, once for the system
 * preference and once for an explicit toggle, and CSS gives no way to share the
 * two. `scripts/stage.mjs` parses both blocks and fails the build if their
 * token names disagree, so the duplication is checked rather than trusted.
 *
 * The media query is guarded with `:not([data-theme="light"])` so that choosing
 * light on a machine set to dark switches the whole query off and the bare
 * `:root` is what is left. That is why there is no `[data-theme="light"]` block:
 * there is nothing for it to restate.
 *
 * ## Contrast
 *
 * Two bars, because two kinds of token land in two different places.
 *
 * The ink ladder sits on the page and on panels, so it is measured against
 * `--ground`, `--surface`, `--surface-2` and `--well`.
 *
 * Everything the instruments hand to `--zi-paint` also letters itself on a nine
 * percent wash of itself, in `.zi-chip` and in the trail's gap marker. That is
 * the worst ground any token ever sees, and it is about ten percent of the
 * ratio, so the paint set is tuned to clear 4.5:1 there and not merely on the
 * page. Values that failed either bar were moved in OKLCH with hue and chroma
 * held, so a corrected token is the same colour at a different lightness rather
 * than a different colour. Each one is noted where it is declared.
 *
 * @author Byte <bhumika@bytevion.com>
 */

/* ══════════════════════════════════════════════════════════════════════════
 * Light. The printed plate.
 * ══════════════════════════════════════════════════════════════════════════ */

:root {
  color-scheme: light;

  /*
   * The ground is darker than every surface on it. That inversion is the whole
   * light theme: a panel reads as paper laid on a desk rather than as a hole
   * cut in the page, and it is why nothing here needs a shadow to separate.
   */
  --ground: #e9e9e9;
  --surface: #eeeeee;
  --surface-2: #f5f5f5;
  --well: #dfdfdf;

  /*
   * The ink ladder alternates neutral and indigo. On a #E9E9E9 ground there is
   * not room for four steps of value that all clear 4.5:1, so the fourth step
   * is carried by hue: --ink-3 is a neutral grey and --ink-4 is the same value
   * in indigo. Against warm grey the two read apart cleanly.
   *
   * --ink-3 was #6A6A6A in the contract and measured 4.46:1 on the ground,
   * which is a fail for the 9.5px table headers and feed stamps it paints.
   * --ink-4 was #8E8C99 and measured 2.72:1, which is below even the 3:1 bar
   * for non text. Both were darkened by the least that clears 4.5:1.
   */
  --ink: #2a2a2a;
  --ink-2: #3d3b4f;
  --ink-3: #626262;
  --ink-4: #646176;

  /*
   * Rules are mixed from the indigo ink, never from black. Black hairlines on a
   * warm grey read as dirt; indigo ones read as ruling.
   *
   * These are rules, not marks that carry meaning, so they sit far below any
   * contrast bar on purpose. Anything that has to be *seen* as a boundary uses
   * --hair-3 at 2.18:1, or a real ink.
   */
  --hair: rgba(85, 83, 104, 0.3);
  --hair-2: rgba(85, 83, 104, 0.15);
  --hair-3: rgba(85, 83, 104, 0.5);

  /*
   * The brand orange is one value in both themes and is never adjusted. It is a
   * fill: the hexagon button, the found state, the registration mark. On this
   * ground it is 2.85:1, which is below the 3:1 bar for a graphical object, so
   * a filled signal surface takes a --hair-3 edge to carry its own boundary.
   * Do not solve that by lightening the ground or darkening the brand.
   *
   * --signal-text is the same signal where it has to be read, and it is what
   * --accent points at. It was #B8410A, which cleared the page at 4.55:1 but
   * dropped to 4.15:1 in a well and 4.20:1 on the chip wash. Darkened until the
   * worst case clears.
   *
   * --signal-on was #FFFFFF, which is 3.46:1 on the brand orange and fails any
   * label under 24px. This is a warm near black instead, at 5.48:1. It pairs
   * with --signal only. A fill of --signal-text is dark enough to take white.
   */
  --signal: #ec5b13;
  --signal-text: #b03a00;
  --signal-on: #1a0e05;

  /*
   * The instruments letter 10px chips and 11px graph labels with --accent, so
   * it cannot be the raw brand orange: #EC5B13 is 2.85:1 here and every tone
   * chip on the light plate would be unreadable. Pointing it at the read
   * variant keeps one orange identity and makes the drawings legible.
   */
  --accent: var(--signal-text);

  /*
   * The categorical chart set. Every one of these is lettered somewhere, in the
   * delegation graph's 11px column labels, so all five clear the text bar
   * rather than the 3:1 graphical one.
   *
   * --a-amber was #B45309 at 4.14:1 and --a-cyan was #0E7490 at 4.41:1. Both
   * failed. --a-mint was #047857 at 4.52:1, which passed the page and failed
   * the well, so it moved with them.
   */
  --a-blue: #1d4ed8;
  --a-amber: #a54500;
  --a-violet: #6d28d9;
  --a-cyan: #006b87;
  --a-mint: #007050;

  /*
   * The state set. Five of these become chip text through `data-tone`, so they
   * are tuned against the nine percent self wash.
   *
   * --ok tracks --a-mint by design and moved with it. --warn was #B45309 at
   * 3.86:1 on the chip. --error was #BD2F2F at 4.39:1 on the chip.
   *
   * --urgent is the one paint token left at the contract value, and it is the
   * only token on the light plate that does not clear every bar: 4.82:1 on the
   * page and 5.04:1 on a panel, but 4.43:1 on a wash of itself and 4.39:1 in a
   * well. It is not chipped by the instruments, which reach it only through a
   * fallback --error already satisfies. The correction it needs would land it
   * within one hex step of --signal-text, so the two would be one colour with
   * two names, which is worse than the miss. Use it as a chart hue and as a
   * rule. Do not put it in a chip or a well; use --warn there.
   *
   * --idle is the muted ink by definition, so it is the same token rather than
   * a second grey that can drift out of step with it.
   */
  --ok: #007050;
  --warn: #a54500;
  --urgent: #b43c0b;
  --error: #b92b2c;
  --info: #1d58c5;
  --idle: var(--ink-3);

  /*
   * Pricing only. --t-max was #B45309 at 4.14:1 and carries the word "Max" in
   * type, so it takes the same correction as the amber it shares a hue with.
   * The other three cleared as given.
   */
  --t-scout: #0369a1;
  --t-operator: #6d28d9;
  --t-max: #a54500;
  --t-team: #be185d;

  /*
   * Highlighter sweeps behind an eyebrow, and band tints. Never type, and never
   * a fill under anything smaller than a label. Ink and --ink-2 clear 4.5:1 on
   * all four; --ink-3 does not on every one of them, so text on a wash is --ink
   * or --ink-2.
   */
  --wash-1: #c5ffd6;
  --wash-2: #ffcffe;
  --wash-3: #ecffa3;
  --wash-4: #d1e5ff;

  /*
   * The only elevation on the site, and it exists for one job: an embedded
   * product panel has to read as a window rather than as page furniture.
   * Nothing else gets a shadow, and nothing gets one on hover.
   *
   * The third layer is a hairline ring baked into the shadow rather than a
   * border, so it costs nothing in box sizing and cannot shift a grid by 2px.
   */
  --elev-figure:
    0 1px 2px 0 rgb(0 0 0 / 0.06), 0 8px 20px -6px rgb(0 0 0 / 0.1), 0 0 0 1px var(--hair-2);
}

/* ══════════════════════════════════════════════════════════════════════════
 * Dark. The console at night.
 *
 * Written twice. Read the note at the top of the file before editing either:
 * change one and the build fails on the other, which is the point. The
 * declarations are twins in the same order; the reasoning lives in the first
 * block only, so there is one place to correct when a value moves.
 * ══════════════════════════════════════════════════════════════════════════ */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    /* Near white on near black renders heavy and fringed with subpixel
       antialiasing. Greyscale is the correct rendering for this ground and the
       wrong one for the plate, so it is set per skin and not globally. */
    -webkit-font-smoothing: antialiased;

    --ground: #1a1a1a;
    --surface: #232323;
    --surface-2: #2c2c2c;
    --well: #141414;

    /* Ninety percent rather than solid, so the brightest thing on the page is
       still a step below paper. The alpha composites, which costs about a fifth
       of the ratio on a raised panel and is already accounted for below.

       ink-3 was #8A8A8A and measured 4.03:1 on the chip wash; ink-4 was
       #737373 at 3.31:1 on a surface. The ladder was lifted one step each, so
       ink-4 now holds the value ink-3 used to. */
    --ink: rgba(250, 250, 250, 0.9);
    --ink-2: #a3a3a3;
    --ink-3: #959595;
    --ink-4: #8a8a8a;

    /* Plain white at low alpha. There is no ink tint on this ground: a warm
       hairline on #1A1A1A reads as a smudge rather than as a rule. */
    --hair: rgba(255, 255, 255, 0.12);
    --hair-2: rgba(255, 255, 255, 0.07);
    --hair-3: rgba(255, 255, 255, 0.22);

    --signal: #ec5b13;
    --signal-text: #ff8a5b;
    --signal-on: #1a0e05;
    --accent: var(--signal-text);

    --a-blue: #7aa5ff;
    --a-amber: #f0a95a;
    --a-violet: #b79bff;
    --a-cyan: #5ec8dd;
    --a-mint: #56d3a4;

    --ok: #34d399;
    --warn: #fbbf24;
    --urgent: #fb923c;
    --error: #f87171;
    --info: #60a5fa;
    --idle: var(--ink-3);

    --t-scout: #59b8ea;
    --t-operator: #b79bff;
    --t-max: #f0a95a;
    --t-team: #f07ab0;

    /* Tints of the same hues the light theme paints flat. Fourteen percent is
       the point where the band is visible on #1A1A1A and still lets --ink-2
       clear 4.5:1 on top of it. */
    --wash-1: rgba(86, 211, 164, 0.14);
    --wash-2: rgba(183, 155, 255, 0.14);
    --wash-3: rgba(240, 169, 90, 0.14);
    --wash-4: rgba(122, 165, 255, 0.14);

    /* Higher alphas than the plate. A shadow on a dark ground has less range to
       work with, so the same geometry needs more of it to read at all. */
    --elev-figure:
      0 1px 2px 0 rgb(0 0 0 / 0.2), 0 8px 20px -6px rgb(0 0 0 / 0.34), 0 0 0 1px var(--hair-2);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;
  -webkit-font-smoothing: antialiased;

  --ground: #1a1a1a;
  --surface: #232323;
  --surface-2: #2c2c2c;
  --well: #141414;

  --ink: rgba(250, 250, 250, 0.9);
  --ink-2: #a3a3a3;
  --ink-3: #959595;
  --ink-4: #8a8a8a;

  --hair: rgba(255, 255, 255, 0.12);
  --hair-2: rgba(255, 255, 255, 0.07);
  --hair-3: rgba(255, 255, 255, 0.22);

  --signal: #ec5b13;
  --signal-text: #ff8a5b;
  --signal-on: #1a0e05;
  --accent: var(--signal-text);

  --a-blue: #7aa5ff;
  --a-amber: #f0a95a;
  --a-violet: #b79bff;
  --a-cyan: #5ec8dd;
  --a-mint: #56d3a4;

  --ok: #34d399;
  --warn: #fbbf24;
  --urgent: #fb923c;
  --error: #f87171;
  --info: #60a5fa;
  --idle: var(--ink-3);

  --t-scout: #59b8ea;
  --t-operator: #b79bff;
  --t-max: #f0a95a;
  --t-team: #f07ab0;

  --wash-1: rgba(86, 211, 164, 0.14);
  --wash-2: rgba(183, 155, 255, 0.14);
  --wash-3: rgba(240, 169, 90, 0.14);
  --wash-4: rgba(122, 165, 255, 0.14);

  --elev-figure:
    0 1px 2px 0 rgb(0 0 0 / 0.2), 0 8px 20px -6px rgb(0 0 0 / 0.34), 0 0 0 1px var(--hair-2);
}

/* ══════════════════════════════════════════════════════════════════════════
 * The scale. None of it is a colour, so none of it is written twice.
 * ══════════════════════════════════════════════════════════════════════════ */

:root {
  --wrap: 1600px;
  /*
   * The prose container, retuned from 720px when the body face changed.
   *
   * A measure is a count of characters, not a number of pixels, and the pixel
   * value that produces a given count is a property of the face. 720px was
   * about 75 characters of JetBrains Mono, which is the top of the comfortable
   * band and was the right call while the body was monospaced. The same box in
   * Inter is 94 characters, because Inter's average advance is 0.48em against
   * mono's 0.60em, and 94 is well past the point where the eye stops finding
   * the start of the next line.
   *
   * 640px is the equivalent number for the new face. `.wrap--prose` subtracts
   * two gutters from it, so the text inside lands at 512px, or 67 characters,
   * at the widths where the gutter has opened to 64px.
   *
   * This token is the layout container. The typographic measure on `.prose`
   * itself is set in `ch` in type.css, so it re-derives from the face rather
   * than from a remembered pixel count.
   */
  --wrap-prose: 640px;
  /*
   * Two steps, and no more.
   *
   * A gutter that ramps at every breakpoint makes the page's left edge move
   * under the reader while the grid inside it also moves, and the two changes
   * are impossible to tell apart. But 24px against a 1600px measure is not a
   * margin, it is a bleed: the headline ends up touching the dashed margin rule
   * it is supposed to sit inside. So the gutter holds at 24px through the
   * widths where the page is one column, and opens once at 1280 where it is
   * demonstrably a sheet with margins.
   */
  --gutter: 24px;
  --grid-gap: 16px;

  --sect-sm: 24px;
  --sect-md: 40px;
  --sect-lg: 64px;
  --sect-xl: 80px;

  /*
   * Two registers and no third. State is anything the visitor caused and
   * expects to see finish before they move on. Enter is content arriving, which
   * they did not ask for and should not have to wait for.
   *
   * Beware the prefix collision with --t-scout and the other tier hues, which
   * are colours and not durations. That is the contract's naming and it is kept
   * so seven agents share one vocabulary, but --t-max is a hue.
   */
  --t-state: 180ms;
  --e-state: cubic-bezier(0.4, 0, 0.2, 1);
  --t-enter: 500ms;
  --e-enter: cubic-bezier(0.22, 1, 0.36, 1);
  --t-slow: 700ms;

  /*
   * A third pair, and it earns the exception: a dissolve is not a state change
   * and not an entrance, it is one thing becoming another thing in the same
   * box. The others are both asymmetric, fast to leave and slow to arrive,
   * which is right when something is going or coming and wrong when two halves
   * have to stay in step. Anything that leaves faster than its replacement
   * arrives puts a hole in the middle of the swap.
   *
   * So: symmetric curve, and long enough to be read as a change of material
   * rather than a flicker. Both halves must use this pair, both directions.
   */
  --t-dissolve: 280ms;
  --e-dissolve: cubic-bezier(0.37, 0, 0.34, 1);

  /*
   * A short register, for things that are small enough that the entrance
   * duration reads as a delay: a chip, a list item, an eyebrow.
   */
  --t-quick: 300ms;

  /*
   * The travel curve, and it exists because --e-enter is wrong for a small
   * move. --e-enter has an initial slope of 4.55 and delivers 40% of its travel
   * in the first tenth of its duration. That is right when the tail is the
   * boring part, and on a short gesture there is no boring part: it spent most
   * of its declared duration moving less than half a pixel, which is why six of
   * the site's ten entrance registers used to sit at or below one pixel a frame
   * and read as an object that was already there rather than as motion.
   *
   * easeOutCubic instead. Slope 2.85, 27% in the first tenth, first pixel
   * inside one frame, and over half the frames of the gesture above a pixel.
   */
  --e-travel: cubic-bezier(0.33, 1, 0.68, 1);

  /*
   * The mask curve, and it exists because --e-enter is measurably wrong for a
   * masked reveal. Measured, --e-enter puts 93 percent of its travel inside the
   * first 300ms of a 700ms transition. That is right for a fade, where the tail
   * is the part nobody watches, and wrong for a line rising out of a clip: the
   * legible part of the gesture is the middle, and front loading it throws the
   * whole thing away in two frames. This holds the middle.
   */
  --e-mask: cubic-bezier(0.32, 0, 0.16, 1);

  /*
   * The band's own pair. A section sets `data-accent` once and everything
   * inside it reads --wash and --accent-section without naming a hue, which is
   * what stops one band's colour leaking into the next.
   *
   * The default is the sky pair rather than the brand, on purpose: the orange
   * is the call to action and the found state, and a page where every eyebrow
   * is already orange has nothing left to point with.
   */
  --wash: var(--wash-4);
  --accent-section: var(--a-blue);

  /*
   * Pricing. --tier is the hue as a fill, a rule or a dot; --tier-ink is the
   * same identity where it has to be read. They resolve to one value today
   * because every tier hue clears the text bar in both skins. The pair exists
   * so that a hue which later needs a deeper reading variant has a seam to
   * split on, instead of every price card learning about it.
   *
   * The default is ink, not a tier. A card with no `data-tier` should render in
   * type colour rather than silently claim to be one of the four.
   */
  --tier: var(--ink-2);
  --tier-ink: var(--ink-2);
}

/* Twelve columns, 16 to 24 to 32. The gap is the only spacing on the site that
   ramps, because a twelve column grid at 1600px with a 16px gap reads as one
   block and at 480px with a 32px gap reads as debris. */
@media (min-width: 768px) {
  :root {
    --grid-gap: 24px;
  }
}
@media (min-width: 1280px) {
  :root {
    /* The one gutter step. See the note beside --gutter. */
    --gutter: 64px;
  }
}

@media (min-width: 1024px) {
  :root {
    --grid-gap: 32px;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
 * Per section and per tier switches.
 *
 * Outside the theme blocks on purpose. Each one points at tokens that are
 * already themed, so one declaration serves both skins and neither block has to
 * restate a mapping. It also keeps them out of the build gate's comparison,
 * which reads the theme blocks and nothing else.
 * ══════════════════════════════════════════════════════════════════════════ */

/* The wash and the ink are switched as a pair and always agree on hue. A mint
   sweep over a violet eyebrow is the exact failure this attribute exists to
   make impossible. */
[data-accent="mint"] {
  --wash: var(--wash-1);
  --accent-section: var(--a-mint);
}
[data-accent="pink"] {
  --wash: var(--wash-2);
  --accent-section: var(--a-violet);
}
[data-accent="lime"] {
  --wash: var(--wash-3);
  --accent-section: var(--a-amber);
}
[data-accent="sky"] {
  --wash: var(--wash-4);
  --accent-section: var(--a-blue);
}

[data-tier="scout"] {
  --tier: var(--t-scout);
  --tier-ink: var(--t-scout);
}
[data-tier="operator"] {
  --tier: var(--t-operator);
  --tier-ink: var(--t-operator);
}
[data-tier="max"] {
  --tier: var(--t-max);
  --tier-ink: var(--t-max);
}
[data-tier="team"] {
  --tier: var(--t-team);
  --tier-ink: var(--t-team);
}

/* ==========================================================================
 * styles/type.css, its :root blocks only. The faces and the size ramp.
 * ========================================================================== */

/**
 * @file type.css
 * @description The three families, the scale, and the reset that decides which
 * face carries which register.
 *
 * ## Sans is the prose face, mono is the instrument face
 *
 * `body { font-family: var(--sans) }`. Everything a person wrote in sentences
 * is Inter; everything a machine annotated is JetBrains Mono. The split is the
 * point. A lab notebook is not printed entirely in the plotter's face: the
 * plotter letters the axis, the stamp and the provenance line, and the person
 * writes the paragraph underneath in their own hand. Setting the paragraphs in
 * mono too collapses that distinction, so nothing on the page reads as machine
 * output any more, because everything does.
 *
 * Mono still carries: labels, eyebrows, figure captions, the MEASURED and
 * STAGED provenance rows, table headers, stat readouts, chips, terminal and
 * code blocks, the status strip and the footer column heads. That is most of
 * the site's surface area by element count and almost none of it by word count,
 * which is the correct shape.
 *
 * ### The trap this creates, and how it is handled here
 *
 * Every one of those mono surfaces used to be mono by inheritance from `body`
 * and not by declaration. Flipping the body face silently re-sets all of them
 * in Inter, and the failure is quiet: the text is still there, still the right
 * size, still uppercase, just in the wrong face. So every mono-flavoured rule
 * in this file now names `var(--mono)` explicitly, `output` and `time` are
 * declared here because they are readouts rather than prose, and `.mono` exists
 * as the escape hatch for anything downstream that needs to opt back in. Do not
 * remove a `font-family: var(--mono)` from this file on the grounds that it
 * looks redundant. None of them are.
 *
 * ## There is no bold in marketing prose
 *
 * The heading ladder is separated by size, by face and by negative tracking.
 * Weight only ever moves between 400, 500 and 600 in anything a reader reads in
 * sentences. 800 exists on the display step and nowhere else. `font-weight:
 * 700` on a paragraph is a defect, so the browser defaults that would produce
 * one are overridden below rather than left for six pages to remember.
 *
 * ## Where the font files come from
 *
 * `@font-face` is not here. The woff2 files live at the repo root, shared with
 * the wiki, and `fonts/fonts.css` declares them; Vite injects that link and
 * preloads the four faces that paint above the fold. This file only names the
 * families.
 *
 * @author Byte <bhumika@bytevion.com>
 */

:root {
  /*
   * Fallbacks matter more than usual here. The faces load with `font-display:
   * swap`, so the first paint is whatever is listed second. Inter's fallback
   * leads with ui-sans-serif and system-ui, which are metrically close enough
   * that the swap is a change of face rather than a change of line count.
   *
   * The mono fallback still leads with ui-monospace for the same reason it
   * always did: a proportional fallback under a column of readings reflows the
   * column, and a table whose figures do not line up for the first 200ms reads
   * as a rendering bug rather than as a font swap.
   */
  --display: "Anybody", ui-sans-serif, system-ui, "Segoe UI", sans-serif;
  --mono: "JetBrains Mono", ui-monospace, "Cascadia Mono", "Segoe UI Mono", Consolas, monospace;
  --sans: "Inter", ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;

  /*
   * Eleven steps. The five fluid ones are written `rem + vw` rather than as a
   * bare `vw`, and that is not a stylistic preference.
   *
   * A middle term of pure `vw` ignores the reader's own font size completely,
   * so a visitor who has set their browser to 24px gets a headline at exactly
   * the same pixel size as everyone else. Keeping a rem term in the middle,
   * and both bounds in rem, means the whole ladder still responds to that
   * setting. It also removes the dead zones: a pure-vw clamp tuned to reach its
   * cap at desktop sits pinned to its floor across every phone and tablet
   * width, so the type does not move at all through the half of the range where
   * the layout changes most.
   *
   * The six reading steps are fixed, because a label that changes size with the
   * viewport stops being a label: the eye reads the eyebrow, the figure number
   * and the axis tick as one register, and that only holds if they are one size
   * everywhere.
   *
   * ### What the scale is tuned to
   *
   *              320    768   1280   1440   1920+     was (max)
   *   display   38.6   47.9   58.5   61.8    72        128
   *   h2        25.3   29.4   34.2   35.5    40         48
   *   h3        19.0   20.7   23.0   23.8    26         30
   *   sub       18.0   18.3   20.0   20.4    22         24
   *   lead      17.0   17.5   18.1   18.4    19         20
   *   prose     16     16     16     16      16         16
   *
   * The compression is a taper, not a uniform shrink: 0.56x at the display
   * step, 0.83 at h2, 0.87 at h3, 0.92 at sub, 0.95 at lead, and 1.0 at every
   * reading and label size. Nothing anybody reads a sentence in moved at all.
   * A 128px headline was a poster, and a poster has one thing on it; the page
   * has a headline and a first paragraph that have to be taken in together, and
   * at 72px they are. The three caps all engage at about 1930px, so the whole
   * ladder stops growing at one width instead of three, and 2560 renders as a
   * wider page rather than as a larger one.
   */
  --f-display: clamp(2.25rem, 2rem + 2.07vw, 4.5rem);
  --f-h2: clamp(1.5rem, 1.4rem + 0.91vw, 2.5rem);
  --f-h3: clamp(1.1875rem, 1.08rem + 0.45vw, 1.625rem);
  --f-sub: clamp(1.125rem, 0.99rem + 0.32vw, 1.375rem);
  --f-lead: clamp(1.0625rem, 1.03rem + 0.13vw, 1.1875rem);
  --f-prose: 1rem;

  /*
   * One value, at every width. This step used to be 14px below 768 and 16px
   * above it, and the reason given was mono density: 14px was the right number
   * of monospaced characters for a phone column. The body is not monospaced any
   * more, so the justification is gone and what is left is 14px body copy on a
   * phone, which is under the floor. Inter at 16px fits about 42 characters in
   * a 320px column where the old mono at 14px fit 34, so this is a better
   * measure and a larger face at the same time.
   */
  --f-body: 1rem;

  /*
   * The mono density step, which is what `--f-body` used to be doing for the
   * terminal and code blocks. Split out under its own name so that raising body
   * copy for readability does not widen a fixed-pitch block that was sized to
   * fit a command line.
   */
  --f-code: 0.875rem;

  --f-label: 0.875rem;
  --f-label-sm: 0.75rem;
  --f-micro: 0.625rem;
}

@media (min-width: 768px) {
  :root {
    /* A desk has room for the full command line; a phone does not. This is the
       only step in the scale that moves at a breakpoint. */
    --f-code: 1rem;
  }
}
