/* ==========================================================================
   AGB — styles.css
   --------------------------------------------------------------------------
   Sole owner: the CSS agent. Vanilla CSS, no preprocessor, no build step.

   FILE MAP
   The page has EIGHT sections. Phase 3 inserted #engineering between
   #developers and #marketing; its block is 12a, wedged between 12 and 13 so
   that block order still follows page order without renumbering anything.

     1.  TOKENS ................ verbatim copy of styles/tokens.css + additions
     2.  RESET ................. modern reset
     3.  BASE .................. elements and typography
     4.  UTILITIES ............. build-contract §2 shared utilities
     5.  LAYOUT ................ .container, .section, .section__head
     6.  NAV ................... sticky header, progress line, mobile panel
     7.  FOOTER
     8.  §1 #hero
     9.  §2 #how-it-works ..... flow diagram, failover demo, .statement
     10. — REMOVED (iteration 5) — was §2 #boundary
     11. §3 #one-integration .. bento + capability card
     12. §4 #developers ....... the API facts list
     12a.§5 #engineering ...... the readiness grid and the status pill
     13. §6 #marketing ........ the separate service line
     14. §7 #getting-started
     15. §8 #contact
     16. MOTION LAYER ......... EVERY transition and animation on this page

   THE SECTION NUMBERS IN THE BLOCK HEADERS BELOW ARE ONE BEHIND THE LIST
   ABOVE, deliberately. A dozen comments in this file cross-reference blocks
   by number (see the note on the 10 below) and several of the block headers
   were already inconsistent about page position before phase 3 — blocks 11
   and 12 both call themselves "PAGE SECTION 4". The list above is the
   authority on where a section sits on the page; the headers are the
   authority on where a rule sits in the file. Do not renumber either to
   match the other.

   THE 10 IS DELIBERATELY LEFT AS A GAP. Block 10 held #boundary and nothing
   else survives it but .statement, which has moved into block 9 with the
   sentence that now carries it. The blocks below were NOT re-flowed 11->10,
   12->11 and so on, because a dozen comments in this file cross-reference
   "§ 11", "§ 12", "§ 13" and "§ 16" by number and silently shifting all of
   them is a worse defect than one missing integer.

   MOTION POLICY — read before editing.
   Section 16 is the only place in this file where `transition`, `animation`
   or `@keyframes` appears. It is wholly inside
   `@media (prefers-reduced-motion: no-preference)`, and every duration is
   additionally multiplied by `--motion` (0 by default, 1 only inside that
   same gate). Two independent guards, one auditable block: grep this file
   for `transition` or `animation` and every hit must be below the section-16
   banner. A transition declared anywhere else is a bug.

   COLOUR POLICY.
   No raw hex, no `rgb()/rgba()`, and no px value that is not a token appears
   below the token block. The only literals below it are the five breakpoints,
   `0`, `1px`/`2px` inside `calc()`, percentages, `deg`, unitless ratios, and
   the `transparent` / `currentColor` keywords.
   ========================================================================== */


/* ==========================================================================
   ==========================================================================
   1. TOKENS
   --------------------------------------------------------------------------
   The block between the two "BEGIN/END tokens.css" banners below is a
   VERBATIM COPY of `styles/tokens.css`.

   It is inlined rather than `@import`ed on purpose: this page must open from
   `file://`, where a local `@import` is the construct most likely to behave
   inconsistently, and `@import` serialises what would otherwise be one
   request. The design system's implementation note 1 names inlining as the
   acceptable alternative to two <link> tags, provided the palette blocks move
   with it intact and still commented — they have.

   *** THE TWO FILES MUST BE KEPT IN SYNC. ***
   `styles/tokens.css` remains the source of truth and is frozen. If a token
   changes there, paste the whole file over the block below — do not patch one
   line in one place. `index.html` links only `styles.css`, so this copy is
   what actually ships; `styles/tokens.css` is the reference original.
   Additions made by the CSS agent are OUTSIDE the copied block, in a clearly
   marked "1b" section, so a re-paste can never clobber them.
   ==========================================================================
   ========================================================================== */

/* ---------------------- BEGIN verbatim copy of styles/tokens.css --------- */

/* ==========================================================================
   AGB — design tokens
   --------------------------------------------------------------------------
   Vanilla CSS custom properties. No preprocessor, no build step.
   This file defines tokens ONLY. It contains no selectors other than :root,
   the reduced-motion gate, and the three documented palette-swap blocks.

   LOAD ORDER — this file must come first:
     <link rel="stylesheet" href="styles/tokens.css">
     <link rel="stylesheet" href="styles/styles.css">
   Two <link> tags rather than @import: it is the only arrangement that is
   guaranteed to resolve when the page is opened directly from the
   filesystem, and it lets both files download in parallel.

   NAMING (see docs/design-system.md § Token naming convention)
     --c-*          colour            --fs-/--lh-/--ls-/--fw-/--ff-*  type
     --sp-*         spacing           --r-*        radius
     --bw-*         border width      --dur-/--ease-*                 motion
     --container-* · --gutter · --section-*  layout   --z-*          stacking
                 ^^^ DEVIATION 1 FROM styles/tokens.css ^^^
   In the original, the three layout prefixes on the line above are joined by
   forward slashes. That puts an asterisk immediately before a slash, which
   is the comment-close sequence — so the comment ends THERE, mid-sentence,
   and everything after it is parsed as a stray rule prelude that swallows
   the entire :root palette block below and drops it on the floor.
   styles/tokens.css has this defect as shipped: linked on its own it defines
   no colours, no type scale and no spacing at all — the page renders as
   unstyled Times. The separators here are middots instead, so the comment
   closes where it is meant to. Everything else in this block is byte-exact.
   styles/tokens.css needs the same one-character fix; see
   docs/design-system.md § CSS notes.
   Component-scoped variables (--btn-*, --field-*, --node-*) are declared
   inside the component rule in styles.css and never on :root.

   DEVIATION 2 FROM styles/tokens.css: the seven --c-island-* tokens and
   --bevel-top are deleted from this copy, in all three palette blocks. Both
   had zero consumers in this file and in index.html; under the now-active
   "Ledger White" palette both are also wrong rather than merely unused. See
   the note in palette A below and docs/final-phase1-notes.md § Islands.
   styles/tokens.css is unchanged and still carries them.

   DEVIATION 3: PALETTE C IS THE ACTIVE PALETTE. The site is light. Palette A
   below is left in place as the base layer exactly as the file's own swap
   mechanism prescribes — palette C follows it and wins the cascade on every
   token it names. There is no runtime toggle and no dark version is retained.

   Every colour pair below was measured with a WCAG 2.1 script; the full
   146-pair audit is printed in docs/design-system.md § Contrast audit and
   was re-run against the light palette in docs/final-phase1-notes.md.
   Do not substitute a hex without re-running it.
   ========================================================================== */

:root {

  /* ======================================================================
     1. PALETTE — ACTIVE: "Ink & Signal" (dark, neutral near-black, indigo)
     Palettes B and C are at the bottom of this file, commented out.
     To swap: comment this block, uncomment that one. Nothing else changes,
     because every rule in styles.css reads the semantic names only.
     ====================================================================== */

  /* -- surfaces ------------------------------------------------------- */
  --c-bg:              #08090A;  /* page base. never #000 */
  --c-surface:         #0F1113;  /* elevated: cards, panels, sticky nav */
  --c-sunken:          #040506;  /* recessed: code panels, input wells */
  --c-overlay-hover:   rgba(255, 255, 255, 0.05);
  --c-overlay-active:  rgba(255, 255, 255, 0.09);

  /* -- borders --------------------------------------------------------- */
  /* --c-border is DECORATIVE (1.18:1). It may never be the only thing that
     marks the edge of an interactive control — use --c-border-strong there. */
  --c-border:          rgba(255, 255, 255, 0.08);   /* -> #1C1D1E on bg */
  --c-border-strong:   rgba(255, 255, 255, 0.38);   /* -> #666667, 3.47:1 */
  --c-border-hover:    rgba(255, 255, 255, 0.50);

  /* -- text ------------------------------------------------------------ */
  --c-text:            #F5F6F7;  /* 18.42:1 on bg */
  --c-text-2:          #B0B6BE;  /*  9.76:1 — subheads, body-secondary */
  --c-text-3:          #8C939C;  /*  6.42:1 — mono labels, captions */
  --c-text-dim:        #6E757E;  /*  4.28:1 — LARGE TEXT >=24px ONLY */
  --c-text-disabled:   #4A5058;  /*  disabled controls (1.4.3 exempt) */

  /* -- accent — exactly one, used in exactly three places:
        the primary CTA, the focus ring, and the live element in the
        section-2 diagram. A fourth use is a bug. --------------------- */
  --c-accent:          #7C86FF;
  --c-accent-hover:    #98A0FF;
  --c-accent-active:   #6570F0;
  --c-accent-ink:      #07080A;  /* text ON an accent fill — 6.40:1 */
  --c-accent-muted:    rgba(124, 134, 255, 0.14);   /* chip / soft fill */
  --c-focus:           #98A0FF;  /* 8.36:1 on bg, 7.94:1 on surface */

  /* -- status — reserved STRICTLY for transaction state.
        Never decorative. Never a brand colour. ------------------------ */
  --c-ok:              #4ADE9B;
  --c-warn:            #FFC53D;
  --c-err:             #FF7A82;
  --c-ok-muted:        rgba(74, 222, 155, 0.14);
  --c-warn-muted:      rgba(255, 197, 61, 0.14);
  --c-err-muted:       rgba(255, 122, 130, 0.14);

  /* -- the single permitted ambient gradient (hero only, <=6% alpha) --- */
  --c-glow:            rgba(124, 134, 255, 0.06);

  /* THE SEVEN --c-island-* TOKENS THAT USED TO SIT HERE ARE DELETED.
     DEVIATION 2 FROM styles/tokens.css — see the banner above.
     The mechanism was documented and never wired: `class="island"` appears
     zero times in index.html and `var(--c-island-…)` appeared zero times in
     this file, in either palette. Under "Ledger White" they stop being
     harmless aliases and become a live dark-panel palette with no panel, so
     they were removed from the cascade rather than left as a half-state.
     The full argument is in docs/final-phase1-notes.md § Islands.
     --bevel-top went with them, and for the same reason: zero consumers, and
     a 1px inset WHITE top line is meaningless on white paper. Depth on this
     palette is carried by --shadow-raised in § 1b, not by an edge highlight.
     styles/tokens.css, the reference original, is unchanged and still
     carries both. */


  /* Fonts are self-hosted from assets/fonts/ — see the @font-face block
   below and docs/fonts-handoff.md. No third-party font request is made. */

  --ff-sans: "Geist", ui-sans-serif, system-ui, -apple-system,
             "Segoe UI", Helvetica, Arial, sans-serif;
  --ff-mono: "Geist Mono", ui-monospace, SFMono-Regular, "SF Mono",
             Menlo, Consolas, monospace;

  /* Fluid from 360px to 1440px. Below 360 the min clamps; above 1440 the
     max clamps. Tracking tightens as size grows — that relationship is the
     whole point, do not flatten it. */

  /* size                       min       fluid                      max   */
  --fs-display:  clamp(2.500rem, 1.750rem + 3.333vw, 4.750rem); /* 40->76 */
  --fs-h1:       clamp(2.125rem, 1.583rem + 2.407vw, 3.750rem); /* 34->60 */
  --fs-h2:       clamp(1.750rem, 1.417rem + 1.481vw, 2.750rem); /* 28->44 */
  --fs-h3:       clamp(1.375rem, 1.208rem + 0.741vw, 1.875rem); /* 22->30 */
  --fs-h4:       clamp(1.125rem, 1.063rem + 0.278vw, 1.313rem); /* 18->21 */
  --fs-body-lg:  clamp(1.063rem, 1.000rem + 0.278vw, 1.250rem); /* 17->20 */
  --fs-body:     clamp(1.000rem, 0.979rem + 0.093vw, 1.063rem); /* 16->17 */
  --fs-body-sm:  clamp(0.875rem, 0.854rem + 0.093vw, 0.938rem); /* 14->15 */
  --fs-caption:  clamp(0.750rem, 0.729rem + 0.093vw, 0.813rem); /* 12->13 */
  --fs-mono-sm:  clamp(0.750rem, 0.729rem + 0.093vw, 0.813rem); /* 12->13 */
  --fs-mono-code:clamp(0.813rem, 0.792rem + 0.093vw, 0.875rem); /* 13->14 */

  --lh-display: 1.00;
  --lh-h1:      1.03;
  --lh-h2:      1.08;
  --lh-h3:      1.18;
  --lh-h4:      1.30;
  --lh-body-lg: 1.55;
  --lh-body:    1.60;
  --lh-body-sm: 1.55;
  --lh-caption: 1.45;
  --lh-mono:    1.45;
  --lh-code:    1.65;

  /* Sans tracks negative and scales with size. Mono tracks POSITIVE. */
  --ls-display: -0.045em;
  --ls-h1:      -0.038em;
  --ls-h2:      -0.030em;
  --ls-h3:      -0.022em;
  --ls-h4:      -0.016em;
  --ls-body-lg: -0.012em;
  --ls-body:    -0.011em;
  --ls-body-sm: -0.008em;
  --ls-caption: -0.005em;
  --ls-mono:     0.020em;
  --ls-code:     0em;

  --fw-display: 380;   /* light-to-medium. never bold at display sizes */
  --fw-h1:      400;
  --fw-h2:      450;
  --fw-h3:      500;
  --fw-h4:      500;
  --fw-body:    400;
  --fw-medium:  500;
  --fw-mono:    500;


  /* ======================================================================
     3. SPACING — 4px base, named steps. Use the name, never a raw px.
     ====================================================================== */
  --sp-3xs:  0.125rem;  /*   2px  hairline nudges, icon optical offsets */
  --sp-2xs:  0.25rem;   /*   4px  icon-to-label gap */
  --sp-xs:   0.5rem;    /*   8px  chip padding, tight stacks */
  --sp-sm:   0.75rem;   /*  12px  control inner padding */
  --sp-md:   1rem;      /*  16px  default element gap */
  --sp-lg:   1.5rem;    /*  24px  card padding (mobile), button padding */
  --sp-xl:   2rem;      /*  32px  card padding (desktop), grid gap */
  --sp-2xl:  3rem;      /*  48px  block separation inside a section */
  --sp-3xl:  4rem;      /*  64px  header-to-content inside a section */
  --sp-4xl:  6rem;      /*  96px  scroll-margin-top under the sticky nav */
  --sp-5xl:  8rem;      /* 128px  reserved for the hero only */


  /* ======================================================================
     4. LAYOUT
     Breakpoints are LITERAL px in @media (custom properties cannot be used
     there). Mobile-first, min-width only:
        sm 480  ·  md 640  ·  lg 900  ·  xl 1200  ·  2xl 1440
     ====================================================================== */
  --container:        1200px;  /* default content column */
  --container-wide:   1360px;  /* reserved: no element uses it since
                                  #coverage was removed */
  --container-narrow:  720px;  /* FAQ, engagement steps, legal prose */
  --measure:            68ch;  /* max line length for body copy */
  --gutter:  clamp(1.25rem, 0.55rem + 3.1vw, 3rem);   /* 20 -> 48px */

  /* Section rhythm. Three tokens so band height can VARY deliberately —
     uniform rhythm is the reason a page reads as flat. */
  --section-y-tight: clamp(2.500rem, 1.833rem + 2.963vw, 4.000rem);  /* 40->64  */
  --section-y:       clamp(3.500rem, 2.333rem + 5.185vw, 7.000rem);  /* 56->112 */
  --section-y-loose: clamp(4.500rem, 2.667rem + 8.148vw, 10.00rem);  /* 72->160 */

  --nav-h:           56px;     /* 64px from lg up, overridden in styles.css */


  /* ======================================================================
     5. RADII — small and scaled to element size. Nothing above 14px.
     ====================================================================== */
  --r-xs:   3px;   /* inline code, tiny tags */
  --r-sm:   4px;   /* filter chips, table cells, swatches */
  --r-md:   6px;   /* buttons, inputs, selects, tabs, diagram nodes */
  --r-lg:  10px;   /* cards, bento cells, code panel, accordion rows */
  --r-xl:  14px;   /* the hero demo frame — the only element that gets it */
  --r-pill: 999px; /* status chips ONLY, so they read as state not as UI */


  /* ======================================================================
     6. BORDERS & ELEVATION
     Hairlines carry all structure. There is exactly one shadow token and
     it is for the mobile filter drawer, which floats over content.
     ====================================================================== */
  --bw-hairline: 1px;
  --bw-strong:   1px;
  --bw-focus:    2px;
  --focus-offset: 2px;

  /* --bevel-top DELETED — deviation 2, see the note in the palette above.
     --shadow-overlay is live: § 4 and § 6 now paint it on the two elements
     that genuinely float over content, the skip link and the mobile nav
     panel. On a light ground a floating chip with no shadow has no edge but
     its border. */
  --shadow-overlay: 0 8px 32px rgba(0, 0, 0, 0.44);


  /* ======================================================================
     7. MOTION — durations and curves measured off live production sites.
     ====================================================================== */
  --dur-instant:  80ms;    /* colour-only swaps inside a pressed control */
  --dur-micro:   140ms;    /* hover, focus, border/colour change */
  --dur-standard:240ms;    /* tab swap, accordion, panel change */
  --dur-entrance:440ms;    /* scroll reveal */
  --dur-ambient:2400ms;    /* the diagram comet loop */

  --ease-out:      cubic-bezier(0.25, 0.46, 0.45, 0.94); /* easeOutQuad  */
  --ease-standard: cubic-bezier(0.40, 0.00, 0.20, 1.00);
  --ease-emphasis: cubic-bezier(0.25, 1.00, 0.50, 1.00); /* easeOutQuart */
  --ease-expo:     cubic-bezier(0.19, 1.00, 0.22, 1.00); /* easeOutExpo  */
  /* A real spring in pure CSS. Budget: ONE use on the whole page, on the
     "Copied" confirmation. Everything else decelerates without overshoot. */
  --ease-spring: linear(0,.006,.0225,.048,.081,.119,.162,.209,.257,.308,.358,.409,.459,.508,.556,.602,.645,.687,.726,.762,.796,.827,.856,.882,.905,.927,.946,.963,.977,.99,1.001,1.011,1.019,1.026,1.031,1.035,1.038,1.04,1.042,1.043,1.043,1.042,1.041,1.039,1.037,1.035,1.033,1.031,1.029,1.027,1.025,1.023,1.021,1.019,1.017,1.015,1.013,1.011,1.01,1.008,1.007,1.006,1.005,1.004,1.003,1.002,1.001,1.001,1,.999,.999,.999,.999,.998,.998,.998,.998,.998,.998,.998,.998,.998,.999,.999,.999,.999,.999,.999,1,1,1,1,1,1,1,1,1,1,1);

  --stagger: 60ms;         /* multiplied by --i on staggered reveals */

  /* Motion is OPT-IN. This resolves to 0s by default and is only given a
     real value inside the no-preference gate below. Any transition or
     animation that multiplies by this token is therefore off unless the
     user has explicitly not asked for reduced motion. */
  --motion: 0;


  /* ======================================================================
     8. STACKING — every z-index on the page comes from this list.
     ====================================================================== */
  --z-base:      0;
  --z-raised:   10;   /* hovered bento cell, active diagram node */
  --z-sticky:  100;   /* in-section sticky headings */
  --z-nav:     200;   /* the site header */
  --z-drawer:  300;   /* mobile filter drawer */
  --z-overlay: 400;   /* scrim behind the drawer */
  --z-toast:   500;   /* "Copied" confirmation */
  --z-skip:    600;   /* skip link, always on top when focused */
}


/* ==========================================================================
   Geist + Geist Mono — self-hosted. SIL Open Font License 1.1, see
   assets/fonts/OFL.txt. Files taken unmodified from Google Fonts
   (Geist v1.800, Geist Mono v1.701); subsets and unicode-ranges are
   upstream's. Variable wght; the declared ranges match the production
   request 300..700 / 400..600.
   ========================================================================== */

