/*! FivePaths microsite base, release 3.6.1
    https://cdn.fivepaths.com/microsite/v3/base-3.6.1.css

    A structural framework for small sites. One shape, many brands: the
    components, the spacing and type roles, the dark-band machinery and the
    accessibility floor are shared; the palette and the typeface are tokens a
    site replaces. The FivePaths colours below are the default, not the system.

      <link rel="preconnect" href="https://cdn.fivepaths.com" crossorigin>
      <link rel="preload" as="font" type="font/woff2" crossorigin
            href="https://cdn.fivepaths.com/microsite/v3/fonts/overpass-latin-wght-normal-2.woff2">
      <link rel="stylesheet" href="https://cdn.fivepaths.com/microsite/v3/base-3.6.1.css">
      <link rel="stylesheet" href="/assets/site.css">

    ── WHAT V3 CHANGES ───────────────────────────────────────────────────────
    v2 styled about sixty classes. v3 styles elements and their position, and
    keeps five. `<div class="card-grid">` told CSS three things and a screen
    reader nothing; `<ul data-list="cards">` tells CSS the same three and
    announces "list, 3 items". Every hook removed here was removed by reaching
    for the element that already meant it, so the markup is shorter and more
    informative at once.

    The trade is real and worth stating: structural selectors are brittle
    under hand editing. Wrap a heading in a div and `header > h2 + p` stops
    matching, with no error. This sheet is for generated markup, where the
    component set is closed and a checker fails a build whose structure does
    not match. A hand-authored site should link v2, which is unchanged and
    stays live.

    MARKUP.md in this repo is the full element-by-element contract. The short
    version:

      section                    a band. data-band="alt|dark|hero|proof|cta"
      section > header           the section head. p:first-child is an eyebrow
                                 when a heading follows; the p after a heading
                                 is the lead
      ul                         a tick list. data-list picks another shape:
                                 cards, logos
      ol                         a numbered list. data-list="steps" for the grid
      [data-layout]              split, feature, shots
      [data-actions]             a button group. The first link is the primary
      figure                     a screenshot, a table, a quotation, a listing,
                                 told apart by what is inside them
      details                    a disclosure
      aside                      a callout. data-severity carries the level
      small                      fine print

    The five classes that remain name something no element or position
    implies: ghost, ink and teal on an action link, tld inside a wordmark, and
    visually-hidden, which is a utility with no element of its own.

    Four more exist and are not design decisions: c, k, m and p on the spans
    inside a syntax-highlighted listing. Those are data about the code, and
    only a highlighter emitting different elements would remove them.

    ── WHAT 3.6.1 CHANGES ────────────────────────────────────────────────────
    The colour-scheme toggle shows its icon alone. The "Dark theme" and
    "Light theme" text stays in the markup, clipped the way visually-hidden
    clips, so it is still the button's accessible name and a screen reader
    hears the scheme on offer. With no text to widen it the button is a 44px
    square, so the target is no smaller. The text no longer takes
    --fp-label-lean, since there is no visible label to centre.

    Content after a component grid has room. Cards, steps, split panels,
    shots and the filter form carry no margin, and a section is a grid, so
    nothing collapses into the gap: a paragraph after a row of cards sat
    against its last row. Anything but a heading or a button group after one
    of them now starts 2rem below it.

    ── WHAT 3.6.0 ADDS ───────────────────────────────────────────────────────
    Forms and a pager, from the first data application on the sheet, whose
    filter form and page links were the largest part of its own stylesheet.

    form[data-layout="stack"] and form[data-layout="filters"] style the
    labels, fields, selects, checkboxes, radios and fieldsets inside them,
    and their errors. A form without data-layout is left as it was, because
    styling every input would resize forms on sites already live; that is a
    v4 change.

    An error is written, and --fp-critical repeats it: a thicker border on
    the control, a rule beside its message, and a critical callout listing
    them. The critical callout borrowed --fp-route-red until now, a data
    colour that is 2.3:1 on the dark grounds; it takes --fp-critical too.

    The .tld in a wordmark takes --fp-ink-soft. In the translucent header,
    scrolled over a dark band, --fp-ink-mute fell to 6.3:1.

    A pager needs nothing new: it is a chip list in a nav. What it lacked was
    a mark for the page in view, and aria-current on a chip now draws one in
    weight and ink rather than colour.

    ── WHAT 3.5.0 FIXES AND ADDS ─────────────────────────────────────────────
    Three things the first data application on the sheet found.

    A link on the warning wash measured 6.97:1, under the floor. --fp-accent
    was measured against the neutral grounds and nobody had paired it with
    --fp-amber-wash. It now takes --fp-accent-strong there, at 8.87, and the
    manifest measures everything a callout puts on the wash. Measuring it
    found one more: in a dark band the callout's label, in --fp-ink-mute, sat
    at 6.98. The dark wash moves one step, #241a00 to #231900, and clears.

    Nine spacing characters rendered with no width: the middle dot, the grave
    accent, and the spacing acute, diaeresis, cedilla, macron, circumflex,
    ring and tilde. Upstream Overpass files them in GDEF as combining marks,
    and a shaper gives a mark no advance of its own and attaches it to the
    letter before it, so "a·b" drew as "ab" and "a´b" as "áb". The face ships
    as overpass-latin-wght-normal-2.woff2 with those nine as base glyphs and
    out of the mark-attachment lookups. No other table changed. The old file
    stays, since the earlier releases name it. A site that preloads the font
    moves its preload with its pin, or the preload fetches a file nothing uses.

    a[data-target] marks a standalone link for the 44px target. The existing
    rule guesses from structure, and `:only-child` counts elements, not text,
    so a paragraph whose only element is a link in a sentence matched and its
    line grew to 44px. Generated markup knows which links stand alone. The
    guess stays as the fallback in 3.x because dropping it would move pages.

    ── WHAT 3.4.2 FIXES ──────────────────────────────────────────────────────
    Every footer link was an inline-flex box at least 44px tall, so it would
    meet the target size on its own. That is right for the byline and the
    version nav, where each link stands alone. It was wrong for a link in the
    fine print, where a 44px box in a sentence stretched every line it sat
    on to 44px and left the paragraph looking double-spaced. A link inside a
    sentence is the case the target-size criterion exempts, as the a11y layer
    already says of main. Fine-print links are now inline, keep the
    line-height of the text around them, and carry their underline at rest
    the way a body link does.

    ── WHAT 3.4.1 FIXES ──────────────────────────────────────────────────────
    3.4.0 made the button padding symmetric and let the flex box centre the
    label. Flex centres the line box, and the ink of a label does not sit in
    the middle of its line box. Overpass declares an ascender of 0.883em and
    a descender of 0.383em around a cap height of 0.70em, so the centre of a
    capital sits (0.383 - 0.883) / 2 + 0.70 / 2 = 0.10em above the centre of
    the line box, at any line-height. Browsers round the ascent and descent
    to whole pixels, and the offset as rendered at the sheet's sizes is 0.11
    to 0.13em: 1.8px in a button at 15.68px, 1.9px in a header pill at
    14.4px, 2.0px in the theme toggle at 17px. Overpass Mono has the same
    metrics and lining figures at cap height. The old 0.13em was that
    metric, not a guess, and removing it raised every label by 2px.

    The lean is back as a token, --fp-label-lean, so a site that swaps
    --fp-sans sets it with the face: (descent - ascent) / 2 + capHeight / 2,
    in em, from the font's hhea and OS/2 tables or from measureText in a
    console. It is applied in one shape wherever a label is centred in a
    padded box: buttons, header links and pills, chips, and the step and list
    numerals. The theme toggle is unpadded, so its text alone takes the lean.
    Top and bottom padding sum to what they did, so no box changes size.

    ── WHAT 3.4.0 FIXES ──────────────────────────────────────────────────────
    Three things that looked right in the source and were not.

    Shadows exist. --fp-shadow and --fp-shadow-lift wrapped light-dark() around
    a whole shadow, and that function takes colours: the value parsed inside the
    custom property and then failed wherever it was used, so every shadow in the
    framework computed to `none` in both schemes. Cards, framed images and lifted
    panels have been flat since light-dark() came in at 3.0.0. The contract test
    now refuses a light-dark() arm that starts with a number.

    A button centres its label by layout, not by a constant measured off
    Overpass. The old padding leaned 0.13em down the box — right for one
    typeface, wrong for every other, and wrong for Overpass itself on any page
    where the font failed to load. There were three copies of that nudge.

    A step counter likewise: place-items already centres a digit.

    ── WHAT 3.3.0 ADDED ──────────────────────────────────────────────────────
    A logo row runs full bleed and finds its own inset, because a band of
    marks is a band and a band goes to the edges.

    ── WHAT 3.2.0 ADDED ───────────────────────────────────────────────────────
    Logos get per-mark optical sizing, because marks nobody drew together have
    nothing in common but their height, and capping them all at one number
    makes a tall square mark tower over a wide wordmark. Each item carries its
    own --logo-h. The row spreads rather than packs, and centres when there is
    no room to spread.

    ul[data-list="chips"], for a list of short identifiers: module names, a
    jump list, a set of tags. Two sites had written it before it moved here.

    [data-layout="split"][data-frame="none"], two balanced columns with no
    panel chrome, for when the halves are one thought rather than a choice.

    A feature row with a single column now spans, instead of sitting in the
    first track with nothing beside it.

    ── WHAT 3.1.0 ADDED ──────────────────────────────────────────────────────
    [data-layout="hero"], the column pair an opening band wants when it
    carries a picture of the product beside its heading.

    ── COLOUR ────────────────────────────────────────────────────────────────
    Two roles, and the default palette gives each one hue.
      fill    a background with dark ink on it. Buttons, chips, highlights.
              Never a text colour on a light ground.
      accent  every text accent: links, eyebrows, list markers, step numbers,
              focus rings.
    And one for state, not brand:
      critical  an error, and a critical callout's rule. Never alone: the
                words carry the state and the colour repeats it.
    Neutral inks and papers do everything else. Multicolour appears only as
    small data blips (--fp-route-*), never as chrome.

    Every text and background pairing defined here clears WCAG AAA (7:1),
    measured against the least favourable ground the token can land on.
    tokens.json beside this file carries the pairings and the repo's test
    measures them. A tint or wash is color-mix() against a token, never a new
    literal, so it follows a site's override and the dark-band remap.

    ── LIGHT / DARK ──────────────────────────────────────────────────────────
    One declaration per token, via light-dark(). v2 wrote the dark palette
    three times: a media query, a pinned [data-theme="dark"] block, and the
    board tokens. Every new token needed four coordinated edits and one of
    them was always forgotten. Here the scheme is chosen by color-scheme, and
    a site pins itself with <html data-theme="light"> or "dark".

    ── DARK BANDS ARE A TOKEN REMAP ──────────────────────────────────────────
    A section with data-band="dark", or hero, proof or cta, re-points the
    neutral tokens at the near-black board palette. Every component below is
    written purely in tokens, so each adapts on its own and there is no
    parallel set of dark rules. Corollary: never hardcode a colour.

    ── LAYERS ────────────────────────────────────────────────────────────────
    tokens, base, layout, components, a11y. v2 depended on source order and
    said so in a comment ("these rules sit at the end of the sheet"). Now it
    is declared. A site's own sheet is unlayered, so it always wins.
*/