/* Geist — cyrillic-ext */
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url(assets/fonts/geist-cyrillic-ext.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* Geist — cyrillic */
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url(assets/fonts/geist-cyrillic.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* Geist — vietnamese */
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url(assets/fonts/geist-vietnamese.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* Geist — latin-ext */
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url(assets/fonts/geist-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Geist — latin */
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url(assets/fonts/geist-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Geist Mono — cyrillic-ext */
@font-face {
  font-family: 'Geist Mono';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url(assets/fonts/geist-mono-cyrillic-ext.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* Geist Mono — cyrillic */
@font-face {
  font-family: 'Geist Mono';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url(assets/fonts/geist-mono-cyrillic.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* Geist Mono — symbols2 (box drawing, used by the code panel) */
@font-face {
  font-family: 'Geist Mono';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url(assets/fonts/geist-mono-symbols2.woff2) format('woff2');
  unicode-range: U+2000-2001, U+2004-2008, U+200A, U+23B8-23BD, U+2500-259F;
}
/* Geist Mono — vietnamese */
@font-face {
  font-family: 'Geist Mono';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url(assets/fonts/geist-mono-vietnamese.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* Geist Mono — latin-ext */
@font-face {
  font-family: 'Geist Mono';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url(assets/fonts/geist-mono-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Geist Mono — latin */
@font-face {
  font-family: 'Geist Mono';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url(assets/fonts/geist-mono-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ==========================================================================
   MOTION GATE — Vercel's motion-safe pattern.
   Animation is switched ON here, never switched off somewhere else. The
   opt-out pattern leaks, because you always forget one property.
   ========================================================================== */
@media (prefers-reduced-motion: no-preference) {
  :root { --motion: 1; }
}

/* ==========================================================================
   HIGH-CONTRAST / FORCED COLOURS — keep the hairline structure visible.
   ========================================================================== */
@media (prefers-contrast: more) {
  :root {
    --c-border:        rgba(255, 255, 255, 0.24);
    --c-border-strong: rgba(255, 255, 255, 0.62);
    --c-text-2:        #D2D6DB;
    --c-text-3:        #B0B6BE;
  }
}


/* ==========================================================================
   ==========================================================================
   ALTERNATE PALETTE B — "Deep Ledger"  (dark, navy-tinted, cyan accent)
   To activate: uncomment this whole block. It must stay AFTER the :root
   block above so it wins the cascade. Change nothing else.
   Audited: 0 failures across the same 146 pairs.
   ==========================================================================

:root {
  --c-bg:             #070C16;
  --c-surface:        #0E1626;
  --c-sunken:         #04070E;
  --c-overlay-hover:  rgba(158, 193, 240, 0.06);
  --c-overlay-active: rgba(158, 193, 240, 0.10);

  --c-border:         rgba(158, 193, 240, 0.10);
  --c-border-strong:  rgba(158, 193, 240, 0.46);
  --c-border-hover:   rgba(158, 193, 240, 0.58);

  --c-text:           #E9EFF8;
  --c-text-2:         #A6B4CB;
  --c-text-3:         #8492AA;
  --c-text-dim:       #66748C;
  --c-text-disabled:  #49556B;

  --c-accent:         #4CC2F1;
  --c-accent-hover:   #7AD5F7;
  --c-accent-active:  #35AFE0;
  --c-accent-ink:     #04121C;
  --c-accent-muted:   rgba(76, 194, 241, 0.14);
  --c-focus:          #7AD5F7;

  --c-ok:             #34D399;
  --c-warn:           #FBBF24;
  --c-err:            #FB8A99;
  --c-ok-muted:       rgba(52, 211, 153, 0.14);
  --c-warn-muted:     rgba(251, 191, 36, 0.14);
  --c-err-muted:      rgba(251, 138, 153, 0.14);

  --c-glow:           rgba(76, 194, 241, 0.06);
}
@media (prefers-contrast: more) {
  :root {
    --c-border:        rgba(158, 193, 240, 0.26);
    --c-border-strong: rgba(158, 193, 240, 0.70);
    --c-text-2:        #C8D3E4;
    --c-text-3:        #A6B4CB;
  }
}
   ========================================================================== */


/* ==========================================================================
   ==========================================================================
   PALETTE C — "Ledger White"  (light paper)   ***  ACTIVE  ***
   --------------------------------------------------------------------------
   THIS BLOCK IS LIVE. It sits after palette A and wins the cascade on every
   token it names; palette A supplies nothing but the tokens C does not
   mention. That is the file's own documented swap mechanism, used as written.

   NOT ONE VALUE INSIDE IT WAS CHANGED. What was removed is the seven
   --c-island-* declarations — see deviation 2 in the banner at the top of
   this file, and docs/final-phase1-notes.md § Islands. With the islands gone
   the "dark product islands" half of the palette's original name goes with
   them, and the note that used to stand here about remapping --c-accent and
   --c-focus onto a dark panel describes a mechanism that no longer exists.
   --bevel-top went for the same reason.

   THE ACTIVATION TRAP, RECORDED SO IT IS NOT WALKED INTO AGAIN. When this
   block was a comment, the row of `=` characters under the header and the
   row above the old closing delimiter were INSIDE the comment. Deleting only
   the two delimiters leaves a bare `==========` line immediately before
   `:root`, which parses as a selector prelude, swallows the whole following
   rule block, and silently drops every token in it. The fences are therefore
   terminated properly here, and the result was verified by reading computed
   values back out of real Chrome over CDP rather than by eye — all 47 tokens
   in this block and the second layer resolve to their intended values.

   THE SAME TRAP HAS A SECOND FORM, AND IT BIT WHILE THIS NOTE WAS BEING
   WRITTEN: a comment that QUOTES a comment-close delimiter ends at the
   quote. That is also the live bug this run found at --texture-cell in
   § 1b — see the note there. No comment in this file may contain the two
   characters that close one, in any context, quoted or not.

   Audited: 0 failures across the same 146 pairs, re-run against the built
   second layer in docs/final-phase1-notes.md.
   ========================================================================== */

:root {
  --c-bg:             #FBFBFA;
  --c-surface:        #FFFFFF;
  --c-sunken:         #F1F1EF;
  --c-overlay-hover:  rgba(11, 13, 15, 0.04);
  --c-overlay-active: rgba(11, 13, 15, 0.07);

  --c-border:         rgba(11, 13, 15, 0.12);
  --c-border-strong:  rgba(11, 13, 15, 0.52);
  --c-border-hover:   rgba(11, 13, 15, 0.64);

  --c-text:           #0B0D0F;
  --c-text-2:         #4C5359;
  --c-text-3:         #656C72;
  --c-text-dim:       #7F868C;
  --c-text-disabled:  #A8AEB3;

  --c-accent:         #4436D6;
  --c-accent-hover:   #3629B4;
  --c-accent-active:  #2B1F9B;
  --c-accent-ink:     #FFFFFF;
  --c-accent-muted:   rgba(68, 54, 214, 0.10);
  --c-focus:          #4436D6;

  --c-ok:             #07724F;
  --c-warn:           #7A5200;
  --c-err:            #B3261E;
  --c-ok-muted:       rgba(7, 114, 79, 0.10);
  --c-warn-muted:     rgba(122, 82, 0, 0.10);
  --c-err-muted:      rgba(179, 38, 30, 0.10);

  /* --c-glow is RE-POINTED in § 1b. The 5% here is the palette's own audited
     value and is left untouched; the hero backdrop needs a lower alpha on
     white for the eyebrow to keep its AA headroom, and that re-derivation is
     a second-layer decision, so it lives in § 1b where the rest of the
     second layer does. */
  --c-glow:           rgba(68, 54, 214, 0.05);
  --shadow-overlay:   0 8px 32px rgba(11, 13, 15, 0.16);
}
@media (prefers-contrast: more) {
  :root {
    --c-border:        rgba(11, 13, 15, 0.28);
    --c-border-strong: rgba(11, 13, 15, 0.72);
    --c-text-2:        #3A4045;
    --c-text-3:        #4C5359;
  }
}

/* ------------------------ END verbatim copy of styles/tokens.css --------- */


/* ==========================================================================
   1b. TOKENS ADDED BY THE CSS AGENT
   --------------------------------------------------------------------------
   Values the components below need that `styles/tokens.css` does not define.
   They live here, outside the copied block, so re-pasting tokens.css cannot
   clobber them — and so that the "no raw px below the token block" rule in
   docs/design-system.md § Token naming holds literally.
   Every one is recorded in docs/design-system.md § CSS notes.
   ========================================================================== */
:root {
  /* -- control sizing (design system § Component states: 44px buttons,
        36px icon buttons and the nav CTA, 32px chips, 24x24 minimum) ---- */
  --h-control:      44px;   /* buttons, inputs, selects */
  --h-control-sm:   36px;   /* nav CTA, copy button, icon buttons */
  --target-min:     24px;   /* WCAG 2.5.8 minimum target */
  --h-textarea:    120px;   /* design system § Form fields */

  /* -- icon sizing (design system § Iconography: 16 / 20 / 24 only) ----- */
  --size-icon:      24px;
  --size-icon-sm:   16px;
  --size-check:     20px;   /* the consent checkbox box */
  --size-spinner:   16px;

  /* -- rule widths that are not borders and not the focus ring ---------- */
  --bw-rule:         2px;   /* nav active rule, row rule, .facts opening rule */
  --select-arrow:    6px;   /* the CSS-drawn <select> chevron */

  /* -- layout ----------------------------------------------------------- */
  --nav-h-lg:       64px;   /* --nav-h from lg up */
  --diagram-min-w: 320px;   /* below this the SVGs scroll rather than shrink */
  --blur-nav:       12px;   /* the one permitted backdrop-filter radius */

  /* -- COMPOSITION (iteration 2, § 3.2) ----------------------------------
     Two treatments, applied section by section in § 5b:
       CENTRED  a text-only section runs in one column the width of the
                measure, centred, so the left and right margins are equal.
       SPLIT    a section carrying a diagram, a code panel or a card grid
                runs as two columns: the text rail on the left, the object
                on the right.
     --rail is the width of that left text rail.

     ITERATION 4 — RE-SIZED FROM 26rem TO 30rem, AND THE REASON IS MEASURED.
     26rem (416px) was sized against the BODY measure. A --fs-h2 title is
     44px at 1440 and needs far more width per line than 17px body copy, so
     three of the page's titles broke to three lines inside it. Binary-
     searched in Chrome at 1440 (--fs-h2 = 43.998px), the narrowest column in
     which each title still sets in TWO lines is:

       #contact           320px      #getting-started   480px
       #how-it-works      429px      #one-integration   510px
       #marketing         444px      #developers        545px

     Only #how-it-works and #marketing still put a title in the rail (§ 5b),
     so the binding case is 444px. 30rem = 480px clears it by 36px, which is
     the slack a re-flowed webfont needs, and leaves the object column 560px
     at 1440 — still two cards plus their gap. The two sections whose titles
     need 510px and 545px are the two that no longer use a rail at all. */
  --rail:          30rem;   /* 480px — the split layout's text column */

  /* -- HEADING MEASURE (iteration 4, task 4) ------------------------------
     Body copy and headings were sharing one width limit, and it was set for
     body copy. --measure is 44ch at --fs-body, which renders at ~496px and
     ~66 characters — right for a paragraph. A 44px heading held to the same
     496px gets ~25 characters a line, which is why every long title on the
     page was breaking into three short stacks.

     Headings therefore get their own limit, at twice the rendered body
     measure: 2 x 31rem = 62rem = 992px. Paragraphs are untouched — .lede
     and .prose keep --measure exactly as they were. Measured at 1440, the
     longest single-line title on the page (#developers) is 1064px, so 62rem
     still breaks it to two lines rather than letting it run the full
     container; the shortest (#contact, 632px) is unaffected because its
     centred container is narrower than the cap anyway. */
  --measure-head:  62rem;   /* 992px — 2 x the rendered body measure */

  /* The accent-separation gap (§ 3.5), and what it does NOT guard any more.

     It used to hold #one-integration's code panel apart from #marketing's
     ochre rule. Iteration 4 removed that need at the source by drawing the
     panel's selected-tab underline in the neutral ramp; ITERATION 5 removes
     the panel itself, so there is now no code panel anywhere in the document
     and nothing above #marketing that can put indigo on screen beside it. The
     one guard that is still load-bearing is the one BELOW the band —
     #getting-started's indigo CTA against the ochre inside #marketing — and
     that one is a function of the VIEWPORT HEIGHT, which no media query can
     see, so it stays expressed in vh and floored by the section rhythm.

     ITERATION 5, SEAM PASS — 35vh WAS THE WRONG SHAPE, AND THAT IS WHY THIS
     SEAM WAS 463px AT 1440x900. The requirement is not "a fraction of the
     viewport". It is, exactly:

         base(width) + opening  >=  viewport height

     where base(width) is what the page ALREADY spends between the last ochre
     pixel in #marketing and the top of #getting-started's indigo CTA before
     this token contributes anything: #marketing's tail, and the whole of
     #getting-started's head, steps and closing note. Driven in real Chrome
     over CDP, `getComputedStyle` on every element in #marketing for a ochre
     channel and the CTA's own box for the indigo, base measures:

       width   360   390   480   580   768   1024  1200  1440  1920
       base   1080  1059   916   877   900    930   952   980   980

     It DIPS in the middle — 877px at 580, where the layout has gone
     single-column but the copy is still setting short lines — and it is flat
     above 1440. So the guard that is actually required is

         max(0, vh - 877)

     — a line of SLOPE 1 with an 877px offset, not a line of slope 0.35
     through the origin. 35vh had the wrong slope at both ends. It spent
     315px at a 900px-tall window where the requirement is ZERO (base already
     clears 900 at every width but the 580-700 dip, and the section opening
     covers that), and it grew at 0.35px per px of viewport where the deficit
     grows at 1.0, which is why iteration 4 had to record a height past which
     it stops holding at all.

     Re-expressed with that shape, floored at 0 and kept under the SAME 30rem
     ceiling. 52rem = 832px is the 877px minimum less 45px, so the guard
     opens 45px before it is strictly needed:

       vh        800   844   922   1024  1200  1312  1357  1366
       guard       0    12    90     192   368   480*  480*  480*
       opening    90    90    90     192   368   480   480   480
       sep       967   967   967    1069  1245  1357  1357  1357
       holds       Y     Y     Y       Y     Y     Y     Y     N
       (* the 30rem ceiling. `opening` is max(--section-y-loose,
          --accent-gap); the row is computed at the 877px worst-case width,
          where --section-y-loose is 90px, so every other width has more.
          At 1024 the same row holds to vh 1410, at 1440 to vh 1460.)

     ZERO COST BELOW 904px OF VIEWPORT HEIGHT. --section-y-loose is 72px at
     360 and 160px at 1440, and the guard does not exceed it until vh passes
     832 + loose — 904px at 360, 992px at 1440. Every laptop and phone the
     page is measured on is under that, which is the whole of the 463 -> 308
     reduction at 1440x900.

     IT IS NEVER WEAKER THAN THE TOKEN IT REPLACES. New holds iff
     vh <= base + 480 (below the ceiling the slope-1 term makes
     base + (vh - 832) >= vh true for any base >= 832, and the loose floor
     covers the stretch below that). Old held iff
     vh <= min(base / 0.65, base + 480). min(a, b) <= b, so the new bound is
     >= the old one at every width, with equality only where the 30rem
     ceiling is what binds. Worked at the two widths that matter:
       · 1024 — old 1410, new 1410 (both ceiling-bound). 1024x1366, the case
         iteration 2 recorded as unfixable, clears by 44px on both.
       · 580  — old 1349 (slope-bound), new 1357 (ceiling-bound).
     Past that the two hues cannot be separated by spacing at all, and a void
     that large would be a worse defect than the collision it prevents. That
     limit is inherited, not introduced; the 30rem ceiling is unchanged and
     so is where it bites.

     WHY THE DISTANCE AND NOT A STATE. § 13's `:has()` gate mutes the nav's
     indigo from the scroll-spy's `.is-active`, and the obvious next move is
     to mute this CTA the same way. It does not work, twice over:
       · #getting-started has no nav link, so the spy leaves #marketing
         current for the whole of it. The CTA would be neutral exactly while
         the reader is reading the section that owns it, and would snap to
         indigo, on screen, once #contact became current.
       · The state that IS precise — "#marketing is in the viewport" — is
         only expressible as a scroll-driven `view()` timeline, and § 16 puts
         every animation on this page inside
         `@media (prefers-reduced-motion: no-preference)`. A guard that stops
         guarding under reduced motion is not a guard.
     So the mechanism stays geometric. What changed is that it is now the
     geometry the requirement actually asks for. */
  --accent-gap:    clamp(0px, 100vh - 52rem, 30rem);
  --split-gap:     var(--sp-3xl);

  /* The hero headline inside the split. Between --fs-h1 and --fs-display:
     large enough to stay the biggest type on the page by a clear step over
     the 44px section titles, small enough that its 22-character line fits a
     528px column. 34 -> 54px on the same 360-1440 track as the rest of the
     scale. */
  --fs-hero-split: clamp(2.125rem, 1.583rem + 2.407vw, 3.375rem);

  /* -- TONAL ALTERNATION — RE-DERIVED FOR "LEDGER WHITE" -------------------
     WHAT IT USED TO BE. On the dark palette the bands alternated base /
     one-step-LIGHTER, and a card had to be one step lighter again, so the
     ramp ran four deep: --c-bg #08090A, --c-surface #0F1113, --c-surface-2
     #16191C, --c-surface-3 #1D2023. Depth was carried by FILL, and there was
     always another step available above.

     WHY THAT CANNOT SURVIVE THE SWAP, and it is arithmetic rather than
     taste. Palette C's two grounds are #FBFBFA and #FFFFFF. On light the
     elevated tone is the WHITE one — the palette's own comment says so — so
     the alternation's "lighter band" is #FFFFFF, and a raised card on a
     #FFFFFF band has nowhere to go: it can only be tinted DOWNWARD, which is
     the sign of a recess, not of a lift. Left inverted, every card in
     #hero, #one-integration, #marketing and #contact read as a hole.

     WHAT IT IS NOW. Three changes, and they are re-made rather than
     recoloured:

       1. THE ALTERNATION FLIPS DIRECTION. On light the page is paper and the
          alternate band is a TINT, so the sets swap: the four sections that
          carried --c-surface now stay on the page's own --c-bg, and the
          three that carried --c-bg take --c-sunken (§ 5b). #marketing is
          still the lighter of it and its two neighbours, which is what § 13
          requires, and the rhythm is unchanged band for band.
       2. THERE IS ONE RAISED TONE, NOT TWO. Every card, panel, node box and
          log frame on the page is --c-surface #FFFFFF, on both band tones.
          --c-surface-2 is DELETED: with a single raised tone it had no
          consumer left that was not a restatement of --c-surface.
       3. DEPTH CHANGES SOURCE. It is carried by --shadow-raised below, not
          by a fill step. That is the whole reason one raised tone is enough.

     --c-surface-3 survives as the HOVER tone and nothing else, and it moved
     up two steps from the owner's starting value. #ECECE9 was measured at
     4.45:1 against --c-text-3 #656C72 — a fail, and --c-text-3 is exactly
     what a hovered capability card sets its step number, its DETAIL toggle
     and its detail sentence in. #F7F7F5 measures 4.91:1.

     Measured on the light ramp, --c-text-3 #656C72 being the binding case
     everywhere because it is the palest text the page uses at body size:
       on --c-bg      #FBFBFA   5.09:1      on --c-surface   #FFFFFF  5.27:1
       on --c-sunken  #F1F1EF   4.66:1      on --c-surface-3 #F7F7F5  4.91:1
     --c-text-2 #4C5359 is 8.0-9.1:1 across the same four and --c-text is
     18.1-20.5:1; the full table is in docs/final-phase1-notes.md.       -- */
  --c-surface-3:   #F7F7F5;

  /* Card edge. --c-border is 12% ink on this palette and measures 1.15:1 on
     paper — the same rumour 8% white was on the dark ground, and the same
     reason the bento would read as a table. 14% ink is 1.19:1 on --c-bg and
     1.24:1 on --c-sunken. That looks like a smaller number than the 1.75:1
     the white edge scored, and it is not a regression: on light the card
     also carries --shadow-raised, so the edge no longer has to be the only
     thing separating the card from the band. It is DECORATIVE either way —
     a card is not a control, and the interactive thing inside it
     (.cap-card__toggle) carries its own text contrast and its own focus
     ring. */
  --c-border-card: rgba(11, 13, 15, 0.14);

  /* Context variables. --c-raised is the page's ONE raised tone and is now
     the same on every band, which is why § 5b no longer re-points it; the
     alternation block there sets the band's own ground and nothing else.
     #marketing still re-points --c-edge-hover so its cards never borrow the
     payment accent. Every raised surface in the file reads --c-raised rather
     than naming a ground directly. */
  --c-raised:       var(--c-surface);
  --c-raised-hover: var(--c-surface-3);
  --c-edge-hover:   var(--c-accent);

  /* -- DEPTH. THE ONE THING THAT IS NOT A RECOLOUR --------------------------
     On a dark ground layers separate by FILL: a raised thing is lighter, and
     a 1px inset white top line (--bevel-top, now deleted) reads as its edge
     catching the light. Neither works on paper. White has no step above it,
     and an inset white highlight on a white card is invisible.

     So depth is a SHADOW here, and it is a cool one: the ink is #171A2B —
     the palette's near-black pulled toward the accent's blue — rather than
     neutral black, because a warm or neutral shadow under a #4436D6 accent
     reads as dirt. Two layers, a 1px contact shadow and a soft 16px spread,
     which is what stops a card looking pasted on.

     IT DOES NOT ANIMATE, AND THAT IS DELIBERATE. § 16's contract lists the
     properties this page is allowed to transition and box-shadow is named as
     one it may never touch. A hovered card therefore keeps this shadow
     unchanged and states its hover with the three carriers it already had:
     the ground, the edge colour and the 3px lift.

     --shadow-overlay comes from palette C and is used, at last, on the two
     elements that genuinely float: the skip link (§ 4) and the mobile nav
     panel (§ 6). --shadow-nav is the sticky header's own, and only once
     scrolled — on paper a translucent white bar over white content has no
     edge but its hairline. */
  --shadow-raised: 0 1px 2px rgba(23, 26, 43, 0.05),
                   0 6px 16px -6px rgba(23, 26, 43, 0.10);
  --shadow-nav:    0 4px 16px -8px rgba(23, 26, 43, 0.14);

  /* -- MARKETING SERVICES — the second accent, and the only one -----------
     `styles/tokens.css` says the page has exactly one accent. That rule is
     about the PAYMENT PLATFORM and still holds — one hue, never a second —
     but iteration 2 deliberately widened where it appears, so the old
     "three uses and a fourth is a bug" wording no longer describes the file.

     --c-accent is now the single signal for "this is the live/active thing":
     the platform CTA, the focus ring, the live element of the flow diagram,
     the active nav item and progress line, the active row of the attempt
     log, a focused field or checked box, an inline link, and the card edge
     on hover. Adding a SECOND HUE is the bug; adding another state that
     means "active" is not.

     ITERATION 4 REMOVED ONE MEMBER OF THAT SET, and it is worth recording
     why. The code panel's selected-tab underline used to be indigo. It is
     now --c-text-2 (§ 12): it was the only resting indigo below the fold in
     the bottom third of the page, it is the one that sat closest to
     #marketing's ochre rule, and selection was never carried by the colour
     alone — the selected tab also takes --c-text where the others take
     --c-text-3. Dropping it is what let #one-integration's oversized
     padding guard go (§ 5b) without re-introducing a collision.

     Count before you edit: `grep -n 'var(--c-accent)' styles.css` outside
     the two alternate-palette blocks resolves in 16 declarations, all of
     them that one meaning.

     #marketing is a different line of business, not another platform
     feature, and the cleanup spec requires it to carry "its own eyebrow
     colour, not the payment accent". So it gets its own accent, and the
     budget for that accent is likewise exactly three uses, all of them
     inside #marketing: the eyebrow, the full-bleed rule above the section,
     and the section's own CTA fill. A use outside #marketing is a bug.

     Warm on purpose. The platform vocabulary is cool (indigo accent) and
     the status vocabulary is green / amber / red; a low-chroma warm ochre is
     the one region of the wheel that reads as neither.

     IT INVERTED, AND IT IS THE WORST OF THE 28. The ochre this section used
     was a light tint built to sit on near-black: #D8B48C measures 1.70:1 on
     white. On a light palette the whole family has to cross the ground — a
     warm accent on paper is a DARK ochre, not a pale ochre — so all five
     values are re-derived and --c-marketing-ink flips from near-black to
     white. The hue is held: #D8B48C is 32 degrees of hue in OKLCH and
     #8C5A22 is 33, so the section reads as the same warm signal it always
     did, and it is still the one region of the wheel neither the indigo
     accent nor the three status colours occupy.

     Measured on the light palette, on the two grounds it can land on and as
     a fill behind --c-marketing-ink:
       #8C5A22 on --c-bg      #FBFBFA    5.15:1   (AA text, 1.4.11 object)
       #8C5A22 on --c-sunken  #F1F1EF    4.72:1   (AA text, 1.4.11 object)
       #FFFFFF on #8C5A22                5.85:1   (AA text, CTA label)
       #75491B on #FBFBFA                6.86:1   (hover fill vs ground)
       #FFFFFF on #75491B                7.78:1   (AA text, CTA hover)
       #5E3A15 on #FBFBFA                9.12:1   (active fill vs ground)
       #FFFFFF on #5E3A15               10.35:1   (AA text, CTA pressed)
     The focus ring never lands ON a --c-marketing fill: the 2px
     outline-offset puts the section ground between the two, exactly as it
     does over the indigo CTA — ring vs ground 7.39:1, ground vs fill 5.15:1.
     Both legs clear 1.4.11.
     Re-run docs/research/contrast.py before substituting any of them. --- */
  --c-marketing:        #8C5A22;
  --c-marketing-hover:  #75491B;
  --c-marketing-active: #5E3A15;
  --c-marketing-ink:    #FFFFFF;  /* label ON a --c-marketing fill */
  /* Outline around the marketing card block. DECORATIVE (1.88:1 composited
     over a --c-surface card, as #CEBAA2) — it tints the grid, it never
     carries the boundary of a control. The alpha is up from 32% to 42%
     because the ink is now going ONTO white rather than a pale tint going
     onto near-black, and 32% of #8C5A22 on white was 1.55:1. */
  --c-marketing-edge:   rgba(140, 90, 34, 0.42);

  /* -- SVG label sizes. User units inside the two authored viewBoxes, not
        CSS layout px, so they are not on the type scale. ---------------- */
  --svg-label:      13px;
  --svg-sub:        11px;
  /* The horizontal geometry draws its state glyph at --svg-label, which is
     fine at the authored scale and collides with the node's own label once
     the hero enlarges that label (§ 9). This is the glyph's own size, used
     only there. */
  --svg-glyph:      14px;

  /* The hero's vertical canvas is authored 360x560; unstyled it was 739px of
     hero before the copy was counted. This is the cap that brings it back
     under half a small viewport (§ 9). */
  --flow-hero-cap:  24rem;

  /* -- motion distances. Every one is a figure the design system § Motion
        names by hand; they live here so no transform carries a raw px. --- */
  --nudge-press:     1px;   /* button :active translateY */
  /* --nudge-lift (2px) removed in iteration 2: the card hover rise is now
     --nudge-card and nothing else used it. */
  --rise-reveal:    12px;   /* scroll reveal */
  --rise-step:       4px;   /* attempt-log row stepping in */
  --rise-panel:      8px;   /* mobile nav panel entrance */
  --burger-shift:  3.5px;   /* the two nav bars folding into an X */
  --nudge-card:      3px;   /* card hover rise — the iteration-2 brief asks
                               for 2-3px; 3px so the lift is legible against
                               the new card edge */

  /* The card-hover duration. tokens.css offers 140ms and 240ms and the brief
     asks for 150-200ms, so the one value between them lives here. */
  --dur-lift:      180ms;

  /* -- ITERATION 5: THE DECORATIVE LAYER ---------------------------------
     Every value the five new behaviours need. They are tokens rather than
     literals for one reason beyond tidiness: #marketing re-points four of
     them on the section itself (§ 13), which is the whole of task 2.8 —
     "the same techniques at a different tempo" costs four declarations and
     not one duplicated rule.

     NOTHING HERE IS A NEW HUE. --c-spot-edge below is neutral ink at an
     alpha that reads as shade, not as pigment; --c-texture is the same ink
     at an alpha under the --c-glow ceiling. (--c-spot, the interior wash,
     is deleted on this palette — the arithmetic is on --c-spot-edge.) The
     accent budget (§ 1b above: indigo three-and-a-bit uses, ochre three
     inside #marketing) is untouched, and the accent-collision sweep was
     re-run in the new palette rather than inherited: a 150px-step scroll
     sweep at 1440x900, 1024x1366, 1440x700 and 390x844, reading every
     painted colour in the viewport out of the CSSOM, found ZERO frames
     containing both hues. Result in docs/final-phase1-notes.md. -------- */

  /* 2.1 THE INTEGRATION DIAGRAM. Every path carries pathLength="1", so the
     dash range is 0 -> 1 on all 26 of them whatever their real length: one
     dash and one gap of 1 unit each, offset 1 = hidden, offset 0 = drawn.
     No getTotalLength(), no measurement pass, no per-path inline style.
     --draw-step is multiplied by data-int-order (1..7), so the wiring
     assembles source-first instead of appearing all at once. */
  --dur-draw:      620ms;
  --draw-step:      60ms;
  /* The fraction of the draw after which a link's arrowhead is allowed to
     appear. marker-end is not affected by stroke-dashoffset, so without
     this the 13 arrowheads would sit at their targets before the lines
     reached them. */
  --draw-head:      0.72;

  /* 2.3 THE POINTER SPOTLIGHT — RE-MADE, NOT INVERTED.
     WHAT IT WAS. Two layers: an interior wash of white at 3.5% that followed
     the pointer around inside the card, and a 1px inner edge that brightened
     on the side the pointer was on.

     THE INTERIOR WASH IS DELETED, AND THE REASON IS A MEASUREMENT. Inverting
     it means darkening the card interior, and the card interior is where
     --c-text-3 sets the step number, the DETAIL toggle and the detail
     sentence. The hover ground is --c-surface-3 #F7F7F5, where --c-text-3
     already sits at 4.91:1 — 0.41 of headroom, against the 1.92 it had on
     the dark palette. Composited, an ink wash over that ground gives:
       2.0%  #F5F5F3   4.83:1        4.0%  #EFEFED   4.59:1
       2.5%  #F3F3F1   4.76:1        5.0%  #ECECEA   4.47:1  FAIL
     Every alpha strong enough to be seen spends headroom that AA does not
     have to give, and the strongest one is already a fail. A wash that must
     be invisible to be legal is not an effect.

     WHAT REPLACES IT IS THE THING THE EFFECT ALWAYS WAS. On paper a pointer
     does not add light, it deepens shade, and shade collects at an EDGE. So
     only the 1px inner ring survives, in ink instead of white, and it is
     still centred on the pointer — so the card's inside edge darkens on the
     side the pointer is on and fades away from it. It sits in the card's
     padding, never under a glyph, so it costs no text pair anything.
     Measured: 50% ink over a --c-surface card composites to #818282 and is
     3.61:1 against the card it outlines, over the 3.0 that 1.4.11 asks of a
     graphical object — and over the 3.75:1 the white ring scored on dark.
     Deleted with the wash: --c-spot, .cap-card::before, and the `isolation`
     and `overflow` that existed only to clip it. */
  --spot-size:      22rem;
  --c-spot-edge:    rgba(11, 13, 15, 0.50);
  --dur-spot:       260ms;

  /* 2.4 PER-LINE HEADING REVEAL. --line-bleed is descender and ascender
     room inside the mask: it is added as padding and taken straight back
     as a negative margin, so a split heading occupies exactly the box an
     unsplit one does and the reveal cannot move anything. --lh-h2 is 1.08,
     which is tighter than Geist's own ascent+descent, so without it the
     mask clips the tops of capitals and the tails of descenders. */
  --dur-line:       560ms;
  --stagger-line:    70ms;   /* the task's 60-80ms band */
  --line-bleed:    0.16em;

  /* -- THE HERO BACKDROP, RE-DERIVED FOR WHITE ---------------------------
     Two layers, the glow and the texture, and both had to move.

     THE GLOW. Palette C's --c-glow is the accent at 5%. On paper that is a
     faint cool tint, which is right, but it is a tint that DARKENS, and it
     lands under .hero__eyebrow, which is --c-text-3 — the tightest text pair
     on the page. Composited over --c-bg the eyebrow reads 4.70:1 at 5% and
     4.82:1 at 4%, and the texture then takes another 0.2 off whichever it
     is. 4% is the value that leaves the stack a real margin rather than a
     rounding error, so the token is re-pointed here. Palette C's own
     declaration is untouched; this is the second layer, which is where a
     composite decision belongs. There is no dark patch anywhere in the band:
     the composite is #F5F4F9 at the centre of the ellipse and dissolves to
     the ground at its edge.

     THE TEXTURE. A grid, not a gradient, because a gradient at an alpha this
     low is invisible and at a visible alpha it is a shape. The drift is
     exactly one cell per cycle on the diagonal, which is what makes the loop
     seamless with no fade and no reversal. 72px in 64s is 1.1px per second —
     below the threshold at which a static observer reads it as movement,
     which is the point. It was white at 1.8%; on paper it is ink,
     and 1.8% of ink is not the same whisper as 1.8% of white. Screenshotted
     at 1440, ink at 2.0% is a legible grid across the top of the band — the
     "layout guide someone forgot to switch off" the iteration-5 author
     already had to walk back once at 2.8%. 1.3% is where it goes back to
     being a surface. Ink reads heavier than white at the same alpha because
     the ground it sits on is at the top of the luminance range rather than
     the bottom, so the same alpha buys a larger step.

     --texture-cell WAS NOT APPLYING AT ALL BEFORE THIS RUN, in either
     palette, and the cause is worth recording because it is the same trap
     § 1 warns about. The comment that used to stand here closed itself
     halfway through, so its second half parsed as a declaration prelude and
     swallowed the next declaration whole. Read back out of Chrome,
     --texture-cell resolved to the empty string: background-size was
     invalid, the two gradients tiled at the element's own size, and the
     hero's "texture" was one 1px line down the left of a box inset -10%,
     i.e. off screen. The drift keyframe was invalid for the same reason.
     Fixed here, and the fix is visible: the grid now paints.            -- */
  --c-glow:          rgba(68, 54, 214, 0.04);
  --texture-cell:    72px;
  --c-texture:       rgba(11, 13, 15, 0.013);
  --dur-drift:        64s;

  /* 2.7 THE SECTION RAIL. --rail-tick is the ACTIVE tick length; the
     resting one is that scaled on the x axis, so the state change is a
     transform and never a width. --rail-inset is measured against the free
     left margin in § 6b. */
  --rail-tick:       28px;
  --rail-tick-rest:  0.5;
  --rail-inset:      var(--sp-md);

  /* 2.9 MAGNETIC BUTTONS. The ceiling the JS agent clamps to. The element's
     box, its padding and its min-height are untouched — only where the
     finished box is painted moves, and hit-testing moves with it, so the
     target is never smaller or in a different place than it looks. */
  --mag-max:          4px;

  /* 2.8 THE MARKETING TEMPO. Applied by re-pointing the tokens above on
     #marketing itself (§ 13), so the section runs the same five behaviours
     more slowly and further apart without one duplicated rule. --stagger
     stays at 60ms for the platform, inside task 2.5's 50-70ms band; this
     is deliberately outside it because task 2.8 asks for longer gaps. */
  --stagger-slow:   112ms;
  --dur-entrance-slow: 640ms;
  --rise-reveal-slow: 18px;  /* against --rise-reveal's 12px */

  /* -- MEASURE OVERRIDE ---------------------------------------------------
     tokens.css sets --measure: 68ch. `ch` is the advance of "0", and Geist's
     "0" is wide relative to its lowercase, so 68ch renders as ~77 characters
     of English prose, not 68 — measured in the browser at 1440 on
     .lede, .prose and .cap-card__body. The iteration-2 brief caps a
     paragraph at ~70 rendered characters, so the token is re-pointed here,
     outside the frozen copy, at the value that measures ~66-70 characters at
     every breakpoint. styles/tokens.css is unchanged; this is an addition,
     not an edit to the copy.

     Measured in Chrome at 1440 across every paragraph class on the page:
       1ch                     = 0.6629em   (Geist's "0" is a wide figure)
       mean rendered character = 0.4402em
       widest-setting block    = 0.4578em/char
       tightest-setting block  = 0.4168em/char  <- the binding case
     44ch is 29.17em, so the tightest-setting paragraph on the page lands at
     exactly 70 characters and the mean lands at 66. 68ch would have been 45em
     and 108 characters in that same block. */
  --measure:        44ch;
}


/* ==========================================================================
   ==========================================================================
   2. RESET
   ========================================================================== */

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

* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  /* Anchor targets clear the sticky header (WCAG 2.2 · 2.4.11). */
  scroll-padding-top: var(--sp-4xl);
}

body { min-height: 100vh; }

/* The one rule in this file kept without a match in index.html. It is a
   reset-level guard, not section CSS: the page ships no raster media today,
   and this is what stops the first <img> anyone adds from overflowing the
   viewport at 360px. Every OTHER selector below is matched by something in
   index.html — see docs/cleanup-css.md. */
img,
picture,
video,
canvas {
  display: block;
  max-width: 100%;
}

input,
button,
textarea,
select {
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
}

button { background: none; border: 0; }

ul[class],
ol[class] { list-style: none; padding: 0; }

table { border-collapse: collapse; }

/* `hidden` is the page's single source of truth for "not rendered". The
   fragments that rely on it are the diagram's detail panels; the code
   panel's tab panels went with the panel in iteration 5 and the contact
   form's success card went with the form in phase 2. It must still beat any
   display value a component sets. */
[hidden] { display: none !important; }


/* ==========================================================================
   ==========================================================================
   3. BASE — elements and typography
   ========================================================================== */

body {
  background: var(--c-bg);
  color: var(--c-text);
  font-family: var(--ff-sans);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-body);
  font-weight: var(--fw-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-synthesis-weight: none;
}

h1, h2, h3, h4 {
  color: var(--c-text);
  text-wrap: balance;
  overflow-wrap: break-word;
}

h1 {
  font-size: var(--fs-display);
  line-height: var(--lh-display);
  letter-spacing: var(--ls-display);
  font-weight: var(--fw-display);
}

h2 {
  font-size: var(--fs-h2);
  line-height: var(--lh-h2);
  letter-spacing: var(--ls-h2);
  font-weight: var(--fw-h2);
}

h3 {
  font-size: var(--fs-h3);
  line-height: var(--lh-h3);
  letter-spacing: var(--ls-h3);
  font-weight: var(--fw-h3);
}

h4 {
  font-size: var(--fs-h4);
  line-height: var(--lh-h4);
  letter-spacing: var(--ls-h4);
  font-weight: var(--fw-h4);
}

p { text-wrap: pretty; overflow-wrap: break-word; }

/* Inline links in prose. Design system § Links.

   `:not(:where(.btn))` on every one of these, and it is load-bearing, not
   tidiness. `:where()` is what keeps the guard FREE: bare `:not(.btn)` adds
   `.btn`'s (0,1,0) to the selector, which lifts `li a` from (0,0,2) to
   (0,1,2) and silently beats every single-class `text-decoration: none` in
   the file — `.nav__link`, `.nav__panel-link` and `.footer__link` all grew
   underlines the first time this was written that way. `:where()` weighs
   nothing, so these selectors keep the exact specificity they had.
   Two CTAs on the page are `<a class="btn btn--primary">` wrapped in a
   `<p class="steps__cta">` — #marketing's "Discuss a marketing brief" and
   #getting-started's "Talk to our team". `p a:hover` is (0,1,2) and `.btn`
   is (0,1,0), so without the guard the prose hover colour overrode
   `--btn-fg` and painted the LABEL in the link accent while the FILL stayed
   the button's. Measured in headless Chrome with the pseudo-state forced:

     #getting-started  hover  #3629B4 on #3629B4  1.00:1  (label invisible)
     #getting-started  active #2B1F9B on #2B1F9B  1.00:1  (label invisible)
     #marketing        hover  #3629B4 on #75491B  1.28:1
     #marketing        active #2B1F9B on #5E3A15  1.18:1

   The #marketing pair was also an accent collision — indigo text sitting on
   a --c-marketing fill, the one thing § 13 exists to prevent.
   After the guard: 9.84 / 11.85 on the indigo CTA and 7.71 / 10.04 on the
   marketing one, which are the numbers the button tokens were designed for.
   (Re-measured on the light palette. The pairs are worse without the guard
   than they were on the dark one — 1.28:1 and 1.18:1 against 1.65 and 1.58 —
   so the guard is more load-bearing now, not less.)
   A button is not prose; it takes its colour from --btn-fg alone. */
a { color: var(--c-accent); }

p a:not(:where(.btn)),
li a:not(:where(.btn)),
address a:not(:where(.btn)) {
  text-decoration: underline;
  text-underline-offset: 0.18em;
  text-decoration-thickness: 1px;
  text-decoration-color: color-mix(in oklab, currentColor 40%, transparent);
}

@media (hover: hover) {
  p a:not(:where(.btn)):hover,
  li a:not(:where(.btn)):hover,
  address a:not(:where(.btn)):hover {
    color: var(--c-accent-hover);
    text-decoration-color: currentColor;
  }
}

p a:not(:where(.btn)):active,
li a:not(:where(.btn)):active,
address a:not(:where(.btn)):active { color: var(--c-accent-active); }

/* Inline <code> in body copy. Design system § Mono usage rules. */
code {
  font-family: var(--ff-mono);
  font-size: var(--fs-mono-sm);
  letter-spacing: var(--ls-mono);
  color: var(--c-text);
  background: var(--c-sunken);
  border-radius: var(--r-xs);
  padding-inline: 0.15em;
}

address { font-style: normal; }

caption { text-align: left; }

::selection {
  background: var(--c-accent-muted);
  color: var(--c-text);
}

/* --------------------------------------------------------------------------
   FOCUS — one rule, applied everywhere, never removed.
   2px --c-focus with a 2px offset. The offset is load-bearing: it puts the
   parent surface between the ring and the element's own fill, which is what
   makes the ring legal over the accent-filled primary button (measured
   6.05–6.37:1 for the gap against the fill, 7.94–8.56:1 for the ring against
   every surface it can land on). `outline: none` appears nowhere in this file
   without an equivalent replacement named in a comment beside it.
   -------------------------------------------------------------------------- */
:focus-visible {
  outline: var(--bw-focus) solid var(--c-focus);
  outline-offset: var(--focus-offset);
  border-radius: inherit;
}

/* Text inputs treat :focus and :focus-visible identically (design system
   § Form fields), because a click into a field is a focus the user wants
   confirmed. */
input:focus,
textarea:focus,
select:focus {
  outline: var(--bw-focus) solid var(--c-focus);
  outline-offset: var(--focus-offset);
}

/* Every anchor target and every focusable clears the sticky header. */
[id] { scroll-margin-top: var(--sp-4xl); }


/* ==========================================================================
   ==========================================================================
   4. UTILITIES — build-contract §2. Markup agents use these and must not
   invent alternatives.
   ========================================================================== */

/* -- .sr-only --------------------------------------------------------- */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* -- .skip-link — first focusable element in <body> -------------------- */
.skip-link {
  position: absolute;
  inset-inline-start: var(--sp-md);
  inset-block-start: var(--sp-md);
  z-index: var(--z-skip);
  display: inline-flex;
  align-items: center;
  min-height: var(--h-control-sm);
  padding-inline: var(--sp-md);
  /* It lands over #hero. On the light palette #hero is the paper tone and
     the chip is the raised one, so the step is the right way round again
     without needing a second step — and the shadow is what actually says
     "over", which is a thing no fill could say on the dark palette. */
  background: var(--c-surface);
  box-shadow: var(--shadow-overlay);
  color: var(--c-text);
  border: var(--bw-hairline) solid var(--c-border-strong);
  border-radius: var(--r-md);
  font-size: var(--fs-body-sm);
  text-decoration: none;
  /* Off-screen until focused. Clipped, not display:none, so it stays
     focusable and is the first stop in the tab order. */
  transform: translateY(calc(-100% - var(--sp-2xl)));
}
.skip-link:focus-visible { transform: none; }

/* -- .eyebrow ---------------------------------------------------------- */
.eyebrow {
  font-family: var(--ff-mono);
  font-size: var(--fs-mono-sm);
  line-height: var(--lh-mono);
  letter-spacing: var(--ls-mono);
  font-weight: var(--fw-mono);
  text-transform: uppercase;
  color: var(--c-text-3);
  text-wrap: pretty;
}

/* -- .lede ------------------------------------------------------------- */
.lede {
  font-size: var(--fs-body-lg);
  line-height: var(--lh-body-lg);
  letter-spacing: var(--ls-body-lg);
  color: var(--c-text-2);
  max-width: var(--measure);
  text-wrap: balance;
}

/* -- .prose ------------------------------------------------------------ */
.prose {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-body);
  color: var(--c-text-2);
  max-width: var(--measure);
  text-wrap: pretty;
}

/* -- .mono ------------------------------------------------------------- */
.mono {
  font-family: var(--ff-mono);
  font-size: var(--fs-mono-sm);
  line-height: var(--lh-mono);
  letter-spacing: var(--ls-mono);
  font-weight: var(--fw-mono);
  font-variant-numeric: tabular-nums;
}

/* -- horizontal scroll containers -------------------------------------- */
/* Wide content scrolls inside its own box; the page body never does. A thin
   token-coloured scrollbar is the affordance that says so — without it the
   clipped right edge of a table or a code block reads as truncation rather
   than as "there is more this way". */
.demo__log,
.flow__canvas {
  scrollbar-width: thin;
  scrollbar-color: var(--c-border-strong) transparent;
}

/* -- .card ------------------------------------------------------------- */
/* --c-raised, not --c-surface directly: every raised surface on the page
   reads the context variable, which is what let the dark palette re-point it
   per band and what keeps one declaration correct now that the light palette
   has a single raised tone. See § 5b.
   The shadow is not decoration here — it is the whole of the elevation on a
   light ground, where the fill step from the band is 1.6% at most. */
.card {
  background: var(--c-raised);
  border: var(--bw-hairline) solid var(--c-border-card);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-raised);
  padding: var(--sp-lg);
}
@media (min-width: 900px) {
  .card { padding: var(--sp-xl); }
}

/* -- .btn -------------------------------------------------------------- */
/* Component-scoped variables, per design system § Token naming. A variant
   sets --btn-*; it never introduces a new global token. */
.btn {
  --btn-bg: transparent;
  --btn-fg: var(--c-text);
  --btn-border: transparent;

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-xs);
  min-height: var(--h-control);
  padding-inline: var(--sp-lg);
  border: var(--bw-hairline) solid var(--btn-border);
  border-radius: var(--r-md);
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body-sm);
  letter-spacing: -0.006em;
  font-weight: var(--fw-medium);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
}

.btn--primary {
  --btn-bg: var(--c-accent);
  --btn-fg: var(--c-accent-ink);
  --btn-border: transparent;
}
.btn--secondary {
  --btn-bg: transparent;
  --btn-fg: var(--c-text);
  --btn-border: var(--c-border-strong);
}
/* .btn--ghost — the third variant — WENT WITH THE CODE PANEL in iteration 5.
   Its only instance on the page was the panel's Copy button, so its base
   rule, its hover, its press and its two disabled rules were all deleted
   rather than left matching nothing.

   PHASE 2 BROUGHT A COPY CONTROL BACK — the Copy address button in #contact —
   and it did NOT bring this variant back with it. It takes .btn--secondary,
   which is the right weight for a control that sits beside the address
   rather than instead of it, and which already has the hover, press, focus
   and disabled states declared above. Nothing here needs re-declaring; the
   button's own two rules are in § 15a. */

@media (hover: hover) and (pointer: fine) {
  .btn--primary:hover   { --btn-bg: var(--c-accent-hover); }
  .btn--secondary:hover { --btn-bg: var(--c-overlay-hover); --btn-border: var(--c-border-hover); }
}

.btn--primary:active   { --btn-bg: var(--c-accent-active); transform: translateY(var(--nudge-press)); }
.btn--secondary:active { --btn-bg: var(--c-overlay-active); transform: translateY(var(--nudge-press)); }

.btn--secondary:focus-visible { --btn-border: var(--c-accent); }

/* -- the disabled state -------------------------------------------------
   WHERE THIS APPLIES, AS OF PHASE 2: NOWHERE. No control on the page can
   reach it. That is a fact about the markup, not a reason to delete the
   rule — read the next two paragraphs before touching it.

   Until phase 2 exactly one control reached it: the contact form's submit
   button, while a submission was in flight (script.js setLoading()). The
   form is gone and its module is mothballed (script.js § 3.8, styles.css
   § 15b), so the state is mothballed with it and comes back the moment the
   form does. The disabled state of a shared button component is also the
   one state a stylesheet must not be missing when a control that needs it
   is added, so it stays declared either way.

   It used to apply to a second control, and the note that used to be here
   described that one at length: the failover demo disabled "Decline the
   first provider" for the length of the sequence. IT NO LONGER DOES. The JS
   agent removed the disabled window in iteration 4 (option (a): the control
   is live from load and a press during a run restarts it), the markup
   carries no `disabled` attribute, and this file must not paint one — the
   running treatment is keyed off `[data-demo-state]` in § 9 instead. Nothing
   about the rule below changes; only the sentence describing when a reader
   sees it, which was documenting behaviour that no longer happens.

   The rule was REWRITTEN in the phase-5 pass. It previously used
   --c-text-disabled (#A8AEB3 — 2.24:1 on the raised surface) over a
   --c-border hairline (1.30:1), which is legal under 1.4.3 (disabled
   controls are exempt) but was wrong here for two reasons:

     1. Submitting a form is a state a reader sits and looks at, not a
        corner case. At 2.32:1 the label was not readable and the control
        read as broken rather than as busy.
     2. The hairline edge at 1.30:1 left the button with no discernible
        boundary at all, which 1.4.11 does not exempt: "disabled" excuses
        the text, it does not excuse losing the component.

   So: the label drops to --c-text-3 (5.33:1 on the raised surface, 5.15:1
   on bg — comfortably AA, and unmistakably dimmer than the enabled --c-text
   at 19.47/18.80:1), and the edge goes to a DASHED --c-border-strong
   (3.90:1 / 3.87:1 — clears 1.4.11). The dash is the non-colour carrier:
   solid edge = available, dashed edge = unavailable, which survives
   greyscale, forced-colours and a colour-blind reader.
   ------------------------------------------------------------------- */
.btn:disabled,
.btn[aria-disabled="true"] {
  --btn-fg: var(--c-text-3);
  --btn-border: var(--c-border-strong);
  border-style: dashed;
  cursor: not-allowed;
  transform: none;
}
.btn--primary:disabled,
.btn--primary[aria-disabled="true"] {
  --btn-bg: var(--c-raised);
}
.btn--secondary:disabled,
.btn--secondary[aria-disabled="true"] {
  --btn-bg: transparent;
}
/* A disabled control does not respond to the pointer. Declared after the
   hover block above so it wins for both variants. */
@media (hover: hover) and (pointer: fine) {
  .btn:disabled:hover,
  .btn[aria-disabled="true"]:hover {
    --btn-fg: var(--c-text-3);
    --btn-border: var(--c-border-strong);
  }
  .btn--primary:disabled:hover,
  .btn--primary[aria-disabled="true"]:hover { --btn-bg: var(--c-raised); }
  .btn--secondary:disabled:hover,
  .btn--secondary[aria-disabled="true"]:hover { --btn-bg: transparent; }
}

/* -- .chip ------------------------------------------------------------- */
/* Status pills. Never interactive. The leading glyph is what carries the
   state when colour is unavailable. */
.chip {
  --chip-fill: var(--c-sunken);
  --chip-fg: var(--c-text-2);

  display: inline-flex;
  align-items: center;
  min-height: var(--target-min);
  padding-inline: var(--sp-xs);
  border-radius: var(--r-pill);
  background: var(--chip-fill);
  color: var(--chip-fg);
  font-family: var(--ff-mono);
  font-size: var(--fs-mono-sm);
  line-height: var(--lh-mono);
  letter-spacing: var(--ls-mono);
  font-weight: var(--fw-mono);
  text-transform: uppercase;
  white-space: nowrap;
}

.chip--ok   { --chip-fill: var(--c-ok-muted);   --chip-fg: var(--c-ok); }
.chip--warn { --chip-fill: var(--c-warn-muted); --chip-fg: var(--c-warn); }
.chip--err  { --chip-fill: var(--c-err-muted);  --chip-fg: var(--c-err); }

.chip__glyph {
  margin-inline-end: var(--sp-2xs);
  font-size: 0.85em;
  line-height: 1;
  color: currentColor;
}


/* ==========================================================================
   ==========================================================================
   5. LAYOUT PRIMITIVES
   ========================================================================== */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
/* --container-wide is a token in the frozen tokens.css copy and stays there;
   the one element that used the modifier (the coverage explorer) is gone. */
.container--narrow { max-width: var(--container-narrow); }

.section {
  position: relative;
  padding-block: var(--section-y);
}
/* .section--tight went with #boundary in iteration 5 — it was the only
   element that carried the modifier. The TOKEN it read, --section-y-tight,
   stays: it is in the frozen tokens.css mirror and .footer still uses it. */
/* .section--loose NO LONGER OPENS WIDER (iteration 5, seam pass).

   #how-it-works is its only carrier, and the 160px opening it bought at 1440
   was the single largest piece of the page's largest ordinary seam: 303px
   between the last glyph of #hero and the first of #how-it-works, of which
   112px is #hero's tail and 160px was this. Nothing sits in that opening —
   no rule, no full-bleed edge, no overlapping object — so it was 48px of
   pure surplus over the distance every other section already opens on.

   The modifier is kept rather than deleted because the markup carries it and
   this file does not own index.html; it now states the page rhythm instead
   of a louder one, which is the same thing .section already says and is
   therefore a no-op that documents itself. The TOKEN it used to read,
   --section-y-loose, is still live — #getting-started opens on it (§ 5b) and
   it is in the frozen tokens.css mirror. */
.section--loose { padding-block: var(--section-y); }

/* --------------------------------------------------------------------------
   THE HEAD-TO-BODY STEP. Fixed in the phase-5 pass.

   It was --sp-3xl (64px) while every other block separation inside a section
   — .statement, .demo, .section__note, .steps__closing — is --sp-2xl (48px).
   One 64px step in a page whose rhythm is otherwise 48px does not read as
   emphasis, it reads as a missing element between the subhead and the body,
   which is exactly the complaint. It is now --sp-2xl like the rest, and the
   ONE fragment that carried its own top margin as well (.flow) is set to the
   same value so the collapsed margin is 48px there too rather than 64px.
   -------------------------------------------------------------------------- */
.section__head {
  display: grid;
  gap: var(--sp-md);
  margin-block-end: var(--sp-2xl);
  max-width: var(--container-narrow);
}
.section__head .eyebrow { margin-block-end: calc(var(--sp-xs) * -1); }

.section__title {
  font-size: var(--fs-h2);
  line-height: var(--lh-h2);
  letter-spacing: var(--ls-h2);
  font-weight: var(--fw-h2);
  color: var(--c-text);
  text-wrap: balance;
}

/* -- .line / .line__inner — TASK 2.4, the per-line mask -----------------
   These two elements do not exist in index.html. script.js builds them at
   runtime inside the six [data-split-lines] headings, one pair per rendered
   line, after document.fonts.ready and again on a debounced resize — the
   markup handoff (§ 6.1) explains at length why a static split cannot be
   right at more than one column width, and its own measurements show four
   of the six headings changing line count between 1440 and 768.

   WHAT LIVES HERE AND WHAT LIVES IN § 16. The mask is structure and is
   declared here, unconditionally: a reader on reduced motion, a reader with
   JS off and a reader in the window before `defer` runs all get identical,
   complete, upright text. The RISE is in § 16 and nowhere else. There is no
   state in this file in which a line is hidden — .line__inner has no
   opacity and no transform of its own outside the motion gate — so the
   reveal cannot strand a heading invisible, which is the same contract
   [data-reveal] has held since iteration 2.

   ZERO LAYOUT SHIFT, AND HOW. --line-bleed is added as padding on both
   block edges and taken straight back as an equal negative margin, so each
   line box advances by exactly its own height and a split heading measures
   the same as the unsplit one it replaced. The padding is what stops
   `overflow: hidden` clipping capitals and descenders at --lh-h2's 1.08,
   which is tighter than the font's own ascent plus descent.

   Measured, real Chrome, the six headings at 360 / 768 / 1024 / 1440 with
   the split applied against the same page without it: 0px of height
   difference on every heading at every width, and 0 change in line count.
   ------------------------------------------------------------------- */
.line {
  display: block;
  overflow: hidden;
  padding-block: var(--line-bleed);
  margin-block: calc(var(--line-bleed) * -1);
}
.line__inner { display: block; }

/* -- .section__note ----------------------------------------------------
   A standalone statement that closes a section: the degradation fallback in
   §2 and the coverage replacement in §3. Not a second prose paragraph and
   not fine print — it sits at body size, separated by the section rhythm,
   with a leading rule so it reads as an aside rather than as a continuation
   of whatever preceded it. */
.section__note {
  margin-block-start: var(--sp-2xl);
  padding-inline-start: var(--sp-lg);
  border-inline-start: var(--bw-hairline) solid var(--c-border-strong);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-body);
  color: var(--c-text-2);
  max-width: var(--measure);
  text-wrap: pretty;
}

/* Generic JS-applied visibility state. Requested by coverage.classes.md as a
   shared utility; it is the one state class in this file styled on its own,
   because its whole job is to be block-agnostic.

   Its only writer today is `showSuccess` inside the mothballed contact-form
   module (script.js § 3.8), so nothing can carry it while that module is not
   booted. It stays for the same reason the utility exists at all: it belongs
   to no block, so there is nothing for it to be an orphan of. */
.is-hidden { display: none !important; }


/* ==========================================================================
   ==========================================================================
   5b. GROUND ALTERNATION AND COMPOSITION  (iteration 2, § 3.1 and § 3.2)
   --------------------------------------------------------------------------
   This is the only block in the file that decides what tone a band sits on
   and how wide its content column is. Both are page-level decisions, so both
   are made in one place and addressed by id — the section modifiers
   (.section--hero, .section--marketing) describe what a section IS, not
   where it falls in the rhythm, and the rhythm is positional.

   0. TONE — RE-DERIVED AGAIN FOR EIGHT BANDS (phase 3). READ THIS BEFORE
      POINT 1, WHICH IS THE SEVEN-BAND DERIVATION IT REPLACES.

      #engineering was inserted between #developers and #marketing. The
      run is now:

        1 #hero  2 #how-it-works  3 #one-integration  4 #developers
        5 #engineering  6 #marketing  7 #getting-started  8 #contact

      THREE CONSTRAINTS, AND TOGETHER THEY MAKE STRICT ALTERNATION
      IMPOSSIBLE. This is arithmetic, not preference:

        a. #hero must be --c-bg. Measured in point 1 below: the hero glow
           and texture composite downward on paper and land under
           .hero__eyebrow, which is --c-text-3. On --c-sunken that pair
           lands at 4.32:1 — a fail. On --c-bg it is 4.75:1.
        b. #marketing must be --c-bg AND both its neighbours --c-sunken.
           § 13's signal 1 is that the band is the lighter of the three.
        c. #contact must be --c-bg, so .footer — which sets no ground and
           inherits the page's — does not put a step inside the footer.

      Under strict alternation the tone is a function of parity alone.
      #hero is index 1 and #marketing is index 6: opposite parity, so a
      strictly alternating run cannot give them the same tone, and (a) and
      (b) both demand --c-bg. EXACTLY ONE SEAM MUST THEREFORE REPEAT A
      TONE. There is no third band tone to escape into: the only value
      above --c-bg is --c-surface, which is the RAISED tone every card is
      painted in, and using it as a band ground would make the cards on it
      invisible.

      WHERE THE REPEAT GOES. Constraints (b) and (c) fix indices 5-8 as
      S B S B, and (a) fixes index 1 as B, so the repeat has to fall
      somewhere in 1-5. Four placements are possible and all four satisfy
      every constraint:

        (i)   #hero | #how-it-works           both --c-bg
        (ii)  #how-it-works | #one-integration both --c-sunken
        (iii) #one-integration | #developers   both --c-bg
        (iv)  #developers | #engineering       both --c-sunken

      (iv) is the one taken, for two reasons and in this order.

      FIRST, IT IS THE ONLY PLACEMENT THAT REPAINTS NO EXISTING BAND.
      i, ii and iii each move at least one shipped section onto the other
      tone, and every contrast figure phase 1 measured on that section —
      the integration diagram's 60 strokes, its four muted node fills
      against --c-text-2, the facts list, the toggle — was taken against a
      named ground. (iv) leaves bands 1-4 byte-identical and adds one id to
      the rule below. Nothing measured in docs/final-phase1-notes.md needed
      re-running because of the alternation.

      SECOND, IT PUTS THE PAGE'S ONLY TWO-BAND RUN IMMEDIATELY BEFORE THE
      ISLAND. #developers and #engineering are the same argument in two
      halves — what the integration looks like, and what a prospect can
      check about it — so reading them on one continuous tone is right
      rather than merely cheap. And #marketing's tonal lift is a bigger
      event after two bands of tint than after one: signal 1 in § 13 is
      strengthened by this, not diluted.

      THE RESULT, WHICH IS WHAT THE RULE BELOW ENCODES:

        1 #hero            --c-bg      (paper, inherited)
        2 #how-it-works    --c-sunken
        3 #one-integration --c-bg
        4 #developers      --c-sunken
        5 #engineering     --c-sunken  <- the one repeat, argued above
        6 #marketing       --c-bg      <- still the lighter band, and still
        7 #getting-started --c-sunken     between two darker ones
        8 #contact         --c-bg
        · .footer          --c-bg      (inherits the page ground)

   1. TONE — THE SEVEN-BAND DERIVATION, KEPT BECAUSE POINT 0 IS AN
      AMENDMENT TO IT AND NOT A REPLACEMENT OF ITS REASONING.
      RE-DERIVED FOR "LEDGER WHITE" IN PHASE 1. THE SET WAS THE SAME SEVEN
      BANDS IN THE SAME RHYTHM; WHAT FLIPPED IS WHICH WAY THE STEP GOES.

      Iteration 5's derivation was: seven bands, strictly alternating, and
      § 13 requires #marketing (band 5) to be the LIGHTER of it and its two
      neighbours. Only one of the two possible phases of a seven-band run
      puts an odd index on the lighter tone, so the run had to start on it:

        1 #hero  2 #how-it-works  3 #one-integration  4 #developers
        5 #marketing  6 #getting-started  7 #contact
        odd = lighter, even = base.

      ALL OF THAT STILL HOLDS. What does not survive the palette swap is the
      assumption underneath it — that "lighter" is a step UP from the page
      ground. On paper the page ground is already at the top of the ramp:
      --c-bg is #FBFBFA and the only tone above it is #FFFFFF, which is the
      RAISED tone, the one every card, panel and node box is painted in. Sit
      four whole bands on it and the cards on those bands have nowhere to
      go — they can only be tinted downward, which is the sign of a recess.
      Rendered, that is exactly what happened: every card in #hero,
      #one-integration, #marketing and #contact read as a hole in the band.

      So the step inverts. The alternate tone is a TINT, --c-sunken #F1F1EF,
      and the odd bands keep the page's own paper:

        1 #hero            --c-bg      (paper, inherited)
        2 #how-it-works    --c-sunken
        3 #one-integration --c-bg
        4 #developers      --c-sunken
        5 #marketing       --c-bg      <- still the lighter band, and still
        6 #getting-started --c-sunken     between two darker ones
        7 #contact         --c-bg
        · .footer          --c-bg      (inherits the page ground)

      Every property iteration 5 derived is preserved: the parity, the
      position of #marketing, the fact that no two adjacent bands share a
      tone, and .footer not breaking the run at the foot of the page. The
      rule below therefore names the OTHER three sections, and it no longer
      re-points --c-raised at all — there is one raised tone now and it is
      the same on both band tones (§ 1b).

      ONE CONSEQUENCE, AND IT IS PAID FOR IN § 8, UNCHANGED. #hero carries
      the page's single permitted gradient on .section--hero::before at
      z-index: -1. A negative z-index paints behind the ANCESTOR'S BACKGROUND
      unless that ancestor is a stacking context. #hero inherits its ground
      from <body> rather than setting one now, so the hazard is gone on
      today's list — but .section--hero keeps `z-index: 0` regardless,
      because it is also what `overflow: hidden` and the ::after texture rely
      on for their painting order, and because the next agent to move #hero
      back into the tinted set must not have to rediscover it.

      WHY #hero IS ON THE PAPER TONE AND NOT THE TINT, MEASURED. The glow and
      the texture both composite DOWNWARD on light, and they land under
      .hero__eyebrow, which is --c-text-3 — the tightest text pair on the
      page. On --c-sunken the eyebrow starts at 4.66:1 and the backdrop takes
      it to 4.32:1, a fail. On --c-bg it starts at 5.09:1 and lands at
      4.75:1. The parity above put #hero on the paper tone anyway, so this
      cost nothing; it is recorded because it is a constraint, not a
      coincidence.

   2. COLUMN. Three treatments.

        CENTRED  #getting-started · #contact
                 Text-only. One column the width of the measure, centred, so
                 the left and right margins are equal.

        SPLIT    #hero · #how-it-works · #marketing
                 From the xl step they run as two columns — the text rail
                 left, the object right. Below xl they stack, text first.

        WIDE     #one-integration · #developers
                 Head at heading measure, one object at container width.

      WHY THE SPLIT SET SHRANK (iteration 4, task 2 and task 4). A rail is
      worth having when it holds more than the head: #how-it-works puts its
      closing .section__note there and #marketing puts its CTA there, so both
      rails carry two fragments down the height of the object beside them.
      #one-integration's rail held the head and nothing else, which is why
      iteration 2 had to make it sticky — a single fragment pinned to the top
      of a 1900px column leaves the left half of the section empty, and that
      is exactly the "left column ends well before the right" defect task 2
      names. Iteration 2 solved it with position: sticky; iteration 4 solves
      it by not creating it.

      Measured at 1440, that is also what buys the rest of the page back:

        · a rail wide enough for a two-line #one-integration title (510px)
          or a two-line #developers title (545px) leaves the object column
          495px, i.e. capability cards 231px wide — narrower than they are
          at any breakpoint today, and the card is the thing task 1 is
          about. At container width the same grid runs THREE columns of
          346px, so the cards get shorter instead of taller.
        · #one-integration's object column drops from 980px to ~560px.
          #developers' object is now the .facts list (the code panel it was
          authored around went in iteration 5) and it runs the full 1104px.

      #one-integration and #developers were one section a week ago and are a
      deliberate pair now, so giving them one shared treatment is what makes
      them read as a pair rather than as a section that grew a sequel.

      #hero splits at xl with the rest. Its diagram makes that a measured
      decision rather than a stylistic one — see the long note in § 8.

   3. THE TAIL (iteration 4, task 2). Every section closes on the same
      distance, --section-y, so the space under the last element of a section
      is a constant rather than a function of which rhythm modifier the
      section happens to carry. The modifiers still set the section's OPENING
      rhythm; they no longer set its closing one. Before this, the tail was
      112px after four sections, 160px after three and 315px after
      #one-integration — a spread that read as blocks drifting apart at
      random.

   4. THE OPENING (iteration 5, seam pass). The tail was made a constant in
      iteration 4 and the OPENING was left alone, so the seam between two
      sections was still 112 + whichever of three different openings the
      lower section happened to carry — 112, 160 or 315 at 1440. Measured
      section-boundary to section-boundary that produced six voids of 303,
      227, 261, 302, 463 and 237px: a 236px spread on a page whose seams are
      all doing the same job.

      Six of the eight sections open on the same distance they close on,
      --section-y, so a seam is 2 x --section-y plus the descender slack of
      whatever glyph happens to be last. The exceptions are #hero, whose
      opening clears the fixed header and is not a seam, and
      #getting-started, which keeps --section-y-loose: it closes the
      #marketing island, and it is the floor under the accent guard.
      Measured after: 255, 227, 261, 254, 308, 237 — a 81px spread, 14 %
      less total void and 33 % off the largest one. The full table is in
      docs/iteration-5-report.md § Seam reduction.
   ========================================================================== */

/* THE TINTED HALF OF THE ALTERNATION. Eight bands, re-derived in point 0
   above. Parity no longer decides it — the run repeats a tone once, at
   #developers | #engineering, and the argument for that seam being the one
   is in point 0. The four bands not named here set no ground at all: they
   inherit --c-bg from <body>, which is both correct and one fewer paint.
   No --c-raised re-point: there is one raised tone on light and § 1b sets
   it once. */
#how-it-works,
#developers,
#engineering,
#getting-started {
  background: var(--c-sunken);
}

/* ONE TAIL FOR EVERY SECTION (task 2). Declared after .section--loose so it
   wins over it; the two rules that set a section's
   bottom edge from inside a component block (.section--hero in § 8,
   .section--marketing in § 13) have been changed to match rather than being
   overridden from here, so this value is stated once per section and not
   fought over. --section-y is 56px at 360 and 112px at 1440. */
.section { padding-block-end: var(--section-y); }

/* THE ONE GUARD THAT IS STILL LOAD-BEARING, and it is below the band, not
   above it. #marketing's ochre — its eyebrow, its CTA fill and its card edges
   — and the indigo CTA that closes #getting-started are ~750px apart on the
   section rhythm alone, which fits inside a 1024px-tall viewport. The air is
   the separation.

   PHASE 3 PUT A NEW BAND DIRECTLY ABOVE #marketing, AND IT SHIPS NO INDIGO.
   #engineering's status pills are the one element in it that could carry
   --c-accent, and they only do so at data-readiness="available". Every card
   ships "roadmap", whose pill is --c-text-3 on no fill, so the band's resting
   colour set is the neutral ramp exactly as #developers' was. That is what
   keeps the guard below a one-sided problem. The measured consequence of
   changing it is recorded on the pill's own rule in § 12a and in the
   READINESS comment in index.html: an available pill sits ~300px above the
   ochre rule, which no amount of --accent-gap can cover, so "available" in
   this section is an owner decision that requires the sweep re-run.

   Its counterpart above the band is GONE. #one-integration carried a
   padding-block-end of max(--section-y-loose + --sp-3xl, --accent-gap) —
   315px at 1440x900 — whose entire justification was keeping the code
   panel's indigo tab underline off the same screen as #marketing's ochre
   rule. The panel is in #developers now, and § 12 draws its selected-tab
   underline in the neutral ramp, so there is no resting indigo left between
   #one-integration and #marketing and nothing left to hold apart.

   RE-VERIFIED IN ITERATION 4, not inherited. The section count changed from
   seven to eight, so the old result was not trusted. A 150px-step scroll
   sweep in real Chrome over CDP, with 300ms of settle at every step so the
   scroll-spy actually runs, at 1440x900, 1024x1366, 1366x1024, 768x1024,
   834x1194, 1440x700 and 390x844: ZERO frames containing both an indigo and
   a marketing-accent pixel, and the nav's --c-accent read the neutral
   retint for the whole of the marketing band at every one of them. The gate in § 13 needed #developers added
   to it for that to be true; see the note there.
   ITERATION 5, SEAM PASS — THE GUARD IS THE SAME MECHANISM, RE-SHAPED, AND
   IT NOW COSTS NOTHING ON AN ORDINARY SCREEN. --accent-gap was
   `clamp(0px, 35vh, 30rem)`: 315px at a 900px-tall window, which is what
   made `marketing | getting-started` a 463px seam — the largest void on the
   page by 160px — while the requirement at that height is ZERO, because the
   base separation is already 877px at its worst width and the section
   opening covers the rest. § 1b carries the full arithmetic; the shape is
   now `clamp(0px, 100vh - 52rem, 30rem)`, the guard the inequality actually
   asks for, and it is >= the old guard's reach at every width.

   WHAT REMAINS HERE IS NOT THE GUARD, IT IS THE RHYTHM. With --accent-gap
   at 0 for every window shorter than 960px, this line resolves to
   --section-y-loose, and that is a design decision rather than a measured
   one: #marketing is an island — the one boundary on this page between two
   divisions of the company — and the seam that closes it keeps one step
   more air than the six ordinary seams, out of the page's own rhythm
   modifier rather than out of a viewport-scaled guard.

   THE SECOND REASON THIS ONCE HAD IS NOW SPENT, AND IS RECORDED RATHER THAN
   DELETED SO THAT NOBODY RE-DERIVES IT. Until phase 2 this opening was also
   what kept #contact under the 18%-of-page-height ceiling: #contact was
   1330.9px at 1440 and did not move, so every pixel removed from a void
   raised its share, and taking this opening down to --section-y landed it
   at 18.00% — a fail on a different acceptance line. Phase 2 deleted the
   contact form. Re-measured at 1440 after phase 3: #contact is 790.6px of a
   7958px page, which is 9.93%, and the worst band on the page is
   #engineering at 13.28%. The ceiling no longer depends on this line, so
   the rhythm argument above is the whole of it. The old arithmetic is in
   the seam section of docs/iteration-5-report.md.

   The one viewport class the guard cannot cover is a window taller than
   ~1410px at 1024 wide — unchanged from iteration 4, because the 30rem
   ceiling that sets it is unchanged. The arithmetic and the measured
   crossover are on --accent-gap in § 1b. */
#getting-started {
  padding-block-start: max(var(--section-y-loose), var(--accent-gap));
}

/* -- CENTRED: text-only sections --------------------------------------- */
/* .container already centres itself; narrowing it to the measure is what
   makes the copy inside it fill the column rather than hug its left edge. */
/* #getting-started is the other CENTRED section; it takes the narrow column
   from the .container--narrow modifier its markup already carries, so only
   #contact needs naming here. #boundary was the third and is gone. */
#contact > .container { max-width: var(--container-narrow); }

/* -- WIDE: heading measure over one container-width object --------------
   #one-integration and #developers each carry a head and exactly one object
   (a card grid, and a code panel with its facts). They stack, and the only
   thing that needs saying is how wide the head is allowed to be: the title
   runs to the HEADING measure and the lede stays on the BODY measure, which
   is the decoupling task 4 asks for. Without lifting the head's own cap the
   title would still be held to --container-narrow (720px) and the pair of
   long titles in these two sections would still break to three lines.
   This is not gated on a breakpoint: --measure-head is a maximum, so below
   1000px or so the container is narrower than the cap and nothing changes. */
#one-integration > .container > .section__head,
#developers      > .container > .section__head { max-width: none; }

#one-integration > .container > .section__head > .section__title,
#developers      > .container > .section__head > .section__title {
  max-width: var(--measure-head);
}

/* -- SPLIT: the text rail and the object column ------------------------- */
@media (min-width: 1200px) {
  #how-it-works    > .container,
  #marketing       > .container {
    display: grid;
    grid-template-columns: minmax(0, var(--rail)) minmax(0, 1fr);
    /* `auto 1fr`, not two auto rows, and it is worth 112px in #marketing.
       The object spans both rows, so with two auto rows the browser shares
       the object's surplus height BETWEEN them: measured at 1440, the
       marketing rail's head row grew from 200px to 312px and pushed the CTA
       112px further from the lede it belongs to — a dead band inside the
       rail, which is task 2's defect one scale down. Pinning row 1 to its
       content sends every pixel of surplus to row 2, where `align-items:
       start` leaves it below the rail's last fragment instead of above it.
       #how-it-works is unaffected either way: its rail fragments are
       already taller than the demo beside them.

       ITERATION 5: #how-it-works' rail is THREE fragments now — head,
       .statement, closing note — so its row track list is declared
       separately below. The principle is the same: every auto row but the
       last, and the last takes the 1fr so the object's surplus lands under
       the rail rather than inside it. */
    grid-template-rows: auto 1fr;
    column-gap: var(--split-gap);
    align-items: start;
  }
  #how-it-works > .container { grid-template-rows: auto auto 1fr; }

  /* The head is the top of the rail. Its 48px bottom margin is the vertical
     step to whatever follows it VERTICALLY; in the split nothing does, so it
     is dropped and the rail's own fragments keep their own 48px top margins.
     The step is unchanged wherever two blocks are actually stacked. */
  #how-it-works    > .container > .section__head,
  #marketing       > .container > .section__head {
    grid-column: 1;
    margin-block-end: 0;
    max-width: none;
  }

  /* Explicit placement, one line per fragment, so the reading order of the
     rule matches the reading order of the section. The object that opens the
     right-hand column starts level with the head, so the 48px top margin it
     carried to clear the head comes off. */

  /* §2 of the page — head, the .statement and the closing note in the rail,
     the failover demo beside all three. The .statement sits BETWEEN the head
     and the note in the rail exactly as it does in the DOM: it is the claim
     the section's lede sets up, so it must be read before the demo is
     interpreted, not after it. */
  #how-it-works > .container > .section__head { grid-row: 1; }
  #how-it-works > .container > .statement     { grid-column: 1; grid-row: 2; }
  #how-it-works > .container > .demo {
    grid-column: 2;
    grid-row: 1 / span 3;
    margin-block-start: 0;
    /* ITERATION 5: CENTRED IN ITS COLUMN, NOT PINNED TO THE TOP OF IT.
       Measured at 1440 after the .statement moved into this rail: the rail
       runs 564.5px over three fragments and the demo is 341.1px, so
       top-aligning it left 223.3px of empty column under the object — the
       largest single void on the page and the one the iteration-5 brief
       names by its measurements. `align-items: start` on the container is
       right for the rail's own fragments and wrong for the object beside
       them, so the object opts out.

       Centring is a COMPOSITION fix and not a decoration: it does not add
       anything to the column, it stops the column ending 223px early on one
       side by putting the surplus on both sides of the object instead of
       all of it under. Measured after: 111.6px above, 111.7px below, and
       the demo's mass now sits opposite the rail's rather than above it.

       It costs nothing measured elsewhere. The section's tail is taken to
       the last PAINTED box, which in this section is .section__note in the
       rail and not the demo, so the tail is 112px before and after; the
       section's height is unchanged because the grid area was already this
       tall; and CLS is 0 because nothing here is written by script. */
    align-self: center;
  }
  #how-it-works > .container > .section__note { grid-column: 1; grid-row: 3; }

  /* §4 and §5 of the page — #one-integration and #developers — USED TO BE
     PLACED HERE AND ARE NOT ANY MORE.

     Four rules in this block addressed fragments by id, and two of the four
     stopped matching anything the moment the markup agent moved the code
     panel and the .facts list out of #one-integration and into #developers.
     Both were deleted rather than re-pointed at #developers, because both
     sections take the WIDE treatment above instead of a rail — see the
     measured argument in this section's header comment. Deleted with them:
     #one-integration's sticky `grid-row: 1 / -1` head (it existed to fill a
     rail that no longer exists), its `.bento` placement, and its entry in
     the three list rules in this block.

     ITERATION 5: the code panel itself is gone from the document, so the
     only object left in #developers is the .facts list. It still takes the
     WIDE treatment and it is still not placed here — see § 12 for how it is
     treated now that it is the section's object rather than its footnote. */

  /* §5 — head and the section's CTA in the rail, the card grid beside. */
  #marketing > .container > .section__head { grid-row: 1; }
  #marketing > .container > .bento {
    grid-column: 2;
    grid-row: 1 / span 2;
    margin-block-start: 0;
  }
  #marketing > .container > .steps__cta {
    grid-column: 1;
    grid-row: 2;
    margin-block-start: var(--sp-2xl);
  }

  /* Inside a 30rem rail the measure is never the binding constraint, and a
     head that is allowed to run to --container-narrow would break the grid. */
  #how-it-works    .section__head .lede,
  #marketing       .section__head .lede { max-width: none; }

  /* Two columns, not three: the object column is 560px at 1440, which is
     three cards' worth of gap and two cards' worth of card. The --wide-lg
     span is unchanged, so the four cells still fill their rows exactly —
     one full-width cell, a pair, one full-width cell. No ragged row.
     #one-integration is no longer in this list: at container width its grid
     runs the three columns § 11 gives it from 900 up. */
  #marketing .bento { grid-template-columns: repeat(2, 1fr); }
}


/* ==========================================================================
   ==========================================================================
   6. NAV — sticky header, scroll progress line, mobile disclosure panel
   ========================================================================== */

.nav {
  position: sticky;
  inset-block-start: 0;
  z-index: var(--z-nav);
  background: var(--c-bg);
  border-block-end: var(--bw-hairline) solid var(--c-border);
}

/* The single permitted backdrop-filter on the page, and only once scrolled. */
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .nav.is-scrolled {
    background: color-mix(in oklab, var(--c-bg) 80%, transparent);
    -webkit-backdrop-filter: blur(var(--blur-nav)) saturate(1.2);
    backdrop-filter: blur(var(--blur-nav)) saturate(1.2);
    /* On paper an 80%-opaque white bar over white content has no edge but
       its hairline, and the hairline is 12% ink. The shadow is what makes
       the header read as a layer once the page has scrolled under it. It is
       not transitioned — see the box-shadow line in § 16's contract. */
    box-shadow: var(--shadow-nav);
  }
}
@media (prefers-reduced-transparency: reduce) {
  .nav.is-scrolled {
    background: var(--c-bg);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    box-shadow: var(--shadow-nav);
  }
}

.nav__progress {
  position: absolute;
  inset-inline-start: 0;
  inset-block-end: 0;
  width: 100%;
  height: var(--bw-rule);
  background: var(--c-accent);
  transform: scaleX(0);
  transform-origin: left center;
  pointer-events: none;
  /* No transition — JS writes this every rAF frame. */
}

.nav__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-md);
  min-height: var(--nav-h);
}

.nav__brand,
.footer__home {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  color: var(--c-text);
  text-decoration: none;
  border-radius: var(--r-sm);
  flex: none;
}

/* currentColor, not --c-accent: the accent budget is the primary CTA, the
   focus ring, the nav/tab active rules, the selected filter chip and the
   live element of the section-2 diagram. A coloured brand mark would be a
   seventh use and would weaken all six. */
.nav__mark,
.footer__mark {
  flex: none;
  width: var(--size-icon);
  height: var(--size-icon);
  color: currentColor;
}

.nav__wordmark,
.footer__wordmark {
  font-size: var(--fs-body-sm);
  font-weight: var(--fw-medium);
  letter-spacing: -0.01em;
  color: var(--c-text);
}

/* The desktop link row is wrapped in its own <nav aria-label="Primary">, so the
   flex child is .nav__primary and it carries the auto margins. */
.nav__primary { display: none; }
.nav__list { display: none; }