@layer tokens, base, layout, components, a11y;

/* ── Type: Overpass / Overpass Mono (SIL OFL 1.1), variable, self-hosted ─── */

@font-face {
  font-family: "Overpass";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("fonts/overpass-latin-wght-normal-2.woff2") format("woff2-variations");
}
@font-face {
  font-family: "Overpass Mono";
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url("fonts/overpass-mono-latin-wght-normal.woff2") format("woff2-variations");
}

@layer tokens {
  :root {
    /* light dark: follow the reader's OS. A site pins itself with data-theme,
       which sets this to one keyword and freezes every light-dark() below. */
    color-scheme: light dark;

    /* ── Neutrals ─────────────────────────────────────────────────────── */
    --fp-bg: light-dark(#ffffff, #10141c);
    --fp-bg-alt: light-dark(#f8f9fb, #161b25);
    --fp-bg-card: light-dark(#ffffff, #1b212c);
    --fp-bg-blur: light-dark(rgb(255 255 255 / 0.92), rgb(16 20 28 / 0.92));
    --fp-ink: light-dark(#141821, #edf0f6);
    --fp-ink-soft: light-dark(#3d4350, #b9c0ce);
    --fp-ink-mute: light-dark(#505562, #a4abbb);
    --fp-rule: light-dark(#e5e7eb, #272e3c);
    /* light-dark() takes colours, not value lists. Wrapped around a whole
       shadow it parses inside the custom property — a custom property holds any
       token stream — and then fails when the property that uses it is parsed,
       which makes box-shadow compute to `none`. Every shadow in the framework
       was dead in both schemes, silently, and the contract test could not see
       it because it measures colours and a shadow is geometry. So the function
       goes where a colour goes, and the geometry sits outside it. One blur for
       both schemes: it was 14/40 against 18/44, a difference nobody can see. */
    --fp-shadow: 0 1px 2px light-dark(rgb(20 24 33 / 0.05), rgb(0 0 0 / 0.4));
    --fp-shadow-lift: 0 16px 42px light-dark(rgb(20 24 33 / 0.10), rgb(0 0 0 / 0.4));

    /* ── Accents ──────────────────────────────────────────────────────── */
    --fp-accent: light-dark(#006060, #4dd2d2);
    --fp-accent-strong: light-dark(#004f4f, #7fdede);
    --fp-bronze: light-dark(#6f5000, #ffc94d);         /* the fill family as text */
    --fp-bronze-strong: light-dark(#5a4100, #ffdd8a);
    --fp-amber-wash: light-dark(#fff8e4, #231900);
    /* an error, and a critical callout's rule. Never on its own: the words
       say it too. Over 7:1 on every ground, so it holds even as text. */
    --fp-critical: light-dark(#9b1c1c, #ff8a80);
    /* --fp-link and --fp-link-hover are read as fallbacks and never declared.
       Leaving them unset defers the lookup to the link itself, so a link in a
       dark band picks up that band's accent. Setting one on :root pins links
       to a single colour everywhere, because a custom property containing
       var() is substituted where it is declared and inherited flat. Setting
       one on a section is the intended way to override deliberately. */

    /* ── Scheme-independent ───────────────────────────────────────────── */
    /* the fill reads the same either way: it is always a ground with dark ink */
    --fp-amber: #ffb300;
    --fp-amber-hover: #ffc94d;
    --fp-amber-ink: #1a1200;
    --fp-teal-solid: #006060;      /* the accent as a fill, with white ink */

    /* the board: near-black grounds for heroes, closing bands, footers, code */
    --fp-board: #10141c;
    --fp-board-2: #161b25;
    --fp-board-deep: #0b0f16;
    --fp-board-line: #272e3c;
    --fp-board-glow: #1e2431;
    --fp-board-ink: #edf0f6;
    --fp-board-soft: #d6dbe7;
    --fp-board-dim: #9fa5b7;
    --fp-board-accent: #4dd2d2;
    --fp-board-accent-2: #7fdede;

    /* data blips: route badges, series keys. Never chrome. */
    --fp-route-blue: #0d5b98;
    --fp-route-green: #006640;
    --fp-route-red: #a72925;
    --fp-route-orange: #f5851f;
    --fp-route-purple: #684b8b;
    --fp-route-ink: #ffffff;       /* white clears 7:1 on every blip but orange */
    --fp-route-ink-dark: #221200;  /* which takes dark ink, 7.2:1 */

    /* code listings, on the board ground */
    --fp-code-comment: #a0a5b4;
    --fp-code-path: #a7d8b6;
    --fp-code-key: #a9bfe0;

    /* ── Type scale ───────────────────────────────────────────────────── */
    --fp-sans: "Overpass", system-ui, -apple-system, "Segoe UI", Roboto,
               "Helvetica Neue", Arial, sans-serif;
    --fp-mono: "Overpass Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo,
               Consolas, "Liberation Mono", monospace;
    /* How far a label's cap centre sits above its line-box centre, in em:
       (descent - ascent) / 2 + capHeight / 2 for the face. Every padded label
       leans down by it. Overpass gives 0.10em from its tables and 0.12em as
       browsers render it; a face whose ascent and descent are symmetric about
       the cap height takes 0. Set it with --fp-sans. */
    --fp-label-lean: 0.12em;
    --fp-text: 1.0625rem;
    --fp-leading: 1.65;
    --fp-h1: clamp(2rem, 5.5vw, 3.25rem);
    --fp-h2: clamp(1.5rem, 3.5vw, 2.125rem);
    --fp-h3: 1.2rem;
    --fp-h4: 1.02rem;
    --fp-lead: clamp(1.1rem, 2.2vw, 1.3rem);
    --fp-tracking-h: -0.015em;
    --fp-weight-h1: 800;
    --fp-weight-h2: 750;
    --fp-weight-h: 700;
    --fp-weight-ui: 650;
    --fp-weight-nav: 550;

    /* WCAG 1.4.8 caps running text at 80 characters. In ch, so it tracks the
       face: v2 hard-coded 29em, derived from Overpass's average character
       width, which stopped being true the moment a site changed --fp-sans.

       52ch is measured, not assumed. The zero glyph is wider than an average
       lowercase letter, so a ch value buys more characters than it names: at
       this size a line of lowercase prose holds about 73, which is inside the
       cap and inside the 45-to-75 range that reads best. The repo's test
       measures it rather than trusting this comment, because the first value
       tried here named 68ch and held 88 characters. */
    --fp-measure: 52ch;

    /* ── Spacing and layout ───────────────────────────────────────────── */
    --fp-space-section: 4.5rem;
    --fp-space-section-tight: 3rem;
    --fp-space-head: 2.5rem;
    --fp-gap: 1.25rem;
    --fp-gap-wide: 3rem;
    --fp-pad-card: 1.5rem;
    --fp-pad-panel: 2rem;
    --fp-wrap: 70rem;              /* the content track */
    --fp-wrap-wide: 90rem;         /* the wide track, for figures */
    --fp-gutter: 1.5rem;
    --fp-radius: 12px;
    --fp-radius-sm: 8px;
    --fp-radius-xs: 6px;
    --fp-pill: 999px;
    --fp-target: 44px;             /* WCAG 2.5.5 */
    --fp-header-clear: 6.5rem;
    --fp-sticky-top: 5.5rem;       /* a sticky sub-element clears the header */
  }

  /* Pinning. One keyword decides every light-dark() above. */
  :root[data-theme="light"] { color-scheme: light; }
  :root[data-theme="dark"] { color-scheme: dark; }

  /* Dark grounds: the neutral tokens point at the board palette. Custom
     properties resolve after the cascade, so every component rule picks up the
     remapped values with no dark-specific rule anywhere. */
  :where([data-band="dark"], [data-band="hero"], [data-band="proof"], [data-band="cta"], body > footer) {
    color-scheme: dark;
    /* The remap alone is not enough. `color` inherits a computed value, so
       text inside a dark band keeps the ink body resolved from the light
       palette unless something re-reads the token here. Headings re-read it
       themselves (see the base layer), which is why they need no rule of
       their own per band. */
    color: var(--fp-ink-soft);
    --fp-bg: var(--fp-board);
    --fp-bg-alt: var(--fp-board-2);
    --fp-bg-card: var(--fp-board-2);
    --fp-bg-blur: rgb(16 20 28 / 0.92);
    --fp-ink: var(--fp-board-ink);
    --fp-ink-soft: var(--fp-board-soft);
    --fp-ink-mute: var(--fp-board-dim);
    --fp-rule: var(--fp-board-line);
    --fp-shadow: 0 1px 2px rgb(0 0 0 / 0.4);
    --fp-shadow-lift: 0 16px 42px rgb(0 0 0 / 0.4);
    --fp-accent: var(--fp-board-accent);
    --fp-accent-strong: var(--fp-board-accent-2);
    --fp-bronze: var(--fp-amber);
    --fp-bronze-strong: var(--fp-amber-hover);
    --fp-amber-wash: #231900;
    --fp-critical: #ff8a80;
  }
}

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

  html {
    scroll-behavior: smooth;
    scroll-padding-top: var(--fp-header-clear);
    -webkit-text-size-adjust: 100%;
  }

  body {
    margin: 0;
    background: var(--fp-bg);
    color: var(--fp-ink);
    font-family: var(--fp-sans);
    font-size: var(--fp-text);
    line-height: var(--fp-leading);
    -webkit-font-smoothing: antialiased;
  }

  /* height:auto is not optional. Generated markup carries width and height on
     every image to stop the page reflowing as each one loads, and max-width
     alone then stretches them vertically. v2 lacked this and distorted every
     image outside a figure. */
  img, svg, video, canvas { max-width: 100%; height: auto; }

  a { color: var(--fp-link, var(--fp-accent)); }
  a:hover { color: var(--fp-link-hover, var(--fp-accent-strong)); }

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

  /* Headings read the ink token at the point of use rather than inheriting a
     computed colour, so a dark band recolours them by remapping the token and
     needs no heading rule of its own. */
  h1, h2, h3, h4 {
    margin: 0 0 0.5em;
    color: var(--fp-ink);
    line-height: 1.15;
    letter-spacing: var(--fp-tracking-h);
    text-wrap: balance;
  }
  h1 { font-size: var(--fp-h1); font-weight: var(--fp-weight-h1); }
  h2 { font-size: var(--fp-h2); font-weight: var(--fp-weight-h2); }
  h3 { font-size: var(--fp-h3); font-weight: var(--fp-weight-h); }
  h4 { font-size: var(--fp-h4); font-weight: var(--fp-weight-h); }

  p { margin: 0 0 1em; text-wrap: pretty; }
  p:last-child { margin-bottom: 0; }

  code, kbd, samp, pre { font-family: var(--fp-mono); }

  /* fine print. v2 painted this with .fine and .fineprint; the element for it
     already existed. */
  small { font-size: 0.9rem; color: var(--fp-ink-mute); }

  strong { color: var(--fp-ink); }

  hr { border: 0; border-top: 1px solid var(--fp-rule); margin: 2rem 0; }

  @media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    *, *::before, *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
    }
  }
}

@layer layout {
  /* ── The content column ────────────────────────────────────────────────
     Two mechanisms, because chrome and content need different things.

     A section is a grid with a centred content track and wider tracks either
     side, so a child sits in `content` by default and anything needing more
     room asks for `wide` or `full` by name. That one mechanism replaces v2's
     .wrap, .wrap-wide and .shot-bleed, and it removes the wrapper div that
     used to sit inside every section. A full-bleed figure in v2 had to break
     out with a negative margin and a 100vw width that overflowed by the
     scrollbar's width; a named grid line has neither problem.

     The header and footer have no full-bleed children, so they take the
     simpler form: padding that collapses to the gutter on a narrow screen and
     grows to centre a capped column on a wide one. That leaves the header
     free to be a flex row, which is what a brand beside a nav wants. */
  main > section {
    display: grid;
    /* The content track is sized with min() rather than minmax(0, ...) so it
       resolves before the tracks around it. With two flexible neighbours the
       grid algorithm shared the shortfall between them instead, and on a
       narrow viewport the breakout tracks took 90px each from the content
       column: the hero indented further than the header above it. */
    grid-template-columns:
      [full-start] minmax(var(--fp-gutter), 1fr)
      [wide-start] minmax(0, calc((var(--fp-wrap-wide) - var(--fp-wrap)) / 2))
      [content-start] min(100% - var(--fp-gutter) * 2, var(--fp-wrap)) [content-end]
      minmax(0, calc((var(--fp-wrap-wide) - var(--fp-wrap)) / 2)) [wide-end]
      minmax(var(--fp-gutter), 1fr) [full-end];
  }
  main > section > * { grid-column: content; }
  main > section > [data-width="wide"] { grid-column: wide; }
  main > section > [data-width="full"] { grid-column: full; }

  body > :is(header, footer) {
    padding-inline: max(var(--fp-gutter), (100% - var(--fp-wrap)) / 2);
  }

  /* ── Bands ────────────────────────────────────────────────────────────── */
  main > section { padding-block: var(--fp-space-section); }
  /* the head's gap to the content below it, the one place a section needs one */
  main > section > header { margin-bottom: var(--fp-space-head); }
  /* a heading that follows content starts a new group and needs air above it.
     A section carrying several components reads as one run without this. */
  main > section > * + :is(h2, h3) { margin-top: 2.5rem; }
  /* The component grids carry no margin, and a section is a grid, so nothing
     collapses into the gap: a paragraph after a row of cards sat on its last
     row. The same holds inside a details or a panel. Headings have the rule
     above; a button group has its own. Feature
     rows are left out, since they pad themselves and stack edge to edge. */
  main :is(ul[data-list="cards"], ol[data-list="steps"], [data-layout="split"], [data-layout="shots"], form[data-layout="filters"])
    + :not(h2, h3, [data-actions]) {
    margin-top: 2rem;
  }
  main > section[data-tight] { padding-block: var(--fp-space-section-tight); }
  main > section[data-band="alt"] { background: var(--fp-bg-alt); }
  main > section[data-band="dark"] { background: var(--fp-board); }

  /* a rule-bounded strip of short reassurances */
  main > section[data-band="strip"] {
    padding-block: 1.1rem;
    border-block: 1px solid var(--fp-rule);
  }

  /* ── Hero ─────────────────────────────────────────────────────────────── */
  main > section[data-band="hero"] {
    padding-block: 5rem 5.5rem;
    overflow: hidden;
    background:
      radial-gradient(1200px 500px at 75% -10%, var(--fp-board-glow) 0%, transparent 60%),
      var(--fp-board);
  }
  main > section[data-band="hero"] h1 { max-width: 16em; }
  main > section[data-band="hero"][data-tight] { padding-block: 4rem; }
  main > section[data-band="hero"][data-tight] h1 { max-width: 18em; }

  /* ── Closing band ─────────────────────────────────────────────────────── */
  main > section[data-band="cta"] {
    padding-block: var(--fp-space-section);
    text-align: center;
    background:
      radial-gradient(900px 400px at 20% 120%, var(--fp-board-glow) 0%, transparent 60%),
      var(--fp-board);
  }
  main > section[data-band="cta"] > * { max-width: 40rem; margin-inline: auto; }
  main > section[data-band="cta"] p { margin-inline: auto; margin-bottom: 1.8rem; }
  main > section[data-band="cta"] small { display: block; margin-top: 1.6rem; }

  /* ── Proof strip ──────────────────────────────────────────────────────
     One row rather than a stack, so it takes the chrome layout: no child of
     a proof strip runs full bleed, and a flex row is what a claim beside a
     badge wants. */
  main > section[data-band="proof"] {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 1.5rem;
    padding-block: 2.4rem;
    padding-inline: max(var(--fp-gutter), (100% - var(--fp-wrap)) / 2);
    background: var(--fp-board);
  }
  main > section[data-band="proof"] > p { margin: 0; font-size: 1.12rem; }

  /* ── The section head ─────────────────────────────────────────────────
     header > p:first-child is an eyebrow only when a heading follows it,
     which is what tells it apart from a lone paragraph of standfirst. */
  main > section > header { max-width: 46em; }
  main > section > header > :last-child { margin-bottom: 0; }
  main > section > header > p:first-child:has(+ :is(h1, h2, h3)) {
    display: inline-block;
    margin-bottom: 0.6rem;
    color: var(--fp-accent);
    font-weight: var(--fp-weight-h);
    font-size: 0.78rem;
    letter-spacing: 0.09em;
    text-transform: uppercase;
  }
  main > section > header > :is(h1, h2, h3) + p {
    font-size: var(--fp-lead);
    color: var(--fp-ink-soft);
    max-width: var(--fp-measure);
  }
  main > section > header > p:not(:first-child):not(:is(h1, h2, h3) + p) {
    color: var(--fp-ink-soft);
    font-size: 1.1rem;
  }

  /* A lead directly under a hero h1 is the same idea without a header. */
  main > section[data-band="hero"] > h1 + p,
  main > section[data-band="cta"] > h2 + p {
    font-size: var(--fp-lead);
    color: var(--fp-ink-soft);
    max-width: var(--fp-measure);
  }
  main > section[data-band="cta"] > h2 + p { margin-inline: auto; }
}

@layer components {
  /* ── Action groups ────────────────────────────────────────────────────
     The first link in the group is the page's primary ask and renders as the
     fill. A second primary now needs a second group, which is visible in the
     markup and in review, so the design guide's "one primary per view" is
     something the stylesheet enforces rather than advises. */
  [data-actions] {
    display: flex;
    align-items: center;
    gap: 0.9rem;
    flex-wrap: wrap;
    margin-top: 1.8rem;
  }
  main > section[data-band="cta"] [data-actions] { justify-content: center; margin-top: 0; }

  [data-actions] > a,
  button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: var(--fp-target);
    /* The flex box centres the line box, and the cap centre of the label sits
       --fp-label-lean above that (the 3.4.1 note in the header has the
       numbers), so the padding leans the label down by the same amount. Top
       and bottom still sum to 1.4rem: the button is the height it was. */
    padding-block: calc(0.7rem + var(--fp-label-lean)) calc(0.7rem - var(--fp-label-lean));
    padding-inline: 1.3rem;
    border: 1.5px solid transparent;
    border-radius: var(--fp-pill);
    font-family: inherit;
    font-size: 0.98rem;
    font-weight: var(--fp-weight-ui);
    line-height: 1.25;
    text-align: center;
    text-decoration: none;
    cursor: pointer;
    transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
    background: var(--fp-amber);
    color: var(--fp-amber-ink);
  }
  [data-actions] > a:hover,
  button:hover {
    background: var(--fp-amber-hover);
    color: var(--fp-amber-ink);
    text-decoration: underline;
    text-underline-offset: 0.2em;
  }

  /* Everything after the first is secondary, so it does not spend the fill. */
  [data-actions] > a:not(:first-child) {
    background: transparent;
    border-color: var(--fp-ink-soft);
    color: var(--fp-ink);
  }
  [data-actions] > a:not(:first-child):hover {
    background: var(--fp-bg-alt);
    border-color: var(--fp-ink);
    color: var(--fp-ink);
  }

  /* Three named variants, because a secondary ask is a choice with no
     structural signal. They are the only classes on an action. */
  [data-actions] > a.ghost { background: transparent; border-color: var(--fp-ink-soft); color: var(--fp-ink); }
  [data-actions] > a.ghost:hover { background: var(--fp-bg-alt); border-color: var(--fp-ink); color: var(--fp-ink); }
  [data-actions] > a.ink, a.ink { background: var(--fp-ink); border-color: transparent; color: var(--fp-bg); }
  [data-actions] > a.ink:hover, a.ink:hover { background: var(--fp-ink-soft); color: var(--fp-bg); }
  [data-actions] > a.teal { background: var(--fp-teal-solid); border-color: transparent; color: #ffffff; }
  [data-actions] > a.teal:hover { background: var(--fp-accent-strong); color: #ffffff; }

  /* ── Lists ────────────────────────────────────────────────────────────
     A bullet list is the system's list: a drawn metro-stop dot, not a glyph,
     so nothing falls back to a box. An ordered list numbers in a ring. Both
     stay in flow; data-list picks a shape that needs a grid. */
  main :where(ul, ol):not([data-list]) {
    display: grid;
    gap: 0.6rem;
    list-style: none;
    margin: 0 0 1em;
    padding: 0;
  }
  main :where(ul, ol):not([data-list]) > li {
    position: relative;
    padding-left: 1.7rem;
    color: var(--fp-ink-soft);
  }
  main ul:not([data-list]) > li::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0.42em;
    width: 0.7rem;
    height: 0.7rem;
    border-radius: 50%;
    border: 3.5px solid var(--fp-accent);
    background: var(--fp-bg);
  }
  /* "verified" rather than "a stop on a line": a drawn check. */
  main ul[data-marker="check"] > li { padding-left: 1.6rem; }
  main ul[data-marker="check"] > li::before {
    top: 0.38em;
    width: 0.85rem;
    height: 0.42rem;
    border: 0 solid var(--fp-accent);
    border-left-width: 3.5px;
    border-bottom-width: 3.5px;
    border-radius: 0;
    background: none;
    transform: rotate(-45deg);
  }
  main ul:not([data-list])[data-cols="3"] { gap: 1rem 2.5rem; }

  main ol:not([data-list]) { counter-reset: step; }
  main ol:not([data-list]) > li { counter-increment: step; padding-left: 2.6rem; }
  main ol:not([data-list]) > li::before {
    content: counter(step);
    position: absolute;
    left: 0;
    top: 0.1em;
    display: inline-grid;
    place-items: center;
    box-sizing: border-box;
    min-width: 1.9rem;
    height: 1.9rem;
    /* place-items centres the line box and the digit's ink sits
       --fp-label-lean above it, so the padding leans by that amount. The
       ring's fixed height absorbs the 0.3em base, which is there only so the
       lean has something to subtract from. The 0.5em is for a second digit. */
    padding-inline: 0.5em;
    padding-block: calc(0.3em + var(--fp-label-lean)) calc(0.3em - var(--fp-label-lean));
    border: 2.5px solid var(--fp-accent);
    border-radius: var(--fp-pill);
    font-family: var(--fp-mono);
    font-weight: var(--fp-weight-h);
    font-size: 0.9rem;
    line-height: 1;
    color: var(--fp-accent);
  }

  /* ── Steps: the same numbering, laid out as a grid ────────────────────── */
  ol[data-list="steps"] {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: var(--fp-gap);
    counter-reset: step;
    list-style: none;
    margin: 0;
    padding: 0;
  }
  ol[data-list="steps"] > li {
    counter-increment: step;
    border-top: 1px solid var(--fp-rule);
    padding-top: 1.1rem;
  }
  ol[data-list="steps"] > li::before {
    content: counter(step);
    display: inline-grid;
    place-items: center;
    box-sizing: border-box;
    min-width: 1.9rem;
    height: 1.9rem;
    /* place-items centres the line box and the digit's ink sits
       --fp-label-lean above it, so the padding leans by that amount. The
       ring's fixed height absorbs the 0.3em base, which is there only so the
       lean has something to subtract from. The 0.5em is for a second digit. */
    padding-inline: 0.5em;
    padding-block: calc(0.3em + var(--fp-label-lean)) calc(0.3em - var(--fp-label-lean));
    border: 2.5px solid var(--fp-accent);
    border-radius: var(--fp-pill);
    font-family: var(--fp-mono);
    font-weight: var(--fp-weight-h);
    font-size: 0.9rem;
    line-height: 1;
    color: var(--fp-accent);
  }
  ol[data-list="steps"] > li > h3 { margin-top: 0.35rem; }
  ol[data-list="steps"] > li > p { color: var(--fp-ink-soft); font-size: 0.98rem; }
  ol[data-list="steps"] > li > :last-child { margin-bottom: 0; }

  /* ── Cards ────────────────────────────────────────────────────────────── */
  ul[data-list="cards"] {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15.5rem, 1fr));
    gap: var(--fp-gap);
    list-style: none;
    margin: 0;
    padding: 0;
  }
  @media (min-width: 52rem) {
    ul[data-list="cards"][data-cols="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    ul[data-list="cards"][data-cols="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    ul[data-list="cards"][data-cols="4"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  }
  @media (min-width: 68rem) {
    ul[data-list="cards"][data-cols="4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  }
  ul[data-list="cards"] > li {
    display: flex;
    background: var(--fp-bg-card);
    border: 1px solid var(--fp-rule);
    border-radius: var(--fp-radius);
  }
  section[data-band="alt"] ul[data-list="cards"] > li { box-shadow: var(--fp-shadow); }
  /* the card's interior belongs to whatever fills it, so the padding goes on
     the child: a whole-card link then covers the entire card. */
  ul[data-list="cards"] > li > * {
    flex: 1;
    padding: var(--fp-pad-card) var(--fp-pad-card) calc(var(--fp-pad-card) - 0.15rem);
  }
  ul[data-list="cards"] > li h3 { margin-top: 0; }
  ul[data-list="cards"] > li p { color: var(--fp-ink-soft); font-size: 0.98rem; }
  ul[data-list="cards"] > li p:last-of-type { margin-bottom: 0; }

  ul[data-list="cards"] > li > a {
    display: block;
    color: inherit;
    text-decoration: none;
    border-radius: inherit;
    transition: border-color 120ms ease, transform 120ms ease;
  }
  ul[data-list="cards"] > li:has(> a:hover) { border-color: var(--fp-ink-mute); transform: translateY(-2px); }
  @media (prefers-reduced-motion: reduce) {
    ul[data-list="cards"] > li:has(> a:hover) { transform: none; }
  }
  /* the affordance closing a card link */
  ul[data-list="cards"] > li > a > span:last-child {
    display: inline-block;
    margin-top: 0.9rem;
    color: var(--fp-accent);
    font-weight: 600;
    font-size: 0.92rem;
  }
  ul[data-list="cards"] > li > a:hover > span:last-child { color: var(--fp-accent-strong); text-decoration: underline; }

  /* a neutral icon chip; colour stays reserved for data */
  ul[data-list="cards"] > li svg:first-child {
    width: 42px;
    height: 42px;
    padding: 8px;
    border-radius: 10px;
    background: var(--fp-bg-alt);
    border: 1px solid var(--fp-rule);
    color: var(--fp-ink);
  }
  ul[data-list="cards"] > li svg:first-child + * { margin-top: 0.9rem; }

  /* ── Logos ────────────────────────────────────────────────────────────
     A row of marks nobody drew together. Their bounding boxes have nothing
     in common, so capping them all at one height makes a tall square mark
     tower over a wide wordmark and the row reads as a mistake. Optical
     weight is per-mark information that only the page can know, so each item
     carries its own height in --logo-h and the default is a starting point
     rather than an answer.

     Centred rather than spread. A spread row distributes each wrapped line
     independently, so nine marks on a line-and-a-bit leave the last two
     stranded at opposite edges and the set reads as a mistake. Centring makes
     a wrapped row look chosen at every width, which matters more than filling
     the line at one width. */
  ul[data-list="logos"] {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 1.75rem 2rem;
    list-style: none;
    margin: 0;
    /* Full-bleed, so the inset is this row's own: the grid track it sits in
       runs to the viewport edges and hands it no gutter. It scales with the
       viewport because a set of marks spread across a wide screen wants more
       air at the ends than a phone can spare. */
    padding-inline: clamp(var(--fp-gutter), 6vw, 6rem);
  }
  @media (max-width: 56rem) {
    ul[data-list="logos"] { gap: 1.5rem 2rem; }
  }
  /* the trademark line under a logo row needs air, or it reads as a caption
     belonging to the last mark */
  ul[data-list="logos"] + small { display: block; margin-top: 2rem; }
  ul[data-list="logos"] img {
    display: block;
    width: auto;
    height: var(--logo-h, 1.7rem);
    max-height: none;
  }
  ul[data-list="logos"] a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: var(--fp-target);
    min-width: var(--fp-target);
    padding: 0 0.25rem;
    /* a mark is a fact, not a call to action, so it sits back until pointed at */
    opacity: 0.8;
    transition: opacity 150ms ease;
  }
  ul[data-list="logos"] a:hover,
  ul[data-list="logos"] a:focus-visible { opacity: 1; }

  /* ── Chips ────────────────────────────────────────────────────────────
     A list of short identifiers: module names, a jump list, a set of tags.
     The dot marker is for a list of claims; a list of names wants to read as
     a set of objects, and wrapping into a block of pills does that. */
  ul[data-list="chips"] {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    list-style: none;
    margin: 0 0 1em;
    padding: 0;
  }
  ul[data-list="chips"] > li { max-width: none; }
  ul[data-list="chips"] > li > a,
  ul[data-list="chips"] > li:not(:has(a)) {
    display: inline-flex;
    align-items: center;
    min-height: var(--fp-target);
    padding: 0.25rem 0.7rem;
    border: 1px solid var(--fp-rule);
    border-radius: var(--fp-pill);
    background: var(--fp-bg-card);
    font-family: var(--fp-mono);
    font-size: 0.85rem;
    color: var(--fp-ink-soft);
    text-decoration: none;
  }
  ul[data-list="chips"] > li > a:hover { border-color: var(--fp-accent); color: var(--fp-accent); }

  /* ── Split panels ─────────────────────────────────────────────────────── */
  [data-layout="split"] {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
    gap: var(--fp-gap);
  }
  [data-layout="split"] > * {
    background: var(--fp-bg-card);
    border: 1px solid var(--fp-rule);
    border-radius: var(--fp-radius);
    padding: var(--fp-pad-panel);
  }
  [data-layout="split"] > * > :last-child { margin-bottom: 0; }
  /* Two balanced columns with no panel around them. The panel is right when
     the two are alternatives to choose between; it is wrong when they are
     simply two halves of one thought, and a border then draws a distinction
     the content is not making. */
  [data-layout="split"][data-frame="none"] > * {
    background: none;
    border: 0;
    border-radius: 0;
    padding: 0;
  }
  [data-layout="split"] [data-actions] { margin-top: 0.6rem; }

  /* ── A hero with something beside the copy ────────────────────────────
     An opening band often carries a picture of the product next to its
     heading. v2 had .hero-grid for this; the engine could not use it, because
     a hero component with no media slot can only put the picture in the block
     after, which reads as a different section. This is that column pair, kept
     out of [data-layout~="feature"] because a feature row carries its own
     vertical rhythm and a rule between siblings, and a hero wants neither. */
  [data-layout="hero"] {
    display: grid;
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
    gap: 3.5rem;
    align-items: center;
  }
  [data-layout="hero"] > * { min-width: 0; }
  @media (max-width: 56rem) {
    [data-layout="hero"] { grid-template-columns: 1fr; gap: 2.5rem; }
  }

  /* ── Feature rows ─────────────────────────────────────────────────────── */
  [data-layout~="feature"] {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--fp-gap-wide);
    align-items: center;
    padding-block: 2.75rem;
  }
  [data-layout~="feature"] + [data-layout~="feature"] { border-top: 1px solid var(--fp-rule); }
  [data-layout~="feature"][data-layout~="uneven"] { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); }
  /* when the columns differ a lot in height, centring the short one against
     the tall one reads as a mistake */
  [data-layout~="feature"][data-layout~="top"] { align-items: start; }
  [data-layout~="feature"][data-layout~="flip"] > :first-child { order: 2; }
  [data-layout~="feature"] > * { min-width: 0; }
  /* A row with one column is one column. The template omits an empty side
     rather than emitting a blank div, and without this the survivor would sit
     in the first track at half width with nothing beside it. */
  [data-layout~="feature"] > :only-child { grid-column: 1 / -1; }
  [data-layout~="feature"] h2 { font-size: clamp(1.35rem, 3vw, 1.75rem); }
  [data-layout~="feature"] h3 { font-size: 1.45rem; }
  [data-layout~="feature"] > * > p { color: var(--fp-ink-soft); }

  /* Stacking. One breakpoint for every two-column arrangement, so nothing is
     half stacked at any width. 56rem is the convention's `stack` value; see
     tokens.json, which is where a site should read it from rather than
     guessing, because a media query cannot take a custom property. */
  @media (max-width: 56rem) {
    main > section[data-band="hero"] { padding-block: 3.5rem 4rem; }
    [data-layout~="feature"],
    [data-layout~="feature"][data-layout~="uneven"] {
      grid-template-columns: 1fr;
      gap: 1.5rem;
    }
    [data-layout~="feature"][data-layout~="flip"] > :first-child { order: 0; }
    [data-layout="shots"][data-uneven] { grid-template-columns: 1fr; }
  }

  /* ── Screenshots and figures ──────────────────────────────────────────
     Told apart by what is inside them, which is also what decides how each
     one is framed. */
  [data-layout="shots"] {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
    gap: 2rem;
    align-items: start;
  }
  [data-layout="shots"][data-uneven] { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); }

  figure { margin: 0; min-width: 0; }
  /* The frame is on "an image that is a figure's content", which is a claim
     about the image's role. Widening it to `picture img` to catch a screenshot
     in a feature row looked equivalent — the media build emits <picture> for a
     raster and a bare <img> for a vector, so it reads as "frame photographs,
     not marks" — and it is not: a logo row happily carries raster marks, and
     they all came back with a 1px border. Role is not format. The feature row
     was the thing out of line, and it now wraps its media in a figure like
     every other component. */
  figure:has(> :is(img, picture, a > img)) img {
    display: block;
    width: 100%;
    height: auto;
    border: 1px solid var(--fp-rule);
    border-radius: var(--fp-radius);
    box-shadow: var(--fp-shadow-lift);
  }
  figure figcaption {
    margin-top: 0.75rem;
    max-width: var(--fp-measure);
    font-size: 0.88rem;
    color: var(--fp-ink-mute);
  }
  /* a full-bleed figure loses the frame it no longer has room for */
  [data-width="full"] :is(img, figure > img) {
    border-radius: 0;
    border-inline: 0;
    box-shadow: none;
  }
  /* the caption is prose, so it keeps the content column even when the
     picture above it does not */
  [data-width="full"] figcaption {
    max-width: var(--fp-wrap);
    margin-inline: auto;
    padding-inline: var(--fp-gutter);
  }

  /* ── Code ─────────────────────────────────────────────────────────────── */
  pre {
    margin: 0;
    padding: 1.2rem 1.4rem;
    background: var(--fp-board);
    color: var(--fp-board-soft);
    border: 1px solid var(--fp-board-line);
    border-radius: var(--fp-radius);
    font-family: var(--fp-mono);
    font-size: 0.86rem;
    line-height: 1.7;
    overflow-x: auto;
  }
  pre .c { color: var(--fp-code-comment); }
  pre .m { color: var(--fp-amber); }
  pre .p { color: var(--fp-code-path); }
  pre .k { color: var(--fp-code-key); }

  /* A listing with a filename is a figure whose caption is the title bar.
     v2 drew three decorative dots with three empty <i> elements; they are a
     border-image's worth of decoration, so they belong in CSS. */
  figure:has(> pre) {
    background: var(--fp-bg-card);
    border: 1px solid var(--fp-rule);
    border-radius: var(--fp-radius);
    overflow: hidden;
    box-shadow: var(--fp-shadow-lift);
  }
  figure:has(> pre) > figcaption {
    /* The dot metrics and the space reserved for them are derived from each
       other, because they were written independently once and the filename
       sat on top of the third dot. */
    --dot: 0.65rem;
    --dot-step: 1.1rem;   /* a dot plus the gap after it */
    --dots-end: calc(0.9rem + 2 * var(--dot-step) + var(--dot));
    display: flex;
    align-items: center;
    margin: 0;
    padding: 0.6rem 0.9rem 0.6rem calc(var(--dots-end) + 0.7rem);
    max-width: none;
    border-bottom: 1px solid var(--fp-rule);
    font-family: var(--fp-mono);
    font-size: 0.72rem;
    letter-spacing: 0.04em;
    color: var(--fp-ink-soft);
    position: relative;
  }
  figure:has(> pre) > figcaption::before {
    content: "";
    position: absolute;
    left: 0.9rem;
    top: 50%;
    translate: 0 -50%;
    width: var(--dot);
    height: var(--dot);
    border-radius: 50%;
    background: var(--fp-rule);
    box-shadow: var(--dot-step) 0 var(--fp-rule), calc(2 * var(--dot-step)) 0 var(--fp-rule);
  }
  figure:has(> pre) > pre { border: 0; border-radius: 0; box-shadow: none; }

  /* ── Tables ───────────────────────────────────────────────────────────── */
  figure:has(> table) { overflow-x: auto; margin-block: 1.5rem; }
  table { border-collapse: collapse; width: 100%; min-width: 32rem; font-size: 0.95rem; }
  caption {
    text-align: left;
    font-family: var(--fp-mono);
    font-size: 0.72rem;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--fp-ink-mute);
    padding-bottom: 0.7rem;
  }
  th, td {
    text-align: left;
    padding: 0.7rem 1.2rem 0.7rem 0;
    border-bottom: 1px solid var(--fp-rule);
    vertical-align: top;
  }
  thead th {
    font-family: var(--fp-mono);
    font-size: 0.71rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--fp-ink-mute);
  }
  tbody th { font-weight: var(--fp-weight-ui); color: var(--fp-ink); }

  /* ── Quotations ───────────────────────────────────────────────────────── */
  figure:has(> blockquote) {
    margin-block: 1.75rem;
    padding-left: 1.15rem;
    border-left: 3px solid var(--fp-rule);
  }
  blockquote { margin: 0; font-size: 1.1rem; line-height: 1.55; color: var(--fp-ink); }
  figure:has(> blockquote) > figcaption { margin-top: 0.6rem; font-size: 0.9rem; }

  /* ── Callouts ─────────────────────────────────────────────────────────
     Severity is carried by the visible label as well as the colour, because
     colour alone is not information (WCAG 1.4.1). */
  aside {
    border-left: 3px solid var(--fp-accent);
    background: var(--fp-bg-alt);
    border-radius: 0 var(--fp-radius-sm) var(--fp-radius-sm) 0;
    padding: 1rem 1.15rem;
    margin-block: 1.5rem;
  }
  aside[data-severity="warning"] { border-left-color: var(--fp-bronze); background: var(--fp-amber-wash); }
  /* --fp-accent on the wash is 6.97:1 in light, under the floor. The strong
     accent is 8.87 there and the ink it darkens to on hover is 16.7. */
  aside[data-severity="warning"] a { color: var(--fp-link, var(--fp-accent-strong)); }
  aside[data-severity="warning"] a:hover { color: var(--fp-link-hover, var(--fp-ink)); }
  /* --fp-route-red, the data blip this borrowed until 3.6.0, is 2.3:1 on
     the dark grounds; --fp-critical is tuned per scheme. */
  aside[data-severity="critical"] { border-left-color: var(--fp-critical); }
  aside > p:first-child {
    font-family: var(--fp-mono);
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--fp-ink-mute);
    margin-bottom: 0.4rem;
  }
  aside > :last-child { margin-bottom: 0; }

  /* ── Disclosure ───────────────────────────────────────────────────────── */
  details {
    border: 1px solid var(--fp-rule);
    border-radius: var(--fp-radius);
    padding: 1rem 1.15rem;
    background: var(--fp-bg-card);
    margin-block: 1rem;
  }
  details > summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    min-height: var(--fp-target);
    font-weight: var(--fp-weight-ui);
    color: var(--fp-ink);
    cursor: pointer;
    /* display:flex on a summary drops the native marker, so the disclosure
       state has to be drawn back. Both spellings: the pseudo-element is
       WebKit's and list-style covers everyone else. */
    list-style: none;
  }
  details > summary::-webkit-details-marker { display: none; }
  /* A triangle pointing at what it will do: right when closed, down when open.
     Drawn with borders rather than a glyph, for the same reason the list
     markers are: there is nothing for a glyph to fall back to. */
  details > summary::after {
    content: "";
    flex: none;
    width: 0;
    height: 0;
    border-left: 0.36rem solid var(--fp-ink-mute);
    border-block: 0.3rem solid transparent;
    transition: rotate 120ms ease;
  }
  details[open] > summary::after { rotate: 90deg; }
  details > :not(summary):last-child { margin-bottom: 0; }

  /* ── Forms ────────────────────────────────────────────────────────────
     In 3.x the controls are styled only inside a form that names its
     layout, because a sheet that styled every input would resize the forms
     on sites already live. v4 can style every form; 3.x lets a site opt in
     one form at a time. The contract test fails on a control rule outside
     form[data-layout].

       form[data-layout="stack"]    one column of fields, a measure wide
       form[data-layout="filters"]  fields across a card, wrapping as room
                                    runs out, for narrowing a list of records

     A field is a div holding a label, then a hint in <small> if it has one,
     then its control. A fieldset groups checkboxes or radios. */
  form[data-layout] { margin-block: 0 1.5rem; }
  form[data-layout="stack"] {
    display: grid;
    gap: 1.1rem;
    max-width: var(--fp-measure);
  }
  form[data-layout="filters"] {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
    gap: 1rem 1.25rem;
    /* labels of different lengths wrap to different heights; the controls
       line up along the bottom of the row instead */
    align-items: end;
    padding: var(--fp-pad-card);
    background: var(--fp-bg-card);
    border: 1px solid var(--fp-rule);
    border-radius: var(--fp-radius);
  }
  section[data-band="alt"] form[data-layout="filters"] { box-shadow: var(--fp-shadow); }

  /* start, not stretch, so a button in a field keeps its own width */
  form[data-layout] > div:not([data-actions]),
  form[data-layout] fieldset > div {
    display: grid;
    gap: 0.35rem;
    justify-items: start;
  }
  form[data-layout] > button { justify-self: start; }
  /* The buttons of a form, in a row: the same group as a row of links. In a
     filter card the row takes a line of its own, because two buttons wrap in
     a cell sized for one field. A lone submit in a field div keeps its cell. */
  form[data-layout] > [data-actions] { margin-top: 0.25rem; }
  form[data-layout="filters"] > [data-actions] { grid-column: 1 / -1; margin-top: 0; }
  form[data-layout] label,
  form[data-layout] legend {
    font-weight: var(--fp-weight-ui);
    font-size: 0.95rem;
    color: var(--fp-ink);
  }

  /* :where() keeps this at the weight of one attribute, so the textarea and
     select rules below can change what they need without restating it. */
  form[data-layout] :where(
    input:is(:not([type]), [type="text"], [type="search"], [type="email"], [type="url"], [type="tel"],
             [type="password"], [type="number"], [type="date"], [type="time"], [type="datetime-local"],
             [type="month"], [type="week"]),
    select,
    textarea
  ) {
    width: 100%;
    min-height: var(--fp-target);
    margin: 0;
    padding: 0.45rem 0.75rem;
    background-color: var(--fp-bg);
    color: var(--fp-ink);
    /* the boundary of a control is non-text contrast (1.4.11); ink-mute is
       over 7:1 on every ground, far past the 3:1 it needs */
    border: 1px solid var(--fp-ink-mute);
    border-radius: var(--fp-radius-sm);
    font: inherit;
    line-height: 1.4;
  }
  /* Safari draws a search field its own way, rounded, ignoring the border */
  form[data-layout] input[type="search"] { appearance: textfield; }
  form[data-layout] textarea { min-height: 8rem; resize: vertical; }
  form[data-layout] :is(input, textarea)::placeholder { color: var(--fp-ink-mute); opacity: 1; }
  form[data-layout] :is(input, select, textarea):disabled {
    background-color: var(--fp-bg-alt);
    color: var(--fp-ink-soft);
    cursor: not-allowed;
  }

  /* A select loses the platform's arrow so its box can match the fields
     beside it, and gets one back drawn in currentColor, so it follows the
     scheme and a dark band like any text. */
  form[data-layout] select:not([multiple], [size]) {
    appearance: none;
    padding-right: 2.4rem;
    background-image:
      linear-gradient(45deg, transparent 50%, currentColor 50%),
      linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position: calc(100% - 1.2rem) 50%, calc(100% - 0.85rem) 50%;
    background-size: 0.35rem 0.35rem;
    background-repeat: no-repeat;
  }

  /* A checkbox or radio sits inside its label, and the label is the 44px
     target, so the box itself stays the size of a letter. */
  form[data-layout] fieldset {
    min-width: 0;
    margin: 0;
    padding: 0.75rem 1.1rem 0.9rem;
    border: 1px solid var(--fp-rule);
    border-radius: var(--fp-radius-sm);
  }
  form[data-layout] legend { padding-inline: 0.35rem; }
  form[data-layout] label:has(> input:is([type="checkbox"], [type="radio"])) {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    /* both ways: a choice labelled with one digit is narrower than 44px */
    min-width: var(--fp-target);
    min-height: var(--fp-target);
    font-weight: inherit;
    font-size: inherit;
    cursor: pointer;
  }
  form[data-layout] input:is([type="checkbox"], [type="radio"]) {
    flex: none;
    width: 1.15rem;
    height: 1.15rem;
    margin: 0;
    accent-color: var(--fp-accent);
  }

  /* The submit takes the fill like any button. A reset is never the ask. */
  form[data-layout] button[type="reset"] {
    background: transparent;
    border-color: var(--fp-ink-soft);
    color: var(--fp-ink);
  }
  form[data-layout] button[type="reset"]:hover {
    background: var(--fp-bg-alt);
    border-color: var(--fp-ink);
    color: var(--fp-ink);
  }

  /* ── Errors ───────────────────────────────────────────────────────────
     A field in error says so in words, and the colour repeats them.

       the control  aria-invalid="true", or :user-invalid once someone has
                    typed, draws a second pixel of border in --fp-critical,
                    inside the first, so nothing around it moves
       the message  a small with data-severity="critical", after the label
                    and before the control, named by the control's
                    aria-describedby. Ink in the UI weight, with the
                    critical rule down its side
       a group      a fieldset whose message follows its legend takes the
                    same border as a control
       the summary  a form that failed to send opens with the critical
                    callout, each error a link to its field

     The message goes above the control, as a hint does, so a control is
     always the last thing in its field and a filter card's row stays on one
     line whatever is written above it. */
  form[data-layout] :where(input:not([type="checkbox"], [type="radio"]), select, textarea):is([aria-invalid="true"], :user-invalid) {
    border-color: var(--fp-critical);
    box-shadow: inset 0 0 0 1px var(--fp-critical);
  }
  /* A legend sits in a gap in its fieldset's border, and an inset shadow has
     no gap: drawn the control's way, the second pixel ran through the
     legend's text. A group thickens its real border instead and takes the
     pixel back from its padding, so it is the size it was. Not the top: the
     legend sets where the content starts, whatever the border's width. */
  form[data-layout] fieldset:has(> small[data-severity="critical"]:not([hidden])) {
    border: 2px solid var(--fp-critical);
    padding-inline: calc(1.1rem - 1px);
    padding-bottom: calc(0.9rem - 1px);
  }
  form[data-layout] small[data-severity="critical"] {
    padding-left: 0.6rem;
    border-left: 3px solid var(--fp-critical);
    color: var(--fp-ink);
    font-weight: var(--fp-weight-ui);
  }
  form[data-layout] fieldset > small:not([hidden]) { display: block; margin-bottom: 0.25rem; }
  form[data-layout] > aside { margin: 0; }
  form[data-layout="filters"] > aside { grid-column: 1 / -1; }

  /* ── Definition lists ─────────────────────────────────────────────────── */
  dl { margin-block: 1.5rem; }
  dt {
    font-weight: var(--fp-weight-ui);
    color: var(--fp-ink);
    margin-top: 1.25rem;
    scroll-margin-top: var(--fp-header-clear);
  }
  dt:first-child { margin-top: 0; }
  dd { margin: 0.35rem 0 0; color: var(--fp-ink-soft); max-width: var(--fp-measure); }

  /* ── Data blips ───────────────────────────────────────────────────────
     The one component that spends colour on meaning. v2 shipped the tokens
     with nothing that used them, so every site that wanted a route badge
     wrote its own. */
  [data-route] {
    display: inline-block;
    padding: 0.1rem 0.5rem;
    border-radius: var(--fp-radius-xs);
    font-family: var(--fp-mono);
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--fp-route-ink);
    background: var(--fp-ink-mute);
  }
  [data-route="blue"] { background: var(--fp-route-blue); }
  [data-route="green"] { background: var(--fp-route-green); }
  [data-route="red"] { background: var(--fp-route-red); }
  [data-route="purple"] { background: var(--fp-route-purple); }
  [data-route="orange"] { background: var(--fp-route-orange); color: var(--fp-route-ink-dark); }

  /* ── A mono chip ──────────────────────────────────────────────────────
     A jump-list entry, a module name, a proof-strip badge. Each had been
     written twice in site sheets before it moved here. */
  [data-chip] {
    display: inline-flex;
    align-items: center;
    min-height: var(--fp-target);
    padding-inline: 0.7rem;
    padding-block: calc(0.2rem + var(--fp-label-lean)) calc(0.2rem - var(--fp-label-lean));
    border: 1px solid var(--fp-rule);
    border-radius: var(--fp-pill);
    font-family: var(--fp-mono);
    font-size: 0.8rem;
    color: var(--fp-ink-soft);
    text-decoration: none;
  }
  a[data-chip]:hover { border-color: var(--fp-ink-mute); color: var(--fp-accent); }
  /* On the proof strip the chip carries the fill colour as text. The board is
     the one ground where that clears AAA, at 10.3:1. */
  section[data-band="proof"] [data-chip] { color: var(--fp-amber); white-space: nowrap; }

  /* ── The current chip ─────────────────────────────────────────────────
     A pager is a jump list: nav > ul[data-list="chips"], one link per page
     and Previous and Next either side, with aria-current on the page in
     view. It is marked in weight and ink, not a colour. The second pixel of
     border is drawn inside the first, so the current pill is the size of the
     ones beside it and nothing along the row moves. */
  /* A chip in a nav is a control, and a page number is narrower than the
     44px target on its own. */
  nav ul[data-list="chips"] > li > a {
    justify-content: center;
    min-width: var(--fp-target);
  }
  :is(ul[data-list="chips"] > li > a, ul[data-list="chips"] > li:not(:has(a)), a[data-chip])[aria-current] {
    border-color: var(--fp-ink);
    box-shadow: inset 0 0 0 1px var(--fp-ink);
    color: var(--fp-ink);
    font-weight: var(--fp-weight-h);
  }

  /* ── Site header ──────────────────────────────────────────────────────── */
  body > header {
    position: sticky;
    top: 0;
    z-index: 50;
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 1rem 1.5rem;
    padding-block: 0.85rem;
    background: var(--fp-bg);
    border-bottom: 1px solid var(--fp-rule);
  }
  @supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    body > header {
      background: var(--fp-bg-blur);
      -webkit-backdrop-filter: blur(10px);
      backdrop-filter: blur(10px);
    }
  }
  body > header > a:first-child {
    display: inline-flex;
    align-items: center;
    gap: 0.55rem;
    min-height: var(--fp-target);
    color: var(--fp-ink);
    font-weight: var(--fp-weight-h1);
    font-size: 1.2rem;
    letter-spacing: -0.02em;
    text-decoration: none;
  }
  body > header > a:first-child:hover { color: var(--fp-ink); }
  body > header > a:first-child :is(svg, img) { display: block; height: var(--fp-logo-height, 3.2rem); width: auto; }
  /* ink-soft, not ink-mute: the sticky header is 92% opaque, and scrolled
     over the darkest ground its own ground falls to #ececed, where ink-mute
     measures 6.3:1. ink-soft is 8.4 there, and 8.2 in the dark scheme over
     the lightest. */
  .tld { color: var(--fp-ink-soft); font-weight: 600; }

  body > header > nav {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 0.85rem 1.4rem;
  }
  body > header > nav a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: var(--fp-target);
    min-width: var(--fp-target);
    /* The lean the buttons take, on every nav link and not only the pills, so
       the row's baselines stay on one line. Well inside the 44px target, so
       nothing grows. */
    padding-block: calc(0.45rem + var(--fp-label-lean)) calc(0.45rem - var(--fp-label-lean));
    color: var(--fp-ink-soft);
    font-weight: var(--fp-weight-nav);
    font-size: 0.95rem;
    text-decoration: none;
  }
  body > header > nav a:hover { color: var(--fp-ink); text-decoration: underline; }
  /* "page" marks the current page; "location" the section in view. Same mark. */
  body > header > nav a[aria-current] { color: var(--fp-ink); box-shadow: 0 2px 0 var(--fp-accent); }
  /* the header's call to action is the small pill; it needs no size class */
  body > header > nav a.ink,
  body > header > nav a.ghost {
    /* padding-block, and its lean, come from the nav link rule above */
    padding-inline: 1rem;
    border: 1.5px solid transparent;
    border-radius: var(--fp-pill);
    font-size: 0.9rem;
    font-weight: var(--fp-weight-ui);
  }
  body > header > nav a.ink:hover { text-decoration: underline; text-underline-offset: 0.2em; }

  @media (max-width: 45rem) {
    body > header { padding-block: 1.15rem 1.4rem; row-gap: 1.15rem; }
    body > header > nav { justify-content: flex-start; }
  }

  /* ── Site footer ──────────────────────────────────────────────────────── */
  body > footer {
    display: grid;
    row-gap: 1.25rem;
    justify-items: start;
    background: var(--fp-board-deep);
    padding-block: 2.25rem 2.75rem;
    font-size: 0.95rem;
  }
  body > footer > * { width: 100%; }
  body > footer :is(h2, h3) {
    margin-bottom: 0.9rem;
    color: var(--fp-ink);
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
  }
  body > footer ul {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem 1.4rem;
    list-style: none;
    margin: 0;
    padding: 0;
    font-size: 0.85rem;
  }
  body > footer li { display: flex; }
  body > footer a {
    display: inline-flex;
    align-items: center;
    min-height: var(--fp-target);
    color: var(--fp-ink-soft);
    text-decoration: none;
  }
  body > footer a:hover { color: var(--fp-ink); text-decoration: underline; }
  /* Fine print is running prose. A link in a sentence keeps the line-height
     of the text around it and its underline at rest; the 44px box above is
     for the byline and the version nav, where a link stands alone. */
  body > footer > small a {
    display: inline;
    min-height: 0;
    text-decoration: underline;
  }
  /* the byline row: the site, then "is a FivePaths product." */
  body > footer > p:first-child {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin: 0;
  }
  body > footer > p:first-child a {
    gap: 0.4rem;
    font-weight: var(--fp-weight-h);
    color: var(--fp-accent);
  }
  body > footer > p:first-child a:hover { color: var(--fp-accent-strong); }
  body > footer > p:first-child img { display: block; height: 1.5rem; width: auto; }
  /* the copyright sits at the far edge of the byline row */
  body > footer > p:first-child > :last-child:not(a) { margin-left: auto; }
  body > footer > small {
    display: block;
    margin-top: 1.5rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--fp-rule);
    font-size: 0.85rem;
  }
  body > footer > small p { margin: 0 0 0.4rem; }
  body > footer > small p:last-child { margin-bottom: 0; }
}