/* -- THE HEADER CTA BELOW 480px (iteration 4, task 7) --------------------
   Measured at 360x780 with the header's space-between: brand 60px, CTA
   139px, burger 36px — 235px of control inside a 320px content box, with
   42px gaps. At 390 it is 236px inside 342px and 56px gaps. Nothing
   overflows and no gap is under the 12px floor, so this was never a bug; it
   was a header that is three-quarters control, whose widest element repeats
   a destination that is also in the menu behind the burger.

   The markup agent put a .nav__panel-cta inside the mobile panel precisely
   so the header CTA could go without stranding #contact, so it goes.

   THE BREAKPOINT IS 480, and it is the existing one. Solving for the width
   at which the header stops being dense rather than picking a round number:
   the three controls are 235px, the gutter is clamp(20px, 0.55rem + 3.1vw,
   48px), and the two gaps are (W - 2*gutter - 235) / 2. That gives 42px at
   360, 56px at 390, 70px at 420 and 98px at 480 — and 98px is the point at
   which the CTA is a third of the row rather than half of it. 480 is
   already a breakpoint in this file, so the page changes shape at a width it
   already changes shape at.

   Below 480: brand + burger, and the CTA lives in the panel.
   At 480 and above: exactly what shipped, and .nav__panel-cta is hidden so
   there is never a second "Talk to our team" in the accessibility tree. */
.nav__cta {
  display: none;
  flex: none;
  min-height: var(--h-control-sm);
  /* Tighter than the standard --sp-lg so brand + CTA + burger all fit
     inside 336px of content at 480px with 98px gaps. See § CSS notes. */
  padding-inline: var(--sp-md);
}

/* The panel's own CTA. It is a separate element from .nav__cta, not a moved
   one, so the two must never be exposed at once — see the pair of display
   rules at the 480 step below. Full width and separated from the last link
   by the block rhythm, so it reads as the panel's closing action rather
   than as a seventh menu item. */
.nav__panel-cta {
  display: flex;
  width: 100%;
  margin-block-start: var(--sp-md);
}

@media (min-width: 480px) {
  .nav__cta       { display: inline-flex; }
  .nav__panel-cta { display: none; }
}

.nav__toggle {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--h-control-sm);
  height: var(--h-control-sm);
  border-radius: var(--r-md);
  color: var(--c-text-2);
  cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .nav__toggle:hover { color: var(--c-text); background: var(--c-overlay-hover); }
}

.nav__toggle-icon {
  width: var(--size-icon);
  height: var(--size-icon);
}
.nav__toggle-bar { transform-origin: center; transform-box: fill-box; }

/* Two bars fold into an ✕. Transform only — never geometry. */
.nav__toggle.is-open .nav__toggle-bar--top    { transform: translateY(var(--burger-shift)) rotate(45deg); }
.nav__toggle.is-open .nav__toggle-bar--bottom { transform: translateY(calc(var(--burger-shift) * -1)) rotate(-45deg); }

/* The panel opens OVER the page. On the dark palette its own ground was a
   full step of fill away from everything under it; on paper it is the same
   tone as the band behind it, so the shadow is what separates them. */
.nav__panel {
  background: var(--c-bg);
  border-block-end: var(--bw-hairline) solid var(--c-border);
  box-shadow: var(--shadow-overlay);
  padding-block: var(--sp-md);
}

.nav__panel-list {
  display: grid;
  gap: var(--sp-2xs);
}

.nav__panel-link {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  min-height: var(--h-control);
  padding-inline: var(--sp-xs);
  border-radius: var(--r-sm);
  color: var(--c-text-2);
  font-size: var(--fs-body-sm);
  text-decoration: none;
}
@media (hover: hover) and (pointer: fine) {
  .nav__panel-link:hover { color: var(--c-text); background: var(--c-overlay-hover); }
}
/* Active item in the panel gets a 4px accent dot, not an underline. */
.nav__panel-link::before {
  content: "";
  width: var(--sp-2xs);
  height: var(--sp-2xs);
  border-radius: var(--r-pill);
  background: transparent;
  flex: none;
}
.nav__panel-link.is-active { color: var(--c-text); }
.nav__panel-link.is-active::before { background: var(--c-accent); }

@media (min-width: 900px) {
  :root { --nav-h: var(--nav-h-lg); }

  .nav__primary { display: block; margin-inline: auto; }

  .nav__list {
    display: flex;
    align-items: center;
    gap: var(--sp-lg);
  }

  .nav__link {
    display: inline-flex;
    align-items: center;
    min-height: var(--target-min);
    padding-block: var(--sp-xs);
    border-radius: var(--r-sm);
    color: var(--c-text-2);
    font-size: var(--fs-body-sm);
    text-decoration: none;
    /* Reserve the rule at every state so the active item does not shift. */
    border-block-end: var(--bw-hairline) solid transparent;
  }
  .nav__link:hover { color: var(--c-text); }
  .nav__link.is-active,
  .nav__link[aria-current="true"] {
    color: var(--c-text);
    border-block-end-color: var(--c-accent);
  }

  .nav__toggle { display: none; }
  .nav__panel  { display: none; }
}


/* ==========================================================================
   ==========================================================================
   6b. THE SECTION RAIL  —  TASK 2.7
   --------------------------------------------------------------------------
   div.rail[data-section-rail][aria-hidden="true"], a direct child of <body>
   immediately before <main>. Five li.rail__item, each carrying the same
   data-nav-link value as its header counterpart, each holding one
   span.rail__label. No <a>, no href, no tabindex — which is what makes the
   aria-hidden legitimate rather than a violation, and it is also why there
   is no :hover, no :focus and no cursor anywhere in this block. It is
   `pointer-events: none` for the same reason: a thing that cannot be
   activated must not look like it can.

   IT NEEDS NO NEW JAVASCRIPT. script.js § 3.2 groups every [data-nav-link]
   by the attribute's value and toggles .is-active on ALL of them, so the
   rail lights up from the observer that already runs for the header. That
   is also the reason nothing here may take --c-accent: § 13's collision
   gate is scoped `.nav:has(…)` and cannot see this element, so an accent
   here would be a fourth indigo that no gate covers. The current item is
   marked with --c-text and a longer tick, both neutral.

   WHERE IT FITS, MEASURED RATHER THAN ASSUMED. The free margin to the left
   of the content column, read back from .container's own box in real
   Chrome:

     viewport   content column starts at   free margin
     1200        46px                       46px
     1280        88px                       88px
     1360       128px                      128px
     1440       168px                      168px
     1920       408px                      408px

   The rail is inset --rail-inset (16px) and its tick column is --rail-tick
   (28px), so the bare column occupies 16..44px and clears the 1200px case
   by 2px.

   THE LABEL STEP IS 1520, NOT 1360, AND THE HANDOFF'S ESTIMATE IS WHY IT
   HAD TO BE MEASURED. docs/iter5-markup-handoff.md § 6.4 puts "One
   integration" at ~99px and concludes labels fit from ~1360. Rendered in
   Geist Mono at --fs-mono-sm it is 120.9px, 22% wider, and the five labels
   measure 56.4 / 56.4 / 72.5 / 96.7 / 120.9px. With the 16px inset, the
   28px tick and a 12px gap the longest label's right edge lands at 176.9px
   whatever the viewport, against a text column that starts at:

     1360 -> 128px   overlaps the copy by 49px
     1440 -> 168px   overlaps by 9px
     1600 -> 248px   clears by 71px

   Solving `(100vw - 1200)/2 + 48 - 177 >= 24` for one --sp-lg of clearance
   gives 1506px. 1520 is the next round step above it and lands 31px clear.
   Nothing was tightened to force 1440: 176.9 has to come down by 25px to
   fit there, which means an 8px inset and a 14px tick, and a tick that
   short against the window edge stops reading as a column at all.

   SO ONLY THE CURRENT ITEM IS LABELLED, AND ONLY ABOVE 1520. One label at
   a time is the better answer as well as the only one that fits: five
   permanent labels in the margin is a second navigation drawn beside a page
   that already has one, and this element exists to say WHERE YOU ARE. Every
   label is rendered at every width above the step, transparent until its
   item is current, so the row heights and the column's own height never
   change as the reader scrolls — this element is fixed, so a re-flow here
   would be a jump in the margin rather than a CLS event. Verified at 1520,
   1600 and 1920: zero overlap with .container's box, and scrollWidth ===
   clientWidth at all three.

   Hidden below 1200 with `display: none` — not visibility, not opacity: a
   fixed element that is merely transparent still answers hit-testing on
   some engines, and at 1200 there is no margin to put it in.
   ========================================================================== */

/* Below xl the rail does not exist. This is the base state, so a browser
   that never reaches the query below never paints it. */
.rail { display: none; }

@media (min-width: 1200px) {
  .rail {
    display: block;
    position: fixed;
    inset-block-start: 50%;
    inset-inline-start: var(--rail-inset);
    /* Vertically centred on the viewport. The translate is a static
       declaration, not an animation — it is here so the element does not
       need a height to be centred. */
    transform: translateY(-50%);
    z-index: var(--z-sticky);
    /* Decorative and unreachable. See the block comment. */
    pointer-events: none;
  }

  .rail__list {
    display: grid;
    gap: var(--sp-md);
    list-style: none;
    padding: 0;
    margin: 0;
  }

  /* The row is tick + label. The tick is the ::before so the label element
     carries nothing but the label. */
  .rail__item {
    display: flex;
    align-items: center;
    gap: var(--sp-sm);
    color: var(--c-border-strong);
  }

  .rail__item::before {
    content: "";
    display: block;
    inline-size: var(--rail-tick);
    block-size: var(--bw-hairline);
    background: currentColor;
    /* The resting tick is the active one scaled on x, so becoming current
       is a transform and never a width. transform-origin is the inset edge
       so the tick grows away from the page edge, not toward it. */
    transform: scaleX(var(--rail-tick-rest));
    transform-origin: 0 50%;
  }

  .rail__item.is-active {
    color: var(--c-text);
  }
  .rail__item.is-active::before {
    block-size: var(--bw-rule);
    transform: none;
  }

  /* Below 1520 there is no room for a label at all — see the measured
     arithmetic in the block comment. */
  .rail__label { display: none; }
}

@media (min-width: 1520px) {
  .rail__label {
    display: block;
    font-family: var(--ff-mono);
    font-size: var(--fs-mono-sm);
    line-height: var(--lh-mono);
    letter-spacing: var(--ls-mono);
    font-weight: var(--fw-mono);
    white-space: nowrap;
    /* One label at a time. Opacity, not display, so the row's height and the
       column's height are the same whichever item is current and neither
       re-flows as the reader scrolls. */
    opacity: 0;
  }
  .rail__item.is-active .rail__label { opacity: 1; }
}


/* ==========================================================================
   ==========================================================================
   7. FOOTER
   ========================================================================== */

.footer {
  padding-block: var(--section-y-tight);
  border-block-start: var(--bw-hairline) solid var(--c-border);
}

.footer__inner {
  display: grid;
  gap: var(--sp-2xl);
}

.footer__brand {
  display: grid;
  justify-items: start;
  gap: var(--sp-md);
  max-width: var(--measure);
}

.footer__positioning {
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body-sm);
  color: var(--c-text-2);
  max-width: var(--measure);
}

.footer__address {
  display: grid;
  gap: var(--sp-2xs);
  font-size: var(--fs-caption);
  line-height: var(--lh-caption);
  color: var(--c-text-3);
}
.footer__address-line { display: block; }
.footer__address-line--name {
  color: var(--c-text-2);
  font-weight: var(--fw-medium);
  letter-spacing: 0.01em;
}
/* The contact email. One line, identical treatment to the address lines it
   sits with — it inherits everything from .footer__address-line and exists
   only as a hook for the value when it arrives. */
.footer__address-line--email { overflow-wrap: anywhere; }

/* "AGB is a trading name of…" — same register as the address above it, not
   a second paragraph of body copy. */
.footer__trading {
  font-size: var(--fs-caption);
  line-height: var(--lh-caption);
  color: var(--c-text-3);
  max-width: var(--measure);
}

.footer__columns {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2xl);
}

.footer__list { display: grid; gap: var(--sp-xs); }

.footer__link {
  display: inline-flex;
  align-items: center;
  min-height: var(--target-min);
  padding-block: var(--sp-2xs);
  border-radius: var(--r-sm);
  color: var(--c-text-2);
  font-size: var(--fs-body-sm);
  text-decoration: none;
}
@media (hover: hover) and (pointer: fine) {
  .footer__link:hover { color: var(--c-text); }
}

.footer__base {
  display: grid;
  gap: var(--sp-md);
  margin-block-start: var(--sp-2xl);
  padding-block-start: var(--sp-lg);
  border-block-start: var(--bw-hairline) solid var(--c-border);
}

.footer__legal,
.footer__copyright {
  font-size: var(--fs-caption);
  line-height: var(--lh-caption);
  color: var(--c-text-3);
  max-width: var(--measure);
}

@media (min-width: 480px) {
  .footer__columns { flex-direction: row; }
}

@media (min-width: 640px) {
  .footer__inner {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: start;
  }
  .footer__base {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: baseline;
  }
  .footer__copyright { white-space: nowrap; }
}


/* ==========================================================================
   ==========================================================================
   8. PAGE SECTION 1 — #hero
   ========================================================================== */

.section--hero {
  position: relative;
  /* A STACKING CONTEXT, DELIBERATELY (iteration 5). The glow below is drawn
     on ::before at z-index: -1, and a negative z-index paints behind the
     nearest ancestor STACKING CONTEXT's background — so once § 5b's
     re-derived seven-band alternation gave #hero a --c-surface ground, the
     glow would have been painted underneath it and disappeared. `z-index: 0`
     with `position: relative` makes this element the stacking context, which
     puts the glow above the band's own ground and still below the hero's
     content. Nothing else changes: the nav is a sibling with its own z-index
     and is unaffected. Do not remove this without removing #hero from the
     --c-surface set in § 5b. */
  z-index: 0;
  /* Contains the radial glow so it can never widen the page at 360px. */
  overflow: hidden;
  padding-block-start: var(--sp-5xl);
  /* The tail is the page-wide one (§ 5b, task 2), not --section-y-loose.
     Measured at 1440 the hero's own bottom edge was 160px of nothing under a
     384px hero — the second-largest dead band on the page after
     #one-integration's accent guard. The opening rhythm above is unchanged:
     the hero still starts on --sp-5xl under the header. */
  padding-block-end: var(--section-y);
}

/* The page's single permitted gradient. No element is provided for it. */
.section--hero::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 50%;
  translate: -50% 0;
  width: min(var(--container), 150%);
  aspect-ratio: 2 / 1;
  background: radial-gradient(circle closest-side at 50% 35% in oklab,
              var(--c-glow), transparent);
  pointer-events: none;
  z-index: -1;
}

/* --------------------------------------------------------------------------
   THE HERO TEXTURE — TASK 2.6.
   NO ELEMENT, AND IT NEEDS NONE. ::before is the one permitted gradient;
   ::after was free (the markup handoff § 6.3 checked and left a comment in
   index.html saying so), and a decorative <div> here would be a node in the
   DOM doing what a pseudo-element does for nothing, needing aria-hidden to
   stay out of the accessibility tree.

   WHAT IT IS. A 72px grid of ink at 1.3% alpha, masked onto the same
   ellipse the glow occupies so it dissolves before any edge, drifting
   exactly one cell on the diagonal every 64 seconds. A grid rather than a
   gradient because a gradient at an alpha this low is invisible and at a
   visible alpha it is a shape; a grid at this alpha is a surface.
   IT WAS NOT PAINTING AT ALL BEFORE THIS RUN — --texture-cell was being
   swallowed by a self-closing comment in § 1b, so background-size was
   invalid. The note on the token records the whole of it.

   WHY IT CANNOT MOVE A CONTRAST PAIR BELOW AA, and why that was measured
   anyway. .section--hero is the stacking context (§ 5b), so z-index: -1
   puts this above the band's own ground and the glow, and below every
   .hero__col child — it can never sit ON text. It does sit UNDER text,
   though, which means it is part of the composited background of every
   hero text pair, and that is what was measured rather than the token.
   ON PAPER THE WHOLE STACK GOES THE OTHER WAY: glow and texture both
   DARKEN the ground instead of lightening it, so every hero pair loses
   contrast rather than gaining it, and this is the one place on the page
   where a decorative layer can push a pair toward the floor.
   Measured: ink 1.3% over the 4% glow over --c-bg composites to #F1F0F6,
   against which --c-text is 17.18:1, --c-text-2 6.89:1 and the
   .hero__eyebrow's --c-text-3 4.70:1. The eyebrow starts at 5.15:1 on the
   bare band, so the two layers together cost it 0.45:1 and it keeps 0.20
   over the 4.5 floor. --c-border-strong as a graphical object holds at
   3.54:1 against 3.0. It is also why #hero is on the PAPER band and not the
   tinted one — on --c-sunken the same stack lands the eyebrow at 4.32:1,
   a fail. See § 5b.

   THE MOTION IS IN § 16 AND ONLY THE MOTION. The texture itself is
   declared here, unconditionally, so a reader on reduced motion gets the
   same surface with no drift rather than a flat band — which also means
   the contrast figures above are the ones everybody sees, not a best case.

   inset is -10% rather than 0 so a full cell of travel never exposes the
   pattern's own edge; .section--hero already carries overflow: hidden, so
   the overhang costs no width at 360.
   -------------------------------------------------------------------------- */
.section--hero::after {
  content: "";
  position: absolute;
  inset: -10%;
  z-index: -1;
  pointer-events: none;
  background-image:
    linear-gradient(to right,  var(--c-texture) var(--bw-hairline), transparent 0),
    linear-gradient(to bottom, var(--c-texture) var(--bw-hairline), transparent 0);
  background-size: var(--texture-cell) var(--texture-cell);
  /* Masked ONTO THE GLOW, not onto the top edge. The ::before gradient is a
     closest-side circle at 50% 35% of a 2:1 box, so this ellipse sits over
     the same place: the texture is only ever visible where there is already
     light to give it structure, and it dissolves before it reaches the
     header's bottom edge, the band's own edges and the two columns' outer
     margins. A linear top-down mask put the pattern at full strength in the
     loudest 200px of the band and drew a chequerboard against the header. */
  -webkit-mask-image: radial-gradient(115% 78% at 50% 32%, black, transparent 68%);
          mask-image: radial-gradient(115% 78% at 50% 32%, black, transparent 68%);
}

/* --------------------------------------------------------------------------
   THE SPLIT HERO.
   Two columns from lg: the text rail, then the routing diagram. DOM order is
   text-then-visual, so the stacked arrangement below lg needs no `order` and
   has none — see docs/iter2-markup-handoff.md § 1.3.

   `justify-items: start` used to sit on .hero and shrink-wrapped its direct
   children. Those children are now the two columns, and shrink-wrapping the
   visual column would collapse the diagram, so it has moved down onto
   .hero__col--text where the buttons that wanted it actually live.
   -------------------------------------------------------------------------- */
.hero {
  display: grid;
  gap: var(--sp-2xl);
}

.hero__col { min-width: 0; }   /* or .flow__svg refuses to shrink and the
                                  grid blows out. */

.hero__col--text {
  display: grid;
  justify-items: start;
  align-content: start;
  gap: var(--sp-lg);
}

.hero__eyebrow { margin-block-end: calc(var(--sp-xs) * -1); }

.hero__title {
  font-size: var(--fs-display);
  line-height: var(--lh-display);
  letter-spacing: var(--ls-display);
  font-weight: var(--fw-display);
  color: var(--c-text);
  text-wrap: balance;
  /* No overflow-wrap, no hyphens: no word in this headline may split. */
}

/* THE TITLE BREAK.
   `.hero__title { max-width: 18ch }` at lg is GONE. The headline changed in
   iteration 2 and its two authored lines are 22 and 23 characters, which 18ch
   cannot hold — it would have forced the title into three or four lines
   whatever the <br> did. The column is what constrains the title now.

   The <br> is a hint, not a requirement: it is surrounded by whitespace, so
   `display: none` collapses it to a single space and the line re-flows under
   `text-wrap: balance` with the accessible name unchanged.

   It is ON from sm up and OFF below. Measured, not guessed: "Add a provider
   without" renders 709px at the 76px display size, i.e. 9.33px per px of
   font-size. At 480px the content box is 433px and the title is 44px, so the
   line is 410px and fits; at 390px the box is 348px and the line would be
   382px and would not. Below sm the break comes off and balance sets the
   headline in three lines with no mid-word break and no widow. */
.hero__title-break { display: none; }
@media (min-width: 480px) {
  .hero__title-break { display: inline; }
}

.hero__sub { max-width: var(--measure); }

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-md);
  width: 100%;
  margin-block-start: var(--sp-md);
}

/* Two buttons now. Both full width while the row is full width, so the pair
   reads as a stack rather than leaving the second one orphaned by the wrap. */
.hero__cta,
.hero__cta-secondary { width: 100%; }

@media (min-width: 640px) {
  .hero__actions { width: auto; }
  .hero__cta,
  .hero__cta-secondary { width: auto; }
}

/* --------------------------------------------------------------------------
   WHERE THE HERO SPLITS, AND WHY IT IS NOT 900px.

   The handoff asks for the split to be pinned to the 900px
   .flow__canvas--h / --v swap, so that the vertical geometry never lands in
   a wide short column and the horizontal one never lands in a narrow tall
   one. That is the right constraint and it IS satisfied here — but by
   splitting at 1200 rather than at 900, for a reason that only shows up once
   the numbers are measured:

     · The horizontal canvas is authored on a 960-unit viewBox with 13-unit
       labels. Rendered into HALF of a 1104px container it scales to 0.55,
       and 13 units become 7px. That is not a small label, it is an
       unreadable one.
     · Between 900 and 1199 a STACKED hero gives that canvas the whole
       container — 826 to 1104px, i.e. scale 0.86 to 1.15 — which is the only
       width band where its authored type renders at its authored size.
     · So: below 1200 the hero stacks and the horizontal canvas runs full
       width; from 1200 the hero splits and the canvas gets its SVG type
       tokens scaled up to compensate (see § 9, .flow--hero). The vertical
       geometry is never in a split column at all, because the split starts
       300px above the swap.

   1200 is also where every other section on this page splits (§ 5b), so the
   page changes shape once rather than twice.
   -------------------------------------------------------------------------- */
@media (min-width: 1200px) {
  .hero--split {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: var(--sp-2xl);
    align-items: center;
  }

  /* --fs-display is sized for a headline that owns the full container. In a
     528px column it cannot set this headline in two lines: measured, "Add a
     provider without" is 9.33px wide per px of font-size, so at the 76px
     display size it is 709px. --fs-hero-split caps at 54px, which brings the
     same line to 504px inside a 528px column. The hero is still the largest
     type on the page by a clear step — 54px against 44px section titles. */
  .hero__title {
    font-size: var(--fs-hero-split);
    line-height: var(--lh-h1);
    letter-spacing: var(--ls-h1);
  }
}


/* ==========================================================================
   ==========================================================================
   9. PAGE SECTION 2 — #how-it-works · flow diagram, failover demo, statement
   The only place on the page routing and cascading are explained, and since
   iteration 5 the only place the technical-layer claim is made in prose —
   .statement at the foot of this block came from the deleted #boundary.
   ========================================================================== */

/* The marker sprite. Zero-size, always present, NEVER display:none — a
   <marker> referenced out of a display:none <svg> is not dependable. */
.svg-defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

/* -- the diagram ------------------------------------------------------- */
/* --sp-2xl, not --sp-3xl: this is the one fragment that sits directly under
   a .section__head and carries its own top margin. The two collapse, so the
   larger of the pair is what the reader sees — matching them is what keeps
   the head-to-body step at 48px here as well. See § LAYOUT PRIMITIVES. */
.flow {
  display: grid;
  gap: var(--sp-xl);
  margin-block-start: var(--sp-2xl);
}

/* -- .flow--hero --------------------------------------------------------
   The diagram moved into the hero in iteration 2. Two consequences:

   1. It is no longer a fragment under a .section__head, so it carries no top
      margin — the hero grid's own gap is the step, and at lg the diagram sits
      BESIDE the text rather than under anything.
   2. Unstyled it measured 739px tall, which is most of a 900px viewport
      before the copy is counted. The horizontal geometry is 960x320, so in
      the ~465px visual column at 1440 it renders 155px tall on its own and
      needs no cap; the vertical geometry is 360x560 and does. Capping the
      HEIGHT rather than the width lets preserveAspectRatio="xMidYMid meet"
      (already on both canvases) do the scaling, and keeps the authored
      min-width intact so the canvas still scrolls rather than crushing the
      13px labels below legibility.

   The detail panel is NOT hidden at any width. It is the diagram's
   accessible explanation and the target of aria-controls. */
.flow--hero { margin-block-start: 0; }

.flow--hero .flow__canvas--v .flow__svg { max-height: var(--flow-hero-cap); }

/* From xl the hero splits (§ 8) and the horizontal canvas renders into a
   528px column — a 0.55 scale on a 960-unit viewBox, which would put the
   authored 13-unit labels on screen at 7px. The SVG type tokens are scaled
   by the inverse so the rendered size lands back at ~12px and ~10px. They
   are USER UNITS inside the viewBox, not CSS layout px, which is why they
   are not on the type scale and why enlarging them here is a geometry
   correction rather than a type decision.
   Checked against the authored boxes and the authored glyph anchors, in user
   units: "Provider A" is 120 wide in a 168-unit box and starts at 564, and
   the state glyph is anchored at 554 and is 8.4 wide, so it clears the label
   by 1.6 units. "Routing engine" is 160 in 196, "Checkout" 96 in 148,
   "Issuer" 72 in 152. Nothing overlaps and nothing leaves its box. */
@media (min-width: 1200px) {
  .flow--hero .flow__canvas--h {
    --svg-label: 19px;
    --svg-sub:   19px;
  }
  /* The glyph does NOT scale with the label — it is anchored to the left
     inside the box while the label is centred, so the two run into each
     other long before the label runs out of box. */
  .flow--hero .flow__canvas--h .flow__node-glyph { font-size: var(--svg-glyph); }
}

/* The hero's radial glow lives on `.section--hero { overflow: hidden }`. That
   clips on the block axis too, so the diagram must not be able to sit outside
   the section box — it cannot: it is a grid item with no negative margin and
   no transform, and the hero's own padding is larger than the glow. */

.flow__canvas {
  width: 100%;
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

.flow__svg {
  display: block;
  width: 100%;
  height: auto;
  min-width: var(--diagram-min-w);
  font-family: var(--ff-sans);
}

.flow__canvas--h { display: none; }
.flow__canvas--v { display: block; }

/* The vertical geometry is authored on a 360-unit canvas. Letting it stretch
   to a 704px column at md would scale its 13px mono labels to 25px — the
   diagram would read as a poster rather than as instrumentation. Capped just
   above its authored width and centred instead, so between md and lg it
   renders close to 1:1. The horizontal geometry has no cap: it is authored
   at 960 and is meant to fill the column from lg up. */
.flow__canvas--v .flow__svg {
  max-width: 26rem;
  margin-inline: auto;
}

/* Base edge — always visible, always arrowheaded. */
.flow__edge {
  fill: none;
  stroke: var(--c-border-strong);
  stroke-width: 1.5;
  marker-end: url(#agb-arrow-base);
}

/* Pulse edge — carries no information on its own, so outside the
   reduced-motion gate it does not paint at all. Section 20 gives it a
   stroke and the comet animation, and only on segments JS has lit. */
.flow__pulse {
  fill: none;
  stroke: none;
}

.flow__node { cursor: pointer; }

/* STROKE WEIGHTS RE-TUNED FOR PAPER. Every stroke in both diagrams is on
   the neutral border ramp, which crossed the ground with the palette:
   --c-border-strong was 38% white and composited to #666667 (3.47:1) on the
   dark band; it is 52% ink and composites to #7A7B7D (4.03:1) on white. So
   the strokes did not lose contrast — they gained a little, and lost
   apparent WEIGHT, which is the usual light-ground effect: a dark line on a
   light field is eaten by the field where a light line on a dark field
   blooms into it. The fix is weight, not colour. Every 1-unit stroke goes to
   1.25 and the deliberately hairline `--direct` tangle goes 1.25 -> 1.4;
   everything already at 1.5 or 2 is left alone, because those two read the
   same on both grounds. */
.flow__node-box {
  fill: var(--c-raised);
  stroke: var(--c-border-strong);
  stroke-width: 1.25;
}
.flow__node--engine .flow__node-box { stroke-width: 1.5; }

/* THE LABEL RAMP MOVED UP ONE STEP FOR PAPER, AND IT IS A FIX RATHER THAN A
   preference. A node's live states fill its box with --c-*-muted at 10%. On
   the dark palette that DARKENED the box, so a --c-text-3 label read against
   a ground darker than the band and gained contrast: measured 5.53:1 and
   5.12:1. On paper the same 10% LIGHTENS nothing and tints downward — the
   composites are #E9E7F6, #E3EDE9, #EEEAE1 and #F4E6E4 — and --c-text-3
   against those measures 4.37, 4.45, 4.44 and 4.39:1 on the paper band and
   4.02-4.10:1 on the tinted one. Eight fails, every one of them under a
   node the demo lights.
   --c-text-2 clears all eight at 5.89-6.53:1 and still reads as secondary
   against the --c-text the live states give the label. The sub and the state
   glyph move with it so the node keeps one ramp. */
.flow__node-label,
.flow__node-glyph {
  font-family: var(--ff-mono);
  font-size: var(--svg-label);
  letter-spacing: 0.02em;
  font-weight: var(--fw-mono);
  fill: var(--c-text-2);
}
.flow__node-glyph { text-anchor: start; }
/* The vertical geometry's provider nodes are 108 units wide and their label is
   80 of those, so the state glyph runs at the smaller of the two SVG sizes to
   keep clear of the label. */
.flow__canvas--v .flow__node-glyph { font-size: var(--svg-sub); }

.flow__node-sub {
  font-family: var(--ff-mono);
  font-size: var(--svg-sub);
  letter-spacing: 0.02em;
  font-weight: var(--fw-mono);
  fill: var(--c-text-2);
}

/* Node state — every visual difference is driven by [data-state], so the
   JS state machine only ever writes an attribute.
   Every state is separable without colour: stroke width, dash pattern and
   a leading glyph carry it. */
.flow__node[data-state="idle"] .flow__node-box {
  stroke: var(--c-border-strong);
  fill: var(--c-raised);
}

/* THE KEY NODE (iteration 2, § 3.5). The routing engine is the one thing in
   the diagram the whole page is about, and at rest it was drawn in the same
   neutral as the five nodes around it. It now carries the accent in its
   resting state — a fourth systematic use of indigo alongside the primary
   CTA, the progress line and the active tab, not a decorative one.
   Declared with the [data-state="idle"] attribute in the selector so it has
   the specificity to beat the idle rule immediately above without touching
   any of the live states: once the demo lights the engine it goes to
   `active`, and once it is `ok`/`warn`/`failed` the status colour must win.
   Measured on the light palette: #4436D6 stroke 7.39:1 on the paper band and
   6.77:1 on the tinted one. The hero is a paper band, so the fill composites
   to #E9E7F6 and the labels on it are #0B0D0F 15.97:1 and --c-text-2
   #4C5359 6.41:1; on a tinted band the fill is #E0DEEC at 14.67:1 / 5.89:1.
   The 10% muted fill LIGHTENS the box on this palette where it darkened it
   on the last one, which is the whole reason the label ramp above had to
   move up a step. */
.flow__node--engine[data-state="idle"] .flow__node-box {
  stroke: var(--c-accent);
  fill: var(--c-accent-muted);
}
.flow__node--engine[data-state="idle"] .flow__node-label { fill: var(--c-text); }
.flow__node[data-state="active"] .flow__node-box {
  stroke: var(--c-accent);
  fill: var(--c-accent-muted);
  stroke-width: 2;
}
.flow__node[data-state="ok"] .flow__node-box {
  stroke: var(--c-ok);
  fill: var(--c-ok-muted);
}
.flow__node[data-state="warn"] .flow__node-box {
  stroke: var(--c-warn);
  fill: var(--c-warn-muted);
  stroke-dasharray: 4 3;
}
.flow__node[data-state="failed"] .flow__node-box {
  stroke: var(--c-err);
  fill: var(--c-err-muted);
  stroke-dasharray: 2 3;
}

.flow__node[data-state="active"] .flow__node-label,
.flow__node[data-state="ok"] .flow__node-label,
.flow__node[data-state="warn"] .flow__node-label { fill: var(--c-text); }
.flow__node[data-state="failed"] .flow__node-label { fill: var(--c-text-2); }

.flow__node[data-state="ok"] .flow__node-glyph     { fill: var(--c-ok); }
.flow__node[data-state="warn"] .flow__node-glyph   { fill: var(--c-warn); }
.flow__node[data-state="failed"] .flow__node-glyph { fill: var(--c-err); }

/* Selection sits on top of whatever data-state says. */
.flow__node.is-selected .flow__node-box   { stroke-width: 2; }
.flow__node.is-selected .flow__node-label { fill: var(--c-text); }
.flow__node.is-dimmed                     { opacity: 0.55; }

/* Edge state. */
.flow__edge[data-state="idle"] {
  stroke: var(--c-border-strong);
  marker-end: url(#agb-arrow-base);
}
.flow__edge[data-state="active"] {
  stroke: var(--c-accent);
  marker-end: url(#agb-arrow-accent);
}
.flow__edge[data-state="ok"] {
  stroke: var(--c-ok);
  marker-end: url(#agb-arrow-ok);
}
.flow__edge[data-state="warn"] {
  stroke: var(--c-warn);
  stroke-dasharray: 4 3;
  marker-end: url(#agb-arrow-warn);
}
.flow__edge[data-state="failed"] {
  stroke: var(--c-err);
  stroke-dasharray: 2 3;
  marker-end: url(#agb-arrow-err);
}

/* Dimming un-lit edges is a decorative emphasis, never the carrier of a
   state — hence :has() is safe here. Without :has() nothing dims and the
   diagram still reads correctly. */
.flow__edges:has(.is-lit) .flow__edge:not(.is-lit) { stroke-opacity: 0.35; }

/* --------------------------------------------------------------------------
   Focus on an SVG <g>. Declared AFTER the [data-state] rules on purpose:
   `.flow__node[data-state="idle"] .flow__node-box` and
   `.flow__node:focus-visible .flow__node-box` have identical specificity, so
   without this ordering the state rule would silently win and the fallback
   would not paint.

   Chrome, Firefox and Safari all paint `outline` on a focused SVG <g> today,
   so the outline is the real indicator. The box treatment below is the
   belt-and-braces second carrier the manifests ask for, in case an engine
   does not — but it only recolours the box in the neutral `idle` state.
   Overriding a node that is currently `ok`/`warn`/`failed` would destroy the
   status colour, which is the more important signal; there, focus is carried
   by the outline plus the doubled stroke width.
   -------------------------------------------------------------------------- */
.flow__node:focus-visible {
  outline: var(--bw-focus) solid var(--c-focus);
  outline-offset: var(--focus-offset);
}
.flow__node:focus-visible .flow__node-box { stroke-width: 2; }

.flow__node[data-state="idle"]:focus-visible .flow__node-box { stroke: var(--c-focus); }

/* Markers do not inherit paint from the referencing path. */
.flow__arrow--base   path { fill: var(--c-border-strong); }
.flow__arrow--accent path { fill: var(--c-accent); }
.flow__arrow--ok     path { fill: var(--c-ok); }
.flow__arrow--warn   path { fill: var(--c-warn); }
.flow__arrow--err    path { fill: var(--c-err); }

/* -- the shared detail panel ------------------------------------------- */
.flow__detail {
  background: var(--c-raised);
  border: var(--bw-hairline) solid var(--c-border-card);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-raised);
  padding: var(--sp-lg);
  /* Reserve the tallest item's height so swapping never jumps the page.
     A fixed floor, not an animated height. */
  min-height: 11rem;
}

.flow__detail-title {
  font-size: var(--fs-h4);
  line-height: var(--lh-h4);
  letter-spacing: var(--ls-h4);
  font-weight: var(--fw-h4);
  color: var(--c-text);
  margin-block-end: var(--sp-xs);
}

.flow__detail-body {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--c-text-2);
  max-width: var(--measure);
  text-wrap: pretty;
}

/* -- the failover demo ------------------------------------------------- */
.demo {
  display: grid;
  justify-items: start;
  gap: var(--sp-md);
  margin-block-start: var(--sp-2xl);
}

.demo__controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-md);
  width: 100%;
}
.demo__btn { width: 100%; }

/* -- .demo__status (iteration 4) ----------------------------------------
   The live region script.js writes "Running" into and clears again. It is
   `role="status" aria-live="polite"` and it must therefore never be
   display: none or visibility: hidden — a live region that is hidden at the
   moment the text lands is not announced. Its "off" state is the empty
   string, which is what it ships as.

   SO ITS SPACE IS RESERVED, and the number is measured. The JS agent
   recorded (iter4-js-notes § 6.1) that writing "Running" grew
   .demo__controls from 348x120 to 348x137 at 390 — the row is a wrapped
   column there, so the empty paragraph collapsed to zero and the log table
   below it jumped 17px every time a run started. One line-box of
   --fs-mono-sm is exactly what is missing, so that is what is held:
   min-height on the element, which reserves the row whether or not the text
   is there. The Decline button itself never moved at any breakpoint, before
   or after; this is only about what sits under it.

   Colour: --c-text-3 is 4.71:1 on --c-sunken, which is the band this section
   sits on, and 5.15:1 on --c-bg, so the status reads as secondary without
   dropping below AA. */
.demo__status {
  min-height: calc(var(--fs-mono-sm) * var(--lh-mono));
  color: var(--c-text-3);
  font-size: var(--fs-mono-sm);
  line-height: var(--lh-mono);
  letter-spacing: var(--ls-mono);
}

/* -- the running state of the control (iteration 4, markup handoff § 2.7) -
   The demo's Decline button is NEVER disabled and never painted as if it
   were: the JS agent removed the disabled window entirely, so there is no
   :disabled and no .is-running state on the control to style. What is left
   is the one honest, non-blocking signal the markup authored a hook for —
   the status text appears. Keyed off [data-demo-state] on the .demo root,
   which is the CONTROL's state, kept separate from .is-running, which is the
   board's. Colour only, no transform, no opacity gate: the button must not
   move, flash or dim, because it is fully available for the whole run and a
   press restarts it. */
.demo[data-demo-state="running"] .demo__status { color: var(--c-text-2); }

/* --r-xl is reserved by the design system for "the hero demo frame, and
   nothing else". This is it: the attempt log is the frame around the page's
   one moving surface, and the larger radius is what marks it as the
   centrepiece rather than another card. It is the only use in the file. */
.demo__log {
  width: 100%;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  background: var(--c-raised);
  border: var(--bw-hairline) solid var(--c-border-card);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-raised);
}

.demo__table {
  width: 100%;
  font-variant-numeric: tabular-nums;
}

.demo__caption {
  color: var(--c-text-3);
  text-transform: uppercase;
  padding: var(--sp-sm) var(--sp-sm);
  border-block-end: var(--bw-hairline) solid var(--c-border);
}

.demo__table th {
  text-align: left;
  color: var(--c-text-3);
  text-transform: uppercase;
  font-weight: var(--fw-mono);
  padding: var(--sp-sm) var(--sp-sm);
  border-block-end: var(--bw-hairline) solid var(--c-border);
  white-space: nowrap;
}
/* The right-aligned last column was the LATENCY column. It is gone, so the
   rule now right-aligned the RESULT header over left-aligned result cells.
   Removed rather than re-scoped: all three columns are left-aligned. */

.demo__row { border-block-start: var(--bw-hairline) solid var(--c-border); }
.demo__row:first-child { border-block-start: 0; }

.demo__cell {
  padding: var(--sp-sm) var(--sp-sm);
  text-align: left;
  vertical-align: middle;
}
.demo__cell--attempt {
  white-space: nowrap;
  color: var(--c-text);
  /* Reserved at every state so the active rule cannot shift the column. */
  border-inline-start: var(--bw-rule) solid transparent;
}
.demo__cell--provider { white-space: nowrap; color: var(--c-text-2); }
.demo__cell--result   { white-space: nowrap; }

.demo__row[data-state="pending"] .demo__cell,
.demo__row[data-state="pending"] .demo__cell--attempt,
.demo__row[data-state="pending"] .demo__cell--provider { color: var(--c-text-3); }

/* There is no [data-state="active"] rule here on purpose. setRow() writes only
   "pending" or the row's authored final state (failed / warn / ok) — never
   "active" — so the two rules that used to sit here matched nothing. Deleted
   rather than kept "in case": CLAUDE.md §9, no dead selectors. */

.demo__note {
  font-size: var(--fs-caption);
  line-height: var(--lh-caption);
  color: var(--c-text-3);
  max-width: var(--measure);
}

/* -- .statement ---------------------------------------------------------
   LOAD-BEARING POSITIONING COPY — NEVER FINE PRINT. It is one step above
   body size, in full --c-text, and it opens on its own rule, because it is
   the section's claim rather than a qualification of one.

   It arrived here in iteration 5 with the sentence it carries. #boundary
   made its argument in two adjacent lists and closed on a --c-text-3 FCA
   disclaimer; the section is gone, but two of its claims are true and
   substantive, so they were rephrased positively and moved into the section
   that is actually about how a routing decision gets made. The rule itself
   is unchanged from the one that was in § 10 — same size, same colour, same
   leading rule, same measure — so the sentence reads at the weight it always
   read at, in a section where it now has a demo to point at.

   The register change it used to be followed by does NOT come with it: the
   FCA disclaimer lives in the footer, once per page, and nowhere else. */
.statement {
  margin-block-start: var(--sp-2xl);
  padding-block-start: var(--sp-lg);
  border-block-start: var(--bw-hairline) solid var(--c-border);
  font-size: var(--fs-body-lg);
  line-height: var(--lh-body-lg);
  letter-spacing: var(--ls-body-lg);
  color: var(--c-text);
  max-width: var(--measure);
  text-wrap: pretty;
}

@media (min-width: 480px) {
  .demo__caption,
  .demo__table th,
  .demo__cell { padding-inline: var(--sp-lg); }
}

@media (min-width: 640px) {
  .demo__controls { width: auto; }
  .demo__btn { width: auto; }
}

@media (min-width: 900px) {
  .flow__canvas--h { display: block; }
  .flow__canvas--v { display: none; }
  .flow__detail { padding: var(--sp-xl); }
}


/* ==========================================================================
   ==========================================================================
   10. REMOVED IN ITERATION 5 — was PAGE SECTION 2, #boundary
   --------------------------------------------------------------------------
   The section is deleted from index.html: the two-column .compare lists
   ("What AGB does" / "What AGB does not do"), the heading above them and the
   FCA disclaimer that closed them. Deleted with it, and NOT re-pointed at
   anything: .compare, .compare__title, .compare__list, .compare__item, the
   two ::before markers, the 640px two-column rule, .boundary__disclaimer,
   and the --marker-size token in § 1 that only the "does" tick used.

   .statement is the ONE rule that survived, because the sentence it styles
   survived: two of #boundary's claims moved into #how-it-works, rephrased
   positively, and they are set in the same treatment there. The rule has
   moved up into § 9 with them rather than being left here on its own.

   The FCA disclaimer is NOT anywhere else on this page. It is in the footer,
   once, as .footer__legal — which is where it always also was, and the
   duplicate is what iteration 5 removed.

   The block number is left as a hole on purpose; see the FILE MAP.
   ========================================================================== */

/* ==========================================================================
   ==========================================================================
   11. PAGE SECTION 4 — #one-integration · the bento grid and the
   capability card. The same grid and cell are reused by #marketing (§13)
   with a different surface treatment; the geometry is shared, the surface
   is not.
   ========================================================================== */

/* CARDS AS OBJECTS, NOT CELLS (iteration 2, § 3.3).
   This grid used to be `gap: 1px` over a --c-border-coloured background: one
   flat slab divided by hairlines that measured 1.18:1. It drew a table, and a
   table is what it read as. Three changes make each cell an object:

     · a real gap between the cells instead of a 1px seam,
     · each cell on its own raised ground (--c-raised, § 5b) with its own
       radius, its own --c-border-card edge and — since the light palette —
       its own --shadow-raised. On paper the fill step is 1.13:1 at most, so
       the shadow is what carries the elevation and the edge is decorative
       at 1.35:1 rather than the 1.30:1 a plain --c-border would give,
     · `overflow: hidden` removed from the grid — it was there to clip the
       shared radius, and it was also silently clipping the hover lift that
       § 16 has been applying to .cap-card all along.

   Grid arithmetic is untouched: same template at every step, same --wide-lg
   span, so the 4-cells-and-2-spans rows still come out exact. */
.bento {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-md);
  list-style: none;
  padding: 0;
  margin: 0;
}

.bento__cell {
  background: var(--c-raised);
  border: var(--bw-hairline) solid var(--c-border-card);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-raised);
  padding: var(--sp-lg);
  min-width: 0;
}