@layer a11y {
  /* The skip link is the first thing in the body and the first thing a
     keyboard reaches. */
  body > a:first-child {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: 100;
    padding: 0.6rem 1rem;
    background: var(--fp-amber);
    color: var(--fp-amber-ink);
    border-radius: 0 var(--fp-radius-sm) 0 0;
    font-weight: var(--fp-weight-h);
    text-decoration: none;
  }
  body > a:first-child:focus { left: 0; }

  /* Target size, WCAG 2.5.5 AAA. A link that is the whole content of its list
     item or paragraph is a standalone control, not a link in a sentence, so
     it needs a real target. v2 enumerated the components this applied to and
     the list grew every time a component was added; stated structurally it
     covers the ones nobody has written yet.

     Links inside running prose are untouched: they keep the line-height of
     the text around them, which is what the criterion's exception is for.

     The :not() is load-bearing. A whole-card link is also the only child of
     its list item, and without the exclusion this rule made it inline-flex
     and laid its heading, prose and affordance out in a row. A link wrapping
     block content is already far larger than the minimum; the rule is for a
     link whose target is only as big as its text.

     The structural half is a guess. `:only-child` counts elements, not text,
     so "See the <a>record</a> for the full text" matches it, and CSS has no
     test for a text sibling. a[data-target] is the answer for markup that
     knows: the engine marks the links that stand alone. The guess stays in 3.x
     because removing it would move pages that rely on it. */
  main :is(li, p, dd) > a:only-child:not(:has(:is(h1, h2, h3, h4, h5, h6, p, ul, ol, dl, div, figure, table))),
  main a[data-target] {
    display: inline-flex;
    align-items: center;
    min-height: var(--fp-target);
    min-width: var(--fp-target);
  }

  /* Measure, WCAG 1.4.8 AAA. A block of running text stays under 80
     characters. Headings, tables and code are not running text and keep the
     full column. */
  main p,
  main li,
  body > footer > small p {
    max-width: var(--fp-measure);
  }
  main :is(section[data-band="cta"], [data-centered]) p { margin-inline: auto; }
  /* a list already indents its text; the marker column is not part of the measure */
  main :where(ul, ol):not([data-list]) > li { max-width: calc(var(--fp-measure) + 1.7rem); }

  /* Text for assistive technology only. Carries the "opens in a new tab"
     note external links need (WCAG 3.2.5), among other things. */
  .visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  /* The colour-scheme control WCAG 1.4.8 asks for. Behaviour is in
     theme-1.0.0.js beside this file. The glyphs show one at a time, so the
     icon always pictures the scheme the button offers to move to; the text
     beside it is clipped, not removed, and stays the accessible name. */
  body > header > nav button {
    min-height: var(--fp-target);
    min-width: var(--fp-target);
    padding: 0;
    background: none;
    border: 0;
    color: inherit;
    font: inherit;
    cursor: pointer;
  }
  body > header > nav button:hover { background: none; color: var(--fp-accent); text-decoration: none; }
  body > header > nav button svg { width: 1.05em; height: 1.05em; flex: none; }
  /* the label, for assistive technology only: visually-hidden, by position */
  body > header > nav button > span {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
  [data-scheme="dark"] { display: inline; }
  [data-scheme="light"] { display: none; }
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) [data-scheme="dark"] { display: none; }
    :root:not([data-theme="light"]) [data-scheme="light"] { display: inline; }
  }
  :root[data-theme="dark"] [data-scheme="dark"] { display: none; }
  :root[data-theme="dark"] [data-scheme="light"] { display: inline; }
  :root[data-theme="light"] [data-scheme="dark"] { display: inline; }
  :root[data-theme="light"] [data-scheme="light"] { display: none; }
}