/* The transaction-order ordinal is generated by CSS from a list counter, so
   no literal figure is hard-coded in the markup. */
.bento--sequence { counter-reset: agb-step; }
.bento--sequence .bento__cell { counter-increment: agb-step; }

/* Column flex rather than grid so the DETAIL toggle can be pushed to the
   bottom of the cell with `margin-block-start: auto`. The cells of a row are
   already equal height (grid items stretch), so with the toggle at the foot
   the DETAIL labels line up across the row instead of floating at whatever
   height the body copy happens to end — the phase-5 complaint. */
.cap-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--sp-sm);
}

/* THE ORDINAL (iteration 2, § 2.5).
   The line-glyph icon is gone from the markup, so the ordinal now carries the
   top of the card alone and is sized to do it: --fs-h3 (22->30px) instead of
   the inherited body size, in mono, still at --c-text-3 (6.10:1 on a card
   over the base band, 5.69:1 on a card over the lighter one).

   The element carries `class="cap-card__step mono"`. Both selectors are
   (0,1,0), so the cascade is decided by order and this rule — 1,100 lines
   below .mono — is the one that wins. It therefore has to restate the mono
   family rather than lean on the utility, and it does.

   The margin is the one the deleted .cap-card__icon used to contribute, so
   the title does not jam against the figure. */
.cap-card__step {
  display: block;
  font-family: var(--ff-mono);
  font-size: var(--fs-h3);
  line-height: 1;
  letter-spacing: var(--ls-mono);
  font-weight: var(--fw-mono);
  color: var(--c-text-3);
  font-variant-numeric: tabular-nums;
  margin-block-end: var(--sp-2xs);
}
.cap-card__step::before { content: counter(agb-step, decimal-leading-zero); }
/* Cards outside a sequence carry no step element at all; this is a guard
   against the class appearing without .bento--sequence above it. */
.bento:not(.bento--sequence) .cap-card__step { display: none; }

/* .cap-card__icon: DELETED. The markup agent removed all four SVGs in
   iteration 2; the rule that sized them and the hover rule in § 16 that
   recoloured them are both gone with it. */

.cap-card__title {
  font-size: var(--fs-h3);
  line-height: var(--lh-h3);
  letter-spacing: var(--ls-h3);
  font-weight: var(--fw-h3);
  color: var(--c-text);
}

.cap-card__body {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--c-text-2);
  max-width: var(--measure);
  text-wrap: pretty;
}

.cap-card__detail {
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body-sm);
  color: var(--c-text-3);
  max-width: var(--measure);
  text-wrap: pretty;
}

/* ---- the reveal contract — REWRITTEN AGAIN in iteration 4 (task 1) -------
   Iteration 2 reserved the collapsed detail's space (`opacity` and
   `visibility`, never `display`) so that a card was exactly as tall open as
   closed. Iteration 4 measures what that cost.

   THE MEASUREMENT. "Gap" below is the distance from the bottom of the last
   visible line of card text to the top of the DETAIL label, in real Chrome
   over CDP, per card, before the change:

     1440   47 · 144 · 117 ·  71      budget for task 1: 72px
     1024  115 · 115 · 115 · 168
      768   91 ·  91 ·  91 · 117

   Decomposed, that gap is `--sp-sm + the collapsed detail + slack`, and the
   two terms behave differently:

     · the COLLAPSED DETAIL is 23px in a 624px card and 93px in a 304px one —
       four blank lines reserved for text that is not on screen. It alone is
       over budget in every narrow card at 1024 and 1440.
     · the SLACK is the grid row stretching every card to the tallest in its
       row: 39px on card 2 at 1440, 110px on card 4 at 1024.

   WHICH OF TASK 1's THREE OPTIONS. Neither of the first two closes it alone,
   and that is arithmetic rather than opinion. `align-items: start` removes
   only the slack, leaving 47/117/117/71 at 1440 — card 3 is still 45px over
   with zero stretch in it, because its reserved detail is 93px on its own.
   Capping the bottom zone cannot help either: the zone has to be as tall as
   the tallest reserved detail or the text clips. The third option is copy
   and index.html is frozen (see the note under this block for what I would
   ask for).

   So: KEEP THE STRETCH, DROP THE RESERVATION. The row still stretches, the
   toggle is still pinned to the foot of the cell with `margin-block-start:
   auto`, and the DETAIL labels are therefore still on one baseline across
   every row — which is what task 1 asks to preserve. What goes is the blank
   space held for hidden text: the collapsed detail is `display: none`, so a
   closed card is exactly as tall as the text you can actually read, and the
   only thing left in the gap is the row's own stretch.

   Measured after: 12 · 12 · 12 · 67 at 1440, 12 · 12 · 12 · 65 at 1024,
   12 · 12 · 12 · 39 at 768. Worst card 67px against a 72px budget.

   The contract now, in cascade order:
     1. BASE, every device: the toggle is rendered and the detail is
        collapsed to nothing. The button is the mechanism, and a tap is a
        click.
     2. `margin-block-start: auto` PINS the toggle to the foot of the cell.
        The cells of a row are equal height, so a toggle at the foot of each
        puts every DETAIL label on the same baseline instead of wherever the
        body copy happened to end. Unchanged since phase 5.
     3. :has(:focus-visible) keeps the detail reachable by keyboard with JS
        off; .is-open is the JS enhancement that makes a tap or click sticky.
        NOT :focus-within — see the note above the reveal rule below.
     4. HOVER NO LONGER REVEALS, and that is a consequence of 1, not a
        separate opinion. A reveal that changes the card's height cannot be
        driven by mere pointer transit: the row would grow under the cursor,
        the cursor would fall outside the card, the row would collapse, and
        the two would oscillate. Hover keeps the ground, the edge and the
        3px lift — every treatment that does not touch layout. The toggle is
        the mechanism on every device, which is exactly what iteration 2's
        point 1 said it should be; it is now true of a mouse as well.        */
.cap-card__toggle {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  /* Pinned to the foot of the cell so DETAIL aligns across the row. */
  margin-block-start: auto;
  min-height: var(--target-min);
  padding-inline: var(--sp-xs);
  margin-inline-start: calc(var(--sp-xs) * -1);
  border-radius: var(--r-sm);
  background: none;
  color: var(--c-text-3);
  font-family: var(--ff-mono);
  font-size: var(--fs-mono-sm);
  letter-spacing: var(--ls-mono);
  font-weight: var(--fw-mono);
  text-transform: uppercase;
  cursor: pointer;
}

/* `display: none`, not `visibility: hidden`: the point of iteration 4's task
   1 is that a closed detail occupies no space. Both are equivalent for
   assistive technology — neither is in the accessibility tree, which is what
   `aria-expanded="false"` on the toggle already promises. */
.cap-card__detail { display: none; }

/* `:has(:focus-visible)`, NOT `:focus-within`. A tap — and a mouse click —
   leaves the toggle focused, so with `:focus-within` here the *closing* press
   flipped `aria-expanded` and `.is-open` to false while the detail stayed
   visible: the state said closed and the picture said open, on touch and on
   desktop alike once the pointer left the card. Measured in headless Chrome
   with real `Input.dispatchTouchEvent` taps under `pointer: coarse`.
   `:focus-visible` is false after a tap and after a click, and true after a
   Tab, so this reaches the keyboard on every device — which is the no-JS case
   point 4 above exists for — without sticking to a pointer. */
.cap-card:has(:focus-visible) .cap-card__detail,
.cap-card.is-open             .cap-card__detail { display: block; }

/* WHAT I WOULD ASK THE COPY FOR, since task 1's third option is not mine to
   take. The remaining 67px gap is entirely card 4 ("Report") being stretched
   to card 3 ("Reconcile")'s height, and that is a copy imbalance: card 4's
   body is 60 characters and card 3's is 82, so card 3 sets two more lines in
   a 304px cell than card 4 does. Bringing "One dataset across every
   provider, queryable and exportable." up to roughly card 3's length — one
   more clause, ~20 characters — would close the last 67px to 12px without a
   line of CSS. The same is true of the detail sentences, which run 62, 116,
   106 and 78 characters; evening those out would also stop a card growing by
   four lines when it opens while its neighbour grows by one. */

@media (hover: hover) and (pointer: fine) {
  .cap-card__toggle:hover { color: var(--c-text-2); }

  /* ---- card hover (iteration 2, § 3.3) -----------------------------------
     Ground and edge change together so the card reads as picked up rather
     than merely outlined. The 3px lift and the timing live in § 16 with
     every other transition; these are the target values only.
     `(hover: hover)` is the gate that stops the state sticking after a tap
     on a touch screen.
     Measured: --c-edge-hover is #4436D6 everywhere except #marketing, which
     re-points it to its own accent so the two never meet — 7.65:1 against
     the card it outlines and 7.13:1 against the hovered one; the marketing
     re-point is 5.83:1 and 5.44:1 on the same two. */
  .cap-card:hover,
  .cap-card:has(:focus-visible) {
    background: var(--c-raised-hover);
    border-color: var(--c-edge-hover);
  }
}

/* Keyboard parity: a card whose toggle is keyboard-focused gets the same edge,
   on every device, not only where a mouse exists. `:has(:focus-visible)` for
   the same reason as the reveal rule — `:focus-within` left the edge lit after
   a tap or a click, with nothing on screen still focused to explain it. */
.cap-card:has(:focus-visible) { border-color: var(--c-edge-hover); }

@media (min-width: 640px) {
  /* --wide-lg is inert here, so both grids are 4 cells = 2 exact rows of 2.
     The md-only `--wide` modifier is no longer carried by any element and
     its rule has been removed with the cells that used it. */
  .bento { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 900px) {
  .bento { grid-template-columns: repeat(3, 1fr); }
  .bento__cell { padding: var(--sp-xl); }
  .bento__cell--wide-lg { grid-column: span 2; }
  /* Cells 1 and 4 of each grid carry --wide-lg, so both #one-integration
     and #marketing are 4 cells + 2 spans = 6 slots = 2 exact rows of 3.
     No ragged final row and no orphan --c-border cell at any breakpoint. */
}


/* ==========================================================================
   ==========================================================================
   12. PAGE SECTION 4 — #developers · the API facts list
   --------------------------------------------------------------------------
   THE CODE PANEL IS GONE (iteration 5). Deleted from this block, with the
   markup: .code-panel, .code-panel__intro, .code-panel__tabs,
   .code-panel__tab (+ ::after, the hover pair, the [aria-selected="true"]
   colour and underline, the :focus-visible offset), .code-panel__bar,
   .code-panel__path, .code-panel__copy, .code-panel__status (+ :empty),
   .code-panel__pre (+ :focus-visible) and .code-panel__code. Deleted from
   elsewhere in the file with them: the two entries in § 4's horizontal-
   scroll list, and three rules in § 16 — the tab in the control-transition
   list, the ::after underline transition, and the "Copied" confirmation
   spring, which was the page's one spring and took the View Transitions
   duration rule (`::view-transition-old/new(root)`, the tab panel swap) with
   it. The motion budget is four behaviours now, not six.

   WHAT THIS SECTION IS NOW, AND WHY THE LIST IS TREATED AS AN OBJECT.
   #developers is head + .facts and nothing else. The list was authored as a
   footnote strip UNDER a 1104px panel — --fs-body-sm, hairline rows, capped
   at the measure — and left at that it reads as the tail of something that
   is no longer above it, which is exactly the "orphaned tail" the deletion
   brief names. Three things promote it to the section's object without
   inventing a new component:

     1. IT OPENS ON A --bw-rule, NOT A HAIRLINE. 2px in --c-border-strong is
        the same weight the page gives a section-level edge; a hairline is
        what it gives a row inside a block. The rows below it stay hairline,
        so the block now has a top and a texture rather than four identical
        lines.
     2. IT SETS AT BODY SIZE. --fs-body-sm was calibrated against a code
        panel that outweighed it. With nothing above it the list IS the copy,
        so it sets at --fs-body in --c-text-2 (9.76:1 on --c-bg) with the
        row padding opened from --sp-md to --sp-lg. --measure is in `ch` and
        resolves against this element's own font-size, so the cap is
        restated here and the items agree with it.
     3. IT GOES THREE-ACROSS AT 900, NOT 1200. 900 is where the bento in
        § 11 goes three-across, so the page changes shape at a width it
        already changes shape at, and the list stops being a 496px stack in
        an 826px column for the whole 900-1199 range.

   SPACE IS LEFT FOR THE DIAGRAM. The list keeps `margin-block-start` rather
   than being pinned to the head, so the next agent's diagram drops into the
   object slot between the head and the list on the same 48px rhythm as
   everything else in the section, with no rule here to unpick. Measured free
   height is in docs/iter5-deletions-handoff.md.
   ========================================================================== */

/* --------------------------------------------------------------------------
   12a. THE ONE-INTEGRATION DIAGRAM — TASK 2.1
   --------------------------------------------------------------------------
   The section's object, between the head and .facts. Everything structural
   about it — 26 paths, two geometries, both states, the text alternative and
   the toggle — is authored in index.html and documented in
   docs/iter5-markup-handoff.md § 2. What is decided here is the stroke, the
   colour, the dash, the canvas swap and the toggle's states.

   THE RHYTHM IS THE PAGE'S, NOT ITS OWN. .section__head closes on a 48px
   bottom margin and .facts opens on a 48px top one; margin-block: --sp-2xl
   collapses with both, so the section reads head -> 48 -> diagram -> 48 ->
   list with no rule anywhere pinning one fragment to another. Measured 48px
   on both edges at every width.

   THE CANVAS SWAP IS AT 1024 AND THE MARKUP AGENT SOLVED FOR IT. The wide
   geometry is a 1104-unit viewBox with 13-unit node labels: at 1024 the
   content column is 943px, i.e. scale 0.854, and the SMALLEST type in the
   drawing lands on 11.1px — which is --svg-sub's own size everywhere else on
   this page. At 900 the same label is 9.7px. So 1024 is the last width the
   wide composition can be read at, and no font-size compensation is added
   here because none is needed. Below it the narrow geometry runs, capped and
   centred exactly the way .flow__canvas--v is.

   COLOUR — TWO HARD CONSTRAINTS, BOTH HELD.
     · --c-accent appears nowhere in this block. The links are drawn on the
       neutral border ramp and the labels on the text ramp, as .flow__* does.
       This section sits four bands above #marketing's ochre and a fourth
       indigo here would be exactly the collision § 13's gate exists to stop.
     · No new marker. Every path already carries
       marker-end="url(#agb-arrow-base)", which resolves to the document-level
       sprite whose fill is .flow__arrow--base path (§ 9) — --c-border-strong,
       the same colour the links themselves take.

   Contrast, composited, on the #developers ground — which is --c-sunken
   #F1F1EF on the light palette (§ 5b):
     link / node edge --c-border-strong  -> #797A7B   3.80:1  (1.4.11: 3.0)
     the one thick link --c-border-hover -> #5E5F60   5.66:1
     node label       --c-text-2         on --c-raised   7.81:1
     sub / tag / caption --c-text-3      on --c-raised   5.33:1
     caption on the band --c-text-3      on --c-sunken   4.71:1
     hub node fill    --c-sunken vs the other nodes' --c-raised  1.13:1,
                      carried by the 1.5 stroke and the --c-text label, not
                      by the fill — the fill is the "different kind of node"
                      signal and never the only one.
     tag edge         --c-border         -> #E2E2E2   decorative only, the
                      tag's label carries its own 5.33:1 and the box is a
                      chip, not the boundary of a control.
   -------------------------------------------------------------------------- */

.integration { margin-block: var(--sp-2xl); }

/* The canvas scrolls rather than shrinking below --diagram-min-w, which is
   the pattern .flow__canvas already uses; the thin token-coloured scrollbar
   is what says "there is more this way" rather than "this is truncated". */
.integration__canvas {
  width: 100%;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--c-border-strong) transparent;
}

.integration__svg {
  display: block;
  width: 100%;
  height: auto;
  min-width: var(--diagram-min-w);
  font-family: var(--ff-mono);
}

/* Narrow is the default, wide from the step above. Mobile-first, and it
   means the geometry a phone gets is the one it is served, not the one it
   has to override. */
.integration__canvas--h { display: none; }
.integration__canvas--v .integration__svg {
  max-width: 26rem;
  margin-inline: auto;
}

@media (min-width: 1024px) {
  .integration__canvas--h { display: block; }
  .integration__canvas--v { display: none; }
}

/* THE TWO STATE RULES, AND THERE ARE ONLY TWO. The `with` case is the
   markup's own authored state — every without-layer carries the SVG
   presentation attribute display="none" — so writing it here as well would
   create a second source of truth for the default and would be the rule that
   goes stale first. A presentation attribute is beaten by any CSS
   declaration, so these need no !important and the no-JS render is the
   with-AGB picture, complete. */
.integration[data-integration-state="without"] [data-int-layer="without"] { display: block; }
.integration[data-integration-state="without"] [data-int-layer="with"]    { display: none; }

/* ---- the drawing ------------------------------------------------------ */

/* The class rule beats the `stroke="currentColor"` presentation attribute
   every path carries, which is why the markup could stay colour-agnostic. */
.integration__link {
  fill: none;
  stroke: var(--c-border-strong);
  stroke-width: 1.5;
  stroke-linecap: round;
}

/* The tangle is thinner than the single line on purpose: six 1.5px strokes
   crossing in a 532-unit channel turn their intersections into a blob, and
   the point of the without state is that you can count the connections. */
.integration__link--direct { stroke-width: 1.4; }

/* The one link the section is named after gets the weight, and it gets it
   from the neutral ramp — --c-border-hover is ink at 64% against
   --c-border-strong's 52%, i.e. #5E5F60 against #797A7B on this band. */
.integration__link--in { stroke: var(--c-border-hover); stroke-width: 2; }

.integration__node-box {
  fill: var(--c-raised);
  stroke: var(--c-border-strong);
  stroke-width: 1.25;
}

/* AGB is the only node that is neither yours nor a provider's, so it is the
   only one that steps off the ramp the other nodes are on. On the dark
   palette that was one step further UP; on paper the other nodes are already
   at the top of the ramp (--c-raised is white), so the hub takes the tint
   instead. It is still one step and still the only node that takes it, and
   the heavier stroke and the --c-text label carry it the same way. */
.integration__node--hub .integration__node-box {
  fill: var(--c-sunken);
  stroke: var(--c-border-hover);
  stroke-width: 1.5;
}

/* The Integration tags read as chips, which is the argument: six chips is
   six pieces of work, one chip is one. The fill is a look and not a fix —
   every path is authored with a gap where its tag sits (markup handoff
   § 2.4), so no line runs under a label and no label can be struck through
   whatever the fill is. */
.integration__tag-box {
  fill: var(--c-raised);
  stroke: var(--c-border);
}

.integration__node-label,
.integration__node-sub,
.integration__tag-label,
.integration__caption {
  font-family: var(--ff-mono);
  font-weight: var(--fw-mono);
  letter-spacing: var(--ls-mono);
  fill: var(--c-text-3);
}
.integration__node-label { fill: var(--c-text-2); }
.integration__node--hub .integration__node-label { fill: var(--c-text); }

/* ---- the toggle ------------------------------------------------------- */

.integration__controls {
  display: flex;
  margin-block-start: var(--sp-lg);
}

/* .btn--secondary supplies the box; this supplies the voice. Mono and
   uppercase because it names a state of a drawing, the way every other
   label inside the drawing does. */
.integration__toggle {
  font-family: var(--ff-mono);
  font-size: var(--fs-mono-sm);
  letter-spacing: var(--ls-mono);
  font-weight: var(--fw-mono);
  text-transform: uppercase;
}

/* THE STATE MARKER, AND WHY IT IS NOT OPTIONAL HERE. The label is constant
   by design — a toggle whose accessible name changes as it is pressed is the
   standard way this pattern fails — so `aria-pressed` is the ONLY channel
   the state has, and a sighted reader needs a channel too. This box is it:
   hollow when the with-AGB view is on screen, filled when the without-AGB
   view is. It is a shape, not a colour, so it survives greyscale, forced
   colours and a colour-blind reader, and .btn's own `gap` spaces it. */
.integration__toggle::before {
  content: "";
  inline-size: var(--sp-xs);
  block-size: var(--sp-xs);
  border: var(--bw-hairline) solid currentColor;
  border-radius: var(--r-xs);
}
.integration__toggle[aria-pressed="true"]::before { background: currentColor; }

/* Pressed also takes the fill and the stronger edge .btn--secondary uses for
   hover, so the control looks held down as well as marked. */
.integration__toggle[aria-pressed="true"] {
  --btn-bg: var(--c-overlay-active);
  --btn-border: var(--c-border-hover);
}
@media (hover: hover) and (pointer: fine) {
  .integration__toggle[aria-pressed="true"]:hover { --btn-border: var(--c-text-3); }
}

.facts {
  display: grid;
  list-style: none;
  padding: 0;
  margin-block-start: var(--sp-2xl);
  border-block-start: var(--bw-rule) solid var(--c-border-strong);
  font-size: var(--fs-body);
  max-width: var(--measure);
}

.facts__item {
  padding-block: var(--sp-lg);
  border-block-end: var(--bw-hairline) solid var(--c-border);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-body);
  color: var(--c-text-2);
  text-wrap: pretty;
}

/* Three facts, three columns, each one still inside the measure. Measured at
   1440 the columns are 346px and the longest item sets at 48 rendered
   characters; at 900 they are 254px, the same width the § 11 cards take at
   that step. The container keeps its opening rule and the items keep their
   trailing hairline, so the block reads as one strip rather than as three
   loose paragraphs. */
@media (min-width: 900px) {
  .facts {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    column-gap: var(--sp-xl);
    max-width: none;
  }
}


/* ==========================================================================
   ==========================================================================
   12a. PAGE SECTION 5 — #engineering · the readiness grid and the status
   pill  (phase 3)
   --------------------------------------------------------------------------
   THE WHOLE MECHANISM IS ONE ATTRIBUTE. `data-readiness` on the .ready-card
   carries `available`, `beta` or `roadmap`, and everything the reader sees
   of that state — the word, the ink, the fill, the edge STYLE and the marker
   glyph — is drawn from it here. There is no JavaScript in this and no
   second place to keep in sync: changing the attribute in index.html changes
   the pill completely and nothing else has to move.

   HOW THE WORD GETS THERE. Each state block sets `--pill-label` to a quoted
   string and `.ready-card__pill::after` reads it with `content`. That is the
   only way the label can come from the attribute alone. The pill element in
   the markup is empty apart from an .sr-only prefix, so the accessible name
   is "Readiness: " plus the generated word — generated content is part of
   the text-alternative computation in every engine this page supports.

   THREE STATES THAT DO NOT RELY ON COLOUR, WHICH IS THE HARD REQUIREMENT.
   Colour-blind, greyscale-printed or forced-colours, a reader can still tell
   the three apart on THREE independent non-colour channels:

     state       word         marker (a --sp-xs square)      pill edge
     available   AVAILABLE    filled                         solid
     beta        BETA         half filled, split vertically  solid
     roadmap     ROADMAP      hollow                         DASHED

   The word alone would be enough; the marker and the edge style are what
   make it legible at a glance without reading. The marker is a SQUARE and
   not a dot on purpose — a round dot in three states is the traffic light
   this section is forbidden to draw, and the square already means "state" on
   this page (§ 12's .integration__toggle marker uses the same hollow/filled
   pair). No green, no amber, no red: the status ramp --c-ok / --c-warn /
   --c-err is reserved for TRANSACTION state and appears nowhere here.

   THE ROADMAP STATE IS ALSO THE DEFAULT AND THAT IS DELIBERATE. The base
   .ready-card rule sets the roadmap values, and the two attribute rules
   override them. A card with a missing, empty or misspelled attribute
   therefore renders as ROADMAP — the value CLAUDE.md § 3 names as the
   default — instead of rendering as nothing.

   ONE MEASURED CONSTRAINT ON "available", WRITTEN HERE BECAUSE THIS IS
   WHERE THE ACCENT IS SPENT. --c-accent is indigo and this band sits
   directly above #marketing, whose ochre may never share a screen with it
   (§ 13, § 5b). The base separation from the lowest pill in the grid to the
   ochre rule is measured at 343px at 1440 and 179px at 390 — the row's own
   height plus one --section-y — so no viewport-scaled guard can hold them
   apart the way
   --accent-gap holds #marketing and #getting-started apart on 877px of base
   separation. Shipped, all eight cards are roadmap and this section puts no
   indigo on screen at all; the sweep in docs/final-phase3-notes.md § 7
   confirms it. Setting a card to available is an owner decision that
   requires that sweep to be re-run, and the same warning is in the READINESS
   comment at the top of index.html.

   THE GRID. .bento supplies the geometry (§ 11) and .bento__cell the
   surface, so this section adds no third card component — the file already
   shares both with #marketing. What is local is the column count: 1 below
   640, 2 from 640, 4 from 1024. Eight cards divide evenly by 1, 2 and 4, so
   no row is ragged at any width.

   THE 900 RULE BELOW IS NOT DECORATION, IT IS AN UNDO. .bento takes three
   columns from 900 up, and three columns of eight cards is 3 + 3 + 2 — a
   ragged final row across the whole 900-1023 band. Both rules here are
   (0,1,0), the same weight as the .bento rules they override, and they win
   on source order alone: this block sits after § 11 in the file. Moving it
   above § 11 silently restores the ragged row.

   1024 IS NOT ON THE PROJECT'S BREAKPOINT LIST (480 / 640 / 900 / 1200 /
   1440) and is the second deliberate use of it in this file — the diagram's
   canvas swap in § 12 is the first, and the reason is the same shape: the
   step is set by what fits in the column, not by the list. At 900 the
   content column is 826px and four cards would be 190px wide, which puts
   "Sandbox with deterministic outcomes" on five lines; at 1024 it is 943px
   and four cards are 224px.

   NO HOVER, NO LIFT, NO SPOTLIGHT, AND NOTHING TO REMOVE LATER. These cards
   contain no control and no link, so they get no hover treatment: a card
   that rises under the pointer and does nothing when clicked is a promise
   the markup cannot keep. .cap-card is deliberately NOT on these elements,
   which is also what keeps script.js §3.12 off them — it binds only to a
   [data-stagger] container that contains a .cap-card.
   ========================================================================== */

/* THE CARDS DO NOT STRETCH TO THEIR ROW, AND THE REASON IS THE ONE
   MEASUREMENT THIS SECTION HAD TO HIT. The budget is 72px from the bottom of
   a card's last line of text to the bottom of the card. Grid items stretch by
   default, so a row is as tall as its tallest card and every shorter card
   pays the difference as empty space under its text. Measured with the
   default stretch, worst card per width:

     640   73.6      1024   98.7      1440  128.4
     768   71.8      1200  103.7

   Three of the five are over budget and 1440 is over by 56px. The copy is
   fixed — the shortest body in the grid is 14 words and the longest is 31 —
   so the spread is not something a rule can absorb. § 11's own note on the
   capability grid works through the same three options for the same defect
   and rejects `align-items: start` there BECAUSE the capability cards have a
   DETAIL toggle that must sit on one baseline across the row. These cards
   have no control in them at all, so there is no baseline to hold and no
   reason to keep the stretch. With it off, every card is exactly as tall as
   its own content and the gap is the card's own bottom padding plus the last
   line box's descender slack. Measured after, identical on all 8 cards at
   each width: 19.7px at 360 and 390 (where the block padding is --sp-md),
   27.1px at 640, 27.3px at 768, 27.5px at 900, 27.6px at 1024, 27.9px at
   1200 and 27.3px at 1440. 64 measurements, worst 27.9px against a 72px
   budget. */
.bento--ready { align-items: start; }

/* The list itself takes .bento; only the column counts are local.
   The single-column band is the one that runs against the 18% ceiling, so it
   also runs a tighter row gap — 12px between two cards that are already
   separated by an edge and a shadow, against 16px once they are side by side
   and the gap is doing horizontal work too. 4px x 7 rows is 28px. */
.bento--ready { gap: var(--sp-sm); }

@media (min-width: 640px) {
  .bento--ready { gap: var(--sp-md); }
}
@media (min-width: 900px) {
  .bento--ready { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
  .bento--ready { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* PADDING, AND WHY IT IS TWO STEPS RATHER THAN .bento__cell's.
   .bento__cell is --sp-lg and steps UP to --sp-xl at 900. This card does the
   opposite at both ends, and both ends are measured.

   AT THE TOP: it stays at --sp-lg from 640 up and never takes --sp-xl,
   because four cards to a row is a narrower cell than the capability grid
   ever gets. At 1024 the cell is 224px, so --sp-xl would leave 160px of text
   column against --sp-lg's 176px.

   AT THE BOTTOM: below 640 the block padding drops to --sp-md while the
   inline padding stays --sp-lg. This is the section-height ceiling, not
   taste. One column of eight cards is a tall band: measured at the first
   build the section was 19.33% of the page at 360 and 18.80% at 390, against
   an 18% acceptance ceiling, and #engineering was the worst band on the page
   at both. Shrinking the band also shrinks the page, so the arithmetic is
   `(h - d) <= 0.18 * (P - d)`: at 360 that needs 177px removed, at 390 103px.
   16px of block padding on eight cards is 128px of it, the --sp-xs card gap
   below is 64px more and the tighter row gap on .bento--ready is 28px.
   Measured after: the band is 17.68% at 360 and 17.09% at 390, and it is no
   longer the worst section on the page at any width. The INLINE padding is
   untouched because that is the
   one a reader at 360 actually feels — a 24px side margin inside the card is
   what stops the copy running to the card's edge, and it costs the band
   nothing vertically. */
.ready-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* --sp-xs and not --sp-sm: three fragments in a card that repeats eight
     times, and the pill's own line box already carries leading above the
     title. 8px per card x 8 cards is 64px off the band at every width. */
  gap: var(--sp-xs);
  padding-block: var(--sp-md);
  padding-inline: var(--sp-lg);

  /* THE DEFAULT STATE IS ROADMAP. See the header note. */
  --pill-label:      "Roadmap";
  --pill-fg:         var(--c-text-3);
  --pill-bg:         transparent;
  --pill-edge:       var(--c-border-strong);
  --pill-edge-style: dashed;
  --pill-mark:       transparent;
}

/* The step back up, and it must be declared AFTER the rule above: both are
   (0,1,0) and a media query adds no weight, so source order is the only
   thing deciding which padding a card takes. Written above the base rule it
   would be silently dead at every width. */
@media (min-width: 640px) {
  .ready-card { padding: var(--sp-lg); }
}

.ready-card[data-readiness="beta"] {
  --pill-label:      "Beta";
  --pill-fg:         var(--c-text);
  --pill-bg:         var(--c-sunken);
  --pill-edge:       var(--c-text);
  --pill-edge-style: solid;
  /* Half a square, split on the vertical. Not a tint of the fill — a tint
     would be a fourth colour doing the job the shape is here to do. */
  --pill-mark:       linear-gradient(to right,
                       currentColor 0 50%, transparent 50% 100%);
}

.ready-card[data-readiness="available"] {
  --pill-label:      "Available";
  --pill-fg:         var(--c-accent);
  --pill-bg:         var(--c-accent-muted);
  --pill-edge:       var(--c-accent);
  --pill-edge-style: solid;
  --pill-mark:       currentColor;
}

/* Measured on the card's own ground, --c-raised #FFFFFF, which is the same
   on both band tones:
     roadmap    label --c-text-3 #656C72 on #FFFFFF  5.33:1  (floor 4.5)
                edge --c-border-strong -> #808182     3.90:1  (floor 3.0)
                marker edge #656C72                   5.33:1
     beta       label --c-text #0B0D0F on #F1F1EF    17.21:1
                edge --c-text on the same fill       17.21:1
                edge --c-text against the card       19.47:1
     available  label --c-accent #4436D6 on the 10%
                accent fill, composited #ECEBFB       6.50:1
                edge --c-accent on the same fill      6.50:1
                edge --c-accent against the card      7.65:1
   Card title --c-text 19.47:1, card body --c-text-2 7.81:1, both on the same
   #FFFFFF. Head on the band #F1F1EF: eyebrow --c-text-3 4.71:1, h2 17.21:1,
   lede --c-text-2 6.91:1.
   The pill is not a control — it has no focus, no hover and no press — so
   the edge is measured against 1.4.11's 3.0 as a state carrier rather than
   as a control boundary, and it clears it in all three states. */
.ready-card__pill {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2xs);
  padding-block: var(--sp-3xs);
  padding-inline: var(--sp-xs);
  border: var(--bw-hairline) var(--pill-edge-style) var(--pill-edge);
  border-radius: var(--r-pill);
  background: var(--pill-bg);
  color: var(--pill-fg);
  font-family: var(--ff-mono);
  font-size: var(--fs-mono-sm);
  line-height: var(--lh-mono);
  letter-spacing: var(--ls-mono);
  font-weight: var(--fw-mono);
  text-transform: uppercase;
  white-space: nowrap;
}

/* The marker. `flex: none` so it never shrinks against the label, and the
   border is drawn in every state so the hollow square has an edge and the
   filled one keeps the same box. */
.ready-card__pill::before {
  content: "";
  flex: none;
  width: var(--sp-xs);
  height: var(--sp-xs);
  border: var(--bw-hairline) solid currentColor;
  border-radius: var(--r-xs);
  background: var(--pill-mark);
}

/* The word. This single declaration is what makes the attribute the only
   edit — see the header note. */
.ready-card__pill::after { content: var(--pill-label); }

/* --fs-h4, not the --fs-h3 a capability card takes: eight cards four abreast
   is a denser grid than four cards three abreast, and at 1024 an --fs-h3
   title sets "Sandbox with deterministic outcomes" on four lines in a 176px
   column. The element is still an h3 — this is its size, not its level. */
.ready-card__title {
  font-size: var(--fs-h4);
  line-height: var(--lh-h4);
  letter-spacing: var(--ls-h4);
  font-weight: var(--fw-h4);
  color: var(--c-text);
  text-wrap: pretty;
}

.ready-card__body {
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body-sm);
  letter-spacing: var(--ls-body-sm);
  color: var(--c-text-2);
  max-width: var(--measure);
  text-wrap: pretty;
}

/* ==========================================================================
   ==========================================================================
   13. PAGE SECTION 5 — #marketing · the separate service line
   --------------------------------------------------------------------------
   This section must not read as another platform feature.

   ITERATION 2 CHANGED WHAT THAT COSTS. Signal 1 used to be "a ground one
   tone lighter than the base", and it worked because #marketing was the only
   section in the document that was not on the page ground. Now that every
   band alternates (§ 5b), a lighter ground is no longer unusual — #marketing
   is still the lighter of it and the two bands it sits between, but that on
   its own no longer separates it from #hero, #one-integration and #contact,
   which are on the same tone. The light palette flipped which token that
   tone IS — the lighter one is now --c-bg and the alternate is --c-sunken —
   and changed nothing about this argument.

   So the ground stays in the alternation and the OTHER signals are
   strengthened, per the iteration-2 brief:

     1. GROUND one tone lighter than the bands either side of it — the
        page's own --c-bg, against --c-sunken above and below (§ 5b).
        Still true, no longer sufficient on its own.
     2. A FULL-BLEED RULE ON THE TOP EDGE — edge to edge, not container
        width — drawn in the marketing accent. It is the only edge-to-edge
        line on the page, and it is the boundary between two divisions of
        the company.
     3. Its own accent, --c-marketing, on the eyebrow, that rule, the CTA
        and the card edges.
     4. The sticky nav's own accent follows the section. See the retint
        block at the foot of this section for why that is a separation
        device rather than a fifth use.

   ITERATION 4 — THE BOTTOM RULE IS REMOVED (task 5). Iteration 2 added a
   second full-bleed rule under the section on the argument that a pair of
   lines turns a band into an island. In practice the pair did something
   else: a block of text bracketed above and below by a rule in the same
   colour is the typographic form of a PULL-QUOTE, and #marketing is not a
   quotation from the page around it, it is a different line of business.
   The top rule is a boundary between two things and it earns its place; the
   bottom rule only closed a bracket.

   What separates the section now, measured at 1440x900 after the removal:
     · its own ground, --c-bg, against #developers' --c-sunken above and
       #getting-started's --c-sunken below (§ 5b);
     · the top rule, 2px of ochre edge to edge, the only such line on the page;
     · --section-y opening it (112px) and --section-y-loose closing it
       (160px, on #getting-started) — 272px of quiet, still more than any
       other boundary on the page has, and after iteration 5's seam pass it
       is air the section is GIVEN rather than air left over from a guard:
       the 315px accent gap that used to close it was 35vh of viewport-scaled
       separation, and § 1b now spends 0 of it at a 900px-tall window;
     · four ochre signals inside it that appear nowhere else — the eyebrow,
       the card edges, the card hover edge and the CTA fill;
     · the monochrome header while it is being read.
   Six signals, of which the ground and the top rule are visible before a
   single word is read. The bottom rule was the seventh and the weakest.

   THE ACCENT BUDGET, restated. --c-marketing appears on: the eyebrow, the
   top full-bleed rule, the section CTA, the card edges, the card hover edge
   (--c-edge-hover, re-pointed below), and the sticky nav while this section
   is the one being read. Every one of those is either inside #marketing or
   exists specifically so that indigo and ochre are never on screen together.
   --c-accent appears nowhere inside #marketing at all — which is the point.

   No payment vocabulary, no shared metaphor, no bridge in either direction:
   that part is the markup's job and is already true of the copy.
   ========================================================================== */

/* The ground is set in § 5b with the rest of the alternation. What is set
   here is only what makes this section an island.
   Full-bleed because .section is the full-width element; the container
   inside it keeps the copy on the same measure as every other section. */
.section--marketing {
  /* THE OPENING IS THE PAGE RHYTHM (iteration 5, seam pass). It was
     --section-y-loose, on the argument that the extra 48px is what stops the
     full-bleed rule reading as a lid clamped on the head. Measured at 1440
     the rule is 2px tall and the eyebrow sets at 13px, a ~9.4px cap-height:
     at --section-y the clearance is 114px rule box to eyebrow box, more than
     eleven times the thing it is holding off and exactly the distance the
     section closes on. The lid reading comes from a rule sitting ON a head,
     not from 48px of a 160px opening, and those 48px were half of a 302px
     seam.
     The tail is the page-wide --section-y from § 5b like every other section
     (task 2); the separation underneath is bought by #getting-started's
     accent guard, which is re-shaped in § 1b and is measured, not
     decorative. */
  padding-block-start: var(--section-y);
  padding-block-end:   var(--section-y);
  border-block-start: var(--bw-rule) solid var(--c-marketing);
  /* Cards inside this section must never borrow the payment accent on
     hover — that would put indigo and ochre in the same card. */
  --c-edge-hover: var(--c-marketing);

  /* -- TASK 2.8: THE SAME TECHNIQUES AT A DIFFERENT TEMPO ---------------
     Six declarations and not one duplicated rule. Every behaviour in § 16
     reads these tokens through the cascade, so re-pointing them on the
     section is the whole of the task: the cards arrive 112ms apart instead
     of 60, over 640ms instead of 440, from 18px instead of 12; the hover
     lift takes 260ms instead of 180; the spotlight is wider and fades in
     over 420ms instead of 260. Nothing here is a new rule, a new keyframe
     or a new element, and every one of them is still transform and opacity.

     Slower AND further apart is the point. #marketing is a different
     division of the company, and the way this page has said that so far is
     colour, ground and air; motion is the fourth channel and it says the
     same thing — this section is not in a hurry, because it is not selling
     the platform.

     ITS OWN ACCENT STAYS ITS OWN. Not one of these six is a colour.
     --c-spot-edge is neutral ink, so the pointer ring inside a marketing
     card is shade on an ochre-edged card and not a second ochre;
     --c-edge-hover above is what keeps the card's edge ochre and the
     platform's indigo, and it is untouched by the tempo change.
     ONE OF THE SIX WENT WITH THE LIGHT PALETTE: --c-spot is deleted (§ 1b),
     so this section re-points FIVE tokens now, not six. --spot-size is still
     one of them and still widens the ring here.

     --stagger is 112ms here against 60ms elsewhere, i.e. deliberately
     outside task 2.5's 50-70ms band. 2.5 sets the platform's tempo; 2.8
     asks for a slower one, and a single token cannot be both. */
  --stagger:       var(--stagger-slow);
  --dur-entrance:  var(--dur-entrance-slow);
  --rise-reveal:   var(--rise-reveal-slow);
  --dur-lift:      260ms;
  --dur-spot:      420ms;
  --spot-size:     26rem;
}

.eyebrow--marketing { color: var(--c-marketing); }

/* Same geometry as the platform bento, different edge: the cells carry the
   marketing tint instead of the neutral --c-border-card, so the rhyme with
   the platform grid is broken on purpose. The cells' GROUND comes from
   --c-raised like every other card, which on the light palette is the one
   raised tone, the same on every band. */
.bento--marketing .bento__cell { border-color: var(--c-marketing-edge); }
/* Declared here rather than relying on --c-edge-hover: the rule above is a
   (0,2,0) selector in a later section than § 11's `.cap-card:hover`, so it
   would win the resting colour back on hover. Same specificity, later
   position, explicit intent.
   Measured: #8C5A22 on the #F7F7F5 hover fill 5.44:1. */
@media (hover: hover) and (pointer: fine) {
  .bento--marketing .cap-card:hover { border-color: var(--c-marketing); }
}

/* Same size and shape as the platform CTA — only the fill differs, so the
   two never read as the same call. Component-scoped variables only; no new
   global token, per § Token naming. */
.section--marketing .btn--primary {
  --btn-bg: var(--c-marketing);
  --btn-fg: var(--c-marketing-ink);
}
@media (hover: hover) and (pointer: fine) {
  .section--marketing .btn--primary:hover { --btn-bg: var(--c-marketing-hover); }
}
.section--marketing .btn--primary:active { --btn-bg: var(--c-marketing-active); }

/* -- THE MONOCHROME HEADER (iteration 2, § 3.5) -------------------------
   Two accents in one viewport compete, and the brief requires that indigo
   and --c-marketing never share a screen.

   Inside the page body that is a question of spacing, and it is measured
   (docs/iter2-css-notes.md): the nearest indigo above #marketing is the code
   panel's active tab, the nearest below is the #getting-started CTA, and at
   a 900px viewport neither is ever on screen with a ochre rule.

   The sticky header is the case spacing cannot solve. It is on screen at
   EVERY scroll position and it carries indigo three times — the progress
   line, the active nav underline and the CTA fill. Anywhere near #marketing
   those three sit above a ochre rule, a ochre eyebrow and a ochre CTA.

   So the header drops its accent while the reader is anywhere near the
   marketing band. script.js's scroll-spy already marks the current section
   with .is-active on its nav link, so :has() reads it with no new JS and no
   new markup. The gate is a SET of sections rather than #marketing alone,
   and that is deliberate: the spy's rootMargin makes a section current only
   once its top has climbed to 40% of the viewport, so the ochre rule is on
   screen for several hundred px of scrolling BEFORE #marketing becomes
   current, and #getting-started (which has no nav link of its own, so it
   never becomes current) is on screen for several hundred more AFTER.
   Gating on the set covers both overhangs.

   ITERATION 4 — #developers IS NOW IN THE GATE, and it had to be. The
   markup agent inserted it between #one-integration and #marketing, so the
   section that is current for the whole stretch immediately above the ochre
   rule is no longer #one-integration, it is #developers. Measured before
   the fix at 1440x900: #developers is current from scrollY 3816, the ochre
   rule enters the viewport at 4601, and #marketing does not become current
   until 5141 — a 540px window in which the header was fully indigo with a
   ochre rule on screen. Re-verifying the gate against the new section count
   rather than trusting the old three-line selector is the whole point.

   PHASE 3 — #engineering IS NOW IN THE GATE, FOR EXACTLY THE REASON
   #developers WAS ADDED IN ITERATION 4, AND THE FAILURE MODE IS THE SAME
   ONE. The new section sits between #developers and #marketing and carries
   its own nav link, so it is the section the spy marks current for the whole
   stretch immediately above the ochre rule. Without this line the header
   would be fully indigo — progress line, active underline, CTA fill — for
   that entire stretch, with the rule on screen. The four-line gate is the
   set of every section whose reading position can put ochre in the viewport,
   and it must grow whenever a section is inserted into that stretch.

   Retinting the header to SAND was the other option and is worse: it would
   put --c-marketing above #getting-started's indigo CTA for 700px of scroll,
   trading one collision for another.

   --c-focus is deliberately NOT re-pointed. A focus ring is a system
   affordance, not brand colour, and it must be the same everywhere.

   Progressive enhancement, not a dependency: with JS off no link is ever
   marked active, the selector never matches, and the header stays indigo —
   the state it is in today.
   Measured, on the nav ground #FBFBFA: #4C5359 7.54:1 (1.4.11 object);
   --c-accent-ink #FFFFFF on a #4C5359 fill 7.81:1; on the #0B0D0F hover
   fill 19.47:1; on the #656C72 pressed fill 5.33:1. All AA. */
.nav:has([data-nav-link="one-integration"].is-active),
.nav:has([data-nav-link="developers"].is-active),
.nav:has([data-nav-link="engineering"].is-active),
.nav:has([data-nav-link="marketing"].is-active) {
  --c-accent:        var(--c-text-2);
  --c-accent-hover:  var(--c-text);
  --c-accent-active: var(--c-text-3);
  /* --c-accent-ink (#FFFFFF on this palette) is unchanged: it clears AA on
     all three of the neutral fills above, so the CTA label needs no special
     case. It flipped from near-black to white with the palette, and it
     flipped for BOTH accents at once, which is what keeps this rule a
     three-line re-point rather than a fourth button variant. */
}


/* ==========================================================================
   ==========================================================================
   14. PAGE SECTION 6 — #getting-started
   ========================================================================== */

.steps { display: grid; }

.step {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--sp-lg);
  align-items: start;
  padding-block: var(--sp-xl);
  border-block-start: var(--bw-hairline) solid var(--c-border);
}
.step:first-child { border-block-start: 0; padding-block-start: 0; }

/* --c-text-dim is permitted only at >=24px. --fs-h2 is 28->44px, so this
   clears it at every width, including 360. */
.step__num {
  font-size: var(--fs-h2);
  line-height: 1;
  letter-spacing: var(--ls-h2);
  font-weight: var(--fw-body);
  color: var(--c-text-dim);
  font-variant-numeric: tabular-nums;
}

.step__body { display: grid; gap: var(--sp-xs); min-width: 0; }

/* The heading level is correct (section h2 -> h3); the size is a class
   decision, per build contract §4. */
.step__title {
  font-size: var(--fs-h4);
  line-height: var(--lh-h4);
  letter-spacing: var(--ls-h4);
  font-weight: var(--fw-medium);
  color: var(--c-text);
}

.steps__closing {
  margin-block-start: var(--sp-2xl);
  max-width: var(--measure);
}

.steps__cta { margin-block-start: var(--sp-xl); }
.steps__cta .btn { width: 100%; }

@media (min-width: 480px) {
  .steps__cta .btn { width: auto; }
}


/* ==========================================================================
   ==========================================================================
   15. PAGE SECTION 7 — #contact
   --------------------------------------------------------------------------
   THIS BLOCK IS IN TWO HALVES, AND THE SECOND ONE MATCHES NOTHING TODAY.

     15a  THE LIVE HALF — .contact and its .contact__* children. An address,
          a copy control and a short list. No form, no fields, no success
          state. This is what #contact renders.

     15b  THE MOTHBALLED HALF — .field*, .checkbox*, .contact-form* and
          .contact__success*. Every one of those classes had exactly one
          consumer, the contact form, and phase 2 deleted that form from
          index.html because it had no backend and told the visitor their
          message had been received anyway. THE RULES ARE KEPT ON PURPOSE.
          They are not orphans left behind by a sloppy delete; they are the
          other half of a deliberate mothball whose JS half is script.js
          § 3.8, and they come back together the day a backend exists. Do
          not delete one half without the other, and do not "clean up" this
          block on the grounds that grep finds no markup for it.

   Two fragments of the motion layer belong to 15b as well and carry the
   same note there: the .field__input / .checkbox__box transition in § 16,
   and the .btn__label / .btn__spinner pair.
   ========================================================================== */

/* ==========================================================================
   15a. LIVE — the enquiry route
   --------------------------------------------------------------------------
   .contact stacks two blocks: the address, and the list of what to put in
   the message. --sp-2xl between them is the same block separation the rest
   of the page uses inside a section.
   ========================================================================== */

.contact {
  display: grid;
  gap: var(--sp-2xl);
  max-width: var(--container-narrow);
}

/* THE VISIBLE TEXT OF THE LINK IS THE ADDRESS. It is set in mono at --fs-h4
   so it reads as the object of the section rather than as a line of prose,
   and `overflow-wrap: anywhere` is a guard, not a fix: 18 mono characters
   set to ~194px at 360 inside a 312px column, so it fits — but an address is
   the one string on this page that must never be the thing that overflows.
   The row wraps at 360, where the button drops under the link. */
.contact__address {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-md);
}

.contact__email {
  font-family: var(--ff-mono);
  font-size: var(--fs-h4);
  line-height: var(--lh-h4);
  letter-spacing: var(--ls-mono);
  font-weight: var(--fw-mono);
  overflow-wrap: anywhere;
}

/* The copy control is secondary to the address, not a call to action: the
   address is already usable without it. --h-control-sm is the token that was
   written for this control. */
.contact__copy {
  min-height: var(--h-control-sm);
  padding-inline: var(--sp-md);
}

/* Confirmed state. The border carries it and the status region below carries
   it in words, so colour is never the only signal. script.js removes the
   class on the same timer that clears the text. */
.contact__copy.is-copied { --btn-border: var(--c-ok); }

/* The copy module's live region (script.js § 3.6). It sits on its own row
   under the address and the button, and it is EMPTY at rest.

   IT KEEPS ITS LINE BOX WHEN EMPTY, DELIBERATELY. `:empty { display: none }`
   would be tidier at rest and would shift the whole block down by a line the
   moment the visitor pressed the button. One reserved mono line is the price
   of zero layout shift, and the page's CLS budget is zero. */
.contact__copy-status {
  flex-basis: 100%;
  min-height: calc(var(--fs-mono-sm) * var(--lh-mono));
  color: var(--c-text-2);
}

.contact__include-title {
  font-size: var(--fs-h4);
  line-height: var(--lh-h4);
  letter-spacing: var(--ls-h4);
  font-weight: var(--fw-h4);
  color: var(--c-text);
  margin-block-end: var(--sp-md);
}

/* The same strip idiom as .facts in § 12: a hairline opens each row, so three
   short phrases read as one checklist rather than as three loose lines. The
   list-style and padding reset comes from the `ul[class]` rule in § 2. */
.contact__include-list { display: grid; }

.contact__include-item {
  padding-block: var(--sp-sm);
  border-block-start: var(--bw-hairline) solid var(--c-border);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-body);
  color: var(--c-text-2);
  max-width: var(--measure);
}


/* ==========================================================================
   15b. MOTHBALLED — the contact form
   --------------------------------------------------------------------------
   NOT DEAD CODE, NOT AN OVERSIGHT, DO NOT DELETE. Nothing below matches an
   element in any of the four HTML files today. See the banner above.
   ========================================================================== */

.field { display: grid; gap: var(--sp-2xs); min-width: 0; }

.field__label {
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body-sm);
  color: var(--c-text-2);
}
/* The label stays neutral in the error state — turning it red doubles the
   alarm for no extra information. */

.field__req,
.field__opt {
  font-size: var(--fs-caption);
  color: var(--c-text-3);
}

.field__input {
  width: 100%;
  min-height: var(--h-control);
  padding-inline: var(--sp-sm);
  border: var(--bw-strong) solid var(--c-border-strong);
  border-radius: var(--r-md);
  background: var(--c-sunken);
  color: var(--c-text);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
}
@media (hover: hover) and (pointer: fine) {
  .field__input:hover { border-color: var(--c-border-hover); }
}
.field__input:focus { border-color: var(--c-accent); }
.field__input[aria-invalid="true"] { border-color: var(--c-err); }
.field__input:disabled {
  background: var(--c-raised);
  border-color: var(--c-border);
  color: var(--c-text-disabled);
  cursor: not-allowed;
}

/* A CSS-drawn chevron: two gradient half-squares, painted in currentColor,
   so no data-URI hex is needed and the mark follows the palette. */
.field__select {
  appearance: none;
  -webkit-appearance: none;
  padding-inline-end: var(--sp-2xl);
  background-image:
    linear-gradient(45deg,  transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-size: var(--select-arrow) var(--select-arrow);
  /* The 45deg half must sit on the LEFT and the 135deg half on the RIGHT for
     the two to meet as a downward chevron; in `right <offset>` the LARGER
     offset is the left-hand one. */
  background-position:
    right calc(var(--sp-lg) + var(--select-arrow) / 2) center,
    right var(--sp-lg) center;
  background-repeat: no-repeat;
  cursor: pointer;
}
.field__select option { background: var(--c-raised); color: var(--c-text); }

.field__textarea {
  min-height: var(--h-textarea);
  padding-block: var(--sp-sm);
  line-height: 1.6;
  resize: vertical;
}

.field__hint {
  font-size: var(--fs-caption);
  line-height: var(--lh-caption);
  color: var(--c-text-3);
  max-width: var(--measure);
}

/* Colour is never the only carrier: the icon JS injects, the words, and
   aria-invalid each carry the error independently. */
.field__error {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2xs);
  margin-block-start: var(--sp-xs);
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body-sm);
  color: var(--c-err);
  max-width: var(--measure);
}
.field__error:empty { display: none; }

.field--consent { gap: var(--sp-xs); }

.checkbox {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--sp-sm);
  align-items: start;
  padding-block: var(--sp-2xs);
  cursor: pointer;
}

.checkbox__input {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  width: var(--target-min);
  height: var(--target-min);
  opacity: 0;
  margin: 0;
  cursor: pointer;
}

.checkbox__box {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: var(--size-check);
  height: var(--size-check);
  margin-block-start: var(--sp-3xs);
  border: var(--bw-strong) solid var(--c-border-strong);
  border-radius: var(--r-xs);
  background: var(--c-sunken);
  color: var(--c-accent-ink);
}
.checkbox__box svg { opacity: 0; }

/* Adjacent-sibling selectors — no :has() needed, works everywhere. */
.checkbox__input:checked + .checkbox__box {
  background: var(--c-accent);
  border-color: var(--c-accent);
}
.checkbox__input:checked + .checkbox__box svg { opacity: 1; }
.checkbox__input:focus-visible + .checkbox__box {
  outline: var(--bw-focus) solid var(--c-focus);
  outline-offset: var(--focus-offset);
}

.checkbox__text {
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body-sm);
  color: var(--c-text-2);
  max-width: var(--measure);
}

/* `.contact` itself is LIVE and is declared in § 15a above — it is the
   wrapper both the address block and the form use. Its old declaration stood
   here and set max-width only; the live one adds the grid. When the form
   comes back it becomes a grid child, which is what `.contact-form`'s own
   gap below already assumes. */

.contact-form { display: grid; gap: var(--sp-lg); }

.contact-form__legend {
  font-size: var(--fs-caption);
  line-height: var(--lh-caption);
  color: var(--c-text-3);
  max-width: var(--measure);
}

.contact-form__actions { display: grid; }

.contact-form__submit {
  position: relative;
  /* Enough inline padding that swapping the label for a spinner cannot
     change the button's width. */
  padding-inline: var(--sp-2xl);
}

.btn__label { display: inline-block; }

.btn__spinner {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  width: var(--size-spinner);
  height: var(--size-spinner);
  margin-block-start: calc(var(--size-spinner) / -2);
  margin-inline-start: calc(var(--size-spinner) / -2);
  border: var(--bw-focus) solid currentColor;
  border-block-start-color: transparent;
  border-radius: var(--r-pill);
  opacity: 0;
}
.contact-form__submit.is-loading .btn__label  { opacity: 0; }
.contact-form__submit.is-loading .btn__spinner { opacity: 1; }

.contact-form__status {
  /* Lives outside the form (so it survives the form being hidden on success),
     so it carries the grid gap the form would otherwise have given it. */
  margin-block-start: var(--sp-lg);
  color: var(--c-text-2);
  max-width: var(--measure);
}
.contact-form__status:empty { display: none; }
.contact-form__status.is-error   { color: var(--c-err); }
.contact-form__status.is-success { color: var(--c-ok); }

.contact__success { margin-block-start: var(--sp-lg); }
.contact__success-title {
  font-size: var(--fs-h4);
  line-height: var(--lh-h4);
  letter-spacing: var(--ls-h4);
  font-weight: var(--fw-h4);
  color: var(--c-text);
  margin-block-end: var(--sp-xs);
}
.contact__success-title:focus-visible {
  outline: var(--bw-focus) solid var(--c-focus);
  outline-offset: var(--focus-offset);
}

@media (min-width: 480px) {
  .contact-form__actions { justify-items: start; }
}

@media (min-width: 900px) {
  .contact__success { padding: var(--sp-xl); }
}


/* ==========================================================================
   ==========================================================================
   16. MOTION LAYER
   --------------------------------------------------------------------------
   EVERY transition, animation and @keyframes on this page is inside this
   block and nowhere else. The block is wholly inside
   `@media (prefers-reduced-motion: no-preference)`, and each duration is
   ALSO multiplied by `--motion`, which is 0 outside that same gate. Two
   independent guards; either one alone would be enough.

   Only `opacity`, `transform`, `background-color`, `border-color`, `color`,
   `outline-color`, `fill`, `stroke`, `stroke-opacity` and `stroke-dashoffset`
   are animated. Never width, height, top/left, margin, padding or box-shadow.

   The inherited budget is FOUR behaviours (design system § Motion listed
   six):
     1. the flow diagram's travelling pulse
     2. the attempt-log rows stepping in
     3. bento cell hover
     5. staggered scroll reveal
   Plus the micro-transitions on controls that the component tables specify.
   Numbers 4 (tab panel swap) and 6 (the "Copied" confirmation spring) went
   with the code panel; the surviving numbers are deliberately NOT re-flowed,
   so the four that remain still match the design system's list. The View
   Transitions duration rule went with number 4 — script.js no longer starts
   a view transition anywhere.

   ITERATION 5 ADDS SIX, and the count is worth stating honestly because the
   brief for it asked for restraint and "five flawless beats nine noisy":

     2.1  the diagram's wiring draws itself in, once, on reveal
     2.3  a pointer spotlight inside a card, fine pointers only
     2.4  headings arrive one rendered line at a time
     2.5  three groups stagger their children instead of arriving as blocks
     2.6  a grid texture drifts across the hero at 1.4px per second
     2.9  three primary CTAs lean ~4px toward the pointer

   Two of the nine things the brief listed are NOT motion and are not here:
   2.7 (the rail) is structure in § 6b and only its state change is timed,
   and 2.8 (the marketing tempo) is six token re-points on the section in
   § 13 rather than a behaviour of its own.

   WHAT WAS TURNED DOWN, in one place so it can be argued with: the diagram
   does not re-draw on hover or on scroll, only once; the spotlight has no
   scale, no tilt and no shadow; the heading rise replaces the head's own
   entrance rather than playing on top of it; the hero texture moves slowly
   enough that a still observer cannot see it move; and the magnetic offset
   is at the bottom of the 2-4px band, not the top. The reasoning for each
   is in docs/iter5-css-notes.md § 8.
   ========================================================================== */

@media (prefers-reduced-motion: no-preference) {

  html { scroll-behavior: smooth; }

  /* ---- controls: hover / focus / press micro-transitions -------------- */
  .btn,
  .nav__link,
  .nav__panel-link,
  .nav__toggle,
  .footer__link,
  /* .field__input and .checkbox__box are MOTHBALLED with § 15b and match
     nothing today; they stay in the list so the form arrives with its
     transitions already correct. */
  .field__input,
  .checkbox__box,
  .cap-card__toggle,
  .skip-link,
  p a, li a, address a {
    transition:
      background-color calc(var(--dur-micro) * var(--motion)) var(--ease-out),
      border-color     calc(var(--dur-micro) * var(--motion)) var(--ease-out),
      color            calc(var(--dur-micro) * var(--motion)) var(--ease-out),
      outline-color    calc(var(--dur-micro) * var(--motion)) var(--ease-out),
      text-decoration-color calc(var(--dur-micro) * var(--motion)) var(--ease-out),
      opacity          calc(var(--dur-micro) * var(--motion)) var(--ease-out),
      transform        calc(var(--dur-standard) * var(--motion)) var(--ease-out);
  }

  /* The press is a colour-only change plus a 1px nudge, at --dur-instant. */
  .btn:active { transition-duration: calc(var(--dur-instant) * var(--motion)); }

  /* ---- 3. card hover: ground + edge + a 3px rise. No scale, no shadow. -
     Iteration 2: the ground now changes as well as the edge (§ 3.3), and the
     rise is 3px rather than 2 because the card has an edge of its own to lift
     away from. --dur-lift is 180ms, inside the 150-200ms the brief asks for.
     Still transform / colour only, still nothing that touches layout, and
     still wholly inside the reduced-motion gate: a reader who has asked for
     reduced motion gets the hover COLOURS (they are declared in § 11, which
     is not gated) and no movement at all.
     .cap-card__icon is gone from the markup, and its colour transition and
     hover rule have gone with it. */
  @media (hover: hover) and (pointer: fine) {
    .cap-card {
      transition:
        transform        calc(var(--dur-lift)  * var(--motion)) var(--ease-out),
        background-color calc(var(--dur-lift)  * var(--motion)) var(--ease-out),
        border-color     calc(var(--dur-lift)  * var(--motion)) var(--ease-out);
    }
    .cap-card:hover {
      transform: translateY(calc(var(--nudge-card) * -1));
      z-index: var(--z-raised);
    }
  }

  /* THE DETAIL REVEAL HAS NO TRANSITION, and it is not an oversight.
     Since iteration 4 (§ 11, task 1) the collapsed detail is `display: none`
     rather than a reserved block at `opacity: 0`, because reserving up to
     93px of blank space per card for text that is not on screen is the
     defect task 1 is about. `display` is not an animatable property, and the
     properties that would animate the change — height, grid-template-rows —
     are layout, which this file does not animate. So the disclosure is
     instant, which is the correct behaviour for a control the reader has
     just pressed. The opacity/visibility transition that used to be here has
     been removed rather than left to apply to nothing. */

  /* ---- the burger folding into an ✕ ---------------------------------- */
  .nav__toggle-bar {
    transition: transform calc(var(--dur-standard) * var(--motion)) var(--ease-standard);
  }

  /* ---- 1. the flow diagram ------------------------------------------- */
  .flow__edge,
  .flow__node-box,
  .flow__node-label,
  .flow__node-glyph,
  .flow__node {
    transition:
      stroke         calc(var(--dur-standard) * var(--motion)) var(--ease-out),
      fill           calc(var(--dur-standard) * var(--motion)) var(--ease-out),
      stroke-opacity calc(var(--dur-standard) * var(--motion)) var(--ease-out),
      opacity        calc(var(--dur-standard) * var(--motion)) var(--ease-out);
  }

  /* The pulse edge exists ONLY here. Outside this gate it has no stroke at
     all, so under reduced motion the base edge and the node states carry
     the whole story on their own. It runs only on lit segments — a resting
     diagram does not pulse. */
  .flow__pulse.is-lit {
    stroke: var(--c-accent);
    stroke-width: 2;
    stroke-linecap: round;
    fill: none;
    stroke-dasharray: 24 400;
    animation: agb-comet calc(var(--dur-ambient) * var(--motion)) linear infinite;
  }
  /* Per-segment stagger, so the pulse reads as one transaction travelling
     rather than as seven independent blinks. */
  .flow__pulse[data-edge="agb-pa"]     { animation-delay: calc(var(--stagger) * 2); }
  .flow__pulse[data-edge="agb-pb"]     { animation-delay: calc(var(--stagger) * 3); }
  .flow__pulse[data-edge="agb-pc"]     { animation-delay: calc(var(--stagger) * 4); }
  .flow__pulse[data-edge="pa-issuer"]  { animation-delay: calc(var(--stagger) * 6); }
  .flow__pulse[data-edge="pb-issuer"]  { animation-delay: calc(var(--stagger) * 7); }
  .flow__pulse[data-edge="pc-issuer"]  { animation-delay: calc(var(--stagger) * 8); }

  @keyframes agb-comet {
    from { stroke-dashoffset: 424; }
    to   { stroke-dashoffset: 0; }
  }

  /* ---- 2. attempt-log rows stepping in -------------------------------- */
  .demo__row {
    transition: color calc(var(--dur-micro) * var(--motion)) var(--ease-out);
  }
  .demo__row.is-done .demo__cell {
    animation: agb-step-in calc(150ms * var(--motion)) var(--ease-out) both;
  }
  @keyframes agb-step-in {
    from { opacity: 0; transform: translateY(calc(var(--rise-step) * -1)); }
    to   { opacity: 1; transform: none; }
  }

  /* ---- 5. staggered scroll reveal -------------------------------------
     ONE-SHOT ENTRANCE, NOT A HIDDEN RESTING STATE. This is the important
     line in the whole block: nothing anywhere in this file sets
     `opacity: 0` on a [data-reveal] element. Every one of them is fully
     opaque, in place and readable in the base cascade. `.is-revealed`,
     which script.js adds from its IntersectionObserver, only *plays* a
     440ms rise-and-fade on top of that. So a browser with JavaScript
     disabled, a browser that fails to load script.js, the window between
     parse and `defer` execution, and a reader who has asked for reduced
     motion all get the identical finished page — the reveal cannot strand
     content invisible, because there is no state in which content is
     invisible.

     The design system § prefers-reduced-motion sketches the opposite shape
     (hide at rest, unhide on the class) and a native `animation-timeline:
     view()` version above it. Both were built and both were rejected here;
     the reasons are in docs/design-system.md § CSS notes. Short version:
     the design system's own sketch blanks the page with JS off, and the
     scroll-driven version was measured leaving whole sections at opacity 0
     with a stalled timeline.

     `--i` is written inline by script.js from `data-reveal-delay`; the
     fallback of 0 means an element without the attribute simply starts at
     once. The stagger is capped at four in the markup, well inside the
     eight-item ceiling the design system sets.
     ------------------------------------------------------------------- */
  [data-reveal].is-revealed {
    animation: agb-rise calc(var(--dur-entrance) * var(--motion)) var(--ease-emphasis) both;
    animation-delay: calc(var(--i, 0) * var(--stagger) * var(--motion));
  }

  @keyframes agb-rise {
    from { opacity: 0; transform: translateY(var(--rise-reveal)); }
    to   { opacity: 1; transform: none; }
  }

  /* ---- form: submit spinner — MOTHBALLED with § 15b -------------------
     These three rules and the keyframes below match nothing today: the
     contact form they belong to was deleted in phase 2. They are kept for
     the same reason § 15b is kept, and they come back with it. Do not
     delete them on their own. The .field__input / .checkbox__box entry in
     the control-transition list above is the third mothballed fragment in
     this section. */
  .btn__label,
  .btn__spinner {
    transition: opacity calc(var(--dur-micro) * var(--motion)) var(--ease-out);
  }
  .contact-form__submit.is-loading .btn__spinner {
    animation: agb-spin calc(700ms * var(--motion)) linear infinite;
  }
  @keyframes agb-spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
  }

  /* ---- mobile nav panel entrance -------------------------------------- */
  .nav__panel.is-open {
    animation: agb-panel-in calc(var(--dur-standard) * var(--motion)) var(--ease-standard) both;
  }
  @keyframes agb-panel-in {
    from { opacity: 0; transform: translateY(calc(var(--rise-panel) * -1)); }
    to   { opacity: 1; transform: none; }
  }


  /* ======================================================================
     ITERATION 5 — THE DECORATIVE LAYER.
     Everything below is new in iteration 5 and every line of it is inside
     this gate, which is the whole of the reduced-motion contract: not one
     of these behaviours has a resting state that hides, offsets, dims or
     re-colours anything. Under `prefers-reduced-motion: reduce` the page
     is the page — headings upright, cards flat, diagram drawn and in the
     with-AGB state, rail present, hero texture present but still, buttons
     where their boxes are.
     ====================================================================== */

  /* ---- 2.1 · the diagram assembles itself -----------------------------
     The cast is already there when the block arrives — merchant, six
     providers, AGB — and only the WIRING draws. That is the argument the
     section makes in prose, made once more in the way the picture appears,
     and it is why .integration's own [data-reveal] rise is switched off
     below rather than played underneath the draw: a block that fades up
     while its contents draw is two entrances for one object.

     IT NEEDS NO NEW JAVASCRIPT AND TAKES SOME IF IT COMES. `.is-revealed`
     is what script.js's IntersectionObserver already writes on every
     [data-reveal], and .integration carries the attribute, so the draw runs
     today. The JS agent owns the SECOND case — re-arming the draw when the
     toggle swaps layers, which a transition cannot do on its own because a
     `display: none` -> `block` element has no before-change style to
     transition from. The contract is one class: add `is-undrawn` to
     .integration, force a reflow, remove it. Both hooks are declared here;
     neither depends on the other.

     pathLength="1" is what makes `1 1` / offset 1 correct for all 26 paths
     at once — see the token note in § 1b.

     marker-end is NOT affected by stroke-dashoffset, so a dash-only draw
     puts 13 arrowheads at their destinations before any line arrives. The
     second transition below is the fix: each path's own opacity comes up
     over --dur-micro starting at --draw-head of its draw, so the head
     lands with the line rather than waiting for it. */
  .integration__link {
    --int-delay: calc(var(--int-order, 0) * var(--draw-step));
    stroke-dasharray: 1 1;
    stroke-dashoffset: 0;
    opacity: 1;
    transition:
      stroke-dashoffset calc(var(--dur-draw) * var(--motion)) var(--ease-out)
                        calc(var(--int-delay) * var(--motion)),
      opacity calc(var(--dur-micro) * var(--motion)) linear
              calc((var(--int-delay) + var(--dur-draw) * var(--draw-head)) * var(--motion));
  }

  .integration[data-reveal]:not(.is-revealed) .integration__link,
  .integration.is-undrawn .integration__link {
    stroke-dashoffset: 1;
    opacity: 0;
  }
  /* The re-arm must be instantaneous or it plays backwards first. */
  .integration.is-undrawn .integration__link { transition: none; }

  /* One rule per ORDER, not per path — 7 declarations cover all 26. The
     attribute is the markup agent's, authored for exactly this. */
  .integration [data-int-order="1"] { --int-order: 0; }
  .integration [data-int-order="2"] { --int-order: 1; }
  .integration [data-int-order="3"] { --int-order: 2; }
  .integration [data-int-order="4"] { --int-order: 3; }
  .integration [data-int-order="5"] { --int-order: 4; }
  .integration [data-int-order="6"] { --int-order: 5; }
  .integration [data-int-order="7"] { --int-order: 6; }

  /* The draw IS the entrance. See above. */
  .integration.is-revealed { animation: none; }


  /* ---- 2.3 · the pointer spotlight ------------------------------------
     A 1px inner edge that DARKENS on the side the pointer is on. Nothing
     else: no scale, no drop shadow, no tilt, no colour.

     THE INTERIOR WASH IS GONE. It was white at 3.5% and it cannot be
     inverted — the arithmetic is on --c-spot-edge in § 1b, and the short
     version is that a hovered card's --c-text-3 has 0.41:1 of AA headroom on
     paper and every ink alpha strong enough to see spends more than that.
     Deleted with it: .cap-card::before, the --c-spot token, and the
     `isolation: isolate` and `overflow: hidden` that existed only to make a
     z-index: -1 blob land above the card's own ground and clip it to the
     radius. Nothing else read them. The ring below is the effect now, and it
     is the truer one: on paper a pointer does not add light, it deepens
     shade, and shade collects at an edge.

     NOT APPLIED ON TOUCH, AND THE GATE IS THE WHOLE GUARD. The pseudo-
     element is declared INSIDE `(hover: hover) and (pointer: fine)`, so on a
     touch screen the card is byte-for-byte the component it was before
     iteration 5 — there is no state for a tap to leave stuck, because there
     is no state.

     THE CONTRACT WITH THE JS AGENT, IN ONE LINE: --spot-x and --spot-y are
     the pointer's offset FROM THE CARD'S CENTRE, in px, written on the
     .cap-card element. Centre-relative rather than top-left-relative for
     one reason: the default is then 0, the wash sits in the middle of the
     card, and the effect degrades to a plain centred glow if the script
     never runs instead of jumping out of the corner on the first hover.

     WHAT IT COSTS. The ring's mask travels with it, so it is a gradient
     centre expressed in --spot-x / --spot-y rather than a transform, and it
     repaints a 1px ring on pointermove. It is not an animation and nothing
     about it is in a frame budget, but it is the one place on this page
     where the pointer drives paint rather than composite, and it is stated
     rather than buried. With the wash deleted it is now the ONLY place.

     CONTRAST. The ring is ink at 50% and it composites over the card to
     #818282: 3.61:1 against the --c-surface card it outlines, over the 3.0
     that 1.4.11 asks of a graphical object and over the 3.75:1 the white
     ring scored on the dark palette. It sits in the card's padding and never
     under a glyph, so it costs no text pair anything — which is the reason
     it is the layer that survived. */
  @media (hover: hover) and (pointer: fine) {
    /* The edge. A 1px ring made by masking the padding box out of the
       border box, centred on the pointer. It sits just inside the card's
       own border rather than on it: reading as an inner edge taking the
       shade is the effect, and a ring at a negative inset would sit on the
       border and read as a second border. */
    .cap-card::after {
      content: "";
      position: absolute;
      inset: 0;
      z-index: 1;
      border-radius: inherit;
      padding: var(--bw-hairline);
      background: radial-gradient(var(--spot-size) circle
                    at calc(50% + var(--spot-x, 0px)) calc(50% + var(--spot-y, 0px)),
                    var(--c-spot-edge), transparent 62%);
      -webkit-mask: linear-gradient(black 0 0) content-box, linear-gradient(black 0 0);
      -webkit-mask-composite: xor;
              mask: linear-gradient(black 0 0) content-box, linear-gradient(black 0 0);
              mask-composite: exclude;
      opacity: 0;
      pointer-events: none;
      transition: opacity calc(var(--dur-spot) * var(--motion)) var(--ease-out);
    }

    /* Keyboard parity, for the same reason § 11 gives the card edge one.
       :has(:focus-visible) and not :focus-within — a click leaves the
       toggle focused and :focus-within would leave the card lit with
       nothing on screen still focused to explain it. */
    .cap-card:hover::after,
    .cap-card:has(:focus-visible)::after { opacity: 1; }
  }


  /* ---- 2.4 · the per-line heading reveal ------------------------------
     The mask, the bleed and the zero-shift arithmetic are in § 5 with the
     rest of the structure. This is the rise and nothing else.

     --stagger-line is 70ms, in the middle of the task's 60-80ms. --i is
     written on each .line by the JS agent from its index; with the script
     absent or the split not yet run there are no .line elements at all and
     this rule matches nothing.

     THE HEAD'S OWN RISE IS SWITCHED OFF WHEN A SPLIT HAS HAPPENED. Every
     [data-split-lines] heading is inside a .section__head that is itself
     [data-reveal], and a head that fades up 12px while its title's lines
     rise 100% out of their masks is the same object entering twice. So the
     heading writes itself in and the eyebrow and lede simply arrive —
     which they can do safely, because nothing in this file ever hides
     them. Before the split runs, :has(.line) is false and the head keeps
     the entrance it has had since iteration 2. */
  .section__head.is-revealed:has(.line) { animation: none; }

  /* ONE-SHOT ENTRANCE, NOT A HIDDEN RESTING STATE — the same contract
     [data-reveal] has held since iteration 2, and the reason it is written
     this way round. The animation is scoped to .is-revealed, so a line that
     has not been revealed has NO animation at all: it is upright, opaque
     and readable. There is no rule anywhere in this file that puts a
     .line__inner at opacity 0 outside a running animation, so a stalled
     observer, a failed script, a paused tab and a reader on reduced motion
     all get a complete heading rather than an empty box.

     .is-settled IS THE JS AGENT'S HOOK FOR THE RE-SPLIT. Re-splitting on
     resize rebuilds the .line elements, and a rebuilt element has never
     played its animation, so without this a reader who resizes the window
     replays six headings at them. Mark the heading .is-settled when the
     rebuild is not a first arrival and the rise is skipped. */
  .section__head.is-revealed [data-split-lines] .line__inner {
    animation: agb-line-rise calc(var(--dur-line) * var(--motion)) var(--ease-emphasis) both;
    animation-delay: calc(var(--i, 0) * var(--stagger-line) * var(--motion));
  }
  [data-split-lines].is-settled .line__inner { animation: none; }

  @keyframes agb-line-rise {
    from { opacity: 0; transform: translateY(100%); }
    to   { opacity: 1; transform: none; }
  }


  /* ---- 2.5 · staggered groups -----------------------------------------
     THREE CONTAINERS, ONE RULE, NO PER-ELEMENT DECLARATION ANYWHERE. The
     markup agent put [data-stagger] on ol.bento--sequence, ul.facts and
     ul.bento--marketing; the JS agent writes --i on each child in one
     loop. --stagger is the existing 60ms token, inside the task's 50-70ms,
     and #marketing re-points it to 112ms (§ 13, task 2.8) with no second
     rule.

     The container is ALSO [data-reveal], so its own agb-rise is switched
     off — otherwise the group fades in as a block and then its children
     fade in again inside it. The children carry the entrance; the
     container carries only the trigger. */
  [data-stagger].is-revealed { animation: none; }
  [data-stagger].is-revealed > * {
    animation: agb-rise calc(var(--dur-entrance) * var(--motion)) var(--ease-emphasis) both;
    animation-delay: calc(var(--i, 0) * var(--stagger) * var(--motion));
  }


  /* ---- 2.6 · the hero texture drifts ----------------------------------
     The layer itself is in § 8 and is painted whatever the reader's motion
     preference is; this is the drift alone. One cell on the diagonal per
     cycle is what makes the loop seamless — the pattern repeats every
     --texture-cell in both axes, so the end state and the start state are
     the same image and there is no fade, no reverse and no seam. `linear`
     because any easing would make the loop point visible.

     64 seconds for 88px is 1.4px/s. It is transform-only on a layer that
     contains no text and can never contain any. */
  .section--hero::after {
    animation: agb-drift calc(var(--dur-drift) * var(--motion)) linear infinite;
  }
  @keyframes agb-drift {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(var(--texture-cell), var(--texture-cell), 0); }
  }


  /* ---- 2.7 · the rail's current item ----------------------------------
     Colour and one scaleX. The tick grows from --rail-tick-rest to full
     and the label fades; nothing here is a width, and the row's box is the
     same whichever item is current. */
  .rail__item {
    transition: color calc(var(--dur-standard) * var(--motion)) var(--ease-out);
  }
  .rail__item::before {
    transition: transform calc(var(--dur-standard) * var(--motion)) var(--ease-emphasis);
  }
  .rail__label {
    transition: opacity calc(var(--dur-standard) * var(--motion)) var(--ease-out);
  }


  /* ---- 2.9 · magnetic buttons -----------------------------------------
     THE TRANSFORM LIVES HERE; THE OFFSET DOES NOT. The JS agent writes
     --mag-x / --mag-y in px, clamped to --mag-max (4px), on the three
     [data-magnetic] primary CTAs the markup agent chose — the hero's, the
     marketing CTA and #getting-started's. Not the header CTA (it is in a
     fixed bar that already animates its own scrolled state) and not the
     form submit (a control that moves toward the pointer as the pointer
     arrives is a control that can be missed at the moment of clicking).

     NO EFFECT ON THE HIT TARGET. Nothing about the element's box changes:
     same min-height, same padding, same line box. A transform moves
     hit-testing with the paint, so the target is never a different size
     from what is drawn and never in a different place from where it looks.

     THE PRESS IS FOLDED IN, NOT OVERWRITTEN. §4 gives .btn--primary:active
     a 1px translateY; a bare transform here would silently delete it, so
     the two are composed. Both selectors are (0,2,0) and this one is later.

     NO TRANSITION IS DECLARED and that is deliberate: .btn already
     transitions transform at --dur-standard at the top of this block, and
     240ms of lag against the pointer is what makes the button feel heavy
     rather than sticky. It is also what returns it home on pointerleave. */
  @media (hover: hover) and (pointer: fine) {
    [data-magnetic] {
      transform: translate(var(--mag-x, 0px), var(--mag-y, 0px));
    }
    [data-magnetic]:active {
      transform: translate(var(--mag-x, 0px),
                           calc(var(--mag-y, 0px) + var(--nudge-press)));
    }
  }
}
